@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
- userId: claims.openhi_user_id,
93
- username: claims.openhi_user_name
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/ehr/r4/Patient.ts
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/ehr/r4/ehr-r4-data-service.ts
168
+ // src/data/dynamo/dynamo-service.ts
158
169
  var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
159
- var import_electrodb2 = require("electrodb");
170
+ var import_electrodb3 = require("electrodb");
160
171
 
161
- // src/data/dynamo/ehr/r4/Patient.ts
172
+ // src/data/dynamo/entities/configuration.ts
162
173
  var import_electrodb = require("electrodb");
163
- var Patient = new import_electrodb.Entity({
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: "fhir",
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/ehr/r4/ehr-r4-data-service.ts
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 EhrR4DataService = new import_electrodb2.Service(entities, { table, client });
353
- function getEhrR4DataService(tableName) {
354
- return new import_electrodb2.Service(entities, { table: tableName, client });
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/import-patient.ts
358
- var import_node_fs = require("fs");
359
- var import_node_path = require("path");
360
- var OPENHI_EXT = "http://openhi.org/fhir/StructureDefinition";
361
- function mergeAuditIntoMeta(meta, audit) {
362
- const existing = meta ?? {};
363
- const ext = [
364
- ...Array.isArray(existing.extension) ? existing.extension : []
365
- ];
366
- const byUrl = new Map(ext.map((e) => [e.url, e]));
367
- function set(url, value, type) {
368
- if (value == null) return;
369
- byUrl.set(url, { url, [type]: value });
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
- function extractPatient(parsed) {
383
- if (parsed && typeof parsed === "object" && "resourceType" in parsed) {
384
- const root = parsed;
385
- if (root.resourceType === "Patient" && root.id) {
386
- return root;
387
- }
388
- if (root.resourceType === "Bundle" && "entry" in parsed) {
389
- const entries = parsed.entry;
390
- if (Array.isArray(entries)) {
391
- const patientEntry = entries.find(
392
- (e) => e?.resource?.resourceType === "Patient" && e.resource.id
393
- );
394
- if (patientEntry?.resource) {
395
- return patientEntry.resource;
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/ehr/r4/Patient.ts
500
- var BASE_PATH = "/ehr/r4/Patient";
501
- var router = import_express.default.Router();
502
- var SK2 = "CURRENT";
503
- var TABLE_NAME = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
504
- async function listPatients(req, res) {
505
- const { tenantId, workspaceId } = req.openhiContext;
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.patient.get({ tenantId, workspaceId, id, sk: "CURRENT" }).go();
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: `Patient ${id} not found`
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({ ...resource, id: result.data.id });
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 Patient error:", err);
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/ohi/Configuration.ts
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 = "/ohi/Configuration";
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 client3 = new import_client_ssm.SSMClient({ region });
627
+ const client2 = new import_client_ssm.SSMClient({ region });
741
628
  try {
742
- const describeResult = await client3.send(
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 client3.send(
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/dynamo/ohi/ohi-data-service.ts
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 = getOhiDataService(TABLE_NAME2);
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: `${BASE_PATH3}/${item.key}`,
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: BASE_PATH3 }],
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
- router2.get("/", listConfigurations);
1002
- async function getConfigurationByKey(req, res) {
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, userId } = ctx;
1006
- const roleId = "roleId" in ctx && typeof ctx.roleId === "string" ? ctx.roleId : "-";
1007
- const service = getOhiDataService(TABLE_NAME2);
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 result = await service.entities.configuration.get({ tenantId, workspaceId, userId, roleId, key, sk: SK3 }).go();
1010
- if (!result.data) {
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 resource = JSON.parse(
1023
- decompressResource(result.data.resource)
1024
- );
1025
- return res.json({
1026
- ...resource,
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: result.data.id,
1029
- key: result.data.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("GET Configuration error:", err);
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
- router2.get("/:key", getConfigurationByKey);
1046
- async function createConfiguration(req, res) {
1047
- const ctx = req.openhiContext;
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: ctxTenantId,
1050
- workspaceId: ctxWorkspaceId,
1051
- userId: ctxUserId,
1052
- date
1053
- } = ctx;
1054
- const body = req.body;
1055
- const key = body?.key;
1056
- if (!key || typeof key !== "string") {
1057
- return res.status(400).json({
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
- const id = body?.id ?? `config-${key}-${Date.now()}`;
1069
- const resourcePayload = body?.resource;
1070
- const resourceStr = typeof resourcePayload === "string" ? resourcePayload : JSON.stringify(resourcePayload ?? {});
1071
- const tenantId = body?.tenantId ?? ctxTenantId;
1072
- const workspaceId = body?.workspaceId ?? ctxWorkspaceId;
1073
- const userId = body?.userId ?? ctxUserId ?? "-";
1074
- const roleId = body?.roleId ?? "-";
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.configuration.put({
1080
- tenantId,
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("POST Configuration error:", err);
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
- router2.post("/", createConfiguration);
1110
- async function updateConfiguration(req, res) {
1111
- const key = String(req.params.key);
1112
- const ctx = req.openhiContext;
1113
- const { tenantId, workspaceId, userId, date } = ctx;
1114
- const roleId = "roleId" in ctx && typeof ctx.roleId === "string" ? ctx.roleId : "-";
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 existing = await service.entities.configuration.get({ tenantId, workspaceId, userId, roleId, key, sk: SK3 }).go();
1122
- if (!existing.data) {
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: `Configuration ${key} not found`
1061
+ diagnostics: `Patient ${id} not found`
1130
1062
  }
1131
1063
  ]
1132
1064
  });
1133
1065
  }
1134
- const nextVid = existing.data.vid != null ? String(Number(existing.data.vid) + 1) : date.replace(/[-:T.Z]/g, "").slice(0, 12) || "2";
1135
- await service.entities.configuration.patch({ tenantId, workspaceId, userId, roleId, key, sk: SK3 }).set({
1136
- resource: compressResource(resourceStr),
1137
- lastUpdated,
1138
- vid: nextVid
1139
- }).go();
1140
- const config = {
1141
- resourceType: "Configuration",
1142
- id: existing.data.id,
1143
- key: existing.data.key,
1144
- resource: typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr),
1145
- meta: { lastUpdated, versionId: nextVid }
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(config);
1105
+ return res.json(bundle);
1148
1106
  } catch (err) {
1149
- console.error("PUT Configuration error:", err);
1107
+ console.error("GET /Patient list error:", err);
1150
1108
  return res.status(500).json({
1151
1109
  resourceType: "OperationOutcome",
1152
1110
  issue: [
1153
- { severity: "error", code: "exception", diagnostics: String(err) }
1111
+ {
1112
+ severity: "error",
1113
+ code: "exception",
1114
+ diagnostics: String(err)
1115
+ }
1154
1116
  ]
1155
1117
  });
1156
1118
  }
1157
1119
  }
1158
- router2.put("/:key", updateConfiguration);
1159
- async function deleteConfiguration(req, res) {
1160
- const key = String(req.params.key);
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, userId } = ctx;
1163
- const roleId = "roleId" in ctx && typeof ctx.roleId === "string" ? ctx.roleId : "-";
1164
- const service = getOhiDataService(TABLE_NAME2);
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.configuration.delete({ tenantId, workspaceId, userId, roleId, key, sk: SK3 }).go();
1167
- return res.status(204).send();
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("DELETE Configuration error:", err);
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
- router2.delete("/:key", deleteConfiguration);
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("/ehr", openHiContextMiddleware);
1192
- app.use("/ohi", openHiContextMiddleware);
1193
- app.use("/ehr/r4/Patient", router);
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 });