@arcote.tech/arc-adapter-db-postgres 0.3.1 → 0.3.3

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,877 +0,0 @@
1
- import {
2
- type ArcContextAny,
3
- type DatabaseAdapter,
4
- type DatabaseAgnosticColumnInfo,
5
- type DatabaseStoreData,
6
- type DatabaseStoreSchema,
7
- type DBAdapterFactory,
8
- type FindOptions,
9
- type ReadTransaction,
10
- type ReadWriteTransaction,
11
- type StoreColumn,
12
- type StoreTable,
13
- type WhereCondition,
14
- extractDatabaseAgnosticSchema,
15
- } from "@arcote.tech/arc";
16
-
17
- export interface PostgreSQLDatabase {
18
- exec(sql: string, params?: any[]): Promise<any>;
19
- execBatch(queries: Array<{ sql: string; params?: any[] }>): Promise<any>;
20
- }
21
-
22
- class PostgreSQLReadTransaction implements ReadTransaction {
23
- constructor(
24
- protected db: PostgreSQLDatabase,
25
- protected tables: Map<string, StoreTable>,
26
- protected adapter?: PostgreSQLAdapter,
27
- ) {}
28
-
29
- protected hasSoftDelete(tableName: string): boolean {
30
- if (this.adapter) {
31
- return this.adapter.hasSoftDelete(tableName);
32
- }
33
- // Fallback: check if table has deleted column
34
- const table = this.tables.get(tableName);
35
- if (!table) return false;
36
- return table.columns.some((col) => col.name === "deleted");
37
- }
38
-
39
- protected deserializeValue(value: any, column: StoreColumn): any {
40
- if (value === null || value === undefined) return null;
41
-
42
- switch (column.type.toLowerCase()) {
43
- case "json":
44
- case "jsonb":
45
- // PostgreSQL JSON/JSONB columns should always be parsed
46
- if (typeof value === "string") {
47
- try {
48
- return JSON.parse(value);
49
- } catch {
50
- return value;
51
- }
52
- }
53
- return value; // Already parsed by PostgreSQL driver
54
- case "text":
55
- // For text columns, only attempt JSON parsing if it looks like JSON
56
- if (
57
- typeof value === "string" &&
58
- (value.startsWith("{") || value.startsWith("["))
59
- ) {
60
- try {
61
- const parsed = JSON.parse(value);
62
- if (typeof parsed === "object" || Array.isArray(parsed)) {
63
- return parsed;
64
- }
65
- } catch {
66
- // Not valid JSON, return as string
67
- }
68
- }
69
- return value;
70
- case "datetime":
71
- case "timestamp":
72
- return new Date(value);
73
- default:
74
- return value;
75
- }
76
- }
77
-
78
- protected deserializeRow(row: any, table: StoreTable): any {
79
- const result: any = {};
80
- for (const column of table.columns) {
81
- const value = row[column.name];
82
- result[column.name] = this.deserializeValue(value, column);
83
- }
84
- return result;
85
- }
86
-
87
- protected getId(store: string, id: any) {
88
- return id;
89
- }
90
-
91
- protected buildWhereClause(
92
- where?: WhereCondition,
93
- tableName?: string,
94
- ): {
95
- sql: string;
96
- params: any[];
97
- } {
98
- const conditions: string[] = [];
99
- const params: any[] = [];
100
- let paramIndex = 1;
101
-
102
- // Only add deleted condition if table has soft delete enabled
103
- if (tableName && this.hasSoftDelete(tableName)) {
104
- conditions.push('"deleted" = $1');
105
- params.push(false);
106
- paramIndex = 2;
107
- }
108
-
109
- if (!where) {
110
- return {
111
- sql: conditions.length > 0 ? conditions.join(" AND ") : "1=1",
112
- params,
113
- };
114
- }
115
-
116
- Object.entries(where).forEach(([key, value]) => {
117
- if (typeof value === "object" && value !== null) {
118
- Object.entries(value as Record<string, unknown>).forEach(
119
- ([operator, operand]) => {
120
- switch (operator) {
121
- case "$eq":
122
- case "$ne":
123
- case "$gt":
124
- case "$gte":
125
- case "$lt":
126
- case "$lte":
127
- conditions.push(
128
- `"${key}" ${this.getOperatorSymbol(operator)} $${paramIndex}`,
129
- );
130
- params.push(operand);
131
- paramIndex++;
132
- break;
133
- case "$in":
134
- case "$nin":
135
- if (Array.isArray(operand)) {
136
- const placeholders = operand
137
- .map(() => `$${paramIndex++}`)
138
- .join(", ");
139
- conditions.push(
140
- `"${key}" ${operator === "$in" ? "IN" : "NOT IN"} (${placeholders})`,
141
- );
142
- params.push(...operand);
143
- }
144
- break;
145
- case "$exists":
146
- if (typeof operand === "boolean") {
147
- conditions.push(
148
- operand ? `"${key}" IS NOT NULL` : `"${key}" IS NULL`,
149
- );
150
- }
151
- break;
152
- }
153
- },
154
- );
155
- } else {
156
- conditions.push(`"${key}" = $${paramIndex}`);
157
- params.push(value);
158
- paramIndex++;
159
- }
160
- });
161
-
162
- return {
163
- sql: conditions.join(" AND "),
164
- params,
165
- };
166
- }
167
-
168
- protected getOperatorSymbol(operator: string): string {
169
- const operators: Record<string, string> = {
170
- $eq: "=",
171
- $ne: "!=",
172
- $gt: ">",
173
- $gte: ">=",
174
- $lt: "<",
175
- $lte: "<=",
176
- };
177
- return operators[operator] || "=";
178
- }
179
-
180
- protected buildOrderByClause(
181
- orderBy?: Record<string, "asc" | "desc">,
182
- ): string {
183
- if (!orderBy) return "";
184
-
185
- const orderClauses = Object.entries(orderBy)
186
- .map(([key, direction]) => `"${key}" ${direction.toUpperCase()}`)
187
- .join(", ");
188
-
189
- return orderClauses ? `ORDER BY ${orderClauses}` : "";
190
- }
191
-
192
- async find<T>(store: string, options: FindOptions<T>): Promise<T[]> {
193
- const { where, limit, offset, orderBy } = options || {};
194
- const whereClause = this.buildWhereClause(where, store);
195
- const orderByClause = this.buildOrderByClause(orderBy);
196
- const table = this.tables.get(store);
197
-
198
- if (!table) {
199
- throw new Error(`Store ${store} not found`);
200
- }
201
-
202
- let query = `
203
- SELECT *
204
- FROM "${table.name}"
205
- WHERE ${whereClause.sql}
206
- ${orderByClause}
207
- `;
208
-
209
- let params = whereClause.params;
210
- let paramIndex = params.length + 1;
211
-
212
- if (limit) {
213
- query += ` LIMIT $${paramIndex}`;
214
- params.push(limit);
215
- paramIndex++;
216
- }
217
-
218
- if (offset) {
219
- query += ` OFFSET $${paramIndex}`;
220
- params.push(offset);
221
- }
222
-
223
- const rows = await this.db.exec(query, params);
224
- return rows.map((row: any) => this.deserializeRow(row, table));
225
- }
226
- }
227
-
228
- class PostgreSQLReadWriteTransaction
229
- extends PostgreSQLReadTransaction
230
- implements ReadWriteTransaction
231
- {
232
- private queries: Array<{ sql: string; params?: any[] }> = [];
233
-
234
- constructor(
235
- db: PostgreSQLDatabase,
236
- tables: Map<string, StoreTable>,
237
- protected adapter: PostgreSQLAdapter,
238
- ) {
239
- super(db, tables);
240
- }
241
- async remove(store: string, id: any) {
242
- const table = this.tables.get(store);
243
- if (!table) {
244
- throw new Error(`Store ${store} not found`);
245
- }
246
- const query = `UPDATE "${table.name}" SET "deleted" = $1, "lastUpdate" = $2 WHERE "${table.primaryKey}" = $3`;
247
- this.queries.push({
248
- sql: query,
249
- params: [true, new Date().toISOString(), id],
250
- });
251
- }
252
-
253
- async set(store: string, item: any) {
254
- const table = this.tables.get(store);
255
- if (!table) {
256
- throw new Error(`Store ${store} not found`);
257
- }
258
-
259
- const hasVersioning = this.adapter.hasVersioning(store);
260
-
261
- if (hasVersioning) {
262
- // For versioned tables, use a transaction with version increment
263
- await this.setWithVersioning(store, item, table);
264
- } else {
265
- // For non-versioned tables, use simple insert
266
- await this.setWithoutVersioning(store, item, table);
267
- }
268
- }
269
-
270
- private async setWithoutVersioning(
271
- store: string,
272
- item: any,
273
- table: StoreTable,
274
- ) {
275
- const columnNames = table.columns.map((col) => col.name);
276
- const values = table.columns.map((column) => {
277
- let value = item[column.name];
278
- if (value === undefined && column.default !== undefined) {
279
- value = column.default;
280
- }
281
- return this.serializeValue(value, column);
282
- });
283
-
284
- const placeholders = columnNames.map((_, i) => `$${i + 1}`).join(", ");
285
- const updateColumns = columnNames
286
- .filter((col) => col !== table.primaryKey)
287
- .map((col) => `"${col}" = EXCLUDED."${col}"`)
288
- .join(", ");
289
-
290
- // For events table, use simple INSERT since events are typically append-only
291
- if (store === "events") {
292
- const simpleInsertSql = `
293
- INSERT INTO "${table.name}"
294
- (${columnNames.map((c) => `"${c}"`).join(", ")})
295
- VALUES (${placeholders})
296
- `;
297
- this.queries.push({
298
- sql: simpleInsertSql,
299
- params: values,
300
- });
301
- } else {
302
- const sql = `
303
- INSERT INTO "${table.name}"
304
- (${columnNames.map((c) => `"${c}"`).join(", ")})
305
- VALUES (${placeholders})
306
- ON CONFLICT ("${table.primaryKey}")
307
- DO UPDATE SET ${updateColumns}
308
- `;
309
- this.queries.push({
310
- sql: sql,
311
- params: values,
312
- });
313
- }
314
- }
315
-
316
- private async setWithVersioning(store: string, item: any, table: StoreTable) {
317
- // Filter out __version from regular columns and handle it separately
318
- const regularColumns = table.columns.filter(
319
- (col) => col.name !== "__version",
320
- );
321
- const columnNames = regularColumns.map((col) => col.name);
322
- const values = regularColumns.map((column) => {
323
- let value = item[column.name];
324
- if (value === undefined && column.default !== undefined) {
325
- value = column.default;
326
- }
327
- return this.serializeValue(value, column);
328
- });
329
-
330
- // Add __version column
331
- columnNames.push("__version");
332
- const placeholders = regularColumns.map((_, i) => `$${i + 1}`).join(", ");
333
- const updateColumns = regularColumns
334
- .filter((col) => col.name !== table.primaryKey)
335
- .map((col) => `"${col.name}" = EXCLUDED."${col.name}"`)
336
- .join(", ");
337
-
338
- // Use a simpler approach: always increment and return the new version
339
- const sql = `
340
- WITH next_version AS (
341
- INSERT INTO __arc_version_counters (table_name, last_version)
342
- VALUES ($${values.length + 1}, 1)
343
- ON CONFLICT(table_name)
344
- DO UPDATE SET last_version = __arc_version_counters.last_version + 1
345
- RETURNING last_version
346
- ),
347
- upsert AS (
348
- INSERT INTO "${table.name}"
349
- (${columnNames.map((c) => `"${c}"`).join(", ")})
350
- VALUES (${placeholders}, (SELECT last_version FROM next_version))
351
- ON CONFLICT ("${table.primaryKey}")
352
- DO UPDATE SET ${updateColumns}${updateColumns ? ", " : ""}"__version" = (SELECT last_version FROM next_version)
353
- RETURNING *
354
- )
355
- SELECT * FROM upsert
356
- `;
357
-
358
- this.queries.push({
359
- sql: sql,
360
- params: [...values, store],
361
- });
362
- }
363
-
364
- async commit() {
365
- if (this.queries.length === 0) {
366
- return Promise.resolve();
367
- }
368
-
369
- try {
370
- await this.db.execBatch(this.queries);
371
- this.queries = [];
372
- } catch (error) {
373
- this.queries = [];
374
- throw error;
375
- }
376
- }
377
-
378
- private serializeValue(value: any, column: StoreColumn): any {
379
- if (value === null || value === undefined) return null;
380
-
381
- switch (column.type.toLowerCase()) {
382
- case "timestamp":
383
- case "datetime":
384
- // Handle various timestamp formats
385
- if (value instanceof Date) {
386
- return value.toISOString();
387
- }
388
- if (typeof value === "number") {
389
- // Unix timestamp (seconds or milliseconds)
390
- const date = value > 1e10 ? new Date(value) : new Date(value * 1000);
391
- return date.toISOString();
392
- }
393
- if (typeof value === "string") {
394
- // Already an ISO string or date string, validate and normalize
395
- const date = new Date(value);
396
- if (!isNaN(date.getTime())) {
397
- return date.toISOString();
398
- }
399
- }
400
- return value; // Pass through if we can't convert
401
- case "json":
402
- case "jsonb":
403
- return JSON.stringify(value);
404
- default:
405
- if (value instanceof Date) {
406
- return value.toISOString();
407
- }
408
- if (Array.isArray(value) || typeof value === "object") {
409
- return JSON.stringify(value);
410
- }
411
- return value;
412
- }
413
- }
414
- }
415
-
416
- export class PostgreSQLAdapter implements DatabaseAdapter {
417
- private tables: Map<string, StoreTable> = new Map();
418
- private tableSchemas: Map<string, any[]> = new Map();
419
- private pendingReinitTables: Array<{
420
- tableName: string;
421
- reinitFn: (tableName: string, dataStorage: any) => Promise<void>;
422
- }> = [];
423
-
424
- private mapType(arcType: string, storeData?: DatabaseStoreData): string {
425
- // Check for database-specific type override
426
- if (storeData?.databaseType?.postgresql) {
427
- return storeData.databaseType.postgresql;
428
- }
429
-
430
- switch (arcType) {
431
- case "string":
432
- case "stringEnum":
433
- return "TEXT";
434
- case "id":
435
- case "customId":
436
- // Use TEXT for regular string IDs, UUID only if explicitly specified
437
- return storeData?.databaseType?.postgresql || "TEXT";
438
- case "number":
439
- return storeData?.isAutoIncrement ? "SERIAL" : "BIGINT";
440
- case "boolean":
441
- return "BOOLEAN";
442
- case "date":
443
- return "TIMESTAMP";
444
- case "object":
445
- case "array":
446
- case "record":
447
- return "JSONB";
448
- case "blob":
449
- return "BYTEA";
450
- default:
451
- return "TEXT";
452
- }
453
- }
454
-
455
- private buildConstraints(storeData?: DatabaseStoreData): string[] {
456
- const constraints: string[] = [];
457
-
458
- if (storeData?.isPrimaryKey) {
459
- constraints.push("PRIMARY KEY");
460
- }
461
- if (storeData?.isUnique) {
462
- constraints.push("UNIQUE");
463
- }
464
- if (storeData?.foreignKey) {
465
- const { table, column, onDelete, onUpdate } = storeData.foreignKey;
466
- let fkConstraint = `REFERENCES ${table}(${column})`;
467
- if (onDelete) fkConstraint += ` ON DELETE ${onDelete}`;
468
- if (onUpdate) fkConstraint += ` ON UPDATE ${onUpdate}`;
469
- constraints.push(fkConstraint);
470
- }
471
-
472
- return constraints;
473
- }
474
-
475
- private generateColumnSQL(
476
- columnInfo: DatabaseAgnosticColumnInfo & { name: string },
477
- ): string {
478
- const type = this.mapType(columnInfo.type, columnInfo.storeData);
479
- const constraints: string[] = [];
480
-
481
- if (!columnInfo.storeData?.isNullable) {
482
- constraints.push("NOT NULL");
483
- }
484
-
485
- constraints.push(...this.buildConstraints(columnInfo.storeData));
486
-
487
- if (columnInfo.defaultValue !== undefined) {
488
- if (type === "UUID" && columnInfo.defaultValue === "auto") {
489
- constraints.push("DEFAULT gen_random_uuid()");
490
- } else if (typeof columnInfo.defaultValue === "string") {
491
- constraints.push(`DEFAULT '${columnInfo.defaultValue}'`);
492
- } else {
493
- constraints.push(`DEFAULT ${columnInfo.defaultValue}`);
494
- }
495
- }
496
-
497
- return `"${columnInfo.name}" ${type} ${constraints.join(" ")}`.trim();
498
- }
499
-
500
- private generateCreateTableSQL(
501
- tableName: string,
502
- columns: (DatabaseAgnosticColumnInfo & { name: string })[],
503
- ): string {
504
- const columnDefinitions = columns.map((col) => this.generateColumnSQL(col));
505
- const indexes = columns
506
- .filter((col) => col.storeData?.hasIndex && !col.storeData?.isPrimaryKey)
507
- .map(
508
- (col) =>
509
- `CREATE INDEX IF NOT EXISTS idx_${tableName}_${col.name} ON "${tableName}"("${col.name}");`,
510
- );
511
-
512
- let sql = `CREATE TABLE IF NOT EXISTS "${tableName}" (\n ${columnDefinitions.join(",\n ")}\n)`;
513
-
514
- if (indexes.length > 0) {
515
- sql += ";\n" + indexes.join("\n");
516
- }
517
-
518
- return sql;
519
- }
520
-
521
- constructor(
522
- private db: PostgreSQLDatabase,
523
- private context: ArcContextAny,
524
- ) {
525
- this.context.elements.forEach((element) => {
526
- if (
527
- "databaseStoreSchema" in element &&
528
- typeof element.databaseStoreSchema === "function"
529
- ) {
530
- const databaseSchema =
531
- element.databaseStoreSchema() as DatabaseStoreSchema;
532
-
533
- databaseSchema.tables.forEach((dbTable) => {
534
- // Extract database-agnostic schema from ArcObject
535
- const agnosticSchema = extractDatabaseAgnosticSchema(
536
- dbTable.schema,
537
- dbTable.name,
538
- );
539
-
540
- // Convert to database-specific columns
541
- const columns = agnosticSchema.columns.map((columnInfo) => ({
542
- name: columnInfo.name,
543
- type: this.mapType(columnInfo.type, columnInfo.storeData),
544
- constraints: this.buildConstraints(columnInfo.storeData),
545
- isNullable: columnInfo.storeData?.isNullable || false,
546
- defaultValue: columnInfo.defaultValue,
547
- isPrimaryKey: columnInfo.storeData?.isPrimaryKey || false,
548
- isAutoIncrement: columnInfo.storeData?.isAutoIncrement || false,
549
- isUnique: columnInfo.storeData?.isUnique || false,
550
- hasIndex: columnInfo.storeData?.hasIndex || false,
551
- foreignKey: columnInfo.storeData?.foreignKey,
552
- }));
553
-
554
- this.tableSchemas.set(dbTable.name, columns);
555
-
556
- // Convert to legacy StoreTable format for compatibility with existing transaction code
557
- const legacyTable: StoreTable = {
558
- name: dbTable.name,
559
- primaryKey: columns.find((col) => col.isPrimaryKey)?.name || "_id",
560
- columns: columns.map((col) => ({
561
- name: col.name,
562
- type: col.type,
563
- isOptional: col.isNullable,
564
- default: col.defaultValue,
565
- })),
566
- };
567
- this.tables.set(dbTable.name, legacyTable);
568
- });
569
- }
570
- });
571
- }
572
-
573
- public async initialize() {
574
- // Create the version counter table first
575
- await this.createVersionCounterTable();
576
- // Create the table versions tracking table
577
- await this.createTableVersionsTable();
578
-
579
- const processedSchemas = new Set<DatabaseStoreSchema>();
580
- const processedTables = new Set<string>();
581
-
582
- for (const element of this.context.elements) {
583
- if (
584
- "databaseStoreSchema" in element &&
585
- typeof element.databaseStoreSchema === "function"
586
- ) {
587
- const databaseSchema =
588
- element.databaseStoreSchema() as DatabaseStoreSchema;
589
-
590
- // Skip if we've already processed this exact schema reference
591
- if (processedSchemas.has(databaseSchema)) {
592
- continue;
593
- }
594
- processedSchemas.add(databaseSchema);
595
-
596
- for (const dbTable of databaseSchema.tables) {
597
- const tableKey = dbTable.version
598
- ? `${dbTable.name}_v${dbTable.version}`
599
- : dbTable.name;
600
-
601
- if (!processedTables.has(tableKey)) {
602
- // Extract database-agnostic schema from ArcObject
603
- const agnosticSchema = extractDatabaseAgnosticSchema(
604
- dbTable.schema,
605
- dbTable.name,
606
- );
607
-
608
- // Add system columns if enabled
609
- let allColumns = [...agnosticSchema.columns];
610
-
611
- if (dbTable.options?.versioning) {
612
- allColumns.push({
613
- name: "__version",
614
- type: "number",
615
- storeData: { isNullable: false, hasIndex: true },
616
- defaultValue: 1,
617
- });
618
- }
619
-
620
- if (dbTable.options?.softDelete) {
621
- allColumns.push({
622
- name: "deleted",
623
- type: "boolean",
624
- storeData: { isNullable: false, hasIndex: true },
625
- defaultValue: false,
626
- });
627
- }
628
-
629
- // Determine physical table name based on version
630
- const physicalTableName = this.getPhysicalTableName(
631
- dbTable.name,
632
- dbTable.version,
633
- );
634
-
635
- // Check if versioned table already exists
636
- if (
637
- dbTable.version &&
638
- (await this.checkVersionedTableExists(
639
- dbTable.name,
640
- dbTable.version,
641
- ))
642
- ) {
643
- console.log(
644
- `Versioned table ${physicalTableName} already exists, skipping creation`,
645
- );
646
- } else {
647
- await this.createTableIfNotExistsNew(
648
- physicalTableName,
649
- allColumns,
650
- );
651
-
652
- // Register the new version if it's a versioned table
653
- if (dbTable.version) {
654
- await this.registerTableVersion(
655
- dbTable.name,
656
- dbTable.version,
657
- physicalTableName,
658
- );
659
-
660
- // Store reinit function for later execution
661
- if (databaseSchema.reinitTable) {
662
- this.pendingReinitTables.push({
663
- tableName: physicalTableName,
664
- reinitFn: databaseSchema.reinitTable,
665
- });
666
- }
667
- }
668
- }
669
-
670
- // Update the legacy table mapping with the actual columns (including system columns)
671
- const legacyTable: StoreTable = {
672
- name: physicalTableName, // Use physical table name for actual queries
673
- primaryKey:
674
- allColumns.find((col) => col.storeData?.isPrimaryKey)?.name ||
675
- "_id",
676
- columns: allColumns.map((col) => ({
677
- name: col.name,
678
- type: this.mapType(col.type, col.storeData),
679
- isOptional: col.storeData?.isNullable || false,
680
- default: col.defaultValue,
681
- })),
682
- };
683
- this.tables.set(dbTable.name, legacyTable); // Still use logical name as key
684
-
685
- processedTables.add(tableKey);
686
- }
687
- }
688
- }
689
- }
690
- }
691
-
692
- private async createTableIfNotExistsNew(
693
- tableName: string,
694
- columns: (DatabaseAgnosticColumnInfo & { name: string })[],
695
- ) {
696
- const createTableSQL = this.generateCreateTableSQL(tableName, columns);
697
- await this.db.exec(createTableSQL);
698
- }
699
-
700
- /**
701
- * Create the version counter table for tracking per-table version sequences
702
- */
703
- private async createVersionCounterTable() {
704
- const sql = `
705
- CREATE TABLE IF NOT EXISTS __arc_version_counters (
706
- table_name TEXT PRIMARY KEY,
707
- last_version BIGINT NOT NULL DEFAULT 0
708
- )
709
- `;
710
- await this.db.exec(sql);
711
- }
712
-
713
- /**
714
- * Create the table versions tracking table for schema versioning
715
- */
716
- private async createTableVersionsTable() {
717
- const sql = `
718
- CREATE TABLE IF NOT EXISTS __arc_table_versions (
719
- table_name TEXT NOT NULL,
720
- version INTEGER NOT NULL,
721
- physical_table_name TEXT NOT NULL,
722
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
723
- is_active BOOLEAN DEFAULT FALSE,
724
- PRIMARY KEY (table_name, version)
725
- )
726
- `;
727
- await this.db.exec(sql);
728
- }
729
-
730
- /**
731
- * Generate physical table name with version suffix
732
- */
733
- private getPhysicalTableName(logicalName: string, version?: number): string {
734
- return version ? `${logicalName}_v${version}` : logicalName;
735
- }
736
-
737
- /**
738
- * Check if a versioned table exists
739
- */
740
- private async checkVersionedTableExists(
741
- logicalName: string,
742
- version: number,
743
- ): Promise<boolean> {
744
- const result = await this.db.exec(
745
- "SELECT COUNT(*) as count FROM __arc_table_versions WHERE table_name = $1 AND version = $2",
746
- [logicalName, version],
747
- );
748
- return result[0]?.count > 0;
749
- }
750
-
751
- /**
752
- * Register a new table version
753
- */
754
- private async registerTableVersion(
755
- logicalName: string,
756
- version: number,
757
- physicalName: string,
758
- ): Promise<void> {
759
- await this.db.exec(
760
- "INSERT INTO __arc_table_versions (table_name, version, physical_table_name, is_active) VALUES ($1, $2, $3, $4)",
761
- [logicalName, version, physicalName, true],
762
- );
763
- }
764
-
765
- /**
766
- * Check if a table has versioning enabled
767
- */
768
- public hasVersioning(tableName: string): boolean {
769
- // Check if the table has a __version column
770
- const table = this.tables.get(tableName);
771
- if (!table) return false;
772
- return table.columns.some((col) => col.name === "__version");
773
- }
774
-
775
- /**
776
- * Check if a table has soft delete enabled
777
- */
778
- public hasSoftDelete(tableName: string): boolean {
779
- // Check if the table has a deleted column
780
- const table = this.tables.get(tableName);
781
- if (!table) return false;
782
- return table.columns.some((col) => col.name === "deleted");
783
- }
784
-
785
- /**
786
- * Execute all pending reinitTable functions
787
- */
788
- public async executeReinitTables(dataStorage: any): Promise<void> {
789
- for (const { tableName, reinitFn } of this.pendingReinitTables) {
790
- try {
791
- await reinitFn(tableName, dataStorage);
792
- console.log(`Successfully reinitialized table: ${tableName}`);
793
- } catch (error) {
794
- console.error(`Failed to reinitialize table ${tableName}:`, error);
795
- throw error;
796
- }
797
- }
798
- // Clear the pending list after execution
799
- this.pendingReinitTables = [];
800
- }
801
-
802
- readWriteTransaction(stores?: string[]) {
803
- return new PostgreSQLReadWriteTransaction(this.db, this.tables, this);
804
- }
805
-
806
- readTransaction(stores?: string[]) {
807
- return new PostgreSQLReadTransaction(this.db, this.tables, this);
808
- }
809
- }
810
-
811
- export const createPostgreSQLAdapterFactory = (
812
- db: PostgreSQLDatabase,
813
- ): DBAdapterFactory => {
814
- return async (context: ArcContextAny): Promise<DatabaseAdapter> => {
815
- const adapter = new PostgreSQLAdapter(db, context);
816
- await adapter.initialize();
817
- return adapter;
818
- };
819
- };
820
-
821
- /**
822
- * Wrapper for the 'postgres' package (postgres.js) to implement PostgreSQLDatabase interface
823
- */
824
- class PostgresJsDatabase implements PostgreSQLDatabase {
825
- constructor(private sql: any) {}
826
-
827
- async exec(sql: string, params?: any[]): Promise<any> {
828
- try {
829
- if (params && params.length > 0) {
830
- const result = await this.sql.unsafe(sql, params);
831
- return Array.from(result);
832
- }
833
- const result = await this.sql.unsafe(sql);
834
- return Array.from(result);
835
- } catch (error) {
836
- console.error("PostgreSQL error:", error);
837
- throw error;
838
- }
839
- }
840
-
841
- async execBatch(
842
- queries: Array<{ sql: string; params?: any[] }>,
843
- ): Promise<any> {
844
- return this.sql.begin(async (tx: any) => {
845
- for (const query of queries) {
846
- if (query.params && query.params.length > 0) {
847
- await tx.unsafe(query.sql, query.params);
848
- } else {
849
- await tx.unsafe(query.sql);
850
- }
851
- }
852
- });
853
- }
854
- }
855
-
856
- import postgres from "postgres";
857
-
858
- /**
859
- * Create a PostgreSQL adapter factory from a connection string
860
- *
861
- * @param connectionString - PostgreSQL connection URL (e.g., "postgresql://user:pass@localhost:5432/db")
862
- *
863
- * @example
864
- * ```ts
865
- * const dbAdapterFactory = createPostgreSQLAdapterFactoryFromUrl("postgresql://user:pass@localhost:5432/db");
866
- * ```
867
- */
868
- export const createPostgreSQLAdapterFactoryFromUrl = (
869
- connectionString: string,
870
- ): DBAdapterFactory => {
871
- const sql = postgres(connectionString, {
872
- // Suppress NOTICE messages like "relation already exists, skipping"
873
- onnotice: () => {},
874
- });
875
- const db = new PostgresJsDatabase(sql);
876
- return createPostgreSQLAdapterFactory(db);
877
- };