@axiom-lattice/pg-stores 2.0.9 → 3.0.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +56 -0
- package/dist/index.d.mts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +576 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +576 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/PostgreSQLA2AApiKeyStore.migrations.test.ts +76 -0
- package/src/__tests__/PostgreSQLA2AApiKeyStore.test.ts +133 -0
- package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +37 -2
- package/src/__tests__/PostgreSQLTaskStore.integration.test.ts +90 -0
- package/src/__tests__/PostgreSQLTaskStore.test.ts +287 -0
- package/src/__tests__/PostgreSQLTaskWorkItemStore.test.ts +104 -0
- package/src/__tests__/add_a2a_key_assistant_ids.test.ts +34 -0
- package/src/__tests__/workspace-project-store.test.ts +48 -0
- package/src/createPgStoreConfig.ts +6 -1
- package/src/migrations/add_a2a_key_assistant_ids.ts +24 -0
- package/src/migrations/add_project_kind_column.ts +29 -0
- package/src/migrations/task_work_items_migration.ts +15 -0
- package/src/stores/PostgreSQLA2AApiKeyStore.ts +23 -8
- package/src/stores/PostgreSQLProjectStore.ts +31 -8
- package/src/stores/PostgreSQLTaskStore.ts +272 -7
- package/src/stores/PostgreSQLTaskWorkItemStore.ts +42 -2
|
@@ -41,6 +41,58 @@ interface TaskRow {
|
|
|
41
41
|
updated_at: string;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Advance the legacy VARCHAR task timestamp monotonically and store canonical UTC ISO text.
|
|
46
|
+
*
|
|
47
|
+
* The task table predates the timestamp-backed stores and persists `updated_at`
|
|
48
|
+
* as VARCHAR(50), so every arithmetic operand must be cast explicitly.
|
|
49
|
+
*/
|
|
50
|
+
function nextUpdatedAtSql(column = "updated_at"): string {
|
|
51
|
+
return `to_char(
|
|
52
|
+
date_trunc('milliseconds', GREATEST(
|
|
53
|
+
clock_timestamp(),
|
|
54
|
+
CASE
|
|
55
|
+
WHEN ${canonicalTimestampValidationSql(column)}
|
|
56
|
+
THEN ${column}::timestamptz + interval '1 millisecond'
|
|
57
|
+
ELSE clock_timestamp()
|
|
58
|
+
END
|
|
59
|
+
))
|
|
60
|
+
AT TIME ZONE 'UTC',
|
|
61
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
62
|
+
)`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function canonicalTimestampValidationSql(column: string, allowMaximum = false): string {
|
|
66
|
+
const year = `substring(${column} FROM 1 FOR 4)::integer`;
|
|
67
|
+
const month = `substring(${column} FROM 6 FOR 2)::integer`;
|
|
68
|
+
const day = `substring(${column} FROM 9 FOR 2)::integer`;
|
|
69
|
+
return `CASE
|
|
70
|
+
WHEN ${column} ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{3}Z$' THEN
|
|
71
|
+
${allowMaximum ? "" : `${column} <> '9999-12-31T23:59:59.999Z' AND`}
|
|
72
|
+
${year} BETWEEN 1 AND 9999
|
|
73
|
+
AND ${month} BETWEEN 1 AND 12
|
|
74
|
+
AND ${day} BETWEEN 1 AND CASE
|
|
75
|
+
WHEN ${month} = 2 THEN CASE
|
|
76
|
+
WHEN (${year} % 400 = 0) OR (${year} % 4 = 0 AND ${year} % 100 <> 0) THEN 29
|
|
77
|
+
ELSE 28
|
|
78
|
+
END
|
|
79
|
+
WHEN ${month} IN (4, 6, 9, 11) THEN 30
|
|
80
|
+
ELSE 31
|
|
81
|
+
END
|
|
82
|
+
AND substring(${column} FROM 12 FOR 2)::integer BETWEEN 0 AND 23
|
|
83
|
+
AND substring(${column} FROM 15 FOR 2)::integer BETWEEN 0 AND 59
|
|
84
|
+
AND substring(${column} FROM 18 FOR 2)::integer BETWEEN 0 AND 59
|
|
85
|
+
ELSE FALSE
|
|
86
|
+
END`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function canonicalTimestampSnapshotSql(column: string, parameter: string, allowMaximum = false): string {
|
|
90
|
+
return `CASE WHEN ${canonicalTimestampValidationSql(column, allowMaximum)}
|
|
91
|
+
THEN ${column}::timestamptz = ${parameter}::timestamptz
|
|
92
|
+
ELSE FALSE
|
|
93
|
+
END`;
|
|
94
|
+
}
|
|
95
|
+
|
|
44
96
|
function parseTaskFiles(raw: unknown): TaskFileRef[] | undefined {
|
|
45
97
|
if (!raw) return undefined;
|
|
46
98
|
try {
|
|
@@ -148,7 +200,7 @@ export class PostgreSQLTaskStore implements TaskStore {
|
|
|
148
200
|
params: CreateTaskRequest & { tenantId: string; ownerType: string; ownerId: string },
|
|
149
201
|
): Promise<TaskItem> {
|
|
150
202
|
await this.ensureInitialized();
|
|
151
|
-
const id = uuidv4();
|
|
203
|
+
const id = params.id ?? uuidv4();
|
|
152
204
|
const now = new Date().toISOString();
|
|
153
205
|
|
|
154
206
|
await this.pool.query(
|
|
@@ -306,7 +358,7 @@ export class PostgreSQLTaskStore implements TaskStore {
|
|
|
306
358
|
}
|
|
307
359
|
if (updates.context !== undefined) {
|
|
308
360
|
setClauses.push(`context = $${paramIndex++}`);
|
|
309
|
-
params.push(JSON.stringify(updates.context));
|
|
361
|
+
params.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
310
362
|
}
|
|
311
363
|
if (updates.ownerType !== undefined) {
|
|
312
364
|
setClauses.push(`owner_type = $${paramIndex++}`);
|
|
@@ -345,16 +397,229 @@ export class PostgreSQLTaskStore implements TaskStore {
|
|
|
345
397
|
return existing;
|
|
346
398
|
}
|
|
347
399
|
|
|
348
|
-
setClauses.push(`updated_at =
|
|
349
|
-
params.push(new Date().toISOString());
|
|
400
|
+
setClauses.push(`updated_at = ${nextUpdatedAtSql()}`);
|
|
350
401
|
|
|
351
402
|
params.push(tenantId, id);
|
|
352
|
-
await this.pool.query(
|
|
353
|
-
`UPDATE lattice_tasks SET ${setClauses.join(", ")} WHERE tenant_id = $${paramIndex++} AND id = $${paramIndex++}
|
|
403
|
+
const result = await this.pool.query<TaskRow>(
|
|
404
|
+
`UPDATE lattice_tasks SET ${setClauses.join(", ")} WHERE tenant_id = $${paramIndex++} AND id = $${paramIndex++} AND updated_at <> '9999-12-31T23:59:59.999Z' RETURNING *`,
|
|
405
|
+
params,
|
|
406
|
+
);
|
|
407
|
+
return result.rows[0] ? mapRowToTask(result.rows[0]) : null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Atomically update a task unless its current status is blocked.
|
|
412
|
+
*
|
|
413
|
+
* @param tenantId Tenant identifier.
|
|
414
|
+
* @param id Task identifier.
|
|
415
|
+
* @param updates Partial task data to update.
|
|
416
|
+
* @param blockedStatuses Current statuses that prevent the update.
|
|
417
|
+
* @returns The updated task, or `null` when missing or blocked.
|
|
418
|
+
*/
|
|
419
|
+
async updateIfStatusNotIn(
|
|
420
|
+
tenantId: string,
|
|
421
|
+
id: string,
|
|
422
|
+
updates: UpdateTaskRequest,
|
|
423
|
+
blockedStatuses: TaskItem["status"][],
|
|
424
|
+
): Promise<TaskItem | null> {
|
|
425
|
+
await this.ensureInitialized();
|
|
426
|
+
const setClauses: string[] = [];
|
|
427
|
+
const params: unknown[] = [];
|
|
428
|
+
let paramIndex = 1;
|
|
429
|
+
|
|
430
|
+
if (updates.title !== undefined) { setClauses.push(`title = $${paramIndex++}`); params.push(updates.title); }
|
|
431
|
+
if (updates.description !== undefined) { setClauses.push(`description = $${paramIndex++}`); params.push(updates.description); }
|
|
432
|
+
if (updates.status !== undefined) { setClauses.push(`status = $${paramIndex++}`); params.push(updates.status); }
|
|
433
|
+
if (updates.priority !== undefined) { setClauses.push(`priority = $${paramIndex++}`); params.push(updates.priority); }
|
|
434
|
+
if (updates.dueDate !== undefined) { setClauses.push(`due_date = $${paramIndex++}`); params.push(updates.dueDate); }
|
|
435
|
+
if (updates.metadata !== undefined) { setClauses.push(`metadata = $${paramIndex++}`); params.push(JSON.stringify(updates.metadata)); }
|
|
436
|
+
if (updates.files !== undefined) { setClauses.push(`files = $${paramIndex++}`); params.push(JSON.stringify(updates.files)); }
|
|
437
|
+
if (updates.parentId !== undefined) { setClauses.push(`parent_id = $${paramIndex++}`); params.push(updates.parentId); }
|
|
438
|
+
if (updates.sourceId !== undefined) { setClauses.push(`source_id = $${paramIndex++}`); params.push(updates.sourceId); }
|
|
439
|
+
if (updates.context !== undefined) {
|
|
440
|
+
setClauses.push(`context = $${paramIndex++}`); params.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
441
|
+
}
|
|
442
|
+
if (updates.ownerType !== undefined) { setClauses.push(`owner_type = $${paramIndex++}`); params.push(updates.ownerType); }
|
|
443
|
+
if (updates.ownerId !== undefined) { setClauses.push(`owner_id = $${paramIndex++}`); params.push(updates.ownerId); }
|
|
444
|
+
if (updates.requireReview !== undefined) { setClauses.push(`require_review = $${paramIndex++}`); params.push(updates.requireReview); }
|
|
445
|
+
if (updates.dependencies !== undefined) { setClauses.push(`dependencies = $${paramIndex++}`); params.push(JSON.stringify(updates.dependencies)); }
|
|
446
|
+
if (updates.result !== undefined) { setClauses.push(`result = $${paramIndex++}`); params.push(updates.result); }
|
|
447
|
+
if (updates.failureReason !== undefined) { setClauses.push(`failure_reason = $${paramIndex++}`); params.push(updates.failureReason); }
|
|
448
|
+
if (updates.workspaceId !== undefined) { setClauses.push(`workspace_id = $${paramIndex++}`); params.push(updates.workspaceId); }
|
|
449
|
+
if (updates.projectId !== undefined) { setClauses.push(`project_id = $${paramIndex++}`); params.push(updates.projectId); }
|
|
450
|
+
|
|
451
|
+
setClauses.push(`updated_at = ${nextUpdatedAtSql()}`);
|
|
452
|
+
const tenantParam = paramIndex++;
|
|
453
|
+
const idParam = paramIndex++;
|
|
454
|
+
params.push(tenantId, id);
|
|
455
|
+
const blockedClause = blockedStatuses.length > 0
|
|
456
|
+
? ` AND status <> ALL($${paramIndex}::text[])`
|
|
457
|
+
: "";
|
|
458
|
+
if (blockedStatuses.length > 0) params.push(blockedStatuses);
|
|
459
|
+
|
|
460
|
+
const result = await this.pool.query<TaskRow>(
|
|
461
|
+
`UPDATE lattice_tasks SET ${setClauses.join(", ")} WHERE tenant_id = $${tenantParam} AND id = $${idParam} AND updated_at <> '9999-12-31T23:59:59.999Z'${blockedClause} RETURNING *`,
|
|
462
|
+
params,
|
|
463
|
+
);
|
|
464
|
+
return result.rows[0] ? mapRowToTask(result.rows[0]) : null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Atomically update a task only when its current status is expected. */
|
|
468
|
+
async updateIfStatusIn(
|
|
469
|
+
tenantId: string,
|
|
470
|
+
id: string,
|
|
471
|
+
updates: UpdateTaskRequest,
|
|
472
|
+
expectedStatuses: TaskItem["status"][],
|
|
473
|
+
): Promise<TaskItem | null> {
|
|
474
|
+
await this.ensureInitialized();
|
|
475
|
+
if (expectedStatuses.length === 0) return null;
|
|
476
|
+
|
|
477
|
+
const setClauses: string[] = [];
|
|
478
|
+
const params: unknown[] = [];
|
|
479
|
+
let paramIndex = 1;
|
|
480
|
+
if (updates.title !== undefined) { setClauses.push(`title = $${paramIndex++}`); params.push(updates.title); }
|
|
481
|
+
if (updates.description !== undefined) { setClauses.push(`description = $${paramIndex++}`); params.push(updates.description); }
|
|
482
|
+
if (updates.status !== undefined) { setClauses.push(`status = $${paramIndex++}`); params.push(updates.status); }
|
|
483
|
+
if (updates.priority !== undefined) { setClauses.push(`priority = $${paramIndex++}`); params.push(updates.priority); }
|
|
484
|
+
if (updates.dueDate !== undefined) { setClauses.push(`due_date = $${paramIndex++}`); params.push(updates.dueDate); }
|
|
485
|
+
if (updates.metadata !== undefined) { setClauses.push(`metadata = $${paramIndex++}`); params.push(JSON.stringify(updates.metadata)); }
|
|
486
|
+
if (updates.files !== undefined) { setClauses.push(`files = $${paramIndex++}`); params.push(JSON.stringify(updates.files)); }
|
|
487
|
+
if (updates.parentId !== undefined) { setClauses.push(`parent_id = $${paramIndex++}`); params.push(updates.parentId); }
|
|
488
|
+
if (updates.sourceId !== undefined) { setClauses.push(`source_id = $${paramIndex++}`); params.push(updates.sourceId); }
|
|
489
|
+
if (updates.context !== undefined) {
|
|
490
|
+
setClauses.push(`context = $${paramIndex++}`); params.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
491
|
+
}
|
|
492
|
+
if (updates.ownerType !== undefined) { setClauses.push(`owner_type = $${paramIndex++}`); params.push(updates.ownerType); }
|
|
493
|
+
if (updates.ownerId !== undefined) { setClauses.push(`owner_id = $${paramIndex++}`); params.push(updates.ownerId); }
|
|
494
|
+
if (updates.requireReview !== undefined) { setClauses.push(`require_review = $${paramIndex++}`); params.push(updates.requireReview); }
|
|
495
|
+
if (updates.dependencies !== undefined) { setClauses.push(`dependencies = $${paramIndex++}`); params.push(JSON.stringify(updates.dependencies)); }
|
|
496
|
+
if (updates.result !== undefined) { setClauses.push(`result = $${paramIndex++}`); params.push(updates.result); }
|
|
497
|
+
if (updates.failureReason !== undefined) { setClauses.push(`failure_reason = $${paramIndex++}`); params.push(updates.failureReason); }
|
|
498
|
+
if (updates.workspaceId !== undefined) { setClauses.push(`workspace_id = $${paramIndex++}`); params.push(updates.workspaceId); }
|
|
499
|
+
if (updates.projectId !== undefined) { setClauses.push(`project_id = $${paramIndex++}`); params.push(updates.projectId); }
|
|
500
|
+
|
|
501
|
+
setClauses.push(`updated_at = ${nextUpdatedAtSql()}`);
|
|
502
|
+
const tenantParam = paramIndex++;
|
|
503
|
+
const idParam = paramIndex++;
|
|
504
|
+
const statusesParam = paramIndex++;
|
|
505
|
+
params.push(tenantId, id, expectedStatuses);
|
|
506
|
+
const result = await this.pool.query<TaskRow>(
|
|
507
|
+
`UPDATE lattice_tasks SET ${setClauses.join(", ")} WHERE tenant_id = $${tenantParam} AND id = $${idParam} AND updated_at <> '9999-12-31T23:59:59.999Z' AND status = ANY($${statusesParam}::text[]) RETURNING *`,
|
|
354
508
|
params,
|
|
355
509
|
);
|
|
510
|
+
return result.rows[0] ? mapRowToTask(result.rows[0]) : null;
|
|
511
|
+
}
|
|
356
512
|
|
|
357
|
-
|
|
513
|
+
/** Atomically update a task only when status and updatedAt match a read snapshot. */
|
|
514
|
+
async updateIfStatusAndUpdatedAt(
|
|
515
|
+
tenantId: string,
|
|
516
|
+
id: string,
|
|
517
|
+
updates: UpdateTaskRequest,
|
|
518
|
+
expectedStatuses: TaskItem["status"][],
|
|
519
|
+
expectedUpdatedAt: Date | string,
|
|
520
|
+
): Promise<TaskItem | null> {
|
|
521
|
+
await this.ensureInitialized();
|
|
522
|
+
if (expectedStatuses.length === 0) return null;
|
|
523
|
+
const expectedIso = new Date(expectedUpdatedAt).toISOString();
|
|
524
|
+
const setClauses: string[] = [];
|
|
525
|
+
const params: unknown[] = [];
|
|
526
|
+
let paramIndex = 1;
|
|
527
|
+
if (updates.title !== undefined) { setClauses.push(`title = $${paramIndex++}`); params.push(updates.title); }
|
|
528
|
+
if (updates.description !== undefined) { setClauses.push(`description = $${paramIndex++}`); params.push(updates.description); }
|
|
529
|
+
if (updates.status !== undefined) { setClauses.push(`status = $${paramIndex++}`); params.push(updates.status); }
|
|
530
|
+
if (updates.priority !== undefined) { setClauses.push(`priority = $${paramIndex++}`); params.push(updates.priority); }
|
|
531
|
+
if (updates.dueDate !== undefined) { setClauses.push(`due_date = $${paramIndex++}`); params.push(updates.dueDate); }
|
|
532
|
+
if (updates.metadata !== undefined) { setClauses.push(`metadata = $${paramIndex++}`); params.push(JSON.stringify(updates.metadata)); }
|
|
533
|
+
if (updates.files !== undefined) { setClauses.push(`files = $${paramIndex++}`); params.push(JSON.stringify(updates.files)); }
|
|
534
|
+
if (updates.parentId !== undefined) { setClauses.push(`parent_id = $${paramIndex++}`); params.push(updates.parentId); }
|
|
535
|
+
if (updates.sourceId !== undefined) { setClauses.push(`source_id = $${paramIndex++}`); params.push(updates.sourceId); }
|
|
536
|
+
if (updates.context !== undefined) {
|
|
537
|
+
setClauses.push(`context = $${paramIndex++}`); params.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
538
|
+
}
|
|
539
|
+
if (updates.ownerType !== undefined) { setClauses.push(`owner_type = $${paramIndex++}`); params.push(updates.ownerType); }
|
|
540
|
+
if (updates.ownerId !== undefined) { setClauses.push(`owner_id = $${paramIndex++}`); params.push(updates.ownerId); }
|
|
541
|
+
if (updates.requireReview !== undefined) { setClauses.push(`require_review = $${paramIndex++}`); params.push(updates.requireReview); }
|
|
542
|
+
if (updates.dependencies !== undefined) { setClauses.push(`dependencies = $${paramIndex++}`); params.push(JSON.stringify(updates.dependencies)); }
|
|
543
|
+
if (updates.result !== undefined) { setClauses.push(`result = $${paramIndex++}`); params.push(updates.result); }
|
|
544
|
+
if (updates.failureReason !== undefined) { setClauses.push(`failure_reason = $${paramIndex++}`); params.push(updates.failureReason); }
|
|
545
|
+
if (updates.workspaceId !== undefined) { setClauses.push(`workspace_id = $${paramIndex++}`); params.push(updates.workspaceId); }
|
|
546
|
+
if (updates.projectId !== undefined) { setClauses.push(`project_id = $${paramIndex++}`); params.push(updates.projectId); }
|
|
547
|
+
|
|
548
|
+
setClauses.push(`updated_at = ${nextUpdatedAtSql()}`);
|
|
549
|
+
const tenantParam = paramIndex++;
|
|
550
|
+
const idParam = paramIndex++;
|
|
551
|
+
const statusesParam = paramIndex++;
|
|
552
|
+
const updatedAtParam = paramIndex++;
|
|
553
|
+
params.push(tenantId, id, expectedStatuses, expectedIso);
|
|
554
|
+
const result = await this.pool.query<TaskRow>(
|
|
555
|
+
`UPDATE lattice_tasks SET ${setClauses.join(", ")} WHERE tenant_id = $${tenantParam} AND id = $${idParam} AND status = ANY($${statusesParam}::text[]) AND ${canonicalTimestampSnapshotSql("updated_at", `$${updatedAtParam}`)} RETURNING *`,
|
|
556
|
+
params,
|
|
557
|
+
);
|
|
558
|
+
return result.rows[0] ? mapRowToTask(result.rows[0]) : null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Atomically update a child only when both child and parent snapshots match. */
|
|
562
|
+
async updateIfStatusUpdatedAtAndParentUpdatedAt(
|
|
563
|
+
tenantId: string,
|
|
564
|
+
id: string,
|
|
565
|
+
updates: UpdateTaskRequest,
|
|
566
|
+
expectedStatuses: TaskItem["status"][],
|
|
567
|
+
expectedUpdatedAt: Date | string,
|
|
568
|
+
parentId: string,
|
|
569
|
+
expectedParentUpdatedAt: Date | string,
|
|
570
|
+
): Promise<TaskItem | null> {
|
|
571
|
+
await this.ensureInitialized();
|
|
572
|
+
if (expectedStatuses.length === 0) return null;
|
|
573
|
+
const setClauses: string[] = [];
|
|
574
|
+
const params: unknown[] = [];
|
|
575
|
+
let paramIndex = 1;
|
|
576
|
+
if (updates.title !== undefined) { setClauses.push(`title = $${paramIndex++}`); params.push(updates.title); }
|
|
577
|
+
if (updates.description !== undefined) { setClauses.push(`description = $${paramIndex++}`); params.push(updates.description); }
|
|
578
|
+
if (updates.status !== undefined) { setClauses.push(`status = $${paramIndex++}`); params.push(updates.status); }
|
|
579
|
+
if (updates.priority !== undefined) { setClauses.push(`priority = $${paramIndex++}`); params.push(updates.priority); }
|
|
580
|
+
if (updates.dueDate !== undefined) { setClauses.push(`due_date = $${paramIndex++}`); params.push(updates.dueDate); }
|
|
581
|
+
if (updates.metadata !== undefined) { setClauses.push(`metadata = $${paramIndex++}`); params.push(JSON.stringify(updates.metadata)); }
|
|
582
|
+
if (updates.files !== undefined) { setClauses.push(`files = $${paramIndex++}`); params.push(JSON.stringify(updates.files)); }
|
|
583
|
+
if (updates.parentId !== undefined) { setClauses.push(`parent_id = $${paramIndex++}`); params.push(updates.parentId); }
|
|
584
|
+
if (updates.sourceId !== undefined) { setClauses.push(`source_id = $${paramIndex++}`); params.push(updates.sourceId); }
|
|
585
|
+
if (updates.context !== undefined) {
|
|
586
|
+
setClauses.push(`context = $${paramIndex++}`); params.push(updates.context === null ? null : JSON.stringify(updates.context));
|
|
587
|
+
}
|
|
588
|
+
if (updates.ownerType !== undefined) { setClauses.push(`owner_type = $${paramIndex++}`); params.push(updates.ownerType); }
|
|
589
|
+
if (updates.ownerId !== undefined) { setClauses.push(`owner_id = $${paramIndex++}`); params.push(updates.ownerId); }
|
|
590
|
+
if (updates.requireReview !== undefined) { setClauses.push(`require_review = $${paramIndex++}`); params.push(updates.requireReview); }
|
|
591
|
+
if (updates.dependencies !== undefined) { setClauses.push(`dependencies = $${paramIndex++}`); params.push(JSON.stringify(updates.dependencies)); }
|
|
592
|
+
if (updates.result !== undefined) { setClauses.push(`result = $${paramIndex++}`); params.push(updates.result); }
|
|
593
|
+
if (updates.failureReason !== undefined) { setClauses.push(`failure_reason = $${paramIndex++}`); params.push(updates.failureReason); }
|
|
594
|
+
if (updates.workspaceId !== undefined) { setClauses.push(`workspace_id = $${paramIndex++}`); params.push(updates.workspaceId); }
|
|
595
|
+
if (updates.projectId !== undefined) { setClauses.push(`project_id = $${paramIndex++}`); params.push(updates.projectId); }
|
|
596
|
+
|
|
597
|
+
setClauses.push(`updated_at = ${nextUpdatedAtSql("child.updated_at")}`);
|
|
598
|
+
const tenantParam = paramIndex++;
|
|
599
|
+
const idParam = paramIndex++;
|
|
600
|
+
const statusesParam = paramIndex++;
|
|
601
|
+
const updatedAtParam = paramIndex++;
|
|
602
|
+
const parentIdParam = paramIndex++;
|
|
603
|
+
const parentUpdatedAtParam = paramIndex++;
|
|
604
|
+
params.push(
|
|
605
|
+
tenantId, id, expectedStatuses, new Date(expectedUpdatedAt).toISOString(),
|
|
606
|
+
parentId, new Date(expectedParentUpdatedAt).toISOString(),
|
|
607
|
+
);
|
|
608
|
+
const result = await this.pool.query<TaskRow>(
|
|
609
|
+
`UPDATE lattice_tasks AS child SET ${setClauses.join(", ")}
|
|
610
|
+
WHERE child.tenant_id = $${tenantParam} AND child.id = $${idParam}
|
|
611
|
+
AND child.status = ANY($${statusesParam}::text[])
|
|
612
|
+
AND ${canonicalTimestampSnapshotSql("child.updated_at", `$${updatedAtParam}`)}
|
|
613
|
+
AND EXISTS (
|
|
614
|
+
SELECT 1 FROM lattice_tasks AS parent
|
|
615
|
+
WHERE parent.tenant_id = child.tenant_id
|
|
616
|
+
AND parent.id = $${parentIdParam}
|
|
617
|
+
AND ${canonicalTimestampSnapshotSql("parent.updated_at", `$${parentUpdatedAtParam}`, true)}
|
|
618
|
+
)
|
|
619
|
+
RETURNING child.*`,
|
|
620
|
+
params,
|
|
621
|
+
);
|
|
622
|
+
return result.rows[0] ? mapRowToTask(result.rows[0]) : null;
|
|
358
623
|
}
|
|
359
624
|
|
|
360
625
|
async delete(tenantId: string, id: string): Promise<boolean> {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Pool } from "pg";
|
|
2
|
-
import type { TaskWorkItemStore, TaskWorkItem, CreateWorkItemRequest, TaskWorkItemListFilter } from "@axiom-lattice/protocols";
|
|
2
|
+
import type { TaskWorkItemStore, TaskWorkItem, CreateWorkItemRequest, CreateWorkItemIfAbsentRequest, TaskWorkItemListFilter } from "@axiom-lattice/protocols";
|
|
3
3
|
import { v4 } from "uuid";
|
|
4
4
|
|
|
5
5
|
export class PostgreSQLTaskWorkItemStore implements TaskWorkItemStore {
|
|
@@ -24,6 +24,36 @@ export class PostgreSQLTaskWorkItemStore implements TaskWorkItemStore {
|
|
|
24
24
|
return this.rowToItem(result.rows[0]);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Find an event by its tenant- and task-scoped key without pagination. */
|
|
28
|
+
async findByEventKey(tenantId: string, taskId: string, eventKey: string): Promise<TaskWorkItem | null> {
|
|
29
|
+
const result = await this.pool.query(
|
|
30
|
+
`SELECT * FROM lattice_task_work_items
|
|
31
|
+
WHERE tenant_id = $1 AND task_id = $2 AND event_key = $3`,
|
|
32
|
+
[tenantId, taskId, eventKey],
|
|
33
|
+
);
|
|
34
|
+
return result.rows[0] ? this.rowToItem(result.rows[0]) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Atomically return an existing event or create it once. */
|
|
38
|
+
async createIfAbsentByEventKey(params: CreateWorkItemIfAbsentRequest): Promise<TaskWorkItem> {
|
|
39
|
+
const result = await this.pool.query(
|
|
40
|
+
`INSERT INTO lattice_task_work_items
|
|
41
|
+
(id, tenant_id, task_id, action, actor, thread_id, summary, detail, attempt, workspace_id, project_id, event_key)
|
|
42
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
43
|
+
ON CONFLICT (tenant_id, task_id, event_key) WHERE event_key IS NOT NULL
|
|
44
|
+
DO UPDATE SET event_key = EXCLUDED.event_key
|
|
45
|
+
RETURNING *`,
|
|
46
|
+
[
|
|
47
|
+
v4(), params.tenantId, params.taskId, params.action, params.actor,
|
|
48
|
+
params.threadId || null, params.summary || null,
|
|
49
|
+
params.detail ? JSON.stringify(params.detail) : null,
|
|
50
|
+
params.attempt ?? null, params.workspaceId || null, params.projectId || null,
|
|
51
|
+
params.eventKey,
|
|
52
|
+
],
|
|
53
|
+
);
|
|
54
|
+
return this.rowToItem(result.rows[0]);
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
async list(filter: TaskWorkItemListFilter): Promise<TaskWorkItem[]> {
|
|
28
58
|
let query = `SELECT * FROM lattice_task_work_items WHERE tenant_id = $1 AND task_id = $2`;
|
|
29
59
|
const params: unknown[] = [filter.tenantId, filter.taskId];
|
|
@@ -32,8 +62,17 @@ export class PostgreSQLTaskWorkItemStore implements TaskWorkItemStore {
|
|
|
32
62
|
query += ` AND action = $${params.length + 1}`;
|
|
33
63
|
params.push(filter.action);
|
|
34
64
|
}
|
|
65
|
+
if (filter.workspaceId) {
|
|
66
|
+
query += ` AND workspace_id = $${params.length + 1}`;
|
|
67
|
+
params.push(filter.workspaceId);
|
|
68
|
+
}
|
|
69
|
+
if (filter.projectId) {
|
|
70
|
+
query += ` AND project_id = $${params.length + 1}`;
|
|
71
|
+
params.push(filter.projectId);
|
|
72
|
+
}
|
|
35
73
|
|
|
36
|
-
|
|
74
|
+
const order = filter.order === 'desc' ? 'DESC' : 'ASC';
|
|
75
|
+
query += ` ORDER BY created_at ${order}, id ${order}`;
|
|
37
76
|
|
|
38
77
|
if (filter.limit) {
|
|
39
78
|
query += ` LIMIT $${params.length + 1}`;
|
|
@@ -61,6 +100,7 @@ export class PostgreSQLTaskWorkItemStore implements TaskWorkItemStore {
|
|
|
61
100
|
attempt: row.attempt as number | undefined,
|
|
62
101
|
workspaceId: row.workspace_id as string | undefined,
|
|
63
102
|
projectId: row.project_id as string | undefined,
|
|
103
|
+
eventKey: row.event_key == null ? undefined : row.event_key as string,
|
|
64
104
|
createdAt: new Date(row.created_at as string),
|
|
65
105
|
};
|
|
66
106
|
}
|