@openhi/constructs 0.0.103 → 0.0.105

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 (35) hide show
  1. package/README.md +14 -0
  2. package/lib/chunk-2PM2NGXI.mjs +31 -0
  3. package/lib/chunk-2PM2NGXI.mjs.map +1 -0
  4. package/lib/chunk-36YCDLLA.mjs +1258 -0
  5. package/lib/chunk-36YCDLLA.mjs.map +1 -0
  6. package/lib/chunk-BXEG7IOZ.mjs +108 -0
  7. package/lib/chunk-BXEG7IOZ.mjs.map +1 -0
  8. package/lib/chunk-WNUH2WDZ.mjs +45 -0
  9. package/lib/chunk-WNUH2WDZ.mjs.map +1 -0
  10. package/lib/events-CVA3_eEB.d.mts +23 -0
  11. package/lib/events-CVA3_eEB.d.ts +23 -0
  12. package/lib/index.d.mts +92 -21
  13. package/lib/index.d.ts +112 -22
  14. package/lib/index.js +214 -72
  15. package/lib/index.js.map +1 -1
  16. package/lib/index.mjs +190 -74
  17. package/lib/index.mjs.map +1 -1
  18. package/lib/post-confirmation.handler.js +50 -904
  19. package/lib/post-confirmation.handler.js.map +1 -1
  20. package/lib/post-confirmation.handler.mjs +36 -111
  21. package/lib/post-confirmation.handler.mjs.map +1 -1
  22. package/lib/pre-token-generation.handler.js +62 -27
  23. package/lib/pre-token-generation.handler.js.map +1 -1
  24. package/lib/pre-token-generation.handler.mjs +22 -31
  25. package/lib/pre-token-generation.handler.mjs.map +1 -1
  26. package/lib/provision-default-workspace.handler.d.mts +13 -0
  27. package/lib/provision-default-workspace.handler.d.ts +13 -0
  28. package/lib/{chunk-MLTYFMSE.mjs → provision-default-workspace.handler.js} +346 -26
  29. package/lib/provision-default-workspace.handler.js.map +1 -0
  30. package/lib/provision-default-workspace.handler.mjs +173 -0
  31. package/lib/provision-default-workspace.handler.mjs.map +1 -0
  32. package/lib/rest-api-lambda.handler.mjs +40 -546
  33. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  34. package/package.json +2 -2
  35. package/lib/chunk-MLTYFMSE.mjs.map +0 -1
@@ -0,0 +1,1258 @@
1
+ // src/data/dynamo/dynamo-control-service.ts
2
+ import { Service } from "electrodb";
3
+
4
+ // src/data/dynamo/dynamo-client.ts
5
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
6
+ var defaultTableName = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
7
+ var dynamoClient = new DynamoDBClient({
8
+ ...process.env.MOCK_DYNAMODB_ENDPOINT && {
9
+ endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
10
+ sslEnabled: false,
11
+ region: "local"
12
+ }
13
+ });
14
+
15
+ // src/data/dynamo/entities/control/configuration-entity.ts
16
+ import { Entity } from "electrodb";
17
+
18
+ // src/data/dynamo/shard.ts
19
+ var SHARD_COUNT = 4;
20
+ function computeShard(id) {
21
+ let hash = 2166136261;
22
+ for (let i = 0; i < id.length; i++) {
23
+ hash ^= id.charCodeAt(i);
24
+ hash = Math.imul(hash, 16777619);
25
+ }
26
+ return (hash >>> 0) % SHARD_COUNT;
27
+ }
28
+
29
+ // src/data/dynamo/entities/control/control-entity-common.ts
30
+ var gsi1ShardAttribute = {
31
+ type: "string",
32
+ watch: ["id"],
33
+ set: (_val, item) => {
34
+ if (typeof item?.id !== "string" || item.id.length === 0) {
35
+ return void 0;
36
+ }
37
+ return String(computeShard(item.id));
38
+ }
39
+ };
40
+
41
+ // src/data/dynamo/entities/control/configuration-entity.ts
42
+ var ConfigurationEntity = new Entity({
43
+ model: {
44
+ entity: "configuration",
45
+ service: "control",
46
+ version: "01"
47
+ },
48
+ attributes: {
49
+ /** Sort key. "CURRENT" for current version; version history in S3. */
50
+ sk: {
51
+ type: "string",
52
+ required: true,
53
+ default: "CURRENT"
54
+ },
55
+ /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
56
+ tenantId: {
57
+ type: "string",
58
+ required: true,
59
+ default: "BASELINE"
60
+ },
61
+ /** Workspace scope. Use "-" when absent. */
62
+ workspaceId: {
63
+ type: "string",
64
+ required: true,
65
+ default: "-"
66
+ },
67
+ /** User scope. Use "-" when absent. */
68
+ userId: {
69
+ type: "string",
70
+ required: true,
71
+ default: "-"
72
+ },
73
+ /** Role scope. Use "-" when absent. */
74
+ roleId: {
75
+ type: "string",
76
+ required: true,
77
+ default: "-"
78
+ },
79
+ /** Config type (category), e.g. endpoints, branding, display. */
80
+ key: {
81
+ type: "string",
82
+ required: true
83
+ },
84
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
85
+ id: {
86
+ type: "string",
87
+ required: true
88
+ },
89
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
90
+ resource: {
91
+ type: "string",
92
+ required: true
93
+ },
94
+ /**
95
+ * Summary projection (key display fields as JSON string: id, key, status).
96
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
97
+ */
98
+ summary: {
99
+ type: "string",
100
+ required: true
101
+ },
102
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
103
+ vid: {
104
+ type: "string",
105
+ required: true
106
+ },
107
+ lastUpdated: {
108
+ type: "string",
109
+ required: true
110
+ },
111
+ gsi1Shard: gsi1ShardAttribute,
112
+ deleted: {
113
+ type: "boolean",
114
+ required: false
115
+ },
116
+ bundleId: {
117
+ type: "string",
118
+ required: false
119
+ },
120
+ msgId: {
121
+ type: "string",
122
+ required: false
123
+ }
124
+ },
125
+ indexes: {
126
+ /** Base table: PK, SK (data store key names). PK is built from tenantId, workspaceId, userId, roleId; SK is built from key and sk. Do not supply PK or SK from outside. */
127
+ record: {
128
+ pk: {
129
+ field: "PK",
130
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
131
+ template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
132
+ },
133
+ sk: {
134
+ field: "SK",
135
+ composite: ["key", "sk"],
136
+ template: "KEY#${key}#SK#${sk}"
137
+ }
138
+ },
139
+ /**
140
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
141
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
142
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
143
+ * hierarchical resolution in one query; use base table GetItem in fallback order
144
+ * (user → workspace → tenant → baseline) for that.
145
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
146
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
147
+ */
148
+ gsi1: {
149
+ index: "GSI1",
150
+ pk: {
151
+ field: "GSI1PK",
152
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
153
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
154
+ },
155
+ sk: {
156
+ field: "GSI1SK",
157
+ casing: "none",
158
+ composite: ["lastUpdated", "id"],
159
+ template: "${lastUpdated}#${id}"
160
+ }
161
+ }
162
+ }
163
+ });
164
+
165
+ // src/data/dynamo/entities/control/membership-entity.ts
166
+ import { Entity as Entity2 } from "electrodb";
167
+ var MembershipEntity = new Entity2({
168
+ model: {
169
+ entity: "membership",
170
+ service: "control",
171
+ version: "01"
172
+ },
173
+ attributes: {
174
+ /** Sort key sentinel. Always "CURRENT". */
175
+ sk: {
176
+ type: "string",
177
+ required: true,
178
+ default: "CURRENT"
179
+ },
180
+ /** Tenant in which the user has membership (required). */
181
+ tenantId: {
182
+ type: "string",
183
+ required: true
184
+ },
185
+ /** FHIR Resource.id; membership id. */
186
+ id: {
187
+ type: "string",
188
+ required: true
189
+ },
190
+ /** Full Membership resource serialized as JSON string. */
191
+ resource: {
192
+ type: "string",
193
+ required: true
194
+ },
195
+ /**
196
+ * Summary projection (key display fields as JSON string: id, displayName, status).
197
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
198
+ */
199
+ summary: {
200
+ type: "string",
201
+ required: true
202
+ },
203
+ /** Version id (e.g. ULID). */
204
+ vid: {
205
+ type: "string",
206
+ required: true
207
+ },
208
+ lastUpdated: {
209
+ type: "string",
210
+ required: true
211
+ },
212
+ gsi1Shard: gsi1ShardAttribute,
213
+ deleted: {
214
+ type: "boolean",
215
+ required: false
216
+ },
217
+ bundleId: {
218
+ type: "string",
219
+ required: false
220
+ },
221
+ msgId: {
222
+ type: "string",
223
+ required: false
224
+ }
225
+ },
226
+ indexes: {
227
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
228
+ record: {
229
+ pk: {
230
+ field: "PK",
231
+ composite: ["tenantId", "id"],
232
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
233
+ },
234
+ sk: {
235
+ field: "SK",
236
+ composite: ["sk"],
237
+ template: "${sk}"
238
+ }
239
+ },
240
+ /**
241
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
242
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
243
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
244
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
245
+ */
246
+ gsi1: {
247
+ index: "GSI1",
248
+ pk: {
249
+ field: "GSI1PK",
250
+ composite: ["tenantId", "gsi1Shard"],
251
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
252
+ },
253
+ sk: {
254
+ field: "GSI1SK",
255
+ casing: "none",
256
+ composite: ["lastUpdated", "id"],
257
+ template: "${lastUpdated}#${id}"
258
+ }
259
+ }
260
+ }
261
+ });
262
+
263
+ // src/data/dynamo/entities/control/role-entity.ts
264
+ import { Entity as Entity3 } from "electrodb";
265
+ var RoleEntity = new Entity3({
266
+ model: {
267
+ entity: "role",
268
+ service: "control",
269
+ version: "01"
270
+ },
271
+ attributes: {
272
+ /** Sort key sentinel. Always "CURRENT". */
273
+ sk: {
274
+ type: "string",
275
+ required: true,
276
+ default: "CURRENT"
277
+ },
278
+ /** FHIR Resource.id; role id. */
279
+ id: {
280
+ type: "string",
281
+ required: true
282
+ },
283
+ /** Full Role resource serialized as JSON string. */
284
+ resource: {
285
+ type: "string",
286
+ required: true
287
+ },
288
+ /**
289
+ * Summary projection (key display fields as JSON string: id, displayName, status).
290
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
291
+ */
292
+ summary: {
293
+ type: "string",
294
+ required: true
295
+ },
296
+ /** Version id (e.g. ULID). */
297
+ vid: {
298
+ type: "string",
299
+ required: true
300
+ },
301
+ lastUpdated: {
302
+ type: "string",
303
+ required: true
304
+ },
305
+ gsi1Shard: gsi1ShardAttribute,
306
+ deleted: {
307
+ type: "boolean",
308
+ required: false
309
+ },
310
+ bundleId: {
311
+ type: "string",
312
+ required: false
313
+ },
314
+ msgId: {
315
+ type: "string",
316
+ required: false
317
+ }
318
+ },
319
+ indexes: {
320
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
321
+ record: {
322
+ pk: {
323
+ field: "PK",
324
+ composite: ["id"],
325
+ template: "ROLE#ID#${id}"
326
+ },
327
+ sk: {
328
+ field: "SK",
329
+ composite: ["sk"],
330
+ template: "${sk}"
331
+ }
332
+ },
333
+ /**
334
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
335
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
336
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
337
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
338
+ */
339
+ gsi1: {
340
+ index: "GSI1",
341
+ pk: {
342
+ field: "GSI1PK",
343
+ composite: ["gsi1Shard"],
344
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
345
+ },
346
+ sk: {
347
+ field: "GSI1SK",
348
+ casing: "none",
349
+ composite: ["lastUpdated", "id"],
350
+ template: "${lastUpdated}#${id}"
351
+ }
352
+ }
353
+ }
354
+ });
355
+
356
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
357
+ import { Entity as Entity4 } from "electrodb";
358
+ var RoleAssignmentEntity = new Entity4({
359
+ model: {
360
+ entity: "roleassignment",
361
+ service: "control",
362
+ version: "01"
363
+ },
364
+ attributes: {
365
+ /** Sort key sentinel. Always "CURRENT". */
366
+ sk: {
367
+ type: "string",
368
+ required: true,
369
+ default: "CURRENT"
370
+ },
371
+ /** Tenant in which the role assignment applies (required). */
372
+ tenantId: {
373
+ type: "string",
374
+ required: true
375
+ },
376
+ /** FHIR Resource.id; role assignment id. */
377
+ id: {
378
+ type: "string",
379
+ required: true
380
+ },
381
+ /** Full RoleAssignment resource serialized as JSON string. */
382
+ resource: {
383
+ type: "string",
384
+ required: true
385
+ },
386
+ /**
387
+ * Summary projection (key display fields as JSON string: id, displayName, status).
388
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
389
+ */
390
+ summary: {
391
+ type: "string",
392
+ required: true
393
+ },
394
+ /** Version id (e.g. ULID). */
395
+ vid: {
396
+ type: "string",
397
+ required: true
398
+ },
399
+ lastUpdated: {
400
+ type: "string",
401
+ required: true
402
+ },
403
+ gsi1Shard: gsi1ShardAttribute,
404
+ deleted: {
405
+ type: "boolean",
406
+ required: false
407
+ },
408
+ bundleId: {
409
+ type: "string",
410
+ required: false
411
+ },
412
+ msgId: {
413
+ type: "string",
414
+ required: false
415
+ }
416
+ },
417
+ indexes: {
418
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
419
+ record: {
420
+ pk: {
421
+ field: "PK",
422
+ composite: ["tenantId", "id"],
423
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
424
+ },
425
+ sk: {
426
+ field: "SK",
427
+ composite: ["sk"],
428
+ template: "${sk}"
429
+ }
430
+ },
431
+ /**
432
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
433
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
434
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
435
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
436
+ */
437
+ gsi1: {
438
+ index: "GSI1",
439
+ pk: {
440
+ field: "GSI1PK",
441
+ composite: ["tenantId", "gsi1Shard"],
442
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
443
+ },
444
+ sk: {
445
+ field: "GSI1SK",
446
+ casing: "none",
447
+ composite: ["lastUpdated", "id"],
448
+ template: "${lastUpdated}#${id}"
449
+ }
450
+ }
451
+ }
452
+ });
453
+
454
+ // src/data/dynamo/entities/control/tenant-entity.ts
455
+ import { Entity as Entity5 } from "electrodb";
456
+ var TenantEntity = new Entity5({
457
+ model: {
458
+ entity: "tenant",
459
+ service: "control",
460
+ version: "01"
461
+ },
462
+ attributes: {
463
+ /** Sort key sentinel. Always "CURRENT". */
464
+ sk: {
465
+ type: "string",
466
+ required: true,
467
+ default: "CURRENT"
468
+ },
469
+ /** The tenant's own id (= resource id). Drives the partition key. */
470
+ tenantId: {
471
+ type: "string",
472
+ required: true
473
+ },
474
+ /** FHIR Resource.id; logical id in URL. Equals tenantId. */
475
+ id: {
476
+ type: "string",
477
+ required: true
478
+ },
479
+ /** Full Tenant resource serialized as JSON string. */
480
+ resource: {
481
+ type: "string",
482
+ required: true
483
+ },
484
+ /**
485
+ * Summary projection (key display fields as JSON string: id, displayName, status).
486
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
487
+ */
488
+ summary: {
489
+ type: "string",
490
+ required: true
491
+ },
492
+ /** Version id (e.g. ULID). */
493
+ vid: {
494
+ type: "string",
495
+ required: true
496
+ },
497
+ lastUpdated: {
498
+ type: "string",
499
+ required: true
500
+ },
501
+ gsi1Shard: gsi1ShardAttribute,
502
+ deleted: {
503
+ type: "boolean",
504
+ required: false
505
+ },
506
+ bundleId: {
507
+ type: "string",
508
+ required: false
509
+ },
510
+ msgId: {
511
+ type: "string",
512
+ required: false
513
+ }
514
+ },
515
+ indexes: {
516
+ /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
517
+ record: {
518
+ pk: {
519
+ field: "PK",
520
+ composite: ["tenantId"],
521
+ template: "TENANT#ID#${tenantId}"
522
+ },
523
+ sk: {
524
+ field: "SK",
525
+ composite: ["sk"],
526
+ template: "${sk}"
527
+ }
528
+ },
529
+ /**
530
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
531
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
532
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is `<ISO-8601 lastUpdated>#<id>` (control-plane
533
+ * unlabeled per DR-004). `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
534
+ */
535
+ gsi1: {
536
+ index: "GSI1",
537
+ pk: {
538
+ field: "GSI1PK",
539
+ composite: ["gsi1Shard"],
540
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
541
+ },
542
+ sk: {
543
+ field: "GSI1SK",
544
+ casing: "none",
545
+ composite: ["lastUpdated", "id"],
546
+ template: "${lastUpdated}#${id}"
547
+ }
548
+ }
549
+ }
550
+ });
551
+
552
+ // src/data/dynamo/entities/control/user-entity.ts
553
+ import { Entity as Entity6 } from "electrodb";
554
+ var UserEntity = new Entity6({
555
+ model: {
556
+ entity: "user",
557
+ service: "control",
558
+ version: "01"
559
+ },
560
+ attributes: {
561
+ /** Sort key sentinel. Always "CURRENT". */
562
+ sk: {
563
+ type: "string",
564
+ required: true,
565
+ default: "CURRENT"
566
+ },
567
+ /** FHIR Resource.id; platform user id (ohi_uid). */
568
+ id: {
569
+ type: "string",
570
+ required: true
571
+ },
572
+ /** Full User resource serialized as JSON string. */
573
+ resource: {
574
+ type: "string",
575
+ required: true
576
+ },
577
+ /**
578
+ * Summary projection (key display fields as JSON string: id, displayName, status).
579
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
580
+ */
581
+ summary: {
582
+ type: "string",
583
+ required: true
584
+ },
585
+ /**
586
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
587
+ * Post Confirmation Lambda (#770) lands; required thereafter.
588
+ */
589
+ cognitoSub: {
590
+ type: "string",
591
+ required: false
592
+ },
593
+ /** Version id (e.g. ULID). */
594
+ vid: {
595
+ type: "string",
596
+ required: true
597
+ },
598
+ lastUpdated: {
599
+ type: "string",
600
+ required: true
601
+ },
602
+ gsi1Shard: gsi1ShardAttribute,
603
+ deleted: {
604
+ type: "boolean",
605
+ required: false
606
+ },
607
+ bundleId: {
608
+ type: "string",
609
+ required: false
610
+ },
611
+ msgId: {
612
+ type: "string",
613
+ required: false
614
+ }
615
+ },
616
+ indexes: {
617
+ /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
618
+ record: {
619
+ pk: {
620
+ field: "PK",
621
+ composite: ["id"],
622
+ template: "USER#ID#${id}"
623
+ },
624
+ sk: {
625
+ field: "SK",
626
+ composite: ["sk"],
627
+ template: "${sk}"
628
+ }
629
+ },
630
+ /**
631
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
632
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
633
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
634
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z` characters.
635
+ */
636
+ gsi1: {
637
+ index: "GSI1",
638
+ pk: {
639
+ field: "GSI1PK",
640
+ composite: ["gsi1Shard"],
641
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
642
+ },
643
+ sk: {
644
+ field: "GSI1SK",
645
+ casing: "none",
646
+ composite: ["lastUpdated", "id"],
647
+ template: "${lastUpdated}#${id}"
648
+ }
649
+ },
650
+ /**
651
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
652
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
653
+ * not indexed.
654
+ */
655
+ gsi2: {
656
+ index: "GSI2",
657
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
658
+ pk: {
659
+ field: "GSI2PK",
660
+ casing: "none",
661
+ composite: ["cognitoSub"],
662
+ template: "USER#SUB#${cognitoSub}"
663
+ },
664
+ sk: {
665
+ field: "GSI2SK",
666
+ casing: "none",
667
+ composite: [],
668
+ template: "CURRENT"
669
+ }
670
+ }
671
+ }
672
+ });
673
+
674
+ // src/data/dynamo/entities/control/workspace-entity.ts
675
+ import { Entity as Entity7 } from "electrodb";
676
+ var WorkspaceEntity = new Entity7({
677
+ model: {
678
+ entity: "workspace",
679
+ service: "control",
680
+ version: "01"
681
+ },
682
+ attributes: {
683
+ /** Sort key sentinel. Always "CURRENT". */
684
+ sk: {
685
+ type: "string",
686
+ required: true,
687
+ default: "CURRENT"
688
+ },
689
+ /** Tenant that contains this workspace (required). */
690
+ tenantId: {
691
+ type: "string",
692
+ required: true
693
+ },
694
+ /** FHIR Resource.id; logical id in URL. */
695
+ id: {
696
+ type: "string",
697
+ required: true
698
+ },
699
+ /** Full Workspace resource serialized as JSON string. */
700
+ resource: {
701
+ type: "string",
702
+ required: true
703
+ },
704
+ /**
705
+ * Summary projection (key display fields as JSON string: id, displayName, status).
706
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
707
+ */
708
+ summary: {
709
+ type: "string",
710
+ required: true
711
+ },
712
+ /** Version id (e.g. ULID). */
713
+ vid: {
714
+ type: "string",
715
+ required: true
716
+ },
717
+ lastUpdated: {
718
+ type: "string",
719
+ required: true
720
+ },
721
+ gsi1Shard: gsi1ShardAttribute,
722
+ deleted: {
723
+ type: "boolean",
724
+ required: false
725
+ },
726
+ bundleId: {
727
+ type: "string",
728
+ required: false
729
+ },
730
+ msgId: {
731
+ type: "string",
732
+ required: false
733
+ }
734
+ },
735
+ indexes: {
736
+ /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
737
+ record: {
738
+ pk: {
739
+ field: "PK",
740
+ composite: ["tenantId", "id"],
741
+ template: "TID#${tenantId}#WORKSPACE#ID#${id}"
742
+ },
743
+ sk: {
744
+ field: "SK",
745
+ composite: ["sk"],
746
+ template: "${sk}"
747
+ }
748
+ },
749
+ /**
750
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
751
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
752
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
753
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
754
+ */
755
+ gsi1: {
756
+ index: "GSI1",
757
+ pk: {
758
+ field: "GSI1PK",
759
+ composite: ["tenantId", "gsi1Shard"],
760
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
761
+ },
762
+ sk: {
763
+ field: "GSI1SK",
764
+ casing: "none",
765
+ composite: ["lastUpdated", "id"],
766
+ template: "${lastUpdated}#${id}"
767
+ }
768
+ }
769
+ }
770
+ });
771
+
772
+ // src/data/dynamo/dynamo-control-service.ts
773
+ var controlPlaneEntities = {
774
+ configuration: ConfigurationEntity,
775
+ membership: MembershipEntity,
776
+ role: RoleEntity,
777
+ roleAssignment: RoleAssignmentEntity,
778
+ tenant: TenantEntity,
779
+ user: UserEntity,
780
+ workspace: WorkspaceEntity
781
+ };
782
+ var controlPlaneService = new Service(controlPlaneEntities, {
783
+ table: defaultTableName,
784
+ client: dynamoClient
785
+ });
786
+ var DynamoControlService = {
787
+ entities: controlPlaneService.entities
788
+ };
789
+ function getDynamoControlService(tableName) {
790
+ const resolved = tableName ?? defaultTableName;
791
+ const service = new Service(controlPlaneEntities, {
792
+ table: resolved,
793
+ client: dynamoClient
794
+ });
795
+ return {
796
+ entities: service.entities
797
+ };
798
+ }
799
+
800
+ // src/data/operations/control/user/user-create-operation.ts
801
+ import { extractSummary } from "@openhi/types";
802
+ async function createUserOperation(params) {
803
+ const { context, body, tableName } = params;
804
+ const service = getDynamoControlService(tableName);
805
+ const id = body.id ?? `user-${Date.now()}`;
806
+ const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
807
+ const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
808
+ const vid = `1`;
809
+ const resource = { resourceType: "User", id, ...parsedResource };
810
+ const summary = JSON.stringify(extractSummary(resource));
811
+ await service.entities.user.put({
812
+ id,
813
+ resource: JSON.stringify(resource),
814
+ summary,
815
+ vid,
816
+ lastUpdated
817
+ }).go();
818
+ return {
819
+ id,
820
+ resource,
821
+ meta: { lastUpdated, versionId: vid }
822
+ };
823
+ }
824
+
825
+ // src/data/operations/control/user/user-delete-operation.ts
826
+ async function deleteUserOperation(params) {
827
+ const { id, tableName } = params;
828
+ const service = getDynamoControlService(tableName);
829
+ await service.entities.user.delete({ id, sk: "CURRENT" }).go();
830
+ }
831
+
832
+ // src/data/errors/domain-errors.ts
833
+ var DomainError = class extends Error {
834
+ constructor(message, code, options) {
835
+ super(message, options);
836
+ this.name = this.constructor.name;
837
+ this.code = code;
838
+ this.details = options?.details;
839
+ Object.setPrototypeOf(this, new.target.prototype);
840
+ }
841
+ };
842
+ var NotFoundError = class extends DomainError {
843
+ constructor(message, options) {
844
+ super(message, "NOT_FOUND", options);
845
+ }
846
+ };
847
+ var ValidationError = class extends DomainError {
848
+ constructor(message, options) {
849
+ super(message, "VALIDATION", options);
850
+ }
851
+ };
852
+ var ConflictError = class extends DomainError {
853
+ constructor(message, options) {
854
+ super(message, "CONFLICT", options);
855
+ }
856
+ };
857
+ function domainErrorToHttpStatus(err) {
858
+ if (err instanceof NotFoundError) return 404;
859
+ if (err instanceof ValidationError) return 400;
860
+ if (err instanceof ConflictError) return 409;
861
+ return null;
862
+ }
863
+
864
+ // src/data/operations/control/user/user-get-by-id-operation.ts
865
+ async function getUserByIdOperation(params) {
866
+ const { id, tableName } = params;
867
+ const service = getDynamoControlService(tableName);
868
+ const response = await service.entities.user.get({ id, sk: "CURRENT" }).go();
869
+ const item = response.data;
870
+ if (!item) {
871
+ throw new NotFoundError(`User not found: ${id}`);
872
+ }
873
+ const parsedResource = JSON.parse(item.resource);
874
+ return {
875
+ id,
876
+ resource: { resourceType: "User", id, ...parsedResource }
877
+ };
878
+ }
879
+
880
+ // src/data/operations/data-operations-common.ts
881
+ import { extractSortKey, extractSummary as extractSummary2 } from "@openhi/types";
882
+
883
+ // src/lib/compression.ts
884
+ import { gzipSync, gunzipSync } from "zlib";
885
+ var ENVELOPE_VERSION = 1;
886
+ var COMPRESSION_ALGOS = {
887
+ NONE: "none",
888
+ GZIP: "gzip",
889
+ BROTLI: "brotli",
890
+ DEFLATE: "deflate"
891
+ };
892
+ function isEnvelope(obj) {
893
+ return typeof obj === "object" && obj !== null && "v" in obj && "algo" in obj && "payload" in obj && typeof obj.payload === "string";
894
+ }
895
+ function compressResource(jsonString, options) {
896
+ const algo = options?.algo ?? COMPRESSION_ALGOS.GZIP;
897
+ if (algo === COMPRESSION_ALGOS.NONE) {
898
+ const envelope2 = {
899
+ v: ENVELOPE_VERSION,
900
+ algo: COMPRESSION_ALGOS.NONE,
901
+ payload: jsonString
902
+ };
903
+ return JSON.stringify(envelope2);
904
+ }
905
+ const buf = Buffer.from(jsonString, "utf-8");
906
+ const payload = gzipSync(buf).toString("base64");
907
+ const envelope = {
908
+ v: ENVELOPE_VERSION,
909
+ algo: COMPRESSION_ALGOS.GZIP,
910
+ payload
911
+ };
912
+ return JSON.stringify(envelope);
913
+ }
914
+ function decompressResource(compressedOrRaw) {
915
+ try {
916
+ const parsed = JSON.parse(compressedOrRaw);
917
+ if (isEnvelope(parsed)) {
918
+ if (parsed.algo === COMPRESSION_ALGOS.GZIP) {
919
+ const buf = Buffer.from(parsed.payload, "base64");
920
+ return gunzipSync(buf).toString("utf-8");
921
+ }
922
+ if (parsed.algo === COMPRESSION_ALGOS.NONE) {
923
+ return parsed.payload;
924
+ }
925
+ return parsed.payload;
926
+ }
927
+ } catch {
928
+ }
929
+ try {
930
+ const buf = Buffer.from(compressedOrRaw, "base64");
931
+ if (buf.length >= 2 && buf[0] === 31 && buf[1] === 139) {
932
+ return gunzipSync(buf).toString("utf-8");
933
+ }
934
+ } catch {
935
+ }
936
+ return compressedOrRaw;
937
+ }
938
+
939
+ // src/data/audit-meta.ts
940
+ var OPENHI_EXT = "http://openhi.org/fhir/StructureDefinition";
941
+ function mergeAuditIntoMeta(meta, audit) {
942
+ const existing = meta ?? {};
943
+ const ext = [
944
+ ...Array.isArray(existing.extension) ? existing.extension : []
945
+ ];
946
+ const byUrl = new Map(ext.map((e) => [e.url, e]));
947
+ function set(url, value, type) {
948
+ if (value == null) return;
949
+ byUrl.set(url, { url, [type]: value });
950
+ }
951
+ set(`${OPENHI_EXT}/created-date`, audit.createdDate, "valueDateTime");
952
+ set(`${OPENHI_EXT}/created-by-id`, audit.createdById, "valueString");
953
+ set(`${OPENHI_EXT}/created-by-name`, audit.createdByName, "valueString");
954
+ set(`${OPENHI_EXT}/modified-date`, audit.modifiedDate, "valueDateTime");
955
+ set(`${OPENHI_EXT}/modified-by-id`, audit.modifiedById, "valueString");
956
+ set(`${OPENHI_EXT}/modified-by-name`, audit.modifiedByName, "valueString");
957
+ set(`${OPENHI_EXT}/deleted-date`, audit.deletedDate, "valueDateTime");
958
+ set(`${OPENHI_EXT}/deleted-by-id`, audit.deletedById, "valueString");
959
+ set(`${OPENHI_EXT}/deleted-by-name`, audit.deletedByName, "valueString");
960
+ return { ...existing, extension: Array.from(byUrl.values()) };
961
+ }
962
+
963
+ // src/data/operations/data-operations-common.ts
964
+ var DATA_ENTITY_SK = "CURRENT";
965
+ async function getDataEntityById(entity, tenantId, workspaceId, id, resourceLabel) {
966
+ const result = await entity.get({
967
+ tenantId,
968
+ workspaceId,
969
+ id,
970
+ sk: DATA_ENTITY_SK
971
+ }).go();
972
+ if (!result.data) {
973
+ throw new NotFoundError(`${resourceLabel} ${id} not found`, {
974
+ details: { id }
975
+ });
976
+ }
977
+ const parsed = JSON.parse(decompressResource(result.data.resource));
978
+ return {
979
+ id: result.data.id,
980
+ resource: { ...parsed, id: result.data.id }
981
+ };
982
+ }
983
+ async function deleteDataEntityById(entity, tenantId, workspaceId, id) {
984
+ await entity.delete({
985
+ tenantId,
986
+ workspaceId,
987
+ id,
988
+ sk: DATA_ENTITY_SK
989
+ }).go();
990
+ }
991
+ var BATCH_GET_MAX_ATTEMPTS = 3;
992
+ var BATCH_GET_BASE_BACKOFF_MS = 50;
993
+ async function batchGetWithRetry(entity, keys) {
994
+ if (keys.length === 0) return [];
995
+ const collected = [];
996
+ let pending = keys;
997
+ let attempt = 0;
998
+ while (pending.length > 0) {
999
+ if (attempt > 0) {
1000
+ await new Promise(
1001
+ (resolve) => setTimeout(resolve, BATCH_GET_BASE_BACKOFF_MS * 2 ** (attempt - 1))
1002
+ );
1003
+ }
1004
+ attempt++;
1005
+ const result = await entity.get(pending).go();
1006
+ collected.push(...result.data);
1007
+ const unprocessed = result.unprocessed ?? [];
1008
+ if (unprocessed.length === 0) break;
1009
+ if (attempt >= BATCH_GET_MAX_ATTEMPTS) {
1010
+ throw new Error(
1011
+ `BatchGet exhausted retries: ${unprocessed.length} key(s) still unprocessed after ${BATCH_GET_MAX_ATTEMPTS} attempt(s)`
1012
+ );
1013
+ }
1014
+ pending = unprocessed;
1015
+ }
1016
+ return collected;
1017
+ }
1018
+ async function dispatchListMode(mode, shardResults, hooks) {
1019
+ if (mode === "count") {
1020
+ let total = 0;
1021
+ for (const shardResult of shardResults) {
1022
+ total += (shardResult.data ?? []).length;
1023
+ }
1024
+ return { entries: [], total };
1025
+ }
1026
+ if (mode === "summary") {
1027
+ const entries2 = [];
1028
+ for (const shardResult of shardResults) {
1029
+ for (const item of shardResult.data ?? []) {
1030
+ if (typeof item.summary !== "string") continue;
1031
+ let parsed;
1032
+ try {
1033
+ parsed = JSON.parse(item.summary);
1034
+ } catch {
1035
+ continue;
1036
+ }
1037
+ entries2.push(hooks.buildSummaryEntry(item.id, parsed));
1038
+ }
1039
+ }
1040
+ return { entries: entries2, total: entries2.length };
1041
+ }
1042
+ const orderedIds = [];
1043
+ for (const shardResult of shardResults) {
1044
+ for (const item of shardResult.data ?? []) {
1045
+ orderedIds.push(item.id);
1046
+ }
1047
+ }
1048
+ if (orderedIds.length === 0) return { entries: [], total: 0 };
1049
+ const items = await hooks.hydrate(orderedIds);
1050
+ const byId = new Map(items.map((item) => [hooks.getId(item), item]));
1051
+ const entries = [];
1052
+ for (const id of orderedIds) {
1053
+ const item = byId.get(id);
1054
+ if (!item) continue;
1055
+ entries.push(hooks.buildEntry(id, item));
1056
+ }
1057
+ return { entries, total: entries.length };
1058
+ }
1059
+ async function listDataEntitiesByWorkspace(entity, tenantId, workspaceId, mode = "full") {
1060
+ const shardResults = await Promise.all(
1061
+ Array.from(
1062
+ { length: SHARD_COUNT },
1063
+ (_, shard) => entity.query.gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) }).go()
1064
+ )
1065
+ );
1066
+ return dispatchListMode(
1067
+ mode,
1068
+ shardResults,
1069
+ {
1070
+ hydrate: (orderedIds) => batchGetWithRetry(
1071
+ entity,
1072
+ orderedIds.map((id) => ({
1073
+ tenantId,
1074
+ workspaceId,
1075
+ id,
1076
+ sk: DATA_ENTITY_SK
1077
+ }))
1078
+ ),
1079
+ getId: (item) => item.id,
1080
+ buildEntry: (id, item) => {
1081
+ const parsed = JSON.parse(decompressResource(item.resource));
1082
+ return { id, resource: { ...parsed, id } };
1083
+ },
1084
+ buildSummaryEntry: (id, parsed) => ({
1085
+ id,
1086
+ resource: { ...parsed, id }
1087
+ })
1088
+ }
1089
+ );
1090
+ }
1091
+ async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourceWithAudit, fallbackDate) {
1092
+ const lastUpdated = resourceWithAudit.meta?.lastUpdated ?? fallbackDate ?? (/* @__PURE__ */ new Date()).toISOString();
1093
+ const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
1094
+ const resourceLike = resourceWithAudit;
1095
+ const summary = JSON.stringify(extractSummary2(resourceLike));
1096
+ const gsi1sk = extractSortKey(resourceLike);
1097
+ await entity.put({
1098
+ sk: DATA_ENTITY_SK,
1099
+ tenantId,
1100
+ workspaceId,
1101
+ id,
1102
+ resource: compressResource(JSON.stringify(resourceWithAudit)),
1103
+ summary,
1104
+ vid,
1105
+ lastUpdated,
1106
+ gsi1sk
1107
+ }).go();
1108
+ return {
1109
+ id,
1110
+ resource: resourceWithAudit
1111
+ };
1112
+ }
1113
+ function buildUpdatedResourceWithAudit(body, id, date, actorId, actorName, existingResourceStr, resourceType) {
1114
+ const existingMeta = JSON.parse(existingResourceStr).meta;
1115
+ const bodyWithMeta = body;
1116
+ const resourceWithVersion = {
1117
+ ...body,
1118
+ resourceType,
1119
+ id,
1120
+ meta: {
1121
+ ...bodyWithMeta.meta ?? {},
1122
+ lastUpdated: date,
1123
+ versionId: "2"
1124
+ }
1125
+ };
1126
+ const resourceWithAudit = {
1127
+ ...resourceWithVersion,
1128
+ meta: mergeAuditIntoMeta(resourceWithVersion.meta ?? existingMeta, {
1129
+ modifiedDate: date,
1130
+ modifiedById: actorId,
1131
+ modifiedByName: actorName
1132
+ })
1133
+ };
1134
+ return {
1135
+ resource: resourceWithAudit,
1136
+ lastUpdated: date
1137
+ };
1138
+ }
1139
+ async function updateDataEntityById(entity, tenantId, workspaceId, id, resourceLabel, context, buildPatched) {
1140
+ const existing = await entity.get({
1141
+ tenantId,
1142
+ workspaceId,
1143
+ id,
1144
+ sk: DATA_ENTITY_SK
1145
+ }).go();
1146
+ if (!existing.data) {
1147
+ throw new NotFoundError(`${resourceLabel} ${id} not found`, {
1148
+ details: { id }
1149
+ });
1150
+ }
1151
+ const existingStr = decompressResource(existing.data.resource);
1152
+ const { resource, lastUpdated } = buildPatched(existingStr);
1153
+ const resourceLike = resource;
1154
+ const summary = JSON.stringify(extractSummary2(resourceLike));
1155
+ const gsi1sk = extractSortKey(resourceLike);
1156
+ await entity.patch({
1157
+ tenantId,
1158
+ workspaceId,
1159
+ id,
1160
+ sk: DATA_ENTITY_SK
1161
+ }).set({
1162
+ resource: compressResource(JSON.stringify(resource)),
1163
+ summary,
1164
+ lastUpdated,
1165
+ gsi1sk
1166
+ }).go();
1167
+ return {
1168
+ id,
1169
+ resource
1170
+ };
1171
+ }
1172
+
1173
+ // src/data/operations/control/user/user-list-operation.ts
1174
+ var SK = "CURRENT";
1175
+ async function listUsersOperation(params) {
1176
+ const { tableName, mode = "full" } = params;
1177
+ const service = getDynamoControlService(tableName);
1178
+ const shardResults = await Promise.all(
1179
+ Array.from(
1180
+ { length: SHARD_COUNT },
1181
+ (_, shard) => service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go()
1182
+ )
1183
+ );
1184
+ return dispatchListMode(mode, shardResults, {
1185
+ hydrate: (orderedIds) => batchGetWithRetry(
1186
+ service.entities.user,
1187
+ orderedIds.map((id) => ({ id, sk: SK }))
1188
+ ),
1189
+ getId: (item) => item.id,
1190
+ buildEntry: (id, item) => ({
1191
+ id,
1192
+ resource: {
1193
+ resourceType: "User",
1194
+ id,
1195
+ ...JSON.parse(item.resource)
1196
+ }
1197
+ }),
1198
+ buildSummaryEntry: (id, parsed) => ({
1199
+ id,
1200
+ resource: { resourceType: "User", id, ...parsed }
1201
+ })
1202
+ });
1203
+ }
1204
+
1205
+ // src/data/operations/control/user/user-update-operation.ts
1206
+ import { extractSummary as extractSummary3 } from "@openhi/types";
1207
+ async function updateUserOperation(params) {
1208
+ const { context, id, body, tableName } = params;
1209
+ const service = getDynamoControlService(tableName);
1210
+ const existing = await service.entities.user.get({ id, sk: "CURRENT" }).go();
1211
+ if (!existing.data) {
1212
+ throw new NotFoundError(`User not found: ${id}`);
1213
+ }
1214
+ const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1215
+ const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1216
+ const vid = `${Date.now()}`;
1217
+ const resource = { resourceType: "User", id, ...parsedResource };
1218
+ const summary = JSON.stringify(extractSummary3(resource));
1219
+ await service.entities.user.put({
1220
+ id,
1221
+ resource: JSON.stringify(resource),
1222
+ summary,
1223
+ vid,
1224
+ lastUpdated
1225
+ }).go();
1226
+ return {
1227
+ id,
1228
+ resource,
1229
+ meta: { lastUpdated, versionId: vid }
1230
+ };
1231
+ }
1232
+
1233
+ export {
1234
+ compressResource,
1235
+ decompressResource,
1236
+ defaultTableName,
1237
+ dynamoClient,
1238
+ SHARD_COUNT,
1239
+ computeShard,
1240
+ getDynamoControlService,
1241
+ NotFoundError,
1242
+ domainErrorToHttpStatus,
1243
+ mergeAuditIntoMeta,
1244
+ getDataEntityById,
1245
+ deleteDataEntityById,
1246
+ batchGetWithRetry,
1247
+ dispatchListMode,
1248
+ listDataEntitiesByWorkspace,
1249
+ createDataEntityRecord,
1250
+ buildUpdatedResourceWithAudit,
1251
+ updateDataEntityById,
1252
+ createUserOperation,
1253
+ deleteUserOperation,
1254
+ getUserByIdOperation,
1255
+ listUsersOperation,
1256
+ updateUserOperation
1257
+ };
1258
+ //# sourceMappingURL=chunk-36YCDLLA.mjs.map