@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.
@@ -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 = $${paramIndex++}`);
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
- return this.getById(tenantId, id);
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
- query += ` ORDER BY created_at ASC`;
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
  }