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