@openhi/constructs 0.0.26 → 0.0.27

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