@openhi/constructs 0.0.110 → 0.0.112
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.
- package/lib/chunk-23PUSHBV.mjs +24 -0
- package/lib/chunk-23PUSHBV.mjs.map +1 -0
- package/lib/chunk-2O3CXY2C.mjs +79 -0
- package/lib/chunk-2O3CXY2C.mjs.map +1 -0
- package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
- package/lib/chunk-6NBGYGFL.mjs +1803 -0
- package/lib/chunk-6NBGYGFL.mjs.map +1 -0
- package/lib/chunk-7RZHFI77.mjs +22 -0
- package/lib/chunk-7RZHFI77.mjs.map +1 -0
- package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
- package/lib/chunk-FYHBHHWK.mjs +47 -0
- package/lib/chunk-FYHBHHWK.mjs.map +1 -0
- package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
- package/lib/chunk-GBDIGTNV.mjs.map +1 -0
- package/lib/chunk-HQ67J7BP.mjs +199 -0
- package/lib/chunk-HQ67J7BP.mjs.map +1 -0
- package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
- package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
- package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
- package/lib/chunk-NZRW7ROK.mjs.map +1 -0
- package/lib/chunk-QJDHVMKT.mjs +117 -0
- package/lib/chunk-QJDHVMKT.mjs.map +1 -0
- package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
- package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
- package/lib/chunk-TRY7JGWO.mjs +16 -0
- package/lib/chunk-TRY7JGWO.mjs.map +1 -0
- package/lib/chunk-W4KR4CSL.mjs +236 -0
- package/lib/chunk-W4KR4CSL.mjs.map +1 -0
- package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
- package/lib/chunk-WQWFVEVX.mjs +66 -0
- package/lib/chunk-WQWFVEVX.mjs.map +1 -0
- package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
- package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
- package/lib/data-store-postgres-replication.handler.js +26 -17
- package/lib/data-store-postgres-replication.handler.js.map +1 -1
- package/lib/data-store-postgres-replication.handler.mjs +5 -65
- package/lib/data-store-postgres-replication.handler.mjs.map +1 -1
- package/lib/delete-chunk.handler.d.mts +29 -0
- package/lib/delete-chunk.handler.d.ts +29 -0
- package/lib/delete-chunk.handler.js +2716 -0
- package/lib/delete-chunk.handler.js.map +1 -0
- package/lib/delete-chunk.handler.mjs +47 -0
- package/lib/delete-chunk.handler.mjs.map +1 -0
- package/lib/events-CjS-sm0W.d.mts +107 -0
- package/lib/events-CjS-sm0W.d.ts +107 -0
- package/lib/events-Da_cFgtc.d.mts +208 -0
- package/lib/events-Da_cFgtc.d.ts +208 -0
- package/lib/finalize.handler.d.mts +35 -0
- package/lib/finalize.handler.d.ts +35 -0
- package/lib/finalize.handler.js +875 -0
- package/lib/finalize.handler.js.map +1 -0
- package/lib/finalize.handler.mjs +166 -0
- package/lib/finalize.handler.mjs.map +1 -0
- package/lib/index.d.mts +189 -2
- package/lib/index.d.ts +500 -3
- package/lib/index.js +1753 -174
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +571 -17
- package/lib/index.mjs.map +1 -1
- package/lib/list-chunks.handler.d.mts +28 -0
- package/lib/list-chunks.handler.d.ts +28 -0
- package/lib/list-chunks.handler.js +2746 -0
- package/lib/list-chunks.handler.js.map +1 -0
- package/lib/list-chunks.handler.mjs +54 -0
- package/lib/list-chunks.handler.mjs.map +1 -0
- package/lib/platform-deploy-bridge.handler.js +76 -1
- package/lib/platform-deploy-bridge.handler.js.map +1 -1
- package/lib/platform-deploy-bridge.handler.mjs +1 -1
- package/lib/pre-token-generation.handler.js +1106 -155
- package/lib/pre-token-generation.handler.js.map +1 -1
- package/lib/pre-token-generation.handler.mjs +6 -4
- package/lib/pre-token-generation.handler.mjs.map +1 -1
- package/lib/provision-default-workspace.handler.js +1529 -142
- package/lib/provision-default-workspace.handler.js.map +1 -1
- package/lib/provision-default-workspace.handler.mjs +8 -4
- package/lib/provision-default-workspace.handler.mjs.map +1 -1
- package/lib/rename-finalize.handler.d.mts +30 -0
- package/lib/rename-finalize.handler.d.ts +30 -0
- package/lib/rename-finalize.handler.js +795 -0
- package/lib/rename-finalize.handler.js.map +1 -0
- package/lib/rename-finalize.handler.mjs +90 -0
- package/lib/rename-finalize.handler.mjs.map +1 -0
- package/lib/rename-list-targets.handler.d.mts +26 -0
- package/lib/rename-list-targets.handler.d.ts +26 -0
- package/lib/rename-list-targets.handler.js +2985 -0
- package/lib/rename-list-targets.handler.js.map +1 -0
- package/lib/rename-list-targets.handler.mjs +431 -0
- package/lib/rename-list-targets.handler.mjs.map +1 -0
- package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
- package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
- package/lib/rename-rewrite-chunk.handler.js +2021 -0
- package/lib/rename-rewrite-chunk.handler.js.map +1 -0
- package/lib/rename-rewrite-chunk.handler.mjs +27 -0
- package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
- package/lib/rest-api-lambda.handler.js +4087 -921
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +1827 -81
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/lib/seed-demo-data.handler.js +1588 -124
- package/lib/seed-demo-data.handler.js.map +1 -1
- package/lib/seed-demo-data.handler.mjs +10 -6
- package/lib/seed-system-data.handler.js +1179 -155
- package/lib/seed-system-data.handler.js.map +1 -1
- package/lib/seed-system-data.handler.mjs +5 -4
- package/lib/seed-system-data.handler.mjs.map +1 -1
- package/package.json +1 -1
- package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
- package/lib/chunk-IS4VQRI4.mjs.map +0 -1
- package/lib/chunk-MULKGFIJ.mjs.map +0 -1
- package/lib/chunk-QR5JVSCF.mjs +0 -862
- package/lib/chunk-QR5JVSCF.mjs.map +0 -1
- package/lib/chunk-SYBADQXI.mjs.map +0 -1
- /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
- /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
- /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
- /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
- /package/lib/{chunk-AGF3RAAZ.mjs.map → chunk-WPCBVDFZ.mjs.map} +0 -0
|
@@ -26,7 +26,7 @@ module.exports = __toCommonJS(pre_token_generation_handler_exports);
|
|
|
26
26
|
var import_types6 = require("@openhi/types");
|
|
27
27
|
|
|
28
28
|
// src/data/dynamo/dynamo-control-service.ts
|
|
29
|
-
var
|
|
29
|
+
var import_electrodb14 = require("electrodb");
|
|
30
30
|
|
|
31
31
|
// src/data/dynamo/dynamo-client.ts
|
|
32
32
|
var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
|
|
@@ -90,6 +90,60 @@ var gsi1skAttribute = {
|
|
|
90
90
|
return label !== void 0 ? `${label}#${id}` : fallback;
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
|
+
function extractRoleId(resource) {
|
|
94
|
+
const flat = resource.roleId;
|
|
95
|
+
if (typeof flat === "string" && flat.length > 0) return flat;
|
|
96
|
+
const role = resource.role;
|
|
97
|
+
if (role && typeof role === "object") {
|
|
98
|
+
const reference = role.reference;
|
|
99
|
+
if (typeof reference === "string" && reference.length > 0) {
|
|
100
|
+
const slash = reference.lastIndexOf("/");
|
|
101
|
+
const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
|
|
102
|
+
if (tail.length > 0) return tail;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
var roleAssignmentGsi1skAttribute = {
|
|
108
|
+
type: "string",
|
|
109
|
+
watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
|
|
110
|
+
set: (_val, item) => {
|
|
111
|
+
const id = typeof item?.id === "string" ? item.id : "";
|
|
112
|
+
const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
|
|
113
|
+
const fallback = `${lastUpdated}#${id}`;
|
|
114
|
+
if (typeof item?.resource !== "string" || item.resource.length === 0) {
|
|
115
|
+
return fallback;
|
|
116
|
+
}
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(item.resource);
|
|
120
|
+
} catch {
|
|
121
|
+
return fallback;
|
|
122
|
+
}
|
|
123
|
+
if (!parsed || typeof parsed !== "object") return fallback;
|
|
124
|
+
const roleId = extractRoleId(parsed);
|
|
125
|
+
if (roleId === void 0) return fallback;
|
|
126
|
+
const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
|
|
127
|
+
const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
|
|
128
|
+
if (normalizedUserName.length === 0) return fallback;
|
|
129
|
+
return `${roleId}#${normalizedUserName}#${id}`;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var membershipGsi1skAttribute = {
|
|
133
|
+
type: "string",
|
|
134
|
+
watch: ["denormalizedUserName", "lastUpdated", "id"],
|
|
135
|
+
set: (_val, item) => {
|
|
136
|
+
const id = typeof item?.id === "string" ? item.id : "";
|
|
137
|
+
const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
|
|
138
|
+
const fallback = `${lastUpdated}#${id}`;
|
|
139
|
+
const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
|
|
140
|
+
const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
|
|
141
|
+
if (normalizedUserName.length === 0) {
|
|
142
|
+
return fallback;
|
|
143
|
+
}
|
|
144
|
+
return `${normalizedUserName}#${id}`;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
93
147
|
|
|
94
148
|
// src/data/dynamo/entities/control/configuration-entity.ts
|
|
95
149
|
var ConfigurationEntity = new import_electrodb.Entity({
|
|
@@ -134,25 +188,743 @@ var ConfigurationEntity = new import_electrodb.Entity({
|
|
|
134
188
|
type: "string",
|
|
135
189
|
required: true
|
|
136
190
|
},
|
|
137
|
-
/** FHIR Resource.id; logical id in URL and for the Configuration resource. */
|
|
191
|
+
/** FHIR Resource.id; logical id in URL and for the Configuration resource. */
|
|
192
|
+
id: {
|
|
193
|
+
type: "string",
|
|
194
|
+
required: true
|
|
195
|
+
},
|
|
196
|
+
/** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
|
|
197
|
+
resource: {
|
|
198
|
+
type: "string",
|
|
199
|
+
required: true
|
|
200
|
+
},
|
|
201
|
+
/**
|
|
202
|
+
* Summary projection (key display fields as JSON string: id, key, status).
|
|
203
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
204
|
+
*/
|
|
205
|
+
summary: {
|
|
206
|
+
type: "string",
|
|
207
|
+
required: true
|
|
208
|
+
},
|
|
209
|
+
/** Version id (e.g. ULID). Tracks current version; S3 history key. */
|
|
210
|
+
vid: {
|
|
211
|
+
type: "string",
|
|
212
|
+
required: true
|
|
213
|
+
},
|
|
214
|
+
lastUpdated: {
|
|
215
|
+
type: "string",
|
|
216
|
+
required: true
|
|
217
|
+
},
|
|
218
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
219
|
+
deleted: {
|
|
220
|
+
type: "boolean",
|
|
221
|
+
required: false
|
|
222
|
+
},
|
|
223
|
+
bundleId: {
|
|
224
|
+
type: "string",
|
|
225
|
+
required: false
|
|
226
|
+
},
|
|
227
|
+
msgId: {
|
|
228
|
+
type: "string",
|
|
229
|
+
required: false
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
indexes: {
|
|
233
|
+
/** Base table: PK, SK (data store key names). PK is built from tenantId, workspaceId, userId, roleId; SK is built from key and sk. Do not supply PK or SK from outside. */
|
|
234
|
+
record: {
|
|
235
|
+
pk: {
|
|
236
|
+
field: "PK",
|
|
237
|
+
composite: ["tenantId", "workspaceId", "userId", "roleId"],
|
|
238
|
+
template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
|
|
239
|
+
},
|
|
240
|
+
sk: {
|
|
241
|
+
field: "SK",
|
|
242
|
+
composite: ["key", "sk"],
|
|
243
|
+
template: "KEY#${key}#SK#${sk}"
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
/**
|
|
247
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
|
|
248
|
+
* (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
|
|
249
|
+
* (workspaceId = "-") or "list configs scoped to this workspace". Does not support
|
|
250
|
+
* hierarchical resolution in one query; use base table GetItem in fallback order
|
|
251
|
+
* (user → workspace → tenant → baseline) for that.
|
|
252
|
+
* SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
|
|
253
|
+
* config category: endpoints, branding, display, …) and the natural sort/lookup
|
|
254
|
+
* dimension. `casing: "none"` preserves the literal key value.
|
|
255
|
+
*/
|
|
256
|
+
gsi1: {
|
|
257
|
+
index: "GSI1",
|
|
258
|
+
pk: {
|
|
259
|
+
field: "GSI1PK",
|
|
260
|
+
composite: ["tenantId", "workspaceId", "gsi1Shard"],
|
|
261
|
+
template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
|
|
262
|
+
},
|
|
263
|
+
sk: {
|
|
264
|
+
field: "GSI1SK",
|
|
265
|
+
casing: "none",
|
|
266
|
+
composite: ["key", "id"],
|
|
267
|
+
template: "${key}#${id}"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/data/dynamo/entities/control/configuration-user-projection-entity.ts
|
|
274
|
+
var import_electrodb2 = require("electrodb");
|
|
275
|
+
var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
|
|
276
|
+
model: {
|
|
277
|
+
entity: "configurationUserProjection",
|
|
278
|
+
service: "control",
|
|
279
|
+
version: "01"
|
|
280
|
+
},
|
|
281
|
+
attributes: {
|
|
282
|
+
/**
|
|
283
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
284
|
+
* base-table PK. Always required — the projection has no meaning
|
|
285
|
+
* outside a user partition.
|
|
286
|
+
*/
|
|
287
|
+
userId: {
|
|
288
|
+
type: "string",
|
|
289
|
+
required: true
|
|
290
|
+
},
|
|
291
|
+
/**
|
|
292
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
293
|
+
* writer via `buildConfigurationUserProjectionSk`. The entity stores
|
|
294
|
+
* the value verbatim so the SK grammar (pattern #10 user-scope) is
|
|
295
|
+
* owned by the operations layer, not duplicated here.
|
|
296
|
+
*/
|
|
297
|
+
sk: {
|
|
298
|
+
type: "string",
|
|
299
|
+
required: true
|
|
300
|
+
},
|
|
301
|
+
/**
|
|
302
|
+
* Configuration canonical-record id. Stored as a discriminating
|
|
303
|
+
* field so consumers can hydrate the canonical row via the
|
|
304
|
+
* Configuration get-by-id operation when the projection's `summary`
|
|
305
|
+
* is insufficient.
|
|
306
|
+
*/
|
|
307
|
+
configurationId: {
|
|
308
|
+
type: "string",
|
|
309
|
+
required: true
|
|
310
|
+
},
|
|
311
|
+
/**
|
|
312
|
+
* Tenant the Configuration is associated with. The canonical row
|
|
313
|
+
* keys off `(tenantId, workspaceId, userId, roleId)`; the projection
|
|
314
|
+
* carries `tenantId` so consumers reconstructing the canonical PK
|
|
315
|
+
* have the tenant segment without a hop.
|
|
316
|
+
*/
|
|
317
|
+
tenantId: {
|
|
318
|
+
type: "string",
|
|
319
|
+
required: true
|
|
320
|
+
},
|
|
321
|
+
/**
|
|
322
|
+
* Scope marker. Always `"user"` on this projection — recorded
|
|
323
|
+
* explicitly so future scope-bearing projections (workspace,
|
|
324
|
+
* tenant, role) can share filter semantics in a unified
|
|
325
|
+
* cross-projection list query if one ever lands.
|
|
326
|
+
*/
|
|
327
|
+
scope: {
|
|
328
|
+
type: "string",
|
|
329
|
+
required: true,
|
|
330
|
+
default: "user"
|
|
331
|
+
},
|
|
332
|
+
/**
|
|
333
|
+
* Configuration's `key` attribute (config category, e.g. endpoints,
|
|
334
|
+
* branding, display). Mirrored from the canonical row so consumers
|
|
335
|
+
* reading the projection get the natural display label without a
|
|
336
|
+
* BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
|
|
337
|
+
* the SK.
|
|
338
|
+
*/
|
|
339
|
+
displayName: {
|
|
340
|
+
type: "string",
|
|
341
|
+
required: false
|
|
342
|
+
},
|
|
343
|
+
/**
|
|
344
|
+
* Summary projection (key display fields as JSON string) — mirrored
|
|
345
|
+
* from the canonical Configuration row so user-partition queries do
|
|
346
|
+
* not need a BatchGet hop.
|
|
347
|
+
*/
|
|
348
|
+
summary: {
|
|
349
|
+
type: "string",
|
|
350
|
+
required: true
|
|
351
|
+
},
|
|
352
|
+
/** Version id mirrored from the canonical Configuration row. */
|
|
353
|
+
vid: {
|
|
354
|
+
type: "string",
|
|
355
|
+
required: true
|
|
356
|
+
},
|
|
357
|
+
/** Last-updated timestamp mirrored from the canonical Configuration row. */
|
|
358
|
+
lastUpdated: {
|
|
359
|
+
type: "string",
|
|
360
|
+
required: true
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
indexes: {
|
|
364
|
+
/**
|
|
365
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
|
|
366
|
+
* single `Query(PK = USER#ID#<userId>, SK begins_with
|
|
367
|
+
* 'CONFIGURATION#')` returns the user's user-scoped Configurations
|
|
368
|
+
* sorted by `<normalizedConfigName>` (then `<configurationId>` as
|
|
369
|
+
* the tiebreaker).
|
|
370
|
+
*/
|
|
371
|
+
record: {
|
|
372
|
+
pk: {
|
|
373
|
+
field: "PK",
|
|
374
|
+
composite: ["userId"],
|
|
375
|
+
template: "USER#ID#${userId}"
|
|
376
|
+
},
|
|
377
|
+
sk: {
|
|
378
|
+
field: "SK",
|
|
379
|
+
casing: "none",
|
|
380
|
+
composite: ["sk"],
|
|
381
|
+
template: "${sk}"
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
|
|
388
|
+
var import_electrodb3 = require("electrodb");
|
|
389
|
+
var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
|
|
390
|
+
model: {
|
|
391
|
+
entity: "configurationWorkspaceProjection",
|
|
392
|
+
service: "control",
|
|
393
|
+
version: "01"
|
|
394
|
+
},
|
|
395
|
+
attributes: {
|
|
396
|
+
/**
|
|
397
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
398
|
+
* of the base-table PK. Always required — the workspace partition
|
|
399
|
+
* is tenant-scoped per ADR-011.
|
|
400
|
+
*/
|
|
401
|
+
tenantId: {
|
|
402
|
+
type: "string",
|
|
403
|
+
required: true
|
|
404
|
+
},
|
|
405
|
+
/**
|
|
406
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
407
|
+
* segment of the base-table PK
|
|
408
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
409
|
+
* the projection has no meaning outside a workspace partition.
|
|
410
|
+
*/
|
|
411
|
+
workspaceId: {
|
|
412
|
+
type: "string",
|
|
413
|
+
required: true
|
|
414
|
+
},
|
|
415
|
+
/**
|
|
416
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
417
|
+
* writer via `buildConfigurationWorkspaceProjectionSk`. The entity
|
|
418
|
+
* stores the value verbatim so the SK grammar (pattern #10
|
|
419
|
+
* workspace-scope) is owned by the operations layer, not
|
|
420
|
+
* duplicated here.
|
|
421
|
+
*/
|
|
422
|
+
sk: {
|
|
423
|
+
type: "string",
|
|
424
|
+
required: true
|
|
425
|
+
},
|
|
426
|
+
/**
|
|
427
|
+
* Configuration canonical-record id. Stored as a discriminating
|
|
428
|
+
* field so consumers can hydrate the canonical row via the
|
|
429
|
+
* Configuration get-by-id operation when the projection's `summary`
|
|
430
|
+
* is insufficient.
|
|
431
|
+
*/
|
|
432
|
+
configurationId: {
|
|
433
|
+
type: "string",
|
|
434
|
+
required: true
|
|
435
|
+
},
|
|
436
|
+
/**
|
|
437
|
+
* Scope marker. Always `"workspace"` on this projection — recorded
|
|
438
|
+
* explicitly so future scope-bearing projections (user, tenant,
|
|
439
|
+
* role) can share filter semantics in a unified cross-projection
|
|
440
|
+
* list query if one ever lands.
|
|
441
|
+
*/
|
|
442
|
+
scope: {
|
|
443
|
+
type: "string",
|
|
444
|
+
required: true,
|
|
445
|
+
default: "workspace"
|
|
446
|
+
},
|
|
447
|
+
/**
|
|
448
|
+
* Configuration's `key` attribute (config category, e.g. endpoints,
|
|
449
|
+
* branding, display). Mirrored from the canonical row so consumers
|
|
450
|
+
* reading the projection get the natural display label without a
|
|
451
|
+
* BatchGet hop. Doubles as the source of `<normalizedConfigName>`
|
|
452
|
+
* in the SK.
|
|
453
|
+
*/
|
|
454
|
+
displayName: {
|
|
455
|
+
type: "string",
|
|
456
|
+
required: false
|
|
457
|
+
},
|
|
458
|
+
/**
|
|
459
|
+
* Summary projection (key display fields as JSON string) — mirrored
|
|
460
|
+
* from the canonical Configuration row so workspace-partition
|
|
461
|
+
* queries do not need a BatchGet hop.
|
|
462
|
+
*/
|
|
463
|
+
summary: {
|
|
464
|
+
type: "string",
|
|
465
|
+
required: true
|
|
466
|
+
},
|
|
467
|
+
/** Version id mirrored from the canonical Configuration row. */
|
|
468
|
+
vid: {
|
|
469
|
+
type: "string",
|
|
470
|
+
required: true
|
|
471
|
+
},
|
|
472
|
+
/** Last-updated timestamp mirrored from the canonical Configuration row. */
|
|
473
|
+
lastUpdated: {
|
|
474
|
+
type: "string",
|
|
475
|
+
required: true
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
indexes: {
|
|
479
|
+
/**
|
|
480
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
481
|
+
* SK = operation-supplied. A single
|
|
482
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
|
|
483
|
+
* returns the workspace's workspace-scoped Configurations sorted by
|
|
484
|
+
* `<normalizedConfigName>` (then `<configurationId>` as the
|
|
485
|
+
* tiebreaker).
|
|
486
|
+
*/
|
|
487
|
+
record: {
|
|
488
|
+
pk: {
|
|
489
|
+
field: "PK",
|
|
490
|
+
composite: ["tenantId", "workspaceId"],
|
|
491
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
492
|
+
},
|
|
493
|
+
sk: {
|
|
494
|
+
field: "SK",
|
|
495
|
+
casing: "none",
|
|
496
|
+
composite: ["sk"],
|
|
497
|
+
template: "${sk}"
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// src/data/dynamo/entities/control/membership-entity.ts
|
|
504
|
+
var import_electrodb4 = require("electrodb");
|
|
505
|
+
var MembershipEntity = new import_electrodb4.Entity({
|
|
506
|
+
model: {
|
|
507
|
+
entity: "membership",
|
|
508
|
+
service: "control",
|
|
509
|
+
version: "01"
|
|
510
|
+
},
|
|
511
|
+
attributes: {
|
|
512
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
513
|
+
sk: {
|
|
514
|
+
type: "string",
|
|
515
|
+
required: true,
|
|
516
|
+
default: "CURRENT"
|
|
517
|
+
},
|
|
518
|
+
/** Tenant in which the user has membership (required). */
|
|
519
|
+
tenantId: {
|
|
520
|
+
type: "string",
|
|
521
|
+
required: true
|
|
522
|
+
},
|
|
523
|
+
/** FHIR Resource.id; membership id. */
|
|
524
|
+
id: {
|
|
525
|
+
type: "string",
|
|
526
|
+
required: true
|
|
527
|
+
},
|
|
528
|
+
/** Full Membership resource serialized as JSON string. */
|
|
529
|
+
resource: {
|
|
530
|
+
type: "string",
|
|
531
|
+
required: true
|
|
532
|
+
},
|
|
533
|
+
/**
|
|
534
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
535
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
536
|
+
*/
|
|
537
|
+
summary: {
|
|
538
|
+
type: "string",
|
|
539
|
+
required: true
|
|
540
|
+
},
|
|
541
|
+
/** Version id (e.g. ULID). */
|
|
542
|
+
vid: {
|
|
543
|
+
type: "string",
|
|
544
|
+
required: true
|
|
545
|
+
},
|
|
546
|
+
lastUpdated: {
|
|
547
|
+
type: "string",
|
|
548
|
+
required: true
|
|
549
|
+
},
|
|
550
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
551
|
+
/**
|
|
552
|
+
* Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
|
|
553
|
+
* pattern #1 so a GSI1 query partitioned on the tenant range-scans
|
|
554
|
+
* by user-name prefix and returns memberships sorted by user name.
|
|
555
|
+
* Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
|
|
556
|
+
* is missing.
|
|
557
|
+
*/
|
|
558
|
+
gsi1sk: membershipGsi1skAttribute,
|
|
559
|
+
deleted: {
|
|
560
|
+
type: "boolean",
|
|
561
|
+
required: false
|
|
562
|
+
},
|
|
563
|
+
bundleId: {
|
|
564
|
+
type: "string",
|
|
565
|
+
required: false
|
|
566
|
+
},
|
|
567
|
+
msgId: {
|
|
568
|
+
type: "string",
|
|
569
|
+
required: false
|
|
570
|
+
},
|
|
571
|
+
/**
|
|
572
|
+
* Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
|
|
573
|
+
* Populated from the FHIR extension on the Membership resource at write
|
|
574
|
+
* time so future GSIs can index data-plane identity lookups without
|
|
575
|
+
* deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
|
|
576
|
+
*/
|
|
577
|
+
linkedDataIdentityRef: {
|
|
578
|
+
type: "string",
|
|
579
|
+
required: false
|
|
580
|
+
},
|
|
581
|
+
/**
|
|
582
|
+
* Denormalized display name of the linked Tenant, captured at row
|
|
583
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
584
|
+
* adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
|
|
585
|
+
* can be composed from a top-level field instead of digging into the
|
|
586
|
+
* `resource` JSON. Optional on the schema so pre-TR-024 rows do not
|
|
587
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
588
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
589
|
+
* source = canonical Tenant.displayName).
|
|
590
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
591
|
+
*/
|
|
592
|
+
denormalizedTenantName: {
|
|
593
|
+
type: "string",
|
|
594
|
+
required: false
|
|
595
|
+
},
|
|
596
|
+
/**
|
|
597
|
+
* Denormalized display name of the linked User, captured at row
|
|
598
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
599
|
+
* adjacency-list canonical-record GSI1SK (pattern #1 —
|
|
600
|
+
* `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
|
|
601
|
+
* can be composed from a top-level field. Optional on the schema so
|
|
602
|
+
* pre-TR-024 rows do not break; the operations-layer multi-write helper
|
|
603
|
+
* (#1010) makes the field load-bearing at write time per TR-024 rule 2
|
|
604
|
+
* (write-time source = canonical User.displayName).
|
|
605
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
606
|
+
*/
|
|
607
|
+
denormalizedUserName: {
|
|
608
|
+
type: "string",
|
|
609
|
+
required: false
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
indexes: {
|
|
613
|
+
/** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
614
|
+
record: {
|
|
615
|
+
pk: {
|
|
616
|
+
field: "PK",
|
|
617
|
+
composite: ["tenantId", "id"],
|
|
618
|
+
template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
|
|
619
|
+
},
|
|
620
|
+
sk: {
|
|
621
|
+
field: "SK",
|
|
622
|
+
composite: ["sk"],
|
|
623
|
+
template: "${sk}"
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
/**
|
|
627
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
|
|
628
|
+
* four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
|
|
629
|
+
* SK is derived via `membershipGsi1skAttribute` — composes
|
|
630
|
+
* `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
|
|
631
|
+
* tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
|
|
632
|
+
* when `denormalizedUserName` is missing. `casing: "none"` preserves
|
|
633
|
+
* the normalized label and ISO-8601 `T`/`Z`.
|
|
634
|
+
*/
|
|
635
|
+
gsi1: {
|
|
636
|
+
index: "GSI1",
|
|
637
|
+
pk: {
|
|
638
|
+
field: "GSI1PK",
|
|
639
|
+
composite: ["tenantId", "gsi1Shard"],
|
|
640
|
+
template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
|
|
641
|
+
},
|
|
642
|
+
sk: {
|
|
643
|
+
field: "GSI1SK",
|
|
644
|
+
casing: "none",
|
|
645
|
+
composite: ["gsi1sk"],
|
|
646
|
+
template: "${gsi1sk}"
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// src/data/dynamo/entities/control/membership-user-projection-entity.ts
|
|
653
|
+
var import_electrodb5 = require("electrodb");
|
|
654
|
+
var MembershipUserProjectionEntity = new import_electrodb5.Entity({
|
|
655
|
+
model: {
|
|
656
|
+
entity: "membershipUserProjection",
|
|
657
|
+
service: "control",
|
|
658
|
+
version: "01"
|
|
659
|
+
},
|
|
660
|
+
attributes: {
|
|
661
|
+
/**
|
|
662
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
663
|
+
* base-table PK. Always required — the projection has no meaning
|
|
664
|
+
* outside a user partition.
|
|
665
|
+
*/
|
|
666
|
+
userId: {
|
|
667
|
+
type: "string",
|
|
668
|
+
required: true
|
|
669
|
+
},
|
|
670
|
+
/**
|
|
671
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
672
|
+
* writer via `buildMembershipUserProjectionSk*` helpers. The entity
|
|
673
|
+
* stores the value verbatim so the SK grammar (patterns #3 and #4)
|
|
674
|
+
* is owned by the operations layer, not duplicated here.
|
|
675
|
+
*/
|
|
676
|
+
sk: {
|
|
677
|
+
type: "string",
|
|
678
|
+
required: true
|
|
679
|
+
},
|
|
680
|
+
/** Tenant in which the membership applies. Always required. */
|
|
681
|
+
tenantId: {
|
|
682
|
+
type: "string",
|
|
683
|
+
required: true
|
|
684
|
+
},
|
|
685
|
+
/**
|
|
686
|
+
* Workspace the membership scopes to. Present iff the projection
|
|
687
|
+
* row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
|
|
688
|
+
* tenant sub-lane rows.
|
|
689
|
+
*/
|
|
690
|
+
workspaceId: {
|
|
691
|
+
type: "string",
|
|
692
|
+
required: false
|
|
693
|
+
},
|
|
694
|
+
/**
|
|
695
|
+
* Membership canonical-record id. Stored as a discriminating field
|
|
696
|
+
* so consumers can hydrate the canonical row via
|
|
697
|
+
* `MembershipEntity.get({ tenantId, id: membershipId })` when the
|
|
698
|
+
* projection's `summary` is insufficient.
|
|
699
|
+
*/
|
|
700
|
+
membershipId: {
|
|
701
|
+
type: "string",
|
|
702
|
+
required: true
|
|
703
|
+
},
|
|
704
|
+
/**
|
|
705
|
+
* Summary projection (key display fields as JSON string: id,
|
|
706
|
+
* displayName, status) — mirrored from the canonical Membership row
|
|
707
|
+
* so user-partition queries do not need a BatchGet hop.
|
|
708
|
+
*/
|
|
709
|
+
summary: {
|
|
710
|
+
type: "string",
|
|
711
|
+
required: true
|
|
712
|
+
},
|
|
713
|
+
/** Version id mirrored from the canonical Membership row. */
|
|
714
|
+
vid: {
|
|
715
|
+
type: "string",
|
|
716
|
+
required: true
|
|
717
|
+
},
|
|
718
|
+
/** Last-updated timestamp mirrored from the canonical Membership row. */
|
|
719
|
+
lastUpdated: {
|
|
720
|
+
type: "string",
|
|
721
|
+
required: true
|
|
722
|
+
},
|
|
723
|
+
/**
|
|
724
|
+
* Denormalized Tenant display name — required to compose pattern-#3
|
|
725
|
+
* SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
|
|
726
|
+
* schema because pre-TR-024 rows may not carry a display name; the
|
|
727
|
+
* operations layer falls back gracefully when missing.
|
|
728
|
+
*/
|
|
729
|
+
denormalizedTenantName: {
|
|
730
|
+
type: "string",
|
|
731
|
+
required: false
|
|
732
|
+
},
|
|
733
|
+
/**
|
|
734
|
+
* Denormalized User display name — mirrored from the canonical
|
|
735
|
+
* Membership row per TR-024 rule 3 (canonical-record symmetry).
|
|
736
|
+
* Carried on the projection so consumers can render the user's
|
|
737
|
+
* display name without a hop to the User record.
|
|
738
|
+
*/
|
|
739
|
+
denormalizedUserName: {
|
|
740
|
+
type: "string",
|
|
741
|
+
required: false
|
|
742
|
+
},
|
|
743
|
+
/**
|
|
744
|
+
* Denormalized Workspace display name — required to compose
|
|
745
|
+
* pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
|
|
746
|
+
* Optional on the schema (TR-024 § Open Item #4 defers a formal
|
|
747
|
+
* Workspace-rename cascade); the operations layer falls back to a
|
|
748
|
+
* sentinel when missing so the SK still has a valid shape.
|
|
749
|
+
*/
|
|
750
|
+
denormalizedWorkspaceName: {
|
|
751
|
+
type: "string",
|
|
752
|
+
required: false
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
indexes: {
|
|
756
|
+
/**
|
|
757
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied.
|
|
758
|
+
* Both pattern #3 and pattern #4 use this same index — the SK string
|
|
759
|
+
* encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
|
|
760
|
+
* `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
|
|
761
|
+
* SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
|
|
762
|
+
*/
|
|
763
|
+
record: {
|
|
764
|
+
pk: {
|
|
765
|
+
field: "PK",
|
|
766
|
+
composite: ["userId"],
|
|
767
|
+
template: "USER#ID#${userId}"
|
|
768
|
+
},
|
|
769
|
+
sk: {
|
|
770
|
+
field: "SK",
|
|
771
|
+
casing: "none",
|
|
772
|
+
composite: ["sk"],
|
|
773
|
+
template: "${sk}"
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
|
|
780
|
+
var import_electrodb6 = require("electrodb");
|
|
781
|
+
var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
|
|
782
|
+
model: {
|
|
783
|
+
entity: "membershipWorkspaceProjection",
|
|
784
|
+
service: "control",
|
|
785
|
+
version: "01"
|
|
786
|
+
},
|
|
787
|
+
attributes: {
|
|
788
|
+
/**
|
|
789
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
790
|
+
* of the base-table PK. Always required — the workspace partition
|
|
791
|
+
* is tenant-scoped per ADR-011.
|
|
792
|
+
*/
|
|
793
|
+
tenantId: {
|
|
794
|
+
type: "string",
|
|
795
|
+
required: true
|
|
796
|
+
},
|
|
797
|
+
/**
|
|
798
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
799
|
+
* segment of the base-table PK
|
|
800
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
801
|
+
* the projection has no meaning outside a workspace partition.
|
|
802
|
+
*/
|
|
803
|
+
workspaceId: {
|
|
804
|
+
type: "string",
|
|
805
|
+
required: true
|
|
806
|
+
},
|
|
807
|
+
/**
|
|
808
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
809
|
+
* writer via `buildMembershipWorkspaceProjectionSk`. The entity
|
|
810
|
+
* stores the value verbatim so the SK grammar (pattern #2) is
|
|
811
|
+
* owned by the operations layer, not duplicated here.
|
|
812
|
+
*/
|
|
813
|
+
sk: {
|
|
814
|
+
type: "string",
|
|
815
|
+
required: true
|
|
816
|
+
},
|
|
817
|
+
/**
|
|
818
|
+
* User the membership links. Stored as a discriminating field so
|
|
819
|
+
* consumers can hydrate the canonical User row via
|
|
820
|
+
* `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
|
|
821
|
+
* projection's `summary` is insufficient.
|
|
822
|
+
*/
|
|
823
|
+
userId: {
|
|
824
|
+
type: "string",
|
|
825
|
+
required: true
|
|
826
|
+
},
|
|
827
|
+
/**
|
|
828
|
+
* Membership canonical-record id. Stored as a discriminating field
|
|
829
|
+
* so consumers can hydrate the canonical row via
|
|
830
|
+
* `MembershipEntity.get({ tenantId, id: membershipId })` when the
|
|
831
|
+
* projection's `summary` is insufficient.
|
|
832
|
+
*/
|
|
833
|
+
membershipId: {
|
|
834
|
+
type: "string",
|
|
835
|
+
required: true
|
|
836
|
+
},
|
|
837
|
+
/**
|
|
838
|
+
* Summary projection (key display fields as JSON string: id,
|
|
839
|
+
* displayName, status) — mirrored from the canonical Membership row
|
|
840
|
+
* so workspace-partition queries do not need a BatchGet hop.
|
|
841
|
+
*/
|
|
842
|
+
summary: {
|
|
843
|
+
type: "string",
|
|
844
|
+
required: true
|
|
845
|
+
},
|
|
846
|
+
/** Version id mirrored from the canonical Membership row. */
|
|
847
|
+
vid: {
|
|
848
|
+
type: "string",
|
|
849
|
+
required: true
|
|
850
|
+
},
|
|
851
|
+
/** Last-updated timestamp mirrored from the canonical Membership row. */
|
|
852
|
+
lastUpdated: {
|
|
853
|
+
type: "string",
|
|
854
|
+
required: true
|
|
855
|
+
},
|
|
856
|
+
/**
|
|
857
|
+
* Denormalized User display name — required to compose the
|
|
858
|
+
* pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
|
|
859
|
+
* the schema because pre-TR-024 rows may not carry a display name;
|
|
860
|
+
* the operations layer falls back to a sentinel when missing so
|
|
861
|
+
* the SK still has a valid shape. The TR-023 rename-cascade
|
|
862
|
+
* pipeline rewrites the SK on a User rename.
|
|
863
|
+
*/
|
|
864
|
+
denormalizedUserName: {
|
|
865
|
+
type: "string",
|
|
866
|
+
required: false
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
indexes: {
|
|
870
|
+
/**
|
|
871
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
872
|
+
* SK = operation-supplied. Pattern #2 uses this index — the SK
|
|
873
|
+
* encodes the entity-type prefix (`MEMBERSHIP#…`) so a
|
|
874
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
|
|
875
|
+
* returns every member projection for the workspace in normalized-
|
|
876
|
+
* user-name sort order.
|
|
877
|
+
*/
|
|
878
|
+
record: {
|
|
879
|
+
pk: {
|
|
880
|
+
field: "PK",
|
|
881
|
+
composite: ["tenantId", "workspaceId"],
|
|
882
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
883
|
+
},
|
|
884
|
+
sk: {
|
|
885
|
+
field: "SK",
|
|
886
|
+
casing: "none",
|
|
887
|
+
composite: ["sk"],
|
|
888
|
+
template: "${sk}"
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// src/data/dynamo/entities/control/role-entity.ts
|
|
895
|
+
var import_electrodb7 = require("electrodb");
|
|
896
|
+
var RoleEntity = new import_electrodb7.Entity({
|
|
897
|
+
model: {
|
|
898
|
+
entity: "role",
|
|
899
|
+
service: "control",
|
|
900
|
+
version: "01"
|
|
901
|
+
},
|
|
902
|
+
attributes: {
|
|
903
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
904
|
+
sk: {
|
|
905
|
+
type: "string",
|
|
906
|
+
required: true,
|
|
907
|
+
default: "CURRENT"
|
|
908
|
+
},
|
|
909
|
+
/** FHIR Resource.id; role id. */
|
|
138
910
|
id: {
|
|
139
911
|
type: "string",
|
|
140
912
|
required: true
|
|
141
913
|
},
|
|
142
|
-
/**
|
|
914
|
+
/** Full Role resource serialized as JSON string. */
|
|
143
915
|
resource: {
|
|
144
916
|
type: "string",
|
|
145
917
|
required: true
|
|
146
918
|
},
|
|
147
919
|
/**
|
|
148
|
-
* Summary projection (key display fields as JSON string: id,
|
|
920
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
149
921
|
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
150
922
|
*/
|
|
151
923
|
summary: {
|
|
152
924
|
type: "string",
|
|
153
925
|
required: true
|
|
154
926
|
},
|
|
155
|
-
/** Version id (e.g. ULID).
|
|
927
|
+
/** Version id (e.g. ULID). */
|
|
156
928
|
vid: {
|
|
157
929
|
type: "string",
|
|
158
930
|
required: true
|
|
@@ -162,6 +934,8 @@ var ConfigurationEntity = new import_electrodb.Entity({
|
|
|
162
934
|
required: true
|
|
163
935
|
},
|
|
164
936
|
gsi1Shard: gsi1ShardAttribute,
|
|
937
|
+
/** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
|
|
938
|
+
gsi1sk: gsi1skAttribute,
|
|
165
939
|
deleted: {
|
|
166
940
|
type: "boolean",
|
|
167
941
|
required: false
|
|
@@ -176,51 +950,48 @@ var ConfigurationEntity = new import_electrodb.Entity({
|
|
|
176
950
|
}
|
|
177
951
|
},
|
|
178
952
|
indexes: {
|
|
179
|
-
/** Base table: PK
|
|
953
|
+
/** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
180
954
|
record: {
|
|
181
955
|
pk: {
|
|
182
956
|
field: "PK",
|
|
183
|
-
composite: ["
|
|
184
|
-
template: "
|
|
957
|
+
composite: ["id"],
|
|
958
|
+
template: "ROLE#ID#${id}"
|
|
185
959
|
},
|
|
186
960
|
sk: {
|
|
187
961
|
field: "SK",
|
|
188
|
-
composite: ["
|
|
189
|
-
template: "
|
|
962
|
+
composite: ["sk"],
|
|
963
|
+
template: "${sk}"
|
|
190
964
|
}
|
|
191
965
|
},
|
|
192
966
|
/**
|
|
193
|
-
* GSI1 — Unified Sharded List per ADR-011: list all
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
|
|
199
|
-
* config category: endpoints, branding, display, …) and the natural sort/lookup
|
|
200
|
-
* dimension. `casing: "none"` preserves the literal key value.
|
|
967
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
|
|
968
|
+
* Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
|
|
969
|
+
* SK is derived via `gsi1skAttribute` — uses the resource's natural label when
|
|
970
|
+
* extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
|
|
971
|
+
* normalized label and ISO-8601 `T`/`Z`.
|
|
201
972
|
*/
|
|
202
973
|
gsi1: {
|
|
203
974
|
index: "GSI1",
|
|
204
975
|
pk: {
|
|
205
976
|
field: "GSI1PK",
|
|
206
|
-
composite: ["
|
|
207
|
-
template: "TID
|
|
977
|
+
composite: ["gsi1Shard"],
|
|
978
|
+
template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
|
|
208
979
|
},
|
|
209
980
|
sk: {
|
|
210
981
|
field: "GSI1SK",
|
|
211
982
|
casing: "none",
|
|
212
|
-
composite: ["
|
|
213
|
-
template: "${
|
|
983
|
+
composite: ["gsi1sk"],
|
|
984
|
+
template: "${gsi1sk}"
|
|
214
985
|
}
|
|
215
986
|
}
|
|
216
987
|
}
|
|
217
988
|
});
|
|
218
989
|
|
|
219
|
-
// src/data/dynamo/entities/control/
|
|
220
|
-
var
|
|
221
|
-
var
|
|
990
|
+
// src/data/dynamo/entities/control/roleassignment-entity.ts
|
|
991
|
+
var import_electrodb8 = require("electrodb");
|
|
992
|
+
var RoleAssignmentEntity = new import_electrodb8.Entity({
|
|
222
993
|
model: {
|
|
223
|
-
entity: "
|
|
994
|
+
entity: "roleassignment",
|
|
224
995
|
service: "control",
|
|
225
996
|
version: "01"
|
|
226
997
|
},
|
|
@@ -231,17 +1002,17 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
231
1002
|
required: true,
|
|
232
1003
|
default: "CURRENT"
|
|
233
1004
|
},
|
|
234
|
-
/** Tenant in which the
|
|
1005
|
+
/** Tenant in which the role assignment applies (required). */
|
|
235
1006
|
tenantId: {
|
|
236
1007
|
type: "string",
|
|
237
1008
|
required: true
|
|
238
1009
|
},
|
|
239
|
-
/** FHIR Resource.id;
|
|
1010
|
+
/** FHIR Resource.id; role assignment id. */
|
|
240
1011
|
id: {
|
|
241
1012
|
type: "string",
|
|
242
1013
|
required: true
|
|
243
1014
|
},
|
|
244
|
-
/** Full
|
|
1015
|
+
/** Full RoleAssignment resource serialized as JSON string. */
|
|
245
1016
|
resource: {
|
|
246
1017
|
type: "string",
|
|
247
1018
|
required: true
|
|
@@ -264,8 +1035,15 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
264
1035
|
required: true
|
|
265
1036
|
},
|
|
266
1037
|
gsi1Shard: gsi1ShardAttribute,
|
|
267
|
-
/**
|
|
268
|
-
|
|
1038
|
+
/**
|
|
1039
|
+
* Derived GSI1 sort key — discriminator-first
|
|
1040
|
+
* `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
|
|
1041
|
+
* GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
|
|
1042
|
+
* to enumerate every user assigned to a given role, sorted by user
|
|
1043
|
+
* name. Falls back to `<lastUpdated>#<id>` when either component is
|
|
1044
|
+
* missing.
|
|
1045
|
+
*/
|
|
1046
|
+
gsi1sk: roleAssignmentGsi1skAttribute,
|
|
269
1047
|
deleted: {
|
|
270
1048
|
type: "boolean",
|
|
271
1049
|
required: false
|
|
@@ -279,23 +1057,60 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
279
1057
|
required: false
|
|
280
1058
|
},
|
|
281
1059
|
/**
|
|
282
|
-
* Denormalized
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
1060
|
+
* Denormalized display name of the linked Tenant, captured at row
|
|
1061
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1062
|
+
* adjacency-list user-projection SK (pattern #5 —
|
|
1063
|
+
* `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
|
|
1064
|
+
* can be composed from a top-level field instead of digging into the
|
|
1065
|
+
* `resource` JSON. Optional on the schema so pre-TR-024 rows do not
|
|
1066
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
1067
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
1068
|
+
* source = canonical Tenant.displayName).
|
|
1069
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
286
1070
|
*/
|
|
287
|
-
|
|
1071
|
+
denormalizedTenantName: {
|
|
1072
|
+
type: "string",
|
|
1073
|
+
required: false
|
|
1074
|
+
},
|
|
1075
|
+
/**
|
|
1076
|
+
* Denormalized display name of the linked User, captured at row
|
|
1077
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1078
|
+
* adjacency-list canonical-record GSI1SK (pattern #8 —
|
|
1079
|
+
* `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
|
|
1080
|
+
* (pattern #9) can be composed from a top-level field. Optional on
|
|
1081
|
+
* the schema so pre-TR-024 rows do not break; the operations-layer
|
|
1082
|
+
* multi-write helper (#1010) makes the field load-bearing at write
|
|
1083
|
+
* time per TR-024 rule 2 (write-time source = canonical
|
|
1084
|
+
* User.displayName).
|
|
1085
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1086
|
+
*/
|
|
1087
|
+
denormalizedUserName: {
|
|
1088
|
+
type: "string",
|
|
1089
|
+
required: false
|
|
1090
|
+
},
|
|
1091
|
+
/**
|
|
1092
|
+
* Denormalized display name of the linked Role, captured at row
|
|
1093
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1094
|
+
* adjacency-list user-projection SK (pattern #5 —
|
|
1095
|
+
* `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
|
|
1096
|
+
* a top-level field. Optional on the schema so pre-TR-024 rows do not
|
|
1097
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
1098
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
1099
|
+
* source = canonical Role.displayName).
|
|
1100
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1101
|
+
*/
|
|
1102
|
+
denormalizedRoleName: {
|
|
288
1103
|
type: "string",
|
|
289
1104
|
required: false
|
|
290
1105
|
}
|
|
291
1106
|
},
|
|
292
1107
|
indexes: {
|
|
293
|
-
/** Base table: PK = TID#<tenantId>#
|
|
1108
|
+
/** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
294
1109
|
record: {
|
|
295
1110
|
pk: {
|
|
296
1111
|
field: "PK",
|
|
297
1112
|
composite: ["tenantId", "id"],
|
|
298
|
-
template: "TID#${tenantId}#
|
|
1113
|
+
template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
|
|
299
1114
|
},
|
|
300
1115
|
sk: {
|
|
301
1116
|
field: "SK",
|
|
@@ -304,18 +1119,21 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
304
1119
|
}
|
|
305
1120
|
},
|
|
306
1121
|
/**
|
|
307
|
-
* GSI1 — Unified Sharded List per ADR-011: list all
|
|
308
|
-
* four shards.
|
|
309
|
-
* SK is derived via `
|
|
310
|
-
*
|
|
311
|
-
*
|
|
1122
|
+
* GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
|
|
1123
|
+
* four shards. Tenant-scoped only, so `WID#-` is a sentinel.
|
|
1124
|
+
* SK is derived via `roleAssignmentGsi1skAttribute` — composes the
|
|
1125
|
+
* discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
|
|
1126
|
+
* ADR-018 pattern #8 (users with a specific role in a tenant, sorted
|
|
1127
|
+
* by user name); falls back to `<lastUpdated>#<id>` when either
|
|
1128
|
+
* component is missing. `casing: "none"` preserves the normalized
|
|
1129
|
+
* label and ISO-8601 `T`/`Z`.
|
|
312
1130
|
*/
|
|
313
1131
|
gsi1: {
|
|
314
1132
|
index: "GSI1",
|
|
315
1133
|
pk: {
|
|
316
1134
|
field: "GSI1PK",
|
|
317
1135
|
composite: ["tenantId", "gsi1Shard"],
|
|
318
|
-
template: "TID#${tenantId}#WID#-#RT#
|
|
1136
|
+
template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
|
|
319
1137
|
},
|
|
320
1138
|
sk: {
|
|
321
1139
|
field: "GSI1SK",
|
|
@@ -327,206 +1145,285 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
327
1145
|
}
|
|
328
1146
|
});
|
|
329
1147
|
|
|
330
|
-
// src/data/dynamo/entities/control/
|
|
331
|
-
var
|
|
332
|
-
var
|
|
1148
|
+
// src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
|
|
1149
|
+
var import_electrodb9 = require("electrodb");
|
|
1150
|
+
var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
|
|
333
1151
|
model: {
|
|
334
|
-
entity: "
|
|
1152
|
+
entity: "roleAssignmentUserProjection",
|
|
335
1153
|
service: "control",
|
|
336
1154
|
version: "01"
|
|
337
1155
|
},
|
|
338
1156
|
attributes: {
|
|
339
|
-
/**
|
|
1157
|
+
/**
|
|
1158
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
1159
|
+
* base-table PK. Always required — the projection has no meaning
|
|
1160
|
+
* outside a user partition.
|
|
1161
|
+
*/
|
|
1162
|
+
userId: {
|
|
1163
|
+
type: "string",
|
|
1164
|
+
required: true
|
|
1165
|
+
},
|
|
1166
|
+
/**
|
|
1167
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
1168
|
+
* writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
|
|
1169
|
+
* entity stores the value verbatim so the SK grammar (tenant-lane
|
|
1170
|
+
* vs workspace-lane) is owned by the operations layer, not
|
|
1171
|
+
* duplicated here.
|
|
1172
|
+
*/
|
|
340
1173
|
sk: {
|
|
341
1174
|
type: "string",
|
|
342
|
-
required: true
|
|
343
|
-
default: "CURRENT"
|
|
1175
|
+
required: true
|
|
344
1176
|
},
|
|
345
|
-
/**
|
|
346
|
-
|
|
1177
|
+
/** Tenant in which the role assignment applies. Always required. */
|
|
1178
|
+
tenantId: {
|
|
347
1179
|
type: "string",
|
|
348
1180
|
required: true
|
|
349
1181
|
},
|
|
350
|
-
/**
|
|
351
|
-
|
|
1182
|
+
/**
|
|
1183
|
+
* Workspace the role assignment scopes to. Present iff the
|
|
1184
|
+
* projection row is the workspace-level sub-lane; absent for
|
|
1185
|
+
* tenant-level sub-lane rows.
|
|
1186
|
+
*/
|
|
1187
|
+
workspaceId: {
|
|
1188
|
+
type: "string",
|
|
1189
|
+
required: false
|
|
1190
|
+
},
|
|
1191
|
+
/**
|
|
1192
|
+
* Role the assignment grants. Stored as a discriminating field so
|
|
1193
|
+
* `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
|
|
1194
|
+
* results carry the role id without a hop to the canonical row.
|
|
1195
|
+
*/
|
|
1196
|
+
roleId: {
|
|
352
1197
|
type: "string",
|
|
353
1198
|
required: true
|
|
354
1199
|
},
|
|
355
1200
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
1201
|
+
* RoleAssignment canonical-record id. Stored as a discriminating
|
|
1202
|
+
* field so consumers can hydrate the canonical row via
|
|
1203
|
+
* `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
|
|
1204
|
+
* when the projection's `summary` is insufficient.
|
|
1205
|
+
*/
|
|
1206
|
+
roleAssignmentId: {
|
|
1207
|
+
type: "string",
|
|
1208
|
+
required: true
|
|
1209
|
+
},
|
|
1210
|
+
/**
|
|
1211
|
+
* Summary projection (key display fields as JSON string: id,
|
|
1212
|
+
* displayName, status) — mirrored from the canonical RoleAssignment
|
|
1213
|
+
* row so user-partition queries do not need a BatchGet hop.
|
|
358
1214
|
*/
|
|
359
1215
|
summary: {
|
|
360
1216
|
type: "string",
|
|
361
1217
|
required: true
|
|
362
1218
|
},
|
|
363
|
-
/** Version id
|
|
1219
|
+
/** Version id mirrored from the canonical RoleAssignment row. */
|
|
364
1220
|
vid: {
|
|
365
1221
|
type: "string",
|
|
366
1222
|
required: true
|
|
367
1223
|
},
|
|
1224
|
+
/** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
|
|
368
1225
|
lastUpdated: {
|
|
369
1226
|
type: "string",
|
|
370
1227
|
required: true
|
|
371
1228
|
},
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1229
|
+
/**
|
|
1230
|
+
* Denormalized Tenant display name — mirrored from the canonical
|
|
1231
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
1232
|
+
* Optional on the schema because pre-TR-024 rows may not carry a
|
|
1233
|
+
* display name; the operations layer falls back gracefully when
|
|
1234
|
+
* missing.
|
|
1235
|
+
*/
|
|
1236
|
+
denormalizedTenantName: {
|
|
1237
|
+
type: "string",
|
|
377
1238
|
required: false
|
|
378
1239
|
},
|
|
379
|
-
|
|
1240
|
+
/**
|
|
1241
|
+
* Denormalized User display name — mirrored from the canonical
|
|
1242
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
1243
|
+
* Carried on the projection so consumers can render the user's
|
|
1244
|
+
* display name without a hop to the User record.
|
|
1245
|
+
*/
|
|
1246
|
+
denormalizedUserName: {
|
|
380
1247
|
type: "string",
|
|
381
1248
|
required: false
|
|
382
1249
|
},
|
|
383
|
-
|
|
1250
|
+
/**
|
|
1251
|
+
* Denormalized Role display name — required to compose the SK's
|
|
1252
|
+
* `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
|
|
1253
|
+
* rows fall back to a sentinel) but expected to be present at write
|
|
1254
|
+
* time per TR-024 rule 2 (write-time source =
|
|
1255
|
+
* canonical Role.displayName).
|
|
1256
|
+
*/
|
|
1257
|
+
denormalizedRoleName: {
|
|
384
1258
|
type: "string",
|
|
385
1259
|
required: false
|
|
386
1260
|
}
|
|
387
1261
|
},
|
|
388
1262
|
indexes: {
|
|
389
|
-
/**
|
|
1263
|
+
/**
|
|
1264
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
|
|
1265
|
+
* sub-lanes (tenant-level and workspace-level) use this same index —
|
|
1266
|
+
* the SK string encodes the lane discriminator
|
|
1267
|
+
* (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
|
|
1268
|
+
* single `Query(PK = USER#ID#<userId>, SK begins_with
|
|
1269
|
+
* 'ROLEASSIGNMENT#')` returns both lanes interleaved.
|
|
1270
|
+
*/
|
|
390
1271
|
record: {
|
|
391
1272
|
pk: {
|
|
392
1273
|
field: "PK",
|
|
393
|
-
composite: ["
|
|
394
|
-
template: "
|
|
1274
|
+
composite: ["userId"],
|
|
1275
|
+
template: "USER#ID#${userId}"
|
|
395
1276
|
},
|
|
396
1277
|
sk: {
|
|
397
1278
|
field: "SK",
|
|
1279
|
+
casing: "none",
|
|
398
1280
|
composite: ["sk"],
|
|
399
1281
|
template: "${sk}"
|
|
400
1282
|
}
|
|
401
|
-
},
|
|
402
|
-
/**
|
|
403
|
-
* GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
|
|
404
|
-
* Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
|
|
405
|
-
* SK is derived via `gsi1skAttribute` — uses the resource's natural label when
|
|
406
|
-
* extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
|
|
407
|
-
* normalized label and ISO-8601 `T`/`Z`.
|
|
408
|
-
*/
|
|
409
|
-
gsi1: {
|
|
410
|
-
index: "GSI1",
|
|
411
|
-
pk: {
|
|
412
|
-
field: "GSI1PK",
|
|
413
|
-
composite: ["gsi1Shard"],
|
|
414
|
-
template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
|
|
415
|
-
},
|
|
416
|
-
sk: {
|
|
417
|
-
field: "GSI1SK",
|
|
418
|
-
casing: "none",
|
|
419
|
-
composite: ["gsi1sk"],
|
|
420
|
-
template: "${gsi1sk}"
|
|
421
|
-
}
|
|
422
1283
|
}
|
|
423
1284
|
}
|
|
424
1285
|
});
|
|
425
1286
|
|
|
426
|
-
// src/data/dynamo/entities/control/roleassignment-entity.ts
|
|
427
|
-
var
|
|
428
|
-
var
|
|
1287
|
+
// src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
|
|
1288
|
+
var import_electrodb10 = require("electrodb");
|
|
1289
|
+
var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
|
|
429
1290
|
model: {
|
|
430
|
-
entity: "
|
|
1291
|
+
entity: "roleAssignmentWorkspaceProjection",
|
|
431
1292
|
service: "control",
|
|
432
1293
|
version: "01"
|
|
433
1294
|
},
|
|
434
1295
|
attributes: {
|
|
435
|
-
/**
|
|
1296
|
+
/**
|
|
1297
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
1298
|
+
* of the base-table PK. Always required — the workspace partition
|
|
1299
|
+
* is tenant-scoped per ADR-011.
|
|
1300
|
+
*/
|
|
1301
|
+
tenantId: {
|
|
1302
|
+
type: "string",
|
|
1303
|
+
required: true
|
|
1304
|
+
},
|
|
1305
|
+
/**
|
|
1306
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
1307
|
+
* segment of the base-table PK
|
|
1308
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
1309
|
+
* the projection has no meaning outside a workspace partition.
|
|
1310
|
+
*/
|
|
1311
|
+
workspaceId: {
|
|
1312
|
+
type: "string",
|
|
1313
|
+
required: true
|
|
1314
|
+
},
|
|
1315
|
+
/**
|
|
1316
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
1317
|
+
* writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
|
|
1318
|
+
* stores the value verbatim so the SK grammar (pattern #9) is
|
|
1319
|
+
* owned by the operations layer, not duplicated here.
|
|
1320
|
+
*/
|
|
436
1321
|
sk: {
|
|
437
1322
|
type: "string",
|
|
438
|
-
required: true
|
|
439
|
-
default: "CURRENT"
|
|
1323
|
+
required: true
|
|
440
1324
|
},
|
|
441
|
-
/**
|
|
442
|
-
|
|
1325
|
+
/**
|
|
1326
|
+
* User the role assignment grants the role to. Stored as a
|
|
1327
|
+
* discriminating field so consumers can hydrate the canonical User
|
|
1328
|
+
* row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
|
|
1329
|
+
* projection's `summary` is insufficient.
|
|
1330
|
+
*/
|
|
1331
|
+
userId: {
|
|
443
1332
|
type: "string",
|
|
444
1333
|
required: true
|
|
445
1334
|
},
|
|
446
|
-
/**
|
|
447
|
-
|
|
1335
|
+
/**
|
|
1336
|
+
* Role the assignment grants. Stored as a discriminating field —
|
|
1337
|
+
* also rendered into the SK as the discriminator-first segment so
|
|
1338
|
+
* `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
|
|
1339
|
+
*/
|
|
1340
|
+
roleId: {
|
|
448
1341
|
type: "string",
|
|
449
1342
|
required: true
|
|
450
1343
|
},
|
|
451
|
-
/**
|
|
452
|
-
|
|
1344
|
+
/**
|
|
1345
|
+
* RoleAssignment canonical-record id. Stored as a discriminating
|
|
1346
|
+
* field so consumers can hydrate the canonical row via
|
|
1347
|
+
* `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
|
|
1348
|
+
* when the projection's `summary` is insufficient.
|
|
1349
|
+
*/
|
|
1350
|
+
roleAssignmentId: {
|
|
453
1351
|
type: "string",
|
|
454
1352
|
required: true
|
|
455
1353
|
},
|
|
456
1354
|
/**
|
|
457
|
-
* Summary projection (key display fields as JSON string: id,
|
|
458
|
-
*
|
|
1355
|
+
* Summary projection (key display fields as JSON string: id,
|
|
1356
|
+
* displayName, status) — mirrored from the canonical RoleAssignment
|
|
1357
|
+
* row so workspace-partition queries do not need a BatchGet hop.
|
|
459
1358
|
*/
|
|
460
1359
|
summary: {
|
|
461
1360
|
type: "string",
|
|
462
1361
|
required: true
|
|
463
1362
|
},
|
|
464
|
-
/** Version id
|
|
1363
|
+
/** Version id mirrored from the canonical RoleAssignment row. */
|
|
465
1364
|
vid: {
|
|
466
1365
|
type: "string",
|
|
467
1366
|
required: true
|
|
468
1367
|
},
|
|
1368
|
+
/** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
|
|
469
1369
|
lastUpdated: {
|
|
470
1370
|
type: "string",
|
|
471
1371
|
required: true
|
|
472
1372
|
},
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1373
|
+
/**
|
|
1374
|
+
* Denormalized User display name — required to compose the
|
|
1375
|
+
* pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
|
|
1376
|
+
* Optional on the schema because pre-TR-024 rows may not carry a
|
|
1377
|
+
* display name; the operations layer falls back to a sentinel when
|
|
1378
|
+
* missing so the SK still has a valid shape. The TR-023 rename-
|
|
1379
|
+
* cascade pipeline rewrites the SK on a User rename.
|
|
1380
|
+
*/
|
|
1381
|
+
denormalizedUserName: {
|
|
481
1382
|
type: "string",
|
|
482
1383
|
required: false
|
|
483
1384
|
},
|
|
484
|
-
|
|
1385
|
+
/**
|
|
1386
|
+
* Denormalized Role display name — mirrored from the canonical
|
|
1387
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
1388
|
+
* Carried on the projection so consumers can render the role's
|
|
1389
|
+
* display name without a hop to the Role record. Not part of the
|
|
1390
|
+
* SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
|
|
1391
|
+
* a Role rename does NOT rewrite this SK.
|
|
1392
|
+
*/
|
|
1393
|
+
denormalizedRoleName: {
|
|
485
1394
|
type: "string",
|
|
486
1395
|
required: false
|
|
487
1396
|
}
|
|
488
1397
|
},
|
|
489
1398
|
indexes: {
|
|
490
|
-
/**
|
|
1399
|
+
/**
|
|
1400
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
1401
|
+
* SK = operation-supplied. Pattern #9 uses this index — the SK
|
|
1402
|
+
* encodes the entity-type prefix and discriminator-first roleId
|
|
1403
|
+
* (`ROLEASSIGNMENT#<roleId>#…`) so
|
|
1404
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
|
|
1405
|
+
* returns every user-assignment for that role in the workspace, sorted
|
|
1406
|
+
* by normalized user name.
|
|
1407
|
+
*/
|
|
491
1408
|
record: {
|
|
492
1409
|
pk: {
|
|
493
1410
|
field: "PK",
|
|
494
|
-
composite: ["tenantId", "
|
|
495
|
-
template: "TID#${tenantId}#
|
|
1411
|
+
composite: ["tenantId", "workspaceId"],
|
|
1412
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
496
1413
|
},
|
|
497
1414
|
sk: {
|
|
498
1415
|
field: "SK",
|
|
1416
|
+
casing: "none",
|
|
499
1417
|
composite: ["sk"],
|
|
500
1418
|
template: "${sk}"
|
|
501
1419
|
}
|
|
502
|
-
},
|
|
503
|
-
/**
|
|
504
|
-
* GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
|
|
505
|
-
* four shards. Tenant-scoped only, so `WID#-` is a sentinel.
|
|
506
|
-
* SK is derived via `gsi1skAttribute` — uses the resource's natural label when
|
|
507
|
-
* extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
|
|
508
|
-
* normalized label and ISO-8601 `T`/`Z`.
|
|
509
|
-
*/
|
|
510
|
-
gsi1: {
|
|
511
|
-
index: "GSI1",
|
|
512
|
-
pk: {
|
|
513
|
-
field: "GSI1PK",
|
|
514
|
-
composite: ["tenantId", "gsi1Shard"],
|
|
515
|
-
template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
|
|
516
|
-
},
|
|
517
|
-
sk: {
|
|
518
|
-
field: "GSI1SK",
|
|
519
|
-
casing: "none",
|
|
520
|
-
composite: ["gsi1sk"],
|
|
521
|
-
template: "${gsi1sk}"
|
|
522
|
-
}
|
|
523
1420
|
}
|
|
524
1421
|
}
|
|
525
1422
|
});
|
|
526
1423
|
|
|
527
1424
|
// src/data/dynamo/entities/control/tenant-entity.ts
|
|
528
|
-
var
|
|
529
|
-
var TenantEntity = new
|
|
1425
|
+
var import_electrodb11 = require("electrodb");
|
|
1426
|
+
var TenantEntity = new import_electrodb11.Entity({
|
|
530
1427
|
model: {
|
|
531
1428
|
entity: "tenant",
|
|
532
1429
|
service: "control",
|
|
@@ -626,8 +1523,8 @@ var TenantEntity = new import_electrodb5.Entity({
|
|
|
626
1523
|
});
|
|
627
1524
|
|
|
628
1525
|
// src/data/dynamo/entities/control/user-entity.ts
|
|
629
|
-
var
|
|
630
|
-
var UserEntity = new
|
|
1526
|
+
var import_electrodb12 = require("electrodb");
|
|
1527
|
+
var UserEntity = new import_electrodb12.Entity({
|
|
631
1528
|
model: {
|
|
632
1529
|
entity: "user",
|
|
633
1530
|
service: "control",
|
|
@@ -682,6 +1579,28 @@ var UserEntity = new import_electrodb6.Entity({
|
|
|
682
1579
|
type: "boolean",
|
|
683
1580
|
required: false
|
|
684
1581
|
},
|
|
1582
|
+
/**
|
|
1583
|
+
* TR-022 / ADR-018 lifecycle state for the cascade pipeline.
|
|
1584
|
+
*
|
|
1585
|
+
* - `active` (or undefined) — normal, readable state.
|
|
1586
|
+
* - `deleting` — intermediate state set synchronously by the
|
|
1587
|
+
* hard-delete API entry point. The owning-delete cascade state
|
|
1588
|
+
* machine fans out from this transition (DynamoDB stream →
|
|
1589
|
+
* `control-plane.owning-delete.v1` → Step Functions). Readers MUST
|
|
1590
|
+
* short-circuit on `deleting` so partial cascades stay invisible.
|
|
1591
|
+
* - `deleted-failed` — terminal failure state set by the cascade
|
|
1592
|
+
* finalize Lambda when the cascade run fails irrecoverably.
|
|
1593
|
+
* Operators recover by re-running the cascade or by direct
|
|
1594
|
+
* intervention.
|
|
1595
|
+
*
|
|
1596
|
+
* The cascade finalize step deletes the canonical record conditional
|
|
1597
|
+
* on `lifecycleState = "deleting"`; on replay the conditional check
|
|
1598
|
+
* fails and the finalize step treats that as a no-op success.
|
|
1599
|
+
*/
|
|
1600
|
+
lifecycleState: {
|
|
1601
|
+
type: ["active", "deleting", "deleted-failed"],
|
|
1602
|
+
required: false
|
|
1603
|
+
},
|
|
685
1604
|
bundleId: {
|
|
686
1605
|
type: "string",
|
|
687
1606
|
required: false
|
|
@@ -751,8 +1670,8 @@ var UserEntity = new import_electrodb6.Entity({
|
|
|
751
1670
|
});
|
|
752
1671
|
|
|
753
1672
|
// src/data/dynamo/entities/control/workspace-entity.ts
|
|
754
|
-
var
|
|
755
|
-
var WorkspaceEntity = new
|
|
1673
|
+
var import_electrodb13 = require("electrodb");
|
|
1674
|
+
var WorkspaceEntity = new import_electrodb13.Entity({
|
|
756
1675
|
model: {
|
|
757
1676
|
entity: "workspace",
|
|
758
1677
|
service: "control",
|
|
@@ -804,6 +1723,28 @@ var WorkspaceEntity = new import_electrodb7.Entity({
|
|
|
804
1723
|
type: "boolean",
|
|
805
1724
|
required: false
|
|
806
1725
|
},
|
|
1726
|
+
/**
|
|
1727
|
+
* TR-022 / ADR-018 lifecycle state for the cascade pipeline.
|
|
1728
|
+
*
|
|
1729
|
+
* - `active` (or undefined) — normal, readable state.
|
|
1730
|
+
* - `deleting` — intermediate state set synchronously by the
|
|
1731
|
+
* hard-delete API entry point. The owning-delete cascade state
|
|
1732
|
+
* machine fans out from this transition (DynamoDB stream →
|
|
1733
|
+
* `control-plane.owning-delete.v1` → Step Functions). Readers MUST
|
|
1734
|
+
* short-circuit on `deleting` so partial cascades stay invisible.
|
|
1735
|
+
* - `deleted-failed` — terminal failure state set by the cascade
|
|
1736
|
+
* finalize Lambda when the cascade run fails irrecoverably.
|
|
1737
|
+
* Operators recover by re-running the cascade or by direct
|
|
1738
|
+
* intervention.
|
|
1739
|
+
*
|
|
1740
|
+
* The cascade finalize step deletes the canonical record conditional
|
|
1741
|
+
* on `lifecycleState = "deleting"`; on replay the conditional check
|
|
1742
|
+
* fails and the finalize step treats that as a no-op success.
|
|
1743
|
+
*/
|
|
1744
|
+
lifecycleState: {
|
|
1745
|
+
type: ["active", "deleting", "deleted-failed"],
|
|
1746
|
+
required: false
|
|
1747
|
+
},
|
|
807
1748
|
bundleId: {
|
|
808
1749
|
type: "string",
|
|
809
1750
|
required: false
|
|
@@ -854,28 +1795,36 @@ var WorkspaceEntity = new import_electrodb7.Entity({
|
|
|
854
1795
|
// src/data/dynamo/dynamo-control-service.ts
|
|
855
1796
|
var controlPlaneEntities = {
|
|
856
1797
|
configuration: ConfigurationEntity,
|
|
1798
|
+
configurationUserProjection: ConfigurationUserProjectionEntity,
|
|
1799
|
+
configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
|
|
857
1800
|
membership: MembershipEntity,
|
|
1801
|
+
membershipUserProjection: MembershipUserProjectionEntity,
|
|
1802
|
+
membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
|
|
858
1803
|
role: RoleEntity,
|
|
859
1804
|
roleAssignment: RoleAssignmentEntity,
|
|
1805
|
+
roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
|
|
1806
|
+
roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
|
|
860
1807
|
tenant: TenantEntity,
|
|
861
1808
|
user: UserEntity,
|
|
862
1809
|
workspace: WorkspaceEntity
|
|
863
1810
|
};
|
|
864
|
-
var controlPlaneService = new
|
|
1811
|
+
var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
|
|
865
1812
|
table: defaultTableName,
|
|
866
1813
|
client: dynamoClient
|
|
867
1814
|
});
|
|
868
1815
|
var DynamoControlService = {
|
|
869
|
-
entities: controlPlaneService.entities
|
|
1816
|
+
entities: controlPlaneService.entities,
|
|
1817
|
+
transaction: controlPlaneService.transaction
|
|
870
1818
|
};
|
|
871
1819
|
function getDynamoControlService(tableName) {
|
|
872
1820
|
const resolved = tableName ?? defaultTableName;
|
|
873
|
-
const service = new
|
|
1821
|
+
const service = new import_electrodb14.Service(controlPlaneEntities, {
|
|
874
1822
|
table: resolved,
|
|
875
1823
|
client: dynamoClient
|
|
876
1824
|
});
|
|
877
1825
|
return {
|
|
878
|
-
entities: service.entities
|
|
1826
|
+
entities: service.entities,
|
|
1827
|
+
transaction: service.transaction
|
|
879
1828
|
};
|
|
880
1829
|
}
|
|
881
1830
|
|
|
@@ -1132,10 +2081,10 @@ function idFromReference(reference, prefix) {
|
|
|
1132
2081
|
var import_types5 = require("@openhi/types");
|
|
1133
2082
|
|
|
1134
2083
|
// src/data/dynamo/dynamo-data-service.ts
|
|
1135
|
-
var
|
|
2084
|
+
var import_electrodb16 = require("electrodb");
|
|
1136
2085
|
|
|
1137
2086
|
// src/data/dynamo/entities/data-entity-common.ts
|
|
1138
|
-
var
|
|
2087
|
+
var import_electrodb15 = require("electrodb");
|
|
1139
2088
|
var dataEntityAttributes = {
|
|
1140
2089
|
/** Sort key. "CURRENT" for current version; version history in S3. */
|
|
1141
2090
|
sk: {
|
|
@@ -1231,7 +2180,7 @@ var dataEntityAttributes = {
|
|
|
1231
2180
|
}
|
|
1232
2181
|
};
|
|
1233
2182
|
function createDataEntity(entity, resourceTypeLabel) {
|
|
1234
|
-
return new
|
|
2183
|
+
return new import_electrodb15.Entity({
|
|
1235
2184
|
model: {
|
|
1236
2185
|
entity,
|
|
1237
2186
|
service: "data",
|
|
@@ -2145,21 +3094,23 @@ var dataPlaneEntities = {
|
|
|
2145
3094
|
visionprescription: VisionPrescriptionEntity,
|
|
2146
3095
|
verificationresult: VerificationResultEntity
|
|
2147
3096
|
};
|
|
2148
|
-
var dataPlaneService = new
|
|
3097
|
+
var dataPlaneService = new import_electrodb16.Service(dataPlaneEntities, {
|
|
2149
3098
|
table: defaultTableName,
|
|
2150
3099
|
client: dynamoClient
|
|
2151
3100
|
});
|
|
2152
3101
|
var DynamoDataService = {
|
|
2153
|
-
entities: dataPlaneService.entities
|
|
3102
|
+
entities: dataPlaneService.entities,
|
|
3103
|
+
transaction: dataPlaneService.transaction
|
|
2154
3104
|
};
|
|
2155
3105
|
function getDynamoDataService(tableName) {
|
|
2156
3106
|
const resolved = tableName ?? defaultTableName;
|
|
2157
|
-
const service = new
|
|
3107
|
+
const service = new import_electrodb16.Service(dataPlaneEntities, {
|
|
2158
3108
|
table: resolved,
|
|
2159
3109
|
client: dynamoClient
|
|
2160
3110
|
});
|
|
2161
3111
|
return {
|
|
2162
|
-
entities: service.entities
|
|
3112
|
+
entities: service.entities,
|
|
3113
|
+
transaction: service.transaction
|
|
2163
3114
|
};
|
|
2164
3115
|
}
|
|
2165
3116
|
|