@openhi/constructs 0.0.25 → 0.0.27

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