@openhi/constructs 0.0.160 → 0.0.162

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 (121) hide show
  1. package/lib/{chunk-HQ67J7BP.mjs → chunk-5S6VFBLT.mjs} +12 -70
  2. package/lib/chunk-5S6VFBLT.mjs.map +1 -0
  3. package/lib/{chunk-MVQWAIMC.mjs → chunk-6BB4CRSS.mjs} +3 -312
  4. package/lib/chunk-6BB4CRSS.mjs.map +1 -0
  5. package/lib/{chunk-WPCBVDFZ.mjs → chunk-76UM2LQ5.mjs} +2 -2
  6. package/lib/{chunk-QFHYTCVY.mjs → chunk-7TRO2STL.mjs} +7 -7
  7. package/lib/chunk-BUAYVN3C.mjs +87 -0
  8. package/lib/chunk-BUAYVN3C.mjs.map +1 -0
  9. package/lib/{chunk-23PUSHBV.mjs → chunk-D2Y6DDOC.mjs} +2 -2
  10. package/lib/chunk-DWSWCUZR.mjs +123 -0
  11. package/lib/chunk-DWSWCUZR.mjs.map +1 -0
  12. package/lib/{chunk-VZCPGQXA.mjs → chunk-EUIP2U5F.mjs} +69 -1
  13. package/lib/{chunk-VZCPGQXA.mjs.map → chunk-EUIP2U5F.mjs.map} +1 -1
  14. package/lib/chunk-GJTPXJKD.mjs +46 -0
  15. package/lib/chunk-GJTPXJKD.mjs.map +1 -0
  16. package/lib/chunk-I6LUPJUY.mjs +61 -0
  17. package/lib/chunk-I6LUPJUY.mjs.map +1 -0
  18. package/lib/{chunk-KR2Y2CVQ.mjs → chunk-KA3OMP3X.mjs} +2 -2
  19. package/lib/{chunk-ZM4GDHHC.mjs → chunk-KMEWULMX.mjs} +51 -3
  20. package/lib/chunk-KMEWULMX.mjs.map +1 -0
  21. package/lib/chunk-LKKLO66E.mjs +25 -0
  22. package/lib/chunk-LKKLO66E.mjs.map +1 -0
  23. package/lib/{chunk-CFJDATDK.mjs → chunk-MLFMW5IF.mjs} +43 -9
  24. package/lib/chunk-MLFMW5IF.mjs.map +1 -0
  25. package/lib/chunk-O5VQWB6U.mjs +315 -0
  26. package/lib/chunk-O5VQWB6U.mjs.map +1 -0
  27. package/lib/{chunk-7BQHLC7U.mjs → chunk-P3CTZWC2.mjs} +8 -40
  28. package/lib/chunk-P3CTZWC2.mjs.map +1 -0
  29. package/lib/{chunk-EFB5OFM7.mjs → chunk-P3NFCKTZ.mjs} +6 -4
  30. package/lib/{chunk-EFB5OFM7.mjs.map → chunk-P3NFCKTZ.mjs.map} +1 -1
  31. package/lib/{chunk-M7Y3BOQW.mjs → chunk-Q3MKITPY.mjs} +5 -5
  32. package/lib/chunk-Q64MOYJ7.mjs +218 -0
  33. package/lib/chunk-Q64MOYJ7.mjs.map +1 -0
  34. package/lib/chunk-RQKJNMX5.mjs +89 -0
  35. package/lib/chunk-RQKJNMX5.mjs.map +1 -0
  36. package/lib/{chunk-ZWSGM6PZ.mjs → chunk-SD7J3N3C.mjs} +2 -2
  37. package/lib/{chunk-7RZHFI77.mjs → chunk-VESULYQQ.mjs} +2 -2
  38. package/lib/{chunk-AOSEKL7U.mjs → chunk-WOTU36P3.mjs} +6 -103
  39. package/lib/chunk-WOTU36P3.mjs.map +1 -0
  40. package/lib/{chunk-X5E4YJGZ.mjs → chunk-YPTJJ35S.mjs} +2 -2
  41. package/lib/counter-apply-operation-DZM3MIDm.d.mts +63 -0
  42. package/lib/counter-apply-operation-DZM3MIDm.d.ts +63 -0
  43. package/lib/counter-maintenance.handler.d.mts +38 -0
  44. package/lib/counter-maintenance.handler.d.ts +38 -0
  45. package/lib/counter-maintenance.handler.js +2885 -0
  46. package/lib/counter-maintenance.handler.js.map +1 -0
  47. package/lib/counter-maintenance.handler.mjs +180 -0
  48. package/lib/counter-maintenance.handler.mjs.map +1 -0
  49. package/lib/counter-reconciliation.handler.d.mts +116 -0
  50. package/lib/counter-reconciliation.handler.d.ts +116 -0
  51. package/lib/counter-reconciliation.handler.js +3324 -0
  52. package/lib/counter-reconciliation.handler.js.map +1 -0
  53. package/lib/counter-reconciliation.handler.mjs +295 -0
  54. package/lib/counter-reconciliation.handler.mjs.map +1 -0
  55. package/lib/data-store-postgres-replication.handler.js +50 -2
  56. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  57. package/lib/data-store-postgres-replication.handler.mjs +2 -2
  58. package/lib/delete-chunk.handler.js +118 -2
  59. package/lib/delete-chunk.handler.js.map +1 -1
  60. package/lib/delete-chunk.handler.mjs +3 -3
  61. package/lib/finalize.handler.js +50 -2
  62. package/lib/finalize.handler.js.map +1 -1
  63. package/lib/finalize.handler.mjs +4 -4
  64. package/lib/firehose-archive-transform.handler.js +50 -2
  65. package/lib/firehose-archive-transform.handler.js.map +1 -1
  66. package/lib/firehose-archive-transform.handler.mjs +2 -2
  67. package/lib/index.d.mts +140 -2
  68. package/lib/index.d.ts +143 -5
  69. package/lib/index.js +493 -196
  70. package/lib/index.js.map +1 -1
  71. package/lib/index.mjs +360 -193
  72. package/lib/index.mjs.map +1 -1
  73. package/lib/list-chunks.handler.js +118 -2
  74. package/lib/list-chunks.handler.js.map +1 -1
  75. package/lib/list-chunks.handler.mjs +3 -3
  76. package/lib/platform-deploy-bridge.handler.js +50 -2
  77. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  78. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  79. package/lib/pre-token-generation.handler.js +68 -0
  80. package/lib/pre-token-generation.handler.js.map +1 -1
  81. package/lib/pre-token-generation.handler.mjs +9 -5
  82. package/lib/pre-token-generation.handler.mjs.map +1 -1
  83. package/lib/provision-default-workspace.handler.js +883 -0
  84. package/lib/provision-default-workspace.handler.js.map +1 -1
  85. package/lib/provision-default-workspace.handler.mjs +10 -5
  86. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  87. package/lib/rename-finalize.handler.js +50 -2
  88. package/lib/rename-finalize.handler.js.map +1 -1
  89. package/lib/rename-finalize.handler.mjs +2 -2
  90. package/lib/rename-list-targets.handler.js +118 -2
  91. package/lib/rename-list-targets.handler.js.map +1 -1
  92. package/lib/rename-list-targets.handler.mjs +11 -9
  93. package/lib/rename-list-targets.handler.mjs.map +1 -1
  94. package/lib/rename-rewrite-chunk.handler.js +68 -0
  95. package/lib/rename-rewrite-chunk.handler.js.map +1 -1
  96. package/lib/rename-rewrite-chunk.handler.mjs +2 -2
  97. package/lib/rest-api-lambda.handler.js +1454 -251
  98. package/lib/rest-api-lambda.handler.js.map +1 -1
  99. package/lib/rest-api-lambda.handler.mjs +415 -291
  100. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  101. package/lib/seed-demo-data.handler.js +205 -8
  102. package/lib/seed-demo-data.handler.js.map +1 -1
  103. package/lib/seed-demo-data.handler.mjs +10 -7
  104. package/lib/seed-system-data.handler.js +118 -2
  105. package/lib/seed-system-data.handler.js.map +1 -1
  106. package/lib/seed-system-data.handler.mjs +5 -5
  107. package/package.json +11 -11
  108. package/lib/chunk-7BQHLC7U.mjs.map +0 -1
  109. package/lib/chunk-AOSEKL7U.mjs.map +0 -1
  110. package/lib/chunk-CFJDATDK.mjs.map +0 -1
  111. package/lib/chunk-HQ67J7BP.mjs.map +0 -1
  112. package/lib/chunk-MVQWAIMC.mjs.map +0 -1
  113. package/lib/chunk-ZM4GDHHC.mjs.map +0 -1
  114. /package/lib/{chunk-WPCBVDFZ.mjs.map → chunk-76UM2LQ5.mjs.map} +0 -0
  115. /package/lib/{chunk-QFHYTCVY.mjs.map → chunk-7TRO2STL.mjs.map} +0 -0
  116. /package/lib/{chunk-23PUSHBV.mjs.map → chunk-D2Y6DDOC.mjs.map} +0 -0
  117. /package/lib/{chunk-KR2Y2CVQ.mjs.map → chunk-KA3OMP3X.mjs.map} +0 -0
  118. /package/lib/{chunk-M7Y3BOQW.mjs.map → chunk-Q3MKITPY.mjs.map} +0 -0
  119. /package/lib/{chunk-ZWSGM6PZ.mjs.map → chunk-SD7J3N3C.mjs.map} +0 -0
  120. /package/lib/{chunk-7RZHFI77.mjs.map → chunk-VESULYQQ.mjs.map} +0 -0
  121. /package/lib/{chunk-X5E4YJGZ.mjs.map → chunk-YPTJJ35S.mjs.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  require_lib
3
- } from "./chunk-ZM4GDHHC.mjs";
3
+ } from "./chunk-KMEWULMX.mjs";
4
4
  import {
5
5
  __toESM
6
6
  } from "./chunk-LZOMFHX3.mjs";
@@ -19,4 +19,4 @@ export {
19
19
  OWNING_DELETE_OPS_EVENT_BUS_ENV_VAR,
20
20
  import_workflows
21
21
  };
22
- //# sourceMappingURL=chunk-7RZHFI77.mjs.map
22
+ //# sourceMappingURL=chunk-VESULYQQ.mjs.map
@@ -1,16 +1,14 @@
1
1
  import {
2
- batchGetWithRetry,
3
- dispatchListMode
4
- } from "./chunk-MVQWAIMC.mjs";
2
+ membershipListByUserOperation
3
+ } from "./chunk-DWSWCUZR.mjs";
5
4
  import {
6
5
  ForbiddenError,
7
6
  NotFoundError,
8
7
  ValidationError
9
8
  } from "./chunk-FYHBHHWK.mjs";
10
9
  import {
11
- SHARD_COUNT,
12
10
  getDynamoControlService
13
- } from "./chunk-VZCPGQXA.mjs";
11
+ } from "./chunk-EUIP2U5F.mjs";
14
12
 
15
13
  // src/data/operations/control/user/user-find-by-sub-operation.ts
16
14
  async function findUserBySubOperation(params) {
@@ -95,102 +93,9 @@ async function getUserByIdOperation(params) {
95
93
  };
96
94
  }
97
95
 
98
- // src/data/operations/control/user/user-list-operation.ts
99
- var SK = "CURRENT";
100
- async function listUsersOperation(params) {
101
- const { tableName, mode = "full" } = params;
102
- const service = getDynamoControlService(tableName);
103
- const shardResults = await Promise.all(
104
- Array.from(
105
- { length: SHARD_COUNT },
106
- (_, shard) => service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go()
107
- )
108
- );
109
- return dispatchListMode(mode, shardResults, {
110
- hydrate: (orderedIds) => batchGetWithRetry(
111
- service.entities.user,
112
- orderedIds.map((id) => ({ id, sk: SK }))
113
- ),
114
- getId: (item) => item.id,
115
- buildEntry: (id, item) => ({
116
- id,
117
- resource: {
118
- resourceType: "User",
119
- id,
120
- ...JSON.parse(item.resource)
121
- }
122
- }),
123
- buildSummaryEntry: (id, parsed) => ({
124
- id,
125
- resource: { resourceType: "User", id, ...parsed }
126
- })
127
- });
128
- }
129
-
130
96
  // src/data/operations/control/user/user-switch-tenant-workspace-operation.ts
131
97
  import { extractSummary as extractSummary2 } from "@openhi/types";
132
-
133
- // src/data/operations/control/membership/membership-list-by-user-operation.ts
134
- function buildSkPrefix(mode, tenantId) {
135
- switch (mode) {
136
- case "tenant":
137
- return "MEMBERSHIP#TENANT#";
138
- case "workspace":
139
- return "MEMBERSHIP#WORKSPACE#";
140
- case "workspaceInTenant":
141
- return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;
142
- case "all":
143
- default:
144
- return "MEMBERSHIP#";
145
- }
146
- }
147
- async function membershipListByUserOperation(params) {
148
- const {
149
- userId,
150
- mode = "all",
151
- tenantId,
152
- cursor = null,
153
- limit,
154
- order,
155
- tableName
156
- } = params;
157
- if (mode === "workspaceInTenant" && !tenantId) {
158
- throw new Error(
159
- 'membershipListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
160
- );
161
- }
162
- const service = getDynamoControlService(tableName);
163
- const skPrefix = buildSkPrefix(mode, tenantId);
164
- const goOptions = {
165
- cursor
166
- };
167
- if (limit !== void 0) {
168
- goOptions.limit = limit;
169
- }
170
- if (order !== void 0) {
171
- goOptions.order = order;
172
- }
173
- const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go(goOptions);
174
- const items = (result.data ?? []).map(
175
- (row) => ({
176
- userId: row.userId,
177
- sk: row.sk,
178
- tenantId: row.tenantId,
179
- workspaceId: row.workspaceId,
180
- membershipId: row.membershipId,
181
- summary: row.summary,
182
- vid: row.vid,
183
- lastUpdated: row.lastUpdated,
184
- denormalizedTenantName: row.denormalizedTenantName,
185
- denormalizedUserName: row.denormalizedUserName,
186
- denormalizedWorkspaceName: row.denormalizedWorkspaceName
187
- })
188
- );
189
- return { items, cursor: result.cursor ?? null };
190
- }
191
-
192
- // src/data/operations/control/user/user-switch-tenant-workspace-operation.ts
193
- var SK2 = "CURRENT";
98
+ var SK = "CURRENT";
194
99
  async function switchUserTenantWorkspaceOperation(params) {
195
100
  const { cognitoSub, tenantReference, workspaceReference, tableName } = params;
196
101
  const tenantId = idFromReference(tenantReference, "Tenant/");
@@ -251,7 +156,7 @@ async function switchUserTenantWorkspaceOperation(params) {
251
156
  extractSummary2(updatedResource)
252
157
  );
253
158
  const service = getDynamoControlService(tableName);
254
- await service.entities.user.patch({ id: user.id, sk: SK2 }).set({
159
+ await service.entities.user.patch({ id: user.id, sk: SK }).set({
255
160
  resource: JSON.stringify(updatedResource),
256
161
  summary,
257
162
  vid,
@@ -303,12 +208,10 @@ export {
303
208
  createUserOperation,
304
209
  deleteUserOperation,
305
210
  getUserByIdOperation,
306
- membershipListByUserOperation,
307
- listUsersOperation,
308
211
  updateUserOperation,
309
212
  findUserBySubOperation,
310
213
  parseUserResource,
311
214
  idFromReference,
312
215
  switchUserTenantWorkspaceOperation
313
216
  };
314
- //# sourceMappingURL=chunk-AOSEKL7U.mjs.map
217
+ //# sourceMappingURL=chunk-WOTU36P3.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data/operations/control/user/user-find-by-sub-operation.ts","../src/data/operations/control/user/user-resource-helpers.ts","../src/data/operations/fhir-reference.ts","../src/data/operations/control/user/user-create-operation.ts","../src/data/operations/control/user/user-get-by-id-operation.ts","../src/data/operations/control/user/user-switch-tenant-workspace-operation.ts","../src/data/operations/control/user/user-update-operation.ts","../src/data/operations/control/user/user-delete-operation.ts"],"sourcesContent":["import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface FindUserBySubParams {\n context: OpenHiContext;\n cognitoSub: string;\n tableName?: string;\n}\n\nexport interface FindUserBySubResult {\n id: string;\n cognitoSub?: string;\n resource: string;\n summary?: string;\n vid: string;\n lastUpdated?: string;\n}\n\n/**\n * Look up a User by Cognito sub via GSI2, then fetch the canonical row\n * from the base table so projection-skipped attributes (`resource`,\n * `summary`, `vid`, `lastUpdated`) are populated. Returns `undefined`\n * when no row matches the sub, or when the GSI2 hit points at a\n * canonical row that has since been hard-deleted.\n *\n * GSI2 projects only `id` (+ ElectroDB markers), so a single index\n * query is insufficient — see #1175.\n */\nexport async function findUserBySubOperation(\n params: FindUserBySubParams,\n): Promise<FindUserBySubResult | undefined> {\n const { cognitoSub, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const indexResult = await service.entities.user.query\n .gsi2({ cognitoSub })\n .go({ limit: 1, attributes: [\"id\"] });\n const indexItem = indexResult.data?.[0];\n if (!indexItem) {\n return undefined;\n }\n\n const canonical = await service.entities.user\n .get({ id: indexItem.id, sk: \"CURRENT\" })\n .go();\n const row = canonical.data;\n if (!row) {\n return undefined;\n }\n\n return {\n id: row.id,\n cognitoSub: row.cognitoSub,\n resource: row.resource,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n };\n}\n","import type { User } from \"@openhi/types\";\n\n/**\n * Helpers for working with persisted OpenHI User resources. Co-located with\n * the User operations because both the Cognito triggers and the onboarding\n * workflow consume these alongside `findUserBySubOperation`.\n */\n\n// Defensive parse — JSON.parse may yield any shape, so every field is optional.\nexport type UserResource = Partial<User>;\n\n/**\n * Existing User resources are stored as JSON strings in the data store; parse\n * defensively so a malformed payload returns `undefined` rather than throwing.\n */\nexport function parseUserResource(resource: string): UserResource | undefined {\n try {\n return JSON.parse(resource) as UserResource;\n } catch {\n return undefined;\n }\n}\n","/**\n * Pure helpers for working with FHIR Reference fields. Shared across data-plane\n * and control-plane operations and the handlers that wrap them.\n */\n\n/**\n * Extract the id portion from a FHIR-style reference such as `Patient/<id>` or\n * `Tenant/<id>`. Returns `undefined` if the reference is missing, does not\n * match the prefix, or has an empty id after the prefix.\n */\nexport function idFromReference(\n reference: string | undefined,\n prefix: string,\n): string | undefined {\n if (!reference || !reference.startsWith(prefix)) {\n return undefined;\n }\n const id = reference.slice(prefix.length);\n return id.length > 0 ? id : undefined;\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface UserCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createUserOperation(\n params: UserCreateParams,\n): Promise<UserCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `user-${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: \"User\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.user\n .put({\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { NotFoundError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserGetByIdParams {\n context: OpenHiContext;\n id: string;\n tableName?: string;\n}\n\nexport interface UserGetByIdResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n}\n\nexport async function getUserByIdOperation(\n params: UserGetByIdParams,\n): Promise<UserGetByIdResult> {\n const { id, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const response = await service.entities.user.get({ id, sk: \"CURRENT\" }).go();\n\n const item = response.data;\n if (!item) {\n throw new NotFoundError(`User not found: ${id}`);\n }\n\n const parsedResource = JSON.parse(item.resource) as Record<string, unknown>;\n\n return {\n id,\n resource: { resourceType: \"User\", id, ...parsedResource },\n };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { findUserBySubOperation } from \"./user-find-by-sub-operation\";\nimport { parseUserResource } from \"./user-resource-helpers\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport {\n ForbiddenError,\n NotFoundError,\n ValidationError,\n} from \"../../../errors\";\nimport { idFromReference } from \"../../fhir-reference\";\nimport { membershipListByUserOperation } from \"../membership/membership-list-by-user-operation\";\n\nconst SK = \"CURRENT\";\n\nexport interface UserSwitchTenantWorkspaceParams {\n cognitoSub: string;\n tenantReference: string;\n workspaceReference: string;\n tableName?: string;\n /** Override the clock — used by tests for deterministic `lastUpdated`. */\n now?: () => Date;\n}\n\nexport interface UserSwitchTenantWorkspaceResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Update `currentTenant` and `currentWorkspace` on the User resource for the\n * caller authenticated by the given Cognito `sub`. All other fields on the\n * User are preserved.\n *\n * Membership pre-condition uses the ADR-018 adjacency-list user projection\n * (pattern #4, workspace sub-lane). A single base-table Query on\n * `PK = USER#ID#<userId>` with\n * `SK begins_with 'MEMBERSHIP#WORKSPACE#TID#<tenantId>#'`\n * (via {@link membershipListByUserOperation} with `mode: \"workspaceInTenant\"`)\n * confirms the caller has a workspace-level Membership in the requested\n * tenant + workspace pair. No GSI1 fan-out, no scan.\n *\n * Throws:\n * - `ValidationError` when either reference is missing or malformed\n * - `NotFoundError` when no User matches the Cognito subject\n * - `ForbiddenError` when the caller has no Membership in the requested\n * `(tenantId, workspaceId)` pair on their user-partition projection\n *\n * @see https://github.com/codedrifters/openhi/issues/769\n * @see https://github.com/codedrifters/openhi/issues/1020\n * @see ADR-018 § Access Pattern Coverage (pattern #4)\n */\nexport async function switchUserTenantWorkspaceOperation(\n params: UserSwitchTenantWorkspaceParams,\n): Promise<UserSwitchTenantWorkspaceResult> {\n const { cognitoSub, tenantReference, workspaceReference, tableName } = params;\n\n const tenantId = idFromReference(tenantReference, \"Tenant/\");\n if (!tenantId) {\n throw new ValidationError(\n \"tenant.reference must be a 'Tenant/<id>' reference.\",\n );\n }\n const workspaceId = idFromReference(workspaceReference, \"Workspace/\");\n if (!workspaceId) {\n throw new ValidationError(\n \"workspace.reference must be a 'Workspace/<id>' reference.\",\n );\n }\n\n const user = await findUserBySubOperation({\n // findUserBySubOperation does not read context fields; pass a stub.\n context: {\n tenantId: \"\",\n workspaceId: \"\",\n date: \"\",\n actorId: \"\",\n actorName: \"\",\n actorType: \"internal-system\",\n },\n cognitoSub,\n tableName,\n });\n if (!user) {\n throw new NotFoundError(\n \"User not yet provisioned for the authenticated Cognito subject.\",\n );\n }\n\n // ADR-018: single Query on the user partition, narrowed to the workspace\n // sub-lane of the requested tenant. The lane includes `workspaceId` on\n // every row; a row with `workspaceId === <requested>` is sufficient and\n // necessary proof that the caller may switch to that pair.\n const projection = await membershipListByUserOperation({\n userId: user.id,\n mode: \"workspaceInTenant\",\n tenantId,\n tableName,\n });\n const hasMembership = projection.items.some(\n (row) => row.workspaceId === workspaceId,\n );\n if (!hasMembership) {\n throw new ForbiddenError(\n `User is not a member of Workspace/${workspaceId} in Tenant/${tenantId}.`,\n );\n }\n\n const existingResource = parseUserResource(user.resource) ?? {};\n const updatedResource: Record<string, unknown> = {\n ...existingResource,\n resourceType: \"User\",\n id: user.id,\n currentTenant: { reference: `Tenant/${tenantId}` },\n currentWorkspace: { reference: `Workspace/${workspaceId}` },\n };\n\n const lastUpdated = (params.now ? params.now() : new Date()).toISOString();\n const vid = `${Date.now()}`;\n const summary = JSON.stringify(\n extractSummary(updatedResource as FhirResourceLike),\n );\n\n const service = getDynamoControlService(tableName);\n await service.entities.user\n .patch({ id: user.id, sk: SK })\n .set({\n resource: JSON.stringify(updatedResource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id: user.id,\n resource: updatedResource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { NotFoundError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserUpdateParams {\n context: OpenHiContext;\n id: string;\n body: { resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface UserUpdateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function updateUserOperation(\n params: UserUpdateParams,\n): Promise<UserUpdateResult> {\n const { context, id, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const existing = await service.entities.user.get({ id, sk: \"CURRENT\" }).go();\n if (!existing.data) {\n throw new NotFoundError(`User not found: ${id}`);\n }\n\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 = `${Date.now()}`;\n\n const resource = { resourceType: \"User\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.user\n .put({\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface UserDeleteParams {\n context: OpenHiContext;\n id: string;\n tableName?: string;\n}\n\nexport async function deleteUserOperation(\n params: UserDeleteParams,\n): Promise<void> {\n const { id, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n await service.entities.user.delete({ id, sk: \"CURRENT\" }).go();\n}\n"],"mappings":"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","names":["extractSummary","extractSummary","extractSummary","extractSummary"]}
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-QJDHVMKT.mjs";
5
5
  import {
6
6
  getDynamoControlService
7
- } from "./chunk-VZCPGQXA.mjs";
7
+ } from "./chunk-EUIP2U5F.mjs";
8
8
 
9
9
  // src/data/operations/control/rename-cascade/rename-cascade-rewrite-chunk-operation.ts
10
10
  var RENAME_CASCADE_MAX_TARGETS_PER_CHUNK = 50;
@@ -63,4 +63,4 @@ export {
63
63
  rewriteRenameCascadeChunkOperation,
64
64
  chunkRenameCascadeTargets
65
65
  };
66
- //# sourceMappingURL=chunk-X5E4YJGZ.mjs.map
66
+ //# sourceMappingURL=chunk-YPTJJ35S.mjs.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ADR-028 atomic counter mutation against a canonical control-plane
3
+ * record. Each call applies one concurrency-safe DynamoDB `ADD` delta
4
+ * (`+1` on a relationship/lifecycle create, `-1` on delete) to a single
5
+ * named counter attribute on the canonical Tenant / Workspace / User row.
6
+ *
7
+ * The mutation goes through ElectroDB's `patch(key).add({ counter })`,
8
+ * which compiles to a DynamoDB `ADD` update expression — atomic, no
9
+ * read-modify-write. `patch` also stamps an `attribute_exists` guard on
10
+ * the partition key, so a counter is never written onto a phantom /
11
+ * deleted canonical record. Using ElectroDB (rather than a raw
12
+ * `UpdateItemCommand`) keeps the composite-key casing in lockstep with
13
+ * the entity definitions.
14
+ *
15
+ * Floor guard contract (ADR-028 § Floor guard): a `-1` against an
16
+ * absent / `0` counter is clamped to a no-op via a `.where(counter > 0)`
17
+ * condition — when the condition fails ElectroDB throws and the
18
+ * operation swallows it, returning `false`. The guard is a safety net,
19
+ * not the source of truth: ADR-028's reconciliation job recomputes the
20
+ * real value from canonical data and owns correctness.
21
+ *
22
+ * @see sites/www-docs/content/packages/@openhi/constructs/data/operations/control/counters/counter-apply-operation.md
23
+ */
24
+ /** Counter attribute names per ADR-028, grouped by the canonical record they live on. */
25
+ declare const TENANT_COUNTERS: readonly ["usersInTenant", "workspacesInTenant"];
26
+ declare const WORKSPACE_COUNTERS: readonly ["usersInWorkspace", "adminUsersInWorkspace", "normalUsersInWorkspace"];
27
+ declare const USER_COUNTERS: readonly ["tenantsForUser", "workspacesForUser"];
28
+ type TenantCounter = (typeof TENANT_COUNTERS)[number];
29
+ type WorkspaceCounter = (typeof WORKSPACE_COUNTERS)[number];
30
+ type UserCounter = (typeof USER_COUNTERS)[number];
31
+ /** The `+1` (create) / `-1` (delete) delta direction. */
32
+ type CounterDelta = 1 | -1;
33
+ /** Which canonical entity the counter lives on. */
34
+ declare const COUNTER_TARGET: {
35
+ readonly Tenant: "Tenant";
36
+ readonly Workspace: "Workspace";
37
+ readonly User: "User";
38
+ };
39
+ type CounterTarget = (typeof COUNTER_TARGET)[keyof typeof COUNTER_TARGET];
40
+ /**
41
+ * A resolved counter mutation: which canonical record (target + the
42
+ * ElectroDB composite-key fields that identify it), the attribute, and
43
+ * the delta. The router emits these; the operation applies them.
44
+ */
45
+ type CounterMutation = {
46
+ readonly target: typeof COUNTER_TARGET.Tenant;
47
+ readonly tenantId: string;
48
+ readonly attribute: TenantCounter;
49
+ readonly delta: CounterDelta;
50
+ } | {
51
+ readonly target: typeof COUNTER_TARGET.Workspace;
52
+ readonly tenantId: string;
53
+ readonly workspaceId: string;
54
+ readonly attribute: WorkspaceCounter;
55
+ readonly delta: CounterDelta;
56
+ } | {
57
+ readonly target: typeof COUNTER_TARGET.User;
58
+ readonly userId: string;
59
+ readonly attribute: UserCounter;
60
+ readonly delta: CounterDelta;
61
+ };
62
+
63
+ export type { CounterMutation as C, CounterTarget as a };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ADR-028 atomic counter mutation against a canonical control-plane
3
+ * record. Each call applies one concurrency-safe DynamoDB `ADD` delta
4
+ * (`+1` on a relationship/lifecycle create, `-1` on delete) to a single
5
+ * named counter attribute on the canonical Tenant / Workspace / User row.
6
+ *
7
+ * The mutation goes through ElectroDB's `patch(key).add({ counter })`,
8
+ * which compiles to a DynamoDB `ADD` update expression — atomic, no
9
+ * read-modify-write. `patch` also stamps an `attribute_exists` guard on
10
+ * the partition key, so a counter is never written onto a phantom /
11
+ * deleted canonical record. Using ElectroDB (rather than a raw
12
+ * `UpdateItemCommand`) keeps the composite-key casing in lockstep with
13
+ * the entity definitions.
14
+ *
15
+ * Floor guard contract (ADR-028 § Floor guard): a `-1` against an
16
+ * absent / `0` counter is clamped to a no-op via a `.where(counter > 0)`
17
+ * condition — when the condition fails ElectroDB throws and the
18
+ * operation swallows it, returning `false`. The guard is a safety net,
19
+ * not the source of truth: ADR-028's reconciliation job recomputes the
20
+ * real value from canonical data and owns correctness.
21
+ *
22
+ * @see sites/www-docs/content/packages/@openhi/constructs/data/operations/control/counters/counter-apply-operation.md
23
+ */
24
+ /** Counter attribute names per ADR-028, grouped by the canonical record they live on. */
25
+ declare const TENANT_COUNTERS: readonly ["usersInTenant", "workspacesInTenant"];
26
+ declare const WORKSPACE_COUNTERS: readonly ["usersInWorkspace", "adminUsersInWorkspace", "normalUsersInWorkspace"];
27
+ declare const USER_COUNTERS: readonly ["tenantsForUser", "workspacesForUser"];
28
+ type TenantCounter = (typeof TENANT_COUNTERS)[number];
29
+ type WorkspaceCounter = (typeof WORKSPACE_COUNTERS)[number];
30
+ type UserCounter = (typeof USER_COUNTERS)[number];
31
+ /** The `+1` (create) / `-1` (delete) delta direction. */
32
+ type CounterDelta = 1 | -1;
33
+ /** Which canonical entity the counter lives on. */
34
+ declare const COUNTER_TARGET: {
35
+ readonly Tenant: "Tenant";
36
+ readonly Workspace: "Workspace";
37
+ readonly User: "User";
38
+ };
39
+ type CounterTarget = (typeof COUNTER_TARGET)[keyof typeof COUNTER_TARGET];
40
+ /**
41
+ * A resolved counter mutation: which canonical record (target + the
42
+ * ElectroDB composite-key fields that identify it), the attribute, and
43
+ * the delta. The router emits these; the operation applies them.
44
+ */
45
+ type CounterMutation = {
46
+ readonly target: typeof COUNTER_TARGET.Tenant;
47
+ readonly tenantId: string;
48
+ readonly attribute: TenantCounter;
49
+ readonly delta: CounterDelta;
50
+ } | {
51
+ readonly target: typeof COUNTER_TARGET.Workspace;
52
+ readonly tenantId: string;
53
+ readonly workspaceId: string;
54
+ readonly attribute: WorkspaceCounter;
55
+ readonly delta: CounterDelta;
56
+ } | {
57
+ readonly target: typeof COUNTER_TARGET.User;
58
+ readonly userId: string;
59
+ readonly attribute: UserCounter;
60
+ readonly delta: CounterDelta;
61
+ };
62
+
63
+ export type { CounterMutation as C, CounterTarget as a };
@@ -0,0 +1,38 @@
1
+ import { WorkflowDedupClient } from '@openhi/workflows';
2
+ import { EventBridgeEvent } from 'aws-lambda';
3
+ import { C as CounterMutation } from './counter-apply-operation-DZM3MIDm.mjs';
4
+
5
+ /**
6
+ * @see sites/www-docs/content/packages/@openhi/constructs/workflows/control-plane/counter-maintenance/counter-maintenance-handler.md
7
+ *
8
+ * ADR-028 counter-maintenance consumer. Invoked once per control-plane
9
+ * domain event on the control event bus. Dedupes the event on
10
+ * `(consumerName, eventId, attempt)` via the TR-015 `WorkflowDedupTable`,
11
+ * then applies the resolved atomic-ADD counter deltas to the affected
12
+ * canonical Tenant / Workspace / User records.
13
+ *
14
+ * Idempotency: an atomic ADD is not idempotent, so a replayed event must
15
+ * be a no-op. `recordIfAbsent` is the circuit-breaker — when it returns
16
+ * `recorded: false` a prior delivery of this exact `(eventId, attempt)`
17
+ * already applied the deltas, so the handler exits without re-applying.
18
+ * The dedup write happens-before the counter mutations, exactly as
19
+ * ADR-028 § Consume requires.
20
+ */
21
+ type CounterMaintenanceEvent = EventBridgeEvent<string, unknown>;
22
+ /** Dependency seam for tests; production wires the real dedup client. */
23
+ interface CounterMaintenanceDependencies {
24
+ readonly dedupClient: WorkflowDedupClient;
25
+ /**
26
+ * Apply one resolved counter mutation. Defaults to
27
+ * {@link applyCounterDeltaOperation}; tests inject a spy.
28
+ */
29
+ readonly applyMutation: (mutation: CounterMutation) => Promise<void>;
30
+ }
31
+ /**
32
+ * Test-visible orchestrator. The production `handler` calls this with
33
+ * real dependencies; unit tests inject fakes.
34
+ */
35
+ declare const runCounterMaintenance: (event: CounterMaintenanceEvent, deps: CounterMaintenanceDependencies) => Promise<void>;
36
+ declare const handler: (event: CounterMaintenanceEvent) => Promise<void>;
37
+
38
+ export { type CounterMaintenanceDependencies, handler, runCounterMaintenance };
@@ -0,0 +1,38 @@
1
+ import { WorkflowDedupClient } from '@openhi/workflows';
2
+ import { EventBridgeEvent } from 'aws-lambda';
3
+ import { C as CounterMutation } from './counter-apply-operation-DZM3MIDm.js';
4
+
5
+ /**
6
+ * @see sites/www-docs/content/packages/@openhi/constructs/workflows/control-plane/counter-maintenance/counter-maintenance-handler.md
7
+ *
8
+ * ADR-028 counter-maintenance consumer. Invoked once per control-plane
9
+ * domain event on the control event bus. Dedupes the event on
10
+ * `(consumerName, eventId, attempt)` via the TR-015 `WorkflowDedupTable`,
11
+ * then applies the resolved atomic-ADD counter deltas to the affected
12
+ * canonical Tenant / Workspace / User records.
13
+ *
14
+ * Idempotency: an atomic ADD is not idempotent, so a replayed event must
15
+ * be a no-op. `recordIfAbsent` is the circuit-breaker — when it returns
16
+ * `recorded: false` a prior delivery of this exact `(eventId, attempt)`
17
+ * already applied the deltas, so the handler exits without re-applying.
18
+ * The dedup write happens-before the counter mutations, exactly as
19
+ * ADR-028 § Consume requires.
20
+ */
21
+ type CounterMaintenanceEvent = EventBridgeEvent<string, unknown>;
22
+ /** Dependency seam for tests; production wires the real dedup client. */
23
+ interface CounterMaintenanceDependencies {
24
+ readonly dedupClient: WorkflowDedupClient;
25
+ /**
26
+ * Apply one resolved counter mutation. Defaults to
27
+ * {@link applyCounterDeltaOperation}; tests inject a spy.
28
+ */
29
+ readonly applyMutation: (mutation: CounterMutation) => Promise<void>;
30
+ }
31
+ /**
32
+ * Test-visible orchestrator. The production `handler` calls this with
33
+ * real dependencies; unit tests inject fakes.
34
+ */
35
+ declare const runCounterMaintenance: (event: CounterMaintenanceEvent, deps: CounterMaintenanceDependencies) => Promise<void>;
36
+ declare const handler: (event: CounterMaintenanceEvent) => Promise<void>;
37
+
38
+ export { type CounterMaintenanceDependencies, handler, runCounterMaintenance };