@axiom-lattice/pg-stores 1.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/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * PostgreSQL Stores Package
3
+ *
4
+ * Provides PostgreSQL implementations for various stores
5
+ */
6
+
7
+ export * from "./stores/PostgreSQLThreadStore";
8
+ export * from "./stores/PostgreSQLAssistantStore";
9
+ export * from "./migrations/migration";
10
+ export * from "./migrations/thread_migrations";
11
+ export * from "./migrations/assistant_migrations";
12
+
13
+ // Re-export for convenience
14
+ export { PostgreSQLThreadStore } from "./stores/PostgreSQLThreadStore";
15
+ export { PostgreSQLAssistantStore } from "./stores/PostgreSQLAssistantStore";
16
+
17
+ // Re-export types from protocols
18
+ export type {
19
+ ThreadStore,
20
+ Thread,
21
+ CreateThreadRequest,
22
+ AssistantStore,
23
+ Assistant,
24
+ CreateAssistantRequest,
25
+ } from "@axiom-lattice/protocols";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Assistant table migrations
3
+ */
4
+
5
+ import { PoolClient } from "pg";
6
+ import { Migration } from "./migration";
7
+
8
+ /**
9
+ * Initial migration: Create assistants table
10
+ */
11
+ export const createAssistantsTable: Migration = {
12
+ version: 1,
13
+ name: "create_assistants_table",
14
+ up: async (client: PoolClient) => {
15
+ await client.query(`
16
+ CREATE TABLE IF NOT EXISTS lattice_assistants (
17
+ id VARCHAR(255) PRIMARY KEY,
18
+ name VARCHAR(255) NOT NULL,
19
+ description TEXT,
20
+ graph_definition JSONB NOT NULL,
21
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
22
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
23
+ )
24
+ `);
25
+
26
+ // Create indexes for better query performance
27
+ await client.query(`
28
+ CREATE INDEX IF NOT EXISTS idx_lattice_assistants_name
29
+ ON lattice_assistants(name)
30
+ `);
31
+
32
+ await client.query(`
33
+ CREATE INDEX IF NOT EXISTS idx_lattice_assistants_created_at
34
+ ON lattice_assistants(created_at DESC)
35
+ `);
36
+ },
37
+ down: async (client: PoolClient) => {
38
+ await client.query(
39
+ "DROP INDEX IF EXISTS idx_lattice_assistants_created_at"
40
+ );
41
+ await client.query("DROP INDEX IF EXISTS idx_lattice_assistants_name");
42
+ await client.query("DROP TABLE IF EXISTS lattice_assistants");
43
+ },
44
+ };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Migration system for database schema management
3
+ */
4
+
5
+ import { Pool, PoolClient } from "pg";
6
+
7
+ /**
8
+ * Migration record stored in database
9
+ */
10
+ interface MigrationRecord {
11
+ version: number;
12
+ name: string;
13
+ applied_at: Date;
14
+ }
15
+
16
+ /**
17
+ * Migration definition
18
+ */
19
+ export interface Migration {
20
+ version: number;
21
+ name: string;
22
+ up: (client: PoolClient) => Promise<void>;
23
+ down?: (client: PoolClient) => Promise<void>;
24
+ }
25
+
26
+ /**
27
+ * Migration manager
28
+ */
29
+ export class MigrationManager {
30
+ private pool: Pool;
31
+ private migrations: Migration[] = [];
32
+
33
+ constructor(pool: Pool) {
34
+ this.pool = pool;
35
+ }
36
+
37
+ /**
38
+ * Register a migration
39
+ */
40
+ register(migration: Migration): void {
41
+ this.migrations.push(migration);
42
+ // Sort migrations by version
43
+ this.migrations.sort((a, b) => a.version - b.version);
44
+ }
45
+
46
+ /**
47
+ * Initialize migrations table if it doesn't exist
48
+ */
49
+ private async ensureMigrationsTable(client: PoolClient): Promise<void> {
50
+ await client.query(`
51
+ CREATE TABLE IF NOT EXISTS lattice_schema_migrations (
52
+ version INTEGER PRIMARY KEY,
53
+ name VARCHAR(255) NOT NULL,
54
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
55
+ )
56
+ `);
57
+ }
58
+
59
+ /**
60
+ * Get applied migrations from database
61
+ */
62
+ private async getAppliedMigrations(
63
+ client: PoolClient
64
+ ): Promise<MigrationRecord[]> {
65
+ await client.query(`
66
+ CREATE TABLE IF NOT EXISTS lattice_schema_migrations (
67
+ version INTEGER PRIMARY KEY,
68
+ name VARCHAR(255) NOT NULL,
69
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
70
+ )
71
+ `);
72
+
73
+ const result = await client.query<MigrationRecord>(
74
+ "SELECT version, name, applied_at FROM lattice_schema_migrations ORDER BY version"
75
+ );
76
+ return result.rows;
77
+ }
78
+
79
+ /**
80
+ * Apply pending migrations
81
+ */
82
+ async migrate(): Promise<void> {
83
+ const client = await this.pool.connect();
84
+ try {
85
+ await client.query("BEGIN");
86
+
87
+ await this.ensureMigrationsTable(client);
88
+ const appliedMigrations = await this.getAppliedMigrations(client);
89
+ const appliedVersions = new Set(appliedMigrations.map((m) => m.version));
90
+
91
+ // Find pending migrations
92
+ const pendingMigrations = this.migrations.filter(
93
+ (m) => !appliedVersions.has(m.version)
94
+ );
95
+
96
+ if (pendingMigrations.length === 0) {
97
+ console.log("No pending migrations");
98
+ await client.query("COMMIT");
99
+ return;
100
+ }
101
+
102
+ // Apply pending migrations
103
+ for (const migration of pendingMigrations) {
104
+ console.log(
105
+ `Applying migration ${migration.version}: ${migration.name}`
106
+ );
107
+ await migration.up(client);
108
+ await client.query(
109
+ "INSERT INTO lattice_schema_migrations (version, name) VALUES ($1, $2)",
110
+ [migration.version, migration.name]
111
+ );
112
+ }
113
+
114
+ await client.query("COMMIT");
115
+ console.log(`Applied ${pendingMigrations.length} migration(s)`);
116
+ } catch (error) {
117
+ await client.query("ROLLBACK");
118
+ throw error;
119
+ } finally {
120
+ client.release();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Rollback last migration
126
+ */
127
+ async rollback(): Promise<void> {
128
+ const client = await this.pool.connect();
129
+ try {
130
+ await client.query("BEGIN");
131
+
132
+ const appliedMigrations = await this.getAppliedMigrations(client);
133
+ if (appliedMigrations.length === 0) {
134
+ console.log("No migrations to rollback");
135
+ await client.query("COMMIT");
136
+ return;
137
+ }
138
+
139
+ const lastMigration = appliedMigrations[appliedMigrations.length - 1];
140
+ const migration = this.migrations.find(
141
+ (m) => m.version === lastMigration.version
142
+ );
143
+
144
+ if (!migration || !migration.down) {
145
+ throw new Error(
146
+ `Migration ${lastMigration.version} does not have a down migration`
147
+ );
148
+ }
149
+
150
+ console.log(
151
+ `Rolling back migration ${lastMigration.version}: ${lastMigration.name}`
152
+ );
153
+ await migration.down(client);
154
+ await client.query(
155
+ "DELETE FROM lattice_schema_migrations WHERE version = $1",
156
+ [lastMigration.version]
157
+ );
158
+
159
+ await client.query("COMMIT");
160
+ console.log("Rollback completed");
161
+ } catch (error) {
162
+ await client.query("ROLLBACK");
163
+ throw error;
164
+ } finally {
165
+ client.release();
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get current migration version
171
+ */
172
+ async getCurrentVersion(): Promise<number> {
173
+ const client = await this.pool.connect();
174
+ try {
175
+ const appliedMigrations = await this.getAppliedMigrations(client);
176
+ if (appliedMigrations.length === 0) {
177
+ return 0;
178
+ }
179
+ return Math.max(...appliedMigrations.map((m) => m.version));
180
+ } finally {
181
+ client.release();
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Thread table migrations
3
+ */
4
+
5
+ import { PoolClient } from "pg";
6
+ import { Migration } from "./migration";
7
+
8
+ /**
9
+ * Initial migration: Create threads table
10
+ */
11
+ export const createThreadsTable: Migration = {
12
+ version: 1,
13
+ name: "create_threads_table",
14
+ up: async (client: PoolClient) => {
15
+ await client.query(`
16
+ CREATE TABLE IF NOT EXISTS lattice_threads (
17
+ id VARCHAR(255) NOT NULL,
18
+ assistant_id VARCHAR(255) NOT NULL,
19
+ metadata JSONB DEFAULT '{}',
20
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
21
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
22
+ PRIMARY KEY (id, assistant_id)
23
+ )
24
+ `);
25
+
26
+ // Create indexes for better query performance
27
+ await client.query(`
28
+ CREATE INDEX IF NOT EXISTS idx_lattice_threads_assistant_id
29
+ ON lattice_threads(assistant_id)
30
+ `);
31
+
32
+ await client.query(`
33
+ CREATE INDEX IF NOT EXISTS idx_lattice_threads_created_at
34
+ ON lattice_threads(created_at DESC)
35
+ `);
36
+ },
37
+ down: async (client: PoolClient) => {
38
+ await client.query("DROP INDEX IF EXISTS idx_lattice_threads_created_at");
39
+ await client.query("DROP INDEX IF EXISTS idx_lattice_threads_assistant_id");
40
+ await client.query("DROP TABLE IF EXISTS lattice_threads");
41
+ },
42
+ };
@@ -0,0 +1,303 @@
1
+ /**
2
+ * PostgreSQL implementation of AssistantStore
3
+ */
4
+
5
+ import { Pool, PoolClient, PoolConfig } from "pg";
6
+ import {
7
+ AssistantStore,
8
+ Assistant,
9
+ CreateAssistantRequest,
10
+ } from "@axiom-lattice/protocols";
11
+ import { MigrationManager } from "../migrations/migration";
12
+ import { createAssistantsTable } from "../migrations/assistant_migrations";
13
+
14
+ /**
15
+ * PostgreSQL AssistantStore options
16
+ */
17
+ export interface PostgreSQLAssistantStoreOptions {
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 AssistantStore
33
+ */
34
+ export class PostgreSQLAssistantStore implements AssistantStore {
35
+ private pool: Pool;
36
+ private migrationManager: MigrationManager;
37
+ private initialized: boolean = false;
38
+ private ownsPool: boolean = true;
39
+
40
+ constructor(options: PostgreSQLAssistantStoreOptions) {
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(createAssistantsTable);
50
+
51
+ // Auto-migrate by default
52
+ if (options.autoMigrate !== false) {
53
+ this.initialize().catch((error) => {
54
+ console.error("Failed to initialize PostgreSQLAssistantStore:", error);
55
+ throw error;
56
+ });
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Initialize the store and run migrations
62
+ */
63
+ async initialize(): Promise<void> {
64
+ if (this.initialized) {
65
+ return;
66
+ }
67
+
68
+ await this.migrationManager.migrate();
69
+ this.initialized = true;
70
+ }
71
+
72
+ /**
73
+ * Get all assistants
74
+ */
75
+ async getAllAssistants(): Promise<Assistant[]> {
76
+ await this.ensureInitialized();
77
+
78
+ const result = await this.pool.query<{
79
+ id: string;
80
+ name: string;
81
+ description: string | null;
82
+ graph_definition: any;
83
+ created_at: Date;
84
+ updated_at: Date;
85
+ }>(
86
+ `
87
+ SELECT id, name, description, graph_definition, created_at, updated_at
88
+ FROM lattice_assistants
89
+ ORDER BY created_at DESC
90
+ `
91
+ );
92
+
93
+ return result.rows.map(this.mapRowToAssistant);
94
+ }
95
+
96
+ /**
97
+ * Get assistant by ID
98
+ */
99
+ async getAssistantById(id: string): Promise<Assistant | null> {
100
+ await this.ensureInitialized();
101
+
102
+ const result = await this.pool.query<{
103
+ id: string;
104
+ name: string;
105
+ description: string | null;
106
+ graph_definition: any;
107
+ created_at: Date;
108
+ updated_at: Date;
109
+ }>(
110
+ `
111
+ SELECT id, name, description, graph_definition, created_at, updated_at
112
+ FROM lattice_assistants
113
+ WHERE id = $1
114
+ `,
115
+ [id]
116
+ );
117
+
118
+ if (result.rows.length === 0) {
119
+ return null;
120
+ }
121
+
122
+ return this.mapRowToAssistant(result.rows[0]);
123
+ }
124
+
125
+ /**
126
+ * Create a new assistant
127
+ */
128
+ async createAssistant(
129
+ id: string,
130
+ data: CreateAssistantRequest
131
+ ): Promise<Assistant> {
132
+ await this.ensureInitialized();
133
+
134
+ const now = new Date();
135
+
136
+ await this.pool.query(
137
+ `
138
+ INSERT INTO lattice_assistants (id, name, description, graph_definition, created_at, updated_at)
139
+ VALUES ($1, $2, $3, $4, $5, $6)
140
+ ON CONFLICT (id) DO UPDATE SET
141
+ name = EXCLUDED.name,
142
+ description = EXCLUDED.description,
143
+ graph_definition = EXCLUDED.graph_definition,
144
+ updated_at = EXCLUDED.updated_at
145
+ `,
146
+ [
147
+ id,
148
+ data.name,
149
+ data.description || null,
150
+ JSON.stringify(data.graphDefinition),
151
+ now,
152
+ now,
153
+ ]
154
+ );
155
+
156
+ return {
157
+ id,
158
+ name: data.name,
159
+ description: data.description,
160
+ graphDefinition: data.graphDefinition,
161
+ createdAt: now,
162
+ updatedAt: now,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Update an existing assistant
168
+ */
169
+ async updateAssistant(
170
+ id: string,
171
+ updates: Partial<CreateAssistantRequest>
172
+ ): Promise<Assistant | null> {
173
+ await this.ensureInitialized();
174
+
175
+ // Get existing assistant
176
+ const existing = await this.getAssistantById(id);
177
+ if (!existing) {
178
+ return null;
179
+ }
180
+
181
+ // Build update query dynamically based on provided fields
182
+ const updateFields: string[] = [];
183
+ const updateValues: any[] = [];
184
+ let paramIndex = 1;
185
+
186
+ if (updates.name !== undefined) {
187
+ updateFields.push(`name = $${paramIndex++}`);
188
+ updateValues.push(updates.name);
189
+ }
190
+
191
+ if (updates.description !== undefined) {
192
+ updateFields.push(`description = $${paramIndex++}`);
193
+ updateValues.push(updates.description || null);
194
+ }
195
+
196
+ if (updates.graphDefinition !== undefined) {
197
+ updateFields.push(`graph_definition = $${paramIndex++}`);
198
+ updateValues.push(JSON.stringify(updates.graphDefinition));
199
+ }
200
+
201
+ if (updateFields.length === 0) {
202
+ // No fields to update
203
+ return existing;
204
+ }
205
+
206
+ // Always update updated_at
207
+ updateFields.push(`updated_at = $${paramIndex++}`);
208
+ updateValues.push(new Date());
209
+
210
+ // Add id for WHERE clause
211
+ updateValues.push(id);
212
+
213
+ await this.pool.query(
214
+ `
215
+ UPDATE lattice_assistants
216
+ SET ${updateFields.join(", ")}
217
+ WHERE id = $${paramIndex}
218
+ `,
219
+ updateValues
220
+ );
221
+
222
+ // Return updated assistant
223
+ return await this.getAssistantById(id);
224
+ }
225
+
226
+ /**
227
+ * Delete an assistant by ID
228
+ */
229
+ async deleteAssistant(id: string): Promise<boolean> {
230
+ await this.ensureInitialized();
231
+
232
+ const result = await this.pool.query(
233
+ `
234
+ DELETE FROM lattice_assistants
235
+ WHERE id = $1
236
+ `,
237
+ [id]
238
+ );
239
+
240
+ return result.rowCount !== null && result.rowCount > 0;
241
+ }
242
+
243
+ /**
244
+ * Check if assistant exists
245
+ */
246
+ async hasAssistant(id: string): Promise<boolean> {
247
+ await this.ensureInitialized();
248
+
249
+ const result = await this.pool.query(
250
+ `
251
+ SELECT 1 FROM lattice_assistants
252
+ WHERE id = $1
253
+ LIMIT 1
254
+ `,
255
+ [id]
256
+ );
257
+
258
+ return result.rows.length > 0;
259
+ }
260
+
261
+ /**
262
+ * Dispose resources and close the connection pool
263
+ * Should be called when the store is no longer needed
264
+ */
265
+ async dispose(): Promise<void> {
266
+ if (this.ownsPool && this.pool) {
267
+ await this.pool.end();
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Ensure store is initialized
273
+ */
274
+ private async ensureInitialized(): Promise<void> {
275
+ if (!this.initialized) {
276
+ await this.initialize();
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Map database row to Assistant object
282
+ */
283
+ private mapRowToAssistant(row: {
284
+ id: string;
285
+ name: string;
286
+ description: string | null;
287
+ graph_definition: any;
288
+ created_at: Date;
289
+ updated_at: Date;
290
+ }): Assistant {
291
+ return {
292
+ id: row.id,
293
+ name: row.name,
294
+ description: row.description || undefined,
295
+ graphDefinition:
296
+ typeof row.graph_definition === "string"
297
+ ? JSON.parse(row.graph_definition)
298
+ : row.graph_definition,
299
+ createdAt: row.created_at,
300
+ updatedAt: row.updated_at,
301
+ };
302
+ }
303
+ }