fhir-persistence 0.9.0 → 0.10.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.
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BetterSqlite3Adapter: () => BetterSqlite3Adapter,
34
34
  ConceptHierarchyRepo: () => ConceptHierarchyRepo,
35
+ ConditionalService: () => ConditionalService,
35
36
  DEFAULT_SEARCH_COUNT: () => DEFAULT_SEARCH_COUNT,
36
37
  DELETED_SCHEMA_VERSION: () => DELETED_SCHEMA_VERSION,
37
38
  ElementIndexRepo: () => ElementIndexRepo,
@@ -98,10 +99,12 @@ __export(index_exports, {
98
99
  buildTwoPhaseSearchSQLv2: () => buildTwoPhaseSearchSQLv2,
99
100
  buildTypeHistorySQLv2: () => buildTypeHistorySQLv2,
100
101
  buildUpdateMainSQLv2: () => buildUpdateMainSQLv2,
102
+ buildUrnMap: () => buildUrnMap,
101
103
  buildWhereClauseV2: () => buildWhereClauseV2,
102
104
  buildWhereFragmentV2: () => buildWhereFragmentV2,
103
105
  compareSchemas: () => compareSchemas,
104
106
  createFhirRuntimeProvider: () => createFhirRuntimeProvider,
107
+ deepResolveUrns: () => deepResolveUrns,
105
108
  executeSearch: () => executeSearchV2,
106
109
  extractPrefix: () => extractPrefix,
107
110
  extractPropertyPath: () => extractPropertyPath,
@@ -124,6 +127,8 @@ __export(index_exports, {
124
127
  parseSortParam: () => parseSortParam,
125
128
  planSearch: () => planSearch,
126
129
  prefixToOperator: () => prefixToOperator,
130
+ processBatchV2: () => processBatchV2,
131
+ processTransactionV2: () => processTransactionV2,
127
132
  reindexAllV2: () => reindexAllV2,
128
133
  reindexResourceTypeV2: () => reindexResourceTypeV2,
129
134
  splitSearchValues: () => splitSearchValues
@@ -4277,6 +4282,18 @@ var ResourceGoneError = class extends RepositoryError {
4277
4282
  this.resourceId = id;
4278
4283
  }
4279
4284
  };
4285
+ var PreconditionFailedError = class extends RepositoryError {
4286
+ name = "PreconditionFailedError";
4287
+ resourceType;
4288
+ matchCount;
4289
+ constructor(resourceType, matchCount) {
4290
+ super(
4291
+ `Conditional operation on ${resourceType} matched ${matchCount} resources (expected 0 or 1)`
4292
+ );
4293
+ this.resourceType = resourceType;
4294
+ this.matchCount = matchCount;
4295
+ }
4296
+ };
4280
4297
  var ResourceVersionConflictError = class extends RepositoryError {
4281
4298
  name = "ResourceVersionConflictError";
4282
4299
  resourceType;
@@ -5254,6 +5271,15 @@ function buildLookupTableFragmentV2(impl, param, resourceType) {
5254
5271
  const colRef = `__lookup."${column}"`;
5255
5272
  const outerIdRef = `"${resourceType}"."id"`;
5256
5273
  if (param.modifier === "exact") {
5274
+ if (table === "HumanName" && column === "name") {
5275
+ const colConds = [];
5276
+ const allVals = [];
5277
+ for (const v of param.values) {
5278
+ colConds.push(`__lookup."family" = ? OR __lookup."given" = ?`);
5279
+ allVals.push(v, v);
5280
+ }
5281
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND (${colConds.join(" OR ")}))`, values: allVals };
5282
+ }
5257
5283
  if (param.values.length === 1) {
5258
5284
  return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND ${colRef} = ?)`, values: [param.values[0]] };
5259
5285
  }
@@ -6434,9 +6460,41 @@ var FhirStore = class {
6434
6460
  const selectSQL = buildSelectByIdSQLv2(resourceType);
6435
6461
  const current = await this.adapter.queryOne(selectSQL, [id]);
6436
6462
  if (!current) {
6463
+ if (options?.upsert) {
6464
+ const created = await this.createResource(resourceType, resource, { assignedId: id });
6465
+ return { resource: created, created: true };
6466
+ }
6437
6467
  throw new ResourceNotFoundError(resourceType, id);
6438
6468
  }
6439
6469
  if (current.deleted === 1) {
6470
+ if (options?.upsert) {
6471
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
6472
+ const versionId2 = (0, import_node_crypto4.randomUUID)();
6473
+ const persisted2 = {
6474
+ ...resource,
6475
+ resourceType,
6476
+ id,
6477
+ meta: { ...resource.meta, versionId: versionId2, lastUpdated: now2 }
6478
+ };
6479
+ const content2 = JSON.stringify(persisted2);
6480
+ const mainRow2 = {
6481
+ id,
6482
+ versionId: versionId2,
6483
+ content: content2,
6484
+ lastUpdated: now2,
6485
+ deleted: 0,
6486
+ _source: persisted2.meta?.source ?? null,
6487
+ _profile: persisted2.meta?.profile ? JSON.stringify(persisted2.meta.profile) : null
6488
+ };
6489
+ const historyRow2 = { id, versionId: versionId2, content: content2, lastUpdated: now2, deleted: 0 };
6490
+ await this.adapter.transaction(async (tx) => {
6491
+ const updateSQL = buildUpdateMainSQLv2(resourceType, mainRow2);
6492
+ await tx.execute(updateSQL.sql, updateSQL.values);
6493
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow2);
6494
+ await tx.execute(histSQL.sql, histSQL.values);
6495
+ });
6496
+ return { resource: persisted2, created: true };
6497
+ }
6440
6498
  throw new ResourceGoneError(resourceType, id);
6441
6499
  }
6442
6500
  if (options?.ifMatch) {
@@ -6487,7 +6545,7 @@ var FhirStore = class {
6487
6545
  const delRefSQL = buildDeleteReferencesSQLv2(`${resourceType}_References`);
6488
6546
  await tx.execute(delRefSQL, [id]);
6489
6547
  });
6490
- return persisted;
6548
+ return { resource: persisted, created: false };
6491
6549
  }
6492
6550
  // ---------------------------------------------------------------------------
6493
6551
  // DELETE (soft)
@@ -6557,6 +6615,24 @@ var FhirStore = class {
6557
6615
  }));
6558
6616
  }
6559
6617
  // ---------------------------------------------------------------------------
6618
+ // TYPE-LEVEL HISTORY (PERS-06)
6619
+ // ---------------------------------------------------------------------------
6620
+ async readTypeHistory(resourceType, options) {
6621
+ const { sql, values } = buildTypeHistorySQLv2(
6622
+ `${resourceType}_History`,
6623
+ options
6624
+ );
6625
+ const rows = await this.adapter.query(sql, values);
6626
+ return rows.map((row) => ({
6627
+ id: row.id,
6628
+ versionId: row.versionId,
6629
+ lastUpdated: row.lastUpdated,
6630
+ deleted: row.deleted === 1,
6631
+ resourceType,
6632
+ resource: row.deleted === 1 ? null : JSON.parse(row.content)
6633
+ }));
6634
+ }
6635
+ // ---------------------------------------------------------------------------
6560
6636
  // WRITE REFERENCES (utility for external callers)
6561
6637
  // ---------------------------------------------------------------------------
6562
6638
  async writeReferences(resourceType, resourceId, refs) {
@@ -6938,6 +7014,25 @@ var FhirPersistence = class {
6938
7014
  // CREATE
6939
7015
  // ---------------------------------------------------------------------------
6940
7016
  async createResource(resourceType, resource, options) {
7017
+ if (options?.ifNoneExist && options.ifNoneExist.length > 0) {
7018
+ const searchSQL = buildSearchSQLv2(
7019
+ { resourceType, params: options.ifNoneExist, count: 2 },
7020
+ this.registry
7021
+ );
7022
+ const matches = await this.adapter.query(
7023
+ searchSQL.sql,
7024
+ searchSQL.values
7025
+ );
7026
+ if (matches.length > 1) {
7027
+ throw new PreconditionFailedError(resourceType, matches.length);
7028
+ }
7029
+ if (matches.length === 1) {
7030
+ return {
7031
+ resource: JSON.parse(matches[0].content),
7032
+ created: false
7033
+ };
7034
+ }
7035
+ }
6941
7036
  const now = (/* @__PURE__ */ new Date()).toISOString();
6942
7037
  const id = options?.assignedId ?? resource.id ?? (0, import_node_crypto5.randomUUID)();
6943
7038
  const versionId = (0, import_node_crypto5.randomUUID)();
@@ -6977,7 +7072,7 @@ var FhirPersistence = class {
6977
7072
  const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
6978
7073
  await tx.execute(histSQL.sql, histSQL.values);
6979
7074
  });
6980
- return persisted;
7075
+ return { resource: persisted, created: true };
6981
7076
  }
6982
7077
  // ---------------------------------------------------------------------------
6983
7078
  // READ
@@ -7004,9 +7099,44 @@ var FhirPersistence = class {
7004
7099
  const selectSQL = buildSelectByIdSQLv2(resourceType);
7005
7100
  const current = await this.adapter.queryOne(selectSQL, [id]);
7006
7101
  if (!current) {
7102
+ if (options?.upsert) {
7103
+ const result = await this.createResource(resourceType, resource, { assignedId: id });
7104
+ return { resource: result.resource, created: true };
7105
+ }
7007
7106
  throw new ResourceNotFoundError(resourceType, id);
7008
7107
  }
7009
7108
  if (current.deleted === 1) {
7109
+ if (options?.upsert) {
7110
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
7111
+ const versionId2 = (0, import_node_crypto5.randomUUID)();
7112
+ const persisted2 = {
7113
+ ...resource,
7114
+ resourceType,
7115
+ id,
7116
+ meta: { ...resource.meta, versionId: versionId2, lastUpdated: now2 }
7117
+ };
7118
+ const impls2 = this.getImpls(resourceType);
7119
+ const indexResult2 = await this.pipeline.indexResource(resourceType, persisted2, impls2);
7120
+ const content2 = JSON.stringify(persisted2);
7121
+ const mainRow2 = {
7122
+ id,
7123
+ versionId: versionId2,
7124
+ content: content2,
7125
+ lastUpdated: now2,
7126
+ deleted: 0,
7127
+ _source: persisted2.meta?.source ?? null,
7128
+ _profile: persisted2.meta?.profile ? JSON.stringify(persisted2.meta.profile) : null,
7129
+ ...indexResult2.searchColumns
7130
+ };
7131
+ const historyRow2 = { id, versionId: versionId2, content: content2, lastUpdated: now2, deleted: 0 };
7132
+ await this.adapter.transaction(async (tx) => {
7133
+ const updateSQL = buildUpdateMainSQLv2(resourceType, mainRow2);
7134
+ await tx.execute(updateSQL.sql, updateSQL.values);
7135
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow2);
7136
+ await tx.execute(histSQL.sql, histSQL.values);
7137
+ });
7138
+ return { resource: persisted2, created: true };
7139
+ }
7010
7140
  throw new ResourceGoneError(resourceType, id);
7011
7141
  }
7012
7142
  if (options?.ifMatch) {
@@ -7058,7 +7188,7 @@ var FhirPersistence = class {
7058
7188
  const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
7059
7189
  await tx.execute(histSQL.sql, histSQL.values);
7060
7190
  });
7061
- return persisted;
7191
+ return { resource: persisted, created: false };
7062
7192
  }
7063
7193
  // ---------------------------------------------------------------------------
7064
7194
  // DELETE (soft)
@@ -7127,6 +7257,24 @@ var FhirPersistence = class {
7127
7257
  }));
7128
7258
  }
7129
7259
  // ---------------------------------------------------------------------------
7260
+ // TYPE-LEVEL HISTORY (PERS-06)
7261
+ // ---------------------------------------------------------------------------
7262
+ async readTypeHistory(resourceType, options) {
7263
+ const { sql, values } = buildTypeHistorySQLv2(
7264
+ `${resourceType}_History`,
7265
+ options
7266
+ );
7267
+ const rows = await this.adapter.query(sql, values);
7268
+ return rows.map((row) => ({
7269
+ id: row.id,
7270
+ versionId: row.versionId,
7271
+ lastUpdated: row.lastUpdated,
7272
+ deleted: row.deleted === 1,
7273
+ resourceType,
7274
+ resource: row.deleted === 1 ? null : JSON.parse(row.content)
7275
+ }));
7276
+ }
7277
+ // ---------------------------------------------------------------------------
7130
7278
  // SEARCH (streaming)
7131
7279
  // ---------------------------------------------------------------------------
7132
7280
  /**
@@ -7177,6 +7325,636 @@ var FhirPersistence = class {
7177
7325
  }
7178
7326
  };
7179
7327
 
7328
+ // src/store/conditional-service.ts
7329
+ var import_node_crypto6 = require("node:crypto");
7330
+ var ConditionalService = class {
7331
+ constructor(adapter, registry) {
7332
+ this.adapter = adapter;
7333
+ this.registry = registry;
7334
+ }
7335
+ // ---------------------------------------------------------------------------
7336
+ // conditionalCreate (If-None-Exist)
7337
+ // ---------------------------------------------------------------------------
7338
+ /**
7339
+ * Conditional create: search for existing matches, create only if none found.
7340
+ *
7341
+ * - 0 matches → create new resource
7342
+ * - 1 match → return existing resource (no-op)
7343
+ * - 2+ matches → PreconditionFailedError
7344
+ *
7345
+ * @param resourceType - FHIR resource type
7346
+ * @param resource - The resource to create
7347
+ * @param searchParams - Search criteria (If-None-Exist)
7348
+ */
7349
+ async conditionalCreate(resourceType, resource, searchParams) {
7350
+ const matches = await this.searchMatches(resourceType, searchParams, 2);
7351
+ if (matches.length > 1) {
7352
+ throw new PreconditionFailedError(resourceType, matches.length);
7353
+ }
7354
+ if (matches.length === 1) {
7355
+ return {
7356
+ outcome: "existing",
7357
+ resource: JSON.parse(matches[0].content)
7358
+ };
7359
+ }
7360
+ const persisted = await this.createInTransaction(resourceType, resource);
7361
+ return { outcome: "created", resource: persisted };
7362
+ }
7363
+ // ---------------------------------------------------------------------------
7364
+ // conditionalUpdate (search-based PUT)
7365
+ // ---------------------------------------------------------------------------
7366
+ /**
7367
+ * Conditional update: search for matches, update if exactly one found.
7368
+ *
7369
+ * - 0 matches → create new resource
7370
+ * - 1 match → update existing resource
7371
+ * - 2+ matches → PreconditionFailedError
7372
+ *
7373
+ * @param resourceType - FHIR resource type
7374
+ * @param resource - The resource to create/update
7375
+ * @param searchParams - Search criteria
7376
+ */
7377
+ async conditionalUpdate(resourceType, resource, searchParams) {
7378
+ const matches = await this.searchMatches(resourceType, searchParams, 2);
7379
+ if (matches.length > 1) {
7380
+ throw new PreconditionFailedError(resourceType, matches.length);
7381
+ }
7382
+ if (matches.length === 0) {
7383
+ const persisted2 = await this.createInTransaction(resourceType, resource);
7384
+ return { outcome: "created", resource: persisted2 };
7385
+ }
7386
+ const existing = matches[0];
7387
+ const persisted = await this.updateInTransaction(
7388
+ resourceType,
7389
+ resource,
7390
+ existing.id
7391
+ );
7392
+ return { outcome: "updated", resource: persisted };
7393
+ }
7394
+ // ---------------------------------------------------------------------------
7395
+ // conditionalDelete
7396
+ // ---------------------------------------------------------------------------
7397
+ /**
7398
+ * Conditional delete: delete all resources matching the search criteria.
7399
+ *
7400
+ * @param resourceType - FHIR resource type
7401
+ * @param searchParams - Search criteria
7402
+ * @returns Number of resources deleted
7403
+ */
7404
+ async conditionalDelete(resourceType, searchParams) {
7405
+ const matches = await this.searchMatches(resourceType, searchParams, 1e3);
7406
+ if (matches.length === 0) {
7407
+ return { count: 0 };
7408
+ }
7409
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7410
+ await this.adapter.transaction(async (tx) => {
7411
+ for (const match of matches) {
7412
+ const versionId = (0, import_node_crypto6.randomUUID)();
7413
+ const deleteRow = {
7414
+ id: match.id,
7415
+ versionId,
7416
+ content: match.content,
7417
+ lastUpdated: now,
7418
+ deleted: 1
7419
+ };
7420
+ const historyRow = {
7421
+ id: match.id,
7422
+ versionId,
7423
+ content: match.content,
7424
+ lastUpdated: now,
7425
+ deleted: 1
7426
+ };
7427
+ const updateSQL = buildUpdateMainSQLv2(resourceType, deleteRow);
7428
+ await tx.execute(updateSQL.sql, updateSQL.values);
7429
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
7430
+ await tx.execute(histSQL.sql, histSQL.values);
7431
+ }
7432
+ });
7433
+ return { count: matches.length };
7434
+ }
7435
+ // ---------------------------------------------------------------------------
7436
+ // Private: Search for matches
7437
+ // ---------------------------------------------------------------------------
7438
+ async searchMatches(resourceType, searchParams, limit) {
7439
+ const searchSQL = buildSearchSQLv2(
7440
+ {
7441
+ resourceType,
7442
+ params: searchParams,
7443
+ count: limit
7444
+ },
7445
+ this.registry
7446
+ );
7447
+ return this.adapter.query(
7448
+ searchSQL.sql,
7449
+ searchSQL.values
7450
+ );
7451
+ }
7452
+ // ---------------------------------------------------------------------------
7453
+ // Private: Create in transaction
7454
+ // ---------------------------------------------------------------------------
7455
+ async createInTransaction(resourceType, resource) {
7456
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7457
+ const id = resource.id ?? (0, import_node_crypto6.randomUUID)();
7458
+ const versionId = (0, import_node_crypto6.randomUUID)();
7459
+ const persisted = {
7460
+ ...resource,
7461
+ resourceType,
7462
+ id,
7463
+ meta: {
7464
+ ...resource.meta,
7465
+ versionId,
7466
+ lastUpdated: now
7467
+ }
7468
+ };
7469
+ const content = JSON.stringify(persisted);
7470
+ const mainRow = {
7471
+ id,
7472
+ versionId,
7473
+ content,
7474
+ lastUpdated: now,
7475
+ deleted: 0
7476
+ };
7477
+ const historyRow = {
7478
+ id,
7479
+ versionId,
7480
+ content,
7481
+ lastUpdated: now,
7482
+ deleted: 0
7483
+ };
7484
+ await this.adapter.transaction(async (tx) => {
7485
+ const mainSQL = buildInsertMainSQLv2(resourceType, mainRow);
7486
+ await tx.execute(mainSQL.sql, mainSQL.values);
7487
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
7488
+ await tx.execute(histSQL.sql, histSQL.values);
7489
+ });
7490
+ return persisted;
7491
+ }
7492
+ // ---------------------------------------------------------------------------
7493
+ // Private: Update in transaction
7494
+ // ---------------------------------------------------------------------------
7495
+ async updateInTransaction(resourceType, resource, existingId) {
7496
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7497
+ const versionId = (0, import_node_crypto6.randomUUID)();
7498
+ const persisted = {
7499
+ ...resource,
7500
+ resourceType,
7501
+ id: existingId,
7502
+ meta: {
7503
+ ...resource.meta,
7504
+ versionId,
7505
+ lastUpdated: now
7506
+ }
7507
+ };
7508
+ const content = JSON.stringify(persisted);
7509
+ const mainRow = {
7510
+ id: existingId,
7511
+ versionId,
7512
+ content,
7513
+ lastUpdated: now,
7514
+ deleted: 0
7515
+ };
7516
+ const historyRow = {
7517
+ id: existingId,
7518
+ versionId,
7519
+ content,
7520
+ lastUpdated: now,
7521
+ deleted: 0
7522
+ };
7523
+ await this.adapter.transaction(async (tx) => {
7524
+ const updateSQL = buildUpdateMainSQLv2(resourceType, mainRow);
7525
+ await tx.execute(updateSQL.sql, updateSQL.values);
7526
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
7527
+ await tx.execute(histSQL.sql, histSQL.values);
7528
+ });
7529
+ return persisted;
7530
+ }
7531
+ };
7532
+
7533
+ // src/transaction/urn-resolver.ts
7534
+ var import_node_crypto7 = require("node:crypto");
7535
+ function buildUrnMap(entries) {
7536
+ const map = /* @__PURE__ */ new Map();
7537
+ for (const entry of entries) {
7538
+ if (entry.request?.method === "POST" && entry.fullUrl?.startsWith("urn:uuid:") && entry.resource?.resourceType) {
7539
+ const newId = (0, import_node_crypto7.randomUUID)();
7540
+ map.set(entry.fullUrl, {
7541
+ id: newId,
7542
+ resourceType: entry.resource.resourceType
7543
+ });
7544
+ }
7545
+ }
7546
+ return map;
7547
+ }
7548
+ function deepResolveUrns(resource, urnMap) {
7549
+ if (urnMap.size === 0) return resource;
7550
+ const clone = structuredClone(resource);
7551
+ const stack = [clone];
7552
+ while (stack.length > 0) {
7553
+ const current = stack.pop();
7554
+ if (current === null || current === void 0 || typeof current !== "object") {
7555
+ continue;
7556
+ }
7557
+ if (Array.isArray(current)) {
7558
+ for (const item of current) {
7559
+ if (typeof item === "object" && item !== null) {
7560
+ stack.push(item);
7561
+ }
7562
+ }
7563
+ continue;
7564
+ }
7565
+ const record = current;
7566
+ if (typeof record.reference === "string") {
7567
+ const target = urnMap.get(record.reference);
7568
+ if (target) {
7569
+ record.reference = `${target.resourceType}/${target.id}`;
7570
+ }
7571
+ }
7572
+ for (const key of Object.keys(record)) {
7573
+ const value = record[key];
7574
+ if (typeof value === "object" && value !== null) {
7575
+ stack.push(value);
7576
+ }
7577
+ }
7578
+ }
7579
+ return clone;
7580
+ }
7581
+
7582
+ // src/transaction/bundle-processor.ts
7583
+ function parseRequestUrl(url) {
7584
+ const qIdx = url.indexOf("?");
7585
+ let path = url;
7586
+ let query;
7587
+ if (qIdx !== -1) {
7588
+ path = url.substring(0, qIdx);
7589
+ query = url.substring(qIdx + 1);
7590
+ }
7591
+ const parts = path.split("/");
7592
+ return { resourceType: parts[0], id: parts[1], query };
7593
+ }
7594
+ async function processTransactionV2(store, adapter, bundle) {
7595
+ const entries = bundle.entry ?? [];
7596
+ if (entries.length === 0) {
7597
+ return { resourceType: "Bundle", type: "transaction-response", entry: [] };
7598
+ }
7599
+ const urnMap = buildUrnMap(entries);
7600
+ try {
7601
+ const responseEntries = await adapter.transaction(async (tx) => {
7602
+ const results = [];
7603
+ for (const entry of entries) {
7604
+ const result = await processEntryInTransaction(tx, entry, urnMap);
7605
+ results.push(result);
7606
+ }
7607
+ return results;
7608
+ });
7609
+ return {
7610
+ resourceType: "Bundle",
7611
+ type: "transaction-response",
7612
+ entry: responseEntries
7613
+ };
7614
+ } catch (err) {
7615
+ const message = err instanceof Error ? err.message : String(err);
7616
+ return {
7617
+ resourceType: "Bundle",
7618
+ type: "transaction-response",
7619
+ entry: [{
7620
+ response: {
7621
+ status: "500",
7622
+ outcome: {
7623
+ issue: [{ severity: "error", code: "exception", diagnostics: message }]
7624
+ }
7625
+ }
7626
+ }]
7627
+ };
7628
+ }
7629
+ }
7630
+ async function processBatchV2(store, bundle) {
7631
+ const entries = bundle.entry ?? [];
7632
+ const responseEntries = [];
7633
+ for (const entry of entries) {
7634
+ if (entry.fullUrl?.startsWith("urn:uuid:")) {
7635
+ responseEntries.push({
7636
+ response: {
7637
+ status: "400",
7638
+ outcome: {
7639
+ issue: [{ severity: "error", code: "invalid", diagnostics: "urn:uuid references are not allowed in batch mode" }]
7640
+ }
7641
+ }
7642
+ });
7643
+ continue;
7644
+ }
7645
+ try {
7646
+ const result = await processBatchEntry(store, entry);
7647
+ responseEntries.push(result);
7648
+ } catch (err) {
7649
+ const message = err instanceof Error ? err.message : String(err);
7650
+ const status = errorToStatus(err);
7651
+ responseEntries.push({
7652
+ response: {
7653
+ status,
7654
+ outcome: {
7655
+ issue: [{ severity: "error", code: "exception", diagnostics: message }]
7656
+ }
7657
+ }
7658
+ });
7659
+ }
7660
+ }
7661
+ return {
7662
+ resourceType: "Bundle",
7663
+ type: "batch-response",
7664
+ entry: responseEntries
7665
+ };
7666
+ }
7667
+ async function processEntryInTransaction(tx, entry, urnMap) {
7668
+ if (!entry.request) {
7669
+ throw new Error("Missing request");
7670
+ }
7671
+ const { method, url } = entry.request;
7672
+ const { resourceType, id } = parseRequestUrl(url);
7673
+ switch (method) {
7674
+ case "POST": {
7675
+ if (!entry.resource) {
7676
+ throw new Error("Missing resource for POST");
7677
+ }
7678
+ if (entry.request.ifNoneExist) {
7679
+ const matchResult = await checkIfNoneExistInTx(tx, resourceType, entry.request.ifNoneExist);
7680
+ if (matchResult.status !== "create") {
7681
+ return matchResult.response;
7682
+ }
7683
+ }
7684
+ const resolved = deepResolveUrns(entry.resource, urnMap);
7685
+ let assignedId;
7686
+ if (entry.fullUrl?.startsWith("urn:uuid:")) {
7687
+ const target = urnMap.get(entry.fullUrl);
7688
+ if (target) assignedId = target.id;
7689
+ }
7690
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7691
+ const { randomUUID: randomUUID7 } = require("node:crypto");
7692
+ const versionId = randomUUID7();
7693
+ const finalId = assignedId ?? resolved.id ?? randomUUID7();
7694
+ const persisted = {
7695
+ ...resolved,
7696
+ resourceType,
7697
+ id: finalId,
7698
+ meta: {
7699
+ ...resolved.meta,
7700
+ versionId,
7701
+ lastUpdated: now
7702
+ }
7703
+ };
7704
+ const content = JSON.stringify(persisted);
7705
+ await tx.execute(
7706
+ `INSERT INTO "${resourceType}" ("id", "versionId", "content", "lastUpdated", "deleted", "_source", "_profile") VALUES (?, ?, ?, ?, 0, ?, ?)`,
7707
+ [finalId, versionId, content, now, persisted.meta?.source ?? null, persisted.meta?.profile ? JSON.stringify(persisted.meta.profile) : null]
7708
+ );
7709
+ await tx.execute(
7710
+ `INSERT INTO "${resourceType}_History" ("id", "versionId", "content", "lastUpdated", "deleted") VALUES (?, ?, ?, ?, 0)`,
7711
+ [finalId, versionId, content, now]
7712
+ );
7713
+ return {
7714
+ resource: persisted,
7715
+ response: {
7716
+ status: "201",
7717
+ location: `${resourceType}/${finalId}/_history/${versionId}`,
7718
+ etag: `W/"${versionId}"`,
7719
+ lastModified: now
7720
+ }
7721
+ };
7722
+ }
7723
+ case "PUT": {
7724
+ if (!entry.resource) {
7725
+ throw new Error("Missing resource for PUT");
7726
+ }
7727
+ if (!id) {
7728
+ throw new Error("PUT requires resource ID in URL");
7729
+ }
7730
+ const resolved = deepResolveUrns(entry.resource, urnMap);
7731
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7732
+ const { randomUUID: randomUUID7 } = require("node:crypto");
7733
+ const versionId = randomUUID7();
7734
+ const persisted = {
7735
+ ...resolved,
7736
+ resourceType,
7737
+ id,
7738
+ meta: {
7739
+ ...resolved.meta,
7740
+ versionId,
7741
+ lastUpdated: now
7742
+ }
7743
+ };
7744
+ const content = JSON.stringify(persisted);
7745
+ const existing = await tx.queryOne(
7746
+ `SELECT "id", "deleted" FROM "${resourceType}" WHERE "id" = ?`,
7747
+ [id]
7748
+ );
7749
+ if (existing) {
7750
+ await tx.execute(
7751
+ `UPDATE "${resourceType}" SET "versionId" = ?, "content" = ?, "lastUpdated" = ?, "deleted" = 0, "_source" = ?, "_profile" = ? WHERE "id" = ?`,
7752
+ [versionId, content, now, persisted.meta?.source ?? null, persisted.meta?.profile ? JSON.stringify(persisted.meta.profile) : null, id]
7753
+ );
7754
+ } else {
7755
+ await tx.execute(
7756
+ `INSERT INTO "${resourceType}" ("id", "versionId", "content", "lastUpdated", "deleted", "_source", "_profile") VALUES (?, ?, ?, ?, 0, ?, ?)`,
7757
+ [id, versionId, content, now, persisted.meta?.source ?? null, persisted.meta?.profile ? JSON.stringify(persisted.meta.profile) : null]
7758
+ );
7759
+ }
7760
+ await tx.execute(
7761
+ `INSERT INTO "${resourceType}_History" ("id", "versionId", "content", "lastUpdated", "deleted") VALUES (?, ?, ?, ?, 0)`,
7762
+ [id, versionId, content, now]
7763
+ );
7764
+ return {
7765
+ resource: persisted,
7766
+ response: {
7767
+ status: existing ? "200" : "201",
7768
+ location: `${resourceType}/${id}/_history/${versionId}`,
7769
+ etag: `W/"${versionId}"`,
7770
+ lastModified: now
7771
+ }
7772
+ };
7773
+ }
7774
+ case "DELETE": {
7775
+ if (!id) {
7776
+ throw new Error("DELETE requires resource ID");
7777
+ }
7778
+ const existing = await tx.queryOne(
7779
+ `SELECT "id", "content", "deleted" FROM "${resourceType}" WHERE "id" = ?`,
7780
+ [id]
7781
+ );
7782
+ if (!existing || existing.deleted === 1) {
7783
+ return { response: { status: "204" } };
7784
+ }
7785
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7786
+ const { randomUUID: randomUUID7 } = require("node:crypto");
7787
+ const versionId = randomUUID7();
7788
+ await tx.execute(
7789
+ `UPDATE "${resourceType}" SET "versionId" = ?, "lastUpdated" = ?, "deleted" = 1 WHERE "id" = ?`,
7790
+ [versionId, now, id]
7791
+ );
7792
+ await tx.execute(
7793
+ `INSERT INTO "${resourceType}_History" ("id", "versionId", "content", "lastUpdated", "deleted") VALUES (?, ?, ?, ?, 1)`,
7794
+ [id, versionId, existing.content, now]
7795
+ );
7796
+ await tx.execute(
7797
+ `DELETE FROM "${resourceType}_References" WHERE "resourceId" = ?`,
7798
+ [id]
7799
+ );
7800
+ return { response: { status: "204" } };
7801
+ }
7802
+ case "GET": {
7803
+ if (!id) {
7804
+ throw new Error("GET requires resource ID");
7805
+ }
7806
+ const row = await tx.queryOne(
7807
+ `SELECT "content", "deleted" FROM "${resourceType}" WHERE "id" = ?`,
7808
+ [id]
7809
+ );
7810
+ if (!row) {
7811
+ throw new ResourceNotFoundError(resourceType, id);
7812
+ }
7813
+ if (row.deleted === 1) {
7814
+ throw new ResourceGoneError(resourceType, id);
7815
+ }
7816
+ const resource = JSON.parse(row.content);
7817
+ return { resource, response: { status: "200" } };
7818
+ }
7819
+ default:
7820
+ throw new Error(`Unsupported method: ${method}`);
7821
+ }
7822
+ }
7823
+ async function checkIfNoneExistInTx(tx, resourceType, ifNoneExist) {
7824
+ const params = new URLSearchParams(ifNoneExist);
7825
+ const conditions = [];
7826
+ const values = [];
7827
+ for (const [key, value] of params.entries()) {
7828
+ if (key === "_id") {
7829
+ conditions.push('"id" = ?');
7830
+ values.push(value);
7831
+ } else if (key === "identifier") {
7832
+ conditions.push('"content" LIKE ?');
7833
+ values.push(`%${value}%`);
7834
+ } else {
7835
+ conditions.push(`"${key}" = ?`);
7836
+ values.push(value);
7837
+ }
7838
+ }
7839
+ if (conditions.length === 0) {
7840
+ return { status: "create" };
7841
+ }
7842
+ const whereClause = conditions.join(" AND ");
7843
+ const rows = await tx.query(
7844
+ `SELECT "id", "content", "versionId", "lastUpdated" FROM "${resourceType}" WHERE "deleted" = 0 AND ${whereClause}`,
7845
+ values
7846
+ );
7847
+ if (rows.length === 0) {
7848
+ return { status: "create" };
7849
+ }
7850
+ if (rows.length === 1) {
7851
+ const existing = JSON.parse(rows[0].content);
7852
+ return {
7853
+ status: "existing",
7854
+ response: {
7855
+ resource: existing,
7856
+ response: {
7857
+ status: "200",
7858
+ location: `${resourceType}/${rows[0].id}/_history/${rows[0].versionId}`,
7859
+ etag: `W/"${rows[0].versionId}"`,
7860
+ lastModified: rows[0].lastUpdated
7861
+ }
7862
+ }
7863
+ };
7864
+ }
7865
+ return {
7866
+ status: "error",
7867
+ response: {
7868
+ response: {
7869
+ status: "412",
7870
+ outcome: {
7871
+ issue: [{
7872
+ severity: "error",
7873
+ code: "duplicate",
7874
+ diagnostics: `If-None-Exist matched ${rows.length} resources for ${resourceType}`
7875
+ }]
7876
+ }
7877
+ }
7878
+ }
7879
+ };
7880
+ }
7881
+ async function processBatchEntry(store, entry) {
7882
+ if (!entry.request) {
7883
+ return errorResponse("400", "Missing request");
7884
+ }
7885
+ const { method, url } = entry.request;
7886
+ const { resourceType, id } = parseRequestUrl(url);
7887
+ switch (method) {
7888
+ case "POST": {
7889
+ if (!entry.resource) {
7890
+ return errorResponse("400", "Missing resource for POST");
7891
+ }
7892
+ const created = await store.createResource(resourceType, entry.resource);
7893
+ const versionId = created.meta?.versionId ?? "";
7894
+ const lastUpdated = created.meta?.lastUpdated ?? "";
7895
+ return {
7896
+ resource: created,
7897
+ response: {
7898
+ status: "201",
7899
+ location: `${resourceType}/${created.id}/_history/${versionId}`,
7900
+ etag: `W/"${versionId}"`,
7901
+ lastModified: lastUpdated
7902
+ }
7903
+ };
7904
+ }
7905
+ case "PUT": {
7906
+ if (!entry.resource || !id) {
7907
+ return errorResponse("400", "PUT requires resource and ID");
7908
+ }
7909
+ const toUpdate = { ...entry.resource, id };
7910
+ const updateResult = await store.updateResource(resourceType, toUpdate, { upsert: true });
7911
+ const updated = updateResult.resource;
7912
+ const versionId = updated.meta?.versionId ?? "";
7913
+ const lastUpdated = updated.meta?.lastUpdated ?? "";
7914
+ return {
7915
+ resource: updated,
7916
+ response: {
7917
+ status: updateResult.created ? "201" : "200",
7918
+ location: `${resourceType}/${id}/_history/${versionId}`,
7919
+ etag: `W/"${versionId}"`,
7920
+ lastModified: lastUpdated
7921
+ }
7922
+ };
7923
+ }
7924
+ case "DELETE": {
7925
+ if (!id) {
7926
+ return errorResponse("400", "DELETE requires resource ID");
7927
+ }
7928
+ await store.deleteResource(resourceType, id);
7929
+ return { response: { status: "204" } };
7930
+ }
7931
+ case "GET": {
7932
+ if (!id) {
7933
+ return errorResponse("400", "GET requires resource ID");
7934
+ }
7935
+ const resource = await store.readResource(resourceType, id);
7936
+ return { resource, response: { status: "200" } };
7937
+ }
7938
+ default:
7939
+ return errorResponse("400", `Unsupported method: ${method}`);
7940
+ }
7941
+ }
7942
+ function errorResponse(status, message) {
7943
+ return {
7944
+ response: {
7945
+ status,
7946
+ outcome: {
7947
+ issue: [{ severity: "error", code: "processing", diagnostics: message }]
7948
+ }
7949
+ }
7950
+ };
7951
+ }
7952
+ function errorToStatus(err) {
7953
+ if (err instanceof ResourceNotFoundError) return "404";
7954
+ if (err instanceof ResourceGoneError) return "410";
7955
+ return "500";
7956
+ }
7957
+
7180
7958
  // src/migration/schema-diff.ts
7181
7959
  function compareSchemas(oldSets, newSets) {
7182
7960
  const deltas = [];
@@ -9634,6 +10412,7 @@ var IGImportOrchestrator = class {
9634
10412
  0 && (module.exports = {
9635
10413
  BetterSqlite3Adapter,
9636
10414
  ConceptHierarchyRepo,
10415
+ ConditionalService,
9637
10416
  DEFAULT_SEARCH_COUNT,
9638
10417
  DELETED_SCHEMA_VERSION,
9639
10418
  ElementIndexRepo,
@@ -9700,10 +10479,12 @@ var IGImportOrchestrator = class {
9700
10479
  buildTwoPhaseSearchSQLv2,
9701
10480
  buildTypeHistorySQLv2,
9702
10481
  buildUpdateMainSQLv2,
10482
+ buildUrnMap,
9703
10483
  buildWhereClauseV2,
9704
10484
  buildWhereFragmentV2,
9705
10485
  compareSchemas,
9706
10486
  createFhirRuntimeProvider,
10487
+ deepResolveUrns,
9707
10488
  executeSearch,
9708
10489
  extractPrefix,
9709
10490
  extractPropertyPath,
@@ -9726,6 +10507,8 @@ var IGImportOrchestrator = class {
9726
10507
  parseSortParam,
9727
10508
  planSearch,
9728
10509
  prefixToOperator,
10510
+ processBatchV2,
10511
+ processTransactionV2,
9729
10512
  reindexAllV2,
9730
10513
  reindexResourceTypeV2,
9731
10514
  splitSearchValues