From 8c3d55024b0dcd6f2bdce70ba78d7b6cd7787c4b Mon Sep 17 00:00:00 2001 From: Corentin Date: Fri, 8 May 2026 12:53:29 +0200 Subject: [PATCH] =?UTF-8?q?fix(acadenice):=20use=20camelCase=20row=20keys?= =?UTF-8?q?=20in=20sync-block=20repo=20=E2=80=94=20Patch=20027?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DatabaseModule registers a global CamelCasePlugin which converts every column name (including SELECT aliases) from snake_case to camelCase at runtime. The sync-block repo declared sql<{...}> result types in snake_case (workspace_id, created_at, etc.) and accessed those keys in mapRow / findUsages. At runtime kysely returned camelCase keys so every property read was undefined, causing 'Invalid time value' on new Date(undefined).toISOString(). Same pattern is likely present in graph.service.ts and backlinks services — Patch 028 will sweep those. Verified via curl: POST /api/acadenice/sync-blocks now returns 201 with the full DTO. Patch 027. --- .../sync-blocks/repos/sync-block.repo.ts | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/apps/server/src/core/acadenice/sync-blocks/repos/sync-block.repo.ts b/apps/server/src/core/acadenice/sync-blocks/repos/sync-block.repo.ts index 6a276856..77492ad5 100644 --- a/apps/server/src/core/acadenice/sync-blocks/repos/sync-block.repo.ts +++ b/apps/server/src/core/acadenice/sync-blocks/repos/sync-block.repo.ts @@ -24,11 +24,11 @@ export class SyncBlockRepo { ): Promise { const result = await sql<{ id: string; - workspace_id: string; + workspaceId: string; content: Record; - created_by: string; - created_at: Date; - updated_at: Date; + createdBy: string; + createdAt: Date; + updatedAt: Date; }>` INSERT INTO acadenice_sync_block (workspace_id, content, created_by) VALUES (${workspaceId}, ${JSON.stringify(content)}::jsonb, ${createdBy}) @@ -44,11 +44,11 @@ export class SyncBlockRepo { ): Promise { const result = await sql<{ id: string; - workspace_id: string; + workspaceId: string; content: Record; - created_by: string; - created_at: Date; - updated_at: Date; + createdBy: string; + createdAt: Date; + updatedAt: Date; }>` SELECT id, workspace_id, content, created_by, created_at, updated_at FROM acadenice_sync_block @@ -67,11 +67,11 @@ export class SyncBlockRepo { ): Promise { const result = await sql<{ id: string; - workspace_id: string; + workspaceId: string; content: Record; - created_by: string; - created_at: Date; - updated_at: Date; + createdBy: string; + createdAt: Date; + updatedAt: Date; }>` UPDATE acadenice_sync_block SET content = ${JSON.stringify(content)}::jsonb, updated_at = NOW() @@ -101,7 +101,7 @@ export class SyncBlockRepo { id: string, workspaceId: string, ): Promise { - const result = await sql<{ yjs_state: Buffer | null }>` + const result = await sql<{ yjsState: Buffer | null }>` SELECT yjs_state FROM acadenice_sync_block WHERE id = ${id} @@ -109,7 +109,7 @@ export class SyncBlockRepo { `.execute(this.db); if (result.rows.length === 0) return null; - return result.rows[0].yjs_state ?? null; + return result.rows[0].yjsState ?? null; } async delete(id: string, workspaceId: string): Promise { @@ -137,12 +137,15 @@ export class SyncBlockRepo { // Pages store their content as JSONB. We search for any object in the // content tree where type='syncBlock' and attrs->>'masterId' = blockId. // Using jsonb_path_exists for recursive search. + // CamelCasePlugin (in DatabaseModule) converts snake_case columns to + // camelCase at runtime, including SELECT aliases. So even though the SQL + // says "page_id" the row property is "pageId". const result = await sql<{ - page_id: string; + pageId: string; title: string | null; - slug_id: string; - space_id: string; - workspace_id: string; + slugId: string; + spaceId: string; + workspaceId: string; }>` SELECT DISTINCT p.id AS page_id, p.title, p.slug_id, p.space_id, s.workspace_id FROM pages p @@ -158,11 +161,11 @@ export class SyncBlockRepo { `.execute(this.db); return result.rows.map((r) => ({ - pageId: r.page_id, + pageId: r.pageId, pageTitle: r.title, - slugId: r.slug_id, - spaceId: r.space_id, - workspaceId: r.workspace_id, + slugId: r.slugId, + spaceId: r.spaceId, + workspaceId: r.workspaceId, })); } @@ -178,19 +181,19 @@ export class SyncBlockRepo { private mapRow(row: { id: string; - workspace_id: string; + workspaceId: string; content: Record; - created_by: string; - created_at: Date | string; - updated_at: Date | string; + createdBy: string; + createdAt: Date | string; + updatedAt: Date | string; }): SyncBlockResponseDto { return { id: row.id, - workspaceId: row.workspace_id, + workspaceId: row.workspaceId, content: row.content, - createdBy: row.created_by, - createdAt: new Date(row.created_at).toISOString(), - updatedAt: new Date(row.updated_at).toISOString(), + createdBy: row.createdBy, + createdAt: new Date(row.createdAt).toISOString(), + updatedAt: new Date(row.updatedAt).toISOString(), }; } }