@axiom-lattice/pg-stores 1.0.13 → 1.0.14

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,282 @@
1
+ /**
2
+ * PostgreSQL implementation of ProjectStore
3
+ */
4
+
5
+ import { Pool, PoolConfig } from "pg";
6
+ import {
7
+ ProjectStore,
8
+ Project,
9
+ CreateProjectRequest,
10
+ UpdateProjectRequest,
11
+ } from "@axiom-lattice/protocols";
12
+ import { MigrationManager } from "../migrations/migration";
13
+ import { createProjectsTable } from "../migrations/project_migrations";
14
+
15
+ /**
16
+ * PostgreSQL ProjectStore options
17
+ */
18
+ export interface PostgreSQLProjectStoreOptions {
19
+ /**
20
+ * PostgreSQL connection pool configuration
21
+ * Can be a connection string or PoolConfig object
22
+ */
23
+ poolConfig: string | PoolConfig;
24
+
25
+ /**
26
+ * Whether to run migrations automatically on initialization
27
+ * @default true
28
+ */
29
+ autoMigrate?: boolean;
30
+ }
31
+
32
+ /**
33
+ * PostgreSQL implementation of ProjectStore
34
+ */
35
+ export class PostgreSQLProjectStore implements ProjectStore {
36
+ private pool: Pool;
37
+ private migrationManager: MigrationManager;
38
+ private initialized: boolean = false;
39
+ private ownsPool: boolean = true;
40
+
41
+ constructor(options: PostgreSQLProjectStoreOptions) {
42
+ // Create Pool from config
43
+ if (typeof options.poolConfig === "string") {
44
+ this.pool = new Pool({ connectionString: options.poolConfig });
45
+ } else {
46
+ this.pool = new Pool(options.poolConfig);
47
+ }
48
+
49
+ this.migrationManager = new MigrationManager(this.pool);
50
+ this.migrationManager.register(createProjectsTable);
51
+
52
+ // Auto-migrate by default
53
+ if (options.autoMigrate !== false) {
54
+ this.initialize().catch((error) => {
55
+ console.error("Failed to initialize PostgreSQLProjectStore:", error);
56
+ throw error;
57
+ });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Dispose resources and close the connection pool
63
+ * Should be called when the store is no longer needed
64
+ */
65
+ async dispose(): Promise<void> {
66
+ if (this.ownsPool && this.pool) {
67
+ await this.pool.end();
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Initialize the store and run migrations
73
+ */
74
+ async initialize(): Promise<void> {
75
+ if (this.initialized) {
76
+ return;
77
+ }
78
+
79
+ await this.migrationManager.migrate();
80
+ this.initialized = true;
81
+ }
82
+
83
+ /**
84
+ * Ensure store is initialized
85
+ */
86
+ private async ensureInitialized(): Promise<void> {
87
+ if (!this.initialized) {
88
+ await this.initialize();
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Map database row to Project object
94
+ */
95
+ private mapRowToProject(row: {
96
+ id: string;
97
+ tenant_id: string;
98
+ workspace_id: string;
99
+ name: string;
100
+ description: string | null;
101
+ created_at: Date;
102
+ updated_at: Date;
103
+ }): Project {
104
+ return {
105
+ id: row.id,
106
+ tenantId: row.tenant_id,
107
+ workspaceId: row.workspace_id,
108
+ name: row.name,
109
+ description: row.description || undefined,
110
+ createdAt: row.created_at,
111
+ updatedAt: row.updated_at,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Get all projects for a specific workspace
117
+ */
118
+ async getProjectsByWorkspace(tenantId: string, workspaceId: string): Promise<Project[]> {
119
+ await this.ensureInitialized();
120
+
121
+ const result = await this.pool.query<{
122
+ id: string;
123
+ tenant_id: string;
124
+ workspace_id: string;
125
+ name: string;
126
+ description: string | null;
127
+ created_at: Date;
128
+ updated_at: Date;
129
+ }>(
130
+ `
131
+ SELECT id, tenant_id, workspace_id, name, description, created_at, updated_at
132
+ FROM lattice_projects
133
+ WHERE tenant_id = $1 AND workspace_id = $2
134
+ ORDER BY created_at DESC
135
+ `,
136
+ [tenantId, workspaceId]
137
+ );
138
+
139
+ return result.rows.map(this.mapRowToProject);
140
+ }
141
+
142
+ /**
143
+ * Get a project by ID for a specific tenant
144
+ */
145
+ async getProjectById(tenantId: string, id: string): Promise<Project | null> {
146
+ await this.ensureInitialized();
147
+
148
+ const result = await this.pool.query<{
149
+ id: string;
150
+ tenant_id: string;
151
+ workspace_id: string;
152
+ name: string;
153
+ description: string | null;
154
+ created_at: Date;
155
+ updated_at: Date;
156
+ }>(
157
+ `
158
+ SELECT id, tenant_id, workspace_id, name, description, created_at, updated_at
159
+ FROM lattice_projects
160
+ WHERE id = $1 AND tenant_id = $2
161
+ `,
162
+ [id, tenantId]
163
+ );
164
+
165
+ if (result.rows.length === 0) {
166
+ return null;
167
+ }
168
+
169
+ return this.mapRowToProject(result.rows[0]);
170
+ }
171
+
172
+ /**
173
+ * Create a new project
174
+ */
175
+ async createProject(
176
+ tenantId: string,
177
+ workspaceId: string,
178
+ id: string,
179
+ data: CreateProjectRequest
180
+ ): Promise<Project> {
181
+ await this.ensureInitialized();
182
+
183
+ const now = new Date();
184
+
185
+ await this.pool.query(
186
+ `
187
+ INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, created_at, updated_at)
188
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
189
+ ON CONFLICT (id, tenant_id) DO UPDATE SET
190
+ workspace_id = EXCLUDED.workspace_id,
191
+ name = EXCLUDED.name,
192
+ description = EXCLUDED.description,
193
+ updated_at = EXCLUDED.updated_at
194
+ `,
195
+ [id, tenantId, workspaceId, data.name, data.description || null, now, now]
196
+ );
197
+
198
+ return {
199
+ id,
200
+ tenantId,
201
+ workspaceId,
202
+ name: data.name,
203
+ description: data.description,
204
+ createdAt: now,
205
+ updatedAt: now,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Update an existing project
211
+ */
212
+ async updateProject(
213
+ tenantId: string,
214
+ id: string,
215
+ updates: UpdateProjectRequest
216
+ ): Promise<Project | null> {
217
+ await this.ensureInitialized();
218
+
219
+ // Get existing project
220
+ const existing = await this.getProjectById(tenantId, id);
221
+ if (!existing) {
222
+ return null;
223
+ }
224
+
225
+ // Build update query dynamically based on provided fields
226
+ const updateFields: string[] = [];
227
+ const updateValues: any[] = [];
228
+ let paramIndex = 1;
229
+
230
+ if (updates.name !== undefined) {
231
+ updateFields.push(`name = $${paramIndex++}`);
232
+ updateValues.push(updates.name);
233
+ }
234
+
235
+ if (updates.description !== undefined) {
236
+ updateFields.push(`description = $${paramIndex++}`);
237
+ updateValues.push(updates.description || null);
238
+ }
239
+
240
+ if (updateFields.length === 0) {
241
+ // No fields to update
242
+ return existing;
243
+ }
244
+
245
+ // Always update updated_at
246
+ updateFields.push(`updated_at = $${paramIndex++}`);
247
+ updateValues.push(new Date());
248
+
249
+ // Add id and tenant_id for WHERE clause
250
+ updateValues.push(id);
251
+ updateValues.push(tenantId);
252
+
253
+ await this.pool.query(
254
+ `
255
+ UPDATE lattice_projects
256
+ SET ${updateFields.join(", ")}
257
+ WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1}
258
+ `,
259
+ updateValues
260
+ );
261
+
262
+ // Return updated project
263
+ return await this.getProjectById(tenantId, id);
264
+ }
265
+
266
+ /**
267
+ * Delete a project by ID
268
+ */
269
+ async deleteProject(tenantId: string, id: string): Promise<boolean> {
270
+ await this.ensureInitialized();
271
+
272
+ const result = await this.pool.query(
273
+ `
274
+ DELETE FROM lattice_projects
275
+ WHERE id = $1 AND tenant_id = $2
276
+ `,
277
+ [id, tenantId]
278
+ );
279
+
280
+ return result.rowCount !== null && result.rowCount > 0;
281
+ }
282
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * PostgreSQL implementation of WorkspaceStore
3
+ */
4
+
5
+ import { Pool, PoolConfig } from "pg";
6
+ import {
7
+ WorkspaceStore,
8
+ Workspace,
9
+ CreateWorkspaceRequest,
10
+ UpdateWorkspaceRequest,
11
+ } from "@axiom-lattice/protocols";
12
+ import { MigrationManager } from "../migrations/migration";
13
+ import { createWorkspacesTable } from "../migrations/workspace_migrations";
14
+
15
+ /**
16
+ * PostgreSQL WorkspaceStore options
17
+ */
18
+ export interface PostgreSQLWorkspaceStoreOptions {
19
+ /**
20
+ * PostgreSQL connection pool configuration
21
+ * Can be a connection string or PoolConfig object
22
+ */
23
+ poolConfig: string | PoolConfig;
24
+
25
+ /**
26
+ * Whether to run migrations automatically on initialization
27
+ * @default true
28
+ */
29
+ autoMigrate?: boolean;
30
+ }
31
+
32
+ /**
33
+ * PostgreSQL implementation of WorkspaceStore
34
+ */
35
+ export class PostgreSQLWorkspaceStore implements WorkspaceStore {
36
+ private pool: Pool;
37
+ private migrationManager: MigrationManager;
38
+ private initialized: boolean = false;
39
+ private ownsPool: boolean = true;
40
+
41
+ constructor(options: PostgreSQLWorkspaceStoreOptions) {
42
+ // Create Pool from config
43
+ if (typeof options.poolConfig === "string") {
44
+ this.pool = new Pool({ connectionString: options.poolConfig });
45
+ } else {
46
+ this.pool = new Pool(options.poolConfig);
47
+ }
48
+
49
+ this.migrationManager = new MigrationManager(this.pool);
50
+ this.migrationManager.register(createWorkspacesTable);
51
+
52
+ // Auto-migrate by default
53
+ if (options.autoMigrate !== false) {
54
+ this.initialize().catch((error) => {
55
+ console.error("Failed to initialize PostgreSQLWorkspaceStore:", error);
56
+ throw error;
57
+ });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Dispose resources and close the connection pool
63
+ * Should be called when the store is no longer needed
64
+ */
65
+ async dispose(): Promise<void> {
66
+ if (this.ownsPool && this.pool) {
67
+ await this.pool.end();
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Initialize the store and run migrations
73
+ */
74
+ async initialize(): Promise<void> {
75
+ if (this.initialized) {
76
+ return;
77
+ }
78
+
79
+ await this.migrationManager.migrate();
80
+ this.initialized = true;
81
+ }
82
+
83
+ /**
84
+ * Ensure store is initialized
85
+ */
86
+ private async ensureInitialized(): Promise<void> {
87
+ if (!this.initialized) {
88
+ await this.initialize();
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Map database row to Workspace object
94
+ */
95
+ private mapRowToWorkspace(row: {
96
+ id: string;
97
+ tenant_id: string;
98
+ name: string;
99
+ description: string | null;
100
+ storage_type: string;
101
+ created_at: Date;
102
+ updated_at: Date;
103
+ }): Workspace {
104
+ return {
105
+ id: row.id,
106
+ tenantId: row.tenant_id,
107
+ name: row.name,
108
+ description: row.description || undefined,
109
+ storageType: row.storage_type as Workspace["storageType"],
110
+ createdAt: row.created_at,
111
+ updatedAt: row.updated_at,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Get all workspaces for a tenant
117
+ */
118
+ async getAllWorkspaces(tenantId: string): Promise<Workspace[]> {
119
+ await this.ensureInitialized();
120
+
121
+ const result = await this.pool.query<{
122
+ id: string;
123
+ tenant_id: string;
124
+ name: string;
125
+ description: string | null;
126
+ storage_type: string;
127
+ created_at: Date;
128
+ updated_at: Date;
129
+ }>(
130
+ `
131
+ SELECT id, tenant_id, name, description, storage_type, created_at, updated_at
132
+ FROM lattice_workspaces
133
+ WHERE tenant_id = $1
134
+ ORDER BY created_at DESC
135
+ `,
136
+ [tenantId]
137
+ );
138
+
139
+ return result.rows.map(this.mapRowToWorkspace);
140
+ }
141
+
142
+ /**
143
+ * Get a workspace by ID for a specific tenant
144
+ */
145
+ async getWorkspaceById(tenantId: string, id: string): Promise<Workspace | null> {
146
+ await this.ensureInitialized();
147
+
148
+ const result = await this.pool.query<{
149
+ id: string;
150
+ tenant_id: string;
151
+ name: string;
152
+ description: string | null;
153
+ storage_type: string;
154
+ created_at: Date;
155
+ updated_at: Date;
156
+ }>(
157
+ `
158
+ SELECT id, tenant_id, name, description, storage_type, created_at, updated_at
159
+ FROM lattice_workspaces
160
+ WHERE id = $1 AND tenant_id = $2
161
+ `,
162
+ [id, tenantId]
163
+ );
164
+
165
+ if (result.rows.length === 0) {
166
+ return null;
167
+ }
168
+
169
+ return this.mapRowToWorkspace(result.rows[0]);
170
+ }
171
+
172
+ /**
173
+ * Create a new workspace
174
+ */
175
+ async createWorkspace(
176
+ tenantId: string,
177
+ id: string,
178
+ data: CreateWorkspaceRequest
179
+ ): Promise<Workspace> {
180
+ await this.ensureInitialized();
181
+
182
+ const now = new Date();
183
+
184
+ await this.pool.query(
185
+ `
186
+ INSERT INTO lattice_workspaces (id, tenant_id, name, description, storage_type, created_at, updated_at)
187
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
188
+ ON CONFLICT (id, tenant_id) DO UPDATE SET
189
+ name = EXCLUDED.name,
190
+ description = EXCLUDED.description,
191
+ storage_type = EXCLUDED.storage_type,
192
+ updated_at = EXCLUDED.updated_at
193
+ `,
194
+ [id, tenantId, data.name, data.description || null, data.storageType, now, now]
195
+ );
196
+
197
+ return {
198
+ id,
199
+ tenantId,
200
+ name: data.name,
201
+ description: data.description,
202
+ storageType: data.storageType,
203
+ createdAt: now,
204
+ updatedAt: now,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Update an existing workspace
210
+ */
211
+ async updateWorkspace(
212
+ tenantId: string,
213
+ id: string,
214
+ updates: UpdateWorkspaceRequest
215
+ ): Promise<Workspace | null> {
216
+ await this.ensureInitialized();
217
+
218
+ // Get existing workspace
219
+ const existing = await this.getWorkspaceById(tenantId, id);
220
+ if (!existing) {
221
+ return null;
222
+ }
223
+
224
+ // Build update query dynamically based on provided fields
225
+ const updateFields: string[] = [];
226
+ const updateValues: any[] = [];
227
+ let paramIndex = 1;
228
+
229
+ if (updates.name !== undefined) {
230
+ updateFields.push(`name = $${paramIndex++}`);
231
+ updateValues.push(updates.name);
232
+ }
233
+
234
+ if (updates.description !== undefined) {
235
+ updateFields.push(`description = $${paramIndex++}`);
236
+ updateValues.push(updates.description || null);
237
+ }
238
+
239
+ if (updates.storageType !== undefined) {
240
+ updateFields.push(`storage_type = $${paramIndex++}`);
241
+ updateValues.push(updates.storageType);
242
+ }
243
+
244
+ if (updateFields.length === 0) {
245
+ // No fields to update
246
+ return existing;
247
+ }
248
+
249
+ // Always update updated_at
250
+ updateFields.push(`updated_at = $${paramIndex++}`);
251
+ updateValues.push(new Date());
252
+
253
+ // Add id and tenant_id for WHERE clause
254
+ updateValues.push(id);
255
+ updateValues.push(tenantId);
256
+
257
+ await this.pool.query(
258
+ `
259
+ UPDATE lattice_workspaces
260
+ SET ${updateFields.join(", ")}
261
+ WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1}
262
+ `,
263
+ updateValues
264
+ );
265
+
266
+ // Return updated workspace
267
+ return await this.getWorkspaceById(tenantId, id);
268
+ }
269
+
270
+ /**
271
+ * Delete a workspace by ID
272
+ */
273
+ async deleteWorkspace(tenantId: string, id: string): Promise<boolean> {
274
+ await this.ensureInitialized();
275
+
276
+ const result = await this.pool.query(
277
+ `
278
+ DELETE FROM lattice_workspaces
279
+ WHERE id = $1 AND tenant_id = $2
280
+ `,
281
+ [id, tenantId]
282
+ );
283
+
284
+ return result.rowCount !== null && result.rowCount > 0;
285
+ }
286
+ }