@openhi/constructs 0.0.85 → 0.0.87

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 (34) hide show
  1. package/lib/{chunk-SWSN6GDD.mjs → chunk-CEOAGPYY.mjs} +1 -5
  2. package/lib/chunk-CEOAGPYY.mjs.map +1 -0
  3. package/lib/chunk-X5MHU7DA.mjs +298 -0
  4. package/lib/chunk-X5MHU7DA.mjs.map +1 -0
  5. package/lib/data-store-postgres-replication.handler.d.mts +55 -0
  6. package/lib/data-store-postgres-replication.handler.d.ts +55 -0
  7. package/lib/data-store-postgres-replication.handler.js +448 -0
  8. package/lib/data-store-postgres-replication.handler.js.map +1 -0
  9. package/lib/data-store-postgres-replication.handler.mjs +313 -0
  10. package/lib/data-store-postgres-replication.handler.mjs.map +1 -0
  11. package/lib/firehose-archive-transform.handler.js +0 -4
  12. package/lib/firehose-archive-transform.handler.js.map +1 -1
  13. package/lib/firehose-archive-transform.handler.mjs +5 -290
  14. package/lib/firehose-archive-transform.handler.mjs.map +1 -1
  15. package/lib/index.d.mts +230 -5
  16. package/lib/index.d.ts +231 -6
  17. package/lib/index.js +489 -117
  18. package/lib/index.js.map +1 -1
  19. package/lib/index.mjs +468 -97
  20. package/lib/index.mjs.map +1 -1
  21. package/lib/post-authentication.handler.d.mts +5 -0
  22. package/lib/post-authentication.handler.d.ts +5 -0
  23. package/lib/post-authentication.handler.js +45 -0
  24. package/lib/post-authentication.handler.js.map +1 -0
  25. package/lib/post-authentication.handler.mjs +25 -0
  26. package/lib/post-authentication.handler.mjs.map +1 -0
  27. package/lib/rest-api-lambda.handler.js +636 -153
  28. package/lib/rest-api-lambda.handler.js.map +1 -1
  29. package/lib/rest-api-lambda.handler.mjs +639 -153
  30. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  31. package/package.json +20 -11
  32. package/scripts/generate-operations.js +2 -2
  33. package/scripts/generate-routes.js +1 -1
  34. package/lib/chunk-SWSN6GDD.mjs.map +0 -1
@@ -172,6 +172,31 @@ var dynamoClient = new import_client_dynamodb.DynamoDBClient({
172
172
 
173
173
  // src/data/dynamo/entities/control/configuration-entity.ts
174
174
  var import_electrodb = require("electrodb");
175
+
176
+ // src/data/dynamo/shard.ts
177
+ var SHARD_COUNT = 4;
178
+ function computeShard(id) {
179
+ let hash = 2166136261;
180
+ for (let i = 0; i < id.length; i++) {
181
+ hash ^= id.charCodeAt(i);
182
+ hash = Math.imul(hash, 16777619);
183
+ }
184
+ return (hash >>> 0) % SHARD_COUNT;
185
+ }
186
+
187
+ // src/data/dynamo/entities/control/control-entity-common.ts
188
+ var gsi1ShardAttribute = {
189
+ type: "string",
190
+ watch: ["id"],
191
+ set: (_val, item) => {
192
+ if (typeof item?.id !== "string" || item.id.length === 0) {
193
+ return void 0;
194
+ }
195
+ return String(computeShard(item.id));
196
+ }
197
+ };
198
+
199
+ // src/data/dynamo/entities/control/configuration-entity.ts
175
200
  var ConfigurationEntity = new import_electrodb.Entity({
176
201
  model: {
177
202
  entity: "configuration",
@@ -224,6 +249,14 @@ var ConfigurationEntity = new import_electrodb.Entity({
224
249
  type: "string",
225
250
  required: true
226
251
  },
252
+ /**
253
+ * Summary projection (key display fields as JSON string: id, key, status).
254
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
255
+ */
256
+ summary: {
257
+ type: "string",
258
+ required: true
259
+ },
227
260
  /** Version id (e.g. ULID). Tracks current version; S3 history key. */
228
261
  vid: {
229
262
  type: "string",
@@ -233,6 +266,7 @@ var ConfigurationEntity = new import_electrodb.Entity({
233
266
  type: "string",
234
267
  required: true
235
268
  },
269
+ gsi1Shard: gsi1ShardAttribute,
236
270
  deleted: {
237
271
  type: "boolean",
238
272
  required: false
@@ -260,19 +294,27 @@ var ConfigurationEntity = new import_electrodb.Entity({
260
294
  template: "KEY#${key}#SK#${sk}"
261
295
  }
262
296
  },
263
- /** GSI4 — Resource Type Index: list all Configuration in a tenant or workspace (no scan). Use for "list configs scoped to this tenant" (workspaceId = "-") or "list configs scoped to this workspace". Does not support hierarchical resolution in one query; use base table GetItem in fallback order (user → workspace → tenant → baseline) for that. */
264
- gsi4: {
265
- index: "GSI4",
266
- condition: () => true,
297
+ /**
298
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
299
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
300
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
301
+ * hierarchical resolution in one query; use base table GetItem in fallback order
302
+ * (user → workspace → tenant → baseline) for that.
303
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
304
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
305
+ */
306
+ gsi1: {
307
+ index: "GSI1",
267
308
  pk: {
268
- field: "GSI4PK",
269
- composite: ["tenantId", "workspaceId"],
270
- template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration"
309
+ field: "GSI1PK",
310
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
311
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
271
312
  },
272
313
  sk: {
273
- field: "GSI4SK",
274
- composite: ["key", "sk"],
275
- template: "KEY#${key}#SK#${sk}"
314
+ field: "GSI1SK",
315
+ casing: "none",
316
+ composite: ["lastUpdated", "id"],
317
+ template: "${lastUpdated}#${id}"
276
318
  }
277
319
  }
278
320
  }
@@ -308,6 +350,14 @@ var MembershipEntity = new import_electrodb2.Entity({
308
350
  type: "string",
309
351
  required: true
310
352
  },
353
+ /**
354
+ * Summary projection (key display fields as JSON string: id, displayName, status).
355
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
356
+ */
357
+ summary: {
358
+ type: "string",
359
+ required: true
360
+ },
311
361
  /** Version id (e.g. ULID). */
312
362
  vid: {
313
363
  type: "string",
@@ -317,6 +367,7 @@ var MembershipEntity = new import_electrodb2.Entity({
317
367
  type: "string",
318
368
  required: true
319
369
  },
370
+ gsi1Shard: gsi1ShardAttribute,
320
371
  deleted: {
321
372
  type: "boolean",
322
373
  required: false
@@ -344,19 +395,24 @@ var MembershipEntity = new import_electrodb2.Entity({
344
395
  template: "${sk}"
345
396
  }
346
397
  },
347
- /** GSI4 — Resource Type Index: list all Memberships for a tenant (no scan). */
348
- gsi4: {
349
- index: "GSI4",
350
- condition: () => true,
398
+ /**
399
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
400
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
401
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
402
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
403
+ */
404
+ gsi1: {
405
+ index: "GSI1",
351
406
  pk: {
352
- field: "GSI4PK",
353
- composite: ["tenantId"],
354
- template: "TID#${tenantId}#RT#Membership"
407
+ field: "GSI1PK",
408
+ composite: ["tenantId", "gsi1Shard"],
409
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
355
410
  },
356
411
  sk: {
357
- field: "GSI4SK",
358
- composite: ["id"],
359
- template: "ID#${id}"
412
+ field: "GSI1SK",
413
+ casing: "none",
414
+ composite: ["lastUpdated", "id"],
415
+ template: "${lastUpdated}#${id}"
360
416
  }
361
417
  }
362
418
  }
@@ -387,6 +443,14 @@ var RoleEntity = new import_electrodb3.Entity({
387
443
  type: "string",
388
444
  required: true
389
445
  },
446
+ /**
447
+ * Summary projection (key display fields as JSON string: id, displayName, status).
448
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
449
+ */
450
+ summary: {
451
+ type: "string",
452
+ required: true
453
+ },
390
454
  /** Version id (e.g. ULID). */
391
455
  vid: {
392
456
  type: "string",
@@ -396,6 +460,7 @@ var RoleEntity = new import_electrodb3.Entity({
396
460
  type: "string",
397
461
  required: true
398
462
  },
463
+ gsi1Shard: gsi1ShardAttribute,
399
464
  deleted: {
400
465
  type: "boolean",
401
466
  required: false
@@ -423,19 +488,24 @@ var RoleEntity = new import_electrodb3.Entity({
423
488
  template: "${sk}"
424
489
  }
425
490
  },
426
- /** GSI4 — Resource Type Index: list all Roles (no scan). */
427
- gsi4: {
428
- index: "GSI4",
429
- condition: () => true,
491
+ /**
492
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
493
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
494
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
495
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
496
+ */
497
+ gsi1: {
498
+ index: "GSI1",
430
499
  pk: {
431
- field: "GSI4PK",
432
- composite: [],
433
- template: "RT#Role"
500
+ field: "GSI1PK",
501
+ composite: ["gsi1Shard"],
502
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
434
503
  },
435
504
  sk: {
436
- field: "GSI4SK",
437
- composite: ["id"],
438
- template: "ID#${id}"
505
+ field: "GSI1SK",
506
+ casing: "none",
507
+ composite: ["lastUpdated", "id"],
508
+ template: "${lastUpdated}#${id}"
439
509
  }
440
510
  }
441
511
  }
@@ -471,6 +541,14 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
471
541
  type: "string",
472
542
  required: true
473
543
  },
544
+ /**
545
+ * Summary projection (key display fields as JSON string: id, displayName, status).
546
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
547
+ */
548
+ summary: {
549
+ type: "string",
550
+ required: true
551
+ },
474
552
  /** Version id (e.g. ULID). */
475
553
  vid: {
476
554
  type: "string",
@@ -480,6 +558,7 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
480
558
  type: "string",
481
559
  required: true
482
560
  },
561
+ gsi1Shard: gsi1ShardAttribute,
483
562
  deleted: {
484
563
  type: "boolean",
485
564
  required: false
@@ -507,19 +586,24 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
507
586
  template: "${sk}"
508
587
  }
509
588
  },
510
- /** GSI4 — Resource Type Index: list all RoleAssignments for a tenant (no scan). */
511
- gsi4: {
512
- index: "GSI4",
513
- condition: () => true,
589
+ /**
590
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
591
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
592
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
593
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
594
+ */
595
+ gsi1: {
596
+ index: "GSI1",
514
597
  pk: {
515
- field: "GSI4PK",
516
- composite: ["tenantId"],
517
- template: "TID#${tenantId}#RT#RoleAssignment"
598
+ field: "GSI1PK",
599
+ composite: ["tenantId", "gsi1Shard"],
600
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
518
601
  },
519
602
  sk: {
520
- field: "GSI4SK",
521
- composite: ["id"],
522
- template: "ID#${id}"
603
+ field: "GSI1SK",
604
+ casing: "none",
605
+ composite: ["lastUpdated", "id"],
606
+ template: "${lastUpdated}#${id}"
523
607
  }
524
608
  }
525
609
  }
@@ -555,6 +639,14 @@ var TenantEntity = new import_electrodb5.Entity({
555
639
  type: "string",
556
640
  required: true
557
641
  },
642
+ /**
643
+ * Summary projection (key display fields as JSON string: id, displayName, status).
644
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
645
+ */
646
+ summary: {
647
+ type: "string",
648
+ required: true
649
+ },
558
650
  /** Version id (e.g. ULID). */
559
651
  vid: {
560
652
  type: "string",
@@ -564,6 +656,7 @@ var TenantEntity = new import_electrodb5.Entity({
564
656
  type: "string",
565
657
  required: true
566
658
  },
659
+ gsi1Shard: gsi1ShardAttribute,
567
660
  deleted: {
568
661
  type: "boolean",
569
662
  required: false
@@ -591,19 +684,24 @@ var TenantEntity = new import_electrodb5.Entity({
591
684
  template: "${sk}"
592
685
  }
593
686
  },
594
- /** GSI4 — Resource Type Index: list all Tenants (no scan). */
595
- gsi4: {
596
- index: "GSI4",
597
- condition: () => true,
687
+ /**
688
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
689
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
690
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is `<ISO-8601 lastUpdated>#<id>` (control-plane
691
+ * unlabeled per DR-004). `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
692
+ */
693
+ gsi1: {
694
+ index: "GSI1",
598
695
  pk: {
599
- field: "GSI4PK",
600
- composite: [],
601
- template: "RT#Tenant"
696
+ field: "GSI1PK",
697
+ composite: ["gsi1Shard"],
698
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
602
699
  },
603
700
  sk: {
604
- field: "GSI4SK",
605
- composite: ["tenantId"],
606
- template: "ID#${tenantId}"
701
+ field: "GSI1SK",
702
+ casing: "none",
703
+ composite: ["lastUpdated", "id"],
704
+ template: "${lastUpdated}#${id}"
607
705
  }
608
706
  }
609
707
  }
@@ -634,6 +732,22 @@ var UserEntity = new import_electrodb6.Entity({
634
732
  type: "string",
635
733
  required: true
636
734
  },
735
+ /**
736
+ * Summary projection (key display fields as JSON string: id, displayName, status).
737
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
738
+ */
739
+ summary: {
740
+ type: "string",
741
+ required: true
742
+ },
743
+ /**
744
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
745
+ * Post Confirmation Lambda (#770) lands; required thereafter.
746
+ */
747
+ cognitoSub: {
748
+ type: "string",
749
+ required: false
750
+ },
637
751
  /** Version id (e.g. ULID). */
638
752
  vid: {
639
753
  type: "string",
@@ -643,6 +757,7 @@ var UserEntity = new import_electrodb6.Entity({
643
757
  type: "string",
644
758
  required: true
645
759
  },
760
+ gsi1Shard: gsi1ShardAttribute,
646
761
  deleted: {
647
762
  type: "boolean",
648
763
  required: false
@@ -670,19 +785,45 @@ var UserEntity = new import_electrodb6.Entity({
670
785
  template: "${sk}"
671
786
  }
672
787
  },
673
- /** GSI4 — Resource Type Index: list all Users (no scan). */
674
- gsi4: {
675
- index: "GSI4",
676
- condition: () => true,
788
+ /**
789
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
790
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
791
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
792
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z` characters.
793
+ */
794
+ gsi1: {
795
+ index: "GSI1",
677
796
  pk: {
678
- field: "GSI4PK",
679
- composite: [],
680
- template: "RT#User"
797
+ field: "GSI1PK",
798
+ composite: ["gsi1Shard"],
799
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
681
800
  },
682
801
  sk: {
683
- field: "GSI4SK",
684
- composite: ["id"],
685
- template: "ID#${id}"
802
+ field: "GSI1SK",
803
+ casing: "none",
804
+ composite: ["lastUpdated", "id"],
805
+ template: "${lastUpdated}#${id}"
806
+ }
807
+ },
808
+ /**
809
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
810
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
811
+ * not indexed.
812
+ */
813
+ gsi2: {
814
+ index: "GSI2",
815
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
816
+ pk: {
817
+ field: "GSI2PK",
818
+ casing: "none",
819
+ composite: ["cognitoSub"],
820
+ template: "USER#SUB#${cognitoSub}"
821
+ },
822
+ sk: {
823
+ field: "GSI2SK",
824
+ casing: "none",
825
+ composite: [],
826
+ template: "CURRENT"
686
827
  }
687
828
  }
688
829
  }
@@ -718,6 +859,14 @@ var WorkspaceEntity = new import_electrodb7.Entity({
718
859
  type: "string",
719
860
  required: true
720
861
  },
862
+ /**
863
+ * Summary projection (key display fields as JSON string: id, displayName, status).
864
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
865
+ */
866
+ summary: {
867
+ type: "string",
868
+ required: true
869
+ },
721
870
  /** Version id (e.g. ULID). */
722
871
  vid: {
723
872
  type: "string",
@@ -727,6 +876,7 @@ var WorkspaceEntity = new import_electrodb7.Entity({
727
876
  type: "string",
728
877
  required: true
729
878
  },
879
+ gsi1Shard: gsi1ShardAttribute,
730
880
  deleted: {
731
881
  type: "boolean",
732
882
  required: false
@@ -754,19 +904,24 @@ var WorkspaceEntity = new import_electrodb7.Entity({
754
904
  template: "${sk}"
755
905
  }
756
906
  },
757
- /** GSI4 — Resource Type Index: list all Workspaces for a tenant (no scan). */
758
- gsi4: {
759
- index: "GSI4",
760
- condition: () => true,
907
+ /**
908
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
909
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
910
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
911
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
912
+ */
913
+ gsi1: {
914
+ index: "GSI1",
761
915
  pk: {
762
- field: "GSI4PK",
763
- composite: ["tenantId"],
764
- template: "TID#${tenantId}#RT#Workspace"
916
+ field: "GSI1PK",
917
+ composite: ["tenantId", "gsi1Shard"],
918
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
765
919
  },
766
920
  sk: {
767
- field: "GSI4SK",
768
- composite: ["id"],
769
- template: "ID#${id}"
921
+ field: "GSI1SK",
922
+ casing: "none",
923
+ composite: ["lastUpdated", "id"],
924
+ template: "${lastUpdated}#${id}"
770
925
  }
771
926
  }
772
927
  }
@@ -820,6 +975,7 @@ async function createConfigurationOperation(params) {
820
975
  const roleId = body.roleId ?? context.roleId ?? "-";
821
976
  const lastUpdated = body.lastUpdated ?? date;
822
977
  const vid = body.vid ?? (date.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36));
978
+ const summary = JSON.stringify({ resourceType: "Configuration", id, key });
823
979
  const service = getDynamoControlService(tableName);
824
980
  await service.entities.configuration.put({
825
981
  tenantId,
@@ -829,6 +985,7 @@ async function createConfigurationOperation(params) {
829
985
  key,
830
986
  id,
831
987
  resource: compressResource(resourceStr),
988
+ summary,
832
989
  vid,
833
990
  lastUpdated,
834
991
  sk: SK
@@ -1235,22 +1392,25 @@ async function listConfigurationsOperation(params) {
1235
1392
  const { context, tableName } = params;
1236
1393
  const { tenantId, workspaceId } = context;
1237
1394
  const service = getDynamoControlService(tableName);
1238
- const result = await service.entities.configuration.query.gsi4({ tenantId, workspaceId }).go();
1239
- const entries = (result.data ?? []).map(
1240
- (item) => {
1241
- const resource = JSON.parse(decompressResource(item.resource));
1242
- return {
1395
+ const shardResults = await Promise.all(
1396
+ Array.from(
1397
+ { length: SHARD_COUNT },
1398
+ (_, shard) => service.entities.configuration.query.gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) }).go()
1399
+ )
1400
+ );
1401
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
1402
+ const resource = JSON.parse(decompressResource(item.resource));
1403
+ return {
1404
+ id: item.id,
1405
+ key: item.key,
1406
+ resource: {
1407
+ resourceType: "Configuration",
1243
1408
  id: item.id,
1244
1409
  key: item.key,
1245
- resource: {
1246
- resourceType: "Configuration",
1247
- id: item.id,
1248
- key: item.key,
1249
- resource
1250
- }
1251
- };
1252
- }
1253
- );
1410
+ resource
1411
+ }
1412
+ };
1413
+ });
1254
1414
  return { entries };
1255
1415
  }
1256
1416
 
@@ -1420,8 +1580,14 @@ async function updateConfigurationOperation(params) {
1420
1580
  const resourceStr = typeof resourcePayload === "string" ? resourcePayload : JSON.stringify(resourcePayload ?? {});
1421
1581
  const lastUpdated = body.lastUpdated ?? date;
1422
1582
  const nextVid = existing.data.vid != null ? String(Number(existing.data.vid) + 1) : date.replace(/[-:T.Z]/g, "").slice(0, 12) || "2";
1583
+ const summary = JSON.stringify({
1584
+ resourceType: "Configuration",
1585
+ id: existing.data.id,
1586
+ key: existing.data.key
1587
+ });
1423
1588
  await service.entities.configuration.patch({ tenantId, workspaceId, userId: actorId, roleId, key: id, sk: SK4 }).set({
1424
1589
  resource: compressResource(resourceStr),
1590
+ summary,
1425
1591
  lastUpdated,
1426
1592
  vid: nextVid
1427
1593
  }).go();
@@ -1499,6 +1665,7 @@ router.delete("/:id", deleteConfigurationRoute);
1499
1665
  var import_express2 = __toESM(require("express"));
1500
1666
 
1501
1667
  // src/data/operations/control/membership/membership-create-operation.ts
1668
+ var import_types = require("@openhi/types");
1502
1669
  async function createMembershipOperation(params) {
1503
1670
  const { context, body, tableName } = params;
1504
1671
  const service = getDynamoControlService(tableName);
@@ -1506,20 +1673,19 @@ async function createMembershipOperation(params) {
1506
1673
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1507
1674
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1508
1675
  const vid = `1`;
1676
+ const resource = { resourceType: "Membership", id, ...parsedResource };
1677
+ const summary = JSON.stringify((0, import_types.extractSummary)(resource));
1509
1678
  await service.entities.membership.put({
1510
1679
  tenantId: context.tenantId,
1511
1680
  id,
1512
- resource: JSON.stringify({
1513
- resourceType: "Membership",
1514
- id,
1515
- ...parsedResource
1516
- }),
1681
+ resource: JSON.stringify(resource),
1682
+ summary,
1517
1683
  vid,
1518
1684
  lastUpdated
1519
1685
  }).go();
1520
1686
  return {
1521
1687
  id,
1522
- resource: { resourceType: "Membership", id, ...parsedResource },
1688
+ resource,
1523
1689
  meta: { lastUpdated, versionId: vid }
1524
1690
  };
1525
1691
  }
@@ -1627,12 +1793,21 @@ async function getMembershipByIdRoute(req, res) {
1627
1793
  async function listMembershipsOperation(params) {
1628
1794
  const { context, tableName } = params;
1629
1795
  const service = getDynamoControlService(tableName);
1630
- const response = await service.entities.membership.query.gsi4({ tenantId: context.tenantId }).go();
1631
- const entries = (response.data ?? []).map((item) => {
1796
+ const shardResults = await Promise.all(
1797
+ Array.from(
1798
+ { length: SHARD_COUNT },
1799
+ (_, shard) => service.entities.membership.query.gsi1({ tenantId: context.tenantId, gsi1Shard: String(shard) }).go()
1800
+ )
1801
+ );
1802
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
1632
1803
  const parsedResource = JSON.parse(item.resource);
1633
1804
  return {
1634
1805
  id: item.id,
1635
- resource: { resourceType: "Membership", id: item.id, ...parsedResource }
1806
+ resource: {
1807
+ resourceType: "Membership",
1808
+ id: item.id,
1809
+ ...parsedResource
1810
+ }
1636
1811
  };
1637
1812
  });
1638
1813
  return { entries };
@@ -1661,6 +1836,7 @@ async function listMembershipsRoute(req, res) {
1661
1836
  }
1662
1837
 
1663
1838
  // src/data/operations/control/membership/membership-update-operation.ts
1839
+ var import_types2 = require("@openhi/types");
1664
1840
  async function updateMembershipOperation(params) {
1665
1841
  const { context, id, body, tableName } = params;
1666
1842
  const service = getDynamoControlService(tableName);
@@ -1671,20 +1847,19 @@ async function updateMembershipOperation(params) {
1671
1847
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1672
1848
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1673
1849
  const vid = `${Date.now()}`;
1850
+ const resource = { resourceType: "Membership", id, ...parsedResource };
1851
+ const summary = JSON.stringify((0, import_types2.extractSummary)(resource));
1674
1852
  await service.entities.membership.put({
1675
1853
  tenantId: context.tenantId,
1676
1854
  id,
1677
- resource: JSON.stringify({
1678
- resourceType: "Membership",
1679
- id,
1680
- ...parsedResource
1681
- }),
1855
+ resource: JSON.stringify(resource),
1856
+ summary,
1682
1857
  vid,
1683
1858
  lastUpdated
1684
1859
  }).go();
1685
1860
  return {
1686
1861
  id,
1687
- resource: { resourceType: "Membership", id, ...parsedResource },
1862
+ resource,
1688
1863
  meta: { lastUpdated, versionId: vid }
1689
1864
  };
1690
1865
  }
@@ -1741,6 +1916,7 @@ router2.delete("/:id", deleteMembershipRoute);
1741
1916
  var import_express3 = __toESM(require("express"));
1742
1917
 
1743
1918
  // src/data/operations/control/role/role-create-operation.ts
1919
+ var import_types3 = require("@openhi/types");
1744
1920
  async function createRoleOperation(params) {
1745
1921
  const { context, body, tableName } = params;
1746
1922
  const service = getDynamoControlService(tableName);
@@ -1748,15 +1924,18 @@ async function createRoleOperation(params) {
1748
1924
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1749
1925
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1750
1926
  const vid = `1`;
1927
+ const resource = { resourceType: "Role", id, ...parsedResource };
1928
+ const summary = JSON.stringify((0, import_types3.extractSummary)(resource));
1751
1929
  await service.entities.role.put({
1752
1930
  id,
1753
- resource: JSON.stringify({ resourceType: "Role", id, ...parsedResource }),
1931
+ resource: JSON.stringify(resource),
1932
+ summary,
1754
1933
  vid,
1755
1934
  lastUpdated
1756
1935
  }).go();
1757
1936
  return {
1758
1937
  id,
1759
- resource: { resourceType: "Role", id, ...parsedResource },
1938
+ resource,
1760
1939
  meta: { lastUpdated, versionId: vid }
1761
1940
  };
1762
1941
  }
@@ -1864,8 +2043,13 @@ async function getRoleByIdRoute(req, res) {
1864
2043
  async function listRolesOperation(params) {
1865
2044
  const { tableName } = params;
1866
2045
  const service = getDynamoControlService(tableName);
1867
- const response = await service.entities.role.query.gsi4({}).go();
1868
- const entries = (response.data ?? []).map((item) => {
2046
+ const shardResults = await Promise.all(
2047
+ Array.from(
2048
+ { length: SHARD_COUNT },
2049
+ (_, shard) => service.entities.role.query.gsi1({ gsi1Shard: String(shard) }).go()
2050
+ )
2051
+ );
2052
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
1869
2053
  const parsedResource = JSON.parse(item.resource);
1870
2054
  return {
1871
2055
  id: item.id,
@@ -1898,6 +2082,7 @@ async function listRolesRoute(req, res) {
1898
2082
  }
1899
2083
 
1900
2084
  // src/data/operations/control/role/role-update-operation.ts
2085
+ var import_types4 = require("@openhi/types");
1901
2086
  async function updateRoleOperation(params) {
1902
2087
  const { context, id, body, tableName } = params;
1903
2088
  const service = getDynamoControlService(tableName);
@@ -1908,15 +2093,18 @@ async function updateRoleOperation(params) {
1908
2093
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1909
2094
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1910
2095
  const vid = `${Date.now()}`;
2096
+ const resource = { resourceType: "Role", id, ...parsedResource };
2097
+ const summary = JSON.stringify((0, import_types4.extractSummary)(resource));
1911
2098
  await service.entities.role.put({
1912
2099
  id,
1913
- resource: JSON.stringify({ resourceType: "Role", id, ...parsedResource }),
2100
+ resource: JSON.stringify(resource),
2101
+ summary,
1914
2102
  vid,
1915
2103
  lastUpdated
1916
2104
  }).go();
1917
2105
  return {
1918
2106
  id,
1919
- resource: { resourceType: "Role", id, ...parsedResource },
2107
+ resource,
1920
2108
  meta: { lastUpdated, versionId: vid }
1921
2109
  };
1922
2110
  }
@@ -1973,6 +2161,7 @@ router3.delete("/:id", deleteRoleRoute);
1973
2161
  var import_express4 = __toESM(require("express"));
1974
2162
 
1975
2163
  // src/data/operations/control/roleassignment/roleassignment-create-operation.ts
2164
+ var import_types5 = require("@openhi/types");
1976
2165
  async function createRoleAssignmentOperation(params) {
1977
2166
  const { context, body, tableName } = params;
1978
2167
  const service = getDynamoControlService(tableName);
@@ -1980,20 +2169,19 @@ async function createRoleAssignmentOperation(params) {
1980
2169
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1981
2170
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1982
2171
  const vid = `1`;
2172
+ const resource = { resourceType: "RoleAssignment", id, ...parsedResource };
2173
+ const summary = JSON.stringify((0, import_types5.extractSummary)(resource));
1983
2174
  await service.entities.roleAssignment.put({
1984
2175
  tenantId: context.tenantId,
1985
2176
  id,
1986
- resource: JSON.stringify({
1987
- resourceType: "RoleAssignment",
1988
- id,
1989
- ...parsedResource
1990
- }),
2177
+ resource: JSON.stringify(resource),
2178
+ summary,
1991
2179
  vid,
1992
2180
  lastUpdated
1993
2181
  }).go();
1994
2182
  return {
1995
2183
  id,
1996
- resource: { resourceType: "RoleAssignment", id, ...parsedResource },
2184
+ resource,
1997
2185
  meta: { lastUpdated, versionId: vid }
1998
2186
  };
1999
2187
  }
@@ -2101,8 +2289,13 @@ async function getRoleAssignmentByIdRoute(req, res) {
2101
2289
  async function listRoleAssignmentsOperation(params) {
2102
2290
  const { context, tableName } = params;
2103
2291
  const service = getDynamoControlService(tableName);
2104
- const response = await service.entities.roleAssignment.query.gsi4({ tenantId: context.tenantId }).go();
2105
- const entries = (response.data ?? []).map((item) => {
2292
+ const shardResults = await Promise.all(
2293
+ Array.from(
2294
+ { length: SHARD_COUNT },
2295
+ (_, shard) => service.entities.roleAssignment.query.gsi1({ tenantId: context.tenantId, gsi1Shard: String(shard) }).go()
2296
+ )
2297
+ );
2298
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
2106
2299
  const parsedResource = JSON.parse(item.resource);
2107
2300
  return {
2108
2301
  id: item.id,
@@ -2139,6 +2332,7 @@ async function listRoleAssignmentsRoute(req, res) {
2139
2332
  }
2140
2333
 
2141
2334
  // src/data/operations/control/roleassignment/roleassignment-update-operation.ts
2335
+ var import_types6 = require("@openhi/types");
2142
2336
  async function updateRoleAssignmentOperation(params) {
2143
2337
  const { context, id, body, tableName } = params;
2144
2338
  const service = getDynamoControlService(tableName);
@@ -2149,20 +2343,19 @@ async function updateRoleAssignmentOperation(params) {
2149
2343
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2150
2344
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
2151
2345
  const vid = `${Date.now()}`;
2346
+ const resource = { resourceType: "RoleAssignment", id, ...parsedResource };
2347
+ const summary = JSON.stringify((0, import_types6.extractSummary)(resource));
2152
2348
  await service.entities.roleAssignment.put({
2153
2349
  tenantId: context.tenantId,
2154
2350
  id,
2155
- resource: JSON.stringify({
2156
- resourceType: "RoleAssignment",
2157
- id,
2158
- ...parsedResource
2159
- }),
2351
+ resource: JSON.stringify(resource),
2352
+ summary,
2160
2353
  vid,
2161
2354
  lastUpdated
2162
2355
  }).go();
2163
2356
  return {
2164
2357
  id,
2165
- resource: { resourceType: "RoleAssignment", id, ...parsedResource },
2358
+ resource,
2166
2359
  meta: { lastUpdated, versionId: vid }
2167
2360
  };
2168
2361
  }
@@ -2219,6 +2412,7 @@ router4.delete("/:id", deleteRoleAssignmentRoute);
2219
2412
  var import_express5 = __toESM(require("express"));
2220
2413
 
2221
2414
  // src/data/operations/control/tenant/tenant-create-operation.ts
2415
+ var import_types7 = require("@openhi/types");
2222
2416
  async function createTenantOperation(params) {
2223
2417
  const { context, body, tableName } = params;
2224
2418
  const service = getDynamoControlService(tableName);
@@ -2227,10 +2421,12 @@ async function createTenantOperation(params) {
2227
2421
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
2228
2422
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2229
2423
  const resource = { resourceType: "Tenant", id, ...parsedResource };
2424
+ const summary = JSON.stringify((0, import_types7.extractSummary)(resource));
2230
2425
  await service.entities.tenant.put({
2231
2426
  tenantId: id,
2232
2427
  id,
2233
2428
  resource: JSON.stringify(resource),
2429
+ summary,
2234
2430
  vid,
2235
2431
  lastUpdated
2236
2432
  }).go();
@@ -2339,8 +2535,13 @@ async function getTenantByIdRoute(req, res) {
2339
2535
  async function listTenantsOperation(params) {
2340
2536
  const { tableName } = params;
2341
2537
  const service = getDynamoControlService(tableName);
2342
- const { data: items } = await service.entities.tenant.query.gsi4({}).go();
2343
- const entries = items.map((item) => {
2538
+ const shardResults = await Promise.all(
2539
+ Array.from(
2540
+ { length: SHARD_COUNT },
2541
+ (_, shard) => service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go()
2542
+ )
2543
+ );
2544
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
2344
2545
  const parsed = JSON.parse(item.resource);
2345
2546
  return {
2346
2547
  id: item.id,
@@ -2373,6 +2574,7 @@ async function listTenantsRoute(req, res) {
2373
2574
  }
2374
2575
 
2375
2576
  // src/data/operations/control/tenant/tenant-update-operation.ts
2577
+ var import_types8 = require("@openhi/types");
2376
2578
  async function updateTenantOperation(params) {
2377
2579
  const { context, id, body, tableName } = params;
2378
2580
  const service = getDynamoControlService(tableName);
@@ -2390,7 +2592,8 @@ async function updateTenantOperation(params) {
2390
2592
  resourceType: "Tenant",
2391
2593
  id
2392
2594
  };
2393
- await service.entities.tenant.patch({ tenantId: id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), vid, lastUpdated }).go();
2595
+ const summary = JSON.stringify((0, import_types8.extractSummary)(updated));
2596
+ await service.entities.tenant.patch({ tenantId: id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), summary, vid, lastUpdated }).go();
2394
2597
  return { id, resource: updated, meta: { lastUpdated, versionId: vid } };
2395
2598
  }
2396
2599
 
@@ -2446,6 +2649,7 @@ router5.delete("/:id", deleteTenantRoute);
2446
2649
  var import_express6 = __toESM(require("express"));
2447
2650
 
2448
2651
  // src/data/operations/control/user/user-create-operation.ts
2652
+ var import_types9 = require("@openhi/types");
2449
2653
  async function createUserOperation(params) {
2450
2654
  const { context, body, tableName } = params;
2451
2655
  const service = getDynamoControlService(tableName);
@@ -2453,15 +2657,18 @@ async function createUserOperation(params) {
2453
2657
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2454
2658
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
2455
2659
  const vid = `1`;
2660
+ const resource = { resourceType: "User", id, ...parsedResource };
2661
+ const summary = JSON.stringify((0, import_types9.extractSummary)(resource));
2456
2662
  await service.entities.user.put({
2457
2663
  id,
2458
- resource: JSON.stringify({ resourceType: "User", id, ...parsedResource }),
2664
+ resource: JSON.stringify(resource),
2665
+ summary,
2459
2666
  vid,
2460
2667
  lastUpdated
2461
2668
  }).go();
2462
2669
  return {
2463
2670
  id,
2464
- resource: { resourceType: "User", id, ...parsedResource },
2671
+ resource,
2465
2672
  meta: { lastUpdated, versionId: vid }
2466
2673
  };
2467
2674
  }
@@ -2569,8 +2776,13 @@ async function getUserByIdRoute(req, res) {
2569
2776
  async function listUsersOperation(params) {
2570
2777
  const { tableName } = params;
2571
2778
  const service = getDynamoControlService(tableName);
2572
- const response = await service.entities.user.query.gsi4({}).go();
2573
- const entries = (response.data ?? []).map((item) => {
2779
+ const shardResults = await Promise.all(
2780
+ Array.from(
2781
+ { length: SHARD_COUNT },
2782
+ (_, shard) => service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go()
2783
+ )
2784
+ );
2785
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
2574
2786
  const parsedResource = JSON.parse(item.resource);
2575
2787
  return {
2576
2788
  id: item.id,
@@ -2603,6 +2815,7 @@ async function listUsersRoute(req, res) {
2603
2815
  }
2604
2816
 
2605
2817
  // src/data/operations/control/user/user-update-operation.ts
2818
+ var import_types10 = require("@openhi/types");
2606
2819
  async function updateUserOperation(params) {
2607
2820
  const { context, id, body, tableName } = params;
2608
2821
  const service = getDynamoControlService(tableName);
@@ -2613,15 +2826,18 @@ async function updateUserOperation(params) {
2613
2826
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2614
2827
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
2615
2828
  const vid = `${Date.now()}`;
2829
+ const resource = { resourceType: "User", id, ...parsedResource };
2830
+ const summary = JSON.stringify((0, import_types10.extractSummary)(resource));
2616
2831
  await service.entities.user.put({
2617
2832
  id,
2618
- resource: JSON.stringify({ resourceType: "User", id, ...parsedResource }),
2833
+ resource: JSON.stringify(resource),
2834
+ summary,
2619
2835
  vid,
2620
2836
  lastUpdated
2621
2837
  }).go();
2622
2838
  return {
2623
2839
  id,
2624
- resource: { resourceType: "User", id, ...parsedResource },
2840
+ resource,
2625
2841
  meta: { lastUpdated, versionId: vid }
2626
2842
  };
2627
2843
  }
@@ -2678,6 +2894,7 @@ router6.delete("/:id", deleteUserRoute);
2678
2894
  var import_express7 = __toESM(require("express"));
2679
2895
 
2680
2896
  // src/data/operations/control/workspace/workspace-create-operation.ts
2897
+ var import_types11 = require("@openhi/types");
2681
2898
  async function createWorkspaceOperation(params) {
2682
2899
  const { context, body, tableName } = params;
2683
2900
  const { tenantId } = context;
@@ -2687,7 +2904,15 @@ async function createWorkspaceOperation(params) {
2687
2904
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
2688
2905
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2689
2906
  const resource = { resourceType: "Workspace", id, ...parsedResource };
2690
- await service.entities.workspace.put({ tenantId, id, resource: JSON.stringify(resource), vid, lastUpdated }).go();
2907
+ const summary = JSON.stringify((0, import_types11.extractSummary)(resource));
2908
+ await service.entities.workspace.put({
2909
+ tenantId,
2910
+ id,
2911
+ resource: JSON.stringify(resource),
2912
+ summary,
2913
+ vid,
2914
+ lastUpdated
2915
+ }).go();
2691
2916
  return { id, resource, meta: { lastUpdated, versionId: vid } };
2692
2917
  }
2693
2918
 
@@ -2796,8 +3021,13 @@ async function listWorkspacesOperation(params) {
2796
3021
  const { context, tableName } = params;
2797
3022
  const { tenantId } = context;
2798
3023
  const service = getDynamoControlService(tableName);
2799
- const { data: items } = await service.entities.workspace.query.gsi4({ tenantId }).go();
2800
- const entries = items.map((item) => {
3024
+ const shardResults = await Promise.all(
3025
+ Array.from(
3026
+ { length: SHARD_COUNT },
3027
+ (_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
3028
+ )
3029
+ );
3030
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
2801
3031
  const parsed = JSON.parse(item.resource);
2802
3032
  return {
2803
3033
  id: item.id,
@@ -2830,6 +3060,7 @@ async function listWorkspacesRoute(req, res) {
2830
3060
  }
2831
3061
 
2832
3062
  // src/data/operations/control/workspace/workspace-update-operation.ts
3063
+ var import_types12 = require("@openhi/types");
2833
3064
  async function updateWorkspaceOperation(params) {
2834
3065
  const { context, id, body, tableName } = params;
2835
3066
  const { tenantId } = context;
@@ -2848,7 +3079,8 @@ async function updateWorkspaceOperation(params) {
2848
3079
  resourceType: "Workspace",
2849
3080
  id
2850
3081
  };
2851
- await service.entities.workspace.patch({ tenantId, id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), vid, lastUpdated }).go();
3082
+ const summary = JSON.stringify((0, import_types12.extractSummary)(updated));
3083
+ await service.entities.workspace.patch({ tenantId, id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), summary, vid, lastUpdated }).go();
2852
3084
  return { id, resource: updated, meta: { lastUpdated, versionId: vid } };
2853
3085
  }
2854
3086
 
@@ -2960,6 +3192,18 @@ var dataEntityAttributes = {
2960
3192
  type: "string",
2961
3193
  required: true
2962
3194
  },
3195
+ /**
3196
+ * Summary projection of the FHIR resource as a JSON string (uncompressed). Populated on every
3197
+ * write via `extractSummary(resource)` so GSI projections can surface list/lookup data without
3198
+ * reading the compressed `resource` blob. Kept uncompressed because the summary is small and
3199
+ * must be fast to retrieve without encode/decode overhead.
3200
+ *
3201
+ * @see sites/www-docs/content/architecture/adr/2026-04-17-02-fhir-summary-projection-for-gsi-access-patterns.md
3202
+ */
3203
+ summary: {
3204
+ type: "string",
3205
+ required: true
3206
+ },
2963
3207
  /** Version id (e.g. ULID). Tracks current version; S3 history key. */
2964
3208
  vid: {
2965
3209
  type: "string",
@@ -2969,6 +3213,41 @@ var dataEntityAttributes = {
2969
3213
  type: "string",
2970
3214
  required: true
2971
3215
  },
3216
+ /**
3217
+ * Shard index segment for the GSI1 partition key. Computed deterministically from `id`
3218
+ * via `computeShard` so updates always land on the same shard. Stored as a string because
3219
+ * it appears as a literal segment in the GSI1 PK template; the underlying value is 0..3.
3220
+ * Not `required` because the value is derived via `watch`/`set`; ElectroDB's required-field
3221
+ * check runs before watch propagation, so callers must not fail validation on a derived field.
3222
+ *
3223
+ * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md — GSI1 (sharded)
3224
+ */
3225
+ gsi1Shard: {
3226
+ type: "string",
3227
+ watch: ["id"],
3228
+ set: (_val, item) => {
3229
+ if (typeof item?.id !== "string" || item.id.length === 0) {
3230
+ return void 0;
3231
+ }
3232
+ return String(computeShard(item.id));
3233
+ }
3234
+ },
3235
+ /**
3236
+ * GSI1 sort key. Written as the index's SK verbatim so list endpoints can
3237
+ * use `BEGINS_WITH` for prefix queries (e.g. `?name=Sm` against Patient).
3238
+ * Computed at write time via `extractSortKey(resource)` per DR-004:
3239
+ * - Labeled types (LABEL_PATHS): `<normalizedLabel>#<id>`
3240
+ * - Unlabeled types: `<ISO-8601 lastUpdated>#<id>`
3241
+ * The factory deliberately does not derive this from `lastUpdated`/`id`
3242
+ * — that would lock every type into the unlabeled fallback and defeat
3243
+ * label-based BEGINS_WITH on labeled types.
3244
+ *
3245
+ * @see openhi-planning DR-004
3246
+ */
3247
+ gsi1sk: {
3248
+ type: "string",
3249
+ required: true
3250
+ },
2972
3251
  deleted: {
2973
3252
  type: "boolean",
2974
3253
  required: false
@@ -3003,18 +3282,26 @@ function createDataEntity(entity, resourceTypeLabel) {
3003
3282
  composite: ["sk"]
3004
3283
  }
3005
3284
  },
3006
- /** GSI4 — Resource Type Index: list all resources of this type in a workspace (no scan). Used by list operations (e.g. GET /Patient). */
3007
- gsi4: {
3008
- index: "GSI4",
3285
+ /**
3286
+ * GSI1 — Unified Sharded List: list all resources of this type in a workspace; reads fan
3287
+ * out across the four shards and merge by SK. SK is the writer-supplied `gsi1sk` verbatim
3288
+ * (per DR-004) so labeled types serve `BEGINS_WITH` prefix queries on the natural label.
3289
+ * `casing: "none"` is required on the SK because the writer (`extractSortKey`) already
3290
+ * applies DR-004 normalization — ElectroDB's default lowercasing would mangle the
3291
+ * ISO-8601 unlabeled fallback (`T`/`Z` → `t`/`z`).
3292
+ */
3293
+ gsi1: {
3294
+ index: "GSI1",
3009
3295
  pk: {
3010
- field: "GSI4PK",
3011
- composite: ["tenantId", "workspaceId"],
3012
- template: `TID#\${tenantId}#WID#\${workspaceId}#RT#${resourceTypeLabel}`
3296
+ field: "GSI1PK",
3297
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
3298
+ template: `TID#\${tenantId}#WID#\${workspaceId}#RT#${resourceTypeLabel}#SHARD#\${gsi1Shard}`
3013
3299
  },
3014
3300
  sk: {
3015
- field: "GSI4SK",
3016
- composite: ["id"],
3017
- template: `ID#\${id}`
3301
+ field: "GSI1SK",
3302
+ casing: "none",
3303
+ composite: ["gsi1sk"],
3304
+ template: `\${gsi1sk}`
3018
3305
  }
3019
3306
  }
3020
3307
  }
@@ -3908,6 +4195,7 @@ function getDynamoDataService(tableName) {
3908
4195
  }
3909
4196
 
3910
4197
  // src/data/operations/data-operations-common.ts
4198
+ var import_types13 = require("@openhi/types");
3911
4199
  var DATA_ENTITY_SK = "CURRENT";
3912
4200
  async function getDataEntityById(entity, tenantId, workspaceId, id, resourceLabel) {
3913
4201
  const result = await entity.get({
@@ -3936,28 +4224,40 @@ async function deleteDataEntityById(entity, tenantId, workspaceId, id) {
3936
4224
  }).go();
3937
4225
  }
3938
4226
  async function listDataEntitiesByWorkspace(entity, tenantId, workspaceId) {
3939
- const result = await entity.query.gsi4({ tenantId, workspaceId }).go();
3940
- const items = result.data ?? [];
3941
- const entries = items.map((item) => {
3942
- const parsed = JSON.parse(decompressResource(item.resource));
3943
- return {
3944
- id: item.id,
3945
- resource: { ...parsed, id: item.id }
3946
- };
3947
- });
4227
+ const shardResults = await Promise.all(
4228
+ Array.from(
4229
+ { length: SHARD_COUNT },
4230
+ (_, shard) => entity.query.gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) }).go()
4231
+ )
4232
+ );
4233
+ const entries = [];
4234
+ for (const shardResult of shardResults) {
4235
+ for (const item of shardResult.data ?? []) {
4236
+ const parsed = JSON.parse(decompressResource(item.resource));
4237
+ entries.push({
4238
+ id: item.id,
4239
+ resource: { ...parsed, id: item.id }
4240
+ });
4241
+ }
4242
+ }
3948
4243
  return { entries };
3949
4244
  }
3950
4245
  async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourceWithAudit, fallbackDate) {
3951
4246
  const lastUpdated = resourceWithAudit.meta?.lastUpdated ?? fallbackDate ?? (/* @__PURE__ */ new Date()).toISOString();
3952
4247
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
4248
+ const resourceLike = resourceWithAudit;
4249
+ const summary = JSON.stringify((0, import_types13.extractSummary)(resourceLike));
4250
+ const gsi1sk = (0, import_types13.extractSortKey)(resourceLike);
3953
4251
  await entity.put({
3954
4252
  sk: DATA_ENTITY_SK,
3955
4253
  tenantId,
3956
4254
  workspaceId,
3957
4255
  id,
3958
4256
  resource: compressResource(JSON.stringify(resourceWithAudit)),
4257
+ summary,
3959
4258
  vid,
3960
- lastUpdated
4259
+ lastUpdated,
4260
+ gsi1sk
3961
4261
  }).go();
3962
4262
  return {
3963
4263
  id,
@@ -4004,6 +4304,9 @@ async function updateDataEntityById(entity, tenantId, workspaceId, id, resourceL
4004
4304
  }
4005
4305
  const existingStr = decompressResource(existing.data.resource);
4006
4306
  const { resource, lastUpdated } = buildPatched(existingStr);
4307
+ const resourceLike = resource;
4308
+ const summary = JSON.stringify((0, import_types13.extractSummary)(resourceLike));
4309
+ const gsi1sk = (0, import_types13.extractSortKey)(resourceLike);
4007
4310
  await entity.patch({
4008
4311
  tenantId,
4009
4312
  workspaceId,
@@ -4011,7 +4314,9 @@ async function updateDataEntityById(entity, tenantId, workspaceId, id, resourceL
4011
4314
  sk: DATA_ENTITY_SK
4012
4315
  }).set({
4013
4316
  resource: compressResource(JSON.stringify(resource)),
4014
- lastUpdated
4317
+ summary,
4318
+ lastUpdated,
4319
+ gsi1sk
4015
4320
  }).go();
4016
4321
  return {
4017
4322
  id,
@@ -12655,9 +12960,165 @@ async function listEncountersOperation(params) {
12655
12960
  );
12656
12961
  }
12657
12962
 
12963
+ // src/data/postgres/data-api-postgres-query-runner.ts
12964
+ var import_client_rds_data = require("@aws-sdk/client-rds-data");
12965
+ var DataApiPostgresQueryRunner = class {
12966
+ constructor(options) {
12967
+ this.client = options.client ?? new import_client_rds_data.RDSDataClient({});
12968
+ this.clusterArn = options.clusterArn;
12969
+ this.secretArn = options.secretArn;
12970
+ this.database = options.database;
12971
+ this.schema = options.schema;
12972
+ }
12973
+ async query(sql, params) {
12974
+ const out = await this.client.send(
12975
+ new import_client_rds_data.ExecuteStatementCommand({
12976
+ resourceArn: this.clusterArn,
12977
+ secretArn: this.secretArn,
12978
+ database: this.database,
12979
+ schema: this.schema,
12980
+ sql,
12981
+ parameters: params.map(toSqlParameter),
12982
+ // Results as named columns so we can map them back to JS objects.
12983
+ includeResultMetadata: true,
12984
+ // Encode JSONB results as strings, then parse client-side. Without
12985
+ // this, Data API returns JSON values inline as their underlying types
12986
+ // and complex JSONB columns get clipped.
12987
+ formatRecordsAs: "JSON"
12988
+ })
12989
+ );
12990
+ if (!out.formattedRecords) {
12991
+ return [];
12992
+ }
12993
+ return JSON.parse(out.formattedRecords);
12994
+ }
12995
+ };
12996
+ function toSqlParameter(param) {
12997
+ if (param.value === null) {
12998
+ return { name: param.name, value: { isNull: true } };
12999
+ }
13000
+ const v = param.value;
13001
+ let field;
13002
+ if (typeof v === "string") {
13003
+ field = { stringValue: v };
13004
+ } else if (typeof v === "boolean") {
13005
+ field = { booleanValue: v };
13006
+ } else if (Number.isInteger(v)) {
13007
+ field = { longValue: v };
13008
+ } else {
13009
+ field = { doubleValue: v };
13010
+ }
13011
+ return { name: param.name, value: field };
13012
+ }
13013
+
13014
+ // src/data/postgres/default-postgres-query-runner.ts
13015
+ var cached;
13016
+ function readEnv(name) {
13017
+ const v = process.env[name]?.trim();
13018
+ if (!v) {
13019
+ throw new Error(
13020
+ `Missing required env var for default PostgresQueryRunner: ${name}`
13021
+ );
13022
+ }
13023
+ return v;
13024
+ }
13025
+ function getDefaultPostgresQueryRunner() {
13026
+ if (!cached) {
13027
+ cached = new DataApiPostgresQueryRunner({
13028
+ clusterArn: readEnv("OPENHI_PG_CLUSTER_ARN"),
13029
+ secretArn: readEnv("OPENHI_PG_SECRET_ARN"),
13030
+ database: readEnv("OPENHI_PG_DATABASE"),
13031
+ schema: readEnv("OPENHI_PG_SCHEMA")
13032
+ });
13033
+ }
13034
+ return cached;
13035
+ }
13036
+
13037
+ // src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
13038
+ var DEFAULT_LIMIT = 100;
13039
+ function buildOpenHiResourceUrn(opts) {
13040
+ return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
13041
+ }
13042
+ function buildSearchEncountersByPatientSql() {
13043
+ return [
13044
+ "SELECT resource_id AS id, resource",
13045
+ "FROM resources",
13046
+ "WHERE tenant_id = :tenantId",
13047
+ " AND workspace_id = :workspaceId",
13048
+ " AND resource_type = 'Encounter'",
13049
+ " AND deleted_at IS NULL",
13050
+ " AND (resource @> :containmentRelative::jsonb",
13051
+ " OR resource @> :containmentUrn::jsonb)",
13052
+ "ORDER BY last_updated DESC",
13053
+ "LIMIT :limit;"
13054
+ ].join("\n");
13055
+ }
13056
+ async function searchEncountersByPatientOperation(params) {
13057
+ const { context, patientId } = params;
13058
+ const { tenantId, workspaceId } = context;
13059
+ const runner = params.runner ?? getDefaultPostgresQueryRunner();
13060
+ const limit = params.limit ?? DEFAULT_LIMIT;
13061
+ const containmentRelative = JSON.stringify({
13062
+ subject: { reference: `Patient/${patientId}` }
13063
+ });
13064
+ const containmentUrn = JSON.stringify({
13065
+ subject: {
13066
+ reference: buildOpenHiResourceUrn({
13067
+ tenantId,
13068
+ workspaceId,
13069
+ resourceType: "Patient",
13070
+ resourceId: patientId
13071
+ })
13072
+ }
13073
+ });
13074
+ const rows = await runner.query(
13075
+ buildSearchEncountersByPatientSql(),
13076
+ [
13077
+ { name: "tenantId", value: tenantId },
13078
+ { name: "workspaceId", value: workspaceId },
13079
+ { name: "containmentRelative", value: containmentRelative },
13080
+ { name: "containmentUrn", value: containmentUrn },
13081
+ { name: "limit", value: limit }
13082
+ ]
13083
+ );
13084
+ const entries = rows.map((row) => ({
13085
+ id: row.id,
13086
+ resource: {
13087
+ ...row.resource,
13088
+ id: row.id
13089
+ }
13090
+ }));
13091
+ return { entries };
13092
+ }
13093
+
12658
13094
  // src/data/rest-api/routes/data/encounter/encounter-list-route.ts
13095
+ function singleStringQueryParam(req, name) {
13096
+ const v = req.query[name];
13097
+ if (typeof v !== "string") {
13098
+ return void 0;
13099
+ }
13100
+ const trimmed = v.trim();
13101
+ return trimmed === "" ? void 0 : trimmed;
13102
+ }
12659
13103
  async function listEncountersRoute(req, res) {
12660
13104
  const ctx = req.openhiContext;
13105
+ const patientId = singleStringQueryParam(req, "patient");
13106
+ if (patientId) {
13107
+ try {
13108
+ const result = await searchEncountersByPatientOperation({
13109
+ context: ctx,
13110
+ patientId
13111
+ });
13112
+ const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
13113
+ return res.json(bundle);
13114
+ } catch (err) {
13115
+ return sendOperationOutcome500(
13116
+ res,
13117
+ err,
13118
+ "GET /Encounter?patient= search error:"
13119
+ );
13120
+ }
13121
+ }
12661
13122
  try {
12662
13123
  const result = await listEncountersOperation({ context: ctx });
12663
13124
  const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
@@ -24574,6 +25035,7 @@ var import_ulid99 = require("ulid");
24574
25035
  // src/data/import-patient.ts
24575
25036
  var import_node_fs = require("fs");
24576
25037
  var import_node_path = require("path");
25038
+ var import_types14 = require("@openhi/types");
24577
25039
  function extractPatient(parsed) {
24578
25040
  if (parsed && typeof parsed === "object" && "resourceType" in parsed) {
24579
25041
  const root = parsed;
@@ -24639,6 +25101,9 @@ function patientToPutAttrs(patient, options) {
24639
25101
  workspaceId,
24640
25102
  id: patient.id,
24641
25103
  resource: compressResource(JSON.stringify(patientWithMeta)),
25104
+ summary: JSON.stringify(
25105
+ (0, import_types14.extractSummary)(patientWithMeta)
25106
+ ),
24642
25107
  vid: lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36),
24643
25108
  lastUpdated,
24644
25109
  identifierSystem: "",
@@ -33959,6 +34424,7 @@ app.use(
33959
34424
  "/MedicinalProductPackaged",
33960
34425
  "/MedicinalProductPharmaceutical",
33961
34426
  "/MedicinalProductUndesirableEffect",
34427
+ "/Membership",
33962
34428
  "/MessageDefinition",
33963
34429
  "/MessageHeader",
33964
34430
  "/MolecularSequence",
@@ -33988,6 +34454,8 @@ app.use(
33988
34454
  "/ResearchSubject",
33989
34455
  "/RiskAssessment",
33990
34456
  "/RiskEvidenceSynthesis",
34457
+ "/Role",
34458
+ "/RoleAssignment",
33991
34459
  "/Schedule",
33992
34460
  "/ServiceRequest",
33993
34461
  "/SearchParameter",
@@ -34007,12 +34475,15 @@ app.use(
34007
34475
  "/SupplyDelivery",
34008
34476
  "/SupplyRequest",
34009
34477
  "/Task",
34478
+ "/Tenant",
34010
34479
  "/TerminologyCapabilities",
34011
34480
  "/TestReport",
34012
34481
  "/TestScript",
34482
+ "/User",
34013
34483
  "/ValueSet",
34014
34484
  "/VerificationResult",
34015
- "/VisionPrescription"
34485
+ "/VisionPrescription",
34486
+ "/Workspace"
34016
34487
  ],
34017
34488
  openHiContextMiddleware
34018
34489
  );
@@ -34174,6 +34645,18 @@ app.use("/RoleAssignment", router4);
34174
34645
  app.use("/Tenant", router5);
34175
34646
  app.use("/User", router6);
34176
34647
  app.use("/Workspace", router7);
34648
+ app.use((_req, res) => {
34649
+ res.status(404).json({
34650
+ resourceType: "OperationOutcome",
34651
+ issue: [
34652
+ {
34653
+ severity: "error",
34654
+ code: "not-supported",
34655
+ diagnostics: "The requested endpoint or resource type is not supported by this server."
34656
+ }
34657
+ ]
34658
+ });
34659
+ });
34177
34660
 
34178
34661
  // src/data/lambda/rest-api-lambda.handler.ts
34179
34662
  var handler = (0, import_serverless_express2.default)({ app });