@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +60 -0
- package/dist/index.d.mts +52 -4
- package/dist/index.d.ts +52 -4
- package/dist/index.js +907 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +905 -123
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/PostgreSQLA2AApiKeyStore.migrations.test.ts +76 -0
- package/src/__tests__/PostgreSQLA2AApiKeyStore.test.ts +133 -0
- package/src/__tests__/PostgreSQLAgentWebAppStore.migrations.test.ts +77 -0
- package/src/__tests__/PostgreSQLAgentWebAppStore.test.ts +290 -0
- package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +37 -2
- package/src/__tests__/PostgreSQLTaskStore.integration.test.ts +90 -0
- package/src/__tests__/PostgreSQLTaskStore.test.ts +287 -0
- package/src/__tests__/PostgreSQLTaskWorkItemStore.test.ts +104 -0
- package/src/__tests__/add_a2a_key_assistant_ids.test.ts +34 -0
- package/src/__tests__/workspace-project-store.test.ts +48 -0
- package/src/createPgStoreConfig.ts +10 -1
- package/src/index.ts +10 -0
- package/src/migrations/add_a2a_key_assistant_ids.ts +24 -0
- package/src/migrations/add_project_kind_column.ts +29 -0
- package/src/migrations/agent_web_apps_migration.ts +37 -0
- package/src/migrations/task_work_items_migration.ts +15 -0
- package/src/stores/PostgreSQLA2AApiKeyStore.ts +23 -8
- package/src/stores/PostgreSQLAgentWebAppStore.ts +280 -0
- package/src/stores/PostgreSQLProjectStore.ts +31 -8
- package/src/stores/PostgreSQLTaskStore.ts +272 -7
- 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
|
|
25
|
-
|
|
25
|
+
project_id: string;
|
|
26
|
+
assistant_ids: string[] | null;
|
|
26
27
|
label: string | null;
|
|
27
28
|
enabled: boolean;
|
|
28
29
|
created_at: Date;
|
|
@@ -38,8 +39,8 @@ function mapRowToRecord(row: KeyRow): A2AApiKeyRecord {
|
|
|
38
39
|
id: row.id,
|
|
39
40
|
key: decrypt(row.key_value),
|
|
40
41
|
tenantId: row.tenant_id,
|
|
41
|
-
projectId: row.project_id
|
|
42
|
-
|
|
42
|
+
projectId: row.project_id,
|
|
43
|
+
assistantIds: row.assistant_ids ?? undefined,
|
|
43
44
|
label: row.label || undefined,
|
|
44
45
|
enabled: row.enabled,
|
|
45
46
|
createdAt: row.created_at,
|
|
@@ -71,7 +72,12 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
71
72
|
: (() => { throw new Error("Either pool or poolConfig must be provided"); })();
|
|
72
73
|
|
|
73
74
|
this.migrationManager = new MigrationManager(this.pool);
|
|
75
|
+
// Standalone deployments (own poolConfig) must apply every migration the
|
|
76
|
+
// queries below depend on. `createPgStoreConfig` registers the same
|
|
77
|
+
// migrations on its own MigrationManager; managers dedupe by name, so the
|
|
78
|
+
// overlap is safe.
|
|
74
79
|
this.migrationManager.register(createA2AApiKeysTable);
|
|
80
|
+
this.migrationManager.register(addA2AKeyAssistantIds);
|
|
75
81
|
|
|
76
82
|
if (options.autoMigrate !== false) {
|
|
77
83
|
this.initialize().catch((error) => {
|
|
@@ -116,6 +122,15 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
116
122
|
return null;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
async findById(id: string): Promise<A2AApiKeyRecord | null> {
|
|
126
|
+
await this.ensureInitialized();
|
|
127
|
+
const result = await this.pool.query<KeyRow>(
|
|
128
|
+
`SELECT * FROM lattice_a2a_api_keys WHERE id = $1`,
|
|
129
|
+
[id],
|
|
130
|
+
);
|
|
131
|
+
return result.rows[0] ? mapRowToRecord(result.rows[0]) : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
async list(params: { tenantId?: string; limit?: number; offset?: number }): Promise<A2AApiKeyRecord[]> {
|
|
120
135
|
await this.ensureInitialized();
|
|
121
136
|
const limit = params.limit || 100;
|
|
@@ -138,9 +153,9 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
138
153
|
await this.ensureInitialized();
|
|
139
154
|
const key = generateApiKey();
|
|
140
155
|
const result = await this.pool.query<KeyRow>(
|
|
141
|
-
`INSERT INTO lattice_a2a_api_keys (key_value, tenant_id, project_id,
|
|
156
|
+
`INSERT INTO lattice_a2a_api_keys (key_value, tenant_id, project_id, assistant_ids, label)
|
|
142
157
|
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
|
143
|
-
[encrypt(key), input.tenantId, input.projectId
|
|
158
|
+
[encrypt(key), input.tenantId, input.projectId, input.assistantIds ? JSON.stringify(input.assistantIds) : null, input.label || null],
|
|
144
159
|
);
|
|
145
160
|
const record = mapRowToRecord(result.rows[0]);
|
|
146
161
|
record.key = key;
|
|
@@ -196,8 +211,8 @@ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
196
211
|
map.set(key, {
|
|
197
212
|
key,
|
|
198
213
|
tenantId: row.tenant_id,
|
|
199
|
-
projectId: row.project_id
|
|
200
|
-
|
|
214
|
+
projectId: row.project_id,
|
|
215
|
+
assistantIds: row.assistant_ids ?? undefined,
|
|
201
216
|
});
|
|
202
217
|
}
|
|
203
218
|
return map;
|
|
@@ -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
|
|
171
|
+
WHERE ${conditions.join(" AND ")}
|
|
158
172
|
ORDER BY created_at DESC
|
|
159
173
|
`,
|
|
160
|
-
|
|
174
|
+
values
|
|
161
175
|
);
|
|
162
176
|
|
|
163
177
|
return result.rows.map(this.mapRowToProject);
|
|
@@ -176,11 +190,12 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
176
190
|
name: string;
|
|
177
191
|
description: string | null;
|
|
178
192
|
config: unknown | null;
|
|
193
|
+
kind: string | null;
|
|
179
194
|
created_at: Date;
|
|
180
195
|
updated_at: Date;
|
|
181
196
|
}>(
|
|
182
197
|
`
|
|
183
|
-
SELECT id, tenant_id, workspace_id, name, description, config, created_at, updated_at
|
|
198
|
+
SELECT id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at
|
|
184
199
|
FROM lattice_projects
|
|
185
200
|
WHERE id = $1 AND tenant_id = $2
|
|
186
201
|
`,
|
|
@@ -206,19 +221,21 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
206
221
|
await this.ensureInitialized();
|
|
207
222
|
|
|
208
223
|
const now = new Date();
|
|
224
|
+
const kind = data.kind || "business";
|
|
209
225
|
|
|
210
226
|
await this.pool.query(
|
|
211
227
|
`
|
|
212
|
-
INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, created_at, updated_at)
|
|
213
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
228
|
+
INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
|
|
229
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
214
230
|
ON CONFLICT (id, tenant_id) DO UPDATE SET
|
|
215
231
|
workspace_id = EXCLUDED.workspace_id,
|
|
216
232
|
name = EXCLUDED.name,
|
|
217
233
|
description = EXCLUDED.description,
|
|
218
234
|
config = EXCLUDED.config,
|
|
235
|
+
kind = EXCLUDED.kind,
|
|
219
236
|
updated_at = EXCLUDED.updated_at
|
|
220
237
|
`,
|
|
221
|
-
[id, tenantId, workspaceId, data.name, data.description || null, data.config || null, now, now]
|
|
238
|
+
[id, tenantId, workspaceId, data.name, data.description || null, data.config || null, kind, now, now]
|
|
222
239
|
);
|
|
223
240
|
|
|
224
241
|
return {
|
|
@@ -228,6 +245,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
228
245
|
name: data.name,
|
|
229
246
|
description: data.description,
|
|
230
247
|
config: data.config,
|
|
248
|
+
kind,
|
|
231
249
|
createdAt: now,
|
|
232
250
|
updatedAt: now,
|
|
233
251
|
};
|
|
@@ -273,6 +291,11 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
273
291
|
updateValues.push(updates.config || null);
|
|
274
292
|
}
|
|
275
293
|
|
|
294
|
+
if (updates.kind !== undefined) {
|
|
295
|
+
updateFields.push(`kind = $${paramIndex++}`);
|
|
296
|
+
updateValues.push(updates.kind);
|
|
297
|
+
}
|
|
298
|
+
|
|
276
299
|
if (updateFields.length === 0) {
|
|
277
300
|
// No fields to update
|
|
278
301
|
return existing;
|