@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/components/dynamodb/dynamodb-stream-record.ts
|
|
2
|
+
function dynamodbValueToJs(av) {
|
|
3
|
+
if (av.S !== void 0) {
|
|
4
|
+
return av.S;
|
|
5
|
+
}
|
|
6
|
+
if (av.N !== void 0) {
|
|
7
|
+
return av.N.includes(".") ? Number.parseFloat(av.N) : Number.parseInt(av.N, 10);
|
|
8
|
+
}
|
|
9
|
+
if (av.BOOL !== void 0) {
|
|
10
|
+
return av.BOOL;
|
|
11
|
+
}
|
|
12
|
+
if (av.NULL !== void 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (av.M !== void 0) {
|
|
16
|
+
return dynamodbImageToPlain(av.M);
|
|
17
|
+
}
|
|
18
|
+
if (av.L !== void 0) {
|
|
19
|
+
return av.L.map((x) => dynamodbValueToJs(x));
|
|
20
|
+
}
|
|
21
|
+
if (av.SS !== void 0) {
|
|
22
|
+
return av.SS;
|
|
23
|
+
}
|
|
24
|
+
if (av.NS !== void 0) {
|
|
25
|
+
return av.NS.map(
|
|
26
|
+
(n) => n.includes(".") ? Number.parseFloat(n) : Number.parseInt(n, 10)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
function dynamodbImageToPlain(image) {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [k, v] of Object.entries(image)) {
|
|
34
|
+
out[k] = dynamodbValueToJs(v);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/components/dynamodb/data-store-change-events.ts
|
|
40
|
+
var DATA_STORE_CHANGE_EVENT_SOURCE = "openhi.data.store";
|
|
41
|
+
var DATA_STORE_CHANGE_DETAIL_TYPE = "FhirCurrentResourceChanged";
|
|
42
|
+
var DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES = 250 * 1024;
|
|
43
|
+
var EXCLUDED_CHANGE_DETAIL_KEYS = /* @__PURE__ */ new Set([
|
|
44
|
+
"PK",
|
|
45
|
+
"SK",
|
|
46
|
+
"GSI1PK",
|
|
47
|
+
"GSI1SK",
|
|
48
|
+
"GSI2PK",
|
|
49
|
+
"GSI2SK",
|
|
50
|
+
"GSI3PK",
|
|
51
|
+
"GSI3SK",
|
|
52
|
+
"GSI4PK",
|
|
53
|
+
"GSI4SK",
|
|
54
|
+
/** Full FHIR JSON may contain PII; never list or ship in the bus payload. */
|
|
55
|
+
"resource"
|
|
56
|
+
]);
|
|
57
|
+
function shallowValueEqual(a, b) {
|
|
58
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
59
|
+
}
|
|
60
|
+
function changedNonResourceAttributeNames(oldImage, newImage) {
|
|
61
|
+
if (!oldImage || !newImage) {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
const names = /* @__PURE__ */ new Set();
|
|
65
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(oldImage), ...Object.keys(newImage)]);
|
|
66
|
+
for (const k of keys) {
|
|
67
|
+
if (EXCLUDED_CHANGE_DETAIL_KEYS.has(k)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!shallowValueEqual(oldImage[k], newImage[k])) {
|
|
71
|
+
names.add(k);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return names.size > 0 ? [...names].sort() : void 0;
|
|
75
|
+
}
|
|
76
|
+
function presentMetadataAttributeNames(image) {
|
|
77
|
+
if (!image) {
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
const names = Object.keys(image).filter(
|
|
81
|
+
(k) => !EXCLUDED_CHANGE_DETAIL_KEYS.has(k)
|
|
82
|
+
);
|
|
83
|
+
return names.length > 0 ? names.sort() : void 0;
|
|
84
|
+
}
|
|
85
|
+
function plainImage(image) {
|
|
86
|
+
if (!image) {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
return dynamodbImageToPlain(image);
|
|
90
|
+
}
|
|
91
|
+
function buildFhirCurrentResourceChangeDetail(record, keys) {
|
|
92
|
+
const rawName = record.eventName;
|
|
93
|
+
const changeType = rawName === "INSERT" || rawName === "MODIFY" || rawName === "REMOVE" ? rawName : "MODIFY";
|
|
94
|
+
const seq = record.dynamodb?.SequenceNumber;
|
|
95
|
+
const approxEpochSec = record.dynamodb?.ApproximateCreationDateTime;
|
|
96
|
+
const newPlain = plainImage(
|
|
97
|
+
record.dynamodb?.NewImage
|
|
98
|
+
);
|
|
99
|
+
const oldPlain = plainImage(
|
|
100
|
+
record.dynamodb?.OldImage
|
|
101
|
+
);
|
|
102
|
+
let changedAttributeNames;
|
|
103
|
+
if (changeType === "MODIFY") {
|
|
104
|
+
changedAttributeNames = changedNonResourceAttributeNames(
|
|
105
|
+
oldPlain,
|
|
106
|
+
newPlain
|
|
107
|
+
);
|
|
108
|
+
} else if (changeType === "INSERT") {
|
|
109
|
+
changedAttributeNames = presentMetadataAttributeNames(newPlain);
|
|
110
|
+
} else {
|
|
111
|
+
changedAttributeNames = presentMetadataAttributeNames(oldPlain);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
changeType,
|
|
115
|
+
tenantId: keys.tenantId,
|
|
116
|
+
workspaceId: keys.workspaceId,
|
|
117
|
+
resourceType: keys.resourceType,
|
|
118
|
+
resourceId: keys.resourceId,
|
|
119
|
+
resourceVersion: keys.version,
|
|
120
|
+
...typeof seq === "string" && seq.length > 0 ? { streamSequenceNumber: seq } : {},
|
|
121
|
+
...typeof approxEpochSec === "number" && Number.isFinite(approxEpochSec) ? { approximateCreationEpochSec: approxEpochSec } : {},
|
|
122
|
+
...changedAttributeNames ? { changedAttributeNames } : {}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
dynamodbImageToPlain,
|
|
128
|
+
DATA_STORE_CHANGE_EVENT_SOURCE,
|
|
129
|
+
DATA_STORE_CHANGE_DETAIL_TYPE,
|
|
130
|
+
DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
|
|
131
|
+
buildFhirCurrentResourceChangeDetail
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=chunk-SWSN6GDD.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 * @see docs/architecture/adr/2026-03-02-01-dynamodb-stream-to-data-event-bus.md\n */\nexport const DATA_STORE_CHANGE_EVENT_SOURCE = \"openhi.data.store\";\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 \"GSI3PK\",\n \"GSI3SK\",\n \"GSI4PK\",\n \"GSI4SK\",\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 /** Seconds since UNIX epoch (DynamoDB Streams `ApproximateCreationDateTime`). */\n approximateCreationEpochSec?: 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 approxEpochSec = 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 approxEpochSec === \"number\" && Number.isFinite(approxEpochSec)\n ? { approximateCreationEpochSec: approxEpochSec }\n : {}),\n ...(changedAttributeNames ? { changedAttributeNames } : {}),\n };\n}\n"],"mappings":";AAkBO,SAAS,kBAAkB,IAA6B;AAC7D,MAAI,GAAG,MAAM,QAAW;AACtB,WAAO,GAAG;AAAA,EACZ;AACA,MAAI,GAAG,MAAM,QAAW;AACtB,WAAO,GAAG,EAAE,SAAS,GAAG,IACpB,OAAO,WAAW,GAAG,CAAC,IACtB,OAAO,SAAS,GAAG,GAAG,EAAE;AAAA,EAC9B;AACA,MAAI,GAAG,SAAS,QAAW;AACzB,WAAO,GAAG;AAAA,EACZ;AACA,MAAI,GAAG,SAAS,QAAW;AACzB,WAAO;AAAA,EACT;AACA,MAAI,GAAG,MAAM,QAAW;AACtB,WAAO,qBAAqB,GAAG,CAAC;AAAA,EAClC;AACA,MAAI,GAAG,MAAM,QAAW;AACtB,WAAO,GAAG,EAAE,IAAI,CAAC,MAAsB,kBAAkB,CAAC,CAAC;AAAA,EAC7D;AACA,MAAI,GAAG,OAAO,QAAW;AACvB,WAAO,GAAG;AAAA,EACZ;AACA,MAAI,GAAG,OAAO,QAAW;AACvB,WAAO,GAAG,GAAG;AAAA,MAAI,CAAC,MAChB,EAAE,SAAS,GAAG,IAAI,OAAO,WAAW,CAAC,IAAI,OAAO,SAAS,GAAG,EAAE;AAAA,IAChE;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,qBACd,OACyB;AACzB,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1C,QAAI,CAAC,IAAI,kBAAkB,CAAC;AAAA,EAC9B;AACA,SAAO;AACT;;;AClDO,IAAM,iCAAiC;AAEvC,IAAM,gCAAgC;AAGtC,IAAM,0CAA0C,MAAM;AAE7D,IAAM,8BAA8B,oBAAI,IAAI;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AACF,CAAC;AAED,SAAS,kBAAkB,GAAY,GAAqB;AAC1D,SAAO,KAAK,UAAU,CAAC,MAAM,KAAK,UAAU,CAAC;AAC/C;AAEA,SAAS,iCACP,UACA,UACsB;AACtB,MAAI,CAAC,YAAY,CAAC,UAAU;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,oBAAI,IAAY;AAC9B,QAAM,OAAO,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,QAAQ,GAAG,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC;AACzE,aAAW,KAAK,MAAM;AACpB,QAAI,4BAA4B,IAAI,CAAC,GAAG;AACtC;AAAA,IACF;AACA,QAAI,CAAC,kBAAkB,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC,GAAG;AAChD,YAAM,IAAI,CAAC;AAAA,IACb;AAAA,EACF;AACA,SAAO,MAAM,OAAO,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,IAAI;AAC9C;AAGA,SAAS,8BACP,OACsB;AACtB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,OAAO,KAAK,KAAK,EAAE;AAAA,IAC/B,CAAC,MAAM,CAAC,4BAA4B,IAAI,CAAC;AAAA,EAC3C;AACA,SAAO,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI;AAC3C;AAEA,SAAS,WACP,OACqC;AACrC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,SAAO,qBAAqB,KAAK;AACnC;AAmBO,SAAS,qCACd,QACA,MAOiC;AACjC,QAAM,UAAU,OAAO;AACvB,QAAM,aACJ,YAAY,YAAY,YAAY,YAAY,YAAY,WACxD,UACA;AAEN,QAAM,MAAM,OAAO,UAAU;AAC7B,QAAM,iBAAiB,OAAO,UAAU;AAExC,QAAM,WAAW;AAAA,IACf,OAAO,UAAU;AAAA,EACnB;AACA,QAAM,WAAW;AAAA,IACf,OAAO,UAAU;AAAA,EACnB;AAEA,MAAI;AACJ,MAAI,eAAe,UAAU;AAC3B,4BAAwB;AAAA,MACtB;AAAA,MACA;AAAA,IACF;AAAA,EACF,WAAW,eAAe,UAAU;AAClC,4BAAwB,8BAA8B,QAAQ;AAAA,EAChE,OAAO;AACL,4BAAwB,8BAA8B,QAAQ;AAAA,EAChE;AAEA,SAAO;AAAA,IACL;AAAA,IACA,UAAU,KAAK;AAAA,IACf,aAAa,KAAK;AAAA,IAClB,cAAc,KAAK;AAAA,IACnB,YAAY,KAAK;AAAA,IACjB,iBAAiB,KAAK;AAAA,IACtB,GAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,IACxC,EAAE,sBAAsB,IAAI,IAC5B,CAAC;AAAA,IACL,GAAI,OAAO,mBAAmB,YAAY,OAAO,SAAS,cAAc,IACpE,EAAE,6BAA6B,eAAe,IAC9C,CAAC;AAAA,IACL,GAAI,wBAAwB,EAAE,sBAAsB,IAAI,CAAC;AAAA,EAC3D;AACF;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AttributeValue } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shape of a DynamoDB change record as delivered inside Kinesis (table stream
|
|
5
|
+
* destination) and decoded by the Firehose transform Lambda.
|
|
6
|
+
*/
|
|
7
|
+
interface DynamoDbStreamKinesisRecord {
|
|
8
|
+
eventName?: string;
|
|
9
|
+
userIdentity?: unknown;
|
|
10
|
+
dynamodb?: {
|
|
11
|
+
Keys?: Record<string, AttributeValue>;
|
|
12
|
+
NewImage?: Record<string, AttributeValue>;
|
|
13
|
+
OldImage?: Record<string, AttributeValue>;
|
|
14
|
+
SequenceNumber?: string;
|
|
15
|
+
ApproximateCreationDateTime?: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type { DynamoDbStreamKinesisRecord as D };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AttributeValue } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shape of a DynamoDB change record as delivered inside Kinesis (table stream
|
|
5
|
+
* destination) and decoded by the Firehose transform Lambda.
|
|
6
|
+
*/
|
|
7
|
+
interface DynamoDbStreamKinesisRecord {
|
|
8
|
+
eventName?: string;
|
|
9
|
+
userIdentity?: unknown;
|
|
10
|
+
dynamodb?: {
|
|
11
|
+
Keys?: Record<string, AttributeValue>;
|
|
12
|
+
NewImage?: Record<string, AttributeValue>;
|
|
13
|
+
OldImage?: Record<string, AttributeValue>;
|
|
14
|
+
SequenceNumber?: string;
|
|
15
|
+
ApproximateCreationDateTime?: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type { DynamoDbStreamKinesisRecord as D };
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import { AttributeValue } from '@aws-sdk/client-dynamodb';
|
|
2
1
|
import { FirehoseTransformationEvent, FirehoseTransformationResult } from 'aws-lambda';
|
|
2
|
+
import { D as DynamoDbStreamKinesisRecord } from './dynamodb-stream-record-CJtV6a1t.mjs';
|
|
3
|
+
import '@aws-sdk/client-dynamodb';
|
|
3
4
|
|
|
4
|
-
interface DynamoDbStreamKinesisRecord {
|
|
5
|
-
eventName?: string;
|
|
6
|
-
userIdentity?: unknown;
|
|
7
|
-
dynamodb?: {
|
|
8
|
-
Keys?: Record<string, AttributeValue>;
|
|
9
|
-
NewImage?: Record<string, AttributeValue>;
|
|
10
|
-
OldImage?: Record<string, AttributeValue>;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
5
|
/**
|
|
14
6
|
* Returns true when this stream/Kinesis record should not be archived because it
|
|
15
7
|
* represents a **replica-side application** of a global-table change (the logical
|
|
@@ -39,4 +31,4 @@ declare function parseCurrentResourceKeys(record: DynamoDbStreamKinesisRecord):
|
|
|
39
31
|
} | null;
|
|
40
32
|
declare function handler(event: FirehoseTransformationEvent): Promise<FirehoseTransformationResult>;
|
|
41
33
|
|
|
42
|
-
export {
|
|
34
|
+
export { DynamoDbStreamKinesisRecord, handler, parseCurrentResourceKeys, shouldDropAsGlobalTableReplicationRecord };
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import { AttributeValue } from '@aws-sdk/client-dynamodb';
|
|
2
1
|
import { FirehoseTransformationEvent, FirehoseTransformationResult } from 'aws-lambda';
|
|
2
|
+
import { D as DynamoDbStreamKinesisRecord } from './dynamodb-stream-record-CJtV6a1t.js';
|
|
3
|
+
import '@aws-sdk/client-dynamodb';
|
|
3
4
|
|
|
4
|
-
interface DynamoDbStreamKinesisRecord {
|
|
5
|
-
eventName?: string;
|
|
6
|
-
userIdentity?: unknown;
|
|
7
|
-
dynamodb?: {
|
|
8
|
-
Keys?: Record<string, AttributeValue>;
|
|
9
|
-
NewImage?: Record<string, AttributeValue>;
|
|
10
|
-
OldImage?: Record<string, AttributeValue>;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
5
|
/**
|
|
14
6
|
* Returns true when this stream/Kinesis record should not be archived because it
|
|
15
7
|
* represents a **replica-side application** of a global-table change (the logical
|
|
@@ -39,4 +31,4 @@ declare function parseCurrentResourceKeys(record: DynamoDbStreamKinesisRecord):
|
|
|
39
31
|
} | null;
|
|
40
32
|
declare function handler(event: FirehoseTransformationEvent): Promise<FirehoseTransformationResult>;
|
|
41
33
|
|
|
42
|
-
export {
|
|
34
|
+
export { DynamoDbStreamKinesisRecord, handler, parseCurrentResourceKeys, shouldDropAsGlobalTableReplicationRecord };
|
|
@@ -25,6 +25,11 @@ __export(firehose_archive_transform_handler_exports, {
|
|
|
25
25
|
shouldDropAsGlobalTableReplicationRecord: () => shouldDropAsGlobalTableReplicationRecord
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(firehose_archive_transform_handler_exports);
|
|
28
|
+
var import_node_crypto = require("crypto");
|
|
29
|
+
var import_client_eventbridge = require("@aws-sdk/client-eventbridge");
|
|
30
|
+
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
31
|
+
|
|
32
|
+
// src/components/dynamodb/dynamodb-stream-record.ts
|
|
28
33
|
function dynamodbValueToJs(av) {
|
|
29
34
|
if (av.S !== void 0) {
|
|
30
35
|
return av.S;
|
|
@@ -61,6 +66,95 @@ function dynamodbImageToPlain(image) {
|
|
|
61
66
|
}
|
|
62
67
|
return out;
|
|
63
68
|
}
|
|
69
|
+
|
|
70
|
+
// src/components/dynamodb/data-store-change-events.ts
|
|
71
|
+
var DATA_STORE_CHANGE_EVENT_SOURCE = "openhi.data.store";
|
|
72
|
+
var DATA_STORE_CHANGE_DETAIL_TYPE = "FhirCurrentResourceChanged";
|
|
73
|
+
var DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES = 250 * 1024;
|
|
74
|
+
var EXCLUDED_CHANGE_DETAIL_KEYS = /* @__PURE__ */ new Set([
|
|
75
|
+
"PK",
|
|
76
|
+
"SK",
|
|
77
|
+
"GSI1PK",
|
|
78
|
+
"GSI1SK",
|
|
79
|
+
"GSI2PK",
|
|
80
|
+
"GSI2SK",
|
|
81
|
+
"GSI3PK",
|
|
82
|
+
"GSI3SK",
|
|
83
|
+
"GSI4PK",
|
|
84
|
+
"GSI4SK",
|
|
85
|
+
/** Full FHIR JSON may contain PII; never list or ship in the bus payload. */
|
|
86
|
+
"resource"
|
|
87
|
+
]);
|
|
88
|
+
function shallowValueEqual(a, b) {
|
|
89
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
90
|
+
}
|
|
91
|
+
function changedNonResourceAttributeNames(oldImage, newImage) {
|
|
92
|
+
if (!oldImage || !newImage) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
const names = /* @__PURE__ */ new Set();
|
|
96
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(oldImage), ...Object.keys(newImage)]);
|
|
97
|
+
for (const k of keys) {
|
|
98
|
+
if (EXCLUDED_CHANGE_DETAIL_KEYS.has(k)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!shallowValueEqual(oldImage[k], newImage[k])) {
|
|
102
|
+
names.add(k);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return names.size > 0 ? [...names].sort() : void 0;
|
|
106
|
+
}
|
|
107
|
+
function presentMetadataAttributeNames(image) {
|
|
108
|
+
if (!image) {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
const names = Object.keys(image).filter(
|
|
112
|
+
(k) => !EXCLUDED_CHANGE_DETAIL_KEYS.has(k)
|
|
113
|
+
);
|
|
114
|
+
return names.length > 0 ? names.sort() : void 0;
|
|
115
|
+
}
|
|
116
|
+
function plainImage(image) {
|
|
117
|
+
if (!image) {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
return dynamodbImageToPlain(image);
|
|
121
|
+
}
|
|
122
|
+
function buildFhirCurrentResourceChangeDetail(record, keys) {
|
|
123
|
+
const rawName = record.eventName;
|
|
124
|
+
const changeType = rawName === "INSERT" || rawName === "MODIFY" || rawName === "REMOVE" ? rawName : "MODIFY";
|
|
125
|
+
const seq = record.dynamodb?.SequenceNumber;
|
|
126
|
+
const approxEpochSec = record.dynamodb?.ApproximateCreationDateTime;
|
|
127
|
+
const newPlain = plainImage(
|
|
128
|
+
record.dynamodb?.NewImage
|
|
129
|
+
);
|
|
130
|
+
const oldPlain = plainImage(
|
|
131
|
+
record.dynamodb?.OldImage
|
|
132
|
+
);
|
|
133
|
+
let changedAttributeNames;
|
|
134
|
+
if (changeType === "MODIFY") {
|
|
135
|
+
changedAttributeNames = changedNonResourceAttributeNames(
|
|
136
|
+
oldPlain,
|
|
137
|
+
newPlain
|
|
138
|
+
);
|
|
139
|
+
} else if (changeType === "INSERT") {
|
|
140
|
+
changedAttributeNames = presentMetadataAttributeNames(newPlain);
|
|
141
|
+
} else {
|
|
142
|
+
changedAttributeNames = presentMetadataAttributeNames(oldPlain);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
changeType,
|
|
146
|
+
tenantId: keys.tenantId,
|
|
147
|
+
workspaceId: keys.workspaceId,
|
|
148
|
+
resourceType: keys.resourceType,
|
|
149
|
+
resourceId: keys.resourceId,
|
|
150
|
+
resourceVersion: keys.version,
|
|
151
|
+
...typeof seq === "string" && seq.length > 0 ? { streamSequenceNumber: seq } : {},
|
|
152
|
+
...typeof approxEpochSec === "number" && Number.isFinite(approxEpochSec) ? { approximateCreationEpochSec: approxEpochSec } : {},
|
|
153
|
+
...changedAttributeNames ? { changedAttributeNames } : {}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/components/dynamodb/firehose-archive-transform.handler.ts
|
|
64
158
|
var CURRENT_SK = "CURRENT";
|
|
65
159
|
var PK_PATTERN = /^TID#(?<tenantId>[^#]+)#WID#(?<workspaceId>[^#]+)#RT#(?<resourceType>[^#]+)#ID#(?<resourceId>.+)$/;
|
|
66
160
|
var AWS_REP_UPDATE_REGION = "aws:rep:updateregion";
|
|
@@ -160,9 +254,131 @@ function buildArchivePayload(record, keys) {
|
|
|
160
254
|
resource: resourcePlain
|
|
161
255
|
};
|
|
162
256
|
}
|
|
163
|
-
|
|
257
|
+
var PUT_EVENTS_BATCH_SIZE = 10;
|
|
258
|
+
var MAX_PUT_EVENTS_ROUNDS = 3;
|
|
259
|
+
var eventBridgeClient;
|
|
260
|
+
function getEventBridgeClient() {
|
|
261
|
+
const bus = process.env.DATA_EVENT_BUS_NAME?.trim();
|
|
262
|
+
if (!bus) {
|
|
263
|
+
return void 0;
|
|
264
|
+
}
|
|
265
|
+
if (!eventBridgeClient) {
|
|
266
|
+
eventBridgeClient = new import_client_eventbridge.EventBridgeClient({});
|
|
267
|
+
}
|
|
268
|
+
return eventBridgeClient;
|
|
269
|
+
}
|
|
270
|
+
var s3ClientForDlq;
|
|
271
|
+
function getS3ClientForDlq() {
|
|
272
|
+
const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();
|
|
273
|
+
if (!bucket) {
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
if (!s3ClientForDlq) {
|
|
277
|
+
s3ClientForDlq = new import_client_s3.S3Client({});
|
|
278
|
+
}
|
|
279
|
+
return s3ClientForDlq;
|
|
280
|
+
}
|
|
281
|
+
async function writePutEventsFailuresToDlq(payload) {
|
|
282
|
+
const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();
|
|
283
|
+
const client = getS3ClientForDlq();
|
|
284
|
+
if (!bucket || !client) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`PutEvents exhausted retries but DATA_STORE_PUT_EVENTS_DLQ_BUCKET is not set (${payload.reason})`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const day = payload.failedAt.slice(0, 10);
|
|
290
|
+
const key = `put-events-failed/${day}/${(0, import_node_crypto.randomUUID)()}.json`;
|
|
291
|
+
await client.send(
|
|
292
|
+
new import_client_s3.PutObjectCommand({
|
|
293
|
+
Bucket: bucket,
|
|
294
|
+
Key: key,
|
|
295
|
+
Body: JSON.stringify(payload),
|
|
296
|
+
ContentType: "application/json"
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
async function putEventsChunkWithRetriesAndDlq(client, entries) {
|
|
301
|
+
if (entries.length === 0) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
let pending = [...entries];
|
|
305
|
+
for (let round = 1; round <= MAX_PUT_EVENTS_ROUNDS; round++) {
|
|
306
|
+
try {
|
|
307
|
+
const out = await client.send(new import_client_eventbridge.PutEventsCommand({ Entries: pending }));
|
|
308
|
+
const failed = out.FailedEntryCount ?? 0;
|
|
309
|
+
if (failed === 0) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const nextPending = [];
|
|
313
|
+
out.Entries?.forEach((e, i) => {
|
|
314
|
+
if (e?.ErrorCode && pending[i]) {
|
|
315
|
+
nextPending.push(pending[i]);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
pending = nextPending;
|
|
319
|
+
if (pending.length === 0) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (round === MAX_PUT_EVENTS_ROUNDS) {
|
|
323
|
+
await writePutEventsFailuresToDlq({
|
|
324
|
+
dlqSchemaVersion: 1,
|
|
325
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
326
|
+
reason: "put_events_partial_failure",
|
|
327
|
+
attemptRounds: MAX_PUT_EVENTS_ROUNDS,
|
|
328
|
+
entries: pending,
|
|
329
|
+
putEventsResultEntries: out.Entries
|
|
330
|
+
});
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
} catch (sdkErr) {
|
|
334
|
+
const sdkMessage = sdkErr instanceof Error ? sdkErr.message : String(sdkErr);
|
|
335
|
+
if (round === MAX_PUT_EVENTS_ROUNDS) {
|
|
336
|
+
await writePutEventsFailuresToDlq({
|
|
337
|
+
dlqSchemaVersion: 1,
|
|
338
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
339
|
+
reason: "put_events_sdk_error",
|
|
340
|
+
attemptRounds: MAX_PUT_EVENTS_ROUNDS,
|
|
341
|
+
entries: pending,
|
|
342
|
+
sdkError: sdkMessage
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
await new Promise((r) => setTimeout(r, 50 * round));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function publishDataStoreChangeEvents(pending) {
|
|
351
|
+
const client = getEventBridgeClient();
|
|
352
|
+
const busName = process.env.DATA_EVENT_BUS_NAME?.trim();
|
|
353
|
+
if (!client || !busName || pending.length === 0) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const entries = [];
|
|
357
|
+
for (const { change, keys } of pending) {
|
|
358
|
+
const detailObj = buildFhirCurrentResourceChangeDetail(change, keys);
|
|
359
|
+
const detail = JSON.stringify(detailObj);
|
|
360
|
+
const detailBytes = Buffer.byteLength(detail, "utf8");
|
|
361
|
+
if (detailBytes > DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`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}).`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
entries.push({
|
|
367
|
+
Source: DATA_STORE_CHANGE_EVENT_SOURCE,
|
|
368
|
+
DetailType: DATA_STORE_CHANGE_DETAIL_TYPE,
|
|
369
|
+
Detail: detail,
|
|
370
|
+
EventBusName: busName
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
for (let i = 0; i < entries.length; i += PUT_EVENTS_BATCH_SIZE) {
|
|
374
|
+
const chunk = entries.slice(i, i + PUT_EVENTS_BATCH_SIZE);
|
|
375
|
+
await putEventsChunkWithRetriesAndDlq(client, chunk);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function handler(event) {
|
|
164
379
|
const records = [];
|
|
165
380
|
const archiveLambdaRegion = process.env.AWS_REGION ?? "";
|
|
381
|
+
const pendingPublish = [];
|
|
166
382
|
for (const rec of event.records) {
|
|
167
383
|
try {
|
|
168
384
|
const payload = Buffer.from(rec.data, "base64").toString("utf8");
|
|
@@ -189,6 +405,7 @@ function handler(event) {
|
|
|
189
405
|
`).toString(
|
|
190
406
|
"base64"
|
|
191
407
|
);
|
|
408
|
+
pendingPublish.push({ change, keys });
|
|
192
409
|
records.push({
|
|
193
410
|
recordId: rec.recordId,
|
|
194
411
|
result: "Ok",
|
|
@@ -211,7 +428,8 @@ function handler(event) {
|
|
|
211
428
|
});
|
|
212
429
|
}
|
|
213
430
|
}
|
|
214
|
-
|
|
431
|
+
await publishDataStoreChangeEvents(pendingPublish);
|
|
432
|
+
return { records };
|
|
215
433
|
}
|
|
216
434
|
// Annotate the CommonJS export names for ESM import in node:
|
|
217
435
|
0 && (module.exports = {
|
|
@@ -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","../src/components/dynamodb/dynamodb-stream-record.ts","../src/components/dynamodb/data-store-change-events.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","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 * @see docs/architecture/adr/2026-03-02-01-dynamodb-stream-to-data-event-bus.md\n */\nexport const DATA_STORE_CHANGE_EVENT_SOURCE = \"openhi.data.store\";\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 \"GSI3PK\",\n \"GSI3SK\",\n \"GSI4PK\",\n \"GSI4SK\",\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 /** Seconds since UNIX epoch (DynamoDB Streams `ApproximateCreationDateTime`). */\n approximateCreationEpochSec?: 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 approxEpochSec = 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 approxEpochSec === \"number\" && Number.isFinite(approxEpochSec)\n ? { approximateCreationEpochSec: approxEpochSec }\n : {}),\n ...(changedAttributeNames ? { changedAttributeNames } : {}),\n };\n}\n"],"mappings":"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","names":[]}
|