fhir-persistence 0.7.0 → 0.9.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,52 @@ 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.9.0] - 2025-03-20
9
+
10
+ ### Fixed
11
+
12
+ #### Bug-3: Token WHERE clause code-only / empty-system matching failure (P0)
13
+
14
+ - **`buildTokenColumnFragmentV2()`** (v2, SQLite) and **`buildTokenColumnFragment()`** (v1, PostgreSQL) — corrected token value resolution logic per FHIR spec:
15
+ - `system|code` → exact match against stored `"system|code"` ✅
16
+ - `|code` → exact match against stored `"|code"` (empty system, keep pipe) ✅
17
+ - `system|` → `LIKE "system|%"` (any code within system) ✅
18
+ - `code` (bare, no pipe) → `LIKE "%|code"` (any system, match code) ✅
19
+ - **Root cause**: Previous logic stripped leading `|` from `|code` values and used exact match for bare codes, causing zero results for `gender=male` (stored as `"|male"`) and `gender=|male`
20
+ - **Impact**: All token searches using bare code or `|code` format now correctly match stored values
21
+ - New helper: `arrayContainsLikeV2()` generates dialect-aware `LIKE` subqueries (SQLite `json_each` / PostgreSQL `unnest`)
22
+
23
+ ### Test Coverage
24
+
25
+ - **1065 total tests** (1057 passing, 8 skipped) across 63 test files — no regressions
26
+ - **5 new Bug-3 regression tests** in v2 and v1 test suites verifying all four token formats
27
+
28
+ ## [0.8.0] - 2025-03-20
29
+
30
+ ### Fixed
31
+
32
+ #### Bug-1 + Bug-2: Token-column WHERE clause column name mismatch (P0)
33
+
34
+ - **`buildTokenColumnFragment()`** (v1, PostgreSQL `$N` placeholders) — changed `__${columnName}Text` → `__${columnName}` to match DDL column name
35
+ - **`buildTokenColumnFragmentV2()`** (v2, SQLite `?` placeholders) — same fix, aligning with DDL `__<name>` column
36
+ - **Root cause**: WHERE clause referenced a non-existent `__genderText` column while DDL only creates `__gender` (TEXT, JSON array of `system|code` strings) and `__genderSort` (TEXT, display)
37
+ - **Impact**: All token search queries (`GET /Patient?gender=male`) previously failed with `no such column: __genderText`
38
+ - DDL / INSERT / WHERE column names now fully aligned: `__<name>` for token array, `__<name>Sort` for display text
39
+
40
+ ### Changed
41
+
42
+ - **`search-parameter-registry.ts`** — Corrected `SearchStrategy` documentation: token-column uses 2 columns (not 3)
43
+
44
+ ### Confirmed
45
+
46
+ - **Enh-1**: R4 standard SP loading (`name`, `identifier`, etc.) depends on external `DefinitionProvider` — not a persistence-layer issue
47
+ - **Enh-2**: `ifMatch` optimistic locking is fully implemented in both `FhirStore` and `FhirPersistence` with test coverage
48
+
49
+ ### Test Coverage
50
+
51
+ - **1062 total tests** (1054 passing, 8 skipped) across 63 test files — no regressions
52
+ - **1 new regression test** for Bug-1: verifies token column references `__<name>` not `__<name>Text`
53
+
8
54
  ## [0.7.0] - 2025-03-18
9
55
 
10
56
  ### 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.7.0** — Conformance storage module for IG Explorer: 6 repos + import orchestrator
8
+ > **v0.9.0** — Token search fix: FHIR-correct code-only / empty-system matching (`|code`, `code`)
9
9
 
10
10
  ## Features
11
11
 
@@ -5339,36 +5339,43 @@ function buildUriFragmentV2(impl, param, dialect) {
5339
5339
  return buildOrFragmentV2(col, "=", param.values);
5340
5340
  }
5341
5341
  function buildTokenColumnFragmentV2(impl, param, dialect) {
5342
- const textCol = quoteColumn(`__${impl.columnName}Text`);
5342
+ const textCol = quoteColumn(`__${impl.columnName}`);
5343
5343
  const sortCol = quoteColumn(`__${impl.columnName}Sort`);
5344
5344
  if (param.modifier === "text") {
5345
5345
  return buildLikeFragmentV2(sortCol, param.values, "", "%");
5346
5346
  }
5347
- const needsLike = param.values.some((v) => v.endsWith("|"));
5348
- if (needsLike) {
5349
- const conds = [];
5350
- const vals = [];
5351
- for (const value of param.values) {
5347
+ const exactValues = [];
5348
+ const likePatterns = [];
5349
+ for (const value of param.values) {
5350
+ if (value.includes("|")) {
5352
5351
  if (value.endsWith("|")) {
5353
- const frag = arrayContainsLikeV2(textCol, value + "%", dialect);
5354
- conds.push(frag.sql);
5355
- vals.push(...frag.values);
5352
+ likePatterns.push(value + "%");
5356
5353
  } else {
5357
- const resolved = value.startsWith("|") ? value.slice(1) : value;
5358
- const frag = arrayContainsV2(textCol, [resolved], dialect);
5359
- conds.push(frag.sql);
5360
- vals.push(...frag.values);
5354
+ exactValues.push(value);
5361
5355
  }
5356
+ } else {
5357
+ likePatterns.push(`%|${value}`);
5362
5358
  }
5363
- const inner = conds.length === 1 ? conds[0] : `(${conds.join(" OR ")})`;
5364
- const sql = param.modifier === "not" ? `NOT (${inner})` : inner;
5365
- return { sql, values: vals };
5366
5359
  }
5367
- const resolvedValues = param.values.map((v) => v.startsWith("|") ? v.slice(1) : v);
5368
- if (param.modifier === "not") {
5369
- return arrayNotContainsV2(textCol, resolvedValues, dialect);
5370
- }
5371
- return arrayContainsV2(textCol, resolvedValues, dialect);
5360
+ const conds = [];
5361
+ const vals = [];
5362
+ if (exactValues.length > 0) {
5363
+ const frag = param.modifier === "not" ? arrayNotContainsV2(textCol, exactValues, dialect) : arrayContainsV2(textCol, exactValues, dialect);
5364
+ conds.push(frag.sql);
5365
+ vals.push(...frag.values);
5366
+ }
5367
+ for (const pattern of likePatterns) {
5368
+ const frag = arrayContainsLikeV2(textCol, pattern, dialect);
5369
+ const sql = param.modifier === "not" ? `NOT (${frag.sql})` : frag.sql;
5370
+ conds.push(sql);
5371
+ vals.push(...frag.values);
5372
+ }
5373
+ if (conds.length === 0) {
5374
+ return { sql: "1=1", values: [] };
5375
+ }
5376
+ const joiner = param.modifier === "not" ? " AND " : " OR ";
5377
+ const inner = conds.length === 1 ? conds[0] : `(${conds.join(joiner)})`;
5378
+ return { sql: inner, values: vals };
5372
5379
  }
5373
5380
  function buildDefaultFragmentV2(impl, param) {
5374
5381
  const col = quoteColumn(impl.columnName);