@axiom-lattice/pg-stores 1.0.70 → 1.0.72

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.70",
3
+ "version": "1.0.72",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -20,10 +20,11 @@
20
20
  "author": "",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
+ "@langchain/langgraph-checkpoint-postgres": "1.0.0",
23
24
  "pg": "^8.16.3",
24
25
  "uuid": "^9.0.1",
25
- "@axiom-lattice/core": "2.1.79",
26
- "@axiom-lattice/protocols": "2.1.41"
26
+ "@axiom-lattice/core": "2.1.81",
27
+ "@axiom-lattice/protocols": "2.1.43"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/node": "^20.11.24",
@@ -36,7 +37,7 @@
36
37
  "scripts": {
37
38
  "build": "tsup src/index.ts --format cjs,esm --dts --sourcemap",
38
39
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
39
- "lint": "tsc --noEmit",
40
+ "lint": "tsc --noEmit && node scripts/check-migration-versions.mjs",
40
41
  "clean": "rimraf dist"
41
42
  }
42
43
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Check for duplicate migration version numbers.
3
+ * Exits with code 1 if any version collision is found.
4
+ * Run as part of lint pipeline.
5
+ */
6
+ import { readFileSync, readdirSync } from "node:fs";
7
+ import { join, dirname } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const MIGRATIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), "../src/migrations");
11
+
12
+ const files = readdirSync(MIGRATIONS_DIR).filter(f => f.endsWith(".ts"));
13
+ const versionMap = new Map();
14
+
15
+ let hasError = false;
16
+
17
+ for (const file of files) {
18
+ const content = readFileSync(join(MIGRATIONS_DIR, file), "utf-8");
19
+ const matches = content.matchAll(/^\s*version:\s*(\d+)/gm);
20
+ for (const m of matches) {
21
+ const version = parseInt(m[1]);
22
+ if (versionMap.has(version)) {
23
+ console.error(
24
+ `\x1b[31mMIGRATION VERSION CONFLICT:\x1b[0m v${version} in both "${versionMap.get(version)}" and "${file}"`
25
+ );
26
+ hasError = true;
27
+ } else {
28
+ versionMap.set(version, file);
29
+ }
30
+ }
31
+ }
32
+
33
+ if (hasError) {
34
+ process.exit(1);
35
+ }
36
+
37
+ console.log(`\x1b[32mMigration versions OK\x1b[0m (${versionMap.size} unique versions in ${files.length} files)`);
@@ -1,10 +1,3 @@
1
- /**
2
- * createPgStoreConfig
3
- *
4
- * Creates a map of all PG store instances from a single connection string,
5
- * ready to pass directly to configureStores().
6
- */
7
-
8
1
  import { PostgreSQLThreadStore } from "./stores/PostgreSQLThreadStore";
9
2
  import { PostgreSQLAssistantStore } from "./stores/PostgreSQLAssistantStore";
10
3
  import { PostgreSQLDatabaseConfigStore } from "./stores/PostgreSQLDatabaseConfigStore";
@@ -22,28 +15,38 @@ import { ChannelBindingStore } from "./stores/ChannelBindingStore";
22
15
  import { PostgreSQLChannelInstallationStore } from "./stores/PostgreSQLChannelInstallationStore";
23
16
  import { PostgreSQLA2AApiKeyStore } from "./stores/PostgreSQLA2AApiKeyStore";
24
17
  import { PostgreSQLScheduleStorage } from "./stores/PostgreSQLScheduleStorage";
18
+ import { PostgreSQLTaskStore } from "./stores/PostgreSQLTaskStore";
19
+ import { MenuStore } from "./stores/MenuStore";
20
+ import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
25
21
 
26
22
  export function createPgStoreConfig(connectionString: string) {
27
- const opts = { poolConfig: connectionString, autoMigrate: false } as const;
28
- const optsAuto = { poolConfig: connectionString, autoMigrate: true } as const;
23
+ const opts = { poolConfig: connectionString, autoMigrate: true } as const;
24
+
25
+ const checkpoint = PostgresSaver.fromConnString(connectionString);
26
+ checkpoint.setup().catch((err) => {
27
+ console.error("[pg-stores] Failed to setup checkpoint table:", err.message || err);
28
+ });
29
29
 
30
30
  return {
31
31
  workspace: new PostgreSQLWorkspaceStore(opts),
32
32
  project: new PostgreSQLProjectStore(opts),
33
- eval: new PostgreSQLEvalStore(optsAuto),
33
+ eval: new PostgreSQLEvalStore(opts),
34
34
  user: new PostgreSQLUserStore(opts),
35
35
  tenant: new PostgreSQLTenantStore(opts),
36
36
  userTenantLink: new PostgreSQLUserTenantLinkStore(opts),
37
- channelBinding: new ChannelBindingStore(optsAuto),
38
- channelInstallation: new PostgreSQLChannelInstallationStore(optsAuto),
37
+ channelBinding: new ChannelBindingStore(opts),
38
+ channelInstallation: new PostgreSQLChannelInstallationStore(opts),
39
39
  thread: new PostgreSQLThreadStore(opts),
40
40
  database: new PostgreSQLDatabaseConfigStore(opts),
41
41
  metrics: new PostgreSQLMetricsServerConfigStore(opts),
42
42
  mcp: new PostgreSQLMcpServerConfigStore(opts),
43
43
  assistant: new PostgreSQLAssistantStore(opts),
44
- workflowTracking: new PostgreSQLWorkflowTrackingStore(optsAuto),
45
- threadMessageQueue: new ThreadMessageQueueStore(optsAuto),
46
- a2aApiKey: new PostgreSQLA2AApiKeyStore(optsAuto),
44
+ workflowTracking: new PostgreSQLWorkflowTrackingStore(opts),
45
+ threadMessageQueue: new ThreadMessageQueueStore(opts),
46
+ task: new PostgreSQLTaskStore(opts),
47
+ a2aApiKey: new PostgreSQLA2AApiKeyStore(opts),
47
48
  schedule: new PostgreSQLScheduleStorage(opts),
49
+ menu: new MenuStore(opts),
50
+ checkpoint,
48
51
  };
49
- }
52
+ }
package/src/index.ts CHANGED
@@ -57,6 +57,8 @@ export * from "./stores/PostgreSQLA2AApiKeyStore";
57
57
  export * from "./stores/PostgreSQLWorkflowTrackingStore";
58
58
  export * from "./stores/PostgreSQLEvalStore";
59
59
  export * from "./migrations/eval_migrations";
60
+ export * from "./migrations/menu_items_migration";
61
+ export * from "./stores/MenuStore";
60
62
 
61
63
  // Re-export for convenience
62
64
  export { PostgreSQLThreadStore } from "./stores/PostgreSQLThreadStore";
@@ -72,6 +74,7 @@ export { PostgreSQLUserStore } from "./stores/PostgreSQLUserStore";
72
74
  export { PostgreSQLTenantStore } from "./stores/PostgreSQLTenantStore";
73
75
  export { PostgreSQLUserTenantLinkStore } from "./stores/PostgreSQLUserTenantLinkStore";
74
76
  export { ChannelBindingStore } from "./stores/ChannelBindingStore";
77
+ export { MenuStore } from "./stores/MenuStore";
75
78
  export { ChannelIdentityMappingStore } from "./stores/ChannelIdentityMappingStore";
76
79
  export { PostgreSQLChannelInstallationStore } from "./stores/PostgreSQLChannelInstallationStore";
77
80
  export { PostgreSQLA2AApiKeyStore } from "./stores/PostgreSQLA2AApiKeyStore";
@@ -102,6 +105,9 @@ export type {
102
105
  Binding,
103
106
  BindingRegistry,
104
107
  CreateBindingInput,
108
+ MenuItem,
109
+ MenuRegistry,
110
+ CreateMenuItemInput,
105
111
  ChannelInstallationStore,
106
112
  ChannelInstallation,
107
113
  CreateChannelInstallationRequest,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Add workspace_id and project_id columns to thread message queue
3
+ * so that thread recovery after server restart uses the correct scope.
4
+ */
5
+
6
+ import { PoolClient } from "pg";
7
+ import { Migration } from "./migration";
8
+
9
+ export const addWorkspaceProjectToQueue: Migration = {
10
+ version: 131,
11
+ name: "add_workspace_project_to_queue",
12
+ up: async (client: PoolClient) => {
13
+ await client.query(`
14
+ ALTER TABLE lattice_thread_message_queue
15
+ ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(255)
16
+ `);
17
+ await client.query(`
18
+ ALTER TABLE lattice_thread_message_queue
19
+ ADD COLUMN IF NOT EXISTS project_id VARCHAR(255)
20
+ `);
21
+ },
22
+ down: async (client: PoolClient) => {
23
+ await client.query(
24
+ "ALTER TABLE lattice_thread_message_queue DROP COLUMN IF EXISTS workspace_id"
25
+ );
26
+ await client.query(
27
+ "ALTER TABLE lattice_thread_message_queue DROP COLUMN IF EXISTS project_id"
28
+ );
29
+ },
30
+ };
@@ -0,0 +1,26 @@
1
+ import { PoolClient } from "pg";
2
+ import { Migration } from "./migration";
3
+
4
+ export const addAssistantOwnerUserId: Migration = {
5
+ version: 132,
6
+ name: "add_assistant_owner_user_id",
7
+ up: async (client: PoolClient) => {
8
+ await client.query(`
9
+ ALTER TABLE lattice_assistants
10
+ ADD COLUMN IF NOT EXISTS owner_user_id VARCHAR(255)
11
+ `);
12
+
13
+ await client.query(`
14
+ CREATE INDEX IF NOT EXISTS idx_lattice_assistants_owner_user_id
15
+ ON lattice_assistants(owner_user_id)
16
+ `);
17
+ },
18
+ down: async (client: PoolClient) => {
19
+ await client.query(
20
+ "DROP INDEX IF EXISTS idx_lattice_assistants_owner_user_id"
21
+ );
22
+ await client.query(
23
+ "ALTER TABLE lattice_assistants DROP COLUMN IF EXISTS owner_user_id"
24
+ );
25
+ },
26
+ };
@@ -0,0 +1,38 @@
1
+ import { PoolClient } from "pg";
2
+ import { Migration } from "./migration";
3
+
4
+ export const createMenuItemsTable: Migration = {
5
+ version: 134,
6
+ name: "create_menu_items_table",
7
+ up: async (client: PoolClient) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS lattice_menu_items (
10
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
11
+ tenant_id VARCHAR(255) NOT NULL,
12
+ menu_target VARCHAR(20) NOT NULL CHECK (menu_target IN ('sidebar', 'workspace')),
13
+ "group" VARCHAR(100),
14
+ name VARCHAR(255) NOT NULL,
15
+ icon VARCHAR(50),
16
+ sort_order INT NOT NULL DEFAULT 0,
17
+ content_type VARCHAR(20) NOT NULL CHECK (content_type IN ('agent', 'html', 'custom')),
18
+ content_config JSONB NOT NULL DEFAULT '{}',
19
+ enabled BOOLEAN NOT NULL DEFAULT true,
20
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
21
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
22
+ )
23
+ `);
24
+
25
+ await client.query(`
26
+ CREATE INDEX IF NOT EXISTS idx_menu_items_tenant_target
27
+ ON lattice_menu_items(tenant_id, menu_target)
28
+ `);
29
+
30
+ await client.query(`
31
+ CREATE INDEX IF NOT EXISTS idx_menu_items_sort
32
+ ON lattice_menu_items(tenant_id, menu_target, sort_order)
33
+ `);
34
+ },
35
+ down: async (client: PoolClient) => {
36
+ await client.query("DROP TABLE IF EXISTS lattice_menu_items");
37
+ },
38
+ };
@@ -0,0 +1,51 @@
1
+ import { PoolClient } from "pg";
2
+ import { Migration } from "./migration";
3
+
4
+ export const createTasksTable: Migration = {
5
+ version: 133,
6
+ name: "create_lattice_tasks_table",
7
+ up: async (client: PoolClient) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS lattice_tasks (
10
+ id VARCHAR(255) PRIMARY KEY,
11
+ tenant_id VARCHAR(255) NOT NULL,
12
+ owner_type VARCHAR(10) NOT NULL DEFAULT 'user',
13
+ owner_id VARCHAR(255) NOT NULL,
14
+ title VARCHAR(500) NOT NULL,
15
+ description TEXT,
16
+ status VARCHAR(20) NOT NULL DEFAULT 'pending',
17
+ priority VARCHAR(10) NOT NULL DEFAULT 'medium',
18
+ due_date VARCHAR(50),
19
+ metadata JSONB,
20
+ parent_id VARCHAR(255),
21
+ source_id VARCHAR(255),
22
+ context JSONB,
23
+ created_at VARCHAR(50) NOT NULL,
24
+ updated_at VARCHAR(50) NOT NULL
25
+ )
26
+ `);
27
+
28
+ await client.query(`
29
+ CREATE INDEX IF NOT EXISTS idx_lattice_tasks_tenant_id
30
+ ON lattice_tasks(tenant_id)
31
+ `);
32
+
33
+ await client.query(`
34
+ CREATE INDEX IF NOT EXISTS idx_lattice_tasks_owner
35
+ ON lattice_tasks(tenant_id, owner_type, owner_id)
36
+ `);
37
+
38
+ await client.query(`
39
+ CREATE INDEX IF NOT EXISTS idx_lattice_tasks_status
40
+ ON lattice_tasks(tenant_id, status)
41
+ `);
42
+
43
+ await client.query(`
44
+ CREATE INDEX IF NOT EXISTS idx_lattice_tasks_parent_id
45
+ ON lattice_tasks(tenant_id, parent_id)
46
+ `);
47
+ },
48
+ down: async (client: PoolClient) => {
49
+ await client.query("DROP TABLE IF EXISTS lattice_tasks");
50
+ },
51
+ };
@@ -77,3 +77,22 @@ export const createWorkflowTrackingTables: Migration = {
77
77
  `);
78
78
  },
79
79
  };
80
+
81
+ export const addStepThreadId: Migration = {
82
+ name: "add_step_thread_id",
83
+ version: 113,
84
+
85
+ up: async (client) => {
86
+ await client.query(`
87
+ ALTER TABLE lattice_workflow_steps
88
+ ADD COLUMN IF NOT EXISTS thread_id VARCHAR(255);
89
+ `);
90
+ },
91
+
92
+ down: async (client) => {
93
+ await client.query(`
94
+ ALTER TABLE lattice_workflow_steps
95
+ DROP COLUMN IF EXISTS thread_id;
96
+ `);
97
+ },
98
+ };
@@ -0,0 +1,204 @@
1
+ import { Pool } from "pg";
2
+ import type { PoolConfig } from "pg";
3
+ import type {
4
+ MenuItem,
5
+ MenuRegistry,
6
+ CreateMenuItemInput,
7
+ UpdateMenuItemInput,
8
+ MenuContentConfig,
9
+ } from "@axiom-lattice/protocols";
10
+ import { MigrationManager } from "../migrations/migration";
11
+ import { createMenuItemsTable } from "../migrations/menu_items_migration";
12
+
13
+ export interface MenuStoreOptions {
14
+ poolConfig: string | PoolConfig;
15
+ autoMigrate?: boolean;
16
+ }
17
+
18
+ type MenuItemRow = {
19
+ id: string;
20
+ tenant_id: string;
21
+ menu_target: string;
22
+ group: string | null;
23
+ name: string;
24
+ icon: string | null;
25
+ sort_order: number;
26
+ content_type: string;
27
+ content_config: Record<string, unknown>;
28
+ enabled: boolean;
29
+ created_at: Date;
30
+ updated_at: Date;
31
+ };
32
+
33
+ export class MenuStore implements MenuRegistry {
34
+ private pool: Pool;
35
+ private migrationManager: MigrationManager;
36
+ private initialized = false;
37
+ private initPromise: Promise<void> | null = null;
38
+
39
+ constructor(options: MenuStoreOptions) {
40
+ this.pool =
41
+ typeof options.poolConfig === "string"
42
+ ? new Pool({ connectionString: options.poolConfig })
43
+ : new Pool(options.poolConfig);
44
+
45
+ this.migrationManager = new MigrationManager(this.pool);
46
+ this.migrationManager.register(createMenuItemsTable);
47
+
48
+ if (options.autoMigrate !== false) {
49
+ this.initialize().catch((error) => {
50
+ console.error("Failed to initialize MenuStore:", error);
51
+ throw error;
52
+ });
53
+ }
54
+ }
55
+
56
+ async initialize(): Promise<void> {
57
+ if (this.initialized) return;
58
+ if (this.initPromise) return this.initPromise;
59
+ this.initPromise = (async () => {
60
+ try {
61
+ await this.migrationManager.migrate();
62
+ this.initialized = true;
63
+ } finally {
64
+ this.initPromise = null;
65
+ }
66
+ })();
67
+ return this.initPromise;
68
+ }
69
+
70
+ async list(params: { tenantId: string; menuTarget?: string }): Promise<MenuItem[]> {
71
+ await this.ensureInitialized();
72
+ const conditions: string[] = ["tenant_id = $1"];
73
+ const values: unknown[] = [params.tenantId];
74
+ let idx = 2;
75
+
76
+ if (params.menuTarget) {
77
+ conditions.push(`menu_target = $${idx++}`);
78
+ values.push(params.menuTarget);
79
+ }
80
+
81
+ const result = await this.pool.query<MenuItemRow>(
82
+ `SELECT * FROM lattice_menu_items
83
+ WHERE ${conditions.join(" AND ")}
84
+ AND enabled = true
85
+ ORDER BY sort_order ASC`,
86
+ values,
87
+ );
88
+ return result.rows.map((r) => this.mapRowToMenuItem(r));
89
+ }
90
+
91
+ async getById(id: string): Promise<MenuItem | null> {
92
+ await this.ensureInitialized();
93
+ const result = await this.pool.query<MenuItemRow>(
94
+ `SELECT * FROM lattice_menu_items WHERE id = $1`,
95
+ [id],
96
+ );
97
+ if (result.rows.length === 0) return null;
98
+ return this.mapRowToMenuItem(result.rows[0]);
99
+ }
100
+
101
+ async create(input: CreateMenuItemInput & { tenantId: string }): Promise<MenuItem> {
102
+ await this.ensureInitialized();
103
+ const result = await this.pool.query<MenuItemRow>(
104
+ `INSERT INTO lattice_menu_items
105
+ (tenant_id, menu_target, "group", name, icon, sort_order, content_type, content_config)
106
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
107
+ RETURNING *`,
108
+ [
109
+ input.tenantId,
110
+ input.menuTarget,
111
+ input.group || null,
112
+ input.name,
113
+ input.icon || null,
114
+ input.sortOrder ?? 0,
115
+ input.contentType,
116
+ JSON.stringify(input.contentConfig),
117
+ ],
118
+ );
119
+ return this.mapRowToMenuItem(result.rows[0]);
120
+ }
121
+
122
+ async update(id: string, patch: UpdateMenuItemInput): Promise<MenuItem> {
123
+ await this.ensureInitialized();
124
+ const existing = await this.pool.query<MenuItemRow>(
125
+ `SELECT * FROM lattice_menu_items WHERE id = $1`,
126
+ [id],
127
+ );
128
+ if (existing.rows.length === 0) {
129
+ throw new Error(`MenuItem ${id} not found`);
130
+ }
131
+
132
+ const row = existing.rows[0];
133
+ const setClauses: string[] = [];
134
+ const values: unknown[] = [];
135
+ let idx = 1;
136
+
137
+ if (patch.group !== undefined) {
138
+ setClauses.push(`"group" = $${idx++}`);
139
+ values.push(patch.group);
140
+ }
141
+ if (patch.name !== undefined) {
142
+ setClauses.push(`name = $${idx++}`);
143
+ values.push(patch.name);
144
+ }
145
+ if (patch.icon !== undefined) {
146
+ setClauses.push(`icon = $${idx++}`);
147
+ values.push(patch.icon);
148
+ }
149
+ if (patch.sortOrder !== undefined) {
150
+ setClauses.push(`sort_order = $${idx++}`);
151
+ values.push(patch.sortOrder);
152
+ }
153
+ if (patch.contentConfig !== undefined) {
154
+ setClauses.push(`content_config = $${idx++}`);
155
+ values.push(JSON.stringify(patch.contentConfig));
156
+ }
157
+ if (patch.enabled !== undefined) {
158
+ setClauses.push(`enabled = $${idx++}`);
159
+ values.push(patch.enabled);
160
+ }
161
+
162
+ if (setClauses.length === 0) {
163
+ return this.mapRowToMenuItem(row);
164
+ }
165
+
166
+ setClauses.push(`updated_at = NOW()`);
167
+ values.push(id);
168
+
169
+ const result = await this.pool.query<MenuItemRow>(
170
+ `UPDATE lattice_menu_items SET ${setClauses.join(", ")} WHERE id = $${idx}
171
+ RETURNING *`,
172
+ values,
173
+ );
174
+ return this.mapRowToMenuItem(result.rows[0]);
175
+ }
176
+
177
+ async delete(id: string): Promise<void> {
178
+ await this.ensureInitialized();
179
+ await this.pool.query(`DELETE FROM lattice_menu_items WHERE id = $1`, [id]);
180
+ }
181
+
182
+ private async ensureInitialized(): Promise<void> {
183
+ if (!this.initialized) {
184
+ await this.initialize();
185
+ }
186
+ }
187
+
188
+ private mapRowToMenuItem(row: MenuItemRow): MenuItem {
189
+ return {
190
+ id: row.id,
191
+ tenantId: row.tenant_id,
192
+ menuTarget: row.menu_target as MenuItem["menuTarget"],
193
+ group: row.group || undefined,
194
+ name: row.name,
195
+ icon: row.icon || undefined,
196
+ sortOrder: row.sort_order,
197
+ contentType: row.content_type as MenuItem["contentType"],
198
+ contentConfig: row.content_config as unknown as MenuContentConfig,
199
+ enabled: row.enabled,
200
+ createdAt: row.created_at,
201
+ updatedAt: row.updated_at,
202
+ };
203
+ }
204
+ }
@@ -13,6 +13,7 @@ import { MigrationManager } from "../migrations/migration";
13
13
  import { createAssistantsTable } from "../migrations/assistant_migrations";
14
14
  import { addAssistantTenantId } from "../migrations/assistant_tenant_migration";
15
15
  import { changeAssistantPrimaryKey } from "../migrations/assistant_pk_migration";
16
+ import { addAssistantOwnerUserId } from "../migrations/assistant_owner_user_migration";
16
17
 
17
18
  /**
18
19
  * PostgreSQL AssistantStore options
@@ -56,6 +57,7 @@ export class PostgreSQLAssistantStore implements AssistantStore {
56
57
  this.migrationManager.register(createAssistantsTable);
57
58
  this.migrationManager.register(addAssistantTenantId);
58
59
  this.migrationManager.register(changeAssistantPrimaryKey);
60
+ this.migrationManager.register(addAssistantOwnerUserId);
59
61
 
60
62
  // Auto-migrate by default
61
63
  if (options.autoMigrate !== false) {
@@ -103,11 +105,12 @@ export class PostgreSQLAssistantStore implements AssistantStore {
103
105
  name: string;
104
106
  description: string | null;
105
107
  graph_definition: any;
108
+ owner_user_id: string | null;
106
109
  created_at: Date;
107
110
  updated_at: Date;
108
111
  }>(
109
112
  `
110
- SELECT id, tenant_id, name, description, graph_definition, created_at, updated_at
113
+ SELECT id, tenant_id, name, description, graph_definition, owner_user_id, created_at, updated_at
111
114
  FROM lattice_assistants
112
115
  WHERE tenant_id = $1
113
116
  ORDER BY created_at DESC
@@ -130,11 +133,12 @@ export class PostgreSQLAssistantStore implements AssistantStore {
130
133
  name: string;
131
134
  description: string | null;
132
135
  graph_definition: any;
136
+ owner_user_id: string | null;
133
137
  created_at: Date;
134
138
  updated_at: Date;
135
139
  }>(
136
140
  `
137
- SELECT id, tenant_id, name, description, graph_definition, created_at, updated_at
141
+ SELECT id, tenant_id, name, description, graph_definition, owner_user_id, created_at, updated_at
138
142
  FROM lattice_assistants
139
143
  WHERE tenant_id = $1 AND id = $2
140
144
  `,
@@ -162,12 +166,13 @@ export class PostgreSQLAssistantStore implements AssistantStore {
162
166
 
163
167
  await this.pool.query(
164
168
  `
165
- INSERT INTO lattice_assistants (id, tenant_id, name, description, graph_definition, created_at, updated_at)
166
- VALUES ($1, $2, $3, $4, $5, $6, $7)
169
+ INSERT INTO lattice_assistants (id, tenant_id, name, description, graph_definition, owner_user_id, created_at, updated_at)
170
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
167
171
  ON CONFLICT (id, tenant_id) DO UPDATE SET
168
172
  name = EXCLUDED.name,
169
173
  description = EXCLUDED.description,
170
174
  graph_definition = EXCLUDED.graph_definition,
175
+ owner_user_id = EXCLUDED.owner_user_id,
171
176
  updated_at = EXCLUDED.updated_at
172
177
  `,
173
178
  [
@@ -176,6 +181,7 @@ export class PostgreSQLAssistantStore implements AssistantStore {
176
181
  data.name,
177
182
  data.description || null,
178
183
  JSON.stringify(data.graphDefinition),
184
+ data.ownerUserId || null,
179
185
  now,
180
186
  now,
181
187
  ]
@@ -187,6 +193,7 @@ export class PostgreSQLAssistantStore implements AssistantStore {
187
193
  name: data.name,
188
194
  description: data.description,
189
195
  graphDefinition: data.graphDefinition,
196
+ ownerUserId: data.ownerUserId,
190
197
  createdAt: now,
191
198
  updatedAt: now,
192
199
  };
@@ -228,6 +235,11 @@ export class PostgreSQLAssistantStore implements AssistantStore {
228
235
  updateValues.push(JSON.stringify(updates.graphDefinition));
229
236
  }
230
237
 
238
+ if (updates.ownerUserId !== undefined) {
239
+ updateFields.push(`owner_user_id = $${paramIndex++}`);
240
+ updateValues.push(updates.ownerUserId || null);
241
+ }
242
+
231
243
  if (updateFields.length === 0) {
232
244
  // No fields to update
233
245
  return existing;
@@ -289,6 +301,35 @@ export class PostgreSQLAssistantStore implements AssistantStore {
289
301
  return result.rows.length > 0;
290
302
  }
291
303
 
304
+ /**
305
+ * Get assistant by owner user ID
306
+ */
307
+ async getByOwner(tenantId: string, userId: string): Promise<Assistant | null> {
308
+ await this.ensureInitialized();
309
+
310
+ const result = await this.pool.query<{
311
+ id: string;
312
+ tenant_id: string;
313
+ name: string;
314
+ description: string | null;
315
+ graph_definition: any;
316
+ owner_user_id: string | null;
317
+ created_at: Date;
318
+ updated_at: Date;
319
+ }>(
320
+ `
321
+ SELECT id, tenant_id, name, description, graph_definition, owner_user_id, created_at, updated_at
322
+ FROM lattice_assistants
323
+ WHERE tenant_id = $1 AND owner_user_id = $2
324
+ LIMIT 1
325
+ `,
326
+ [tenantId, userId]
327
+ );
328
+
329
+ if (result.rows.length === 0) return null;
330
+ return this.mapRowToAssistant(result.rows[0]);
331
+ }
332
+
292
333
  /**
293
334
  * Dispose resources and close the connection pool
294
335
  * Should be called when the store is no longer needed
@@ -317,6 +358,7 @@ export class PostgreSQLAssistantStore implements AssistantStore {
317
358
  name: string;
318
359
  description: string | null;
319
360
  graph_definition: any;
361
+ owner_user_id: string | null;
320
362
  created_at: Date;
321
363
  updated_at: Date;
322
364
  }): Assistant {
@@ -329,6 +371,7 @@ export class PostgreSQLAssistantStore implements AssistantStore {
329
371
  typeof row.graph_definition === "string"
330
372
  ? JSON.parse(row.graph_definition)
331
373
  : row.graph_definition,
374
+ ownerUserId: row.owner_user_id || undefined,
332
375
  createdAt: row.created_at,
333
376
  updatedAt: row.updated_at,
334
377
  };