@axiom-lattice/pg-stores 2.0.10 → 3.0.1

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.
Files changed (30) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +60 -0
  3. package/dist/index.d.mts +52 -4
  4. package/dist/index.d.ts +52 -4
  5. package/dist/index.js +907 -123
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +905 -123
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/__tests__/PostgreSQLA2AApiKeyStore.migrations.test.ts +76 -0
  11. package/src/__tests__/PostgreSQLA2AApiKeyStore.test.ts +133 -0
  12. package/src/__tests__/PostgreSQLAgentWebAppStore.migrations.test.ts +77 -0
  13. package/src/__tests__/PostgreSQLAgentWebAppStore.test.ts +290 -0
  14. package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +37 -2
  15. package/src/__tests__/PostgreSQLTaskStore.integration.test.ts +90 -0
  16. package/src/__tests__/PostgreSQLTaskStore.test.ts +287 -0
  17. package/src/__tests__/PostgreSQLTaskWorkItemStore.test.ts +104 -0
  18. package/src/__tests__/add_a2a_key_assistant_ids.test.ts +34 -0
  19. package/src/__tests__/workspace-project-store.test.ts +48 -0
  20. package/src/createPgStoreConfig.ts +10 -1
  21. package/src/index.ts +10 -0
  22. package/src/migrations/add_a2a_key_assistant_ids.ts +24 -0
  23. package/src/migrations/add_project_kind_column.ts +29 -0
  24. package/src/migrations/agent_web_apps_migration.ts +37 -0
  25. package/src/migrations/task_work_items_migration.ts +15 -0
  26. package/src/stores/PostgreSQLA2AApiKeyStore.ts +23 -8
  27. package/src/stores/PostgreSQLAgentWebAppStore.ts +280 -0
  28. package/src/stores/PostgreSQLProjectStore.ts +31 -8
  29. package/src/stores/PostgreSQLTaskStore.ts +272 -7
  30. package/src/stores/PostgreSQLTaskWorkItemStore.ts +42 -2
@@ -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
+ };
@@ -0,0 +1,37 @@
1
+ import type { PoolClient } from "pg";
2
+
3
+ import type { Migration } from "./migration";
4
+
5
+ export const createAgentWebAppsTable: Migration = {
6
+ version: 169,
7
+ name: "create_agent_web_apps_table",
8
+ up: async (client: PoolClient) => {
9
+ await client.query(`
10
+ CREATE TABLE IF NOT EXISTS lattice_agent_web_apps (
11
+ id VARCHAR(255) PRIMARY KEY,
12
+ tenant_id VARCHAR(255) NOT NULL,
13
+ assistant_id VARCHAR(255) NOT NULL,
14
+ name VARCHAR(255) NOT NULL,
15
+ description TEXT,
16
+ status VARCHAR(32) NOT NULL DEFAULT 'draft'
17
+ CONSTRAINT chk_lattice_agent_web_apps_status CHECK (status IN ('draft', 'active', 'disabled')),
18
+ integration JSONB NOT NULL,
19
+ scope JSONB NOT NULL,
20
+ features JSONB NOT NULL,
21
+ appearance JSONB NOT NULL DEFAULT '{}'::jsonb,
22
+ created_at TIMESTAMPTZ NOT NULL DEFAULT date_trunc('milliseconds', NOW()),
23
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT date_trunc('milliseconds', NOW())
24
+ )
25
+ `);
26
+ await client.query(`
27
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_web_apps_tenant_assistant
28
+ ON lattice_agent_web_apps(tenant_id, assistant_id)
29
+ `);
30
+ },
31
+ down: async (client: PoolClient) => {
32
+ await client.query(
33
+ "DROP INDEX IF EXISTS idx_lattice_agent_web_apps_tenant_assistant",
34
+ );
35
+ await client.query("DROP TABLE IF EXISTS lattice_agent_web_apps");
36
+ },
37
+ };
@@ -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;
@@ -0,0 +1,280 @@
1
+ import { randomUUID } from "crypto";
2
+ import type {
3
+ AgentWebApp,
4
+ AgentWebAppAppearance,
5
+ AgentWebAppFeatures,
6
+ AgentWebAppScope,
7
+ AgentWebAppStatus,
8
+ AgentWebAppStore,
9
+ AgentWebAppStorePatch,
10
+ AgentWebAppUpdateOptions,
11
+ CreateAgentWebAppInput,
12
+ } from "@axiom-lattice/protocols";
13
+ import { Pool } from "pg";
14
+ import type { PoolConfig } from "pg";
15
+
16
+ import { createAgentWebAppsTable } from "../migrations/agent_web_apps_migration";
17
+ import { MigrationManager } from "../migrations/migration";
18
+
19
+ export interface PostgreSQLAgentWebAppStoreOptions {
20
+ pool?: Pool;
21
+ poolConfig?: string | PoolConfig;
22
+ autoMigrate?: boolean;
23
+ }
24
+
25
+ type AgentWebAppRow = {
26
+ id: string;
27
+ tenant_id: string;
28
+ assistant_id: string;
29
+ name: string;
30
+ description: string | null;
31
+ status: AgentWebAppStatus;
32
+ integration: unknown;
33
+ scope: unknown;
34
+ features: unknown;
35
+ appearance: unknown;
36
+ created_at: Date;
37
+ updated_at: Date;
38
+ };
39
+
40
+ function isRecord(value: unknown): value is Record<string, unknown> {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+
44
+ function isStringArray(value: unknown): value is string[] {
45
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
46
+ }
47
+
48
+ function isIntegration(value: unknown): value is AgentWebApp["integration"] {
49
+ return isRecord(value) && value.type === "react_sdk";
50
+ }
51
+
52
+ function isScope(value: unknown): value is AgentWebAppScope {
53
+ return isRecord(value)
54
+ && typeof value.defaultProjectId === "string"
55
+ && isStringArray(value.allowedProjectIds)
56
+ && (value.defaultModelKey === undefined || typeof value.defaultModelKey === "string")
57
+ && (value.allowedModelKeys === undefined || isStringArray(value.allowedModelKeys));
58
+ }
59
+
60
+ function isFeatures(value: unknown): value is AgentWebAppFeatures {
61
+ return isRecord(value)
62
+ && typeof value.projectSelector === "boolean"
63
+ && typeof value.modelSelector === "boolean"
64
+ && typeof value.threadManagement === "boolean"
65
+ && typeof value.attachments === "boolean"
66
+ && typeof value.hitl === "boolean"
67
+ && typeof value.genUI === "boolean";
68
+ }
69
+
70
+ function isAppearance(value: unknown): value is AgentWebAppAppearance {
71
+ return isRecord(value)
72
+ && (value.title === undefined || typeof value.title === "string")
73
+ && (value.welcomeMessage === undefined || typeof value.welcomeMessage === "string")
74
+ && (value.primaryColor === undefined || typeof value.primaryColor === "string");
75
+ }
76
+
77
+ function isStatus(value: unknown): value is AgentWebAppStatus {
78
+ return value === "draft" || value === "active" || value === "disabled";
79
+ }
80
+
81
+ function mapRow(row: AgentWebAppRow): AgentWebApp {
82
+ if (
83
+ !isStatus(row.status)
84
+ || !isIntegration(row.integration)
85
+ || !isScope(row.scope)
86
+ || !isFeatures(row.features)
87
+ || !isAppearance(row.appearance)
88
+ ) {
89
+ throw new Error(`Invalid agent web app row: ${row.id}`);
90
+ }
91
+
92
+ return {
93
+ id: row.id,
94
+ tenantId: row.tenant_id,
95
+ assistantId: row.assistant_id,
96
+ name: row.name,
97
+ description: row.description ?? undefined,
98
+ status: row.status,
99
+ integration: row.integration,
100
+ scope: row.scope,
101
+ features: row.features,
102
+ appearance: row.appearance,
103
+ createdAt: row.created_at,
104
+ updatedAt: row.updated_at,
105
+ };
106
+ }
107
+
108
+ export class PostgreSQLAgentWebAppStore implements AgentWebAppStore {
109
+ private pool: Pool;
110
+ private migrationManager!: MigrationManager;
111
+ private initialized = false;
112
+ private ownsPool = true;
113
+ private initPromise: Promise<void> | null = null;
114
+
115
+ constructor(options: PostgreSQLAgentWebAppStoreOptions) {
116
+ if (options.pool) {
117
+ this.pool = options.pool;
118
+ this.ownsPool = false;
119
+ this.initialized = true;
120
+ return;
121
+ }
122
+
123
+ this.pool = typeof options.poolConfig === "string"
124
+ ? new Pool({ connectionString: options.poolConfig })
125
+ : options.poolConfig
126
+ ? new Pool(options.poolConfig)
127
+ : (() => { throw new Error("Either pool or poolConfig must be provided"); })();
128
+
129
+ this.migrationManager = new MigrationManager(this.pool);
130
+ this.migrationManager.register(createAgentWebAppsTable);
131
+
132
+ if (options.autoMigrate !== false) {
133
+ this.startInitialization();
134
+ }
135
+ }
136
+
137
+ async initialize(): Promise<void> {
138
+ if (this.initialized) return;
139
+ if (this.initPromise) return this.initPromise;
140
+ return this.startInitialization();
141
+ }
142
+
143
+ private startInitialization(): Promise<void> {
144
+ this.initPromise = this.migrationManager.migrate().then(() => {
145
+ this.initialized = true;
146
+ });
147
+ // Eager migration errors remain on initPromise for initialize/operations.
148
+ void this.initPromise.catch(() => undefined);
149
+ return this.initPromise;
150
+ }
151
+
152
+ async dispose(): Promise<void> {
153
+ if (this.ownsPool) await this.pool.end();
154
+ }
155
+
156
+ private async ensureInitialized(): Promise<void> {
157
+ if (!this.initialized) await this.initialize();
158
+ }
159
+
160
+ async list(tenantId: string, assistantId?: string): Promise<AgentWebApp[]> {
161
+ await this.ensureInitialized();
162
+ const result = assistantId === undefined
163
+ ? await this.pool.query<AgentWebAppRow>(
164
+ `SELECT * FROM lattice_agent_web_apps
165
+ WHERE tenant_id = $1
166
+ ORDER BY created_at DESC, id DESC`,
167
+ [tenantId],
168
+ )
169
+ : await this.pool.query<AgentWebAppRow>(
170
+ `SELECT * FROM lattice_agent_web_apps
171
+ WHERE tenant_id = $1 AND assistant_id = $2
172
+ ORDER BY created_at DESC, id DESC`,
173
+ [tenantId, assistantId],
174
+ );
175
+ return result.rows.map(mapRow);
176
+ }
177
+
178
+ async getById(tenantId: string, webAppId: string): Promise<AgentWebApp | null> {
179
+ await this.ensureInitialized();
180
+ const result = await this.pool.query<AgentWebAppRow>(
181
+ `SELECT * FROM lattice_agent_web_apps
182
+ WHERE tenant_id = $1 AND id = $2`,
183
+ [tenantId, webAppId],
184
+ );
185
+ return result.rows[0] ? mapRow(result.rows[0]) : null;
186
+ }
187
+
188
+ async findById(webAppId: string): Promise<AgentWebApp | null> {
189
+ await this.ensureInitialized();
190
+ const result = await this.pool.query<AgentWebAppRow>(
191
+ `SELECT * FROM lattice_agent_web_apps
192
+ WHERE id = $1`,
193
+ [webAppId],
194
+ );
195
+ return result.rows[0] ? mapRow(result.rows[0]) : null;
196
+ }
197
+
198
+ async create(tenantId: string, input: CreateAgentWebAppInput): Promise<AgentWebApp> {
199
+ await this.ensureInitialized();
200
+ const id = `webapp_${randomUUID().replace(/-/g, "")}`;
201
+ const result = await this.pool.query<AgentWebAppRow>(
202
+ `INSERT INTO lattice_agent_web_apps
203
+ (id, tenant_id, assistant_id, name, description, status, integration, scope, features, appearance)
204
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, $9::jsonb, $10::jsonb)
205
+ RETURNING *`,
206
+ [
207
+ id,
208
+ tenantId,
209
+ input.assistantId,
210
+ input.name,
211
+ input.description ?? null,
212
+ "draft",
213
+ JSON.stringify(input.integration),
214
+ JSON.stringify(input.scope),
215
+ JSON.stringify(input.features),
216
+ JSON.stringify(input.appearance),
217
+ ],
218
+ );
219
+ return mapRow(result.rows[0]);
220
+ }
221
+
222
+ async update(
223
+ tenantId: string,
224
+ webAppId: string,
225
+ patch: AgentWebAppStorePatch,
226
+ options?: AgentWebAppUpdateOptions,
227
+ ): Promise<AgentWebApp | null> {
228
+ await this.ensureInitialized();
229
+ const assignments: string[] = [];
230
+ const values: unknown[] = [];
231
+ const add = (column: string, value: unknown): void => {
232
+ values.push(value);
233
+ assignments.push(`${column} = $${values.length}`);
234
+ };
235
+
236
+ if (patch.name !== undefined) add("name", patch.name);
237
+ if (Object.prototype.hasOwnProperty.call(patch, "description")) {
238
+ add("description", patch.description ?? null);
239
+ }
240
+ if (patch.scope !== undefined) {
241
+ add("scope", JSON.stringify(patch.scope));
242
+ }
243
+ if (patch.features !== undefined) {
244
+ add("features", JSON.stringify(patch.features));
245
+ }
246
+ if (patch.appearance !== undefined) {
247
+ add("appearance", JSON.stringify(patch.appearance));
248
+ }
249
+ if (patch.status !== undefined) add("status", patch.status);
250
+
251
+ if (assignments.length === 0) return this.getById(tenantId, webAppId);
252
+ values.push(tenantId, webAppId);
253
+ const tenantParam = values.length - 1;
254
+ const idParam = values.length;
255
+ const expectedUpdatedAtClause = options?.expectedUpdatedAt
256
+ ? ` AND date_trunc('milliseconds', updated_at) = $${values.push(options.expectedUpdatedAt)}`
257
+ : "";
258
+ const result = await this.pool.query<AgentWebAppRow>(
259
+ `UPDATE lattice_agent_web_apps
260
+ SET ${assignments.join(", ")}, updated_at = GREATEST(
261
+ date_trunc('milliseconds', clock_timestamp()),
262
+ updated_at + interval '1 millisecond'
263
+ )
264
+ WHERE tenant_id = $${tenantParam} AND id = $${idParam}${expectedUpdatedAtClause}
265
+ RETURNING *`,
266
+ values,
267
+ );
268
+ return result.rows[0] ? mapRow(result.rows[0]) : null;
269
+ }
270
+
271
+ async delete(tenantId: string, webAppId: string): Promise<boolean> {
272
+ await this.ensureInitialized();
273
+ const result = await this.pool.query(
274
+ `DELETE FROM lattice_agent_web_apps
275
+ WHERE tenant_id = $1 AND id = $2`,
276
+ [tenantId, webAppId],
277
+ );
278
+ return result.rowCount === 1;
279
+ }
280
+ }
@@ -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;