fix(acadenice): pass content object directly to jsonb param + remove empty text nodes — Patch 028

Two bugs in template seed/instantiate:

1. ${JSON.stringify(content)}::jsonb made Postgres store the content as
   a jsonb scalar string (jsonb_typeof = 'string'), not an object. The
   instantiate read it back as a JSON-encoded string, which ProseMirror
   tried to parse as a node tree and crashed with 'Unknown node type:
   undefined' on the outer string. Pass the object directly with
   ${content as unknown as string}::jsonb so postgres-js binds it as
   a JSONB value.

2. Built-in template seed used { type: 'paragraph', content: [{ type:
   'text', text: '' }] } for empty paragraphs / list items / task
   items. ProseMirror schema rejects empty text nodes ('Empty text
   nodes are not allowed'). Replaced with content: [].

Verified via curl: POST /api/acadenice/templates/{id}/instantiate now
returns 201 with the new pageId/slugId.

Patch 028.
This commit is contained in:
Corentin JOGUET 2026-05-08 13:01:10 +02:00
parent 8c3d55024b
commit 9139fb8728
2 changed files with 16 additions and 16 deletions

View file

@ -35,13 +35,13 @@ const BUILT_IN_TEMPLATES: ReadonlyArray<BuiltInSpec> = [
{ type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: 'Meeting Note' }] },
{ type: 'paragraph', content: [{ type: 'text', marks: [{ type: 'bold' }], text: 'Date: ' }, { type: 'text', text: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Attendees' }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Agenda' }] },
{ type: 'orderedList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'orderedList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Notes' }] },
{ type: 'paragraph', content: [{ type: 'text', text: '' }] },
{ type: 'paragraph', content: [] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Action Items' }] },
{ type: 'taskList', content: [{ type: 'taskItem', attrs: { checked: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'taskList', content: [{ type: 'taskItem', attrs: { checked: false }, content: [{ type: 'paragraph', content: [] }] }] },
],
},
},
@ -59,11 +59,11 @@ const BUILT_IN_TEMPLATES: ReadonlyArray<BuiltInSpec> = [
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Scope' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'What is in scope / out of scope?' }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Stakeholders' }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Timeline' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'Key milestones and deadlines.' }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Risks' }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
],
},
},
@ -77,11 +77,11 @@ const BUILT_IN_TEMPLATES: ReadonlyArray<BuiltInSpec> = [
content: [
{ type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: 'Daily Standup' }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Yesterday' }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Today' }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Blockers' }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
],
},
},
@ -95,11 +95,11 @@ const BUILT_IN_TEMPLATES: ReadonlyArray<BuiltInSpec> = [
content: [
{ type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: 'Weekly Review' }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Wins' }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Challenges' }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [] }] }] },
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Next Week Priorities' }] },
{ type: 'taskList', content: [{ type: 'taskItem', attrs: { checked: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '' }] }] }] },
{ type: 'taskList', content: [{ type: 'taskItem', attrs: { checked: false }, content: [{ type: 'paragraph', content: [] }] }] },
],
},
},

View file

@ -157,7 +157,7 @@ export class TemplateService {
${dto.icon ?? null},
${dto.coverUrl ?? null},
${dto.category ?? null},
${JSON.stringify(content)}::jsonb,
${content as unknown as string}::jsonb,
${dto.sourcePageId ?? null},
false,
${userId}
@ -227,7 +227,7 @@ export class TemplateService {
}
const contentParam = dto.content
? sql`${JSON.stringify(dto.content)}::jsonb`
? sql`${dto.content as unknown as string}::jsonb`
: sql`content`;
const result = await sql<TemplateDto>`
@ -331,7 +331,7 @@ export class TemplateService {
${workspaceId},
${userId},
${userId},
${JSON.stringify(content)}::jsonb,
${content as unknown as string}::jsonb,
${textContent ?? ''},
${ydoc},
${nextPosition},
@ -461,7 +461,7 @@ export class TemplateService {
${spec.description},
${spec.icon ?? null},
${spec.category},
${JSON.stringify(spec.content)}::jsonb,
${spec.content as unknown as string}::jsonb,
true,
${systemUserId}
)