@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +9 -0
- package/README_DATABASE_CONFIG.md +217 -0
- package/dist/index.d.mts +253 -3
- package/dist/index.d.ts +253 -3
- package/dist/index.js +759 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +752 -1
- package/dist/index.mjs.map +1 -1
- package/examples/database-config-store.example.ts +178 -0
- package/examples/workspace-project-store.example.ts +151 -0
- package/package.json +3 -3
- package/src/__tests__/workspace-project-store.test.ts +235 -0
- package/src/index.ts +24 -0
- package/src/migrations/database_config_migrations.ts +47 -0
- package/src/migrations/project_migrations.ts +50 -0
- package/src/migrations/workspace_migrations.ts +44 -0
- package/src/stores/PostgreSQLDatabaseConfigStore.ts +434 -0
- package/src/stores/PostgreSQLProjectStore.ts +282 -0
- package/src/stores/PostgreSQLWorkspaceStore.ts +286 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project table migrations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PoolClient } from "pg";
|
|
6
|
+
import { Migration } from "./migration";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initial migration: Create projects table
|
|
10
|
+
*/
|
|
11
|
+
export const createProjectsTable: Migration = {
|
|
12
|
+
version: 6,
|
|
13
|
+
name: "create_projects_table",
|
|
14
|
+
up: async (client: PoolClient) => {
|
|
15
|
+
await client.query(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS lattice_projects (
|
|
17
|
+
id VARCHAR(255) NOT NULL,
|
|
18
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
19
|
+
workspace_id VARCHAR(255) NOT NULL,
|
|
20
|
+
name VARCHAR(255) NOT NULL,
|
|
21
|
+
description TEXT,
|
|
22
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
23
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
PRIMARY KEY (id, tenant_id)
|
|
25
|
+
)
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
// Create indexes for better query performance
|
|
29
|
+
await client.query(`
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_projects_tenant_id
|
|
31
|
+
ON lattice_projects(tenant_id)
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
await client.query(`
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_projects_workspace_id
|
|
36
|
+
ON lattice_projects(workspace_id)
|
|
37
|
+
`);
|
|
38
|
+
|
|
39
|
+
await client.query(`
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_projects_created_at
|
|
41
|
+
ON lattice_projects(created_at DESC)
|
|
42
|
+
`);
|
|
43
|
+
},
|
|
44
|
+
down: async (client: PoolClient) => {
|
|
45
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_projects_created_at");
|
|
46
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_projects_workspace_id");
|
|
47
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_projects_tenant_id");
|
|
48
|
+
await client.query("DROP TABLE IF EXISTS lattice_projects");
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace table migrations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PoolClient } from "pg";
|
|
6
|
+
import { Migration } from "./migration";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initial migration: Create workspaces table
|
|
10
|
+
*/
|
|
11
|
+
export const createWorkspacesTable: Migration = {
|
|
12
|
+
version: 5,
|
|
13
|
+
name: "create_workspaces_table",
|
|
14
|
+
up: async (client: PoolClient) => {
|
|
15
|
+
await client.query(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS lattice_workspaces (
|
|
17
|
+
id VARCHAR(255) NOT NULL,
|
|
18
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
19
|
+
name VARCHAR(255) NOT NULL,
|
|
20
|
+
description TEXT,
|
|
21
|
+
storage_type VARCHAR(50) NOT NULL DEFAULT 'sandbox',
|
|
22
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
23
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
PRIMARY KEY (id, tenant_id)
|
|
25
|
+
)
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
// Create indexes for better query performance
|
|
29
|
+
await client.query(`
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_workspaces_tenant_id
|
|
31
|
+
ON lattice_workspaces(tenant_id)
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
await client.query(`
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_workspaces_created_at
|
|
36
|
+
ON lattice_workspaces(created_at DESC)
|
|
37
|
+
`);
|
|
38
|
+
},
|
|
39
|
+
down: async (client: PoolClient) => {
|
|
40
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_workspaces_created_at");
|
|
41
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_workspaces_tenant_id");
|
|
42
|
+
await client.query("DROP TABLE IF EXISTS lattice_workspaces");
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL implementation of DatabaseConfigStore
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Pool, PoolClient, PoolConfig } from 'pg';
|
|
6
|
+
import {
|
|
7
|
+
DatabaseConfigStore,
|
|
8
|
+
DatabaseConfigEntry,
|
|
9
|
+
CreateDatabaseConfigRequest,
|
|
10
|
+
UpdateDatabaseConfigRequest,
|
|
11
|
+
DatabaseConfig,
|
|
12
|
+
} from '@axiom-lattice/protocols';
|
|
13
|
+
import { MigrationManager } from '../migrations/migration';
|
|
14
|
+
import { createDatabaseConfigsTable } from '../migrations/database_config_migrations';
|
|
15
|
+
import { encrypt, decrypt } from '@axiom-lattice/core';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* PostgreSQL DatabaseConfigStore options
|
|
19
|
+
*/
|
|
20
|
+
export interface PostgreSQLDatabaseConfigStoreOptions {
|
|
21
|
+
/**
|
|
22
|
+
* PostgreSQL connection pool configuration
|
|
23
|
+
* Can be a connection string or PoolConfig object
|
|
24
|
+
*/
|
|
25
|
+
poolConfig: string | PoolConfig;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether to run migrations automatically on initialization
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
autoMigrate?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* PostgreSQL implementation of DatabaseConfigStore
|
|
36
|
+
*
|
|
37
|
+
* Features:
|
|
38
|
+
* - Multi-tenant isolation via tenant_id
|
|
39
|
+
* - Automatic password encryption/decryption
|
|
40
|
+
* - Unique constraint on (tenant_id, key)
|
|
41
|
+
*/
|
|
42
|
+
export class PostgreSQLDatabaseConfigStore implements DatabaseConfigStore {
|
|
43
|
+
private pool: Pool;
|
|
44
|
+
private migrationManager: MigrationManager;
|
|
45
|
+
private initialized: boolean = false;
|
|
46
|
+
private initPromise: Promise<void> | null = null;
|
|
47
|
+
|
|
48
|
+
constructor(options: PostgreSQLDatabaseConfigStoreOptions) {
|
|
49
|
+
// Create Pool from config
|
|
50
|
+
if (typeof options.poolConfig === 'string') {
|
|
51
|
+
this.pool = new Pool({ connectionString: options.poolConfig });
|
|
52
|
+
} else {
|
|
53
|
+
this.pool = new Pool(options.poolConfig);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.migrationManager = new MigrationManager(this.pool);
|
|
57
|
+
this.migrationManager.register(createDatabaseConfigsTable);
|
|
58
|
+
|
|
59
|
+
// Auto-migrate by default
|
|
60
|
+
if (options.autoMigrate !== false) {
|
|
61
|
+
this.initialize().catch((error) => {
|
|
62
|
+
console.error('Failed to initialize PostgreSQLDatabaseConfigStore:', error);
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize the store and run migrations
|
|
70
|
+
* Uses a promise-based lock to prevent concurrent initialization
|
|
71
|
+
*/
|
|
72
|
+
async initialize(): Promise<void> {
|
|
73
|
+
if (this.initialized) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.initPromise) {
|
|
78
|
+
return this.initPromise;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.initPromise = (async () => {
|
|
82
|
+
try {
|
|
83
|
+
await this.migrationManager.migrate();
|
|
84
|
+
this.initialized = true;
|
|
85
|
+
} finally {
|
|
86
|
+
this.initPromise = null;
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
|
|
90
|
+
return this.initPromise;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get all database configurations for a tenant
|
|
95
|
+
*/
|
|
96
|
+
async getAllConfigs(tenantId: string): Promise<DatabaseConfigEntry[]> {
|
|
97
|
+
await this.ensureInitialized();
|
|
98
|
+
|
|
99
|
+
const result = await this.pool.query<{
|
|
100
|
+
id: string;
|
|
101
|
+
tenant_id: string;
|
|
102
|
+
key: string;
|
|
103
|
+
name: string | null;
|
|
104
|
+
description: string | null;
|
|
105
|
+
config: any;
|
|
106
|
+
created_at: Date;
|
|
107
|
+
updated_at: Date;
|
|
108
|
+
}>(
|
|
109
|
+
`
|
|
110
|
+
SELECT id, tenant_id, key, name, description, config, created_at, updated_at
|
|
111
|
+
FROM lattice_database_configs
|
|
112
|
+
WHERE tenant_id = $1
|
|
113
|
+
ORDER BY created_at DESC
|
|
114
|
+
`,
|
|
115
|
+
[tenantId]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return result.rows.map((row) => this.mapRowToEntry(row));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get all database configurations across all tenants
|
|
123
|
+
*/
|
|
124
|
+
async getAllConfigsWithoutTenant(): Promise<DatabaseConfigEntry[]> {
|
|
125
|
+
await this.ensureInitialized();
|
|
126
|
+
|
|
127
|
+
const result = await this.pool.query<{
|
|
128
|
+
id: string;
|
|
129
|
+
tenant_id: string;
|
|
130
|
+
key: string;
|
|
131
|
+
name: string | null;
|
|
132
|
+
description: string | null;
|
|
133
|
+
config: any;
|
|
134
|
+
created_at: Date;
|
|
135
|
+
updated_at: Date;
|
|
136
|
+
}>(
|
|
137
|
+
`
|
|
138
|
+
SELECT id, tenant_id, key, name, description, config, created_at, updated_at
|
|
139
|
+
FROM lattice_database_configs
|
|
140
|
+
ORDER BY created_at DESC
|
|
141
|
+
`
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return result.rows.map((row) => this.mapRowToEntry(row));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get database configuration by ID
|
|
149
|
+
*/
|
|
150
|
+
async getConfigById(tenantId: string, id: string): Promise<DatabaseConfigEntry | null> {
|
|
151
|
+
await this.ensureInitialized();
|
|
152
|
+
|
|
153
|
+
const result = await this.pool.query<{
|
|
154
|
+
id: string;
|
|
155
|
+
tenant_id: string;
|
|
156
|
+
key: string;
|
|
157
|
+
name: string | null;
|
|
158
|
+
description: string | null;
|
|
159
|
+
config: any;
|
|
160
|
+
created_at: Date;
|
|
161
|
+
updated_at: Date;
|
|
162
|
+
}>(
|
|
163
|
+
`
|
|
164
|
+
SELECT id, tenant_id, key, name, description, config, created_at, updated_at
|
|
165
|
+
FROM lattice_database_configs
|
|
166
|
+
WHERE tenant_id = $1 AND id = $2
|
|
167
|
+
`,
|
|
168
|
+
[tenantId, id]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (result.rows.length === 0) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return this.mapRowToEntry(result.rows[0]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get database configuration by business key
|
|
180
|
+
*/
|
|
181
|
+
async getConfigByKey(tenantId: string, key: string): Promise<DatabaseConfigEntry | null> {
|
|
182
|
+
await this.ensureInitialized();
|
|
183
|
+
|
|
184
|
+
const result = await this.pool.query<{
|
|
185
|
+
id: string;
|
|
186
|
+
tenant_id: string;
|
|
187
|
+
key: string;
|
|
188
|
+
name: string | null;
|
|
189
|
+
description: string | null;
|
|
190
|
+
config: any;
|
|
191
|
+
created_at: Date;
|
|
192
|
+
updated_at: Date;
|
|
193
|
+
}>(
|
|
194
|
+
`
|
|
195
|
+
SELECT id, tenant_id, key, name, description, config, created_at, updated_at
|
|
196
|
+
FROM lattice_database_configs
|
|
197
|
+
WHERE tenant_id = $1 AND key = $2
|
|
198
|
+
`,
|
|
199
|
+
[tenantId, key]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (result.rows.length === 0) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return this.mapRowToEntry(result.rows[0]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create a new database configuration
|
|
211
|
+
*/
|
|
212
|
+
async createConfig(
|
|
213
|
+
tenantId: string,
|
|
214
|
+
id: string,
|
|
215
|
+
data: CreateDatabaseConfigRequest
|
|
216
|
+
): Promise<DatabaseConfigEntry> {
|
|
217
|
+
await this.ensureInitialized();
|
|
218
|
+
|
|
219
|
+
const now = new Date();
|
|
220
|
+
const nowString = now.toISOString();
|
|
221
|
+
const configWithEncryptedPassword = this.encryptPasswordInConfig(data.config);
|
|
222
|
+
|
|
223
|
+
await this.pool.query(
|
|
224
|
+
`
|
|
225
|
+
INSERT INTO lattice_database_configs (id, tenant_id, key, name, description, config, created_at, updated_at)
|
|
226
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7::timestamp, $8::timestamp)
|
|
227
|
+
ON CONFLICT (tenant_id, id) DO UPDATE SET
|
|
228
|
+
key = EXCLUDED.key,
|
|
229
|
+
name = EXCLUDED.name,
|
|
230
|
+
description = EXCLUDED.description,
|
|
231
|
+
config = EXCLUDED.config,
|
|
232
|
+
updated_at = EXCLUDED.updated_at
|
|
233
|
+
`,
|
|
234
|
+
[
|
|
235
|
+
id,
|
|
236
|
+
tenantId,
|
|
237
|
+
data.key,
|
|
238
|
+
data.name || null,
|
|
239
|
+
data.description || null,
|
|
240
|
+
JSON.stringify(configWithEncryptedPassword),
|
|
241
|
+
nowString,
|
|
242
|
+
nowString,
|
|
243
|
+
]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
id,
|
|
248
|
+
tenantId,
|
|
249
|
+
key: data.key,
|
|
250
|
+
config: data.config,
|
|
251
|
+
name: data.name,
|
|
252
|
+
description: data.description,
|
|
253
|
+
createdAt: now,
|
|
254
|
+
updatedAt: now,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Update an existing database configuration
|
|
260
|
+
*/
|
|
261
|
+
async updateConfig(
|
|
262
|
+
tenantId: string,
|
|
263
|
+
id: string,
|
|
264
|
+
updates: Partial<UpdateDatabaseConfigRequest>
|
|
265
|
+
): Promise<DatabaseConfigEntry | null> {
|
|
266
|
+
await this.ensureInitialized();
|
|
267
|
+
|
|
268
|
+
const existing = await this.getConfigById(tenantId, id);
|
|
269
|
+
if (!existing) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Build update fields and values as key-value pairs for clarity
|
|
274
|
+
const updateData: Record<string, any> = {};
|
|
275
|
+
|
|
276
|
+
if (updates.key !== undefined) {
|
|
277
|
+
updateData.key = updates.key;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (updates.name !== undefined) {
|
|
281
|
+
updateData.name = updates.name || null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (updates.description !== undefined) {
|
|
285
|
+
updateData.description = updates.description || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (updates.config !== undefined) {
|
|
289
|
+
const configWithEncryptedPassword = this.encryptPasswordInConfig(updates.config);
|
|
290
|
+
updateData.config = JSON.stringify(configWithEncryptedPassword);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (Object.keys(updateData).length === 0) {
|
|
294
|
+
return existing;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Always update the updated_at timestamp
|
|
298
|
+
updateData.updated_at = new Date().toISOString();
|
|
299
|
+
|
|
300
|
+
// Build SQL with tenantId and id at fixed positions at the end
|
|
301
|
+
const fields = Object.keys(updateData);
|
|
302
|
+
const values = Object.values(updateData);
|
|
303
|
+
|
|
304
|
+
// Add WHERE clause values at the end
|
|
305
|
+
values.push(tenantId);
|
|
306
|
+
values.push(id);
|
|
307
|
+
|
|
308
|
+
// Build SET clause with numbered parameters
|
|
309
|
+
const setClauses = fields.map((field, index) => {
|
|
310
|
+
if (field === 'updated_at') {
|
|
311
|
+
return `${field} = $${index + 1}::timestamp`;
|
|
312
|
+
}
|
|
313
|
+
return `${field} = $${index + 1}`;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// WHERE clause parameters are at the end
|
|
317
|
+
const whereTenantIndex = fields.length + 1;
|
|
318
|
+
const whereIdIndex = fields.length + 2;
|
|
319
|
+
|
|
320
|
+
const sql = `
|
|
321
|
+
UPDATE lattice_database_configs
|
|
322
|
+
SET ${setClauses.join(', ')}
|
|
323
|
+
WHERE tenant_id = $${whereTenantIndex} AND id = $${whereIdIndex}
|
|
324
|
+
`;
|
|
325
|
+
|
|
326
|
+
await this.pool.query(sql, values);
|
|
327
|
+
|
|
328
|
+
return await this.getConfigById(tenantId, id);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Delete a database configuration by ID
|
|
333
|
+
*/
|
|
334
|
+
async deleteConfig(tenantId: string, id: string): Promise<boolean> {
|
|
335
|
+
await this.ensureInitialized();
|
|
336
|
+
|
|
337
|
+
const result = await this.pool.query(
|
|
338
|
+
`
|
|
339
|
+
DELETE FROM lattice_database_configs
|
|
340
|
+
WHERE tenant_id = $1 AND id = $2
|
|
341
|
+
`,
|
|
342
|
+
[tenantId, id]
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Check if configuration exists
|
|
350
|
+
*/
|
|
351
|
+
async hasConfig(tenantId: string, id: string): Promise<boolean> {
|
|
352
|
+
await this.ensureInitialized();
|
|
353
|
+
|
|
354
|
+
const result = await this.pool.query(
|
|
355
|
+
`
|
|
356
|
+
SELECT 1 FROM lattice_database_configs
|
|
357
|
+
WHERE tenant_id = $1 AND id = $2
|
|
358
|
+
LIMIT 1
|
|
359
|
+
`,
|
|
360
|
+
[tenantId, id]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
return result.rows.length > 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Dispose resources and close the connection pool
|
|
368
|
+
*/
|
|
369
|
+
async dispose(): Promise<void> {
|
|
370
|
+
await this.pool.end();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Ensure store is initialized
|
|
375
|
+
*/
|
|
376
|
+
private async ensureInitialized(): Promise<void> {
|
|
377
|
+
if (!this.initialized) {
|
|
378
|
+
await this.initialize();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Map database row to DatabaseConfigEntry object
|
|
384
|
+
* Automatically decrypts password if present
|
|
385
|
+
*/
|
|
386
|
+
private mapRowToEntry(row: {
|
|
387
|
+
id: string;
|
|
388
|
+
tenant_id: string;
|
|
389
|
+
key: string;
|
|
390
|
+
name: string | null;
|
|
391
|
+
description: string | null;
|
|
392
|
+
config: any;
|
|
393
|
+
created_at: Date;
|
|
394
|
+
updated_at: Date;
|
|
395
|
+
}): DatabaseConfigEntry {
|
|
396
|
+
const config: DatabaseConfig = typeof row.config === 'string'
|
|
397
|
+
? JSON.parse(row.config)
|
|
398
|
+
: row.config;
|
|
399
|
+
|
|
400
|
+
// Decrypt password if present
|
|
401
|
+
if (config.password) {
|
|
402
|
+
try {
|
|
403
|
+
config.password = decrypt(config.password);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error('Failed to decrypt database password:', error);
|
|
406
|
+
throw new Error('Failed to decrypt database configuration');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
id: row.id,
|
|
412
|
+
tenantId: row.tenant_id,
|
|
413
|
+
key: row.key,
|
|
414
|
+
config,
|
|
415
|
+
name: row.name || undefined,
|
|
416
|
+
description: row.description || undefined,
|
|
417
|
+
createdAt: row.created_at,
|
|
418
|
+
updatedAt: row.updated_at,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Encrypt password in config before storing
|
|
424
|
+
*/
|
|
425
|
+
private encryptPasswordInConfig(config: DatabaseConfig): DatabaseConfig {
|
|
426
|
+
const configCopy = { ...config };
|
|
427
|
+
|
|
428
|
+
if (configCopy.password) {
|
|
429
|
+
configCopy.password = encrypt(configCopy.password);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return configCopy;
|
|
433
|
+
}
|
|
434
|
+
}
|