@openhi/constructs 0.0.150 → 0.0.152

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.
Files changed (39) hide show
  1. package/lib/{chunk-AWYZJFPL.mjs → chunk-CFJDATDK.mjs} +8 -1
  2. package/lib/chunk-CFJDATDK.mjs.map +1 -0
  3. package/lib/{chunk-I7IIPV5X.mjs → chunk-KR2Y2CVQ.mjs} +10 -4
  4. package/lib/chunk-KR2Y2CVQ.mjs.map +1 -0
  5. package/lib/{chunk-WGA43MMY.mjs → chunk-SXYY5WHG.mjs} +194 -71
  6. package/lib/chunk-SXYY5WHG.mjs.map +1 -0
  7. package/lib/{chunk-CEOAGPYY.mjs → chunk-ZXPA6W3G.mjs} +1 -3
  8. package/lib/chunk-ZXPA6W3G.mjs.map +1 -0
  9. package/lib/data-store-postgres-replication.handler.js +687 -0
  10. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  11. package/lib/data-store-postgres-replication.handler.mjs +3 -2
  12. package/lib/data-store-postgres-replication.handler.mjs.map +1 -1
  13. package/lib/{events-CMG8xanm.d.ts → events-DTgo2dcW.d.mts} +2 -14
  14. package/lib/{events-CMG8xanm.d.mts → events-DTgo2dcW.d.ts} +2 -14
  15. package/lib/firehose-archive-transform.handler.js +688 -2
  16. package/lib/firehose-archive-transform.handler.js.map +1 -1
  17. package/lib/firehose-archive-transform.handler.mjs +3 -2
  18. package/lib/index.d.mts +62 -8
  19. package/lib/index.d.ts +62 -20
  20. package/lib/index.js +53 -22
  21. package/lib/index.js.map +1 -1
  22. package/lib/index.mjs +36 -7
  23. package/lib/index.mjs.map +1 -1
  24. package/lib/provision-default-workspace.handler.js +6 -0
  25. package/lib/provision-default-workspace.handler.js.map +1 -1
  26. package/lib/provision-default-workspace.handler.mjs +1 -1
  27. package/lib/rest-api-lambda.handler.js +6 -0
  28. package/lib/rest-api-lambda.handler.js.map +1 -1
  29. package/lib/rest-api-lambda.handler.mjs +1 -1
  30. package/lib/seed-demo-data.handler.d.mts +14 -1
  31. package/lib/seed-demo-data.handler.d.ts +14 -1
  32. package/lib/seed-demo-data.handler.js +199 -68
  33. package/lib/seed-demo-data.handler.js.map +1 -1
  34. package/lib/seed-demo-data.handler.mjs +2 -2
  35. package/package.json +5 -5
  36. package/lib/chunk-AWYZJFPL.mjs.map +0 -1
  37. package/lib/chunk-CEOAGPYY.mjs.map +0 -1
  38. package/lib/chunk-I7IIPV5X.mjs.map +0 -1
  39. package/lib/chunk-WGA43MMY.mjs.map +0 -1
@@ -22,6 +22,9 @@ import {
22
22
  getDynamoControlService
23
23
  } from "./chunk-VZCPGQXA.mjs";
24
24
 
25
+ // src/data/operations/control/membership-constraints/platform-scope-tenant-id.ts
26
+ var PLATFORM_SCOPE_TENANT_ID = "platform";
27
+
25
28
  // src/data/operations/control/membership/membership-create-operation.ts
26
29
  import {
27
30
  assertLinkedDataIdentityCardinality,
@@ -170,6 +173,9 @@ import { extractSummary as extractSummary2 } from "@openhi/types";
170
173
  var TENANT_LANE_SK_PREFIX = "MEMBERSHIP#TENANT#";
171
174
  async function assertUserHasTenantMembershipOperation(params) {
172
175
  const { userId, tenantId, tableName } = params;
176
+ if (tenantId === PLATFORM_SCOPE_TENANT_ID) {
177
+ return;
178
+ }
173
179
  const service = getDynamoControlService(tableName);
174
180
  const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: TENANT_LANE_SK_PREFIX }).go();
175
181
  const matched = (result.data ?? []).some((row) => row.tenantId === tenantId);
@@ -380,10 +386,11 @@ async function createWorkspaceOperation(params) {
380
386
  }
381
387
 
382
388
  export {
389
+ PLATFORM_SCOPE_TENANT_ID,
383
390
  extractDenormalizedReferenceDisplay,
384
391
  createMembershipOperation,
385
392
  createRoleAssignmentOperation,
386
393
  createTenantOperation,
387
394
  createWorkspaceOperation
388
395
  };
389
- //# sourceMappingURL=chunk-AWYZJFPL.mjs.map
396
+ //# sourceMappingURL=chunk-CFJDATDK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data/operations/control/membership-constraints/platform-scope-tenant-id.ts","../src/data/operations/control/membership/membership-create-operation.ts","../src/data/operations/control/denormalized-display-names.ts","../src/data/operations/control/membership-constraints/assert-workspace-in-tenant-operation.ts","../src/data/operations/control/roleassignment/roleassignment-create-operation.ts","../src/data/operations/control/membership-constraints/assert-user-has-tenant-membership-operation.ts","../src/data/operations/control/tenant/tenant-create-operation.ts","../src/data/operations/control/workspace/workspace-create-operation.ts","../src/data/operations/data/organization/organization-provision-for-workspace-operation.ts"],"sourcesContent":["/**\n * Sentinel `tenantId` used on platform-scoped `RoleAssignment` records.\n * A platform-scoped RA (e.g. `system-admin`) spans every tenant, but\n * the `RoleAssignment` entity requires a `tenantId` on its key for\n * sharding — there is no real tenant to point at. The `\"platform\"`\n * literal is a reserved value that never matches a real `Tenant.id`\n * and signals \"this RA scopes across all tenants\".\n *\n * The constant lives in the data layer so both the\n * {@link assertUserHasTenantMembershipOperation} write-time constraint\n * and the seed-demo-data workflow can read it without the constraint\n * helper having to reach back into the workflow package — which would\n * create a backwards data-layer-to-workflow dependency. The\n * `workflows/control-plane/seed-demo-data/events.ts` module re-exports\n * the constant under the same name for back-compat with existing\n * imports that point at the workflow's barrel.\n *\n * Renaming this constant is a wire-format break — the seed-demo-data\n * handler emits `RoleAssignment` records keyed on this value, and the\n * in-band records written under it become unreachable if the sentinel\n * changes.\n */\nexport const PLATFORM_SCOPE_TENANT_ID = \"platform\";\n","import {\n assertLinkedDataIdentityCardinality,\n extractSummary,\n type Extension,\n type FhirResourceLike,\n LinkedDataIdentityCardinalityError,\n} from \"@openhi/types\";\nimport {\n buildMembershipUserProjectionItem,\n extractReferenceSlug,\n} from \"./membership-user-projection\";\nimport { buildMembershipWorkspaceProjectionItem } from \"./membership-workspace-projection\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ValidationError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport { extractDenormalizedReferenceDisplay } from \"../denormalized-display-names\";\nimport { assertWorkspaceInTenantOperation } from \"../membership-constraints/assert-workspace-in-tenant-operation\";\nimport {\n executeMultiWrite,\n type MultiWriteTriple,\n} from \"../multi-write-operation\";\n\nexport interface MembershipCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface MembershipCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createMembershipOperation(\n params: MembershipCreateParams,\n): Promise<MembershipCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `membership-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"Membership\", id, ...parsedResource };\n\n let linkedDataIdentityRef: string | undefined;\n try {\n const ext = assertLinkedDataIdentityCardinality(\n resource as { extension?: Array<Extension> },\n );\n linkedDataIdentityRef = ext?.valueReference?.reference;\n } catch (e) {\n if (e instanceof LinkedDataIdentityCardinalityError) {\n throw new ValidationError(e.message, { cause: e });\n }\n throw e;\n }\n\n // TR-024 denormalized display-name attributes. The authoritative\n // write-time source per TR-024 rule 2 is the canonical Tenant / User\n // record's `displayName`. Until the carrier-record-lookup pass lands\n // (#1010 follow-up), the foundational fallback (#1009) reads the\n // FHIR Reference.display values supplied on the resource so the\n // adjacency-list projection writer here can compose SKs from\n // top-level attributes.\n const resourceRecord = resource as Record<string, unknown>;\n const denormalizedTenantName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"tenant\",\n );\n const denormalizedUserName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"user\",\n );\n const denormalizedWorkspaceName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"workspace\",\n );\n\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n // ADR-018 patterns #2 / #3 / #4 — user- and workspace-partition\n // projection rows. The projection's discriminating fields come from\n // the same FHIR Reference shape the canonical row reads, so the\n // canonical and projection writes always agree on the underlying\n // user / workspace identifiers. Missing identifiers (no user\n // reference at all) skip the user-projection; tenant-scoped\n // Memberships (no workspaceId) skip the workspace-projection — the\n // canonical row still lands either way.\n const userIdFromResource = extractReferenceSlug(resourceRecord, \"user\");\n const workspaceIdFromResource = extractReferenceSlug(\n resourceRecord,\n \"workspace\",\n );\n\n // ADR 2026-03-13-02 § 2 Consequences/Negative — when the resource\n // names a workspace, that workspace must belong to the referenced\n // tenant. Prefer the resource's `tenant.reference` (the issue's\n // \"referenced tenant\" wording) and fall back to the context tenant\n // when the body omits the reference. The check fires before any\n // write so the constraint is enforced atomically with respect to\n // the canonical row.\n if (workspaceIdFromResource !== undefined) {\n const tenantIdFromResource = extractReferenceSlug(resourceRecord, \"tenant\");\n const referencedTenantId = tenantIdFromResource ?? context.tenantId;\n await assertWorkspaceInTenantOperation({\n tenantId: referencedTenantId,\n workspaceId: workspaceIdFromResource,\n tableName,\n });\n }\n\n const userProjectionItem =\n userIdFromResource !== undefined\n ? buildMembershipUserProjectionItem({\n tenantId: context.tenantId,\n userId: userIdFromResource,\n workspaceId: workspaceIdFromResource,\n membershipId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedWorkspaceName,\n })\n : undefined;\n\n const workspaceProjectionItem =\n userIdFromResource !== undefined && workspaceIdFromResource !== undefined\n ? buildMembershipWorkspaceProjectionItem({\n tenantId: context.tenantId,\n workspaceId: workspaceIdFromResource,\n userId: userIdFromResource,\n membershipId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedUserName,\n })\n : undefined;\n\n const canonicalItem = {\n tenantId: context.tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n linkedDataIdentityRef,\n denormalizedTenantName,\n denormalizedUserName,\n };\n\n const triples: Array<MultiWriteTriple> = [\n { entity: \"membership\", action: \"put\", item: canonicalItem },\n ];\n if (userProjectionItem) {\n triples.push({\n entity: \"membershipUserProjection\",\n action: \"put\",\n item: userProjectionItem as unknown as Record<string, unknown>,\n });\n }\n if (workspaceProjectionItem) {\n triples.push({\n entity: \"membershipWorkspaceProjection\",\n action: \"put\",\n item: workspaceProjectionItem as unknown as Record<string, unknown>,\n });\n }\n\n await executeMultiWrite({ service, triples });\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","/**\n * Helpers that capture denormalized display-name attributes from an\n * incoming FHIR Reference on Membership and RoleAssignment resources.\n *\n * **Foundational implementation for TR-024.** ADR-018 § Implementation\n * Notes and TR-024 § Recommendation pin the authoritative write-time\n * source as the carrier entity's canonical `displayName` — i.e. the\n * operations layer reads the canonical Tenant / User / Role record\n * by id and copies its display name into the relationship's\n * `denormalized<CarrierEntity>Name` field on the same\n * `TransactWriteItems`. That contract is owned by the operations-layer\n * multi-write helper filed as #1010.\n *\n * This module is the **foundational fallback** that issue #1009 uses\n * before #1010 lands: it reads the display string a client already\n * supplies on the resource's `Reference.display` field. The fallback\n * keeps Membership / RoleAssignment write paths populating the new\n * top-level attributes today (so adjacency-list projection writers\n * downstream can rely on them) while letting #1010 own the canonical-\n * row lookup without re-plumbing the write paths.\n *\n * @see TR-024 — Denormalized display-name attributes\n * @see ADR-018 § Implementation Notes\n */\n\n/**\n * Returns the trimmed display string from `resource[fieldName].display`\n * when present and non-empty; otherwise returns `undefined`. Used by the\n * Membership and RoleAssignment create / update operations to populate\n * top-level `denormalized<CarrierEntity>Name` attributes from incoming\n * FHIR Reference fields.\n *\n * Field name maps to the FHIR field on the resource:\n * - `Membership.tenant` → `\"tenant\"`\n * - `Membership.user` → `\"user\"`\n * - `RoleAssignment.tenant` → `\"tenant\"`\n * - `RoleAssignment.user` → `\"user\"`\n * - `RoleAssignment.role` → `\"role\"`\n *\n * Guards against malformed payloads (non-object `field`, non-string\n * `display`, empty strings after trim) so a single bad write never\n * blocks an entity put — matching the same defensive posture\n * `gsi1skAttribute` takes for the `resource` JSON parse.\n */\nexport function extractDenormalizedReferenceDisplay(\n resource: Record<string, unknown>,\n fieldName: string,\n): string | undefined {\n const field = resource[fieldName];\n if (!field || typeof field !== \"object\") {\n return undefined;\n }\n const display = (field as { display?: unknown }).display;\n if (typeof display !== \"string\") {\n return undefined;\n }\n const trimmed = display.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ConflictError } from \"../../../errors\";\n\n/** Inputs to {@link assertWorkspaceInTenantOperation}. */\nexport interface AssertWorkspaceInTenantParams {\n /** Tenant the workspace must belong to. */\n readonly tenantId: string;\n /** Workspace id that must exist under `tenantId`. */\n readonly workspaceId: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Throws {@link ConflictError} when no `Workspace` with id `workspaceId`\n * exists under `tenantId`.\n *\n * Implementation: a single base-table `get` against the Workspace entity\n * (`PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK = CURRENT`). The\n * workspace's tenant is encoded into the PK by construction, so a hit\n * *is* the constraint check — there is no separate `managingOrganization`\n * comparison required.\n *\n * @see ADR 2026-03-13-02 § 2 — Consequences/Negative\n */\nexport async function assertWorkspaceInTenantOperation(\n params: AssertWorkspaceInTenantParams,\n): Promise<void> {\n const { tenantId, workspaceId, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const { data: item } = await service.entities.workspace\n .get({ tenantId, id: workspaceId, sk: \"CURRENT\" })\n .go();\n\n if (!item) {\n throw new ConflictError(\n `Workspace ${workspaceId} does not belong to tenant ${tenantId}; the workspace must be created in the referenced tenant before this resource can reference it.`,\n { details: { tenantId, workspaceId } },\n );\n }\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport {\n buildRoleAssignmentUserProjectionItem,\n extractReferenceSlug,\n} from \"./roleassignment-user-projection\";\nimport { buildRoleAssignmentWorkspaceProjectionItem } from \"./roleassignment-workspace-projection\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport { extractDenormalizedReferenceDisplay } from \"../denormalized-display-names\";\nimport { assertUserHasTenantMembershipOperation } from \"../membership-constraints/assert-user-has-tenant-membership-operation\";\nimport { assertWorkspaceInTenantOperation } from \"../membership-constraints/assert-workspace-in-tenant-operation\";\nimport {\n executeMultiWrite,\n type MultiWriteTriple,\n} from \"../multi-write-operation\";\n\nexport interface RoleAssignmentCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface RoleAssignmentCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createRoleAssignmentOperation(\n params: RoleAssignmentCreateParams,\n): Promise<RoleAssignmentCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `roleassignment-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"RoleAssignment\", id, ...parsedResource };\n\n // TR-024 denormalized display-name attributes. The authoritative\n // write-time source per TR-024 rule 2 is the canonical Tenant / User /\n // Role record's `displayName`. Until the carrier-record-lookup pass\n // lands (#1010 follow-up), the foundational fallback (#1009) reads\n // the FHIR Reference.display values supplied on the resource so the\n // adjacency-list projection writer here can compose SKs from\n // top-level attributes.\n const resourceRecord = resource as Record<string, unknown>;\n const denormalizedTenantName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"tenant\",\n );\n const denormalizedUserName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"user\",\n );\n const denormalizedRoleName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"role\",\n );\n\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n // ADR-018 patterns #5 / #9 — user- and workspace-partition projection\n // rows. The projection's discriminating fields come from the same\n // FHIR Reference shape the canonical row reads, so the canonical and\n // projection writes always agree on the underlying user / role /\n // workspace identifiers. Missing user or role references skip the\n // user-projection; tenant-scoped RoleAssignments (no workspaceId)\n // skip the workspace-projection — the canonical row still lands\n // either way.\n const userIdFromResource = extractReferenceSlug(resourceRecord, \"user\");\n const roleIdFromResource = extractReferenceSlug(resourceRecord, \"role\");\n const workspaceIdFromResource = extractReferenceSlug(\n resourceRecord,\n \"workspace\",\n );\n\n // ADR 2026-03-13-02 § 2 Consequences/Negative — a RoleAssignment may\n // only be created when the referenced user already holds a\n // tenant-level Membership in the referenced tenant, and (when\n // workspace-scoped) that workspace must belong to the same tenant.\n // Prefer the resource's `tenant.reference` (the issue's \"referenced\n // tenant\" wording) and fall back to the context tenant when the\n // body omits the reference. Constraints fire before any write so\n // the rejection is atomic with respect to the canonical row.\n if (userIdFromResource !== undefined) {\n const tenantIdFromResource = extractReferenceSlug(resourceRecord, \"tenant\");\n const referencedTenantId = tenantIdFromResource ?? context.tenantId;\n await assertUserHasTenantMembershipOperation({\n userId: userIdFromResource,\n tenantId: referencedTenantId,\n tableName,\n });\n if (workspaceIdFromResource !== undefined) {\n await assertWorkspaceInTenantOperation({\n tenantId: referencedTenantId,\n workspaceId: workspaceIdFromResource,\n tableName,\n });\n }\n }\n\n const userProjectionItem =\n userIdFromResource !== undefined && roleIdFromResource !== undefined\n ? buildRoleAssignmentUserProjectionItem({\n tenantId: context.tenantId,\n userId: userIdFromResource,\n workspaceId: workspaceIdFromResource,\n roleId: roleIdFromResource,\n roleAssignmentId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedRoleName,\n })\n : undefined;\n\n const workspaceProjectionItem =\n userIdFromResource !== undefined &&\n roleIdFromResource !== undefined &&\n workspaceIdFromResource !== undefined\n ? buildRoleAssignmentWorkspaceProjectionItem({\n tenantId: context.tenantId,\n workspaceId: workspaceIdFromResource,\n userId: userIdFromResource,\n roleId: roleIdFromResource,\n roleAssignmentId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedUserName,\n denormalizedRoleName,\n })\n : undefined;\n\n const canonicalItem = {\n tenantId: context.tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedRoleName,\n };\n\n const triples: Array<MultiWriteTriple> = [\n { entity: \"roleAssignment\", action: \"put\", item: canonicalItem },\n ];\n if (userProjectionItem) {\n triples.push({\n entity: \"roleAssignmentUserProjection\",\n action: \"put\",\n item: userProjectionItem as unknown as Record<string, unknown>,\n });\n }\n if (workspaceProjectionItem) {\n triples.push({\n entity: \"roleAssignmentWorkspaceProjection\",\n action: \"put\",\n item: workspaceProjectionItem as unknown as Record<string, unknown>,\n });\n }\n\n await executeMultiWrite({ service, triples });\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { PLATFORM_SCOPE_TENANT_ID } from \"./platform-scope-tenant-id\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ConflictError } from \"../../../errors\";\n\n/**\n * SK prefix for the tenant-level Membership sub-lane (ADR-018 pattern #3).\n *\n * Tenant-level Memberships project under the user partition with\n * `SK = MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<membershipId>`.\n * Because the `<tenantId>` segment appears *after* `<normalizedTenantName>`,\n * a `begins_with` filter cannot narrow to a single tenant — we read every\n * tenant-lane row for the user and filter on the projection-row's\n * `tenantId` attribute in memory.\n */\nconst TENANT_LANE_SK_PREFIX = \"MEMBERSHIP#TENANT#\";\n\n/** Inputs to {@link assertUserHasTenantMembershipOperation}. */\nexport interface AssertUserHasTenantMembershipParams {\n /** The user that must already be a member of `tenantId`. */\n readonly userId: string;\n /** The tenant the user must hold a tenant-level Membership in. */\n readonly tenantId: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Throws {@link ConflictError} when `userId` has no tenant-level\n * `Membership` in `tenantId`.\n *\n * Implementation: queries the ADR-018 user-partition projection\n * (`MembershipUserProjectionEntity`) under\n * `PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#TENANT#'` — a single\n * strongly-consistent base-table query with no GSI hop — and filters the\n * returned rows for `tenantId` in memory.\n *\n * The filter cannot be expressed as an additional `begins_with` because\n * the projection SK encodes `<normalizedTenantName>` *before* `<tenantId>`\n * (`MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<id>`). The\n * scan cost is bounded by the user's tenant-membership fan-out, which is\n * small by construction.\n *\n * @see ADR 2026-03-13-02 § 2 — Consequences/Negative\n * @see ADR-018 § Access Pattern Coverage (pattern #3)\n */\nexport async function assertUserHasTenantMembershipOperation(\n params: AssertUserHasTenantMembershipParams,\n): Promise<void> {\n const { userId, tenantId, tableName } = params;\n\n // The platform-scope sentinel is not a real tenant — it is a\n // reserved `tenantId` value used on platform-scoped RoleAssignments\n // (e.g. `system-admin`). No `Membership` is ever created under it,\n // and a Membership there would have no semantic meaning. Skip the\n // tenant-membership constraint so platform-scoped RAs can be\n // created without requiring an impossible-to-create prerequisite\n // Membership row.\n if (tenantId === PLATFORM_SCOPE_TENANT_ID) {\n return;\n }\n\n const service = getDynamoControlService(tableName);\n\n const result = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: TENANT_LANE_SK_PREFIX })\n .go();\n\n const matched = (result.data ?? []).some((row) => row.tenantId === tenantId);\n if (!matched) {\n throw new ConflictError(\n `User ${userId} has no tenant-level Membership in tenant ${tenantId}; a Membership must exist before a RoleAssignment can be created.`,\n { details: { userId, tenantId } },\n );\n }\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface CreateTenantParams {\n context: OpenHiContext;\n body: {\n id?: string;\n resource?: Record<string, unknown> | string;\n };\n tableName?: string;\n}\n\nexport interface CreateTenantResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Creates a Tenant. Generates an id if not provided.\n */\nexport async function createTenantOperation(\n params: CreateTenantParams,\n): Promise<CreateTenantResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `tenant-${Date.now()}`;\n const lastUpdated = context.date;\n const vid =\n lastUpdated.replace(/[-:T.Z]/g, \"\").slice(0, 12) || Date.now().toString(36);\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const resource = { resourceType: \"Tenant\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.tenant\n .put({\n tenantId: id,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return { id, resource, meta: { lastUpdated, versionId: vid } };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport { provisionOrganizationForWorkspaceOperation } from \"../../data/organization/organization-provision-for-workspace-operation\";\n\nexport interface CreateWorkspaceParams {\n context: OpenHiContext;\n body: {\n id?: string;\n resource?: Record<string, unknown> | string;\n };\n tableName?: string;\n}\n\nexport interface CreateWorkspaceResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Creates a Workspace scoped to the context tenant. Generates an id if not provided.\n */\nexport async function createWorkspaceOperation(\n params: CreateWorkspaceParams,\n): Promise<CreateWorkspaceResult> {\n const { context, body, tableName } = params;\n const { tenantId } = context;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `workspace-${Date.now()}`;\n const lastUpdated = context.date;\n const vid =\n lastUpdated.replace(/[-:T.Z]/g, \"\").slice(0, 12) || Date.now().toString(36);\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const resource = { resourceType: \"Workspace\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.workspace\n .put({\n tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n const workspaceName =\n typeof parsedResource.name === \"string\" ? parsedResource.name : undefined;\n await provisionOrganizationForWorkspaceOperation({\n context,\n workspaceId: id,\n workspaceName,\n tableName,\n });\n\n return { id, resource, meta: { lastUpdated, versionId: vid } };\n}\n","import type { Organization, Reference } from \"@openhi/types\";\nimport { getDynamoDataService } from \"../../../dynamo/dynamo-data-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n createDataEntityRecord,\n DATA_ENTITY_SK,\n type SingleResourceResult,\n} from \"../../data-operations-common\";\n\n/**\n * Provision the slim, id-share Organization that represents a Workspace on the data plane.\n *\n * - `Organization.id === workspaceId` (id-share with the Workspace).\n * - `name` is populated from the Workspace's name when provided.\n * - `partOf` is populated when the Workspace's Tenant has an Organization (id-share at the\n * tenant level: `Tenant.id === TenantOrganization.id`). Tenant-level provisioning is out of\n * scope for #1001 — the lookup returns `undefined` until Tenant Organizations exist.\n *\n * Idempotent: re-running with the same params overwrites the same PK with the same payload.\n *\n * @see https://github.com/codedrifters/openhi/issues/1001\n */\nexport interface ProvisionOrganizationForWorkspaceParams {\n context: OpenHiContext;\n /** Workspace id; the provisioned Organization's id equals this value. */\n workspaceId: string;\n /** Name to record on the Organization. Derived from the Workspace's `name` by the caller. */\n workspaceName?: string;\n /** Optional table name override; resolved by data service from DYNAMO_TABLE_NAME when omitted. */\n tableName?: string;\n}\n\nexport type ProvisionOrganizationForWorkspaceResult =\n SingleResourceResult<Organization>;\n\nexport async function provisionOrganizationForWorkspaceOperation(\n params: ProvisionOrganizationForWorkspaceParams,\n): Promise<ProvisionOrganizationForWorkspaceResult> {\n const { context, workspaceId, workspaceName, tableName } = params;\n const { tenantId, date } = context;\n const service = getDynamoDataService(tableName);\n\n const partOf = await resolveTenantOrganizationReference(service, tenantId);\n\n const resource: Organization = {\n resourceType: \"Organization\",\n id: workspaceId,\n ...(workspaceName !== undefined && workspaceName !== \"\"\n ? { name: workspaceName }\n : {}),\n ...(partOf !== undefined ? { partOf } : {}),\n meta: {\n lastUpdated: date,\n versionId: \"1\",\n },\n };\n\n return createDataEntityRecord<Organization>(\n service.entities.organization as Parameters<\n typeof createDataEntityRecord\n >[0],\n tenantId,\n workspaceId,\n workspaceId,\n resource,\n date,\n );\n}\n\n/**\n * Resolve the Tenant's Organization reference for `Organization.partOf`. Returns `undefined`\n * when the Tenant has no Organization yet (the default until Tenant Organization provisioning\n * is wired up in a follow-up).\n *\n * Lookup convention: the Tenant's Organization is stored at\n * `(tenantId, workspaceId=tenantId, id=tenantId)` — a self-scope key that mirrors the\n * Workspace id-share pattern one level up. This scoping convention will be ratified by the\n * Tenant Organization provisioning issue; the OrganizationEntity comment notes strict\n * isolation requires both `tenantId` and `workspaceId`, so the self-scope is the\n * simplest deterministic location.\n */\nasync function resolveTenantOrganizationReference(\n service: ReturnType<typeof getDynamoDataService>,\n tenantId: string,\n): Promise<Reference | undefined> {\n const result = await (\n service.entities.organization as {\n get(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<{ data: unknown }> };\n }\n )\n .get({\n tenantId,\n workspaceId: tenantId,\n id: tenantId,\n sk: DATA_ENTITY_SK,\n })\n .go();\n\n if (result.data === null || result.data === undefined) {\n return undefined;\n }\n\n return { reference: `Organization/${tenantId}` };\n}\n"],"mappings":"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","names":["extractSummary","extractSummary","extractReferenceSlug","extractSummary","extractSummary","extractSummary","extractSummary"]}
@@ -1,12 +1,18 @@
1
1
  import {
2
2
  DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
3
3
  DATA_STORE_CHANGE_DETAIL_TYPE,
4
- DATA_STORE_CHANGE_EVENT_SOURCE,
5
4
  buildFhirCurrentResourceChangeDetail,
6
5
  dynamodbImageToPlain
7
- } from "./chunk-CEOAGPYY.mjs";
6
+ } from "./chunk-ZXPA6W3G.mjs";
7
+ import {
8
+ require_lib
9
+ } from "./chunk-ZM4GDHHC.mjs";
10
+ import {
11
+ __toESM
12
+ } from "./chunk-LZOMFHX3.mjs";
8
13
 
9
14
  // src/components/dynamodb/firehose-archive-transform.handler.ts
15
+ var import_workflows = __toESM(require_lib());
10
16
  import { randomUUID } from "crypto";
11
17
  import {
12
18
  EventBridgeClient,
@@ -242,7 +248,7 @@ async function publishDataStoreChangeEvents(pending) {
242
248
  );
243
249
  }
244
250
  entries.push({
245
- Source: DATA_STORE_CHANGE_EVENT_SOURCE,
251
+ Source: import_workflows.OPENHI_DATA_SOURCE,
246
252
  DetailType: DATA_STORE_CHANGE_DETAIL_TYPE,
247
253
  Detail: detail,
248
254
  EventBusName: busName
@@ -315,4 +321,4 @@ export {
315
321
  parseCurrentResourceKeys,
316
322
  handler
317
323
  };
318
- //# sourceMappingURL=chunk-I7IIPV5X.mjs.map
324
+ //# sourceMappingURL=chunk-KR2Y2CVQ.mjs.map
@@ -0,0 +1 @@
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\";\n// Canonical bus-source constants live in `@openhi/workflows` per\n// TR-016 §Configuration Standards; this handler imports the data-bus\n// `Source` value from there rather than a local re-export so every\n// publisher and every consumer reads from the same symbol.\nimport { OPENHI_DATA_SOURCE } from \"@openhi/workflows\";\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 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\n/**\n * PK pattern emitted by the data-entity factory and ElectroDB. The literal\n * segments (`TID#`, `WID#`, `RT#`, `ID#`) are matched case-insensitively\n * because ElectroDB lowercases the entire composite key by default (the\n * factory's GSI1SK uses `casing: \"none\"` to opt out, but the base-table PK\n * does not — see `data-entity-common.ts` and the test at line 56 of its spec\n * for the lowercased `tid#…#wid#…#rt#…#shard#…` shape).\n */\nconst PK_PATTERN =\n /^TID#(?<tenantId>[^#]+)#WID#(?<workspaceId>[^#]+)#RT#(?<resourceType>[^#]+)#ID#(?<resourceId>.+)$/i;\n\n/**\n * SK shape that identifies a \"current\" (non-versioned) item. The data-entity\n * factory uses a single-attribute composite SK (`sk` → default `\"CURRENT\"`)\n * with no explicit template, so ElectroDB wraps the value into its standard\n * `$<entity>_<version>#sk_<lowercased-value>` format — e.g.\n * `$patient_01#sk_current`. The historical bare-`CURRENT` form is still\n * accepted so any consumer writing directly to DynamoDB without ElectroDB\n * (tests, integration fixtures, ad-hoc admin writes) does not get silently\n * filtered out.\n */\nconst CURRENT_SK_PATTERN = /^(CURRENT|\\$[A-Za-z0-9_-]+_[0-9]+#sk_current)$/i;\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_PATTERN.test(skAttr)) {\n return null;\n }\n const m = PK_PATTERN.exec(pkAttr);\n if (!m?.groups) {\n return null;\n }\n const {\n tenantId,\n workspaceId,\n resourceType: pkResourceType,\n resourceId,\n } = 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 // The PK segment carries a lowercased copy of the resource type (ElectroDB\n // default casing), which loses the FHIR-spec PascalCase shape consumers\n // depend on (`resource_type` column matches in Postgres search, EventBridge\n // rules pattern-matching, archive partition naming). Recover the canonical\n // case from the resource JSON itself when available; fall back to the PK\n // token if the resource is missing or unparseable so soft-deletes and other\n // edge cases still produce a key tuple.\n const resourceType =\n extractFhirResourceTypeFromImage(plain) ?? pkResourceType;\n return { tenantId, workspaceId, resourceType, resourceId, version };\n}\n\nfunction extractFhirResourceTypeFromImage(\n plain: Record<string, unknown>,\n): string | undefined {\n const resourceStr =\n typeof plain.resource === \"string\" ? plain.resource : undefined;\n if (!resourceStr) {\n return undefined;\n }\n try {\n const parsed = JSON.parse(resourceStr) as { resourceType?: unknown };\n if (typeof parsed.resourceType === \"string\" && parsed.resourceType !== \"\") {\n return parsed.resourceType;\n }\n } catch {\n // Malformed resource JSON — defer to caller's fallback.\n }\n return undefined;\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: OPENHI_DATA_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":[]}
@@ -10,11 +10,12 @@ import {
10
10
  require_lib
11
11
  } from "./chunk-ZM4GDHHC.mjs";
12
12
  import {
13
+ PLATFORM_SCOPE_TENANT_ID,
13
14
  createMembershipOperation,
14
15
  createRoleAssignmentOperation,
15
16
  createTenantOperation,
16
17
  createWorkspaceOperation
17
- } from "./chunk-AWYZJFPL.mjs";
18
+ } from "./chunk-CFJDATDK.mjs";
18
19
  import {
19
20
  NotFoundError
20
21
  } from "./chunk-FYHBHHWK.mjs";
@@ -50,7 +51,6 @@ var SEED_DEMO_DATA_CONSUMER_NAME = "seed-demo-data";
50
51
  var DEMO_URN_SYSTEM = "urn:openhi:demo";
51
52
  var OPENHI_RESOURCE_URN_SYSTEM = "http://openhi.org/";
52
53
  var DEMO_PERIOD = { start: "2026-01-01T00:00:00Z" };
53
- var PLATFORM_SCOPE_TENANT_ID = "platform";
54
54
  var PLACEHOLDER_TENANT_ID = "placeholder-tenant-id";
55
55
  var PLACEHOLDER_WORKSPACE_ID = "placeholder-workspace-id";
56
56
  var DEV_USERS = [
@@ -561,6 +561,25 @@ var errorMessage = (err) => {
561
561
  }
562
562
  return String(err);
563
563
  };
564
+ var tryRun = async (failures, phase, scope, resourceType, resourceId, fn) => {
565
+ try {
566
+ await fn();
567
+ return true;
568
+ } catch (err) {
569
+ failures.push({ phase, scope, resourceType, resourceId, error: err });
570
+ return false;
571
+ }
572
+ };
573
+ var aggregateFailureError = (failures) => {
574
+ const summary = failures.map(
575
+ (f) => `${f.phase} ${f.scope}/${f.resourceType}/${f.resourceId}: ${errorMessage(
576
+ f.error
577
+ )}`
578
+ ).join("; ");
579
+ return new Error(
580
+ `seed-demo-data: ${failures.length} item(s) failed across phases: ${summary}`
581
+ );
582
+ };
564
583
  var idForRoleCode = (code) => {
565
584
  for (const key of Object.keys(PLATFORM_ROLE_IDS)) {
566
585
  if (PLATFORM_ROLE_CONCEPTS[key].code === code) {
@@ -670,95 +689,180 @@ var upsertUser = async (context, user, cognitoSub) => {
670
689
  lastUpdated: context.date ?? (/* @__PURE__ */ new Date()).toISOString()
671
690
  }).go();
672
691
  };
673
- var seedWorkspaceDataPlane = async (baseContext, group) => {
692
+ var seedWorkspaceDataPlane = async (baseContext, group, failures) => {
674
693
  const workspaceContext = {
675
694
  ...baseContext,
676
695
  tenantId: group.tenantId,
677
696
  workspaceId: group.workspaceId
678
697
  };
698
+ const scope = `${group.tenantId}/${group.workspaceId}`;
679
699
  for (const patient of group.patients) {
680
- await createPatientOperation({
681
- context: workspaceContext,
682
- body: patient
683
- });
700
+ await tryRun(
701
+ failures,
702
+ "phase-3",
703
+ scope,
704
+ "Patient",
705
+ patient.id ?? "",
706
+ () => createPatientOperation({
707
+ context: workspaceContext,
708
+ body: patient
709
+ })
710
+ );
684
711
  }
685
712
  for (const practitioner of group.practitioners) {
686
- await createPractitionerOperation({
687
- context: workspaceContext,
688
- body: practitioner
689
- });
713
+ await tryRun(
714
+ failures,
715
+ "phase-3",
716
+ scope,
717
+ "Practitioner",
718
+ practitioner.id ?? "",
719
+ () => createPractitionerOperation({
720
+ context: workspaceContext,
721
+ body: practitioner
722
+ })
723
+ );
690
724
  }
691
725
  for (const observation of group.observations) {
692
- await createObservationOperation({
693
- context: workspaceContext,
694
- body: observation
695
- });
726
+ await tryRun(
727
+ failures,
728
+ "phase-3",
729
+ scope,
730
+ "Observation",
731
+ observation.id ?? "",
732
+ () => createObservationOperation({
733
+ context: workspaceContext,
734
+ body: observation
735
+ })
736
+ );
696
737
  }
697
738
  for (const encounter of group.encounters) {
698
- await createEncounterOperation({
699
- context: workspaceContext,
700
- body: encounter
701
- });
739
+ await tryRun(
740
+ failures,
741
+ "phase-3",
742
+ scope,
743
+ "Encounter",
744
+ encounter.id ?? "",
745
+ () => createEncounterOperation({
746
+ context: workspaceContext,
747
+ body: encounter
748
+ })
749
+ );
702
750
  }
703
751
  for (const account of group.accounts) {
704
- await createAccountOperation({
705
- context: workspaceContext,
706
- body: account
707
- });
752
+ await tryRun(
753
+ failures,
754
+ "phase-3",
755
+ scope,
756
+ "Account",
757
+ account.id ?? "",
758
+ () => createAccountOperation({
759
+ context: workspaceContext,
760
+ body: account
761
+ })
762
+ );
708
763
  }
709
764
  };
710
765
  var seedDemoGraph = async (params) => {
711
766
  const { baseContext, devUsers, cognito } = params;
767
+ const failures = [];
712
768
  for (const spec of DEMO_TENANT_SPECS) {
713
769
  const tenantContext = {
714
770
  ...baseContext,
715
771
  tenantId: spec.tenantId
716
772
  };
717
- await createTenantOperation({
718
- context: tenantContext,
719
- body: { id: spec.tenantId, resource: tenantResourceBody(spec) }
720
- });
721
- for (const workspace of spec.workspaces) {
722
- await createWorkspaceOperation({
773
+ await tryRun(
774
+ failures,
775
+ "phase-1",
776
+ spec.tenantId,
777
+ "Tenant",
778
+ spec.tenantId,
779
+ () => createTenantOperation({
723
780
  context: tenantContext,
724
- body: {
725
- id: workspace.id,
726
- resource: workspaceResourceBody(spec, workspace)
727
- }
728
- });
781
+ body: { id: spec.tenantId, resource: tenantResourceBody(spec) }
782
+ })
783
+ );
784
+ for (const workspace of spec.workspaces) {
785
+ await tryRun(
786
+ failures,
787
+ "phase-1",
788
+ spec.tenantId,
789
+ "Workspace",
790
+ workspace.id,
791
+ () => createWorkspaceOperation({
792
+ context: tenantContext,
793
+ body: {
794
+ id: workspace.id,
795
+ resource: workspaceResourceBody(spec, workspace)
796
+ }
797
+ })
798
+ );
729
799
  }
730
800
  }
731
801
  for (const user of devUsers) {
732
- const cognitoSub = await cognito.ensureUser(user.email);
733
- await upsertUser(baseContext, user, cognitoSub);
802
+ let cognitoSub;
803
+ try {
804
+ cognitoSub = await cognito.ensureUser(user.email);
805
+ } catch (err) {
806
+ failures.push({
807
+ phase: "phase-2",
808
+ scope: user.id,
809
+ resourceType: "CognitoUser",
810
+ resourceId: user.email,
811
+ error: err
812
+ });
813
+ continue;
814
+ }
815
+ await tryRun(
816
+ failures,
817
+ "phase-2",
818
+ user.id,
819
+ "User",
820
+ user.id,
821
+ () => upsertUser(baseContext, user, cognitoSub)
822
+ );
734
823
  for (const spec of DEMO_TENANT_SPECS) {
735
824
  const tenantContext = {
736
825
  ...baseContext,
737
826
  tenantId: spec.tenantId
738
827
  };
828
+ const userScope = `${user.id}@${spec.tenantId}`;
739
829
  const membershipId = demoMembershipId(user.id, spec.tenantId);
740
- await createMembershipOperation({
741
- context: tenantContext,
742
- body: {
743
- id: membershipId,
744
- resource: membershipResourceBody(spec, user, membershipId)
745
- }
746
- });
747
- for (const roleCode of demoRolesForUserInTenant(user, spec.tenantId)) {
748
- const raId = demoRoleAssignmentId(user.id, spec.tenantId, roleCode);
749
- await createRoleAssignmentOperation({
830
+ await tryRun(
831
+ failures,
832
+ "phase-2",
833
+ userScope,
834
+ "Membership",
835
+ membershipId,
836
+ () => createMembershipOperation({
750
837
  context: tenantContext,
751
838
  body: {
752
- id: raId,
753
- resource: roleAssignmentResourceBody(
754
- spec.scenario,
755
- spec.tenantId,
756
- user,
757
- roleCode,
758
- raId
759
- )
839
+ id: membershipId,
840
+ resource: membershipResourceBody(spec, user, membershipId)
760
841
  }
761
- });
842
+ })
843
+ );
844
+ for (const roleCode of demoRolesForUserInTenant(user, spec.tenantId)) {
845
+ const raId = demoRoleAssignmentId(user.id, spec.tenantId, roleCode);
846
+ await tryRun(
847
+ failures,
848
+ "phase-2",
849
+ userScope,
850
+ "RoleAssignment",
851
+ raId,
852
+ () => createRoleAssignmentOperation({
853
+ context: tenantContext,
854
+ body: {
855
+ id: raId,
856
+ resource: roleAssignmentResourceBody(
857
+ spec.scenario,
858
+ spec.tenantId,
859
+ user,
860
+ roleCode,
861
+ raId
862
+ )
863
+ }
864
+ })
865
+ );
762
866
  }
763
867
  }
764
868
  const platformContext = {
@@ -771,22 +875,42 @@ var seedDemoGraph = async (params) => {
771
875
  PLATFORM_SCOPE_TENANT_ID,
772
876
  platformRoleCode
773
877
  );
774
- await createRoleAssignmentOperation({
775
- context: platformContext,
776
- body: {
777
- id: platformRaId,
778
- resource: roleAssignmentResourceBody(
779
- "platform",
780
- PLATFORM_SCOPE_TENANT_ID,
781
- user,
782
- platformRoleCode,
783
- platformRaId
784
- )
785
- }
786
- });
878
+ await tryRun(
879
+ failures,
880
+ "phase-2",
881
+ `${user.id}@${PLATFORM_SCOPE_TENANT_ID}`,
882
+ "RoleAssignment",
883
+ platformRaId,
884
+ () => createRoleAssignmentOperation({
885
+ context: platformContext,
886
+ body: {
887
+ id: platformRaId,
888
+ resource: roleAssignmentResourceBody(
889
+ "platform",
890
+ PLATFORM_SCOPE_TENANT_ID,
891
+ user,
892
+ platformRoleCode,
893
+ platformRaId
894
+ )
895
+ }
896
+ })
897
+ );
787
898
  }
788
899
  for (const group of DEMO_DATA_PLANE_FIXTURES) {
789
- await seedWorkspaceDataPlane(baseContext, group);
900
+ try {
901
+ await seedWorkspaceDataPlane(baseContext, group, failures);
902
+ } catch (err) {
903
+ failures.push({
904
+ phase: "phase-3",
905
+ scope: `${group.tenantId}/${group.workspaceId}`,
906
+ resourceType: "Workspace",
907
+ resourceId: group.workspaceId,
908
+ error: err
909
+ });
910
+ }
911
+ }
912
+ if (failures.length > 0) {
913
+ throw aggregateFailureError(failures);
790
914
  }
791
915
  };
792
916
  var runSeedDemoData = async (event, deps, devUsers) => {
@@ -914,7 +1038,6 @@ export {
914
1038
  DEMO_URN_SYSTEM,
915
1039
  OPENHI_RESOURCE_URN_SYSTEM,
916
1040
  DEMO_PERIOD,
917
- PLATFORM_SCOPE_TENANT_ID,
918
1041
  PLACEHOLDER_TENANT_ID,
919
1042
  PLACEHOLDER_WORKSPACE_ID,
920
1043
  DEV_USERS,
@@ -932,4 +1055,4 @@ export {
932
1055
  productionCognitoProvisioner,
933
1056
  handler
934
1057
  };
935
- //# sourceMappingURL=chunk-WGA43MMY.mjs.map
1058
+ //# sourceMappingURL=chunk-SXYY5WHG.mjs.map