@openhi/constructs 0.0.85 → 0.0.86

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.
@@ -142,6 +142,31 @@ var dynamoClient = new DynamoDBClient({
142
142
 
143
143
  // src/data/dynamo/entities/control/configuration-entity.ts
144
144
  import { Entity } from "electrodb";
145
+
146
+ // src/data/dynamo/shard.ts
147
+ var SHARD_COUNT = 4;
148
+ function computeShard(id) {
149
+ let hash = 2166136261;
150
+ for (let i = 0; i < id.length; i++) {
151
+ hash ^= id.charCodeAt(i);
152
+ hash = Math.imul(hash, 16777619);
153
+ }
154
+ return (hash >>> 0) % SHARD_COUNT;
155
+ }
156
+
157
+ // src/data/dynamo/entities/control/control-entity-common.ts
158
+ var gsi1ShardAttribute = {
159
+ type: "string",
160
+ watch: ["id"],
161
+ set: (_val, item) => {
162
+ if (typeof item?.id !== "string" || item.id.length === 0) {
163
+ return void 0;
164
+ }
165
+ return String(computeShard(item.id));
166
+ }
167
+ };
168
+
169
+ // src/data/dynamo/entities/control/configuration-entity.ts
145
170
  var ConfigurationEntity = new Entity({
146
171
  model: {
147
172
  entity: "configuration",
@@ -194,6 +219,14 @@ var ConfigurationEntity = new Entity({
194
219
  type: "string",
195
220
  required: true
196
221
  },
222
+ /**
223
+ * Summary projection (key display fields as JSON string: id, key, status).
224
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
225
+ */
226
+ summary: {
227
+ type: "string",
228
+ required: true
229
+ },
197
230
  /** Version id (e.g. ULID). Tracks current version; S3 history key. */
198
231
  vid: {
199
232
  type: "string",
@@ -203,6 +236,7 @@ var ConfigurationEntity = new Entity({
203
236
  type: "string",
204
237
  required: true
205
238
  },
239
+ gsi1Shard: gsi1ShardAttribute,
206
240
  deleted: {
207
241
  type: "boolean",
208
242
  required: false
@@ -230,19 +264,27 @@ var ConfigurationEntity = new Entity({
230
264
  template: "KEY#${key}#SK#${sk}"
231
265
  }
232
266
  },
233
- /** 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. */
234
- gsi4: {
235
- index: "GSI4",
236
- condition: () => true,
267
+ /**
268
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
269
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
270
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
271
+ * hierarchical resolution in one query; use base table GetItem in fallback order
272
+ * (user → workspace → tenant → baseline) for that.
273
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
274
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
275
+ */
276
+ gsi1: {
277
+ index: "GSI1",
237
278
  pk: {
238
- field: "GSI4PK",
239
- composite: ["tenantId", "workspaceId"],
240
- template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration"
279
+ field: "GSI1PK",
280
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
281
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
241
282
  },
242
283
  sk: {
243
- field: "GSI4SK",
244
- composite: ["key", "sk"],
245
- template: "KEY#${key}#SK#${sk}"
284
+ field: "GSI1SK",
285
+ casing: "none",
286
+ composite: ["lastUpdated", "id"],
287
+ template: "${lastUpdated}#${id}"
246
288
  }
247
289
  }
248
290
  }
@@ -278,6 +320,14 @@ var MembershipEntity = new Entity2({
278
320
  type: "string",
279
321
  required: true
280
322
  },
323
+ /**
324
+ * Summary projection (key display fields as JSON string: id, displayName, status).
325
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
326
+ */
327
+ summary: {
328
+ type: "string",
329
+ required: true
330
+ },
281
331
  /** Version id (e.g. ULID). */
282
332
  vid: {
283
333
  type: "string",
@@ -287,6 +337,7 @@ var MembershipEntity = new Entity2({
287
337
  type: "string",
288
338
  required: true
289
339
  },
340
+ gsi1Shard: gsi1ShardAttribute,
290
341
  deleted: {
291
342
  type: "boolean",
292
343
  required: false
@@ -314,19 +365,24 @@ var MembershipEntity = new Entity2({
314
365
  template: "${sk}"
315
366
  }
316
367
  },
317
- /** GSI4 — Resource Type Index: list all Memberships for a tenant (no scan). */
318
- gsi4: {
319
- index: "GSI4",
320
- condition: () => true,
368
+ /**
369
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
370
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
371
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
372
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
373
+ */
374
+ gsi1: {
375
+ index: "GSI1",
321
376
  pk: {
322
- field: "GSI4PK",
323
- composite: ["tenantId"],
324
- template: "TID#${tenantId}#RT#Membership"
377
+ field: "GSI1PK",
378
+ composite: ["tenantId", "gsi1Shard"],
379
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
325
380
  },
326
381
  sk: {
327
- field: "GSI4SK",
328
- composite: ["id"],
329
- template: "ID#${id}"
382
+ field: "GSI1SK",
383
+ casing: "none",
384
+ composite: ["lastUpdated", "id"],
385
+ template: "${lastUpdated}#${id}"
330
386
  }
331
387
  }
332
388
  }
@@ -357,6 +413,14 @@ var RoleEntity = new Entity3({
357
413
  type: "string",
358
414
  required: true
359
415
  },
416
+ /**
417
+ * Summary projection (key display fields as JSON string: id, displayName, status).
418
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
419
+ */
420
+ summary: {
421
+ type: "string",
422
+ required: true
423
+ },
360
424
  /** Version id (e.g. ULID). */
361
425
  vid: {
362
426
  type: "string",
@@ -366,6 +430,7 @@ var RoleEntity = new Entity3({
366
430
  type: "string",
367
431
  required: true
368
432
  },
433
+ gsi1Shard: gsi1ShardAttribute,
369
434
  deleted: {
370
435
  type: "boolean",
371
436
  required: false
@@ -393,19 +458,24 @@ var RoleEntity = new Entity3({
393
458
  template: "${sk}"
394
459
  }
395
460
  },
396
- /** GSI4 — Resource Type Index: list all Roles (no scan). */
397
- gsi4: {
398
- index: "GSI4",
399
- condition: () => true,
461
+ /**
462
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
463
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
464
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
465
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
466
+ */
467
+ gsi1: {
468
+ index: "GSI1",
400
469
  pk: {
401
- field: "GSI4PK",
402
- composite: [],
403
- template: "RT#Role"
470
+ field: "GSI1PK",
471
+ composite: ["gsi1Shard"],
472
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
404
473
  },
405
474
  sk: {
406
- field: "GSI4SK",
407
- composite: ["id"],
408
- template: "ID#${id}"
475
+ field: "GSI1SK",
476
+ casing: "none",
477
+ composite: ["lastUpdated", "id"],
478
+ template: "${lastUpdated}#${id}"
409
479
  }
410
480
  }
411
481
  }
@@ -441,6 +511,14 @@ var RoleAssignmentEntity = new Entity4({
441
511
  type: "string",
442
512
  required: true
443
513
  },
514
+ /**
515
+ * Summary projection (key display fields as JSON string: id, displayName, status).
516
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
517
+ */
518
+ summary: {
519
+ type: "string",
520
+ required: true
521
+ },
444
522
  /** Version id (e.g. ULID). */
445
523
  vid: {
446
524
  type: "string",
@@ -450,6 +528,7 @@ var RoleAssignmentEntity = new Entity4({
450
528
  type: "string",
451
529
  required: true
452
530
  },
531
+ gsi1Shard: gsi1ShardAttribute,
453
532
  deleted: {
454
533
  type: "boolean",
455
534
  required: false
@@ -477,19 +556,24 @@ var RoleAssignmentEntity = new Entity4({
477
556
  template: "${sk}"
478
557
  }
479
558
  },
480
- /** GSI4 — Resource Type Index: list all RoleAssignments for a tenant (no scan). */
481
- gsi4: {
482
- index: "GSI4",
483
- condition: () => true,
559
+ /**
560
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
561
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
562
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
563
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
564
+ */
565
+ gsi1: {
566
+ index: "GSI1",
484
567
  pk: {
485
- field: "GSI4PK",
486
- composite: ["tenantId"],
487
- template: "TID#${tenantId}#RT#RoleAssignment"
568
+ field: "GSI1PK",
569
+ composite: ["tenantId", "gsi1Shard"],
570
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
488
571
  },
489
572
  sk: {
490
- field: "GSI4SK",
491
- composite: ["id"],
492
- template: "ID#${id}"
573
+ field: "GSI1SK",
574
+ casing: "none",
575
+ composite: ["lastUpdated", "id"],
576
+ template: "${lastUpdated}#${id}"
493
577
  }
494
578
  }
495
579
  }
@@ -525,6 +609,14 @@ var TenantEntity = new Entity5({
525
609
  type: "string",
526
610
  required: true
527
611
  },
612
+ /**
613
+ * Summary projection (key display fields as JSON string: id, displayName, status).
614
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
615
+ */
616
+ summary: {
617
+ type: "string",
618
+ required: true
619
+ },
528
620
  /** Version id (e.g. ULID). */
529
621
  vid: {
530
622
  type: "string",
@@ -534,6 +626,7 @@ var TenantEntity = new Entity5({
534
626
  type: "string",
535
627
  required: true
536
628
  },
629
+ gsi1Shard: gsi1ShardAttribute,
537
630
  deleted: {
538
631
  type: "boolean",
539
632
  required: false
@@ -561,19 +654,24 @@ var TenantEntity = new Entity5({
561
654
  template: "${sk}"
562
655
  }
563
656
  },
564
- /** GSI4 — Resource Type Index: list all Tenants (no scan). */
565
- gsi4: {
566
- index: "GSI4",
567
- condition: () => true,
657
+ /**
658
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
659
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
660
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is `<ISO-8601 lastUpdated>#<id>` (control-plane
661
+ * unlabeled per DR-004). `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
662
+ */
663
+ gsi1: {
664
+ index: "GSI1",
568
665
  pk: {
569
- field: "GSI4PK",
570
- composite: [],
571
- template: "RT#Tenant"
666
+ field: "GSI1PK",
667
+ composite: ["gsi1Shard"],
668
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
572
669
  },
573
670
  sk: {
574
- field: "GSI4SK",
575
- composite: ["tenantId"],
576
- template: "ID#${tenantId}"
671
+ field: "GSI1SK",
672
+ casing: "none",
673
+ composite: ["lastUpdated", "id"],
674
+ template: "${lastUpdated}#${id}"
577
675
  }
578
676
  }
579
677
  }
@@ -604,6 +702,22 @@ var UserEntity = new Entity6({
604
702
  type: "string",
605
703
  required: true
606
704
  },
705
+ /**
706
+ * Summary projection (key display fields as JSON string: id, displayName, status).
707
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
708
+ */
709
+ summary: {
710
+ type: "string",
711
+ required: true
712
+ },
713
+ /**
714
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
715
+ * Post Confirmation Lambda (#770) lands; required thereafter.
716
+ */
717
+ cognitoSub: {
718
+ type: "string",
719
+ required: false
720
+ },
607
721
  /** Version id (e.g. ULID). */
608
722
  vid: {
609
723
  type: "string",
@@ -613,6 +727,7 @@ var UserEntity = new Entity6({
613
727
  type: "string",
614
728
  required: true
615
729
  },
730
+ gsi1Shard: gsi1ShardAttribute,
616
731
  deleted: {
617
732
  type: "boolean",
618
733
  required: false
@@ -640,19 +755,45 @@ var UserEntity = new Entity6({
640
755
  template: "${sk}"
641
756
  }
642
757
  },
643
- /** GSI4 — Resource Type Index: list all Users (no scan). */
644
- gsi4: {
645
- index: "GSI4",
646
- condition: () => true,
758
+ /**
759
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
760
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
761
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
762
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z` characters.
763
+ */
764
+ gsi1: {
765
+ index: "GSI1",
647
766
  pk: {
648
- field: "GSI4PK",
649
- composite: [],
650
- template: "RT#User"
767
+ field: "GSI1PK",
768
+ composite: ["gsi1Shard"],
769
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
651
770
  },
652
771
  sk: {
653
- field: "GSI4SK",
654
- composite: ["id"],
655
- template: "ID#${id}"
772
+ field: "GSI1SK",
773
+ casing: "none",
774
+ composite: ["lastUpdated", "id"],
775
+ template: "${lastUpdated}#${id}"
776
+ }
777
+ },
778
+ /**
779
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
780
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
781
+ * not indexed.
782
+ */
783
+ gsi2: {
784
+ index: "GSI2",
785
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
786
+ pk: {
787
+ field: "GSI2PK",
788
+ casing: "none",
789
+ composite: ["cognitoSub"],
790
+ template: "USER#SUB#${cognitoSub}"
791
+ },
792
+ sk: {
793
+ field: "GSI2SK",
794
+ casing: "none",
795
+ composite: [],
796
+ template: "CURRENT"
656
797
  }
657
798
  }
658
799
  }
@@ -688,6 +829,14 @@ var WorkspaceEntity = new Entity7({
688
829
  type: "string",
689
830
  required: true
690
831
  },
832
+ /**
833
+ * Summary projection (key display fields as JSON string: id, displayName, status).
834
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
835
+ */
836
+ summary: {
837
+ type: "string",
838
+ required: true
839
+ },
691
840
  /** Version id (e.g. ULID). */
692
841
  vid: {
693
842
  type: "string",
@@ -697,6 +846,7 @@ var WorkspaceEntity = new Entity7({
697
846
  type: "string",
698
847
  required: true
699
848
  },
849
+ gsi1Shard: gsi1ShardAttribute,
700
850
  deleted: {
701
851
  type: "boolean",
702
852
  required: false
@@ -724,19 +874,24 @@ var WorkspaceEntity = new Entity7({
724
874
  template: "${sk}"
725
875
  }
726
876
  },
727
- /** GSI4 — Resource Type Index: list all Workspaces for a tenant (no scan). */
728
- gsi4: {
729
- index: "GSI4",
730
- condition: () => true,
877
+ /**
878
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
879
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
880
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
881
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
882
+ */
883
+ gsi1: {
884
+ index: "GSI1",
731
885
  pk: {
732
- field: "GSI4PK",
733
- composite: ["tenantId"],
734
- template: "TID#${tenantId}#RT#Workspace"
886
+ field: "GSI1PK",
887
+ composite: ["tenantId", "gsi1Shard"],
888
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
735
889
  },
736
890
  sk: {
737
- field: "GSI4SK",
738
- composite: ["id"],
739
- template: "ID#${id}"
891
+ field: "GSI1SK",
892
+ casing: "none",
893
+ composite: ["lastUpdated", "id"],
894
+ template: "${lastUpdated}#${id}"
740
895
  }
741
896
  }
742
897
  }
@@ -790,6 +945,7 @@ async function createConfigurationOperation(params) {
790
945
  const roleId = body.roleId ?? context.roleId ?? "-";
791
946
  const lastUpdated = body.lastUpdated ?? date;
792
947
  const vid = body.vid ?? (date.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36));
948
+ const summary = JSON.stringify({ resourceType: "Configuration", id, key });
793
949
  const service = getDynamoControlService(tableName);
794
950
  await service.entities.configuration.put({
795
951
  tenantId,
@@ -799,6 +955,7 @@ async function createConfigurationOperation(params) {
799
955
  key,
800
956
  id,
801
957
  resource: compressResource(resourceStr),
958
+ summary,
802
959
  vid,
803
960
  lastUpdated,
804
961
  sk: SK
@@ -1205,22 +1362,25 @@ async function listConfigurationsOperation(params) {
1205
1362
  const { context, tableName } = params;
1206
1363
  const { tenantId, workspaceId } = context;
1207
1364
  const service = getDynamoControlService(tableName);
1208
- const result = await service.entities.configuration.query.gsi4({ tenantId, workspaceId }).go();
1209
- const entries = (result.data ?? []).map(
1210
- (item) => {
1211
- const resource = JSON.parse(decompressResource(item.resource));
1212
- return {
1365
+ const shardResults = await Promise.all(
1366
+ Array.from(
1367
+ { length: SHARD_COUNT },
1368
+ (_, shard) => service.entities.configuration.query.gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) }).go()
1369
+ )
1370
+ );
1371
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
1372
+ const resource = JSON.parse(decompressResource(item.resource));
1373
+ return {
1374
+ id: item.id,
1375
+ key: item.key,
1376
+ resource: {
1377
+ resourceType: "Configuration",
1213
1378
  id: item.id,
1214
1379
  key: item.key,
1215
- resource: {
1216
- resourceType: "Configuration",
1217
- id: item.id,
1218
- key: item.key,
1219
- resource
1220
- }
1221
- };
1222
- }
1223
- );
1380
+ resource
1381
+ }
1382
+ };
1383
+ });
1224
1384
  return { entries };
1225
1385
  }
1226
1386
 
@@ -1394,8 +1554,14 @@ async function updateConfigurationOperation(params) {
1394
1554
  const resourceStr = typeof resourcePayload === "string" ? resourcePayload : JSON.stringify(resourcePayload ?? {});
1395
1555
  const lastUpdated = body.lastUpdated ?? date;
1396
1556
  const nextVid = existing.data.vid != null ? String(Number(existing.data.vid) + 1) : date.replace(/[-:T.Z]/g, "").slice(0, 12) || "2";
1557
+ const summary = JSON.stringify({
1558
+ resourceType: "Configuration",
1559
+ id: existing.data.id,
1560
+ key: existing.data.key
1561
+ });
1397
1562
  await service.entities.configuration.patch({ tenantId, workspaceId, userId: actorId, roleId, key: id, sk: SK4 }).set({
1398
1563
  resource: compressResource(resourceStr),
1564
+ summary,
1399
1565
  lastUpdated,
1400
1566
  vid: nextVid
1401
1567
  }).go();
@@ -1473,6 +1639,7 @@ router.delete("/:id", deleteConfigurationRoute);
1473
1639
  import express2 from "express";
1474
1640
 
1475
1641
  // src/data/operations/control/membership/membership-create-operation.ts
1642
+ import { extractSummary } from "@openhi/types";
1476
1643
  async function createMembershipOperation(params) {
1477
1644
  const { context, body, tableName } = params;
1478
1645
  const service = getDynamoControlService(tableName);
@@ -1480,20 +1647,19 @@ async function createMembershipOperation(params) {
1480
1647
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1481
1648
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1482
1649
  const vid = `1`;
1650
+ const resource = { resourceType: "Membership", id, ...parsedResource };
1651
+ const summary = JSON.stringify(extractSummary(resource));
1483
1652
  await service.entities.membership.put({
1484
1653
  tenantId: context.tenantId,
1485
1654
  id,
1486
- resource: JSON.stringify({
1487
- resourceType: "Membership",
1488
- id,
1489
- ...parsedResource
1490
- }),
1655
+ resource: JSON.stringify(resource),
1656
+ summary,
1491
1657
  vid,
1492
1658
  lastUpdated
1493
1659
  }).go();
1494
1660
  return {
1495
1661
  id,
1496
- resource: { resourceType: "Membership", id, ...parsedResource },
1662
+ resource,
1497
1663
  meta: { lastUpdated, versionId: vid }
1498
1664
  };
1499
1665
  }
@@ -1601,12 +1767,21 @@ async function getMembershipByIdRoute(req, res) {
1601
1767
  async function listMembershipsOperation(params) {
1602
1768
  const { context, tableName } = params;
1603
1769
  const service = getDynamoControlService(tableName);
1604
- const response = await service.entities.membership.query.gsi4({ tenantId: context.tenantId }).go();
1605
- const entries = (response.data ?? []).map((item) => {
1770
+ const shardResults = await Promise.all(
1771
+ Array.from(
1772
+ { length: SHARD_COUNT },
1773
+ (_, shard) => service.entities.membership.query.gsi1({ tenantId: context.tenantId, gsi1Shard: String(shard) }).go()
1774
+ )
1775
+ );
1776
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
1606
1777
  const parsedResource = JSON.parse(item.resource);
1607
1778
  return {
1608
1779
  id: item.id,
1609
- resource: { resourceType: "Membership", id: item.id, ...parsedResource }
1780
+ resource: {
1781
+ resourceType: "Membership",
1782
+ id: item.id,
1783
+ ...parsedResource
1784
+ }
1610
1785
  };
1611
1786
  });
1612
1787
  return { entries };
@@ -1635,6 +1810,7 @@ async function listMembershipsRoute(req, res) {
1635
1810
  }
1636
1811
 
1637
1812
  // src/data/operations/control/membership/membership-update-operation.ts
1813
+ import { extractSummary as extractSummary2 } from "@openhi/types";
1638
1814
  async function updateMembershipOperation(params) {
1639
1815
  const { context, id, body, tableName } = params;
1640
1816
  const service = getDynamoControlService(tableName);
@@ -1645,20 +1821,19 @@ async function updateMembershipOperation(params) {
1645
1821
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1646
1822
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1647
1823
  const vid = `${Date.now()}`;
1824
+ const resource = { resourceType: "Membership", id, ...parsedResource };
1825
+ const summary = JSON.stringify(extractSummary2(resource));
1648
1826
  await service.entities.membership.put({
1649
1827
  tenantId: context.tenantId,
1650
1828
  id,
1651
- resource: JSON.stringify({
1652
- resourceType: "Membership",
1653
- id,
1654
- ...parsedResource
1655
- }),
1829
+ resource: JSON.stringify(resource),
1830
+ summary,
1656
1831
  vid,
1657
1832
  lastUpdated
1658
1833
  }).go();
1659
1834
  return {
1660
1835
  id,
1661
- resource: { resourceType: "Membership", id, ...parsedResource },
1836
+ resource,
1662
1837
  meta: { lastUpdated, versionId: vid }
1663
1838
  };
1664
1839
  }
@@ -1715,6 +1890,7 @@ router2.delete("/:id", deleteMembershipRoute);
1715
1890
  import express3 from "express";
1716
1891
 
1717
1892
  // src/data/operations/control/role/role-create-operation.ts
1893
+ import { extractSummary as extractSummary3 } from "@openhi/types";
1718
1894
  async function createRoleOperation(params) {
1719
1895
  const { context, body, tableName } = params;
1720
1896
  const service = getDynamoControlService(tableName);
@@ -1722,15 +1898,18 @@ async function createRoleOperation(params) {
1722
1898
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1723
1899
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1724
1900
  const vid = `1`;
1901
+ const resource = { resourceType: "Role", id, ...parsedResource };
1902
+ const summary = JSON.stringify(extractSummary3(resource));
1725
1903
  await service.entities.role.put({
1726
1904
  id,
1727
- resource: JSON.stringify({ resourceType: "Role", id, ...parsedResource }),
1905
+ resource: JSON.stringify(resource),
1906
+ summary,
1728
1907
  vid,
1729
1908
  lastUpdated
1730
1909
  }).go();
1731
1910
  return {
1732
1911
  id,
1733
- resource: { resourceType: "Role", id, ...parsedResource },
1912
+ resource,
1734
1913
  meta: { lastUpdated, versionId: vid }
1735
1914
  };
1736
1915
  }
@@ -1838,8 +2017,13 @@ async function getRoleByIdRoute(req, res) {
1838
2017
  async function listRolesOperation(params) {
1839
2018
  const { tableName } = params;
1840
2019
  const service = getDynamoControlService(tableName);
1841
- const response = await service.entities.role.query.gsi4({}).go();
1842
- const entries = (response.data ?? []).map((item) => {
2020
+ const shardResults = await Promise.all(
2021
+ Array.from(
2022
+ { length: SHARD_COUNT },
2023
+ (_, shard) => service.entities.role.query.gsi1({ gsi1Shard: String(shard) }).go()
2024
+ )
2025
+ );
2026
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
1843
2027
  const parsedResource = JSON.parse(item.resource);
1844
2028
  return {
1845
2029
  id: item.id,
@@ -1872,6 +2056,7 @@ async function listRolesRoute(req, res) {
1872
2056
  }
1873
2057
 
1874
2058
  // src/data/operations/control/role/role-update-operation.ts
2059
+ import { extractSummary as extractSummary4 } from "@openhi/types";
1875
2060
  async function updateRoleOperation(params) {
1876
2061
  const { context, id, body, tableName } = params;
1877
2062
  const service = getDynamoControlService(tableName);
@@ -1882,15 +2067,18 @@ async function updateRoleOperation(params) {
1882
2067
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1883
2068
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1884
2069
  const vid = `${Date.now()}`;
2070
+ const resource = { resourceType: "Role", id, ...parsedResource };
2071
+ const summary = JSON.stringify(extractSummary4(resource));
1885
2072
  await service.entities.role.put({
1886
2073
  id,
1887
- resource: JSON.stringify({ resourceType: "Role", id, ...parsedResource }),
2074
+ resource: JSON.stringify(resource),
2075
+ summary,
1888
2076
  vid,
1889
2077
  lastUpdated
1890
2078
  }).go();
1891
2079
  return {
1892
2080
  id,
1893
- resource: { resourceType: "Role", id, ...parsedResource },
2081
+ resource,
1894
2082
  meta: { lastUpdated, versionId: vid }
1895
2083
  };
1896
2084
  }
@@ -1947,6 +2135,7 @@ router3.delete("/:id", deleteRoleRoute);
1947
2135
  import express4 from "express";
1948
2136
 
1949
2137
  // src/data/operations/control/roleassignment/roleassignment-create-operation.ts
2138
+ import { extractSummary as extractSummary5 } from "@openhi/types";
1950
2139
  async function createRoleAssignmentOperation(params) {
1951
2140
  const { context, body, tableName } = params;
1952
2141
  const service = getDynamoControlService(tableName);
@@ -1954,20 +2143,19 @@ async function createRoleAssignmentOperation(params) {
1954
2143
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
1955
2144
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
1956
2145
  const vid = `1`;
2146
+ const resource = { resourceType: "RoleAssignment", id, ...parsedResource };
2147
+ const summary = JSON.stringify(extractSummary5(resource));
1957
2148
  await service.entities.roleAssignment.put({
1958
2149
  tenantId: context.tenantId,
1959
2150
  id,
1960
- resource: JSON.stringify({
1961
- resourceType: "RoleAssignment",
1962
- id,
1963
- ...parsedResource
1964
- }),
2151
+ resource: JSON.stringify(resource),
2152
+ summary,
1965
2153
  vid,
1966
2154
  lastUpdated
1967
2155
  }).go();
1968
2156
  return {
1969
2157
  id,
1970
- resource: { resourceType: "RoleAssignment", id, ...parsedResource },
2158
+ resource,
1971
2159
  meta: { lastUpdated, versionId: vid }
1972
2160
  };
1973
2161
  }
@@ -2075,8 +2263,13 @@ async function getRoleAssignmentByIdRoute(req, res) {
2075
2263
  async function listRoleAssignmentsOperation(params) {
2076
2264
  const { context, tableName } = params;
2077
2265
  const service = getDynamoControlService(tableName);
2078
- const response = await service.entities.roleAssignment.query.gsi4({ tenantId: context.tenantId }).go();
2079
- const entries = (response.data ?? []).map((item) => {
2266
+ const shardResults = await Promise.all(
2267
+ Array.from(
2268
+ { length: SHARD_COUNT },
2269
+ (_, shard) => service.entities.roleAssignment.query.gsi1({ tenantId: context.tenantId, gsi1Shard: String(shard) }).go()
2270
+ )
2271
+ );
2272
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
2080
2273
  const parsedResource = JSON.parse(item.resource);
2081
2274
  return {
2082
2275
  id: item.id,
@@ -2113,6 +2306,7 @@ async function listRoleAssignmentsRoute(req, res) {
2113
2306
  }
2114
2307
 
2115
2308
  // src/data/operations/control/roleassignment/roleassignment-update-operation.ts
2309
+ import { extractSummary as extractSummary6 } from "@openhi/types";
2116
2310
  async function updateRoleAssignmentOperation(params) {
2117
2311
  const { context, id, body, tableName } = params;
2118
2312
  const service = getDynamoControlService(tableName);
@@ -2123,20 +2317,19 @@ async function updateRoleAssignmentOperation(params) {
2123
2317
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2124
2318
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
2125
2319
  const vid = `${Date.now()}`;
2320
+ const resource = { resourceType: "RoleAssignment", id, ...parsedResource };
2321
+ const summary = JSON.stringify(extractSummary6(resource));
2126
2322
  await service.entities.roleAssignment.put({
2127
2323
  tenantId: context.tenantId,
2128
2324
  id,
2129
- resource: JSON.stringify({
2130
- resourceType: "RoleAssignment",
2131
- id,
2132
- ...parsedResource
2133
- }),
2325
+ resource: JSON.stringify(resource),
2326
+ summary,
2134
2327
  vid,
2135
2328
  lastUpdated
2136
2329
  }).go();
2137
2330
  return {
2138
2331
  id,
2139
- resource: { resourceType: "RoleAssignment", id, ...parsedResource },
2332
+ resource,
2140
2333
  meta: { lastUpdated, versionId: vid }
2141
2334
  };
2142
2335
  }
@@ -2193,6 +2386,7 @@ router4.delete("/:id", deleteRoleAssignmentRoute);
2193
2386
  import express5 from "express";
2194
2387
 
2195
2388
  // src/data/operations/control/tenant/tenant-create-operation.ts
2389
+ import { extractSummary as extractSummary7 } from "@openhi/types";
2196
2390
  async function createTenantOperation(params) {
2197
2391
  const { context, body, tableName } = params;
2198
2392
  const service = getDynamoControlService(tableName);
@@ -2201,10 +2395,12 @@ async function createTenantOperation(params) {
2201
2395
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
2202
2396
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2203
2397
  const resource = { resourceType: "Tenant", id, ...parsedResource };
2398
+ const summary = JSON.stringify(extractSummary7(resource));
2204
2399
  await service.entities.tenant.put({
2205
2400
  tenantId: id,
2206
2401
  id,
2207
2402
  resource: JSON.stringify(resource),
2403
+ summary,
2208
2404
  vid,
2209
2405
  lastUpdated
2210
2406
  }).go();
@@ -2313,8 +2509,13 @@ async function getTenantByIdRoute(req, res) {
2313
2509
  async function listTenantsOperation(params) {
2314
2510
  const { tableName } = params;
2315
2511
  const service = getDynamoControlService(tableName);
2316
- const { data: items } = await service.entities.tenant.query.gsi4({}).go();
2317
- const entries = items.map((item) => {
2512
+ const shardResults = await Promise.all(
2513
+ Array.from(
2514
+ { length: SHARD_COUNT },
2515
+ (_, shard) => service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go()
2516
+ )
2517
+ );
2518
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
2318
2519
  const parsed = JSON.parse(item.resource);
2319
2520
  return {
2320
2521
  id: item.id,
@@ -2347,6 +2548,7 @@ async function listTenantsRoute(req, res) {
2347
2548
  }
2348
2549
 
2349
2550
  // src/data/operations/control/tenant/tenant-update-operation.ts
2551
+ import { extractSummary as extractSummary8 } from "@openhi/types";
2350
2552
  async function updateTenantOperation(params) {
2351
2553
  const { context, id, body, tableName } = params;
2352
2554
  const service = getDynamoControlService(tableName);
@@ -2364,7 +2566,8 @@ async function updateTenantOperation(params) {
2364
2566
  resourceType: "Tenant",
2365
2567
  id
2366
2568
  };
2367
- await service.entities.tenant.patch({ tenantId: id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), vid, lastUpdated }).go();
2569
+ const summary = JSON.stringify(extractSummary8(updated));
2570
+ await service.entities.tenant.patch({ tenantId: id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), summary, vid, lastUpdated }).go();
2368
2571
  return { id, resource: updated, meta: { lastUpdated, versionId: vid } };
2369
2572
  }
2370
2573
 
@@ -2420,6 +2623,7 @@ router5.delete("/:id", deleteTenantRoute);
2420
2623
  import express6 from "express";
2421
2624
 
2422
2625
  // src/data/operations/control/user/user-create-operation.ts
2626
+ import { extractSummary as extractSummary9 } from "@openhi/types";
2423
2627
  async function createUserOperation(params) {
2424
2628
  const { context, body, tableName } = params;
2425
2629
  const service = getDynamoControlService(tableName);
@@ -2427,15 +2631,18 @@ async function createUserOperation(params) {
2427
2631
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2428
2632
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
2429
2633
  const vid = `1`;
2634
+ const resource = { resourceType: "User", id, ...parsedResource };
2635
+ const summary = JSON.stringify(extractSummary9(resource));
2430
2636
  await service.entities.user.put({
2431
2637
  id,
2432
- resource: JSON.stringify({ resourceType: "User", id, ...parsedResource }),
2638
+ resource: JSON.stringify(resource),
2639
+ summary,
2433
2640
  vid,
2434
2641
  lastUpdated
2435
2642
  }).go();
2436
2643
  return {
2437
2644
  id,
2438
- resource: { resourceType: "User", id, ...parsedResource },
2645
+ resource,
2439
2646
  meta: { lastUpdated, versionId: vid }
2440
2647
  };
2441
2648
  }
@@ -2543,8 +2750,13 @@ async function getUserByIdRoute(req, res) {
2543
2750
  async function listUsersOperation(params) {
2544
2751
  const { tableName } = params;
2545
2752
  const service = getDynamoControlService(tableName);
2546
- const response = await service.entities.user.query.gsi4({}).go();
2547
- const entries = (response.data ?? []).map((item) => {
2753
+ const shardResults = await Promise.all(
2754
+ Array.from(
2755
+ { length: SHARD_COUNT },
2756
+ (_, shard) => service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go()
2757
+ )
2758
+ );
2759
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
2548
2760
  const parsedResource = JSON.parse(item.resource);
2549
2761
  return {
2550
2762
  id: item.id,
@@ -2577,6 +2789,7 @@ async function listUsersRoute(req, res) {
2577
2789
  }
2578
2790
 
2579
2791
  // src/data/operations/control/user/user-update-operation.ts
2792
+ import { extractSummary as extractSummary10 } from "@openhi/types";
2580
2793
  async function updateUserOperation(params) {
2581
2794
  const { context, id, body, tableName } = params;
2582
2795
  const service = getDynamoControlService(tableName);
@@ -2587,15 +2800,18 @@ async function updateUserOperation(params) {
2587
2800
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2588
2801
  const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
2589
2802
  const vid = `${Date.now()}`;
2803
+ const resource = { resourceType: "User", id, ...parsedResource };
2804
+ const summary = JSON.stringify(extractSummary10(resource));
2590
2805
  await service.entities.user.put({
2591
2806
  id,
2592
- resource: JSON.stringify({ resourceType: "User", id, ...parsedResource }),
2807
+ resource: JSON.stringify(resource),
2808
+ summary,
2593
2809
  vid,
2594
2810
  lastUpdated
2595
2811
  }).go();
2596
2812
  return {
2597
2813
  id,
2598
- resource: { resourceType: "User", id, ...parsedResource },
2814
+ resource,
2599
2815
  meta: { lastUpdated, versionId: vid }
2600
2816
  };
2601
2817
  }
@@ -2652,6 +2868,7 @@ router6.delete("/:id", deleteUserRoute);
2652
2868
  import express7 from "express";
2653
2869
 
2654
2870
  // src/data/operations/control/workspace/workspace-create-operation.ts
2871
+ import { extractSummary as extractSummary11 } from "@openhi/types";
2655
2872
  async function createWorkspaceOperation(params) {
2656
2873
  const { context, body, tableName } = params;
2657
2874
  const { tenantId } = context;
@@ -2661,7 +2878,15 @@ async function createWorkspaceOperation(params) {
2661
2878
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
2662
2879
  const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
2663
2880
  const resource = { resourceType: "Workspace", id, ...parsedResource };
2664
- await service.entities.workspace.put({ tenantId, id, resource: JSON.stringify(resource), vid, lastUpdated }).go();
2881
+ const summary = JSON.stringify(extractSummary11(resource));
2882
+ await service.entities.workspace.put({
2883
+ tenantId,
2884
+ id,
2885
+ resource: JSON.stringify(resource),
2886
+ summary,
2887
+ vid,
2888
+ lastUpdated
2889
+ }).go();
2665
2890
  return { id, resource, meta: { lastUpdated, versionId: vid } };
2666
2891
  }
2667
2892
 
@@ -2770,8 +2995,13 @@ async function listWorkspacesOperation(params) {
2770
2995
  const { context, tableName } = params;
2771
2996
  const { tenantId } = context;
2772
2997
  const service = getDynamoControlService(tableName);
2773
- const { data: items } = await service.entities.workspace.query.gsi4({ tenantId }).go();
2774
- const entries = items.map((item) => {
2998
+ const shardResults = await Promise.all(
2999
+ Array.from(
3000
+ { length: SHARD_COUNT },
3001
+ (_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
3002
+ )
3003
+ );
3004
+ const entries = shardResults.flatMap((shardResult) => shardResult.data ?? []).map((item) => {
2775
3005
  const parsed = JSON.parse(item.resource);
2776
3006
  return {
2777
3007
  id: item.id,
@@ -2804,6 +3034,7 @@ async function listWorkspacesRoute(req, res) {
2804
3034
  }
2805
3035
 
2806
3036
  // src/data/operations/control/workspace/workspace-update-operation.ts
3037
+ import { extractSummary as extractSummary12 } from "@openhi/types";
2807
3038
  async function updateWorkspaceOperation(params) {
2808
3039
  const { context, id, body, tableName } = params;
2809
3040
  const { tenantId } = context;
@@ -2822,7 +3053,8 @@ async function updateWorkspaceOperation(params) {
2822
3053
  resourceType: "Workspace",
2823
3054
  id
2824
3055
  };
2825
- await service.entities.workspace.patch({ tenantId, id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), vid, lastUpdated }).go();
3056
+ const summary = JSON.stringify(extractSummary12(updated));
3057
+ await service.entities.workspace.patch({ tenantId, id, sk: "CURRENT" }).set({ resource: JSON.stringify(updated), summary, vid, lastUpdated }).go();
2826
3058
  return { id, resource: updated, meta: { lastUpdated, versionId: vid } };
2827
3059
  }
2828
3060
 
@@ -2934,6 +3166,18 @@ var dataEntityAttributes = {
2934
3166
  type: "string",
2935
3167
  required: true
2936
3168
  },
3169
+ /**
3170
+ * Summary projection of the FHIR resource as a JSON string (uncompressed). Populated on every
3171
+ * write via `extractSummary(resource)` so GSI projections can surface list/lookup data without
3172
+ * reading the compressed `resource` blob. Kept uncompressed because the summary is small and
3173
+ * must be fast to retrieve without encode/decode overhead.
3174
+ *
3175
+ * @see sites/www-docs/content/architecture/adr/2026-04-17-02-fhir-summary-projection-for-gsi-access-patterns.md
3176
+ */
3177
+ summary: {
3178
+ type: "string",
3179
+ required: true
3180
+ },
2937
3181
  /** Version id (e.g. ULID). Tracks current version; S3 history key. */
2938
3182
  vid: {
2939
3183
  type: "string",
@@ -2943,6 +3187,41 @@ var dataEntityAttributes = {
2943
3187
  type: "string",
2944
3188
  required: true
2945
3189
  },
3190
+ /**
3191
+ * Shard index segment for the GSI1 partition key. Computed deterministically from `id`
3192
+ * via `computeShard` so updates always land on the same shard. Stored as a string because
3193
+ * it appears as a literal segment in the GSI1 PK template; the underlying value is 0..3.
3194
+ * Not `required` because the value is derived via `watch`/`set`; ElectroDB's required-field
3195
+ * check runs before watch propagation, so callers must not fail validation on a derived field.
3196
+ *
3197
+ * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/single-table-design.md — GSI1 (sharded)
3198
+ */
3199
+ gsi1Shard: {
3200
+ type: "string",
3201
+ watch: ["id"],
3202
+ set: (_val, item) => {
3203
+ if (typeof item?.id !== "string" || item.id.length === 0) {
3204
+ return void 0;
3205
+ }
3206
+ return String(computeShard(item.id));
3207
+ }
3208
+ },
3209
+ /**
3210
+ * GSI1 sort key. Written as the index's SK verbatim so list endpoints can
3211
+ * use `BEGINS_WITH` for prefix queries (e.g. `?name=Sm` against Patient).
3212
+ * Computed at write time via `extractSortKey(resource)` per DR-004:
3213
+ * - Labeled types (LABEL_PATHS): `<normalizedLabel>#<id>`
3214
+ * - Unlabeled types: `<ISO-8601 lastUpdated>#<id>`
3215
+ * The factory deliberately does not derive this from `lastUpdated`/`id`
3216
+ * — that would lock every type into the unlabeled fallback and defeat
3217
+ * label-based BEGINS_WITH on labeled types.
3218
+ *
3219
+ * @see openhi-planning DR-004
3220
+ */
3221
+ gsi1sk: {
3222
+ type: "string",
3223
+ required: true
3224
+ },
2946
3225
  deleted: {
2947
3226
  type: "boolean",
2948
3227
  required: false
@@ -2977,18 +3256,26 @@ function createDataEntity(entity, resourceTypeLabel) {
2977
3256
  composite: ["sk"]
2978
3257
  }
2979
3258
  },
2980
- /** GSI4 — Resource Type Index: list all resources of this type in a workspace (no scan). Used by list operations (e.g. GET /Patient). */
2981
- gsi4: {
2982
- index: "GSI4",
3259
+ /**
3260
+ * GSI1 — Unified Sharded List: list all resources of this type in a workspace; reads fan
3261
+ * out across the four shards and merge by SK. SK is the writer-supplied `gsi1sk` verbatim
3262
+ * (per DR-004) so labeled types serve `BEGINS_WITH` prefix queries on the natural label.
3263
+ * `casing: "none"` is required on the SK because the writer (`extractSortKey`) already
3264
+ * applies DR-004 normalization — ElectroDB's default lowercasing would mangle the
3265
+ * ISO-8601 unlabeled fallback (`T`/`Z` → `t`/`z`).
3266
+ */
3267
+ gsi1: {
3268
+ index: "GSI1",
2983
3269
  pk: {
2984
- field: "GSI4PK",
2985
- composite: ["tenantId", "workspaceId"],
2986
- template: `TID#\${tenantId}#WID#\${workspaceId}#RT#${resourceTypeLabel}`
3270
+ field: "GSI1PK",
3271
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
3272
+ template: `TID#\${tenantId}#WID#\${workspaceId}#RT#${resourceTypeLabel}#SHARD#\${gsi1Shard}`
2987
3273
  },
2988
3274
  sk: {
2989
- field: "GSI4SK",
2990
- composite: ["id"],
2991
- template: `ID#\${id}`
3275
+ field: "GSI1SK",
3276
+ casing: "none",
3277
+ composite: ["gsi1sk"],
3278
+ template: `\${gsi1sk}`
2992
3279
  }
2993
3280
  }
2994
3281
  }
@@ -3882,6 +4169,7 @@ function getDynamoDataService(tableName) {
3882
4169
  }
3883
4170
 
3884
4171
  // src/data/operations/data-operations-common.ts
4172
+ import { extractSortKey, extractSummary as extractSummary13 } from "@openhi/types";
3885
4173
  var DATA_ENTITY_SK = "CURRENT";
3886
4174
  async function getDataEntityById(entity, tenantId, workspaceId, id, resourceLabel) {
3887
4175
  const result = await entity.get({
@@ -3910,28 +4198,40 @@ async function deleteDataEntityById(entity, tenantId, workspaceId, id) {
3910
4198
  }).go();
3911
4199
  }
3912
4200
  async function listDataEntitiesByWorkspace(entity, tenantId, workspaceId) {
3913
- const result = await entity.query.gsi4({ tenantId, workspaceId }).go();
3914
- const items = result.data ?? [];
3915
- const entries = items.map((item) => {
3916
- const parsed = JSON.parse(decompressResource(item.resource));
3917
- return {
3918
- id: item.id,
3919
- resource: { ...parsed, id: item.id }
3920
- };
3921
- });
4201
+ const shardResults = await Promise.all(
4202
+ Array.from(
4203
+ { length: SHARD_COUNT },
4204
+ (_, shard) => entity.query.gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) }).go()
4205
+ )
4206
+ );
4207
+ const entries = [];
4208
+ for (const shardResult of shardResults) {
4209
+ for (const item of shardResult.data ?? []) {
4210
+ const parsed = JSON.parse(decompressResource(item.resource));
4211
+ entries.push({
4212
+ id: item.id,
4213
+ resource: { ...parsed, id: item.id }
4214
+ });
4215
+ }
4216
+ }
3922
4217
  return { entries };
3923
4218
  }
3924
4219
  async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourceWithAudit, fallbackDate) {
3925
4220
  const lastUpdated = resourceWithAudit.meta?.lastUpdated ?? fallbackDate ?? (/* @__PURE__ */ new Date()).toISOString();
3926
4221
  const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
4222
+ const resourceLike = resourceWithAudit;
4223
+ const summary = JSON.stringify(extractSummary13(resourceLike));
4224
+ const gsi1sk = extractSortKey(resourceLike);
3927
4225
  await entity.put({
3928
4226
  sk: DATA_ENTITY_SK,
3929
4227
  tenantId,
3930
4228
  workspaceId,
3931
4229
  id,
3932
4230
  resource: compressResource(JSON.stringify(resourceWithAudit)),
4231
+ summary,
3933
4232
  vid,
3934
- lastUpdated
4233
+ lastUpdated,
4234
+ gsi1sk
3935
4235
  }).go();
3936
4236
  return {
3937
4237
  id,
@@ -3978,6 +4278,9 @@ async function updateDataEntityById(entity, tenantId, workspaceId, id, resourceL
3978
4278
  }
3979
4279
  const existingStr = decompressResource(existing.data.resource);
3980
4280
  const { resource, lastUpdated } = buildPatched(existingStr);
4281
+ const resourceLike = resource;
4282
+ const summary = JSON.stringify(extractSummary13(resourceLike));
4283
+ const gsi1sk = extractSortKey(resourceLike);
3981
4284
  await entity.patch({
3982
4285
  tenantId,
3983
4286
  workspaceId,
@@ -3985,7 +4288,9 @@ async function updateDataEntityById(entity, tenantId, workspaceId, id, resourceL
3985
4288
  sk: DATA_ENTITY_SK
3986
4289
  }).set({
3987
4290
  resource: compressResource(JSON.stringify(resource)),
3988
- lastUpdated
4291
+ summary,
4292
+ lastUpdated,
4293
+ gsi1sk
3989
4294
  }).go();
3990
4295
  return {
3991
4296
  id,
@@ -12629,9 +12934,168 @@ async function listEncountersOperation(params) {
12629
12934
  );
12630
12935
  }
12631
12936
 
12937
+ // src/data/postgres/data-api-postgres-query-runner.ts
12938
+ import {
12939
+ ExecuteStatementCommand,
12940
+ RDSDataClient
12941
+ } from "@aws-sdk/client-rds-data";
12942
+ var DataApiPostgresQueryRunner = class {
12943
+ constructor(options) {
12944
+ this.client = options.client ?? new RDSDataClient({});
12945
+ this.clusterArn = options.clusterArn;
12946
+ this.secretArn = options.secretArn;
12947
+ this.database = options.database;
12948
+ this.schema = options.schema;
12949
+ }
12950
+ async query(sql, params) {
12951
+ const out = await this.client.send(
12952
+ new ExecuteStatementCommand({
12953
+ resourceArn: this.clusterArn,
12954
+ secretArn: this.secretArn,
12955
+ database: this.database,
12956
+ schema: this.schema,
12957
+ sql,
12958
+ parameters: params.map(toSqlParameter),
12959
+ // Results as named columns so we can map them back to JS objects.
12960
+ includeResultMetadata: true,
12961
+ // Encode JSONB results as strings, then parse client-side. Without
12962
+ // this, Data API returns JSON values inline as their underlying types
12963
+ // and complex JSONB columns get clipped.
12964
+ formatRecordsAs: "JSON"
12965
+ })
12966
+ );
12967
+ if (!out.formattedRecords) {
12968
+ return [];
12969
+ }
12970
+ return JSON.parse(out.formattedRecords);
12971
+ }
12972
+ };
12973
+ function toSqlParameter(param) {
12974
+ if (param.value === null) {
12975
+ return { name: param.name, value: { isNull: true } };
12976
+ }
12977
+ const v = param.value;
12978
+ let field;
12979
+ if (typeof v === "string") {
12980
+ field = { stringValue: v };
12981
+ } else if (typeof v === "boolean") {
12982
+ field = { booleanValue: v };
12983
+ } else if (Number.isInteger(v)) {
12984
+ field = { longValue: v };
12985
+ } else {
12986
+ field = { doubleValue: v };
12987
+ }
12988
+ return { name: param.name, value: field };
12989
+ }
12990
+
12991
+ // src/data/postgres/default-postgres-query-runner.ts
12992
+ var cached;
12993
+ function readEnv(name) {
12994
+ const v = process.env[name]?.trim();
12995
+ if (!v) {
12996
+ throw new Error(
12997
+ `Missing required env var for default PostgresQueryRunner: ${name}`
12998
+ );
12999
+ }
13000
+ return v;
13001
+ }
13002
+ function getDefaultPostgresQueryRunner() {
13003
+ if (!cached) {
13004
+ cached = new DataApiPostgresQueryRunner({
13005
+ clusterArn: readEnv("OPENHI_PG_CLUSTER_ARN"),
13006
+ secretArn: readEnv("OPENHI_PG_SECRET_ARN"),
13007
+ database: readEnv("OPENHI_PG_DATABASE"),
13008
+ schema: readEnv("OPENHI_PG_SCHEMA")
13009
+ });
13010
+ }
13011
+ return cached;
13012
+ }
13013
+
13014
+ // src/data/operations/data/encounter/encounter-search-by-patient-operation.ts
13015
+ var DEFAULT_LIMIT = 100;
13016
+ function buildOpenHiResourceUrn(opts) {
13017
+ return `urn:ohi:${opts.tenantId}:${opts.workspaceId}:${opts.resourceType}:${opts.resourceId}`;
13018
+ }
13019
+ function buildSearchEncountersByPatientSql() {
13020
+ return [
13021
+ "SELECT resource_id AS id, resource",
13022
+ "FROM resources",
13023
+ "WHERE tenant_id = :tenantId",
13024
+ " AND workspace_id = :workspaceId",
13025
+ " AND resource_type = 'Encounter'",
13026
+ " AND deleted_at IS NULL",
13027
+ " AND (resource @> :containmentRelative::jsonb",
13028
+ " OR resource @> :containmentUrn::jsonb)",
13029
+ "ORDER BY last_updated DESC",
13030
+ "LIMIT :limit;"
13031
+ ].join("\n");
13032
+ }
13033
+ async function searchEncountersByPatientOperation(params) {
13034
+ const { context, patientId } = params;
13035
+ const { tenantId, workspaceId } = context;
13036
+ const runner = params.runner ?? getDefaultPostgresQueryRunner();
13037
+ const limit = params.limit ?? DEFAULT_LIMIT;
13038
+ const containmentRelative = JSON.stringify({
13039
+ subject: { reference: `Patient/${patientId}` }
13040
+ });
13041
+ const containmentUrn = JSON.stringify({
13042
+ subject: {
13043
+ reference: buildOpenHiResourceUrn({
13044
+ tenantId,
13045
+ workspaceId,
13046
+ resourceType: "Patient",
13047
+ resourceId: patientId
13048
+ })
13049
+ }
13050
+ });
13051
+ const rows = await runner.query(
13052
+ buildSearchEncountersByPatientSql(),
13053
+ [
13054
+ { name: "tenantId", value: tenantId },
13055
+ { name: "workspaceId", value: workspaceId },
13056
+ { name: "containmentRelative", value: containmentRelative },
13057
+ { name: "containmentUrn", value: containmentUrn },
13058
+ { name: "limit", value: limit }
13059
+ ]
13060
+ );
13061
+ const entries = rows.map((row) => ({
13062
+ id: row.id,
13063
+ resource: {
13064
+ ...row.resource,
13065
+ id: row.id
13066
+ }
13067
+ }));
13068
+ return { entries };
13069
+ }
13070
+
12632
13071
  // src/data/rest-api/routes/data/encounter/encounter-list-route.ts
13072
+ function singleStringQueryParam(req, name) {
13073
+ const v = req.query[name];
13074
+ if (typeof v !== "string") {
13075
+ return void 0;
13076
+ }
13077
+ const trimmed = v.trim();
13078
+ return trimmed === "" ? void 0 : trimmed;
13079
+ }
12633
13080
  async function listEncountersRoute(req, res) {
12634
13081
  const ctx = req.openhiContext;
13082
+ const patientId = singleStringQueryParam(req, "patient");
13083
+ if (patientId) {
13084
+ try {
13085
+ const result = await searchEncountersByPatientOperation({
13086
+ context: ctx,
13087
+ patientId
13088
+ });
13089
+ const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
13090
+ return res.json(bundle);
13091
+ } catch (err) {
13092
+ return sendOperationOutcome500(
13093
+ res,
13094
+ err,
13095
+ "GET /Encounter?patient= search error:"
13096
+ );
13097
+ }
13098
+ }
12635
13099
  try {
12636
13100
  const result = await listEncountersOperation({ context: ctx });
12637
13101
  const bundle = buildSearchsetBundle(BASE_PATH.ENCOUNTER, result.entries);
@@ -24548,6 +25012,7 @@ import { ulid as ulid99 } from "ulid";
24548
25012
  // src/data/import-patient.ts
24549
25013
  import { readFileSync } from "fs";
24550
25014
  import { resolve } from "path";
25015
+ import { extractSummary as extractSummary14 } from "@openhi/types";
24551
25016
  function extractPatient(parsed) {
24552
25017
  if (parsed && typeof parsed === "object" && "resourceType" in parsed) {
24553
25018
  const root = parsed;
@@ -24613,6 +25078,9 @@ function patientToPutAttrs(patient, options) {
24613
25078
  workspaceId,
24614
25079
  id: patient.id,
24615
25080
  resource: compressResource(JSON.stringify(patientWithMeta)),
25081
+ summary: JSON.stringify(
25082
+ extractSummary14(patientWithMeta)
25083
+ ),
24616
25084
  vid: lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36),
24617
25085
  lastUpdated,
24618
25086
  identifierSystem: "",
@@ -33933,6 +34401,7 @@ app.use(
33933
34401
  "/MedicinalProductPackaged",
33934
34402
  "/MedicinalProductPharmaceutical",
33935
34403
  "/MedicinalProductUndesirableEffect",
34404
+ "/Membership",
33936
34405
  "/MessageDefinition",
33937
34406
  "/MessageHeader",
33938
34407
  "/MolecularSequence",
@@ -33962,6 +34431,8 @@ app.use(
33962
34431
  "/ResearchSubject",
33963
34432
  "/RiskAssessment",
33964
34433
  "/RiskEvidenceSynthesis",
34434
+ "/Role",
34435
+ "/RoleAssignment",
33965
34436
  "/Schedule",
33966
34437
  "/ServiceRequest",
33967
34438
  "/SearchParameter",
@@ -33981,12 +34452,15 @@ app.use(
33981
34452
  "/SupplyDelivery",
33982
34453
  "/SupplyRequest",
33983
34454
  "/Task",
34455
+ "/Tenant",
33984
34456
  "/TerminologyCapabilities",
33985
34457
  "/TestReport",
33986
34458
  "/TestScript",
34459
+ "/User",
33987
34460
  "/ValueSet",
33988
34461
  "/VerificationResult",
33989
- "/VisionPrescription"
34462
+ "/VisionPrescription",
34463
+ "/Workspace"
33990
34464
  ],
33991
34465
  openHiContextMiddleware
33992
34466
  );
@@ -34148,6 +34622,18 @@ app.use("/RoleAssignment", router4);
34148
34622
  app.use("/Tenant", router5);
34149
34623
  app.use("/User", router6);
34150
34624
  app.use("/Workspace", router7);
34625
+ app.use((_req, res) => {
34626
+ res.status(404).json({
34627
+ resourceType: "OperationOutcome",
34628
+ issue: [
34629
+ {
34630
+ severity: "error",
34631
+ code: "not-supported",
34632
+ diagnostics: "The requested endpoint or resource type is not supported by this server."
34633
+ }
34634
+ ]
34635
+ });
34636
+ });
34151
34637
 
34152
34638
  // src/data/lambda/rest-api-lambda.handler.ts
34153
34639
  var handler = serverlessExpress({ app });