@axiom-lattice/pg-stores 1.0.89 → 1.0.90

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "1.0.89",
3
+ "version": "1.0.90",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -25,14 +25,17 @@
25
25
  "@langchain/core": "1.1.30",
26
26
  "pg": "^8.16.3",
27
27
  "uuid": "^9.0.1",
28
- "@axiom-lattice/core": "2.1.98",
29
- "@axiom-lattice/protocols": "2.1.50"
28
+ "@axiom-lattice/core": "2.1.99",
29
+ "@axiom-lattice/protocols": "2.1.51"
30
30
  },
31
31
  "devDependencies": {
32
+ "@types/jest": "^29.5.14",
32
33
  "@types/node": "^20.11.24",
33
34
  "@types/pg": "^8.11.10",
34
35
  "@types/uuid": "^9.0.8",
36
+ "jest": "^29.7.0",
35
37
  "rimraf": "^5.0.5",
38
+ "ts-jest": "^29.4.0",
36
39
  "tsup": "^8.0.1",
37
40
  "typescript": "^5.4.2"
38
41
  },
@@ -4,7 +4,7 @@
4
4
  * These are pure-function tests — no database required.
5
5
  * They verify that corrupted JSON in stored columns won't crash the API.
6
6
  */
7
- import { safeParse, mapRowToWorkflowRun, mapRowToRunStep } from "../stores/PostgreSQLWorkflowTrackingStore";
7
+ import { safeParse, mapRowToWorkflowRun, mapRowToRunStep, PostgreSQLWorkflowTrackingStore } from "../stores/PostgreSQLWorkflowTrackingStore";
8
8
 
9
9
  // ─── safeParse ──────────────────────────────────────────────────────────────
10
10
 
@@ -231,3 +231,73 @@ describe("mapRowToRunStep", () => {
231
231
  expect(result.output).toEqual({ deep: { nested: { arr: [1, { x: "y" }] } } });
232
232
  });
233
233
  });
234
+
235
+ // ─── queryWorkflowRuns ──────────────────────────────────────────────────────
236
+
237
+ describe("queryWorkflowRuns", () => {
238
+ const runRow = {
239
+ id: "run-1",
240
+ tenant_id: "t1",
241
+ assistant_id: "a1",
242
+ thread_id: "th1",
243
+ status: "interrupted",
244
+ topology_edges: [],
245
+ total_edges: 0,
246
+ completed_edges: 0,
247
+ error_message: null,
248
+ metadata: {},
249
+ started_at: new Date("2026-01-02"),
250
+ completed_at: null,
251
+ created_at: new Date("2026-01-02"),
252
+ updated_at: new Date("2026-01-02"),
253
+ };
254
+
255
+ function makeStore(queryImpl: (sql: string, params: unknown[]) => { rows: unknown[] }) {
256
+ const query = jest.fn(async (sql: unknown, params: unknown) =>
257
+ queryImpl(sql as string, params as unknown[]));
258
+ const store = new PostgreSQLWorkflowTrackingStore({ pool: { query } as never });
259
+ return { store, query };
260
+ }
261
+
262
+ it("filters by status, paginates, and parses COUNT string to number", async () => {
263
+ const { store, query } = makeStore((sql) =>
264
+ sql.includes("COUNT(*)") ? { rows: [{ count: "7" }] } : { rows: [runRow] });
265
+
266
+ const result = await store.queryWorkflowRuns("t1", { status: "interrupted", limit: 20, offset: 40 });
267
+
268
+ const [countSql, countParams] = query.mock.calls[0] as [string, unknown[]];
269
+ expect(countSql).toBe("SELECT COUNT(*) AS count FROM lattice_workflow_runs WHERE tenant_id = $1 AND status = $2");
270
+ expect(countParams).toEqual(["t1", "interrupted"]);
271
+
272
+ const [selectSql, selectParams] = query.mock.calls[1] as [string, unknown[]];
273
+ expect(selectSql).toBe("SELECT * FROM lattice_workflow_runs WHERE tenant_id = $1 AND status = $2 ORDER BY updated_at DESC, id DESC LIMIT $3 OFFSET $4");
274
+ expect(selectParams).toEqual(["t1", "interrupted", 20, 40]);
275
+
276
+ expect(result.total).toBe(7);
277
+ expect(result.records).toHaveLength(1);
278
+ expect(result.records[0].id).toBe("run-1");
279
+ });
280
+
281
+ it("omits LIMIT/OFFSET when not paginating", async () => {
282
+ const { store, query } = makeStore((sql) =>
283
+ sql.includes("COUNT(*)") ? { rows: [{ count: "1" }] } : { rows: [runRow] });
284
+
285
+ await store.queryWorkflowRuns("t1");
286
+
287
+ const [selectSql, selectParams] = query.mock.calls[1] as [string, unknown[]];
288
+ expect(selectSql).toBe("SELECT * FROM lattice_workflow_runs WHERE tenant_id = $1 ORDER BY updated_at DESC, id DESC");
289
+ expect(selectParams).toEqual(["t1"]);
290
+ expect(selectSql).not.toContain("LIMIT");
291
+ });
292
+
293
+ it("adds assistant_id condition when assistantId is given", async () => {
294
+ const { store, query } = makeStore((sql) =>
295
+ sql.includes("COUNT(*)") ? { rows: [{ count: "0" }] } : { rows: [] });
296
+
297
+ await store.queryWorkflowRuns("t1", { assistantId: "a9", limit: 5 });
298
+
299
+ const [selectSql, selectParams] = query.mock.calls[1] as [string, unknown[]];
300
+ expect(selectSql).toBe("SELECT * FROM lattice_workflow_runs WHERE tenant_id = $1 AND assistant_id = $2 ORDER BY updated_at DESC, id DESC LIMIT $3");
301
+ expect(selectParams).toEqual(["t1", "a9", 5]);
302
+ });
303
+ });
@@ -59,7 +59,7 @@ import { addPriorityAndCommandColumns } from "./migrations/add_priority_command_
59
59
  import { addCustomRunConfigColumn } from "./migrations/add_custom_run_config_column";
60
60
  import { alterMessageQueueIdColumn } from "./migrations/alter_message_queue_id_column";
61
61
  import { addWorkspaceProjectToQueue } from "./migrations/add_workspace_project_to_queue";
62
- import { createWorkflowTrackingTables, addStepThreadId } from "./migrations/workflow_tracking_migrations";
62
+ import { createWorkflowTrackingTables, addStepThreadId, addWorkflowRunsTenantStatusUpdatedIndex } from "./migrations/workflow_tracking_migrations";
63
63
  import { evalMigrations } from "./migrations/eval_migrations";
64
64
  import { createA2AApiKeysTable } from "./migrations/a2a_api_key_migration";
65
65
  import { createTasksTable } from "./migrations/task_migration";
@@ -119,6 +119,7 @@ export async function createPgStoreConfig(connectionString: string) {
119
119
  mm.register(addFileContentType); // v136
120
120
  mm.register(createCollectionsTable); // v137
121
121
  mm.register(createConnectionConfigsTable); // v160
122
+ mm.register(addWorkflowRunsTenantStatusUpdatedIndex); // v161
122
123
 
123
124
  await mm.migrate();
124
125
 
@@ -96,3 +96,21 @@ export const addStepThreadId: Migration = {
96
96
  `);
97
97
  },
98
98
  };
99
+
100
+ export const addWorkflowRunsTenantStatusUpdatedIndex: Migration = {
101
+ name: "add_workflow_runs_tenant_status_updated_index",
102
+ version: 161,
103
+
104
+ up: async (client) => {
105
+ await client.query(`
106
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_tenant_status
107
+ ON lattice_workflow_runs (tenant_id, status, updated_at DESC);
108
+ `);
109
+ },
110
+
111
+ down: async (client) => {
112
+ await client.query(`
113
+ DROP INDEX IF EXISTS idx_workflow_runs_tenant_status;
114
+ `);
115
+ },
116
+ };
@@ -14,9 +14,11 @@ import {
14
14
  UpdateRunStepRequest,
15
15
  StepType,
16
16
  WorkflowRunStatus,
17
+ QueryWorkflowRunsOptions,
18
+ QueryWorkflowRunsResult,
17
19
  } from "@axiom-lattice/protocols";
18
20
  import { MigrationManager } from "../migrations/migration";
19
- import { createWorkflowTrackingTables, addStepThreadId } from "../migrations/workflow_tracking_migrations";
21
+ import { createWorkflowTrackingTables, addStepThreadId, addWorkflowRunsTenantStatusUpdatedIndex } from "../migrations/workflow_tracking_migrations";
20
22
 
21
23
  export interface PostgreSQLWorkflowTrackingStoreOptions {
22
24
  pool?: Pool;
@@ -51,6 +53,7 @@ export class PostgreSQLWorkflowTrackingStore implements WorkflowTrackingStore {
51
53
  this.migrationManager = new MigrationManager(this.pool);
52
54
  this.migrationManager.register(createWorkflowTrackingTables);
53
55
  this.migrationManager.register(addStepThreadId);
56
+ this.migrationManager.register(addWorkflowRunsTenantStatusUpdatedIndex);
54
57
 
55
58
  if (options.autoMigrate !== false) {
56
59
  this.initialize().catch((error) => {
@@ -180,6 +183,41 @@ export class PostgreSQLWorkflowTrackingStore implements WorkflowTrackingStore {
180
183
  return result.rows.map(mapRowToWorkflowRun);
181
184
  }
182
185
 
186
+ async queryWorkflowRuns(tenantId: string, options: QueryWorkflowRunsOptions = {}): Promise<QueryWorkflowRunsResult> {
187
+ await this.ensureInitialized();
188
+ const conditions: string[] = ["tenant_id = $1"];
189
+ const whereParams: unknown[] = [tenantId];
190
+ if (options.status) {
191
+ whereParams.push(options.status);
192
+ conditions.push(`status = $${whereParams.length}`);
193
+ }
194
+ if (options.assistantId) {
195
+ whereParams.push(options.assistantId);
196
+ conditions.push(`assistant_id = $${whereParams.length}`);
197
+ }
198
+ const where = conditions.join(" AND ");
199
+
200
+ const countResult = await this.pool.query(
201
+ `SELECT COUNT(*) AS count FROM lattice_workflow_runs WHERE ${where}`,
202
+ whereParams
203
+ );
204
+ const total = parseInt(countResult.rows[0].count, 10);
205
+
206
+ const selectParams: unknown[] = [...whereParams];
207
+ let sql = `SELECT * FROM lattice_workflow_runs WHERE ${where} ORDER BY updated_at DESC, id DESC`;
208
+ if (options.limit !== undefined) {
209
+ selectParams.push(options.limit);
210
+ sql += ` LIMIT $${selectParams.length}`;
211
+ }
212
+ if (options.offset !== undefined) {
213
+ selectParams.push(options.offset);
214
+ sql += ` OFFSET $${selectParams.length}`;
215
+ }
216
+
217
+ const result = await this.pool.query(sql, selectParams);
218
+ return { records: result.rows.map(mapRowToWorkflowRun), total };
219
+ }
220
+
183
221
  async createRunStep(request: CreateRunStepRequest): Promise<RunStep> {
184
222
  await this.ensureInitialized();
185
223
  const now = new Date();