2
0

feat(editor): Team workspaces

This commit is contained in:
Baptiste Arnaud
2022-05-13 15:22:44 -07:00
parent 6c2986590b
commit f0fdf08b00
132 changed files with 3354 additions and 1228 deletions

View File

@@ -0,0 +1,84 @@
-- CreateEnum
CREATE TYPE "WorkspaceRole" AS ENUM ('ADMIN', 'MEMBER', 'GUEST');
-- AlterEnum
ALTER TYPE "CollaborationType" ADD VALUE 'FULL_ACCESS';
-- AlterEnum
ALTER TYPE "Plan" ADD VALUE 'TEAM';
-- AlterTable
ALTER TABLE "Credentials" ADD COLUMN "workspaceId" TEXT,
ALTER COLUMN "ownerId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "CustomDomain" ADD COLUMN "workspaceId" TEXT,
ALTER COLUMN "ownerId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "DashboardFolder" ADD COLUMN "workspaceId" TEXT,
ALTER COLUMN "ownerId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Typebot" ADD COLUMN "workspaceId" TEXT,
ALTER COLUMN "ownerId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "plan" DROP NOT NULL;
-- CreateTable
CREATE TABLE "Workspace" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"icon" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"plan" "Plan" NOT NULL DEFAULT E'FREE',
"stripeId" TEXT,
CONSTRAINT "Workspace_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MemberInWorkspace" (
"userId" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
"role" "WorkspaceRole" NOT NULL
);
-- CreateTable
CREATE TABLE "WorkspaceInvitation" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"email" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
"type" "WorkspaceRole" NOT NULL,
CONSTRAINT "WorkspaceInvitation_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Workspace_stripeId_key" ON "Workspace"("stripeId");
-- CreateIndex
CREATE UNIQUE INDEX "MemberInWorkspace_userId_workspaceId_key" ON "MemberInWorkspace"("userId", "workspaceId");
-- AddForeignKey
ALTER TABLE "MemberInWorkspace" ADD CONSTRAINT "MemberInWorkspace_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MemberInWorkspace" ADD CONSTRAINT "MemberInWorkspace_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WorkspaceInvitation" ADD CONSTRAINT "WorkspaceInvitation_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomDomain" ADD CONSTRAINT "CustomDomain_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Credentials" ADD CONSTRAINT "Credentials_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DashboardFolder" ADD CONSTRAINT "DashboardFolder_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Typebot" ADD CONSTRAINT "Typebot_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -50,7 +50,7 @@ model User {
sessions Session[]
typebots Typebot[]
folders DashboardFolder[]
plan Plan @default(FREE)
plan Plan? @default(FREE)
stripeId String? @unique
credentials Credentials[]
customDomains CustomDomain[]
@@ -59,6 +59,47 @@ model User {
company String?
onboardingCategories String[]
graphNavigation GraphNavigation?
workspaces MemberInWorkspace[]
}
model Workspace {
id String @id @default(cuid())
name String
icon String?
members MemberInWorkspace[]
folders DashboardFolder[]
typebots Typebot[]
createdAt DateTime @default(now())
plan Plan @default(FREE)
stripeId String? @unique
customDomains CustomDomain[]
credentials Credentials[]
invitations WorkspaceInvitation[]
}
model MemberInWorkspace {
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
role WorkspaceRole
@@unique([userId, workspaceId])
}
enum WorkspaceRole {
ADMIN
MEMBER
GUEST
}
model WorkspaceInvitation {
id String @id @default(cuid())
createdAt DateTime @default(now())
email String
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
type WorkspaceRole
}
enum GraphNavigation {
@@ -67,21 +108,25 @@ enum GraphNavigation {
}
model CustomDomain {
name String @id
createdAt DateTime @default(now())
ownerId String
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
name String @id
createdAt DateTime @default(now())
ownerId String?
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
}
model Credentials {
id String @id @default(cuid())
createdAt DateTime @default(now())
ownerId String
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
data String // Encrypted data
name String
type String
iv String
id String @id @default(cuid())
createdAt DateTime @default(now())
ownerId String?
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
data String // Encrypted data
name String
type String
iv String
@@unique([name, type, ownerId])
}
@@ -89,6 +134,7 @@ model Credentials {
enum Plan {
FREE
PRO
TEAM
LIFETIME
OFFERED
}
@@ -106,12 +152,14 @@ model DashboardFolder {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
name String
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
ownerId String
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
ownerId String?
parentFolderId String?
parentFolder DashboardFolder? @relation("ParentChild", fields: [parentFolderId], references: [id])
childrenFolder DashboardFolder[] @relation("ParentChild")
typebots Typebot[]
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([id, ownerId])
}
@@ -122,8 +170,8 @@ model Typebot {
updatedAt DateTime @default(now()) @updatedAt
icon String?
name String
ownerId String
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
ownerId String?
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
publishedTypebotId String?
publishedTypebot PublicTypebot?
results Result[]
@@ -139,6 +187,8 @@ model Typebot {
collaborators CollaboratorsOnTypebots[]
invitations Invitation[]
webhooks Webhook[]
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([id, ownerId])
}
@@ -166,6 +216,7 @@ model CollaboratorsOnTypebots {
enum CollaborationType {
READ
WRITE
FULL_ACCESS
}
model PublicTypebot {

View File

@@ -1 +1,2 @@
DATABASE_URL=postgresql://postgres:@localhost:5432/typebot
DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot
ENCRYPTION_SECRET=

View File

@@ -1,5 +1,5 @@
import { PrismaClient } from 'db'
import path from 'path'
import { migrateWorkspace } from './workspaceMigration'
require('dotenv').config({
path: path.join(
@@ -8,7 +8,8 @@ require('dotenv').config({
),
})
const prisma = new PrismaClient()
const main = async () => {}
const main = async () => {
await migrateWorkspace()
}
main().then()

View File

@@ -6,7 +6,8 @@
"private": true,
"scripts": {
"start:local": "ts-node index.ts",
"start:prod": "NODE_ENV=production ts-node index.ts"
"start:prod": "NODE_ENV=production ts-node index.ts",
"start:workspaces:migration": "ts-node workspaceMigration.ts"
},
"devDependencies": {
"db": "*",

View File

@@ -0,0 +1,85 @@
import { Plan, PrismaClient, WorkspaceRole } from 'db'
import path from 'path'
const prisma = new PrismaClient()
export const migrateWorkspace = async () => {
const users = await prisma.user.findMany({
where: { workspaces: { none: {} } },
include: {
folders: true,
typebots: true,
credentials: true,
customDomains: true,
CollaboratorsOnTypebots: {
include: { typebot: { select: { workspaceId: true } } },
},
},
})
let i = 1
for (const user of users) {
console.log('Updating', user.email, `(${i}/${users.length})`)
i += 1
const newWorkspace = await prisma.workspace.create({
data: {
name: user.name ? `${user.name}'s workspace` : 'My workspace',
members: { create: { userId: user.id, role: WorkspaceRole.ADMIN } },
stripeId: user.stripeId,
plan: user.plan ?? Plan.FREE,
},
})
await prisma.credentials.updateMany({
where: { id: { in: user.credentials.map((c) => c.id) } },
data: { workspaceId: newWorkspace.id, ownerId: null },
})
await prisma.customDomain.updateMany({
where: {
name: { in: user.customDomains.map((c) => c.name) },
ownerId: user.id,
},
data: { workspaceId: newWorkspace.id, ownerId: null },
})
await prisma.dashboardFolder.updateMany({
where: {
id: { in: user.folders.map((c) => c.id) },
},
data: { workspaceId: newWorkspace.id, ownerId: null },
})
await prisma.typebot.updateMany({
where: {
id: { in: user.typebots.map((c) => c.id) },
},
data: { workspaceId: newWorkspace.id, ownerId: null },
})
for (const collab of user.CollaboratorsOnTypebots) {
if (!collab.typebot.workspaceId) continue
await prisma.memberInWorkspace.upsert({
where: {
userId_workspaceId: {
userId: user.id,
workspaceId: collab.typebot.workspaceId,
},
},
create: {
role: WorkspaceRole.GUEST,
userId: user.id,
workspaceId: collab.typebot.workspaceId,
},
update: {},
})
}
}
}
require('dotenv').config({
path: path.join(
__dirname,
process.env.NODE_ENV === 'production' ? '.env.production' : '.env.local'
),
})
const main = async () => {
await migrateWorkspace()
}
main().then()