@openhi/constructs 0.0.159 → 0.0.161
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-HQ67J7BP.mjs → chunk-5S6VFBLT.mjs} +12 -70
- package/lib/chunk-5S6VFBLT.mjs.map +1 -0
- package/lib/{chunk-MVQWAIMC.mjs → chunk-6BB4CRSS.mjs} +3 -312
- package/lib/chunk-6BB4CRSS.mjs.map +1 -0
- package/lib/{chunk-WPCBVDFZ.mjs → chunk-76UM2LQ5.mjs} +2 -2
- package/lib/chunk-7TRO2STL.mjs +4616 -0
- package/lib/chunk-7TRO2STL.mjs.map +1 -0
- package/lib/chunk-BUAYVN3C.mjs +87 -0
- package/lib/chunk-BUAYVN3C.mjs.map +1 -0
- package/lib/{chunk-23PUSHBV.mjs → chunk-D2Y6DDOC.mjs} +2 -2
- package/lib/chunk-DWSWCUZR.mjs +123 -0
- package/lib/chunk-DWSWCUZR.mjs.map +1 -0
- package/lib/{chunk-VZCPGQXA.mjs → chunk-EUIP2U5F.mjs} +69 -1
- package/lib/{chunk-VZCPGQXA.mjs.map → chunk-EUIP2U5F.mjs.map} +1 -1
- package/lib/chunk-GJTPXJKD.mjs +46 -0
- package/lib/chunk-GJTPXJKD.mjs.map +1 -0
- package/lib/chunk-I6LUPJUY.mjs +61 -0
- package/lib/chunk-I6LUPJUY.mjs.map +1 -0
- package/lib/{chunk-KR2Y2CVQ.mjs → chunk-KA3OMP3X.mjs} +2 -2
- package/lib/{chunk-ZM4GDHHC.mjs → chunk-KMEWULMX.mjs} +51 -3
- package/lib/chunk-KMEWULMX.mjs.map +1 -0
- package/lib/chunk-LKKLO66E.mjs +25 -0
- package/lib/chunk-LKKLO66E.mjs.map +1 -0
- package/lib/{chunk-CFJDATDK.mjs → chunk-MLFMW5IF.mjs} +43 -9
- package/lib/chunk-MLFMW5IF.mjs.map +1 -0
- package/lib/chunk-O5VQWB6U.mjs +315 -0
- package/lib/chunk-O5VQWB6U.mjs.map +1 -0
- package/lib/{chunk-7BQHLC7U.mjs → chunk-P3CTZWC2.mjs} +8 -40
- package/lib/chunk-P3CTZWC2.mjs.map +1 -0
- package/lib/chunk-P3NFCKTZ.mjs +502 -0
- package/lib/chunk-P3NFCKTZ.mjs.map +1 -0
- package/lib/{chunk-M7Y3BOQW.mjs → chunk-Q3MKITPY.mjs} +5 -5
- package/lib/chunk-Q64MOYJ7.mjs +218 -0
- package/lib/chunk-Q64MOYJ7.mjs.map +1 -0
- package/lib/chunk-RQKJNMX5.mjs +89 -0
- package/lib/chunk-RQKJNMX5.mjs.map +1 -0
- package/lib/{chunk-ZWSGM6PZ.mjs → chunk-SD7J3N3C.mjs} +2 -2
- package/lib/{chunk-7RZHFI77.mjs → chunk-VESULYQQ.mjs} +2 -2
- package/lib/{chunk-AOSEKL7U.mjs → chunk-WOTU36P3.mjs} +6 -103
- package/lib/chunk-WOTU36P3.mjs.map +1 -0
- package/lib/{chunk-X5E4YJGZ.mjs → chunk-YPTJJ35S.mjs} +2 -2
- package/lib/counter-apply-operation-DZM3MIDm.d.mts +63 -0
- package/lib/counter-apply-operation-DZM3MIDm.d.ts +63 -0
- package/lib/counter-maintenance.handler.d.mts +38 -0
- package/lib/counter-maintenance.handler.d.ts +38 -0
- package/lib/counter-maintenance.handler.js +2885 -0
- package/lib/counter-maintenance.handler.js.map +1 -0
- package/lib/counter-maintenance.handler.mjs +180 -0
- package/lib/counter-maintenance.handler.mjs.map +1 -0
- package/lib/counter-reconciliation.handler.d.mts +116 -0
- package/lib/counter-reconciliation.handler.d.ts +116 -0
- package/lib/counter-reconciliation.handler.js +3324 -0
- package/lib/counter-reconciliation.handler.js.map +1 -0
- package/lib/counter-reconciliation.handler.mjs +295 -0
- package/lib/counter-reconciliation.handler.mjs.map +1 -0
- package/lib/data-store-postgres-replication.handler.js +50 -2
- package/lib/data-store-postgres-replication.handler.js.map +1 -1
- package/lib/data-store-postgres-replication.handler.mjs +2 -2
- package/lib/delete-chunk.handler.js +118 -2
- package/lib/delete-chunk.handler.js.map +1 -1
- package/lib/delete-chunk.handler.mjs +3 -3
- package/lib/{events-DTgo2dcW.d.mts → events-TG654e7L.d.mts} +68 -19
- package/lib/{events-DTgo2dcW.d.ts → events-TG654e7L.d.ts} +68 -19
- package/lib/finalize.handler.js +50 -2
- package/lib/finalize.handler.js.map +1 -1
- package/lib/finalize.handler.mjs +4 -4
- package/lib/firehose-archive-transform.handler.js +50 -2
- package/lib/firehose-archive-transform.handler.js.map +1 -1
- package/lib/firehose-archive-transform.handler.mjs +2 -2
- package/lib/index.d.mts +1283 -4
- package/lib/index.d.ts +1389 -24
- package/lib/index.js +4113 -320
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +602 -195
- package/lib/index.mjs.map +1 -1
- package/lib/list-chunks.handler.js +118 -2
- package/lib/list-chunks.handler.js.map +1 -1
- package/lib/list-chunks.handler.mjs +3 -3
- package/lib/platform-deploy-bridge.handler.js +50 -2
- package/lib/platform-deploy-bridge.handler.js.map +1 -1
- package/lib/platform-deploy-bridge.handler.mjs +1 -1
- package/lib/pre-token-generation.handler.js +68 -0
- package/lib/pre-token-generation.handler.js.map +1 -1
- package/lib/pre-token-generation.handler.mjs +9 -5
- package/lib/pre-token-generation.handler.mjs.map +1 -1
- package/lib/provision-default-workspace.handler.js +887 -4
- package/lib/provision-default-workspace.handler.js.map +1 -1
- package/lib/provision-default-workspace.handler.mjs +14 -9
- package/lib/provision-default-workspace.handler.mjs.map +1 -1
- package/lib/rename-finalize.handler.js +50 -2
- package/lib/rename-finalize.handler.js.map +1 -1
- package/lib/rename-finalize.handler.mjs +2 -2
- package/lib/rename-list-targets.handler.js +118 -2
- package/lib/rename-list-targets.handler.js.map +1 -1
- package/lib/rename-list-targets.handler.mjs +11 -9
- package/lib/rename-list-targets.handler.mjs.map +1 -1
- package/lib/rename-rewrite-chunk.handler.js +68 -0
- package/lib/rename-rewrite-chunk.handler.js.map +1 -1
- package/lib/rename-rewrite-chunk.handler.mjs +2 -2
- package/lib/rest-api-lambda.handler.js +1454 -251
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +673 -821
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/lib/seed-demo-data.handler.d.mts +1 -1
- package/lib/seed-demo-data.handler.d.ts +1 -1
- package/lib/seed-demo-data.handler.js +4004 -201
- package/lib/seed-demo-data.handler.js.map +1 -1
- package/lib/seed-demo-data.handler.mjs +10 -7
- package/lib/seed-system-data.handler.js +118 -2
- package/lib/seed-system-data.handler.js.map +1 -1
- package/lib/seed-system-data.handler.mjs +5 -5
- package/package.json +1 -1
- package/lib/chunk-7BQHLC7U.mjs.map +0 -1
- package/lib/chunk-AOSEKL7U.mjs.map +0 -1
- package/lib/chunk-BQMJSDOD.mjs +0 -1136
- package/lib/chunk-BQMJSDOD.mjs.map +0 -1
- package/lib/chunk-CFJDATDK.mjs.map +0 -1
- package/lib/chunk-E6MCKJVS.mjs +0 -212
- package/lib/chunk-E6MCKJVS.mjs.map +0 -1
- package/lib/chunk-HQ67J7BP.mjs.map +0 -1
- package/lib/chunk-MVQWAIMC.mjs.map +0 -1
- package/lib/chunk-ZM4GDHHC.mjs.map +0 -1
- /package/lib/{chunk-WPCBVDFZ.mjs.map → chunk-76UM2LQ5.mjs.map} +0 -0
- /package/lib/{chunk-23PUSHBV.mjs.map → chunk-D2Y6DDOC.mjs.map} +0 -0
- /package/lib/{chunk-KR2Y2CVQ.mjs.map → chunk-KA3OMP3X.mjs.map} +0 -0
- /package/lib/{chunk-M7Y3BOQW.mjs.map → chunk-Q3MKITPY.mjs.map} +0 -0
- /package/lib/{chunk-ZWSGM6PZ.mjs.map → chunk-SD7J3N3C.mjs.map} +0 -0
- /package/lib/{chunk-7RZHFI77.mjs.map → chunk-VESULYQQ.mjs.map} +0 -0
- /package/lib/{chunk-X5E4YJGZ.mjs.map → chunk-YPTJJ35S.mjs.map} +0 -0
|
@@ -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 { publishMembershipCreated } from \"../control-event-publisher\";\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 // ADR-028: publish the membership-created control event so the\n // counter-maintenance consumer can increment the denormalized counters.\n // Best-effort — see control-event-publisher (the canonical write above is\n // authoritative; reconciliation repairs any missed event).\n await publishMembershipCreated(context, {\n membershipId: id,\n tenantId: context.tenantId,\n ...(userIdFromResource !== undefined && { userId: userIdFromResource }),\n ...(workspaceIdFromResource !== undefined && {\n workspaceId: workspaceIdFromResource,\n }),\n });\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 {\n extractRoleLevel,\n publishRoleAssignmentCreated,\n} from \"../control-event-publisher\";\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 // ADR-028: publish the role-assignment-created control event so the\n // counter-maintenance consumer can adjust the workspace admin/normal-user\n // breakdown. The role level/type is extracted from the resource so the\n // consumer classifies admin vs normal without re-reading the Role record.\n await publishRoleAssignmentCreated(context, {\n roleAssignmentId: id,\n tenantId: context.tenantId,\n ...(userIdFromResource !== undefined && { userId: userIdFromResource }),\n ...(workspaceIdFromResource !== undefined && {\n workspaceId: workspaceIdFromResource,\n }),\n ...(roleIdFromResource !== undefined && { roleId: roleIdFromResource }),\n ...(extractRoleLevel(resourceRecord) !== undefined && {\n roleLevel: extractRoleLevel(resourceRecord),\n }),\n });\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\";\nimport { publishWorkspaceCreated } from \"../control-event-publisher\";\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 // ADR-028 (Open Item #1): publish the workspace-created control event so\n // the counter-maintenance consumer can increment Tenant.workspacesInTenant.\n // Best-effort — see control-event-publisher (the canonical write above is\n // authoritative; reconciliation repairs any missed event).\n await publishWorkspaceCreated(context, {\n workspaceId: id,\n tenantId,\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"]}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NotFoundError
|
|
3
|
+
} from "./chunk-FYHBHHWK.mjs";
|
|
4
|
+
import {
|
|
5
|
+
SHARD_COUNT
|
|
6
|
+
} from "./chunk-EUIP2U5F.mjs";
|
|
7
|
+
|
|
8
|
+
// src/data/audit-meta.ts
|
|
9
|
+
var OPENHI_EXT = "http://openhi.org/fhir/StructureDefinition";
|
|
10
|
+
function mergeAuditIntoMeta(meta, audit) {
|
|
11
|
+
const existing = meta ?? {};
|
|
12
|
+
const ext = [
|
|
13
|
+
...Array.isArray(existing.extension) ? existing.extension : []
|
|
14
|
+
];
|
|
15
|
+
const byUrl = new Map(ext.map((e) => [e.url, e]));
|
|
16
|
+
function set(url, value, type) {
|
|
17
|
+
if (value == null) return;
|
|
18
|
+
byUrl.set(url, { url, [type]: value });
|
|
19
|
+
}
|
|
20
|
+
set(`${OPENHI_EXT}/created-date`, audit.createdDate, "valueDateTime");
|
|
21
|
+
set(`${OPENHI_EXT}/created-by-id`, audit.createdById, "valueString");
|
|
22
|
+
set(`${OPENHI_EXT}/created-by-name`, audit.createdByName, "valueString");
|
|
23
|
+
set(`${OPENHI_EXT}/modified-date`, audit.modifiedDate, "valueDateTime");
|
|
24
|
+
set(`${OPENHI_EXT}/modified-by-id`, audit.modifiedById, "valueString");
|
|
25
|
+
set(`${OPENHI_EXT}/modified-by-name`, audit.modifiedByName, "valueString");
|
|
26
|
+
set(`${OPENHI_EXT}/deleted-date`, audit.deletedDate, "valueDateTime");
|
|
27
|
+
set(`${OPENHI_EXT}/deleted-by-id`, audit.deletedById, "valueString");
|
|
28
|
+
set(`${OPENHI_EXT}/deleted-by-name`, audit.deletedByName, "valueString");
|
|
29
|
+
return { ...existing, extension: Array.from(byUrl.values()) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/data/operations/data-operations-common.ts
|
|
33
|
+
import { extractSortKey, extractSummary } from "@openhi/types";
|
|
34
|
+
|
|
35
|
+
// src/lib/compression.ts
|
|
36
|
+
import { gzipSync, gunzipSync } from "zlib";
|
|
37
|
+
var ENVELOPE_VERSION = 1;
|
|
38
|
+
var COMPRESSION_ALGOS = {
|
|
39
|
+
NONE: "none",
|
|
40
|
+
GZIP: "gzip",
|
|
41
|
+
BROTLI: "brotli",
|
|
42
|
+
DEFLATE: "deflate"
|
|
43
|
+
};
|
|
44
|
+
function isEnvelope(obj) {
|
|
45
|
+
return typeof obj === "object" && obj !== null && "v" in obj && "algo" in obj && "payload" in obj && typeof obj.payload === "string";
|
|
46
|
+
}
|
|
47
|
+
function compressResource(jsonString, options) {
|
|
48
|
+
const algo = options?.algo ?? COMPRESSION_ALGOS.GZIP;
|
|
49
|
+
if (algo === COMPRESSION_ALGOS.NONE) {
|
|
50
|
+
const envelope2 = {
|
|
51
|
+
v: ENVELOPE_VERSION,
|
|
52
|
+
algo: COMPRESSION_ALGOS.NONE,
|
|
53
|
+
payload: jsonString
|
|
54
|
+
};
|
|
55
|
+
return JSON.stringify(envelope2);
|
|
56
|
+
}
|
|
57
|
+
const buf = Buffer.from(jsonString, "utf-8");
|
|
58
|
+
const payload = gzipSync(buf).toString("base64");
|
|
59
|
+
const envelope = {
|
|
60
|
+
v: ENVELOPE_VERSION,
|
|
61
|
+
algo: COMPRESSION_ALGOS.GZIP,
|
|
62
|
+
payload
|
|
63
|
+
};
|
|
64
|
+
return JSON.stringify(envelope);
|
|
65
|
+
}
|
|
66
|
+
function decompressResource(compressedOrRaw) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(compressedOrRaw);
|
|
69
|
+
if (isEnvelope(parsed)) {
|
|
70
|
+
if (parsed.algo === COMPRESSION_ALGOS.GZIP) {
|
|
71
|
+
const buf = Buffer.from(parsed.payload, "base64");
|
|
72
|
+
return gunzipSync(buf).toString("utf-8");
|
|
73
|
+
}
|
|
74
|
+
if (parsed.algo === COMPRESSION_ALGOS.NONE) {
|
|
75
|
+
return parsed.payload;
|
|
76
|
+
}
|
|
77
|
+
return parsed.payload;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const buf = Buffer.from(compressedOrRaw, "base64");
|
|
83
|
+
if (buf.length >= 2 && buf[0] === 31 && buf[1] === 139) {
|
|
84
|
+
return gunzipSync(buf).toString("utf-8");
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
return compressedOrRaw;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/data/operations/data-operations-common.ts
|
|
92
|
+
var DATA_ENTITY_SK = "CURRENT";
|
|
93
|
+
async function getDataEntityById(entity, tenantId, workspaceId, id, resourceLabel) {
|
|
94
|
+
const result = await entity.get({
|
|
95
|
+
tenantId,
|
|
96
|
+
workspaceId,
|
|
97
|
+
id,
|
|
98
|
+
sk: DATA_ENTITY_SK
|
|
99
|
+
}).go();
|
|
100
|
+
if (!result.data) {
|
|
101
|
+
throw new NotFoundError(`${resourceLabel} ${id} not found`, {
|
|
102
|
+
details: { id }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const parsed = JSON.parse(decompressResource(result.data.resource));
|
|
106
|
+
return {
|
|
107
|
+
id: result.data.id,
|
|
108
|
+
resource: { ...parsed, id: result.data.id }
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function deleteDataEntityById(entity, tenantId, workspaceId, id) {
|
|
112
|
+
await entity.delete({
|
|
113
|
+
tenantId,
|
|
114
|
+
workspaceId,
|
|
115
|
+
id,
|
|
116
|
+
sk: DATA_ENTITY_SK
|
|
117
|
+
}).go();
|
|
118
|
+
}
|
|
119
|
+
var BATCH_GET_MAX_ATTEMPTS = 3;
|
|
120
|
+
var BATCH_GET_BASE_BACKOFF_MS = 50;
|
|
121
|
+
async function batchGetWithRetry(entity, keys) {
|
|
122
|
+
if (keys.length === 0) return [];
|
|
123
|
+
const collected = [];
|
|
124
|
+
let pending = keys;
|
|
125
|
+
let attempt = 0;
|
|
126
|
+
while (pending.length > 0) {
|
|
127
|
+
if (attempt > 0) {
|
|
128
|
+
await new Promise(
|
|
129
|
+
(resolve) => setTimeout(resolve, BATCH_GET_BASE_BACKOFF_MS * 2 ** (attempt - 1))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
attempt++;
|
|
133
|
+
const result = await entity.get(pending).go();
|
|
134
|
+
collected.push(...result.data);
|
|
135
|
+
const unprocessed = result.unprocessed ?? [];
|
|
136
|
+
if (unprocessed.length === 0) break;
|
|
137
|
+
if (attempt >= BATCH_GET_MAX_ATTEMPTS) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`BatchGet exhausted retries: ${unprocessed.length} key(s) still unprocessed after ${BATCH_GET_MAX_ATTEMPTS} attempt(s)`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
pending = unprocessed;
|
|
143
|
+
}
|
|
144
|
+
return collected;
|
|
145
|
+
}
|
|
146
|
+
async function dispatchListMode(mode, shardResults, hooks) {
|
|
147
|
+
if (mode === "count") {
|
|
148
|
+
let total = 0;
|
|
149
|
+
for (const shardResult of shardResults) {
|
|
150
|
+
total += (shardResult.data ?? []).length;
|
|
151
|
+
}
|
|
152
|
+
return { entries: [], total };
|
|
153
|
+
}
|
|
154
|
+
if (mode === "summary") {
|
|
155
|
+
const entries2 = [];
|
|
156
|
+
for (const shardResult of shardResults) {
|
|
157
|
+
for (const item of shardResult.data ?? []) {
|
|
158
|
+
if (typeof item.summary !== "string") continue;
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(item.summary);
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
entries2.push(hooks.buildSummaryEntry(item.id, parsed));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { entries: entries2, total: entries2.length };
|
|
169
|
+
}
|
|
170
|
+
const orderedIds = [];
|
|
171
|
+
for (const shardResult of shardResults) {
|
|
172
|
+
for (const item of shardResult.data ?? []) {
|
|
173
|
+
orderedIds.push(item.id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (orderedIds.length === 0) return { entries: [], total: 0 };
|
|
177
|
+
const items = await hooks.hydrate(orderedIds);
|
|
178
|
+
const byId = new Map(items.map((item) => [hooks.getId(item), item]));
|
|
179
|
+
const entries = [];
|
|
180
|
+
for (const id of orderedIds) {
|
|
181
|
+
const item = byId.get(id);
|
|
182
|
+
if (!item) continue;
|
|
183
|
+
entries.push(hooks.buildEntry(id, item));
|
|
184
|
+
}
|
|
185
|
+
return { entries, total: entries.length };
|
|
186
|
+
}
|
|
187
|
+
async function listDataEntitiesByWorkspace(entity, tenantId, workspaceId, mode = "full") {
|
|
188
|
+
const shardResults = await Promise.all(
|
|
189
|
+
Array.from(
|
|
190
|
+
{ length: SHARD_COUNT },
|
|
191
|
+
(_, shard) => entity.query.gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) }).go()
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
return dispatchListMode(
|
|
195
|
+
mode,
|
|
196
|
+
shardResults,
|
|
197
|
+
{
|
|
198
|
+
hydrate: (orderedIds) => batchGetWithRetry(
|
|
199
|
+
entity,
|
|
200
|
+
orderedIds.map((id) => ({
|
|
201
|
+
tenantId,
|
|
202
|
+
workspaceId,
|
|
203
|
+
id,
|
|
204
|
+
sk: DATA_ENTITY_SK
|
|
205
|
+
}))
|
|
206
|
+
),
|
|
207
|
+
getId: (item) => item.id,
|
|
208
|
+
buildEntry: (id, item) => {
|
|
209
|
+
const parsed = JSON.parse(decompressResource(item.resource));
|
|
210
|
+
return { id, resource: { ...parsed, id } };
|
|
211
|
+
},
|
|
212
|
+
buildSummaryEntry: (id, parsed) => ({
|
|
213
|
+
id,
|
|
214
|
+
resource: { ...parsed, id }
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourceWithAudit, fallbackDate) {
|
|
220
|
+
const lastUpdated = resourceWithAudit.meta?.lastUpdated ?? fallbackDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
221
|
+
const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
|
|
222
|
+
const resourceLike = resourceWithAudit;
|
|
223
|
+
const summary = JSON.stringify(extractSummary(resourceLike));
|
|
224
|
+
const gsi1sk = extractSortKey(resourceLike);
|
|
225
|
+
await entity.put({
|
|
226
|
+
sk: DATA_ENTITY_SK,
|
|
227
|
+
tenantId,
|
|
228
|
+
workspaceId,
|
|
229
|
+
id,
|
|
230
|
+
resource: compressResource(JSON.stringify(resourceWithAudit)),
|
|
231
|
+
summary,
|
|
232
|
+
vid,
|
|
233
|
+
lastUpdated,
|
|
234
|
+
gsi1sk
|
|
235
|
+
}).go();
|
|
236
|
+
return {
|
|
237
|
+
id,
|
|
238
|
+
resource: resourceWithAudit
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function buildUpdatedResourceWithAudit(body, id, date, actorId, actorName, existingResourceStr, resourceType) {
|
|
242
|
+
const existingMeta = JSON.parse(existingResourceStr).meta;
|
|
243
|
+
const bodyWithMeta = body;
|
|
244
|
+
const resourceWithVersion = {
|
|
245
|
+
...body,
|
|
246
|
+
resourceType,
|
|
247
|
+
id,
|
|
248
|
+
meta: {
|
|
249
|
+
...bodyWithMeta.meta ?? {},
|
|
250
|
+
lastUpdated: date,
|
|
251
|
+
versionId: "2"
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const resourceWithAudit = {
|
|
255
|
+
...resourceWithVersion,
|
|
256
|
+
meta: mergeAuditIntoMeta(resourceWithVersion.meta ?? existingMeta, {
|
|
257
|
+
modifiedDate: date,
|
|
258
|
+
modifiedById: actorId,
|
|
259
|
+
modifiedByName: actorName
|
|
260
|
+
})
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
resource: resourceWithAudit,
|
|
264
|
+
lastUpdated: date
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function updateDataEntityById(entity, tenantId, workspaceId, id, resourceLabel, context, buildPatched) {
|
|
268
|
+
const existing = await entity.get({
|
|
269
|
+
tenantId,
|
|
270
|
+
workspaceId,
|
|
271
|
+
id,
|
|
272
|
+
sk: DATA_ENTITY_SK
|
|
273
|
+
}).go();
|
|
274
|
+
if (!existing.data) {
|
|
275
|
+
throw new NotFoundError(`${resourceLabel} ${id} not found`, {
|
|
276
|
+
details: { id }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const existingStr = decompressResource(existing.data.resource);
|
|
280
|
+
const { resource, lastUpdated } = buildPatched(existingStr);
|
|
281
|
+
const resourceLike = resource;
|
|
282
|
+
const summary = JSON.stringify(extractSummary(resourceLike));
|
|
283
|
+
const gsi1sk = extractSortKey(resourceLike);
|
|
284
|
+
await entity.patch({
|
|
285
|
+
tenantId,
|
|
286
|
+
workspaceId,
|
|
287
|
+
id,
|
|
288
|
+
sk: DATA_ENTITY_SK
|
|
289
|
+
}).set({
|
|
290
|
+
resource: compressResource(JSON.stringify(resource)),
|
|
291
|
+
summary,
|
|
292
|
+
lastUpdated,
|
|
293
|
+
gsi1sk
|
|
294
|
+
}).go();
|
|
295
|
+
return {
|
|
296
|
+
id,
|
|
297
|
+
resource
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export {
|
|
302
|
+
compressResource,
|
|
303
|
+
decompressResource,
|
|
304
|
+
mergeAuditIntoMeta,
|
|
305
|
+
DATA_ENTITY_SK,
|
|
306
|
+
getDataEntityById,
|
|
307
|
+
deleteDataEntityById,
|
|
308
|
+
batchGetWithRetry,
|
|
309
|
+
dispatchListMode,
|
|
310
|
+
listDataEntitiesByWorkspace,
|
|
311
|
+
createDataEntityRecord,
|
|
312
|
+
buildUpdatedResourceWithAudit,
|
|
313
|
+
updateDataEntityById
|
|
314
|
+
};
|
|
315
|
+
//# sourceMappingURL=chunk-O5VQWB6U.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/audit-meta.ts","../src/data/operations/data-operations-common.ts","../src/lib/compression.ts"],"sourcesContent":["import type { Extension, Meta } from \"@openhi/types\";\n\n/**\n * Shared audit/meta helpers for FHIR resources. Used by data operations and import.\n * OpenHI audit is stored in resource meta.extension (per ADR 2026-01-13-06).\n *\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/entity-standards.md\n */\n\n/** OpenHI extension URLs for audit in resource meta (per ADR 2026-01-13-06). */\nconst OPENHI_EXT = \"http://openhi.org/fhir/StructureDefinition\";\n\n/** Meta with optional OpenHI audit extensions (created/modified by, etc.). */\nexport type MetaWithExtensions = Meta & { extension?: Array<Extension> };\n\n/** Audit fields stored in FHIR resource meta.extension. */\nexport interface AuditFields {\n createdDate?: string;\n createdById?: string;\n createdByName?: string;\n modifiedDate?: string;\n modifiedById?: string;\n modifiedByName?: string;\n deletedDate?: string;\n deletedById?: string;\n deletedByName?: string;\n}\n\n/** Audit extension entry shape (subset of Extension used by OpenHI audit). */\ntype AuditExtensionEntry = Pick<\n Extension,\n \"url\" | \"valueString\" | \"valueDateTime\"\n>;\n\n/** Builds meta.extension entries for audit; merges with existing meta. */\nexport function mergeAuditIntoMeta(\n meta: MetaWithExtensions | Record<string, unknown> | undefined,\n audit: AuditFields,\n): MetaWithExtensions {\n const existing = (meta ?? {}) as MetaWithExtensions;\n const ext: Array<AuditExtensionEntry> = [\n ...(Array.isArray(existing.extension)\n ? (existing.extension as Array<AuditExtensionEntry>)\n : []),\n ];\n const byUrl = new Map(ext.map((e) => [e.url, e]));\n function set(\n url: string,\n value: string | undefined,\n type: \"valueString\" | \"valueDateTime\",\n ) {\n if (value == null) return;\n byUrl.set(url, { url, [type]: value });\n }\n set(`${OPENHI_EXT}/created-date`, audit.createdDate, \"valueDateTime\");\n set(`${OPENHI_EXT}/created-by-id`, audit.createdById, \"valueString\");\n set(`${OPENHI_EXT}/created-by-name`, audit.createdByName, \"valueString\");\n set(`${OPENHI_EXT}/modified-date`, audit.modifiedDate, \"valueDateTime\");\n set(`${OPENHI_EXT}/modified-by-id`, audit.modifiedById, \"valueString\");\n set(`${OPENHI_EXT}/modified-by-name`, audit.modifiedByName, \"valueString\");\n set(`${OPENHI_EXT}/deleted-date`, audit.deletedDate, \"valueDateTime\");\n set(`${OPENHI_EXT}/deleted-by-id`, audit.deletedById, \"valueString\");\n set(`${OPENHI_EXT}/deleted-by-name`, audit.deletedByName, \"valueString\");\n return { ...existing, extension: Array.from(byUrl.values()) };\n}\n","import { extractSortKey, extractSummary } from \"@openhi/types\";\nimport type { Meta, FhirResourceLike } from \"@openhi/types\";\nimport { compressResource, decompressResource } from \"../../lib/compression\";\nimport { mergeAuditIntoMeta, type MetaWithExtensions } from \"../audit-meta\";\nimport { SHARD_COUNT } from \"../dynamo/shard\";\nimport { NotFoundError } from \"../errors\";\nimport type { OpenHiContext } from \"../openhi-context\";\n\n/**\n * Sort key for the current record version. Matches Dynamo record index SK default.\n * Use this in get/update/delete (and create where applicable) for data-plane entities.\n */\nexport const DATA_ENTITY_SK = \"CURRENT\" as const;\n\n/** Base params for data-entity operations: context and optional table override. */\nexport interface BaseDataEntityParams {\n context: OpenHiContext;\n /** Optional table name override; resolved by data service from DYNAMO_TABLE_NAME when omitted. */\n tableName?: string;\n}\n\n/** Params for get-by-id and delete (context + id + optional tableName). */\nexport interface GetByIdParams extends BaseDataEntityParams {\n id: string;\n}\n\n/**\n * Mode for list operations introduced by #853 to back FHIR `_summary` opt-ins.\n *\n * - `full` (default): GSI1 fan-out → BatchGet hydration → full FHIR resource per entry.\n * - `summary`: GSI1 fan-out only; the `summary` JSON projected onto GSI1 is parsed and used\n * as `resource` per entry. Skips BatchGet entirely — that's the cost win the FHIR spec\n * intends `_summary=true` to deliver.\n * - `count`: GSI1 fan-out only; entries are dropped, only `total` is populated. Routes\n * should pass `total` into `buildSearchsetBundle({ mode: \"count\", total })`.\n *\n * `_elements` is implemented at the route layer as `full` mode + post-hydration pruning,\n * since per-element pruning has to happen after decompression and is FHIR-spec-defined\n * (always retain root-level mandatories — see `prune-resource-by-elements.ts`).\n */\nexport type ListOperationMode = \"full\" | \"summary\" | \"count\";\n\n/** Params for list (context + optional tableName + optional mode for #853 `_summary`). */\nexport interface ListParams extends BaseDataEntityParams {\n /** Defaults to `\"full\"` (current behavior); routes pass other modes for `_summary` opt-ins. */\n mode?: ListOperationMode;\n}\n\n/** Result for create / get-by-id / update: single resource. */\nexport interface SingleResourceResult<T> {\n id: string;\n resource: T;\n}\n\n/** Entry shape for list results. */\nexport interface ListEntry<T> {\n id: string;\n resource: T;\n}\n\n/**\n * Result for list: entries array plus total count.\n *\n * - For `mode === \"full\"` and `mode === \"summary\"`, `total === entries.length`.\n * - For `mode === \"count\"`, `entries` is empty and `total` is the GSI1 fan-out count.\n *\n * Splitting `total` from `entries.length` lets count-mode routes report a true count\n * while skipping any per-entry work.\n */\nexport interface ListResult<T> {\n entries: Array<ListEntry<T>>;\n total: number;\n}\n\n/** Minimal entity shape for get (used by getDataEntityById). */\ninterface EntityWithGet {\n get(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<{ data: { id: string; resource: string } | null }> };\n}\n\n/** Minimal entity shape for delete (used by deleteDataEntityById). */\ninterface EntityWithDelete {\n delete(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<unknown> };\n}\n\n/**\n * Minimal entity shape for list via GSI1 + BatchGet hydration (used by listDataEntitiesByWorkspace).\n * GSI1 is sharded per ADR-011, so listing fans out to each shard and concatenates ids; the\n * `resource` attribute is NOT projected onto GSI1 (per the summary projection in\n * `dynamo-db-data-store.ts`), so the second phase BatchGets the base table for full items.\n *\n * GSI1 INCLUDE projection (per `dynamo-db-data-store.ts`) carries `summary`, `vid`, `lastUpdated`,\n * `createdDate`, `modifiedDate`, `createdById`, `modifiedById` alongside the key attributes.\n * `summary` is what `mode: \"summary\"` returns to the caller without hitting the base table.\n */\ninterface DataEntityWithListAndBatchGet {\n query: {\n gsi1(params: {\n tenantId: string;\n workspaceId: string;\n gsi1Shard: string;\n }): {\n go(): Promise<{\n data: Array<{\n id: string;\n summary?: string;\n vid?: string;\n lastUpdated?: string;\n createdDate?: string;\n modifiedDate?: string;\n createdById?: string;\n modifiedById?: string;\n }> | null;\n }>;\n };\n };\n get(\n keys: Array<{\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }>,\n ): {\n go(): Promise<{\n data: Array<{ id: string; resource: string }>;\n unprocessed: Array<{\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }>;\n }>;\n };\n}\n\n/** Minimal entity shape for put (used by createDataEntityRecord). */\ninterface EntityWithPut {\n put(attrs: {\n sk: string;\n tenantId: string;\n workspaceId: string;\n id: string;\n resource: string;\n summary: string;\n vid: string;\n lastUpdated: string;\n gsi1sk: string;\n }): { go(): Promise<unknown> };\n}\n\n/** Minimal entity shape for patch (used by updateDataEntityById). */\ninterface EntityWithPatch {\n get(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<{ data: { id: string; resource: string } | null }> };\n patch(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): {\n set(attrs: {\n resource: string;\n summary: string;\n lastUpdated: string;\n gsi1sk: string;\n }): {\n go(): Promise<unknown>;\n };\n };\n}\n\n/**\n * Get a single data-entity record by id. Decompresses and parses resource; throws NotFoundError if missing.\n * Use from get-by-id operations with the appropriate entity and resource type.\n */\nexport async function getDataEntityById<T>(\n entity: EntityWithGet,\n tenantId: string,\n workspaceId: string,\n id: string,\n resourceLabel: string,\n): Promise<SingleResourceResult<T>> {\n const result = await entity\n .get({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })\n .go();\n\n if (!result.data) {\n throw new NotFoundError(`${resourceLabel} ${id} not found`, {\n details: { id },\n });\n }\n\n const parsed = JSON.parse(decompressResource(result.data.resource)) as T & {\n id?: string;\n };\n return {\n id: result.data.id,\n resource: { ...parsed, id: result.data.id } as T,\n };\n}\n\n/**\n * Delete a data-entity record by id. Idempotent (no-op if not found).\n * Use from delete operations with the appropriate entity.\n */\nexport async function deleteDataEntityById(\n entity: EntityWithDelete,\n tenantId: string,\n workspaceId: string,\n id: string,\n): Promise<void> {\n await entity\n .delete({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })\n .go();\n}\n\n/** Maximum total attempts (initial + retries) when hydrating list ids via BatchGet. */\nconst BATCH_GET_MAX_ATTEMPTS = 3;\n/** Base backoff in milliseconds applied to BatchGet retries; doubles each attempt. */\nconst BATCH_GET_BASE_BACKOFF_MS = 50;\n\n/** Minimal entity shape for BatchGet hydration on the base table; chunks of 100 are handled by ElectroDB. */\ninterface EntityWithBatchGet<TKey, TItem> {\n get(keys: TKey[]): {\n go(): Promise<{ data: TItem[]; unprocessed: TKey[] }>;\n };\n}\n\n/**\n * BatchGet wrapper that retries `UnprocessedKeys` with exponential backoff. ElectroDB chunks the\n * input keys into groups of 100 internally, but does not retry unprocessed keys — DynamoDB can\n * return some keys unprocessed under throttling or partial failure, and the caller must reissue\n * them. Throws after `BATCH_GET_MAX_ATTEMPTS` if any keys remain unprocessed; intended for list\n * hydration (#854) where partial results would silently truncate the response.\n */\nexport async function batchGetWithRetry<TKey, TItem>(\n entity: EntityWithBatchGet<TKey, TItem>,\n keys: TKey[],\n): Promise<TItem[]> {\n if (keys.length === 0) return [];\n\n const collected: TItem[] = [];\n let pending = keys;\n let attempt = 0;\n\n while (pending.length > 0) {\n if (attempt > 0) {\n await new Promise((resolve) =>\n setTimeout(resolve, BATCH_GET_BASE_BACKOFF_MS * 2 ** (attempt - 1)),\n );\n }\n attempt++;\n const result = await entity.get(pending).go();\n collected.push(...result.data);\n const unprocessed = result.unprocessed ?? [];\n if (unprocessed.length === 0) break;\n if (attempt >= BATCH_GET_MAX_ATTEMPTS) {\n throw new Error(\n `BatchGet exhausted retries: ${unprocessed.length} key(s) still unprocessed after ${BATCH_GET_MAX_ATTEMPTS} attempt(s)`,\n );\n }\n pending = unprocessed;\n }\n\n return collected;\n}\n\n/** GSI1 index item shape — what a sharded `gsi1.query().go()` returns per row. */\nexport interface ShardedListIndexItem {\n id: string;\n summary?: string;\n}\n\n/** Hooks that adapt a generic mode-dispatching list to a specific entity's hydration and entry shape. */\nexport interface DispatchListModeHooks<TItem, TEntry> {\n /** Hydrate the base table for the given ids (typically `batchGetWithRetry(entity, keys)`). */\n hydrate: (orderedIds: string[]) => Promise<TItem[]>;\n /** Extract the canonical id from a hydrated item so it can be matched back to the GSI1 order. */\n getId: (item: TItem) => string;\n /** Build the result entry for `mode === \"full\"` from a hydrated base-table item. */\n buildEntry: (id: string, item: TItem) => TEntry;\n /** Build the result entry for `mode === \"summary\"` from the parsed GSI1 `summary` JSON. */\n buildSummaryEntry: (\n id: string,\n parsedSummary: Record<string, unknown>,\n ) => TEntry;\n}\n\n/**\n * Mode dispatcher shared by data-plane and control-plane list operations (#853).\n *\n * Given pre-fetched `shardResults` from a sharded GSI1 fan-out, returns either:\n * - `mode === \"count\"` — `{ entries: [], total }` where total is the sum of shard row counts.\n * - `mode === \"summary\"` — entries built from each shard row's parsed `summary` JSON; rows with\n * missing or unparseable `summary` are dropped.\n * - `mode === \"full\"` — calls `hydrate(orderedIds)` (typically a BatchGet) and builds entries\n * from hydrated items in per-shard GSI1 sort order; missing items are dropped.\n *\n * Lives here (alongside `listDataEntitiesByWorkspace`) because the same dispatch logic is needed\n * by every list operation that backs a FHIR list/search endpoint, including the seven\n * control-plane peers (User, Role, Tenant, Workspace, Membership, RoleAssignment, Configuration).\n */\nexport async function dispatchListMode<TItem, TEntry>(\n mode: ListOperationMode,\n shardResults: Array<{ data: Array<ShardedListIndexItem> | null }>,\n hooks: DispatchListModeHooks<TItem, TEntry>,\n): Promise<{ entries: TEntry[]; total: number }> {\n if (mode === \"count\") {\n let total = 0;\n for (const shardResult of shardResults) {\n total += (shardResult.data ?? []).length;\n }\n return { entries: [], total };\n }\n\n if (mode === \"summary\") {\n const entries: TEntry[] = [];\n for (const shardResult of shardResults) {\n for (const item of shardResult.data ?? []) {\n if (typeof item.summary !== \"string\") continue;\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(item.summary) as Record<string, unknown>;\n } catch {\n continue;\n }\n entries.push(hooks.buildSummaryEntry(item.id, parsed));\n }\n }\n return { entries, total: entries.length };\n }\n\n const orderedIds: string[] = [];\n for (const shardResult of shardResults) {\n for (const item of shardResult.data ?? []) {\n orderedIds.push(item.id);\n }\n }\n\n if (orderedIds.length === 0) return { entries: [], total: 0 };\n\n const items = await hooks.hydrate(orderedIds);\n const byId = new Map(items.map((item) => [hooks.getId(item), item]));\n\n const entries: TEntry[] = [];\n for (const id of orderedIds) {\n const item = byId.get(id);\n if (!item) continue;\n entries.push(hooks.buildEntry(id, item));\n }\n\n return { entries, total: entries.length };\n}\n\n/**\n * List data-entity records in a workspace via GSI1.\n *\n * `mode` (default `\"full\"`) selects the read shape — see `dispatchListMode`. The data-plane\n * binding here adds the four-shard fan-out (per ADR-011) and the BatchGet hydration with\n * decompression for `mode === \"full\"`. K-way merge by `gsi1sk` is intentionally NOT done here\n * — full server-side natural sort lands with the FHIR list-endpoint plumbing that adds\n * pagination tokens.\n */\nexport async function listDataEntitiesByWorkspace<T>(\n entity: DataEntityWithListAndBatchGet,\n tenantId: string,\n workspaceId: string,\n mode: ListOperationMode = \"full\",\n): Promise<ListResult<T>> {\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n entity.query\n .gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) })\n .go(),\n ),\n );\n\n return dispatchListMode<{ id: string; resource: string }, ListEntry<T>>(\n mode,\n shardResults,\n {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n entity,\n orderedIds.map((id) => ({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })),\n ),\n getId: (item) => item.id,\n buildEntry: (id, item) => {\n const parsed = JSON.parse(decompressResource(item.resource)) as T & {\n id?: string;\n };\n return { id, resource: { ...parsed, id } as T };\n },\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: { ...parsed, id } as T,\n }),\n },\n );\n}\n\n/**\n * Create a data-entity record with put. Computes vid from lastUpdated (from resource meta or fallback).\n * Use from create operations (e.g. Practitioner, Encounter) that build the resource with audit in meta.\n */\nexport async function createDataEntityRecord<T>(\n entity: EntityWithPut,\n tenantId: string,\n workspaceId: string,\n id: string,\n resourceWithAudit: T & { meta?: { lastUpdated?: string } },\n fallbackDate: string,\n): Promise<SingleResourceResult<T>> {\n const lastUpdated =\n resourceWithAudit.meta?.lastUpdated ??\n fallbackDate ??\n new Date().toISOString();\n const vid =\n lastUpdated.replace(/[-:T.Z]/g, \"\").slice(0, 12) || Date.now().toString(36);\n\n const resourceLike = resourceWithAudit as unknown as FhirResourceLike;\n const summary = JSON.stringify(extractSummary(resourceLike));\n const gsi1sk = extractSortKey(resourceLike);\n\n await entity\n .put({\n sk: DATA_ENTITY_SK,\n tenantId,\n workspaceId,\n id,\n resource: compressResource(JSON.stringify(resourceWithAudit)),\n summary,\n vid,\n lastUpdated,\n gsi1sk,\n })\n .go();\n\n return {\n id,\n resource: resourceWithAudit as T,\n };\n}\n\n/**\n * Build an updated resource with audit in meta for use with updateDataEntityById.\n * Parses existing resource string for existing meta, merges body with id/resourceType/meta (versionId \"2\"),\n * then merges modified audit (modifiedDate, modifiedById, modifiedByName) into meta.\n * Use from update operations (Patient, Encounter, Practitioner) to avoid duplicating this logic.\n */\nexport function buildUpdatedResourceWithAudit<T extends { meta?: Meta }>(\n body: T,\n id: string,\n date: string,\n actorId: string,\n actorName: string,\n existingResourceStr: string,\n resourceType: string,\n): {\n resource: T & { id: string; meta: MetaWithExtensions };\n lastUpdated: string;\n} {\n const existingMeta: MetaWithExtensions | undefined = (\n JSON.parse(existingResourceStr) as { meta?: MetaWithExtensions }\n ).meta;\n\n const bodyWithMeta = body as T & { id?: string; meta?: Meta };\n const resourceWithVersion: T & { id: string; meta?: Meta } = {\n ...body,\n resourceType: resourceType as T[\"resourceType\"],\n id,\n meta: {\n ...(bodyWithMeta.meta ?? {}),\n lastUpdated: date,\n versionId: \"2\",\n },\n };\n\n const resourceWithAudit: T & { id: string; meta: MetaWithExtensions } = {\n ...resourceWithVersion,\n meta: mergeAuditIntoMeta(resourceWithVersion.meta ?? existingMeta, {\n modifiedDate: date,\n modifiedById: actorId,\n modifiedByName: actorName,\n }),\n };\n\n return {\n resource: resourceWithAudit,\n lastUpdated: date,\n };\n}\n\n/**\n * Update a data-entity record by id: get existing, throw if not found, then call builder with\n * decompressed existing resource string; builder returns \\{ resource, lastUpdated \\}; then patch.\n * Use from update operations with the appropriate entity and resource type.\n */\nexport async function updateDataEntityById<T>(\n entity: EntityWithPatch,\n tenantId: string,\n workspaceId: string,\n id: string,\n resourceLabel: string,\n context: OpenHiContext,\n buildPatched: (existingResourceStr: string) => {\n resource: unknown;\n lastUpdated: string;\n },\n): Promise<SingleResourceResult<T>> {\n const existing = await entity\n .get({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })\n .go();\n\n if (!existing.data) {\n throw new NotFoundError(`${resourceLabel} ${id} not found`, {\n details: { id },\n });\n }\n\n const existingStr = decompressResource(existing.data.resource);\n const { resource, lastUpdated } = buildPatched(existingStr);\n\n const resourceLike = resource as FhirResourceLike;\n const summary = JSON.stringify(extractSummary(resourceLike));\n const gsi1sk = extractSortKey(resourceLike);\n\n await entity\n .patch({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })\n .set({\n resource: compressResource(JSON.stringify(resource)),\n summary,\n lastUpdated,\n gsi1sk,\n })\n .go();\n\n return {\n id,\n resource: resource as T,\n };\n}\n","import { gzipSync, gunzipSync } from \"node:zlib\";\n\n/**\n * @see sites/www-docs/content/packages/@openhi/constructs/lib/compression.md\n */\n\n/** Envelope format version. See ADR 2026-02-15-02 (data layer compression). */\nconst ENVELOPE_VERSION = 1;\n\n/**\n * Compression algorithm identifiers supported by the envelope (string values).\n * Only algos that Node.js supports out of the box (zlib): gzip, brotli, deflate.\n * \"none\" = uncompressed payload. zstd was considered in the ADR but requires native addon/WASM.\n */\nexport const COMPRESSION_ALGOS = {\n NONE: \"none\",\n GZIP: \"gzip\",\n BROTLI: \"brotli\",\n DEFLATE: \"deflate\",\n} as const;\n\n/** Algorithm value for envelope `algo`; only gzip and none are implemented today. */\nexport type CompressionAlgo =\n (typeof COMPRESSION_ALGOS)[keyof typeof COMPRESSION_ALGOS];\n\n/** Stored value is a JSON string of this envelope. */\ninterface CompressionEnvelope {\n v: number;\n algo: string;\n payload: string;\n}\n\nfunction isEnvelope(obj: unknown): obj is CompressionEnvelope {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n \"v\" in obj &&\n \"algo\" in obj &&\n \"payload\" in obj &&\n typeof (obj as CompressionEnvelope).payload === \"string\"\n );\n}\n\n/**\n * Compresses a JSON string (e.g. serialized FHIR resource) for storage in DynamoDB.\n * Uses a versioned envelope: \\{ v, algo, payload \\} with gzip+base64 in payload.\n * Used by the data layer on write; see REST API docs (compression in data layer).\n * Optional compression: pass `{ algo: COMPRESSION_ALGOS.NONE }` to store in envelope without compressing.\n */\nexport function compressResource(\n jsonString: string,\n options?: { algo?: CompressionAlgo },\n): string {\n const algo = options?.algo ?? COMPRESSION_ALGOS.GZIP;\n if (algo === COMPRESSION_ALGOS.NONE) {\n const envelope: CompressionEnvelope = {\n v: ENVELOPE_VERSION,\n algo: COMPRESSION_ALGOS.NONE,\n payload: jsonString,\n };\n return JSON.stringify(envelope);\n }\n const buf = Buffer.from(jsonString, \"utf-8\");\n const payload = gzipSync(buf).toString(\"base64\");\n const envelope: CompressionEnvelope = {\n v: ENVELOPE_VERSION,\n algo: COMPRESSION_ALGOS.GZIP,\n payload,\n };\n return JSON.stringify(envelope);\n}\n\n/**\n * Decompresses a stored value: versioned envelope (v, algo, payload) or legacy gzip+base64 / raw.\n * If the value is not valid envelope JSON, falls back to legacy: try gzip magic on base64, else return as-is.\n */\nexport function decompressResource(compressedOrRaw: string): string {\n try {\n const parsed = JSON.parse(compressedOrRaw) as unknown;\n if (isEnvelope(parsed)) {\n if (parsed.algo === COMPRESSION_ALGOS.GZIP) {\n const buf = Buffer.from(parsed.payload, \"base64\");\n return gunzipSync(buf).toString(\"utf-8\");\n }\n if (parsed.algo === COMPRESSION_ALGOS.NONE) {\n return parsed.payload;\n }\n // Unknown algo: return payload as-is (safe fallback per ADR)\n return parsed.payload;\n }\n } catch {\n // Not valid envelope JSON — legacy path\n }\n\n // Legacy: pre-envelope gzip+base64 or raw\n try {\n const buf = Buffer.from(compressedOrRaw, \"base64\");\n if (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) {\n return gunzipSync(buf).toString(\"utf-8\");\n }\n } catch {\n // not base64 or gunzip failed\n }\n return compressedOrRaw;\n}\n"],"mappings":"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","names":["envelope","entries"]}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDynamoDataService
|
|
3
|
+
} from "./chunk-6BB4CRSS.mjs";
|
|
1
4
|
import {
|
|
2
5
|
batchGetWithRetry,
|
|
3
6
|
dispatchListMode,
|
|
4
|
-
getDynamoDataService,
|
|
5
7
|
listDataEntitiesByWorkspace
|
|
6
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-O5VQWB6U.mjs";
|
|
7
9
|
import {
|
|
8
10
|
SHARD_COUNT,
|
|
9
11
|
getDynamoControlService
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-EUIP2U5F.mjs";
|
|
11
13
|
|
|
12
14
|
// src/data/operations/data/practitionerrole/practitionerrole-list-operation.ts
|
|
13
15
|
async function listPractitionerRolesOperation(params) {
|
|
@@ -22,41 +24,8 @@ async function listPractitionerRolesOperation(params) {
|
|
|
22
24
|
);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
// src/data/operations/control/membership/membership-list-operation.ts
|
|
26
|
-
var SK = "CURRENT";
|
|
27
|
-
async function listMembershipsOperation(params) {
|
|
28
|
-
const { context, tableName, mode = "full" } = params;
|
|
29
|
-
const tenantId = context.tenantId;
|
|
30
|
-
const service = getDynamoControlService(tableName);
|
|
31
|
-
const shardResults = await Promise.all(
|
|
32
|
-
Array.from(
|
|
33
|
-
{ length: SHARD_COUNT },
|
|
34
|
-
(_, shard) => service.entities.membership.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
|
|
35
|
-
)
|
|
36
|
-
);
|
|
37
|
-
return dispatchListMode(mode, shardResults, {
|
|
38
|
-
hydrate: (orderedIds) => batchGetWithRetry(
|
|
39
|
-
service.entities.membership,
|
|
40
|
-
orderedIds.map((id) => ({ tenantId, id, sk: SK }))
|
|
41
|
-
),
|
|
42
|
-
getId: (item) => item.id,
|
|
43
|
-
buildEntry: (id, item) => ({
|
|
44
|
-
id,
|
|
45
|
-
resource: {
|
|
46
|
-
resourceType: "Membership",
|
|
47
|
-
id,
|
|
48
|
-
...JSON.parse(item.resource)
|
|
49
|
-
}
|
|
50
|
-
}),
|
|
51
|
-
buildSummaryEntry: (id, parsed) => ({
|
|
52
|
-
id,
|
|
53
|
-
resource: { resourceType: "Membership", id, ...parsed }
|
|
54
|
-
})
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
27
|
// src/data/operations/control/roleassignment/roleassignment-list-operation.ts
|
|
59
|
-
var
|
|
28
|
+
var SK = "CURRENT";
|
|
60
29
|
async function listRoleAssignmentsOperation(params) {
|
|
61
30
|
const { context, tableName, mode = "full" } = params;
|
|
62
31
|
const tenantId = context.tenantId;
|
|
@@ -70,7 +39,7 @@ async function listRoleAssignmentsOperation(params) {
|
|
|
70
39
|
return dispatchListMode(mode, shardResults, {
|
|
71
40
|
hydrate: (orderedIds) => batchGetWithRetry(
|
|
72
41
|
service.entities.roleAssignment,
|
|
73
|
-
orderedIds.map((id) => ({ tenantId, id, sk:
|
|
42
|
+
orderedIds.map((id) => ({ tenantId, id, sk: SK }))
|
|
74
43
|
),
|
|
75
44
|
getId: (item) => item.id,
|
|
76
45
|
buildEntry: (id, item) => ({
|
|
@@ -90,7 +59,6 @@ async function listRoleAssignmentsOperation(params) {
|
|
|
90
59
|
|
|
91
60
|
export {
|
|
92
61
|
listPractitionerRolesOperation,
|
|
93
|
-
listMembershipsOperation,
|
|
94
62
|
listRoleAssignmentsOperation
|
|
95
63
|
};
|
|
96
|
-
//# sourceMappingURL=chunk-
|
|
64
|
+
//# sourceMappingURL=chunk-P3CTZWC2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/operations/data/practitionerrole/practitionerrole-list-operation.ts","../src/data/operations/control/roleassignment/roleassignment-list-operation.ts"],"sourcesContent":["import type { PractitionerRole } from \"@openhi/types\";\nimport { getDynamoDataService } from \"../../../dynamo/dynamo-data-service\";\nimport {\n type ListParams,\n listDataEntitiesByWorkspace,\n type ListResult,\n type ListEntry,\n} from \"../../data-operations-common\";\n\n/**\n * List PractitionerRoles in a workspace (GSI1, sharded). Returns domain result for adapters to map to FHIR Bundle or other formats.\n *\n * @see sites/www-docs/content/packages/@openhi/constructs/data/shared-data-layer-layout.md\n */\nexport type ListPractitionerRolesParams = ListParams;\n\nexport type PractitionerRoleListEntry = ListEntry<PractitionerRole>;\n\nexport type ListPractitionerRolesResult = ListResult<PractitionerRole>;\n\n/**\n * Lists all PractitionerRoles in the workspace. Uses GSI1 (Unified Sharded List per ADR-011).\n * Throws on service errors; adapters map to HTTP/GraphQL/Step Function.\n */\nexport async function listPractitionerRolesOperation(\n params: ListPractitionerRolesParams,\n): Promise<ListPractitionerRolesResult> {\n const { context, tableName, mode } = params;\n const { tenantId, workspaceId } = context;\n const service = getDynamoDataService(tableName);\n return listDataEntitiesByWorkspace<PractitionerRole>(\n service.entities.practitionerrole as Parameters<\n typeof listDataEntitiesByWorkspace\n >[0],\n tenantId,\n workspaceId,\n mode,\n );\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface RoleAssignmentListParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\nexport interface RoleAssignmentListResult {\n entries: Array<{\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n }>;\n total: number;\n}\n\n/**\n * Lists RoleAssignments for the context tenant via GSI1 (sharded). See `dispatchListMode` for\n * the mode contract (#853).\n */\nexport async function listRoleAssignmentsOperation(\n params: RoleAssignmentListParams,\n): Promise<RoleAssignmentListResult> {\n const { context, tableName, mode = \"full\" } = params;\n const tenantId = context.tenantId;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.roleAssignment.query\n .gsi1({ tenantId, gsi1Shard: String(shard) })\n .go(),\n ),\n );\n\n return dispatchListMode<\n { id: string; resource: string },\n RoleAssignmentListResult[\"entries\"][number]\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.roleAssignment,\n orderedIds.map((id) => ({ tenantId, id, sk: SK })),\n ) as Promise<Array<{ id: string; resource: string }>>,\n getId: (item) => item.id,\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"RoleAssignment\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n },\n }),\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: { resourceType: \"RoleAssignment\", id, ...parsed },\n }),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,eAAsB,+BACpB,QACsC;AACtC,QAAM,EAAE,SAAS,WAAW,KAAK,IAAI;AACrC,QAAM,EAAE,UAAU,YAAY,IAAI;AAClC,QAAM,UAAU,qBAAqB,SAAS;AAC9C,SAAO;AAAA,IACL,QAAQ,SAAS;AAAA,IAGjB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC7BA,IAAM,KAAK;AAqBX,eAAsB,6BACpB,QACmC;AACnC,QAAM,EAAE,SAAS,WAAW,OAAO,OAAO,IAAI;AAC9C,QAAM,WAAW,QAAQ;AACzB,QAAM,UAAU,wBAAwB,SAAS;AAEjD,QAAM,eAAe,MAAM,QAAQ;AAAA,IACjC,MAAM;AAAA,MAAK,EAAE,QAAQ,YAAY;AAAA,MAAG,CAAC,GAAG,UACtC,QAAQ,SAAS,eAAe,MAC7B,KAAK,EAAE,UAAU,WAAW,OAAO,KAAK,EAAE,CAAC,EAC3C,GAAG;AAAA,IACR;AAAA,EACF;AAEA,SAAO,iBAGL,MAAM,cAAc;AAAA,IACpB,SAAS,CAAC,eACR;AAAA,MACE,QAAQ,SAAS;AAAA,MACjB,WAAW,IAAI,CAAC,QAAQ,EAAE,UAAU,IAAI,IAAI,GAAG,EAAE;AAAA,IACnD;AAAA,IACF,OAAO,CAAC,SAAS,KAAK;AAAA,IACtB,YAAY,CAAC,IAAI,UAAU;AAAA,MACzB;AAAA,MACA,UAAU;AAAA,QACR,cAAc;AAAA,QACd;AAAA,QACA,GAAI,KAAK,MAAM,KAAK,QAAQ;AAAA,MAC9B;AAAA,IACF;AAAA,IACA,mBAAmB,CAAC,IAAI,YAAY;AAAA,MAClC;AAAA,MACA,UAAU,EAAE,cAAc,kBAAkB,IAAI,GAAG,OAAO;AAAA,IAC5D;AAAA,EACF,CAAC;AACH;","names":[]}
|