@openhi/constructs 0.0.111 → 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-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/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 +4021 -932
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +1786 -80
- 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 +3 -3
- 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
|
@@ -24,10 +24,10 @@ __export(provision_default_workspace_handler_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(provision_default_workspace_handler_exports);
|
|
26
26
|
var import_node_crypto = require("crypto");
|
|
27
|
-
var
|
|
27
|
+
var import_types14 = require("@openhi/types");
|
|
28
28
|
|
|
29
29
|
// src/data/dynamo/dynamo-control-service.ts
|
|
30
|
-
var
|
|
30
|
+
var import_electrodb14 = require("electrodb");
|
|
31
31
|
|
|
32
32
|
// src/data/dynamo/dynamo-client.ts
|
|
33
33
|
var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
|
|
@@ -91,6 +91,60 @@ var gsi1skAttribute = {
|
|
|
91
91
|
return label !== void 0 ? `${label}#${id}` : fallback;
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
|
+
function extractRoleId(resource) {
|
|
95
|
+
const flat = resource.roleId;
|
|
96
|
+
if (typeof flat === "string" && flat.length > 0) return flat;
|
|
97
|
+
const role = resource.role;
|
|
98
|
+
if (role && typeof role === "object") {
|
|
99
|
+
const reference = role.reference;
|
|
100
|
+
if (typeof reference === "string" && reference.length > 0) {
|
|
101
|
+
const slash = reference.lastIndexOf("/");
|
|
102
|
+
const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
|
|
103
|
+
if (tail.length > 0) return tail;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
var roleAssignmentGsi1skAttribute = {
|
|
109
|
+
type: "string",
|
|
110
|
+
watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
|
|
111
|
+
set: (_val, item) => {
|
|
112
|
+
const id = typeof item?.id === "string" ? item.id : "";
|
|
113
|
+
const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
|
|
114
|
+
const fallback = `${lastUpdated}#${id}`;
|
|
115
|
+
if (typeof item?.resource !== "string" || item.resource.length === 0) {
|
|
116
|
+
return fallback;
|
|
117
|
+
}
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(item.resource);
|
|
121
|
+
} catch {
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
if (!parsed || typeof parsed !== "object") return fallback;
|
|
125
|
+
const roleId = extractRoleId(parsed);
|
|
126
|
+
if (roleId === void 0) return fallback;
|
|
127
|
+
const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
|
|
128
|
+
const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
|
|
129
|
+
if (normalizedUserName.length === 0) return fallback;
|
|
130
|
+
return `${roleId}#${normalizedUserName}#${id}`;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var membershipGsi1skAttribute = {
|
|
134
|
+
type: "string",
|
|
135
|
+
watch: ["denormalizedUserName", "lastUpdated", "id"],
|
|
136
|
+
set: (_val, item) => {
|
|
137
|
+
const id = typeof item?.id === "string" ? item.id : "";
|
|
138
|
+
const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
|
|
139
|
+
const fallback = `${lastUpdated}#${id}`;
|
|
140
|
+
const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
|
|
141
|
+
const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
|
|
142
|
+
if (normalizedUserName.length === 0) {
|
|
143
|
+
return fallback;
|
|
144
|
+
}
|
|
145
|
+
return `${normalizedUserName}#${id}`;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
94
148
|
|
|
95
149
|
// src/data/dynamo/entities/control/configuration-entity.ts
|
|
96
150
|
var ConfigurationEntity = new import_electrodb.Entity({
|
|
@@ -217,9 +271,239 @@ var ConfigurationEntity = new import_electrodb.Entity({
|
|
|
217
271
|
}
|
|
218
272
|
});
|
|
219
273
|
|
|
220
|
-
// src/data/dynamo/entities/control/
|
|
274
|
+
// src/data/dynamo/entities/control/configuration-user-projection-entity.ts
|
|
221
275
|
var import_electrodb2 = require("electrodb");
|
|
222
|
-
var
|
|
276
|
+
var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
|
|
277
|
+
model: {
|
|
278
|
+
entity: "configurationUserProjection",
|
|
279
|
+
service: "control",
|
|
280
|
+
version: "01"
|
|
281
|
+
},
|
|
282
|
+
attributes: {
|
|
283
|
+
/**
|
|
284
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
285
|
+
* base-table PK. Always required — the projection has no meaning
|
|
286
|
+
* outside a user partition.
|
|
287
|
+
*/
|
|
288
|
+
userId: {
|
|
289
|
+
type: "string",
|
|
290
|
+
required: true
|
|
291
|
+
},
|
|
292
|
+
/**
|
|
293
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
294
|
+
* writer via `buildConfigurationUserProjectionSk`. The entity stores
|
|
295
|
+
* the value verbatim so the SK grammar (pattern #10 user-scope) is
|
|
296
|
+
* owned by the operations layer, not duplicated here.
|
|
297
|
+
*/
|
|
298
|
+
sk: {
|
|
299
|
+
type: "string",
|
|
300
|
+
required: true
|
|
301
|
+
},
|
|
302
|
+
/**
|
|
303
|
+
* Configuration canonical-record id. Stored as a discriminating
|
|
304
|
+
* field so consumers can hydrate the canonical row via the
|
|
305
|
+
* Configuration get-by-id operation when the projection's `summary`
|
|
306
|
+
* is insufficient.
|
|
307
|
+
*/
|
|
308
|
+
configurationId: {
|
|
309
|
+
type: "string",
|
|
310
|
+
required: true
|
|
311
|
+
},
|
|
312
|
+
/**
|
|
313
|
+
* Tenant the Configuration is associated with. The canonical row
|
|
314
|
+
* keys off `(tenantId, workspaceId, userId, roleId)`; the projection
|
|
315
|
+
* carries `tenantId` so consumers reconstructing the canonical PK
|
|
316
|
+
* have the tenant segment without a hop.
|
|
317
|
+
*/
|
|
318
|
+
tenantId: {
|
|
319
|
+
type: "string",
|
|
320
|
+
required: true
|
|
321
|
+
},
|
|
322
|
+
/**
|
|
323
|
+
* Scope marker. Always `"user"` on this projection — recorded
|
|
324
|
+
* explicitly so future scope-bearing projections (workspace,
|
|
325
|
+
* tenant, role) can share filter semantics in a unified
|
|
326
|
+
* cross-projection list query if one ever lands.
|
|
327
|
+
*/
|
|
328
|
+
scope: {
|
|
329
|
+
type: "string",
|
|
330
|
+
required: true,
|
|
331
|
+
default: "user"
|
|
332
|
+
},
|
|
333
|
+
/**
|
|
334
|
+
* Configuration's `key` attribute (config category, e.g. endpoints,
|
|
335
|
+
* branding, display). Mirrored from the canonical row so consumers
|
|
336
|
+
* reading the projection get the natural display label without a
|
|
337
|
+
* BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
|
|
338
|
+
* the SK.
|
|
339
|
+
*/
|
|
340
|
+
displayName: {
|
|
341
|
+
type: "string",
|
|
342
|
+
required: false
|
|
343
|
+
},
|
|
344
|
+
/**
|
|
345
|
+
* Summary projection (key display fields as JSON string) — mirrored
|
|
346
|
+
* from the canonical Configuration row so user-partition queries do
|
|
347
|
+
* not need a BatchGet hop.
|
|
348
|
+
*/
|
|
349
|
+
summary: {
|
|
350
|
+
type: "string",
|
|
351
|
+
required: true
|
|
352
|
+
},
|
|
353
|
+
/** Version id mirrored from the canonical Configuration row. */
|
|
354
|
+
vid: {
|
|
355
|
+
type: "string",
|
|
356
|
+
required: true
|
|
357
|
+
},
|
|
358
|
+
/** Last-updated timestamp mirrored from the canonical Configuration row. */
|
|
359
|
+
lastUpdated: {
|
|
360
|
+
type: "string",
|
|
361
|
+
required: true
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
indexes: {
|
|
365
|
+
/**
|
|
366
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
|
|
367
|
+
* single `Query(PK = USER#ID#<userId>, SK begins_with
|
|
368
|
+
* 'CONFIGURATION#')` returns the user's user-scoped Configurations
|
|
369
|
+
* sorted by `<normalizedConfigName>` (then `<configurationId>` as
|
|
370
|
+
* the tiebreaker).
|
|
371
|
+
*/
|
|
372
|
+
record: {
|
|
373
|
+
pk: {
|
|
374
|
+
field: "PK",
|
|
375
|
+
composite: ["userId"],
|
|
376
|
+
template: "USER#ID#${userId}"
|
|
377
|
+
},
|
|
378
|
+
sk: {
|
|
379
|
+
field: "SK",
|
|
380
|
+
casing: "none",
|
|
381
|
+
composite: ["sk"],
|
|
382
|
+
template: "${sk}"
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
|
|
389
|
+
var import_electrodb3 = require("electrodb");
|
|
390
|
+
var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
|
|
391
|
+
model: {
|
|
392
|
+
entity: "configurationWorkspaceProjection",
|
|
393
|
+
service: "control",
|
|
394
|
+
version: "01"
|
|
395
|
+
},
|
|
396
|
+
attributes: {
|
|
397
|
+
/**
|
|
398
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
399
|
+
* of the base-table PK. Always required — the workspace partition
|
|
400
|
+
* is tenant-scoped per ADR-011.
|
|
401
|
+
*/
|
|
402
|
+
tenantId: {
|
|
403
|
+
type: "string",
|
|
404
|
+
required: true
|
|
405
|
+
},
|
|
406
|
+
/**
|
|
407
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
408
|
+
* segment of the base-table PK
|
|
409
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
410
|
+
* the projection has no meaning outside a workspace partition.
|
|
411
|
+
*/
|
|
412
|
+
workspaceId: {
|
|
413
|
+
type: "string",
|
|
414
|
+
required: true
|
|
415
|
+
},
|
|
416
|
+
/**
|
|
417
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
418
|
+
* writer via `buildConfigurationWorkspaceProjectionSk`. The entity
|
|
419
|
+
* stores the value verbatim so the SK grammar (pattern #10
|
|
420
|
+
* workspace-scope) is owned by the operations layer, not
|
|
421
|
+
* duplicated here.
|
|
422
|
+
*/
|
|
423
|
+
sk: {
|
|
424
|
+
type: "string",
|
|
425
|
+
required: true
|
|
426
|
+
},
|
|
427
|
+
/**
|
|
428
|
+
* Configuration canonical-record id. Stored as a discriminating
|
|
429
|
+
* field so consumers can hydrate the canonical row via the
|
|
430
|
+
* Configuration get-by-id operation when the projection's `summary`
|
|
431
|
+
* is insufficient.
|
|
432
|
+
*/
|
|
433
|
+
configurationId: {
|
|
434
|
+
type: "string",
|
|
435
|
+
required: true
|
|
436
|
+
},
|
|
437
|
+
/**
|
|
438
|
+
* Scope marker. Always `"workspace"` on this projection — recorded
|
|
439
|
+
* explicitly so future scope-bearing projections (user, tenant,
|
|
440
|
+
* role) can share filter semantics in a unified cross-projection
|
|
441
|
+
* list query if one ever lands.
|
|
442
|
+
*/
|
|
443
|
+
scope: {
|
|
444
|
+
type: "string",
|
|
445
|
+
required: true,
|
|
446
|
+
default: "workspace"
|
|
447
|
+
},
|
|
448
|
+
/**
|
|
449
|
+
* Configuration's `key` attribute (config category, e.g. endpoints,
|
|
450
|
+
* branding, display). Mirrored from the canonical row so consumers
|
|
451
|
+
* reading the projection get the natural display label without a
|
|
452
|
+
* BatchGet hop. Doubles as the source of `<normalizedConfigName>`
|
|
453
|
+
* in the SK.
|
|
454
|
+
*/
|
|
455
|
+
displayName: {
|
|
456
|
+
type: "string",
|
|
457
|
+
required: false
|
|
458
|
+
},
|
|
459
|
+
/**
|
|
460
|
+
* Summary projection (key display fields as JSON string) — mirrored
|
|
461
|
+
* from the canonical Configuration row so workspace-partition
|
|
462
|
+
* queries do not need a BatchGet hop.
|
|
463
|
+
*/
|
|
464
|
+
summary: {
|
|
465
|
+
type: "string",
|
|
466
|
+
required: true
|
|
467
|
+
},
|
|
468
|
+
/** Version id mirrored from the canonical Configuration row. */
|
|
469
|
+
vid: {
|
|
470
|
+
type: "string",
|
|
471
|
+
required: true
|
|
472
|
+
},
|
|
473
|
+
/** Last-updated timestamp mirrored from the canonical Configuration row. */
|
|
474
|
+
lastUpdated: {
|
|
475
|
+
type: "string",
|
|
476
|
+
required: true
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
indexes: {
|
|
480
|
+
/**
|
|
481
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
482
|
+
* SK = operation-supplied. A single
|
|
483
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
|
|
484
|
+
* returns the workspace's workspace-scoped Configurations sorted by
|
|
485
|
+
* `<normalizedConfigName>` (then `<configurationId>` as the
|
|
486
|
+
* tiebreaker).
|
|
487
|
+
*/
|
|
488
|
+
record: {
|
|
489
|
+
pk: {
|
|
490
|
+
field: "PK",
|
|
491
|
+
composite: ["tenantId", "workspaceId"],
|
|
492
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
493
|
+
},
|
|
494
|
+
sk: {
|
|
495
|
+
field: "SK",
|
|
496
|
+
casing: "none",
|
|
497
|
+
composite: ["sk"],
|
|
498
|
+
template: "${sk}"
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// src/data/dynamo/entities/control/membership-entity.ts
|
|
505
|
+
var import_electrodb4 = require("electrodb");
|
|
506
|
+
var MembershipEntity = new import_electrodb4.Entity({
|
|
223
507
|
model: {
|
|
224
508
|
entity: "membership",
|
|
225
509
|
service: "control",
|
|
@@ -265,8 +549,14 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
265
549
|
required: true
|
|
266
550
|
},
|
|
267
551
|
gsi1Shard: gsi1ShardAttribute,
|
|
268
|
-
/**
|
|
269
|
-
|
|
552
|
+
/**
|
|
553
|
+
* Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
|
|
554
|
+
* pattern #1 so a GSI1 query partitioned on the tenant range-scans
|
|
555
|
+
* by user-name prefix and returns memberships sorted by user name.
|
|
556
|
+
* Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
|
|
557
|
+
* is missing.
|
|
558
|
+
*/
|
|
559
|
+
gsi1sk: membershipGsi1skAttribute,
|
|
270
560
|
deleted: {
|
|
271
561
|
type: "boolean",
|
|
272
562
|
required: false
|
|
@@ -288,6 +578,36 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
288
578
|
linkedDataIdentityRef: {
|
|
289
579
|
type: "string",
|
|
290
580
|
required: false
|
|
581
|
+
},
|
|
582
|
+
/**
|
|
583
|
+
* Denormalized display name of the linked Tenant, captured at row
|
|
584
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
585
|
+
* adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
|
|
586
|
+
* can be composed from a top-level field instead of digging into the
|
|
587
|
+
* `resource` JSON. Optional on the schema so pre-TR-024 rows do not
|
|
588
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
589
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
590
|
+
* source = canonical Tenant.displayName).
|
|
591
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
592
|
+
*/
|
|
593
|
+
denormalizedTenantName: {
|
|
594
|
+
type: "string",
|
|
595
|
+
required: false
|
|
596
|
+
},
|
|
597
|
+
/**
|
|
598
|
+
* Denormalized display name of the linked User, captured at row
|
|
599
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
600
|
+
* adjacency-list canonical-record GSI1SK (pattern #1 —
|
|
601
|
+
* `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
|
|
602
|
+
* can be composed from a top-level field. Optional on the schema so
|
|
603
|
+
* pre-TR-024 rows do not break; the operations-layer multi-write helper
|
|
604
|
+
* (#1010) makes the field load-bearing at write time per TR-024 rule 2
|
|
605
|
+
* (write-time source = canonical User.displayName).
|
|
606
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
607
|
+
*/
|
|
608
|
+
denormalizedUserName: {
|
|
609
|
+
type: "string",
|
|
610
|
+
required: false
|
|
291
611
|
}
|
|
292
612
|
},
|
|
293
613
|
indexes: {
|
|
@@ -307,9 +627,11 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
307
627
|
/**
|
|
308
628
|
* GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
|
|
309
629
|
* four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
|
|
310
|
-
* SK is derived via `
|
|
311
|
-
*
|
|
312
|
-
*
|
|
630
|
+
* SK is derived via `membershipGsi1skAttribute` — composes
|
|
631
|
+
* `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
|
|
632
|
+
* tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
|
|
633
|
+
* when `denormalizedUserName` is missing. `casing: "none"` preserves
|
|
634
|
+
* the normalized label and ISO-8601 `T`/`Z`.
|
|
313
635
|
*/
|
|
314
636
|
gsi1: {
|
|
315
637
|
index: "GSI1",
|
|
@@ -328,206 +650,781 @@ var MembershipEntity = new import_electrodb2.Entity({
|
|
|
328
650
|
}
|
|
329
651
|
});
|
|
330
652
|
|
|
331
|
-
// src/data/dynamo/entities/control/
|
|
332
|
-
var
|
|
333
|
-
var
|
|
653
|
+
// src/data/dynamo/entities/control/membership-user-projection-entity.ts
|
|
654
|
+
var import_electrodb5 = require("electrodb");
|
|
655
|
+
var MembershipUserProjectionEntity = new import_electrodb5.Entity({
|
|
334
656
|
model: {
|
|
335
|
-
entity: "
|
|
657
|
+
entity: "membershipUserProjection",
|
|
336
658
|
service: "control",
|
|
337
659
|
version: "01"
|
|
338
660
|
},
|
|
339
661
|
attributes: {
|
|
340
|
-
/**
|
|
662
|
+
/**
|
|
663
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
664
|
+
* base-table PK. Always required — the projection has no meaning
|
|
665
|
+
* outside a user partition.
|
|
666
|
+
*/
|
|
667
|
+
userId: {
|
|
668
|
+
type: "string",
|
|
669
|
+
required: true
|
|
670
|
+
},
|
|
671
|
+
/**
|
|
672
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
673
|
+
* writer via `buildMembershipUserProjectionSk*` helpers. The entity
|
|
674
|
+
* stores the value verbatim so the SK grammar (patterns #3 and #4)
|
|
675
|
+
* is owned by the operations layer, not duplicated here.
|
|
676
|
+
*/
|
|
341
677
|
sk: {
|
|
342
678
|
type: "string",
|
|
343
|
-
required: true
|
|
344
|
-
default: "CURRENT"
|
|
679
|
+
required: true
|
|
345
680
|
},
|
|
346
|
-
/**
|
|
347
|
-
|
|
681
|
+
/** Tenant in which the membership applies. Always required. */
|
|
682
|
+
tenantId: {
|
|
348
683
|
type: "string",
|
|
349
684
|
required: true
|
|
350
685
|
},
|
|
351
|
-
/**
|
|
352
|
-
|
|
686
|
+
/**
|
|
687
|
+
* Workspace the membership scopes to. Present iff the projection
|
|
688
|
+
* row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
|
|
689
|
+
* tenant sub-lane rows.
|
|
690
|
+
*/
|
|
691
|
+
workspaceId: {
|
|
692
|
+
type: "string",
|
|
693
|
+
required: false
|
|
694
|
+
},
|
|
695
|
+
/**
|
|
696
|
+
* Membership canonical-record id. Stored as a discriminating field
|
|
697
|
+
* so consumers can hydrate the canonical row via
|
|
698
|
+
* `MembershipEntity.get({ tenantId, id: membershipId })` when the
|
|
699
|
+
* projection's `summary` is insufficient.
|
|
700
|
+
*/
|
|
701
|
+
membershipId: {
|
|
353
702
|
type: "string",
|
|
354
703
|
required: true
|
|
355
704
|
},
|
|
356
705
|
/**
|
|
357
|
-
* Summary projection (key display fields as JSON string: id,
|
|
358
|
-
*
|
|
706
|
+
* Summary projection (key display fields as JSON string: id,
|
|
707
|
+
* displayName, status) — mirrored from the canonical Membership row
|
|
708
|
+
* so user-partition queries do not need a BatchGet hop.
|
|
359
709
|
*/
|
|
360
710
|
summary: {
|
|
361
711
|
type: "string",
|
|
362
712
|
required: true
|
|
363
713
|
},
|
|
364
|
-
/** Version id
|
|
714
|
+
/** Version id mirrored from the canonical Membership row. */
|
|
365
715
|
vid: {
|
|
366
716
|
type: "string",
|
|
367
717
|
required: true
|
|
368
718
|
},
|
|
719
|
+
/** Last-updated timestamp mirrored from the canonical Membership row. */
|
|
369
720
|
lastUpdated: {
|
|
370
721
|
type: "string",
|
|
371
722
|
required: true
|
|
372
723
|
},
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
724
|
+
/**
|
|
725
|
+
* Denormalized Tenant display name — required to compose pattern-#3
|
|
726
|
+
* SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
|
|
727
|
+
* schema because pre-TR-024 rows may not carry a display name; the
|
|
728
|
+
* operations layer falls back gracefully when missing.
|
|
729
|
+
*/
|
|
730
|
+
denormalizedTenantName: {
|
|
731
|
+
type: "string",
|
|
378
732
|
required: false
|
|
379
733
|
},
|
|
380
|
-
|
|
734
|
+
/**
|
|
735
|
+
* Denormalized User display name — mirrored from the canonical
|
|
736
|
+
* Membership row per TR-024 rule 3 (canonical-record symmetry).
|
|
737
|
+
* Carried on the projection so consumers can render the user's
|
|
738
|
+
* display name without a hop to the User record.
|
|
739
|
+
*/
|
|
740
|
+
denormalizedUserName: {
|
|
381
741
|
type: "string",
|
|
382
742
|
required: false
|
|
383
743
|
},
|
|
384
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Denormalized Workspace display name — required to compose
|
|
746
|
+
* pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
|
|
747
|
+
* Optional on the schema (TR-024 § Open Item #4 defers a formal
|
|
748
|
+
* Workspace-rename cascade); the operations layer falls back to a
|
|
749
|
+
* sentinel when missing so the SK still has a valid shape.
|
|
750
|
+
*/
|
|
751
|
+
denormalizedWorkspaceName: {
|
|
385
752
|
type: "string",
|
|
386
753
|
required: false
|
|
387
754
|
}
|
|
388
755
|
},
|
|
389
756
|
indexes: {
|
|
390
|
-
/**
|
|
757
|
+
/**
|
|
758
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied.
|
|
759
|
+
* Both pattern #3 and pattern #4 use this same index — the SK string
|
|
760
|
+
* encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
|
|
761
|
+
* `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
|
|
762
|
+
* SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
|
|
763
|
+
*/
|
|
764
|
+
record: {
|
|
765
|
+
pk: {
|
|
766
|
+
field: "PK",
|
|
767
|
+
composite: ["userId"],
|
|
768
|
+
template: "USER#ID#${userId}"
|
|
769
|
+
},
|
|
770
|
+
sk: {
|
|
771
|
+
field: "SK",
|
|
772
|
+
casing: "none",
|
|
773
|
+
composite: ["sk"],
|
|
774
|
+
template: "${sk}"
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
|
|
781
|
+
var import_electrodb6 = require("electrodb");
|
|
782
|
+
var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
|
|
783
|
+
model: {
|
|
784
|
+
entity: "membershipWorkspaceProjection",
|
|
785
|
+
service: "control",
|
|
786
|
+
version: "01"
|
|
787
|
+
},
|
|
788
|
+
attributes: {
|
|
789
|
+
/**
|
|
790
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
791
|
+
* of the base-table PK. Always required — the workspace partition
|
|
792
|
+
* is tenant-scoped per ADR-011.
|
|
793
|
+
*/
|
|
794
|
+
tenantId: {
|
|
795
|
+
type: "string",
|
|
796
|
+
required: true
|
|
797
|
+
},
|
|
798
|
+
/**
|
|
799
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
800
|
+
* segment of the base-table PK
|
|
801
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
802
|
+
* the projection has no meaning outside a workspace partition.
|
|
803
|
+
*/
|
|
804
|
+
workspaceId: {
|
|
805
|
+
type: "string",
|
|
806
|
+
required: true
|
|
807
|
+
},
|
|
808
|
+
/**
|
|
809
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
810
|
+
* writer via `buildMembershipWorkspaceProjectionSk`. The entity
|
|
811
|
+
* stores the value verbatim so the SK grammar (pattern #2) is
|
|
812
|
+
* owned by the operations layer, not duplicated here.
|
|
813
|
+
*/
|
|
814
|
+
sk: {
|
|
815
|
+
type: "string",
|
|
816
|
+
required: true
|
|
817
|
+
},
|
|
818
|
+
/**
|
|
819
|
+
* User the membership links. Stored as a discriminating field so
|
|
820
|
+
* consumers can hydrate the canonical User row via
|
|
821
|
+
* `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
|
|
822
|
+
* projection's `summary` is insufficient.
|
|
823
|
+
*/
|
|
824
|
+
userId: {
|
|
825
|
+
type: "string",
|
|
826
|
+
required: true
|
|
827
|
+
},
|
|
828
|
+
/**
|
|
829
|
+
* Membership canonical-record id. Stored as a discriminating field
|
|
830
|
+
* so consumers can hydrate the canonical row via
|
|
831
|
+
* `MembershipEntity.get({ tenantId, id: membershipId })` when the
|
|
832
|
+
* projection's `summary` is insufficient.
|
|
833
|
+
*/
|
|
834
|
+
membershipId: {
|
|
835
|
+
type: "string",
|
|
836
|
+
required: true
|
|
837
|
+
},
|
|
838
|
+
/**
|
|
839
|
+
* Summary projection (key display fields as JSON string: id,
|
|
840
|
+
* displayName, status) — mirrored from the canonical Membership row
|
|
841
|
+
* so workspace-partition queries do not need a BatchGet hop.
|
|
842
|
+
*/
|
|
843
|
+
summary: {
|
|
844
|
+
type: "string",
|
|
845
|
+
required: true
|
|
846
|
+
},
|
|
847
|
+
/** Version id mirrored from the canonical Membership row. */
|
|
848
|
+
vid: {
|
|
849
|
+
type: "string",
|
|
850
|
+
required: true
|
|
851
|
+
},
|
|
852
|
+
/** Last-updated timestamp mirrored from the canonical Membership row. */
|
|
853
|
+
lastUpdated: {
|
|
854
|
+
type: "string",
|
|
855
|
+
required: true
|
|
856
|
+
},
|
|
857
|
+
/**
|
|
858
|
+
* Denormalized User display name — required to compose the
|
|
859
|
+
* pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
|
|
860
|
+
* the schema because pre-TR-024 rows may not carry a display name;
|
|
861
|
+
* the operations layer falls back to a sentinel when missing so
|
|
862
|
+
* the SK still has a valid shape. The TR-023 rename-cascade
|
|
863
|
+
* pipeline rewrites the SK on a User rename.
|
|
864
|
+
*/
|
|
865
|
+
denormalizedUserName: {
|
|
866
|
+
type: "string",
|
|
867
|
+
required: false
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
indexes: {
|
|
871
|
+
/**
|
|
872
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
873
|
+
* SK = operation-supplied. Pattern #2 uses this index — the SK
|
|
874
|
+
* encodes the entity-type prefix (`MEMBERSHIP#…`) so a
|
|
875
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
|
|
876
|
+
* returns every member projection for the workspace in normalized-
|
|
877
|
+
* user-name sort order.
|
|
878
|
+
*/
|
|
879
|
+
record: {
|
|
880
|
+
pk: {
|
|
881
|
+
field: "PK",
|
|
882
|
+
composite: ["tenantId", "workspaceId"],
|
|
883
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
884
|
+
},
|
|
885
|
+
sk: {
|
|
886
|
+
field: "SK",
|
|
887
|
+
casing: "none",
|
|
888
|
+
composite: ["sk"],
|
|
889
|
+
template: "${sk}"
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// src/data/dynamo/entities/control/role-entity.ts
|
|
896
|
+
var import_electrodb7 = require("electrodb");
|
|
897
|
+
var RoleEntity = new import_electrodb7.Entity({
|
|
898
|
+
model: {
|
|
899
|
+
entity: "role",
|
|
900
|
+
service: "control",
|
|
901
|
+
version: "01"
|
|
902
|
+
},
|
|
903
|
+
attributes: {
|
|
904
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
905
|
+
sk: {
|
|
906
|
+
type: "string",
|
|
907
|
+
required: true,
|
|
908
|
+
default: "CURRENT"
|
|
909
|
+
},
|
|
910
|
+
/** FHIR Resource.id; role id. */
|
|
911
|
+
id: {
|
|
912
|
+
type: "string",
|
|
913
|
+
required: true
|
|
914
|
+
},
|
|
915
|
+
/** Full Role resource serialized as JSON string. */
|
|
916
|
+
resource: {
|
|
917
|
+
type: "string",
|
|
918
|
+
required: true
|
|
919
|
+
},
|
|
920
|
+
/**
|
|
921
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
922
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
923
|
+
*/
|
|
924
|
+
summary: {
|
|
925
|
+
type: "string",
|
|
926
|
+
required: true
|
|
927
|
+
},
|
|
928
|
+
/** Version id (e.g. ULID). */
|
|
929
|
+
vid: {
|
|
930
|
+
type: "string",
|
|
931
|
+
required: true
|
|
932
|
+
},
|
|
933
|
+
lastUpdated: {
|
|
934
|
+
type: "string",
|
|
935
|
+
required: true
|
|
936
|
+
},
|
|
937
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
938
|
+
/** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
|
|
939
|
+
gsi1sk: gsi1skAttribute,
|
|
940
|
+
deleted: {
|
|
941
|
+
type: "boolean",
|
|
942
|
+
required: false
|
|
943
|
+
},
|
|
944
|
+
bundleId: {
|
|
945
|
+
type: "string",
|
|
946
|
+
required: false
|
|
947
|
+
},
|
|
948
|
+
msgId: {
|
|
949
|
+
type: "string",
|
|
950
|
+
required: false
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
indexes: {
|
|
954
|
+
/** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
955
|
+
record: {
|
|
956
|
+
pk: {
|
|
957
|
+
field: "PK",
|
|
958
|
+
composite: ["id"],
|
|
959
|
+
template: "ROLE#ID#${id}"
|
|
960
|
+
},
|
|
961
|
+
sk: {
|
|
962
|
+
field: "SK",
|
|
963
|
+
composite: ["sk"],
|
|
964
|
+
template: "${sk}"
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
/**
|
|
968
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
|
|
969
|
+
* Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
|
|
970
|
+
* SK is derived via `gsi1skAttribute` — uses the resource's natural label when
|
|
971
|
+
* extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
|
|
972
|
+
* normalized label and ISO-8601 `T`/`Z`.
|
|
973
|
+
*/
|
|
974
|
+
gsi1: {
|
|
975
|
+
index: "GSI1",
|
|
976
|
+
pk: {
|
|
977
|
+
field: "GSI1PK",
|
|
978
|
+
composite: ["gsi1Shard"],
|
|
979
|
+
template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
|
|
980
|
+
},
|
|
981
|
+
sk: {
|
|
982
|
+
field: "GSI1SK",
|
|
983
|
+
casing: "none",
|
|
984
|
+
composite: ["gsi1sk"],
|
|
985
|
+
template: "${gsi1sk}"
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// src/data/dynamo/entities/control/roleassignment-entity.ts
|
|
992
|
+
var import_electrodb8 = require("electrodb");
|
|
993
|
+
var RoleAssignmentEntity = new import_electrodb8.Entity({
|
|
994
|
+
model: {
|
|
995
|
+
entity: "roleassignment",
|
|
996
|
+
service: "control",
|
|
997
|
+
version: "01"
|
|
998
|
+
},
|
|
999
|
+
attributes: {
|
|
1000
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
1001
|
+
sk: {
|
|
1002
|
+
type: "string",
|
|
1003
|
+
required: true,
|
|
1004
|
+
default: "CURRENT"
|
|
1005
|
+
},
|
|
1006
|
+
/** Tenant in which the role assignment applies (required). */
|
|
1007
|
+
tenantId: {
|
|
1008
|
+
type: "string",
|
|
1009
|
+
required: true
|
|
1010
|
+
},
|
|
1011
|
+
/** FHIR Resource.id; role assignment id. */
|
|
1012
|
+
id: {
|
|
1013
|
+
type: "string",
|
|
1014
|
+
required: true
|
|
1015
|
+
},
|
|
1016
|
+
/** Full RoleAssignment resource serialized as JSON string. */
|
|
1017
|
+
resource: {
|
|
1018
|
+
type: "string",
|
|
1019
|
+
required: true
|
|
1020
|
+
},
|
|
1021
|
+
/**
|
|
1022
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
1023
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
1024
|
+
*/
|
|
1025
|
+
summary: {
|
|
1026
|
+
type: "string",
|
|
1027
|
+
required: true
|
|
1028
|
+
},
|
|
1029
|
+
/** Version id (e.g. ULID). */
|
|
1030
|
+
vid: {
|
|
1031
|
+
type: "string",
|
|
1032
|
+
required: true
|
|
1033
|
+
},
|
|
1034
|
+
lastUpdated: {
|
|
1035
|
+
type: "string",
|
|
1036
|
+
required: true
|
|
1037
|
+
},
|
|
1038
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
1039
|
+
/**
|
|
1040
|
+
* Derived GSI1 sort key — discriminator-first
|
|
1041
|
+
* `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
|
|
1042
|
+
* GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
|
|
1043
|
+
* to enumerate every user assigned to a given role, sorted by user
|
|
1044
|
+
* name. Falls back to `<lastUpdated>#<id>` when either component is
|
|
1045
|
+
* missing.
|
|
1046
|
+
*/
|
|
1047
|
+
gsi1sk: roleAssignmentGsi1skAttribute,
|
|
1048
|
+
deleted: {
|
|
1049
|
+
type: "boolean",
|
|
1050
|
+
required: false
|
|
1051
|
+
},
|
|
1052
|
+
bundleId: {
|
|
1053
|
+
type: "string",
|
|
1054
|
+
required: false
|
|
1055
|
+
},
|
|
1056
|
+
msgId: {
|
|
1057
|
+
type: "string",
|
|
1058
|
+
required: false
|
|
1059
|
+
},
|
|
1060
|
+
/**
|
|
1061
|
+
* Denormalized display name of the linked Tenant, captured at row
|
|
1062
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1063
|
+
* adjacency-list user-projection SK (pattern #5 —
|
|
1064
|
+
* `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
|
|
1065
|
+
* can be composed from a top-level field instead of digging into the
|
|
1066
|
+
* `resource` JSON. Optional on the schema so pre-TR-024 rows do not
|
|
1067
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
1068
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
1069
|
+
* source = canonical Tenant.displayName).
|
|
1070
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1071
|
+
*/
|
|
1072
|
+
denormalizedTenantName: {
|
|
1073
|
+
type: "string",
|
|
1074
|
+
required: false
|
|
1075
|
+
},
|
|
1076
|
+
/**
|
|
1077
|
+
* Denormalized display name of the linked User, captured at row
|
|
1078
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1079
|
+
* adjacency-list canonical-record GSI1SK (pattern #8 —
|
|
1080
|
+
* `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
|
|
1081
|
+
* (pattern #9) can be composed from a top-level field. Optional on
|
|
1082
|
+
* the schema so pre-TR-024 rows do not break; the operations-layer
|
|
1083
|
+
* multi-write helper (#1010) makes the field load-bearing at write
|
|
1084
|
+
* time per TR-024 rule 2 (write-time source = canonical
|
|
1085
|
+
* User.displayName).
|
|
1086
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1087
|
+
*/
|
|
1088
|
+
denormalizedUserName: {
|
|
1089
|
+
type: "string",
|
|
1090
|
+
required: false
|
|
1091
|
+
},
|
|
1092
|
+
/**
|
|
1093
|
+
* Denormalized display name of the linked Role, captured at row
|
|
1094
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1095
|
+
* adjacency-list user-projection SK (pattern #5 —
|
|
1096
|
+
* `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
|
|
1097
|
+
* a top-level field. Optional on the schema so pre-TR-024 rows do not
|
|
1098
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
1099
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
1100
|
+
* source = canonical Role.displayName).
|
|
1101
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1102
|
+
*/
|
|
1103
|
+
denormalizedRoleName: {
|
|
1104
|
+
type: "string",
|
|
1105
|
+
required: false
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
indexes: {
|
|
1109
|
+
/** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
1110
|
+
record: {
|
|
1111
|
+
pk: {
|
|
1112
|
+
field: "PK",
|
|
1113
|
+
composite: ["tenantId", "id"],
|
|
1114
|
+
template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
|
|
1115
|
+
},
|
|
1116
|
+
sk: {
|
|
1117
|
+
field: "SK",
|
|
1118
|
+
composite: ["sk"],
|
|
1119
|
+
template: "${sk}"
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
/**
|
|
1123
|
+
* GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
|
|
1124
|
+
* four shards. Tenant-scoped only, so `WID#-` is a sentinel.
|
|
1125
|
+
* SK is derived via `roleAssignmentGsi1skAttribute` — composes the
|
|
1126
|
+
* discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
|
|
1127
|
+
* ADR-018 pattern #8 (users with a specific role in a tenant, sorted
|
|
1128
|
+
* by user name); falls back to `<lastUpdated>#<id>` when either
|
|
1129
|
+
* component is missing. `casing: "none"` preserves the normalized
|
|
1130
|
+
* label and ISO-8601 `T`/`Z`.
|
|
1131
|
+
*/
|
|
1132
|
+
gsi1: {
|
|
1133
|
+
index: "GSI1",
|
|
1134
|
+
pk: {
|
|
1135
|
+
field: "GSI1PK",
|
|
1136
|
+
composite: ["tenantId", "gsi1Shard"],
|
|
1137
|
+
template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
|
|
1138
|
+
},
|
|
1139
|
+
sk: {
|
|
1140
|
+
field: "GSI1SK",
|
|
1141
|
+
casing: "none",
|
|
1142
|
+
composite: ["gsi1sk"],
|
|
1143
|
+
template: "${gsi1sk}"
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
|
|
1150
|
+
var import_electrodb9 = require("electrodb");
|
|
1151
|
+
var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
|
|
1152
|
+
model: {
|
|
1153
|
+
entity: "roleAssignmentUserProjection",
|
|
1154
|
+
service: "control",
|
|
1155
|
+
version: "01"
|
|
1156
|
+
},
|
|
1157
|
+
attributes: {
|
|
1158
|
+
/**
|
|
1159
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
1160
|
+
* base-table PK. Always required — the projection has no meaning
|
|
1161
|
+
* outside a user partition.
|
|
1162
|
+
*/
|
|
1163
|
+
userId: {
|
|
1164
|
+
type: "string",
|
|
1165
|
+
required: true
|
|
1166
|
+
},
|
|
1167
|
+
/**
|
|
1168
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
1169
|
+
* writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
|
|
1170
|
+
* entity stores the value verbatim so the SK grammar (tenant-lane
|
|
1171
|
+
* vs workspace-lane) is owned by the operations layer, not
|
|
1172
|
+
* duplicated here.
|
|
1173
|
+
*/
|
|
1174
|
+
sk: {
|
|
1175
|
+
type: "string",
|
|
1176
|
+
required: true
|
|
1177
|
+
},
|
|
1178
|
+
/** Tenant in which the role assignment applies. Always required. */
|
|
1179
|
+
tenantId: {
|
|
1180
|
+
type: "string",
|
|
1181
|
+
required: true
|
|
1182
|
+
},
|
|
1183
|
+
/**
|
|
1184
|
+
* Workspace the role assignment scopes to. Present iff the
|
|
1185
|
+
* projection row is the workspace-level sub-lane; absent for
|
|
1186
|
+
* tenant-level sub-lane rows.
|
|
1187
|
+
*/
|
|
1188
|
+
workspaceId: {
|
|
1189
|
+
type: "string",
|
|
1190
|
+
required: false
|
|
1191
|
+
},
|
|
1192
|
+
/**
|
|
1193
|
+
* Role the assignment grants. Stored as a discriminating field so
|
|
1194
|
+
* `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
|
|
1195
|
+
* results carry the role id without a hop to the canonical row.
|
|
1196
|
+
*/
|
|
1197
|
+
roleId: {
|
|
1198
|
+
type: "string",
|
|
1199
|
+
required: true
|
|
1200
|
+
},
|
|
1201
|
+
/**
|
|
1202
|
+
* RoleAssignment canonical-record id. Stored as a discriminating
|
|
1203
|
+
* field so consumers can hydrate the canonical row via
|
|
1204
|
+
* `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
|
|
1205
|
+
* when the projection's `summary` is insufficient.
|
|
1206
|
+
*/
|
|
1207
|
+
roleAssignmentId: {
|
|
1208
|
+
type: "string",
|
|
1209
|
+
required: true
|
|
1210
|
+
},
|
|
1211
|
+
/**
|
|
1212
|
+
* Summary projection (key display fields as JSON string: id,
|
|
1213
|
+
* displayName, status) — mirrored from the canonical RoleAssignment
|
|
1214
|
+
* row so user-partition queries do not need a BatchGet hop.
|
|
1215
|
+
*/
|
|
1216
|
+
summary: {
|
|
1217
|
+
type: "string",
|
|
1218
|
+
required: true
|
|
1219
|
+
},
|
|
1220
|
+
/** Version id mirrored from the canonical RoleAssignment row. */
|
|
1221
|
+
vid: {
|
|
1222
|
+
type: "string",
|
|
1223
|
+
required: true
|
|
1224
|
+
},
|
|
1225
|
+
/** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
|
|
1226
|
+
lastUpdated: {
|
|
1227
|
+
type: "string",
|
|
1228
|
+
required: true
|
|
1229
|
+
},
|
|
1230
|
+
/**
|
|
1231
|
+
* Denormalized Tenant display name — mirrored from the canonical
|
|
1232
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
1233
|
+
* Optional on the schema because pre-TR-024 rows may not carry a
|
|
1234
|
+
* display name; the operations layer falls back gracefully when
|
|
1235
|
+
* missing.
|
|
1236
|
+
*/
|
|
1237
|
+
denormalizedTenantName: {
|
|
1238
|
+
type: "string",
|
|
1239
|
+
required: false
|
|
1240
|
+
},
|
|
1241
|
+
/**
|
|
1242
|
+
* Denormalized User display name — mirrored from the canonical
|
|
1243
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
1244
|
+
* Carried on the projection so consumers can render the user's
|
|
1245
|
+
* display name without a hop to the User record.
|
|
1246
|
+
*/
|
|
1247
|
+
denormalizedUserName: {
|
|
1248
|
+
type: "string",
|
|
1249
|
+
required: false
|
|
1250
|
+
},
|
|
1251
|
+
/**
|
|
1252
|
+
* Denormalized Role display name — required to compose the SK's
|
|
1253
|
+
* `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
|
|
1254
|
+
* rows fall back to a sentinel) but expected to be present at write
|
|
1255
|
+
* time per TR-024 rule 2 (write-time source =
|
|
1256
|
+
* canonical Role.displayName).
|
|
1257
|
+
*/
|
|
1258
|
+
denormalizedRoleName: {
|
|
1259
|
+
type: "string",
|
|
1260
|
+
required: false
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
indexes: {
|
|
1264
|
+
/**
|
|
1265
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
|
|
1266
|
+
* sub-lanes (tenant-level and workspace-level) use this same index —
|
|
1267
|
+
* the SK string encodes the lane discriminator
|
|
1268
|
+
* (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
|
|
1269
|
+
* single `Query(PK = USER#ID#<userId>, SK begins_with
|
|
1270
|
+
* 'ROLEASSIGNMENT#')` returns both lanes interleaved.
|
|
1271
|
+
*/
|
|
391
1272
|
record: {
|
|
392
1273
|
pk: {
|
|
393
1274
|
field: "PK",
|
|
394
|
-
composite: ["
|
|
395
|
-
template: "
|
|
1275
|
+
composite: ["userId"],
|
|
1276
|
+
template: "USER#ID#${userId}"
|
|
396
1277
|
},
|
|
397
1278
|
sk: {
|
|
398
1279
|
field: "SK",
|
|
1280
|
+
casing: "none",
|
|
399
1281
|
composite: ["sk"],
|
|
400
1282
|
template: "${sk}"
|
|
401
1283
|
}
|
|
402
|
-
},
|
|
403
|
-
/**
|
|
404
|
-
* GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
|
|
405
|
-
* Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
|
|
406
|
-
* SK is derived via `gsi1skAttribute` — uses the resource's natural label when
|
|
407
|
-
* extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
|
|
408
|
-
* normalized label and ISO-8601 `T`/`Z`.
|
|
409
|
-
*/
|
|
410
|
-
gsi1: {
|
|
411
|
-
index: "GSI1",
|
|
412
|
-
pk: {
|
|
413
|
-
field: "GSI1PK",
|
|
414
|
-
composite: ["gsi1Shard"],
|
|
415
|
-
template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
|
|
416
|
-
},
|
|
417
|
-
sk: {
|
|
418
|
-
field: "GSI1SK",
|
|
419
|
-
casing: "none",
|
|
420
|
-
composite: ["gsi1sk"],
|
|
421
|
-
template: "${gsi1sk}"
|
|
422
|
-
}
|
|
423
1284
|
}
|
|
424
1285
|
}
|
|
425
1286
|
});
|
|
426
1287
|
|
|
427
|
-
// src/data/dynamo/entities/control/roleassignment-entity.ts
|
|
428
|
-
var
|
|
429
|
-
var
|
|
1288
|
+
// src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
|
|
1289
|
+
var import_electrodb10 = require("electrodb");
|
|
1290
|
+
var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
|
|
430
1291
|
model: {
|
|
431
|
-
entity: "
|
|
1292
|
+
entity: "roleAssignmentWorkspaceProjection",
|
|
432
1293
|
service: "control",
|
|
433
1294
|
version: "01"
|
|
434
1295
|
},
|
|
435
1296
|
attributes: {
|
|
436
|
-
/**
|
|
1297
|
+
/**
|
|
1298
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
1299
|
+
* of the base-table PK. Always required — the workspace partition
|
|
1300
|
+
* is tenant-scoped per ADR-011.
|
|
1301
|
+
*/
|
|
1302
|
+
tenantId: {
|
|
1303
|
+
type: "string",
|
|
1304
|
+
required: true
|
|
1305
|
+
},
|
|
1306
|
+
/**
|
|
1307
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
1308
|
+
* segment of the base-table PK
|
|
1309
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
1310
|
+
* the projection has no meaning outside a workspace partition.
|
|
1311
|
+
*/
|
|
1312
|
+
workspaceId: {
|
|
1313
|
+
type: "string",
|
|
1314
|
+
required: true
|
|
1315
|
+
},
|
|
1316
|
+
/**
|
|
1317
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
1318
|
+
* writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
|
|
1319
|
+
* stores the value verbatim so the SK grammar (pattern #9) is
|
|
1320
|
+
* owned by the operations layer, not duplicated here.
|
|
1321
|
+
*/
|
|
437
1322
|
sk: {
|
|
438
1323
|
type: "string",
|
|
439
|
-
required: true
|
|
440
|
-
default: "CURRENT"
|
|
1324
|
+
required: true
|
|
441
1325
|
},
|
|
442
|
-
/**
|
|
443
|
-
|
|
1326
|
+
/**
|
|
1327
|
+
* User the role assignment grants the role to. Stored as a
|
|
1328
|
+
* discriminating field so consumers can hydrate the canonical User
|
|
1329
|
+
* row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
|
|
1330
|
+
* projection's `summary` is insufficient.
|
|
1331
|
+
*/
|
|
1332
|
+
userId: {
|
|
444
1333
|
type: "string",
|
|
445
1334
|
required: true
|
|
446
1335
|
},
|
|
447
|
-
/**
|
|
448
|
-
|
|
1336
|
+
/**
|
|
1337
|
+
* Role the assignment grants. Stored as a discriminating field —
|
|
1338
|
+
* also rendered into the SK as the discriminator-first segment so
|
|
1339
|
+
* `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
|
|
1340
|
+
*/
|
|
1341
|
+
roleId: {
|
|
449
1342
|
type: "string",
|
|
450
1343
|
required: true
|
|
451
1344
|
},
|
|
452
|
-
/**
|
|
453
|
-
|
|
1345
|
+
/**
|
|
1346
|
+
* RoleAssignment canonical-record id. Stored as a discriminating
|
|
1347
|
+
* field so consumers can hydrate the canonical row via
|
|
1348
|
+
* `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
|
|
1349
|
+
* when the projection's `summary` is insufficient.
|
|
1350
|
+
*/
|
|
1351
|
+
roleAssignmentId: {
|
|
454
1352
|
type: "string",
|
|
455
1353
|
required: true
|
|
456
1354
|
},
|
|
457
1355
|
/**
|
|
458
|
-
* Summary projection (key display fields as JSON string: id,
|
|
459
|
-
*
|
|
1356
|
+
* Summary projection (key display fields as JSON string: id,
|
|
1357
|
+
* displayName, status) — mirrored from the canonical RoleAssignment
|
|
1358
|
+
* row so workspace-partition queries do not need a BatchGet hop.
|
|
460
1359
|
*/
|
|
461
1360
|
summary: {
|
|
462
1361
|
type: "string",
|
|
463
1362
|
required: true
|
|
464
1363
|
},
|
|
465
|
-
/** Version id
|
|
1364
|
+
/** Version id mirrored from the canonical RoleAssignment row. */
|
|
466
1365
|
vid: {
|
|
467
1366
|
type: "string",
|
|
468
1367
|
required: true
|
|
469
1368
|
},
|
|
1369
|
+
/** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
|
|
470
1370
|
lastUpdated: {
|
|
471
1371
|
type: "string",
|
|
472
1372
|
required: true
|
|
473
1373
|
},
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
1374
|
+
/**
|
|
1375
|
+
* Denormalized User display name — required to compose the
|
|
1376
|
+
* pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
|
|
1377
|
+
* Optional on the schema because pre-TR-024 rows may not carry a
|
|
1378
|
+
* display name; the operations layer falls back to a sentinel when
|
|
1379
|
+
* missing so the SK still has a valid shape. The TR-023 rename-
|
|
1380
|
+
* cascade pipeline rewrites the SK on a User rename.
|
|
1381
|
+
*/
|
|
1382
|
+
denormalizedUserName: {
|
|
482
1383
|
type: "string",
|
|
483
1384
|
required: false
|
|
484
1385
|
},
|
|
485
|
-
|
|
1386
|
+
/**
|
|
1387
|
+
* Denormalized Role display name — mirrored from the canonical
|
|
1388
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
1389
|
+
* Carried on the projection so consumers can render the role's
|
|
1390
|
+
* display name without a hop to the Role record. Not part of the
|
|
1391
|
+
* SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
|
|
1392
|
+
* a Role rename does NOT rewrite this SK.
|
|
1393
|
+
*/
|
|
1394
|
+
denormalizedRoleName: {
|
|
486
1395
|
type: "string",
|
|
487
1396
|
required: false
|
|
488
1397
|
}
|
|
489
1398
|
},
|
|
490
1399
|
indexes: {
|
|
491
|
-
/**
|
|
1400
|
+
/**
|
|
1401
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
1402
|
+
* SK = operation-supplied. Pattern #9 uses this index — the SK
|
|
1403
|
+
* encodes the entity-type prefix and discriminator-first roleId
|
|
1404
|
+
* (`ROLEASSIGNMENT#<roleId>#…`) so
|
|
1405
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
|
|
1406
|
+
* returns every user-assignment for that role in the workspace, sorted
|
|
1407
|
+
* by normalized user name.
|
|
1408
|
+
*/
|
|
492
1409
|
record: {
|
|
493
1410
|
pk: {
|
|
494
1411
|
field: "PK",
|
|
495
|
-
composite: ["tenantId", "
|
|
496
|
-
template: "TID#${tenantId}#
|
|
1412
|
+
composite: ["tenantId", "workspaceId"],
|
|
1413
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
497
1414
|
},
|
|
498
1415
|
sk: {
|
|
499
1416
|
field: "SK",
|
|
1417
|
+
casing: "none",
|
|
500
1418
|
composite: ["sk"],
|
|
501
1419
|
template: "${sk}"
|
|
502
1420
|
}
|
|
503
|
-
},
|
|
504
|
-
/**
|
|
505
|
-
* GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
|
|
506
|
-
* four shards. Tenant-scoped only, so `WID#-` is a sentinel.
|
|
507
|
-
* SK is derived via `gsi1skAttribute` — uses the resource's natural label when
|
|
508
|
-
* extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
|
|
509
|
-
* normalized label and ISO-8601 `T`/`Z`.
|
|
510
|
-
*/
|
|
511
|
-
gsi1: {
|
|
512
|
-
index: "GSI1",
|
|
513
|
-
pk: {
|
|
514
|
-
field: "GSI1PK",
|
|
515
|
-
composite: ["tenantId", "gsi1Shard"],
|
|
516
|
-
template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
|
|
517
|
-
},
|
|
518
|
-
sk: {
|
|
519
|
-
field: "GSI1SK",
|
|
520
|
-
casing: "none",
|
|
521
|
-
composite: ["gsi1sk"],
|
|
522
|
-
template: "${gsi1sk}"
|
|
523
|
-
}
|
|
524
1421
|
}
|
|
525
1422
|
}
|
|
526
1423
|
});
|
|
527
1424
|
|
|
528
1425
|
// src/data/dynamo/entities/control/tenant-entity.ts
|
|
529
|
-
var
|
|
530
|
-
var TenantEntity = new
|
|
1426
|
+
var import_electrodb11 = require("electrodb");
|
|
1427
|
+
var TenantEntity = new import_electrodb11.Entity({
|
|
531
1428
|
model: {
|
|
532
1429
|
entity: "tenant",
|
|
533
1430
|
service: "control",
|
|
@@ -627,8 +1524,8 @@ var TenantEntity = new import_electrodb5.Entity({
|
|
|
627
1524
|
});
|
|
628
1525
|
|
|
629
1526
|
// src/data/dynamo/entities/control/user-entity.ts
|
|
630
|
-
var
|
|
631
|
-
var UserEntity = new
|
|
1527
|
+
var import_electrodb12 = require("electrodb");
|
|
1528
|
+
var UserEntity = new import_electrodb12.Entity({
|
|
632
1529
|
model: {
|
|
633
1530
|
entity: "user",
|
|
634
1531
|
service: "control",
|
|
@@ -683,6 +1580,28 @@ var UserEntity = new import_electrodb6.Entity({
|
|
|
683
1580
|
type: "boolean",
|
|
684
1581
|
required: false
|
|
685
1582
|
},
|
|
1583
|
+
/**
|
|
1584
|
+
* TR-022 / ADR-018 lifecycle state for the cascade pipeline.
|
|
1585
|
+
*
|
|
1586
|
+
* - `active` (or undefined) — normal, readable state.
|
|
1587
|
+
* - `deleting` — intermediate state set synchronously by the
|
|
1588
|
+
* hard-delete API entry point. The owning-delete cascade state
|
|
1589
|
+
* machine fans out from this transition (DynamoDB stream →
|
|
1590
|
+
* `control-plane.owning-delete.v1` → Step Functions). Readers MUST
|
|
1591
|
+
* short-circuit on `deleting` so partial cascades stay invisible.
|
|
1592
|
+
* - `deleted-failed` — terminal failure state set by the cascade
|
|
1593
|
+
* finalize Lambda when the cascade run fails irrecoverably.
|
|
1594
|
+
* Operators recover by re-running the cascade or by direct
|
|
1595
|
+
* intervention.
|
|
1596
|
+
*
|
|
1597
|
+
* The cascade finalize step deletes the canonical record conditional
|
|
1598
|
+
* on `lifecycleState = "deleting"`; on replay the conditional check
|
|
1599
|
+
* fails and the finalize step treats that as a no-op success.
|
|
1600
|
+
*/
|
|
1601
|
+
lifecycleState: {
|
|
1602
|
+
type: ["active", "deleting", "deleted-failed"],
|
|
1603
|
+
required: false
|
|
1604
|
+
},
|
|
686
1605
|
bundleId: {
|
|
687
1606
|
type: "string",
|
|
688
1607
|
required: false
|
|
@@ -752,8 +1671,8 @@ var UserEntity = new import_electrodb6.Entity({
|
|
|
752
1671
|
});
|
|
753
1672
|
|
|
754
1673
|
// src/data/dynamo/entities/control/workspace-entity.ts
|
|
755
|
-
var
|
|
756
|
-
var WorkspaceEntity = new
|
|
1674
|
+
var import_electrodb13 = require("electrodb");
|
|
1675
|
+
var WorkspaceEntity = new import_electrodb13.Entity({
|
|
757
1676
|
model: {
|
|
758
1677
|
entity: "workspace",
|
|
759
1678
|
service: "control",
|
|
@@ -805,6 +1724,28 @@ var WorkspaceEntity = new import_electrodb7.Entity({
|
|
|
805
1724
|
type: "boolean",
|
|
806
1725
|
required: false
|
|
807
1726
|
},
|
|
1727
|
+
/**
|
|
1728
|
+
* TR-022 / ADR-018 lifecycle state for the cascade pipeline.
|
|
1729
|
+
*
|
|
1730
|
+
* - `active` (or undefined) — normal, readable state.
|
|
1731
|
+
* - `deleting` — intermediate state set synchronously by the
|
|
1732
|
+
* hard-delete API entry point. The owning-delete cascade state
|
|
1733
|
+
* machine fans out from this transition (DynamoDB stream →
|
|
1734
|
+
* `control-plane.owning-delete.v1` → Step Functions). Readers MUST
|
|
1735
|
+
* short-circuit on `deleting` so partial cascades stay invisible.
|
|
1736
|
+
* - `deleted-failed` — terminal failure state set by the cascade
|
|
1737
|
+
* finalize Lambda when the cascade run fails irrecoverably.
|
|
1738
|
+
* Operators recover by re-running the cascade or by direct
|
|
1739
|
+
* intervention.
|
|
1740
|
+
*
|
|
1741
|
+
* The cascade finalize step deletes the canonical record conditional
|
|
1742
|
+
* on `lifecycleState = "deleting"`; on replay the conditional check
|
|
1743
|
+
* fails and the finalize step treats that as a no-op success.
|
|
1744
|
+
*/
|
|
1745
|
+
lifecycleState: {
|
|
1746
|
+
type: ["active", "deleting", "deleted-failed"],
|
|
1747
|
+
required: false
|
|
1748
|
+
},
|
|
808
1749
|
bundleId: {
|
|
809
1750
|
type: "string",
|
|
810
1751
|
required: false
|
|
@@ -855,33 +1796,127 @@ var WorkspaceEntity = new import_electrodb7.Entity({
|
|
|
855
1796
|
// src/data/dynamo/dynamo-control-service.ts
|
|
856
1797
|
var controlPlaneEntities = {
|
|
857
1798
|
configuration: ConfigurationEntity,
|
|
1799
|
+
configurationUserProjection: ConfigurationUserProjectionEntity,
|
|
1800
|
+
configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
|
|
858
1801
|
membership: MembershipEntity,
|
|
1802
|
+
membershipUserProjection: MembershipUserProjectionEntity,
|
|
1803
|
+
membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
|
|
859
1804
|
role: RoleEntity,
|
|
860
1805
|
roleAssignment: RoleAssignmentEntity,
|
|
1806
|
+
roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
|
|
1807
|
+
roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
|
|
861
1808
|
tenant: TenantEntity,
|
|
862
1809
|
user: UserEntity,
|
|
863
1810
|
workspace: WorkspaceEntity
|
|
864
1811
|
};
|
|
865
|
-
var controlPlaneService = new
|
|
1812
|
+
var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
|
|
866
1813
|
table: defaultTableName,
|
|
867
1814
|
client: dynamoClient
|
|
868
1815
|
});
|
|
869
1816
|
var DynamoControlService = {
|
|
870
|
-
entities: controlPlaneService.entities
|
|
1817
|
+
entities: controlPlaneService.entities,
|
|
1818
|
+
transaction: controlPlaneService.transaction
|
|
871
1819
|
};
|
|
872
1820
|
function getDynamoControlService(tableName) {
|
|
873
1821
|
const resolved = tableName ?? defaultTableName;
|
|
874
|
-
const service = new
|
|
1822
|
+
const service = new import_electrodb14.Service(controlPlaneEntities, {
|
|
875
1823
|
table: resolved,
|
|
876
1824
|
client: dynamoClient
|
|
877
1825
|
});
|
|
878
1826
|
return {
|
|
879
|
-
entities: service.entities
|
|
1827
|
+
entities: service.entities,
|
|
1828
|
+
transaction: service.transaction
|
|
880
1829
|
};
|
|
881
1830
|
}
|
|
882
1831
|
|
|
883
1832
|
// src/data/operations/control/membership/membership-create-operation.ts
|
|
1833
|
+
var import_types4 = require("@openhi/types");
|
|
1834
|
+
|
|
1835
|
+
// src/data/operations/control/membership/membership-user-projection.ts
|
|
884
1836
|
var import_types2 = require("@openhi/types");
|
|
1837
|
+
var MISSING_NAME_SENTINEL = "-";
|
|
1838
|
+
function buildMembershipUserProjectionSkTenantLane(params) {
|
|
1839
|
+
const normalizedTenantName = typeof params.denormalizedTenantName === "string" && params.denormalizedTenantName.length > 0 ? (0, import_types2.normalizeLabel)(params.denormalizedTenantName) : MISSING_NAME_SENTINEL;
|
|
1840
|
+
return `MEMBERSHIP#TENANT#${normalizedTenantName}#TID#${params.tenantId}#${params.membershipId}`;
|
|
1841
|
+
}
|
|
1842
|
+
function buildMembershipUserProjectionSkWorkspaceLane(params) {
|
|
1843
|
+
const normalizedWorkspaceName = typeof params.denormalizedWorkspaceName === "string" && params.denormalizedWorkspaceName.length > 0 ? (0, import_types2.normalizeLabel)(params.denormalizedWorkspaceName) : MISSING_NAME_SENTINEL;
|
|
1844
|
+
return `MEMBERSHIP#WORKSPACE#TID#${params.tenantId}#${normalizedWorkspaceName}#WID#${params.workspaceId}#${params.membershipId}`;
|
|
1845
|
+
}
|
|
1846
|
+
function buildMembershipUserProjectionItem(input) {
|
|
1847
|
+
if (!input.userId || input.userId.length === 0) {
|
|
1848
|
+
return void 0;
|
|
1849
|
+
}
|
|
1850
|
+
const hasWorkspace = typeof input.workspaceId === "string" && input.workspaceId.length > 0;
|
|
1851
|
+
const sk = hasWorkspace ? buildMembershipUserProjectionSkWorkspaceLane({
|
|
1852
|
+
tenantId: input.tenantId,
|
|
1853
|
+
workspaceId: input.workspaceId,
|
|
1854
|
+
membershipId: input.membershipId,
|
|
1855
|
+
denormalizedWorkspaceName: input.denormalizedWorkspaceName
|
|
1856
|
+
}) : buildMembershipUserProjectionSkTenantLane({
|
|
1857
|
+
tenantId: input.tenantId,
|
|
1858
|
+
membershipId: input.membershipId,
|
|
1859
|
+
denormalizedTenantName: input.denormalizedTenantName
|
|
1860
|
+
});
|
|
1861
|
+
return {
|
|
1862
|
+
userId: input.userId,
|
|
1863
|
+
sk,
|
|
1864
|
+
tenantId: input.tenantId,
|
|
1865
|
+
workspaceId: hasWorkspace ? input.workspaceId : void 0,
|
|
1866
|
+
membershipId: input.membershipId,
|
|
1867
|
+
summary: input.summary,
|
|
1868
|
+
vid: input.vid,
|
|
1869
|
+
lastUpdated: input.lastUpdated,
|
|
1870
|
+
denormalizedTenantName: input.denormalizedTenantName,
|
|
1871
|
+
denormalizedUserName: input.denormalizedUserName,
|
|
1872
|
+
denormalizedWorkspaceName: hasWorkspace ? input.denormalizedWorkspaceName : void 0
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function extractReferenceSlug(resource, fieldName) {
|
|
1876
|
+
const field = resource[fieldName];
|
|
1877
|
+
if (!field || typeof field !== "object") {
|
|
1878
|
+
return void 0;
|
|
1879
|
+
}
|
|
1880
|
+
const reference = field.reference;
|
|
1881
|
+
if (typeof reference !== "string" || reference.length === 0) {
|
|
1882
|
+
return void 0;
|
|
1883
|
+
}
|
|
1884
|
+
const slash = reference.lastIndexOf("/");
|
|
1885
|
+
const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
|
|
1886
|
+
return tail.length > 0 ? tail : void 0;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/data/operations/control/membership/membership-workspace-projection.ts
|
|
1890
|
+
var import_types3 = require("@openhi/types");
|
|
1891
|
+
var MISSING_NAME_SENTINEL2 = "-";
|
|
1892
|
+
function buildMembershipWorkspaceProjectionSk(params) {
|
|
1893
|
+
const normalizedUserName = typeof params.denormalizedUserName === "string" && params.denormalizedUserName.length > 0 ? (0, import_types3.normalizeLabel)(params.denormalizedUserName) : MISSING_NAME_SENTINEL2;
|
|
1894
|
+
return `MEMBERSHIP#${normalizedUserName}#USER#${params.userId}#${params.membershipId}`;
|
|
1895
|
+
}
|
|
1896
|
+
function buildMembershipWorkspaceProjectionItem(input) {
|
|
1897
|
+
if (!input.workspaceId || input.workspaceId.length === 0) {
|
|
1898
|
+
return void 0;
|
|
1899
|
+
}
|
|
1900
|
+
if (!input.userId || input.userId.length === 0) {
|
|
1901
|
+
return void 0;
|
|
1902
|
+
}
|
|
1903
|
+
const sk = buildMembershipWorkspaceProjectionSk({
|
|
1904
|
+
userId: input.userId,
|
|
1905
|
+
membershipId: input.membershipId,
|
|
1906
|
+
denormalizedUserName: input.denormalizedUserName
|
|
1907
|
+
});
|
|
1908
|
+
return {
|
|
1909
|
+
tenantId: input.tenantId,
|
|
1910
|
+
workspaceId: input.workspaceId,
|
|
1911
|
+
sk,
|
|
1912
|
+
userId: input.userId,
|
|
1913
|
+
membershipId: input.membershipId,
|
|
1914
|
+
summary: input.summary,
|
|
1915
|
+
vid: input.vid,
|
|
1916
|
+
lastUpdated: input.lastUpdated,
|
|
1917
|
+
denormalizedUserName: input.denormalizedUserName
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
885
1920
|
|
|
886
1921
|
// src/data/errors/domain-errors.ts
|
|
887
1922
|
var DomainError = class extends Error {
|
|
@@ -898,6 +1933,131 @@ var ValidationError = class extends DomainError {
|
|
|
898
1933
|
super(message, "VALIDATION", options);
|
|
899
1934
|
}
|
|
900
1935
|
};
|
|
1936
|
+
var ConflictError = class extends DomainError {
|
|
1937
|
+
constructor(message, options) {
|
|
1938
|
+
super(message, "CONFLICT", options);
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
// src/data/operations/control/denormalized-display-names.ts
|
|
1943
|
+
function extractDenormalizedReferenceDisplay(resource, fieldName) {
|
|
1944
|
+
const field = resource[fieldName];
|
|
1945
|
+
if (!field || typeof field !== "object") {
|
|
1946
|
+
return void 0;
|
|
1947
|
+
}
|
|
1948
|
+
const display = field.display;
|
|
1949
|
+
if (typeof display !== "string") {
|
|
1950
|
+
return void 0;
|
|
1951
|
+
}
|
|
1952
|
+
const trimmed = display.trim();
|
|
1953
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// src/data/operations/control/multi-write-operation.ts
|
|
1957
|
+
var TRANSACT_WRITE_ITEM_LIMIT = 100;
|
|
1958
|
+
async function executeMultiWrite(params) {
|
|
1959
|
+
const { service, triples, token } = params;
|
|
1960
|
+
if (triples.length === 0) {
|
|
1961
|
+
throw new ValidationError(
|
|
1962
|
+
"executeMultiWrite called with zero triples; at least one triple is required"
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
if (triples.length > TRANSACT_WRITE_ITEM_LIMIT) {
|
|
1966
|
+
throw new ValidationError(
|
|
1967
|
+
`executeMultiWrite received ${triples.length} triples; DynamoDB TransactWriteItems is limited to ${TRANSACT_WRITE_ITEM_LIMIT} items per call`,
|
|
1968
|
+
{
|
|
1969
|
+
details: {
|
|
1970
|
+
itemsRequested: triples.length,
|
|
1971
|
+
limit: TRANSACT_WRITE_ITEM_LIMIT
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
for (const [index, triple] of triples.entries()) {
|
|
1977
|
+
if (!triple || typeof triple !== "object") {
|
|
1978
|
+
throw new ValidationError(
|
|
1979
|
+
`executeMultiWrite triple at index ${index} is not an object`
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
if (typeof triple.entity !== "string" || triple.entity.length === 0) {
|
|
1983
|
+
throw new ValidationError(
|
|
1984
|
+
`executeMultiWrite triple at index ${index} is missing a non-empty 'entity' key`
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
if (!isSupportedAction(triple.action)) {
|
|
1988
|
+
throw new ValidationError(
|
|
1989
|
+
`executeMultiWrite triple at index ${index} has unsupported action '${String(
|
|
1990
|
+
triple.action
|
|
1991
|
+
)}'; supported: 'put' | 'create' | 'delete'`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
if (!triple.item || typeof triple.item !== "object") {
|
|
1995
|
+
throw new ValidationError(
|
|
1996
|
+
`executeMultiWrite triple at index ${index} is missing an 'item' payload`
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
let result;
|
|
2001
|
+
try {
|
|
2002
|
+
result = await service.transaction.write(
|
|
2003
|
+
(entities) => triples.map((triple, index) => {
|
|
2004
|
+
const transactEntity = entities[triple.entity];
|
|
2005
|
+
if (transactEntity === void 0) {
|
|
2006
|
+
throw new ValidationError(
|
|
2007
|
+
`executeMultiWrite triple at index ${index} references unknown entity '${triple.entity}'; ensure the service exposes it`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
switch (triple.action) {
|
|
2011
|
+
case "put":
|
|
2012
|
+
return transactEntity.put(triple.item).commit();
|
|
2013
|
+
case "create":
|
|
2014
|
+
return transactEntity.create(triple.item).commit();
|
|
2015
|
+
case "delete":
|
|
2016
|
+
return transactEntity.delete(triple.item).commit();
|
|
2017
|
+
default:
|
|
2018
|
+
throw new ValidationError(
|
|
2019
|
+
`executeMultiWrite triple at index ${index} has unsupported action '${String(
|
|
2020
|
+
triple.action
|
|
2021
|
+
)}'`
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
})
|
|
2025
|
+
).go(token === void 0 ? void 0 : { token });
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
if (err instanceof DomainError) {
|
|
2028
|
+
throw err;
|
|
2029
|
+
}
|
|
2030
|
+
throw new ConflictError(buildCancellationMessage(err), {
|
|
2031
|
+
cause: err,
|
|
2032
|
+
details: extractCancellationReasons(err)
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
if (result.canceled) {
|
|
2036
|
+
throw new ConflictError(
|
|
2037
|
+
"TransactWriteItems was canceled by DynamoDB (check CancellationReasons on the cause for details)",
|
|
2038
|
+
{ details: { canceled: true, data: result.data } }
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
return { itemsWritten: triples.length, canceled: false };
|
|
2042
|
+
}
|
|
2043
|
+
function isSupportedAction(value) {
|
|
2044
|
+
return value === "put" || value === "create" || value === "delete";
|
|
2045
|
+
}
|
|
2046
|
+
function buildCancellationMessage(err) {
|
|
2047
|
+
if (err instanceof Error && err.message) {
|
|
2048
|
+
return `TransactWriteItems failed: ${err.message}`;
|
|
2049
|
+
}
|
|
2050
|
+
return "TransactWriteItems failed (no error message available)";
|
|
2051
|
+
}
|
|
2052
|
+
function extractCancellationReasons(err) {
|
|
2053
|
+
if (err && typeof err === "object") {
|
|
2054
|
+
const cancellationReasons = err.CancellationReasons;
|
|
2055
|
+
if (cancellationReasons !== void 0) {
|
|
2056
|
+
return { CancellationReasons: cancellationReasons };
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return void 0;
|
|
2060
|
+
}
|
|
901
2061
|
|
|
902
2062
|
// src/data/operations/control/membership/membership-create-operation.ts
|
|
903
2063
|
async function createMembershipOperation(params) {
|
|
@@ -910,26 +2070,86 @@ async function createMembershipOperation(params) {
|
|
|
910
2070
|
const resource = { resourceType: "Membership", id, ...parsedResource };
|
|
911
2071
|
let linkedDataIdentityRef;
|
|
912
2072
|
try {
|
|
913
|
-
const ext = (0,
|
|
2073
|
+
const ext = (0, import_types4.assertLinkedDataIdentityCardinality)(
|
|
914
2074
|
resource
|
|
915
2075
|
);
|
|
916
2076
|
linkedDataIdentityRef = ext?.valueReference?.reference;
|
|
917
2077
|
} catch (e) {
|
|
918
|
-
if (e instanceof
|
|
2078
|
+
if (e instanceof import_types4.LinkedDataIdentityCardinalityError) {
|
|
919
2079
|
throw new ValidationError(e.message, { cause: e });
|
|
920
2080
|
}
|
|
921
2081
|
throw e;
|
|
922
2082
|
}
|
|
923
|
-
const
|
|
924
|
-
|
|
2083
|
+
const resourceRecord = resource;
|
|
2084
|
+
const denormalizedTenantName = extractDenormalizedReferenceDisplay(
|
|
2085
|
+
resourceRecord,
|
|
2086
|
+
"tenant"
|
|
2087
|
+
);
|
|
2088
|
+
const denormalizedUserName = extractDenormalizedReferenceDisplay(
|
|
2089
|
+
resourceRecord,
|
|
2090
|
+
"user"
|
|
2091
|
+
);
|
|
2092
|
+
const denormalizedWorkspaceName = extractDenormalizedReferenceDisplay(
|
|
2093
|
+
resourceRecord,
|
|
2094
|
+
"workspace"
|
|
2095
|
+
);
|
|
2096
|
+
const summary = JSON.stringify((0, import_types4.extractSummary)(resource));
|
|
2097
|
+
const userIdFromResource = extractReferenceSlug(resourceRecord, "user");
|
|
2098
|
+
const workspaceIdFromResource = extractReferenceSlug(
|
|
2099
|
+
resourceRecord,
|
|
2100
|
+
"workspace"
|
|
2101
|
+
);
|
|
2102
|
+
const userProjectionItem = userIdFromResource !== void 0 ? buildMembershipUserProjectionItem({
|
|
2103
|
+
tenantId: context.tenantId,
|
|
2104
|
+
userId: userIdFromResource,
|
|
2105
|
+
workspaceId: workspaceIdFromResource,
|
|
2106
|
+
membershipId: id,
|
|
2107
|
+
summary,
|
|
2108
|
+
vid,
|
|
2109
|
+
lastUpdated,
|
|
2110
|
+
denormalizedTenantName,
|
|
2111
|
+
denormalizedUserName,
|
|
2112
|
+
denormalizedWorkspaceName
|
|
2113
|
+
}) : void 0;
|
|
2114
|
+
const workspaceProjectionItem = userIdFromResource !== void 0 && workspaceIdFromResource !== void 0 ? buildMembershipWorkspaceProjectionItem({
|
|
2115
|
+
tenantId: context.tenantId,
|
|
2116
|
+
workspaceId: workspaceIdFromResource,
|
|
2117
|
+
userId: userIdFromResource,
|
|
2118
|
+
membershipId: id,
|
|
2119
|
+
summary,
|
|
2120
|
+
vid,
|
|
2121
|
+
lastUpdated,
|
|
2122
|
+
denormalizedUserName
|
|
2123
|
+
}) : void 0;
|
|
2124
|
+
const canonicalItem = {
|
|
925
2125
|
tenantId: context.tenantId,
|
|
926
2126
|
id,
|
|
927
2127
|
resource: JSON.stringify(resource),
|
|
928
2128
|
summary,
|
|
929
2129
|
vid,
|
|
930
2130
|
lastUpdated,
|
|
931
|
-
linkedDataIdentityRef
|
|
932
|
-
|
|
2131
|
+
linkedDataIdentityRef,
|
|
2132
|
+
denormalizedTenantName,
|
|
2133
|
+
denormalizedUserName
|
|
2134
|
+
};
|
|
2135
|
+
const triples = [
|
|
2136
|
+
{ entity: "membership", action: "put", item: canonicalItem }
|
|
2137
|
+
];
|
|
2138
|
+
if (userProjectionItem) {
|
|
2139
|
+
triples.push({
|
|
2140
|
+
entity: "membershipUserProjection",
|
|
2141
|
+
action: "put",
|
|
2142
|
+
item: userProjectionItem
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
if (workspaceProjectionItem) {
|
|
2146
|
+
triples.push({
|
|
2147
|
+
entity: "membershipWorkspaceProjection",
|
|
2148
|
+
action: "put",
|
|
2149
|
+
item: workspaceProjectionItem
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
await executeMultiWrite({ service, triples });
|
|
933
2153
|
return {
|
|
934
2154
|
id,
|
|
935
2155
|
resource,
|
|
@@ -938,7 +2158,107 @@ async function createMembershipOperation(params) {
|
|
|
938
2158
|
}
|
|
939
2159
|
|
|
940
2160
|
// src/data/operations/control/roleassignment/roleassignment-create-operation.ts
|
|
941
|
-
var
|
|
2161
|
+
var import_types7 = require("@openhi/types");
|
|
2162
|
+
|
|
2163
|
+
// src/data/operations/control/roleassignment/roleassignment-user-projection.ts
|
|
2164
|
+
var import_types5 = require("@openhi/types");
|
|
2165
|
+
var MISSING_NAME_SENTINEL3 = "-";
|
|
2166
|
+
function buildRoleAssignmentUserProjectionSkTenantLane(params) {
|
|
2167
|
+
const normalizedRoleName = typeof params.denormalizedRoleName === "string" && params.denormalizedRoleName.length > 0 ? (0, import_types5.normalizeLabel)(params.denormalizedRoleName) : MISSING_NAME_SENTINEL3;
|
|
2168
|
+
return `ROLEASSIGNMENT#TENANT#${normalizedRoleName}#${params.roleId}#TID#${params.tenantId}#${params.roleAssignmentId}`;
|
|
2169
|
+
}
|
|
2170
|
+
function buildRoleAssignmentUserProjectionSkWorkspaceLane(params) {
|
|
2171
|
+
const normalizedRoleName = typeof params.denormalizedRoleName === "string" && params.denormalizedRoleName.length > 0 ? (0, import_types5.normalizeLabel)(params.denormalizedRoleName) : MISSING_NAME_SENTINEL3;
|
|
2172
|
+
return `ROLEASSIGNMENT#WORKSPACE#${normalizedRoleName}#${params.roleId}#TID#${params.tenantId}#WID#${params.workspaceId}#${params.roleAssignmentId}`;
|
|
2173
|
+
}
|
|
2174
|
+
function buildRoleAssignmentUserProjectionItem(input) {
|
|
2175
|
+
if (!input.userId || input.userId.length === 0) {
|
|
2176
|
+
return void 0;
|
|
2177
|
+
}
|
|
2178
|
+
if (!input.roleId || input.roleId.length === 0) {
|
|
2179
|
+
return void 0;
|
|
2180
|
+
}
|
|
2181
|
+
const hasWorkspace = typeof input.workspaceId === "string" && input.workspaceId.length > 0;
|
|
2182
|
+
const sk = hasWorkspace ? buildRoleAssignmentUserProjectionSkWorkspaceLane({
|
|
2183
|
+
tenantId: input.tenantId,
|
|
2184
|
+
workspaceId: input.workspaceId,
|
|
2185
|
+
roleId: input.roleId,
|
|
2186
|
+
roleAssignmentId: input.roleAssignmentId,
|
|
2187
|
+
denormalizedRoleName: input.denormalizedRoleName
|
|
2188
|
+
}) : buildRoleAssignmentUserProjectionSkTenantLane({
|
|
2189
|
+
tenantId: input.tenantId,
|
|
2190
|
+
roleId: input.roleId,
|
|
2191
|
+
roleAssignmentId: input.roleAssignmentId,
|
|
2192
|
+
denormalizedRoleName: input.denormalizedRoleName
|
|
2193
|
+
});
|
|
2194
|
+
return {
|
|
2195
|
+
userId: input.userId,
|
|
2196
|
+
sk,
|
|
2197
|
+
tenantId: input.tenantId,
|
|
2198
|
+
workspaceId: hasWorkspace ? input.workspaceId : void 0,
|
|
2199
|
+
roleId: input.roleId,
|
|
2200
|
+
roleAssignmentId: input.roleAssignmentId,
|
|
2201
|
+
summary: input.summary,
|
|
2202
|
+
vid: input.vid,
|
|
2203
|
+
lastUpdated: input.lastUpdated,
|
|
2204
|
+
denormalizedTenantName: input.denormalizedTenantName,
|
|
2205
|
+
denormalizedUserName: input.denormalizedUserName,
|
|
2206
|
+
denormalizedRoleName: input.denormalizedRoleName
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
function extractReferenceSlug2(resource, fieldName) {
|
|
2210
|
+
const field = resource[fieldName];
|
|
2211
|
+
if (!field || typeof field !== "object") {
|
|
2212
|
+
return void 0;
|
|
2213
|
+
}
|
|
2214
|
+
const reference = field.reference;
|
|
2215
|
+
if (typeof reference !== "string" || reference.length === 0) {
|
|
2216
|
+
return void 0;
|
|
2217
|
+
}
|
|
2218
|
+
const slash = reference.lastIndexOf("/");
|
|
2219
|
+
const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
|
|
2220
|
+
return tail.length > 0 ? tail : void 0;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// src/data/operations/control/roleassignment/roleassignment-workspace-projection.ts
|
|
2224
|
+
var import_types6 = require("@openhi/types");
|
|
2225
|
+
var MISSING_NAME_SENTINEL4 = "-";
|
|
2226
|
+
function buildRoleAssignmentWorkspaceProjectionSk(params) {
|
|
2227
|
+
const normalizedUserName = typeof params.denormalizedUserName === "string" && params.denormalizedUserName.length > 0 ? (0, import_types6.normalizeLabel)(params.denormalizedUserName) : MISSING_NAME_SENTINEL4;
|
|
2228
|
+
return `ROLEASSIGNMENT#${params.roleId}#${normalizedUserName}#USER#${params.userId}#${params.roleAssignmentId}`;
|
|
2229
|
+
}
|
|
2230
|
+
function buildRoleAssignmentWorkspaceProjectionItem(input) {
|
|
2231
|
+
if (!input.workspaceId || input.workspaceId.length === 0) {
|
|
2232
|
+
return void 0;
|
|
2233
|
+
}
|
|
2234
|
+
if (!input.userId || input.userId.length === 0) {
|
|
2235
|
+
return void 0;
|
|
2236
|
+
}
|
|
2237
|
+
if (!input.roleId || input.roleId.length === 0) {
|
|
2238
|
+
return void 0;
|
|
2239
|
+
}
|
|
2240
|
+
const sk = buildRoleAssignmentWorkspaceProjectionSk({
|
|
2241
|
+
roleId: input.roleId,
|
|
2242
|
+
userId: input.userId,
|
|
2243
|
+
roleAssignmentId: input.roleAssignmentId,
|
|
2244
|
+
denormalizedUserName: input.denormalizedUserName
|
|
2245
|
+
});
|
|
2246
|
+
return {
|
|
2247
|
+
tenantId: input.tenantId,
|
|
2248
|
+
workspaceId: input.workspaceId,
|
|
2249
|
+
sk,
|
|
2250
|
+
userId: input.userId,
|
|
2251
|
+
roleId: input.roleId,
|
|
2252
|
+
roleAssignmentId: input.roleAssignmentId,
|
|
2253
|
+
summary: input.summary,
|
|
2254
|
+
vid: input.vid,
|
|
2255
|
+
lastUpdated: input.lastUpdated,
|
|
2256
|
+
denormalizedUserName: input.denormalizedUserName,
|
|
2257
|
+
denormalizedRoleName: input.denormalizedRoleName
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// src/data/operations/control/roleassignment/roleassignment-create-operation.ts
|
|
942
2262
|
async function createRoleAssignmentOperation(params) {
|
|
943
2263
|
const { context, body, tableName } = params;
|
|
944
2264
|
const service = getDynamoControlService(tableName);
|
|
@@ -947,15 +2267,80 @@ async function createRoleAssignmentOperation(params) {
|
|
|
947
2267
|
const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
948
2268
|
const vid = `1`;
|
|
949
2269
|
const resource = { resourceType: "RoleAssignment", id, ...parsedResource };
|
|
950
|
-
const
|
|
951
|
-
|
|
2270
|
+
const resourceRecord = resource;
|
|
2271
|
+
const denormalizedTenantName = extractDenormalizedReferenceDisplay(
|
|
2272
|
+
resourceRecord,
|
|
2273
|
+
"tenant"
|
|
2274
|
+
);
|
|
2275
|
+
const denormalizedUserName = extractDenormalizedReferenceDisplay(
|
|
2276
|
+
resourceRecord,
|
|
2277
|
+
"user"
|
|
2278
|
+
);
|
|
2279
|
+
const denormalizedRoleName = extractDenormalizedReferenceDisplay(
|
|
2280
|
+
resourceRecord,
|
|
2281
|
+
"role"
|
|
2282
|
+
);
|
|
2283
|
+
const summary = JSON.stringify((0, import_types7.extractSummary)(resource));
|
|
2284
|
+
const userIdFromResource = extractReferenceSlug2(resourceRecord, "user");
|
|
2285
|
+
const roleIdFromResource = extractReferenceSlug2(resourceRecord, "role");
|
|
2286
|
+
const workspaceIdFromResource = extractReferenceSlug2(
|
|
2287
|
+
resourceRecord,
|
|
2288
|
+
"workspace"
|
|
2289
|
+
);
|
|
2290
|
+
const userProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 ? buildRoleAssignmentUserProjectionItem({
|
|
2291
|
+
tenantId: context.tenantId,
|
|
2292
|
+
userId: userIdFromResource,
|
|
2293
|
+
workspaceId: workspaceIdFromResource,
|
|
2294
|
+
roleId: roleIdFromResource,
|
|
2295
|
+
roleAssignmentId: id,
|
|
2296
|
+
summary,
|
|
2297
|
+
vid,
|
|
2298
|
+
lastUpdated,
|
|
2299
|
+
denormalizedTenantName,
|
|
2300
|
+
denormalizedUserName,
|
|
2301
|
+
denormalizedRoleName
|
|
2302
|
+
}) : void 0;
|
|
2303
|
+
const workspaceProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 && workspaceIdFromResource !== void 0 ? buildRoleAssignmentWorkspaceProjectionItem({
|
|
2304
|
+
tenantId: context.tenantId,
|
|
2305
|
+
workspaceId: workspaceIdFromResource,
|
|
2306
|
+
userId: userIdFromResource,
|
|
2307
|
+
roleId: roleIdFromResource,
|
|
2308
|
+
roleAssignmentId: id,
|
|
2309
|
+
summary,
|
|
2310
|
+
vid,
|
|
2311
|
+
lastUpdated,
|
|
2312
|
+
denormalizedUserName,
|
|
2313
|
+
denormalizedRoleName
|
|
2314
|
+
}) : void 0;
|
|
2315
|
+
const canonicalItem = {
|
|
952
2316
|
tenantId: context.tenantId,
|
|
953
2317
|
id,
|
|
954
2318
|
resource: JSON.stringify(resource),
|
|
955
2319
|
summary,
|
|
956
2320
|
vid,
|
|
957
|
-
lastUpdated
|
|
958
|
-
|
|
2321
|
+
lastUpdated,
|
|
2322
|
+
denormalizedTenantName,
|
|
2323
|
+
denormalizedUserName,
|
|
2324
|
+
denormalizedRoleName
|
|
2325
|
+
};
|
|
2326
|
+
const triples = [
|
|
2327
|
+
{ entity: "roleAssignment", action: "put", item: canonicalItem }
|
|
2328
|
+
];
|
|
2329
|
+
if (userProjectionItem) {
|
|
2330
|
+
triples.push({
|
|
2331
|
+
entity: "roleAssignmentUserProjection",
|
|
2332
|
+
action: "put",
|
|
2333
|
+
item: userProjectionItem
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
if (workspaceProjectionItem) {
|
|
2337
|
+
triples.push({
|
|
2338
|
+
entity: "roleAssignmentWorkspaceProjection",
|
|
2339
|
+
action: "put",
|
|
2340
|
+
item: workspaceProjectionItem
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
await executeMultiWrite({ service, triples });
|
|
959
2344
|
return {
|
|
960
2345
|
id,
|
|
961
2346
|
resource,
|
|
@@ -964,7 +2349,7 @@ async function createRoleAssignmentOperation(params) {
|
|
|
964
2349
|
}
|
|
965
2350
|
|
|
966
2351
|
// src/data/operations/control/tenant/tenant-create-operation.ts
|
|
967
|
-
var
|
|
2352
|
+
var import_types8 = require("@openhi/types");
|
|
968
2353
|
async function createTenantOperation(params) {
|
|
969
2354
|
const { context, body, tableName } = params;
|
|
970
2355
|
const service = getDynamoControlService(tableName);
|
|
@@ -973,7 +2358,7 @@ async function createTenantOperation(params) {
|
|
|
973
2358
|
const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
|
|
974
2359
|
const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
|
|
975
2360
|
const resource = { resourceType: "Tenant", id, ...parsedResource };
|
|
976
|
-
const summary = JSON.stringify((0,
|
|
2361
|
+
const summary = JSON.stringify((0, import_types8.extractSummary)(resource));
|
|
977
2362
|
await service.entities.tenant.put({
|
|
978
2363
|
tenantId: id,
|
|
979
2364
|
id,
|
|
@@ -986,7 +2371,7 @@ async function createTenantOperation(params) {
|
|
|
986
2371
|
}
|
|
987
2372
|
|
|
988
2373
|
// src/data/operations/control/user/user-create-operation.ts
|
|
989
|
-
var
|
|
2374
|
+
var import_types9 = require("@openhi/types");
|
|
990
2375
|
|
|
991
2376
|
// src/data/operations/control/user/user-find-by-sub-operation.ts
|
|
992
2377
|
async function findUserBySubOperation(params) {
|
|
@@ -1006,7 +2391,7 @@ async function findUserBySubOperation(params) {
|
|
|
1006
2391
|
}
|
|
1007
2392
|
|
|
1008
2393
|
// src/data/operations/data-operations-common.ts
|
|
1009
|
-
var
|
|
2394
|
+
var import_types10 = require("@openhi/types");
|
|
1010
2395
|
|
|
1011
2396
|
// src/lib/compression.ts
|
|
1012
2397
|
var import_node_zlib = require("zlib");
|
|
@@ -1043,8 +2428,8 @@ async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourc
|
|
|
1043
2428
|
const lastUpdated = resourceWithAudit.meta?.lastUpdated ?? fallbackDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1044
2429
|
const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
|
|
1045
2430
|
const resourceLike = resourceWithAudit;
|
|
1046
|
-
const summary = JSON.stringify((0,
|
|
1047
|
-
const gsi1sk = (0,
|
|
2431
|
+
const summary = JSON.stringify((0, import_types10.extractSummary)(resourceLike));
|
|
2432
|
+
const gsi1sk = (0, import_types10.extractSortKey)(resourceLike);
|
|
1048
2433
|
await entity.put({
|
|
1049
2434
|
sk: DATA_ENTITY_SK,
|
|
1050
2435
|
tenantId,
|
|
@@ -1063,7 +2448,7 @@ async function createDataEntityRecord(entity, tenantId, workspaceId, id, resourc
|
|
|
1063
2448
|
}
|
|
1064
2449
|
|
|
1065
2450
|
// src/data/operations/control/user/user-switch-tenant-workspace-operation.ts
|
|
1066
|
-
var
|
|
2451
|
+
var import_types11 = require("@openhi/types");
|
|
1067
2452
|
|
|
1068
2453
|
// src/data/operations/control/user/user-resource-helpers.ts
|
|
1069
2454
|
function parseUserResource(resource) {
|
|
@@ -1084,16 +2469,16 @@ function idFromReference(reference, prefix) {
|
|
|
1084
2469
|
}
|
|
1085
2470
|
|
|
1086
2471
|
// src/data/operations/control/user/user-update-operation.ts
|
|
1087
|
-
var
|
|
2472
|
+
var import_types12 = require("@openhi/types");
|
|
1088
2473
|
|
|
1089
2474
|
// src/data/operations/control/workspace/workspace-create-operation.ts
|
|
1090
|
-
var
|
|
2475
|
+
var import_types13 = require("@openhi/types");
|
|
1091
2476
|
|
|
1092
2477
|
// src/data/dynamo/dynamo-data-service.ts
|
|
1093
|
-
var
|
|
2478
|
+
var import_electrodb16 = require("electrodb");
|
|
1094
2479
|
|
|
1095
2480
|
// src/data/dynamo/entities/data-entity-common.ts
|
|
1096
|
-
var
|
|
2481
|
+
var import_electrodb15 = require("electrodb");
|
|
1097
2482
|
var dataEntityAttributes = {
|
|
1098
2483
|
/** Sort key. "CURRENT" for current version; version history in S3. */
|
|
1099
2484
|
sk: {
|
|
@@ -1189,7 +2574,7 @@ var dataEntityAttributes = {
|
|
|
1189
2574
|
}
|
|
1190
2575
|
};
|
|
1191
2576
|
function createDataEntity(entity, resourceTypeLabel) {
|
|
1192
|
-
return new
|
|
2577
|
+
return new import_electrodb15.Entity({
|
|
1193
2578
|
model: {
|
|
1194
2579
|
entity,
|
|
1195
2580
|
service: "data",
|
|
@@ -2103,21 +3488,23 @@ var dataPlaneEntities = {
|
|
|
2103
3488
|
visionprescription: VisionPrescriptionEntity,
|
|
2104
3489
|
verificationresult: VerificationResultEntity
|
|
2105
3490
|
};
|
|
2106
|
-
var dataPlaneService = new
|
|
3491
|
+
var dataPlaneService = new import_electrodb16.Service(dataPlaneEntities, {
|
|
2107
3492
|
table: defaultTableName,
|
|
2108
3493
|
client: dynamoClient
|
|
2109
3494
|
});
|
|
2110
3495
|
var DynamoDataService = {
|
|
2111
|
-
entities: dataPlaneService.entities
|
|
3496
|
+
entities: dataPlaneService.entities,
|
|
3497
|
+
transaction: dataPlaneService.transaction
|
|
2112
3498
|
};
|
|
2113
3499
|
function getDynamoDataService(tableName) {
|
|
2114
3500
|
const resolved = tableName ?? defaultTableName;
|
|
2115
|
-
const service = new
|
|
3501
|
+
const service = new import_electrodb16.Service(dataPlaneEntities, {
|
|
2116
3502
|
table: resolved,
|
|
2117
3503
|
client: dynamoClient
|
|
2118
3504
|
});
|
|
2119
3505
|
return {
|
|
2120
|
-
entities: service.entities
|
|
3506
|
+
entities: service.entities,
|
|
3507
|
+
transaction: service.transaction
|
|
2121
3508
|
};
|
|
2122
3509
|
}
|
|
2123
3510
|
|
|
@@ -2169,7 +3556,7 @@ async function createWorkspaceOperation(params) {
|
|
|
2169
3556
|
const vid = lastUpdated.replace(/[-:T.Z]/g, "").slice(0, 12) || Date.now().toString(36);
|
|
2170
3557
|
const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
|
|
2171
3558
|
const resource = { resourceType: "Workspace", id, ...parsedResource };
|
|
2172
|
-
const summary = JSON.stringify((0,
|
|
3559
|
+
const summary = JSON.stringify((0, import_types13.extractSummary)(resource));
|
|
2173
3560
|
await service.entities.workspace.put({
|
|
2174
3561
|
tenantId,
|
|
2175
3562
|
id,
|
|
@@ -2192,7 +3579,7 @@ async function createWorkspaceOperation(params) {
|
|
|
2192
3579
|
var CURRENT_SK = "CURRENT";
|
|
2193
3580
|
var VID = "1";
|
|
2194
3581
|
var summaryFor = (resource) => {
|
|
2195
|
-
return JSON.stringify((0,
|
|
3582
|
+
return JSON.stringify((0, import_types14.extractSummary)(resource));
|
|
2196
3583
|
};
|
|
2197
3584
|
var stableOnboardingId = (kind, cognitoSub) => {
|
|
2198
3585
|
return (0, import_node_crypto.createHash)("sha256").update(kind).update("\0").update(cognitoSub).digest("hex").slice(0, 26).toUpperCase();
|