@openhi/constructs 0.0.170 → 0.0.172

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.
@@ -3,7 +3,7 @@ import {
3
3
  DATA_STORE_CHANGE_DETAIL_TYPE,
4
4
  buildFhirCurrentResourceChangeDetail,
5
5
  dynamodbImageToPlain
6
- } from "./chunk-ZXPA6W3G.mjs";
6
+ } from "./chunk-RC7HHZR6.mjs";
7
7
  import {
8
8
  decompressResource
9
9
  } from "./chunk-APVVG7BO.mjs";
@@ -327,4 +327,4 @@ export {
327
327
  buildArchivePayload,
328
328
  handler
329
329
  };
330
- //# sourceMappingURL=chunk-USNOOCSZ.mjs.map
330
+ //# sourceMappingURL=chunk-AWAWCRWW.mjs.map
@@ -87,7 +87,7 @@ function buildFhirCurrentResourceChangeDetail(record, keys) {
87
87
  const rawName = record.eventName;
88
88
  const changeType = rawName === "INSERT" || rawName === "MODIFY" || rawName === "REMOVE" ? rawName : "MODIFY";
89
89
  const seq = record.dynamodb?.SequenceNumber;
90
- const approxEpochSec = record.dynamodb?.ApproximateCreationDateTime;
90
+ const approxEpochMs = record.dynamodb?.ApproximateCreationDateTime;
91
91
  const newPlain = plainImage(
92
92
  record.dynamodb?.NewImage
93
93
  );
@@ -113,7 +113,7 @@ function buildFhirCurrentResourceChangeDetail(record, keys) {
113
113
  resourceId: keys.resourceId,
114
114
  resourceVersion: keys.version,
115
115
  ...typeof seq === "string" && seq.length > 0 ? { streamSequenceNumber: seq } : {},
116
- ...typeof approxEpochSec === "number" && Number.isFinite(approxEpochSec) ? { approximateCreationEpochSec: approxEpochSec } : {},
116
+ ...typeof approxEpochMs === "number" && Number.isFinite(approxEpochMs) ? { approximateCreationEpochMs: approxEpochMs } : {},
117
117
  ...changedAttributeNames ? { changedAttributeNames } : {}
118
118
  };
119
119
  }
@@ -124,4 +124,4 @@ export {
124
124
  DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
125
125
  buildFhirCurrentResourceChangeDetail
126
126
  };
127
- //# sourceMappingURL=chunk-ZXPA6W3G.mjs.map
127
+ //# sourceMappingURL=chunk-RC7HHZR6.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/dynamodb/dynamodb-stream-record.ts","../src/components/dynamodb/data-store-change-events.ts"],"sourcesContent":["import type { AttributeValue } from \"@aws-sdk/client-dynamodb\";\n\n/**\n * Shape of a DynamoDB change record as delivered inside Kinesis (table stream\n * destination) and decoded by the Firehose transform Lambda.\n */\nexport interface DynamoDbStreamKinesisRecord {\n eventName?: string;\n userIdentity?: unknown;\n dynamodb?: {\n Keys?: Record<string, AttributeValue>;\n NewImage?: Record<string, AttributeValue>;\n OldImage?: Record<string, AttributeValue>;\n SequenceNumber?: string;\n ApproximateCreationDateTime?: number;\n };\n}\n\nexport function dynamodbValueToJs(av: AttributeValue): unknown {\n if (av.S !== undefined) {\n return av.S;\n }\n if (av.N !== undefined) {\n return av.N.includes(\".\")\n ? Number.parseFloat(av.N)\n : Number.parseInt(av.N, 10);\n }\n if (av.BOOL !== undefined) {\n return av.BOOL;\n }\n if (av.NULL !== undefined) {\n return null;\n }\n if (av.M !== undefined) {\n return dynamodbImageToPlain(av.M);\n }\n if (av.L !== undefined) {\n return av.L.map((x: AttributeValue) => dynamodbValueToJs(x));\n }\n if (av.SS !== undefined) {\n return av.SS;\n }\n if (av.NS !== undefined) {\n return av.NS.map((n: string) =>\n n.includes(\".\") ? Number.parseFloat(n) : Number.parseInt(n, 10),\n );\n }\n return undefined;\n}\n\nexport function dynamodbImageToPlain(\n image: Record<string, AttributeValue>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(image)) {\n out[k] = dynamodbValueToJs(v);\n }\n return out;\n}\n","import type { AttributeValue } from \"@aws-sdk/client-dynamodb\";\nimport type { DynamoDbStreamKinesisRecord } from \"./dynamodb-stream-record\";\nimport { dynamodbImageToPlain } from \"./dynamodb-stream-record\";\n\n/**\n * EventBridge envelope constants for data-store CDC (no CDK imports).\n *\n * The bus-`Source` value is intentionally NOT defined here. Per TR-016\n * §Configuration Standards, the canonical per-bus `Source` constants\n * live in `@openhi/workflows` (`OPENHI_DATA_SOURCE = \"openhi.data\"`).\n * Importers should read that symbol directly so producer and consumer\n * never drift from a single source of truth.\n *\n * @see docs/architecture/adr/2026-03-02-01-dynamodb-stream-to-data-event-bus.md\n * @see https://github.com/codedrifters/openhi-planning/blob/main/docs/src/content/docs/requirements/technical-requirements/TR-016-openhi-workflows-envelope-package.md\n */\nexport const DATA_STORE_CHANGE_DETAIL_TYPE = \"FhirCurrentResourceChanged\";\n\n/** AWS PutEvents per-entry detail limit is 256 KiB; stay under for headroom. */\nexport const DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES = 250 * 1024;\n\nconst EXCLUDED_CHANGE_DETAIL_KEYS = new Set([\n \"PK\",\n \"SK\",\n \"GSI1PK\",\n \"GSI1SK\",\n \"GSI2PK\",\n \"GSI2SK\",\n /** Full FHIR JSON may contain PII; never list or ship in the bus payload. */\n \"resource\",\n]);\n\nfunction shallowValueEqual(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\nfunction changedNonResourceAttributeNames(\n oldImage: Record<string, unknown> | undefined,\n newImage: Record<string, unknown> | undefined,\n): string[] | undefined {\n if (!oldImage || !newImage) {\n return undefined;\n }\n const names = new Set<string>();\n const keys = new Set([...Object.keys(oldImage), ...Object.keys(newImage)]);\n for (const k of keys) {\n if (EXCLUDED_CHANGE_DETAIL_KEYS.has(k)) {\n continue;\n }\n if (!shallowValueEqual(oldImage[k], newImage[k])) {\n names.add(k);\n }\n }\n return names.size > 0 ? [...names].sort() : undefined;\n}\n\n/** Non-excluded attribute names present on an item (for INSERT / REMOVE). */\nfunction presentMetadataAttributeNames(\n image: Record<string, unknown> | undefined,\n): string[] | undefined {\n if (!image) {\n return undefined;\n }\n const names = Object.keys(image).filter(\n (k) => !EXCLUDED_CHANGE_DETAIL_KEYS.has(k),\n );\n return names.length > 0 ? names.sort() : undefined;\n}\n\nfunction plainImage(\n image: Record<string, AttributeValue> | undefined,\n): Record<string, unknown> | undefined {\n if (!image) {\n return undefined;\n }\n return dynamodbImageToPlain(image);\n}\n\nexport interface FhirCurrentResourceChangeDetail {\n changeType: \"INSERT\" | \"MODIFY\" | \"REMOVE\";\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n resourceVersion: string;\n streamSequenceNumber?: string;\n /**\n * Milliseconds since UNIX epoch. Mirrors the Kinesis-Data-Streams-for-DynamoDB\n * envelope's `ApproximateCreationDateTime`, which is ms — NOT seconds (the\n * legacy DynamoDB Streams API of the same name was in seconds).\n */\n approximateCreationEpochMs?: number;\n /**\n * MODIFY: attributes whose values differ between old and new images.\n * INSERT / REMOVE: attributes present on the written or removed image (metadata only).\n */\n changedAttributeNames?: string[];\n}\n\nexport function buildFhirCurrentResourceChangeDetail(\n record: DynamoDbStreamKinesisRecord,\n keys: {\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n version: string;\n },\n): FhirCurrentResourceChangeDetail {\n const rawName = record.eventName;\n const changeType =\n rawName === \"INSERT\" || rawName === \"MODIFY\" || rawName === \"REMOVE\"\n ? rawName\n : \"MODIFY\";\n\n const seq = record.dynamodb?.SequenceNumber;\n const approxEpochMs = record.dynamodb?.ApproximateCreationDateTime;\n\n const newPlain = plainImage(\n record.dynamodb?.NewImage as Record<string, AttributeValue> | undefined,\n );\n const oldPlain = plainImage(\n record.dynamodb?.OldImage as Record<string, AttributeValue> | undefined,\n );\n\n let changedAttributeNames: string[] | undefined;\n if (changeType === \"MODIFY\") {\n changedAttributeNames = changedNonResourceAttributeNames(\n oldPlain,\n newPlain,\n );\n } else if (changeType === \"INSERT\") {\n changedAttributeNames = presentMetadataAttributeNames(newPlain);\n } else {\n changedAttributeNames = presentMetadataAttributeNames(oldPlain);\n }\n\n return {\n changeType,\n tenantId: keys.tenantId,\n workspaceId: keys.workspaceId,\n resourceType: keys.resourceType,\n resourceId: keys.resourceId,\n resourceVersion: keys.version,\n ...(typeof seq === \"string\" && seq.length > 0\n ? { streamSequenceNumber: seq }\n : {}),\n ...(typeof approxEpochMs === \"number\" && Number.isFinite(approxEpochMs)\n ? { approximateCreationEpochMs: approxEpochMs }\n : {}),\n ...(changedAttributeNames ? { changedAttributeNames } : {}),\n };\n}\n"],"mappings":"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","names":[]}
@@ -1117,7 +1117,7 @@ function decodeKinesisRecord(record) {
1117
1117
  const json = Buffer.from(record.kinesis.data, "base64").toString("utf8");
1118
1118
  return JSON.parse(json);
1119
1119
  }
1120
- function deriveLastUpdated(resource, approximateCreationEpochSec) {
1120
+ function deriveLastUpdated(resource, approximateCreationEpochMs) {
1121
1121
  if (resource && typeof resource === "object") {
1122
1122
  const meta = resource.meta;
1123
1123
  const lu = meta?.lastUpdated;
@@ -1128,8 +1128,8 @@ function deriveLastUpdated(resource, approximateCreationEpochSec) {
1128
1128
  }
1129
1129
  }
1130
1130
  }
1131
- if (typeof approximateCreationEpochSec === "number" && Number.isFinite(approximateCreationEpochSec)) {
1132
- return new Date(approximateCreationEpochSec * 1e3);
1131
+ if (typeof approximateCreationEpochMs === "number" && Number.isFinite(approximateCreationEpochMs)) {
1132
+ return new Date(approximateCreationEpochMs);
1133
1133
  }
1134
1134
  return /* @__PURE__ */ new Date();
1135
1135
  }
@@ -1147,7 +1147,7 @@ function buildWriteIntent(change, awsRegion) {
1147
1147
  return { kind: "drop" };
1148
1148
  }
1149
1149
  const plain = dynamodbImageToPlain(image);
1150
- const approxEpochSec = change.dynamodb?.ApproximateCreationDateTime;
1150
+ const approxEpochMs = change.dynamodb?.ApproximateCreationDateTime;
1151
1151
  if (isRemove) {
1152
1152
  return {
1153
1153
  kind: "delete",
@@ -1157,7 +1157,7 @@ function buildWriteIntent(change, awsRegion) {
1157
1157
  resourceType: keys.resourceType,
1158
1158
  resourceId: keys.resourceId,
1159
1159
  version: keys.version,
1160
- deletedAt: deriveLastUpdated(void 0, approxEpochSec)
1160
+ deletedAt: deriveLastUpdated(void 0, approxEpochMs)
1161
1161
  }
1162
1162
  };
1163
1163
  }
@@ -1180,7 +1180,7 @@ function buildWriteIntent(change, awsRegion) {
1180
1180
  resourceType: keys.resourceType,
1181
1181
  resourceId: keys.resourceId,
1182
1182
  version: keys.version,
1183
- lastUpdated: deriveLastUpdated(resourceObj, approxEpochSec),
1183
+ lastUpdated: deriveLastUpdated(resourceObj, approxEpochMs),
1184
1184
  resourceJson
1185
1185
  }
1186
1186
  };