@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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import { PostgreSQLTaskWorkItemStore } from "../stores/PostgreSQLTaskWorkItemStore";
|
|
3
|
+
import { addTaskWorkItemEventKeyMigration } from "../migrations/task_work_items_migration";
|
|
4
|
+
|
|
5
|
+
describe("PostgreSQLTaskWorkItemStore", () => {
|
|
6
|
+
const row = {
|
|
7
|
+
id: "item-1", tenant_id: "tenant-1", task_id: "task-1", event_key: "evidence:key",
|
|
8
|
+
action: "completion_evidence", actor: "agent:a", thread_id: null, summary: "first",
|
|
9
|
+
detail: null, attempt: null, workspace_id: null, project_id: null,
|
|
10
|
+
created_at: "2026-01-01T00:00:00.000Z",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it.each([
|
|
14
|
+
[undefined, "ASC"],
|
|
15
|
+
["asc", "ASC"],
|
|
16
|
+
["desc", "DESC"],
|
|
17
|
+
] as const)("uses %s order as %s with deterministic IDs", async (order, sqlOrder) => {
|
|
18
|
+
const query = jest.fn().mockResolvedValue({ rows: [] });
|
|
19
|
+
const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
|
|
20
|
+
|
|
21
|
+
await store.list({
|
|
22
|
+
tenantId: "tenant-1",
|
|
23
|
+
taskId: "task-1",
|
|
24
|
+
limit: 100,
|
|
25
|
+
...(order && { order }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(query).toHaveBeenCalledWith(
|
|
29
|
+
expect.stringContaining(`ORDER BY created_at ${sqlOrder}, id ${sqlOrder} LIMIT $3`),
|
|
30
|
+
["tenant-1", "task-1", 100],
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("filters by workspaceId and projectId", async () => {
|
|
35
|
+
const query = jest.fn().mockResolvedValue({ rows: [] });
|
|
36
|
+
const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
|
|
37
|
+
|
|
38
|
+
await store.list({
|
|
39
|
+
tenantId: "tenant-1",
|
|
40
|
+
taskId: "task-1",
|
|
41
|
+
workspaceId: "workspace-1",
|
|
42
|
+
projectId: "project-1",
|
|
43
|
+
order: "desc",
|
|
44
|
+
limit: 100,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(query).toHaveBeenCalledWith(
|
|
48
|
+
expect.stringContaining(
|
|
49
|
+
"workspace_id = $3 AND project_id = $4 ORDER BY created_at DESC, id DESC LIMIT $5",
|
|
50
|
+
),
|
|
51
|
+
["tenant-1", "task-1", "workspace-1", "project-1", 100],
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("looks up an event key by tenant and task", async () => {
|
|
56
|
+
const query = jest.fn().mockResolvedValue({ rows: [row] });
|
|
57
|
+
const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
|
|
58
|
+
|
|
59
|
+
const result = await store.findByEventKey("tenant-1", "task-1", "evidence:key");
|
|
60
|
+
|
|
61
|
+
expect(query).toHaveBeenCalledWith(
|
|
62
|
+
expect.stringContaining("tenant_id = $1 AND task_id = $2 AND event_key = $3"),
|
|
63
|
+
["tenant-1", "task-1", "evidence:key"],
|
|
64
|
+
);
|
|
65
|
+
expect(result?.eventKey).toBe("evidence:key");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("maps a null event key to undefined", async () => {
|
|
69
|
+
const query = jest.fn().mockResolvedValue({ rows: [{ ...row, event_key: null }] });
|
|
70
|
+
const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
|
|
71
|
+
|
|
72
|
+
const result = await store.list({ tenantId: "tenant-1", taskId: "task-1" });
|
|
73
|
+
|
|
74
|
+
expect(result[0].eventKey).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("atomically inserts by task-scoped event key or returns the existing row", async () => {
|
|
78
|
+
const query = jest.fn().mockResolvedValue({ rows: [row] });
|
|
79
|
+
const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
|
|
80
|
+
|
|
81
|
+
const result = await store.createIfAbsentByEventKey({
|
|
82
|
+
tenantId: "tenant-1", taskId: "task-1", eventKey: "evidence:key",
|
|
83
|
+
action: "completion_evidence", actor: "agent:a", summary: "first",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(query).toHaveBeenCalledWith(
|
|
88
|
+
expect.stringMatching(/INSERT[\s\S]+ON CONFLICT \(tenant_id, task_id, event_key\)[\s\S]+RETURNING/),
|
|
89
|
+
expect.arrayContaining(["tenant-1", "task-1", "evidence:key"]),
|
|
90
|
+
);
|
|
91
|
+
expect(result.eventKey).toBe("evidence:key");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("migrates a task-scoped unique event key", async () => {
|
|
95
|
+
const query = jest.fn().mockResolvedValue({ rows: [] });
|
|
96
|
+
|
|
97
|
+
await addTaskWorkItemEventKeyMigration.up({ query } as never);
|
|
98
|
+
|
|
99
|
+
expect(query).toHaveBeenCalledWith(expect.stringContaining("ADD COLUMN IF NOT EXISTS event_key TEXT"));
|
|
100
|
+
expect(query).toHaveBeenCalledWith(expect.stringContaining(
|
|
101
|
+
"(tenant_id, task_id, event_key) WHERE event_key IS NOT NULL",
|
|
102
|
+
));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* add_a2a_key_assistant_ids migration tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies the migration adds (and removes) the assistant_ids JSONB
|
|
5
|
+
* column on lattice_a2a_api_keys.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
9
|
+
import type { PoolClient } from "pg";
|
|
10
|
+
import { addA2AKeyAssistantIds } from "../migrations/add_a2a_key_assistant_ids";
|
|
11
|
+
|
|
12
|
+
describe("add_a2a_key_assistant_ids migration", () => {
|
|
13
|
+
it("has name add_a2a_key_assistant_ids", () => {
|
|
14
|
+
expect(addA2AKeyAssistantIds.name).toBe("add_a2a_key_assistant_ids");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("has a version greater than the current max (166)", () => {
|
|
18
|
+
expect(addA2AKeyAssistantIds.version).toBe(167);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("emits ADD COLUMN assistant_ids JSONB in up()", async () => {
|
|
22
|
+
const queries: string[] = [];
|
|
23
|
+
const client = { query: jest.fn(async (sql: string) => { queries.push(sql); }) };
|
|
24
|
+
await addA2AKeyAssistantIds.up(client as unknown as PoolClient);
|
|
25
|
+
expect(queries.some((q) => q.includes("ADD COLUMN IF NOT EXISTS assistant_ids JSONB"))).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("emits DROP COLUMN assistant_ids in down()", async () => {
|
|
29
|
+
const queries: string[] = [];
|
|
30
|
+
const client = { query: jest.fn(async (sql: string) => { queries.push(sql); }) };
|
|
31
|
+
await addA2AKeyAssistantIds.down?.(client as unknown as PoolClient);
|
|
32
|
+
expect(queries.some((q) => q.includes("DROP COLUMN IF EXISTS assistant_ids"))).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -232,4 +232,52 @@ describeIfDb("PostgreSQLProjectStore", () => {
|
|
|
232
232
|
const deleted = await store.deleteProject(tenantId, "non-existent");
|
|
233
233
|
expect(deleted).toBe(false);
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
test("should default kind to 'business' when omitted on create", async () => {
|
|
237
|
+
const project = await store.createProject(tenantId, workspaceId, "test-proj-kind-1", {
|
|
238
|
+
name: "Default Kind",
|
|
239
|
+
});
|
|
240
|
+
expect(project.kind).toBe("business");
|
|
241
|
+
|
|
242
|
+
const fetched = await store.getProjectById(tenantId, "test-proj-kind-1");
|
|
243
|
+
expect(fetched?.kind).toBe("business");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("should round-trip an explicit kind through create and get", async () => {
|
|
247
|
+
const project = await store.createProject(tenantId, workspaceId, "test-proj-kind-2", {
|
|
248
|
+
name: "Training Project",
|
|
249
|
+
kind: "training",
|
|
250
|
+
});
|
|
251
|
+
expect(project.kind).toBe("training");
|
|
252
|
+
|
|
253
|
+
const fetched = await store.getProjectById(tenantId, "test-proj-kind-2");
|
|
254
|
+
expect(fetched?.kind).toBe("training");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("should update kind", async () => {
|
|
258
|
+
await store.createProject(tenantId, workspaceId, "test-proj-kind-3", {
|
|
259
|
+
name: "Reclassified",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const updated = await store.updateProject(tenantId, "test-proj-kind-3", { kind: "personal" });
|
|
263
|
+
expect(updated?.kind).toBe("personal");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("should filter projects by kind", async () => {
|
|
267
|
+
await store.createProject(tenantId, workspaceId, "test-proj-kind-4", { name: "Biz", kind: "business" });
|
|
268
|
+
await store.createProject(tenantId, workspaceId, "test-proj-kind-5", { name: "Train", kind: "training" });
|
|
269
|
+
|
|
270
|
+
const training = await store.getProjectsByWorkspace(tenantId, workspaceId, { kind: "training" });
|
|
271
|
+
const ids = training.filter((p) => p.id.startsWith("test-proj-kind-")).map((p) => p.id);
|
|
272
|
+
expect(ids).toEqual(["test-proj-kind-5"]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("should return all projects when kind filter is omitted", async () => {
|
|
276
|
+
await store.createProject(tenantId, workspaceId, "test-proj-kind-6", { name: "Biz", kind: "business" });
|
|
277
|
+
await store.createProject(tenantId, workspaceId, "test-proj-kind-7", { name: "Train", kind: "training" });
|
|
278
|
+
|
|
279
|
+
const all = await store.getProjectsByWorkspace(tenantId, workspaceId);
|
|
280
|
+
const ids = all.filter((p) => p.id.startsWith("test-proj-kind-")).map((p) => p.id);
|
|
281
|
+
expect(ids).toHaveLength(2);
|
|
282
|
+
});
|
|
235
283
|
});
|
|
@@ -43,6 +43,7 @@ import { changeSkillPrimaryKey } from "./migrations/skill_pk_migration";
|
|
|
43
43
|
import { createWorkspacesTable } from "./migrations/workspace_migrations";
|
|
44
44
|
import { createProjectsTable } from "./migrations/project_migrations";
|
|
45
45
|
import { addProjectConfigColumn } from "./migrations/add_project_config_column";
|
|
46
|
+
import { addProjectKindColumn } from "./migrations/add_project_kind_column";
|
|
46
47
|
import { createUsersTable } from "./migrations/user_migrations";
|
|
47
48
|
import { addUserStatusColumn } from "./migrations/user_status_migration";
|
|
48
49
|
import { createTenantsTable } from "./migrations/tenant_migrations";
|
|
@@ -63,12 +64,13 @@ import { addWorkspaceProjectToQueue } from "./migrations/add_workspace_project_t
|
|
|
63
64
|
import { createWorkflowTrackingTables, addStepThreadId, addWorkflowRunsTenantStatusUpdatedIndex } from "./migrations/workflow_tracking_migrations";
|
|
64
65
|
import { evalMigrations } from "./migrations/eval_migrations";
|
|
65
66
|
import { createA2AApiKeysTable } from "./migrations/a2a_api_key_migration";
|
|
67
|
+
import { addA2AKeyAssistantIds } from "./migrations/add_a2a_key_assistant_ids";
|
|
66
68
|
import { taskMigrations } from "./migrations/task_migration";
|
|
67
69
|
import { createMenuItemsTable } from "./migrations/menu_items_migration";
|
|
68
70
|
import { addFileContentType } from "./migrations/menu_items_add_file_type";
|
|
69
71
|
import { createSharedResourcesTable } from "./migrations/shared_resources_migration";
|
|
70
72
|
import { createCollectionsTable } from "./migrations/collection_migrations";
|
|
71
|
-
import { createTaskWorkItemsMigration, addWorkItemProjectFieldsMigration } from "./migrations/task_work_items_migration";
|
|
73
|
+
import { createTaskWorkItemsMigration, addWorkItemProjectFieldsMigration, addTaskWorkItemEventKeyMigration } from "./migrations/task_work_items_migration";
|
|
72
74
|
|
|
73
75
|
export async function createPgStoreConfig(connectionString: string) {
|
|
74
76
|
const pool = new Pool({ connectionString });
|
|
@@ -126,6 +128,9 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
126
128
|
mm.register(addWorkItemProjectFieldsMigration); // v140
|
|
127
129
|
mm.register(createConnectionConfigsTable); // v160
|
|
128
130
|
mm.register(addWorkflowRunsTenantStatusUpdatedIndex); // v161
|
|
131
|
+
mm.register(addProjectKindColumn); // v165
|
|
132
|
+
mm.register(addA2AKeyAssistantIds); // v167
|
|
133
|
+
mm.register(addTaskWorkItemEventKeyMigration); // v168
|
|
129
134
|
|
|
130
135
|
await mm.migrate();
|
|
131
136
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Migration } from "./migration";
|
|
2
|
+
import type { PoolClient } from "pg";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Adds the assistant_ids JSONB whitelist column to A2A API keys.
|
|
6
|
+
*
|
|
7
|
+
* Empty/undefined assistant_ids means all exposed agents in the tenant.
|
|
8
|
+
*/
|
|
9
|
+
export const addA2AKeyAssistantIds: Migration = {
|
|
10
|
+
version: 167,
|
|
11
|
+
name: "add_a2a_key_assistant_ids",
|
|
12
|
+
up: async (client: PoolClient) => {
|
|
13
|
+
await client.query(`
|
|
14
|
+
ALTER TABLE lattice_a2a_api_keys
|
|
15
|
+
ADD COLUMN IF NOT EXISTS assistant_ids JSONB
|
|
16
|
+
`);
|
|
17
|
+
},
|
|
18
|
+
down: async (client: PoolClient) => {
|
|
19
|
+
await client.query(`
|
|
20
|
+
ALTER TABLE lattice_a2a_api_keys
|
|
21
|
+
DROP COLUMN IF EXISTS assistant_ids
|
|
22
|
+
`);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: Add kind column to lattice_projects
|
|
3
|
+
*
|
|
4
|
+
* Adds a TEXT `kind` column (NOT NULL DEFAULT 'business') to the
|
|
5
|
+
* `lattice_projects` table for project classification
|
|
6
|
+
* ('business' | 'training' | 'personal'). Existing rows are backfilled
|
|
7
|
+
* with 'business' via the column default.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { PoolClient } from "pg";
|
|
11
|
+
import { Migration } from "./migration";
|
|
12
|
+
|
|
13
|
+
/** Adds kind TEXT column to lattice_projects */
|
|
14
|
+
export const addProjectKindColumn: Migration = {
|
|
15
|
+
version: 165,
|
|
16
|
+
name: "add_project_kind_column",
|
|
17
|
+
up: async (client: PoolClient) => {
|
|
18
|
+
await client.query(`
|
|
19
|
+
ALTER TABLE lattice_projects
|
|
20
|
+
ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'business'
|
|
21
|
+
`);
|
|
22
|
+
},
|
|
23
|
+
down: async (client: PoolClient) => {
|
|
24
|
+
await client.query(`
|
|
25
|
+
ALTER TABLE lattice_projects
|
|
26
|
+
DROP COLUMN IF EXISTS kind
|
|
27
|
+
`);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -49,3 +49,18 @@ export const addWorkItemProjectFieldsMigration: Migration = {
|
|
|
49
49
|
`);
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
|
+
|
|
53
|
+
export const addTaskWorkItemEventKeyMigration: Migration = {
|
|
54
|
+
version: 168,
|
|
55
|
+
name: "add_task_work_item_event_key",
|
|
56
|
+
up: async (client: PoolClient) => {
|
|
57
|
+
await client.query(`
|
|
58
|
+
ALTER TABLE lattice_task_work_items
|
|
59
|
+
ADD COLUMN IF NOT EXISTS event_key TEXT
|
|
60
|
+
`);
|
|
61
|
+
await client.query(`
|
|
62
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_work_items_event_key
|
|
63
|
+
ON lattice_task_work_items (tenant_id, task_id, event_key) WHERE event_key IS NOT NULL
|
|
64
|
+
`);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from "@axiom-lattice/protocols";
|
|
9
9
|
import { MigrationManager } from "../migrations/migration";
|
|
10
10
|
import { createA2AApiKeysTable } from "../migrations/a2a_api_key_migration";
|
|
11
|
+
import { addA2AKeyAssistantIds } from "../migrations/add_a2a_key_assistant_ids";
|
|
11
12
|
import { encrypt, decrypt } from "@axiom-lattice/core";
|
|
12
13
|
import { randomUUID } from "crypto";
|
|
13
14
|
|
|
@@ -21,8 +22,8 @@ type KeyRow = {
|
|
|
21
22
|
id: string;
|
|
22
23
|
key_value: string;
|
|
23
24
|
tenant_id: string;
|
|
24
|
-
project_id: string
|
|
25
|
-
|
|
25
|
+
project_id: string;
|
|
26
|
+
assistant_ids: string[] | null;
|
|
26
27
|
label: string | null;
|
|
27
28
|
enabled: boolean;
|
|
28
29
|
created_at: Date;
|
|
@@ -38,8 +39,8 @@ function mapRowToRecord(row: KeyRow): A2AApiKeyRecord {
|
|
|
38
39
|
id: row.id,
|
|
39
40
|
key: decrypt(row.key_value),
|
|
40
41
|
tenantId: row.tenant_id,
|
|
41
|
-
projectId: row.project_id
|
|
42
|
-
|
|
42
|
+
projectId: row.project_id,
|
|
43
|
+
assistantIds: row.assistant_ids ?? undefined,
|
|
43
44
|
label: row.label || undefined,
|
|
44
45
|
enabled: row.enabled,
|
|
45
46
|
createdAt: row.created_at,
|
|
@@ -71,7 +72,12 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
71
72
|
: (() => { throw new Error("Either pool or poolConfig must be provided"); })();
|
|
72
73
|
|
|
73
74
|
this.migrationManager = new MigrationManager(this.pool);
|
|
75
|
+
// Standalone deployments (own poolConfig) must apply every migration the
|
|
76
|
+
// queries below depend on. `createPgStoreConfig` registers the same
|
|
77
|
+
// migrations on its own MigrationManager; managers dedupe by name, so the
|
|
78
|
+
// overlap is safe.
|
|
74
79
|
this.migrationManager.register(createA2AApiKeysTable);
|
|
80
|
+
this.migrationManager.register(addA2AKeyAssistantIds);
|
|
75
81
|
|
|
76
82
|
if (options.autoMigrate !== false) {
|
|
77
83
|
this.initialize().catch((error) => {
|
|
@@ -116,6 +122,15 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
116
122
|
return null;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
async findById(id: string): Promise<A2AApiKeyRecord | null> {
|
|
126
|
+
await this.ensureInitialized();
|
|
127
|
+
const result = await this.pool.query<KeyRow>(
|
|
128
|
+
`SELECT * FROM lattice_a2a_api_keys WHERE id = $1`,
|
|
129
|
+
[id],
|
|
130
|
+
);
|
|
131
|
+
return result.rows[0] ? mapRowToRecord(result.rows[0]) : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
async list(params: { tenantId?: string; limit?: number; offset?: number }): Promise<A2AApiKeyRecord[]> {
|
|
120
135
|
await this.ensureInitialized();
|
|
121
136
|
const limit = params.limit || 100;
|
|
@@ -138,9 +153,9 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
138
153
|
await this.ensureInitialized();
|
|
139
154
|
const key = generateApiKey();
|
|
140
155
|
const result = await this.pool.query<KeyRow>(
|
|
141
|
-
`INSERT INTO lattice_a2a_api_keys (key_value, tenant_id, project_id,
|
|
156
|
+
`INSERT INTO lattice_a2a_api_keys (key_value, tenant_id, project_id, assistant_ids, label)
|
|
142
157
|
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
|
143
|
-
[encrypt(key), input.tenantId, input.projectId
|
|
158
|
+
[encrypt(key), input.tenantId, input.projectId, input.assistantIds ? JSON.stringify(input.assistantIds) : null, input.label || null],
|
|
144
159
|
);
|
|
145
160
|
const record = mapRowToRecord(result.rows[0]);
|
|
146
161
|
record.key = key;
|
|
@@ -196,8 +211,8 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
196
211
|
map.set(key, {
|
|
197
212
|
key,
|
|
198
213
|
tenantId: row.tenant_id,
|
|
199
|
-
projectId: row.project_id
|
|
200
|
-
|
|
214
|
+
projectId: row.project_id,
|
|
215
|
+
assistantIds: row.assistant_ids ?? undefined,
|
|
201
216
|
});
|
|
202
217
|
}
|
|
203
218
|
return map;
|
|
@@ -7,6 +7,7 @@ import type { PoolConfig } from "pg";
|
|
|
7
7
|
import {
|
|
8
8
|
ProjectStore,
|
|
9
9
|
Project,
|
|
10
|
+
ProjectFilter,
|
|
10
11
|
CreateProjectRequest,
|
|
11
12
|
UpdateProjectRequest,
|
|
12
13
|
} from "@axiom-lattice/protocols";
|
|
@@ -14,6 +15,7 @@ import { MigrationManager } from "../migrations/migration";
|
|
|
14
15
|
import { createProjectsTable } from "../migrations/project_migrations";
|
|
15
16
|
import { addProjectConfigColumn } from "../migrations/add_project_config_column";
|
|
16
17
|
import { dropProjectFolderPathColumn } from "../migrations/drop_project_folder_path";
|
|
18
|
+
import { addProjectKindColumn } from "../migrations/add_project_kind_column";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* PostgreSQL ProjectStore options
|
|
@@ -69,6 +71,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
69
71
|
this.migrationManager.register(createProjectsTable);
|
|
70
72
|
this.migrationManager.register(addProjectConfigColumn);
|
|
71
73
|
this.migrationManager.register(dropProjectFolderPathColumn);
|
|
74
|
+
this.migrationManager.register(addProjectKindColumn);
|
|
72
75
|
|
|
73
76
|
// Auto-migrate by default
|
|
74
77
|
if (options.autoMigrate !== false) {
|
|
@@ -120,6 +123,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
120
123
|
name: string;
|
|
121
124
|
description: string | null;
|
|
122
125
|
config: unknown | null;
|
|
126
|
+
kind: string | null;
|
|
123
127
|
created_at: Date;
|
|
124
128
|
updated_at: Date;
|
|
125
129
|
}): Project {
|
|
@@ -130,6 +134,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
130
134
|
name: row.name,
|
|
131
135
|
description: row.description || undefined,
|
|
132
136
|
config: row.config as Record<string, unknown> || undefined,
|
|
137
|
+
kind: (row.kind || "business") as Project["kind"],
|
|
133
138
|
createdAt: row.created_at,
|
|
134
139
|
updatedAt: row.updated_at,
|
|
135
140
|
};
|
|
@@ -138,9 +143,17 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
138
143
|
/**
|
|
139
144
|
* Get all projects for a specific workspace
|
|
140
145
|
*/
|
|
141
|
-
async getProjectsByWorkspace(tenantId: string, workspaceId: string): Promise<Project[]> {
|
|
146
|
+
async getProjectsByWorkspace(tenantId: string, workspaceId: string, filter?: ProjectFilter): Promise<Project[]> {
|
|
142
147
|
await this.ensureInitialized();
|
|
143
148
|
|
|
149
|
+
const conditions = ["tenant_id = $1", "workspace_id = $2"];
|
|
150
|
+
const values: unknown[] = [tenantId, workspaceId];
|
|
151
|
+
|
|
152
|
+
if (filter?.kind !== undefined) {
|
|
153
|
+
values.push(filter.kind);
|
|
154
|
+
conditions.push(`kind = $${values.length}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
144
157
|
const result = await this.pool.query<{
|
|
145
158
|
id: string;
|
|
146
159
|
tenant_id: string;
|
|
@@ -148,16 +161,17 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
148
161
|
name: string;
|
|
149
162
|
description: string | null;
|
|
150
163
|
config: unknown | null;
|
|
164
|
+
kind: string | null;
|
|
151
165
|
created_at: Date;
|
|
152
166
|
updated_at: Date;
|
|
153
167
|
}>(
|
|
154
168
|
`
|
|
155
|
-
SELECT id, tenant_id, workspace_id, name, description, config, created_at, updated_at
|
|
169
|
+
SELECT id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at
|
|
156
170
|
FROM lattice_projects
|
|
157
|
-
WHERE
|
|
171
|
+
WHERE ${conditions.join(" AND ")}
|
|
158
172
|
ORDER BY created_at DESC
|
|
159
173
|
`,
|
|
160
|
-
|
|
174
|
+
values
|
|
161
175
|
);
|
|
162
176
|
|
|
163
177
|
return result.rows.map(this.mapRowToProject);
|
|
@@ -176,11 +190,12 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
176
190
|
name: string;
|
|
177
191
|
description: string | null;
|
|
178
192
|
config: unknown | null;
|
|
193
|
+
kind: string | null;
|
|
179
194
|
created_at: Date;
|
|
180
195
|
updated_at: Date;
|
|
181
196
|
}>(
|
|
182
197
|
`
|
|
183
|
-
SELECT id, tenant_id, workspace_id, name, description, config, created_at, updated_at
|
|
198
|
+
SELECT id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at
|
|
184
199
|
FROM lattice_projects
|
|
185
200
|
WHERE id = $1 AND tenant_id = $2
|
|
186
201
|
`,
|
|
@@ -206,19 +221,21 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
206
221
|
await this.ensureInitialized();
|
|
207
222
|
|
|
208
223
|
const now = new Date();
|
|
224
|
+
const kind = data.kind || "business";
|
|
209
225
|
|
|
210
226
|
await this.pool.query(
|
|
211
227
|
`
|
|
212
|
-
INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, created_at, updated_at)
|
|
213
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
228
|
+
INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
|
|
229
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
214
230
|
ON CONFLICT (id, tenant_id) DO UPDATE SET
|
|
215
231
|
workspace_id = EXCLUDED.workspace_id,
|
|
216
232
|
name = EXCLUDED.name,
|
|
217
233
|
description = EXCLUDED.description,
|
|
218
234
|
config = EXCLUDED.config,
|
|
235
|
+
kind = EXCLUDED.kind,
|
|
219
236
|
updated_at = EXCLUDED.updated_at
|
|
220
237
|
`,
|
|
221
|
-
[id, tenantId, workspaceId, data.name, data.description || null, data.config || null, now, now]
|
|
238
|
+
[id, tenantId, workspaceId, data.name, data.description || null, data.config || null, kind, now, now]
|
|
222
239
|
);
|
|
223
240
|
|
|
224
241
|
return {
|
|
@@ -228,6 +245,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
228
245
|
name: data.name,
|
|
229
246
|
description: data.description,
|
|
230
247
|
config: data.config,
|
|
248
|
+
kind,
|
|
231
249
|
createdAt: now,
|
|
232
250
|
updatedAt: now,
|
|
233
251
|
};
|
|
@@ -273,6 +291,11 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
273
291
|
updateValues.push(updates.config || null);
|
|
274
292
|
}
|
|
275
293
|
|
|
294
|
+
if (updates.kind !== undefined) {
|
|
295
|
+
updateFields.push(`kind = $${paramIndex++}`);
|
|
296
|
+
updateValues.push(updates.kind);
|
|
297
|
+
}
|
|
298
|
+
|
|
276
299
|
if (updateFields.length === 0) {
|
|
277
300
|
// No fields to update
|
|
278
301
|
return existing;
|