fhir-persistence 0.3.0 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2025-03-15
9
+
10
+ ### Fixed
11
+
12
+ #### PostgreSQL DDL Compatibility (Phase D)
13
+
14
+ - **`migration-runner.ts`** — Tracking table `_migrations` now uses `TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP` on PostgreSQL instead of SQLite-only `datetime('now')`
15
+ - **`package-registry-repo.ts`** — `_packages` and `_schema_version` tables use dialect-aware timestamp defaults; `INSERT OR REPLACE` replaced with `INSERT ... ON CONFLICT ... DO UPDATE` on PostgreSQL
16
+ - **`reindex-scheduler.ts`** — `_reindex_jobs` table uses `SERIAL PRIMARY KEY` on PostgreSQL instead of `AUTOINCREMENT`; `datetime('now')` replaced with `CURRENT_TIMESTAMP`
17
+ - **`valueset-repo.ts`** — `terminology_valuesets` table uses dialect-aware timestamp defaults; `INSERT OR REPLACE` replaced with `INSERT ... ON CONFLICT ... DO UPDATE` on PostgreSQL
18
+ - **`lookup-table-writer.ts`** — 4 global lookup tables (`HumanName`, `Address`, `ContactPoint`, `Identifier`) use `SERIAL PRIMARY KEY` on PostgreSQL instead of `AUTOINCREMENT`
19
+
20
+ ### Changed
21
+
22
+ - **`ig-persistence-manager.ts`** — Now passes `dialect` to `PackageRegistryRepo`, `MigrationRunnerV2`, and `ReindexScheduler` constructors
23
+ - **`indexing-pipeline.ts`** — `IndexingPipelineOptions` accepts `dialect` parameter, passed to `LookupTableWriter`
24
+ - **`fhir-system.ts`** — Passes `dialect` through to `IndexingPipeline` via `FhirPersistence` options
25
+ - All new `dialect` parameters default to `'sqlite'` — fully backward-compatible
26
+
27
+ ### Test Coverage
28
+
29
+ - **1014 total tests** (1006 passing, 8 skipped) across 56 test files — no regressions
30
+
8
31
  ## [0.3.0] - 2025-03-15
9
32
 
10
33
  ### Added
package/README.md CHANGED
@@ -5,7 +5,7 @@ Embedded FHIR R4 persistence layer — CRUD, search, indexing, and schema migrat
5
5
  [![npm version](https://img.shields.io/npm/v/fhir-persistence)](https://www.npmjs.com/package/fhir-persistence)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
7
7
 
8
- > **v0.3.0** — Dual-backend validated: 1014 tests across SQLite and PostgreSQL
8
+ > **v0.4.0** — Full PostgreSQL DDL compatibility for all system and lookup tables
9
9
 
10
10
  ## Features
11
11
 
@@ -6560,16 +6560,18 @@ var FhirStore = class {
6560
6560
  var import_node_crypto5 = require("node:crypto");
6561
6561
 
6562
6562
  // src/repo/lookup-table-writer.ts
6563
- var LOOKUP_TABLE_DDL = {
6564
- HumanName: `CREATE TABLE IF NOT EXISTS "HumanName" (
6565
- "id" INTEGER PRIMARY KEY AUTOINCREMENT,
6563
+ function buildLookupTableDDL(dialect) {
6564
+ const pk = dialect === "postgres" ? '"id" SERIAL PRIMARY KEY' : '"id" INTEGER PRIMARY KEY AUTOINCREMENT';
6565
+ return {
6566
+ HumanName: `CREATE TABLE IF NOT EXISTS "HumanName" (
6567
+ ${pk},
6566
6568
  "resourceId" TEXT NOT NULL,
6567
6569
  "name" TEXT,
6568
6570
  "given" TEXT,
6569
6571
  "family" TEXT
6570
6572
  )`,
6571
- Address: `CREATE TABLE IF NOT EXISTS "Address" (
6572
- "id" INTEGER PRIMARY KEY AUTOINCREMENT,
6573
+ Address: `CREATE TABLE IF NOT EXISTS "Address" (
6574
+ ${pk},
6573
6575
  "resourceId" TEXT NOT NULL,
6574
6576
  "address" TEXT,
6575
6577
  "city" TEXT,
@@ -6578,20 +6580,21 @@ var LOOKUP_TABLE_DDL = {
6578
6580
  "state" TEXT,
6579
6581
  "use" TEXT
6580
6582
  )`,
6581
- ContactPoint: `CREATE TABLE IF NOT EXISTS "ContactPoint" (
6582
- "id" INTEGER PRIMARY KEY AUTOINCREMENT,
6583
+ ContactPoint: `CREATE TABLE IF NOT EXISTS "ContactPoint" (
6584
+ ${pk},
6583
6585
  "resourceId" TEXT NOT NULL,
6584
6586
  "system" TEXT,
6585
6587
  "value" TEXT,
6586
6588
  "use" TEXT
6587
6589
  )`,
6588
- Identifier: `CREATE TABLE IF NOT EXISTS "Identifier" (
6589
- "id" INTEGER PRIMARY KEY AUTOINCREMENT,
6590
+ Identifier: `CREATE TABLE IF NOT EXISTS "Identifier" (
6591
+ ${pk},
6590
6592
  "resourceId" TEXT NOT NULL,
6591
6593
  "system" TEXT,
6592
6594
  "value" TEXT
6593
6595
  )`
6594
- };
6596
+ };
6597
+ }
6595
6598
  var LOOKUP_TABLE_INDEXES = {
6596
6599
  HumanName: [
6597
6600
  'CREATE INDEX IF NOT EXISTS "HumanName_resourceId_idx" ON "HumanName" ("resourceId")',
@@ -6617,10 +6620,12 @@ var LOOKUP_COLUMNS = {
6617
6620
  Identifier: ["resourceId", "system", "value"]
6618
6621
  };
6619
6622
  var LookupTableWriter = class {
6620
- constructor(adapter) {
6623
+ constructor(adapter, dialect = "sqlite") {
6621
6624
  this.adapter = adapter;
6625
+ this.ddl = buildLookupTableDDL(dialect);
6622
6626
  }
6623
6627
  initialized = false;
6628
+ ddl;
6624
6629
  // ---------------------------------------------------------------------------
6625
6630
  // DDL
6626
6631
  // ---------------------------------------------------------------------------
@@ -6629,8 +6634,8 @@ var LookupTableWriter = class {
6629
6634
  */
6630
6635
  async ensureTables() {
6631
6636
  if (this.initialized) return;
6632
- for (const table of Object.keys(LOOKUP_TABLE_DDL)) {
6633
- await this.adapter.execute(LOOKUP_TABLE_DDL[table]);
6637
+ for (const table of Object.keys(this.ddl)) {
6638
+ await this.adapter.execute(this.ddl[table]);
6634
6639
  for (const idx of LOOKUP_TABLE_INDEXES[table]) {
6635
6640
  await this.adapter.execute(idx);
6636
6641
  }
@@ -6679,7 +6684,7 @@ var LookupTableWriter = class {
6679
6684
  */
6680
6685
  async deleteRows(resourceId) {
6681
6686
  await this.ensureTables();
6682
- for (const table of Object.keys(LOOKUP_TABLE_DDL)) {
6687
+ for (const table of Object.keys(this.ddl)) {
6683
6688
  await this.adapter.execute(
6684
6689
  `DELETE FROM "${table}" WHERE "resourceId" = ?`,
6685
6690
  [resourceId]
@@ -6705,7 +6710,7 @@ var LookupTableWriter = class {
6705
6710
  var IndexingPipeline = class {
6706
6711
  constructor(adapter, options) {
6707
6712
  this.adapter = adapter;
6708
- this.lookupWriter = new LookupTableWriter(adapter);
6713
+ this.lookupWriter = new LookupTableWriter(adapter, options?.dialect ?? "sqlite");
6709
6714
  this.runtimeProvider = options?.runtimeProvider;
6710
6715
  this.options = {
6711
6716
  enableLookupTables: options?.enableLookupTables ?? true,
@@ -7403,35 +7408,43 @@ function mapColumnTypeForDialect(type, dialect) {
7403
7408
 
7404
7409
  // src/registry/package-registry-repo.ts
7405
7410
  var PACKAGES_TABLE = "_packages";
7406
- var CREATE_PACKAGES_TABLE = `
7411
+ var SCHEMA_VERSION_TABLE = "_schema_version";
7412
+ function createPackagesTableDDL(dialect) {
7413
+ const ts = dialect === "postgres" ? "TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" : "TEXT NOT NULL DEFAULT (datetime('now'))";
7414
+ return `
7407
7415
  CREATE TABLE IF NOT EXISTS "${PACKAGES_TABLE}" (
7408
7416
  "name" TEXT NOT NULL,
7409
7417
  "version" TEXT NOT NULL,
7410
7418
  "checksum" TEXT NOT NULL,
7411
7419
  "schemaSnapshot" TEXT,
7412
- "installedAt" TEXT NOT NULL DEFAULT (datetime('now')),
7420
+ "installedAt" ${ts},
7413
7421
  "status" TEXT NOT NULL DEFAULT 'active',
7414
7422
  PRIMARY KEY ("name", "version")
7415
7423
  );
7416
7424
  `;
7417
- var SCHEMA_VERSION_TABLE = "_schema_version";
7418
- var CREATE_SCHEMA_VERSION_TABLE = `
7425
+ }
7426
+ function createSchemaVersionTableDDL(dialect) {
7427
+ const ts = dialect === "postgres" ? "TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" : "TEXT NOT NULL DEFAULT (datetime('now'))";
7428
+ return `
7419
7429
  CREATE TABLE IF NOT EXISTS "${SCHEMA_VERSION_TABLE}" (
7420
7430
  "version" INTEGER NOT NULL PRIMARY KEY,
7421
7431
  "packageList" TEXT NOT NULL,
7422
7432
  "description" TEXT NOT NULL DEFAULT '',
7423
- "appliedAt" TEXT NOT NULL DEFAULT (datetime('now'))
7433
+ "appliedAt" ${ts}
7424
7434
  );
7425
7435
  `;
7436
+ }
7426
7437
  var PackageRegistryRepo = class {
7427
- constructor(adapter) {
7438
+ constructor(adapter, dialect = "sqlite") {
7428
7439
  this.adapter = adapter;
7440
+ this.dialect = dialect;
7429
7441
  }
7442
+ dialect;
7430
7443
  /**
7431
7444
  * Ensure the packages tracking table exists.
7432
7445
  */
7433
7446
  async ensureTable() {
7434
- await this.adapter.execute(CREATE_PACKAGES_TABLE);
7447
+ await this.adapter.execute(createPackagesTableDDL(this.dialect));
7435
7448
  }
7436
7449
  /**
7437
7450
  * Get the active version of a package by name.
@@ -7476,7 +7489,7 @@ var PackageRegistryRepo = class {
7476
7489
  [pkg.name]
7477
7490
  );
7478
7491
  await this.adapter.execute(
7479
- `INSERT OR REPLACE INTO "${PACKAGES_TABLE}" ("name", "version", "checksum", "schemaSnapshot", "status") VALUES (?, ?, ?, ?, 'active')`,
7492
+ this.upsertSQL(),
7480
7493
  [pkg.name, pkg.version, pkg.checksum, pkg.schemaSnapshot ?? null]
7481
7494
  );
7482
7495
  await this.recordSchemaVersion(description ?? `Register ${pkg.name}@${pkg.version}`);
@@ -7493,10 +7506,19 @@ var PackageRegistryRepo = class {
7493
7506
  [pkg.name]
7494
7507
  );
7495
7508
  await this.adapter.execute(
7496
- `INSERT OR REPLACE INTO "${PACKAGES_TABLE}" ("name", "version", "checksum", "schemaSnapshot", "status") VALUES (?, ?, ?, ?, 'active')`,
7509
+ this.upsertSQL(),
7497
7510
  [pkg.name, pkg.version, pkg.checksum, pkg.schemaSnapshot ?? null]
7498
7511
  );
7499
7512
  }
7513
+ /**
7514
+ * Generate dialect-aware UPSERT SQL.
7515
+ */
7516
+ upsertSQL() {
7517
+ if (this.dialect === "postgres") {
7518
+ return `INSERT INTO "${PACKAGES_TABLE}" ("name", "version", "checksum", "schemaSnapshot", "status") VALUES (?, ?, ?, ?, 'active') ON CONFLICT ("name", "version") DO UPDATE SET "checksum" = EXCLUDED."checksum", "schemaSnapshot" = EXCLUDED."schemaSnapshot", "status" = 'active'`;
7519
+ }
7520
+ return `INSERT OR REPLACE INTO "${PACKAGES_TABLE}" ("name", "version", "checksum", "schemaSnapshot", "status") VALUES (?, ?, ?, ?, 'active')`;
7521
+ }
7500
7522
  /**
7501
7523
  * Remove all versions of a package.
7502
7524
  */
@@ -7528,7 +7550,7 @@ var PackageRegistryRepo = class {
7528
7550
  * Ensure the schema version table exists.
7529
7551
  */
7530
7552
  async ensureSchemaVersionTable() {
7531
- await this.adapter.execute(CREATE_SCHEMA_VERSION_TABLE);
7553
+ await this.adapter.execute(createSchemaVersionTableDDL(this.dialect));
7532
7554
  }
7533
7555
  /**
7534
7556
  * Record a schema version with the current active package list.
@@ -7578,12 +7600,25 @@ CREATE TABLE IF NOT EXISTS "${TRACKING_TABLE_V2}" (
7578
7600
  "applied_at" TEXT NOT NULL DEFAULT (datetime('now'))
7579
7601
  );
7580
7602
  `;
7603
+ var CREATE_TRACKING_TABLE_V2_POSTGRES = `
7604
+ CREATE TABLE IF NOT EXISTS "${TRACKING_TABLE_V2}" (
7605
+ "version" INTEGER PRIMARY KEY,
7606
+ "description" TEXT NOT NULL,
7607
+ "type" TEXT NOT NULL DEFAULT 'file',
7608
+ "applied_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
7609
+ );
7610
+ `;
7611
+ function createTrackingDDL(dialect) {
7612
+ return dialect === "postgres" ? CREATE_TRACKING_TABLE_V2_POSTGRES : CREATE_TRACKING_TABLE_V2_SQLITE;
7613
+ }
7581
7614
  var MigrationRunnerV2 = class {
7582
7615
  adapter;
7583
7616
  migrations;
7584
- constructor(adapter, migrations = []) {
7617
+ dialect;
7618
+ constructor(adapter, migrations = [], dialect = "sqlite") {
7585
7619
  this.adapter = adapter;
7586
7620
  this.migrations = [...migrations].sort((a, b) => a.version - b.version);
7621
+ this.dialect = dialect;
7587
7622
  }
7588
7623
  // ---------------------------------------------------------------------------
7589
7624
  // Public API
@@ -7592,7 +7627,7 @@ var MigrationRunnerV2 = class {
7592
7627
  * Ensure the tracking table exists.
7593
7628
  */
7594
7629
  async ensureTrackingTable() {
7595
- await this.adapter.execute(CREATE_TRACKING_TABLE_V2_SQLITE);
7630
+ await this.adapter.execute(createTrackingDDL(this.dialect));
7596
7631
  }
7597
7632
  /**
7598
7633
  * Apply all pending migrations (or up to a target version).
@@ -7768,7 +7803,23 @@ var MigrationRunnerV2 = class {
7768
7803
 
7769
7804
  // src/migration/reindex-scheduler.ts
7770
7805
  var REINDEX_JOBS_TABLE = "_reindex_jobs";
7771
- var CREATE_REINDEX_JOBS_TABLE = `
7806
+ function createReindexJobsTableDDL(dialect) {
7807
+ if (dialect === "postgres") {
7808
+ return `
7809
+ CREATE TABLE IF NOT EXISTS "${REINDEX_JOBS_TABLE}" (
7810
+ "id" SERIAL PRIMARY KEY,
7811
+ "resourceType" TEXT NOT NULL,
7812
+ "searchParamCode" TEXT NOT NULL,
7813
+ "expression" TEXT NOT NULL,
7814
+ "status" TEXT NOT NULL DEFAULT 'pending',
7815
+ "cursor" TEXT,
7816
+ "processedCount" INTEGER NOT NULL DEFAULT 0,
7817
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
7818
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
7819
+ );
7820
+ `;
7821
+ }
7822
+ return `
7772
7823
  CREATE TABLE IF NOT EXISTS "${REINDEX_JOBS_TABLE}" (
7773
7824
  "id" INTEGER PRIMARY KEY AUTOINCREMENT,
7774
7825
  "resourceType" TEXT NOT NULL,
@@ -7781,15 +7832,21 @@ CREATE TABLE IF NOT EXISTS "${REINDEX_JOBS_TABLE}" (
7781
7832
  "updatedAt" TEXT NOT NULL DEFAULT (datetime('now'))
7782
7833
  );
7783
7834
  `;
7835
+ }
7836
+ function nowExpression(dialect) {
7837
+ return dialect === "postgres" ? "CURRENT_TIMESTAMP" : "datetime('now')";
7838
+ }
7784
7839
  var ReindexScheduler = class {
7785
- constructor(adapter) {
7840
+ constructor(adapter, dialect = "sqlite") {
7786
7841
  this.adapter = adapter;
7842
+ this.dialect = dialect;
7787
7843
  }
7844
+ dialect;
7788
7845
  /**
7789
7846
  * Ensure the reindex jobs table exists.
7790
7847
  */
7791
7848
  async ensureTable() {
7792
- await this.adapter.execute(CREATE_REINDEX_JOBS_TABLE);
7849
+ await this.adapter.execute(createReindexJobsTableDDL(this.dialect));
7793
7850
  }
7794
7851
  /**
7795
7852
  * Schedule reindex jobs from REINDEX deltas.
@@ -7856,7 +7913,7 @@ var ReindexScheduler = class {
7856
7913
  sets.push('"processedCount" = ?');
7857
7914
  values.push(update.processedCount);
7858
7915
  }
7859
- sets.push(`"updatedAt" = datetime('now')`);
7916
+ sets.push(`"updatedAt" = ${nowExpression(this.dialect)}`);
7860
7917
  values.push(id);
7861
7918
  await this.adapter.execute(
7862
7919
  `UPDATE "${REINDEX_JOBS_TABLE}" SET ${sets.join(", ")} WHERE "id" = ?`,
@@ -7889,9 +7946,9 @@ var IGPersistenceManager = class {
7889
7946
  reindexScheduler;
7890
7947
  constructor(adapter, dialect = "sqlite") {
7891
7948
  this.dialect = dialect;
7892
- this.packageRepo = new PackageRegistryRepo(adapter);
7893
- this.migrationRunner = new MigrationRunnerV2(adapter);
7894
- this.reindexScheduler = new ReindexScheduler(adapter);
7949
+ this.packageRepo = new PackageRegistryRepo(adapter, dialect);
7950
+ this.migrationRunner = new MigrationRunnerV2(adapter, [], dialect);
7951
+ this.reindexScheduler = new ReindexScheduler(adapter, dialect);
7895
7952
  }
7896
7953
  /**
7897
7954
  * Initialize an IG package — the main entry point.
@@ -8072,25 +8129,30 @@ var TerminologyCodeRepo = class {
8072
8129
 
8073
8130
  // src/terminology/valueset-repo.ts
8074
8131
  var VALUESETS_TABLE = "terminology_valuesets";
8075
- var CREATE_VALUESETS_TABLE = `
8132
+ function createValuesetsTableDDL(dialect) {
8133
+ const ts = dialect === "postgres" ? "TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" : "TEXT NOT NULL DEFAULT (datetime('now'))";
8134
+ return `
8076
8135
  CREATE TABLE IF NOT EXISTS "${VALUESETS_TABLE}" (
8077
8136
  "url" TEXT NOT NULL,
8078
8137
  "version" TEXT NOT NULL,
8079
8138
  "name" TEXT,
8080
8139
  "content" TEXT NOT NULL,
8081
- "storedAt" TEXT NOT NULL DEFAULT (datetime('now')),
8140
+ "storedAt" ${ts},
8082
8141
  PRIMARY KEY ("url", "version")
8083
8142
  );
8084
8143
  `;
8144
+ }
8085
8145
  var ValueSetRepo = class {
8086
- constructor(adapter) {
8146
+ constructor(adapter, dialect = "sqlite") {
8087
8147
  this.adapter = adapter;
8148
+ this.dialect = dialect;
8088
8149
  }
8150
+ dialect;
8089
8151
  /**
8090
8152
  * Ensure the terminology_valuesets table exists.
8091
8153
  */
8092
8154
  async ensureTable() {
8093
- await this.adapter.execute(CREATE_VALUESETS_TABLE);
8155
+ await this.adapter.execute(createValuesetsTableDDL(this.dialect));
8094
8156
  }
8095
8157
  /**
8096
8158
  * Insert or update a ValueSet.
@@ -8098,10 +8160,8 @@ var ValueSetRepo = class {
8098
8160
  */
8099
8161
  async upsert(input) {
8100
8162
  await this.ensureTable();
8101
- await this.adapter.execute(
8102
- `INSERT OR REPLACE INTO "${VALUESETS_TABLE}" ("url", "version", "name", "content") VALUES (?, ?, ?, ?)`,
8103
- [input.url, input.version, input.name ?? null, input.content]
8104
- );
8163
+ const sql = this.dialect === "postgres" ? `INSERT INTO "${VALUESETS_TABLE}" ("url", "version", "name", "content") VALUES (?, ?, ?, ?) ON CONFLICT ("url", "version") DO UPDATE SET "name" = EXCLUDED."name", "content" = EXCLUDED."content"` : `INSERT OR REPLACE INTO "${VALUESETS_TABLE}" ("url", "version", "name", "content") VALUES (?, ?, ?, ?)`;
8164
+ await this.adapter.execute(sql, [input.url, input.version, input.name ?? null, input.content]);
8105
8165
  }
8106
8166
  /**
8107
8167
  * Get a specific ValueSet by url and version.
@@ -8560,7 +8620,8 @@ var FhirSystem = class {
8560
8620
  indexing: {
8561
8621
  enableLookupTables: this.options.enableLookupTables,
8562
8622
  enableReferences: this.options.enableReferences,
8563
- runtimeProvider: this.options.runtimeProvider
8623
+ runtimeProvider: this.options.runtimeProvider,
8624
+ dialect: this.dialect
8564
8625
  }
8565
8626
  });
8566
8627
  return {