@openhi/constructs 0.0.5 → 0.0.6

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.
@@ -8,7 +8,7 @@ import serverlessExpress from "@codegenie/serverless-express";
8
8
  // src/data/rest-api/rest-api.ts
9
9
  import path from "path";
10
10
  import cors from "cors";
11
- import express2 from "express";
11
+ import express3 from "express";
12
12
 
13
13
  // src/data/middleware/open-hi-context.ts
14
14
  var STATIC_TENANT_ID = "tenant-1";
@@ -625,18 +625,486 @@ async function deletePatient(req, res) {
625
625
  }
626
626
  router.delete("/:id", deletePatient);
627
627
 
628
+ // src/data/rest-api/ohi/Configuration.ts
629
+ import express2 from "express";
630
+
631
+ // src/data/rest-api/ohi/dynamic-configuration.ts
632
+ import {
633
+ DescribeParametersCommand,
634
+ GetParametersCommand,
635
+ SSMClient
636
+ } from "@aws-sdk/client-ssm";
637
+ var BASE_PATH2 = "/ohi/Configuration";
638
+ var TAG_KEY_BRANCH = "openhi:branch-name";
639
+ var TAG_KEY_HTTP_API_PARAM = "openhi:param-name";
640
+ function getSsmDynamicConfigEnvFilter() {
641
+ const branchTagValue = process.env.BRANCH_TAG_VALUE;
642
+ const httpApiTagValue = process.env.HTTP_API_TAG_VALUE;
643
+ if (branchTagValue == null || branchTagValue === "" || httpApiTagValue == null || httpApiTagValue === "") {
644
+ return null;
645
+ }
646
+ return { branchTagValue, httpApiTagValue };
647
+ }
648
+ async function getDynamicConfigurationEntries(context) {
649
+ const envFilter = getSsmDynamicConfigEnvFilter();
650
+ if (envFilter == null) {
651
+ return getStaticDummyEntry(context);
652
+ }
653
+ const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
654
+ const client3 = new SSMClient({ region });
655
+ try {
656
+ const describeResult = await client3.send(
657
+ new DescribeParametersCommand({
658
+ ParameterFilters: [
659
+ {
660
+ Key: `tag:${TAG_KEY_BRANCH}`,
661
+ Option: "Equals",
662
+ Values: [envFilter.branchTagValue]
663
+ },
664
+ {
665
+ Key: `tag:${TAG_KEY_HTTP_API_PARAM}`,
666
+ Option: "Equals",
667
+ Values: [envFilter.httpApiTagValue]
668
+ }
669
+ ],
670
+ MaxResults: 50
671
+ })
672
+ );
673
+ const names = (describeResult.Parameters ?? []).map((p) => p.Name).filter((n) => n != null);
674
+ if (names.length === 0) {
675
+ return getStaticDummyEntry(context);
676
+ }
677
+ const parameters = [];
678
+ for (let i = 0; i < names.length; i += 10) {
679
+ const batch = names.slice(i, i + 10);
680
+ const getResult = await client3.send(
681
+ new GetParametersCommand({
682
+ Names: batch,
683
+ WithDecryption: true
684
+ })
685
+ );
686
+ for (const p of getResult.Parameters ?? []) {
687
+ const name = p.Name;
688
+ const value = p.Value;
689
+ if (name != null && value != null) {
690
+ parameters.push({ name, value });
691
+ }
692
+ }
693
+ }
694
+ const parameterList = parameters.map((p) => {
695
+ const shortName = p.name.includes("/") ? p.name.split("/").slice(-1)[0] : p.name;
696
+ return { name: shortName, valueString: p.value };
697
+ });
698
+ const entry = {
699
+ fullUrl: `${BASE_PATH2}/ssm-dynamic`,
700
+ resource: {
701
+ resourceType: "Configuration",
702
+ id: "ssm-dynamic",
703
+ key: "ssm-dynamic",
704
+ resource: {
705
+ parameter: parameterList
706
+ },
707
+ meta: {
708
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
709
+ versionId: "1"
710
+ }
711
+ }
712
+ };
713
+ return [entry];
714
+ } catch (err) {
715
+ console.error("getDynamicConfigurationEntries SSM error:", err);
716
+ return getStaticDummyEntry(context);
717
+ }
718
+ }
719
+ function getStaticDummyEntry(_context) {
720
+ const dummy = {
721
+ fullUrl: `${BASE_PATH2}/dynamic-dummy`,
722
+ resource: {
723
+ resourceType: "Configuration",
724
+ id: "dynamic-dummy",
725
+ key: "dynamic-dummy",
726
+ resource: {
727
+ parameter: [
728
+ {
729
+ name: "description",
730
+ valueString: "Statically generated dummy configuration (not from DynamoDB)."
731
+ },
732
+ { name: "source", valueString: "dynamic-configuration" }
733
+ ]
734
+ },
735
+ meta: {
736
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
737
+ versionId: "1"
738
+ }
739
+ }
740
+ };
741
+ return [dummy];
742
+ }
743
+
744
+ // src/data/dynamo/ohi/ohi-data-service.ts
745
+ import { DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
746
+ import { Service as Service2 } from "electrodb";
747
+
748
+ // src/data/dynamo/ohi/Configuration.ts
749
+ import { Entity as Entity2 } from "electrodb";
750
+ var Configuration = new Entity2({
751
+ model: {
752
+ entity: "configuration",
753
+ service: "ohi",
754
+ version: "01"
755
+ },
756
+ attributes: {
757
+ /** Sort key. "CURRENT" for current version; version history in S3. */
758
+ sk: {
759
+ type: "string",
760
+ required: true,
761
+ default: "CURRENT"
762
+ },
763
+ /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
764
+ tenantId: {
765
+ type: "string",
766
+ required: true,
767
+ default: "BASELINE"
768
+ },
769
+ /** Workspace scope. Use "-" when absent. */
770
+ workspaceId: {
771
+ type: "string",
772
+ required: true,
773
+ default: "-"
774
+ },
775
+ /** User scope. Use "-" when absent. */
776
+ userId: {
777
+ type: "string",
778
+ required: true,
779
+ default: "-"
780
+ },
781
+ /** Role scope. Use "-" when absent. */
782
+ roleId: {
783
+ type: "string",
784
+ required: true,
785
+ default: "-"
786
+ },
787
+ /** Config type (category), e.g. endpoints, branding, display. */
788
+ key: {
789
+ type: "string",
790
+ required: true
791
+ },
792
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
793
+ id: {
794
+ type: "string",
795
+ required: true
796
+ },
797
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
798
+ resource: {
799
+ type: "string",
800
+ required: true
801
+ },
802
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
803
+ vid: {
804
+ type: "string",
805
+ required: true
806
+ },
807
+ lastUpdated: {
808
+ type: "string",
809
+ required: true
810
+ },
811
+ deleted: {
812
+ type: "boolean",
813
+ required: false
814
+ },
815
+ bundleId: {
816
+ type: "string",
817
+ required: false
818
+ },
819
+ msgId: {
820
+ type: "string",
821
+ required: false
822
+ }
823
+ },
824
+ indexes: {
825
+ /** 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. */
826
+ record: {
827
+ pk: {
828
+ field: "PK",
829
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
830
+ template: "OHI#CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
831
+ },
832
+ sk: {
833
+ field: "SK",
834
+ composite: ["key", "sk"],
835
+ template: "KEY#${key}#SK#${sk}"
836
+ }
837
+ },
838
+ /** 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. */
839
+ gsi4: {
840
+ index: "GSI4",
841
+ condition: () => true,
842
+ pk: {
843
+ field: "GSI4PK",
844
+ composite: ["tenantId", "workspaceId"],
845
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration"
846
+ },
847
+ sk: {
848
+ field: "GSI4SK",
849
+ composite: ["key", "sk"],
850
+ template: "KEY#${key}#SK#${sk}"
851
+ }
852
+ }
853
+ }
854
+ });
855
+
856
+ // src/data/dynamo/ohi/ohi-data-service.ts
857
+ var table2 = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
858
+ var client2 = new DynamoDBClient2({
859
+ ...process.env.MOCK_DYNAMODB_ENDPOINT && {
860
+ endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
861
+ sslEnabled: false,
862
+ region: "local"
863
+ }
864
+ });
865
+ var entities2 = { configuration: Configuration };
866
+ var OhiDataService = new Service2(entities2, { table: table2, client: client2 });
867
+ function getOhiDataService(tableName) {
868
+ return new Service2(entities2, { table: tableName, client: client2 });
869
+ }
870
+
871
+ // src/data/rest-api/ohi/Configuration.ts
872
+ var BASE_PATH3 = "/ohi/Configuration";
873
+ var router2 = express2.Router();
874
+ var SK3 = "CURRENT";
875
+ var TABLE_NAME2 = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
876
+ async function listConfigurations(req, res) {
877
+ const { tenantId, workspaceId } = req.openhiContext;
878
+ const service = getOhiDataService(TABLE_NAME2);
879
+ try {
880
+ const result = await service.entities.configuration.query.gsi4({ tenantId, workspaceId }).go();
881
+ const dynamoEntries = (result.data ?? []).map((item) => {
882
+ const resource = JSON.parse(decompressResource(item.resource));
883
+ return {
884
+ fullUrl: `${BASE_PATH3}/${item.key}`,
885
+ resource: { ...resource, id: item.id, key: item.key }
886
+ };
887
+ });
888
+ const dynamicEntries = await getDynamicConfigurationEntries({
889
+ tenantId,
890
+ workspaceId
891
+ });
892
+ const entries = [...dynamoEntries, ...dynamicEntries];
893
+ const bundle = {
894
+ resourceType: "Bundle",
895
+ type: "searchset",
896
+ total: entries.length,
897
+ link: [{ relation: "self", url: BASE_PATH3 }],
898
+ entry: entries
899
+ };
900
+ return res.json(bundle);
901
+ } catch (err) {
902
+ console.error("GET /Configuration list error:", err);
903
+ return res.status(500).json({
904
+ resourceType: "OperationOutcome",
905
+ issue: [
906
+ {
907
+ severity: "error",
908
+ code: "exception",
909
+ diagnostics: String(err)
910
+ }
911
+ ]
912
+ });
913
+ }
914
+ }
915
+ router2.get("/", listConfigurations);
916
+ async function getConfigurationByKey(req, res) {
917
+ const key = String(req.params.key);
918
+ const ctx = req.openhiContext;
919
+ const { tenantId, workspaceId, userId } = ctx;
920
+ const roleId = "roleId" in ctx && typeof ctx.roleId === "string" ? ctx.roleId : "-";
921
+ const service = getOhiDataService(TABLE_NAME2);
922
+ try {
923
+ const result = await service.entities.configuration.get({ tenantId, workspaceId, userId, roleId, key, sk: SK3 }).go();
924
+ if (!result.data) {
925
+ return res.status(404).json({
926
+ resourceType: "OperationOutcome",
927
+ issue: [
928
+ {
929
+ severity: "error",
930
+ code: "not-found",
931
+ diagnostics: `Configuration ${key} not found`
932
+ }
933
+ ]
934
+ });
935
+ }
936
+ const resource = JSON.parse(
937
+ decompressResource(result.data.resource)
938
+ );
939
+ return res.json({
940
+ ...resource,
941
+ resourceType: "Configuration",
942
+ id: result.data.id,
943
+ key: result.data.key
944
+ });
945
+ } catch (err) {
946
+ console.error("GET Configuration error:", err);
947
+ return res.status(500).json({
948
+ resourceType: "OperationOutcome",
949
+ issue: [
950
+ {
951
+ severity: "error",
952
+ code: "exception",
953
+ diagnostics: String(err)
954
+ }
955
+ ]
956
+ });
957
+ }
958
+ }
959
+ router2.get("/:key", getConfigurationByKey);
960
+ async function createConfiguration(req, res) {
961
+ const ctx = req.openhiContext;
962
+ const {
963
+ tenantId: ctxTenantId,
964
+ workspaceId: ctxWorkspaceId,
965
+ userId: ctxUserId,
966
+ date
967
+ } = ctx;
968
+ const body = req.body;
969
+ const key = body?.key;
970
+ if (!key || typeof key !== "string") {
971
+ return res.status(400).json({
972
+ resourceType: "OperationOutcome",
973
+ issue: [
974
+ {
975
+ severity: "error",
976
+ code: "required",
977
+ diagnostics: "Configuration key is required"
978
+ }
979
+ ]
980
+ });
981
+ }
982
+ const id = body?.id ?? `config-${key}-${Date.now()}`;
983
+ const resourcePayload = body?.resource;
984
+ const resourceStr = typeof resourcePayload === "string" ? resourcePayload : JSON.stringify(resourcePayload ?? {});
985
+ const tenantId = body?.tenantId ?? ctxTenantId;
986
+ const workspaceId = body?.workspaceId ?? ctxWorkspaceId;
987
+ const userId = body?.userId ?? ctxUserId ?? "-";
988
+ const roleId = body?.roleId ?? "-";
989
+ const vid = body?.vid ?? (date.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36));
990
+ const lastUpdated = body?.lastUpdated ?? date;
991
+ const service = getOhiDataService(TABLE_NAME2);
992
+ try {
993
+ await service.entities.configuration.put({
994
+ tenantId,
995
+ workspaceId,
996
+ userId,
997
+ roleId,
998
+ key,
999
+ id,
1000
+ resource: compressResource(resourceStr),
1001
+ vid,
1002
+ lastUpdated,
1003
+ sk: SK3
1004
+ }).go();
1005
+ const config = {
1006
+ resourceType: "Configuration",
1007
+ id,
1008
+ key,
1009
+ resource: typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr),
1010
+ meta: { lastUpdated, versionId: vid }
1011
+ };
1012
+ return res.status(201).location(`${BASE_PATH3}/${key}`).json(config);
1013
+ } catch (err) {
1014
+ console.error("POST Configuration error:", err);
1015
+ return res.status(500).json({
1016
+ resourceType: "OperationOutcome",
1017
+ issue: [
1018
+ { severity: "error", code: "exception", diagnostics: String(err) }
1019
+ ]
1020
+ });
1021
+ }
1022
+ }
1023
+ router2.post("/", createConfiguration);
1024
+ async function updateConfiguration(req, res) {
1025
+ const key = String(req.params.key);
1026
+ const ctx = req.openhiContext;
1027
+ const { tenantId, workspaceId, userId, date } = ctx;
1028
+ const roleId = "roleId" in ctx && typeof ctx.roleId === "string" ? ctx.roleId : "-";
1029
+ const body = req.body;
1030
+ const resourcePayload = body?.resource;
1031
+ const resourceStr = typeof resourcePayload === "string" ? resourcePayload : JSON.stringify(resourcePayload ?? {});
1032
+ const lastUpdated = body?.lastUpdated ?? date;
1033
+ const service = getOhiDataService(TABLE_NAME2);
1034
+ try {
1035
+ const existing = await service.entities.configuration.get({ tenantId, workspaceId, userId, roleId, key, sk: SK3 }).go();
1036
+ if (!existing.data) {
1037
+ return res.status(404).json({
1038
+ resourceType: "OperationOutcome",
1039
+ issue: [
1040
+ {
1041
+ severity: "error",
1042
+ code: "not-found",
1043
+ diagnostics: `Configuration ${key} not found`
1044
+ }
1045
+ ]
1046
+ });
1047
+ }
1048
+ const nextVid = existing.data.vid != null ? String(Number(existing.data.vid) + 1) : date.replace(/[-:T.Z]/g, "").slice(0, 12) || "2";
1049
+ await service.entities.configuration.patch({ tenantId, workspaceId, userId, roleId, key, sk: SK3 }).set({
1050
+ resource: compressResource(resourceStr),
1051
+ lastUpdated,
1052
+ vid: nextVid
1053
+ }).go();
1054
+ const config = {
1055
+ resourceType: "Configuration",
1056
+ id: existing.data.id,
1057
+ key: existing.data.key,
1058
+ resource: typeof resourcePayload === "object" ? resourcePayload : JSON.parse(resourceStr),
1059
+ meta: { lastUpdated, versionId: nextVid }
1060
+ };
1061
+ return res.json(config);
1062
+ } catch (err) {
1063
+ console.error("PUT Configuration error:", err);
1064
+ return res.status(500).json({
1065
+ resourceType: "OperationOutcome",
1066
+ issue: [
1067
+ { severity: "error", code: "exception", diagnostics: String(err) }
1068
+ ]
1069
+ });
1070
+ }
1071
+ }
1072
+ router2.put("/:key", updateConfiguration);
1073
+ async function deleteConfiguration(req, res) {
1074
+ const key = String(req.params.key);
1075
+ const ctx = req.openhiContext;
1076
+ const { tenantId, workspaceId, userId } = ctx;
1077
+ const roleId = "roleId" in ctx && typeof ctx.roleId === "string" ? ctx.roleId : "-";
1078
+ const service = getOhiDataService(TABLE_NAME2);
1079
+ try {
1080
+ await service.entities.configuration.delete({ tenantId, workspaceId, userId, roleId, key, sk: SK3 }).go();
1081
+ return res.status(204).send();
1082
+ } catch (err) {
1083
+ console.error("DELETE Configuration error:", err);
1084
+ return res.status(500).json({
1085
+ resourceType: "OperationOutcome",
1086
+ issue: [
1087
+ { severity: "error", code: "exception", diagnostics: String(err) }
1088
+ ]
1089
+ });
1090
+ }
1091
+ }
1092
+ router2.delete("/:key", deleteConfiguration);
1093
+
628
1094
  // src/data/rest-api/rest-api.ts
629
- var app = express2();
1095
+ var app = express3();
630
1096
  app.set("view engine", "ejs");
631
1097
  app.set("views", path.join(__dirname, "views"));
632
1098
  app.use(cors());
633
- app.use(express2.json());
634
- app.use(express2.urlencoded({ extended: true }));
1099
+ app.use(express3.json());
1100
+ app.use(express3.urlencoded({ extended: true }));
635
1101
  app.get("/", (_req, res) => {
636
1102
  return res.status(200).json({ message: "POC App is running" });
637
1103
  });
638
1104
  app.use("/ehr", openHiContextMiddleware);
1105
+ app.use("/ohi", openHiContextMiddleware);
639
1106
  app.use("/ehr/r4/Patient", router);
1107
+ app.use("/ohi/Configuration", router2);
640
1108
 
641
1109
  // src/data/lambda/rest-api-lambda.handler.ts
642
1110
  var handler = serverlessExpress({ app });