feat: setup schema for workspace roles
This commit is contained in:
151
packages/db/migrations/20260126100000_AddWorkspaceRoles.sql
Normal file
151
packages/db/migrations/20260126100000_AddWorkspaceRoles.sql
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
-- Create workspace_roles table
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_roles" (
|
||||||
|
"id" bigserial PRIMARY KEY NOT NULL,
|
||||||
|
"workspaceId" bigint NOT NULL,
|
||||||
|
"name" varchar(64) NOT NULL,
|
||||||
|
"description" varchar(255),
|
||||||
|
"hierarchyLevel" integer NOT NULL,
|
||||||
|
"isSystem" boolean DEFAULT false NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "workspace_roles" ENABLE ROW LEVEL SECURITY;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "unique_role_per_workspace" ON "workspace_roles" USING btree ("workspaceId","name");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "workspace_roles_workspace_idx" ON "workspace_roles" USING btree ("workspaceId");
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "workspace_roles" ADD CONSTRAINT "workspace_roles_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Create workspace_role_permissions table
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_role_permissions" (
|
||||||
|
"id" bigserial PRIMARY KEY NOT NULL,
|
||||||
|
"workspaceRoleId" bigint NOT NULL,
|
||||||
|
"permission" varchar(64) NOT NULL,
|
||||||
|
"granted" boolean DEFAULT true NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "workspace_role_permissions" ENABLE ROW LEVEL SECURITY;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "unique_role_permission" ON "workspace_role_permissions" USING btree ("workspaceRoleId","permission");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "role_permissions_role_idx" ON "workspace_role_permissions" USING btree ("workspaceRoleId");
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "workspace_role_permissions" ADD CONSTRAINT "workspace_role_permissions_workspaceRoleId_workspace_roles_id_fk" FOREIGN KEY ("workspaceRoleId") REFERENCES "public"."workspace_roles"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Add roleId column to workspace_members (nullable for migration)
|
||||||
|
ALTER TABLE "workspace_members" ADD COLUMN IF NOT EXISTS "roleId" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_roleId_workspace_roles_id_fk" FOREIGN KEY ("roleId") REFERENCES "public"."workspace_roles"("id") ON DELETE restrict ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Seed system roles for each existing workspace
|
||||||
|
INSERT INTO "workspace_roles" ("workspaceId", "name", "description", "hierarchyLevel", "isSystem", "createdAt")
|
||||||
|
SELECT w.id, 'admin', 'Full access to all workspace features', 100, true, NOW()
|
||||||
|
FROM "workspace" w
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM "workspace_roles" wr
|
||||||
|
WHERE wr."workspaceId" = w.id AND wr."name" = 'admin'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO "workspace_roles" ("workspaceId", "name", "description", "hierarchyLevel", "isSystem", "createdAt")
|
||||||
|
SELECT w.id, 'member', 'Standard member with create and edit permissions', 50, true, NOW()
|
||||||
|
FROM "workspace" w
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM "workspace_roles" wr
|
||||||
|
WHERE wr."workspaceId" = w.id AND wr."name" = 'member'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO "workspace_roles" ("workspaceId", "name", "description", "hierarchyLevel", "isSystem", "createdAt")
|
||||||
|
SELECT w.id, 'guest', 'View-only access', 10, true, NOW()
|
||||||
|
FROM "workspace" w
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM "workspace_roles" wr
|
||||||
|
WHERE wr."workspaceId" = w.id AND wr."name" = 'guest'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Seed admin role permissions (all permissions)
|
||||||
|
INSERT INTO "workspace_role_permissions" ("workspaceRoleId", "permission", "granted", "createdAt")
|
||||||
|
SELECT wr.id, p.permission, true, NOW()
|
||||||
|
FROM "workspace_roles" wr
|
||||||
|
CROSS JOIN (
|
||||||
|
VALUES
|
||||||
|
('workspace:view'), ('workspace:edit'), ('workspace:delete'), ('workspace:manage'),
|
||||||
|
('board:view'), ('board:create'), ('board:edit'), ('board:delete'),
|
||||||
|
('list:view'), ('list:create'), ('list:edit'), ('list:delete'),
|
||||||
|
('card:view'), ('card:create'), ('card:edit'), ('card:delete'),
|
||||||
|
('comment:view'), ('comment:create'), ('comment:edit'), ('comment:delete'),
|
||||||
|
('member:view'), ('member:invite'), ('member:edit'), ('member:remove')
|
||||||
|
) AS p(permission)
|
||||||
|
WHERE wr."name" = 'admin' AND wr."isSystem" = true
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM "workspace_role_permissions" wrp
|
||||||
|
WHERE wrp."workspaceRoleId" = wr.id AND wrp."permission" = p.permission
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Seed member role permissions
|
||||||
|
INSERT INTO "workspace_role_permissions" ("workspaceRoleId", "permission", "granted", "createdAt")
|
||||||
|
SELECT wr.id, p.permission, true, NOW()
|
||||||
|
FROM "workspace_roles" wr
|
||||||
|
CROSS JOIN (
|
||||||
|
VALUES
|
||||||
|
('workspace:view'),
|
||||||
|
('board:view'), ('board:create'),
|
||||||
|
('list:view'), ('list:create'), ('list:edit'),
|
||||||
|
('card:view'), ('card:create'), ('card:edit'), ('card:delete'),
|
||||||
|
('comment:view'), ('comment:create'), ('comment:edit'), ('comment:delete'),
|
||||||
|
('member:view')
|
||||||
|
) AS p(permission)
|
||||||
|
WHERE wr."name" = 'member' AND wr."isSystem" = true
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM "workspace_role_permissions" wrp
|
||||||
|
WHERE wrp."workspaceRoleId" = wr.id AND wrp."permission" = p.permission
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Seed guest role permissions (view only)
|
||||||
|
INSERT INTO "workspace_role_permissions" ("workspaceRoleId", "permission", "granted", "createdAt")
|
||||||
|
SELECT wr.id, p.permission, true, NOW()
|
||||||
|
FROM "workspace_roles" wr
|
||||||
|
CROSS JOIN (
|
||||||
|
VALUES
|
||||||
|
('workspace:view'),
|
||||||
|
('board:view'),
|
||||||
|
('list:view'),
|
||||||
|
('card:view'),
|
||||||
|
('comment:view'),
|
||||||
|
('member:view')
|
||||||
|
) AS p(permission)
|
||||||
|
WHERE wr."name" = 'guest' AND wr."isSystem" = true
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM "workspace_role_permissions" wrp
|
||||||
|
WHERE wrp."workspaceRoleId" = wr.id AND wrp."permission" = p.permission
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
|
||||||
|
-- Migrate existing workspace_members to use roleId
|
||||||
|
UPDATE "workspace_members" wm
|
||||||
|
SET "roleId" = wr.id
|
||||||
|
FROM "workspace_roles" wr
|
||||||
|
WHERE wm."workspaceId" = wr."workspaceId"
|
||||||
|
AND wm."role"::text = wr."name"
|
||||||
|
AND wm."roleId" IS NULL;
|
||||||
|
|
||||||
@@ -12,3 +12,4 @@ export * from "./integrations";
|
|||||||
export * from "./workspaces";
|
export * from "./workspaces";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./workspaceInviteLinks";
|
export * from "./workspaceInviteLinks";
|
||||||
|
export * from "./permissions";
|
||||||
|
|||||||
97
packages/db/src/schema/permissions.ts
Normal file
97
packages/db/src/schema/permissions.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
bigint,
|
||||||
|
bigserial,
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
pgTable,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
import { workspaces } from "./workspaces";
|
||||||
|
|
||||||
|
export const workspaceRoles = pgTable(
|
||||||
|
"workspace_roles",
|
||||||
|
{
|
||||||
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||||
|
workspaceId: bigint("workspaceId", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => workspaces.id, { onDelete: "cascade" }),
|
||||||
|
name: varchar("name", { length: 64 }).notNull(),
|
||||||
|
description: varchar("description", { length: 255 }),
|
||||||
|
hierarchyLevel: integer("hierarchyLevel").notNull(),
|
||||||
|
isSystem: boolean("isSystem").notNull().default(false),
|
||||||
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updatedAt"),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("unique_role_per_workspace").on(table.workspaceId, table.name),
|
||||||
|
index("workspace_roles_workspace_idx").on(table.workspaceId),
|
||||||
|
],
|
||||||
|
).enableRLS();
|
||||||
|
|
||||||
|
export const workspaceRolesRelations = relations(
|
||||||
|
workspaceRoles,
|
||||||
|
({ one, many }) => ({
|
||||||
|
workspace: one(workspaces, {
|
||||||
|
fields: [workspaceRoles.workspaceId],
|
||||||
|
references: [workspaces.id],
|
||||||
|
relationName: "workspaceRoles",
|
||||||
|
}),
|
||||||
|
permissions: many(workspaceRolePermissions),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const workspaceRolePermissions = pgTable(
|
||||||
|
"workspace_role_permissions",
|
||||||
|
{
|
||||||
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||||
|
workspaceRoleId: bigint("workspaceRoleId", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => workspaceRoles.id, { onDelete: "cascade" }),
|
||||||
|
permission: varchar("permission", { length: 64 }).notNull(),
|
||||||
|
granted: boolean("granted").notNull().default(true),
|
||||||
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("unique_role_permission").on(
|
||||||
|
table.workspaceRoleId,
|
||||||
|
table.permission,
|
||||||
|
),
|
||||||
|
index("role_permissions_role_idx").on(table.workspaceRoleId),
|
||||||
|
],
|
||||||
|
).enableRLS();
|
||||||
|
|
||||||
|
export const workspaceRolePermissionsRelations = relations(
|
||||||
|
workspaceRolePermissions,
|
||||||
|
({ one }) => ({
|
||||||
|
role: one(workspaceRoles, {
|
||||||
|
fields: [workspaceRolePermissions.workspaceRoleId],
|
||||||
|
references: [workspaceRoles.id],
|
||||||
|
relationName: "rolePermissions",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const workspaceMemberPermissions = pgTable(
|
||||||
|
"workspace_member_permissions",
|
||||||
|
{
|
||||||
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||||
|
workspaceMemberId: bigint("workspaceMemberId", { mode: "number" }).notNull(),
|
||||||
|
permission: varchar("permission", { length: 64 }).notNull(),
|
||||||
|
granted: boolean("granted").notNull().default(true),
|
||||||
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updatedAt"),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("unique_member_permission").on(
|
||||||
|
table.workspaceMemberId,
|
||||||
|
table.permission,
|
||||||
|
),
|
||||||
|
index("permission_member_idx").on(table.workspaceMemberId),
|
||||||
|
],
|
||||||
|
).enableRLS();
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
import { boards } from "./boards";
|
import { boards } from "./boards";
|
||||||
|
import { workspaceMemberPermissions, workspaceRoles } from "./permissions";
|
||||||
import { subscription } from "./subscriptions";
|
import { subscription } from "./subscriptions";
|
||||||
import { users } from "./users";
|
import { users } from "./users";
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ export const workspaceRelations = relations(workspaces, ({ one, many }) => ({
|
|||||||
members: many(workspaceMembers),
|
members: many(workspaceMembers),
|
||||||
boards: many(boards),
|
boards: many(boards),
|
||||||
subscriptions: many(subscription),
|
subscriptions: many(subscription),
|
||||||
|
roles: many(workspaceRoles),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const workspaceMembers = pgTable("workspace_members", {
|
export const workspaceMembers = pgTable("workspace_members", {
|
||||||
@@ -86,13 +88,18 @@ export const workspaceMembers = pgTable("workspace_members", {
|
|||||||
deletedBy: uuid("deletedBy").references(() => users.id, {
|
deletedBy: uuid("deletedBy").references(() => users.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
// Legacy role enum
|
||||||
role: memberRoleEnum("role").notNull(),
|
role: memberRoleEnum("role").notNull(),
|
||||||
|
roleId: bigint("roleId", { mode: "number" }).references(
|
||||||
|
() => workspaceRoles.id,
|
||||||
|
{ onDelete: "restrict" },
|
||||||
|
),
|
||||||
status: memberStatusEnum("status").default("invited").notNull(),
|
status: memberStatusEnum("status").default("invited").notNull(),
|
||||||
}).enableRLS();
|
}).enableRLS();
|
||||||
|
|
||||||
export const workspaceMembersRelations = relations(
|
export const workspaceMembersRelations = relations(
|
||||||
workspaceMembers,
|
workspaceMembers,
|
||||||
({ one }) => ({
|
({ one, many }) => ({
|
||||||
user: one(users, {
|
user: one(users, {
|
||||||
fields: [workspaceMembers.userId],
|
fields: [workspaceMembers.userId],
|
||||||
references: [users.id],
|
references: [users.id],
|
||||||
@@ -103,6 +110,23 @@ export const workspaceMembersRelations = relations(
|
|||||||
references: [workspaces.id],
|
references: [workspaces.id],
|
||||||
relationName: "workspaceMembersWorkspace",
|
relationName: "workspaceMembersWorkspace",
|
||||||
}),
|
}),
|
||||||
|
workspaceRole: one(workspaceRoles, {
|
||||||
|
fields: [workspaceMembers.roleId],
|
||||||
|
references: [workspaceRoles.id],
|
||||||
|
relationName: "workspaceMemberRole",
|
||||||
|
}),
|
||||||
|
permissions: many(workspaceMemberPermissions),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const workspaceMemberPermissionsRelations = relations(
|
||||||
|
workspaceMemberPermissions,
|
||||||
|
({ one }) => ({
|
||||||
|
member: one(workspaceMembers, {
|
||||||
|
fields: [workspaceMemberPermissions.workspaceMemberId],
|
||||||
|
references: [workspaceMembers.id],
|
||||||
|
relationName: "memberPermissions",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user