@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.
@@ -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 | null;
25
- workspace_id: string | null;
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 || undefined,
42
- workspaceId: row.workspace_id || undefined,
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, workspace_id, label)
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 || null, input.workspaceId || null, input.label || null],
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 || undefined,
200
- workspaceId: row.workspace_id || undefined,
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 tenant_id = $1 AND workspace_id = $2
171
+ WHERE ${conditions.join(" AND ")}
158
172
  ORDER BY created_at DESC
159
173
  `,
160
- [tenantId, workspaceId]
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;