@axiom-lattice/pg-stores 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }