@openhi/constructs 0.0.26 → 0.0.27
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.
|
@@ -85,19 +85,31 @@ function openHiContextMiddleware(req, res, next) {
|
|
|
85
85
|
});
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
+
const invoke = (0, import_serverless_express.getCurrentInvoke)();
|
|
89
|
+
const event = invoke?.event ?? req.apiGateway?.event;
|
|
90
|
+
const requestId = typeof event?.requestContext?.requestId === "string" ? event.requestContext.requestId : void 0;
|
|
88
91
|
req.openhiContext = {
|
|
89
92
|
tenantId: claims.openhi_tenant_id,
|
|
90
93
|
workspaceId: claims.openhi_workspace_id,
|
|
91
94
|
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
actorId: claims.openhi_user_id,
|
|
96
|
+
actorName: claims.openhi_user_name,
|
|
97
|
+
actorType: "human",
|
|
98
|
+
roleId: typeof claims.openhi_role_id === "string" && claims.openhi_role_id !== "" ? claims.openhi_role_id : void 0,
|
|
99
|
+
requestId,
|
|
100
|
+
source: "rest",
|
|
101
|
+
clientId: typeof claims.openhi_client_id === "string" && claims.openhi_client_id !== "" ? claims.openhi_client_id : void 0
|
|
94
102
|
};
|
|
95
103
|
next();
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
// src/data/rest-api/
|
|
106
|
+
// src/data/rest-api/routes/configuration/configuration.ts
|
|
99
107
|
var import_express = __toESM(require("express"));
|
|
100
108
|
|
|
109
|
+
// src/data/rest-api/routes/configuration/configuration-common.ts
|
|
110
|
+
var BASE_PATH = "/Configuration";
|
|
111
|
+
var SK = "CURRENT";
|
|
112
|
+
|
|
101
113
|
// src/lib/compression.ts
|
|
102
114
|
var import_node_zlib = require("zlib");
|
|
103
115
|
var ENVELOPE_VERSION = 1;
|
|
@@ -154,16 +166,124 @@ function decompressResource(compressedOrRaw) {
|
|
|
154
166
|
return compressedOrRaw;
|
|
155
167
|
}
|
|
156
168
|
|
|
157
|
-
// src/data/dynamo/
|
|
169
|
+
// src/data/dynamo/dynamo-service.ts
|
|
158
170
|
var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
|
|
159
|
-
var
|
|
171
|
+
var import_electrodb3 = require("electrodb");
|
|
160
172
|
|
|
161
|
-
// src/data/dynamo/
|
|
173
|
+
// src/data/dynamo/entities/configuration.ts
|
|
162
174
|
var import_electrodb = require("electrodb");
|
|
163
|
-
var
|
|
175
|
+
var Configuration = new import_electrodb.Entity({
|
|
176
|
+
model: {
|
|
177
|
+
entity: "configuration",
|
|
178
|
+
service: "openhi",
|
|
179
|
+
version: "01"
|
|
180
|
+
},
|
|
181
|
+
attributes: {
|
|
182
|
+
/** Sort key. "CURRENT" for current version; version history in S3. */
|
|
183
|
+
sk: {
|
|
184
|
+
type: "string",
|
|
185
|
+
required: true,
|
|
186
|
+
default: "CURRENT"
|
|
187
|
+
},
|
|
188
|
+
/** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
|
|
189
|
+
tenantId: {
|
|
190
|
+
type: "string",
|
|
191
|
+
required: true,
|
|
192
|
+
default: "BASELINE"
|
|
193
|
+
},
|
|
194
|
+
/** Workspace scope. Use "-" when absent. */
|
|
195
|
+
workspaceId: {
|
|
196
|
+
type: "string",
|
|
197
|
+
required: true,
|
|
198
|
+
default: "-"
|
|
199
|
+
},
|
|
200
|
+
/** User scope. Use "-" when absent. */
|
|
201
|
+
userId: {
|
|
202
|
+
type: "string",
|
|
203
|
+
required: true,
|
|
204
|
+
default: "-"
|
|
205
|
+
},
|
|
206
|
+
/** Role scope. Use "-" when absent. */
|
|
207
|
+
roleId: {
|
|
208
|
+
type: "string",
|
|
209
|
+
required: true,
|
|
210
|
+
default: "-"
|
|
211
|
+
},
|
|
212
|
+
/** Config type (category), e.g. endpoints, branding, display. */
|
|
213
|
+
key: {
|
|
214
|
+
type: "string",
|
|
215
|
+
required: true
|
|
216
|
+
},
|
|
217
|
+
/** FHIR Resource.id; logical id in URL and for the Configuration resource. */
|
|
218
|
+
id: {
|
|
219
|
+
type: "string",
|
|
220
|
+
required: true
|
|
221
|
+
},
|
|
222
|
+
/** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
|
|
223
|
+
resource: {
|
|
224
|
+
type: "string",
|
|
225
|
+
required: true
|
|
226
|
+
},
|
|
227
|
+
/** Version id (e.g. ULID). Tracks current version; S3 history key. */
|
|
228
|
+
vid: {
|
|
229
|
+
type: "string",
|
|
230
|
+
required: true
|
|
231
|
+
},
|
|
232
|
+
lastUpdated: {
|
|
233
|
+
type: "string",
|
|
234
|
+
required: true
|
|
235
|
+
},
|
|
236
|
+
deleted: {
|
|
237
|
+
type: "boolean",
|
|
238
|
+
required: false
|
|
239
|
+
},
|
|
240
|
+
bundleId: {
|
|
241
|
+
type: "string",
|
|
242
|
+
required: false
|
|
243
|
+
},
|
|
244
|
+
msgId: {
|
|
245
|
+
type: "string",
|
|
246
|
+
required: false
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
indexes: {
|
|
250
|
+
/** Base table: PK, SK (data store key names). PK is built from tenantId, workspaceId, userId, roleId; SK is built from key and sk. Do not supply PK or SK from outside. */
|
|
251
|
+
record: {
|
|
252
|
+
pk: {
|
|
253
|
+
field: "PK",
|
|
254
|
+
composite: ["tenantId", "workspaceId", "userId", "roleId"],
|
|
255
|
+
template: "OHI#CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
|
|
256
|
+
},
|
|
257
|
+
sk: {
|
|
258
|
+
field: "SK",
|
|
259
|
+
composite: ["key", "sk"],
|
|
260
|
+
template: "KEY#${key}#SK#${sk}"
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
/** GSI4 — Resource Type Index: list all Configuration in a tenant or workspace (no scan). Use for "list configs scoped to this tenant" (workspaceId = "-") or "list configs scoped to this workspace". Does not support hierarchical resolution in one query; use base table GetItem in fallback order (user → workspace → tenant → baseline) for that. */
|
|
264
|
+
gsi4: {
|
|
265
|
+
index: "GSI4",
|
|
266
|
+
condition: () => true,
|
|
267
|
+
pk: {
|
|
268
|
+
field: "GSI4PK",
|
|
269
|
+
composite: ["tenantId", "workspaceId"],
|
|
270
|
+
template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration"
|
|
271
|
+
},
|
|
272
|
+
sk: {
|
|
273
|
+
field: "GSI4SK",
|
|
274
|
+
composite: ["key", "sk"],
|
|
275
|
+
template: "KEY#${key}#SK#${sk}"
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// src/data/dynamo/entities/patient.ts
|
|
282
|
+
var import_electrodb2 = require("electrodb");
|
|
283
|
+
var Patient = new import_electrodb2.Entity({
|
|
164
284
|
model: {
|
|
165
285
|
entity: "patient",
|
|
166
|
-
service: "
|
|
286
|
+
service: "openhi",
|
|
167
287
|
version: "01"
|
|
168
288
|
},
|
|
169
289
|
attributes: {
|
|
@@ -339,7 +459,7 @@ var Patient = new import_electrodb.Entity({
|
|
|
339
459
|
}
|
|
340
460
|
});
|
|
341
461
|
|
|
342
|
-
// src/data/dynamo/
|
|
462
|
+
// src/data/dynamo/dynamo-service.ts
|
|
343
463
|
var table = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
|
|
344
464
|
var client = new import_client_dynamodb.DynamoDBClient({
|
|
345
465
|
...process.env.MOCK_DYNAMODB_ENDPOINT && {
|
|
@@ -348,200 +468,108 @@ var client = new import_client_dynamodb.DynamoDBClient({
|
|
|
348
468
|
region: "local"
|
|
349
469
|
}
|
|
350
470
|
});
|
|
351
|
-
var entities = { patient: Patient };
|
|
352
|
-
var
|
|
353
|
-
function
|
|
354
|
-
|
|
471
|
+
var entities = { patient: Patient, configuration: Configuration };
|
|
472
|
+
var DynamoDataService = new import_electrodb3.Service(entities, { table, client });
|
|
473
|
+
function getDynamoDataService(tableName) {
|
|
474
|
+
const resolved = tableName ?? table;
|
|
475
|
+
return new import_electrodb3.Service(entities, { table: resolved, client });
|
|
355
476
|
}
|
|
356
477
|
|
|
357
|
-
// src/data/
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
478
|
+
// src/data/rest-api/routes/configuration/configuration-create.ts
|
|
479
|
+
async function createConfiguration(req, res) {
|
|
480
|
+
const ctx = req.openhiContext;
|
|
481
|
+
const {
|
|
482
|
+
tenantId: ctxTenantId,
|
|
483
|
+
workspaceId: ctxWorkspaceId,
|
|
484
|
+
actorId: ctxActorId,
|
|
485
|
+
date
|
|
486
|
+
} = ctx;
|
|
487
|
+
const body = req.body;
|
|
488
|
+
const key = body?.key;
|
|
489
|
+
if (!key || typeof key !== "string") {
|
|
490
|
+
return res.status(400).json({
|
|
491
|
+
resourceType: "OperationOutcome",
|
|
492
|
+
issue: [
|
|
493
|
+
{
|
|
494
|
+
severity: "error",
|
|
495
|
+
code: "required",
|
|
496
|
+
diagnostics: "Configuration key is required"
|
|
497
|
+
}
|
|
498
|
+
]
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const id = body?.id ?? `config-${key}-${Date.now()}`;
|
|
502
|
+
const resourcePayload = body?.resource;
|
|
503
|
+
const resourceStr = typeof resourcePayload === "string" ? resourcePayload : JSON.stringify(resourcePayload ?? {});
|
|
504
|
+
const tenantId = body?.tenantId ?? ctxTenantId;
|
|
505
|
+
const workspaceId = body?.workspaceId ?? ctxWorkspaceId;
|
|
506
|
+
const userId = body?.userId ?? ctxActorId ?? "-";
|
|
507
|
+
const roleId = body?.roleId ?? "-";
|
|
508
|
+
const vid = body?.vid ?? (date.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36));
|
|
509
|
+
const lastUpdated = body?.lastUpdated ?? date;
|
|
510
|
+
const service = getDynamoDataService();
|
|
511
|
+
try {
|
|
512
|
+
await service.entities.configuration.put({
|
|
513
|
+
tenantId,
|
|
514
|
+
workspaceId,
|
|
515
|
+
userId,
|
|
516
|
+
roleId,
|
|
517
|
+
key,
|
|
518
|
+
id,
|
|
519
|
+
resource: compressResource(resourceStr),
|
|
520
|
+
vid,
|
|
521
|
+
lastUpdated,
|
|
522
|
+
sk: SK
|
|
523
|
+
}).go();
|
|
524
|
+
const config = {
|
|
525
|
+
resourceType: "Configuration",
|
|
526
|
+
id,
|
|
527
|
+
key,
|
|
528
|
+
resource: typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr),
|
|
529
|
+
meta: { lastUpdated, versionId: vid }
|
|
530
|
+
};
|
|
531
|
+
return res.status(201).location(`${BASE_PATH}/${key}`).json(config);
|
|
532
|
+
} catch (err) {
|
|
533
|
+
console.error("POST Configuration error:", err);
|
|
534
|
+
return res.status(500).json({
|
|
535
|
+
resourceType: "OperationOutcome",
|
|
536
|
+
issue: [
|
|
537
|
+
{ severity: "error", code: "exception", diagnostics: String(err) }
|
|
538
|
+
]
|
|
539
|
+
});
|
|
370
540
|
}
|
|
371
|
-
set(`${OPENHI_EXT}/created-date`, audit.createdDate, "valueDateTime");
|
|
372
|
-
set(`${OPENHI_EXT}/created-by-id`, audit.createdById, "valueString");
|
|
373
|
-
set(`${OPENHI_EXT}/created-by-name`, audit.createdByName, "valueString");
|
|
374
|
-
set(`${OPENHI_EXT}/modified-date`, audit.modifiedDate, "valueDateTime");
|
|
375
|
-
set(`${OPENHI_EXT}/modified-by-id`, audit.modifiedById, "valueString");
|
|
376
|
-
set(`${OPENHI_EXT}/modified-by-name`, audit.modifiedByName, "valueString");
|
|
377
|
-
set(`${OPENHI_EXT}/deleted-date`, audit.deletedDate, "valueDateTime");
|
|
378
|
-
set(`${OPENHI_EXT}/deleted-by-id`, audit.deletedById, "valueString");
|
|
379
|
-
set(`${OPENHI_EXT}/deleted-by-name`, audit.deletedByName, "valueString");
|
|
380
|
-
return { ...existing, extension: Array.from(byUrl.values()) };
|
|
381
541
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
542
|
+
|
|
543
|
+
// src/data/rest-api/routes/configuration/configuration-delete.ts
|
|
544
|
+
async function deleteConfiguration(req, res) {
|
|
545
|
+
const key = String(req.params.key);
|
|
546
|
+
const ctx = req.openhiContext;
|
|
547
|
+
const { tenantId, workspaceId, actorId, roleId: ctxRoleId } = ctx;
|
|
548
|
+
const roleId = ctxRoleId ?? "-";
|
|
549
|
+
const service = getDynamoDataService();
|
|
550
|
+
try {
|
|
551
|
+
await service.entities.configuration.delete({ tenantId, workspaceId, userId: actorId, roleId, key, sk: SK }).go();
|
|
552
|
+
return res.status(204).send();
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.error("DELETE Configuration error:", err);
|
|
555
|
+
return res.status(500).json({
|
|
556
|
+
resourceType: "OperationOutcome",
|
|
557
|
+
issue: [
|
|
558
|
+
{ severity: "error", code: "exception", diagnostics: String(err) }
|
|
559
|
+
]
|
|
560
|
+
});
|
|
399
561
|
}
|
|
400
|
-
throw new Error(
|
|
401
|
-
"File must be a FHIR Patient resource or a Bundle containing at least one Patient entry"
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
var SK = "CURRENT";
|
|
405
|
-
var defaultAudit = {
|
|
406
|
-
createdDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
407
|
-
createdById: "import",
|
|
408
|
-
createdByName: "Bulk import",
|
|
409
|
-
modifiedDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
410
|
-
modifiedById: "import",
|
|
411
|
-
modifiedByName: "Bulk import"
|
|
412
|
-
};
|
|
413
|
-
function patientToPutAttrs(patient, options) {
|
|
414
|
-
const {
|
|
415
|
-
tenantId,
|
|
416
|
-
workspaceId,
|
|
417
|
-
createdDate,
|
|
418
|
-
createdById,
|
|
419
|
-
createdByName,
|
|
420
|
-
modifiedDate,
|
|
421
|
-
modifiedById,
|
|
422
|
-
modifiedByName
|
|
423
|
-
} = options;
|
|
424
|
-
const lastUpdated = patient.meta?.lastUpdated ?? modifiedDate ?? defaultAudit.modifiedDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
425
|
-
const auditMerged = {
|
|
426
|
-
...defaultAudit,
|
|
427
|
-
...createdDate != null && { createdDate },
|
|
428
|
-
...createdById != null && { createdById },
|
|
429
|
-
...createdByName != null && { createdByName },
|
|
430
|
-
...modifiedDate != null && { modifiedDate },
|
|
431
|
-
...modifiedById != null && { modifiedById },
|
|
432
|
-
...modifiedByName != null && { modifiedByName }
|
|
433
|
-
};
|
|
434
|
-
const patientWithMeta = {
|
|
435
|
-
...patient,
|
|
436
|
-
meta: mergeAuditIntoMeta(patient.meta, auditMerged)
|
|
437
|
-
};
|
|
438
|
-
if (lastUpdated && !patientWithMeta.meta.lastUpdated) {
|
|
439
|
-
patientWithMeta.meta.lastUpdated = lastUpdated;
|
|
440
|
-
}
|
|
441
|
-
return {
|
|
442
|
-
sk: SK,
|
|
443
|
-
tenantId,
|
|
444
|
-
workspaceId,
|
|
445
|
-
id: patient.id,
|
|
446
|
-
resource: compressResource(JSON.stringify(patientWithMeta)),
|
|
447
|
-
vid: lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36),
|
|
448
|
-
lastUpdated,
|
|
449
|
-
identifierSystem: "",
|
|
450
|
-
identifierValue: "",
|
|
451
|
-
facilityId: "",
|
|
452
|
-
normalizedName: ""
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
async function importPatientFromFile(filePath, options) {
|
|
456
|
-
const resolved = (0, import_node_path.resolve)(filePath);
|
|
457
|
-
const raw = (0, import_node_fs.readFileSync)(resolved, "utf-8");
|
|
458
|
-
const parsed = JSON.parse(raw);
|
|
459
|
-
const patient = extractPatient(parsed);
|
|
460
|
-
const tableName = options.tableName ?? process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
|
|
461
|
-
const service = getEhrR4DataService(tableName);
|
|
462
|
-
const attrs = patientToPutAttrs(patient, options);
|
|
463
|
-
const result = await service.entities.patient.put(attrs).go();
|
|
464
|
-
const data = result.data;
|
|
465
|
-
if (!data) {
|
|
466
|
-
throw new Error(`Put failed for Patient ${patient.id}`);
|
|
467
|
-
}
|
|
468
|
-
return {
|
|
469
|
-
id: data.id,
|
|
470
|
-
tenantId: data.tenantId,
|
|
471
|
-
workspaceId: data.workspaceId
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
async function main() {
|
|
475
|
-
const [, , fileArg, tenantId = "tenant-1", workspaceId = "ws-1"] = process.argv;
|
|
476
|
-
if (!fileArg) {
|
|
477
|
-
console.error(
|
|
478
|
-
"Usage: import-patient.ts <path-to-patient.json> [tenantId] [workspaceId]"
|
|
479
|
-
);
|
|
480
|
-
process.exit(1);
|
|
481
|
-
}
|
|
482
|
-
try {
|
|
483
|
-
const result = await importPatientFromFile(fileArg, {
|
|
484
|
-
tenantId,
|
|
485
|
-
workspaceId
|
|
486
|
-
});
|
|
487
|
-
console.log(
|
|
488
|
-
`Imported Patient ${result.id} (tenant=${result.tenantId}, workspace=${result.workspaceId})`
|
|
489
|
-
);
|
|
490
|
-
} catch (err) {
|
|
491
|
-
console.error(err);
|
|
492
|
-
process.exit(1);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
if (require.main === module) {
|
|
496
|
-
void main();
|
|
497
562
|
}
|
|
498
563
|
|
|
499
|
-
// src/data/rest-api/
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
const service = getEhrR4DataService(TABLE_NAME);
|
|
507
|
-
try {
|
|
508
|
-
const result = await service.entities.patient.query.gsi4({ tenantId, workspaceId }).go();
|
|
509
|
-
const entries = (result.data ?? []).map((item) => {
|
|
510
|
-
const resource = JSON.parse(decompressResource(item.resource));
|
|
511
|
-
return {
|
|
512
|
-
fullUrl: `${BASE_PATH}/${item.id}`,
|
|
513
|
-
resource: { ...resource, id: item.id }
|
|
514
|
-
};
|
|
515
|
-
});
|
|
516
|
-
const bundle = {
|
|
517
|
-
resourceType: "Bundle",
|
|
518
|
-
type: "searchset",
|
|
519
|
-
total: entries.length,
|
|
520
|
-
link: [{ relation: "self", url: BASE_PATH }],
|
|
521
|
-
entry: entries
|
|
522
|
-
};
|
|
523
|
-
return res.json(bundle);
|
|
524
|
-
} catch (err) {
|
|
525
|
-
console.error("GET /Patient list error:", err);
|
|
526
|
-
return res.status(500).json({
|
|
527
|
-
resourceType: "OperationOutcome",
|
|
528
|
-
issue: [
|
|
529
|
-
{
|
|
530
|
-
severity: "error",
|
|
531
|
-
code: "exception",
|
|
532
|
-
diagnostics: String(err)
|
|
533
|
-
}
|
|
534
|
-
]
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
router.get("/", listPatients);
|
|
539
|
-
async function getPatientById(req, res) {
|
|
540
|
-
const id = String(req.params.id);
|
|
541
|
-
const { tenantId, workspaceId } = req.openhiContext;
|
|
542
|
-
const service = getEhrR4DataService(TABLE_NAME);
|
|
564
|
+
// src/data/rest-api/routes/configuration/configuration-get-by-key.ts
|
|
565
|
+
async function getConfigurationByKey(req, res) {
|
|
566
|
+
const key = String(req.params.key);
|
|
567
|
+
const ctx = req.openhiContext;
|
|
568
|
+
const { tenantId, workspaceId, actorId, roleId: ctxRoleId } = ctx;
|
|
569
|
+
const roleId = ctxRoleId ?? "-";
|
|
570
|
+
const service = getDynamoDataService();
|
|
543
571
|
try {
|
|
544
|
-
const result = await service.entities.
|
|
572
|
+
const result = await service.entities.configuration.get({ tenantId, workspaceId, userId: actorId, roleId, key, sk: SK }).go();
|
|
545
573
|
if (!result.data) {
|
|
546
574
|
return res.status(404).json({
|
|
547
575
|
resourceType: "OperationOutcome",
|
|
@@ -549,7 +577,7 @@ async function getPatientById(req, res) {
|
|
|
549
577
|
{
|
|
550
578
|
severity: "error",
|
|
551
579
|
code: "not-found",
|
|
552
|
-
diagnostics: `
|
|
580
|
+
diagnostics: `Configuration ${key} not found`
|
|
553
581
|
}
|
|
554
582
|
]
|
|
555
583
|
});
|
|
@@ -557,9 +585,14 @@ async function getPatientById(req, res) {
|
|
|
557
585
|
const resource = JSON.parse(
|
|
558
586
|
decompressResource(result.data.resource)
|
|
559
587
|
);
|
|
560
|
-
return res.json({
|
|
588
|
+
return res.json({
|
|
589
|
+
...resource,
|
|
590
|
+
resourceType: "Configuration",
|
|
591
|
+
id: result.data.id,
|
|
592
|
+
key: result.data.key
|
|
593
|
+
});
|
|
561
594
|
} catch (err) {
|
|
562
|
-
console.error("GET
|
|
595
|
+
console.error("GET Configuration error:", err);
|
|
563
596
|
return res.status(500).json({
|
|
564
597
|
resourceType: "OperationOutcome",
|
|
565
598
|
issue: [
|
|
@@ -572,155 +605,10 @@ async function getPatientById(req, res) {
|
|
|
572
605
|
});
|
|
573
606
|
}
|
|
574
607
|
}
|
|
575
|
-
router.get("/:id", getPatientById);
|
|
576
|
-
function requireJsonBody(req, res) {
|
|
577
|
-
const raw = req.body;
|
|
578
|
-
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
579
|
-
return {
|
|
580
|
-
errorResponse: res.status(400).json({
|
|
581
|
-
resourceType: "OperationOutcome",
|
|
582
|
-
issue: [
|
|
583
|
-
{
|
|
584
|
-
severity: "error",
|
|
585
|
-
code: "invalid",
|
|
586
|
-
diagnostics: "Request body must be a JSON object."
|
|
587
|
-
}
|
|
588
|
-
]
|
|
589
|
-
})
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
return { body: raw };
|
|
593
|
-
}
|
|
594
|
-
async function createPatient(req, res) {
|
|
595
|
-
const bodyResult = requireJsonBody(req, res);
|
|
596
|
-
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
597
|
-
const ctx = req.openhiContext;
|
|
598
|
-
const { tenantId, workspaceId, date, userId, username } = ctx;
|
|
599
|
-
const body = bodyResult.body;
|
|
600
|
-
const id = body?.id ?? `patient-${Date.now()}`;
|
|
601
|
-
const patient = {
|
|
602
|
-
...body,
|
|
603
|
-
resourceType: "Patient",
|
|
604
|
-
id,
|
|
605
|
-
meta: {
|
|
606
|
-
...body?.meta ?? {},
|
|
607
|
-
lastUpdated: date,
|
|
608
|
-
versionId: "1"
|
|
609
|
-
}
|
|
610
|
-
};
|
|
611
|
-
const options = {
|
|
612
|
-
tenantId,
|
|
613
|
-
workspaceId,
|
|
614
|
-
createdDate: date,
|
|
615
|
-
createdById: userId,
|
|
616
|
-
createdByName: username,
|
|
617
|
-
modifiedDate: date,
|
|
618
|
-
modifiedById: userId,
|
|
619
|
-
modifiedByName: username
|
|
620
|
-
};
|
|
621
|
-
const service = getEhrR4DataService(TABLE_NAME);
|
|
622
|
-
try {
|
|
623
|
-
const attrs = patientToPutAttrs(patient, options);
|
|
624
|
-
await service.entities.patient.put(
|
|
625
|
-
attrs
|
|
626
|
-
).go();
|
|
627
|
-
return res.status(201).location(`${BASE_PATH}/${id}`).json(patient);
|
|
628
|
-
} catch (err) {
|
|
629
|
-
console.error("POST Patient error:", err);
|
|
630
|
-
return res.status(500).json({
|
|
631
|
-
resourceType: "OperationOutcome",
|
|
632
|
-
issue: [
|
|
633
|
-
{ severity: "error", code: "exception", diagnostics: String(err) }
|
|
634
|
-
]
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
router.post("/", createPatient);
|
|
639
|
-
async function updatePatient(req, res) {
|
|
640
|
-
const bodyResult = requireJsonBody(req, res);
|
|
641
|
-
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
642
|
-
const id = String(req.params.id);
|
|
643
|
-
const ctx = req.openhiContext;
|
|
644
|
-
const { tenantId, workspaceId, date, userId, username } = ctx;
|
|
645
|
-
const body = bodyResult.body;
|
|
646
|
-
const patient = {
|
|
647
|
-
...body,
|
|
648
|
-
resourceType: "Patient",
|
|
649
|
-
id,
|
|
650
|
-
meta: {
|
|
651
|
-
...body?.meta ?? {},
|
|
652
|
-
lastUpdated: date,
|
|
653
|
-
versionId: "2"
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
const service = getEhrR4DataService(TABLE_NAME);
|
|
657
|
-
try {
|
|
658
|
-
const existing = await service.entities.patient.get({ tenantId, workspaceId, id, sk: SK2 }).go();
|
|
659
|
-
if (!existing.data) {
|
|
660
|
-
return res.status(404).json({
|
|
661
|
-
resourceType: "OperationOutcome",
|
|
662
|
-
issue: [
|
|
663
|
-
{
|
|
664
|
-
severity: "error",
|
|
665
|
-
code: "not-found",
|
|
666
|
-
diagnostics: `Patient ${id} not found`
|
|
667
|
-
}
|
|
668
|
-
]
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
const existingMeta = existing.data.resource != null ? JSON.parse(decompressResource(existing.data.resource)).meta : void 0;
|
|
672
|
-
const patientWithMeta = {
|
|
673
|
-
...patient,
|
|
674
|
-
meta: mergeAuditIntoMeta(
|
|
675
|
-
patient.meta ?? existingMeta,
|
|
676
|
-
{
|
|
677
|
-
modifiedDate: date,
|
|
678
|
-
modifiedById: userId,
|
|
679
|
-
modifiedByName: username
|
|
680
|
-
}
|
|
681
|
-
)
|
|
682
|
-
};
|
|
683
|
-
await service.entities.patient.patch({ tenantId, workspaceId, id, sk: SK2 }).set({
|
|
684
|
-
resource: compressResource(JSON.stringify(patientWithMeta)),
|
|
685
|
-
lastUpdated: date
|
|
686
|
-
}).go();
|
|
687
|
-
return res.json(patientWithMeta);
|
|
688
|
-
} catch (err) {
|
|
689
|
-
console.error("PUT Patient error:", err);
|
|
690
|
-
return res.status(500).json({
|
|
691
|
-
resourceType: "OperationOutcome",
|
|
692
|
-
issue: [
|
|
693
|
-
{ severity: "error", code: "exception", diagnostics: String(err) }
|
|
694
|
-
]
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
router.put("/:id", updatePatient);
|
|
699
|
-
async function deletePatient(req, res) {
|
|
700
|
-
const id = String(req.params.id);
|
|
701
|
-
const { tenantId, workspaceId } = req.openhiContext;
|
|
702
|
-
const service = getEhrR4DataService(TABLE_NAME);
|
|
703
|
-
try {
|
|
704
|
-
await service.entities.patient.delete({ tenantId, workspaceId, id, sk: SK2 }).go();
|
|
705
|
-
return res.status(204).send();
|
|
706
|
-
} catch (err) {
|
|
707
|
-
console.error("DELETE Patient error:", err);
|
|
708
|
-
return res.status(500).json({
|
|
709
|
-
resourceType: "OperationOutcome",
|
|
710
|
-
issue: [
|
|
711
|
-
{ severity: "error", code: "exception", diagnostics: String(err) }
|
|
712
|
-
]
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
router.delete("/:id", deletePatient);
|
|
717
608
|
|
|
718
|
-
// src/data/rest-api/
|
|
719
|
-
var import_express2 = __toESM(require("express"));
|
|
720
|
-
|
|
721
|
-
// src/data/rest-api/ohi/dynamic-configuration.ts
|
|
609
|
+
// src/data/rest-api/dynamic-configuration.ts
|
|
722
610
|
var import_client_ssm = require("@aws-sdk/client-ssm");
|
|
723
|
-
var BASE_PATH2 = "/
|
|
611
|
+
var BASE_PATH2 = "/Configuration";
|
|
724
612
|
var TAG_KEY_BRANCH = "openhi:branch-name";
|
|
725
613
|
var TAG_KEY_HTTP_API_PARAM = "openhi:param-name";
|
|
726
614
|
function getSsmDynamicConfigEnvFilter() {
|
|
@@ -737,9 +625,9 @@ async function getDynamicConfigurationEntries(context) {
|
|
|
737
625
|
return getStaticDummyEntry(context);
|
|
738
626
|
}
|
|
739
627
|
const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
|
|
740
|
-
const
|
|
628
|
+
const client2 = new import_client_ssm.SSMClient({ region });
|
|
741
629
|
try {
|
|
742
|
-
const describeResult = await
|
|
630
|
+
const describeResult = await client2.send(
|
|
743
631
|
new import_client_ssm.DescribeParametersCommand({
|
|
744
632
|
ParameterFilters: [
|
|
745
633
|
{
|
|
@@ -763,7 +651,7 @@ async function getDynamicConfigurationEntries(context) {
|
|
|
763
651
|
const parameters = [];
|
|
764
652
|
for (let i = 0; i < names.length; i += 10) {
|
|
765
653
|
const batch = names.slice(i, i + 10);
|
|
766
|
-
const getResult = await
|
|
654
|
+
const getResult = await client2.send(
|
|
767
655
|
new import_client_ssm.GetParametersCommand({
|
|
768
656
|
Names: batch,
|
|
769
657
|
WithDecryption: true
|
|
@@ -827,147 +715,16 @@ function getStaticDummyEntry(_context) {
|
|
|
827
715
|
return [dummy];
|
|
828
716
|
}
|
|
829
717
|
|
|
830
|
-
// src/data/
|
|
831
|
-
var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
|
|
832
|
-
var import_electrodb4 = require("electrodb");
|
|
833
|
-
|
|
834
|
-
// src/data/dynamo/ohi/Configuration.ts
|
|
835
|
-
var import_electrodb3 = require("electrodb");
|
|
836
|
-
var Configuration = new import_electrodb3.Entity({
|
|
837
|
-
model: {
|
|
838
|
-
entity: "configuration",
|
|
839
|
-
service: "ohi",
|
|
840
|
-
version: "01"
|
|
841
|
-
},
|
|
842
|
-
attributes: {
|
|
843
|
-
/** Sort key. "CURRENT" for current version; version history in S3. */
|
|
844
|
-
sk: {
|
|
845
|
-
type: "string",
|
|
846
|
-
required: true,
|
|
847
|
-
default: "CURRENT"
|
|
848
|
-
},
|
|
849
|
-
/** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
|
|
850
|
-
tenantId: {
|
|
851
|
-
type: "string",
|
|
852
|
-
required: true,
|
|
853
|
-
default: "BASELINE"
|
|
854
|
-
},
|
|
855
|
-
/** Workspace scope. Use "-" when absent. */
|
|
856
|
-
workspaceId: {
|
|
857
|
-
type: "string",
|
|
858
|
-
required: true,
|
|
859
|
-
default: "-"
|
|
860
|
-
},
|
|
861
|
-
/** User scope. Use "-" when absent. */
|
|
862
|
-
userId: {
|
|
863
|
-
type: "string",
|
|
864
|
-
required: true,
|
|
865
|
-
default: "-"
|
|
866
|
-
},
|
|
867
|
-
/** Role scope. Use "-" when absent. */
|
|
868
|
-
roleId: {
|
|
869
|
-
type: "string",
|
|
870
|
-
required: true,
|
|
871
|
-
default: "-"
|
|
872
|
-
},
|
|
873
|
-
/** Config type (category), e.g. endpoints, branding, display. */
|
|
874
|
-
key: {
|
|
875
|
-
type: "string",
|
|
876
|
-
required: true
|
|
877
|
-
},
|
|
878
|
-
/** FHIR Resource.id; logical id in URL and for the Configuration resource. */
|
|
879
|
-
id: {
|
|
880
|
-
type: "string",
|
|
881
|
-
required: true
|
|
882
|
-
},
|
|
883
|
-
/** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
|
|
884
|
-
resource: {
|
|
885
|
-
type: "string",
|
|
886
|
-
required: true
|
|
887
|
-
},
|
|
888
|
-
/** Version id (e.g. ULID). Tracks current version; S3 history key. */
|
|
889
|
-
vid: {
|
|
890
|
-
type: "string",
|
|
891
|
-
required: true
|
|
892
|
-
},
|
|
893
|
-
lastUpdated: {
|
|
894
|
-
type: "string",
|
|
895
|
-
required: true
|
|
896
|
-
},
|
|
897
|
-
deleted: {
|
|
898
|
-
type: "boolean",
|
|
899
|
-
required: false
|
|
900
|
-
},
|
|
901
|
-
bundleId: {
|
|
902
|
-
type: "string",
|
|
903
|
-
required: false
|
|
904
|
-
},
|
|
905
|
-
msgId: {
|
|
906
|
-
type: "string",
|
|
907
|
-
required: false
|
|
908
|
-
}
|
|
909
|
-
},
|
|
910
|
-
indexes: {
|
|
911
|
-
/** Base table: PK, SK (data store key names). PK is built from tenantId, workspaceId, userId, roleId; SK is built from key and sk. Do not supply PK or SK from outside. */
|
|
912
|
-
record: {
|
|
913
|
-
pk: {
|
|
914
|
-
field: "PK",
|
|
915
|
-
composite: ["tenantId", "workspaceId", "userId", "roleId"],
|
|
916
|
-
template: "OHI#CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
|
|
917
|
-
},
|
|
918
|
-
sk: {
|
|
919
|
-
field: "SK",
|
|
920
|
-
composite: ["key", "sk"],
|
|
921
|
-
template: "KEY#${key}#SK#${sk}"
|
|
922
|
-
}
|
|
923
|
-
},
|
|
924
|
-
/** GSI4 — Resource Type Index: list all Configuration in a tenant or workspace (no scan). Use for "list configs scoped to this tenant" (workspaceId = "-") or "list configs scoped to this workspace". Does not support hierarchical resolution in one query; use base table GetItem in fallback order (user → workspace → tenant → baseline) for that. */
|
|
925
|
-
gsi4: {
|
|
926
|
-
index: "GSI4",
|
|
927
|
-
condition: () => true,
|
|
928
|
-
pk: {
|
|
929
|
-
field: "GSI4PK",
|
|
930
|
-
composite: ["tenantId", "workspaceId"],
|
|
931
|
-
template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration"
|
|
932
|
-
},
|
|
933
|
-
sk: {
|
|
934
|
-
field: "GSI4SK",
|
|
935
|
-
composite: ["key", "sk"],
|
|
936
|
-
template: "KEY#${key}#SK#${sk}"
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
// src/data/dynamo/ohi/ohi-data-service.ts
|
|
943
|
-
var table2 = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
|
|
944
|
-
var client2 = new import_client_dynamodb2.DynamoDBClient({
|
|
945
|
-
...process.env.MOCK_DYNAMODB_ENDPOINT && {
|
|
946
|
-
endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
|
|
947
|
-
sslEnabled: false,
|
|
948
|
-
region: "local"
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
|
-
var entities2 = { configuration: Configuration };
|
|
952
|
-
var OhiDataService = new import_electrodb4.Service(entities2, { table: table2, client: client2 });
|
|
953
|
-
function getOhiDataService(tableName) {
|
|
954
|
-
return new import_electrodb4.Service(entities2, { table: tableName, client: client2 });
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// src/data/rest-api/ohi/Configuration.ts
|
|
958
|
-
var BASE_PATH3 = "/ohi/Configuration";
|
|
959
|
-
var router2 = import_express2.default.Router();
|
|
960
|
-
var SK3 = "CURRENT";
|
|
961
|
-
var TABLE_NAME2 = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
|
|
718
|
+
// src/data/rest-api/routes/configuration/configuration-list.ts
|
|
962
719
|
async function listConfigurations(req, res) {
|
|
963
720
|
const { tenantId, workspaceId } = req.openhiContext;
|
|
964
|
-
const service =
|
|
721
|
+
const service = getDynamoDataService();
|
|
965
722
|
try {
|
|
966
723
|
const result = await service.entities.configuration.query.gsi4({ tenantId, workspaceId }).go();
|
|
967
724
|
const dynamoEntries = (result.data ?? []).map((item) => {
|
|
968
725
|
const resource = JSON.parse(decompressResource(item.resource));
|
|
969
726
|
return {
|
|
970
|
-
fullUrl: `${
|
|
727
|
+
fullUrl: `${BASE_PATH}/${item.key}`,
|
|
971
728
|
resource: { ...resource, id: item.id, key: item.key }
|
|
972
729
|
};
|
|
973
730
|
});
|
|
@@ -980,7 +737,7 @@ async function listConfigurations(req, res) {
|
|
|
980
737
|
resourceType: "Bundle",
|
|
981
738
|
type: "searchset",
|
|
982
739
|
total: entries.length,
|
|
983
|
-
link: [{ relation: "self", url:
|
|
740
|
+
link: [{ relation: "self", url: BASE_PATH }],
|
|
984
741
|
entry: entries
|
|
985
742
|
};
|
|
986
743
|
return res.json(bundle);
|
|
@@ -998,16 +755,21 @@ async function listConfigurations(req, res) {
|
|
|
998
755
|
});
|
|
999
756
|
}
|
|
1000
757
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
758
|
+
|
|
759
|
+
// src/data/rest-api/routes/configuration/configuration-update.ts
|
|
760
|
+
async function updateConfiguration(req, res) {
|
|
1003
761
|
const key = String(req.params.key);
|
|
1004
762
|
const ctx = req.openhiContext;
|
|
1005
|
-
const { tenantId, workspaceId,
|
|
1006
|
-
const roleId =
|
|
1007
|
-
const
|
|
763
|
+
const { tenantId, workspaceId, actorId, date, roleId: ctxRoleId } = ctx;
|
|
764
|
+
const roleId = ctxRoleId ?? "-";
|
|
765
|
+
const body = req.body;
|
|
766
|
+
const resourcePayload = body?.resource;
|
|
767
|
+
const resourceStr = typeof resourcePayload === "string" ? resourcePayload : JSON.stringify(resourcePayload ?? {});
|
|
768
|
+
const lastUpdated = body?.lastUpdated ?? date;
|
|
769
|
+
const service = getDynamoDataService();
|
|
1008
770
|
try {
|
|
1009
|
-
const
|
|
1010
|
-
if (!
|
|
771
|
+
const existing = await service.entities.configuration.get({ tenantId, workspaceId, userId: actorId, roleId, key, sk: SK }).go();
|
|
772
|
+
if (!existing.data) {
|
|
1011
773
|
return res.status(404).json({
|
|
1012
774
|
resourceType: "OperationOutcome",
|
|
1013
775
|
issue: [
|
|
@@ -1019,85 +781,261 @@ async function getConfigurationByKey(req, res) {
|
|
|
1019
781
|
]
|
|
1020
782
|
});
|
|
1021
783
|
}
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
784
|
+
const nextVid = existing.data.vid != null ? String(Number(existing.data.vid) + 1) : date.replace(/[-:T.Z]/g, "").slice(0, 12) || "2";
|
|
785
|
+
await service.entities.configuration.patch({ tenantId, workspaceId, userId: actorId, roleId, key, sk: SK }).set({
|
|
786
|
+
resource: compressResource(resourceStr),
|
|
787
|
+
lastUpdated,
|
|
788
|
+
vid: nextVid
|
|
789
|
+
}).go();
|
|
790
|
+
const config = {
|
|
1027
791
|
resourceType: "Configuration",
|
|
1028
|
-
id:
|
|
1029
|
-
key:
|
|
1030
|
-
|
|
792
|
+
id: existing.data.id,
|
|
793
|
+
key: existing.data.key,
|
|
794
|
+
resource: typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr),
|
|
795
|
+
meta: { lastUpdated, versionId: nextVid }
|
|
796
|
+
};
|
|
797
|
+
return res.json(config);
|
|
1031
798
|
} catch (err) {
|
|
1032
|
-
console.error("
|
|
799
|
+
console.error("PUT Configuration error:", err);
|
|
1033
800
|
return res.status(500).json({
|
|
1034
801
|
resourceType: "OperationOutcome",
|
|
1035
802
|
issue: [
|
|
1036
|
-
{
|
|
1037
|
-
severity: "error",
|
|
1038
|
-
code: "exception",
|
|
1039
|
-
diagnostics: String(err)
|
|
1040
|
-
}
|
|
803
|
+
{ severity: "error", code: "exception", diagnostics: String(err) }
|
|
1041
804
|
]
|
|
1042
805
|
});
|
|
1043
806
|
}
|
|
1044
807
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
808
|
+
|
|
809
|
+
// src/data/rest-api/routes/configuration/configuration.ts
|
|
810
|
+
var router = import_express.default.Router();
|
|
811
|
+
router.get("/", listConfigurations);
|
|
812
|
+
router.get("/:key", getConfigurationByKey);
|
|
813
|
+
router.post("/", createConfiguration);
|
|
814
|
+
router.put("/:key", updateConfiguration);
|
|
815
|
+
router.delete("/:key", deleteConfiguration);
|
|
816
|
+
|
|
817
|
+
// src/data/rest-api/routes/patient/patient.ts
|
|
818
|
+
var import_express2 = __toESM(require("express"));
|
|
819
|
+
|
|
820
|
+
// src/data/rest-api/routes/patient/patient-common.ts
|
|
821
|
+
var BASE_PATH3 = "/Patient";
|
|
822
|
+
var SK2 = "CURRENT";
|
|
823
|
+
function requireJsonBody(req, res) {
|
|
824
|
+
const raw = req.body;
|
|
825
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
826
|
+
return {
|
|
827
|
+
errorResponse: res.status(400).json({
|
|
828
|
+
resourceType: "OperationOutcome",
|
|
829
|
+
issue: [
|
|
830
|
+
{
|
|
831
|
+
severity: "error",
|
|
832
|
+
code: "invalid",
|
|
833
|
+
diagnostics: "Request body must be a JSON object."
|
|
834
|
+
}
|
|
835
|
+
]
|
|
836
|
+
})
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
return { body: raw };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/data/import-patient.ts
|
|
843
|
+
var import_node_fs = require("fs");
|
|
844
|
+
var import_node_path = require("path");
|
|
845
|
+
var OPENHI_EXT = "http://openhi.org/fhir/StructureDefinition";
|
|
846
|
+
function mergeAuditIntoMeta(meta, audit) {
|
|
847
|
+
const existing = meta ?? {};
|
|
848
|
+
const ext = [
|
|
849
|
+
...Array.isArray(existing.extension) ? existing.extension : []
|
|
850
|
+
];
|
|
851
|
+
const byUrl = new Map(ext.map((e) => [e.url, e]));
|
|
852
|
+
function set(url, value, type) {
|
|
853
|
+
if (value == null) return;
|
|
854
|
+
byUrl.set(url, { url, [type]: value });
|
|
855
|
+
}
|
|
856
|
+
set(`${OPENHI_EXT}/created-date`, audit.createdDate, "valueDateTime");
|
|
857
|
+
set(`${OPENHI_EXT}/created-by-id`, audit.createdById, "valueString");
|
|
858
|
+
set(`${OPENHI_EXT}/created-by-name`, audit.createdByName, "valueString");
|
|
859
|
+
set(`${OPENHI_EXT}/modified-date`, audit.modifiedDate, "valueDateTime");
|
|
860
|
+
set(`${OPENHI_EXT}/modified-by-id`, audit.modifiedById, "valueString");
|
|
861
|
+
set(`${OPENHI_EXT}/modified-by-name`, audit.modifiedByName, "valueString");
|
|
862
|
+
set(`${OPENHI_EXT}/deleted-date`, audit.deletedDate, "valueDateTime");
|
|
863
|
+
set(`${OPENHI_EXT}/deleted-by-id`, audit.deletedById, "valueString");
|
|
864
|
+
set(`${OPENHI_EXT}/deleted-by-name`, audit.deletedByName, "valueString");
|
|
865
|
+
return { ...existing, extension: Array.from(byUrl.values()) };
|
|
866
|
+
}
|
|
867
|
+
function extractPatient(parsed) {
|
|
868
|
+
if (parsed && typeof parsed === "object" && "resourceType" in parsed) {
|
|
869
|
+
const root = parsed;
|
|
870
|
+
if (root.resourceType === "Patient" && root.id) {
|
|
871
|
+
return root;
|
|
872
|
+
}
|
|
873
|
+
if (root.resourceType === "Bundle" && "entry" in parsed) {
|
|
874
|
+
const entries = parsed.entry;
|
|
875
|
+
if (Array.isArray(entries)) {
|
|
876
|
+
const patientEntry = entries.find(
|
|
877
|
+
(e) => e?.resource?.resourceType === "Patient" && e.resource.id
|
|
878
|
+
);
|
|
879
|
+
if (patientEntry?.resource) {
|
|
880
|
+
return patientEntry.resource;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
throw new Error(
|
|
886
|
+
"File must be a FHIR Patient resource or a Bundle containing at least one Patient entry"
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
var SK3 = "CURRENT";
|
|
890
|
+
var defaultAudit = {
|
|
891
|
+
createdDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
892
|
+
createdById: "import",
|
|
893
|
+
createdByName: "Bulk import",
|
|
894
|
+
modifiedDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
895
|
+
modifiedById: "import",
|
|
896
|
+
modifiedByName: "Bulk import"
|
|
897
|
+
};
|
|
898
|
+
function patientToPutAttrs(patient, options) {
|
|
1048
899
|
const {
|
|
1049
|
-
tenantId
|
|
1050
|
-
workspaceId
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
900
|
+
tenantId,
|
|
901
|
+
workspaceId,
|
|
902
|
+
createdDate,
|
|
903
|
+
createdById,
|
|
904
|
+
createdByName,
|
|
905
|
+
modifiedDate,
|
|
906
|
+
modifiedById,
|
|
907
|
+
modifiedByName
|
|
908
|
+
} = options;
|
|
909
|
+
const lastUpdated = patient.meta?.lastUpdated ?? modifiedDate ?? defaultAudit.modifiedDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
910
|
+
const auditMerged = {
|
|
911
|
+
...defaultAudit,
|
|
912
|
+
...createdDate != null && { createdDate },
|
|
913
|
+
...createdById != null && { createdById },
|
|
914
|
+
...createdByName != null && { createdByName },
|
|
915
|
+
...modifiedDate != null && { modifiedDate },
|
|
916
|
+
...modifiedById != null && { modifiedById },
|
|
917
|
+
...modifiedByName != null && { modifiedByName }
|
|
918
|
+
};
|
|
919
|
+
const patientWithMeta = {
|
|
920
|
+
...patient,
|
|
921
|
+
meta: mergeAuditIntoMeta(patient.meta, auditMerged)
|
|
922
|
+
};
|
|
923
|
+
if (lastUpdated && !patientWithMeta.meta.lastUpdated) {
|
|
924
|
+
patientWithMeta.meta.lastUpdated = lastUpdated;
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
sk: SK3,
|
|
928
|
+
tenantId,
|
|
929
|
+
workspaceId,
|
|
930
|
+
id: patient.id,
|
|
931
|
+
resource: compressResource(JSON.stringify(patientWithMeta)),
|
|
932
|
+
vid: lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36),
|
|
933
|
+
lastUpdated,
|
|
934
|
+
identifierSystem: "",
|
|
935
|
+
identifierValue: "",
|
|
936
|
+
facilityId: "",
|
|
937
|
+
normalizedName: ""
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
async function importPatientFromFile(filePath, options) {
|
|
941
|
+
const resolved = (0, import_node_path.resolve)(filePath);
|
|
942
|
+
const raw = (0, import_node_fs.readFileSync)(resolved, "utf-8");
|
|
943
|
+
const parsed = JSON.parse(raw);
|
|
944
|
+
const patient = extractPatient(parsed);
|
|
945
|
+
const service = getDynamoDataService(options.tableName);
|
|
946
|
+
const attrs = patientToPutAttrs(patient, options);
|
|
947
|
+
const result = await service.entities.patient.put(attrs).go();
|
|
948
|
+
const data = result.data;
|
|
949
|
+
if (!data) {
|
|
950
|
+
throw new Error(`Put failed for Patient ${patient.id}`);
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
id: data.id,
|
|
954
|
+
tenantId: data.tenantId,
|
|
955
|
+
workspaceId: data.workspaceId
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
async function main() {
|
|
959
|
+
const [, , fileArg, tenantId = "tenant-1", workspaceId = "ws-1"] = process.argv;
|
|
960
|
+
if (!fileArg) {
|
|
961
|
+
console.error(
|
|
962
|
+
"Usage: import-patient.ts <path-to-patient.json> [tenantId] [workspaceId]"
|
|
963
|
+
);
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
const result = await importPatientFromFile(fileArg, {
|
|
968
|
+
tenantId,
|
|
969
|
+
workspaceId
|
|
970
|
+
});
|
|
971
|
+
console.log(
|
|
972
|
+
`Imported Patient ${result.id} (tenant=${result.tenantId}, workspace=${result.workspaceId})`
|
|
973
|
+
);
|
|
974
|
+
} catch (err) {
|
|
975
|
+
console.error(err);
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (require.main === module) {
|
|
980
|
+
void main();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/data/rest-api/routes/patient/patient-create.ts
|
|
984
|
+
async function createPatient(req, res) {
|
|
985
|
+
const bodyResult = requireJsonBody(req, res);
|
|
986
|
+
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
987
|
+
const ctx = req.openhiContext;
|
|
988
|
+
const { tenantId, workspaceId, date, actorId, actorName } = ctx;
|
|
989
|
+
const body = bodyResult.body;
|
|
990
|
+
const id = body?.id ?? `patient-${Date.now()}`;
|
|
991
|
+
const patient = {
|
|
992
|
+
...body,
|
|
993
|
+
resourceType: "Patient",
|
|
994
|
+
id,
|
|
995
|
+
meta: {
|
|
996
|
+
...body?.meta ?? {},
|
|
997
|
+
lastUpdated: date,
|
|
998
|
+
versionId: "1"
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
const options = {
|
|
1002
|
+
tenantId,
|
|
1003
|
+
workspaceId,
|
|
1004
|
+
createdDate: date,
|
|
1005
|
+
createdById: actorId,
|
|
1006
|
+
createdByName: actorName,
|
|
1007
|
+
modifiedDate: date,
|
|
1008
|
+
modifiedById: actorId,
|
|
1009
|
+
modifiedByName: actorName
|
|
1010
|
+
};
|
|
1011
|
+
const service = getDynamoDataService();
|
|
1012
|
+
try {
|
|
1013
|
+
const attrs = patientToPutAttrs(patient, options);
|
|
1014
|
+
await service.entities.patient.put(
|
|
1015
|
+
attrs
|
|
1016
|
+
).go();
|
|
1017
|
+
return res.status(201).location(`${BASE_PATH3}/${id}`).json(patient);
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
console.error("POST Patient error:", err);
|
|
1020
|
+
return res.status(500).json({
|
|
1058
1021
|
resourceType: "OperationOutcome",
|
|
1059
1022
|
issue: [
|
|
1060
|
-
{
|
|
1061
|
-
severity: "error",
|
|
1062
|
-
code: "required",
|
|
1063
|
-
diagnostics: "Configuration key is required"
|
|
1064
|
-
}
|
|
1023
|
+
{ severity: "error", code: "exception", diagnostics: String(err) }
|
|
1065
1024
|
]
|
|
1066
1025
|
});
|
|
1067
1026
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
const
|
|
1073
|
-
const
|
|
1074
|
-
const
|
|
1075
|
-
const vid = body?.vid ?? (date.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36));
|
|
1076
|
-
const lastUpdated = body?.lastUpdated ?? date;
|
|
1077
|
-
const service = getOhiDataService(TABLE_NAME2);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/data/rest-api/routes/patient/patient-delete.ts
|
|
1030
|
+
async function deletePatient(req, res) {
|
|
1031
|
+
const id = String(req.params.id);
|
|
1032
|
+
const { tenantId, workspaceId } = req.openhiContext;
|
|
1033
|
+
const service = getDynamoDataService();
|
|
1078
1034
|
try {
|
|
1079
|
-
await service.entities.
|
|
1080
|
-
|
|
1081
|
-
workspaceId,
|
|
1082
|
-
userId,
|
|
1083
|
-
roleId,
|
|
1084
|
-
key,
|
|
1085
|
-
id,
|
|
1086
|
-
resource: compressResource(resourceStr),
|
|
1087
|
-
vid,
|
|
1088
|
-
lastUpdated,
|
|
1089
|
-
sk: SK3
|
|
1090
|
-
}).go();
|
|
1091
|
-
const config = {
|
|
1092
|
-
resourceType: "Configuration",
|
|
1093
|
-
id,
|
|
1094
|
-
key,
|
|
1095
|
-
resource: typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr),
|
|
1096
|
-
meta: { lastUpdated, versionId: vid }
|
|
1097
|
-
};
|
|
1098
|
-
return res.status(201).location(`${BASE_PATH3}/${key}`).json(config);
|
|
1035
|
+
await service.entities.patient.delete({ tenantId, workspaceId, id, sk: SK2 }).go();
|
|
1036
|
+
return res.status(204).send();
|
|
1099
1037
|
} catch (err) {
|
|
1100
|
-
console.error("
|
|
1038
|
+
console.error("DELETE Patient error:", err);
|
|
1101
1039
|
return res.status(500).json({
|
|
1102
1040
|
resourceType: "OperationOutcome",
|
|
1103
1041
|
issue: [
|
|
@@ -1106,67 +1044,133 @@ async function createConfiguration(req, res) {
|
|
|
1106
1044
|
});
|
|
1107
1045
|
}
|
|
1108
1046
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const
|
|
1113
|
-
const { tenantId, workspaceId
|
|
1114
|
-
const
|
|
1115
|
-
const body = req.body;
|
|
1116
|
-
const resourcePayload = body?.resource;
|
|
1117
|
-
const resourceStr = typeof resourcePayload === "string" ? resourcePayload : JSON.stringify(resourcePayload ?? {});
|
|
1118
|
-
const lastUpdated = body?.lastUpdated ?? date;
|
|
1119
|
-
const service = getOhiDataService(TABLE_NAME2);
|
|
1047
|
+
|
|
1048
|
+
// src/data/rest-api/routes/patient/patient-get-by-id.ts
|
|
1049
|
+
async function getPatientById(req, res) {
|
|
1050
|
+
const id = String(req.params.id);
|
|
1051
|
+
const { tenantId, workspaceId } = req.openhiContext;
|
|
1052
|
+
const service = getDynamoDataService();
|
|
1120
1053
|
try {
|
|
1121
|
-
const
|
|
1122
|
-
if (!
|
|
1054
|
+
const result = await service.entities.patient.get({ tenantId, workspaceId, id, sk: SK2 }).go();
|
|
1055
|
+
if (!result.data) {
|
|
1123
1056
|
return res.status(404).json({
|
|
1124
1057
|
resourceType: "OperationOutcome",
|
|
1125
1058
|
issue: [
|
|
1126
1059
|
{
|
|
1127
1060
|
severity: "error",
|
|
1128
1061
|
code: "not-found",
|
|
1129
|
-
diagnostics: `
|
|
1062
|
+
diagnostics: `Patient ${id} not found`
|
|
1130
1063
|
}
|
|
1131
1064
|
]
|
|
1132
1065
|
});
|
|
1133
1066
|
}
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
resourceType: "
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1067
|
+
const resource = JSON.parse(
|
|
1068
|
+
decompressResource(result.data.resource)
|
|
1069
|
+
);
|
|
1070
|
+
return res.json({ ...resource, id: result.data.id });
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
console.error("GET Patient error:", err);
|
|
1073
|
+
return res.status(500).json({
|
|
1074
|
+
resourceType: "OperationOutcome",
|
|
1075
|
+
issue: [
|
|
1076
|
+
{
|
|
1077
|
+
severity: "error",
|
|
1078
|
+
code: "exception",
|
|
1079
|
+
diagnostics: String(err)
|
|
1080
|
+
}
|
|
1081
|
+
]
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/data/rest-api/routes/patient/patient-list.ts
|
|
1087
|
+
async function listPatients(req, res) {
|
|
1088
|
+
const { tenantId, workspaceId } = req.openhiContext;
|
|
1089
|
+
const service = getDynamoDataService();
|
|
1090
|
+
try {
|
|
1091
|
+
const result = await service.entities.patient.query.gsi4({ tenantId, workspaceId }).go();
|
|
1092
|
+
const entries = (result.data ?? []).map((item) => {
|
|
1093
|
+
const resource = JSON.parse(decompressResource(item.resource));
|
|
1094
|
+
return {
|
|
1095
|
+
fullUrl: `${BASE_PATH3}/${item.id}`,
|
|
1096
|
+
resource: { ...resource, id: item.id }
|
|
1097
|
+
};
|
|
1098
|
+
});
|
|
1099
|
+
const bundle = {
|
|
1100
|
+
resourceType: "Bundle",
|
|
1101
|
+
type: "searchset",
|
|
1102
|
+
total: entries.length,
|
|
1103
|
+
link: [{ relation: "self", url: BASE_PATH3 }],
|
|
1104
|
+
entry: entries
|
|
1146
1105
|
};
|
|
1147
|
-
return res.json(
|
|
1106
|
+
return res.json(bundle);
|
|
1148
1107
|
} catch (err) {
|
|
1149
|
-
console.error("
|
|
1108
|
+
console.error("GET /Patient list error:", err);
|
|
1150
1109
|
return res.status(500).json({
|
|
1151
1110
|
resourceType: "OperationOutcome",
|
|
1152
1111
|
issue: [
|
|
1153
|
-
{
|
|
1112
|
+
{
|
|
1113
|
+
severity: "error",
|
|
1114
|
+
code: "exception",
|
|
1115
|
+
diagnostics: String(err)
|
|
1116
|
+
}
|
|
1154
1117
|
]
|
|
1155
1118
|
});
|
|
1156
1119
|
}
|
|
1157
1120
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1121
|
+
|
|
1122
|
+
// src/data/rest-api/routes/patient/patient-update.ts
|
|
1123
|
+
async function updatePatient(req, res) {
|
|
1124
|
+
const bodyResult = requireJsonBody(req, res);
|
|
1125
|
+
if ("errorResponse" in bodyResult) return bodyResult.errorResponse;
|
|
1126
|
+
const id = String(req.params.id);
|
|
1161
1127
|
const ctx = req.openhiContext;
|
|
1162
|
-
const { tenantId, workspaceId,
|
|
1163
|
-
const
|
|
1164
|
-
const
|
|
1128
|
+
const { tenantId, workspaceId, date, actorId, actorName } = ctx;
|
|
1129
|
+
const body = bodyResult.body;
|
|
1130
|
+
const patient = {
|
|
1131
|
+
...body,
|
|
1132
|
+
resourceType: "Patient",
|
|
1133
|
+
id,
|
|
1134
|
+
meta: {
|
|
1135
|
+
...body?.meta ?? {},
|
|
1136
|
+
lastUpdated: date,
|
|
1137
|
+
versionId: "2"
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
const service = getDynamoDataService();
|
|
1165
1141
|
try {
|
|
1166
|
-
await service.entities.
|
|
1167
|
-
|
|
1142
|
+
const existing = await service.entities.patient.get({ tenantId, workspaceId, id, sk: SK2 }).go();
|
|
1143
|
+
if (!existing.data) {
|
|
1144
|
+
return res.status(404).json({
|
|
1145
|
+
resourceType: "OperationOutcome",
|
|
1146
|
+
issue: [
|
|
1147
|
+
{
|
|
1148
|
+
severity: "error",
|
|
1149
|
+
code: "not-found",
|
|
1150
|
+
diagnostics: `Patient ${id} not found`
|
|
1151
|
+
}
|
|
1152
|
+
]
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
const existingMeta = existing.data.resource != null ? JSON.parse(decompressResource(existing.data.resource)).meta : void 0;
|
|
1156
|
+
const patientWithMeta = {
|
|
1157
|
+
...patient,
|
|
1158
|
+
meta: mergeAuditIntoMeta(
|
|
1159
|
+
patient.meta ?? existingMeta,
|
|
1160
|
+
{
|
|
1161
|
+
modifiedDate: date,
|
|
1162
|
+
modifiedById: actorId,
|
|
1163
|
+
modifiedByName: actorName
|
|
1164
|
+
}
|
|
1165
|
+
)
|
|
1166
|
+
};
|
|
1167
|
+
await service.entities.patient.patch({ tenantId, workspaceId, id, sk: SK2 }).set({
|
|
1168
|
+
resource: compressResource(JSON.stringify(patientWithMeta)),
|
|
1169
|
+
lastUpdated: date
|
|
1170
|
+
}).go();
|
|
1171
|
+
return res.json(patientWithMeta);
|
|
1168
1172
|
} catch (err) {
|
|
1169
|
-
console.error("
|
|
1173
|
+
console.error("PUT Patient error:", err);
|
|
1170
1174
|
return res.status(500).json({
|
|
1171
1175
|
resourceType: "OperationOutcome",
|
|
1172
1176
|
issue: [
|
|
@@ -1175,7 +1179,14 @@ async function deleteConfiguration(req, res) {
|
|
|
1175
1179
|
});
|
|
1176
1180
|
}
|
|
1177
1181
|
}
|
|
1178
|
-
|
|
1182
|
+
|
|
1183
|
+
// src/data/rest-api/routes/patient/patient.ts
|
|
1184
|
+
var router2 = import_express2.default.Router();
|
|
1185
|
+
router2.get("/", listPatients);
|
|
1186
|
+
router2.get("/:id", getPatientById);
|
|
1187
|
+
router2.post("/", createPatient);
|
|
1188
|
+
router2.put("/:id", updatePatient);
|
|
1189
|
+
router2.delete("/:id", deletePatient);
|
|
1179
1190
|
|
|
1180
1191
|
// src/data/rest-api/rest-api.ts
|
|
1181
1192
|
var app = (0, import_express3.default)();
|
|
@@ -1188,10 +1199,9 @@ app.use(normalizeJsonBodyMiddleware);
|
|
|
1188
1199
|
app.get("/", (_req, res) => {
|
|
1189
1200
|
return res.status(200).json({ message: "POC App is running" });
|
|
1190
1201
|
});
|
|
1191
|
-
app.use("/
|
|
1192
|
-
app.use("/
|
|
1193
|
-
app.use("/
|
|
1194
|
-
app.use("/ohi/Configuration", router2);
|
|
1202
|
+
app.use(["/Patient", "/Configuration"], openHiContextMiddleware);
|
|
1203
|
+
app.use("/Patient", router2);
|
|
1204
|
+
app.use("/Configuration", router);
|
|
1195
1205
|
|
|
1196
1206
|
// src/data/lambda/rest-api-lambda.handler.ts
|
|
1197
1207
|
var handler = (0, import_serverless_express2.default)({ app });
|