@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,505 @@
|
|
|
1
|
+
import {
|
|
2
|
+
db,
|
|
3
|
+
generateId,
|
|
4
|
+
now,
|
|
5
|
+
queryOne,
|
|
6
|
+
queryAll,
|
|
7
|
+
execute,
|
|
8
|
+
ok,
|
|
9
|
+
err,
|
|
10
|
+
toDate,
|
|
11
|
+
} from './base';
|
|
12
|
+
import type { Result, Section } from '../domain/types';
|
|
13
|
+
import { ContentFormat, EntityType, NotFoundError, ValidationError } from '../domain/types';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Type Definitions
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
interface SectionRow {
|
|
20
|
+
id: string;
|
|
21
|
+
entity_type: string;
|
|
22
|
+
entity_id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
usage_description: string;
|
|
25
|
+
content: string;
|
|
26
|
+
content_format: string;
|
|
27
|
+
ordinal: number;
|
|
28
|
+
tags: string;
|
|
29
|
+
version: number;
|
|
30
|
+
created_at: string;
|
|
31
|
+
modified_at: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AddSectionParams {
|
|
35
|
+
entityType: string;
|
|
36
|
+
entityId: string;
|
|
37
|
+
title: string;
|
|
38
|
+
usageDescription: string;
|
|
39
|
+
content: string;
|
|
40
|
+
contentFormat?: string;
|
|
41
|
+
ordinal?: number;
|
|
42
|
+
tags?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface GetSectionsParams {
|
|
46
|
+
includeContent?: boolean;
|
|
47
|
+
tags?: string;
|
|
48
|
+
sectionIds?: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface UpdateSectionParams {
|
|
52
|
+
title?: string;
|
|
53
|
+
usageDescription?: string;
|
|
54
|
+
content?: string;
|
|
55
|
+
contentFormat?: string;
|
|
56
|
+
tags?: string;
|
|
57
|
+
version: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface BulkCreateSectionParams {
|
|
61
|
+
entityType: string;
|
|
62
|
+
entityId: string;
|
|
63
|
+
title: string;
|
|
64
|
+
usageDescription: string;
|
|
65
|
+
content: string;
|
|
66
|
+
contentFormat?: string;
|
|
67
|
+
tags?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Mapper Functions
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
function rowToSection(row: SectionRow): Section {
|
|
75
|
+
return {
|
|
76
|
+
id: row.id,
|
|
77
|
+
entityType: row.entity_type as EntityType,
|
|
78
|
+
entityId: row.entity_id,
|
|
79
|
+
title: row.title,
|
|
80
|
+
usageDescription: row.usage_description,
|
|
81
|
+
content: row.content,
|
|
82
|
+
contentFormat: row.content_format as ContentFormat,
|
|
83
|
+
ordinal: row.ordinal,
|
|
84
|
+
tags: row.tags,
|
|
85
|
+
version: row.version,
|
|
86
|
+
createdAt: toDate(row.created_at),
|
|
87
|
+
modifiedAt: toDate(row.modified_at),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Helper Functions
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
function getNextOrdinal(entityId: string, entityType: string): number {
|
|
96
|
+
const result = queryOne<{ max_ordinal: number | null }>(
|
|
97
|
+
'SELECT MAX(ordinal) as max_ordinal FROM sections WHERE entity_id = ? AND entity_type = ?',
|
|
98
|
+
[entityId, entityType]
|
|
99
|
+
);
|
|
100
|
+
return (result?.max_ordinal ?? -1) + 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateContentFormat(format: string): boolean {
|
|
104
|
+
return Object.values(ContentFormat).includes(format as ContentFormat);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Repository Functions
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Add a new section to an entity
|
|
113
|
+
*/
|
|
114
|
+
export function addSection(params: AddSectionParams): Result<Section> {
|
|
115
|
+
try {
|
|
116
|
+
// Validate content format if provided
|
|
117
|
+
const contentFormat = params.contentFormat ?? ContentFormat.MARKDOWN;
|
|
118
|
+
if (!validateContentFormat(contentFormat)) {
|
|
119
|
+
return err(`Invalid content format: ${contentFormat}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Get ordinal
|
|
123
|
+
const ordinal = params.ordinal ?? getNextOrdinal(params.entityId, params.entityType);
|
|
124
|
+
|
|
125
|
+
// Check for ordinal conflict
|
|
126
|
+
const existing = queryOne<{ id: string }>(
|
|
127
|
+
'SELECT id FROM sections WHERE entity_type = ? AND entity_id = ? AND ordinal = ?',
|
|
128
|
+
[params.entityType, params.entityId, ordinal]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (existing) {
|
|
132
|
+
return err(`Section with ordinal ${ordinal} already exists for this entity`, 'CONFLICT');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const id = generateId();
|
|
136
|
+
const timestamp = now();
|
|
137
|
+
const tags = params.tags ?? '';
|
|
138
|
+
|
|
139
|
+
execute(
|
|
140
|
+
`INSERT INTO sections (
|
|
141
|
+
id, entity_type, entity_id, title, usage_description,
|
|
142
|
+
content, content_format, ordinal, tags, version,
|
|
143
|
+
created_at, modified_at
|
|
144
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
145
|
+
[
|
|
146
|
+
id,
|
|
147
|
+
params.entityType,
|
|
148
|
+
params.entityId,
|
|
149
|
+
params.title,
|
|
150
|
+
params.usageDescription,
|
|
151
|
+
params.content,
|
|
152
|
+
contentFormat,
|
|
153
|
+
ordinal,
|
|
154
|
+
tags,
|
|
155
|
+
timestamp,
|
|
156
|
+
timestamp,
|
|
157
|
+
]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const row = queryOne<SectionRow>('SELECT * FROM sections WHERE id = ?', [id]);
|
|
161
|
+
if (!row) {
|
|
162
|
+
return err('Failed to create section');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return ok(rowToSection(row));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return err(`Failed to add section: ${error}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get sections for an entity
|
|
173
|
+
*/
|
|
174
|
+
export function getSections(
|
|
175
|
+
entityId: string,
|
|
176
|
+
entityType: string,
|
|
177
|
+
params?: GetSectionsParams
|
|
178
|
+
): Result<Section[]> {
|
|
179
|
+
try {
|
|
180
|
+
let sql = 'SELECT * FROM sections WHERE entity_id = ? AND entity_type = ?';
|
|
181
|
+
const sqlParams: any[] = [entityId, entityType];
|
|
182
|
+
|
|
183
|
+
// Filter by tags if provided
|
|
184
|
+
if (params?.tags) {
|
|
185
|
+
const tagList = params.tags.split(',').map(t => t.trim());
|
|
186
|
+
const tagConditions = tagList.map(() => 'tags LIKE ?').join(' OR ');
|
|
187
|
+
sql += ` AND (${tagConditions})`;
|
|
188
|
+
tagList.forEach(tag => sqlParams.push(`%${tag}%`));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Filter by section IDs if provided
|
|
192
|
+
if (params?.sectionIds && params.sectionIds.length > 0) {
|
|
193
|
+
const placeholders = params.sectionIds.map(() => '?').join(',');
|
|
194
|
+
sql += ` AND id IN (${placeholders})`;
|
|
195
|
+
sqlParams.push(...params.sectionIds);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
sql += ' ORDER BY ordinal ASC';
|
|
199
|
+
|
|
200
|
+
const rows = queryAll<SectionRow>(sql, sqlParams);
|
|
201
|
+
|
|
202
|
+
// If includeContent is false, clear content field for token savings
|
|
203
|
+
if (params?.includeContent === false) {
|
|
204
|
+
const sections = rows.map(row => rowToSection({ ...row, content: '' }));
|
|
205
|
+
return ok(sections);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const sections = rows.map(rowToSection);
|
|
209
|
+
return ok(sections);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return err(`Failed to get sections: ${error}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Update a section
|
|
217
|
+
*/
|
|
218
|
+
export function updateSection(id: string, params: UpdateSectionParams): Result<Section> {
|
|
219
|
+
try {
|
|
220
|
+
// Validate version
|
|
221
|
+
const existing = queryOne<SectionRow>('SELECT * FROM sections WHERE id = ?', [id]);
|
|
222
|
+
if (!existing) {
|
|
223
|
+
return err(`Section not found: ${id}`, 'NOT_FOUND');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (existing.version !== params.version) {
|
|
227
|
+
return err(
|
|
228
|
+
`Version mismatch: expected ${existing.version}, got ${params.version}`,
|
|
229
|
+
'VERSION_CONFLICT'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Validate content format if provided
|
|
234
|
+
if (params.contentFormat && !validateContentFormat(params.contentFormat)) {
|
|
235
|
+
return err(`Invalid content format: ${params.contentFormat}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Build update query dynamically
|
|
239
|
+
const updates: string[] = [];
|
|
240
|
+
const sqlParams: any[] = [];
|
|
241
|
+
|
|
242
|
+
if (params.title !== undefined) {
|
|
243
|
+
updates.push('title = ?');
|
|
244
|
+
sqlParams.push(params.title);
|
|
245
|
+
}
|
|
246
|
+
if (params.usageDescription !== undefined) {
|
|
247
|
+
updates.push('usage_description = ?');
|
|
248
|
+
sqlParams.push(params.usageDescription);
|
|
249
|
+
}
|
|
250
|
+
if (params.content !== undefined) {
|
|
251
|
+
updates.push('content = ?');
|
|
252
|
+
sqlParams.push(params.content);
|
|
253
|
+
}
|
|
254
|
+
if (params.contentFormat !== undefined) {
|
|
255
|
+
updates.push('content_format = ?');
|
|
256
|
+
sqlParams.push(params.contentFormat);
|
|
257
|
+
}
|
|
258
|
+
if (params.tags !== undefined) {
|
|
259
|
+
updates.push('tags = ?');
|
|
260
|
+
sqlParams.push(params.tags);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (updates.length === 0) {
|
|
264
|
+
return err('No fields to update');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Always update version and modified_at
|
|
268
|
+
updates.push('version = version + 1');
|
|
269
|
+
updates.push('modified_at = ?');
|
|
270
|
+
sqlParams.push(now());
|
|
271
|
+
|
|
272
|
+
// Add id for WHERE clause
|
|
273
|
+
sqlParams.push(id);
|
|
274
|
+
|
|
275
|
+
const sql = `UPDATE sections SET ${updates.join(', ')} WHERE id = ?`;
|
|
276
|
+
const changes = execute(sql, sqlParams);
|
|
277
|
+
|
|
278
|
+
if (changes === 0) {
|
|
279
|
+
return err(`Section not found: ${id}`, 'NOT_FOUND');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const updated = queryOne<SectionRow>('SELECT * FROM sections WHERE id = ?', [id]);
|
|
283
|
+
if (!updated) {
|
|
284
|
+
return err('Failed to retrieve updated section');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return ok(rowToSection(updated));
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return err(`Failed to update section: ${error}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Update only the text content of a section
|
|
295
|
+
*/
|
|
296
|
+
export function updateSectionText(id: string, content: string, version: number): Result<Section> {
|
|
297
|
+
try {
|
|
298
|
+
// Validate version
|
|
299
|
+
const existing = queryOne<SectionRow>('SELECT * FROM sections WHERE id = ?', [id]);
|
|
300
|
+
if (!existing) {
|
|
301
|
+
return err(`Section not found: ${id}`, 'NOT_FOUND');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (existing.version !== version) {
|
|
305
|
+
return err(
|
|
306
|
+
`Version mismatch: expected ${existing.version}, got ${version}`,
|
|
307
|
+
'VERSION_CONFLICT'
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const changes = execute(
|
|
312
|
+
'UPDATE sections SET content = ?, version = version + 1, modified_at = ? WHERE id = ?',
|
|
313
|
+
[content, now(), id]
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
if (changes === 0) {
|
|
317
|
+
return err(`Section not found: ${id}`, 'NOT_FOUND');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const updated = queryOne<SectionRow>('SELECT * FROM sections WHERE id = ?', [id]);
|
|
321
|
+
if (!updated) {
|
|
322
|
+
return err('Failed to retrieve updated section');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return ok(rowToSection(updated));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return err(`Failed to update section text: ${error}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Delete a section
|
|
333
|
+
*/
|
|
334
|
+
export function deleteSection(id: string): Result<boolean> {
|
|
335
|
+
try {
|
|
336
|
+
// Check if section exists
|
|
337
|
+
const existing = queryOne<{ id: string }>('SELECT id FROM sections WHERE id = ?', [id]);
|
|
338
|
+
if (!existing) {
|
|
339
|
+
return err(`Section not found: ${id}`, 'NOT_FOUND');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const changes = execute('DELETE FROM sections WHERE id = ?', [id]);
|
|
343
|
+
return ok(changes > 0);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return err(`Failed to delete section: ${error}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Reorder sections for an entity
|
|
351
|
+
*
|
|
352
|
+
* Uses a two-phase approach to avoid UNIQUE constraint violations:
|
|
353
|
+
* 1. First, set all ordinals to temporary negative values
|
|
354
|
+
* 2. Then update them to their final values
|
|
355
|
+
*/
|
|
356
|
+
export function reorderSections(
|
|
357
|
+
entityId: string,
|
|
358
|
+
entityType: string,
|
|
359
|
+
orderedIds: string[]
|
|
360
|
+
): Result<boolean> {
|
|
361
|
+
try {
|
|
362
|
+
// Use transaction for atomic updates
|
|
363
|
+
db.run('BEGIN TRANSACTION');
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const timestamp = now();
|
|
367
|
+
|
|
368
|
+
// Phase 1: Set all sections to temporary negative ordinals to avoid UNIQUE constraint conflicts
|
|
369
|
+
for (let i = 0; i < orderedIds.length; i++) {
|
|
370
|
+
const changes = execute(
|
|
371
|
+
'UPDATE sections SET ordinal = ?, modified_at = ? WHERE id = ? AND entity_id = ? AND entity_type = ?',
|
|
372
|
+
[-(i + 1), timestamp, orderedIds[i], entityId, entityType]
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
if (changes === 0) {
|
|
376
|
+
throw new Error(`Section not found or does not belong to entity: ${orderedIds[i]}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Phase 2: Set final ordinal values
|
|
381
|
+
for (let i = 0; i < orderedIds.length; i++) {
|
|
382
|
+
execute(
|
|
383
|
+
'UPDATE sections SET ordinal = ? WHERE id = ? AND entity_id = ? AND entity_type = ?',
|
|
384
|
+
[i, orderedIds[i], entityId, entityType]
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
db.run('COMMIT');
|
|
389
|
+
return ok(true);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
db.run('ROLLBACK');
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return err(`Failed to reorder sections: ${error}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Bulk create sections
|
|
401
|
+
*/
|
|
402
|
+
export function bulkCreateSections(
|
|
403
|
+
sections: BulkCreateSectionParams[]
|
|
404
|
+
): Result<Section[]> {
|
|
405
|
+
try {
|
|
406
|
+
if (sections.length === 0) {
|
|
407
|
+
return ok([]);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Use transaction for atomic bulk insert
|
|
411
|
+
db.run('BEGIN TRANSACTION');
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const created: Section[] = [];
|
|
415
|
+
|
|
416
|
+
// Group by entity to get proper ordinals
|
|
417
|
+
const entityMap = new Map<string, number>();
|
|
418
|
+
|
|
419
|
+
for (const section of sections) {
|
|
420
|
+
const entityKey = `${section.entityType}:${section.entityId}`;
|
|
421
|
+
|
|
422
|
+
// Get or calculate starting ordinal for this entity
|
|
423
|
+
if (!entityMap.has(entityKey)) {
|
|
424
|
+
const startOrdinal = getNextOrdinal(section.entityId, section.entityType);
|
|
425
|
+
entityMap.set(entityKey, startOrdinal);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const ordinal = entityMap.get(entityKey)!;
|
|
429
|
+
entityMap.set(entityKey, ordinal + 1);
|
|
430
|
+
|
|
431
|
+
const contentFormat = section.contentFormat ?? ContentFormat.MARKDOWN;
|
|
432
|
+
if (!validateContentFormat(contentFormat)) {
|
|
433
|
+
throw new Error(`Invalid content format: ${contentFormat}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const id = generateId();
|
|
437
|
+
const timestamp = now();
|
|
438
|
+
const tags = section.tags ?? '';
|
|
439
|
+
|
|
440
|
+
execute(
|
|
441
|
+
`INSERT INTO sections (
|
|
442
|
+
id, entity_type, entity_id, title, usage_description,
|
|
443
|
+
content, content_format, ordinal, tags, version,
|
|
444
|
+
created_at, modified_at
|
|
445
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
446
|
+
[
|
|
447
|
+
id,
|
|
448
|
+
section.entityType,
|
|
449
|
+
section.entityId,
|
|
450
|
+
section.title,
|
|
451
|
+
section.usageDescription,
|
|
452
|
+
section.content,
|
|
453
|
+
contentFormat,
|
|
454
|
+
ordinal,
|
|
455
|
+
tags,
|
|
456
|
+
timestamp,
|
|
457
|
+
timestamp,
|
|
458
|
+
]
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const row = queryOne<SectionRow>('SELECT * FROM sections WHERE id = ?', [id]);
|
|
462
|
+
if (!row) {
|
|
463
|
+
throw new Error('Failed to retrieve created section');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
created.push(rowToSection(row));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
db.run('COMMIT');
|
|
470
|
+
return ok(created);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
db.run('ROLLBACK');
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
return err(`Failed to bulk create sections: ${error}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Bulk delete sections
|
|
482
|
+
*/
|
|
483
|
+
export function bulkDeleteSections(ids: string[]): Result<number> {
|
|
484
|
+
try {
|
|
485
|
+
if (ids.length === 0) {
|
|
486
|
+
return ok(0);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Use transaction for atomic bulk delete
|
|
490
|
+
db.run('BEGIN TRANSACTION');
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
494
|
+
const changes = execute(`DELETE FROM sections WHERE id IN (${placeholders})`, ids);
|
|
495
|
+
|
|
496
|
+
db.run('COMMIT');
|
|
497
|
+
return ok(changes);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
db.run('ROLLBACK');
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return err(`Failed to bulk delete sections: ${error}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of the tags repository
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun run src/repos/tags.example.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { runMigrations } from '../db/migrate';
|
|
8
|
+
import { db, generateId, now } from '../db/client';
|
|
9
|
+
import { listTags, getTagUsage, renameTag } from './tags';
|
|
10
|
+
|
|
11
|
+
// Initialize the database
|
|
12
|
+
runMigrations();
|
|
13
|
+
|
|
14
|
+
// Clean up any existing test data
|
|
15
|
+
db.run('DELETE FROM entity_tags');
|
|
16
|
+
|
|
17
|
+
console.log('=== Tags Repository Examples ===\n');
|
|
18
|
+
|
|
19
|
+
// Example 1: Add some tags
|
|
20
|
+
console.log('1. Adding tags to entities...');
|
|
21
|
+
const projectId = generateId();
|
|
22
|
+
const featureId = generateId();
|
|
23
|
+
const taskId = generateId();
|
|
24
|
+
|
|
25
|
+
db.run(
|
|
26
|
+
'INSERT INTO entity_tags (id, entity_id, entity_type, tag, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
27
|
+
[generateId(), projectId, 'PROJECT', 'backend', now()]
|
|
28
|
+
);
|
|
29
|
+
db.run(
|
|
30
|
+
'INSERT INTO entity_tags (id, entity_id, entity_type, tag, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
31
|
+
[generateId(), projectId, 'PROJECT', 'api', now()]
|
|
32
|
+
);
|
|
33
|
+
db.run(
|
|
34
|
+
'INSERT INTO entity_tags (id, entity_id, entity_type, tag, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
35
|
+
[generateId(), featureId, 'FEATURE', 'authentication', now()]
|
|
36
|
+
);
|
|
37
|
+
db.run(
|
|
38
|
+
'INSERT INTO entity_tags (id, entity_id, entity_type, tag, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
39
|
+
[generateId(), featureId, 'FEATURE', 'api', now()]
|
|
40
|
+
);
|
|
41
|
+
db.run(
|
|
42
|
+
'INSERT INTO entity_tags (id, entity_id, entity_type, tag, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
43
|
+
[generateId(), taskId, 'TASK', 'bugfix', now()]
|
|
44
|
+
);
|
|
45
|
+
console.log('Added 5 tags across 3 entities\n');
|
|
46
|
+
|
|
47
|
+
// Example 2: List all tags
|
|
48
|
+
console.log('2. Listing all tags with counts...');
|
|
49
|
+
const allTags = listTags();
|
|
50
|
+
if (allTags.success) {
|
|
51
|
+
console.table(allTags.data);
|
|
52
|
+
} else {
|
|
53
|
+
console.error('Error:', allTags.error);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Example 3: List tags filtered by entity type
|
|
57
|
+
console.log('\n3. Listing tags for PROJECT entities only...');
|
|
58
|
+
const projectTags = listTags({ entityType: 'PROJECT' });
|
|
59
|
+
if (projectTags.success) {
|
|
60
|
+
console.table(projectTags.data);
|
|
61
|
+
} else {
|
|
62
|
+
console.error('Error:', projectTags.error);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Example 4: Get usage of a specific tag
|
|
66
|
+
console.log('\n4. Getting usage of "api" tag...');
|
|
67
|
+
const apiUsage = getTagUsage('api');
|
|
68
|
+
if (apiUsage.success) {
|
|
69
|
+
console.table(apiUsage.data);
|
|
70
|
+
} else {
|
|
71
|
+
console.error('Error:', apiUsage.error);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Example 5: Rename a tag (dry run)
|
|
75
|
+
console.log('\n5. Dry run: Renaming "api" to "rest-api"...');
|
|
76
|
+
const dryRunResult = renameTag('api', 'rest-api', { dryRun: true });
|
|
77
|
+
if (dryRunResult.success) {
|
|
78
|
+
console.log(`Would affect ${dryRunResult.data.affected} entities:`);
|
|
79
|
+
console.table(dryRunResult.data.entities);
|
|
80
|
+
} else {
|
|
81
|
+
console.error('Error:', dryRunResult.error);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Example 6: Rename a tag (actual)
|
|
85
|
+
console.log('\n6. Actually renaming "api" to "rest-api"...');
|
|
86
|
+
const renameResult = renameTag('api', 'rest-api');
|
|
87
|
+
if (renameResult.success) {
|
|
88
|
+
console.log(`Renamed tag on ${renameResult.data.affected} entities`);
|
|
89
|
+
} else {
|
|
90
|
+
console.error('Error:', renameResult.error);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Example 7: Verify the rename
|
|
94
|
+
console.log('\n7. Verifying the rename...');
|
|
95
|
+
const afterRename = listTags();
|
|
96
|
+
if (afterRename.success) {
|
|
97
|
+
console.table(afterRename.data);
|
|
98
|
+
} else {
|
|
99
|
+
console.error('Error:', afterRename.error);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Example 8: Handle conflict scenario
|
|
103
|
+
console.log('\n8. Testing conflict handling...');
|
|
104
|
+
db.run(
|
|
105
|
+
'INSERT INTO entity_tags (id, entity_id, entity_type, tag, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
106
|
+
[generateId(), projectId, 'PROJECT', 'server', now()]
|
|
107
|
+
);
|
|
108
|
+
console.log('Added "server" tag to project that has "backend"');
|
|
109
|
+
|
|
110
|
+
const conflictRename = renameTag('backend', 'server');
|
|
111
|
+
if (conflictRename.success) {
|
|
112
|
+
console.log(`Handled conflict, affected ${conflictRename.data.affected} entities`);
|
|
113
|
+
console.log('(Deleted old tag instead of creating duplicate)');
|
|
114
|
+
} else {
|
|
115
|
+
console.error('Error:', conflictRename.error);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Verify no duplicates
|
|
119
|
+
const finalTags = db
|
|
120
|
+
.query('SELECT * FROM entity_tags WHERE entity_id = ? AND entity_type = ?')
|
|
121
|
+
.all(projectId, 'PROJECT') as any[];
|
|
122
|
+
console.log(`\nProject now has ${finalTags.length} tags (no duplicates):`);
|
|
123
|
+
console.table(finalTags.map(t => ({ tag: t.tag })));
|
|
124
|
+
|
|
125
|
+
console.log('\n=== Examples Complete ===');
|