1+ CREATE TYPE "public "." skill_member_role" AS ENUM(' admin' , ' member' );-- > statement-breakpoint
2+ CREATE TYPE "public "." skill_member_status" AS ENUM(' active' , ' revoked' );-- > statement-breakpoint
3+ CREATE TABLE "skill_member " (
4+ " id" text PRIMARY KEY NOT NULL ,
5+ " skill_id" text NOT NULL ,
6+ " user_id" text NOT NULL ,
7+ " role" " skill_member_role" DEFAULT ' member' NOT NULL ,
8+ " status" " skill_member_status" DEFAULT ' active' NOT NULL ,
9+ " joined_at" timestamp ,
10+ " invited_by" text ,
11+ " created_at" timestamp DEFAULT now() NOT NULL ,
12+ " updated_at" timestamp DEFAULT now() NOT NULL
13+ );
14+ -- > statement-breakpoint
15+ ALTER TABLE " skill" ADD COLUMN " workspace_shared" boolean DEFAULT true NOT NULL ;-- > statement-breakpoint
16+ ALTER TABLE " skill_member" ADD CONSTRAINT " skill_member_skill_id_skill_id_fk" FOREIGN KEY (" skill_id" ) REFERENCES " public" ." skill" (" id" ) ON DELETE cascade ON UPDATE no action;-- > statement-breakpoint
17+ ALTER TABLE " skill_member" ADD CONSTRAINT " skill_member_user_id_user_id_fk" FOREIGN KEY (" user_id" ) REFERENCES " public" ." user" (" id" ) ON DELETE cascade ON UPDATE no action;-- > statement-breakpoint
18+ ALTER TABLE " skill_member" ADD CONSTRAINT " skill_member_invited_by_user_id_fk" FOREIGN KEY (" invited_by" ) REFERENCES " public" ." user" (" id" ) ON DELETE set null ON UPDATE no action;-- > statement-breakpoint
19+ CREATE INDEX "skill_member_user_id_idx " ON " skill_member" USING btree (" user_id" );-- > statement-breakpoint
20+ CREATE INDEX "skill_member_role_idx " ON " skill_member" USING btree (" role" );-- > statement-breakpoint
21+ CREATE INDEX "skill_member_status_idx " ON " skill_member" USING btree (" status" );-- > statement-breakpoint
22+ CREATE UNIQUE INDEX "skill_member_unique " ON " skill_member" USING btree (" skill_id" ," user_id" );-- > statement-breakpoint
23+ -- Backfill: existing skills predate per-skill ACLs, where any workspace `write` user
24+ -- could edit any skill. Grant those users an explicit skill admin row so their edit
25+ -- rights survive the cutover. Workspace admins get no rows (derived skill admins at
26+ -- runtime), and `read` users get no rows (covered by the implicit member grant from
27+ -- skill.workspace_shared, which the column default just turned on for every existing
28+ -- skill). The permissions table is unique on (user_id, entity_type, entity_id), so the
29+ -- write-row join alone excludes admins. Idempotent: deterministic ids + ON CONFLICT.
30+ -- Deploy window: a skill created by a still-running OLD pod after this runs gets no
31+ -- creator admin row (old code doesn't write skill_member), leaving its creator an
32+ -- implicit member until promoted. Re-running this INSERT once after full cutover
33+ -- heals those skills — it is deterministic and conflict-safe by design.
34+ INSERT INTO " skill_member" (" id" , " skill_id" , " user_id" , " role" , " status" , " joined_at" , " invited_by" , " created_at" , " updated_at" )
35+ SELECT
36+ ' skillm_' || md5(s." id" || ' :' || p." user_id" ),
37+ s." id" ,
38+ p." user_id" ,
39+ ' admin' ::" skill_member_role" ,
40+ ' active' ::" skill_member_status" ,
41+ now(),
42+ s." user_id" ,
43+ now(),
44+ now()
45+ FROM " skill" s
46+ INNER JOIN " permissions" p
47+ ON p." entity_type" = ' workspace'
48+ AND p." entity_id" = s." workspace_id"
49+ AND p." permission_type" = ' write'
50+ WHERE s." workspace_id" IS NOT NULL
51+ ON CONFLICT DO NOTHING;
0 commit comments