@openhi/constructs 0.0.177 → 0.0.179

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 (54) hide show
  1. package/lib/{chunk-Z4PZSLYY.mjs → chunk-3M4QTQH6.mjs} +2 -2
  2. package/lib/{chunk-JUSVETWK.mjs → chunk-4LQR32D2.mjs} +38 -40
  3. package/lib/{chunk-JUSVETWK.mjs.map → chunk-4LQR32D2.mjs.map} +1 -1
  4. package/lib/{chunk-XNUCKVSE.mjs → chunk-7GMTHOYF.mjs} +2 -2
  5. package/lib/{chunk-E2OWEBBH.mjs → chunk-DIVYB6GD.mjs} +18 -4
  6. package/lib/chunk-DIVYB6GD.mjs.map +1 -0
  7. package/lib/chunk-F2LY4TEI.mjs +272 -0
  8. package/lib/chunk-F2LY4TEI.mjs.map +1 -0
  9. package/lib/{chunk-GG2WD6TA.mjs → chunk-JJ3AQ6G5.mjs} +9 -3
  10. package/lib/{chunk-GG2WD6TA.mjs.map → chunk-JJ3AQ6G5.mjs.map} +1 -1
  11. package/lib/{chunk-EBB4RNUG.mjs → chunk-PIQISEGW.mjs} +2 -2
  12. package/lib/{chunk-FDBBTNCI.mjs → chunk-Q4KQD2NB.mjs} +117 -5
  13. package/lib/chunk-Q4KQD2NB.mjs.map +1 -0
  14. package/lib/{chunk-Y4RGUAM2.mjs → chunk-V6KLFEHC.mjs} +105 -34
  15. package/lib/chunk-V6KLFEHC.mjs.map +1 -0
  16. package/lib/chunk-VQY57NOV.mjs +60 -0
  17. package/lib/chunk-VQY57NOV.mjs.map +1 -0
  18. package/lib/counter-maintenance.handler.mjs +4 -4
  19. package/lib/counter-reconciliation.handler.js +2 -2
  20. package/lib/counter-reconciliation.handler.js.map +1 -1
  21. package/lib/counter-reconciliation.handler.mjs +9 -267
  22. package/lib/counter-reconciliation.handler.mjs.map +1 -1
  23. package/lib/index.d.mts +117 -2
  24. package/lib/index.d.ts +117 -2
  25. package/lib/index.js +6454 -6243
  26. package/lib/index.js.map +1 -1
  27. package/lib/index.mjs +106 -4
  28. package/lib/index.mjs.map +1 -1
  29. package/lib/pre-token-generation.handler.js +28 -19
  30. package/lib/pre-token-generation.handler.js.map +1 -1
  31. package/lib/pre-token-generation.handler.mjs +4 -5
  32. package/lib/pre-token-generation.handler.mjs.map +1 -1
  33. package/lib/provision-default-workspace.handler.js +22 -19
  34. package/lib/provision-default-workspace.handler.js.map +1 -1
  35. package/lib/provision-default-workspace.handler.mjs +3 -4
  36. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  37. package/lib/rest-api-lambda.handler.js +400 -214
  38. package/lib/rest-api-lambda.handler.js.map +1 -1
  39. package/lib/rest-api-lambda.handler.mjs +243 -171
  40. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  41. package/lib/seed-demo-data.handler.d.mts +19 -0
  42. package/lib/seed-demo-data.handler.d.ts +19 -0
  43. package/lib/seed-demo-data.handler.js +805 -159
  44. package/lib/seed-demo-data.handler.js.map +1 -1
  45. package/lib/seed-demo-data.handler.mjs +8 -4
  46. package/package.json +3 -3
  47. package/lib/chunk-6HGSR3TG.mjs +0 -123
  48. package/lib/chunk-6HGSR3TG.mjs.map +0 -1
  49. package/lib/chunk-E2OWEBBH.mjs.map +0 -1
  50. package/lib/chunk-FDBBTNCI.mjs.map +0 -1
  51. package/lib/chunk-Y4RGUAM2.mjs.map +0 -1
  52. /package/lib/{chunk-Z4PZSLYY.mjs.map → chunk-3M4QTQH6.mjs.map} +0 -0
  53. /package/lib/{chunk-XNUCKVSE.mjs.map → chunk-7GMTHOYF.mjs.map} +0 -0
  54. /package/lib/{chunk-EBB4RNUG.mjs.map → chunk-PIQISEGW.mjs.map} +0 -0
@@ -23,7 +23,7 @@ import {
23
23
  import {
24
24
  DATA_ENTITY_SK,
25
25
  createDataEntityRecord
26
- } from "./chunk-FDBBTNCI.mjs";
26
+ } from "./chunk-Q4KQD2NB.mjs";
27
27
  import {
28
28
  ConflictError,
29
29
  ValidationError
@@ -431,4 +431,4 @@ export {
431
431
  createTenantOperation,
432
432
  createWorkspaceOperation
433
433
  };
434
- //# sourceMappingURL=chunk-Z4PZSLYY.mjs.map
434
+ //# sourceMappingURL=chunk-3M4QTQH6.mjs.map
@@ -1,33 +1,32 @@
1
- import {
2
- buildSkPrefix
3
- } from "./chunk-6HGSR3TG.mjs";
4
1
  import {
5
2
  batchGetWithRetry,
3
+ buildSkPrefix,
6
4
  dispatchListMode
7
- } from "./chunk-FDBBTNCI.mjs";
5
+ } from "./chunk-Q4KQD2NB.mjs";
8
6
  import {
9
7
  SHARD_COUNT,
10
8
  getDynamoControlService
11
9
  } from "./chunk-EUIP2U5F.mjs";
12
10
 
13
- // src/data/operations/control/tenant/tenant-list-operation.ts
11
+ // src/data/operations/control/workspace/workspace-list-operation.ts
14
12
  var SK = "CURRENT";
15
13
  function counterValue(value) {
16
14
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
17
15
  }
18
- async function listTenantsOperation(params) {
19
- const { tableName, mode = "full" } = params;
16
+ async function listWorkspacesOperation(params) {
17
+ const { context, tableName, mode = "full" } = params;
18
+ const { tenantId } = context;
20
19
  const service = getDynamoControlService(tableName);
21
20
  const shardResults = await Promise.all(
22
21
  Array.from(
23
22
  { length: SHARD_COUNT },
24
- (_, shard) => service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go()
23
+ (_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
25
24
  )
26
25
  );
27
26
  return dispatchListMode(mode, shardResults, {
28
27
  hydrate: (orderedIds) => batchGetWithRetry(
29
- service.entities.tenant,
30
- orderedIds.map((id) => ({ tenantId: id, sk: SK }))
28
+ service.entities.workspace,
29
+ orderedIds.map((id) => ({ tenantId, id, sk: SK }))
31
30
  ),
32
31
  getId: (item) => item.id,
33
32
  // FULL mode (admin list default): read the ADR-028 counters off the
@@ -36,48 +35,52 @@ async function listTenantsOperation(params) {
36
35
  buildEntry: (id, item) => ({
37
36
  id,
38
37
  resource: {
39
- resourceType: "Tenant",
38
+ resourceType: "Workspace",
40
39
  id,
41
40
  ...JSON.parse(item.resource),
42
41
  counts: {
43
- usersInTenant: counterValue(item.usersInTenant),
44
- workspacesInTenant: counterValue(item.workspacesInTenant)
42
+ usersInWorkspace: counterValue(item.usersInWorkspace),
43
+ adminUsersInWorkspace: counterValue(item.adminUsersInWorkspace),
44
+ normalUsersInWorkspace: counterValue(item.normalUsersInWorkspace)
45
45
  }
46
46
  }
47
47
  }),
48
- // SUMMARY mode reads only the GSI1 `summary` projection, which does
49
- // not carry the counters; surface zeros so the shape stays uniform.
48
+ // SUMMARY mode reads only the GSI1 `summary` projection (no
49
+ // counters); surface zeros so the shape stays uniform.
50
50
  buildSummaryEntry: (id, parsed) => ({
51
51
  id,
52
52
  resource: {
53
- resourceType: "Tenant",
53
+ resourceType: "Workspace",
54
54
  id,
55
55
  ...parsed,
56
- counts: { usersInTenant: 0, workspacesInTenant: 0 }
56
+ counts: {
57
+ usersInWorkspace: 0,
58
+ adminUsersInWorkspace: 0,
59
+ normalUsersInWorkspace: 0
60
+ }
57
61
  }
58
62
  })
59
63
  });
60
64
  }
61
65
 
62
- // src/data/operations/control/workspace/workspace-list-operation.ts
66
+ // src/data/operations/control/tenant/tenant-list-operation.ts
63
67
  var SK2 = "CURRENT";
64
68
  function counterValue2(value) {
65
69
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
66
70
  }
67
- async function listWorkspacesOperation(params) {
68
- const { context, tableName, mode = "full" } = params;
69
- const { tenantId } = context;
71
+ async function listTenantsOperation(params) {
72
+ const { tableName, mode = "full" } = params;
70
73
  const service = getDynamoControlService(tableName);
71
74
  const shardResults = await Promise.all(
72
75
  Array.from(
73
76
  { length: SHARD_COUNT },
74
- (_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
77
+ (_, shard) => service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go()
75
78
  )
76
79
  );
77
80
  return dispatchListMode(mode, shardResults, {
78
81
  hydrate: (orderedIds) => batchGetWithRetry(
79
- service.entities.workspace,
80
- orderedIds.map((id) => ({ tenantId, id, sk: SK2 }))
82
+ service.entities.tenant,
83
+ orderedIds.map((id) => ({ tenantId: id, sk: SK2 }))
81
84
  ),
82
85
  getId: (item) => item.id,
83
86
  // FULL mode (admin list default): read the ADR-028 counters off the
@@ -86,29 +89,24 @@ async function listWorkspacesOperation(params) {
86
89
  buildEntry: (id, item) => ({
87
90
  id,
88
91
  resource: {
89
- resourceType: "Workspace",
92
+ resourceType: "Tenant",
90
93
  id,
91
94
  ...JSON.parse(item.resource),
92
95
  counts: {
93
- usersInWorkspace: counterValue2(item.usersInWorkspace),
94
- adminUsersInWorkspace: counterValue2(item.adminUsersInWorkspace),
95
- normalUsersInWorkspace: counterValue2(item.normalUsersInWorkspace)
96
+ usersInTenant: counterValue2(item.usersInTenant),
97
+ workspacesInTenant: counterValue2(item.workspacesInTenant)
96
98
  }
97
99
  }
98
100
  }),
99
- // SUMMARY mode reads only the GSI1 `summary` projection (no
100
- // counters); surface zeros so the shape stays uniform.
101
+ // SUMMARY mode reads only the GSI1 `summary` projection, which does
102
+ // not carry the counters; surface zeros so the shape stays uniform.
101
103
  buildSummaryEntry: (id, parsed) => ({
102
104
  id,
103
105
  resource: {
104
- resourceType: "Workspace",
106
+ resourceType: "Tenant",
105
107
  id,
106
108
  ...parsed,
107
- counts: {
108
- usersInWorkspace: 0,
109
- adminUsersInWorkspace: 0,
110
- normalUsersInWorkspace: 0
111
- }
109
+ counts: { usersInTenant: 0, workspacesInTenant: 0 }
112
110
  }
113
111
  })
114
112
  });
@@ -209,10 +207,10 @@ async function roleAssignmentListByWorkspaceOperation(params) {
209
207
  }
210
208
 
211
209
  export {
212
- listTenantsOperation,
213
- listWorkspacesOperation,
214
210
  countMembershipsByUserOperation,
215
211
  membershipListByWorkspaceOperation,
216
- roleAssignmentListByWorkspaceOperation
212
+ roleAssignmentListByWorkspaceOperation,
213
+ listWorkspacesOperation,
214
+ listTenantsOperation
217
215
  };
218
- //# sourceMappingURL=chunk-JUSVETWK.mjs.map
216
+ //# sourceMappingURL=chunk-4LQR32D2.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/data/operations/control/tenant/tenant-list-operation.ts","../src/data/operations/control/workspace/workspace-list-operation.ts","../src/data/operations/control/membership/membership-count-by-user-operation.ts","../src/data/operations/control/membership/membership-list-by-workspace-operation.ts","../src/data/operations/control/roleassignment/roleassignment-list-by-workspace-operation.ts"],"sourcesContent":["import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface ListTenantsParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\n/**\n * ADR-028 denormalized counter shape surfaced on a Tenant list entry's\n * `resource.counts`. Missing counters render as `0` so the admin console\n * never sees `undefined`.\n */\nexport interface TenantCounts {\n usersInTenant: number;\n workspacesInTenant: number;\n}\n\nexport interface TenantListEntry {\n id: string;\n resource: Record<string, unknown> & { counts: TenantCounts };\n}\n\nexport interface ListTenantsResult {\n entries: TenantListEntry[];\n total: number;\n}\n\n/** Coerce a possibly-absent counter attribute to a non-negative number (default 0). */\nfunction counterValue(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Lists all Tenants (platform-wide, no scope filter) via GSI1 (sharded). Tenant uses tenantId\n * as its identity (tenantId === id), so BatchGet keys reuse `id` as `tenantId`. See\n * `dispatchListMode` for the mode contract (#853).\n */\nexport async function listTenantsOperation(\n params: ListTenantsParams,\n): Promise<ListTenantsResult> {\n const { tableName, mode = \"full\" } = params;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go(),\n ),\n );\n\n return dispatchListMode<\n {\n id: string;\n resource: string;\n usersInTenant?: number;\n workspacesInTenant?: number;\n },\n TenantListEntry\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.tenant,\n orderedIds.map((id) => ({ tenantId: id, sk: SK })),\n ) as Promise<\n Array<{\n id: string;\n resource: string;\n usersInTenant?: number;\n workspacesInTenant?: number;\n }>\n >,\n getId: (item) => item.id,\n // FULL mode (admin list default): read the ADR-028 counters off the\n // canonical record hydrated by BatchGet and expose them as\n // `resource.counts`. Missing counters render as 0.\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"Tenant\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n counts: {\n usersInTenant: counterValue(item.usersInTenant),\n workspacesInTenant: counterValue(item.workspacesInTenant),\n },\n },\n }),\n // SUMMARY mode reads only the GSI1 `summary` projection, which does\n // not carry the counters; surface zeros so the shape stays uniform.\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: {\n resourceType: \"Tenant\",\n id,\n ...parsed,\n counts: { usersInTenant: 0, workspacesInTenant: 0 },\n },\n }),\n });\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface ListWorkspacesParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\n/**\n * ADR-028 denormalized counter shape surfaced on a Workspace list\n * entry's `resource.counts`. Missing counters render as `0`.\n */\nexport interface WorkspaceCounts {\n usersInWorkspace: number;\n adminUsersInWorkspace: number;\n normalUsersInWorkspace: number;\n}\n\nexport interface WorkspaceListEntry {\n id: string;\n resource: Record<string, unknown> & { counts: WorkspaceCounts };\n}\n\nexport interface ListWorkspacesResult {\n entries: WorkspaceListEntry[];\n total: number;\n}\n\n/** Coerce a possibly-absent counter attribute to a non-negative number (default 0). */\nfunction counterValue(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Lists all Workspaces for the context tenant via GSI1 (sharded). See `dispatchListMode` for\n * the mode contract (#853).\n */\nexport async function listWorkspacesOperation(\n params: ListWorkspacesParams,\n): Promise<ListWorkspacesResult> {\n const { context, tableName, mode = \"full\" } = params;\n const { tenantId } = context;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.workspace.query\n .gsi1({ tenantId, gsi1Shard: String(shard) })\n .go(),\n ),\n );\n\n return dispatchListMode<\n {\n id: string;\n resource: string;\n usersInWorkspace?: number;\n adminUsersInWorkspace?: number;\n normalUsersInWorkspace?: number;\n },\n WorkspaceListEntry\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.workspace,\n orderedIds.map((id) => ({ tenantId, id, sk: SK })),\n ) as Promise<\n Array<{\n id: string;\n resource: string;\n usersInWorkspace?: number;\n adminUsersInWorkspace?: number;\n normalUsersInWorkspace?: number;\n }>\n >,\n getId: (item) => item.id,\n // FULL mode (admin list default): read the ADR-028 counters off the\n // canonical record hydrated by BatchGet and expose them as\n // `resource.counts`. Missing counters render as 0.\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"Workspace\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n counts: {\n usersInWorkspace: counterValue(item.usersInWorkspace),\n adminUsersInWorkspace: counterValue(item.adminUsersInWorkspace),\n normalUsersInWorkspace: counterValue(item.normalUsersInWorkspace),\n },\n },\n }),\n // SUMMARY mode reads only the GSI1 `summary` projection (no\n // counters); surface zeros so the shape stays uniform.\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: {\n resourceType: \"Workspace\",\n id,\n ...parsed,\n counts: {\n usersInWorkspace: 0,\n adminUsersInWorkspace: 0,\n normalUsersInWorkspace: 0,\n },\n },\n }),\n });\n}\n","import {\n buildSkPrefix,\n type MembershipListByUserMode,\n} from \"./membership-list-by-user-operation\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link countMembershipsByUserOperation}. */\nexport interface CountMembershipsByUserParams {\n readonly userId: string;\n /** Filter mode — see {@link MembershipListByUserMode}. Defaults to `\"all\"`. */\n readonly mode?: MembershipListByUserMode;\n /** Required only when `mode === \"workspaceInTenant\"`. */\n readonly tenantId?: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Count a user's Memberships via the ADR-018 user-partition projection, with the\n * same `SK begins_with` lane discriminator as {@link membershipListByUserOperation}.\n *\n * Pages through every matching projection row (`{ pages: \"all\" }`) projecting only\n * the `membershipId` key, and returns the row count. Backs\n * `GET /User/:id/Membership?_summary=count`.\n *\n * Note: the per-user counts (`tenantsForUser`, `workspacesForUser`) are also\n * maintained as denormalized counters on the User record per ADR-028; this\n * recompute-from-projection count is the FHIR `_summary=count` shape and the\n * authoritative source the counters are reconciled against.\n */\nexport async function countMembershipsByUserOperation(\n params: CountMembershipsByUserParams,\n): Promise<number> {\n const { userId, mode = \"all\", tenantId, tableName } = params;\n\n if (mode === \"workspaceInTenant\" && !tenantId) {\n throw new Error(\n 'countMembershipsByUserOperation: tenantId is required when mode === \"workspaceInTenant\"',\n );\n }\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(mode, tenantId);\n\n const result = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: skPrefix })\n .go({ pages: \"all\", attributes: [\"membershipId\"] });\n\n return (result.data ?? []).length;\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link membershipListByWorkspaceOperation}. */\nexport interface MembershipListByWorkspaceParams {\n readonly tenantId: string;\n readonly workspaceId: string;\n /** ElectroDB cursor from a prior page. Forwarded to `.go({ cursor })`. */\n readonly cursor?: string | null;\n /** Per-page item limit forwarded to `.go({ limit })`. */\n readonly limit?: number;\n /** Sort order forwarded to `.go({ order })`. Defaults to ElectroDB's `\"asc\"`. */\n readonly order?: \"asc\" | \"desc\";\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** One projection-row payload as returned to a consumer. */\nexport interface MembershipWorkspaceProjectionEntry {\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly sk: string;\n readonly userId: string;\n readonly membershipId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedUserName?: string;\n}\n\n/** Page returned by {@link membershipListByWorkspaceOperation}. */\nexport interface MembershipListByWorkspaceResult {\n readonly items: Array<MembershipWorkspaceProjectionEntry>;\n /** ElectroDB cursor for the next page, or `null` when exhausted. */\n readonly cursor: string | null;\n}\n\n/**\n * List Memberships for a workspace via the ADR-018 workspace-partition\n * projection (no GSI hop).\n *\n * Reads `MembershipWorkspaceProjectionEntity` rows under\n * `PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>` with\n * `SK begins_with 'MEMBERSHIP#'`. The projection's SK shape\n * (`MEMBERSHIP#<normalizedUserName>#USER#<userId>#<membershipId>`) sorts\n * members alphabetically by user name within the workspace partition, so\n * the natural Query order is exactly what the access pattern expects —\n * no client-side sort.\n *\n * Returns the projection rows verbatim (`summary`, `vid`, `lastUpdated`\n * plus the projection-discriminating fields) — full canonical-resource\n * hydration is opt-in for callers via\n * `MembershipEntity.get({ tenantId, id: membershipId })`. Pagination\n * mirrors ElectroDB's native `.go({ cursor })` shape; the returned\n * `cursor` is opaque to callers.\n *\n * @see ADR-018 § Access Pattern Coverage (pattern #2)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar)\n */\nexport async function membershipListByWorkspaceOperation(\n params: MembershipListByWorkspaceParams,\n): Promise<MembershipListByWorkspaceResult> {\n const {\n tenantId,\n workspaceId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n const service = getDynamoControlService(tableName);\n\n const goOptions: {\n cursor?: string | null;\n limit?: number;\n order?: \"asc\" | \"desc\";\n } = {\n cursor,\n };\n if (limit !== undefined) {\n goOptions.limit = limit;\n }\n if (order !== undefined) {\n goOptions.order = order;\n }\n\n const result = await service.entities.membershipWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: \"MEMBERSHIP#\" })\n .go(goOptions);\n\n const items: Array<MembershipWorkspaceProjectionEntry> = (\n result.data ?? []\n ).map((row) => ({\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n userId: row.userId,\n membershipId: row.membershipId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedUserName: row.denormalizedUserName,\n }));\n\n return { items, cursor: result.cursor ?? null };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link roleAssignmentListByWorkspaceOperation}. */\nexport interface RoleAssignmentListByWorkspaceParams {\n readonly tenantId: string;\n readonly workspaceId: string;\n /**\n * Optional role discriminator. When supplied, narrows the listing to\n * users assigned to a single role via the discriminator-first\n * `begins_with('ROLEASSIGNMENT#<roleId>#')` prefix — the central read\n * pattern (#9 is \"users with role X in workspace Y\"). Omit to return\n * every role assignment in the workspace interleaved by `<roleId>`.\n */\n readonly roleId?: string;\n /** ElectroDB cursor from a prior page. Forwarded to `.go({ cursor })`. */\n readonly cursor?: string | null;\n /** Per-page item limit forwarded to `.go({ limit })`. */\n readonly limit?: number;\n /** Sort order forwarded to `.go({ order })`. Defaults to ElectroDB's `\"asc\"`. */\n readonly order?: \"asc\" | \"desc\";\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** One projection-row payload as returned to a consumer. */\nexport interface RoleAssignmentWorkspaceProjectionEntry {\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly sk: string;\n readonly userId: string;\n readonly roleId: string;\n readonly roleAssignmentId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedUserName?: string;\n readonly denormalizedRoleName?: string;\n}\n\n/** Page returned by {@link roleAssignmentListByWorkspaceOperation}. */\nexport interface RoleAssignmentListByWorkspaceResult {\n readonly items: Array<RoleAssignmentWorkspaceProjectionEntry>;\n /** ElectroDB cursor for the next page, or `null` when exhausted. */\n readonly cursor: string | null;\n}\n\n/**\n * Compose the SK prefix for the workspace-projection list. The\n * pattern-#9 SK is **discriminator-first on the raw `<roleId>`**\n * (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#<id>`)\n * so a `begins_with('ROLEASSIGNMENT#<roleId>#')` filter returns every\n * user assigned to that role in the workspace. The trailing `#` after\n * the role id is critical — without it `ROLEASSIGNMENT#role-1` would\n * also match `ROLEASSIGNMENT#role-10`, `role-100`, etc. Omitting the\n * `roleId` arg falls back to the wider `ROLEASSIGNMENT#` prefix.\n */\nfunction buildSkPrefix(roleId: string | undefined): string {\n if (roleId === undefined || roleId.length === 0) {\n return \"ROLEASSIGNMENT#\";\n }\n return `ROLEASSIGNMENT#${roleId}#`;\n}\n\n/**\n * List RoleAssignments for a workspace via the ADR-018 workspace-\n * partition projection (no GSI hop).\n *\n * Reads `RoleAssignmentWorkspaceProjectionEntity` rows under\n * `PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>` with an\n * `SK begins_with` filter:\n *\n * | `roleId` arg | SK begins_with | Covers |\n * |---|---|---|\n * | omitted | `ROLEASSIGNMENT#` | Every role assignment in the workspace, interleaved by `<roleId>` |\n * | supplied | `ROLEASSIGNMENT#<roleId>#` | Pattern #9 — every user assigned to that role, sorted alphabetically by `<normalizedUserName>` |\n *\n * The projection's SK shape\n * (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#<id>`)\n * is discriminator-first on the raw `<roleId>` (mirroring the canonical\n * GSI1SK from pattern #8) so the natural Query order is exactly what\n * the access pattern expects — no client-side sort. Tenant-scoped\n * RoleAssignments (no `workspaceId`) skip this projection entirely;\n * they live only in the user-projection's tenant sub-lane.\n *\n * Returns the projection rows verbatim (`summary`, `vid`, `lastUpdated`\n * plus the projection-discriminating fields) — full canonical-resource\n * hydration is opt-in for callers via\n * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`.\n * Pagination mirrors ElectroDB's native `.go({ cursor })` shape; the\n * returned `cursor` is opaque to callers.\n *\n * @see ADR-018 § Access Pattern Coverage (pattern #9)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n */\nexport async function roleAssignmentListByWorkspaceOperation(\n params: RoleAssignmentListByWorkspaceParams,\n): Promise<RoleAssignmentListByWorkspaceResult> {\n const {\n tenantId,\n workspaceId,\n roleId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(roleId);\n\n const goOptions: {\n cursor?: string | null;\n limit?: number;\n order?: \"asc\" | \"desc\";\n } = {\n cursor,\n };\n if (limit !== undefined) {\n goOptions.limit = limit;\n }\n if (order !== undefined) {\n goOptions.order = order;\n }\n\n const result = await service.entities.roleAssignmentWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: skPrefix })\n .go(goOptions);\n\n const items: Array<RoleAssignmentWorkspaceProjectionEntry> = (\n result.data ?? []\n ).map((row) => ({\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n userId: row.userId,\n roleId: row.roleId,\n roleAssignmentId: row.roleAssignmentId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedUserName: row.denormalizedUserName,\n denormalizedRoleName: row.denormalizedRoleName,\n }));\n\n return { items, cursor: result.cursor ?? null };\n}\n"],"mappings":"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","names":["SK","counterValue","buildSkPrefix"]}
1
+ {"version":3,"sources":["../src/data/operations/control/workspace/workspace-list-operation.ts","../src/data/operations/control/tenant/tenant-list-operation.ts","../src/data/operations/control/membership/membership-count-by-user-operation.ts","../src/data/operations/control/membership/membership-list-by-workspace-operation.ts","../src/data/operations/control/roleassignment/roleassignment-list-by-workspace-operation.ts"],"sourcesContent":["import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface ListWorkspacesParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\n/**\n * ADR-028 denormalized counter shape surfaced on a Workspace list\n * entry's `resource.counts`. Missing counters render as `0`.\n */\nexport interface WorkspaceCounts {\n usersInWorkspace: number;\n adminUsersInWorkspace: number;\n normalUsersInWorkspace: number;\n}\n\nexport interface WorkspaceListEntry {\n id: string;\n resource: Record<string, unknown> & { counts: WorkspaceCounts };\n}\n\nexport interface ListWorkspacesResult {\n entries: WorkspaceListEntry[];\n total: number;\n}\n\n/** Coerce a possibly-absent counter attribute to a non-negative number (default 0). */\nfunction counterValue(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Lists all Workspaces for the context tenant via GSI1 (sharded). See `dispatchListMode` for\n * the mode contract (#853).\n */\nexport async function listWorkspacesOperation(\n params: ListWorkspacesParams,\n): Promise<ListWorkspacesResult> {\n const { context, tableName, mode = \"full\" } = params;\n const { tenantId } = context;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.workspace.query\n .gsi1({ tenantId, gsi1Shard: String(shard) })\n .go(),\n ),\n );\n\n return dispatchListMode<\n {\n id: string;\n resource: string;\n usersInWorkspace?: number;\n adminUsersInWorkspace?: number;\n normalUsersInWorkspace?: number;\n },\n WorkspaceListEntry\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.workspace,\n orderedIds.map((id) => ({ tenantId, id, sk: SK })),\n ) as Promise<\n Array<{\n id: string;\n resource: string;\n usersInWorkspace?: number;\n adminUsersInWorkspace?: number;\n normalUsersInWorkspace?: number;\n }>\n >,\n getId: (item) => item.id,\n // FULL mode (admin list default): read the ADR-028 counters off the\n // canonical record hydrated by BatchGet and expose them as\n // `resource.counts`. Missing counters render as 0.\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"Workspace\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n counts: {\n usersInWorkspace: counterValue(item.usersInWorkspace),\n adminUsersInWorkspace: counterValue(item.adminUsersInWorkspace),\n normalUsersInWorkspace: counterValue(item.normalUsersInWorkspace),\n },\n },\n }),\n // SUMMARY mode reads only the GSI1 `summary` projection (no\n // counters); surface zeros so the shape stays uniform.\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: {\n resourceType: \"Workspace\",\n id,\n ...parsed,\n counts: {\n usersInWorkspace: 0,\n adminUsersInWorkspace: 0,\n normalUsersInWorkspace: 0,\n },\n },\n }),\n });\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { SHARD_COUNT } from \"../../../dynamo/shard\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n batchGetWithRetry,\n dispatchListMode,\n type ListOperationMode,\n} from \"../../data-operations-common\";\n\nconst SK = \"CURRENT\";\n\nexport interface ListTenantsParams {\n context: OpenHiContext;\n tableName?: string;\n /** #853: defaults to `\"full\"`. `\"summary\"` skips BatchGet, `\"count\"` returns total only. */\n mode?: ListOperationMode;\n}\n\n/**\n * ADR-028 denormalized counter shape surfaced on a Tenant list entry's\n * `resource.counts`. Missing counters render as `0` so the admin console\n * never sees `undefined`.\n */\nexport interface TenantCounts {\n usersInTenant: number;\n workspacesInTenant: number;\n}\n\nexport interface TenantListEntry {\n id: string;\n resource: Record<string, unknown> & { counts: TenantCounts };\n}\n\nexport interface ListTenantsResult {\n entries: TenantListEntry[];\n total: number;\n}\n\n/** Coerce a possibly-absent counter attribute to a non-negative number (default 0). */\nfunction counterValue(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Lists all Tenants (platform-wide, no scope filter) via GSI1 (sharded). Tenant uses tenantId\n * as its identity (tenantId === id), so BatchGet keys reuse `id` as `tenantId`. See\n * `dispatchListMode` for the mode contract (#853).\n */\nexport async function listTenantsOperation(\n params: ListTenantsParams,\n): Promise<ListTenantsResult> {\n const { tableName, mode = \"full\" } = params;\n const service = getDynamoControlService(tableName);\n\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go(),\n ),\n );\n\n return dispatchListMode<\n {\n id: string;\n resource: string;\n usersInTenant?: number;\n workspacesInTenant?: number;\n },\n TenantListEntry\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.tenant,\n orderedIds.map((id) => ({ tenantId: id, sk: SK })),\n ) as Promise<\n Array<{\n id: string;\n resource: string;\n usersInTenant?: number;\n workspacesInTenant?: number;\n }>\n >,\n getId: (item) => item.id,\n // FULL mode (admin list default): read the ADR-028 counters off the\n // canonical record hydrated by BatchGet and expose them as\n // `resource.counts`. Missing counters render as 0.\n buildEntry: (id, item) => ({\n id,\n resource: {\n resourceType: \"Tenant\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n counts: {\n usersInTenant: counterValue(item.usersInTenant),\n workspacesInTenant: counterValue(item.workspacesInTenant),\n },\n },\n }),\n // SUMMARY mode reads only the GSI1 `summary` projection, which does\n // not carry the counters; surface zeros so the shape stays uniform.\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: {\n resourceType: \"Tenant\",\n id,\n ...parsed,\n counts: { usersInTenant: 0, workspacesInTenant: 0 },\n },\n }),\n });\n}\n","import {\n buildSkPrefix,\n type MembershipListByUserMode,\n} from \"./membership-list-by-user-operation\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link countMembershipsByUserOperation}. */\nexport interface CountMembershipsByUserParams {\n readonly userId: string;\n /** Filter mode — see {@link MembershipListByUserMode}. Defaults to `\"all\"`. */\n readonly mode?: MembershipListByUserMode;\n /** Required only when `mode === \"workspaceInTenant\"`. */\n readonly tenantId?: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Count a user's Memberships via the ADR-018 user-partition projection, with the\n * same `SK begins_with` lane discriminator as {@link membershipListByUserOperation}.\n *\n * Pages through every matching projection row (`{ pages: \"all\" }`) projecting only\n * the `membershipId` key, and returns the row count. Backs\n * `GET /User/:id/Membership?_summary=count`.\n *\n * Note: the per-user counts (`tenantsForUser`, `workspacesForUser`) are also\n * maintained as denormalized counters on the User record per ADR-028; this\n * recompute-from-projection count is the FHIR `_summary=count` shape and the\n * authoritative source the counters are reconciled against.\n */\nexport async function countMembershipsByUserOperation(\n params: CountMembershipsByUserParams,\n): Promise<number> {\n const { userId, mode = \"all\", tenantId, tableName } = params;\n\n if (mode === \"workspaceInTenant\" && !tenantId) {\n throw new Error(\n 'countMembershipsByUserOperation: tenantId is required when mode === \"workspaceInTenant\"',\n );\n }\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(mode, tenantId);\n\n const result = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: skPrefix })\n .go({ pages: \"all\", attributes: [\"membershipId\"] });\n\n return (result.data ?? []).length;\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link membershipListByWorkspaceOperation}. */\nexport interface MembershipListByWorkspaceParams {\n readonly tenantId: string;\n readonly workspaceId: string;\n /** ElectroDB cursor from a prior page. Forwarded to `.go({ cursor })`. */\n readonly cursor?: string | null;\n /** Per-page item limit forwarded to `.go({ limit })`. */\n readonly limit?: number;\n /** Sort order forwarded to `.go({ order })`. Defaults to ElectroDB's `\"asc\"`. */\n readonly order?: \"asc\" | \"desc\";\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** One projection-row payload as returned to a consumer. */\nexport interface MembershipWorkspaceProjectionEntry {\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly sk: string;\n readonly userId: string;\n readonly membershipId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedUserName?: string;\n}\n\n/** Page returned by {@link membershipListByWorkspaceOperation}. */\nexport interface MembershipListByWorkspaceResult {\n readonly items: Array<MembershipWorkspaceProjectionEntry>;\n /** ElectroDB cursor for the next page, or `null` when exhausted. */\n readonly cursor: string | null;\n}\n\n/**\n * List Memberships for a workspace via the ADR-018 workspace-partition\n * projection (no GSI hop).\n *\n * Reads `MembershipWorkspaceProjectionEntity` rows under\n * `PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>` with\n * `SK begins_with 'MEMBERSHIP#'`. The projection's SK shape\n * (`MEMBERSHIP#<normalizedUserName>#USER#<userId>#<membershipId>`) sorts\n * members alphabetically by user name within the workspace partition, so\n * the natural Query order is exactly what the access pattern expects —\n * no client-side sort.\n *\n * Returns the projection rows verbatim (`summary`, `vid`, `lastUpdated`\n * plus the projection-discriminating fields) — full canonical-resource\n * hydration is opt-in for callers via\n * `MembershipEntity.get({ tenantId, id: membershipId })`. Pagination\n * mirrors ElectroDB's native `.go({ cursor })` shape; the returned\n * `cursor` is opaque to callers.\n *\n * @see ADR-018 § Access Pattern Coverage (pattern #2)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar)\n */\nexport async function membershipListByWorkspaceOperation(\n params: MembershipListByWorkspaceParams,\n): Promise<MembershipListByWorkspaceResult> {\n const {\n tenantId,\n workspaceId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n const service = getDynamoControlService(tableName);\n\n const goOptions: {\n cursor?: string | null;\n limit?: number;\n order?: \"asc\" | \"desc\";\n } = {\n cursor,\n };\n if (limit !== undefined) {\n goOptions.limit = limit;\n }\n if (order !== undefined) {\n goOptions.order = order;\n }\n\n const result = await service.entities.membershipWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: \"MEMBERSHIP#\" })\n .go(goOptions);\n\n const items: Array<MembershipWorkspaceProjectionEntry> = (\n result.data ?? []\n ).map((row) => ({\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n userId: row.userId,\n membershipId: row.membershipId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedUserName: row.denormalizedUserName,\n }));\n\n return { items, cursor: result.cursor ?? null };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/** Inputs accepted by {@link roleAssignmentListByWorkspaceOperation}. */\nexport interface RoleAssignmentListByWorkspaceParams {\n readonly tenantId: string;\n readonly workspaceId: string;\n /**\n * Optional role discriminator. When supplied, narrows the listing to\n * users assigned to a single role via the discriminator-first\n * `begins_with('ROLEASSIGNMENT#<roleId>#')` prefix — the central read\n * pattern (#9 is \"users with role X in workspace Y\"). Omit to return\n * every role assignment in the workspace interleaved by `<roleId>`.\n */\n readonly roleId?: string;\n /** ElectroDB cursor from a prior page. Forwarded to `.go({ cursor })`. */\n readonly cursor?: string | null;\n /** Per-page item limit forwarded to `.go({ limit })`. */\n readonly limit?: number;\n /** Sort order forwarded to `.go({ order })`. Defaults to ElectroDB's `\"asc\"`. */\n readonly order?: \"asc\" | \"desc\";\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/** One projection-row payload as returned to a consumer. */\nexport interface RoleAssignmentWorkspaceProjectionEntry {\n readonly tenantId: string;\n readonly workspaceId: string;\n readonly sk: string;\n readonly userId: string;\n readonly roleId: string;\n readonly roleAssignmentId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedUserName?: string;\n readonly denormalizedRoleName?: string;\n}\n\n/** Page returned by {@link roleAssignmentListByWorkspaceOperation}. */\nexport interface RoleAssignmentListByWorkspaceResult {\n readonly items: Array<RoleAssignmentWorkspaceProjectionEntry>;\n /** ElectroDB cursor for the next page, or `null` when exhausted. */\n readonly cursor: string | null;\n}\n\n/**\n * Compose the SK prefix for the workspace-projection list. The\n * pattern-#9 SK is **discriminator-first on the raw `<roleId>`**\n * (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#<id>`)\n * so a `begins_with('ROLEASSIGNMENT#<roleId>#')` filter returns every\n * user assigned to that role in the workspace. The trailing `#` after\n * the role id is critical — without it `ROLEASSIGNMENT#role-1` would\n * also match `ROLEASSIGNMENT#role-10`, `role-100`, etc. Omitting the\n * `roleId` arg falls back to the wider `ROLEASSIGNMENT#` prefix.\n */\nfunction buildSkPrefix(roleId: string | undefined): string {\n if (roleId === undefined || roleId.length === 0) {\n return \"ROLEASSIGNMENT#\";\n }\n return `ROLEASSIGNMENT#${roleId}#`;\n}\n\n/**\n * List RoleAssignments for a workspace via the ADR-018 workspace-\n * partition projection (no GSI hop).\n *\n * Reads `RoleAssignmentWorkspaceProjectionEntity` rows under\n * `PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>` with an\n * `SK begins_with` filter:\n *\n * | `roleId` arg | SK begins_with | Covers |\n * |---|---|---|\n * | omitted | `ROLEASSIGNMENT#` | Every role assignment in the workspace, interleaved by `<roleId>` |\n * | supplied | `ROLEASSIGNMENT#<roleId>#` | Pattern #9 — every user assigned to that role, sorted alphabetically by `<normalizedUserName>` |\n *\n * The projection's SK shape\n * (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#USER#<userId>#<id>`)\n * is discriminator-first on the raw `<roleId>` (mirroring the canonical\n * GSI1SK from pattern #8) so the natural Query order is exactly what\n * the access pattern expects — no client-side sort. Tenant-scoped\n * RoleAssignments (no `workspaceId`) skip this projection entirely;\n * they live only in the user-projection's tenant sub-lane.\n *\n * Returns the projection rows verbatim (`summary`, `vid`, `lastUpdated`\n * plus the projection-discriminating fields) — full canonical-resource\n * hydration is opt-in for callers via\n * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`.\n * Pagination mirrors ElectroDB's native `.go({ cursor })` shape; the\n * returned `cursor` is opaque to callers.\n *\n * @see ADR-018 § Access Pattern Coverage (pattern #9)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar) and § 2 (attribute set)\n */\nexport async function roleAssignmentListByWorkspaceOperation(\n params: RoleAssignmentListByWorkspaceParams,\n): Promise<RoleAssignmentListByWorkspaceResult> {\n const {\n tenantId,\n workspaceId,\n roleId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(roleId);\n\n const goOptions: {\n cursor?: string | null;\n limit?: number;\n order?: \"asc\" | \"desc\";\n } = {\n cursor,\n };\n if (limit !== undefined) {\n goOptions.limit = limit;\n }\n if (order !== undefined) {\n goOptions.order = order;\n }\n\n const result = await service.entities.roleAssignmentWorkspaceProjection.query\n .record({ tenantId, workspaceId })\n .begins({ sk: skPrefix })\n .go(goOptions);\n\n const items: Array<RoleAssignmentWorkspaceProjectionEntry> = (\n result.data ?? []\n ).map((row) => ({\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n sk: row.sk,\n userId: row.userId,\n roleId: row.roleId,\n roleAssignmentId: row.roleAssignmentId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedUserName: row.denormalizedUserName,\n denormalizedRoleName: row.denormalizedRoleName,\n }));\n\n return { items, cursor: result.cursor ?? null };\n}\n"],"mappings":";;;;;;;;;;;AASA,IAAM,KAAK;AA8BX,SAAS,aAAa,OAAwB;AAC5C,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE;AAMA,eAAsB,wBACpB,QAC+B;AAC/B,QAAM,EAAE,SAAS,WAAW,OAAO,OAAO,IAAI;AAC9C,QAAM,EAAE,SAAS,IAAI;AACrB,QAAM,UAAU,wBAAwB,SAAS;AAEjD,QAAM,eAAe,MAAM,QAAQ;AAAA,IACjC,MAAM;AAAA,MAAK,EAAE,QAAQ,YAAY;AAAA,MAAG,CAAC,GAAG,UACtC,QAAQ,SAAS,UAAU,MACxB,KAAK,EAAE,UAAU,WAAW,OAAO,KAAK,EAAE,CAAC,EAC3C,GAAG;AAAA,IACR;AAAA,EACF;AAEA,SAAO,iBASL,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,IASF,OAAO,CAAC,SAAS,KAAK;AAAA;AAAA;AAAA;AAAA,IAItB,YAAY,CAAC,IAAI,UAAU;AAAA,MACzB;AAAA,MACA,UAAU;AAAA,QACR,cAAc;AAAA,QACd;AAAA,QACA,GAAI,KAAK,MAAM,KAAK,QAAQ;AAAA,QAC5B,QAAQ;AAAA,UACN,kBAAkB,aAAa,KAAK,gBAAgB;AAAA,UACpD,uBAAuB,aAAa,KAAK,qBAAqB;AAAA,UAC9D,wBAAwB,aAAa,KAAK,sBAAsB;AAAA,QAClE;AAAA,MACF;AAAA,IACF;AAAA;AAAA;AAAA,IAGA,mBAAmB,CAAC,IAAI,YAAY;AAAA,MAClC;AAAA,MACA,UAAU;AAAA,QACR,cAAc;AAAA,QACd;AAAA,QACA,GAAG;AAAA,QACH,QAAQ;AAAA,UACN,kBAAkB;AAAA,UAClB,uBAAuB;AAAA,UACvB,wBAAwB;AAAA,QAC1B;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;AC7GA,IAAMA,MAAK;AA8BX,SAASC,cAAa,OAAwB;AAC5C,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE;AAOA,eAAsB,qBACpB,QAC4B;AAC5B,QAAM,EAAE,WAAW,OAAO,OAAO,IAAI;AACrC,QAAM,UAAU,wBAAwB,SAAS;AAEjD,QAAM,eAAe,MAAM,QAAQ;AAAA,IACjC,MAAM;AAAA,MAAK,EAAE,QAAQ,YAAY;AAAA,MAAG,CAAC,GAAG,UACtC,QAAQ,SAAS,OAAO,MAAM,KAAK,EAAE,WAAW,OAAO,KAAK,EAAE,CAAC,EAAE,GAAG;AAAA,IACtE;AAAA,EACF;AAEA,SAAO,iBAQL,MAAM,cAAc;AAAA,IACpB,SAAS,CAAC,eACR;AAAA,MACE,QAAQ,SAAS;AAAA,MACjB,WAAW,IAAI,CAAC,QAAQ,EAAE,UAAU,IAAI,IAAID,IAAG,EAAE;AAAA,IACnD;AAAA,IAQF,OAAO,CAAC,SAAS,KAAK;AAAA;AAAA;AAAA;AAAA,IAItB,YAAY,CAAC,IAAI,UAAU;AAAA,MACzB;AAAA,MACA,UAAU;AAAA,QACR,cAAc;AAAA,QACd;AAAA,QACA,GAAI,KAAK,MAAM,KAAK,QAAQ;AAAA,QAC5B,QAAQ;AAAA,UACN,eAAeC,cAAa,KAAK,aAAa;AAAA,UAC9C,oBAAoBA,cAAa,KAAK,kBAAkB;AAAA,QAC1D;AAAA,MACF;AAAA,IACF;AAAA;AAAA;AAAA,IAGA,mBAAmB,CAAC,IAAI,YAAY;AAAA,MAClC;AAAA,MACA,UAAU;AAAA,QACR,cAAc;AAAA,QACd;AAAA,QACA,GAAG;AAAA,QACH,QAAQ,EAAE,eAAe,GAAG,oBAAoB,EAAE;AAAA,MACpD;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;AC/EA,eAAsB,gCACpB,QACiB;AACjB,QAAM,EAAE,QAAQ,OAAO,OAAO,UAAU,UAAU,IAAI;AAEtD,MAAI,SAAS,uBAAuB,CAAC,UAAU;AAC7C,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,wBAAwB,SAAS;AACjD,QAAM,WAAW,cAAc,MAAM,QAAQ;AAE7C,QAAM,SAAS,MAAM,QAAQ,SAAS,yBAAyB,MAC5D,OAAO,EAAE,OAAO,CAAC,EACjB,OAAO,EAAE,IAAI,SAAS,CAAC,EACvB,GAAG,EAAE,OAAO,OAAO,YAAY,CAAC,cAAc,EAAE,CAAC;AAEpD,UAAQ,OAAO,QAAQ,CAAC,GAAG;AAC7B;;;ACQA,eAAsB,mCACpB,QAC0C;AAC1C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,UAAU,wBAAwB,SAAS;AAEjD,QAAM,YAIF;AAAA,IACF;AAAA,EACF;AACA,MAAI,UAAU,QAAW;AACvB,cAAU,QAAQ;AAAA,EACpB;AACA,MAAI,UAAU,QAAW;AACvB,cAAU,QAAQ;AAAA,EACpB;AAEA,QAAM,SAAS,MAAM,QAAQ,SAAS,8BAA8B,MACjE,OAAO,EAAE,UAAU,YAAY,CAAC,EAChC,OAAO,EAAE,IAAI,cAAc,CAAC,EAC5B,GAAG,SAAS;AAEf,QAAM,SACJ,OAAO,QAAQ,CAAC,GAChB,IAAI,CAAC,SAAS;AAAA,IACd,UAAU,IAAI;AAAA,IACd,aAAa,IAAI;AAAA,IACjB,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,cAAc,IAAI;AAAA,IAClB,SAAS,IAAI;AAAA,IACb,KAAK,IAAI;AAAA,IACT,aAAa,IAAI;AAAA,IACjB,sBAAsB,IAAI;AAAA,EAC5B,EAAE;AAEF,SAAO,EAAE,OAAO,QAAQ,OAAO,UAAU,KAAK;AAChD;;;AClDA,SAASC,eAAc,QAAoC;AACzD,MAAI,WAAW,UAAa,OAAO,WAAW,GAAG;AAC/C,WAAO;AAAA,EACT;AACA,SAAO,kBAAkB,MAAM;AACjC;AAiCA,eAAsB,uCACpB,QAC8C;AAC9C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,UAAU,wBAAwB,SAAS;AACjD,QAAM,WAAWA,eAAc,MAAM;AAErC,QAAM,YAIF;AAAA,IACF;AAAA,EACF;AACA,MAAI,UAAU,QAAW;AACvB,cAAU,QAAQ;AAAA,EACpB;AACA,MAAI,UAAU,QAAW;AACvB,cAAU,QAAQ;AAAA,EACpB;AAEA,QAAM,SAAS,MAAM,QAAQ,SAAS,kCAAkC,MACrE,OAAO,EAAE,UAAU,YAAY,CAAC,EAChC,OAAO,EAAE,IAAI,SAAS,CAAC,EACvB,GAAG,SAAS;AAEf,QAAM,SACJ,OAAO,QAAQ,CAAC,GAChB,IAAI,CAAC,SAAS;AAAA,IACd,UAAU,IAAI;AAAA,IACd,aAAa,IAAI;AAAA,IACjB,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,IACZ,kBAAkB,IAAI;AAAA,IACtB,SAAS,IAAI;AAAA,IACb,KAAK,IAAI;AAAA,IACT,aAAa,IAAI;AAAA,IACjB,sBAAsB,IAAI;AAAA,IAC1B,sBAAsB,IAAI;AAAA,EAC5B,EAAE;AAEF,SAAO,EAAE,OAAO,QAAQ,OAAO,UAAU,KAAK;AAChD;","names":["SK","counterValue","buildSkPrefix"]}
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  createDataEntityRecord,
6
6
  mergeAuditIntoMeta
7
- } from "./chunk-FDBBTNCI.mjs";
7
+ } from "./chunk-Q4KQD2NB.mjs";
8
8
  import {
9
9
  NotFoundError
10
10
  } from "./chunk-FYHBHHWK.mjs";
@@ -499,4 +499,4 @@ export {
499
499
  createPractitionerOperation,
500
500
  createProcedureOperation
501
501
  };
502
- //# sourceMappingURL=chunk-XNUCKVSE.mjs.map
502
+ //# sourceMappingURL=chunk-7GMTHOYF.mjs.map
@@ -1,3 +1,6 @@
1
+ import {
2
+ reconcileAllCountersOperation
3
+ } from "./chunk-F2LY4TEI.mjs";
1
4
  import {
2
5
  createAccountOperation,
3
6
  createAppointmentOperation,
@@ -13,14 +16,14 @@ import {
13
16
  createPractitionerOperation,
14
17
  createProcedureOperation,
15
18
  getRoleByIdOperation
16
- } from "./chunk-XNUCKVSE.mjs";
19
+ } from "./chunk-7GMTHOYF.mjs";
17
20
  import {
18
21
  PLATFORM_SCOPE_TENANT_ID,
19
22
  createMembershipOperation,
20
23
  createRoleAssignmentOperation,
21
24
  createTenantOperation,
22
25
  createWorkspaceOperation
23
- } from "./chunk-Z4PZSLYY.mjs";
26
+ } from "./chunk-3M4QTQH6.mjs";
24
27
  import {
25
28
  NotFoundError
26
29
  } from "./chunk-FYHBHHWK.mjs";
@@ -4570,7 +4573,13 @@ var seedWorkspaceDataPlane = async (baseContext, group, failures) => {
4570
4573
  }
4571
4574
  };
4572
4575
  var seedDemoGraph = async (params) => {
4573
- const { baseContext, devUsers, cognito, onSiteTesters = [] } = params;
4576
+ const {
4577
+ baseContext,
4578
+ devUsers,
4579
+ cognito,
4580
+ onSiteTesters = [],
4581
+ reconcileCounters = () => reconcileAllCountersOperation({})
4582
+ } = params;
4574
4583
  const failures = [];
4575
4584
  for (const spec of [...DEMO_TENANT_SPECS, ...ON_SITE_UAT_TENANT_SPECS]) {
4576
4585
  const tenantContext = {
@@ -4744,6 +4753,11 @@ var seedDemoGraph = async (params) => {
4744
4753
  tester.role
4745
4754
  );
4746
4755
  }
4756
+ try {
4757
+ await reconcileCounters();
4758
+ } catch (err) {
4759
+ console.error("seed-demo-data: counter reconciliation failed:", err);
4760
+ }
4747
4761
  for (const group of DEMO_DATA_PLANE_FIXTURES) {
4748
4762
  try {
4749
4763
  await seedWorkspaceDataPlane(baseContext, group, failures);
@@ -5110,4 +5124,4 @@ export {
5110
5124
  productionCognitoProvisioner,
5111
5125
  handler
5112
5126
  };
5113
- //# sourceMappingURL=chunk-E2OWEBBH.mjs.map
5127
+ //# sourceMappingURL=chunk-DIVYB6GD.mjs.map