From b0831a6872ebc59d3656bdc307012eae4331e376 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 11 May 2026 11:06:41 +0200 Subject: [PATCH] feat(documents): ensureSystemRoots + wire into createPort Adds idempotent root-folder bootstrap (Clients/Companies/Yachts) called on every port-init. ON CONFLICT DO NOTHING on the sibling-name unique index prevents racing inserts; the re-SELECT returns the stable row set in SYSTEM_ROOT_NAMES order. Same helper is invoked by the backfill script in a later task. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/services/document-folders.service.ts | 38 +++++++++++ src/lib/services/ports.service.ts | 3 + .../document-folders-system-folders.test.ts | 63 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 tests/unit/document-folders-system-folders.test.ts diff --git a/src/lib/services/document-folders.service.ts b/src/lib/services/document-folders.service.ts index fda8c515..46ca9541 100644 --- a/src/lib/services/document-folders.service.ts +++ b/src/lib/services/document-folders.service.ts @@ -299,3 +299,41 @@ export function collectDescendantIds(tree: FolderNode[], rootId: string): string visit(tree, false); return out; } + +const SYSTEM_ROOT_NAMES = ['Clients', 'Companies', 'Yachts'] as const; +type SystemRootName = (typeof SYSTEM_ROOT_NAMES)[number]; + +/** + * Idempotently create the three system root folders for a port + * (`Clients/`, `Companies/`, `Yachts/`). Returns the rows in stable + * order. Safe to call on every port-init and on every backfill run. + * + * Uses INSERT … ON CONFLICT … DO NOTHING via the sibling-name unique + * index (`uniq_document_folders_sibling_name`) so a concurrent caller + * can't race two inserts of the same root. Re-SELECTs on conflict so + * the return shape is always populated. + */ +export async function ensureSystemRoots(portId: string, userId: string): Promise { + const values = SYSTEM_ROOT_NAMES.map((name) => ({ + portId, + parentId: null, + name, + systemManaged: true, + entityType: 'root' as const, + entityId: null, + createdBy: userId, + })); + + await db.insert(documentFolders).values(values).onConflictDoNothing(); + + const rows = await db + .select() + .from(documentFolders) + .where(and(eq(documentFolders.portId, portId), eq(documentFolders.entityType, 'root'))); + + return SYSTEM_ROOT_NAMES.map((name: SystemRootName) => { + const row = rows.find((r) => r.name === name); + if (!row) throw new Error(`ensureSystemRoots: missing root ${name} after upsert`); + return row; + }); +} diff --git a/src/lib/services/ports.service.ts b/src/lib/services/ports.service.ts index 843fde46..9dc837ea 100644 --- a/src/lib/services/ports.service.ts +++ b/src/lib/services/ports.service.ts @@ -7,6 +7,7 @@ import { createAuditLog, type AuditMeta } from '@/lib/audit'; import { ConflictError, NotFoundError } from '@/lib/errors'; import { emitToRoom } from '@/lib/socket/server'; import type { CreatePortInput, UpdatePortInput } from '@/lib/validators/ports'; +import { ensureSystemRoots } from '@/lib/services/document-folders.service'; export async function listPorts() { return db.select().from(ports).orderBy(ports.name); @@ -40,6 +41,8 @@ export async function createPort(data: CreatePortInput, meta: AuditMeta) { }) .returning(); + await ensureSystemRoots(port!.id, meta.userId); + void createAuditLog({ userId: meta.userId, portId: meta.portId, diff --git a/tests/unit/document-folders-system-folders.test.ts b/tests/unit/document-folders-system-folders.test.ts new file mode 100644 index 00000000..fd750f3f --- /dev/null +++ b/tests/unit/document-folders-system-folders.test.ts @@ -0,0 +1,63 @@ +/** + * Task 2 — ensureSystemRoots (TDD). + * + * Fixture convention: makePort from helpers/factories (async DB insert); + * TEST_USER_ID resolved once via beforeAll from a seeded user — same pattern + * as document-folders-crud.test.ts and alerts-tenant-isolation.test.ts. + */ + +import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; +import { and, eq } from 'drizzle-orm'; + +import { db } from '@/lib/db'; +import { documentFolders } from '@/lib/db/schema/documents'; +import { user } from '@/lib/db/schema/users'; +import { ensureSystemRoots } from '@/lib/services/document-folders.service'; +import { makePort } from '../helpers/factories'; + +let TEST_USER_ID = ''; + +beforeAll(async () => { + const [u] = await db.select({ id: user.id }).from(user).limit(1); + if (!u) throw new Error('No user available; run pnpm db:seed first'); + TEST_USER_ID = u.id; +}); + +describe('document-folders service · ensureSystemRoots', () => { + let portId: string; + + beforeEach(async () => { + const port = await makePort(); + portId = port.id; + await db.delete(documentFolders).where(eq(documentFolders.portId, portId)); + }); + + it('creates Clients, Companies, and Yachts root folders with system_managed=true', async () => { + await ensureSystemRoots(portId, TEST_USER_ID); + const rows = await db + .select() + .from(documentFolders) + .where(and(eq(documentFolders.portId, portId), eq(documentFolders.entityType, 'root'))); + expect(rows.map((r) => r.name).sort()).toEqual(['Clients', 'Companies', 'Yachts']); + for (const r of rows) { + expect(r.systemManaged).toBe(true); + expect(r.parentId).toBeNull(); + expect(r.entityId).toBeNull(); + } + }); + + it('is idempotent — second call does not create duplicates', async () => { + await ensureSystemRoots(portId, TEST_USER_ID); + await ensureSystemRoots(portId, TEST_USER_ID); + const rows = await db + .select() + .from(documentFolders) + .where(and(eq(documentFolders.portId, portId), eq(documentFolders.entityType, 'root'))); + expect(rows).toHaveLength(3); + }); + + it('returns the three root rows in a stable order (Clients, Companies, Yachts)', async () => { + const roots = await ensureSystemRoots(portId, TEST_USER_ID); + expect(roots.map((r) => r.name)).toEqual(['Clients', 'Companies', 'Yachts']); + }); +});