feat: add private project access

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-01 22:54:01 +00:00
1 parent df05b1a968
commit c761282d76
29 files changed
+1197 -29

No files matched your search

@@ -0,0 +1,41 @@
ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "visibility" text DEFAULT 'open' NOT NULL;
ALTER TABLE "projects" ADD COLUMN IF NOT EXISTS "personal_owner_user_id" text;
DO $$ BEGIN
ALTER TABLE "projects"
ADD CONSTRAINT "projects_visibility_check" CHECK ("visibility" IN ('open', 'private'));
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS "projects_company_personal_owner_uq"
ON "projects" USING btree ("company_id", "personal_owner_user_id")
WHERE "personal_owner_user_id" IS NOT NULL;
CREATE TABLE IF NOT EXISTS "project_access_members" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"project_id" uuid NOT NULL,
"subject_type" text NOT NULL,
"subject_id" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "project_access_members_subject_type_check" CHECK ("subject_type" IN ('user', 'agent'))
);
DO $$ BEGIN
ALTER TABLE "project_access_members"
ADD CONSTRAINT "project_access_members_company_id_companies_id_fk"
FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
ALTER TABLE "project_access_members"
ADD CONSTRAINT "project_access_members_project_id_projects_id_fk"
FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS "project_access_members_project_subject_uq"
ON "project_access_members" USING btree ("project_id", "subject_type", "subject_id");
CREATE INDEX IF NOT EXISTS "project_access_members_subject_lookup_idx"
ON "project_access_members" USING btree ("company_id", "subject_type", "subject_id", "project_id");
@@ -1387,6 +1387,13 @@
"when": 1785628800000,
"tag": "0199_heartbeat_run_issue_privacy",
"breakpoints": true
},
{
"idx": 200,
"version": "7",
"when": 1785715200000,
"tag": "0200_private_projects",
"breakpoints": true
}
]
}
+1
View File
@@ -24,6 +24,7 @@ export { agentTaskSessions } from "./agent_task_sessions.js";
export { agentWakeupRequests } from "./agent_wakeup_requests.js";
export { projects } from "./projects.js";
export { projectMemberships } from "./project_memberships.js";
export { projectAccessMembers } from "./project_access_members.js";
export { documentMemberships } from "./document_memberships.js";
export { projectWorkspaces } from "./project_workspaces.js";
export { executionWorkspaces } from "./execution_workspaces.js";
@@ -0,0 +1,34 @@
import { sql } from "drizzle-orm";
import { check, index, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { projects } from "./projects.js";
/** Authorization membership for private projects; unrelated to sidebar project_memberships. */
export const projectAccessMembers = pgTable(
"project_access_members",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
subjectType: text("subject_type").notNull(),
subjectId: text("subject_id").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
subjectTypeCheck: check(
"project_access_members_subject_type_check",
sql`${table.subjectType} in ('user', 'agent')`,
),
projectSubjectUq: uniqueIndex("project_access_members_project_subject_uq").on(
table.projectId,
table.subjectType,
table.subjectId,
),
subjectLookupIdx: index("project_access_members_subject_lookup_idx").on(
table.companyId,
table.subjectType,
table.subjectId,
table.projectId,
),
}),
);
+8 -1
View File
@@ -1,4 +1,5 @@
import { pgTable, uuid, text, timestamp, date, index, jsonb } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { pgTable, uuid, text, timestamp, date, index, jsonb, check, uniqueIndex } from "drizzle-orm/pg-core";
import type { AgentEnvConfig } from "@paperclipai/shared";
import { companies } from "./companies.js";
import { goals } from "./goals.js";
@@ -12,6 +13,8 @@ export const projects = pgTable(
goalId: uuid("goal_id").references(() => goals.id),
name: text("name").notNull(),
description: text("description"),
visibility: text("visibility").notNull().default("open"),
personalOwnerUserId: text("personal_owner_user_id"),
status: text("status").notNull().default("backlog"),
leadAgentId: uuid("lead_agent_id").references(() => agents.id),
targetDate: date("target_date"),
@@ -27,5 +30,9 @@ export const projects = pgTable(
},
(table) => ({
companyIdx: index("projects_company_idx").on(table.companyId),
visibilityCheck: check("projects_visibility_check", sql`${table.visibility} in ('open', 'private')`),
personalOwnerUq: uniqueIndex("projects_company_personal_owner_uq")
.on(table.companyId, table.personalOwnerUserId)
.where(sql`${table.personalOwnerUserId} is not null`),
}),
);