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