@openhi/constructs 0.0.26 → 0.0.28

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