@axiom-lattice/pg-stores 3.1.0 → 3.1.2
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 +18 -0
- package/dist/index.d.mts +283 -23
- package/dist/index.d.ts +283 -23
- package/dist/index.js +1867 -224
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1854 -217
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/ChannelBindingStore.test.ts +122 -0
- package/src/__tests__/PostgreSQLCapabilityBundleStore.migrations.test.ts +67 -0
- package/src/__tests__/PostgreSQLCapabilityBundleStore.test.ts +383 -0
- package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +16 -0
- package/src/__tests__/PostgreSQLProjectBotMembershipStore.test.ts +117 -0
- package/src/__tests__/PostgreSQLProjectMembershipStore.test.ts +235 -0
- package/src/__tests__/PostgreSQLProjectRoomMessageStore.test.ts +103 -0
- package/src/__tests__/PostgreSQLProjectRoomStore.migrations.test.ts +114 -0
- package/src/__tests__/PostgreSQLProjectRoomStore.test.ts +48 -0
- package/src/__tests__/PostgreSQLTaskStore.integration.test.ts +27 -0
- package/src/__tests__/PostgreSQLTaskStore.test.ts +57 -0
- package/src/__tests__/PostgreSQLTaskWorkItemStore.integration.test.ts +107 -0
- package/src/__tests__/PostgreSQLTaskWorkItemStore.migrations.test.ts +62 -0
- package/src/__tests__/PostgreSQLTaskWorkItemStore.test.ts +162 -1
- package/src/__tests__/ThreadMessageQueueStore.migrations.test.ts +63 -0
- package/src/__tests__/ThreadMessageQueueStore.test.ts +209 -4
- package/src/__tests__/add_workspace_project_to_queue.test.ts +15 -0
- package/src/__tests__/migration-name-version-compatibility.test.ts +67 -0
- package/src/__tests__/task-files.test.ts +4 -3
- package/src/createPgStoreConfig.ts +25 -1
- package/src/index.ts +13 -0
- package/src/migrations/add_trusted_run_context_column.ts +18 -0
- package/src/migrations/capability_bundle_migration.ts +20 -0
- package/src/migrations/migration.ts +2 -1
- package/src/migrations/project_room_migration.ts +128 -0
- package/src/migrations/task_migration.ts +15 -0
- package/src/migrations/task_work_items_migration.ts +45 -1
- package/src/stores/ChannelBindingStore.ts +99 -59
- package/src/stores/PostgreSQLCapabilityBundleStore.ts +306 -0
- package/src/stores/PostgreSQLChannelInstallationStore.ts +12 -30
- package/src/stores/PostgreSQLProjectBotMembershipStore.ts +199 -0
- package/src/stores/PostgreSQLProjectMembershipStore.ts +140 -0
- package/src/stores/PostgreSQLProjectRoomMessageStore.ts +177 -0
- package/src/stores/PostgreSQLProjectRoomStore.ts +57 -0
- package/src/stores/PostgreSQLProjectStore.ts +230 -50
- package/src/stores/PostgreSQLTaskStore.ts +89 -3
- package/src/stores/PostgreSQLTaskWorkItemStore.ts +198 -8
- package/src/stores/ThreadMessageQueueStore.ts +130 -32
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { ThreadMessageQueueStore } from '../stores/ThreadMessageQueueStore';
|
|
8
|
+
import type { AddMessageParams } from '@axiom-lattice/core';
|
|
8
9
|
|
|
9
10
|
// Mock pg Pool
|
|
10
11
|
const mockQuery = jest.fn();
|
|
@@ -23,6 +24,13 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
23
24
|
const mockThreadId = 'test-thread';
|
|
24
25
|
const mockTenantId = 'test-tenant';
|
|
25
26
|
const mockAssistantId = 'test-assistant';
|
|
27
|
+
const trusted = {
|
|
28
|
+
projectRoom: {
|
|
29
|
+
tenantId: mockTenantId, workspaceId: 'workspace', projectId: 'project', roomId: 'room',
|
|
30
|
+
sourceRoomMessageId: 'room-message', membershipId: 'membership', assistantId: mockAssistantId,
|
|
31
|
+
inputMessageId: 'input-1', role: 'coordinator' as const, title: 'Coordinator',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
26
34
|
|
|
27
35
|
beforeEach(() => {
|
|
28
36
|
store = new ThreadMessageQueueStore({
|
|
@@ -34,6 +42,102 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
34
42
|
});
|
|
35
43
|
|
|
36
44
|
describe('addMessage', () => {
|
|
45
|
+
it.each(['addMessage', 'addMessageIfCapacity', 'addMessageAtHead'] as const)(
|
|
46
|
+
'persists trusted metadata through %s',
|
|
47
|
+
async (method) => {
|
|
48
|
+
const row = {
|
|
49
|
+
id: 'input-1', message_content: JSON.stringify('message'), message_type: 'human',
|
|
50
|
+
sequence_order: 1, created_at: new Date(), trusted_run_context: trusted,
|
|
51
|
+
execution_mode: 'followup',
|
|
52
|
+
};
|
|
53
|
+
const params = {
|
|
54
|
+
threadId: mockThreadId, tenantId: mockTenantId, assistantId: mockAssistantId,
|
|
55
|
+
workspaceId: 'workspace', projectId: 'project', id: 'input-1', content: 'message',
|
|
56
|
+
trusted_run_context: trusted, execution_mode: 'followup' as const,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (method === 'addMessageIfCapacity') {
|
|
60
|
+
const client = {
|
|
61
|
+
query: jest.fn()
|
|
62
|
+
.mockResolvedValueOnce({}).mockResolvedValueOnce({})
|
|
63
|
+
.mockResolvedValueOnce({ rows: [{ count: '0' }] })
|
|
64
|
+
.mockResolvedValueOnce({ rows: [{ next_seq: 1 }] })
|
|
65
|
+
.mockResolvedValueOnce({ rows: [row] }).mockResolvedValueOnce({}),
|
|
66
|
+
release: jest.fn(),
|
|
67
|
+
};
|
|
68
|
+
mockConnect.mockResolvedValueOnce(client);
|
|
69
|
+
await store.addMessageIfCapacity(params, 10);
|
|
70
|
+
expect(client.query.mock.calls[4][0]).toEqual(expect.stringContaining('trusted_run_context'));
|
|
71
|
+
expect(client.query.mock.calls[4][1]).toEqual(expect.arrayContaining([JSON.stringify(trusted), 'followup']));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
mockQuery.mockResolvedValueOnce({ rows: [{ next_seq: 1 }] }).mockResolvedValueOnce({ rows: [row] });
|
|
76
|
+
await store[method](params);
|
|
77
|
+
expect(mockQuery).toHaveBeenNthCalledWith(2, expect.stringContaining('trusted_run_context'),
|
|
78
|
+
expect.arrayContaining([JSON.stringify(trusted), 'followup']));
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
it.each(['addMessage', 'addMessageIfCapacity', 'addMessageAtHead'] as const)(
|
|
82
|
+
'rejects malformed trusted context before querying through %s',
|
|
83
|
+
async (method) => {
|
|
84
|
+
const params = {
|
|
85
|
+
threadId: mockThreadId, tenantId: mockTenantId, assistantId: mockAssistantId, content: 'message',
|
|
86
|
+
trusted_run_context: { projectRoom: { tenantId: mockTenantId } },
|
|
87
|
+
} as unknown as AddMessageParams;
|
|
88
|
+
|
|
89
|
+
const operation = method === 'addMessageIfCapacity'
|
|
90
|
+
? store.addMessageIfCapacity(params, 10)
|
|
91
|
+
: store[method](params);
|
|
92
|
+
await expect(operation).rejects.toThrow('Invalid trusted agent run context');
|
|
93
|
+
expect(mockQuery).not.toHaveBeenCalled();
|
|
94
|
+
expect(mockConnect).not.toHaveBeenCalled();
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
it.each(['addMessage', 'addMessageIfCapacity', 'addMessageAtHead'] as const)(
|
|
99
|
+
'rejects invalid execution mode before querying through %s',
|
|
100
|
+
async (method) => {
|
|
101
|
+
const params: AddMessageParams = {
|
|
102
|
+
threadId: mockThreadId, tenantId: mockTenantId, assistantId: mockAssistantId, content: 'message',
|
|
103
|
+
execution_mode: 'steer' as unknown as AddMessageParams['execution_mode'],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const operation = method === 'addMessageIfCapacity'
|
|
107
|
+
? store.addMessageIfCapacity(params, 10)
|
|
108
|
+
: store[method](params);
|
|
109
|
+
await expect(operation).rejects.toThrow('Invalid queued execution mode');
|
|
110
|
+
expect(mockQuery).not.toHaveBeenCalled();
|
|
111
|
+
expect(mockConnect).not.toHaveBeenCalled();
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
it('atomically rejects enqueue after the scoped pending capacity is reached', async () => {
|
|
115
|
+
const client = {
|
|
116
|
+
query: jest.fn()
|
|
117
|
+
.mockResolvedValueOnce({})
|
|
118
|
+
.mockResolvedValueOnce({})
|
|
119
|
+
.mockResolvedValueOnce({ rows: [{ count: '2' }] })
|
|
120
|
+
.mockResolvedValueOnce({}),
|
|
121
|
+
release: jest.fn(),
|
|
122
|
+
};
|
|
123
|
+
mockConnect.mockResolvedValueOnce(client);
|
|
124
|
+
|
|
125
|
+
await expect(store.addMessageIfCapacity({
|
|
126
|
+
threadId: mockThreadId,
|
|
127
|
+
tenantId: mockTenantId,
|
|
128
|
+
assistantId: mockAssistantId,
|
|
129
|
+
workspaceId: null,
|
|
130
|
+
projectId: null,
|
|
131
|
+
content: 'full',
|
|
132
|
+
}, 2)).resolves.toBe(false);
|
|
133
|
+
|
|
134
|
+
expect(client.query).toHaveBeenCalledWith(
|
|
135
|
+
'SELECT pg_advisory_xact_lock(hashtext($1))',
|
|
136
|
+
[`${mockTenantId}:${mockAssistantId}:${mockThreadId}::`],
|
|
137
|
+
);
|
|
138
|
+
expect(client.release).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
37
141
|
it('should add message to queue', async () => {
|
|
38
142
|
const mockMessage = {
|
|
39
143
|
id: 'msg-1',
|
|
@@ -131,7 +235,6 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
131
235
|
it('should insert message with high priority', async () => {
|
|
132
236
|
// Mock thread info query
|
|
133
237
|
mockQuery
|
|
134
|
-
.mockResolvedValueOnce({ rows: [{ tenant_id: mockTenantId, assistant_id: mockAssistantId }] }) // Get thread info
|
|
135
238
|
.mockResolvedValueOnce({ rows: [{ next_seq: 1 }] }) // Get next sequence
|
|
136
239
|
.mockResolvedValueOnce({
|
|
137
240
|
rows: [{
|
|
@@ -158,7 +261,6 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
158
261
|
|
|
159
262
|
it('should use default tenant/assistant when no existing messages', async () => {
|
|
160
263
|
mockQuery
|
|
161
|
-
.mockResolvedValueOnce({ rows: [] }) // No existing messages
|
|
162
264
|
.mockResolvedValueOnce({ rows: [{ next_seq: 0 }] }) // Get next sequence
|
|
163
265
|
.mockResolvedValueOnce({
|
|
164
266
|
rows: [{
|
|
@@ -259,6 +361,79 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
259
361
|
expect(result[0].custom_run_config).toEqual(customRunConfig);
|
|
260
362
|
expect((result[0].content as { command?: unknown }).command).toBeUndefined();
|
|
261
363
|
});
|
|
364
|
+
|
|
365
|
+
it('hydrates trusted metadata for pending and processing rows', async () => {
|
|
366
|
+
const row = {
|
|
367
|
+
id: 'input-1', message_content: JSON.stringify('message'), message_type: 'human',
|
|
368
|
+
sequence_order: 1, created_at: new Date(), trusted_run_context: JSON.stringify(trusted),
|
|
369
|
+
execution_mode: 'followup',
|
|
370
|
+
};
|
|
371
|
+
mockQuery.mockResolvedValueOnce({ rows: [row] }).mockResolvedValueOnce({ rows: [row] });
|
|
372
|
+
|
|
373
|
+
expect((await store.getPendingMessages(mockThreadId))[0]).toMatchObject({
|
|
374
|
+
trusted_run_context: trusted, execution_mode: 'followup',
|
|
375
|
+
});
|
|
376
|
+
expect((await store.getProcessingMessages(mockThreadId))[0]).toMatchObject({
|
|
377
|
+
trusted_run_context: trusted, execution_mode: 'followup',
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('rejects hostile trusted JSON read from PostgreSQL', async () => {
|
|
382
|
+
mockQuery.mockResolvedValueOnce({ rows: [{
|
|
383
|
+
id: 'hostile', message_content: JSON.stringify('message'), message_type: 'human',
|
|
384
|
+
sequence_order: 1, created_at: new Date(),
|
|
385
|
+
trusted_run_context: { projectRoom: { tenantId: mockTenantId } }, execution_mode: 'followup',
|
|
386
|
+
}] });
|
|
387
|
+
|
|
388
|
+
await expect(store.getPendingMessages(mockThreadId)).rejects.toThrow('Invalid trusted agent run context');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('matches nullish dimensions as NULL when a scope is supplied', async () => {
|
|
392
|
+
mockQuery.mockResolvedValueOnce({ rows: [] });
|
|
393
|
+
|
|
394
|
+
await store.getPendingMessages(mockThreadId, {
|
|
395
|
+
tenantId: mockTenantId,
|
|
396
|
+
assistantId: mockAssistantId,
|
|
397
|
+
workspaceId: undefined,
|
|
398
|
+
projectId: undefined,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
402
|
+
expect.stringMatching(/tenant_id = \$2.*assistant_id = \$3.*workspace_id IS NULL.*project_id IS NULL/s),
|
|
403
|
+
[mockThreadId, mockTenantId, mockAssistantId],
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('applies null-sensitive full scope predicates to every scoped operation', async () => {
|
|
408
|
+
const scope = {
|
|
409
|
+
tenantId: mockTenantId,
|
|
410
|
+
assistantId: mockAssistantId,
|
|
411
|
+
workspaceId: undefined,
|
|
412
|
+
projectId: undefined,
|
|
413
|
+
};
|
|
414
|
+
mockQuery
|
|
415
|
+
.mockResolvedValueOnce({ rows: [] })
|
|
416
|
+
.mockResolvedValueOnce({ rows: [] })
|
|
417
|
+
.mockResolvedValueOnce({ rows: [{ count: '0' }] })
|
|
418
|
+
.mockResolvedValueOnce({ rowCount: 0 })
|
|
419
|
+
.mockResolvedValueOnce({ rowCount: 0 })
|
|
420
|
+
.mockResolvedValueOnce({ rowCount: 0 })
|
|
421
|
+
.mockResolvedValueOnce({ rowCount: 0 });
|
|
422
|
+
|
|
423
|
+
await store.getProcessingMessages(mockThreadId, scope);
|
|
424
|
+
await store.getPendingMessages(mockThreadId, scope);
|
|
425
|
+
await store.getQueueSize(mockThreadId, scope);
|
|
426
|
+
await store.markProcessing('msg-1', undefined, scope);
|
|
427
|
+
await store.resetProcessingToPending(mockThreadId, scope);
|
|
428
|
+
await store.removeMessage('msg-1', scope);
|
|
429
|
+
await store.clearMessages(mockThreadId, scope);
|
|
430
|
+
|
|
431
|
+
expect(mockQuery).toHaveBeenCalledTimes(7);
|
|
432
|
+
for (const [sql, params] of mockQuery.mock.calls as Array<[string, unknown[]]>) {
|
|
433
|
+
expect(sql).toMatch(/tenant_id = \$2.*assistant_id = \$3.*workspace_id IS NULL.*project_id IS NULL/s);
|
|
434
|
+
expect(params).toEqual(expect.arrayContaining([mockTenantId, mockAssistantId]));
|
|
435
|
+
}
|
|
436
|
+
});
|
|
262
437
|
});
|
|
263
438
|
|
|
264
439
|
describe('getQueueSize', () => {
|
|
@@ -296,6 +471,26 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
296
471
|
expect(result[1].threadId).toBe('thread-2');
|
|
297
472
|
expect(result[2].threadId).toBe('thread-3');
|
|
298
473
|
});
|
|
474
|
+
|
|
475
|
+
it('preserves NULL workspace and project identity for legacy rows', async () => {
|
|
476
|
+
mockQuery.mockResolvedValueOnce({
|
|
477
|
+
rows: [{
|
|
478
|
+
tenant_id: mockTenantId,
|
|
479
|
+
assistant_id: mockAssistantId,
|
|
480
|
+
thread_id: mockThreadId,
|
|
481
|
+
workspace_id: null,
|
|
482
|
+
project_id: null,
|
|
483
|
+
}],
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
await expect(store.getThreadsWithPendingMessages()).resolves.toEqual([{
|
|
487
|
+
tenantId: mockTenantId,
|
|
488
|
+
assistantId: mockAssistantId,
|
|
489
|
+
threadId: mockThreadId,
|
|
490
|
+
workspaceId: null,
|
|
491
|
+
projectId: null,
|
|
492
|
+
}]);
|
|
493
|
+
});
|
|
299
494
|
});
|
|
300
495
|
|
|
301
496
|
describe('removeMessage', () => {
|
|
@@ -323,10 +518,20 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
323
518
|
await store.markProcessing('msg-1');
|
|
324
519
|
|
|
325
520
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
326
|
-
expect.stringContaining("UPDATE
|
|
521
|
+
expect.stringContaining("UPDATE lattice_thread_message_queue"),
|
|
327
522
|
['msg-1']
|
|
328
523
|
);
|
|
329
524
|
});
|
|
525
|
+
|
|
526
|
+
it('updates only status and custom config, preserving trusted columns', async () => {
|
|
527
|
+
mockQuery.mockResolvedValueOnce({ rowCount: 1 });
|
|
528
|
+
await store.markProcessing('input-1', { model: 'updated' });
|
|
529
|
+
|
|
530
|
+
const sql = mockQuery.mock.calls[0][0] as string;
|
|
531
|
+
expect(sql).toContain("status = 'processing', custom_run_config = $2");
|
|
532
|
+
expect(sql).not.toContain('trusted_run_context =');
|
|
533
|
+
expect(sql).not.toContain('execution_mode =');
|
|
534
|
+
});
|
|
330
535
|
});
|
|
331
536
|
|
|
332
537
|
describe('clearMessages', () => {
|
|
@@ -336,7 +541,7 @@ describe('ThreadMessageQueueStore', () => {
|
|
|
336
541
|
await store.clearMessages(mockThreadId);
|
|
337
542
|
|
|
338
543
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
339
|
-
expect.stringContaining("DELETE FROM
|
|
544
|
+
expect.stringContaining("DELETE FROM lattice_thread_message_queue"),
|
|
340
545
|
[mockThreadId]
|
|
341
546
|
);
|
|
342
547
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PoolClient } from "pg";
|
|
2
|
+
import { addWorkspaceProjectToQueue } from "../migrations/add_workspace_project_to_queue";
|
|
3
|
+
|
|
4
|
+
describe("addWorkspaceProjectToQueue", () => {
|
|
5
|
+
it("adds nullable scope columns so legacy rows retain NULL identity", async () => {
|
|
6
|
+
const query = jest.fn().mockResolvedValue({ rows: [] });
|
|
7
|
+
|
|
8
|
+
await addWorkspaceProjectToQueue.up({ query } as unknown as PoolClient);
|
|
9
|
+
|
|
10
|
+
const sql = query.mock.calls.map(([statement]) => statement as string).join("\n");
|
|
11
|
+
expect(sql).toContain("ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(255)");
|
|
12
|
+
expect(sql).toContain("ADD COLUMN IF NOT EXISTS project_id VARCHAR(255)");
|
|
13
|
+
expect(sql).not.toMatch(/SET\s+DEFAULT|NOT\s+NULL|UPDATE\s+lattice_thread_message_queue/i);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
2
|
+
import type { Pool, PoolClient } from "pg";
|
|
3
|
+
import { addTrustedRunContextColumn } from "../migrations/add_trusted_run_context_column";
|
|
4
|
+
import { MigrationManager, type Migration } from "../migrations/migration";
|
|
5
|
+
import { createProjectRoomTables } from "../migrations/project_room_migration";
|
|
6
|
+
import { addTaskDependenciesGinIndex } from "../migrations/task_migration";
|
|
7
|
+
import {
|
|
8
|
+
addProjectLifecycleEventIndex,
|
|
9
|
+
addTaskWorkItemPendingIndexesMigration,
|
|
10
|
+
} from "../migrations/task_work_items_migration";
|
|
11
|
+
|
|
12
|
+
describe("migration name and version compatibility", () => {
|
|
13
|
+
it("skips renumbered feature migrations already applied under their stable names", async () => {
|
|
14
|
+
const previouslyApplied = [
|
|
15
|
+
{ name: "create_project_room_tables", version: 171, applied_at: new Date() },
|
|
16
|
+
{ name: "add_thread_queue_trusted_run_context", version: 172, applied_at: new Date() },
|
|
17
|
+
{ name: "add_task_dependencies_gin_index", version: 173, applied_at: new Date() },
|
|
18
|
+
{ name: "add_project_lifecycle_event_index", version: 174, applied_at: new Date() },
|
|
19
|
+
];
|
|
20
|
+
const executedUpNames: string[] = [];
|
|
21
|
+
const insertedNames: string[] = [];
|
|
22
|
+
const release = jest.fn();
|
|
23
|
+
const query = jest.fn(async (sql: string, params?: unknown[]) => {
|
|
24
|
+
if (sql.includes("information_schema.tables")) return { rows: [{ exists: true }] };
|
|
25
|
+
if (sql.includes("pg_index")) {
|
|
26
|
+
return { rows: [{ column_name: "name", constraint_name: "lattice_schema_migrations_pkey" }] };
|
|
27
|
+
}
|
|
28
|
+
if (sql === "SELECT name, version, applied_at FROM lattice_schema_migrations ORDER BY version") {
|
|
29
|
+
return { rows: previouslyApplied };
|
|
30
|
+
}
|
|
31
|
+
if (sql.includes("INSERT INTO lattice_schema_migrations")) {
|
|
32
|
+
insertedNames.push(params?.[0] as string);
|
|
33
|
+
}
|
|
34
|
+
return { rows: [] };
|
|
35
|
+
});
|
|
36
|
+
const client = { query, release } as unknown as PoolClient;
|
|
37
|
+
const pool = { connect: jest.fn(async () => client) } as unknown as Pool;
|
|
38
|
+
const manager = new MigrationManager(pool);
|
|
39
|
+
const migrations = [
|
|
40
|
+
addTaskWorkItemPendingIndexesMigration,
|
|
41
|
+
createProjectRoomTables,
|
|
42
|
+
addTrustedRunContextColumn,
|
|
43
|
+
addTaskDependenciesGinIndex,
|
|
44
|
+
addProjectLifecycleEventIndex,
|
|
45
|
+
];
|
|
46
|
+
for (const migration of migrations) {
|
|
47
|
+
const wrapped: Migration = {
|
|
48
|
+
...migration,
|
|
49
|
+
up: async (migrationClient) => {
|
|
50
|
+
executedUpNames.push(migration.name);
|
|
51
|
+
await migration.up(migrationClient);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
manager.register(wrapped);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await manager.migrate();
|
|
58
|
+
|
|
59
|
+
expect(migrations.map(({ version }) => version).every(
|
|
60
|
+
(version, index, versions) => index === 0 || version > versions[index - 1],
|
|
61
|
+
)).toBe(true);
|
|
62
|
+
expect(executedUpNames).toEqual(["add_task_work_item_pending_indexes"]);
|
|
63
|
+
expect(insertedNames).toEqual(["add_task_work_item_pending_indexes"]);
|
|
64
|
+
expect(insertedNames).not.toEqual(expect.arrayContaining(previouslyApplied.map(({ name }) => name)));
|
|
65
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -13,9 +13,10 @@ describe("add_files_to_tasks migration", () => {
|
|
|
13
13
|
expect(taskMigrations.map((m) => m.name)).toContain("add_files_to_tasks");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
it("has a version greater than
|
|
17
|
-
const
|
|
18
|
-
|
|
16
|
+
it("has a version greater than the task migrations registered before it", () => {
|
|
17
|
+
const migrationIndex = taskMigrations.indexOf(addFilesToTasks);
|
|
18
|
+
const precedingVersions = taskMigrations.slice(0, migrationIndex).map((m) => m.version);
|
|
19
|
+
expect(addFilesToTasks.version).toBeGreaterThan(Math.max(...precedingVersions));
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
it("emits ADD COLUMN files JSONB in up()", async () => {
|
|
@@ -62,6 +62,7 @@ import { addPriorityAndCommandColumns } from "./migrations/add_priority_command_
|
|
|
62
62
|
import { addCustomRunConfigColumn } from "./migrations/add_custom_run_config_column";
|
|
63
63
|
import { alterMessageQueueIdColumn } from "./migrations/alter_message_queue_id_column";
|
|
64
64
|
import { addWorkspaceProjectToQueue } from "./migrations/add_workspace_project_to_queue";
|
|
65
|
+
import { addTrustedRunContextColumn } from "./migrations/add_trusted_run_context_column";
|
|
65
66
|
import { createWorkflowTrackingTables, addStepThreadId, addWorkflowRunsTenantStatusUpdatedIndex } from "./migrations/workflow_tracking_migrations";
|
|
66
67
|
import { evalMigrations } from "./migrations/eval_migrations";
|
|
67
68
|
import { createA2AApiKeysTable } from "./migrations/a2a_api_key_migration";
|
|
@@ -71,8 +72,21 @@ import { createMenuItemsTable } from "./migrations/menu_items_migration";
|
|
|
71
72
|
import { addFileContentType } from "./migrations/menu_items_add_file_type";
|
|
72
73
|
import { createSharedResourcesTable } from "./migrations/shared_resources_migration";
|
|
73
74
|
import { createCollectionsTable } from "./migrations/collection_migrations";
|
|
74
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
createTaskWorkItemsMigration,
|
|
77
|
+
addWorkItemProjectFieldsMigration,
|
|
78
|
+
addTaskWorkItemEventKeyMigration,
|
|
79
|
+
addTaskWorkItemPendingIndexesMigration,
|
|
80
|
+
addProjectLifecycleEventIndex,
|
|
81
|
+
} from "./migrations/task_work_items_migration";
|
|
75
82
|
import { createAgentWebAppsTable } from "./migrations/agent_web_apps_migration";
|
|
83
|
+
import { createCapabilityBundlesTable } from "./migrations/capability_bundle_migration";
|
|
84
|
+
import { createProjectRoomTables } from "./migrations/project_room_migration";
|
|
85
|
+
import { PostgreSQLCapabilityBundleStore } from "./stores/PostgreSQLCapabilityBundleStore";
|
|
86
|
+
import { PostgreSQLProjectRoomStore } from "./stores/PostgreSQLProjectRoomStore";
|
|
87
|
+
import { PostgreSQLProjectMembershipStore } from "./stores/PostgreSQLProjectMembershipStore";
|
|
88
|
+
import { PostgreSQLProjectBotMembershipStore } from "./stores/PostgreSQLProjectBotMembershipStore";
|
|
89
|
+
import { PostgreSQLProjectRoomMessageStore } from "./stores/PostgreSQLProjectRoomMessageStore";
|
|
76
90
|
|
|
77
91
|
export async function createPgStoreConfig(connectionString: string) {
|
|
78
92
|
const pool = new Pool({ connectionString });
|
|
@@ -134,6 +148,11 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
134
148
|
mm.register(addA2AKeyAssistantIds); // v167
|
|
135
149
|
mm.register(addTaskWorkItemEventKeyMigration); // v168
|
|
136
150
|
mm.register(createAgentWebAppsTable); // v169
|
|
151
|
+
mm.register(createCapabilityBundlesTable); // v170
|
|
152
|
+
mm.register(addTaskWorkItemPendingIndexesMigration); // v171
|
|
153
|
+
mm.register(createProjectRoomTables); // v172
|
|
154
|
+
mm.register(addTrustedRunContextColumn); // v173
|
|
155
|
+
mm.register(addProjectLifecycleEventIndex); // v175
|
|
137
156
|
|
|
138
157
|
await mm.migrate();
|
|
139
158
|
|
|
@@ -167,10 +186,15 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
167
186
|
taskWorkItem: taskWorkItemStore,
|
|
168
187
|
a2aApiKey: new PostgreSQLA2AApiKeyStore(opts),
|
|
169
188
|
agentWebApp: new PostgreSQLAgentWebAppStore(opts),
|
|
189
|
+
capabilityBundle: new PostgreSQLCapabilityBundleStore(opts),
|
|
170
190
|
schedule: new PostgreSQLScheduleStorage(opts),
|
|
171
191
|
menu: new MenuStore(opts),
|
|
172
192
|
sharedResource: new PostgresSharedResourceStore(opts),
|
|
173
193
|
collection: new PostgreSQLCollectionStore(opts),
|
|
194
|
+
projectRoom: new PostgreSQLProjectRoomStore(opts),
|
|
195
|
+
projectMembership: new PostgreSQLProjectMembershipStore(opts),
|
|
196
|
+
projectBotMembership: new PostgreSQLProjectBotMembershipStore(opts),
|
|
197
|
+
projectRoomMessage: new PostgreSQLProjectRoomMessageStore(opts),
|
|
174
198
|
vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
|
|
175
199
|
checkpoint,
|
|
176
200
|
};
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,9 @@ export * from "./stores/ChannelIdentityMappingStore";
|
|
|
58
58
|
export * from "./stores/PostgreSQLChannelInstallationStore";
|
|
59
59
|
export * from "./stores/PostgreSQLA2AApiKeyStore";
|
|
60
60
|
export * from "./stores/PostgreSQLAgentWebAppStore";
|
|
61
|
+
export * from "./stores/PostgreSQLTaskWorkItemStore";
|
|
62
|
+
export * from "./stores/PostgreSQLCapabilityBundleStore";
|
|
63
|
+
export * from "./migrations/capability_bundle_migration";
|
|
61
64
|
export * from "./stores/PostgreSQLWorkflowTrackingStore";
|
|
62
65
|
export * from "./stores/PostgreSQLEvalStore";
|
|
63
66
|
export * from "./migrations/eval_migrations";
|
|
@@ -68,6 +71,11 @@ export * from "./stores/MenuStore";
|
|
|
68
71
|
export * from "./stores/SharedResourceStore";
|
|
69
72
|
export * from "./stores/PostgresSharedResourceStore";
|
|
70
73
|
export * from "./stores/PostgreSQLCollectionStore";
|
|
74
|
+
export * from "./stores/PostgreSQLProjectRoomStore";
|
|
75
|
+
export * from "./stores/PostgreSQLProjectMembershipStore";
|
|
76
|
+
export * from "./stores/PostgreSQLProjectBotMembershipStore";
|
|
77
|
+
export * from "./stores/PostgreSQLProjectRoomMessageStore";
|
|
78
|
+
export * from "./migrations/project_room_migration";
|
|
71
79
|
|
|
72
80
|
export * from "./PGVectorStoreProvider";
|
|
73
81
|
|
|
@@ -125,6 +133,7 @@ export type {
|
|
|
125
133
|
CreateMenuItemInput,
|
|
126
134
|
ChannelInstallationStore,
|
|
127
135
|
ChannelInstallation,
|
|
136
|
+
CreateChannelInstallationInput,
|
|
128
137
|
CreateChannelInstallationRequest,
|
|
129
138
|
UpdateChannelInstallationRequest,
|
|
130
139
|
LarkChannelInstallationConfig,
|
|
@@ -191,4 +200,8 @@ export type {
|
|
|
191
200
|
AgentWebAppAppearance,
|
|
192
201
|
CreateAgentWebAppInput,
|
|
193
202
|
UpdateAgentWebAppInput,
|
|
203
|
+
CapabilityBundleStore,
|
|
204
|
+
CapabilityBundle,
|
|
205
|
+
CreateCapabilityBundleInput,
|
|
206
|
+
UpdateCapabilityBundleInput,
|
|
194
207
|
} from "@axiom-lattice/protocols";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Migration } from "./migration";
|
|
2
|
+
|
|
3
|
+
/** Adds durable privileged context and execution mode to queued thread messages. */
|
|
4
|
+
export const addTrustedRunContextColumn: Migration = {
|
|
5
|
+
version: 173,
|
|
6
|
+
name: "add_thread_queue_trusted_run_context",
|
|
7
|
+
up: async (client): Promise<void> => {
|
|
8
|
+
await client.query(`ALTER TABLE lattice_thread_message_queue
|
|
9
|
+
ADD COLUMN IF NOT EXISTS trusted_run_context JSONB,
|
|
10
|
+
ADD COLUMN IF NOT EXISTS execution_mode VARCHAR(20)
|
|
11
|
+
CHECK (execution_mode IS NULL OR execution_mode = 'followup')`);
|
|
12
|
+
},
|
|
13
|
+
down: async (client): Promise<void> => {
|
|
14
|
+
await client.query(`ALTER TABLE lattice_thread_message_queue
|
|
15
|
+
DROP COLUMN IF EXISTS execution_mode,
|
|
16
|
+
DROP COLUMN IF EXISTS trusted_run_context`);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Migration } from "./migration";
|
|
2
|
+
|
|
3
|
+
/** Creates the tenant-scoped capability bundle table. */
|
|
4
|
+
export const createCapabilityBundlesTable: Migration = {
|
|
5
|
+
version: 170,
|
|
6
|
+
name: "create_capability_bundles_table",
|
|
7
|
+
up: async (client) => {
|
|
8
|
+
await client.query(`CREATE TABLE IF NOT EXISTS lattice_capability_bundles (
|
|
9
|
+
id UUID PRIMARY KEY, tenant_id VARCHAR(255) NOT NULL, bundle_key VARCHAR(255) NOT NULL,
|
|
10
|
+
name VARCHAR(255) NOT NULL, description TEXT, capabilities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
11
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
12
|
+
UNIQUE (tenant_id, bundle_key)
|
|
13
|
+
)`);
|
|
14
|
+
await client.query("CREATE INDEX IF NOT EXISTS idx_lattice_capability_bundles_tenant ON lattice_capability_bundles(tenant_id)");
|
|
15
|
+
},
|
|
16
|
+
down: async (client) => {
|
|
17
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_capability_bundles_tenant");
|
|
18
|
+
await client.query("DROP TABLE IF EXISTS lattice_capability_bundles");
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -83,7 +83,8 @@ export class MigrationManager {
|
|
|
83
83
|
const tableExists = await client.query(`
|
|
84
84
|
SELECT EXISTS (
|
|
85
85
|
SELECT FROM information_schema.tables
|
|
86
|
-
WHERE
|
|
86
|
+
WHERE table_schema = current_schema()
|
|
87
|
+
AND table_name = 'lattice_schema_migrations'
|
|
87
88
|
)
|
|
88
89
|
`);
|
|
89
90
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Migration } from "./migration";
|
|
2
|
+
|
|
3
|
+
/** Creates the tenant-scoped project room, roster, and message tables. */
|
|
4
|
+
export const createProjectRoomTables: Migration = {
|
|
5
|
+
version: 172,
|
|
6
|
+
name: "create_project_room_tables",
|
|
7
|
+
up: async (client) => {
|
|
8
|
+
await client.query(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS lattice_project_rooms (
|
|
10
|
+
id VARCHAR(255) NOT NULL,
|
|
11
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
12
|
+
workspace_id VARCHAR(255) NOT NULL,
|
|
13
|
+
project_id VARCHAR(255) NOT NULL,
|
|
14
|
+
name VARCHAR(255) NOT NULL,
|
|
15
|
+
type VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_rooms_type
|
|
16
|
+
CHECK (type IN ('main')),
|
|
17
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
18
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
19
|
+
PRIMARY KEY (tenant_id, id),
|
|
20
|
+
UNIQUE (tenant_id, project_id, type)
|
|
21
|
+
)
|
|
22
|
+
`);
|
|
23
|
+
|
|
24
|
+
await client.query(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS lattice_project_memberships (
|
|
26
|
+
id VARCHAR(255) NOT NULL,
|
|
27
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
28
|
+
project_id VARCHAR(255) NOT NULL,
|
|
29
|
+
user_id VARCHAR(255) NOT NULL,
|
|
30
|
+
role VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_memberships_role
|
|
31
|
+
CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
|
|
32
|
+
status VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_memberships_status
|
|
33
|
+
CHECK (status IN ('active', 'removed')),
|
|
34
|
+
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
35
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
36
|
+
PRIMARY KEY (tenant_id, id),
|
|
37
|
+
UNIQUE (tenant_id, project_id, user_id)
|
|
38
|
+
)
|
|
39
|
+
`);
|
|
40
|
+
await client.query(`
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_project_memberships_project
|
|
42
|
+
ON lattice_project_memberships (tenant_id, project_id)
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
await client.query(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS lattice_project_bot_memberships (
|
|
47
|
+
id VARCHAR(255) NOT NULL,
|
|
48
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
49
|
+
workspace_id VARCHAR(255) NOT NULL,
|
|
50
|
+
project_id VARCHAR(255) NOT NULL,
|
|
51
|
+
room_id VARCHAR(255) NOT NULL,
|
|
52
|
+
assistant_id VARCHAR(255) NOT NULL,
|
|
53
|
+
role VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_bot_memberships_role
|
|
54
|
+
CHECK (role IN ('coordinator', 'specialist')),
|
|
55
|
+
title VARCHAR(255) NOT NULL,
|
|
56
|
+
responsibility TEXT,
|
|
57
|
+
mention_name VARCHAR(255) NOT NULL,
|
|
58
|
+
status VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_bot_memberships_status
|
|
59
|
+
CHECK (status IN ('active', 'paused', 'removed')),
|
|
60
|
+
room_thread_id VARCHAR(255) NOT NULL,
|
|
61
|
+
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
62
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
63
|
+
PRIMARY KEY (tenant_id, id),
|
|
64
|
+
UNIQUE (tenant_id, project_id, assistant_id)
|
|
65
|
+
)
|
|
66
|
+
`);
|
|
67
|
+
await client.query(`
|
|
68
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_bot_memberships_coordinator
|
|
69
|
+
ON lattice_project_bot_memberships (tenant_id, project_id)
|
|
70
|
+
WHERE role = 'coordinator' AND status IN ('active', 'paused')
|
|
71
|
+
`);
|
|
72
|
+
await client.query(`
|
|
73
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_bot_memberships_mention
|
|
74
|
+
ON lattice_project_bot_memberships (tenant_id, room_id, mention_name)
|
|
75
|
+
WHERE status IN ('active', 'paused')
|
|
76
|
+
`);
|
|
77
|
+
await client.query(`
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_project_bot_memberships_project
|
|
79
|
+
ON lattice_project_bot_memberships (tenant_id, project_id)
|
|
80
|
+
`);
|
|
81
|
+
await client.query(`
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_project_bot_memberships_room
|
|
83
|
+
ON lattice_project_bot_memberships (tenant_id, room_id)
|
|
84
|
+
`);
|
|
85
|
+
|
|
86
|
+
await client.query(`
|
|
87
|
+
CREATE TABLE IF NOT EXISTS lattice_project_room_messages (
|
|
88
|
+
id VARCHAR(255) NOT NULL,
|
|
89
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
90
|
+
workspace_id VARCHAR(255) NOT NULL,
|
|
91
|
+
project_id VARCHAR(255) NOT NULL,
|
|
92
|
+
room_id VARCHAR(255) NOT NULL,
|
|
93
|
+
author JSONB NOT NULL,
|
|
94
|
+
content JSONB NOT NULL,
|
|
95
|
+
mentions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
96
|
+
reply_to_message_id VARCHAR(255),
|
|
97
|
+
source VARCHAR(32) NOT NULL CONSTRAINT chk_lattice_project_room_messages_source
|
|
98
|
+
CHECK (source IN ('user', 'agent', 'task', 'routine', 'system')),
|
|
99
|
+
source_id VARCHAR(255),
|
|
100
|
+
idempotency_key VARCHAR(255),
|
|
101
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
102
|
+
PRIMARY KEY (tenant_id, id)
|
|
103
|
+
)
|
|
104
|
+
`);
|
|
105
|
+
await client.query(`
|
|
106
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_project_room_messages_idempotency
|
|
107
|
+
ON lattice_project_room_messages (tenant_id, room_id, idempotency_key)
|
|
108
|
+
WHERE idempotency_key IS NOT NULL
|
|
109
|
+
`);
|
|
110
|
+
await client.query(`
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_project_room_messages_room_created
|
|
112
|
+
ON lattice_project_room_messages (tenant_id, room_id, created_at DESC, id DESC)
|
|
113
|
+
`);
|
|
114
|
+
},
|
|
115
|
+
down: async (client) => {
|
|
116
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_project_room_messages_room_created");
|
|
117
|
+
await client.query("DROP INDEX IF EXISTS uq_lattice_project_room_messages_idempotency");
|
|
118
|
+
await client.query("DROP TABLE IF EXISTS lattice_project_room_messages");
|
|
119
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_project_bot_memberships_room");
|
|
120
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_project_bot_memberships_project");
|
|
121
|
+
await client.query("DROP INDEX IF EXISTS uq_lattice_project_bot_memberships_mention");
|
|
122
|
+
await client.query("DROP INDEX IF EXISTS uq_lattice_project_bot_memberships_coordinator");
|
|
123
|
+
await client.query("DROP TABLE IF EXISTS lattice_project_bot_memberships");
|
|
124
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_project_memberships_project");
|
|
125
|
+
await client.query("DROP TABLE IF EXISTS lattice_project_memberships");
|
|
126
|
+
await client.query("DROP TABLE IF EXISTS lattice_project_rooms");
|
|
127
|
+
},
|
|
128
|
+
};
|