fhir-persistence 0.4.0 → 0.5.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,26 @@ 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.5.0] - 2025-03-16
9
+
10
+ ### Fixed
11
+
12
+ #### PostgreSQL Migration Path Fixes
13
+
14
+ - **`migration-generator.ts`** — IG migration path now creates PostgreSQL extensions (`pg_trgm`, `btree_gin`) and helper function (`token_array_to_text`) before generating GIN indexes, fixing "no default operator class for access method gin" error
15
+ - **`where-builder.ts`** — Lookup table EXISTS subqueries now use fully-qualified `"ResourceType"."id"` instead of ambiguous `"id"`, fixing PostgreSQL "operator does not exist: text = integer" error in name/address/telecom searches
16
+
17
+ ### Changed
18
+
19
+ - **`buildWhereFragment` (v1)** — Added optional `resourceType` parameter, passed to `buildLookupTableFragment`
20
+ - **`buildWhereFragmentV2` (v2)** — Added optional `resourceType` parameter, passed to `buildLookupTableFragmentV2`
21
+ - Both `buildLookupTableFragment*` functions now generate `outerIdRef = "ResourceType"."id"` to eliminate column name ambiguity in PostgreSQL
22
+
23
+ ### Test Coverage
24
+
25
+ - **1014 total tests** (1006 passing, 8 skipped) across 56 test files — no regressions
26
+ - Updated 7 lookup table test assertions to use qualified column references
27
+
8
28
  ## [0.4.0] - 2025-03-15
9
29
 
10
30
  ### Fixed
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.4.0** — Full PostgreSQL DDL compatibility for all system and lookup tables
8
+ > **v0.5.0** — PostgreSQL migration path complete: GIN extensions + lookup table fixes
9
9
 
10
10
  ## Features
11
11
 
@@ -5202,12 +5202,12 @@ function arrayContainsLikeV2(col, value, dialect) {
5202
5202
  }
5203
5203
  return { sql: `EXISTS (SELECT 1 FROM json_each(${col}) WHERE json_each.value LIKE ?)`, values: [value] };
5204
5204
  }
5205
- function buildWhereFragmentV2(impl, param, dialect) {
5205
+ function buildWhereFragmentV2(impl, param, dialect, resourceType) {
5206
5206
  if (param.modifier === "missing") {
5207
5207
  return buildMissingFragmentV2(impl, param);
5208
5208
  }
5209
5209
  if (impl.strategy === "lookup-table") {
5210
- return buildLookupTableFragmentV2(impl, param);
5210
+ return buildLookupTableFragmentV2(impl, param, resourceType ?? "Resource");
5211
5211
  }
5212
5212
  if (impl.strategy === "token-column") {
5213
5213
  return buildTokenColumnFragmentV2(impl, param, dialect);
@@ -5235,7 +5235,7 @@ function buildMissingFragmentV2(impl, param) {
5235
5235
  const col = quoteColumn(impl.columnName);
5236
5236
  return { sql: isMissing ? `${col} IS NULL` : `${col} IS NOT NULL`, values: [] };
5237
5237
  }
5238
- function buildLookupTableFragmentV2(impl, param) {
5238
+ function buildLookupTableFragmentV2(impl, param, resourceType) {
5239
5239
  const mapping = LOOKUP_TABLE_MAP[impl.code];
5240
5240
  if (!mapping) {
5241
5241
  const sortCol = quoteColumn(`__${impl.columnName}Sort`);
@@ -5245,25 +5245,26 @@ function buildLookupTableFragmentV2(impl, param) {
5245
5245
  }
5246
5246
  const { table, column } = mapping;
5247
5247
  const colRef = `__lookup."${column}"`;
5248
+ const outerIdRef = `"${resourceType}"."id"`;
5248
5249
  if (param.modifier === "exact") {
5249
5250
  if (param.values.length === 1) {
5250
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND ${colRef} = ?)`, values: [param.values[0]] };
5251
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND ${colRef} = ?)`, values: [param.values[0]] };
5251
5252
  }
5252
5253
  const conds2 = param.values.map(() => `${colRef} = ?`);
5253
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND (${conds2.join(" OR ")}))`, values: [...param.values] };
5254
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND (${conds2.join(" OR ")}))`, values: [...param.values] };
5254
5255
  }
5255
5256
  if (param.modifier === "contains") {
5256
5257
  if (param.values.length === 1) {
5257
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND LOWER(${colRef}) LIKE ?)`, values: [`%${param.values[0].toLowerCase()}%`] };
5258
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND LOWER(${colRef}) LIKE ?)`, values: [`%${param.values[0].toLowerCase()}%`] };
5258
5259
  }
5259
5260
  const conds2 = param.values.map(() => `LOWER(${colRef}) LIKE ?`);
5260
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND (${conds2.join(" OR ")}))`, values: param.values.map((v) => `%${v.toLowerCase()}%`) };
5261
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND (${conds2.join(" OR ")}))`, values: param.values.map((v) => `%${v.toLowerCase()}%`) };
5261
5262
  }
5262
5263
  if (param.values.length === 1) {
5263
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND LOWER(${colRef}) LIKE ?)`, values: [`${param.values[0].toLowerCase()}%`] };
5264
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND LOWER(${colRef}) LIKE ?)`, values: [`${param.values[0].toLowerCase()}%`] };
5264
5265
  }
5265
5266
  const conds = param.values.map(() => `LOWER(${colRef}) LIKE ?`);
5266
- return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = "id" AND (${conds.join(" OR ")}))`, values: param.values.map((v) => `${v.toLowerCase()}%`) };
5267
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND (${conds.join(" OR ")}))`, values: param.values.map((v) => `${v.toLowerCase()}%`) };
5267
5268
  }
5268
5269
  function buildStringFragmentV2(impl, param) {
5269
5270
  const col = quoteColumn(impl.columnName);
@@ -5432,7 +5433,7 @@ function buildWhereClauseV2(params, registry, resourceType, dialect) {
5432
5433
  }
5433
5434
  const impl = resolveImplV2(param, registry, resourceType);
5434
5435
  if (!impl) continue;
5435
- const fragment = buildWhereFragmentV2(impl, param, dialect);
5436
+ const fragment = buildWhereFragmentV2(impl, param, dialect, resourceType);
5436
5437
  if (fragment) {
5437
5438
  fragments.push(fragment);
5438
5439
  }
@@ -7280,6 +7281,14 @@ function generateMigration(deltas, dialect) {
7280
7281
  const down = [];
7281
7282
  const reindexDeltas = [];
7282
7283
  const descriptions = [];
7284
+ const needsPgExtensions = deltas.some((d) => d.kind === "ADD_TABLE" || d.kind === "ADD_INDEX");
7285
+ if (dialect === "postgres" && needsPgExtensions) {
7286
+ up.push("CREATE EXTENSION IF NOT EXISTS pg_trgm;");
7287
+ up.push("CREATE EXTENSION IF NOT EXISTS btree_gin;");
7288
+ up.push(
7289
+ `CREATE OR REPLACE FUNCTION token_array_to_text(arr text[]) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT array_to_string(arr, ' ') $$;`
7290
+ );
7291
+ }
7283
7292
  for (const delta of deltas) {
7284
7293
  switch (delta.kind) {
7285
7294
  case "ADD_TABLE": {