feat: migrate Canvas and Groups API to Prisma ORM

This commit is contained in:
2026-04-26 14:37:12 +05:00
parent f9e35cb9e7
commit e040d8cacd
14 changed files with 183 additions and 223 deletions
+4
View File
@@ -47,5 +47,9 @@ jobs:
cp -r .output/. /opt/xflow/.output/
cp -r prisma/. /opt/xflow/prisma/
cp package.json /opt/xflow/package.json
cp prisma.config.ts /opt/xflow/prisma.config.ts
docker-compose -f /opt/xflow/docker-compose.yml up -d --force-recreate xflow
echo "⏳ Waiting for container to start..."
sleep 5
docker exec xflow sh -c "cd /app && npx prisma migrate deploy"
echo "✅ XFlow deployed at $(date)"
+1 -1
View File
@@ -188,6 +188,7 @@
},
"active": "3f38aefa8626b1f8",
"lastOpenFiles": [
"session-2026-04-23.canvas",
"app/server/utils/prisma.ts",
"prisma.config.ts",
"prisma/schema.prisma",
@@ -198,7 +199,6 @@
"node_modules/mysql2/typings/mysql/lib/protocol/packets/params/ErrorPacketParams.d.ts",
"node_modules/mysql2/typings/mysql/lib/protocol/sequences/Sequence.d.ts",
"node_modules/mysql2/typings/mysql/lib/protocol/sequences/QueryableBase.d.ts",
"node_modules/mysql2/typings/mysql/lib/protocol/sequences/Query.d.ts",
"node_modules/mysql2/lib/auth_plugins/caching_sha2_password.md",
"node_modules/mysql2/README.md",
"node_modules/remeda/README.md",
+26 -35
View File
@@ -1,5 +1,5 @@
import { generateToken } from '~/server/utils/auth'
import { users } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
interface FirebaseUser {
localId: string
@@ -23,13 +23,9 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 500, message: 'Firebase API key not configured' })
}
// Verify token via Firebase Auth REST API
const verifyRes = await $fetch<any>(
`https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${apiKey}`,
{
method: 'POST',
body: { idToken }
}
{ method: 'POST', body: { idToken } }
).catch(() => null)
if (!verifyRes?.users?.[0]) {
@@ -37,48 +33,43 @@ export default defineEventHandler(async (event) => {
}
const fbUser: FirebaseUser = verifyRes.users[0]
const email = fbUser.email
const existing = users.get(email)
const username = fbUser.displayName || email.split('@')[0]
const userId = fbUser.localId
let role = 'user'
let userId = fbUser.localId
if (existing) {
// Update existing
users.set(email, {
...existing,
const user = await prisma.user.upsert({
where: { email },
update: { username },
create: {
id: userId,
email,
username: fbUser.displayName || email.split('@')[0]
})
role = existing.role || 'user'
} else {
// Create new user
users.set(email, {
id: userId,
email,
username: fbUser.displayName || email.split('@')[0],
username,
password: 'firebase-managed',
role: 'user'
})
}
role: 'user',
profile: {
create: {
displayName: username,
avatar: fbUser.photoUrl || `https://api.dicebear.com/7.x/avataaars/svg?seed=${userId}`
}
}
}
})
const token = generateToken({
id: userId,
email,
username: fbUser.displayName || email.split('@')[0],
role
id: user.id,
email: user.email,
username: user.username,
role: user.role
})
return {
message: 'Google sign-in successful',
token,
user: {
id: userId,
email,
username: fbUser.displayName || email.split('@')[0],
role,
id: user.id,
email: user.email,
username: user.username,
role: user.role,
photoUrl: fbUser.photoUrl
}
}
+8 -7
View File
@@ -1,15 +1,16 @@
import { requireAuth } from '~/server/utils/auth'
import { canvases } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
requireAuth(event)
const user = requireAuth(event)
const id = getRouterParam(event, 'id')
if (!id) throw createError({ statusCode: 400, message: 'Canvas ID is required' })
if (!canvases.has(id || '')) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
const canvas = await prisma.canvas.findUnique({ where: { id } })
if (!canvas) throw createError({ statusCode: 404, message: 'Canvas not found' })
if (canvas.ownerId !== user.id) throw createError({ statusCode: 403, message: 'Access denied' })
canvases.delete(id || '')
await prisma.canvas.delete({ where: { id } })
return { success: true, message: 'Canvas deleted' }
})
})
+10 -47
View File
@@ -1,61 +1,24 @@
import { requireAuth } from '~/server/utils/auth'
import { canvases, userGroups } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) throw createError({ statusCode: 400, message: 'Canvas ID is required' })
if (!id) {
throw createError({ statusCode: 400, message: 'Canvas ID is required' })
}
const canvas = await prisma.canvas.findUnique({ where: { id } })
if (!canvas) throw createError({ statusCode: 404, message: 'Canvas not found' })
const canvas = canvases.get(id)
if (!canvas) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
if (canvas.isPublic) return canvas
// Check if canvas is public
if (canvas.isPublic) {
return {
id: canvas.id,
name: canvas.name,
nodes: canvas.nodes,
edges: canvas.edges,
isPublic: true,
ownerId: canvas.ownerId
}
}
// For private canvases, require authentication and check permissions
const user = requireAuth(event)
// Check if user owns the canvas
if (canvas.ownerId === user.id) {
return {
id: canvas.id,
name: canvas.name,
nodes: canvas.nodes,
edges: canvas.edges,
isPublic: false,
ownerId: canvas.ownerId
}
}
if (canvas.ownerId === user.id) return canvas
// Check if user has access through group membership
if (canvas.groupId) {
const membership = Array.from(userGroups.values()).find(
m => m.userId === user.id && m.groupId === canvas.groupId
)
if (membership) {
return {
id: canvas.id,
name: canvas.name,
nodes: canvas.nodes,
edges: canvas.edges,
isPublic: false,
ownerId: canvas.ownerId,
groupId: canvas.groupId
}
}
const membership = await prisma.groupMember.findUnique({
where: { userId_groupId: { userId: user.id, groupId: canvas.groupId } }
})
if (membership) return canvas
}
throw createError({ statusCode: 403, message: 'Access denied' })
+20 -16
View File
@@ -1,24 +1,28 @@
import { requireAuth } from '~/server/utils/auth'
import { canvases } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
requireAuth(event)
const user = requireAuth(event)
const id = getRouterParam(event, 'id')
if (!id) throw createError({ statusCode: 400, message: 'Canvas ID is required' })
const body = await readBody(event)
const existing = canvases.get(id || '')
if (!existing) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
const existing = await prisma.canvas.findUnique({ where: { id } })
if (!existing) throw createError({ statusCode: 404, message: 'Canvas not found' })
if (existing.ownerId !== user.id) throw createError({ statusCode: 403, message: 'Access denied' })
const updated = {
...existing,
...body,
id: existing.id,
updatedAt: new Date().toISOString()
}
const { name, nodes, edges, isPublic, groupId } = body
const canvas = await prisma.canvas.update({
where: { id },
data: {
...(name !== undefined && { name }),
...(nodes !== undefined && { nodes }),
...(edges !== undefined && { edges }),
...(isPublic !== undefined && { isPublic }),
...(groupId !== undefined && { groupId: groupId || null })
}
})
canvases.set(id || '', updated)
return { canvas: updated }
})
return { canvas }
})
+32 -25
View File
@@ -1,34 +1,41 @@
import { requireAuth } from '~/server/utils/auth'
import { canvases, userGroups } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
// Get all canvases the user can access
const userGroupIds = Array.from(userGroups.values())
.filter(membership => membership.userId === user.id)
.map(membership => membership.groupId)
const userGroups = await prisma.groupMember.findMany({
where: { userId: user.id },
select: { groupId: true }
})
const groupIds = userGroups.map(m => m.groupId)
const accessibleCanvases = Array.from(canvases.values()).filter(canvas => {
// User can see their own canvases
if (canvas.ownerId === user.id) return true
// User can see public canvases
if (canvas.isPublic) return true
// User can see group canvases they're a member of
if (canvas.groupId && userGroupIds.includes(canvas.groupId)) return true
return false
const canvasList = await prisma.canvas.findMany({
where: {
OR: [
{ ownerId: user.id },
{ isPublic: true },
{ groupId: { in: groupIds } }
]
},
select: {
id: true,
name: true,
ownerId: true,
groupId: true,
isPublic: true,
createdAt: true,
updatedAt: true,
nodes: true
},
orderBy: { updatedAt: 'desc' }
})
const canvasList = accessibleCanvases.map(canvas => ({
id: canvas.id,
name: canvas.name,
ownerId: canvas.ownerId,
createdAt: canvas.createdAt,
updatedAt: canvas.updatedAt,
isPublic: canvas.isPublic,
groupId: canvas.groupId,
nodeCount: canvas.nodes.length
}))
return { canvases: canvasList }
return {
canvases: canvasList.map(c => ({
...c,
nodeCount: Array.isArray(c.nodes) ? (c.nodes as any[]).length : 0,
nodes: undefined
}))
}
})
+16 -20
View File
@@ -1,36 +1,32 @@
import { requireAuth } from '~/server/utils/auth'
import { canvases, userGroups } from '~/server/utils/db'
import type { CanvasData } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const body = await readBody(event)
const { name = 'New Canvas', nodes = [], edges = [], isPublic = false, groupId } = body
// Check group permissions if groupId is provided
if (groupId) {
const membership = Array.from(userGroups.values()).find(
m => m.userId === user.id && m.groupId === groupId
)
const membership = await prisma.groupMember.findUnique({
where: { userId_groupId: { userId: user.id, groupId } }
})
if (!membership) {
throw createError({ statusCode: 403, message: 'Not a member of this group' })
}
}
const id = `canvas-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const canvas: CanvasData = {
id,
name,
nodes,
edges,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
ownerId: user.id,
groupId,
isPublic
}
canvases.set(id, canvas)
const canvas = await prisma.canvas.create({
data: {
id,
name,
nodes,
edges,
isPublic,
ownerId: user.id,
groupId: groupId || null
}
})
return { canvas }
})
})
+3 -6
View File
@@ -1,20 +1,17 @@
import { requireAuth } from '~/server/utils/auth'
import { groups, userGroups } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const groupId = getRouterParam(event, 'id')!
const group = groups.get(groupId)
const group = await prisma.group.findUnique({ where: { id: groupId } })
if (!group) throw createError({ statusCode: 404, message: 'Group not found' })
if (group.ownerId !== user.id && user.role !== 'admin') {
throw createError({ statusCode: 403, message: 'Only owner can delete group' })
}
groups.delete(groupId)
for (const [key] of userGroups) {
if (key.endsWith(`-${groupId}`)) userGroups.delete(key)
}
await prisma.group.delete({ where: { id: groupId } })
return { success: true }
})
+10 -5
View File
@@ -1,17 +1,22 @@
import { requireAuth } from '~/server/utils/auth'
import { groups, userGroups } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const groupId = getRouterParam(event, 'id')!
const group = groups.get(groupId)
const group = await prisma.group.findUnique({ where: { id: groupId } })
if (!group) throw createError({ statusCode: 404, message: 'Group not found' })
if (!group.isPublic) throw createError({ statusCode: 403, message: 'Group is private' })
const key = `${user.id}-${groupId}`
if (userGroups.has(key)) return { message: 'Already a member' }
const existing = await prisma.groupMember.findUnique({
where: { userId_groupId: { userId: user.id, groupId } }
})
if (existing) return { message: 'Already a member' }
await prisma.groupMember.create({
data: { userId: user.id, groupId, role: 'member' }
})
userGroups.set(key, { userId: user.id, groupId, role: 'member', joinedAt: new Date().toISOString() })
return { success: true, message: `Joined group "${group.name}"` }
})
+8 -5
View File
@@ -1,17 +1,20 @@
import { requireAuth } from '~/server/utils/auth'
import { groups, userGroups } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const groupId = getRouterParam(event, 'id')!
const group = groups.get(groupId)
const group = await prisma.group.findUnique({ where: { id: groupId } })
if (!group) throw createError({ statusCode: 404, message: 'Group not found' })
if (group.ownerId === user.id) throw createError({ statusCode: 400, message: 'Owner cannot leave group. Delete it instead.' })
const key = `${user.id}-${groupId}`
if (!userGroups.has(key)) throw createError({ statusCode: 400, message: 'Not a member' })
const membership = await prisma.groupMember.findUnique({
where: { userId_groupId: { userId: user.id, groupId } }
})
if (!membership) throw createError({ statusCode: 400, message: 'Not a member' })
await prisma.groupMember.delete({ where: { id: membership.id } })
userGroups.delete(key)
return { success: true, message: `Left group "${group.name}"` }
})
+26 -4
View File
@@ -1,8 +1,30 @@
import { requireAuth } from '~/server/utils/auth'
import { groups } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
requireAuth(event)
const allGroups = Array.from(groups.values()).filter(g => g.isPublic)
return { groups: allGroups }
const user = requireAuth(event)
const allGroups = await prisma.group.findMany({
where: { isPublic: true },
include: {
members: { include: { user: { select: { id: true, username: true } } } },
_count: { select: { members: true, canvases: true } }
},
orderBy: { createdAt: 'desc' }
})
return {
groups: allGroups.map(g => ({
id: g.id,
name: g.name,
description: g.description,
isPublic: g.isPublic,
ownerId: g.ownerId,
createdAt: g.createdAt,
updatedAt: g.updatedAt,
memberCount: g._count.members,
canvasCount: g._count.canvases,
isMember: g.members.some(m => m.userId === user.id)
}))
}
})
+14 -12
View File
@@ -1,5 +1,5 @@
import { requireAuth } from '~/server/utils/auth'
import { groups, userGroups } from '~/server/utils/db'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
@@ -10,17 +10,19 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 400, message: 'Group name is required' })
}
const id = `group-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
const now = new Date().toISOString()
const group = { id, name: name.trim(), description, isPublic, createdAt: now, updatedAt: now, ownerId: user.id }
groups.set(id, group)
userGroups.set(`${user.id}-${id}`, {
userId: user.id,
groupId: id,
role: 'admin',
joinedAt: now
const group = await prisma.group.create({
data: {
name: name.trim(),
description,
isPublic,
ownerId: user.id,
members: {
create: {
userId: user.id,
role: 'admin'
}
}
}
})
return { group }
+5 -40
View File
@@ -5,6 +5,7 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
@@ -44,49 +45,13 @@ model Canvas {
ownerId String
groupId String?
isPublic Boolean @default(false)
nodes Json @default("[]")
edges Json @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
nodes CanvasNode[]
edges CanvasEdge[]
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
group Group? @relation(fields: [groupId], references: [id], onDelete: SetNull)
}
model CanvasNode {
id String @id
canvasId String
type String
x Float
y Float
width Float
height Float
text String?
file String?
state String?
label String?
assignedTo String?
alarm String?
scriptId String?
prompt String?
url String?
method String?
repo String?
branch String?
styleJson String?
canvas Canvas @relation(fields: [canvasId], references: [id], onDelete: Cascade)
}
model CanvasEdge {
id String @id
canvasId String
fromNode String
fromSide String
toNode String
toSide String
canvas Canvas @relation(fields: [canvasId], references: [id], onDelete: Cascade)
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
group Group? @relation(fields: [groupId], references: [id], onDelete: SetNull)
}
model Group {