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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-adapter-db-postgres",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -67,8 +67,24 @@ class PostgreSQLReadTransaction implements ReadTransaction {
67
67
  }
68
68
  }
69
69
  return value;
70
+ case "date":
70
71
  case "datetime":
71
72
  case "timestamp":
73
+ // PostgreSQL returns Date objects directly via postgres.js driver
74
+ if (value instanceof Date) {
75
+ return value;
76
+ }
77
+ if (typeof value === "string") {
78
+ // If the string doesn't have timezone info, treat as UTC
79
+ if (
80
+ !value.endsWith("Z") &&
81
+ !value.includes("+") &&
82
+ !value.includes("-", 10)
83
+ ) {
84
+ return new Date(value + "Z");
85
+ }
86
+ return new Date(value);
87
+ }
72
88
  return new Date(value);
73
89
  default:
74
90
  return value;
@@ -99,6 +115,21 @@ class PostgreSQLReadTransaction implements ReadTransaction {
99
115
  const params: any[] = [];
100
116
  let paramIndex = 1;
101
117
 
118
+ // Debug logging
119
+ if (where && (Array.isArray(where) || typeof where !== "object")) {
120
+ console.error(
121
+ "[buildWhereClause] Invalid where clause type:",
122
+ typeof where,
123
+ Array.isArray(where) ? "(array)" : "",
124
+ );
125
+ console.error(
126
+ "[buildWhereClause] where value:",
127
+ JSON.stringify(where, null, 2),
128
+ );
129
+ console.error("[buildWhereClause] tableName:", tableName);
130
+ console.trace("[buildWhereClause] Stack trace:");
131
+ }
132
+
102
133
  // Only add deleted condition if table has soft delete enabled
103
134
  if (tableName && this.hasSoftDelete(tableName)) {
104
135
  conditions.push('"deleted" = $1');
@@ -160,7 +191,7 @@ class PostgreSQLReadTransaction implements ReadTransaction {
160
191
  });
161
192
 
162
193
  return {
163
- sql: conditions.join(" AND "),
194
+ sql: conditions.length > 0 ? conditions.join(" AND ") : "1=1",
164
195
  params,
165
196
  };
166
197
  }
@@ -258,12 +289,30 @@ class PostgreSQLReadWriteTransaction
258
289
 
259
290
  const hasVersioning = this.adapter.hasVersioning(store);
260
291
 
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);
292
+ try {
293
+ if (hasVersioning) {
294
+ // For versioned tables, use a transaction with version increment
295
+ await this.setWithVersioning(store, item, table);
296
+ } else {
297
+ // For non-versioned tables, use simple insert
298
+ await this.setWithoutVersioning(store, item, table);
299
+ }
300
+ } catch (error) {
301
+ console.error(`[PostgreSQL] Error in set() for store "${store}":`);
302
+ console.error(
303
+ `[PostgreSQL] Item data:`,
304
+ JSON.stringify(
305
+ item,
306
+ (key, value) => {
307
+ if (value instanceof Date) {
308
+ return `[Date: ${value.toString()} | ISO: ${isNaN(value.getTime()) ? "Invalid Date" : value.toISOString()}]`;
309
+ }
310
+ return value;
311
+ },
312
+ 2,
313
+ ),
314
+ );
315
+ throw error;
267
316
  }
268
317
  }
269
318
 
@@ -381,13 +430,31 @@ class PostgreSQLReadWriteTransaction
381
430
  switch (column.type.toLowerCase()) {
382
431
  case "timestamp":
383
432
  case "datetime":
433
+ case "timestamptz":
384
434
  // Handle various timestamp formats
385
435
  if (value instanceof Date) {
436
+ if (isNaN(value.getTime())) {
437
+ console.error(
438
+ `[PostgreSQL] Invalid Date in column "${column.name}":`,
439
+ value,
440
+ );
441
+ console.error(`[PostgreSQL] Column type: ${column.type}`);
442
+ throw new Error(`Invalid Date value in column "${column.name}"`);
443
+ }
386
444
  return value.toISOString();
387
445
  }
388
446
  if (typeof value === "number") {
389
447
  // Unix timestamp (seconds or milliseconds)
390
448
  const date = value > 1e10 ? new Date(value) : new Date(value * 1000);
449
+ if (isNaN(date.getTime())) {
450
+ console.error(
451
+ `[PostgreSQL] Invalid Date from number in column "${column.name}":`,
452
+ value,
453
+ );
454
+ throw new Error(
455
+ `Invalid Date value (from number) in column "${column.name}"`,
456
+ );
457
+ }
391
458
  return date.toISOString();
392
459
  }
393
460
  if (typeof value === "string") {
@@ -396,13 +463,34 @@ class PostgreSQLReadWriteTransaction
396
463
  if (!isNaN(date.getTime())) {
397
464
  return date.toISOString();
398
465
  }
466
+ console.error(
467
+ `[PostgreSQL] Invalid Date string in column "${column.name}":`,
468
+ value,
469
+ );
470
+ throw new Error(
471
+ `Invalid Date string in column "${column.name}": "${value}"`,
472
+ );
399
473
  }
400
- return value; // Pass through if we can't convert
474
+ console.error(
475
+ `[PostgreSQL] Unexpected date value type in column "${column.name}":`,
476
+ typeof value,
477
+ value,
478
+ );
479
+ throw new Error(
480
+ `Unexpected date value type in column "${column.name}": ${typeof value}`,
481
+ );
401
482
  case "json":
402
483
  case "jsonb":
403
484
  return JSON.stringify(value);
404
485
  default:
405
486
  if (value instanceof Date) {
487
+ if (isNaN(value.getTime())) {
488
+ console.error(
489
+ `[PostgreSQL] Invalid Date in column "${column.name}" (default case):`,
490
+ value,
491
+ );
492
+ throw new Error(`Invalid Date value in column "${column.name}"`);
493
+ }
406
494
  return value.toISOString();
407
495
  }
408
496
  if (Array.isArray(value) || typeof value === "object") {
@@ -440,7 +528,7 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
440
528
  case "boolean":
441
529
  return "BOOLEAN";
442
530
  case "date":
443
- return "TIMESTAMP";
531
+ return "TIMESTAMPTZ";
444
532
  case "object":
445
533
  case "array":
446
534
  case "record":
@@ -452,7 +540,10 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
452
540
  }
453
541
  }
454
542
 
455
- private buildConstraints(storeData?: DatabaseStoreData): string[] {
543
+ private buildConstraints(
544
+ storeData?: DatabaseStoreData,
545
+ skipForeignKey = false,
546
+ ): string[] {
456
547
  const constraints: string[] = [];
457
548
 
458
549
  if (storeData?.isPrimaryKey) {
@@ -461,13 +552,8 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
461
552
  if (storeData?.isUnique) {
462
553
  constraints.push("UNIQUE");
463
554
  }
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
- }
555
+ // Foreign keys are now added separately after all tables are created
556
+ // to avoid dependency ordering issues
471
557
 
472
558
  return constraints;
473
559
  }
@@ -500,7 +586,7 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
500
586
  private generateCreateTableSQL(
501
587
  tableName: string,
502
588
  columns: (DatabaseAgnosticColumnInfo & { name: string })[],
503
- ): string {
589
+ ): { createTableSql: string; foreignKeySql: string[] } {
504
590
  const columnDefinitions = columns.map((col) => this.generateColumnSQL(col));
505
591
  const indexes = columns
506
592
  .filter((col) => col.storeData?.hasIndex && !col.storeData?.isPrimaryKey)
@@ -509,13 +595,25 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
509
595
  `CREATE INDEX IF NOT EXISTS idx_${tableName}_${col.name} ON "${tableName}"("${col.name}");`,
510
596
  );
511
597
 
598
+ // Collect foreign key constraints to add after table creation
599
+ const foreignKeySql: string[] = [];
600
+ for (const col of columns) {
601
+ if (col.storeData?.foreignKey) {
602
+ const { table, column, onDelete, onUpdate } = col.storeData.foreignKey;
603
+ let fkSql = `ALTER TABLE "${tableName}" ADD CONSTRAINT fk_${tableName}_${col.name} FOREIGN KEY ("${col.name}") REFERENCES "${table}"("${column}")`;
604
+ if (onDelete) fkSql += ` ON DELETE ${onDelete}`;
605
+ if (onUpdate) fkSql += ` ON UPDATE ${onUpdate}`;
606
+ foreignKeySql.push(fkSql);
607
+ }
608
+ }
609
+
512
610
  let sql = `CREATE TABLE IF NOT EXISTS "${tableName}" (\n ${columnDefinitions.join(",\n ")}\n)`;
513
611
 
514
612
  if (indexes.length > 0) {
515
613
  sql += ";\n" + indexes.join("\n");
516
614
  }
517
615
 
518
- return sql;
616
+ return { createTableSql: sql, foreignKeySql };
519
617
  }
520
618
 
521
619
  constructor(
@@ -570,6 +668,8 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
570
668
  });
571
669
  }
572
670
 
671
+ private pendingForeignKeys: string[] = [];
672
+
573
673
  public async initialize() {
574
674
  // Create the version counter table first
575
675
  await this.createVersionCounterTable();
@@ -579,6 +679,9 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
579
679
  const processedSchemas = new Set<DatabaseStoreSchema>();
580
680
  const processedTables = new Set<string>();
581
681
 
682
+ // Clear pending foreign keys
683
+ this.pendingForeignKeys = [];
684
+
582
685
  for (const element of this.context.elements) {
583
686
  if (
584
687
  "databaseStoreSchema" in element &&
@@ -687,14 +790,34 @@ export class PostgreSQLAdapter implements DatabaseAdapter {
687
790
  }
688
791
  }
689
792
  }
793
+
794
+ // Apply all foreign key constraints after all tables are created
795
+ for (const fkSql of this.pendingForeignKeys) {
796
+ try {
797
+ await this.db.exec(fkSql);
798
+ } catch (error: any) {
799
+ // Ignore if constraint already exists
800
+ if (!error.message?.includes("already exists")) {
801
+ console.warn(
802
+ `Failed to add foreign key constraint: ${error.message}`,
803
+ );
804
+ }
805
+ }
806
+ }
807
+ this.pendingForeignKeys = [];
690
808
  }
691
809
 
692
810
  private async createTableIfNotExistsNew(
693
811
  tableName: string,
694
812
  columns: (DatabaseAgnosticColumnInfo & { name: string })[],
695
813
  ) {
696
- const createTableSQL = this.generateCreateTableSQL(tableName, columns);
697
- await this.db.exec(createTableSQL);
814
+ const { createTableSql, foreignKeySql } = this.generateCreateTableSQL(
815
+ tableName,
816
+ columns,
817
+ );
818
+ await this.db.exec(createTableSql);
819
+ // Collect foreign keys to be added after all tables are created
820
+ this.pendingForeignKeys.push(...foreignKeySql);
698
821
  }
699
822
 
700
823
  /**
@@ -834,6 +957,8 @@ class PostgresJsDatabase implements PostgreSQLDatabase {
834
957
  return Array.from(result);
835
958
  } catch (error) {
836
959
  console.error("PostgreSQL error:", error);
960
+ console.error("Failed SQL:", sql);
961
+ console.error("Failed params:", JSON.stringify(params, null, 2));
837
962
  throw error;
838
963
  }
839
964
  }