@axiom-lattice/pg-stores 1.0.21 → 1.0.23

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * PostgreSQL implementation of SkillStore
2
+ * PostgreSQL implementation of SkillStore with tenant isolation
3
3
  */
4
4
 
5
5
  import { Pool, PoolConfig } from "pg";
@@ -10,35 +10,28 @@ import {
10
10
  } from "@axiom-lattice/protocols";
11
11
  import { MigrationManager } from "../migrations/migration";
12
12
  import { createSkillsTable } from "../migrations/skill_migrations";
13
+ import { addSkillTenantId } from "../migrations/skill_tenant_migration";
14
+ import { changeSkillPrimaryKey } from "../migrations/skill_pk_migration";
13
15
 
14
- /**
15
- * PostgreSQL SkillStore options
16
- */
17
16
  export interface PostgreSQLSkillStoreOptions {
18
- /**
19
- * PostgreSQL connection pool configuration
20
- * Can be a connection string or PoolConfig object
21
- */
22
17
  poolConfig: string | PoolConfig;
23
-
24
- /**
25
- * Whether to run migrations automatically on initialization
26
- * @default true
27
- */
28
18
  autoMigrate?: boolean;
29
19
  }
30
20
 
31
21
  /**
32
- * PostgreSQL implementation of SkillStore
22
+ * PostgreSQL implementation of SkillStore with tenant isolation
23
+ *
24
+ * Features:
25
+ * - Multi-tenant isolation via tenant_id
33
26
  */
34
27
  export class PostgreSQLSkillStore implements SkillStore {
35
28
  private pool: Pool;
36
29
  private migrationManager: MigrationManager;
37
30
  private initialized: boolean = false;
38
31
  private ownsPool: boolean = true;
32
+ private initPromise: Promise<void> | null = null;
39
33
 
40
34
  constructor(options: PostgreSQLSkillStoreOptions) {
41
- // Create Pool from config
42
35
  if (typeof options.poolConfig === "string") {
43
36
  this.pool = new Pool({ connectionString: options.poolConfig });
44
37
  } else {
@@ -47,8 +40,9 @@ export class PostgreSQLSkillStore implements SkillStore {
47
40
 
48
41
  this.migrationManager = new MigrationManager(this.pool);
49
42
  this.migrationManager.register(createSkillsTable);
43
+ this.migrationManager.register(addSkillTenantId);
44
+ this.migrationManager.register(changeSkillPrimaryKey);
50
45
 
51
- // Auto-migrate by default
52
46
  if (options.autoMigrate !== false) {
53
47
  this.initialize().catch((error) => {
54
48
  console.error("Failed to initialize PostgreSQLSkillStore:", error);
@@ -57,31 +51,37 @@ export class PostgreSQLSkillStore implements SkillStore {
57
51
  }
58
52
  }
59
53
 
60
- /**
61
- * Dispose resources and close the connection pool
62
- * Should be called when the store is no longer needed
63
- */
64
- async dispose(): Promise<void> {
65
- if (this.ownsPool && this.pool) {
66
- await this.pool.end();
67
- }
68
- }
69
-
70
54
  /**
71
55
  * Initialize the store and run migrations
56
+ * Uses a promise-based lock to prevent concurrent initialization
72
57
  */
73
58
  async initialize(): Promise<void> {
74
59
  if (this.initialized) {
75
60
  return;
76
61
  }
77
62
 
78
- await this.migrationManager.migrate();
79
- this.initialized = true;
63
+ if (this.initPromise) {
64
+ return this.initPromise;
65
+ }
66
+
67
+ this.initPromise = (async () => {
68
+ try {
69
+ await this.migrationManager.migrate();
70
+ this.initialized = true;
71
+ } finally {
72
+ this.initPromise = null;
73
+ }
74
+ })();
75
+
76
+ return this.initPromise;
77
+ }
78
+
79
+ async dispose(): Promise<void> {
80
+ if (this.ownsPool && this.pool) {
81
+ await this.pool.end();
82
+ }
80
83
  }
81
84
 
82
- /**
83
- * Ensure store is initialized
84
- */
85
85
  private async ensureInitialized(): Promise<void> {
86
86
  if (!this.initialized) {
87
87
  await this.initialize();
@@ -93,6 +93,7 @@ export class PostgreSQLSkillStore implements SkillStore {
93
93
  */
94
94
  private mapRowToSkill(row: {
95
95
  id: string;
96
+ tenant_id: string;
96
97
  name: string;
97
98
  description: string;
98
99
  license: string | null;
@@ -105,28 +106,28 @@ export class PostgreSQLSkillStore implements SkillStore {
105
106
  }): Skill {
106
107
  return {
107
108
  id: row.id,
109
+ tenantId: row.tenant_id,
108
110
  name: row.name,
109
111
  description: row.description,
110
112
  license: row.license || undefined,
111
113
  compatibility: row.compatibility || undefined,
112
114
  metadata: row.metadata || {},
113
115
  content: row.content || undefined,
114
- subSkills: Array.isArray(row.sub_skills) && row.sub_skills.length > 0
115
- ? row.sub_skills
116
- : undefined,
116
+ subSkills: Array.isArray(row.sub_skills) ? row.sub_skills : undefined,
117
117
  createdAt: row.created_at,
118
118
  updatedAt: row.updated_at,
119
119
  };
120
120
  }
121
121
 
122
122
  /**
123
- * Get all skills
123
+ * Get all skills for a tenant
124
124
  */
125
- async getAllSkills(): Promise<Skill[]> {
125
+ async getAllSkills(tenantId: string): Promise<Skill[]> {
126
126
  await this.ensureInitialized();
127
127
 
128
128
  const result = await this.pool.query<{
129
129
  id: string;
130
+ tenant_id: string;
130
131
  name: string;
131
132
  description: string;
132
133
  license: string | null;
@@ -138,23 +139,26 @@ export class PostgreSQLSkillStore implements SkillStore {
138
139
  updated_at: Date;
139
140
  }>(
140
141
  `
141
- SELECT id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
142
+ SELECT id, tenant_id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
142
143
  FROM lattice_skills
144
+ WHERE tenant_id = $1
143
145
  ORDER BY created_at DESC
144
- `
146
+ `,
147
+ [tenantId]
145
148
  );
146
149
 
147
150
  return result.rows.map(this.mapRowToSkill);
148
151
  }
149
152
 
150
153
  /**
151
- * Get skill by ID
154
+ * Get skill by ID for a tenant
152
155
  */
153
- async getSkillById(id: string): Promise<Skill | null> {
156
+ async getSkillById(tenantId: string, id: string): Promise<Skill | null> {
154
157
  await this.ensureInitialized();
155
158
 
156
159
  const result = await this.pool.query<{
157
160
  id: string;
161
+ tenant_id: string;
158
162
  name: string;
159
163
  description: string;
160
164
  license: string | null;
@@ -166,11 +170,11 @@ export class PostgreSQLSkillStore implements SkillStore {
166
170
  updated_at: Date;
167
171
  }>(
168
172
  `
169
- SELECT id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
173
+ SELECT id, tenant_id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
170
174
  FROM lattice_skills
171
- WHERE id = $1
175
+ WHERE tenant_id = $1 AND id = $2
172
176
  `,
173
- [id]
177
+ [tenantId, id]
174
178
  );
175
179
 
176
180
  if (result.rows.length === 0) {
@@ -181,19 +185,23 @@ export class PostgreSQLSkillStore implements SkillStore {
181
185
  }
182
186
 
183
187
  /**
184
- * Create a new skill
188
+ * Create a new skill for a tenant
185
189
  */
186
- async createSkill(id: string, data: CreateSkillRequest): Promise<Skill> {
190
+ async createSkill(
191
+ tenantId: string,
192
+ id: string,
193
+ data: CreateSkillRequest
194
+ ): Promise<Skill> {
187
195
  await this.ensureInitialized();
188
196
 
189
197
  const now = new Date();
190
- const metadata = data.metadata || {};
191
198
 
192
199
  await this.pool.query(
193
200
  `
194
- INSERT INTO lattice_skills (id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at)
195
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
201
+ INSERT INTO lattice_skills (id, tenant_id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at)
202
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
196
203
  ON CONFLICT (id) DO UPDATE SET
204
+ tenant_id = EXCLUDED.tenant_id,
197
205
  name = EXCLUDED.name,
198
206
  description = EXCLUDED.description,
199
207
  license = EXCLUDED.license,
@@ -205,11 +213,12 @@ export class PostgreSQLSkillStore implements SkillStore {
205
213
  `,
206
214
  [
207
215
  id,
216
+ tenantId,
208
217
  data.name,
209
218
  data.description,
210
219
  data.license || null,
211
220
  data.compatibility || null,
212
- JSON.stringify(metadata),
221
+ JSON.stringify(data.metadata || {}),
213
222
  data.content || null,
214
223
  JSON.stringify(data.subSkills || []),
215
224
  now,
@@ -217,116 +226,143 @@ export class PostgreSQLSkillStore implements SkillStore {
217
226
  ]
218
227
  );
219
228
 
220
- return this.getSkillById(id) as Promise<Skill>;
229
+ return {
230
+ id,
231
+ tenantId,
232
+ name: data.name,
233
+ description: data.description,
234
+ license: data.license,
235
+ compatibility: data.compatibility,
236
+ metadata: data.metadata || {},
237
+ content: data.content,
238
+ subSkills: data.subSkills,
239
+ createdAt: now,
240
+ updatedAt: now,
241
+ };
221
242
  }
222
243
 
223
244
  /**
224
- * Update an existing skill
245
+ * Update an existing skill for a tenant
225
246
  */
226
247
  async updateSkill(
248
+ tenantId: string,
227
249
  id: string,
228
250
  updates: Partial<CreateSkillRequest>
229
251
  ): Promise<Skill | null> {
230
252
  await this.ensureInitialized();
231
253
 
254
+ // Get existing skill
255
+ const existing = await this.getSkillById(tenantId, id);
256
+ if (!existing) {
257
+ return null;
258
+ }
259
+
260
+ // Build update query dynamically based on provided fields
232
261
  const updateFields: string[] = [];
233
- const values: any[] = [];
262
+ const updateValues: any[] = [];
234
263
  let paramIndex = 1;
235
264
 
236
265
  if (updates.name !== undefined) {
237
266
  updateFields.push(`name = $${paramIndex++}`);
238
- values.push(updates.name);
267
+ updateValues.push(updates.name);
239
268
  }
240
269
 
241
270
  if (updates.description !== undefined) {
242
271
  updateFields.push(`description = $${paramIndex++}`);
243
- values.push(updates.description);
272
+ updateValues.push(updates.description);
244
273
  }
245
274
 
246
275
  if (updates.license !== undefined) {
247
276
  updateFields.push(`license = $${paramIndex++}`);
248
- values.push(updates.license || null);
277
+ updateValues.push(updates.license || null);
249
278
  }
250
279
 
251
280
  if (updates.compatibility !== undefined) {
252
281
  updateFields.push(`compatibility = $${paramIndex++}`);
253
- values.push(updates.compatibility || null);
282
+ updateValues.push(updates.compatibility || null);
254
283
  }
255
284
 
256
285
  if (updates.metadata !== undefined) {
257
286
  updateFields.push(`metadata = $${paramIndex++}`);
258
- values.push(JSON.stringify(updates.metadata || {}));
287
+ updateValues.push(JSON.stringify(updates.metadata || {}));
259
288
  }
260
289
 
261
290
  if (updates.content !== undefined) {
262
291
  updateFields.push(`content = $${paramIndex++}`);
263
- values.push(updates.content || null);
292
+ updateValues.push(updates.content || null);
264
293
  }
265
294
 
266
295
  if (updates.subSkills !== undefined) {
267
296
  updateFields.push(`sub_skills = $${paramIndex++}`);
268
- values.push(JSON.stringify(updates.subSkills || []));
297
+ updateValues.push(JSON.stringify(updates.subSkills || []));
269
298
  }
270
299
 
271
300
  if (updateFields.length === 0) {
272
- return this.getSkillById(id);
301
+ // No fields to update
302
+ return existing;
273
303
  }
274
304
 
305
+ // Always update updated_at
275
306
  updateFields.push(`updated_at = $${paramIndex++}`);
276
- values.push(new Date());
277
- values.push(id);
307
+ updateValues.push(new Date());
308
+
309
+ // Add tenant_id and id for WHERE clause
310
+ updateValues.push(tenantId);
311
+ updateValues.push(id);
278
312
 
279
313
  await this.pool.query(
280
314
  `
281
315
  UPDATE lattice_skills
282
316
  SET ${updateFields.join(", ")}
283
- WHERE id = $${paramIndex}
317
+ WHERE tenant_id = $${paramIndex++} AND id = $${paramIndex}
284
318
  `,
285
- values
319
+ updateValues
286
320
  );
287
321
 
288
- return this.getSkillById(id);
322
+ // Return updated skill
323
+ return await this.getSkillById(tenantId, id);
289
324
  }
290
325
 
291
326
  /**
292
- * Delete a skill by ID
327
+ * Delete a skill by ID for a tenant
293
328
  */
294
- async deleteSkill(id: string): Promise<boolean> {
329
+ async deleteSkill(tenantId: string, id: string): Promise<boolean> {
295
330
  await this.ensureInitialized();
296
331
 
297
332
  const result = await this.pool.query(
298
333
  `
299
334
  DELETE FROM lattice_skills
300
- WHERE id = $1
335
+ WHERE tenant_id = $1 AND id = $2
301
336
  `,
302
- [id]
337
+ [tenantId, id]
303
338
  );
304
339
 
305
340
  return result.rowCount !== null && result.rowCount > 0;
306
341
  }
307
342
 
308
343
  /**
309
- * Check if skill exists
344
+ * Check if skill exists for a tenant
310
345
  */
311
- async hasSkill(id: string): Promise<boolean> {
346
+ async hasSkill(tenantId: string, id: string): Promise<boolean> {
312
347
  await this.ensureInitialized();
313
348
 
314
- const result = await this.pool.query<{ count: string }>(
349
+ const result = await this.pool.query(
315
350
  `
316
- SELECT COUNT(*) as count
317
- FROM lattice_skills
318
- WHERE id = $1
351
+ SELECT 1 FROM lattice_skills
352
+ WHERE tenant_id = $1 AND id = $2
353
+ LIMIT 1
319
354
  `,
320
- [id]
355
+ [tenantId, id]
321
356
  );
322
357
 
323
- return parseInt(result.rows[0].count, 10) > 0;
358
+ return result.rows.length > 0;
324
359
  }
325
360
 
326
361
  /**
327
- * Search skills by metadata
362
+ * Search skills by metadata within a tenant
328
363
  */
329
364
  async searchByMetadata(
365
+ tenantId: string,
330
366
  metadataKey: string,
331
367
  metadataValue: string
332
368
  ): Promise<Skill[]> {
@@ -334,6 +370,7 @@ export class PostgreSQLSkillStore implements SkillStore {
334
370
 
335
371
  const result = await this.pool.query<{
336
372
  id: string;
373
+ tenant_id: string;
337
374
  name: string;
338
375
  description: string;
339
376
  license: string | null;
@@ -345,25 +382,29 @@ export class PostgreSQLSkillStore implements SkillStore {
345
382
  updated_at: Date;
346
383
  }>(
347
384
  `
348
- SELECT id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
385
+ SELECT id, tenant_id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
349
386
  FROM lattice_skills
350
- WHERE metadata->>$1 = $2
387
+ WHERE tenant_id = $1 AND metadata ->> $2 = $3
351
388
  ORDER BY created_at DESC
352
389
  `,
353
- [metadataKey, metadataValue]
390
+ [tenantId, metadataKey, metadataValue]
354
391
  );
355
392
 
356
393
  return result.rows.map(this.mapRowToSkill);
357
394
  }
358
395
 
359
396
  /**
360
- * Filter skills by compatibility
397
+ * Filter skills by compatibility within a tenant
361
398
  */
362
- async filterByCompatibility(compatibility: string): Promise<Skill[]> {
399
+ async filterByCompatibility(
400
+ tenantId: string,
401
+ compatibility: string
402
+ ): Promise<Skill[]> {
363
403
  await this.ensureInitialized();
364
404
 
365
405
  const result = await this.pool.query<{
366
406
  id: string;
407
+ tenant_id: string;
367
408
  name: string;
368
409
  description: string;
369
410
  license: string | null;
@@ -375,25 +416,29 @@ export class PostgreSQLSkillStore implements SkillStore {
375
416
  updated_at: Date;
376
417
  }>(
377
418
  `
378
- SELECT id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
419
+ SELECT id, tenant_id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
379
420
  FROM lattice_skills
380
- WHERE compatibility = $1
421
+ WHERE tenant_id = $1 AND compatibility = $2
381
422
  ORDER BY created_at DESC
382
423
  `,
383
- [compatibility]
424
+ [tenantId, compatibility]
384
425
  );
385
426
 
386
427
  return result.rows.map(this.mapRowToSkill);
387
428
  }
388
429
 
389
430
  /**
390
- * Filter skills by license
431
+ * Filter skills by license within a tenant
391
432
  */
392
- async filterByLicense(license: string): Promise<Skill[]> {
433
+ async filterByLicense(
434
+ tenantId: string,
435
+ license: string
436
+ ): Promise<Skill[]> {
393
437
  await this.ensureInitialized();
394
438
 
395
439
  const result = await this.pool.query<{
396
440
  id: string;
441
+ tenant_id: string;
397
442
  name: string;
398
443
  description: string;
399
444
  license: string | null;
@@ -405,32 +450,30 @@ export class PostgreSQLSkillStore implements SkillStore {
405
450
  updated_at: Date;
406
451
  }>(
407
452
  `
408
- SELECT id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
453
+ SELECT id, tenant_id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
409
454
  FROM lattice_skills
410
- WHERE license = $1
455
+ WHERE tenant_id = $1 AND license = $2
411
456
  ORDER BY created_at DESC
412
457
  `,
413
- [license]
458
+ [tenantId, license]
414
459
  );
415
460
 
416
461
  return result.rows.map(this.mapRowToSkill);
417
462
  }
418
463
 
419
464
  /**
420
- * Get sub-skills of a parent skill
465
+ * Get sub-skills of a parent skill within a tenant
466
+ * Note: This searches for skills that have the parent skill name in their subSkills array
421
467
  */
422
- async getSubSkills(parentSkillName: string): Promise<Skill[]> {
468
+ async getSubSkills(
469
+ tenantId: string,
470
+ parentSkillName: string
471
+ ): Promise<Skill[]> {
423
472
  await this.ensureInitialized();
424
473
 
425
- const parentSkill = await this.getSkillById(parentSkillName);
426
- if (!parentSkill || !parentSkill.subSkills || parentSkill.subSkills.length === 0) {
427
- return [];
428
- }
429
-
430
- // Query all sub-skills in one query
431
- const placeholders = parentSkill.subSkills.map((_, index) => `$${index + 1}`).join(", ");
432
474
  const result = await this.pool.query<{
433
475
  id: string;
476
+ tenant_id: string;
434
477
  name: string;
435
478
  description: string;
436
479
  license: string | null;
@@ -442,12 +485,12 @@ export class PostgreSQLSkillStore implements SkillStore {
442
485
  updated_at: Date;
443
486
  }>(
444
487
  `
445
- SELECT id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
488
+ SELECT id, tenant_id, name, description, license, compatibility, metadata, content, sub_skills, created_at, updated_at
446
489
  FROM lattice_skills
447
- WHERE name IN (${placeholders})
490
+ WHERE tenant_id = $1 AND sub_skills @> to_jsonb($2::text)
448
491
  ORDER BY created_at DESC
449
492
  `,
450
- parentSkill.subSkills
493
+ [tenantId, JSON.stringify([parentSkillName])]
451
494
  );
452
495
 
453
496
  return result.rows.map(this.mapRowToSkill);