From 8488dca712f32d2928e29c498bbfcd4f9490d624 Mon Sep 17 00:00:00 2001 From: Sai Prakash Date: Tue, 14 Jul 2026 16:02:50 -0400 Subject: [PATCH 1/2] Derive plan assignees from task assignment; drop code_plan_assignees table Plan-level assignees were a separately-maintained join table that nothing in the UI ever wrote to (create always sent an empty array, the edit sheet had no assignee field), so every plan created through the app was permanently stuck showing "Unassigned" despite the widget's query logic being correct. Rather than add an assignee picker, make assignees reflect reality: the unique set of users assigned to the plan's tasks. This can't drift from what's actually happening, and needs no extra UI to keep in sync. - getCodePlan/getCodePlans (lib/db/queries.ts) derive assigneeIds from task.assigneeId instead of joining code_plan_assignees. - Dropped the now-write-only assigneeIds param from createCodePlan/ updateCodePlan and syncPlanAssignees entirely (lib/db/mutations.ts). - Dropped the code_plan_assignees table (schema + migration, both dialects) and its now-dead references in seed-demo.ts and tests. - Also: moved the Tasks page's "New Task" button onto the same row as the list/board view switcher (was a separate row above, pushing the table down for no reason). Co-Authored-By: Claude Sonnet 5 --- app/(dashboard)/actions.ts | 1 - app/(dashboard)/tasks/tasks-client.tsx | 16 +- app/api/mcp/[transport]/route.ts | 2 +- .../postgres/0010_drop_plan_assignees.sql | 1 + lib/db/migrations/postgres/meta/_journal.json | 7 + .../sqlite/0010_drop_plan_assignees.sql | 1 + .../migrations/sqlite/meta/0010_snapshot.json | 1960 +++++++++++++++++ lib/db/migrations/sqlite/meta/_journal.json | 7 + lib/db/mutations.ts | 20 +- lib/db/queries.ts | 36 +- lib/db/schema.pg.ts | 8 - lib/db/schema.sqlite.ts | 8 - lib/db/schema.ts | 1 - lib/db/seed-demo.ts | 27 +- tests/helpers/db.ts | 10 +- tests/lib/db/mutations.test.ts | 32 +- 16 files changed, 2050 insertions(+), 87 deletions(-) create mode 100644 lib/db/migrations/postgres/0010_drop_plan_assignees.sql create mode 100644 lib/db/migrations/sqlite/0010_drop_plan_assignees.sql create mode 100644 lib/db/migrations/sqlite/meta/0010_snapshot.json diff --git a/app/(dashboard)/actions.ts b/app/(dashboard)/actions.ts index 156dd5b..b0addfc 100644 --- a/app/(dashboard)/actions.ts +++ b/app/(dashboard)/actions.ts @@ -243,7 +243,6 @@ export async function createCodePlanAction(formData: FormData) { type, tags, targetAssetIds: [], - assigneeIds: [], deadline, specUrl, }, diff --git a/app/(dashboard)/tasks/tasks-client.tsx b/app/(dashboard)/tasks/tasks-client.tsx index 8d21556..cae8ee7 100644 --- a/app/(dashboard)/tasks/tasks-client.tsx +++ b/app/(dashboard)/tasks/tasks-client.tsx @@ -218,15 +218,9 @@ export function TasksClient({ return ( <> -
-
-

Tasks

-

Track and manage tasks across all code plans

-
- +
+

Tasks

+

Track and manage tasks across all code plans

+ { setStatusFilter(v as TaskStatus | 'all' | 'open'); setPage(0) }} className="w-full sm:w-auto"> Open diff --git a/app/api/mcp/[transport]/route.ts b/app/api/mcp/[transport]/route.ts index 0dd859e..9430834 100644 --- a/app/api/mcp/[transport]/route.ts +++ b/app/api/mcp/[transport]/route.ts @@ -432,7 +432,7 @@ const handler = createMcpHandler( async ({ workItemIds, ownerEmail, ...data }, extra) => { requireWrite(extra) const ownerId = ownerEmail ? await resolveAssigneeEmail(uid(extra), ownerEmail) : undefined - const plan = await createCodePlan({ ...data, ownerId, assigneeIds: [] }, uid(extra)) + const plan = await createCodePlan({ ...data, ownerId }, uid(extra)) for (const workItemId of workItemIds) await linkWorkItemToPlan(workItemId, plan.id) return json(plan) }, diff --git a/lib/db/migrations/postgres/0010_drop_plan_assignees.sql b/lib/db/migrations/postgres/0010_drop_plan_assignees.sql new file mode 100644 index 0000000..e68212a --- /dev/null +++ b/lib/db/migrations/postgres/0010_drop_plan_assignees.sql @@ -0,0 +1 @@ +DROP TABLE "code_plan_assignees"; diff --git a/lib/db/migrations/postgres/meta/_journal.json b/lib/db/migrations/postgres/meta/_journal.json index c615466..f56ca84 100644 --- a/lib/db/migrations/postgres/meta/_journal.json +++ b/lib/db/migrations/postgres/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1783480000000, "tag": "0009_task_percent", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1784058835573, + "tag": "0010_drop_plan_assignees", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/migrations/sqlite/0010_drop_plan_assignees.sql b/lib/db/migrations/sqlite/0010_drop_plan_assignees.sql new file mode 100644 index 0000000..21ceaf0 --- /dev/null +++ b/lib/db/migrations/sqlite/0010_drop_plan_assignees.sql @@ -0,0 +1 @@ +DROP TABLE `code_plan_assignees`; \ No newline at end of file diff --git a/lib/db/migrations/sqlite/meta/0010_snapshot.json b/lib/db/migrations/sqlite/meta/0010_snapshot.json new file mode 100644 index 0000000..d1cbc33 --- /dev/null +++ b/lib/db/migrations/sqlite/meta/0010_snapshot.json @@ -0,0 +1,1960 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1289b0db-bfaa-41e4-8db3-cc14ec419502", + "prevId": "c0c08b3f-8fc6-4df5-ae3b-6c4b47dba93a", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read'" + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "columns": [ + "key_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "asset_dependencies": { + "name": "asset_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_asset_id": { + "name": "source_asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_asset_id": { + "name": "target_asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dependency_type": { + "name": "dependency_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "asset_dependencies_source_asset_id_assets_id_fk": { + "name": "asset_dependencies_source_asset_id_assets_id_fk", + "tableFrom": "asset_dependencies", + "tableTo": "assets", + "columnsFrom": [ + "source_asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "asset_dependencies_target_asset_id_assets_id_fk": { + "name": "asset_dependencies_target_asset_id_assets_id_fk", + "tableFrom": "asset_dependencies", + "tableTo": "assets", + "columnsFrom": [ + "target_asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "health": { + "name": "health", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'healthy'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "tech_debt_score": { + "name": "tech_debt_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_url": { + "name": "repository_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_path": { + "name": "repo_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "documentation_url": { + "name": "documentation_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "assets_product_id_products_id_fk": { + "name": "assets_product_id_products_id_fk", + "tableFrom": "assets", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "code_plan_assets": { + "name": "code_plan_assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code_plan_id": { + "name": "code_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_status": { + "name": "pr_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "code_plan_assets_plan_asset_idx": { + "name": "code_plan_assets_plan_asset_idx", + "columns": [ + "code_plan_id", + "asset_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "code_plan_assets_code_plan_id_code_plans_id_fk": { + "name": "code_plan_assets_code_plan_id_code_plans_id_fk", + "tableFrom": "code_plan_assets", + "tableTo": "code_plans", + "columnsFrom": [ + "code_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_plan_assets_asset_id_assets_id_fk": { + "name": "code_plan_assets_asset_id_assets_id_fk", + "tableFrom": "code_plan_assets", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "code_plans": { + "name": "code_plans", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deadline": { + "name": "deadline", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "spec_url": { + "name": "spec_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'native'" + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_data": { + "name": "external_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "external_deleted": { + "name": "external_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "synced_at": { + "name": "synced_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "code_plans_connection_external_idx": { + "name": "code_plans_connection_external_idx", + "columns": [ + "connection_id", + "external_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "code_plans_product_id_products_id_fk": { + "name": "code_plans_product_id_products_id_fk", + "tableFrom": "code_plans", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_plans_creator_id_users_id_fk": { + "name": "code_plans_creator_id_users_id_fk", + "tableFrom": "code_plans", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "code_plans_owner_id_users_id_fk": { + "name": "code_plans_owner_id_users_id_fk", + "tableFrom": "code_plans", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "code_plans_connection_id_integrations_id_fk": { + "name": "code_plans_connection_id_integrations_id_fk", + "tableFrom": "code_plans", + "tableTo": "integrations", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "new_email": { + "name": "new_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email_verification_tokens_token_unique": { + "name": "email_verification_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrations": { + "name": "integrations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_ref": { + "name": "auth_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_encrypted": { + "name": "token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrations_organization_id_organizations_id_fk": { + "name": "integrations_organization_id_organizations_id_fk", + "tableFrom": "integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization_members": { + "name": "organization_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'viewer'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_invited_by_users_id_fk": { + "name": "organization_members_invited_by_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "billing_tier": { + "name": "billing_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'free'" + }, + "product_limit": { + "name": "product_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "organizations_owner_id_users_id_fk": { + "name": "organizations_owner_id_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "products": { + "name": "products", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "products_slug_unique": { + "name": "products_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "products_organization_id_organizations_id_fk": { + "name": "products_organization_id_organizations_id_fk", + "tableFrom": "products", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "products_creator_id_users_id_fk": { + "name": "products_creator_id_users_id_fk", + "tableFrom": "products", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_log": { + "name": "sync_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sync_log_org_created_idx": { + "name": "sync_log_org_created_idx", + "columns": [ + "organization_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_log_organization_id_organizations_id_fk": { + "name": "sync_log_organization_id_organizations_id_fk", + "tableFrom": "sync_log", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_log_connection_id_integrations_id_fk": { + "name": "sync_log_connection_id_integrations_id_fk", + "tableFrom": "sync_log", + "tableTo": "integrations", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "sync_log_actor_id_users_id_fk": { + "name": "sync_log_actor_id_users_id_fk", + "tableFrom": "sync_log", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code_plan_id": { + "name": "code_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'not_started'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'medium'" + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "assignee_id": { + "name": "assignee_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "percent_complete": { + "name": "percent_complete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "estimated_effort": { + "name": "estimated_effort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actual_effort": { + "name": "actual_effort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'native'" + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_data": { + "name": "external_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "external_deleted": { + "name": "external_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "synced_at": { + "name": "synced_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tasks_connection_external_idx": { + "name": "tasks_connection_external_idx", + "columns": [ + "connection_id", + "external_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "tasks_code_plan_id_code_plans_id_fk": { + "name": "tasks_code_plan_id_code_plans_id_fk", + "tableFrom": "tasks", + "tableTo": "code_plans", + "columnsFrom": [ + "code_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_asset_id_assets_id_fk": { + "name": "tasks_asset_id_assets_id_fk", + "tableFrom": "tasks", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_connection_id_integrations_id_fk": { + "name": "tasks_connection_id_integrations_id_fk", + "tableFrom": "tasks", + "tableTo": "integrations", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "billing_tier": { + "name": "billing_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'free'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'viewer'" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feature_flags": { + "name": "feature_flags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "work_item_code_plans": { + "name": "work_item_code_plans", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "work_item_id": { + "name": "work_item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_plan_id": { + "name": "code_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "work_item_code_plans_item_plan_idx": { + "name": "work_item_code_plans_item_plan_idx", + "columns": [ + "work_item_id", + "code_plan_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "work_item_code_plans_work_item_id_work_items_id_fk": { + "name": "work_item_code_plans_work_item_id_work_items_id_fk", + "tableFrom": "work_item_code_plans", + "tableTo": "work_items", + "columnsFrom": [ + "work_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "work_item_code_plans_code_plan_id_code_plans_id_fk": { + "name": "work_item_code_plans_code_plan_id_code_plans_id_fk", + "tableFrom": "work_item_code_plans", + "tableTo": "code_plans", + "columnsFrom": [ + "code_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "work_items": { + "name": "work_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'medium'" + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "spec_url": { + "name": "spec_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'native'" + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_data": { + "name": "external_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "external_deleted": { + "name": "external_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "synced_at": { + "name": "synced_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "work_items_connection_external_idx": { + "name": "work_items_connection_external_idx", + "columns": [ + "connection_id", + "external_id" + ], + "isUnique": true + }, + "work_items_product_idx": { + "name": "work_items_product_idx", + "columns": [ + "product_id" + ], + "isUnique": false + }, + "work_items_asset_idx": { + "name": "work_items_asset_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "work_items_product_id_products_id_fk": { + "name": "work_items_product_id_products_id_fk", + "tableFrom": "work_items", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "work_items_asset_id_assets_id_fk": { + "name": "work_items_asset_id_assets_id_fk", + "tableFrom": "work_items", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "work_items_parent_id_work_items_id_fk": { + "name": "work_items_parent_id_work_items_id_fk", + "tableFrom": "work_items", + "tableTo": "work_items", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "work_items_reporter_id_users_id_fk": { + "name": "work_items_reporter_id_users_id_fk", + "tableFrom": "work_items", + "tableTo": "users", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "work_items_owner_id_users_id_fk": { + "name": "work_items_owner_id_users_id_fk", + "tableFrom": "work_items", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "work_items_connection_id_integrations_id_fk": { + "name": "work_items_connection_id_integrations_id_fk", + "tableFrom": "work_items", + "tableTo": "integrations", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/lib/db/migrations/sqlite/meta/_journal.json b/lib/db/migrations/sqlite/meta/_journal.json index dc752aa..5a661bd 100644 --- a/lib/db/migrations/sqlite/meta/_journal.json +++ b/lib/db/migrations/sqlite/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1783565221947, "tag": "0009_daffy_the_executioner", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1784058835573, + "tag": "0010_drop_plan_assignees", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/mutations.ts b/lib/db/mutations.ts index 988b012..55256f0 100644 --- a/lib/db/mutations.ts +++ b/lib/db/mutations.ts @@ -6,7 +6,6 @@ import { integrations, codePlans, codePlanAssets, - codePlanAssignees, workItems, workItemCodePlans, tasks, @@ -120,11 +119,11 @@ type CreateCodePlanData = { deadline?: string specUrl?: string ownerId?: string | null - assigneeIds: string[] } -// Join tables are the source of truth for plan↔asset and plan↔assignee links -// (the deprecated array columns were dropped in v0.3.0). +// codePlanAssets is the source of truth for plan↔asset links (the deprecated +// array column was dropped in v0.3.0). Plan assignees aren't a stored link at +// all — see getCodePlan/getCodePlans, which derive them from task.assigneeId. async function syncPlanAssets(planId: string, assetIds: string[]) { const existing = await db @@ -147,15 +146,8 @@ async function syncPlanAssets(planId: string, assetIds: string[]) { } } -async function syncPlanAssignees(planId: string, userIds: string[]) { - await db.delete(codePlanAssignees).where(eq(codePlanAssignees.codePlanId, planId)) - if (userIds.length > 0) { - await db.insert(codePlanAssignees).values(userIds.map((userId) => ({ codePlanId: planId, userId }))) - } -} - export async function createCodePlan(data: CreateCodePlanData, userId: string) { - const { targetAssetIds, assigneeIds, ...columns } = data + const { targetAssetIds, ...columns } = data const [plan] = await db .insert(codePlans) .values({ @@ -165,7 +157,6 @@ export async function createCodePlan(data: CreateCodePlanData, userId: string) { }) .returning() await syncPlanAssets(plan.id, targetAssetIds) - await syncPlanAssignees(plan.id, assigneeIds) return plan } @@ -176,7 +167,7 @@ type UpdateCodePlanData = Partial< > export async function updateCodePlan(id: string, data: UpdateCodePlanData) { - const { targetAssetIds, assigneeIds, ...columns } = data + const { targetAssetIds, ...columns } = data const [plan] = await db .update(codePlans) .set({ ...columns, updatedAt: new Date() }) @@ -184,7 +175,6 @@ export async function updateCodePlan(id: string, data: UpdateCodePlanData) { .returning() if (!plan) return null if (targetAssetIds !== undefined) await syncPlanAssets(id, targetAssetIds) - if (assigneeIds !== undefined) await syncPlanAssignees(id, assigneeIds) return plan } diff --git a/lib/db/queries.ts b/lib/db/queries.ts index 7c05f0e..573b883 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -5,7 +5,6 @@ import { assetDependencies, codePlans, codePlanAssets, - codePlanAssignees, workItems, workItemCodePlans, tasks, @@ -316,16 +315,18 @@ export async function getCodePlans(userId: string, filters: PlanFilters = {}): P .orderBy(desc(codePlans.updatedAt)) const planIds = rows.map((r) => r.id) - const [assetLinks, assigneeLinks] = planIds.length + // Assignees are derived from who's actually assigned to the plan's tasks — + // not a separately-maintained list, so they can never drift from reality. + const [assetLinks, taskAssigneeLinks] = planIds.length ? await Promise.all([ db .select({ codePlanId: codePlanAssets.codePlanId, assetId: codePlanAssets.assetId }) .from(codePlanAssets) .where(inArray(codePlanAssets.codePlanId, planIds)), db - .select({ codePlanId: codePlanAssignees.codePlanId, userId: codePlanAssignees.userId }) - .from(codePlanAssignees) - .where(inArray(codePlanAssignees.codePlanId, planIds)), + .select({ codePlanId: tasks.codePlanId, assigneeId: tasks.assigneeId }) + .from(tasks) + .where(and(inArray(tasks.codePlanId, planIds), isNotNull(tasks.assigneeId))), ]) : [[], []] @@ -333,9 +334,12 @@ export async function getCodePlans(userId: string, filters: PlanFilters = {}): P for (const l of assetLinks) { assetsByPlan.set(l.codePlanId, [...(assetsByPlan.get(l.codePlanId) ?? []), l.assetId]) } - const assigneesByPlan = new Map() - for (const l of assigneeLinks) { - assigneesByPlan.set(l.codePlanId, [...(assigneesByPlan.get(l.codePlanId) ?? []), l.userId]) + const assigneesByPlan = new Map>() + for (const l of taskAssigneeLinks) { + if (!l.assigneeId) continue + const set = assigneesByPlan.get(l.codePlanId) ?? new Set() + set.add(l.assigneeId) + assigneesByPlan.set(l.codePlanId, set) } return rows.map((r) => ({ @@ -343,7 +347,7 @@ export async function getCodePlans(userId: string, filters: PlanFilters = {}): P ownerId: r.ownerId ?? undefined, specUrl: r.specUrl ?? undefined, targetAssetIds: assetsByPlan.get(r.id) ?? [], - assigneeIds: assigneesByPlan.get(r.id) ?? [], + assigneeIds: [...(assigneesByPlan.get(r.id) ?? [])], startDate: r.startDate ?? undefined, endDate: r.endDate ?? undefined, deadline: r.deadline ?? undefined, @@ -375,7 +379,7 @@ export async function getCodePlan(id: string, userId: string): Promise ({ id: r.assetId, name: r.assetName })) + // Assignees are derived from who's actually assigned to the plan's tasks — + // not a separately-maintained list, so they can never drift from reality. + const assigneeIds = [...new Set(planTasks.map((t) => t.assigneeId).filter((x): x is string => !!x))] + const resolvedAssignees = assigneeIds.length + ? await db.select({ id: users.id, name: users.name }).from(users).where(inArray(users.id, assigneeIds)) + : [] + const taskCount = planTasks.length const completedTaskCount = planTasks.filter((t) => t.status === 'done').length diff --git a/lib/db/schema.pg.ts b/lib/db/schema.pg.ts index 1910251..1cdc39a 100644 --- a/lib/db/schema.pg.ts +++ b/lib/db/schema.pg.ts @@ -175,14 +175,6 @@ export const codePlanAssets = pgTable('code_plan_assets', { uniqueIndex('code_plan_assets_plan_asset_idx').on(t.codePlanId, t.assetId), ]) -export const codePlanAssignees = pgTable('code_plan_assignees', { - codePlanId: uuid('code_plan_id').notNull().references(() => codePlans.id, { onDelete: 'cascade' }), - userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), - createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), -}, (t) => [ - primaryKey({ columns: [t.codePlanId, t.userId] }), -]) - export const workItems = pgTable('work_items', { id: uuid('id').primaryKey().defaultRandom(), productId: uuid('product_id').notNull().references(() => products.id, { onDelete: 'cascade' }), diff --git a/lib/db/schema.sqlite.ts b/lib/db/schema.sqlite.ts index c207c8c..c1c34e5 100644 --- a/lib/db/schema.sqlite.ts +++ b/lib/db/schema.sqlite.ts @@ -169,14 +169,6 @@ export const codePlanAssets = sqliteTable('code_plan_assets', { uniqueIndex('code_plan_assets_plan_asset_idx').on(t.codePlanId, t.assetId), ]) -export const codePlanAssignees = sqliteTable('code_plan_assignees', { - codePlanId: text('code_plan_id').notNull().references(() => codePlans.id, { onDelete: 'cascade' }), - userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), -}, (t) => [ - primaryKey({ columns: [t.codePlanId, t.userId] }), -]) - export const workItems = sqliteTable('work_items', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), productId: text('product_id').notNull().references(() => products.id, { onDelete: 'cascade' }), diff --git a/lib/db/schema.ts b/lib/db/schema.ts index d2232f9..10cffe8 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -20,7 +20,6 @@ export const { assetDependencies, codePlans, codePlanAssets, - codePlanAssignees, workItems, workItemCodePlans, tasks, diff --git a/lib/db/seed-demo.ts b/lib/db/seed-demo.ts index 3b66027..1ea7858 100644 --- a/lib/db/seed-demo.ts +++ b/lib/db/seed-demo.ts @@ -18,7 +18,6 @@ import { assets, codePlans, codePlanAssets, - codePlanAssignees, tasks, } from './schema' import { eq, inArray } from 'drizzle-orm' @@ -282,7 +281,7 @@ async function seed() { async function findOrCreatePlan( title: string, - values: typeof codePlans.$inferInsert & { targetAssetIds?: string[]; assigneeIds?: string[] }, + values: typeof codePlans.$inferInsert & { targetAssetIds?: string[] }, ) { const existing = await db.query.codePlans.findFirst({ where: (p, { eq }) => eq(p.title, title), @@ -291,16 +290,12 @@ async function seed() { console.log(` plan exists: ${title}`) return existing.id } - const { targetAssetIds: _t, assigneeIds: _a, ...columns } = values + const { targetAssetIds: _t, ...columns } = values const [p] = await db.insert(codePlans).values(columns).returning() const assetIds = (values.targetAssetIds ?? []) as string[] if (assetIds.length > 0) { await db.insert(codePlanAssets).values(assetIds.map((assetId) => ({ codePlanId: p.id, assetId }))) } - const assigneeIds = (values.assigneeIds ?? []) as string[] - if (assigneeIds.length > 0) { - await db.insert(codePlanAssignees).values(assigneeIds.map((userId) => ({ codePlanId: p.id, userId }))) - } console.log(` created plan: ${title}`) return p.id } @@ -312,7 +307,7 @@ async function seed() { tags: ['ai', 'llm', 'q2'], targetAssetIds: [webAppId, planEngineId], startDate: '2026-04-01', endDate: '2026-05-20', deadline: '2026-06-01', - creatorId: alexId, assigneeIds: [alexId, mikeId], + creatorId: alexId, }) const collabPlanId = await findOrCreatePlan('Real-time Collaboration', { @@ -322,7 +317,7 @@ async function seed() { tags: ['realtime', 'websockets', 'collab'], targetAssetIds: [webAppId, planEngineId, redisId], startDate: '2026-04-15', endDate: '2026-06-01', deadline: '2026-06-15', - creatorId: sarahId, assigneeIds: [sarahId, lisaId], + creatorId: sarahId, }) const uiLibPlanId = await findOrCreatePlan('Component Library v2', { @@ -332,7 +327,7 @@ async function seed() { tags: ['frontend', 'a11y', 'design-system'], targetAssetIds: [uiLibId], startDate: '2026-03-01', endDate: '2026-04-15', deadline: '2026-04-20', - creatorId: mikeId, assigneeIds: [mikeId], + creatorId: mikeId, }) const apiV2PlanId = await findOrCreatePlan('API Gateway v2', { @@ -342,7 +337,7 @@ async function seed() { tags: ['api', 'backend', 'performance'], targetAssetIds: [apiGatewayId, workerSvcId], startDate: '2026-03-20', endDate: '2026-05-10', deadline: '2026-05-20', - creatorId: sarahId, assigneeIds: [sarahId, lisaId], + creatorId: sarahId, }) const analyticsPlanId = await findOrCreatePlan('Analytics Pipeline Overhaul', { @@ -352,7 +347,7 @@ async function seed() { tags: ['analytics', 'performance', 'urgent'], targetAssetIds: [analyticsDbId], startDate: '2026-05-01', endDate: '2026-05-31', deadline: '2026-06-07', - creatorId: alexId, assigneeIds: [alexId, lisaId], + creatorId: alexId, }) const pushPlanId = await findOrCreatePlan('Push Notifications', { @@ -362,7 +357,7 @@ async function seed() { tags: ['mobile', 'notifications', 'feature'], targetAssetIds: [iosId, androidId, bffId], startDate: '2026-04-10', endDate: '2026-05-15', deadline: '2026-05-20', - creatorId: sarahId, assigneeIds: [sarahId], + creatorId: sarahId, }) const androidPlanId = await findOrCreatePlan('Android Performance Optimization', { @@ -372,7 +367,7 @@ async function seed() { tags: ['mobile', 'android', 'performance'], targetAssetIds: [androidId], startDate: '2026-05-10', endDate: '2026-06-05', deadline: '2026-06-10', - creatorId: lisaId, assigneeIds: [lisaId], + creatorId: lisaId, }) await findOrCreatePlan('SSO & OAuth Integration', { @@ -382,7 +377,7 @@ async function seed() { tags: ['auth', 'sso', 'oauth'], targetAssetIds: [authSvcId], startDate: '2026-01-20', endDate: '2026-03-10', deadline: '2026-03-15', - creatorId: alexId, assigneeIds: [alexId, sarahId], + creatorId: alexId, }) await findOrCreatePlan('Database Schema v2', { @@ -391,7 +386,7 @@ async function seed() { productId: apiId, type: 'refactor', status: 'draft', tags: ['database', 'maintenance'], targetAssetIds: [postgresId], - creatorId: alexId, assigneeIds: [], + creatorId: alexId, }) // ── Tasks ────────────────────────────────────────────────────────────────── diff --git a/tests/helpers/db.ts b/tests/helpers/db.ts index a4ecc7e..43c4371 100644 --- a/tests/helpers/db.ts +++ b/tests/helpers/db.ts @@ -11,7 +11,6 @@ import { assets, codePlans, codePlanAssets, - codePlanAssignees, workItems, workItemCodePlans, tasks, @@ -33,7 +32,6 @@ export async function clearTables() { await d.delete(workItemCodePlans) await d.delete(workItems) await d.delete(codePlanAssets) - await d.delete(codePlanAssignees) await d.delete(tasks) await d.delete(codePlans) await d.delete(assets) @@ -224,14 +222,12 @@ export async function seedFixtures() { }, ]) - // 7b. Plan↔asset and plan↔assignee join rows (source of truth; arrays kept in step 7 - // to mirror the one-release rollback window) + // 7b. Plan↔asset join rows (source of truth for targeted assets). Plan + // assignees aren't a stored link — they're derived from task.assigneeId + // (task1 below, assigned to bob, is what makes planActive show bob). await d.insert(codePlanAssets).values([ { codePlanId: F.planActive, assetId: F.assetApi }, ]) - await d.insert(codePlanAssignees).values([ - { codePlanId: F.planActive, userId: F.bob }, - ]) // 8. Tasks await d.insert(tasks).values([ diff --git a/tests/lib/db/mutations.test.ts b/tests/lib/db/mutations.test.ts index b7bd1c2..066a2e7 100644 --- a/tests/lib/db/mutations.test.ts +++ b/tests/lib/db/mutations.test.ts @@ -178,7 +178,6 @@ describe('createCodePlan', () => { type: 'improvement', tags: ['perf'], targetAssetIds: [F.assetApi], - assigneeIds: [F.bob], }, F.alice, ) @@ -187,10 +186,11 @@ describe('createCodePlan', () => { expect(plan.status).toBe('draft') expect(plan.creatorId).toBe(F.alice) - // Links live in the join tables (array columns dropped in v0.3.0) + // Target assets live in the join table (array column dropped in v0.3.0). + // Assignees aren't a stored link at all — a fresh plan with no tasks has none. const detail = await getCodePlan(plan.id, F.alice) expect(detail!.targetAssetIds).toEqual([F.assetApi]) - expect(detail!.assigneeIds).toEqual([F.bob]) + expect(detail!.assignees).toEqual([]) }) }) @@ -205,12 +205,9 @@ describe('updateCodePlan', () => { const updated = await updateCodePlan(F.planActive, { title: 'Renamed Plan', tags: ['new-tag'], - assigneeIds: [], }) expect(updated!.title).toBe('Renamed Plan') expect(updated!.tags).toEqual(['new-tag']) - const detail = await getCodePlan(F.planActive, F.alice) - expect(detail!.assigneeIds).toEqual([]) }) it('returns null for non-existent plan', async () => { @@ -219,6 +216,29 @@ describe('updateCodePlan', () => { }) }) +describe('plan assignees (derived from task assignment)', () => { + it('reflects the unique set of assignees across the plan\'s tasks', async () => { + // Fixture: planActive already has task1 assigned to bob. + let detail = await getCodePlan(F.planActive, F.alice) + expect(detail!.assignees.map((a) => a.id)).toEqual([F.bob]) + + // Assigning task2 to carol adds her to the derived set. + await updateTask(F.task2, { assigneeId: F.carol }) + detail = await getCodePlan(F.planActive, F.alice) + expect(detail!.assignees.map((a) => a.id).sort()).toEqual([F.bob, F.carol].sort()) + + // Unassigning task1 removes bob once no task in the plan points to him. + await updateTask(F.task1, { assigneeId: null }) + detail = await getCodePlan(F.planActive, F.alice) + expect(detail!.assignees.map((a) => a.id)).toEqual([F.carol]) + }) + + it('a plan with no tasks has no assignees', async () => { + const detail = await getCodePlan(F.planDraft, F.alice) + expect(detail!.assignees).toEqual([]) + }) +}) + describe('deleteCodePlan', () => { it('deletes plan when userId is creator', async () => { const result = await deleteCodePlan(F.planDraft, F.alice) From f490e53d78df7a6b1a2f005d1a334af0ed8fa1e4 Mon Sep 17 00:00:00 2001 From: Sai Prakash Date: Tue, 14 Jul 2026 16:07:56 -0400 Subject: [PATCH 2/2] v0.3.22 Co-Authored-By: Claude Sonnet 5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ddb45a..2fcdf5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeplans", - "version": "0.3.21", + "version": "0.3.22", "description": "Manage and track coordinated changes across your software architecture.", "author": "Sai Prakash ", "homepage": "https://codeplans.ai",