@axiom-lattice/pg-stores 1.0.4 → 1.0.5

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