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.
- package/CHANGELOG.md +50 -0
- package/README.md +7 -4
- package/dist/cjs/index.cjs +786 -3
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/cjs/index.d.ts +221 -12
- package/dist/esm/index.d.ts +221 -12
- package/dist/esm/index.mjs +788 -3
- package/dist/esm/index.mjs.map +4 -4
- package/dist/index.d.ts +221 -12
- package/dist/lib/index.d.ts +8 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/search/where-builder.d.ts.map +1 -1
- package/dist/lib/store/fhir-persistence.d.ts +20 -2
- package/dist/lib/store/fhir-persistence.d.ts.map +1 -1
- package/dist/lib/store/fhir-store.d.ts +10 -1
- package/dist/lib/store/fhir-store.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|