@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
@@ -0,0 +1,87 @@
1
+ import {
2
+ require_lib
3
+ } from "./chunk-KMEWULMX.mjs";
4
+ import {
5
+ __toESM
6
+ } from "./chunk-LZOMFHX3.mjs";
7
+
8
+ // src/data/operations/control/control-event-publisher.ts
9
+ var import_workflows = __toESM(require_lib());
10
+ import { EventBridgeClient } from "@aws-sdk/client-eventbridge";
11
+ var CONTROL_EVENT_BUS_NAME_ENV_VAR = "CONTROL_EVENT_BUS_NAME";
12
+ var cachedClient;
13
+ function getClient() {
14
+ if (!cachedClient) {
15
+ cachedClient = new EventBridgeClient({
16
+ region: process.env.AWS_REGION ?? "us-east-1"
17
+ });
18
+ }
19
+ return cachedClient;
20
+ }
21
+ function actorFromContext(context) {
22
+ return {
23
+ ohi_tid: context.tenantId,
24
+ ohi_wid: context.workspaceId,
25
+ ohi_uid: context.actorId,
26
+ ohi_uname: context.actorName
27
+ };
28
+ }
29
+ async function publishControlEvent(entry, payload, context) {
30
+ const busName = process.env[CONTROL_EVENT_BUS_NAME_ENV_VAR];
31
+ if (!busName) {
32
+ return;
33
+ }
34
+ try {
35
+ await (0, import_workflows.publishWorkflowEvent)(
36
+ getClient(),
37
+ entry,
38
+ payload,
39
+ { actor: actorFromContext(context) },
40
+ { busNameByPlane: { [import_workflows.OPENHI_CONTROL_SOURCE]: busName } }
41
+ );
42
+ } catch (err) {
43
+ console.error(`control-event publish failed for ${entry.detailType}:`, err);
44
+ }
45
+ }
46
+ async function publishMembershipCreated(context, detail) {
47
+ await publishControlEvent(import_workflows.ControlPlaneMembershipCreatedV1, detail, context);
48
+ }
49
+ async function publishMembershipDeleted(context, detail) {
50
+ await publishControlEvent(import_workflows.ControlPlaneMembershipDeletedV1, detail, context);
51
+ }
52
+ async function publishRoleAssignmentCreated(context, detail) {
53
+ await publishControlEvent(
54
+ import_workflows.ControlPlaneRoleAssignmentCreatedV1,
55
+ detail,
56
+ context
57
+ );
58
+ }
59
+ async function publishRoleAssignmentDeleted(context, detail) {
60
+ await publishControlEvent(
61
+ import_workflows.ControlPlaneRoleAssignmentDeletedV1,
62
+ detail,
63
+ context
64
+ );
65
+ }
66
+ async function publishWorkspaceCreated(context, detail) {
67
+ await publishControlEvent(import_workflows.ControlPlaneWorkspaceCreatedV1, detail, context);
68
+ }
69
+ async function publishWorkspaceDeleted(context, detail) {
70
+ await publishControlEvent(import_workflows.ControlPlaneWorkspaceDeletedV1, detail, context);
71
+ }
72
+ function extractRoleLevel(resource) {
73
+ const code = resource?.code;
74
+ const first = code?.coding?.[0]?.code;
75
+ return typeof first === "string" && first.length > 0 ? first : void 0;
76
+ }
77
+
78
+ export {
79
+ publishMembershipCreated,
80
+ publishMembershipDeleted,
81
+ publishRoleAssignmentCreated,
82
+ publishRoleAssignmentDeleted,
83
+ publishWorkspaceCreated,
84
+ publishWorkspaceDeleted,
85
+ extractRoleLevel
86
+ };
87
+ //# sourceMappingURL=chunk-BUAYVN3C.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data/operations/control/control-event-publisher.ts"],"sourcesContent":["import { EventBridgeClient } from \"@aws-sdk/client-eventbridge\";\nimport {\n ControlPlaneMembershipCreatedV1,\n ControlPlaneMembershipDeletedV1,\n ControlPlaneRoleAssignmentCreatedV1,\n ControlPlaneRoleAssignmentDeletedV1,\n ControlPlaneWorkspaceCreatedV1,\n ControlPlaneWorkspaceDeletedV1,\n OPENHI_CONTROL_SOURCE,\n publishWorkflowEvent,\n type ControlPlaneMembershipChangedV1Detail,\n type ControlPlaneRoleAssignmentChangedV1Detail,\n type ControlPlaneWorkspaceChangedV1Detail,\n type WorkflowActor,\n type WorkflowDetailTypeEntry,\n} from \"@openhi/workflows\";\nimport { OpenHiContext } from \"../../openhi-context\";\n\n/**\n * Env var carrying the name of the control event bus the REST API Lambda\n * publishes control-plane domain events to. Set by\n * {@link RestApiLambda} from the deterministic `ControlEventBus` name.\n * When unset (seed / import / unit-test contexts) the publisher is a no-op,\n * so operations stay usable outside the Lambda and the counter backfill in\n * the reconciliation job (#1319) repairs any rows created without an event.\n */\nexport const CONTROL_EVENT_BUS_NAME_ENV_VAR = \"CONTROL_EVENT_BUS_NAME\";\n\nlet cachedClient: EventBridgeClient | undefined;\n\nfunction getClient(): EventBridgeClient {\n if (!cachedClient) {\n cachedClient = new EventBridgeClient({\n region: process.env.AWS_REGION ?? \"us-east-1\",\n });\n }\n return cachedClient;\n}\n\nfunction actorFromContext(context: OpenHiContext): WorkflowActor {\n return {\n ohi_tid: context.tenantId,\n ohi_wid: context.workspaceId,\n ohi_uid: context.actorId,\n ohi_uname: context.actorName,\n };\n}\n\n/**\n * Publish one control-plane domain event to the control event bus.\n *\n * Best-effort by contract (ADR-028): the canonical multi-write has already\n * committed by the time this runs, the counters it feeds are eventually\n * consistent, and the reconciliation job (#1319) is the correctness backstop.\n * A publish failure is therefore logged and swallowed so it never fails the\n * operation (which would 500 a request whose data write already succeeded).\n * When `CONTROL_EVENT_BUS_NAME` is unset the publish is skipped entirely.\n */\nasync function publishControlEvent<TDetail>(\n entry: WorkflowDetailTypeEntry<TDetail>,\n payload: TDetail,\n context: OpenHiContext,\n): Promise<void> {\n const busName = process.env[CONTROL_EVENT_BUS_NAME_ENV_VAR];\n if (!busName) {\n return;\n }\n try {\n await publishWorkflowEvent(\n getClient(),\n entry,\n payload,\n { actor: actorFromContext(context) },\n { busNameByPlane: { [OPENHI_CONTROL_SOURCE]: busName } },\n );\n } catch (err) {\n console.error(`control-event publish failed for ${entry.detailType}:`, err);\n }\n}\n\n/** Publish `control-plane.membership-created.v1`. */\nexport async function publishMembershipCreated(\n context: OpenHiContext,\n detail: ControlPlaneMembershipChangedV1Detail,\n): Promise<void> {\n await publishControlEvent(ControlPlaneMembershipCreatedV1, detail, context);\n}\n\n/** Publish `control-plane.membership-deleted.v1`. */\nexport async function publishMembershipDeleted(\n context: OpenHiContext,\n detail: ControlPlaneMembershipChangedV1Detail,\n): Promise<void> {\n await publishControlEvent(ControlPlaneMembershipDeletedV1, detail, context);\n}\n\n/** Publish `control-plane.role-assignment-created.v1`. */\nexport async function publishRoleAssignmentCreated(\n context: OpenHiContext,\n detail: ControlPlaneRoleAssignmentChangedV1Detail,\n): Promise<void> {\n await publishControlEvent(\n ControlPlaneRoleAssignmentCreatedV1,\n detail,\n context,\n );\n}\n\n/** Publish `control-plane.role-assignment-deleted.v1`. */\nexport async function publishRoleAssignmentDeleted(\n context: OpenHiContext,\n detail: ControlPlaneRoleAssignmentChangedV1Detail,\n): Promise<void> {\n await publishControlEvent(\n ControlPlaneRoleAssignmentDeletedV1,\n detail,\n context,\n );\n}\n\n/** Publish `control-plane.workspace-created.v1`. */\nexport async function publishWorkspaceCreated(\n context: OpenHiContext,\n detail: ControlPlaneWorkspaceChangedV1Detail,\n): Promise<void> {\n await publishControlEvent(ControlPlaneWorkspaceCreatedV1, detail, context);\n}\n\n/** Publish `control-plane.workspace-deleted.v1`. */\nexport async function publishWorkspaceDeleted(\n context: OpenHiContext,\n detail: ControlPlaneWorkspaceChangedV1Detail,\n): Promise<void> {\n await publishControlEvent(ControlPlaneWorkspaceDeletedV1, detail, context);\n}\n\n/**\n * Extract the ADR-019 organization-role level / type from a RoleAssignment\n * resource so the counter-maintenance consumer can classify a workspace-scoped\n * assignment as admin vs normal without re-reading the Role record. Reads the\n * slim `PractitionerRole.code` coding per ADR-019 §1.2; returns `undefined`\n * when no code is present.\n */\nexport function extractRoleLevel(\n resource: Record<string, unknown> | undefined,\n): string | undefined {\n const code = resource?.code as\n | { coding?: Array<{ code?: unknown }> }\n | undefined;\n const first = code?.coding?.[0]?.code;\n return typeof first === \"string\" && first.length > 0 ? first : undefined;\n}\n"],"mappings":";;;;;;;;AACA,uBAcO;AAfP,SAAS,yBAAyB;AA0B3B,IAAM,iCAAiC;AAE9C,IAAI;AAEJ,SAAS,YAA+B;AACtC,MAAI,CAAC,cAAc;AACjB,mBAAe,IAAI,kBAAkB;AAAA,MACnC,QAAQ,QAAQ,IAAI,cAAc;AAAA,IACpC,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,SAAuC;AAC/D,SAAO;AAAA,IACL,SAAS,QAAQ;AAAA,IACjB,SAAS,QAAQ;AAAA,IACjB,SAAS,QAAQ;AAAA,IACjB,WAAW,QAAQ;AAAA,EACrB;AACF;AAYA,eAAe,oBACb,OACA,SACA,SACe;AACf,QAAM,UAAU,QAAQ,IAAI,8BAA8B;AAC1D,MAAI,CAAC,SAAS;AACZ;AAAA,EACF;AACA,MAAI;AACF,cAAM;AAAA,MACJ,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA,EAAE,OAAO,iBAAiB,OAAO,EAAE;AAAA,MACnC,EAAE,gBAAgB,EAAE,CAAC,sCAAqB,GAAG,QAAQ,EAAE;AAAA,IACzD;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,MAAM,oCAAoC,MAAM,UAAU,KAAK,GAAG;AAAA,EAC5E;AACF;AAGA,eAAsB,yBACpB,SACA,QACe;AACf,QAAM,oBAAoB,kDAAiC,QAAQ,OAAO;AAC5E;AAGA,eAAsB,yBACpB,SACA,QACe;AACf,QAAM,oBAAoB,kDAAiC,QAAQ,OAAO;AAC5E;AAGA,eAAsB,6BACpB,SACA,QACe;AACf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAGA,eAAsB,6BACpB,SACA,QACe;AACf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAGA,eAAsB,wBACpB,SACA,QACe;AACf,QAAM,oBAAoB,iDAAgC,QAAQ,OAAO;AAC3E;AAGA,eAAsB,wBACpB,SACA,QACe;AACf,QAAM,oBAAoB,iDAAgC,QAAQ,OAAO;AAC3E;AASO,SAAS,iBACd,UACoB;AACpB,QAAM,OAAO,UAAU;AAGvB,QAAM,QAAQ,MAAM,SAAS,CAAC,GAAG;AACjC,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;","names":[]}
@@ -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";
@@ -21,4 +21,4 @@ export {
21
21
  RENAME_CASCADE_OPS_EVENT_BUS_ENV_VAR,
22
22
  import_workflows
23
23
  };
24
- //# sourceMappingURL=chunk-23PUSHBV.mjs.map
24
+ //# sourceMappingURL=chunk-D2Y6DDOC.mjs.map
@@ -0,0 +1,123 @@
1
+ import {
2
+ batchGetWithRetry,
3
+ dispatchListMode
4
+ } from "./chunk-O5VQWB6U.mjs";
5
+ import {
6
+ SHARD_COUNT,
7
+ getDynamoControlService
8
+ } from "./chunk-EUIP2U5F.mjs";
9
+
10
+ // src/data/operations/control/user/user-list-operation.ts
11
+ var SK = "CURRENT";
12
+ function counterValue(value) {
13
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
14
+ }
15
+ async function listUsersOperation(params) {
16
+ const { tableName, mode = "full" } = params;
17
+ const service = getDynamoControlService(tableName);
18
+ const shardResults = await Promise.all(
19
+ Array.from(
20
+ { length: SHARD_COUNT },
21
+ (_, shard) => service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go()
22
+ )
23
+ );
24
+ return dispatchListMode(mode, shardResults, {
25
+ hydrate: (orderedIds) => batchGetWithRetry(
26
+ service.entities.user,
27
+ orderedIds.map((id) => ({ id, sk: SK }))
28
+ ),
29
+ getId: (item) => item.id,
30
+ // FULL mode (admin list default): read the ADR-028 counters off the
31
+ // canonical record hydrated by BatchGet and expose them as
32
+ // `resource.counts`. Missing counters render as 0.
33
+ buildEntry: (id, item) => ({
34
+ id,
35
+ resource: {
36
+ resourceType: "User",
37
+ id,
38
+ ...JSON.parse(item.resource),
39
+ counts: {
40
+ tenantsForUser: counterValue(item.tenantsForUser),
41
+ workspacesForUser: counterValue(item.workspacesForUser)
42
+ }
43
+ }
44
+ }),
45
+ // SUMMARY mode reads only the GSI1 `summary` projection (no
46
+ // counters); surface zeros so the shape stays uniform.
47
+ buildSummaryEntry: (id, parsed) => ({
48
+ id,
49
+ resource: {
50
+ resourceType: "User",
51
+ id,
52
+ ...parsed,
53
+ counts: { tenantsForUser: 0, workspacesForUser: 0 }
54
+ }
55
+ })
56
+ });
57
+ }
58
+
59
+ // src/data/operations/control/membership/membership-list-by-user-operation.ts
60
+ function buildSkPrefix(mode, tenantId) {
61
+ switch (mode) {
62
+ case "tenant":
63
+ return "MEMBERSHIP#TENANT#";
64
+ case "workspace":
65
+ return "MEMBERSHIP#WORKSPACE#";
66
+ case "workspaceInTenant":
67
+ return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;
68
+ case "all":
69
+ default:
70
+ return "MEMBERSHIP#";
71
+ }
72
+ }
73
+ async function membershipListByUserOperation(params) {
74
+ const {
75
+ userId,
76
+ mode = "all",
77
+ tenantId,
78
+ cursor = null,
79
+ limit,
80
+ order,
81
+ tableName
82
+ } = params;
83
+ if (mode === "workspaceInTenant" && !tenantId) {
84
+ throw new Error(
85
+ 'membershipListByUserOperation: tenantId is required when mode === "workspaceInTenant"'
86
+ );
87
+ }
88
+ const service = getDynamoControlService(tableName);
89
+ const skPrefix = buildSkPrefix(mode, tenantId);
90
+ const goOptions = {
91
+ cursor
92
+ };
93
+ if (limit !== void 0) {
94
+ goOptions.limit = limit;
95
+ }
96
+ if (order !== void 0) {
97
+ goOptions.order = order;
98
+ }
99
+ const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go(goOptions);
100
+ const items = (result.data ?? []).map(
101
+ (row) => ({
102
+ userId: row.userId,
103
+ sk: row.sk,
104
+ tenantId: row.tenantId,
105
+ workspaceId: row.workspaceId,
106
+ membershipId: row.membershipId,
107
+ summary: row.summary,
108
+ vid: row.vid,
109
+ lastUpdated: row.lastUpdated,
110
+ denormalizedTenantName: row.denormalizedTenantName,
111
+ denormalizedUserName: row.denormalizedUserName,
112
+ denormalizedWorkspaceName: row.denormalizedWorkspaceName
113
+ })
114
+ );
115
+ return { items, cursor: result.cursor ?? null };
116
+ }
117
+
118
+ export {
119
+ buildSkPrefix,
120
+ membershipListByUserOperation,
121
+ listUsersOperation
122
+ };
123
+ //# sourceMappingURL=chunk-DWSWCUZR.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data/operations/control/user/user-list-operation.ts","../src/data/operations/control/membership/membership-list-by-user-operation.ts"],"sourcesContent":["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 UserListParams {\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 User list entry's\n * `resource.counts`. Missing counters render as `0`.\n */\nexport interface UserCounts {\n tenantsForUser: number;\n workspacesForUser: number;\n}\n\nexport interface UserListResult {\n entries: Array<{\n id: string;\n resource: {\n resourceType: string;\n id: string;\n counts: UserCounts;\n [key: string]: unknown;\n };\n }>;\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 Users via GSI1 (sharded). `mode` (default `\"full\"`) selects between BatchGet hydration,\n * summary-only (parse `summary` JSON projected on GSI1), or count-only (skip both). See\n * `dispatchListMode` in data-operations-common for the canonical mode contract.\n */\nexport async function listUsersOperation(\n params: UserListParams,\n): Promise<UserListResult> {\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.user.query.gsi1({ gsi1Shard: String(shard) }).go(),\n ),\n );\n\n return dispatchListMode<\n {\n id: string;\n resource: string;\n tenantsForUser?: number;\n workspacesForUser?: number;\n },\n UserListResult[\"entries\"][number]\n >(mode, shardResults, {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n service.entities.user,\n orderedIds.map((id) => ({ id, sk: SK })),\n ) as Promise<\n Array<{\n id: string;\n resource: string;\n tenantsForUser?: number;\n workspacesForUser?: 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: \"User\",\n id,\n ...(JSON.parse(item.resource) as Record<string, unknown>),\n counts: {\n tenantsForUser: counterValue(item.tenantsForUser),\n workspacesForUser: counterValue(item.workspacesForUser),\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: \"User\",\n id,\n ...parsed,\n counts: { tenantsForUser: 0, workspacesForUser: 0 },\n },\n }),\n });\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\n\n/**\n * Filter modes for {@link membershipListByUserOperation}.\n *\n * Maps directly to the ADR-018 sub-lane discriminator in the user-projection\n * SK (`MEMBERSHIP#TENANT#…` vs `MEMBERSHIP#WORKSPACE#…`):\n *\n * - `\"all\"` — `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')`.\n * Returns both lanes interleaved in raw SK order.\n * - `\"tenant\"` — `SK begins_with 'MEMBERSHIP#TENANT#'`. Pattern #3 only.\n * - `\"workspace\"` — `SK begins_with 'MEMBERSHIP#WORKSPACE#'`. Pattern #4\n * across every tenant.\n * - `\"workspaceInTenant\"` — `SK begins_with 'MEMBERSHIP#WORKSPACE#TID#<tenantId>#'`.\n * Pattern #4 narrowed to one tenant. Requires `tenantId`.\n */\nexport type MembershipListByUserMode =\n | \"all\"\n | \"tenant\"\n | \"workspace\"\n | \"workspaceInTenant\";\n\n/** Inputs accepted by {@link membershipListByUserOperation}. */\nexport interface MembershipListByUserParams {\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 /** 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 MembershipUserProjectionEntry {\n readonly userId: string;\n readonly sk: string;\n readonly tenantId: string;\n readonly workspaceId?: string;\n readonly membershipId: string;\n readonly summary: string;\n readonly vid: string;\n readonly lastUpdated: string;\n readonly denormalizedTenantName?: string;\n readonly denormalizedUserName?: string;\n readonly denormalizedWorkspaceName?: string;\n}\n\n/** Page returned by {@link membershipListByUserOperation}. */\nexport interface MembershipListByUserResult {\n readonly items: Array<MembershipUserProjectionEntry>;\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 a given filter mode. Centralizing the\n * prefix string here keeps the SK grammar (owned by\n * `membership-user-projection.ts`) the single source of truth for the\n * lane discriminators — this function reads them, it does not invent them.\n */\nexport function buildSkPrefix(\n mode: MembershipListByUserMode,\n tenantId: string | undefined,\n): string {\n switch (mode) {\n case \"tenant\":\n return \"MEMBERSHIP#TENANT#\";\n case \"workspace\":\n return \"MEMBERSHIP#WORKSPACE#\";\n case \"workspaceInTenant\":\n // Pattern-#4 SK places `<tenantId>` directly after the\n // `MEMBERSHIP#WORKSPACE#TID#` segment so a `begins_with` filter\n // narrows the workspace lane to a single tenant.\n return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;\n case \"all\":\n default:\n return \"MEMBERSHIP#\";\n }\n}\n\n/**\n * List Memberships for a user via the ADR-018 user-partition projection\n * (no GSI hop).\n *\n * Reads `MembershipUserProjectionEntity` rows under `PK = USER#ID#<userId>`\n * with an `SK begins_with` filter selected by `mode`:\n *\n * | Mode | SK begins_with | Covers |\n * |---|---|---|\n * | `all` (default) | `MEMBERSHIP#` | patterns #3 + #4 interleaved |\n * | `tenant` | `MEMBERSHIP#TENANT#` | pattern #3 only |\n * | `workspace` | `MEMBERSHIP#WORKSPACE#` | pattern #4 only, across tenants |\n * | `workspaceInTenant` | `MEMBERSHIP#WORKSPACE#TID#<tenantId>#` | pattern #4 in one tenant |\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 (patterns #3 and #4)\n * @see .state/adr-018-implementation-guide.md § 1 (SK grammar)\n */\nexport async function membershipListByUserOperation(\n params: MembershipListByUserParams,\n): Promise<MembershipListByUserResult> {\n const {\n userId,\n mode = \"all\",\n tenantId,\n cursor = null,\n limit,\n order,\n tableName,\n } = params;\n\n if (mode === \"workspaceInTenant\" && !tenantId) {\n throw new Error(\n 'membershipListByUserOperation: tenantId is required when mode === \"workspaceInTenant\"',\n );\n }\n\n const service = getDynamoControlService(tableName);\n const skPrefix = buildSkPrefix(mode, tenantId);\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.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: skPrefix })\n .go(goOptions);\n\n const items: Array<MembershipUserProjectionEntry> = (result.data ?? []).map(\n (row) => ({\n userId: row.userId,\n sk: row.sk,\n tenantId: row.tenantId,\n workspaceId: row.workspaceId,\n membershipId: row.membershipId,\n summary: row.summary,\n vid: row.vid,\n lastUpdated: row.lastUpdated,\n denormalizedTenantName: row.denormalizedTenantName,\n denormalizedUserName: row.denormalizedUserName,\n denormalizedWorkspaceName: row.denormalizedWorkspaceName,\n }),\n );\n\n return { items, cursor: result.cursor ?? null };\n}\n"],"mappings":"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","names":[]}
@@ -1436,6 +1436,24 @@ var TenantEntity = new Entity11({
1436
1436
  type: "string",
1437
1437
  required: true
1438
1438
  },
1439
+ /**
1440
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
1441
+ * (users) in this tenant. Maintained by the counter-maintenance
1442
+ * consumer via atomic ADD; absent/0 until first event or reconciliation.
1443
+ */
1444
+ usersInTenant: {
1445
+ type: "number",
1446
+ required: false
1447
+ },
1448
+ /**
1449
+ * ADR-028 denormalized counter — number of Workspaces in this tenant.
1450
+ * Maintained by the counter-maintenance consumer via atomic ADD;
1451
+ * absent/0 until first event or reconciliation.
1452
+ */
1453
+ workspacesInTenant: {
1454
+ type: "number",
1455
+ required: false
1456
+ },
1439
1457
  gsi1Shard: gsi1ShardAttribute,
1440
1458
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1441
1459
  gsi1sk: gsi1skAttribute,
@@ -1540,6 +1558,26 @@ var UserEntity = new Entity12({
1540
1558
  type: "string",
1541
1559
  required: true
1542
1560
  },
1561
+ /**
1562
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
1563
+ * (tenants) this user belongs to. Maintained by the
1564
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
1565
+ * event or reconciliation.
1566
+ */
1567
+ tenantsForUser: {
1568
+ type: "number",
1569
+ required: false
1570
+ },
1571
+ /**
1572
+ * ADR-028 denormalized counter — number of workspace-scoped
1573
+ * Memberships (workspaces) this user belongs to. Maintained by the
1574
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
1575
+ * event or reconciliation.
1576
+ */
1577
+ workspacesForUser: {
1578
+ type: "number",
1579
+ required: false
1580
+ },
1543
1581
  gsi1Shard: gsi1ShardAttribute,
1544
1582
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1545
1583
  gsi1sk: gsi1skAttribute,
@@ -1684,6 +1722,36 @@ var WorkspaceEntity = new Entity13({
1684
1722
  type: "string",
1685
1723
  required: true
1686
1724
  },
1725
+ /**
1726
+ * ADR-028 denormalized counter — number of workspace-scoped
1727
+ * Memberships (users) in this workspace. Maintained by the
1728
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
1729
+ * event or reconciliation.
1730
+ */
1731
+ usersInWorkspace: {
1732
+ type: "number",
1733
+ required: false
1734
+ },
1735
+ /**
1736
+ * ADR-028 denormalized counter — number of workspace-scoped
1737
+ * RoleAssignments classified as admin-tier in this workspace.
1738
+ * Maintained by the counter-maintenance consumer via atomic ADD;
1739
+ * absent/0 until first event or reconciliation.
1740
+ */
1741
+ adminUsersInWorkspace: {
1742
+ type: "number",
1743
+ required: false
1744
+ },
1745
+ /**
1746
+ * ADR-028 denormalized counter — number of workspace-scoped
1747
+ * RoleAssignments classified as non-admin in this workspace.
1748
+ * Maintained by the counter-maintenance consumer via atomic ADD;
1749
+ * absent/0 until first event or reconciliation.
1750
+ */
1751
+ normalUsersInWorkspace: {
1752
+ type: "number",
1753
+ required: false
1754
+ },
1687
1755
  gsi1Shard: gsi1ShardAttribute,
1688
1756
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1689
1757
  gsi1sk: gsi1skAttribute,
@@ -1801,4 +1869,4 @@ export {
1801
1869
  computeShard,
1802
1870
  getDynamoControlService
1803
1871
  };
1804
- //# sourceMappingURL=chunk-VZCPGQXA.mjs.map
1872
+ //# sourceMappingURL=chunk-EUIP2U5F.mjs.map