@openhi/constructs 0.0.76 → 0.0.78
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.
- package/lib/chunk-SWSN6GDD.mjs +133 -0
- package/lib/chunk-SWSN6GDD.mjs.map +1 -0
- package/lib/dynamodb-stream-record-CJtV6a1t.d.mts +19 -0
- package/lib/dynamodb-stream-record-CJtV6a1t.d.ts +19 -0
- package/lib/firehose-archive-transform.handler.d.mts +3 -11
- package/lib/firehose-archive-transform.handler.d.ts +3 -11
- package/lib/firehose-archive-transform.handler.js +220 -2
- package/lib/firehose-archive-transform.handler.js.map +1 -1
- package/lib/firehose-archive-transform.handler.mjs +139 -38
- package/lib/firehose-archive-transform.handler.mjs.map +1 -1
- package/lib/index.d.mts +52 -4
- package/lib/index.d.ts +68 -5
- package/lib/index.js +151 -2
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +28 -2
- package/lib/index.mjs.map +1 -1
- package/package.json +5 -3
|
@@ -1,42 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
|
|
3
|
+
DATA_STORE_CHANGE_DETAIL_TYPE,
|
|
4
|
+
DATA_STORE_CHANGE_EVENT_SOURCE,
|
|
5
|
+
buildFhirCurrentResourceChangeDetail,
|
|
6
|
+
dynamodbImageToPlain
|
|
7
|
+
} from "./chunk-SWSN6GDD.mjs";
|
|
1
8
|
import "./chunk-LZOMFHX3.mjs";
|
|
2
9
|
|
|
3
10
|
// src/components/dynamodb/firehose-archive-transform.handler.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
if (av.BOOL !== void 0) {
|
|
12
|
-
return av.BOOL;
|
|
13
|
-
}
|
|
14
|
-
if (av.NULL !== void 0) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
if (av.M !== void 0) {
|
|
18
|
-
return dynamodbImageToPlain(av.M);
|
|
19
|
-
}
|
|
20
|
-
if (av.L !== void 0) {
|
|
21
|
-
return av.L.map((x) => dynamodbValueToJs(x));
|
|
22
|
-
}
|
|
23
|
-
if (av.SS !== void 0) {
|
|
24
|
-
return av.SS;
|
|
25
|
-
}
|
|
26
|
-
if (av.NS !== void 0) {
|
|
27
|
-
return av.NS.map(
|
|
28
|
-
(n) => n.includes(".") ? Number.parseFloat(n) : Number.parseInt(n, 10)
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return void 0;
|
|
32
|
-
}
|
|
33
|
-
function dynamodbImageToPlain(image) {
|
|
34
|
-
const out = {};
|
|
35
|
-
for (const [k, v] of Object.entries(image)) {
|
|
36
|
-
out[k] = dynamodbValueToJs(v);
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
import {
|
|
13
|
+
EventBridgeClient,
|
|
14
|
+
PutEventsCommand
|
|
15
|
+
} from "@aws-sdk/client-eventbridge";
|
|
16
|
+
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
40
17
|
var CURRENT_SK = "CURRENT";
|
|
41
18
|
var PK_PATTERN = /^TID#(?<tenantId>[^#]+)#WID#(?<workspaceId>[^#]+)#RT#(?<resourceType>[^#]+)#ID#(?<resourceId>.+)$/;
|
|
42
19
|
var AWS_REP_UPDATE_REGION = "aws:rep:updateregion";
|
|
@@ -136,9 +113,131 @@ function buildArchivePayload(record, keys) {
|
|
|
136
113
|
resource: resourcePlain
|
|
137
114
|
};
|
|
138
115
|
}
|
|
139
|
-
|
|
116
|
+
var PUT_EVENTS_BATCH_SIZE = 10;
|
|
117
|
+
var MAX_PUT_EVENTS_ROUNDS = 3;
|
|
118
|
+
var eventBridgeClient;
|
|
119
|
+
function getEventBridgeClient() {
|
|
120
|
+
const bus = process.env.DATA_EVENT_BUS_NAME?.trim();
|
|
121
|
+
if (!bus) {
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
if (!eventBridgeClient) {
|
|
125
|
+
eventBridgeClient = new EventBridgeClient({});
|
|
126
|
+
}
|
|
127
|
+
return eventBridgeClient;
|
|
128
|
+
}
|
|
129
|
+
var s3ClientForDlq;
|
|
130
|
+
function getS3ClientForDlq() {
|
|
131
|
+
const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();
|
|
132
|
+
if (!bucket) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
if (!s3ClientForDlq) {
|
|
136
|
+
s3ClientForDlq = new S3Client({});
|
|
137
|
+
}
|
|
138
|
+
return s3ClientForDlq;
|
|
139
|
+
}
|
|
140
|
+
async function writePutEventsFailuresToDlq(payload) {
|
|
141
|
+
const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();
|
|
142
|
+
const client = getS3ClientForDlq();
|
|
143
|
+
if (!bucket || !client) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`PutEvents exhausted retries but DATA_STORE_PUT_EVENTS_DLQ_BUCKET is not set (${payload.reason})`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const day = payload.failedAt.slice(0, 10);
|
|
149
|
+
const key = `put-events-failed/${day}/${randomUUID()}.json`;
|
|
150
|
+
await client.send(
|
|
151
|
+
new PutObjectCommand({
|
|
152
|
+
Bucket: bucket,
|
|
153
|
+
Key: key,
|
|
154
|
+
Body: JSON.stringify(payload),
|
|
155
|
+
ContentType: "application/json"
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
async function putEventsChunkWithRetriesAndDlq(client, entries) {
|
|
160
|
+
if (entries.length === 0) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let pending = [...entries];
|
|
164
|
+
for (let round = 1; round <= MAX_PUT_EVENTS_ROUNDS; round++) {
|
|
165
|
+
try {
|
|
166
|
+
const out = await client.send(new PutEventsCommand({ Entries: pending }));
|
|
167
|
+
const failed = out.FailedEntryCount ?? 0;
|
|
168
|
+
if (failed === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const nextPending = [];
|
|
172
|
+
out.Entries?.forEach((e, i) => {
|
|
173
|
+
if (e?.ErrorCode && pending[i]) {
|
|
174
|
+
nextPending.push(pending[i]);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
pending = nextPending;
|
|
178
|
+
if (pending.length === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (round === MAX_PUT_EVENTS_ROUNDS) {
|
|
182
|
+
await writePutEventsFailuresToDlq({
|
|
183
|
+
dlqSchemaVersion: 1,
|
|
184
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
185
|
+
reason: "put_events_partial_failure",
|
|
186
|
+
attemptRounds: MAX_PUT_EVENTS_ROUNDS,
|
|
187
|
+
entries: pending,
|
|
188
|
+
putEventsResultEntries: out.Entries
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
} catch (sdkErr) {
|
|
193
|
+
const sdkMessage = sdkErr instanceof Error ? sdkErr.message : String(sdkErr);
|
|
194
|
+
if (round === MAX_PUT_EVENTS_ROUNDS) {
|
|
195
|
+
await writePutEventsFailuresToDlq({
|
|
196
|
+
dlqSchemaVersion: 1,
|
|
197
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
198
|
+
reason: "put_events_sdk_error",
|
|
199
|
+
attemptRounds: MAX_PUT_EVENTS_ROUNDS,
|
|
200
|
+
entries: pending,
|
|
201
|
+
sdkError: sdkMessage
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await new Promise((r) => setTimeout(r, 50 * round));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function publishDataStoreChangeEvents(pending) {
|
|
210
|
+
const client = getEventBridgeClient();
|
|
211
|
+
const busName = process.env.DATA_EVENT_BUS_NAME?.trim();
|
|
212
|
+
if (!client || !busName || pending.length === 0) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const entries = [];
|
|
216
|
+
for (const { change, keys } of pending) {
|
|
217
|
+
const detailObj = buildFhirCurrentResourceChangeDetail(change, keys);
|
|
218
|
+
const detail = JSON.stringify(detailObj);
|
|
219
|
+
const detailBytes = Buffer.byteLength(detail, "utf8");
|
|
220
|
+
if (detailBytes > DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Event detail is ${detailBytes} bytes (max ${DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES}); oversize strategy deferred per ADR 2026-03-02-01 (${keys.resourceType}/${keys.resourceId}).`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
entries.push({
|
|
226
|
+
Source: DATA_STORE_CHANGE_EVENT_SOURCE,
|
|
227
|
+
DetailType: DATA_STORE_CHANGE_DETAIL_TYPE,
|
|
228
|
+
Detail: detail,
|
|
229
|
+
EventBusName: busName
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
for (let i = 0; i < entries.length; i += PUT_EVENTS_BATCH_SIZE) {
|
|
233
|
+
const chunk = entries.slice(i, i + PUT_EVENTS_BATCH_SIZE);
|
|
234
|
+
await putEventsChunkWithRetriesAndDlq(client, chunk);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function handler(event) {
|
|
140
238
|
const records = [];
|
|
141
239
|
const archiveLambdaRegion = process.env.AWS_REGION ?? "";
|
|
240
|
+
const pendingPublish = [];
|
|
142
241
|
for (const rec of event.records) {
|
|
143
242
|
try {
|
|
144
243
|
const payload = Buffer.from(rec.data, "base64").toString("utf8");
|
|
@@ -165,6 +264,7 @@ function handler(event) {
|
|
|
165
264
|
`).toString(
|
|
166
265
|
"base64"
|
|
167
266
|
);
|
|
267
|
+
pendingPublish.push({ change, keys });
|
|
168
268
|
records.push({
|
|
169
269
|
recordId: rec.recordId,
|
|
170
270
|
result: "Ok",
|
|
@@ -187,7 +287,8 @@ function handler(event) {
|
|
|
187
287
|
});
|
|
188
288
|
}
|
|
189
289
|
}
|
|
190
|
-
|
|
290
|
+
await publishDataStoreChangeEvents(pendingPublish);
|
|
291
|
+
return { records };
|
|
191
292
|
}
|
|
192
293
|
export {
|
|
193
294
|
handler,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/dynamodb/firehose-archive-transform.handler.ts"],"sourcesContent":["import type { AttributeValue } from \"@aws-sdk/client-dynamodb\";\nimport type {\n FirehoseTransformationEvent,\n FirehoseTransformationResult,\n FirehoseTransformationResultRecord,\n} from \"aws-lambda\";\n\nfunction 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) => dynamodbValueToJs(x));\n }\n if (av.SS !== undefined) {\n return av.SS;\n }\n if (av.NS !== undefined) {\n return av.NS.map((n) =>\n n.includes(\".\") ? Number.parseFloat(n) : Number.parseInt(n, 10),\n );\n }\n return undefined;\n}\n\nfunction 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\n/**\n * Firehose data-transformation handler: filters DynamoDB change records to\n * current FHIR resource items (SK = CURRENT, TID#…#WID#…#RT#…#ID#… PK),\n * writes archive JSON to S3 via Firehose, and sets dynamic partition keys\n * tenantId/workspaceId/resourceType/resourceId/version per ADR 2026-03-11-02.\n */\n\nconst CURRENT_SK = \"CURRENT\";\nconst PK_PATTERN =\n /^TID#(?<tenantId>[^#]+)#WID#(?<workspaceId>[^#]+)#RT#(?<resourceType>[^#]+)#ID#(?<resourceId>.+)$/;\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 };\n}\n\n/** DynamoDB-managed attribute on global table items (see AWS Global Tables legacy / replication docs). */\nconst AWS_REP_UPDATE_REGION = \"aws:rep:updateregion\";\n\nfunction getDynamoDbStringAttr(\n image: Record<string, AttributeValue> | undefined,\n name: string,\n): string | undefined {\n if (!image) {\n return undefined;\n }\n const av = image[name];\n if (typeof av?.S === \"string\" && av.S.trim() !== \"\") {\n return av.S.trim();\n }\n return undefined;\n}\n\nfunction primaryImageForReplicationCheck(\n record: DynamoDbStreamKinesisRecord,\n): Record<string, AttributeValue> | undefined {\n if (record.eventName === \"REMOVE\") {\n return record.dynamodb?.OldImage;\n }\n return record.dynamodb?.NewImage;\n}\n\n/**\n * Returns true when this stream/Kinesis record should not be archived because it\n * represents a **replica-side application** of a global-table change (the logical\n * write originated in another Region).\n *\n * - If `aws:rep:updateregion` is present on the item image and differs from\n * `archiveLambdaRegion`, the change was replicated into this Region (archive\n * only in the Region that matches `aws:rep:updateregion`).\n * - Otherwise, if `userIdentity` matches the DynamoDB replication service SLR,\n * treat as replication. **Excluded:** TTL deletes (`type` Service and\n * `principalId` `dynamodb.amazonaws.com`) per AWS stream Identity docs.\n *\n * For MREC global tables version 2019.11.21, AWS documents that stream records\n * may not carry distinguishable metadata; the recommended approach is a custom\n * “write region” attribute on items. If neither that attribute nor\n * `aws:rep:updateregion` nor replication `userIdentity` applies, this function\n * returns false (no drop)—duplicate archives are possible if identical pipelines\n * run in every Region without those signals.\n */\nexport function shouldDropAsGlobalTableReplicationRecord(\n record: DynamoDbStreamKinesisRecord,\n archiveLambdaRegion: string,\n): boolean {\n const image = primaryImageForReplicationCheck(record);\n const updateRegion = getDynamoDbStringAttr(image, AWS_REP_UPDATE_REGION);\n if (\n updateRegion &&\n archiveLambdaRegion &&\n updateRegion !== archiveLambdaRegion\n ) {\n return true;\n }\n\n return isDynamoDbReplicationUserIdentity(record.userIdentity);\n}\n\nfunction isDynamoDbReplicationUserIdentity(userIdentity: unknown): boolean {\n if (!userIdentity || typeof userIdentity !== \"object\") {\n return false;\n }\n const ui = userIdentity as Record<string, unknown>;\n const principalRaw = ui.principalId ?? ui.PrincipalId;\n const typeRaw = ui.type ?? ui.Type;\n const principal =\n typeof principalRaw === \"string\" ? principalRaw.toLowerCase() : \"\";\n const type = typeof typeRaw === \"string\" ? typeRaw.toLowerCase() : \"\";\n\n if (type === \"service\" && principal === \"dynamodb.amazonaws.com\") {\n return false;\n }\n\n const replicationMarkers = [\n \"awsservicerolefordynamodbreplication\",\n \"replication.dynamodb.amazonaws.com\",\n ];\n return replicationMarkers.some((m) => principal.includes(m));\n}\n\nexport function parseCurrentResourceKeys(record: DynamoDbStreamKinesisRecord): {\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n version: string;\n} | null {\n const keys = record.dynamodb?.Keys;\n if (!keys) {\n return null;\n }\n const pkAttr = keys.PK?.S;\n const skAttr = keys.SK?.S;\n if (!pkAttr || skAttr !== CURRENT_SK) {\n return null;\n }\n const m = PK_PATTERN.exec(pkAttr);\n if (!m?.groups) {\n return null;\n }\n const { tenantId, workspaceId, resourceType, resourceId } = m.groups;\n const image =\n record.eventName === \"REMOVE\"\n ? record.dynamodb?.OldImage\n : record.dynamodb?.NewImage;\n if (!image) {\n return null;\n }\n const plain = dynamodbImageToPlain(image as Record<string, AttributeValue>);\n const version = typeof plain.vid === \"string\" ? plain.vid : null;\n if (!version) {\n return null;\n }\n return { tenantId, workspaceId, resourceType, resourceId, version };\n}\n\nfunction partitionToken(value: string): string {\n if (!value || value.trim() === \"\") {\n return \"-\";\n }\n return value.replace(/[/\\\\]/g, \"_\");\n}\n\nfunction buildArchivePayload(\n record: DynamoDbStreamKinesisRecord,\n keys: ReturnType<typeof parseCurrentResourceKeys>,\n): Record<string, unknown> {\n const newImage = record.dynamodb?.NewImage;\n const oldImage = record.dynamodb?.OldImage;\n const resourceImage = record.eventName === \"REMOVE\" ? oldImage : newImage;\n const resourcePlain = resourceImage\n ? dynamodbImageToPlain(resourceImage as Record<string, AttributeValue>)\n : {};\n\n if (typeof resourcePlain.resource === \"string\") {\n try {\n resourcePlain.resource = JSON.parse(resourcePlain.resource) as unknown;\n } catch {\n /* keep raw string if not valid JSON */\n }\n }\n\n return {\n eventName: record.eventName,\n archivedAt: new Date().toISOString(),\n tenantId: keys!.tenantId,\n workspaceId: keys!.workspaceId,\n resourceType: keys!.resourceType,\n resourceId: keys!.resourceId,\n version: keys!.version,\n resource: resourcePlain,\n };\n}\n\nexport function handler(\n event: FirehoseTransformationEvent,\n): Promise<FirehoseTransformationResult> {\n const records: FirehoseTransformationResultRecord[] = [];\n const archiveLambdaRegion = process.env.AWS_REGION ?? \"\";\n\n for (const rec of event.records) {\n try {\n const payload = Buffer.from(rec.data, \"base64\").toString(\"utf8\");\n const change = JSON.parse(payload) as DynamoDbStreamKinesisRecord;\n\n if (\n shouldDropAsGlobalTableReplicationRecord(change, archiveLambdaRegion)\n ) {\n records.push({\n recordId: rec.recordId,\n result: \"Dropped\",\n data: rec.data,\n });\n continue;\n }\n\n const keys = parseCurrentResourceKeys(change);\n\n if (!keys) {\n records.push({\n recordId: rec.recordId,\n result: \"Dropped\",\n data: rec.data,\n });\n continue;\n }\n\n const archive = buildArchivePayload(change, keys);\n const out = Buffer.from(`${JSON.stringify(archive)}\\n`).toString(\n \"base64\",\n );\n\n records.push({\n recordId: rec.recordId,\n result: \"Ok\",\n data: out,\n metadata: {\n partitionKeys: {\n tenantId: partitionToken(keys.tenantId),\n workspaceId: partitionToken(keys.workspaceId),\n resourceType: partitionToken(keys.resourceType),\n resourceId: partitionToken(keys.resourceId),\n version: partitionToken(keys.version),\n },\n },\n });\n } catch {\n records.push({\n recordId: rec.recordId,\n result: \"ProcessingFailed\",\n data: rec.data,\n });\n }\n }\n\n return Promise.resolve({ records });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/components/dynamodb/firehose-archive-transform.handler.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { AttributeValue } from \"@aws-sdk/client-dynamodb\";\nimport {\n EventBridgeClient,\n PutEventsCommand,\n type PutEventsRequestEntry,\n type PutEventsResultEntry,\n} from \"@aws-sdk/client-eventbridge\";\nimport { PutObjectCommand, S3Client } from \"@aws-sdk/client-s3\";\nimport type {\n FirehoseTransformationEvent,\n FirehoseTransformationResult,\n FirehoseTransformationResultRecord,\n} from \"aws-lambda\";\nimport {\n DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,\n DATA_STORE_CHANGE_DETAIL_TYPE,\n DATA_STORE_CHANGE_EVENT_SOURCE,\n buildFhirCurrentResourceChangeDetail,\n} from \"./data-store-change-events\";\nimport {\n type DynamoDbStreamKinesisRecord,\n dynamodbImageToPlain,\n} from \"./dynamodb-stream-record\";\n\nexport type { DynamoDbStreamKinesisRecord } from \"./dynamodb-stream-record\";\n\n/**\n * Firehose data-transformation handler: filters DynamoDB change records to\n * current FHIR resource items (SK = CURRENT, TID#…#WID#…#RT#…#ID#… PK),\n * writes archive JSON to S3 via Firehose, sets dynamic partition keys per\n * ADR 2026-03-11-02, and publishes de-identified change notifications to the\n * data event bus via PutEvents per ADR 2026-03-02-01, with retries and an S3\n * dead-letter bucket for entries that still fail.\n */\n\nconst CURRENT_SK = \"CURRENT\";\nconst PK_PATTERN =\n /^TID#(?<tenantId>[^#]+)#WID#(?<workspaceId>[^#]+)#RT#(?<resourceType>[^#]+)#ID#(?<resourceId>.+)$/;\n\n/** DynamoDB-managed attribute on global table items (see AWS Global Tables legacy / replication docs). */\nconst AWS_REP_UPDATE_REGION = \"aws:rep:updateregion\";\n\nfunction getDynamoDbStringAttr(\n image: Record<string, AttributeValue> | undefined,\n name: string,\n): string | undefined {\n if (!image) {\n return undefined;\n }\n const av = image[name];\n if (typeof av?.S === \"string\" && av.S.trim() !== \"\") {\n return av.S.trim();\n }\n return undefined;\n}\n\nfunction primaryImageForReplicationCheck(\n record: DynamoDbStreamKinesisRecord,\n): Record<string, AttributeValue> | undefined {\n if (record.eventName === \"REMOVE\") {\n return record.dynamodb?.OldImage;\n }\n return record.dynamodb?.NewImage;\n}\n\n/**\n * Returns true when this stream/Kinesis record should not be archived because it\n * represents a **replica-side application** of a global-table change (the logical\n * write originated in another Region).\n *\n * - If `aws:rep:updateregion` is present on the item image and differs from\n * `archiveLambdaRegion`, the change was replicated into this Region (archive\n * only in the Region that matches `aws:rep:updateregion`).\n * - Otherwise, if `userIdentity` matches the DynamoDB replication service SLR,\n * treat as replication. **Excluded:** TTL deletes (`type` Service and\n * `principalId` `dynamodb.amazonaws.com`) per AWS stream Identity docs.\n *\n * For MREC global tables version 2019.11.21, AWS documents that stream records\n * may not carry distinguishable metadata; the recommended approach is a custom\n * “write region” attribute on items. If neither that attribute nor\n * `aws:rep:updateregion` nor replication `userIdentity` applies, this function\n * returns false (no drop)—duplicate archives are possible if identical pipelines\n * run in every Region without those signals.\n */\nexport function shouldDropAsGlobalTableReplicationRecord(\n record: DynamoDbStreamKinesisRecord,\n archiveLambdaRegion: string,\n): boolean {\n const image = primaryImageForReplicationCheck(record);\n const updateRegion = getDynamoDbStringAttr(image, AWS_REP_UPDATE_REGION);\n if (\n updateRegion &&\n archiveLambdaRegion &&\n updateRegion !== archiveLambdaRegion\n ) {\n return true;\n }\n\n return isDynamoDbReplicationUserIdentity(record.userIdentity);\n}\n\nfunction isDynamoDbReplicationUserIdentity(userIdentity: unknown): boolean {\n if (!userIdentity || typeof userIdentity !== \"object\") {\n return false;\n }\n const ui = userIdentity as Record<string, unknown>;\n const principalRaw = ui.principalId ?? ui.PrincipalId;\n const typeRaw = ui.type ?? ui.Type;\n const principal =\n typeof principalRaw === \"string\" ? principalRaw.toLowerCase() : \"\";\n const type = typeof typeRaw === \"string\" ? typeRaw.toLowerCase() : \"\";\n\n if (type === \"service\" && principal === \"dynamodb.amazonaws.com\") {\n return false;\n }\n\n const replicationMarkers = [\n \"awsservicerolefordynamodbreplication\",\n \"replication.dynamodb.amazonaws.com\",\n ];\n return replicationMarkers.some((m) => principal.includes(m));\n}\n\nexport function parseCurrentResourceKeys(record: DynamoDbStreamKinesisRecord): {\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n version: string;\n} | null {\n const keys = record.dynamodb?.Keys;\n if (!keys) {\n return null;\n }\n const pkAttr = keys.PK?.S;\n const skAttr = keys.SK?.S;\n if (!pkAttr || skAttr !== CURRENT_SK) {\n return null;\n }\n const m = PK_PATTERN.exec(pkAttr);\n if (!m?.groups) {\n return null;\n }\n const { tenantId, workspaceId, resourceType, resourceId } = m.groups;\n const image =\n record.eventName === \"REMOVE\"\n ? record.dynamodb?.OldImage\n : record.dynamodb?.NewImage;\n if (!image) {\n return null;\n }\n const plain = dynamodbImageToPlain(image as Record<string, AttributeValue>);\n const version = typeof plain.vid === \"string\" ? plain.vid : null;\n if (!version) {\n return null;\n }\n return { tenantId, workspaceId, resourceType, resourceId, version };\n}\n\nfunction partitionToken(value: string): string {\n if (!value || value.trim() === \"\") {\n return \"-\";\n }\n return value.replace(/[/\\\\]/g, \"_\");\n}\n\nfunction buildArchivePayload(\n record: DynamoDbStreamKinesisRecord,\n keys: ReturnType<typeof parseCurrentResourceKeys>,\n): Record<string, unknown> {\n const newImage = record.dynamodb?.NewImage;\n const oldImage = record.dynamodb?.OldImage;\n const resourceImage = record.eventName === \"REMOVE\" ? oldImage : newImage;\n const resourcePlain = resourceImage\n ? dynamodbImageToPlain(resourceImage as Record<string, AttributeValue>)\n : {};\n\n if (typeof resourcePlain.resource === \"string\") {\n try {\n resourcePlain.resource = JSON.parse(resourcePlain.resource) as unknown;\n } catch {\n /* keep raw string if not valid JSON */\n }\n }\n\n return {\n eventName: record.eventName,\n archivedAt: new Date().toISOString(),\n tenantId: keys!.tenantId,\n workspaceId: keys!.workspaceId,\n resourceType: keys!.resourceType,\n resourceId: keys!.resourceId,\n version: keys!.version,\n resource: resourcePlain,\n };\n}\n\nconst PUT_EVENTS_BATCH_SIZE = 10;\n\n/** Full PutEvents rounds per chunk (initial attempt + failure-driven retries). */\nconst MAX_PUT_EVENTS_ROUNDS = 3;\n\nlet eventBridgeClient: EventBridgeClient | undefined;\n\nfunction getEventBridgeClient(): EventBridgeClient | undefined {\n const bus = process.env.DATA_EVENT_BUS_NAME?.trim();\n if (!bus) {\n return undefined;\n }\n if (!eventBridgeClient) {\n eventBridgeClient = new EventBridgeClient({});\n }\n return eventBridgeClient;\n}\n\nlet s3ClientForDlq: S3Client | undefined;\n\nfunction getS3ClientForDlq(): S3Client | undefined {\n const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();\n if (!bucket) {\n return undefined;\n }\n if (!s3ClientForDlq) {\n s3ClientForDlq = new S3Client({});\n }\n return s3ClientForDlq;\n}\n\ntype PutEventsEntry = PutEventsRequestEntry;\n\ninterface PutEventsDlqPayload {\n dlqSchemaVersion: 1;\n failedAt: string;\n reason: \"put_events_partial_failure\" | \"put_events_sdk_error\";\n attemptRounds: number;\n entries: PutEventsEntry[];\n putEventsResultEntries?: PutEventsResultEntry[];\n sdkError?: string;\n}\n\nasync function writePutEventsFailuresToDlq(\n payload: PutEventsDlqPayload,\n): Promise<void> {\n const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();\n const client = getS3ClientForDlq();\n if (!bucket || !client) {\n throw new Error(\n `PutEvents exhausted retries but DATA_STORE_PUT_EVENTS_DLQ_BUCKET is not set (${payload.reason})`,\n );\n }\n const day = payload.failedAt.slice(0, 10);\n const key = `put-events-failed/${day}/${randomUUID()}.json`;\n await client.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(payload),\n ContentType: \"application/json\",\n }),\n );\n}\n\n/**\n * Sends one PutEvents batch (≤10 entries) with up to {@link MAX_PUT_EVENTS_ROUNDS}\n * rounds. After the last round, remaining failures or a final SDK error are\n * written to the DLQ S3 bucket (if configured); DLQ write failure throws.\n */\nasync function putEventsChunkWithRetriesAndDlq(\n client: EventBridgeClient,\n entries: PutEventsEntry[],\n): Promise<void> {\n if (entries.length === 0) {\n return;\n }\n\n let pending = [...entries];\n\n for (let round = 1; round <= MAX_PUT_EVENTS_ROUNDS; round++) {\n try {\n const out = await client.send(new PutEventsCommand({ Entries: pending }));\n const failed = out.FailedEntryCount ?? 0;\n if (failed === 0) {\n return;\n }\n\n const nextPending: PutEventsEntry[] = [];\n out.Entries?.forEach((e: PutEventsResultEntry | undefined, i: number) => {\n if (e?.ErrorCode && pending[i]) {\n nextPending.push(pending[i]!);\n }\n });\n pending = nextPending;\n\n if (pending.length === 0) {\n return;\n }\n\n if (round === MAX_PUT_EVENTS_ROUNDS) {\n await writePutEventsFailuresToDlq({\n dlqSchemaVersion: 1,\n failedAt: new Date().toISOString(),\n reason: \"put_events_partial_failure\",\n attemptRounds: MAX_PUT_EVENTS_ROUNDS,\n entries: pending,\n putEventsResultEntries: out.Entries,\n });\n return;\n }\n } catch (sdkErr) {\n const sdkMessage =\n sdkErr instanceof Error ? sdkErr.message : String(sdkErr);\n if (round === MAX_PUT_EVENTS_ROUNDS) {\n await writePutEventsFailuresToDlq({\n dlqSchemaVersion: 1,\n failedAt: new Date().toISOString(),\n reason: \"put_events_sdk_error\",\n attemptRounds: MAX_PUT_EVENTS_ROUNDS,\n entries: pending,\n sdkError: sdkMessage,\n });\n return;\n }\n await new Promise((r) => setTimeout(r, 50 * round));\n }\n }\n}\n\nasync function publishDataStoreChangeEvents(\n pending: Array<{\n change: DynamoDbStreamKinesisRecord;\n keys: NonNullable<ReturnType<typeof parseCurrentResourceKeys>>;\n }>,\n): Promise<void> {\n const client = getEventBridgeClient();\n const busName = process.env.DATA_EVENT_BUS_NAME?.trim();\n if (!client || !busName || pending.length === 0) {\n return;\n }\n\n const entries: PutEventsEntry[] = [];\n for (const { change, keys } of pending) {\n const detailObj = buildFhirCurrentResourceChangeDetail(change, keys);\n const detail = JSON.stringify(detailObj);\n const detailBytes = Buffer.byteLength(detail, \"utf8\");\n if (detailBytes > DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES) {\n throw new Error(\n `Event detail is ${detailBytes} bytes (max ${DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES}); ` +\n `oversize strategy deferred per ADR 2026-03-02-01 (${keys.resourceType}/${keys.resourceId}).`,\n );\n }\n entries.push({\n Source: DATA_STORE_CHANGE_EVENT_SOURCE,\n DetailType: DATA_STORE_CHANGE_DETAIL_TYPE,\n Detail: detail,\n EventBusName: busName,\n });\n }\n\n for (let i = 0; i < entries.length; i += PUT_EVENTS_BATCH_SIZE) {\n const chunk = entries.slice(i, i + PUT_EVENTS_BATCH_SIZE);\n await putEventsChunkWithRetriesAndDlq(client, chunk);\n }\n}\n\nexport async function handler(\n event: FirehoseTransformationEvent,\n): Promise<FirehoseTransformationResult> {\n const records: FirehoseTransformationResultRecord[] = [];\n const archiveLambdaRegion = process.env.AWS_REGION ?? \"\";\n const pendingPublish: Array<{\n change: DynamoDbStreamKinesisRecord;\n keys: NonNullable<ReturnType<typeof parseCurrentResourceKeys>>;\n }> = [];\n\n for (const rec of event.records) {\n try {\n const payload = Buffer.from(rec.data, \"base64\").toString(\"utf8\");\n const change = JSON.parse(payload) as DynamoDbStreamKinesisRecord;\n\n if (\n shouldDropAsGlobalTableReplicationRecord(change, archiveLambdaRegion)\n ) {\n records.push({\n recordId: rec.recordId,\n result: \"Dropped\",\n data: rec.data,\n });\n continue;\n }\n\n const keys = parseCurrentResourceKeys(change);\n\n if (!keys) {\n records.push({\n recordId: rec.recordId,\n result: \"Dropped\",\n data: rec.data,\n });\n continue;\n }\n\n const archive = buildArchivePayload(change, keys);\n const out = Buffer.from(`${JSON.stringify(archive)}\\n`).toString(\n \"base64\",\n );\n\n pendingPublish.push({ change, keys });\n\n records.push({\n recordId: rec.recordId,\n result: \"Ok\",\n data: out,\n metadata: {\n partitionKeys: {\n tenantId: partitionToken(keys.tenantId),\n workspaceId: partitionToken(keys.workspaceId),\n resourceType: partitionToken(keys.resourceType),\n resourceId: partitionToken(keys.resourceId),\n version: partitionToken(keys.version),\n },\n },\n });\n } catch {\n records.push({\n recordId: rec.recordId,\n result: \"ProcessingFailed\",\n data: rec.data,\n });\n }\n }\n\n await publishDataStoreChangeEvents(pendingPublish);\n\n return { records };\n}\n"],"mappings":"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","names":[]}
|
package/lib/index.d.mts
CHANGED
|
@@ -7,16 +7,19 @@ import { GraphqlApi, IGraphqlApi, GraphqlApiProps } from 'aws-cdk-lib/aws-appsyn
|
|
|
7
7
|
import { UserPool, UserPoolProps, UserPoolClient, UserPoolClientProps, UserPoolDomain, UserPoolDomainProps, IUserPool, IUserPoolClient, IUserPoolDomain } from 'aws-cdk-lib/aws-cognito';
|
|
8
8
|
import { Key, KeyProps, IKey } from 'aws-cdk-lib/aws-kms';
|
|
9
9
|
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
10
|
+
import { D as DynamoDbStreamKinesisRecord } from './dynamodb-stream-record-CJtV6a1t.mjs';
|
|
11
|
+
import * as events from 'aws-cdk-lib/aws-events';
|
|
12
|
+
import { EventBus, EventBusProps, IEventBus } from 'aws-cdk-lib/aws-events';
|
|
10
13
|
import * as kinesis from 'aws-cdk-lib/aws-kinesis';
|
|
11
14
|
import * as kinesisfirehose from 'aws-cdk-lib/aws-kinesisfirehose';
|
|
12
15
|
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
13
16
|
import { IBucket, BucketProps } from 'aws-cdk-lib/aws-s3';
|
|
14
17
|
import { Table, TableProps, ITable } from 'aws-cdk-lib/aws-dynamodb';
|
|
15
|
-
import { EventBus, EventBusProps, IEventBus } from 'aws-cdk-lib/aws-events';
|
|
16
18
|
import { HostedZone, HostedZoneProps, IHostedZone, HostedZoneAttributes } from 'aws-cdk-lib/aws-route53';
|
|
17
19
|
import { StringParameterProps, StringParameter } from 'aws-cdk-lib/aws-ssm';
|
|
18
20
|
import { Distribution, DistributionProps } from 'aws-cdk-lib/aws-cloudfront';
|
|
19
21
|
import { IFunction } from 'aws-cdk-lib/aws-lambda';
|
|
22
|
+
import '@aws-sdk/client-dynamodb';
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Properties for creating an OpenHiStage instance.
|
|
@@ -436,6 +439,38 @@ declare class PreTokenGenerationLambda extends Construct {
|
|
|
436
439
|
constructor(scope: Construct);
|
|
437
440
|
}
|
|
438
441
|
|
|
442
|
+
/**
|
|
443
|
+
* EventBridge envelope constants for data-store CDC (no CDK imports).
|
|
444
|
+
* @see docs/architecture/adr/2026-03-02-01-dynamodb-stream-to-data-event-bus.md
|
|
445
|
+
*/
|
|
446
|
+
declare const DATA_STORE_CHANGE_EVENT_SOURCE = "openhi.data.store";
|
|
447
|
+
declare const DATA_STORE_CHANGE_DETAIL_TYPE = "FhirCurrentResourceChanged";
|
|
448
|
+
/** AWS PutEvents per-entry detail limit is 256 KiB; stay under for headroom. */
|
|
449
|
+
declare const DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES: number;
|
|
450
|
+
interface FhirCurrentResourceChangeDetail {
|
|
451
|
+
changeType: "INSERT" | "MODIFY" | "REMOVE";
|
|
452
|
+
tenantId: string;
|
|
453
|
+
workspaceId: string;
|
|
454
|
+
resourceType: string;
|
|
455
|
+
resourceId: string;
|
|
456
|
+
resourceVersion: string;
|
|
457
|
+
streamSequenceNumber?: string;
|
|
458
|
+
/** Seconds since UNIX epoch (DynamoDB Streams `ApproximateCreationDateTime`). */
|
|
459
|
+
approximateCreationEpochSec?: number;
|
|
460
|
+
/**
|
|
461
|
+
* MODIFY: attributes whose values differ between old and new images.
|
|
462
|
+
* INSERT / REMOVE: attributes present on the written or removed image (metadata only).
|
|
463
|
+
*/
|
|
464
|
+
changedAttributeNames?: string[];
|
|
465
|
+
}
|
|
466
|
+
declare function buildFhirCurrentResourceChangeDetail(record: DynamoDbStreamKinesisRecord, keys: {
|
|
467
|
+
tenantId: string;
|
|
468
|
+
workspaceId: string;
|
|
469
|
+
resourceType: string;
|
|
470
|
+
resourceId: string;
|
|
471
|
+
version: string;
|
|
472
|
+
}): FhirCurrentResourceChangeDetail;
|
|
473
|
+
|
|
439
474
|
interface DataStoreHistoricalArchiveProps {
|
|
440
475
|
/**
|
|
441
476
|
* Kinesis stream that receives DynamoDB item-level changes (table Kinesis destination).
|
|
@@ -449,13 +484,25 @@ interface DataStoreHistoricalArchiveProps {
|
|
|
449
484
|
* Short hash for unique stream/bucket naming within the deployment.
|
|
450
485
|
*/
|
|
451
486
|
readonly stackHash: string;
|
|
487
|
+
/**
|
|
488
|
+
* When set, the Firehose transform Lambda publishes qualifying changes to
|
|
489
|
+
* this bus via PutEvents (ADR 2026-03-02-01).
|
|
490
|
+
*/
|
|
491
|
+
readonly dataEventBus?: events.IEventBus;
|
|
452
492
|
}
|
|
453
493
|
/**
|
|
454
494
|
* DynamoDB change stream → Kinesis → Firehose → S3 with a transform Lambda for
|
|
455
|
-
* scope filtering and dynamic partitioning (ADR 2026-03-11-02).
|
|
495
|
+
* scope filtering and dynamic partitioning (ADR 2026-03-11-02). The same Lambda
|
|
496
|
+
* publishes qualifying current-resource changes to the data event bus (ADR 2026-03-02-01)
|
|
497
|
+
* when {@link DataStoreHistoricalArchiveProps.dataEventBus} is set.
|
|
456
498
|
*/
|
|
457
499
|
declare class DataStoreHistoricalArchive extends Construct {
|
|
458
500
|
readonly archiveBucket: s3.Bucket;
|
|
501
|
+
/**
|
|
502
|
+
* Receives PutEvents payloads that still fail after in-Lambda retries when
|
|
503
|
+
* {@link DataStoreHistoricalArchiveProps.dataEventBus} is configured.
|
|
504
|
+
*/
|
|
505
|
+
readonly putEventsFailureDlqBucket?: s3.Bucket;
|
|
459
506
|
readonly deliveryStream: kinesisfirehose.IDeliveryStream;
|
|
460
507
|
readonly transformFunction: NodejsFunction;
|
|
461
508
|
constructor(scope: Construct, id: string, props: DataStoreHistoricalArchiveProps);
|
|
@@ -932,7 +979,8 @@ declare class OpenHiDataService extends OpenHiService {
|
|
|
932
979
|
*/
|
|
933
980
|
readonly dataStoreChangeStream: kinesis.IStream;
|
|
934
981
|
/**
|
|
935
|
-
*
|
|
982
|
+
* Historical archive pipeline (Kinesis → Firehose → S3) and data-event-bus
|
|
983
|
+
* notifications for current FHIR resources (ADRs 2026-03-11-02, 2026-03-02-01).
|
|
936
984
|
*/
|
|
937
985
|
readonly dataStoreHistoricalArchive: DataStoreHistoricalArchive;
|
|
938
986
|
constructor(ohEnv: OpenHiEnvironment, props?: OpenHiDataServiceProps);
|
|
@@ -975,4 +1023,4 @@ declare class OpenHiGraphqlService extends OpenHiService {
|
|
|
975
1023
|
protected createRootGraphqlApi(): RootGraphqlApi;
|
|
976
1024
|
}
|
|
977
1025
|
|
|
978
|
-
export { type BuildParameterNameProps, ChildHostedZone, type ChildHostedZoneProps, CognitoUserPool, CognitoUserPoolClient, CognitoUserPoolDomain, CognitoUserPoolKmsKey, DataEventBus, DataStoreHistoricalArchive, type DataStoreHistoricalArchiveProps, DiscoverableStringParameter, type DiscoverableStringParameterProps, DynamoDbDataStore, type DynamoDbDataStoreProps, OpenHiApp, type OpenHiAppProps, OpenHiAuthService, type OpenHiAuthServiceProps, OpenHiDataService, type OpenHiDataServiceProps, OpenHiEnvironment, type OpenHiEnvironmentProps, OpenHiGlobalService, type OpenHiGlobalServiceProps, OpenHiGraphqlService, type OpenHiGraphqlServiceProps, OpenHiRestApiService, type OpenHiRestApiServiceProps, OpenHiService, type OpenHiServiceProps, type OpenHiServiceType, OpenHiStage, type OpenHiStageProps, OpsEventBus, PreTokenGenerationLambda, REST_API_BASE_URL_SSM_NAME, RootGraphqlApi, type RootGraphqlApiProps, RootHostedZone, RootHttpApi, type RootHttpApiProps, RootWildcardCertificate, STATIC_HOSTING_SERVICE_TYPE, StaticHosting, type StaticHostingProps, getDynamoDbDataStoreTableName };
|
|
1026
|
+
export { type BuildParameterNameProps, ChildHostedZone, type ChildHostedZoneProps, CognitoUserPool, CognitoUserPoolClient, CognitoUserPoolDomain, CognitoUserPoolKmsKey, DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES, DATA_STORE_CHANGE_DETAIL_TYPE, DATA_STORE_CHANGE_EVENT_SOURCE, DataEventBus, DataStoreHistoricalArchive, type DataStoreHistoricalArchiveProps, DiscoverableStringParameter, type DiscoverableStringParameterProps, DynamoDbDataStore, type DynamoDbDataStoreProps, type FhirCurrentResourceChangeDetail, OpenHiApp, type OpenHiAppProps, OpenHiAuthService, type OpenHiAuthServiceProps, OpenHiDataService, type OpenHiDataServiceProps, OpenHiEnvironment, type OpenHiEnvironmentProps, OpenHiGlobalService, type OpenHiGlobalServiceProps, OpenHiGraphqlService, type OpenHiGraphqlServiceProps, OpenHiRestApiService, type OpenHiRestApiServiceProps, OpenHiService, type OpenHiServiceProps, type OpenHiServiceType, OpenHiStage, type OpenHiStageProps, OpsEventBus, PreTokenGenerationLambda, REST_API_BASE_URL_SSM_NAME, RootGraphqlApi, type RootGraphqlApiProps, RootHostedZone, RootHttpApi, type RootHttpApiProps, RootWildcardCertificate, STATIC_HOSTING_SERVICE_TYPE, StaticHosting, type StaticHostingProps, buildFhirCurrentResourceChangeDetail, getDynamoDbDataStoreTableName };
|
package/lib/index.d.ts
CHANGED
|
@@ -6,12 +6,14 @@ import { IGraphqlApi, GraphqlApi, GraphqlApiProps } from 'aws-cdk-lib/aws-appsyn
|
|
|
6
6
|
import { UserPool, UserPoolProps, UserPoolClient, UserPoolClientProps, UserPoolDomain, UserPoolDomainProps, IUserPool, IUserPoolClient, IUserPoolDomain } from 'aws-cdk-lib/aws-cognito';
|
|
7
7
|
import { Key, KeyProps, IKey } from 'aws-cdk-lib/aws-kms';
|
|
8
8
|
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
9
|
+
import { AttributeValue } from '@aws-sdk/client-dynamodb';
|
|
10
|
+
import * as events from 'aws-cdk-lib/aws-events';
|
|
11
|
+
import { EventBus, EventBusProps, IEventBus } from 'aws-cdk-lib/aws-events';
|
|
9
12
|
import * as kinesis from 'aws-cdk-lib/aws-kinesis';
|
|
10
13
|
import * as kinesisfirehose from 'aws-cdk-lib/aws-kinesisfirehose';
|
|
11
14
|
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
12
15
|
import { IBucket, BucketProps } from 'aws-cdk-lib/aws-s3';
|
|
13
16
|
import { Table, TableProps, ITable } from 'aws-cdk-lib/aws-dynamodb';
|
|
14
|
-
import { EventBus, EventBusProps, IEventBus } from 'aws-cdk-lib/aws-events';
|
|
15
17
|
import { HostedZone, HostedZoneProps, IHostedZone, HostedZoneAttributes } from 'aws-cdk-lib/aws-route53';
|
|
16
18
|
import { StringParameterProps, StringParameter } from 'aws-cdk-lib/aws-ssm';
|
|
17
19
|
import { Distribution, DistributionProps } from 'aws-cdk-lib/aws-cloudfront';
|
|
@@ -98,6 +100,22 @@ interface OpenHiConfig {
|
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Shape of a DynamoDB change record as delivered inside Kinesis (table stream
|
|
105
|
+
* destination) and decoded by the Firehose transform Lambda.
|
|
106
|
+
*/
|
|
107
|
+
interface DynamoDbStreamKinesisRecord {
|
|
108
|
+
eventName?: string;
|
|
109
|
+
userIdentity?: unknown;
|
|
110
|
+
dynamodb?: {
|
|
111
|
+
Keys?: Record<string, AttributeValue>;
|
|
112
|
+
NewImage?: Record<string, AttributeValue>;
|
|
113
|
+
OldImage?: Record<string, AttributeValue>;
|
|
114
|
+
SequenceNumber?: string;
|
|
115
|
+
ApproximateCreationDateTime?: number;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
/**
|
|
102
120
|
* Properties for creating an OpenHiStage instance.
|
|
103
121
|
*/
|
|
@@ -516,6 +534,38 @@ declare class PreTokenGenerationLambda extends Construct {
|
|
|
516
534
|
constructor(scope: Construct);
|
|
517
535
|
}
|
|
518
536
|
|
|
537
|
+
/**
|
|
538
|
+
* EventBridge envelope constants for data-store CDC (no CDK imports).
|
|
539
|
+
* @see docs/architecture/adr/2026-03-02-01-dynamodb-stream-to-data-event-bus.md
|
|
540
|
+
*/
|
|
541
|
+
declare const DATA_STORE_CHANGE_EVENT_SOURCE = "openhi.data.store";
|
|
542
|
+
declare const DATA_STORE_CHANGE_DETAIL_TYPE = "FhirCurrentResourceChanged";
|
|
543
|
+
/** AWS PutEvents per-entry detail limit is 256 KiB; stay under for headroom. */
|
|
544
|
+
declare const DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES: number;
|
|
545
|
+
interface FhirCurrentResourceChangeDetail {
|
|
546
|
+
changeType: "INSERT" | "MODIFY" | "REMOVE";
|
|
547
|
+
tenantId: string;
|
|
548
|
+
workspaceId: string;
|
|
549
|
+
resourceType: string;
|
|
550
|
+
resourceId: string;
|
|
551
|
+
resourceVersion: string;
|
|
552
|
+
streamSequenceNumber?: string;
|
|
553
|
+
/** Seconds since UNIX epoch (DynamoDB Streams `ApproximateCreationDateTime`). */
|
|
554
|
+
approximateCreationEpochSec?: number;
|
|
555
|
+
/**
|
|
556
|
+
* MODIFY: attributes whose values differ between old and new images.
|
|
557
|
+
* INSERT / REMOVE: attributes present on the written or removed image (metadata only).
|
|
558
|
+
*/
|
|
559
|
+
changedAttributeNames?: string[];
|
|
560
|
+
}
|
|
561
|
+
declare function buildFhirCurrentResourceChangeDetail(record: DynamoDbStreamKinesisRecord, keys: {
|
|
562
|
+
tenantId: string;
|
|
563
|
+
workspaceId: string;
|
|
564
|
+
resourceType: string;
|
|
565
|
+
resourceId: string;
|
|
566
|
+
version: string;
|
|
567
|
+
}): FhirCurrentResourceChangeDetail;
|
|
568
|
+
|
|
519
569
|
interface DataStoreHistoricalArchiveProps {
|
|
520
570
|
/**
|
|
521
571
|
* Kinesis stream that receives DynamoDB item-level changes (table Kinesis destination).
|
|
@@ -529,13 +579,25 @@ interface DataStoreHistoricalArchiveProps {
|
|
|
529
579
|
* Short hash for unique stream/bucket naming within the deployment.
|
|
530
580
|
*/
|
|
531
581
|
readonly stackHash: string;
|
|
582
|
+
/**
|
|
583
|
+
* When set, the Firehose transform Lambda publishes qualifying changes to
|
|
584
|
+
* this bus via PutEvents (ADR 2026-03-02-01).
|
|
585
|
+
*/
|
|
586
|
+
readonly dataEventBus?: events.IEventBus;
|
|
532
587
|
}
|
|
533
588
|
/**
|
|
534
589
|
* DynamoDB change stream → Kinesis → Firehose → S3 with a transform Lambda for
|
|
535
|
-
* scope filtering and dynamic partitioning (ADR 2026-03-11-02).
|
|
590
|
+
* scope filtering and dynamic partitioning (ADR 2026-03-11-02). The same Lambda
|
|
591
|
+
* publishes qualifying current-resource changes to the data event bus (ADR 2026-03-02-01)
|
|
592
|
+
* when {@link DataStoreHistoricalArchiveProps.dataEventBus} is set.
|
|
536
593
|
*/
|
|
537
594
|
declare class DataStoreHistoricalArchive extends Construct {
|
|
538
595
|
readonly archiveBucket: s3.Bucket;
|
|
596
|
+
/**
|
|
597
|
+
* Receives PutEvents payloads that still fail after in-Lambda retries when
|
|
598
|
+
* {@link DataStoreHistoricalArchiveProps.dataEventBus} is configured.
|
|
599
|
+
*/
|
|
600
|
+
readonly putEventsFailureDlqBucket?: s3.Bucket;
|
|
539
601
|
readonly deliveryStream: kinesisfirehose.IDeliveryStream;
|
|
540
602
|
readonly transformFunction: NodejsFunction;
|
|
541
603
|
constructor(scope: Construct, id: string, props: DataStoreHistoricalArchiveProps);
|
|
@@ -1012,7 +1074,8 @@ declare class OpenHiDataService extends OpenHiService {
|
|
|
1012
1074
|
*/
|
|
1013
1075
|
readonly dataStoreChangeStream: kinesis.IStream;
|
|
1014
1076
|
/**
|
|
1015
|
-
*
|
|
1077
|
+
* Historical archive pipeline (Kinesis → Firehose → S3) and data-event-bus
|
|
1078
|
+
* notifications for current FHIR resources (ADRs 2026-03-11-02, 2026-03-02-01).
|
|
1016
1079
|
*/
|
|
1017
1080
|
readonly dataStoreHistoricalArchive: DataStoreHistoricalArchive;
|
|
1018
1081
|
constructor(ohEnv: OpenHiEnvironment, props?: OpenHiDataServiceProps);
|
|
@@ -1055,5 +1118,5 @@ declare class OpenHiGraphqlService extends OpenHiService {
|
|
|
1055
1118
|
protected createRootGraphqlApi(): RootGraphqlApi;
|
|
1056
1119
|
}
|
|
1057
1120
|
|
|
1058
|
-
export { ChildHostedZone, CognitoUserPool, CognitoUserPoolClient, CognitoUserPoolDomain, CognitoUserPoolKmsKey, DataEventBus, DataStoreHistoricalArchive, DiscoverableStringParameter, DynamoDbDataStore, OpenHiApp, OpenHiAuthService, OpenHiDataService, OpenHiEnvironment, OpenHiGlobalService, OpenHiGraphqlService, OpenHiRestApiService, OpenHiService, OpenHiStage, OpsEventBus, PreTokenGenerationLambda, REST_API_BASE_URL_SSM_NAME, RootGraphqlApi, RootHostedZone, RootHttpApi, RootWildcardCertificate, STATIC_HOSTING_SERVICE_TYPE, StaticHosting, getDynamoDbDataStoreTableName };
|
|
1059
|
-
export type { BuildParameterNameProps, ChildHostedZoneProps, DataStoreHistoricalArchiveProps, DiscoverableStringParameterProps, DynamoDbDataStoreProps, OpenHiAppProps, OpenHiAuthServiceProps, OpenHiDataServiceProps, OpenHiEnvironmentProps, OpenHiGlobalServiceProps, OpenHiGraphqlServiceProps, OpenHiRestApiServiceProps, OpenHiServiceProps, OpenHiServiceType, OpenHiStageProps, RootGraphqlApiProps, RootHttpApiProps, StaticHostingProps };
|
|
1121
|
+
export { ChildHostedZone, CognitoUserPool, CognitoUserPoolClient, CognitoUserPoolDomain, CognitoUserPoolKmsKey, DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES, DATA_STORE_CHANGE_DETAIL_TYPE, DATA_STORE_CHANGE_EVENT_SOURCE, DataEventBus, DataStoreHistoricalArchive, DiscoverableStringParameter, DynamoDbDataStore, OpenHiApp, OpenHiAuthService, OpenHiDataService, OpenHiEnvironment, OpenHiGlobalService, OpenHiGraphqlService, OpenHiRestApiService, OpenHiService, OpenHiStage, OpsEventBus, PreTokenGenerationLambda, REST_API_BASE_URL_SSM_NAME, RootGraphqlApi, RootHostedZone, RootHttpApi, RootWildcardCertificate, STATIC_HOSTING_SERVICE_TYPE, StaticHosting, buildFhirCurrentResourceChangeDetail, getDynamoDbDataStoreTableName };
|
|
1122
|
+
export type { BuildParameterNameProps, ChildHostedZoneProps, DataStoreHistoricalArchiveProps, DiscoverableStringParameterProps, DynamoDbDataStoreProps, FhirCurrentResourceChangeDetail, OpenHiAppProps, OpenHiAuthServiceProps, OpenHiDataServiceProps, OpenHiEnvironmentProps, OpenHiGlobalServiceProps, OpenHiGraphqlServiceProps, OpenHiRestApiServiceProps, OpenHiServiceProps, OpenHiServiceType, OpenHiStageProps, RootGraphqlApiProps, RootHttpApiProps, StaticHostingProps };
|