diff --git a/app/(dashboard)/actions.ts b/app/(dashboard)/actions.ts index a90ac1e..5cc80bd 100644 --- a/app/(dashboard)/actions.ts +++ b/app/(dashboard)/actions.ts @@ -2,7 +2,7 @@ import { getSpec, linkSpec } from '@/lib/db/specs' import { createdBy } from '@/lib/db/attribution' -import { isOrgOwner, canDeleteCodePlan, canDeleteRelease, canDeleteWorkItem, canDeleteTask } from '@/lib/db/authz' +import { isOrgOwner, canDeleteCodePlan, canDeleteRelease, canDeleteWorkItem, canDeleteTask, canDeleteAsset } from '@/lib/db/authz' import { getWorkItem } from '@/lib/db/queries' import { redirect } from 'next/navigation' import { revalidatePath } from 'next/cache' @@ -17,7 +17,8 @@ import { deleteProduct, createAsset, updateAsset, - deleteAsset, + archiveAsset, + restoreAsset, setAssetOwners, createCodePlan, updateCodePlan, @@ -198,10 +199,26 @@ export async function updateAssetAction(id: string, productSlug: string, formDat revalidatePath(`/products/${productSlug}`) } -export async function deleteAssetAction(id: string, productSlug: string) { - await requireUser() - await deleteAsset(id, await currentEditor()) +export async function archiveAssetAction(id: string, productSlug: string, reason?: string) { + const authUser = await requireUser() + if (!(await canDeleteAsset(authUser.id, id))) { + return { error: 'Only the asset\'s creator/owner, or an org owner/admin, can archive it.' } + } + await archiveAsset(id, reason, await currentEditor()) + revalidatePath(`/products/${productSlug}`) + revalidatePath(`/assets/${id}`) + revalidatePath('/assets') +} + +export async function restoreAssetAction(id: string, productSlug: string) { + const authUser = await requireUser() + if (!(await canDeleteAsset(authUser.id, id))) { + return { error: 'Only the asset\'s creator/owner, or an org owner/admin, can restore it.' } + } + await restoreAsset(id, await currentEditor()) revalidatePath(`/products/${productSlug}`) + revalidatePath(`/assets/${id}`) + revalidatePath('/assets') } export async function setAssetOwnersAction(assetId: string, productSlug: string, userIds: string[]) { diff --git a/app/(dashboard)/products/[slug]/assets-section.tsx b/app/(dashboard)/products/[slug]/assets-section.tsx index b7e59c3..14cddfb 100644 --- a/app/(dashboard)/products/[slug]/assets-section.tsx +++ b/app/(dashboard)/products/[slug]/assets-section.tsx @@ -32,7 +32,8 @@ import { import { Plus, Pencil, - Trash2, + Archive, + ArchiveRestore, Box, Server, Library, @@ -49,7 +50,7 @@ import type { Asset, AssetType } from '@/lib/types' import { cn } from '@/lib/utils' import { toast } from 'sonner' import { OwnerAvatars } from '@/components/owner-avatars' -import { createAssetAction, updateAssetAction, deleteAssetAction, setAssetOwnersAction } from '../../actions' +import { createAssetAction, updateAssetAction, archiveAssetAction, restoreAssetAction, setAssetOwnersAction } from '../../actions' const assetTypeIcons: Record = { app: Box, @@ -138,11 +139,15 @@ export function AssetsSection({ members?: MemberOption[] }) { const [openAsset, setOpenAsset] = useState(null) + const [showArchived, setShowArchived] = useState(false) // Keep the panel in sync with refreshed server data after an edit const currentAsset = openAsset ? assets.find((a) => a.id === openAsset.id) ?? null : null - const assetsByType = assets.reduce((acc, asset) => { + const activeAssets = assets.filter((a) => !a.archivedAt) + const archivedAssets = assets.filter((a) => a.archivedAt) + + const assetsByType = activeAssets.reduce((acc, asset) => { if (!acc[asset.type]) acc[asset.type] = [] acc[asset.type].push(asset) return acc @@ -168,7 +173,7 @@ export function AssetsSection({ ) })} - {assets.length === 0 && ( + {activeAssets.length === 0 && ( @@ -181,6 +186,29 @@ export function AssetsSection({ )} + {archivedAssets.length > 0 && ( +
+ + {showArchived && ( +
+ {archivedAssets.map((asset) => ( + + ))} +
+ )} +
+ )} + { if (!o) setOpenAsset(null) }}> {currentAsset && ( @@ -209,7 +237,10 @@ function AssetCard({ asset, onOpen }: { asset: Asset; onOpen: (asset: Asset) =>
- {asset.name} +
+ {asset.name} + {asset.archivedAt && Archived} +

{assetTypeLabels[asset.type]}

@@ -323,13 +354,28 @@ function AssetEditor({ }) } - function handleDelete() { + function handleArchive() { startTransition(async () => { - await deleteAssetAction(asset.id, productSlug) + const result = await archiveAssetAction(asset.id, productSlug) + if (result?.error) { + toast.error(result.error) + return + } onDeleted() }) } + function handleRestore() { + startTransition(async () => { + const result = await restoreAssetAction(asset.id, productSlug) + if (result?.error) { + toast.error(result.error) + return + } + toast.success('Asset restored') + }) + } + return ( <> @@ -338,7 +384,10 @@ function AssetEditor({
- {asset.name} +
+ {asset.name} + {asset.archivedAt && Archived} +
{assetTypeLabels[asset.type]}
@@ -458,28 +507,35 @@ function AssetEditor({ - - - - - - - Delete asset? - - This will permanently delete “{asset.name}”. Tasks and work items pointing at it lose their asset link. This action cannot be undone. - - - - Cancel - - {isPending ? 'Deleting…' : 'Delete'} - - - - + {asset.archivedAt ? ( + + ) : ( + + + + + + + Archive this asset? + + “{asset.name}” will be hidden from asset lists, pickers, and Atlas. Tasks, work items, dependency edges, and plan/release links that already point at it keep working — nothing is deleted or unlinked. You can restore it later. + + + + Cancel + + {isPending ? 'Archiving…' : 'Archive'} + + + + + )} ) diff --git a/app/api/mcp/[transport]/route.ts b/app/api/mcp/[transport]/route.ts index 1db83df..7c387b1 100644 --- a/app/api/mcp/[transport]/route.ts +++ b/app/api/mcp/[transport]/route.ts @@ -2,7 +2,7 @@ import { createSpec, updateSpec, supersedeSpec, linkSpec, unlinkSpec, getSpec, l import { createMcpHandler, withMcpAuth } from 'mcp-handler' import { z } from 'zod' import { verifyApiKey } from '@/lib/mcp/auth' -import { canDeleteCodePlan, canDeleteRelease, canDeleteWorkItem, canDeleteTask } from '@/lib/db/authz' +import { canDeleteCodePlan, canDeleteRelease, canDeleteWorkItem, canDeleteTask, canDeleteAsset } from '@/lib/db/authz' import { getProducts, getProduct, @@ -21,6 +21,8 @@ import { updateProduct, createAsset, updateAsset, + archiveAsset, + restoreAsset, setAssetOwners, createAssetDependency, deleteAssetDependency, @@ -325,6 +327,58 @@ const handler = createMcpHandler( }, ) + server.tool( + 'archive_asset', + "Archive an asset — the delete-equivalent action for assets. Hides it from asset lists/pickers and Atlas, but does NOT delete it or touch anything referencing it: work items, tasks, dependency edges, plan/release links all keep their reference (they just can no longer be newly assigned to it). Reversible via restore_asset. Only the asset's creator/owner, or an org owner/admin, may archive it.", + { id: z.string(), reason: z.string().optional() }, + async ({ id, reason }, extra) => { + requireWrite(extra) + const userId = uid(extra) + const options = await getAssetOptions(userId) + if (!options.some((a) => a.id === id)) return json({ error: 'Asset not found, not accessible, or already archived' }) + if (!(await canDeleteAsset(userId, id))) { + return json({ error: "Only the asset's creator/owner, or an org owner/admin, can archive it." }) + } + const { db } = await import('@/lib/db') + const { workItems, tasks, assetDependencies, codePlanAssets, releaseAssets } = await import('@/lib/db/schema') + const { eq, or } = await import('drizzle-orm') + const [workItemRows, taskRows, depRows, planAssetRows, releaseAssetRows] = await Promise.all([ + db.select({ id: workItems.id }).from(workItems).where(eq(workItems.assetId, id)), + db.select({ id: tasks.id }).from(tasks).where(eq(tasks.assetId, id)), + db.select({ id: assetDependencies.id }).from(assetDependencies).where(or(eq(assetDependencies.sourceAssetId, id), eq(assetDependencies.targetAssetId, id))), + db.select({ id: codePlanAssets.id }).from(codePlanAssets).where(eq(codePlanAssets.assetId, id)), + db.select({ id: releaseAssets.id }).from(releaseAssets).where(eq(releaseAssets.assetId, id)), + ]) + const archived = await archiveAsset(id, reason, { id: userId, kind: 'agent' }) + if (!archived) return json({ error: 'Asset not found' }) + return json({ + archived: true, + id, + referencedByWorkItemCount: workItemRows.length, + referencedByTaskCount: taskRows.length, + dependencyEdgeCount: depRows.length, + planTargetCount: planAssetRows.length, + releaseStampCount: releaseAssetRows.length, + note: 'None of the above were deleted or unlinked — they keep referencing this asset, it just no longer appears in lists or pickers until restored.', + }) + }, + ) + + server.tool( + 'restore_asset', + 'Restore a previously archived asset, making it visible again in lists, pickers, and Atlas. Same authorization as archive_asset.', + { id: z.string() }, + async ({ id }, extra) => { + requireWrite(extra) + const userId = uid(extra) + if (!(await canDeleteAsset(userId, id))) { + return json({ error: "Only the asset's creator/owner, or an org owner/admin, can restore it." }) + } + const restored = await restoreAsset(id, { id: userId, kind: 'agent' }) + return json(restored ?? { error: 'Asset not found' }) + }, + ) + server.tool( 'add_asset_dependency', 'Record that one asset depends on / integrates with / aggregates another — powers plan impact analysis. Map COORDINATION RISK, not the import graph: only edges where a change forces cross-asset coordination. Twenty curated edges beat two hundred stale ones.', diff --git a/lib/db/authz.ts b/lib/db/authz.ts index 15a6d97..79d2c82 100644 --- a/lib/db/authz.ts +++ b/lib/db/authz.ts @@ -1,5 +1,5 @@ import { db } from './index' -import { organizations, organizationMembers, products, codePlans, releases, workItems, tasks } from './schema' +import { organizations, organizationMembers, products, codePlans, releases, workItems, tasks, assets, assetOwners } from './schema' import { eq, and } from 'drizzle-orm' /** @@ -80,3 +80,20 @@ export async function canDeleteTask(userId: string, taskId: string): Promise { + const asset = await db.query.assets.findFirst({ where: eq(assets.id, assetId) }) + if (!asset) return false + if (await hasOrgOverride(await getProductOrgId(asset.productId), userId)) return true + if (asset.createdById === userId) return true + const owner = await db.query.assetOwners.findFirst({ + where: and(eq(assetOwners.assetId, assetId), eq(assetOwners.userId, userId)), + }) + return !!owner +} diff --git a/lib/db/migrations/postgres/0021_phase3_asset_archive.sql b/lib/db/migrations/postgres/0021_phase3_asset_archive.sql new file mode 100644 index 0000000..d44412c --- /dev/null +++ b/lib/db/migrations/postgres/0021_phase3_asset_archive.sql @@ -0,0 +1,7 @@ +ALTER TABLE "assets" ADD COLUMN "archived_at" timestamp with time zone; +--> statement-breakpoint +ALTER TABLE "assets" ADD COLUMN "archived_by_id" uuid; +--> statement-breakpoint +ALTER TABLE "assets" ADD COLUMN "archived_by_kind" text; +--> statement-breakpoint +ALTER TABLE "assets" ADD CONSTRAINT "assets_archived_by_id_users_id_fk" FOREIGN KEY ("archived_by_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action; diff --git a/lib/db/migrations/postgres/meta/_journal.json b/lib/db/migrations/postgres/meta/_journal.json index f7a28c6..1009593 100644 --- a/lib/db/migrations/postgres/meta/_journal.json +++ b/lib/db/migrations/postgres/meta/_journal.json @@ -148,6 +148,13 @@ "when": 1789412521000, "tag": "0020_backfill_creator_attribution", "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1789420000000, + "tag": "0021_phase3_asset_archive", + "breakpoints": true } ] } diff --git a/lib/db/migrations/sqlite/0021_phase3_asset_archive.sql b/lib/db/migrations/sqlite/0021_phase3_asset_archive.sql new file mode 100644 index 0000000..dd2bbc4 --- /dev/null +++ b/lib/db/migrations/sqlite/0021_phase3_asset_archive.sql @@ -0,0 +1,3 @@ +ALTER TABLE `assets` ADD `archived_at` integer;--> statement-breakpoint +ALTER TABLE `assets` ADD `archived_by_id` text REFERENCES users(id);--> statement-breakpoint +ALTER TABLE `assets` ADD `archived_by_kind` text; \ No newline at end of file diff --git a/lib/db/migrations/sqlite/meta/0021_snapshot.json b/lib/db/migrations/sqlite/meta/0021_snapshot.json new file mode 100644 index 0000000..d8f2cb5 --- /dev/null +++ b/lib/db/migrations/sqlite/meta/0021_snapshot.json @@ -0,0 +1,3865 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "bb879600-d324-4d3c-83d0-2542cb707c62", + "prevId": "a8bebcab-0912-4098-9afb-4ee1b50b6a41", + "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_capabilities": { + "name": "asset_capabilities", + "columns": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "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": "''" + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'graduated'" + }, + "origin_work_item_id": { + "name": "origin_work_item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_code_plan_id": { + "name": "origin_code_plan_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_release_id": { + "name": "origin_release_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_spec_id": { + "name": "source_spec_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_spec_version": { + "name": "source_spec_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_summary": { + "name": "origin_summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "verified_at": { + "name": "verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "removed_at": { + "name": "removed_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": { + "asset_capabilities_asset_idx": { + "name": "asset_capabilities_asset_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + }, + "asset_capabilities_origin_item_idx": { + "name": "asset_capabilities_origin_item_idx", + "columns": [ + "origin_work_item_id" + ], + "isUnique": true, + "where": "\"asset_capabilities\".\"origin_work_item_id\" IS NOT NULL" + } + }, + "foreignKeys": { + "asset_capabilities_created_by_id_users_id_fk": { + "name": "asset_capabilities_created_by_id_users_id_fk", + "tableFrom": "asset_capabilities", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_capabilities_updated_by_id_users_id_fk": { + "name": "asset_capabilities_updated_by_id_users_id_fk", + "tableFrom": "asset_capabilities", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_capabilities_asset_id_assets_id_fk": { + "name": "asset_capabilities_asset_id_assets_id_fk", + "tableFrom": "asset_capabilities", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "asset_capabilities_origin_work_item_id_work_items_id_fk": { + "name": "asset_capabilities_origin_work_item_id_work_items_id_fk", + "tableFrom": "asset_capabilities", + "tableTo": "work_items", + "columnsFrom": [ + "origin_work_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_capabilities_origin_code_plan_id_code_plans_id_fk": { + "name": "asset_capabilities_origin_code_plan_id_code_plans_id_fk", + "tableFrom": "asset_capabilities", + "tableTo": "code_plans", + "columnsFrom": [ + "origin_code_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_capabilities_origin_release_id_releases_id_fk": { + "name": "asset_capabilities_origin_release_id_releases_id_fk", + "tableFrom": "asset_capabilities", + "tableTo": "releases", + "columnsFrom": [ + "origin_release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_capabilities_source_spec_id_specs_id_fk": { + "name": "asset_capabilities_source_spec_id_specs_id_fk", + "tableFrom": "asset_capabilities", + "tableTo": "specs", + "columnsFrom": [ + "source_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "asset_dependencies": { + "name": "asset_dependencies", + "columns": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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_created_by_id_users_id_fk": { + "name": "asset_dependencies_created_by_id_users_id_fk", + "tableFrom": "asset_dependencies", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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": {} + }, + "asset_design_log": { + "name": "asset_design_log", + "columns": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_plan_id": { + "name": "code_plan_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "author_kind": { + "name": "author_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "author_id": { + "name": "author_id", + "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": { + "asset_design_log_asset_idx": { + "name": "asset_design_log_asset_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "asset_design_log_created_by_id_users_id_fk": { + "name": "asset_design_log_created_by_id_users_id_fk", + "tableFrom": "asset_design_log", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_design_log_updated_by_id_users_id_fk": { + "name": "asset_design_log_updated_by_id_users_id_fk", + "tableFrom": "asset_design_log", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_design_log_asset_id_assets_id_fk": { + "name": "asset_design_log_asset_id_assets_id_fk", + "tableFrom": "asset_design_log", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "asset_design_log_release_id_releases_id_fk": { + "name": "asset_design_log_release_id_releases_id_fk", + "tableFrom": "asset_design_log", + "tableTo": "releases", + "columnsFrom": [ + "release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_design_log_code_plan_id_code_plans_id_fk": { + "name": "asset_design_log_code_plan_id_code_plans_id_fk", + "tableFrom": "asset_design_log", + "tableTo": "code_plans", + "columnsFrom": [ + "code_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_design_log_author_id_users_id_fk": { + "name": "asset_design_log_author_id_users_id_fk", + "tableFrom": "asset_design_log", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "asset_owners": { + "name": "asset_owners", + "columns": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "asset_owners_asset_user_idx": { + "name": "asset_owners_asset_user_idx", + "columns": [ + "asset_id", + "user_id" + ], + "isUnique": true + }, + "asset_owners_user_idx": { + "name": "asset_owners_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "asset_owners_created_by_id_users_id_fk": { + "name": "asset_owners_created_by_id_users_id_fk", + "tableFrom": "asset_owners", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_owners_asset_id_assets_id_fk": { + "name": "asset_owners_asset_id_assets_id_fk", + "tableFrom": "asset_owners", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "asset_owners_user_id_users_id_fk": { + "name": "asset_owners_user_id_users_id_fk", + "tableFrom": "asset_owners", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "notes": { + "name": "notes", + "type": "text", + "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 + }, + "layer": { + "name": "layer", + "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": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_by_id": { + "name": "archived_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_by_kind": { + "name": "archived_by_kind", + "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": { + "assets_created_by_id_users_id_fk": { + "name": "assets_created_by_id_users_id_fk", + "tableFrom": "assets", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assets_updated_by_id_users_id_fk": { + "name": "assets_updated_by_id_users_id_fk", + "tableFrom": "assets", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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" + }, + "assets_archived_by_id_users_id_fk": { + "name": "assets_archived_by_id_users_id_fk", + "tableFrom": "assets", + "tableTo": "users", + "columnsFrom": [ + "archived_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "release_id": { + "name": "release_id", + "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_created_by_id_users_id_fk": { + "name": "code_plans_created_by_id_users_id_fk", + "tableFrom": "code_plans", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "code_plans_updated_by_id_users_id_fk": { + "name": "code_plans_updated_by_id_users_id_fk", + "tableFrom": "code_plans", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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_release_id_releases_id_fk": { + "name": "code_plans_release_id_releases_id_fk", + "tableFrom": "code_plans", + "tableTo": "releases", + "columnsFrom": [ + "release_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": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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_created_by_id_users_id_fk": { + "name": "organization_members_created_by_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "organizations_created_by_id_users_id_fk": { + "name": "organizations_created_by_id_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "organizations_updated_by_id_users_id_fk": { + "name": "organizations_updated_by_id_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "products_slug_unique": { + "name": "products_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "products_created_by_id_users_id_fk": { + "name": "products_created_by_id_users_id_fk", + "tableFrom": "products", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "products_updated_by_id_users_id_fk": { + "name": "products_updated_by_id_users_id_fk", + "tableFrom": "products", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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": {} + }, + "release_assets": { + "name": "release_assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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": { + "release_assets_release_asset_idx": { + "name": "release_assets_release_asset_idx", + "columns": [ + "release_id", + "asset_id" + ], + "isUnique": true + }, + "release_assets_asset_idx": { + "name": "release_assets_asset_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "release_assets_release_id_releases_id_fk": { + "name": "release_assets_release_id_releases_id_fk", + "tableFrom": "release_assets", + "tableTo": "releases", + "columnsFrom": [ + "release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_assets_asset_id_assets_id_fk": { + "name": "release_assets_asset_id_assets_id_fk", + "tableFrom": "release_assets", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "releases": { + "name": "releases", + "columns": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'planned'" + }, + "shipped_at": { + "name": "shipped_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "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": { + "releases_connection_external_idx": { + "name": "releases_connection_external_idx", + "columns": [ + "connection_id", + "external_id" + ], + "isUnique": true + }, + "releases_product_idx": { + "name": "releases_product_idx", + "columns": [ + "product_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "releases_created_by_id_users_id_fk": { + "name": "releases_created_by_id_users_id_fk", + "tableFrom": "releases", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "releases_updated_by_id_users_id_fk": { + "name": "releases_updated_by_id_users_id_fk", + "tableFrom": "releases", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "releases_product_id_products_id_fk": { + "name": "releases_product_id_products_id_fk", + "tableFrom": "releases", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "releases_creator_id_users_id_fk": { + "name": "releases_creator_id_users_id_fk", + "tableFrom": "releases", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "releases_connection_id_integrations_id_fk": { + "name": "releases_connection_id_integrations_id_fk", + "tableFrom": "releases", + "tableTo": "integrations", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "spec_events": { + "name": "spec_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "spec_id": { + "name": "spec_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spec_title": { + "name": "spec_title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spec_type": { + "name": "spec_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_version": { + "name": "from_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "to_version": { + "name": "to_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "work_item_id": { + "name": "work_item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note_id": { + "name": "note_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "spec_events_asset_idx": { + "name": "spec_events_asset_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "spec_events_spec_id_specs_id_fk": { + "name": "spec_events_spec_id_specs_id_fk", + "tableFrom": "spec_events", + "tableTo": "specs", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_events_asset_id_assets_id_fk": { + "name": "spec_events_asset_id_assets_id_fk", + "tableFrom": "spec_events", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_events_plan_id_code_plans_id_fk": { + "name": "spec_events_plan_id_code_plans_id_fk", + "tableFrom": "spec_events", + "tableTo": "code_plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_events_work_item_id_work_items_id_fk": { + "name": "spec_events_work_item_id_work_items_id_fk", + "tableFrom": "spec_events", + "tableTo": "work_items", + "columnsFrom": [ + "work_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_events_note_id_asset_design_log_id_fk": { + "name": "spec_events_note_id_asset_design_log_id_fk", + "tableFrom": "spec_events", + "tableTo": "asset_design_log", + "columnsFrom": [ + "note_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "spec_links": { + "name": "spec_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "spec_id": { + "name": "spec_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "relationship_type": { + "name": "relationship_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "spec_links_target_idx": { + "name": "spec_links_target_idx", + "columns": [ + "spec_id", + "target_type", + "target_id" + ], + "isUnique": true + }, + "spec_links_lookup_idx": { + "name": "spec_links_lookup_idx", + "columns": [ + "target_type", + "target_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "spec_links_spec_id_specs_id_fk": { + "name": "spec_links_spec_id_specs_id_fk", + "tableFrom": "spec_links", + "tableTo": "specs", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "specs": { + "name": "specs", + "columns": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spec_type": { + "name": "spec_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "supersedes": { + "name": "supersedes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "superseded_by": { + "name": "superseded_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'native'" + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "needs_review": { + "name": "needs_review", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "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": { + "specs_product_idx": { + "name": "specs_product_idx", + "columns": [ + "product_id" + ], + "isUnique": false + }, + "specs_import_url_idx": { + "name": "specs_import_url_idx", + "columns": [ + "product_id", + "source_url" + ], + "isUnique": true, + "where": "\"specs\".\"source_type\" = 'git_import' AND \"specs\".\"supersedes\" IS NULL" + } + }, + "foreignKeys": { + "specs_created_by_id_users_id_fk": { + "name": "specs_created_by_id_users_id_fk", + "tableFrom": "specs", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "specs_updated_by_id_users_id_fk": { + "name": "specs_updated_by_id_users_id_fk", + "tableFrom": "specs", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "specs_product_id_products_id_fk": { + "name": "specs_product_id_products_id_fk", + "tableFrom": "specs", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "specs_supersedes_specs_id_fk": { + "name": "specs_supersedes_specs_id_fk", + "tableFrom": "specs", + "tableTo": "specs", + "columnsFrom": [ + "supersedes" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "specs_superseded_by_specs_id_fk": { + "name": "specs_superseded_by_specs_id_fk", + "tableFrom": "specs", + "tableTo": "specs", + "columnsFrom": [ + "superseded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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_created_by_id_users_id_fk": { + "name": "tasks_created_by_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_updated_by_id_users_id_fk": { + "name": "tasks_updated_by_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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_created_by_id_users_id_fk": { + "name": "work_item_code_plans_created_by_id_users_id_fk", + "tableFrom": "work_item_code_plans", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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": { + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_kind": { + "name": "created_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by_kind": { + "name": "updated_by_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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_created_by_id_users_id_fk": { + "name": "work_items_created_by_id_users_id_fk", + "tableFrom": "work_items", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "work_items_updated_by_id_users_id_fk": { + "name": "work_items_updated_by_id_users_id_fk", + "tableFrom": "work_items", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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 447a61a..399a3bf 100644 --- a/lib/db/migrations/sqlite/meta/_journal.json +++ b/lib/db/migrations/sqlite/meta/_journal.json @@ -148,6 +148,13 @@ "when": 1789412521000, "tag": "0020_backfill_creator_attribution", "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1789420000000, + "tag": "0021_phase3_asset_archive", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/mutations.ts b/lib/db/mutations.ts index 68db6e3..0b27273 100644 --- a/lib/db/mutations.ts +++ b/lib/db/mutations.ts @@ -154,6 +154,12 @@ export async function updateAsset(id: string, data: UpdateAssetData, actor?: Art return asset ?? null } +/** + * Hard delete. Not called by the normal UI/MCP path — archiveAsset is the + * user-facing "delete" action (spec "Deletion & Cascade Design for Core + * Entities", Phase 3). Kept for a possible future admin-only purge of + * genuinely empty/orphaned assets. + */ export async function deleteAsset(id: string, actor?: ArtifactActor) { const [deleted] = await db .delete(assets) @@ -166,6 +172,39 @@ export async function deleteAsset(id: string, actor?: ArtifactActor) { return deleted ?? null } +/** + * Soft-delete tombstone — the asset row is kept, just hidden from default + * views (see the isNull(assets.archivedAt) filters in lib/db/queries.ts). + * Idempotent: archiving an already-archived asset just updates who/when. + */ +export async function archiveAsset(id: string, reason?: string, actor?: ArtifactActor) { + const existing = await db.query.assets.findFirst({ where: eq(assets.id, id) }) + if (!existing) return null + const [archived] = await db + .update(assets) + .set({ + archivedAt: new Date(), + archivedById: actor?.id ?? null, + archivedByKind: actor ? (actor.kind ?? 'user') : null, + updatedAt: new Date(), + ...(reason ? { notes: `${existing.notes ? existing.notes + '\n\n' : ''}**Archived:** ${reason}` } : {}), + }) + .where(eq(assets.id, id)) + .returning() + if (archived) await logAudit({ entityType: 'asset', entityId: archived.id, event: 'archived', actor, payload: { name: archived.name } }) + return archived ?? null +} + +export async function restoreAsset(id: string, actor?: ArtifactActor) { + const [restored] = await db + .update(assets) + .set({ archivedAt: null, archivedById: null, archivedByKind: null, updatedAt: new Date() }) + .where(eq(assets.id, id)) + .returning() + if (restored) await logAudit({ entityType: 'asset', entityId: restored.id, event: 'restored', actor, payload: { name: restored.name } }) + return restored ?? null +} + /** Replace the full owner set for an asset. */ export async function setAssetOwners(assetId: string, userIds: string[], actor?: ArtifactActor) { await db.delete(assetOwners).where(eq(assetOwners.assetId, assetId)) diff --git a/lib/db/queries.ts b/lib/db/queries.ts index f6f7c3b..b9e135b 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -21,7 +21,7 @@ import { assetDesignLog, assetCapabilities, } from './schema' -import { eq, and, sql, desc, or, inArray, gte, isNotNull } from 'drizzle-orm' +import { eq, and, sql, desc, or, inArray, gte, isNotNull, isNull } from 'drizzle-orm' import type { Product, Asset, @@ -104,7 +104,7 @@ export async function getDashboardStats(userId: string, productId?: string): Pro db .select({ count: sql`CAST(count(*) AS INTEGER)` }) .from(assets) - .where(inArray(assets.productId, ids)), + .where(and(inArray(assets.productId, ids), isNull(assets.archivedAt))), db .select({ count: sql`CAST(count(*) AS INTEGER)` }) .from(codePlans) @@ -234,6 +234,8 @@ export async function getProduct(slug: string, userId: string): Promise<(Product const product = await db.query.products.findFirst({ where: productFilter }) if (!product) return null + // Includes archived assets — the product page shows them in a separate + // section (with a restore action) rather than hiding them entirely. const productAssets = await db.query.assets.findMany({ where: eq(assets.productId, product.id), orderBy: desc(assets.createdAt), @@ -276,7 +278,7 @@ export async function getProduct(slug: string, userId: string): Promise<(Product tags: product.tags, organizationId: product.organizationId ?? undefined, creatorId: product.creatorId, - assetCount: productAssets.length, + assetCount: productAssets.filter((a) => !a.archivedAt).length, activePlanCount: activePlanCount?.count ?? 0, createdAt: product.createdAt.toISOString(), assets: productAssets.map((a) => ({ @@ -297,6 +299,13 @@ export async function getProduct(slug: string, userId: string): Promise<(Product documentationUrl: a.documentationUrl ?? undefined, dependencies: [], // asset_dependencies resolved separately when needed createdAt: a.createdAt.toISOString(), + createdById: a.createdById, + createdByKind: a.createdByKind, + updatedById: a.updatedById, + updatedByKind: a.updatedByKind, + archivedAt: a.archivedAt?.toISOString() ?? null, + archivedById: a.archivedById, + archivedByKind: a.archivedByKind, })), } } @@ -837,7 +846,7 @@ export async function getAssetOptions( .select({ id: assets.id, name: assets.name, productId: assets.productId }) .from(assets) .innerJoin(products, eq(assets.productId, products.id)) - .where(productFilter) + .where(and(productFilter, isNull(assets.archivedAt))) .orderBy(assets.name) } @@ -856,7 +865,7 @@ export async function getAssetDebtInfo(userId: string): Promise .select({ id: assets.id, health: assets.health, techDebtScore: assets.techDebtScore }) .from(assets) .innerJoin(products, eq(assets.productId, products.id)) - .where(productFilter) + .where(and(productFilter, isNull(assets.archivedAt))) const owners = await ownersByAsset(rows.map((r) => r.id)) return rows.map((r) => ({ ...r, owners: owners.get(r.id) ?? [] })) } @@ -889,7 +898,7 @@ export async function getOwnedAssets(userId: string): Promise { .from(assetOwners) .innerJoin(assets, eq(assetOwners.assetId, assets.id)) .innerJoin(products, eq(assets.productId, products.id)) - .where(eq(assetOwners.userId, userId)) + .where(and(eq(assetOwners.userId, userId), isNull(assets.archivedAt))) .orderBy(assets.name) if (rows.length === 0) return [] @@ -1017,7 +1026,7 @@ export async function getAssetDetail(id: string, userId: string): Promise r.assetId), createdAt: asset.createdAt.toISOString(), + createdById: asset.createdById, + createdByKind: asset.createdByKind, + updatedById: asset.updatedById, + updatedByKind: asset.updatedByKind, + archivedAt: asset.archivedAt?.toISOString() ?? null, + archivedById: asset.archivedById, + archivedByKind: asset.archivedByKind, productName: product.name, productSlug: product.slug, plans: planRows.map((r) => ({ @@ -1338,7 +1354,7 @@ export async function getProductDependencyEdges(productId: string): Promise p.id) const assetRows = await db.query.assets.findMany({ - where: inArray(assets.productId, productIds), + where: and(inArray(assets.productId, productIds), isNull(assets.archivedAt)), orderBy: assets.name, }) const assetIds = assetRows.map((a) => a.id) diff --git a/lib/db/schema.pg.ts b/lib/db/schema.pg.ts index 986ee6b..fab2352 100644 --- a/lib/db/schema.pg.ts +++ b/lib/db/schema.pg.ts @@ -144,6 +144,13 @@ export const assets = pgTable('assets', { layer: text('layer'), documentationUrl: text('documentation_url'), metadata: jsonb('metadata').notNull().default({}), + // Soft-delete tombstone (spec "Deletion & Cascade Design for Core Entities", + // Phase 3) — archived means removedAt IS NOT NULL. Never hard-deleted by + // the normal UI/MCP path; deleteAsset remains for a possible future + // admin-only purge of genuinely empty/orphaned assets. + archivedAt: timestamp('archived_at', { withTimezone: true }), + archivedById: uuid('archived_by_id').references(() => users.id, { onDelete: 'set null' }), + archivedByKind: text('archived_by_kind'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }) diff --git a/lib/db/schema.sqlite.ts b/lib/db/schema.sqlite.ts index 640a2ca..cb1b226 100644 --- a/lib/db/schema.sqlite.ts +++ b/lib/db/schema.sqlite.ts @@ -138,6 +138,13 @@ export const assets = sqliteTable('assets', { layer: text('layer'), documentationUrl: text('documentation_url'), metadata: text('metadata', { mode: 'json' }).$type>().notNull().default({}), + // Soft-delete tombstone (spec "Deletion & Cascade Design for Core Entities", + // Phase 3) — archived means archivedAt IS NOT NULL. Never hard-deleted by + // the normal UI/MCP path; deleteAsset remains for a possible future + // admin-only purge of genuinely empty/orphaned assets. + archivedAt: integer('archived_at', { mode: 'timestamp' }), + archivedById: text('archived_by_id').references(() => users.id, { onDelete: 'set null' }), + archivedByKind: text('archived_by_kind'), createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), }) diff --git a/lib/types.ts b/lib/types.ts index a519b92..ae0dd4e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -76,7 +76,7 @@ export interface AssetOwner { avatarUrl?: string } -export interface Asset { +export interface Asset extends Attribution { id: string productId: string name: string @@ -100,6 +100,10 @@ export interface Asset { documentationUrl?: string dependencies: string[] createdAt: string + /** Soft-delete tombstone — null means active. See lib/db/authz.ts canDeleteAsset. */ + archivedAt?: string | null + archivedById?: string | null + archivedByKind?: string | null } /** diff --git a/tests/lib/ai.test.ts b/tests/lib/ai.test.ts index 7c3397c..b596fbb 100644 --- a/tests/lib/ai.test.ts +++ b/tests/lib/ai.test.ts @@ -12,6 +12,10 @@ const release: ReleaseDetail = { creatorId: 'user-1', createdAt: '2026-08-01T00:00:00.000Z', updatedAt: '2026-08-01T00:00:00.000Z', + createdById: null, + createdByKind: null, + updatedById: null, + updatedByKind: null, productName: 'Shared Product', productSlug: 'shared-product', planCount: 1, diff --git a/tests/lib/db/authz.test.ts b/tests/lib/db/authz.test.ts index 784a3bd..355a173 100644 --- a/tests/lib/db/authz.test.ts +++ b/tests/lib/db/authz.test.ts @@ -7,8 +7,9 @@ import { canDeleteRelease, canDeleteWorkItem, canDeleteTask, + canDeleteAsset, } from '@/lib/db/authz' -import { createCodePlan, createRelease, createWorkItem } from '@/lib/db/mutations' +import { createCodePlan, createRelease, createWorkItem, createAsset, setAssetOwners } from '@/lib/db/mutations' beforeAll(async () => { await runMigrations() @@ -132,3 +133,39 @@ describe('canDeleteTask', () => { expect(await canDeleteTask(F.carol, F.task1)).toBe(false) }) }) + +describe('canDeleteAsset', () => { + it('blocks an unrelated editor with no creator/owner relationship', async () => { + // F.assetApi has no createdById and no assetOwners row in seedFixtures. + expect(await canDeleteAsset(F.bob, F.assetApi)).toBe(false) + }) + + it('allows the org owner even without creating or owning the asset', async () => { + expect(await canDeleteAsset(F.alice, F.assetApi)).toBe(true) + }) + + it('allows the creator', async () => { + const asset = await createAsset({ + productId: F.productShared, + name: 'Notification Service', + type: 'service', + description: 'Sends notifications', + tags: [], + }, { id: F.bob }) + expect(await canDeleteAsset(F.bob, asset.id)).toBe(true) + expect(await canDeleteAsset(F.carol, asset.id)).toBe(false) + }) + + it('allows a declared asset owner who did not create it', async () => { + await setAssetOwners(F.assetDb, [F.bob]) + expect(await canDeleteAsset(F.bob, F.assetDb)).toBe(true) + }) + + it('blocks a user unrelated to the org entirely', async () => { + expect(await canDeleteAsset(F.carol, F.assetApi)).toBe(false) + }) + + it('returns false for a nonexistent asset', async () => { + expect(await canDeleteAsset(F.alice, 'asset-does-not-exist')).toBe(false) + }) +}) diff --git a/tests/lib/db/mutations.test.ts b/tests/lib/db/mutations.test.ts index d190972..88796e4 100644 --- a/tests/lib/db/mutations.test.ts +++ b/tests/lib/db/mutations.test.ts @@ -7,6 +7,8 @@ import { createAsset, updateAsset, deleteAsset, + archiveAsset, + restoreAsset, createCodePlan, updateCodePlan, deleteCodePlan, @@ -164,6 +166,57 @@ describe('deleteAsset', () => { }) }) +describe('archiveAsset', () => { + it('sets archivedAt/archivedById/archivedByKind and stamps updatedAt', async () => { + const before = Math.floor(Date.now() / 1000) * 1000 + const archived = await archiveAsset(F.assetDb, undefined, { id: F.alice }) + expect(archived).not.toBeNull() + expect(archived!.archivedAt).not.toBeNull() + expect(archived!.archivedById).toBe(F.alice) + expect(archived!.archivedByKind).toBe('user') + expect(archived!.updatedAt.getTime()).toBeGreaterThanOrEqual(before) + }) + + it('appends the optional reason to notes', async () => { + const archived = await archiveAsset(F.assetDb, 'Replaced by managed Postgres', { id: F.alice }) + expect(archived!.notes).toContain('Replaced by managed Postgres') + }) + + it('does not delete the row or touch anything referencing it', async () => { + const archived = await archiveAsset(F.assetApi) + expect(archived).not.toBeNull() + const plan = await getCodePlan(F.planActive, F.alice) + expect(plan!.targetAssets.some((a) => a.id === F.assetApi)).toBe(true) + }) + + it('is idempotent — archiving an already-archived asset just updates who/when', async () => { + await archiveAsset(F.assetDb, undefined, { id: F.alice }) + const second = await archiveAsset(F.assetDb, undefined, { id: F.bob }) + expect(second!.archivedById).toBe(F.bob) + }) + + it('returns null for non-existent asset', async () => { + const result = await archiveAsset('nonexistent') + expect(result).toBeNull() + }) +}) + +describe('restoreAsset', () => { + it('clears archivedAt/archivedById/archivedByKind', async () => { + await archiveAsset(F.assetDb, undefined, { id: F.alice }) + const restored = await restoreAsset(F.assetDb) + expect(restored).not.toBeNull() + expect(restored!.archivedAt).toBeNull() + expect(restored!.archivedById).toBeNull() + expect(restored!.archivedByKind).toBeNull() + }) + + it('returns null for non-existent asset', async () => { + const result = await restoreAsset('nonexistent') + expect(result).toBeNull() + }) +}) + // --------------------------------------------------------------------------- // Code Plans // ---------------------------------------------------------------------------