@allpepper/task-orchestrator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/package.json +51 -0
- package/src/db/client.ts +34 -0
- package/src/db/index.ts +1 -0
- package/src/db/migrate.ts +51 -0
- package/src/db/migrations/001_initial_schema.sql +160 -0
- package/src/domain/index.ts +1 -0
- package/src/domain/types.ts +225 -0
- package/src/index.ts +7 -0
- package/src/repos/base.ts +151 -0
- package/src/repos/dependencies.ts +356 -0
- package/src/repos/features.ts +507 -0
- package/src/repos/index.ts +4 -0
- package/src/repos/projects.ts +350 -0
- package/src/repos/sections.ts +505 -0
- package/src/repos/tags.example.ts +125 -0
- package/src/repos/tags.ts +175 -0
- package/src/repos/tasks.ts +581 -0
- package/src/repos/templates.ts +649 -0
- package/src/server.ts +121 -0
- package/src/services/index.ts +2 -0
- package/src/services/status-validator.ts +100 -0
- package/src/services/workflow.ts +104 -0
- package/src/tools/apply-template.ts +129 -0
- package/src/tools/get-blocked-tasks.ts +63 -0
- package/src/tools/get-next-status.ts +183 -0
- package/src/tools/get-next-task.ts +75 -0
- package/src/tools/get-tag-usage.ts +54 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-tags.ts +56 -0
- package/src/tools/manage-container.ts +333 -0
- package/src/tools/manage-dependency.ts +198 -0
- package/src/tools/manage-sections.ts +388 -0
- package/src/tools/manage-template.ts +313 -0
- package/src/tools/query-container.ts +296 -0
- package/src/tools/query-dependencies.ts +68 -0
- package/src/tools/query-sections.ts +70 -0
- package/src/tools/query-templates.ts +137 -0
- package/src/tools/query-workflow-state.ts +198 -0
- package/src/tools/registry.ts +180 -0
- package/src/tools/rename-tag.ts +64 -0
- package/src/tools/setup-project.ts +189 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import {
|
|
2
|
+
db,
|
|
3
|
+
generateId,
|
|
4
|
+
now,
|
|
5
|
+
queryOne,
|
|
6
|
+
queryAll,
|
|
7
|
+
execute,
|
|
8
|
+
ok,
|
|
9
|
+
err,
|
|
10
|
+
saveTags,
|
|
11
|
+
deleteTags
|
|
12
|
+
} from './base';
|
|
13
|
+
import type { Result, Template, TemplateSection, Section } from '../domain/types';
|
|
14
|
+
import {
|
|
15
|
+
EntityType,
|
|
16
|
+
ContentFormat,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
ValidationError
|
|
19
|
+
} from '../domain/types';
|
|
20
|
+
import { transaction } from '../db/client';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Internal Types (DB Row Mapping)
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
interface TemplateRow {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
target_entity_type: string;
|
|
31
|
+
is_built_in: number;
|
|
32
|
+
is_protected: number;
|
|
33
|
+
is_enabled: number;
|
|
34
|
+
created_by: string | null;
|
|
35
|
+
tags: string;
|
|
36
|
+
created_at: string;
|
|
37
|
+
modified_at: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TemplateSectionRow {
|
|
41
|
+
id: string;
|
|
42
|
+
template_id: string;
|
|
43
|
+
title: string;
|
|
44
|
+
usage_description: string;
|
|
45
|
+
content_sample: string;
|
|
46
|
+
content_format: string;
|
|
47
|
+
ordinal: number;
|
|
48
|
+
is_required: number;
|
|
49
|
+
tags: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Mapping Helpers
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
function mapTemplateRow(row: TemplateRow): Template {
|
|
57
|
+
return {
|
|
58
|
+
id: row.id,
|
|
59
|
+
name: row.name,
|
|
60
|
+
description: row.description,
|
|
61
|
+
targetEntityType: row.target_entity_type as EntityType,
|
|
62
|
+
isBuiltIn: row.is_built_in === 1,
|
|
63
|
+
isProtected: row.is_protected === 1,
|
|
64
|
+
isEnabled: row.is_enabled === 1,
|
|
65
|
+
createdBy: row.created_by || undefined,
|
|
66
|
+
tags: row.tags,
|
|
67
|
+
createdAt: new Date(row.created_at),
|
|
68
|
+
modifiedAt: new Date(row.modified_at)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mapTemplateSectionRow(row: TemplateSectionRow): TemplateSection {
|
|
73
|
+
return {
|
|
74
|
+
id: row.id,
|
|
75
|
+
templateId: row.template_id,
|
|
76
|
+
title: row.title,
|
|
77
|
+
usageDescription: row.usage_description,
|
|
78
|
+
contentSample: row.content_sample,
|
|
79
|
+
contentFormat: row.content_format as ContentFormat,
|
|
80
|
+
ordinal: row.ordinal,
|
|
81
|
+
isRequired: row.is_required === 1,
|
|
82
|
+
tags: row.tags
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Public API
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
export interface CreateTemplateParams {
|
|
91
|
+
name: string;
|
|
92
|
+
description: string;
|
|
93
|
+
targetEntityType: string;
|
|
94
|
+
isBuiltIn?: boolean;
|
|
95
|
+
isProtected?: boolean;
|
|
96
|
+
createdBy?: string;
|
|
97
|
+
tags?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createTemplate(params: CreateTemplateParams): Result<Template> {
|
|
101
|
+
try {
|
|
102
|
+
// Validation
|
|
103
|
+
if (!params.name?.trim()) {
|
|
104
|
+
throw new ValidationError('Template name is required');
|
|
105
|
+
}
|
|
106
|
+
if (!params.description?.trim()) {
|
|
107
|
+
throw new ValidationError('Template description is required');
|
|
108
|
+
}
|
|
109
|
+
if (!params.targetEntityType?.trim()) {
|
|
110
|
+
throw new ValidationError('Target entity type is required');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check for duplicate name
|
|
114
|
+
const existing = queryOne<{ id: string }>(
|
|
115
|
+
'SELECT id FROM templates WHERE name = ?',
|
|
116
|
+
[params.name.trim()]
|
|
117
|
+
);
|
|
118
|
+
if (existing) {
|
|
119
|
+
return err('Template with this name already exists', 'DUPLICATE_NAME');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const template = transaction(() => {
|
|
123
|
+
const id = generateId();
|
|
124
|
+
const timestamp = now();
|
|
125
|
+
|
|
126
|
+
execute(
|
|
127
|
+
`INSERT INTO templates (
|
|
128
|
+
id, name, description, target_entity_type,
|
|
129
|
+
is_built_in, is_protected, is_enabled, created_by,
|
|
130
|
+
tags, created_at, modified_at
|
|
131
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
132
|
+
[
|
|
133
|
+
id,
|
|
134
|
+
params.name.trim(),
|
|
135
|
+
params.description.trim(),
|
|
136
|
+
params.targetEntityType.trim(),
|
|
137
|
+
params.isBuiltIn ? 1 : 0,
|
|
138
|
+
params.isProtected ? 1 : 0,
|
|
139
|
+
1, // is_enabled defaults to true
|
|
140
|
+
params.createdBy || null,
|
|
141
|
+
params.tags || '',
|
|
142
|
+
timestamp,
|
|
143
|
+
timestamp
|
|
144
|
+
]
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const row = queryOne<TemplateRow>('SELECT * FROM templates WHERE id = ?', [id]);
|
|
148
|
+
if (!row) {
|
|
149
|
+
throw new Error('Failed to create template');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return mapTemplateRow(row);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return ok(template);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof ValidationError) {
|
|
158
|
+
return err(error.message, 'VALIDATION_ERROR');
|
|
159
|
+
}
|
|
160
|
+
return err(`Failed to create template: ${(error as Error).message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface GetTemplateResult {
|
|
165
|
+
template: Template;
|
|
166
|
+
sections?: TemplateSection[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getTemplate(id: string, includeSections = false): Result<GetTemplateResult> {
|
|
170
|
+
try {
|
|
171
|
+
if (!id?.trim()) {
|
|
172
|
+
throw new ValidationError('Template ID is required');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const row = queryOne<TemplateRow>('SELECT * FROM templates WHERE id = ?', [id]);
|
|
176
|
+
if (!row) {
|
|
177
|
+
throw new NotFoundError('Template', id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const template = mapTemplateRow(row);
|
|
181
|
+
const result: GetTemplateResult = { template };
|
|
182
|
+
|
|
183
|
+
if (includeSections) {
|
|
184
|
+
const sectionRows = queryAll<TemplateSectionRow>(
|
|
185
|
+
'SELECT * FROM template_sections WHERE template_id = ? ORDER BY ordinal',
|
|
186
|
+
[id]
|
|
187
|
+
);
|
|
188
|
+
result.sections = sectionRows.map(mapTemplateSectionRow);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return ok(result);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
|
194
|
+
return err(error.message, error.name.toUpperCase());
|
|
195
|
+
}
|
|
196
|
+
return err(`Failed to get template: ${(error as Error).message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface ListTemplatesParams {
|
|
201
|
+
targetEntityType?: string;
|
|
202
|
+
isBuiltIn?: boolean;
|
|
203
|
+
isEnabled?: boolean;
|
|
204
|
+
tags?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function listTemplates(params?: ListTemplatesParams): Result<Template[]> {
|
|
208
|
+
try {
|
|
209
|
+
let sql = 'SELECT * FROM templates WHERE 1=1';
|
|
210
|
+
const sqlParams: any[] = [];
|
|
211
|
+
|
|
212
|
+
if (params?.targetEntityType) {
|
|
213
|
+
sql += ' AND target_entity_type = ?';
|
|
214
|
+
sqlParams.push(params.targetEntityType);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (params?.isBuiltIn !== undefined) {
|
|
218
|
+
sql += ' AND is_built_in = ?';
|
|
219
|
+
sqlParams.push(params.isBuiltIn ? 1 : 0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (params?.isEnabled !== undefined) {
|
|
223
|
+
sql += ' AND is_enabled = ?';
|
|
224
|
+
sqlParams.push(params.isEnabled ? 1 : 0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (params?.tags) {
|
|
228
|
+
sql += ' AND tags LIKE ?';
|
|
229
|
+
sqlParams.push(`%${params.tags}%`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
sql += ' ORDER BY name';
|
|
233
|
+
|
|
234
|
+
const rows = queryAll<TemplateRow>(sql, sqlParams);
|
|
235
|
+
return ok(rows.map(mapTemplateRow));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return err(`Failed to list templates: ${(error as Error).message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface UpdateTemplateParams {
|
|
242
|
+
name?: string;
|
|
243
|
+
description?: string;
|
|
244
|
+
tags?: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function updateTemplate(id: string, params: UpdateTemplateParams): Result<Template> {
|
|
248
|
+
try {
|
|
249
|
+
if (!id?.trim()) {
|
|
250
|
+
throw new ValidationError('Template ID is required');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check if template exists and is not protected
|
|
254
|
+
const existing = queryOne<{ is_protected: number }>(
|
|
255
|
+
'SELECT is_protected FROM templates WHERE id = ?',
|
|
256
|
+
[id]
|
|
257
|
+
);
|
|
258
|
+
if (!existing) {
|
|
259
|
+
throw new NotFoundError('Template', id);
|
|
260
|
+
}
|
|
261
|
+
if (existing.is_protected === 1) {
|
|
262
|
+
return err('Cannot update protected template', 'PROTECTED_TEMPLATE');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Build update query
|
|
266
|
+
const updates: string[] = [];
|
|
267
|
+
const sqlParams: any[] = [];
|
|
268
|
+
|
|
269
|
+
if (params.name !== undefined) {
|
|
270
|
+
if (!params.name.trim()) {
|
|
271
|
+
throw new ValidationError('Template name cannot be empty');
|
|
272
|
+
}
|
|
273
|
+
// Check for duplicate name (excluding current template)
|
|
274
|
+
const duplicate = queryOne<{ id: string }>(
|
|
275
|
+
'SELECT id FROM templates WHERE name = ? AND id != ?',
|
|
276
|
+
[params.name.trim(), id]
|
|
277
|
+
);
|
|
278
|
+
if (duplicate) {
|
|
279
|
+
return err('Template with this name already exists', 'DUPLICATE_NAME');
|
|
280
|
+
}
|
|
281
|
+
updates.push('name = ?');
|
|
282
|
+
sqlParams.push(params.name.trim());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (params.description !== undefined) {
|
|
286
|
+
if (!params.description.trim()) {
|
|
287
|
+
throw new ValidationError('Template description cannot be empty');
|
|
288
|
+
}
|
|
289
|
+
updates.push('description = ?');
|
|
290
|
+
sqlParams.push(params.description.trim());
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (params.tags !== undefined) {
|
|
294
|
+
updates.push('tags = ?');
|
|
295
|
+
sqlParams.push(params.tags);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (updates.length === 0) {
|
|
299
|
+
// Nothing to update, just return current template
|
|
300
|
+
const result = getTemplate(id);
|
|
301
|
+
if (!result.success) return result as Result<Template>;
|
|
302
|
+
return ok(result.data.template);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
updates.push('modified_at = ?');
|
|
306
|
+
sqlParams.push(now());
|
|
307
|
+
sqlParams.push(id);
|
|
308
|
+
|
|
309
|
+
const template = transaction(() => {
|
|
310
|
+
execute(
|
|
311
|
+
`UPDATE templates SET ${updates.join(', ')} WHERE id = ?`,
|
|
312
|
+
sqlParams
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const row = queryOne<TemplateRow>('SELECT * FROM templates WHERE id = ?', [id]);
|
|
316
|
+
if (!row) {
|
|
317
|
+
throw new Error('Failed to update template');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return mapTemplateRow(row);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return ok(template);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
|
326
|
+
return err(error.message, error.name.toUpperCase());
|
|
327
|
+
}
|
|
328
|
+
return err(`Failed to update template: ${(error as Error).message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function deleteTemplate(id: string): Result<boolean> {
|
|
333
|
+
try {
|
|
334
|
+
if (!id?.trim()) {
|
|
335
|
+
throw new ValidationError('Template ID is required');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check if template exists and is not protected
|
|
339
|
+
const existing = queryOne<{ is_protected: number }>(
|
|
340
|
+
'SELECT is_protected FROM templates WHERE id = ?',
|
|
341
|
+
[id]
|
|
342
|
+
);
|
|
343
|
+
if (!existing) {
|
|
344
|
+
throw new NotFoundError('Template', id);
|
|
345
|
+
}
|
|
346
|
+
if (existing.is_protected === 1) {
|
|
347
|
+
return err('Cannot delete protected template', 'PROTECTED_TEMPLATE');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
transaction(() => {
|
|
351
|
+
// Delete template sections first (foreign key)
|
|
352
|
+
execute('DELETE FROM template_sections WHERE template_id = ?', [id]);
|
|
353
|
+
// Delete template
|
|
354
|
+
execute('DELETE FROM templates WHERE id = ?', [id]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return ok(true);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
|
360
|
+
return err(error.message, error.name.toUpperCase());
|
|
361
|
+
}
|
|
362
|
+
return err(`Failed to delete template: ${(error as Error).message}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function enableTemplate(id: string): Result<Template> {
|
|
367
|
+
try {
|
|
368
|
+
if (!id?.trim()) {
|
|
369
|
+
throw new ValidationError('Template ID is required');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const existing = queryOne<{ id: string }>(
|
|
373
|
+
'SELECT id FROM templates WHERE id = ?',
|
|
374
|
+
[id]
|
|
375
|
+
);
|
|
376
|
+
if (!existing) {
|
|
377
|
+
throw new NotFoundError('Template', id);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const template = transaction(() => {
|
|
381
|
+
execute(
|
|
382
|
+
'UPDATE templates SET is_enabled = ?, modified_at = ? WHERE id = ?',
|
|
383
|
+
[1, now(), id]
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const row = queryOne<TemplateRow>('SELECT * FROM templates WHERE id = ?', [id]);
|
|
387
|
+
if (!row) {
|
|
388
|
+
throw new Error('Failed to enable template');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return mapTemplateRow(row);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return ok(template);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
|
397
|
+
return err(error.message, error.name.toUpperCase());
|
|
398
|
+
}
|
|
399
|
+
return err(`Failed to enable template: ${(error as Error).message}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function disableTemplate(id: string): Result<Template> {
|
|
404
|
+
try {
|
|
405
|
+
if (!id?.trim()) {
|
|
406
|
+
throw new ValidationError('Template ID is required');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const existing = queryOne<{ id: string }>(
|
|
410
|
+
'SELECT id FROM templates WHERE id = ?',
|
|
411
|
+
[id]
|
|
412
|
+
);
|
|
413
|
+
if (!existing) {
|
|
414
|
+
throw new NotFoundError('Template', id);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const template = transaction(() => {
|
|
418
|
+
execute(
|
|
419
|
+
'UPDATE templates SET is_enabled = ?, modified_at = ? WHERE id = ?',
|
|
420
|
+
[0, now(), id]
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const row = queryOne<TemplateRow>('SELECT * FROM templates WHERE id = ?', [id]);
|
|
424
|
+
if (!row) {
|
|
425
|
+
throw new Error('Failed to disable template');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return mapTemplateRow(row);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return ok(template);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
|
434
|
+
return err(error.message, error.name.toUpperCase());
|
|
435
|
+
}
|
|
436
|
+
return err(`Failed to disable template: ${(error as Error).message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export interface AddTemplateSectionParams {
|
|
441
|
+
templateId: string;
|
|
442
|
+
title: string;
|
|
443
|
+
usageDescription: string;
|
|
444
|
+
contentSample: string;
|
|
445
|
+
contentFormat?: string;
|
|
446
|
+
isRequired?: boolean;
|
|
447
|
+
tags?: string;
|
|
448
|
+
ordinal?: number;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function addTemplateSection(params: AddTemplateSectionParams): Result<TemplateSection> {
|
|
452
|
+
try {
|
|
453
|
+
// Validation
|
|
454
|
+
if (!params.templateId?.trim()) {
|
|
455
|
+
throw new ValidationError('Template ID is required');
|
|
456
|
+
}
|
|
457
|
+
if (!params.title?.trim()) {
|
|
458
|
+
throw new ValidationError('Section title is required');
|
|
459
|
+
}
|
|
460
|
+
if (!params.usageDescription?.trim()) {
|
|
461
|
+
throw new ValidationError('Section usage description is required');
|
|
462
|
+
}
|
|
463
|
+
if (!params.contentSample?.trim()) {
|
|
464
|
+
throw new ValidationError('Section content sample is required');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Check template exists
|
|
468
|
+
const template = queryOne<{ id: string }>(
|
|
469
|
+
'SELECT id FROM templates WHERE id = ?',
|
|
470
|
+
[params.templateId]
|
|
471
|
+
);
|
|
472
|
+
if (!template) {
|
|
473
|
+
throw new NotFoundError('Template', params.templateId);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const section = transaction(() => {
|
|
477
|
+
const id = generateId();
|
|
478
|
+
|
|
479
|
+
// Auto-calculate ordinal if not provided
|
|
480
|
+
let ordinal = params.ordinal;
|
|
481
|
+
if (ordinal === undefined) {
|
|
482
|
+
const maxOrdinal = queryOne<{ max_ordinal: number | null }>(
|
|
483
|
+
'SELECT MAX(ordinal) as max_ordinal FROM template_sections WHERE template_id = ?',
|
|
484
|
+
[params.templateId]
|
|
485
|
+
);
|
|
486
|
+
ordinal = (maxOrdinal?.max_ordinal ?? -1) + 1;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
execute(
|
|
490
|
+
`INSERT INTO template_sections (
|
|
491
|
+
id, template_id, title, usage_description,
|
|
492
|
+
content_sample, content_format, ordinal,
|
|
493
|
+
is_required, tags
|
|
494
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
495
|
+
[
|
|
496
|
+
id,
|
|
497
|
+
params.templateId,
|
|
498
|
+
params.title.trim(),
|
|
499
|
+
params.usageDescription.trim(),
|
|
500
|
+
params.contentSample.trim(),
|
|
501
|
+
params.contentFormat || 'MARKDOWN',
|
|
502
|
+
ordinal,
|
|
503
|
+
params.isRequired ? 1 : 0,
|
|
504
|
+
params.tags || ''
|
|
505
|
+
]
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const row = queryOne<TemplateSectionRow>(
|
|
509
|
+
'SELECT * FROM template_sections WHERE id = ?',
|
|
510
|
+
[id]
|
|
511
|
+
);
|
|
512
|
+
if (!row) {
|
|
513
|
+
throw new Error('Failed to create template section');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return mapTemplateSectionRow(row);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
return ok(section);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
|
522
|
+
return err(error.message, error.name.toUpperCase());
|
|
523
|
+
}
|
|
524
|
+
return err(`Failed to add template section: ${(error as Error).message}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
interface SectionRow {
|
|
529
|
+
id: string;
|
|
530
|
+
entity_type: string;
|
|
531
|
+
entity_id: string;
|
|
532
|
+
title: string;
|
|
533
|
+
usage_description: string;
|
|
534
|
+
content: string;
|
|
535
|
+
content_format: string;
|
|
536
|
+
ordinal: number;
|
|
537
|
+
tags: string;
|
|
538
|
+
version: number;
|
|
539
|
+
created_at: string;
|
|
540
|
+
modified_at: string;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function mapSectionRow(row: SectionRow): Section {
|
|
544
|
+
return {
|
|
545
|
+
id: row.id,
|
|
546
|
+
entityType: row.entity_type as EntityType,
|
|
547
|
+
entityId: row.entity_id,
|
|
548
|
+
title: row.title,
|
|
549
|
+
usageDescription: row.usage_description,
|
|
550
|
+
content: row.content,
|
|
551
|
+
contentFormat: row.content_format as ContentFormat,
|
|
552
|
+
ordinal: row.ordinal,
|
|
553
|
+
tags: row.tags,
|
|
554
|
+
version: row.version,
|
|
555
|
+
createdAt: new Date(row.created_at),
|
|
556
|
+
modifiedAt: new Date(row.modified_at)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function applyTemplate(
|
|
561
|
+
templateId: string,
|
|
562
|
+
entityType: string,
|
|
563
|
+
entityId: string
|
|
564
|
+
): Result<Section[]> {
|
|
565
|
+
try {
|
|
566
|
+
// Validation
|
|
567
|
+
if (!templateId?.trim()) {
|
|
568
|
+
throw new ValidationError('Template ID is required');
|
|
569
|
+
}
|
|
570
|
+
if (!entityType?.trim()) {
|
|
571
|
+
throw new ValidationError('Entity type is required');
|
|
572
|
+
}
|
|
573
|
+
if (!entityId?.trim()) {
|
|
574
|
+
throw new ValidationError('Entity ID is required');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Get template with sections
|
|
578
|
+
const templateResult = getTemplate(templateId, true);
|
|
579
|
+
if (!templateResult.success) {
|
|
580
|
+
return templateResult as Result<Section[]>;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const { template, sections: templateSections } = templateResult.data;
|
|
584
|
+
|
|
585
|
+
// Validate template is enabled
|
|
586
|
+
if (!template.isEnabled) {
|
|
587
|
+
return err('Cannot apply disabled template', 'TEMPLATE_DISABLED');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Validate entity type matches
|
|
591
|
+
if (template.targetEntityType !== entityType) {
|
|
592
|
+
return err(
|
|
593
|
+
`Template target entity type (${template.targetEntityType}) does not match provided entity type (${entityType})`,
|
|
594
|
+
'ENTITY_TYPE_MISMATCH'
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (!templateSections || templateSections.length === 0) {
|
|
599
|
+
return ok([]);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const sections = transaction(() => {
|
|
603
|
+
const createdSections: Section[] = [];
|
|
604
|
+
const timestamp = now();
|
|
605
|
+
|
|
606
|
+
for (const templateSection of templateSections) {
|
|
607
|
+
const id = generateId();
|
|
608
|
+
|
|
609
|
+
execute(
|
|
610
|
+
`INSERT INTO sections (
|
|
611
|
+
id, entity_type, entity_id, title,
|
|
612
|
+
usage_description, content, content_format,
|
|
613
|
+
ordinal, tags, version, created_at, modified_at
|
|
614
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
615
|
+
[
|
|
616
|
+
id,
|
|
617
|
+
entityType,
|
|
618
|
+
entityId,
|
|
619
|
+
templateSection.title,
|
|
620
|
+
templateSection.usageDescription,
|
|
621
|
+
templateSection.contentSample,
|
|
622
|
+
templateSection.contentFormat,
|
|
623
|
+
templateSection.ordinal,
|
|
624
|
+
templateSection.tags,
|
|
625
|
+
1,
|
|
626
|
+
timestamp,
|
|
627
|
+
timestamp
|
|
628
|
+
]
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const row = queryOne<SectionRow>('SELECT * FROM sections WHERE id = ?', [id]);
|
|
632
|
+
if (!row) {
|
|
633
|
+
throw new Error('Failed to create section from template');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
createdSections.push(mapSectionRow(row));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return createdSections;
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return ok(sections);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
if (error instanceof NotFoundError || error instanceof ValidationError) {
|
|
645
|
+
return err(error.message, error.name.toUpperCase());
|
|
646
|
+
}
|
|
647
|
+
return err(`Failed to apply template: ${(error as Error).message}`);
|
|
648
|
+
}
|
|
649
|
+
}
|