@axiom-lattice/pg-stores 1.0.89 → 1.0.91
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 +17 -0
- package/dist/index.d.mts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +69 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +68 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
- package/src/__tests__/PostgreSQLWorkflowTrackingStore.test.ts +72 -2
- package/src/createPgStoreConfig.ts +2 -1
- package/src/migrations/workflow_tracking_migrations.ts +18 -0
- package/src/stores/PostgreSQLConnectionStore.ts +8 -0
- package/src/stores/PostgreSQLEvalStore.ts +12 -0
- package/src/stores/PostgreSQLWorkflowTrackingStore.ts +42 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/pg-stores",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.91",
|
|
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.
|
|
29
|
-
"@axiom-lattice/protocols": "2.1.
|
|
28
|
+
"@axiom-lattice/core": "2.1.100",
|
|
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
|
|
|
@@ -220,7 +220,7 @@ describe("mapRowToRunStep", () => {
|
|
|
220
220
|
output: "also invalid",
|
|
221
221
|
});
|
|
222
222
|
expect(result.input).toBeUndefined();
|
|
223
|
-
expect(result.output).
|
|
223
|
+
expect(result.output).toEqual("also invalid"); // text output preserved as-is (jsonb column)
|
|
224
224
|
});
|
|
225
225
|
|
|
226
226
|
test("handles deeply nested objects", () => {
|
|
@@ -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
|
+
};
|
|
@@ -30,6 +30,14 @@ export class PostgreSQLConnectionStore implements ConnectionStore {
|
|
|
30
30
|
this.db = db;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
async listByTenant(tenantId: string): Promise<ConnectionEntry[]> {
|
|
34
|
+
const result = await this.db.query<ConnectionRow>(
|
|
35
|
+
`SELECT * FROM ${TABLE} WHERE tenant_id = $1 ORDER BY created_at`,
|
|
36
|
+
[tenantId],
|
|
37
|
+
);
|
|
38
|
+
return result.rows.map((row) => this.mapRow(row));
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
async listByType(tenantId: string, type: string): Promise<ConnectionEntry[]> {
|
|
34
42
|
const result = await this.db.query<ConnectionRow>(
|
|
35
43
|
`SELECT * FROM ${TABLE} WHERE tenant_id = $1 AND type = $2 ORDER BY created_at`,
|
|
@@ -828,6 +828,18 @@ export class PostgreSQLEvalStore implements EvalStore {
|
|
|
828
828
|
return rows.length ? this.mapRowToRunResult(rows[0] as unknown as Record<string, unknown>) : null;
|
|
829
829
|
}
|
|
830
830
|
|
|
831
|
+
/** Delete a run result by ID */
|
|
832
|
+
async deleteRunResult(tenantId: string, id: string): Promise<boolean> {
|
|
833
|
+
await this.ensureInitialized();
|
|
834
|
+
const result = await this.pool.query(
|
|
835
|
+
`DELETE FROM lattice_eval_run_results
|
|
836
|
+
WHERE id = $1
|
|
837
|
+
AND run_id IN (SELECT id FROM lattice_eval_runs WHERE tenant_id = $2)`,
|
|
838
|
+
[id, tenantId],
|
|
839
|
+
);
|
|
840
|
+
return (result.rowCount ?? 0) > 0;
|
|
841
|
+
}
|
|
842
|
+
|
|
831
843
|
/** Get a single run result by ID with tenant isolation */
|
|
832
844
|
private async getRunResultById(tenantId: string, id: string): Promise<EvalRunResult | null> {
|
|
833
845
|
await this.ensureInitialized();
|
|
@@ -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();
|
|
@@ -336,7 +374,9 @@ export function mapRowToRunStep(row: any): RunStep {
|
|
|
336
374
|
edgeTo: row.edge_to,
|
|
337
375
|
edgePurpose: row.edge_purpose,
|
|
338
376
|
input: safeParse(row.input, undefined),
|
|
339
|
-
output:
|
|
377
|
+
output: typeof row.output === "string"
|
|
378
|
+
? safeParse(row.output, row.output as any)
|
|
379
|
+
: row.output ?? undefined,
|
|
340
380
|
status: row.status,
|
|
341
381
|
errorMessage: row.error_message,
|
|
342
382
|
startedAt: row.started_at,
|