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.
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/registry/structure-definition-registry.ts
2
9
  var StructureDefinitionRegistry = class {
3
10
  profiles = /* @__PURE__ */ new Map();
@@ -4145,6 +4152,18 @@ var ResourceGoneError = class extends RepositoryError {
4145
4152
  this.resourceId = id;
4146
4153
  }
4147
4154
  };
4155
+ var PreconditionFailedError = class extends RepositoryError {
4156
+ name = "PreconditionFailedError";
4157
+ resourceType;
4158
+ matchCount;
4159
+ constructor(resourceType, matchCount) {
4160
+ super(
4161
+ `Conditional operation on ${resourceType} matched ${matchCount} resources (expected 0 or 1)`
4162
+ );
4163
+ this.resourceType = resourceType;
4164
+ this.matchCount = matchCount;
4165
+ }
4166
+ };
4148
4167
  var ResourceVersionConflictError = class extends RepositoryError {
4149
4168
  name = "ResourceVersionConflictError";
4150
4169
  resourceType;
@@ -5122,6 +5141,15 @@ function buildLookupTableFragmentV2(impl, param, resourceType) {
5122
5141
  const colRef = `__lookup."${column}"`;
5123
5142
  const outerIdRef = `"${resourceType}"."id"`;
5124
5143
  if (param.modifier === "exact") {
5144
+ if (table === "HumanName" && column === "name") {
5145
+ const colConds = [];
5146
+ const allVals = [];
5147
+ for (const v of param.values) {
5148
+ colConds.push(`__lookup."family" = ? OR __lookup."given" = ?`);
5149
+ allVals.push(v, v);
5150
+ }
5151
+ return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND (${colConds.join(" OR ")}))`, values: allVals };
5152
+ }
5125
5153
  if (param.values.length === 1) {
5126
5154
  return { sql: `EXISTS (SELECT 1 FROM "${table}" __lookup WHERE __lookup."resourceId" = ${outerIdRef} AND ${colRef} = ?)`, values: [param.values[0]] };
5127
5155
  }
@@ -6302,9 +6330,41 @@ var FhirStore = class {
6302
6330
  const selectSQL = buildSelectByIdSQLv2(resourceType);
6303
6331
  const current = await this.adapter.queryOne(selectSQL, [id]);
6304
6332
  if (!current) {
6333
+ if (options?.upsert) {
6334
+ const created = await this.createResource(resourceType, resource, { assignedId: id });
6335
+ return { resource: created, created: true };
6336
+ }
6305
6337
  throw new ResourceNotFoundError(resourceType, id);
6306
6338
  }
6307
6339
  if (current.deleted === 1) {
6340
+ if (options?.upsert) {
6341
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
6342
+ const versionId2 = randomUUID3();
6343
+ const persisted2 = {
6344
+ ...resource,
6345
+ resourceType,
6346
+ id,
6347
+ meta: { ...resource.meta, versionId: versionId2, lastUpdated: now2 }
6348
+ };
6349
+ const content2 = JSON.stringify(persisted2);
6350
+ const mainRow2 = {
6351
+ id,
6352
+ versionId: versionId2,
6353
+ content: content2,
6354
+ lastUpdated: now2,
6355
+ deleted: 0,
6356
+ _source: persisted2.meta?.source ?? null,
6357
+ _profile: persisted2.meta?.profile ? JSON.stringify(persisted2.meta.profile) : null
6358
+ };
6359
+ const historyRow2 = { id, versionId: versionId2, content: content2, lastUpdated: now2, deleted: 0 };
6360
+ await this.adapter.transaction(async (tx) => {
6361
+ const updateSQL = buildUpdateMainSQLv2(resourceType, mainRow2);
6362
+ await tx.execute(updateSQL.sql, updateSQL.values);
6363
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow2);
6364
+ await tx.execute(histSQL.sql, histSQL.values);
6365
+ });
6366
+ return { resource: persisted2, created: true };
6367
+ }
6308
6368
  throw new ResourceGoneError(resourceType, id);
6309
6369
  }
6310
6370
  if (options?.ifMatch) {
@@ -6355,7 +6415,7 @@ var FhirStore = class {
6355
6415
  const delRefSQL = buildDeleteReferencesSQLv2(`${resourceType}_References`);
6356
6416
  await tx.execute(delRefSQL, [id]);
6357
6417
  });
6358
- return persisted;
6418
+ return { resource: persisted, created: false };
6359
6419
  }
6360
6420
  // ---------------------------------------------------------------------------
6361
6421
  // DELETE (soft)
@@ -6425,6 +6485,24 @@ var FhirStore = class {
6425
6485
  }));
6426
6486
  }
6427
6487
  // ---------------------------------------------------------------------------
6488
+ // TYPE-LEVEL HISTORY (PERS-06)
6489
+ // ---------------------------------------------------------------------------
6490
+ async readTypeHistory(resourceType, options) {
6491
+ const { sql, values } = buildTypeHistorySQLv2(
6492
+ `${resourceType}_History`,
6493
+ options
6494
+ );
6495
+ const rows = await this.adapter.query(sql, values);
6496
+ return rows.map((row) => ({
6497
+ id: row.id,
6498
+ versionId: row.versionId,
6499
+ lastUpdated: row.lastUpdated,
6500
+ deleted: row.deleted === 1,
6501
+ resourceType,
6502
+ resource: row.deleted === 1 ? null : JSON.parse(row.content)
6503
+ }));
6504
+ }
6505
+ // ---------------------------------------------------------------------------
6428
6506
  // WRITE REFERENCES (utility for external callers)
6429
6507
  // ---------------------------------------------------------------------------
6430
6508
  async writeReferences(resourceType, resourceId, refs) {
@@ -6806,6 +6884,25 @@ var FhirPersistence = class {
6806
6884
  // CREATE
6807
6885
  // ---------------------------------------------------------------------------
6808
6886
  async createResource(resourceType, resource, options) {
6887
+ if (options?.ifNoneExist && options.ifNoneExist.length > 0) {
6888
+ const searchSQL = buildSearchSQLv2(
6889
+ { resourceType, params: options.ifNoneExist, count: 2 },
6890
+ this.registry
6891
+ );
6892
+ const matches = await this.adapter.query(
6893
+ searchSQL.sql,
6894
+ searchSQL.values
6895
+ );
6896
+ if (matches.length > 1) {
6897
+ throw new PreconditionFailedError(resourceType, matches.length);
6898
+ }
6899
+ if (matches.length === 1) {
6900
+ return {
6901
+ resource: JSON.parse(matches[0].content),
6902
+ created: false
6903
+ };
6904
+ }
6905
+ }
6809
6906
  const now = (/* @__PURE__ */ new Date()).toISOString();
6810
6907
  const id = options?.assignedId ?? resource.id ?? randomUUID4();
6811
6908
  const versionId = randomUUID4();
@@ -6845,7 +6942,7 @@ var FhirPersistence = class {
6845
6942
  const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
6846
6943
  await tx.execute(histSQL.sql, histSQL.values);
6847
6944
  });
6848
- return persisted;
6945
+ return { resource: persisted, created: true };
6849
6946
  }
6850
6947
  // ---------------------------------------------------------------------------
6851
6948
  // READ
@@ -6872,9 +6969,44 @@ var FhirPersistence = class {
6872
6969
  const selectSQL = buildSelectByIdSQLv2(resourceType);
6873
6970
  const current = await this.adapter.queryOne(selectSQL, [id]);
6874
6971
  if (!current) {
6972
+ if (options?.upsert) {
6973
+ const result = await this.createResource(resourceType, resource, { assignedId: id });
6974
+ return { resource: result.resource, created: true };
6975
+ }
6875
6976
  throw new ResourceNotFoundError(resourceType, id);
6876
6977
  }
6877
6978
  if (current.deleted === 1) {
6979
+ if (options?.upsert) {
6980
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
6981
+ const versionId2 = randomUUID4();
6982
+ const persisted2 = {
6983
+ ...resource,
6984
+ resourceType,
6985
+ id,
6986
+ meta: { ...resource.meta, versionId: versionId2, lastUpdated: now2 }
6987
+ };
6988
+ const impls2 = this.getImpls(resourceType);
6989
+ const indexResult2 = await this.pipeline.indexResource(resourceType, persisted2, impls2);
6990
+ const content2 = JSON.stringify(persisted2);
6991
+ const mainRow2 = {
6992
+ id,
6993
+ versionId: versionId2,
6994
+ content: content2,
6995
+ lastUpdated: now2,
6996
+ deleted: 0,
6997
+ _source: persisted2.meta?.source ?? null,
6998
+ _profile: persisted2.meta?.profile ? JSON.stringify(persisted2.meta.profile) : null,
6999
+ ...indexResult2.searchColumns
7000
+ };
7001
+ const historyRow2 = { id, versionId: versionId2, content: content2, lastUpdated: now2, deleted: 0 };
7002
+ await this.adapter.transaction(async (tx) => {
7003
+ const updateSQL = buildUpdateMainSQLv2(resourceType, mainRow2);
7004
+ await tx.execute(updateSQL.sql, updateSQL.values);
7005
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow2);
7006
+ await tx.execute(histSQL.sql, histSQL.values);
7007
+ });
7008
+ return { resource: persisted2, created: true };
7009
+ }
6878
7010
  throw new ResourceGoneError(resourceType, id);
6879
7011
  }
6880
7012
  if (options?.ifMatch) {
@@ -6926,7 +7058,7 @@ var FhirPersistence = class {
6926
7058
  const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
6927
7059
  await tx.execute(histSQL.sql, histSQL.values);
6928
7060
  });
6929
- return persisted;
7061
+ return { resource: persisted, created: false };
6930
7062
  }
6931
7063
  // ---------------------------------------------------------------------------
6932
7064
  // DELETE (soft)
@@ -6995,6 +7127,24 @@ var FhirPersistence = class {
6995
7127
  }));
6996
7128
  }
6997
7129
  // ---------------------------------------------------------------------------
7130
+ // TYPE-LEVEL HISTORY (PERS-06)
7131
+ // ---------------------------------------------------------------------------
7132
+ async readTypeHistory(resourceType, options) {
7133
+ const { sql, values } = buildTypeHistorySQLv2(
7134
+ `${resourceType}_History`,
7135
+ options
7136
+ );
7137
+ const rows = await this.adapter.query(sql, values);
7138
+ return rows.map((row) => ({
7139
+ id: row.id,
7140
+ versionId: row.versionId,
7141
+ lastUpdated: row.lastUpdated,
7142
+ deleted: row.deleted === 1,
7143
+ resourceType,
7144
+ resource: row.deleted === 1 ? null : JSON.parse(row.content)
7145
+ }));
7146
+ }
7147
+ // ---------------------------------------------------------------------------
6998
7148
  // SEARCH (streaming)
6999
7149
  // ---------------------------------------------------------------------------
7000
7150
  /**
@@ -7045,6 +7195,636 @@ var FhirPersistence = class {
7045
7195
  }
7046
7196
  };
7047
7197
 
7198
+ // src/store/conditional-service.ts
7199
+ import { randomUUID as randomUUID5 } from "node:crypto";
7200
+ var ConditionalService = class {
7201
+ constructor(adapter, registry) {
7202
+ this.adapter = adapter;
7203
+ this.registry = registry;
7204
+ }
7205
+ // ---------------------------------------------------------------------------
7206
+ // conditionalCreate (If-None-Exist)
7207
+ // ---------------------------------------------------------------------------
7208
+ /**
7209
+ * Conditional create: search for existing matches, create only if none found.
7210
+ *
7211
+ * - 0 matches → create new resource
7212
+ * - 1 match → return existing resource (no-op)
7213
+ * - 2+ matches → PreconditionFailedError
7214
+ *
7215
+ * @param resourceType - FHIR resource type
7216
+ * @param resource - The resource to create
7217
+ * @param searchParams - Search criteria (If-None-Exist)
7218
+ */
7219
+ async conditionalCreate(resourceType, resource, searchParams) {
7220
+ const matches = await this.searchMatches(resourceType, searchParams, 2);
7221
+ if (matches.length > 1) {
7222
+ throw new PreconditionFailedError(resourceType, matches.length);
7223
+ }
7224
+ if (matches.length === 1) {
7225
+ return {
7226
+ outcome: "existing",
7227
+ resource: JSON.parse(matches[0].content)
7228
+ };
7229
+ }
7230
+ const persisted = await this.createInTransaction(resourceType, resource);
7231
+ return { outcome: "created", resource: persisted };
7232
+ }
7233
+ // ---------------------------------------------------------------------------
7234
+ // conditionalUpdate (search-based PUT)
7235
+ // ---------------------------------------------------------------------------
7236
+ /**
7237
+ * Conditional update: search for matches, update if exactly one found.
7238
+ *
7239
+ * - 0 matches → create new resource
7240
+ * - 1 match → update existing resource
7241
+ * - 2+ matches → PreconditionFailedError
7242
+ *
7243
+ * @param resourceType - FHIR resource type
7244
+ * @param resource - The resource to create/update
7245
+ * @param searchParams - Search criteria
7246
+ */
7247
+ async conditionalUpdate(resourceType, resource, searchParams) {
7248
+ const matches = await this.searchMatches(resourceType, searchParams, 2);
7249
+ if (matches.length > 1) {
7250
+ throw new PreconditionFailedError(resourceType, matches.length);
7251
+ }
7252
+ if (matches.length === 0) {
7253
+ const persisted2 = await this.createInTransaction(resourceType, resource);
7254
+ return { outcome: "created", resource: persisted2 };
7255
+ }
7256
+ const existing = matches[0];
7257
+ const persisted = await this.updateInTransaction(
7258
+ resourceType,
7259
+ resource,
7260
+ existing.id
7261
+ );
7262
+ return { outcome: "updated", resource: persisted };
7263
+ }
7264
+ // ---------------------------------------------------------------------------
7265
+ // conditionalDelete
7266
+ // ---------------------------------------------------------------------------
7267
+ /**
7268
+ * Conditional delete: delete all resources matching the search criteria.
7269
+ *
7270
+ * @param resourceType - FHIR resource type
7271
+ * @param searchParams - Search criteria
7272
+ * @returns Number of resources deleted
7273
+ */
7274
+ async conditionalDelete(resourceType, searchParams) {
7275
+ const matches = await this.searchMatches(resourceType, searchParams, 1e3);
7276
+ if (matches.length === 0) {
7277
+ return { count: 0 };
7278
+ }
7279
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7280
+ await this.adapter.transaction(async (tx) => {
7281
+ for (const match of matches) {
7282
+ const versionId = randomUUID5();
7283
+ const deleteRow = {
7284
+ id: match.id,
7285
+ versionId,
7286
+ content: match.content,
7287
+ lastUpdated: now,
7288
+ deleted: 1
7289
+ };
7290
+ const historyRow = {
7291
+ id: match.id,
7292
+ versionId,
7293
+ content: match.content,
7294
+ lastUpdated: now,
7295
+ deleted: 1
7296
+ };
7297
+ const updateSQL = buildUpdateMainSQLv2(resourceType, deleteRow);
7298
+ await tx.execute(updateSQL.sql, updateSQL.values);
7299
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
7300
+ await tx.execute(histSQL.sql, histSQL.values);
7301
+ }
7302
+ });
7303
+ return { count: matches.length };
7304
+ }
7305
+ // ---------------------------------------------------------------------------
7306
+ // Private: Search for matches
7307
+ // ---------------------------------------------------------------------------
7308
+ async searchMatches(resourceType, searchParams, limit) {
7309
+ const searchSQL = buildSearchSQLv2(
7310
+ {
7311
+ resourceType,
7312
+ params: searchParams,
7313
+ count: limit
7314
+ },
7315
+ this.registry
7316
+ );
7317
+ return this.adapter.query(
7318
+ searchSQL.sql,
7319
+ searchSQL.values
7320
+ );
7321
+ }
7322
+ // ---------------------------------------------------------------------------
7323
+ // Private: Create in transaction
7324
+ // ---------------------------------------------------------------------------
7325
+ async createInTransaction(resourceType, resource) {
7326
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7327
+ const id = resource.id ?? randomUUID5();
7328
+ const versionId = randomUUID5();
7329
+ const persisted = {
7330
+ ...resource,
7331
+ resourceType,
7332
+ id,
7333
+ meta: {
7334
+ ...resource.meta,
7335
+ versionId,
7336
+ lastUpdated: now
7337
+ }
7338
+ };
7339
+ const content = JSON.stringify(persisted);
7340
+ const mainRow = {
7341
+ id,
7342
+ versionId,
7343
+ content,
7344
+ lastUpdated: now,
7345
+ deleted: 0
7346
+ };
7347
+ const historyRow = {
7348
+ id,
7349
+ versionId,
7350
+ content,
7351
+ lastUpdated: now,
7352
+ deleted: 0
7353
+ };
7354
+ await this.adapter.transaction(async (tx) => {
7355
+ const mainSQL = buildInsertMainSQLv2(resourceType, mainRow);
7356
+ await tx.execute(mainSQL.sql, mainSQL.values);
7357
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
7358
+ await tx.execute(histSQL.sql, histSQL.values);
7359
+ });
7360
+ return persisted;
7361
+ }
7362
+ // ---------------------------------------------------------------------------
7363
+ // Private: Update in transaction
7364
+ // ---------------------------------------------------------------------------
7365
+ async updateInTransaction(resourceType, resource, existingId) {
7366
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7367
+ const versionId = randomUUID5();
7368
+ const persisted = {
7369
+ ...resource,
7370
+ resourceType,
7371
+ id: existingId,
7372
+ meta: {
7373
+ ...resource.meta,
7374
+ versionId,
7375
+ lastUpdated: now
7376
+ }
7377
+ };
7378
+ const content = JSON.stringify(persisted);
7379
+ const mainRow = {
7380
+ id: existingId,
7381
+ versionId,
7382
+ content,
7383
+ lastUpdated: now,
7384
+ deleted: 0
7385
+ };
7386
+ const historyRow = {
7387
+ id: existingId,
7388
+ versionId,
7389
+ content,
7390
+ lastUpdated: now,
7391
+ deleted: 0
7392
+ };
7393
+ await this.adapter.transaction(async (tx) => {
7394
+ const updateSQL = buildUpdateMainSQLv2(resourceType, mainRow);
7395
+ await tx.execute(updateSQL.sql, updateSQL.values);
7396
+ const histSQL = buildInsertHistorySQLv2(`${resourceType}_History`, historyRow);
7397
+ await tx.execute(histSQL.sql, histSQL.values);
7398
+ });
7399
+ return persisted;
7400
+ }
7401
+ };
7402
+
7403
+ // src/transaction/urn-resolver.ts
7404
+ import { randomUUID as randomUUID6 } from "node:crypto";
7405
+ function buildUrnMap(entries) {
7406
+ const map = /* @__PURE__ */ new Map();
7407
+ for (const entry of entries) {
7408
+ if (entry.request?.method === "POST" && entry.fullUrl?.startsWith("urn:uuid:") && entry.resource?.resourceType) {
7409
+ const newId = randomUUID6();
7410
+ map.set(entry.fullUrl, {
7411
+ id: newId,
7412
+ resourceType: entry.resource.resourceType
7413
+ });
7414
+ }
7415
+ }
7416
+ return map;
7417
+ }
7418
+ function deepResolveUrns(resource, urnMap) {
7419
+ if (urnMap.size === 0) return resource;
7420
+ const clone = structuredClone(resource);
7421
+ const stack = [clone];
7422
+ while (stack.length > 0) {
7423
+ const current = stack.pop();
7424
+ if (current === null || current === void 0 || typeof current !== "object") {
7425
+ continue;
7426
+ }
7427
+ if (Array.isArray(current)) {
7428
+ for (const item of current) {
7429
+ if (typeof item === "object" && item !== null) {
7430
+ stack.push(item);
7431
+ }
7432
+ }
7433
+ continue;
7434
+ }
7435
+ const record = current;
7436
+ if (typeof record.reference === "string") {
7437
+ const target = urnMap.get(record.reference);
7438
+ if (target) {
7439
+ record.reference = `${target.resourceType}/${target.id}`;
7440
+ }
7441
+ }
7442
+ for (const key of Object.keys(record)) {
7443
+ const value = record[key];
7444
+ if (typeof value === "object" && value !== null) {
7445
+ stack.push(value);
7446
+ }
7447
+ }
7448
+ }
7449
+ return clone;
7450
+ }
7451
+
7452
+ // src/transaction/bundle-processor.ts
7453
+ function parseRequestUrl(url) {
7454
+ const qIdx = url.indexOf("?");
7455
+ let path = url;
7456
+ let query;
7457
+ if (qIdx !== -1) {
7458
+ path = url.substring(0, qIdx);
7459
+ query = url.substring(qIdx + 1);
7460
+ }
7461
+ const parts = path.split("/");
7462
+ return { resourceType: parts[0], id: parts[1], query };
7463
+ }
7464
+ async function processTransactionV2(store, adapter, bundle) {
7465
+ const entries = bundle.entry ?? [];
7466
+ if (entries.length === 0) {
7467
+ return { resourceType: "Bundle", type: "transaction-response", entry: [] };
7468
+ }
7469
+ const urnMap = buildUrnMap(entries);
7470
+ try {
7471
+ const responseEntries = await adapter.transaction(async (tx) => {
7472
+ const results = [];
7473
+ for (const entry of entries) {
7474
+ const result = await processEntryInTransaction(tx, entry, urnMap);
7475
+ results.push(result);
7476
+ }
7477
+ return results;
7478
+ });
7479
+ return {
7480
+ resourceType: "Bundle",
7481
+ type: "transaction-response",
7482
+ entry: responseEntries
7483
+ };
7484
+ } catch (err) {
7485
+ const message = err instanceof Error ? err.message : String(err);
7486
+ return {
7487
+ resourceType: "Bundle",
7488
+ type: "transaction-response",
7489
+ entry: [{
7490
+ response: {
7491
+ status: "500",
7492
+ outcome: {
7493
+ issue: [{ severity: "error", code: "exception", diagnostics: message }]
7494
+ }
7495
+ }
7496
+ }]
7497
+ };
7498
+ }
7499
+ }
7500
+ async function processBatchV2(store, bundle) {
7501
+ const entries = bundle.entry ?? [];
7502
+ const responseEntries = [];
7503
+ for (const entry of entries) {
7504
+ if (entry.fullUrl?.startsWith("urn:uuid:")) {
7505
+ responseEntries.push({
7506
+ response: {
7507
+ status: "400",
7508
+ outcome: {
7509
+ issue: [{ severity: "error", code: "invalid", diagnostics: "urn:uuid references are not allowed in batch mode" }]
7510
+ }
7511
+ }
7512
+ });
7513
+ continue;
7514
+ }
7515
+ try {
7516
+ const result = await processBatchEntry(store, entry);
7517
+ responseEntries.push(result);
7518
+ } catch (err) {
7519
+ const message = err instanceof Error ? err.message : String(err);
7520
+ const status = errorToStatus(err);
7521
+ responseEntries.push({
7522
+ response: {
7523
+ status,
7524
+ outcome: {
7525
+ issue: [{ severity: "error", code: "exception", diagnostics: message }]
7526
+ }
7527
+ }
7528
+ });
7529
+ }
7530
+ }
7531
+ return {
7532
+ resourceType: "Bundle",
7533
+ type: "batch-response",
7534
+ entry: responseEntries
7535
+ };
7536
+ }
7537
+ async function processEntryInTransaction(tx, entry, urnMap) {
7538
+ if (!entry.request) {
7539
+ throw new Error("Missing request");
7540
+ }
7541
+ const { method, url } = entry.request;
7542
+ const { resourceType, id } = parseRequestUrl(url);
7543
+ switch (method) {
7544
+ case "POST": {
7545
+ if (!entry.resource) {
7546
+ throw new Error("Missing resource for POST");
7547
+ }
7548
+ if (entry.request.ifNoneExist) {
7549
+ const matchResult = await checkIfNoneExistInTx(tx, resourceType, entry.request.ifNoneExist);
7550
+ if (matchResult.status !== "create") {
7551
+ return matchResult.response;
7552
+ }
7553
+ }
7554
+ const resolved = deepResolveUrns(entry.resource, urnMap);
7555
+ let assignedId;
7556
+ if (entry.fullUrl?.startsWith("urn:uuid:")) {
7557
+ const target = urnMap.get(entry.fullUrl);
7558
+ if (target) assignedId = target.id;
7559
+ }
7560
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7561
+ const { randomUUID: randomUUID7 } = __require("node:crypto");
7562
+ const versionId = randomUUID7();
7563
+ const finalId = assignedId ?? resolved.id ?? randomUUID7();
7564
+ const persisted = {
7565
+ ...resolved,
7566
+ resourceType,
7567
+ id: finalId,
7568
+ meta: {
7569
+ ...resolved.meta,
7570
+ versionId,
7571
+ lastUpdated: now
7572
+ }
7573
+ };
7574
+ const content = JSON.stringify(persisted);
7575
+ await tx.execute(
7576
+ `INSERT INTO "${resourceType}" ("id", "versionId", "content", "lastUpdated", "deleted", "_source", "_profile") VALUES (?, ?, ?, ?, 0, ?, ?)`,
7577
+ [finalId, versionId, content, now, persisted.meta?.source ?? null, persisted.meta?.profile ? JSON.stringify(persisted.meta.profile) : null]
7578
+ );
7579
+ await tx.execute(
7580
+ `INSERT INTO "${resourceType}_History" ("id", "versionId", "content", "lastUpdated", "deleted") VALUES (?, ?, ?, ?, 0)`,
7581
+ [finalId, versionId, content, now]
7582
+ );
7583
+ return {
7584
+ resource: persisted,
7585
+ response: {
7586
+ status: "201",
7587
+ location: `${resourceType}/${finalId}/_history/${versionId}`,
7588
+ etag: `W/"${versionId}"`,
7589
+ lastModified: now
7590
+ }
7591
+ };
7592
+ }
7593
+ case "PUT": {
7594
+ if (!entry.resource) {
7595
+ throw new Error("Missing resource for PUT");
7596
+ }
7597
+ if (!id) {
7598
+ throw new Error("PUT requires resource ID in URL");
7599
+ }
7600
+ const resolved = deepResolveUrns(entry.resource, urnMap);
7601
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7602
+ const { randomUUID: randomUUID7 } = __require("node:crypto");
7603
+ const versionId = randomUUID7();
7604
+ const persisted = {
7605
+ ...resolved,
7606
+ resourceType,
7607
+ id,
7608
+ meta: {
7609
+ ...resolved.meta,
7610
+ versionId,
7611
+ lastUpdated: now
7612
+ }
7613
+ };
7614
+ const content = JSON.stringify(persisted);
7615
+ const existing = await tx.queryOne(
7616
+ `SELECT "id", "deleted" FROM "${resourceType}" WHERE "id" = ?`,
7617
+ [id]
7618
+ );
7619
+ if (existing) {
7620
+ await tx.execute(
7621
+ `UPDATE "${resourceType}" SET "versionId" = ?, "content" = ?, "lastUpdated" = ?, "deleted" = 0, "_source" = ?, "_profile" = ? WHERE "id" = ?`,
7622
+ [versionId, content, now, persisted.meta?.source ?? null, persisted.meta?.profile ? JSON.stringify(persisted.meta.profile) : null, id]
7623
+ );
7624
+ } else {
7625
+ await tx.execute(
7626
+ `INSERT INTO "${resourceType}" ("id", "versionId", "content", "lastUpdated", "deleted", "_source", "_profile") VALUES (?, ?, ?, ?, 0, ?, ?)`,
7627
+ [id, versionId, content, now, persisted.meta?.source ?? null, persisted.meta?.profile ? JSON.stringify(persisted.meta.profile) : null]
7628
+ );
7629
+ }
7630
+ await tx.execute(
7631
+ `INSERT INTO "${resourceType}_History" ("id", "versionId", "content", "lastUpdated", "deleted") VALUES (?, ?, ?, ?, 0)`,
7632
+ [id, versionId, content, now]
7633
+ );
7634
+ return {
7635
+ resource: persisted,
7636
+ response: {
7637
+ status: existing ? "200" : "201",
7638
+ location: `${resourceType}/${id}/_history/${versionId}`,
7639
+ etag: `W/"${versionId}"`,
7640
+ lastModified: now
7641
+ }
7642
+ };
7643
+ }
7644
+ case "DELETE": {
7645
+ if (!id) {
7646
+ throw new Error("DELETE requires resource ID");
7647
+ }
7648
+ const existing = await tx.queryOne(
7649
+ `SELECT "id", "content", "deleted" FROM "${resourceType}" WHERE "id" = ?`,
7650
+ [id]
7651
+ );
7652
+ if (!existing || existing.deleted === 1) {
7653
+ return { response: { status: "204" } };
7654
+ }
7655
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7656
+ const { randomUUID: randomUUID7 } = __require("node:crypto");
7657
+ const versionId = randomUUID7();
7658
+ await tx.execute(
7659
+ `UPDATE "${resourceType}" SET "versionId" = ?, "lastUpdated" = ?, "deleted" = 1 WHERE "id" = ?`,
7660
+ [versionId, now, id]
7661
+ );
7662
+ await tx.execute(
7663
+ `INSERT INTO "${resourceType}_History" ("id", "versionId", "content", "lastUpdated", "deleted") VALUES (?, ?, ?, ?, 1)`,
7664
+ [id, versionId, existing.content, now]
7665
+ );
7666
+ await tx.execute(
7667
+ `DELETE FROM "${resourceType}_References" WHERE "resourceId" = ?`,
7668
+ [id]
7669
+ );
7670
+ return { response: { status: "204" } };
7671
+ }
7672
+ case "GET": {
7673
+ if (!id) {
7674
+ throw new Error("GET requires resource ID");
7675
+ }
7676
+ const row = await tx.queryOne(
7677
+ `SELECT "content", "deleted" FROM "${resourceType}" WHERE "id" = ?`,
7678
+ [id]
7679
+ );
7680
+ if (!row) {
7681
+ throw new ResourceNotFoundError(resourceType, id);
7682
+ }
7683
+ if (row.deleted === 1) {
7684
+ throw new ResourceGoneError(resourceType, id);
7685
+ }
7686
+ const resource = JSON.parse(row.content);
7687
+ return { resource, response: { status: "200" } };
7688
+ }
7689
+ default:
7690
+ throw new Error(`Unsupported method: ${method}`);
7691
+ }
7692
+ }
7693
+ async function checkIfNoneExistInTx(tx, resourceType, ifNoneExist) {
7694
+ const params = new URLSearchParams(ifNoneExist);
7695
+ const conditions = [];
7696
+ const values = [];
7697
+ for (const [key, value] of params.entries()) {
7698
+ if (key === "_id") {
7699
+ conditions.push('"id" = ?');
7700
+ values.push(value);
7701
+ } else if (key === "identifier") {
7702
+ conditions.push('"content" LIKE ?');
7703
+ values.push(`%${value}%`);
7704
+ } else {
7705
+ conditions.push(`"${key}" = ?`);
7706
+ values.push(value);
7707
+ }
7708
+ }
7709
+ if (conditions.length === 0) {
7710
+ return { status: "create" };
7711
+ }
7712
+ const whereClause = conditions.join(" AND ");
7713
+ const rows = await tx.query(
7714
+ `SELECT "id", "content", "versionId", "lastUpdated" FROM "${resourceType}" WHERE "deleted" = 0 AND ${whereClause}`,
7715
+ values
7716
+ );
7717
+ if (rows.length === 0) {
7718
+ return { status: "create" };
7719
+ }
7720
+ if (rows.length === 1) {
7721
+ const existing = JSON.parse(rows[0].content);
7722
+ return {
7723
+ status: "existing",
7724
+ response: {
7725
+ resource: existing,
7726
+ response: {
7727
+ status: "200",
7728
+ location: `${resourceType}/${rows[0].id}/_history/${rows[0].versionId}`,
7729
+ etag: `W/"${rows[0].versionId}"`,
7730
+ lastModified: rows[0].lastUpdated
7731
+ }
7732
+ }
7733
+ };
7734
+ }
7735
+ return {
7736
+ status: "error",
7737
+ response: {
7738
+ response: {
7739
+ status: "412",
7740
+ outcome: {
7741
+ issue: [{
7742
+ severity: "error",
7743
+ code: "duplicate",
7744
+ diagnostics: `If-None-Exist matched ${rows.length} resources for ${resourceType}`
7745
+ }]
7746
+ }
7747
+ }
7748
+ }
7749
+ };
7750
+ }
7751
+ async function processBatchEntry(store, entry) {
7752
+ if (!entry.request) {
7753
+ return errorResponse("400", "Missing request");
7754
+ }
7755
+ const { method, url } = entry.request;
7756
+ const { resourceType, id } = parseRequestUrl(url);
7757
+ switch (method) {
7758
+ case "POST": {
7759
+ if (!entry.resource) {
7760
+ return errorResponse("400", "Missing resource for POST");
7761
+ }
7762
+ const created = await store.createResource(resourceType, entry.resource);
7763
+ const versionId = created.meta?.versionId ?? "";
7764
+ const lastUpdated = created.meta?.lastUpdated ?? "";
7765
+ return {
7766
+ resource: created,
7767
+ response: {
7768
+ status: "201",
7769
+ location: `${resourceType}/${created.id}/_history/${versionId}`,
7770
+ etag: `W/"${versionId}"`,
7771
+ lastModified: lastUpdated
7772
+ }
7773
+ };
7774
+ }
7775
+ case "PUT": {
7776
+ if (!entry.resource || !id) {
7777
+ return errorResponse("400", "PUT requires resource and ID");
7778
+ }
7779
+ const toUpdate = { ...entry.resource, id };
7780
+ const updateResult = await store.updateResource(resourceType, toUpdate, { upsert: true });
7781
+ const updated = updateResult.resource;
7782
+ const versionId = updated.meta?.versionId ?? "";
7783
+ const lastUpdated = updated.meta?.lastUpdated ?? "";
7784
+ return {
7785
+ resource: updated,
7786
+ response: {
7787
+ status: updateResult.created ? "201" : "200",
7788
+ location: `${resourceType}/${id}/_history/${versionId}`,
7789
+ etag: `W/"${versionId}"`,
7790
+ lastModified: lastUpdated
7791
+ }
7792
+ };
7793
+ }
7794
+ case "DELETE": {
7795
+ if (!id) {
7796
+ return errorResponse("400", "DELETE requires resource ID");
7797
+ }
7798
+ await store.deleteResource(resourceType, id);
7799
+ return { response: { status: "204" } };
7800
+ }
7801
+ case "GET": {
7802
+ if (!id) {
7803
+ return errorResponse("400", "GET requires resource ID");
7804
+ }
7805
+ const resource = await store.readResource(resourceType, id);
7806
+ return { resource, response: { status: "200" } };
7807
+ }
7808
+ default:
7809
+ return errorResponse("400", `Unsupported method: ${method}`);
7810
+ }
7811
+ }
7812
+ function errorResponse(status, message) {
7813
+ return {
7814
+ response: {
7815
+ status,
7816
+ outcome: {
7817
+ issue: [{ severity: "error", code: "processing", diagnostics: message }]
7818
+ }
7819
+ }
7820
+ };
7821
+ }
7822
+ function errorToStatus(err) {
7823
+ if (err instanceof ResourceNotFoundError) return "404";
7824
+ if (err instanceof ResourceGoneError) return "410";
7825
+ return "500";
7826
+ }
7827
+
7048
7828
  // src/migration/schema-diff.ts
7049
7829
  function compareSchemas(oldSets, newSets) {
7050
7830
  const deltas = [];
@@ -9501,6 +10281,7 @@ var IGImportOrchestrator = class {
9501
10281
  export {
9502
10282
  BetterSqlite3Adapter,
9503
10283
  ConceptHierarchyRepo,
10284
+ ConditionalService,
9504
10285
  DEFAULT_SEARCH_COUNT,
9505
10286
  DELETED_SCHEMA_VERSION,
9506
10287
  ElementIndexRepo,
@@ -9567,10 +10348,12 @@ export {
9567
10348
  buildTwoPhaseSearchSQLv2,
9568
10349
  buildTypeHistorySQLv2,
9569
10350
  buildUpdateMainSQLv2,
10351
+ buildUrnMap,
9570
10352
  buildWhereClauseV2,
9571
10353
  buildWhereFragmentV2,
9572
10354
  compareSchemas,
9573
10355
  createFhirRuntimeProvider,
10356
+ deepResolveUrns,
9574
10357
  executeSearchV2 as executeSearch,
9575
10358
  extractPrefix,
9576
10359
  extractPropertyPath,
@@ -9593,6 +10376,8 @@ export {
9593
10376
  parseSortParam,
9594
10377
  planSearch,
9595
10378
  prefixToOperator,
10379
+ processBatchV2,
10380
+ processTransactionV2,
9596
10381
  reindexAllV2,
9597
10382
  reindexResourceTypeV2,
9598
10383
  splitSearchValues