@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
|
@@ -0,0 +1,2985 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// ../workflows/lib/envelope-version.js
|
|
34
|
+
var require_envelope_version = __commonJS({
|
|
35
|
+
"../workflows/lib/envelope-version.js"(exports2) {
|
|
36
|
+
"use strict";
|
|
37
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
38
|
+
exports2.ENVELOPE_VERSION = void 0;
|
|
39
|
+
exports2.isSupportedEnvelopeVersion = isSupportedEnvelopeVersion;
|
|
40
|
+
exports2.ENVELOPE_VERSION = "1.0";
|
|
41
|
+
var ENVELOPE_VERSION_PATTERN = /^\d+\.\d+$/;
|
|
42
|
+
var MIN_SUPPORTED_MAJOR = 1;
|
|
43
|
+
var MAX_SUPPORTED_MAJOR = 1;
|
|
44
|
+
function isSupportedEnvelopeVersion(version) {
|
|
45
|
+
if (!ENVELOPE_VERSION_PATTERN.test(version)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const major = Number.parseInt(version.split(".")[0], 10);
|
|
49
|
+
return major >= MIN_SUPPORTED_MAJOR && major <= MAX_SUPPORTED_MAJOR;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ../workflows/lib/envelope.js
|
|
55
|
+
var require_envelope = __commonJS({
|
|
56
|
+
"../workflows/lib/envelope.js"(exports2) {
|
|
57
|
+
"use strict";
|
|
58
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
59
|
+
exports2.MissingActorContextError = void 0;
|
|
60
|
+
exports2.isWorkflowUserActor = isWorkflowUserActor;
|
|
61
|
+
exports2.isWorkflowSystemActor = isWorkflowSystemActor;
|
|
62
|
+
exports2.workflowUserActorFromClaims = workflowUserActorFromClaims;
|
|
63
|
+
function isWorkflowUserActor(actor) {
|
|
64
|
+
return actor.ohi_uid !== void 0;
|
|
65
|
+
}
|
|
66
|
+
function isWorkflowSystemActor(actor) {
|
|
67
|
+
return actor.system !== void 0;
|
|
68
|
+
}
|
|
69
|
+
function workflowUserActorFromClaims(claims) {
|
|
70
|
+
if (claims.ohi_tid === void 0 || claims.ohi_wid === void 0) {
|
|
71
|
+
throw new MissingActorContextError("workflowUserActorFromClaims: ohi_tid and ohi_wid are required on the workflow user-actor; the caller's JWT is missing one or both. Use a system-actor for pre-provisioning bootstrap workflows.");
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
ohi_tid: claims.ohi_tid,
|
|
75
|
+
ohi_wid: claims.ohi_wid,
|
|
76
|
+
ohi_uid: claims.ohi_uid,
|
|
77
|
+
ohi_uname: claims.ohi_uname
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var MissingActorContextError = class extends Error {
|
|
81
|
+
/** @param message - human-readable description of the missing claim. */
|
|
82
|
+
constructor(message) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "MissingActorContextError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
exports2.MissingActorContextError = MissingActorContextError;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ../workflows/lib/sources.js
|
|
92
|
+
var require_sources = __commonJS({
|
|
93
|
+
"../workflows/lib/sources.js"(exports2) {
|
|
94
|
+
"use strict";
|
|
95
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
96
|
+
exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = void 0;
|
|
97
|
+
exports2.OPENHI_CONTROL_SOURCE = "openhi.control";
|
|
98
|
+
exports2.OPENHI_DATA_SOURCE = "openhi.data";
|
|
99
|
+
exports2.OPENHI_OPS_SOURCE = "openhi.ops";
|
|
100
|
+
exports2.DEFAULT_BUS_NAME_BY_SOURCE = {
|
|
101
|
+
[exports2.OPENHI_CONTROL_SOURCE]: "openhi-control-event-bus",
|
|
102
|
+
[exports2.OPENHI_DATA_SOURCE]: "openhi-data-event-bus",
|
|
103
|
+
[exports2.OPENHI_OPS_SOURCE]: "openhi-ops-event-bus"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ../workflows/lib/detail-types/registry.js
|
|
109
|
+
var require_registry = __commonJS({
|
|
110
|
+
"../workflows/lib/detail-types/registry.js"(exports2) {
|
|
111
|
+
"use strict";
|
|
112
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
113
|
+
exports2.InvalidDetailTypeRegistrationError = void 0;
|
|
114
|
+
exports2.defineDetailType = defineDetailType;
|
|
115
|
+
exports2.isWellFormedDetailType = isWellFormedDetailType;
|
|
116
|
+
function defineDetailType(entry) {
|
|
117
|
+
if (!isWellFormedDetailType(entry.detailType)) {
|
|
118
|
+
throw new InvalidDetailTypeRegistrationError(`Detail-type "${entry.detailType}" does not match the platform-wide format <area>.<event>.v<integer>. See TR-016 \xA7Open Items #2.`);
|
|
119
|
+
}
|
|
120
|
+
return entry;
|
|
121
|
+
}
|
|
122
|
+
var DETAIL_TYPE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*\.v\d+$/;
|
|
123
|
+
function isWellFormedDetailType(detailType) {
|
|
124
|
+
return DETAIL_TYPE_PATTERN.test(detailType);
|
|
125
|
+
}
|
|
126
|
+
var InvalidDetailTypeRegistrationError = class extends Error {
|
|
127
|
+
/** @param message - human-readable description of the violation. */
|
|
128
|
+
constructor(message) {
|
|
129
|
+
super(message);
|
|
130
|
+
this.name = "InvalidDetailTypeRegistrationError";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
exports2.InvalidDetailTypeRegistrationError = InvalidDetailTypeRegistrationError;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ../workflows/lib/detail-types/control-plane.js
|
|
138
|
+
var require_control_plane = __commonJS({
|
|
139
|
+
"../workflows/lib/detail-types/control-plane.js"(exports2) {
|
|
140
|
+
"use strict";
|
|
141
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
142
|
+
exports2.ControlPlaneRenameFailedV1 = exports2.ControlPlaneRenameCompleteV1 = exports2.ControlPlaneRenameV1 = exports2.RENAMABLE_ENTITY_TYPE = exports2.ControlPlaneOwningDeleteFailedV1 = exports2.ControlPlaneOwningDeleteCompleteV1 = exports2.ControlPlaneOwningDeleteV1 = exports2.OWNING_ENTITY_TYPE = void 0;
|
|
143
|
+
var sources_1 = require_sources();
|
|
144
|
+
var registry_1 = require_registry();
|
|
145
|
+
exports2.OWNING_ENTITY_TYPE = {
|
|
146
|
+
Workspace: "Workspace",
|
|
147
|
+
User: "User"
|
|
148
|
+
};
|
|
149
|
+
exports2.ControlPlaneOwningDeleteV1 = (0, registry_1.defineDetailType)({
|
|
150
|
+
detailType: "control-plane.owning-delete.v1",
|
|
151
|
+
source: sources_1.OPENHI_DATA_SOURCE,
|
|
152
|
+
dedupRequired: true
|
|
153
|
+
});
|
|
154
|
+
exports2.ControlPlaneOwningDeleteCompleteV1 = (0, registry_1.defineDetailType)({
|
|
155
|
+
detailType: "control-plane.owning-delete-complete.v1",
|
|
156
|
+
source: sources_1.OPENHI_OPS_SOURCE,
|
|
157
|
+
dedupRequired: true
|
|
158
|
+
});
|
|
159
|
+
exports2.ControlPlaneOwningDeleteFailedV1 = (0, registry_1.defineDetailType)({
|
|
160
|
+
detailType: "control-plane.owning-delete-failed.v1",
|
|
161
|
+
source: sources_1.OPENHI_OPS_SOURCE,
|
|
162
|
+
dedupRequired: true
|
|
163
|
+
});
|
|
164
|
+
exports2.RENAMABLE_ENTITY_TYPE = {
|
|
165
|
+
Tenant: "Tenant",
|
|
166
|
+
User: "User",
|
|
167
|
+
Role: "Role"
|
|
168
|
+
};
|
|
169
|
+
exports2.ControlPlaneRenameV1 = (0, registry_1.defineDetailType)({
|
|
170
|
+
detailType: "control-plane.rename.v1",
|
|
171
|
+
source: sources_1.OPENHI_DATA_SOURCE,
|
|
172
|
+
dedupRequired: true
|
|
173
|
+
});
|
|
174
|
+
exports2.ControlPlaneRenameCompleteV1 = (0, registry_1.defineDetailType)({
|
|
175
|
+
detailType: "control-plane.rename-complete.v1",
|
|
176
|
+
source: sources_1.OPENHI_OPS_SOURCE,
|
|
177
|
+
dedupRequired: true
|
|
178
|
+
});
|
|
179
|
+
exports2.ControlPlaneRenameFailedV1 = (0, registry_1.defineDetailType)({
|
|
180
|
+
detailType: "control-plane.rename-failed.v1",
|
|
181
|
+
source: sources_1.OPENHI_OPS_SOURCE,
|
|
182
|
+
dedupRequired: true
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ../workflows/lib/detail-types/platform.js
|
|
188
|
+
var require_platform = __commonJS({
|
|
189
|
+
"../workflows/lib/detail-types/platform.js"(exports2) {
|
|
190
|
+
"use strict";
|
|
191
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
192
|
+
exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = void 0;
|
|
193
|
+
var sources_1 = require_sources();
|
|
194
|
+
var registry_1 = require_registry();
|
|
195
|
+
exports2.PlatformDeploymentCompletedV1 = (0, registry_1.defineDetailType)({
|
|
196
|
+
detailType: "platform.deployment-completed.v1",
|
|
197
|
+
source: sources_1.OPENHI_CONTROL_SOURCE,
|
|
198
|
+
dedupRequired: true
|
|
199
|
+
});
|
|
200
|
+
exports2.PlatformSystemDataSeededV1 = (0, registry_1.defineDetailType)({
|
|
201
|
+
detailType: "platform.system-data-seeded.v1",
|
|
202
|
+
source: sources_1.OPENHI_CONTROL_SOURCE,
|
|
203
|
+
dedupRequired: true
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ../workflows/lib/detail-types/index.js
|
|
209
|
+
var require_detail_types = __commonJS({
|
|
210
|
+
"../workflows/lib/detail-types/index.js"(exports2) {
|
|
211
|
+
"use strict";
|
|
212
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
213
|
+
if (k2 === void 0) k2 = k;
|
|
214
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
215
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
216
|
+
desc = { enumerable: true, get: function() {
|
|
217
|
+
return m[k];
|
|
218
|
+
} };
|
|
219
|
+
}
|
|
220
|
+
Object.defineProperty(o, k2, desc);
|
|
221
|
+
}) : (function(o, m, k, k2) {
|
|
222
|
+
if (k2 === void 0) k2 = k;
|
|
223
|
+
o[k2] = m[k];
|
|
224
|
+
}));
|
|
225
|
+
var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
|
|
226
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
|
|
227
|
+
};
|
|
228
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
229
|
+
__exportStar(require_control_plane(), exports2);
|
|
230
|
+
__exportStar(require_platform(), exports2);
|
|
231
|
+
__exportStar(require_registry(), exports2);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ../workflows/lib/publisher.js
|
|
236
|
+
var require_publisher = __commonJS({
|
|
237
|
+
"../workflows/lib/publisher.js"(exports2) {
|
|
238
|
+
"use strict";
|
|
239
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
240
|
+
exports2.WorkflowPublishError = void 0;
|
|
241
|
+
exports2.workflowsClient = workflowsClient;
|
|
242
|
+
exports2.publishWorkflowEvent = publishWorkflowEvent;
|
|
243
|
+
var node_crypto_1 = require("crypto");
|
|
244
|
+
var client_eventbridge_1 = require("@aws-sdk/client-eventbridge");
|
|
245
|
+
var envelope_version_1 = require_envelope_version();
|
|
246
|
+
var sources_1 = require_sources();
|
|
247
|
+
function workflowsClient(bridge, options = {}) {
|
|
248
|
+
return {
|
|
249
|
+
publish: (entry, payload, ctx) => publishWorkflowEvent(bridge, entry, payload, ctx, options)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async function publishWorkflowEvent(bridge, entry, payload, ctx, options = {}) {
|
|
253
|
+
const eventIdGenerator = options.eventIdGenerator ?? (() => (0, node_crypto_1.randomUUID)());
|
|
254
|
+
const correlationIdGenerator = options.correlationIdGenerator ?? (() => (0, node_crypto_1.randomUUID)());
|
|
255
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
256
|
+
const envelope = {
|
|
257
|
+
eventId: eventIdGenerator(),
|
|
258
|
+
attempt: 1,
|
|
259
|
+
correlationId: ctx.correlationId ?? correlationIdGenerator(),
|
|
260
|
+
causationId: ctx.causationId ?? null,
|
|
261
|
+
actor: ctx.actor,
|
|
262
|
+
occurredAt: now().toISOString(),
|
|
263
|
+
envelopeVersion: envelope_version_1.ENVELOPE_VERSION,
|
|
264
|
+
payload
|
|
265
|
+
};
|
|
266
|
+
const busName = options.busNameByPlane?.[entry.source] ?? sources_1.DEFAULT_BUS_NAME_BY_SOURCE[entry.source];
|
|
267
|
+
const result = await bridge.send(new client_eventbridge_1.PutEventsCommand({
|
|
268
|
+
Entries: [
|
|
269
|
+
{
|
|
270
|
+
EventBusName: busName,
|
|
271
|
+
Source: entry.source,
|
|
272
|
+
DetailType: entry.detailType,
|
|
273
|
+
Detail: JSON.stringify(envelope)
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
}));
|
|
277
|
+
if ((result.FailedEntryCount ?? 0) > 0) {
|
|
278
|
+
const first = result.Entries?.[0];
|
|
279
|
+
throw new WorkflowPublishError(`EventBridge rejected ${entry.detailType} publish on bus ${busName}: ${first?.ErrorCode ?? "unknown"} \u2014 ${first?.ErrorMessage ?? "no error message"}`);
|
|
280
|
+
}
|
|
281
|
+
return { eventId: envelope.eventId };
|
|
282
|
+
}
|
|
283
|
+
var WorkflowPublishError = class extends Error {
|
|
284
|
+
/** @param message - human-readable description of the failed publish. */
|
|
285
|
+
constructor(message) {
|
|
286
|
+
super(message);
|
|
287
|
+
this.name = "WorkflowPublishError";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
exports2.WorkflowPublishError = WorkflowPublishError;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ../workflows/lib/consumer.js
|
|
295
|
+
var require_consumer = __commonJS({
|
|
296
|
+
"../workflows/lib/consumer.js"(exports2) {
|
|
297
|
+
"use strict";
|
|
298
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
299
|
+
exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = void 0;
|
|
300
|
+
exports2.parseWorkflowEvent = parseWorkflowEvent;
|
|
301
|
+
var envelope_version_1 = require_envelope_version();
|
|
302
|
+
function parseWorkflowEvent(event, expected) {
|
|
303
|
+
if (event.source !== expected.source) {
|
|
304
|
+
throw new InvalidWorkflowEventError(`EventBridge source "${event.source}" does not match expected detail-type's source "${expected.source}".`);
|
|
305
|
+
}
|
|
306
|
+
if (event["detail-type"] !== expected.detailType) {
|
|
307
|
+
throw new InvalidWorkflowEventError(`EventBridge detail-type "${event["detail-type"]}" does not match expected "${expected.detailType}".`);
|
|
308
|
+
}
|
|
309
|
+
const candidate = asEnvelopeCandidate(event.detail);
|
|
310
|
+
if (!(0, envelope_version_1.isSupportedEnvelopeVersion)(candidate.envelopeVersion)) {
|
|
311
|
+
throw new UnsupportedEnvelopeVersionError(`Envelope version "${candidate.envelopeVersion}" is outside the SDK's supported range.`);
|
|
312
|
+
}
|
|
313
|
+
const envelope = {
|
|
314
|
+
eventId: candidate.eventId,
|
|
315
|
+
attempt: candidate.attempt,
|
|
316
|
+
correlationId: candidate.correlationId,
|
|
317
|
+
causationId: candidate.causationId,
|
|
318
|
+
actor: candidate.actor,
|
|
319
|
+
occurredAt: candidate.occurredAt,
|
|
320
|
+
envelopeVersion: candidate.envelopeVersion,
|
|
321
|
+
payload: candidate.payload
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
envelope,
|
|
325
|
+
dedupKey: { eventId: envelope.eventId, attempt: envelope.attempt }
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function asEnvelopeCandidate(detail) {
|
|
329
|
+
if (detail === null || typeof detail !== "object") {
|
|
330
|
+
throw new InvalidWorkflowEventError("EventBridge detail is not a non-null object.");
|
|
331
|
+
}
|
|
332
|
+
const obj = detail;
|
|
333
|
+
assertString(obj, "eventId");
|
|
334
|
+
assertPositiveInteger(obj, "attempt");
|
|
335
|
+
assertString(obj, "correlationId");
|
|
336
|
+
assertCausationId(obj);
|
|
337
|
+
assertActor(obj);
|
|
338
|
+
assertString(obj, "occurredAt");
|
|
339
|
+
assertString(obj, "envelopeVersion");
|
|
340
|
+
if (!("payload" in obj)) {
|
|
341
|
+
throw new InvalidWorkflowEventError("Envelope is missing required field: payload.");
|
|
342
|
+
}
|
|
343
|
+
return obj;
|
|
344
|
+
}
|
|
345
|
+
function assertString(obj, field) {
|
|
346
|
+
const value = obj[field];
|
|
347
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
348
|
+
throw new InvalidWorkflowEventError(`Envelope field "${field}" must be a non-empty string.`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function assertPositiveInteger(obj, field) {
|
|
352
|
+
const value = obj[field];
|
|
353
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
354
|
+
throw new InvalidWorkflowEventError(`Envelope field "${field}" must be a 1-indexed integer.`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function assertCausationId(obj) {
|
|
358
|
+
if (!("causationId" in obj)) {
|
|
359
|
+
throw new InvalidWorkflowEventError("Envelope is missing required field: causationId.");
|
|
360
|
+
}
|
|
361
|
+
const value = obj.causationId;
|
|
362
|
+
if (value !== null && (typeof value !== "string" || value.length === 0)) {
|
|
363
|
+
throw new InvalidWorkflowEventError('Envelope field "causationId" must be a non-empty string or null.');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function assertActor(obj) {
|
|
367
|
+
const actor = obj.actor;
|
|
368
|
+
if (actor === null || typeof actor !== "object") {
|
|
369
|
+
throw new InvalidWorkflowEventError('Envelope field "actor" must be an object.');
|
|
370
|
+
}
|
|
371
|
+
const actorObj = actor;
|
|
372
|
+
const isUserActor = typeof actorObj.ohi_uid === "string" && typeof actorObj.ohi_uname === "string" && typeof actorObj.ohi_tid === "string" && typeof actorObj.ohi_wid === "string";
|
|
373
|
+
const isSystemActor = typeof actorObj.system === "string";
|
|
374
|
+
if (!isUserActor && !isSystemActor) {
|
|
375
|
+
throw new InvalidWorkflowEventError('Envelope field "actor" must be either a user-actor (ohi_tid, ohi_wid, ohi_uid, ohi_uname) or a system-actor ({ system: string }).');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
var InvalidWorkflowEventError = class extends Error {
|
|
379
|
+
/** @param message - human-readable description of the validation failure. */
|
|
380
|
+
constructor(message) {
|
|
381
|
+
super(message);
|
|
382
|
+
this.name = "InvalidWorkflowEventError";
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
exports2.InvalidWorkflowEventError = InvalidWorkflowEventError;
|
|
386
|
+
var UnsupportedEnvelopeVersionError = class extends Error {
|
|
387
|
+
/** @param message - human-readable description of the unsupported version. */
|
|
388
|
+
constructor(message) {
|
|
389
|
+
super(message);
|
|
390
|
+
this.name = "UnsupportedEnvelopeVersionError";
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
exports2.UnsupportedEnvelopeVersionError = UnsupportedEnvelopeVersionError;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ../workflows/lib/dedup/env.js
|
|
398
|
+
var require_env = __commonJS({
|
|
399
|
+
"../workflows/lib/dedup/env.js"(exports2) {
|
|
400
|
+
"use strict";
|
|
401
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
402
|
+
exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = void 0;
|
|
403
|
+
exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = "OPENHI_WORKFLOW_DEDUP_TABLE_NAME";
|
|
404
|
+
exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = 14 * 24 * 60 * 60;
|
|
405
|
+
exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = 64;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ../workflows/lib/dedup/workflow-dedup-client.js
|
|
410
|
+
var require_workflow_dedup_client = __commonJS({
|
|
411
|
+
"../workflows/lib/dedup/workflow-dedup-client.js"(exports2) {
|
|
412
|
+
"use strict";
|
|
413
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
414
|
+
exports2.WorkflowDedupInvalidInputError = exports2.WorkflowDedupTableNameMissingError = void 0;
|
|
415
|
+
exports2.workflowDedupClient = workflowDedupClient;
|
|
416
|
+
exports2.recordIfAbsent = recordIfAbsent;
|
|
417
|
+
exports2.markFailed = markFailed;
|
|
418
|
+
exports2.encodeSortKey = encodeSortKey;
|
|
419
|
+
var client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
420
|
+
var env_1 = require_env();
|
|
421
|
+
function workflowDedupClient(dynamodb, options = {}) {
|
|
422
|
+
return {
|
|
423
|
+
recordIfAbsent: (input) => recordIfAbsent(dynamodb, input, options),
|
|
424
|
+
markFailed: (input) => markFailed(dynamodb, input, options)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function recordIfAbsent(dynamodb, input, options = {}) {
|
|
428
|
+
assertConsumerName(input.consumerName);
|
|
429
|
+
assertPositiveInteger(input.attempt, "attempt");
|
|
430
|
+
const ttlSeconds = input.ttlSeconds ?? options.defaultTtlSeconds ?? env_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
|
|
431
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
|
|
432
|
+
throw new WorkflowDedupInvalidInputError(`ttlSeconds must be a positive integer; got ${ttlSeconds}.`);
|
|
433
|
+
}
|
|
434
|
+
const tableName = resolveTableName(options.tableName);
|
|
435
|
+
const now = (options.now ?? defaultNow)();
|
|
436
|
+
const sk = encodeSortKey(input.eventId, input.attempt);
|
|
437
|
+
const expiresAt = Math.floor(now.getTime() / 1e3) + ttlSeconds;
|
|
438
|
+
try {
|
|
439
|
+
await dynamodb.send(new client_dynamodb_1.PutItemCommand({
|
|
440
|
+
TableName: tableName,
|
|
441
|
+
Item: {
|
|
442
|
+
consumerName: { S: input.consumerName },
|
|
443
|
+
sk: { S: sk },
|
|
444
|
+
eventId: { S: input.eventId },
|
|
445
|
+
attempt: { N: String(input.attempt) },
|
|
446
|
+
recordedAt: { S: now.toISOString() },
|
|
447
|
+
expiresAt: { N: String(expiresAt) }
|
|
448
|
+
},
|
|
449
|
+
ConditionExpression: "attribute_not_exists(consumerName) AND attribute_not_exists(sk)"
|
|
450
|
+
}));
|
|
451
|
+
return { recorded: true };
|
|
452
|
+
} catch (err) {
|
|
453
|
+
if (err instanceof client_dynamodb_1.ConditionalCheckFailedException) {
|
|
454
|
+
return { recorded: false, alreadyProcessed: true };
|
|
455
|
+
}
|
|
456
|
+
throw err;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function markFailed(dynamodb, input, options = {}) {
|
|
460
|
+
assertConsumerName(input.consumerName);
|
|
461
|
+
assertPositiveInteger(input.attempt, "attempt");
|
|
462
|
+
if (input.reason.length === 0) {
|
|
463
|
+
throw new WorkflowDedupInvalidInputError("reason must be non-empty.");
|
|
464
|
+
}
|
|
465
|
+
const tableName = resolveTableName(options.tableName);
|
|
466
|
+
const now = (options.now ?? defaultNow)();
|
|
467
|
+
const sk = encodeSortKey(input.eventId, input.attempt);
|
|
468
|
+
await dynamodb.send(new client_dynamodb_1.UpdateItemCommand({
|
|
469
|
+
TableName: tableName,
|
|
470
|
+
Key: {
|
|
471
|
+
consumerName: { S: input.consumerName },
|
|
472
|
+
sk: { S: sk }
|
|
473
|
+
},
|
|
474
|
+
UpdateExpression: "SET #failed = :failed, #failureReason = :reason, #failedAt = :failedAt",
|
|
475
|
+
ExpressionAttributeNames: {
|
|
476
|
+
"#failed": "failed",
|
|
477
|
+
"#failureReason": "failureReason",
|
|
478
|
+
"#failedAt": "failedAt"
|
|
479
|
+
},
|
|
480
|
+
ExpressionAttributeValues: {
|
|
481
|
+
":failed": { BOOL: true },
|
|
482
|
+
":reason": { S: input.reason },
|
|
483
|
+
":failedAt": { S: now.toISOString() }
|
|
484
|
+
}
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
function encodeSortKey(eventId, attempt) {
|
|
488
|
+
if (eventId.length === 0) {
|
|
489
|
+
throw new WorkflowDedupInvalidInputError("eventId must be non-empty.");
|
|
490
|
+
}
|
|
491
|
+
return `${eventId}#${attempt}`;
|
|
492
|
+
}
|
|
493
|
+
function resolveTableName(explicit) {
|
|
494
|
+
const name = explicit ?? process.env[env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR];
|
|
495
|
+
if (!name) {
|
|
496
|
+
throw new WorkflowDedupTableNameMissingError(`Workflow dedup table name not set. Pass options.tableName or set ${env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR}.`);
|
|
497
|
+
}
|
|
498
|
+
return name;
|
|
499
|
+
}
|
|
500
|
+
function assertConsumerName(consumerName) {
|
|
501
|
+
if (consumerName.length === 0) {
|
|
502
|
+
throw new WorkflowDedupInvalidInputError("consumerName must be non-empty.");
|
|
503
|
+
}
|
|
504
|
+
if (consumerName.length > env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH) {
|
|
505
|
+
throw new WorkflowDedupInvalidInputError(`consumerName must be \u2264${env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH} chars; got ${consumerName.length}.`);
|
|
506
|
+
}
|
|
507
|
+
if (/\s/.test(consumerName)) {
|
|
508
|
+
throw new WorkflowDedupInvalidInputError("consumerName must not contain whitespace.");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function assertPositiveInteger(value, field) {
|
|
512
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
513
|
+
throw new WorkflowDedupInvalidInputError(`${field} must be a 1-indexed integer; got ${value}.`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function defaultNow() {
|
|
517
|
+
return /* @__PURE__ */ new Date();
|
|
518
|
+
}
|
|
519
|
+
var WorkflowDedupTableNameMissingError = class extends Error {
|
|
520
|
+
/** @param message - human-readable description. */
|
|
521
|
+
constructor(message) {
|
|
522
|
+
super(message);
|
|
523
|
+
this.name = "WorkflowDedupTableNameMissingError";
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
exports2.WorkflowDedupTableNameMissingError = WorkflowDedupTableNameMissingError;
|
|
527
|
+
var WorkflowDedupInvalidInputError = class extends Error {
|
|
528
|
+
/** @param message - human-readable description. */
|
|
529
|
+
constructor(message) {
|
|
530
|
+
super(message);
|
|
531
|
+
this.name = "WorkflowDedupInvalidInputError";
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
exports2.WorkflowDedupInvalidInputError = WorkflowDedupInvalidInputError;
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ../workflows/lib/dedup/index.js
|
|
539
|
+
var require_dedup = __commonJS({
|
|
540
|
+
"../workflows/lib/dedup/index.js"(exports2) {
|
|
541
|
+
"use strict";
|
|
542
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
543
|
+
exports2.workflowDedupClient = exports2.recordIfAbsent = exports2.markFailed = exports2.encodeSortKey = exports2.WorkflowDedupTableNameMissingError = exports2.WorkflowDedupInvalidInputError = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = void 0;
|
|
544
|
+
var env_1 = require_env();
|
|
545
|
+
Object.defineProperty(exports2, "WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS", { enumerable: true, get: function() {
|
|
546
|
+
return env_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
|
|
547
|
+
} });
|
|
548
|
+
Object.defineProperty(exports2, "WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH", { enumerable: true, get: function() {
|
|
549
|
+
return env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH;
|
|
550
|
+
} });
|
|
551
|
+
Object.defineProperty(exports2, "WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR", { enumerable: true, get: function() {
|
|
552
|
+
return env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR;
|
|
553
|
+
} });
|
|
554
|
+
var workflow_dedup_client_1 = require_workflow_dedup_client();
|
|
555
|
+
Object.defineProperty(exports2, "WorkflowDedupInvalidInputError", { enumerable: true, get: function() {
|
|
556
|
+
return workflow_dedup_client_1.WorkflowDedupInvalidInputError;
|
|
557
|
+
} });
|
|
558
|
+
Object.defineProperty(exports2, "WorkflowDedupTableNameMissingError", { enumerable: true, get: function() {
|
|
559
|
+
return workflow_dedup_client_1.WorkflowDedupTableNameMissingError;
|
|
560
|
+
} });
|
|
561
|
+
Object.defineProperty(exports2, "encodeSortKey", { enumerable: true, get: function() {
|
|
562
|
+
return workflow_dedup_client_1.encodeSortKey;
|
|
563
|
+
} });
|
|
564
|
+
Object.defineProperty(exports2, "markFailed", { enumerable: true, get: function() {
|
|
565
|
+
return workflow_dedup_client_1.markFailed;
|
|
566
|
+
} });
|
|
567
|
+
Object.defineProperty(exports2, "recordIfAbsent", { enumerable: true, get: function() {
|
|
568
|
+
return workflow_dedup_client_1.recordIfAbsent;
|
|
569
|
+
} });
|
|
570
|
+
Object.defineProperty(exports2, "workflowDedupClient", { enumerable: true, get: function() {
|
|
571
|
+
return workflow_dedup_client_1.workflowDedupClient;
|
|
572
|
+
} });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// ../workflows/lib/index.js
|
|
577
|
+
var require_lib = __commonJS({
|
|
578
|
+
"../workflows/lib/index.js"(exports2) {
|
|
579
|
+
"use strict";
|
|
580
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
581
|
+
exports2.workflowDedupClient = exports2.recordIfAbsent = exports2.markFailed = exports2.encodeSortKey = exports2.WorkflowDedupTableNameMissingError = exports2.WorkflowDedupInvalidInputError = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.parseWorkflowEvent = exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = exports2.workflowsClient = exports2.publishWorkflowEvent = exports2.WorkflowPublishError = exports2.isWellFormedDetailType = exports2.defineDetailType = exports2.RENAMABLE_ENTITY_TYPE = exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = exports2.OWNING_ENTITY_TYPE = exports2.InvalidDetailTypeRegistrationError = exports2.ControlPlaneRenameV1 = exports2.ControlPlaneRenameFailedV1 = exports2.ControlPlaneRenameCompleteV1 = exports2.ControlPlaneOwningDeleteV1 = exports2.ControlPlaneOwningDeleteFailedV1 = exports2.ControlPlaneOwningDeleteCompleteV1 = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.workflowUserActorFromClaims = exports2.isWorkflowUserActor = exports2.isWorkflowSystemActor = exports2.MissingActorContextError = exports2.isSupportedEnvelopeVersion = exports2.ENVELOPE_VERSION = void 0;
|
|
582
|
+
var envelope_version_1 = require_envelope_version();
|
|
583
|
+
Object.defineProperty(exports2, "ENVELOPE_VERSION", { enumerable: true, get: function() {
|
|
584
|
+
return envelope_version_1.ENVELOPE_VERSION;
|
|
585
|
+
} });
|
|
586
|
+
Object.defineProperty(exports2, "isSupportedEnvelopeVersion", { enumerable: true, get: function() {
|
|
587
|
+
return envelope_version_1.isSupportedEnvelopeVersion;
|
|
588
|
+
} });
|
|
589
|
+
var envelope_1 = require_envelope();
|
|
590
|
+
Object.defineProperty(exports2, "MissingActorContextError", { enumerable: true, get: function() {
|
|
591
|
+
return envelope_1.MissingActorContextError;
|
|
592
|
+
} });
|
|
593
|
+
Object.defineProperty(exports2, "isWorkflowSystemActor", { enumerable: true, get: function() {
|
|
594
|
+
return envelope_1.isWorkflowSystemActor;
|
|
595
|
+
} });
|
|
596
|
+
Object.defineProperty(exports2, "isWorkflowUserActor", { enumerable: true, get: function() {
|
|
597
|
+
return envelope_1.isWorkflowUserActor;
|
|
598
|
+
} });
|
|
599
|
+
Object.defineProperty(exports2, "workflowUserActorFromClaims", { enumerable: true, get: function() {
|
|
600
|
+
return envelope_1.workflowUserActorFromClaims;
|
|
601
|
+
} });
|
|
602
|
+
var sources_1 = require_sources();
|
|
603
|
+
Object.defineProperty(exports2, "DEFAULT_BUS_NAME_BY_SOURCE", { enumerable: true, get: function() {
|
|
604
|
+
return sources_1.DEFAULT_BUS_NAME_BY_SOURCE;
|
|
605
|
+
} });
|
|
606
|
+
Object.defineProperty(exports2, "OPENHI_CONTROL_SOURCE", { enumerable: true, get: function() {
|
|
607
|
+
return sources_1.OPENHI_CONTROL_SOURCE;
|
|
608
|
+
} });
|
|
609
|
+
Object.defineProperty(exports2, "OPENHI_DATA_SOURCE", { enumerable: true, get: function() {
|
|
610
|
+
return sources_1.OPENHI_DATA_SOURCE;
|
|
611
|
+
} });
|
|
612
|
+
Object.defineProperty(exports2, "OPENHI_OPS_SOURCE", { enumerable: true, get: function() {
|
|
613
|
+
return sources_1.OPENHI_OPS_SOURCE;
|
|
614
|
+
} });
|
|
615
|
+
var detail_types_1 = require_detail_types();
|
|
616
|
+
Object.defineProperty(exports2, "ControlPlaneOwningDeleteCompleteV1", { enumerable: true, get: function() {
|
|
617
|
+
return detail_types_1.ControlPlaneOwningDeleteCompleteV1;
|
|
618
|
+
} });
|
|
619
|
+
Object.defineProperty(exports2, "ControlPlaneOwningDeleteFailedV1", { enumerable: true, get: function() {
|
|
620
|
+
return detail_types_1.ControlPlaneOwningDeleteFailedV1;
|
|
621
|
+
} });
|
|
622
|
+
Object.defineProperty(exports2, "ControlPlaneOwningDeleteV1", { enumerable: true, get: function() {
|
|
623
|
+
return detail_types_1.ControlPlaneOwningDeleteV1;
|
|
624
|
+
} });
|
|
625
|
+
Object.defineProperty(exports2, "ControlPlaneRenameCompleteV1", { enumerable: true, get: function() {
|
|
626
|
+
return detail_types_1.ControlPlaneRenameCompleteV1;
|
|
627
|
+
} });
|
|
628
|
+
Object.defineProperty(exports2, "ControlPlaneRenameFailedV1", { enumerable: true, get: function() {
|
|
629
|
+
return detail_types_1.ControlPlaneRenameFailedV1;
|
|
630
|
+
} });
|
|
631
|
+
Object.defineProperty(exports2, "ControlPlaneRenameV1", { enumerable: true, get: function() {
|
|
632
|
+
return detail_types_1.ControlPlaneRenameV1;
|
|
633
|
+
} });
|
|
634
|
+
Object.defineProperty(exports2, "InvalidDetailTypeRegistrationError", { enumerable: true, get: function() {
|
|
635
|
+
return detail_types_1.InvalidDetailTypeRegistrationError;
|
|
636
|
+
} });
|
|
637
|
+
Object.defineProperty(exports2, "OWNING_ENTITY_TYPE", { enumerable: true, get: function() {
|
|
638
|
+
return detail_types_1.OWNING_ENTITY_TYPE;
|
|
639
|
+
} });
|
|
640
|
+
Object.defineProperty(exports2, "PlatformDeploymentCompletedV1", { enumerable: true, get: function() {
|
|
641
|
+
return detail_types_1.PlatformDeploymentCompletedV1;
|
|
642
|
+
} });
|
|
643
|
+
Object.defineProperty(exports2, "PlatformSystemDataSeededV1", { enumerable: true, get: function() {
|
|
644
|
+
return detail_types_1.PlatformSystemDataSeededV1;
|
|
645
|
+
} });
|
|
646
|
+
Object.defineProperty(exports2, "RENAMABLE_ENTITY_TYPE", { enumerable: true, get: function() {
|
|
647
|
+
return detail_types_1.RENAMABLE_ENTITY_TYPE;
|
|
648
|
+
} });
|
|
649
|
+
Object.defineProperty(exports2, "defineDetailType", { enumerable: true, get: function() {
|
|
650
|
+
return detail_types_1.defineDetailType;
|
|
651
|
+
} });
|
|
652
|
+
Object.defineProperty(exports2, "isWellFormedDetailType", { enumerable: true, get: function() {
|
|
653
|
+
return detail_types_1.isWellFormedDetailType;
|
|
654
|
+
} });
|
|
655
|
+
var publisher_1 = require_publisher();
|
|
656
|
+
Object.defineProperty(exports2, "WorkflowPublishError", { enumerable: true, get: function() {
|
|
657
|
+
return publisher_1.WorkflowPublishError;
|
|
658
|
+
} });
|
|
659
|
+
Object.defineProperty(exports2, "publishWorkflowEvent", { enumerable: true, get: function() {
|
|
660
|
+
return publisher_1.publishWorkflowEvent;
|
|
661
|
+
} });
|
|
662
|
+
Object.defineProperty(exports2, "workflowsClient", { enumerable: true, get: function() {
|
|
663
|
+
return publisher_1.workflowsClient;
|
|
664
|
+
} });
|
|
665
|
+
var consumer_1 = require_consumer();
|
|
666
|
+
Object.defineProperty(exports2, "InvalidWorkflowEventError", { enumerable: true, get: function() {
|
|
667
|
+
return consumer_1.InvalidWorkflowEventError;
|
|
668
|
+
} });
|
|
669
|
+
Object.defineProperty(exports2, "UnsupportedEnvelopeVersionError", { enumerable: true, get: function() {
|
|
670
|
+
return consumer_1.UnsupportedEnvelopeVersionError;
|
|
671
|
+
} });
|
|
672
|
+
Object.defineProperty(exports2, "parseWorkflowEvent", { enumerable: true, get: function() {
|
|
673
|
+
return consumer_1.parseWorkflowEvent;
|
|
674
|
+
} });
|
|
675
|
+
var dedup_1 = require_dedup();
|
|
676
|
+
Object.defineProperty(exports2, "WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS", { enumerable: true, get: function() {
|
|
677
|
+
return dedup_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
|
|
678
|
+
} });
|
|
679
|
+
Object.defineProperty(exports2, "WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH", { enumerable: true, get: function() {
|
|
680
|
+
return dedup_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH;
|
|
681
|
+
} });
|
|
682
|
+
Object.defineProperty(exports2, "WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR", { enumerable: true, get: function() {
|
|
683
|
+
return dedup_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR;
|
|
684
|
+
} });
|
|
685
|
+
Object.defineProperty(exports2, "WorkflowDedupInvalidInputError", { enumerable: true, get: function() {
|
|
686
|
+
return dedup_1.WorkflowDedupInvalidInputError;
|
|
687
|
+
} });
|
|
688
|
+
Object.defineProperty(exports2, "WorkflowDedupTableNameMissingError", { enumerable: true, get: function() {
|
|
689
|
+
return dedup_1.WorkflowDedupTableNameMissingError;
|
|
690
|
+
} });
|
|
691
|
+
Object.defineProperty(exports2, "encodeSortKey", { enumerable: true, get: function() {
|
|
692
|
+
return dedup_1.encodeSortKey;
|
|
693
|
+
} });
|
|
694
|
+
Object.defineProperty(exports2, "markFailed", { enumerable: true, get: function() {
|
|
695
|
+
return dedup_1.markFailed;
|
|
696
|
+
} });
|
|
697
|
+
Object.defineProperty(exports2, "recordIfAbsent", { enumerable: true, get: function() {
|
|
698
|
+
return dedup_1.recordIfAbsent;
|
|
699
|
+
} });
|
|
700
|
+
Object.defineProperty(exports2, "workflowDedupClient", { enumerable: true, get: function() {
|
|
701
|
+
return dedup_1.workflowDedupClient;
|
|
702
|
+
} });
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// src/workflows/control-plane/rename-cascade/rename-list-targets.handler.ts
|
|
707
|
+
var rename_list_targets_handler_exports = {};
|
|
708
|
+
__export(rename_list_targets_handler_exports, {
|
|
709
|
+
handler: () => handler
|
|
710
|
+
});
|
|
711
|
+
module.exports = __toCommonJS(rename_list_targets_handler_exports);
|
|
712
|
+
var import_node_crypto = require("crypto");
|
|
713
|
+
|
|
714
|
+
// src/data/operations/control/rename-cascade/rename-cascade-list-targets-operation.ts
|
|
715
|
+
var import_workflows = __toESM(require_lib());
|
|
716
|
+
|
|
717
|
+
// src/data/dynamo/dynamo-control-service.ts
|
|
718
|
+
var import_electrodb14 = require("electrodb");
|
|
719
|
+
|
|
720
|
+
// src/data/dynamo/dynamo-client.ts
|
|
721
|
+
var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
|
|
722
|
+
var defaultTableName = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
|
|
723
|
+
var dynamoClient = new import_client_dynamodb.DynamoDBClient({
|
|
724
|
+
...process.env.MOCK_DYNAMODB_ENDPOINT && {
|
|
725
|
+
endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
|
|
726
|
+
sslEnabled: false,
|
|
727
|
+
region: "local"
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// src/data/dynamo/entities/control/configuration-entity.ts
|
|
732
|
+
var import_electrodb = require("electrodb");
|
|
733
|
+
|
|
734
|
+
// src/data/dynamo/entities/control/control-entity-common.ts
|
|
735
|
+
var import_types = require("@openhi/types");
|
|
736
|
+
|
|
737
|
+
// src/data/dynamo/shard.ts
|
|
738
|
+
var SHARD_COUNT = 4;
|
|
739
|
+
function computeShard(id) {
|
|
740
|
+
let hash = 2166136261;
|
|
741
|
+
for (let i = 0; i < id.length; i++) {
|
|
742
|
+
hash ^= id.charCodeAt(i);
|
|
743
|
+
hash = Math.imul(hash, 16777619);
|
|
744
|
+
}
|
|
745
|
+
return (hash >>> 0) % SHARD_COUNT;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/data/dynamo/entities/control/control-entity-common.ts
|
|
749
|
+
var gsi1ShardAttribute = {
|
|
750
|
+
type: "string",
|
|
751
|
+
watch: ["id"],
|
|
752
|
+
set: (_val, item) => {
|
|
753
|
+
if (typeof item?.id !== "string" || item.id.length === 0) {
|
|
754
|
+
return void 0;
|
|
755
|
+
}
|
|
756
|
+
return String(computeShard(item.id));
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
var gsi1skAttribute = {
|
|
760
|
+
type: "string",
|
|
761
|
+
watch: ["resource", "lastUpdated", "id"],
|
|
762
|
+
set: (_val, item) => {
|
|
763
|
+
const id = typeof item?.id === "string" ? item.id : "";
|
|
764
|
+
const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
|
|
765
|
+
const fallback = `${lastUpdated}#${id}`;
|
|
766
|
+
if (typeof item?.resource !== "string" || item.resource.length === 0) {
|
|
767
|
+
return fallback;
|
|
768
|
+
}
|
|
769
|
+
let parsed;
|
|
770
|
+
try {
|
|
771
|
+
parsed = JSON.parse(item.resource);
|
|
772
|
+
} catch {
|
|
773
|
+
return fallback;
|
|
774
|
+
}
|
|
775
|
+
if (!parsed || typeof parsed !== "object") return fallback;
|
|
776
|
+
const resourceType = parsed.resourceType;
|
|
777
|
+
if (typeof resourceType !== "string") return fallback;
|
|
778
|
+
const label = (0, import_types.extractLabel)(parsed);
|
|
779
|
+
return label !== void 0 ? `${label}#${id}` : fallback;
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
function extractRoleId(resource) {
|
|
783
|
+
const flat = resource.roleId;
|
|
784
|
+
if (typeof flat === "string" && flat.length > 0) return flat;
|
|
785
|
+
const role = resource.role;
|
|
786
|
+
if (role && typeof role === "object") {
|
|
787
|
+
const reference = role.reference;
|
|
788
|
+
if (typeof reference === "string" && reference.length > 0) {
|
|
789
|
+
const slash = reference.lastIndexOf("/");
|
|
790
|
+
const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
|
|
791
|
+
if (tail.length > 0) return tail;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return void 0;
|
|
795
|
+
}
|
|
796
|
+
var roleAssignmentGsi1skAttribute = {
|
|
797
|
+
type: "string",
|
|
798
|
+
watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
|
|
799
|
+
set: (_val, item) => {
|
|
800
|
+
const id = typeof item?.id === "string" ? item.id : "";
|
|
801
|
+
const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
|
|
802
|
+
const fallback = `${lastUpdated}#${id}`;
|
|
803
|
+
if (typeof item?.resource !== "string" || item.resource.length === 0) {
|
|
804
|
+
return fallback;
|
|
805
|
+
}
|
|
806
|
+
let parsed;
|
|
807
|
+
try {
|
|
808
|
+
parsed = JSON.parse(item.resource);
|
|
809
|
+
} catch {
|
|
810
|
+
return fallback;
|
|
811
|
+
}
|
|
812
|
+
if (!parsed || typeof parsed !== "object") return fallback;
|
|
813
|
+
const roleId = extractRoleId(parsed);
|
|
814
|
+
if (roleId === void 0) return fallback;
|
|
815
|
+
const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
|
|
816
|
+
const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
|
|
817
|
+
if (normalizedUserName.length === 0) return fallback;
|
|
818
|
+
return `${roleId}#${normalizedUserName}#${id}`;
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
var membershipGsi1skAttribute = {
|
|
822
|
+
type: "string",
|
|
823
|
+
watch: ["denormalizedUserName", "lastUpdated", "id"],
|
|
824
|
+
set: (_val, item) => {
|
|
825
|
+
const id = typeof item?.id === "string" ? item.id : "";
|
|
826
|
+
const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
|
|
827
|
+
const fallback = `${lastUpdated}#${id}`;
|
|
828
|
+
const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
|
|
829
|
+
const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
|
|
830
|
+
if (normalizedUserName.length === 0) {
|
|
831
|
+
return fallback;
|
|
832
|
+
}
|
|
833
|
+
return `${normalizedUserName}#${id}`;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// src/data/dynamo/entities/control/configuration-entity.ts
|
|
838
|
+
var ConfigurationEntity = new import_electrodb.Entity({
|
|
839
|
+
model: {
|
|
840
|
+
entity: "configuration",
|
|
841
|
+
service: "control",
|
|
842
|
+
version: "01"
|
|
843
|
+
},
|
|
844
|
+
attributes: {
|
|
845
|
+
/** Sort key. "CURRENT" for current version; version history in S3. */
|
|
846
|
+
sk: {
|
|
847
|
+
type: "string",
|
|
848
|
+
required: true,
|
|
849
|
+
default: "CURRENT"
|
|
850
|
+
},
|
|
851
|
+
/** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
|
|
852
|
+
tenantId: {
|
|
853
|
+
type: "string",
|
|
854
|
+
required: true,
|
|
855
|
+
default: "BASELINE"
|
|
856
|
+
},
|
|
857
|
+
/** Workspace scope. Use "-" when absent. */
|
|
858
|
+
workspaceId: {
|
|
859
|
+
type: "string",
|
|
860
|
+
required: true,
|
|
861
|
+
default: "-"
|
|
862
|
+
},
|
|
863
|
+
/** User scope. Use "-" when absent. */
|
|
864
|
+
userId: {
|
|
865
|
+
type: "string",
|
|
866
|
+
required: true,
|
|
867
|
+
default: "-"
|
|
868
|
+
},
|
|
869
|
+
/** Role scope. Use "-" when absent. */
|
|
870
|
+
roleId: {
|
|
871
|
+
type: "string",
|
|
872
|
+
required: true,
|
|
873
|
+
default: "-"
|
|
874
|
+
},
|
|
875
|
+
/** Config type (category), e.g. endpoints, branding, display. */
|
|
876
|
+
key: {
|
|
877
|
+
type: "string",
|
|
878
|
+
required: true
|
|
879
|
+
},
|
|
880
|
+
/** FHIR Resource.id; logical id in URL and for the Configuration resource. */
|
|
881
|
+
id: {
|
|
882
|
+
type: "string",
|
|
883
|
+
required: true
|
|
884
|
+
},
|
|
885
|
+
/** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
|
|
886
|
+
resource: {
|
|
887
|
+
type: "string",
|
|
888
|
+
required: true
|
|
889
|
+
},
|
|
890
|
+
/**
|
|
891
|
+
* Summary projection (key display fields as JSON string: id, key, status).
|
|
892
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
893
|
+
*/
|
|
894
|
+
summary: {
|
|
895
|
+
type: "string",
|
|
896
|
+
required: true
|
|
897
|
+
},
|
|
898
|
+
/** Version id (e.g. ULID). Tracks current version; S3 history key. */
|
|
899
|
+
vid: {
|
|
900
|
+
type: "string",
|
|
901
|
+
required: true
|
|
902
|
+
},
|
|
903
|
+
lastUpdated: {
|
|
904
|
+
type: "string",
|
|
905
|
+
required: true
|
|
906
|
+
},
|
|
907
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
908
|
+
deleted: {
|
|
909
|
+
type: "boolean",
|
|
910
|
+
required: false
|
|
911
|
+
},
|
|
912
|
+
bundleId: {
|
|
913
|
+
type: "string",
|
|
914
|
+
required: false
|
|
915
|
+
},
|
|
916
|
+
msgId: {
|
|
917
|
+
type: "string",
|
|
918
|
+
required: false
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
indexes: {
|
|
922
|
+
/** 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. */
|
|
923
|
+
record: {
|
|
924
|
+
pk: {
|
|
925
|
+
field: "PK",
|
|
926
|
+
composite: ["tenantId", "workspaceId", "userId", "roleId"],
|
|
927
|
+
template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
|
|
928
|
+
},
|
|
929
|
+
sk: {
|
|
930
|
+
field: "SK",
|
|
931
|
+
composite: ["key", "sk"],
|
|
932
|
+
template: "KEY#${key}#SK#${sk}"
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
/**
|
|
936
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
|
|
937
|
+
* (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
|
|
938
|
+
* (workspaceId = "-") or "list configs scoped to this workspace". Does not support
|
|
939
|
+
* hierarchical resolution in one query; use base table GetItem in fallback order
|
|
940
|
+
* (user → workspace → tenant → baseline) for that.
|
|
941
|
+
* SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
|
|
942
|
+
* config category: endpoints, branding, display, …) and the natural sort/lookup
|
|
943
|
+
* dimension. `casing: "none"` preserves the literal key value.
|
|
944
|
+
*/
|
|
945
|
+
gsi1: {
|
|
946
|
+
index: "GSI1",
|
|
947
|
+
pk: {
|
|
948
|
+
field: "GSI1PK",
|
|
949
|
+
composite: ["tenantId", "workspaceId", "gsi1Shard"],
|
|
950
|
+
template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
|
|
951
|
+
},
|
|
952
|
+
sk: {
|
|
953
|
+
field: "GSI1SK",
|
|
954
|
+
casing: "none",
|
|
955
|
+
composite: ["key", "id"],
|
|
956
|
+
template: "${key}#${id}"
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// src/data/dynamo/entities/control/configuration-user-projection-entity.ts
|
|
963
|
+
var import_electrodb2 = require("electrodb");
|
|
964
|
+
var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
|
|
965
|
+
model: {
|
|
966
|
+
entity: "configurationUserProjection",
|
|
967
|
+
service: "control",
|
|
968
|
+
version: "01"
|
|
969
|
+
},
|
|
970
|
+
attributes: {
|
|
971
|
+
/**
|
|
972
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
973
|
+
* base-table PK. Always required — the projection has no meaning
|
|
974
|
+
* outside a user partition.
|
|
975
|
+
*/
|
|
976
|
+
userId: {
|
|
977
|
+
type: "string",
|
|
978
|
+
required: true
|
|
979
|
+
},
|
|
980
|
+
/**
|
|
981
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
982
|
+
* writer via `buildConfigurationUserProjectionSk`. The entity stores
|
|
983
|
+
* the value verbatim so the SK grammar (pattern #10 user-scope) is
|
|
984
|
+
* owned by the operations layer, not duplicated here.
|
|
985
|
+
*/
|
|
986
|
+
sk: {
|
|
987
|
+
type: "string",
|
|
988
|
+
required: true
|
|
989
|
+
},
|
|
990
|
+
/**
|
|
991
|
+
* Configuration canonical-record id. Stored as a discriminating
|
|
992
|
+
* field so consumers can hydrate the canonical row via the
|
|
993
|
+
* Configuration get-by-id operation when the projection's `summary`
|
|
994
|
+
* is insufficient.
|
|
995
|
+
*/
|
|
996
|
+
configurationId: {
|
|
997
|
+
type: "string",
|
|
998
|
+
required: true
|
|
999
|
+
},
|
|
1000
|
+
/**
|
|
1001
|
+
* Tenant the Configuration is associated with. The canonical row
|
|
1002
|
+
* keys off `(tenantId, workspaceId, userId, roleId)`; the projection
|
|
1003
|
+
* carries `tenantId` so consumers reconstructing the canonical PK
|
|
1004
|
+
* have the tenant segment without a hop.
|
|
1005
|
+
*/
|
|
1006
|
+
tenantId: {
|
|
1007
|
+
type: "string",
|
|
1008
|
+
required: true
|
|
1009
|
+
},
|
|
1010
|
+
/**
|
|
1011
|
+
* Scope marker. Always `"user"` on this projection — recorded
|
|
1012
|
+
* explicitly so future scope-bearing projections (workspace,
|
|
1013
|
+
* tenant, role) can share filter semantics in a unified
|
|
1014
|
+
* cross-projection list query if one ever lands.
|
|
1015
|
+
*/
|
|
1016
|
+
scope: {
|
|
1017
|
+
type: "string",
|
|
1018
|
+
required: true,
|
|
1019
|
+
default: "user"
|
|
1020
|
+
},
|
|
1021
|
+
/**
|
|
1022
|
+
* Configuration's `key` attribute (config category, e.g. endpoints,
|
|
1023
|
+
* branding, display). Mirrored from the canonical row so consumers
|
|
1024
|
+
* reading the projection get the natural display label without a
|
|
1025
|
+
* BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
|
|
1026
|
+
* the SK.
|
|
1027
|
+
*/
|
|
1028
|
+
displayName: {
|
|
1029
|
+
type: "string",
|
|
1030
|
+
required: false
|
|
1031
|
+
},
|
|
1032
|
+
/**
|
|
1033
|
+
* Summary projection (key display fields as JSON string) — mirrored
|
|
1034
|
+
* from the canonical Configuration row so user-partition queries do
|
|
1035
|
+
* not need a BatchGet hop.
|
|
1036
|
+
*/
|
|
1037
|
+
summary: {
|
|
1038
|
+
type: "string",
|
|
1039
|
+
required: true
|
|
1040
|
+
},
|
|
1041
|
+
/** Version id mirrored from the canonical Configuration row. */
|
|
1042
|
+
vid: {
|
|
1043
|
+
type: "string",
|
|
1044
|
+
required: true
|
|
1045
|
+
},
|
|
1046
|
+
/** Last-updated timestamp mirrored from the canonical Configuration row. */
|
|
1047
|
+
lastUpdated: {
|
|
1048
|
+
type: "string",
|
|
1049
|
+
required: true
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
indexes: {
|
|
1053
|
+
/**
|
|
1054
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
|
|
1055
|
+
* single `Query(PK = USER#ID#<userId>, SK begins_with
|
|
1056
|
+
* 'CONFIGURATION#')` returns the user's user-scoped Configurations
|
|
1057
|
+
* sorted by `<normalizedConfigName>` (then `<configurationId>` as
|
|
1058
|
+
* the tiebreaker).
|
|
1059
|
+
*/
|
|
1060
|
+
record: {
|
|
1061
|
+
pk: {
|
|
1062
|
+
field: "PK",
|
|
1063
|
+
composite: ["userId"],
|
|
1064
|
+
template: "USER#ID#${userId}"
|
|
1065
|
+
},
|
|
1066
|
+
sk: {
|
|
1067
|
+
field: "SK",
|
|
1068
|
+
casing: "none",
|
|
1069
|
+
composite: ["sk"],
|
|
1070
|
+
template: "${sk}"
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
|
|
1077
|
+
var import_electrodb3 = require("electrodb");
|
|
1078
|
+
var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
|
|
1079
|
+
model: {
|
|
1080
|
+
entity: "configurationWorkspaceProjection",
|
|
1081
|
+
service: "control",
|
|
1082
|
+
version: "01"
|
|
1083
|
+
},
|
|
1084
|
+
attributes: {
|
|
1085
|
+
/**
|
|
1086
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
1087
|
+
* of the base-table PK. Always required — the workspace partition
|
|
1088
|
+
* is tenant-scoped per ADR-011.
|
|
1089
|
+
*/
|
|
1090
|
+
tenantId: {
|
|
1091
|
+
type: "string",
|
|
1092
|
+
required: true
|
|
1093
|
+
},
|
|
1094
|
+
/**
|
|
1095
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
1096
|
+
* segment of the base-table PK
|
|
1097
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
1098
|
+
* the projection has no meaning outside a workspace partition.
|
|
1099
|
+
*/
|
|
1100
|
+
workspaceId: {
|
|
1101
|
+
type: "string",
|
|
1102
|
+
required: true
|
|
1103
|
+
},
|
|
1104
|
+
/**
|
|
1105
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
1106
|
+
* writer via `buildConfigurationWorkspaceProjectionSk`. The entity
|
|
1107
|
+
* stores the value verbatim so the SK grammar (pattern #10
|
|
1108
|
+
* workspace-scope) is owned by the operations layer, not
|
|
1109
|
+
* duplicated here.
|
|
1110
|
+
*/
|
|
1111
|
+
sk: {
|
|
1112
|
+
type: "string",
|
|
1113
|
+
required: true
|
|
1114
|
+
},
|
|
1115
|
+
/**
|
|
1116
|
+
* Configuration canonical-record id. Stored as a discriminating
|
|
1117
|
+
* field so consumers can hydrate the canonical row via the
|
|
1118
|
+
* Configuration get-by-id operation when the projection's `summary`
|
|
1119
|
+
* is insufficient.
|
|
1120
|
+
*/
|
|
1121
|
+
configurationId: {
|
|
1122
|
+
type: "string",
|
|
1123
|
+
required: true
|
|
1124
|
+
},
|
|
1125
|
+
/**
|
|
1126
|
+
* Scope marker. Always `"workspace"` on this projection — recorded
|
|
1127
|
+
* explicitly so future scope-bearing projections (user, tenant,
|
|
1128
|
+
* role) can share filter semantics in a unified cross-projection
|
|
1129
|
+
* list query if one ever lands.
|
|
1130
|
+
*/
|
|
1131
|
+
scope: {
|
|
1132
|
+
type: "string",
|
|
1133
|
+
required: true,
|
|
1134
|
+
default: "workspace"
|
|
1135
|
+
},
|
|
1136
|
+
/**
|
|
1137
|
+
* Configuration's `key` attribute (config category, e.g. endpoints,
|
|
1138
|
+
* branding, display). Mirrored from the canonical row so consumers
|
|
1139
|
+
* reading the projection get the natural display label without a
|
|
1140
|
+
* BatchGet hop. Doubles as the source of `<normalizedConfigName>`
|
|
1141
|
+
* in the SK.
|
|
1142
|
+
*/
|
|
1143
|
+
displayName: {
|
|
1144
|
+
type: "string",
|
|
1145
|
+
required: false
|
|
1146
|
+
},
|
|
1147
|
+
/**
|
|
1148
|
+
* Summary projection (key display fields as JSON string) — mirrored
|
|
1149
|
+
* from the canonical Configuration row so workspace-partition
|
|
1150
|
+
* queries do not need a BatchGet hop.
|
|
1151
|
+
*/
|
|
1152
|
+
summary: {
|
|
1153
|
+
type: "string",
|
|
1154
|
+
required: true
|
|
1155
|
+
},
|
|
1156
|
+
/** Version id mirrored from the canonical Configuration row. */
|
|
1157
|
+
vid: {
|
|
1158
|
+
type: "string",
|
|
1159
|
+
required: true
|
|
1160
|
+
},
|
|
1161
|
+
/** Last-updated timestamp mirrored from the canonical Configuration row. */
|
|
1162
|
+
lastUpdated: {
|
|
1163
|
+
type: "string",
|
|
1164
|
+
required: true
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
indexes: {
|
|
1168
|
+
/**
|
|
1169
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
1170
|
+
* SK = operation-supplied. A single
|
|
1171
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
|
|
1172
|
+
* returns the workspace's workspace-scoped Configurations sorted by
|
|
1173
|
+
* `<normalizedConfigName>` (then `<configurationId>` as the
|
|
1174
|
+
* tiebreaker).
|
|
1175
|
+
*/
|
|
1176
|
+
record: {
|
|
1177
|
+
pk: {
|
|
1178
|
+
field: "PK",
|
|
1179
|
+
composite: ["tenantId", "workspaceId"],
|
|
1180
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
1181
|
+
},
|
|
1182
|
+
sk: {
|
|
1183
|
+
field: "SK",
|
|
1184
|
+
casing: "none",
|
|
1185
|
+
composite: ["sk"],
|
|
1186
|
+
template: "${sk}"
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// src/data/dynamo/entities/control/membership-entity.ts
|
|
1193
|
+
var import_electrodb4 = require("electrodb");
|
|
1194
|
+
var MembershipEntity = new import_electrodb4.Entity({
|
|
1195
|
+
model: {
|
|
1196
|
+
entity: "membership",
|
|
1197
|
+
service: "control",
|
|
1198
|
+
version: "01"
|
|
1199
|
+
},
|
|
1200
|
+
attributes: {
|
|
1201
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
1202
|
+
sk: {
|
|
1203
|
+
type: "string",
|
|
1204
|
+
required: true,
|
|
1205
|
+
default: "CURRENT"
|
|
1206
|
+
},
|
|
1207
|
+
/** Tenant in which the user has membership (required). */
|
|
1208
|
+
tenantId: {
|
|
1209
|
+
type: "string",
|
|
1210
|
+
required: true
|
|
1211
|
+
},
|
|
1212
|
+
/** FHIR Resource.id; membership id. */
|
|
1213
|
+
id: {
|
|
1214
|
+
type: "string",
|
|
1215
|
+
required: true
|
|
1216
|
+
},
|
|
1217
|
+
/** Full Membership resource serialized as JSON string. */
|
|
1218
|
+
resource: {
|
|
1219
|
+
type: "string",
|
|
1220
|
+
required: true
|
|
1221
|
+
},
|
|
1222
|
+
/**
|
|
1223
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
1224
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
1225
|
+
*/
|
|
1226
|
+
summary: {
|
|
1227
|
+
type: "string",
|
|
1228
|
+
required: true
|
|
1229
|
+
},
|
|
1230
|
+
/** Version id (e.g. ULID). */
|
|
1231
|
+
vid: {
|
|
1232
|
+
type: "string",
|
|
1233
|
+
required: true
|
|
1234
|
+
},
|
|
1235
|
+
lastUpdated: {
|
|
1236
|
+
type: "string",
|
|
1237
|
+
required: true
|
|
1238
|
+
},
|
|
1239
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
1240
|
+
/**
|
|
1241
|
+
* Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
|
|
1242
|
+
* pattern #1 so a GSI1 query partitioned on the tenant range-scans
|
|
1243
|
+
* by user-name prefix and returns memberships sorted by user name.
|
|
1244
|
+
* Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
|
|
1245
|
+
* is missing.
|
|
1246
|
+
*/
|
|
1247
|
+
gsi1sk: membershipGsi1skAttribute,
|
|
1248
|
+
deleted: {
|
|
1249
|
+
type: "boolean",
|
|
1250
|
+
required: false
|
|
1251
|
+
},
|
|
1252
|
+
bundleId: {
|
|
1253
|
+
type: "string",
|
|
1254
|
+
required: false
|
|
1255
|
+
},
|
|
1256
|
+
msgId: {
|
|
1257
|
+
type: "string",
|
|
1258
|
+
required: false
|
|
1259
|
+
},
|
|
1260
|
+
/**
|
|
1261
|
+
* Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
|
|
1262
|
+
* Populated from the FHIR extension on the Membership resource at write
|
|
1263
|
+
* time so future GSIs can index data-plane identity lookups without
|
|
1264
|
+
* deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
|
|
1265
|
+
*/
|
|
1266
|
+
linkedDataIdentityRef: {
|
|
1267
|
+
type: "string",
|
|
1268
|
+
required: false
|
|
1269
|
+
},
|
|
1270
|
+
/**
|
|
1271
|
+
* Denormalized display name of the linked Tenant, captured at row
|
|
1272
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1273
|
+
* adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
|
|
1274
|
+
* can be composed from a top-level field instead of digging into the
|
|
1275
|
+
* `resource` JSON. Optional on the schema so pre-TR-024 rows do not
|
|
1276
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
1277
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
1278
|
+
* source = canonical Tenant.displayName).
|
|
1279
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1280
|
+
*/
|
|
1281
|
+
denormalizedTenantName: {
|
|
1282
|
+
type: "string",
|
|
1283
|
+
required: false
|
|
1284
|
+
},
|
|
1285
|
+
/**
|
|
1286
|
+
* Denormalized display name of the linked User, captured at row
|
|
1287
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1288
|
+
* adjacency-list canonical-record GSI1SK (pattern #1 —
|
|
1289
|
+
* `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
|
|
1290
|
+
* can be composed from a top-level field. Optional on the schema so
|
|
1291
|
+
* pre-TR-024 rows do not break; the operations-layer multi-write helper
|
|
1292
|
+
* (#1010) makes the field load-bearing at write time per TR-024 rule 2
|
|
1293
|
+
* (write-time source = canonical User.displayName).
|
|
1294
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1295
|
+
*/
|
|
1296
|
+
denormalizedUserName: {
|
|
1297
|
+
type: "string",
|
|
1298
|
+
required: false
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
indexes: {
|
|
1302
|
+
/** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
1303
|
+
record: {
|
|
1304
|
+
pk: {
|
|
1305
|
+
field: "PK",
|
|
1306
|
+
composite: ["tenantId", "id"],
|
|
1307
|
+
template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
|
|
1308
|
+
},
|
|
1309
|
+
sk: {
|
|
1310
|
+
field: "SK",
|
|
1311
|
+
composite: ["sk"],
|
|
1312
|
+
template: "${sk}"
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
/**
|
|
1316
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
|
|
1317
|
+
* four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
|
|
1318
|
+
* SK is derived via `membershipGsi1skAttribute` — composes
|
|
1319
|
+
* `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
|
|
1320
|
+
* tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
|
|
1321
|
+
* when `denormalizedUserName` is missing. `casing: "none"` preserves
|
|
1322
|
+
* the normalized label and ISO-8601 `T`/`Z`.
|
|
1323
|
+
*/
|
|
1324
|
+
gsi1: {
|
|
1325
|
+
index: "GSI1",
|
|
1326
|
+
pk: {
|
|
1327
|
+
field: "GSI1PK",
|
|
1328
|
+
composite: ["tenantId", "gsi1Shard"],
|
|
1329
|
+
template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
|
|
1330
|
+
},
|
|
1331
|
+
sk: {
|
|
1332
|
+
field: "GSI1SK",
|
|
1333
|
+
casing: "none",
|
|
1334
|
+
composite: ["gsi1sk"],
|
|
1335
|
+
template: "${gsi1sk}"
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
// src/data/dynamo/entities/control/membership-user-projection-entity.ts
|
|
1342
|
+
var import_electrodb5 = require("electrodb");
|
|
1343
|
+
var MembershipUserProjectionEntity = new import_electrodb5.Entity({
|
|
1344
|
+
model: {
|
|
1345
|
+
entity: "membershipUserProjection",
|
|
1346
|
+
service: "control",
|
|
1347
|
+
version: "01"
|
|
1348
|
+
},
|
|
1349
|
+
attributes: {
|
|
1350
|
+
/**
|
|
1351
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
1352
|
+
* base-table PK. Always required — the projection has no meaning
|
|
1353
|
+
* outside a user partition.
|
|
1354
|
+
*/
|
|
1355
|
+
userId: {
|
|
1356
|
+
type: "string",
|
|
1357
|
+
required: true
|
|
1358
|
+
},
|
|
1359
|
+
/**
|
|
1360
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
1361
|
+
* writer via `buildMembershipUserProjectionSk*` helpers. The entity
|
|
1362
|
+
* stores the value verbatim so the SK grammar (patterns #3 and #4)
|
|
1363
|
+
* is owned by the operations layer, not duplicated here.
|
|
1364
|
+
*/
|
|
1365
|
+
sk: {
|
|
1366
|
+
type: "string",
|
|
1367
|
+
required: true
|
|
1368
|
+
},
|
|
1369
|
+
/** Tenant in which the membership applies. Always required. */
|
|
1370
|
+
tenantId: {
|
|
1371
|
+
type: "string",
|
|
1372
|
+
required: true
|
|
1373
|
+
},
|
|
1374
|
+
/**
|
|
1375
|
+
* Workspace the membership scopes to. Present iff the projection
|
|
1376
|
+
* row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
|
|
1377
|
+
* tenant sub-lane rows.
|
|
1378
|
+
*/
|
|
1379
|
+
workspaceId: {
|
|
1380
|
+
type: "string",
|
|
1381
|
+
required: false
|
|
1382
|
+
},
|
|
1383
|
+
/**
|
|
1384
|
+
* Membership canonical-record id. Stored as a discriminating field
|
|
1385
|
+
* so consumers can hydrate the canonical row via
|
|
1386
|
+
* `MembershipEntity.get({ tenantId, id: membershipId })` when the
|
|
1387
|
+
* projection's `summary` is insufficient.
|
|
1388
|
+
*/
|
|
1389
|
+
membershipId: {
|
|
1390
|
+
type: "string",
|
|
1391
|
+
required: true
|
|
1392
|
+
},
|
|
1393
|
+
/**
|
|
1394
|
+
* Summary projection (key display fields as JSON string: id,
|
|
1395
|
+
* displayName, status) — mirrored from the canonical Membership row
|
|
1396
|
+
* so user-partition queries do not need a BatchGet hop.
|
|
1397
|
+
*/
|
|
1398
|
+
summary: {
|
|
1399
|
+
type: "string",
|
|
1400
|
+
required: true
|
|
1401
|
+
},
|
|
1402
|
+
/** Version id mirrored from the canonical Membership row. */
|
|
1403
|
+
vid: {
|
|
1404
|
+
type: "string",
|
|
1405
|
+
required: true
|
|
1406
|
+
},
|
|
1407
|
+
/** Last-updated timestamp mirrored from the canonical Membership row. */
|
|
1408
|
+
lastUpdated: {
|
|
1409
|
+
type: "string",
|
|
1410
|
+
required: true
|
|
1411
|
+
},
|
|
1412
|
+
/**
|
|
1413
|
+
* Denormalized Tenant display name — required to compose pattern-#3
|
|
1414
|
+
* SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
|
|
1415
|
+
* schema because pre-TR-024 rows may not carry a display name; the
|
|
1416
|
+
* operations layer falls back gracefully when missing.
|
|
1417
|
+
*/
|
|
1418
|
+
denormalizedTenantName: {
|
|
1419
|
+
type: "string",
|
|
1420
|
+
required: false
|
|
1421
|
+
},
|
|
1422
|
+
/**
|
|
1423
|
+
* Denormalized User display name — mirrored from the canonical
|
|
1424
|
+
* Membership row per TR-024 rule 3 (canonical-record symmetry).
|
|
1425
|
+
* Carried on the projection so consumers can render the user's
|
|
1426
|
+
* display name without a hop to the User record.
|
|
1427
|
+
*/
|
|
1428
|
+
denormalizedUserName: {
|
|
1429
|
+
type: "string",
|
|
1430
|
+
required: false
|
|
1431
|
+
},
|
|
1432
|
+
/**
|
|
1433
|
+
* Denormalized Workspace display name — required to compose
|
|
1434
|
+
* pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
|
|
1435
|
+
* Optional on the schema (TR-024 § Open Item #4 defers a formal
|
|
1436
|
+
* Workspace-rename cascade); the operations layer falls back to a
|
|
1437
|
+
* sentinel when missing so the SK still has a valid shape.
|
|
1438
|
+
*/
|
|
1439
|
+
denormalizedWorkspaceName: {
|
|
1440
|
+
type: "string",
|
|
1441
|
+
required: false
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
indexes: {
|
|
1445
|
+
/**
|
|
1446
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied.
|
|
1447
|
+
* Both pattern #3 and pattern #4 use this same index — the SK string
|
|
1448
|
+
* encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
|
|
1449
|
+
* `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
|
|
1450
|
+
* SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
|
|
1451
|
+
*/
|
|
1452
|
+
record: {
|
|
1453
|
+
pk: {
|
|
1454
|
+
field: "PK",
|
|
1455
|
+
composite: ["userId"],
|
|
1456
|
+
template: "USER#ID#${userId}"
|
|
1457
|
+
},
|
|
1458
|
+
sk: {
|
|
1459
|
+
field: "SK",
|
|
1460
|
+
casing: "none",
|
|
1461
|
+
composite: ["sk"],
|
|
1462
|
+
template: "${sk}"
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
// src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
|
|
1469
|
+
var import_electrodb6 = require("electrodb");
|
|
1470
|
+
var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
|
|
1471
|
+
model: {
|
|
1472
|
+
entity: "membershipWorkspaceProjection",
|
|
1473
|
+
service: "control",
|
|
1474
|
+
version: "01"
|
|
1475
|
+
},
|
|
1476
|
+
attributes: {
|
|
1477
|
+
/**
|
|
1478
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
1479
|
+
* of the base-table PK. Always required — the workspace partition
|
|
1480
|
+
* is tenant-scoped per ADR-011.
|
|
1481
|
+
*/
|
|
1482
|
+
tenantId: {
|
|
1483
|
+
type: "string",
|
|
1484
|
+
required: true
|
|
1485
|
+
},
|
|
1486
|
+
/**
|
|
1487
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
1488
|
+
* segment of the base-table PK
|
|
1489
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
1490
|
+
* the projection has no meaning outside a workspace partition.
|
|
1491
|
+
*/
|
|
1492
|
+
workspaceId: {
|
|
1493
|
+
type: "string",
|
|
1494
|
+
required: true
|
|
1495
|
+
},
|
|
1496
|
+
/**
|
|
1497
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
1498
|
+
* writer via `buildMembershipWorkspaceProjectionSk`. The entity
|
|
1499
|
+
* stores the value verbatim so the SK grammar (pattern #2) is
|
|
1500
|
+
* owned by the operations layer, not duplicated here.
|
|
1501
|
+
*/
|
|
1502
|
+
sk: {
|
|
1503
|
+
type: "string",
|
|
1504
|
+
required: true
|
|
1505
|
+
},
|
|
1506
|
+
/**
|
|
1507
|
+
* User the membership links. Stored as a discriminating field so
|
|
1508
|
+
* consumers can hydrate the canonical User row via
|
|
1509
|
+
* `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
|
|
1510
|
+
* projection's `summary` is insufficient.
|
|
1511
|
+
*/
|
|
1512
|
+
userId: {
|
|
1513
|
+
type: "string",
|
|
1514
|
+
required: true
|
|
1515
|
+
},
|
|
1516
|
+
/**
|
|
1517
|
+
* Membership canonical-record id. Stored as a discriminating field
|
|
1518
|
+
* so consumers can hydrate the canonical row via
|
|
1519
|
+
* `MembershipEntity.get({ tenantId, id: membershipId })` when the
|
|
1520
|
+
* projection's `summary` is insufficient.
|
|
1521
|
+
*/
|
|
1522
|
+
membershipId: {
|
|
1523
|
+
type: "string",
|
|
1524
|
+
required: true
|
|
1525
|
+
},
|
|
1526
|
+
/**
|
|
1527
|
+
* Summary projection (key display fields as JSON string: id,
|
|
1528
|
+
* displayName, status) — mirrored from the canonical Membership row
|
|
1529
|
+
* so workspace-partition queries do not need a BatchGet hop.
|
|
1530
|
+
*/
|
|
1531
|
+
summary: {
|
|
1532
|
+
type: "string",
|
|
1533
|
+
required: true
|
|
1534
|
+
},
|
|
1535
|
+
/** Version id mirrored from the canonical Membership row. */
|
|
1536
|
+
vid: {
|
|
1537
|
+
type: "string",
|
|
1538
|
+
required: true
|
|
1539
|
+
},
|
|
1540
|
+
/** Last-updated timestamp mirrored from the canonical Membership row. */
|
|
1541
|
+
lastUpdated: {
|
|
1542
|
+
type: "string",
|
|
1543
|
+
required: true
|
|
1544
|
+
},
|
|
1545
|
+
/**
|
|
1546
|
+
* Denormalized User display name — required to compose the
|
|
1547
|
+
* pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
|
|
1548
|
+
* the schema because pre-TR-024 rows may not carry a display name;
|
|
1549
|
+
* the operations layer falls back to a sentinel when missing so
|
|
1550
|
+
* the SK still has a valid shape. The TR-023 rename-cascade
|
|
1551
|
+
* pipeline rewrites the SK on a User rename.
|
|
1552
|
+
*/
|
|
1553
|
+
denormalizedUserName: {
|
|
1554
|
+
type: "string",
|
|
1555
|
+
required: false
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1558
|
+
indexes: {
|
|
1559
|
+
/**
|
|
1560
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
1561
|
+
* SK = operation-supplied. Pattern #2 uses this index — the SK
|
|
1562
|
+
* encodes the entity-type prefix (`MEMBERSHIP#…`) so a
|
|
1563
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
|
|
1564
|
+
* returns every member projection for the workspace in normalized-
|
|
1565
|
+
* user-name sort order.
|
|
1566
|
+
*/
|
|
1567
|
+
record: {
|
|
1568
|
+
pk: {
|
|
1569
|
+
field: "PK",
|
|
1570
|
+
composite: ["tenantId", "workspaceId"],
|
|
1571
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
1572
|
+
},
|
|
1573
|
+
sk: {
|
|
1574
|
+
field: "SK",
|
|
1575
|
+
casing: "none",
|
|
1576
|
+
composite: ["sk"],
|
|
1577
|
+
template: "${sk}"
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
// src/data/dynamo/entities/control/role-entity.ts
|
|
1584
|
+
var import_electrodb7 = require("electrodb");
|
|
1585
|
+
var RoleEntity = new import_electrodb7.Entity({
|
|
1586
|
+
model: {
|
|
1587
|
+
entity: "role",
|
|
1588
|
+
service: "control",
|
|
1589
|
+
version: "01"
|
|
1590
|
+
},
|
|
1591
|
+
attributes: {
|
|
1592
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
1593
|
+
sk: {
|
|
1594
|
+
type: "string",
|
|
1595
|
+
required: true,
|
|
1596
|
+
default: "CURRENT"
|
|
1597
|
+
},
|
|
1598
|
+
/** FHIR Resource.id; role id. */
|
|
1599
|
+
id: {
|
|
1600
|
+
type: "string",
|
|
1601
|
+
required: true
|
|
1602
|
+
},
|
|
1603
|
+
/** Full Role resource serialized as JSON string. */
|
|
1604
|
+
resource: {
|
|
1605
|
+
type: "string",
|
|
1606
|
+
required: true
|
|
1607
|
+
},
|
|
1608
|
+
/**
|
|
1609
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
1610
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
1611
|
+
*/
|
|
1612
|
+
summary: {
|
|
1613
|
+
type: "string",
|
|
1614
|
+
required: true
|
|
1615
|
+
},
|
|
1616
|
+
/** Version id (e.g. ULID). */
|
|
1617
|
+
vid: {
|
|
1618
|
+
type: "string",
|
|
1619
|
+
required: true
|
|
1620
|
+
},
|
|
1621
|
+
lastUpdated: {
|
|
1622
|
+
type: "string",
|
|
1623
|
+
required: true
|
|
1624
|
+
},
|
|
1625
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
1626
|
+
/** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
|
|
1627
|
+
gsi1sk: gsi1skAttribute,
|
|
1628
|
+
deleted: {
|
|
1629
|
+
type: "boolean",
|
|
1630
|
+
required: false
|
|
1631
|
+
},
|
|
1632
|
+
bundleId: {
|
|
1633
|
+
type: "string",
|
|
1634
|
+
required: false
|
|
1635
|
+
},
|
|
1636
|
+
msgId: {
|
|
1637
|
+
type: "string",
|
|
1638
|
+
required: false
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
indexes: {
|
|
1642
|
+
/** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
1643
|
+
record: {
|
|
1644
|
+
pk: {
|
|
1645
|
+
field: "PK",
|
|
1646
|
+
composite: ["id"],
|
|
1647
|
+
template: "ROLE#ID#${id}"
|
|
1648
|
+
},
|
|
1649
|
+
sk: {
|
|
1650
|
+
field: "SK",
|
|
1651
|
+
composite: ["sk"],
|
|
1652
|
+
template: "${sk}"
|
|
1653
|
+
}
|
|
1654
|
+
},
|
|
1655
|
+
/**
|
|
1656
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
|
|
1657
|
+
* Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
|
|
1658
|
+
* SK is derived via `gsi1skAttribute` — uses the resource's natural label when
|
|
1659
|
+
* extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
|
|
1660
|
+
* normalized label and ISO-8601 `T`/`Z`.
|
|
1661
|
+
*/
|
|
1662
|
+
gsi1: {
|
|
1663
|
+
index: "GSI1",
|
|
1664
|
+
pk: {
|
|
1665
|
+
field: "GSI1PK",
|
|
1666
|
+
composite: ["gsi1Shard"],
|
|
1667
|
+
template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
|
|
1668
|
+
},
|
|
1669
|
+
sk: {
|
|
1670
|
+
field: "GSI1SK",
|
|
1671
|
+
casing: "none",
|
|
1672
|
+
composite: ["gsi1sk"],
|
|
1673
|
+
template: "${gsi1sk}"
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
// src/data/dynamo/entities/control/roleassignment-entity.ts
|
|
1680
|
+
var import_electrodb8 = require("electrodb");
|
|
1681
|
+
var RoleAssignmentEntity = new import_electrodb8.Entity({
|
|
1682
|
+
model: {
|
|
1683
|
+
entity: "roleassignment",
|
|
1684
|
+
service: "control",
|
|
1685
|
+
version: "01"
|
|
1686
|
+
},
|
|
1687
|
+
attributes: {
|
|
1688
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
1689
|
+
sk: {
|
|
1690
|
+
type: "string",
|
|
1691
|
+
required: true,
|
|
1692
|
+
default: "CURRENT"
|
|
1693
|
+
},
|
|
1694
|
+
/** Tenant in which the role assignment applies (required). */
|
|
1695
|
+
tenantId: {
|
|
1696
|
+
type: "string",
|
|
1697
|
+
required: true
|
|
1698
|
+
},
|
|
1699
|
+
/** FHIR Resource.id; role assignment id. */
|
|
1700
|
+
id: {
|
|
1701
|
+
type: "string",
|
|
1702
|
+
required: true
|
|
1703
|
+
},
|
|
1704
|
+
/** Full RoleAssignment resource serialized as JSON string. */
|
|
1705
|
+
resource: {
|
|
1706
|
+
type: "string",
|
|
1707
|
+
required: true
|
|
1708
|
+
},
|
|
1709
|
+
/**
|
|
1710
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
1711
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
1712
|
+
*/
|
|
1713
|
+
summary: {
|
|
1714
|
+
type: "string",
|
|
1715
|
+
required: true
|
|
1716
|
+
},
|
|
1717
|
+
/** Version id (e.g. ULID). */
|
|
1718
|
+
vid: {
|
|
1719
|
+
type: "string",
|
|
1720
|
+
required: true
|
|
1721
|
+
},
|
|
1722
|
+
lastUpdated: {
|
|
1723
|
+
type: "string",
|
|
1724
|
+
required: true
|
|
1725
|
+
},
|
|
1726
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
1727
|
+
/**
|
|
1728
|
+
* Derived GSI1 sort key — discriminator-first
|
|
1729
|
+
* `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
|
|
1730
|
+
* GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
|
|
1731
|
+
* to enumerate every user assigned to a given role, sorted by user
|
|
1732
|
+
* name. Falls back to `<lastUpdated>#<id>` when either component is
|
|
1733
|
+
* missing.
|
|
1734
|
+
*/
|
|
1735
|
+
gsi1sk: roleAssignmentGsi1skAttribute,
|
|
1736
|
+
deleted: {
|
|
1737
|
+
type: "boolean",
|
|
1738
|
+
required: false
|
|
1739
|
+
},
|
|
1740
|
+
bundleId: {
|
|
1741
|
+
type: "string",
|
|
1742
|
+
required: false
|
|
1743
|
+
},
|
|
1744
|
+
msgId: {
|
|
1745
|
+
type: "string",
|
|
1746
|
+
required: false
|
|
1747
|
+
},
|
|
1748
|
+
/**
|
|
1749
|
+
* Denormalized display name of the linked Tenant, captured at row
|
|
1750
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1751
|
+
* adjacency-list user-projection SK (pattern #5 —
|
|
1752
|
+
* `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
|
|
1753
|
+
* can be composed from a top-level field instead of digging into the
|
|
1754
|
+
* `resource` JSON. Optional on the schema so pre-TR-024 rows do not
|
|
1755
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
1756
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
1757
|
+
* source = canonical Tenant.displayName).
|
|
1758
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1759
|
+
*/
|
|
1760
|
+
denormalizedTenantName: {
|
|
1761
|
+
type: "string",
|
|
1762
|
+
required: false
|
|
1763
|
+
},
|
|
1764
|
+
/**
|
|
1765
|
+
* Denormalized display name of the linked User, captured at row
|
|
1766
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1767
|
+
* adjacency-list canonical-record GSI1SK (pattern #8 —
|
|
1768
|
+
* `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
|
|
1769
|
+
* (pattern #9) can be composed from a top-level field. Optional on
|
|
1770
|
+
* the schema so pre-TR-024 rows do not break; the operations-layer
|
|
1771
|
+
* multi-write helper (#1010) makes the field load-bearing at write
|
|
1772
|
+
* time per TR-024 rule 2 (write-time source = canonical
|
|
1773
|
+
* User.displayName).
|
|
1774
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1775
|
+
*/
|
|
1776
|
+
denormalizedUserName: {
|
|
1777
|
+
type: "string",
|
|
1778
|
+
required: false
|
|
1779
|
+
},
|
|
1780
|
+
/**
|
|
1781
|
+
* Denormalized display name of the linked Role, captured at row
|
|
1782
|
+
* last-write time. Promoted to a top-level attribute so the ADR-018
|
|
1783
|
+
* adjacency-list user-projection SK (pattern #5 —
|
|
1784
|
+
* `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
|
|
1785
|
+
* a top-level field. Optional on the schema so pre-TR-024 rows do not
|
|
1786
|
+
* break; the operations-layer multi-write helper (#1010) makes the
|
|
1787
|
+
* field load-bearing at write time per TR-024 rule 2 (write-time
|
|
1788
|
+
* source = canonical Role.displayName).
|
|
1789
|
+
* @see TR-024 — Denormalized display-name attributes
|
|
1790
|
+
*/
|
|
1791
|
+
denormalizedRoleName: {
|
|
1792
|
+
type: "string",
|
|
1793
|
+
required: false
|
|
1794
|
+
}
|
|
1795
|
+
},
|
|
1796
|
+
indexes: {
|
|
1797
|
+
/** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
1798
|
+
record: {
|
|
1799
|
+
pk: {
|
|
1800
|
+
field: "PK",
|
|
1801
|
+
composite: ["tenantId", "id"],
|
|
1802
|
+
template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
|
|
1803
|
+
},
|
|
1804
|
+
sk: {
|
|
1805
|
+
field: "SK",
|
|
1806
|
+
composite: ["sk"],
|
|
1807
|
+
template: "${sk}"
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
/**
|
|
1811
|
+
* GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
|
|
1812
|
+
* four shards. Tenant-scoped only, so `WID#-` is a sentinel.
|
|
1813
|
+
* SK is derived via `roleAssignmentGsi1skAttribute` — composes the
|
|
1814
|
+
* discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
|
|
1815
|
+
* ADR-018 pattern #8 (users with a specific role in a tenant, sorted
|
|
1816
|
+
* by user name); falls back to `<lastUpdated>#<id>` when either
|
|
1817
|
+
* component is missing. `casing: "none"` preserves the normalized
|
|
1818
|
+
* label and ISO-8601 `T`/`Z`.
|
|
1819
|
+
*/
|
|
1820
|
+
gsi1: {
|
|
1821
|
+
index: "GSI1",
|
|
1822
|
+
pk: {
|
|
1823
|
+
field: "GSI1PK",
|
|
1824
|
+
composite: ["tenantId", "gsi1Shard"],
|
|
1825
|
+
template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
|
|
1826
|
+
},
|
|
1827
|
+
sk: {
|
|
1828
|
+
field: "GSI1SK",
|
|
1829
|
+
casing: "none",
|
|
1830
|
+
composite: ["gsi1sk"],
|
|
1831
|
+
template: "${gsi1sk}"
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
// src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
|
|
1838
|
+
var import_electrodb9 = require("electrodb");
|
|
1839
|
+
var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
|
|
1840
|
+
model: {
|
|
1841
|
+
entity: "roleAssignmentUserProjection",
|
|
1842
|
+
service: "control",
|
|
1843
|
+
version: "01"
|
|
1844
|
+
},
|
|
1845
|
+
attributes: {
|
|
1846
|
+
/**
|
|
1847
|
+
* User partition discriminator. Renders as `USER#ID#<userId>` on the
|
|
1848
|
+
* base-table PK. Always required — the projection has no meaning
|
|
1849
|
+
* outside a user partition.
|
|
1850
|
+
*/
|
|
1851
|
+
userId: {
|
|
1852
|
+
type: "string",
|
|
1853
|
+
required: true
|
|
1854
|
+
},
|
|
1855
|
+
/**
|
|
1856
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
1857
|
+
* writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
|
|
1858
|
+
* entity stores the value verbatim so the SK grammar (tenant-lane
|
|
1859
|
+
* vs workspace-lane) is owned by the operations layer, not
|
|
1860
|
+
* duplicated here.
|
|
1861
|
+
*/
|
|
1862
|
+
sk: {
|
|
1863
|
+
type: "string",
|
|
1864
|
+
required: true
|
|
1865
|
+
},
|
|
1866
|
+
/** Tenant in which the role assignment applies. Always required. */
|
|
1867
|
+
tenantId: {
|
|
1868
|
+
type: "string",
|
|
1869
|
+
required: true
|
|
1870
|
+
},
|
|
1871
|
+
/**
|
|
1872
|
+
* Workspace the role assignment scopes to. Present iff the
|
|
1873
|
+
* projection row is the workspace-level sub-lane; absent for
|
|
1874
|
+
* tenant-level sub-lane rows.
|
|
1875
|
+
*/
|
|
1876
|
+
workspaceId: {
|
|
1877
|
+
type: "string",
|
|
1878
|
+
required: false
|
|
1879
|
+
},
|
|
1880
|
+
/**
|
|
1881
|
+
* Role the assignment grants. Stored as a discriminating field so
|
|
1882
|
+
* `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
|
|
1883
|
+
* results carry the role id without a hop to the canonical row.
|
|
1884
|
+
*/
|
|
1885
|
+
roleId: {
|
|
1886
|
+
type: "string",
|
|
1887
|
+
required: true
|
|
1888
|
+
},
|
|
1889
|
+
/**
|
|
1890
|
+
* RoleAssignment canonical-record id. Stored as a discriminating
|
|
1891
|
+
* field so consumers can hydrate the canonical row via
|
|
1892
|
+
* `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
|
|
1893
|
+
* when the projection's `summary` is insufficient.
|
|
1894
|
+
*/
|
|
1895
|
+
roleAssignmentId: {
|
|
1896
|
+
type: "string",
|
|
1897
|
+
required: true
|
|
1898
|
+
},
|
|
1899
|
+
/**
|
|
1900
|
+
* Summary projection (key display fields as JSON string: id,
|
|
1901
|
+
* displayName, status) — mirrored from the canonical RoleAssignment
|
|
1902
|
+
* row so user-partition queries do not need a BatchGet hop.
|
|
1903
|
+
*/
|
|
1904
|
+
summary: {
|
|
1905
|
+
type: "string",
|
|
1906
|
+
required: true
|
|
1907
|
+
},
|
|
1908
|
+
/** Version id mirrored from the canonical RoleAssignment row. */
|
|
1909
|
+
vid: {
|
|
1910
|
+
type: "string",
|
|
1911
|
+
required: true
|
|
1912
|
+
},
|
|
1913
|
+
/** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
|
|
1914
|
+
lastUpdated: {
|
|
1915
|
+
type: "string",
|
|
1916
|
+
required: true
|
|
1917
|
+
},
|
|
1918
|
+
/**
|
|
1919
|
+
* Denormalized Tenant display name — mirrored from the canonical
|
|
1920
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
1921
|
+
* Optional on the schema because pre-TR-024 rows may not carry a
|
|
1922
|
+
* display name; the operations layer falls back gracefully when
|
|
1923
|
+
* missing.
|
|
1924
|
+
*/
|
|
1925
|
+
denormalizedTenantName: {
|
|
1926
|
+
type: "string",
|
|
1927
|
+
required: false
|
|
1928
|
+
},
|
|
1929
|
+
/**
|
|
1930
|
+
* Denormalized User display name — mirrored from the canonical
|
|
1931
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
1932
|
+
* Carried on the projection so consumers can render the user's
|
|
1933
|
+
* display name without a hop to the User record.
|
|
1934
|
+
*/
|
|
1935
|
+
denormalizedUserName: {
|
|
1936
|
+
type: "string",
|
|
1937
|
+
required: false
|
|
1938
|
+
},
|
|
1939
|
+
/**
|
|
1940
|
+
* Denormalized Role display name — required to compose the SK's
|
|
1941
|
+
* `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
|
|
1942
|
+
* rows fall back to a sentinel) but expected to be present at write
|
|
1943
|
+
* time per TR-024 rule 2 (write-time source =
|
|
1944
|
+
* canonical Role.displayName).
|
|
1945
|
+
*/
|
|
1946
|
+
denormalizedRoleName: {
|
|
1947
|
+
type: "string",
|
|
1948
|
+
required: false
|
|
1949
|
+
}
|
|
1950
|
+
},
|
|
1951
|
+
indexes: {
|
|
1952
|
+
/**
|
|
1953
|
+
* Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
|
|
1954
|
+
* sub-lanes (tenant-level and workspace-level) use this same index —
|
|
1955
|
+
* the SK string encodes the lane discriminator
|
|
1956
|
+
* (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
|
|
1957
|
+
* single `Query(PK = USER#ID#<userId>, SK begins_with
|
|
1958
|
+
* 'ROLEASSIGNMENT#')` returns both lanes interleaved.
|
|
1959
|
+
*/
|
|
1960
|
+
record: {
|
|
1961
|
+
pk: {
|
|
1962
|
+
field: "PK",
|
|
1963
|
+
composite: ["userId"],
|
|
1964
|
+
template: "USER#ID#${userId}"
|
|
1965
|
+
},
|
|
1966
|
+
sk: {
|
|
1967
|
+
field: "SK",
|
|
1968
|
+
casing: "none",
|
|
1969
|
+
composite: ["sk"],
|
|
1970
|
+
template: "${sk}"
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
// src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
|
|
1977
|
+
var import_electrodb10 = require("electrodb");
|
|
1978
|
+
var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
|
|
1979
|
+
model: {
|
|
1980
|
+
entity: "roleAssignmentWorkspaceProjection",
|
|
1981
|
+
service: "control",
|
|
1982
|
+
version: "01"
|
|
1983
|
+
},
|
|
1984
|
+
attributes: {
|
|
1985
|
+
/**
|
|
1986
|
+
* Tenant the workspace belongs to. Renders as the leading segment
|
|
1987
|
+
* of the base-table PK. Always required — the workspace partition
|
|
1988
|
+
* is tenant-scoped per ADR-011.
|
|
1989
|
+
*/
|
|
1990
|
+
tenantId: {
|
|
1991
|
+
type: "string",
|
|
1992
|
+
required: true
|
|
1993
|
+
},
|
|
1994
|
+
/**
|
|
1995
|
+
* Workspace partition discriminator. Renders as the trailing
|
|
1996
|
+
* segment of the base-table PK
|
|
1997
|
+
* (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
|
|
1998
|
+
* the projection has no meaning outside a workspace partition.
|
|
1999
|
+
*/
|
|
2000
|
+
workspaceId: {
|
|
2001
|
+
type: "string",
|
|
2002
|
+
required: true
|
|
2003
|
+
},
|
|
2004
|
+
/**
|
|
2005
|
+
* Pre-composed sort key — built by the operations-layer projection
|
|
2006
|
+
* writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
|
|
2007
|
+
* stores the value verbatim so the SK grammar (pattern #9) is
|
|
2008
|
+
* owned by the operations layer, not duplicated here.
|
|
2009
|
+
*/
|
|
2010
|
+
sk: {
|
|
2011
|
+
type: "string",
|
|
2012
|
+
required: true
|
|
2013
|
+
},
|
|
2014
|
+
/**
|
|
2015
|
+
* User the role assignment grants the role to. Stored as a
|
|
2016
|
+
* discriminating field so consumers can hydrate the canonical User
|
|
2017
|
+
* row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
|
|
2018
|
+
* projection's `summary` is insufficient.
|
|
2019
|
+
*/
|
|
2020
|
+
userId: {
|
|
2021
|
+
type: "string",
|
|
2022
|
+
required: true
|
|
2023
|
+
},
|
|
2024
|
+
/**
|
|
2025
|
+
* Role the assignment grants. Stored as a discriminating field —
|
|
2026
|
+
* also rendered into the SK as the discriminator-first segment so
|
|
2027
|
+
* `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
|
|
2028
|
+
*/
|
|
2029
|
+
roleId: {
|
|
2030
|
+
type: "string",
|
|
2031
|
+
required: true
|
|
2032
|
+
},
|
|
2033
|
+
/**
|
|
2034
|
+
* RoleAssignment canonical-record id. Stored as a discriminating
|
|
2035
|
+
* field so consumers can hydrate the canonical row via
|
|
2036
|
+
* `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
|
|
2037
|
+
* when the projection's `summary` is insufficient.
|
|
2038
|
+
*/
|
|
2039
|
+
roleAssignmentId: {
|
|
2040
|
+
type: "string",
|
|
2041
|
+
required: true
|
|
2042
|
+
},
|
|
2043
|
+
/**
|
|
2044
|
+
* Summary projection (key display fields as JSON string: id,
|
|
2045
|
+
* displayName, status) — mirrored from the canonical RoleAssignment
|
|
2046
|
+
* row so workspace-partition queries do not need a BatchGet hop.
|
|
2047
|
+
*/
|
|
2048
|
+
summary: {
|
|
2049
|
+
type: "string",
|
|
2050
|
+
required: true
|
|
2051
|
+
},
|
|
2052
|
+
/** Version id mirrored from the canonical RoleAssignment row. */
|
|
2053
|
+
vid: {
|
|
2054
|
+
type: "string",
|
|
2055
|
+
required: true
|
|
2056
|
+
},
|
|
2057
|
+
/** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
|
|
2058
|
+
lastUpdated: {
|
|
2059
|
+
type: "string",
|
|
2060
|
+
required: true
|
|
2061
|
+
},
|
|
2062
|
+
/**
|
|
2063
|
+
* Denormalized User display name — required to compose the
|
|
2064
|
+
* pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
|
|
2065
|
+
* Optional on the schema because pre-TR-024 rows may not carry a
|
|
2066
|
+
* display name; the operations layer falls back to a sentinel when
|
|
2067
|
+
* missing so the SK still has a valid shape. The TR-023 rename-
|
|
2068
|
+
* cascade pipeline rewrites the SK on a User rename.
|
|
2069
|
+
*/
|
|
2070
|
+
denormalizedUserName: {
|
|
2071
|
+
type: "string",
|
|
2072
|
+
required: false
|
|
2073
|
+
},
|
|
2074
|
+
/**
|
|
2075
|
+
* Denormalized Role display name — mirrored from the canonical
|
|
2076
|
+
* RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
|
|
2077
|
+
* Carried on the projection so consumers can render the role's
|
|
2078
|
+
* display name without a hop to the Role record. Not part of the
|
|
2079
|
+
* SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
|
|
2080
|
+
* a Role rename does NOT rewrite this SK.
|
|
2081
|
+
*/
|
|
2082
|
+
denormalizedRoleName: {
|
|
2083
|
+
type: "string",
|
|
2084
|
+
required: false
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
indexes: {
|
|
2088
|
+
/**
|
|
2089
|
+
* Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
|
|
2090
|
+
* SK = operation-supplied. Pattern #9 uses this index — the SK
|
|
2091
|
+
* encodes the entity-type prefix and discriminator-first roleId
|
|
2092
|
+
* (`ROLEASSIGNMENT#<roleId>#…`) so
|
|
2093
|
+
* `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
|
|
2094
|
+
* returns every user-assignment for that role in the workspace, sorted
|
|
2095
|
+
* by normalized user name.
|
|
2096
|
+
*/
|
|
2097
|
+
record: {
|
|
2098
|
+
pk: {
|
|
2099
|
+
field: "PK",
|
|
2100
|
+
composite: ["tenantId", "workspaceId"],
|
|
2101
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
|
|
2102
|
+
},
|
|
2103
|
+
sk: {
|
|
2104
|
+
field: "SK",
|
|
2105
|
+
casing: "none",
|
|
2106
|
+
composite: ["sk"],
|
|
2107
|
+
template: "${sk}"
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
// src/data/dynamo/entities/control/tenant-entity.ts
|
|
2114
|
+
var import_electrodb11 = require("electrodb");
|
|
2115
|
+
var TenantEntity = new import_electrodb11.Entity({
|
|
2116
|
+
model: {
|
|
2117
|
+
entity: "tenant",
|
|
2118
|
+
service: "control",
|
|
2119
|
+
version: "01"
|
|
2120
|
+
},
|
|
2121
|
+
attributes: {
|
|
2122
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
2123
|
+
sk: {
|
|
2124
|
+
type: "string",
|
|
2125
|
+
required: true,
|
|
2126
|
+
default: "CURRENT"
|
|
2127
|
+
},
|
|
2128
|
+
/** The tenant's own id (= resource id). Drives the partition key. */
|
|
2129
|
+
tenantId: {
|
|
2130
|
+
type: "string",
|
|
2131
|
+
required: true
|
|
2132
|
+
},
|
|
2133
|
+
/** FHIR Resource.id; logical id in URL. Equals tenantId. */
|
|
2134
|
+
id: {
|
|
2135
|
+
type: "string",
|
|
2136
|
+
required: true
|
|
2137
|
+
},
|
|
2138
|
+
/** Full Tenant resource serialized as JSON string. */
|
|
2139
|
+
resource: {
|
|
2140
|
+
type: "string",
|
|
2141
|
+
required: true
|
|
2142
|
+
},
|
|
2143
|
+
/**
|
|
2144
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
2145
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
2146
|
+
*/
|
|
2147
|
+
summary: {
|
|
2148
|
+
type: "string",
|
|
2149
|
+
required: true
|
|
2150
|
+
},
|
|
2151
|
+
/** Version id (e.g. ULID). */
|
|
2152
|
+
vid: {
|
|
2153
|
+
type: "string",
|
|
2154
|
+
required: true
|
|
2155
|
+
},
|
|
2156
|
+
lastUpdated: {
|
|
2157
|
+
type: "string",
|
|
2158
|
+
required: true
|
|
2159
|
+
},
|
|
2160
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
2161
|
+
/** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
|
|
2162
|
+
gsi1sk: gsi1skAttribute,
|
|
2163
|
+
deleted: {
|
|
2164
|
+
type: "boolean",
|
|
2165
|
+
required: false
|
|
2166
|
+
},
|
|
2167
|
+
bundleId: {
|
|
2168
|
+
type: "string",
|
|
2169
|
+
required: false
|
|
2170
|
+
},
|
|
2171
|
+
msgId: {
|
|
2172
|
+
type: "string",
|
|
2173
|
+
required: false
|
|
2174
|
+
}
|
|
2175
|
+
},
|
|
2176
|
+
indexes: {
|
|
2177
|
+
/** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
2178
|
+
record: {
|
|
2179
|
+
pk: {
|
|
2180
|
+
field: "PK",
|
|
2181
|
+
composite: ["tenantId"],
|
|
2182
|
+
template: "TENANT#ID#${tenantId}"
|
|
2183
|
+
},
|
|
2184
|
+
sk: {
|
|
2185
|
+
field: "SK",
|
|
2186
|
+
composite: ["sk"],
|
|
2187
|
+
template: "${sk}"
|
|
2188
|
+
}
|
|
2189
|
+
},
|
|
2190
|
+
/**
|
|
2191
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
|
|
2192
|
+
* Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
|
|
2193
|
+
* sentinels precede `RT#Tenant#SHARD#<n>`. SK is derived via `gsi1skAttribute` —
|
|
2194
|
+
* `<normalizedName>#<id>` when the resource carries a `name`, else `<lastUpdated>#<id>`
|
|
2195
|
+
* (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
|
|
2196
|
+
*/
|
|
2197
|
+
gsi1: {
|
|
2198
|
+
index: "GSI1",
|
|
2199
|
+
pk: {
|
|
2200
|
+
field: "GSI1PK",
|
|
2201
|
+
composite: ["gsi1Shard"],
|
|
2202
|
+
template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
|
|
2203
|
+
},
|
|
2204
|
+
sk: {
|
|
2205
|
+
field: "GSI1SK",
|
|
2206
|
+
casing: "none",
|
|
2207
|
+
composite: ["gsi1sk"],
|
|
2208
|
+
template: "${gsi1sk}"
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
// src/data/dynamo/entities/control/user-entity.ts
|
|
2215
|
+
var import_electrodb12 = require("electrodb");
|
|
2216
|
+
var UserEntity = new import_electrodb12.Entity({
|
|
2217
|
+
model: {
|
|
2218
|
+
entity: "user",
|
|
2219
|
+
service: "control",
|
|
2220
|
+
version: "01"
|
|
2221
|
+
},
|
|
2222
|
+
attributes: {
|
|
2223
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
2224
|
+
sk: {
|
|
2225
|
+
type: "string",
|
|
2226
|
+
required: true,
|
|
2227
|
+
default: "CURRENT"
|
|
2228
|
+
},
|
|
2229
|
+
/** FHIR Resource.id; platform user id (ohi_uid). */
|
|
2230
|
+
id: {
|
|
2231
|
+
type: "string",
|
|
2232
|
+
required: true
|
|
2233
|
+
},
|
|
2234
|
+
/** Full User resource serialized as JSON string. */
|
|
2235
|
+
resource: {
|
|
2236
|
+
type: "string",
|
|
2237
|
+
required: true
|
|
2238
|
+
},
|
|
2239
|
+
/**
|
|
2240
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
2241
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
2242
|
+
*/
|
|
2243
|
+
summary: {
|
|
2244
|
+
type: "string",
|
|
2245
|
+
required: true
|
|
2246
|
+
},
|
|
2247
|
+
/**
|
|
2248
|
+
* Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
|
|
2249
|
+
* Post Confirmation Lambda (#770) lands; required thereafter.
|
|
2250
|
+
*/
|
|
2251
|
+
cognitoSub: {
|
|
2252
|
+
type: "string",
|
|
2253
|
+
required: false
|
|
2254
|
+
},
|
|
2255
|
+
/** Version id (e.g. ULID). */
|
|
2256
|
+
vid: {
|
|
2257
|
+
type: "string",
|
|
2258
|
+
required: true
|
|
2259
|
+
},
|
|
2260
|
+
lastUpdated: {
|
|
2261
|
+
type: "string",
|
|
2262
|
+
required: true
|
|
2263
|
+
},
|
|
2264
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
2265
|
+
/** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
|
|
2266
|
+
gsi1sk: gsi1skAttribute,
|
|
2267
|
+
deleted: {
|
|
2268
|
+
type: "boolean",
|
|
2269
|
+
required: false
|
|
2270
|
+
},
|
|
2271
|
+
/**
|
|
2272
|
+
* TR-022 / ADR-018 lifecycle state for the cascade pipeline.
|
|
2273
|
+
*
|
|
2274
|
+
* - `active` (or undefined) — normal, readable state.
|
|
2275
|
+
* - `deleting` — intermediate state set synchronously by the
|
|
2276
|
+
* hard-delete API entry point. The owning-delete cascade state
|
|
2277
|
+
* machine fans out from this transition (DynamoDB stream →
|
|
2278
|
+
* `control-plane.owning-delete.v1` → Step Functions). Readers MUST
|
|
2279
|
+
* short-circuit on `deleting` so partial cascades stay invisible.
|
|
2280
|
+
* - `deleted-failed` — terminal failure state set by the cascade
|
|
2281
|
+
* finalize Lambda when the cascade run fails irrecoverably.
|
|
2282
|
+
* Operators recover by re-running the cascade or by direct
|
|
2283
|
+
* intervention.
|
|
2284
|
+
*
|
|
2285
|
+
* The cascade finalize step deletes the canonical record conditional
|
|
2286
|
+
* on `lifecycleState = "deleting"`; on replay the conditional check
|
|
2287
|
+
* fails and the finalize step treats that as a no-op success.
|
|
2288
|
+
*/
|
|
2289
|
+
lifecycleState: {
|
|
2290
|
+
type: ["active", "deleting", "deleted-failed"],
|
|
2291
|
+
required: false
|
|
2292
|
+
},
|
|
2293
|
+
bundleId: {
|
|
2294
|
+
type: "string",
|
|
2295
|
+
required: false
|
|
2296
|
+
},
|
|
2297
|
+
msgId: {
|
|
2298
|
+
type: "string",
|
|
2299
|
+
required: false
|
|
2300
|
+
}
|
|
2301
|
+
},
|
|
2302
|
+
indexes: {
|
|
2303
|
+
/** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
2304
|
+
record: {
|
|
2305
|
+
pk: {
|
|
2306
|
+
field: "PK",
|
|
2307
|
+
composite: ["id"],
|
|
2308
|
+
template: "USER#ID#${id}"
|
|
2309
|
+
},
|
|
2310
|
+
sk: {
|
|
2311
|
+
field: "SK",
|
|
2312
|
+
composite: ["sk"],
|
|
2313
|
+
template: "${sk}"
|
|
2314
|
+
}
|
|
2315
|
+
},
|
|
2316
|
+
/**
|
|
2317
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
|
|
2318
|
+
* Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
|
|
2319
|
+
* SK is derived via `gsi1skAttribute` — uses the resource's natural label when
|
|
2320
|
+
* extractable (string `name`/`title` via introspection), else `<lastUpdated>#<id>`
|
|
2321
|
+
* (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
|
|
2322
|
+
*/
|
|
2323
|
+
gsi1: {
|
|
2324
|
+
index: "GSI1",
|
|
2325
|
+
pk: {
|
|
2326
|
+
field: "GSI1PK",
|
|
2327
|
+
composite: ["gsi1Shard"],
|
|
2328
|
+
template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
|
|
2329
|
+
},
|
|
2330
|
+
sk: {
|
|
2331
|
+
field: "GSI1SK",
|
|
2332
|
+
casing: "none",
|
|
2333
|
+
composite: ["gsi1sk"],
|
|
2334
|
+
template: "${gsi1sk}"
|
|
2335
|
+
}
|
|
2336
|
+
},
|
|
2337
|
+
/**
|
|
2338
|
+
* GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
|
|
2339
|
+
* `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
|
|
2340
|
+
* not indexed.
|
|
2341
|
+
*/
|
|
2342
|
+
gsi2: {
|
|
2343
|
+
index: "GSI2",
|
|
2344
|
+
condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
|
|
2345
|
+
pk: {
|
|
2346
|
+
field: "GSI2PK",
|
|
2347
|
+
casing: "none",
|
|
2348
|
+
composite: ["cognitoSub"],
|
|
2349
|
+
template: "USER#SUB#${cognitoSub}"
|
|
2350
|
+
},
|
|
2351
|
+
sk: {
|
|
2352
|
+
field: "GSI2SK",
|
|
2353
|
+
casing: "none",
|
|
2354
|
+
composite: [],
|
|
2355
|
+
template: "CURRENT"
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
// src/data/dynamo/entities/control/workspace-entity.ts
|
|
2362
|
+
var import_electrodb13 = require("electrodb");
|
|
2363
|
+
var WorkspaceEntity = new import_electrodb13.Entity({
|
|
2364
|
+
model: {
|
|
2365
|
+
entity: "workspace",
|
|
2366
|
+
service: "control",
|
|
2367
|
+
version: "01"
|
|
2368
|
+
},
|
|
2369
|
+
attributes: {
|
|
2370
|
+
/** Sort key sentinel. Always "CURRENT". */
|
|
2371
|
+
sk: {
|
|
2372
|
+
type: "string",
|
|
2373
|
+
required: true,
|
|
2374
|
+
default: "CURRENT"
|
|
2375
|
+
},
|
|
2376
|
+
/** Tenant that contains this workspace (required). */
|
|
2377
|
+
tenantId: {
|
|
2378
|
+
type: "string",
|
|
2379
|
+
required: true
|
|
2380
|
+
},
|
|
2381
|
+
/** FHIR Resource.id; logical id in URL. */
|
|
2382
|
+
id: {
|
|
2383
|
+
type: "string",
|
|
2384
|
+
required: true
|
|
2385
|
+
},
|
|
2386
|
+
/** Full Workspace resource serialized as JSON string. */
|
|
2387
|
+
resource: {
|
|
2388
|
+
type: "string",
|
|
2389
|
+
required: true
|
|
2390
|
+
},
|
|
2391
|
+
/**
|
|
2392
|
+
* Summary projection (key display fields as JSON string: id, displayName, status).
|
|
2393
|
+
* Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
|
|
2394
|
+
*/
|
|
2395
|
+
summary: {
|
|
2396
|
+
type: "string",
|
|
2397
|
+
required: true
|
|
2398
|
+
},
|
|
2399
|
+
/** Version id (e.g. ULID). */
|
|
2400
|
+
vid: {
|
|
2401
|
+
type: "string",
|
|
2402
|
+
required: true
|
|
2403
|
+
},
|
|
2404
|
+
lastUpdated: {
|
|
2405
|
+
type: "string",
|
|
2406
|
+
required: true
|
|
2407
|
+
},
|
|
2408
|
+
gsi1Shard: gsi1ShardAttribute,
|
|
2409
|
+
/** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
|
|
2410
|
+
gsi1sk: gsi1skAttribute,
|
|
2411
|
+
deleted: {
|
|
2412
|
+
type: "boolean",
|
|
2413
|
+
required: false
|
|
2414
|
+
},
|
|
2415
|
+
/**
|
|
2416
|
+
* TR-022 / ADR-018 lifecycle state for the cascade pipeline.
|
|
2417
|
+
*
|
|
2418
|
+
* - `active` (or undefined) — normal, readable state.
|
|
2419
|
+
* - `deleting` — intermediate state set synchronously by the
|
|
2420
|
+
* hard-delete API entry point. The owning-delete cascade state
|
|
2421
|
+
* machine fans out from this transition (DynamoDB stream →
|
|
2422
|
+
* `control-plane.owning-delete.v1` → Step Functions). Readers MUST
|
|
2423
|
+
* short-circuit on `deleting` so partial cascades stay invisible.
|
|
2424
|
+
* - `deleted-failed` — terminal failure state set by the cascade
|
|
2425
|
+
* finalize Lambda when the cascade run fails irrecoverably.
|
|
2426
|
+
* Operators recover by re-running the cascade or by direct
|
|
2427
|
+
* intervention.
|
|
2428
|
+
*
|
|
2429
|
+
* The cascade finalize step deletes the canonical record conditional
|
|
2430
|
+
* on `lifecycleState = "deleting"`; on replay the conditional check
|
|
2431
|
+
* fails and the finalize step treats that as a no-op success.
|
|
2432
|
+
*/
|
|
2433
|
+
lifecycleState: {
|
|
2434
|
+
type: ["active", "deleting", "deleted-failed"],
|
|
2435
|
+
required: false
|
|
2436
|
+
},
|
|
2437
|
+
bundleId: {
|
|
2438
|
+
type: "string",
|
|
2439
|
+
required: false
|
|
2440
|
+
},
|
|
2441
|
+
msgId: {
|
|
2442
|
+
type: "string",
|
|
2443
|
+
required: false
|
|
2444
|
+
}
|
|
2445
|
+
},
|
|
2446
|
+
indexes: {
|
|
2447
|
+
/** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
|
|
2448
|
+
record: {
|
|
2449
|
+
pk: {
|
|
2450
|
+
field: "PK",
|
|
2451
|
+
composite: ["tenantId", "id"],
|
|
2452
|
+
template: "TID#${tenantId}#WORKSPACE#ID#${id}"
|
|
2453
|
+
},
|
|
2454
|
+
sk: {
|
|
2455
|
+
field: "SK",
|
|
2456
|
+
composite: ["sk"],
|
|
2457
|
+
template: "${sk}"
|
|
2458
|
+
}
|
|
2459
|
+
},
|
|
2460
|
+
/**
|
|
2461
|
+
* GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
|
|
2462
|
+
* four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
|
|
2463
|
+
* SK is derived via `gsi1skAttribute` — `<normalizedName>#<id>` when the resource
|
|
2464
|
+
* carries a `name`, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves
|
|
2465
|
+
* the normalized label and ISO-8601 `T`/`Z`.
|
|
2466
|
+
*/
|
|
2467
|
+
gsi1: {
|
|
2468
|
+
index: "GSI1",
|
|
2469
|
+
pk: {
|
|
2470
|
+
field: "GSI1PK",
|
|
2471
|
+
composite: ["tenantId", "gsi1Shard"],
|
|
2472
|
+
template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
|
|
2473
|
+
},
|
|
2474
|
+
sk: {
|
|
2475
|
+
field: "GSI1SK",
|
|
2476
|
+
casing: "none",
|
|
2477
|
+
composite: ["gsi1sk"],
|
|
2478
|
+
template: "${gsi1sk}"
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
// src/data/dynamo/dynamo-control-service.ts
|
|
2485
|
+
var controlPlaneEntities = {
|
|
2486
|
+
configuration: ConfigurationEntity,
|
|
2487
|
+
configurationUserProjection: ConfigurationUserProjectionEntity,
|
|
2488
|
+
configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
|
|
2489
|
+
membership: MembershipEntity,
|
|
2490
|
+
membershipUserProjection: MembershipUserProjectionEntity,
|
|
2491
|
+
membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
|
|
2492
|
+
role: RoleEntity,
|
|
2493
|
+
roleAssignment: RoleAssignmentEntity,
|
|
2494
|
+
roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
|
|
2495
|
+
roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
|
|
2496
|
+
tenant: TenantEntity,
|
|
2497
|
+
user: UserEntity,
|
|
2498
|
+
workspace: WorkspaceEntity
|
|
2499
|
+
};
|
|
2500
|
+
var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
|
|
2501
|
+
table: defaultTableName,
|
|
2502
|
+
client: dynamoClient
|
|
2503
|
+
});
|
|
2504
|
+
var DynamoControlService = {
|
|
2505
|
+
entities: controlPlaneService.entities,
|
|
2506
|
+
transaction: controlPlaneService.transaction
|
|
2507
|
+
};
|
|
2508
|
+
function getDynamoControlService(tableName) {
|
|
2509
|
+
const resolved = tableName ?? defaultTableName;
|
|
2510
|
+
const service = new import_electrodb14.Service(controlPlaneEntities, {
|
|
2511
|
+
table: resolved,
|
|
2512
|
+
client: dynamoClient
|
|
2513
|
+
});
|
|
2514
|
+
return {
|
|
2515
|
+
entities: service.entities,
|
|
2516
|
+
transaction: service.transaction
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// src/data/operations/control/membership/membership-user-projection.ts
|
|
2521
|
+
var import_types2 = require("@openhi/types");
|
|
2522
|
+
var MISSING_NAME_SENTINEL = "-";
|
|
2523
|
+
function buildMembershipUserProjectionSkTenantLane(params) {
|
|
2524
|
+
const normalizedTenantName = typeof params.denormalizedTenantName === "string" && params.denormalizedTenantName.length > 0 ? (0, import_types2.normalizeLabel)(params.denormalizedTenantName) : MISSING_NAME_SENTINEL;
|
|
2525
|
+
return `MEMBERSHIP#TENANT#${normalizedTenantName}#TID#${params.tenantId}#${params.membershipId}`;
|
|
2526
|
+
}
|
|
2527
|
+
function buildMembershipUserProjectionSkWorkspaceLane(params) {
|
|
2528
|
+
const normalizedWorkspaceName = typeof params.denormalizedWorkspaceName === "string" && params.denormalizedWorkspaceName.length > 0 ? (0, import_types2.normalizeLabel)(params.denormalizedWorkspaceName) : MISSING_NAME_SENTINEL;
|
|
2529
|
+
return `MEMBERSHIP#WORKSPACE#TID#${params.tenantId}#${normalizedWorkspaceName}#WID#${params.workspaceId}#${params.membershipId}`;
|
|
2530
|
+
}
|
|
2531
|
+
function extractReferenceSlug(resource, fieldName) {
|
|
2532
|
+
const field = resource[fieldName];
|
|
2533
|
+
if (!field || typeof field !== "object") {
|
|
2534
|
+
return void 0;
|
|
2535
|
+
}
|
|
2536
|
+
const reference = field.reference;
|
|
2537
|
+
if (typeof reference !== "string" || reference.length === 0) {
|
|
2538
|
+
return void 0;
|
|
2539
|
+
}
|
|
2540
|
+
const slash = reference.lastIndexOf("/");
|
|
2541
|
+
const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
|
|
2542
|
+
return tail.length > 0 ? tail : void 0;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// src/data/operations/control/membership/membership-workspace-projection.ts
|
|
2546
|
+
var import_types3 = require("@openhi/types");
|
|
2547
|
+
var MISSING_NAME_SENTINEL2 = "-";
|
|
2548
|
+
function buildMembershipWorkspaceProjectionSk(params) {
|
|
2549
|
+
const normalizedUserName = typeof params.denormalizedUserName === "string" && params.denormalizedUserName.length > 0 ? (0, import_types3.normalizeLabel)(params.denormalizedUserName) : MISSING_NAME_SENTINEL2;
|
|
2550
|
+
return `MEMBERSHIP#${normalizedUserName}#USER#${params.userId}#${params.membershipId}`;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
// src/data/operations/control/roleassignment/roleassignment-user-projection.ts
|
|
2554
|
+
var import_types4 = require("@openhi/types");
|
|
2555
|
+
var MISSING_NAME_SENTINEL3 = "-";
|
|
2556
|
+
function buildRoleAssignmentUserProjectionSkTenantLane(params) {
|
|
2557
|
+
const normalizedRoleName = typeof params.denormalizedRoleName === "string" && params.denormalizedRoleName.length > 0 ? (0, import_types4.normalizeLabel)(params.denormalizedRoleName) : MISSING_NAME_SENTINEL3;
|
|
2558
|
+
return `ROLEASSIGNMENT#TENANT#${normalizedRoleName}#${params.roleId}#TID#${params.tenantId}#${params.roleAssignmentId}`;
|
|
2559
|
+
}
|
|
2560
|
+
function buildRoleAssignmentUserProjectionSkWorkspaceLane(params) {
|
|
2561
|
+
const normalizedRoleName = typeof params.denormalizedRoleName === "string" && params.denormalizedRoleName.length > 0 ? (0, import_types4.normalizeLabel)(params.denormalizedRoleName) : MISSING_NAME_SENTINEL3;
|
|
2562
|
+
return `ROLEASSIGNMENT#WORKSPACE#${normalizedRoleName}#${params.roleId}#TID#${params.tenantId}#WID#${params.workspaceId}#${params.roleAssignmentId}`;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// src/data/operations/control/roleassignment/roleassignment-workspace-projection.ts
|
|
2566
|
+
var import_types5 = require("@openhi/types");
|
|
2567
|
+
var MISSING_NAME_SENTINEL4 = "-";
|
|
2568
|
+
function buildRoleAssignmentWorkspaceProjectionSk(params) {
|
|
2569
|
+
const normalizedUserName = typeof params.denormalizedUserName === "string" && params.denormalizedUserName.length > 0 ? (0, import_types5.normalizeLabel)(params.denormalizedUserName) : MISSING_NAME_SENTINEL4;
|
|
2570
|
+
return `ROLEASSIGNMENT#${params.roleId}#${normalizedUserName}#USER#${params.userId}#${params.roleAssignmentId}`;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
// src/data/operations/control/rename-cascade/rename-cascade-list-targets-operation.ts
|
|
2574
|
+
var DEFAULT_PAGE_SIZE = 100;
|
|
2575
|
+
var STREAMS_FOR_ENTITY_TYPE = {
|
|
2576
|
+
Tenant: ["membershipUserProjection", "roleAssignmentUserProjection"],
|
|
2577
|
+
User: [
|
|
2578
|
+
"membershipUserProjection",
|
|
2579
|
+
"roleAssignmentUserProjection",
|
|
2580
|
+
"membershipWorkspaceProjection",
|
|
2581
|
+
"roleAssignmentWorkspaceProjection"
|
|
2582
|
+
],
|
|
2583
|
+
Role: ["roleAssignmentUserProjection", "roleAssignmentWorkspaceProjection"]
|
|
2584
|
+
};
|
|
2585
|
+
async function listRenameCascadeTargetsOperation(params) {
|
|
2586
|
+
const {
|
|
2587
|
+
entityType,
|
|
2588
|
+
entityId,
|
|
2589
|
+
tenantId,
|
|
2590
|
+
oldName,
|
|
2591
|
+
newName,
|
|
2592
|
+
oldNormalizedName,
|
|
2593
|
+
newNormalizedName,
|
|
2594
|
+
cursors = {},
|
|
2595
|
+
limit = DEFAULT_PAGE_SIZE,
|
|
2596
|
+
tableName
|
|
2597
|
+
} = params;
|
|
2598
|
+
if (!entityId || entityId.length === 0) {
|
|
2599
|
+
throw new Error("listRenameCascadeTargetsOperation: entityId is required");
|
|
2600
|
+
}
|
|
2601
|
+
switch (entityType) {
|
|
2602
|
+
case import_workflows.RENAMABLE_ENTITY_TYPE.User:
|
|
2603
|
+
return pageUserRename({
|
|
2604
|
+
userId: entityId,
|
|
2605
|
+
oldNormalizedName,
|
|
2606
|
+
newNormalizedName,
|
|
2607
|
+
newName,
|
|
2608
|
+
cursors,
|
|
2609
|
+
limit,
|
|
2610
|
+
tableName
|
|
2611
|
+
});
|
|
2612
|
+
case import_workflows.RENAMABLE_ENTITY_TYPE.Role:
|
|
2613
|
+
return pageRoleRename({
|
|
2614
|
+
roleId: entityId,
|
|
2615
|
+
tenantId,
|
|
2616
|
+
newName,
|
|
2617
|
+
cursors,
|
|
2618
|
+
limit,
|
|
2619
|
+
tableName
|
|
2620
|
+
});
|
|
2621
|
+
case import_workflows.RENAMABLE_ENTITY_TYPE.Tenant:
|
|
2622
|
+
return pageTenantRename({
|
|
2623
|
+
tenantId: entityId,
|
|
2624
|
+
oldName,
|
|
2625
|
+
newName,
|
|
2626
|
+
cursors,
|
|
2627
|
+
limit,
|
|
2628
|
+
tableName
|
|
2629
|
+
});
|
|
2630
|
+
default: {
|
|
2631
|
+
const exhaustive = entityType;
|
|
2632
|
+
throw new Error(
|
|
2633
|
+
`listRenameCascadeTargetsOperation: unsupported entityType '${String(
|
|
2634
|
+
exhaustive
|
|
2635
|
+
)}'`
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
async function pageUserRename(params) {
|
|
2641
|
+
const { userId, newName, cursors, limit, tableName } = params;
|
|
2642
|
+
const service = getDynamoControlService(tableName);
|
|
2643
|
+
const nextCursors = {};
|
|
2644
|
+
const targets = [];
|
|
2645
|
+
const muStream = cursors.membershipUserProjection;
|
|
2646
|
+
if (muStream !== null) {
|
|
2647
|
+
const page = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: "MEMBERSHIP#" }).go({ cursor: muStream ?? null, limit });
|
|
2648
|
+
for (const row of page.data ?? []) {
|
|
2649
|
+
const oldKey = { userId: row.userId, sk: row.sk };
|
|
2650
|
+
const newSk = row.sk;
|
|
2651
|
+
const newKey = { userId: row.userId, sk: newSk };
|
|
2652
|
+
targets.push({
|
|
2653
|
+
entity: "membershipUserProjection",
|
|
2654
|
+
oldKey,
|
|
2655
|
+
newKey,
|
|
2656
|
+
newItem: {
|
|
2657
|
+
...row,
|
|
2658
|
+
sk: newSk,
|
|
2659
|
+
denormalizedUserName: newName
|
|
2660
|
+
},
|
|
2661
|
+
skRewriteRequired: false
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
nextCursors.membershipUserProjection = page.cursor ?? null;
|
|
2665
|
+
} else {
|
|
2666
|
+
nextCursors.membershipUserProjection = null;
|
|
2667
|
+
}
|
|
2668
|
+
const raUStream = cursors.roleAssignmentUserProjection;
|
|
2669
|
+
if (raUStream !== null) {
|
|
2670
|
+
const page = await service.entities.roleAssignmentUserProjection.query.record({ userId }).begins({ sk: "ROLEASSIGNMENT#" }).go({ cursor: raUStream ?? null, limit });
|
|
2671
|
+
for (const row of page.data ?? []) {
|
|
2672
|
+
const oldKey = { userId: row.userId, sk: row.sk };
|
|
2673
|
+
const newKey = { userId: row.userId, sk: row.sk };
|
|
2674
|
+
targets.push({
|
|
2675
|
+
entity: "roleAssignmentUserProjection",
|
|
2676
|
+
oldKey,
|
|
2677
|
+
newKey,
|
|
2678
|
+
newItem: {
|
|
2679
|
+
...row,
|
|
2680
|
+
denormalizedUserName: newName
|
|
2681
|
+
},
|
|
2682
|
+
skRewriteRequired: false
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
nextCursors.roleAssignmentUserProjection = page.cursor ?? null;
|
|
2686
|
+
} else {
|
|
2687
|
+
nextCursors.roleAssignmentUserProjection = null;
|
|
2688
|
+
}
|
|
2689
|
+
const discoveryCursor = cursors.workspaceDiscovery;
|
|
2690
|
+
if (discoveryCursor !== null) {
|
|
2691
|
+
const discovery = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: "MEMBERSHIP#WORKSPACE#" }).go({ cursor: discoveryCursor ?? null, limit });
|
|
2692
|
+
for (const member of discovery.data ?? []) {
|
|
2693
|
+
if (!member.workspaceId || !member.tenantId) {
|
|
2694
|
+
continue;
|
|
2695
|
+
}
|
|
2696
|
+
await collectWorkspaceUserRenameTargets({
|
|
2697
|
+
service,
|
|
2698
|
+
tenantId: member.tenantId,
|
|
2699
|
+
workspaceId: member.workspaceId,
|
|
2700
|
+
userId,
|
|
2701
|
+
oldNormalizedName: params.oldNormalizedName,
|
|
2702
|
+
newNormalizedName: params.newNormalizedName,
|
|
2703
|
+
newName,
|
|
2704
|
+
targets
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
nextCursors.workspaceDiscovery = discovery.cursor ?? null;
|
|
2708
|
+
} else {
|
|
2709
|
+
nextCursors.workspaceDiscovery = null;
|
|
2710
|
+
}
|
|
2711
|
+
nextCursors.membershipWorkspaceProjection = null;
|
|
2712
|
+
nextCursors.roleAssignmentWorkspaceProjection = null;
|
|
2713
|
+
const exhausted = STREAMS_FOR_ENTITY_TYPE.User.every((s) => nextCursors[s] === null) && nextCursors.workspaceDiscovery === null;
|
|
2714
|
+
return { targets, cursors: nextCursors, exhausted };
|
|
2715
|
+
}
|
|
2716
|
+
async function collectWorkspaceUserRenameTargets(params) {
|
|
2717
|
+
const {
|
|
2718
|
+
service,
|
|
2719
|
+
tenantId,
|
|
2720
|
+
workspaceId,
|
|
2721
|
+
userId,
|
|
2722
|
+
oldNormalizedName,
|
|
2723
|
+
newName,
|
|
2724
|
+
targets
|
|
2725
|
+
} = params;
|
|
2726
|
+
const mwPage = await service.entities.membershipWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: `MEMBERSHIP#${oldNormalizedName}#USER#${userId}#` }).go({});
|
|
2727
|
+
for (const row of mwPage.data ?? []) {
|
|
2728
|
+
const newSk = buildMembershipWorkspaceProjectionSk({
|
|
2729
|
+
userId: row.userId,
|
|
2730
|
+
membershipId: row.membershipId,
|
|
2731
|
+
denormalizedUserName: newName
|
|
2732
|
+
});
|
|
2733
|
+
targets.push({
|
|
2734
|
+
entity: "membershipWorkspaceProjection",
|
|
2735
|
+
oldKey: {
|
|
2736
|
+
tenantId: row.tenantId,
|
|
2737
|
+
workspaceId: row.workspaceId,
|
|
2738
|
+
sk: row.sk
|
|
2739
|
+
},
|
|
2740
|
+
newKey: {
|
|
2741
|
+
tenantId: row.tenantId,
|
|
2742
|
+
workspaceId: row.workspaceId,
|
|
2743
|
+
sk: newSk
|
|
2744
|
+
},
|
|
2745
|
+
newItem: {
|
|
2746
|
+
...row,
|
|
2747
|
+
sk: newSk,
|
|
2748
|
+
denormalizedUserName: newName
|
|
2749
|
+
},
|
|
2750
|
+
skRewriteRequired: row.sk !== newSk
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
const raPage = await service.entities.roleAssignmentWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: "ROLEASSIGNMENT#" }).where((attr, op) => op.eq(attr.userId, userId)).go({});
|
|
2754
|
+
for (const row of raPage.data ?? []) {
|
|
2755
|
+
const newSk = buildRoleAssignmentWorkspaceProjectionSk({
|
|
2756
|
+
roleId: row.roleId,
|
|
2757
|
+
userId: row.userId,
|
|
2758
|
+
roleAssignmentId: row.roleAssignmentId,
|
|
2759
|
+
denormalizedUserName: newName
|
|
2760
|
+
});
|
|
2761
|
+
targets.push({
|
|
2762
|
+
entity: "roleAssignmentWorkspaceProjection",
|
|
2763
|
+
oldKey: {
|
|
2764
|
+
tenantId: row.tenantId,
|
|
2765
|
+
workspaceId: row.workspaceId,
|
|
2766
|
+
sk: row.sk
|
|
2767
|
+
},
|
|
2768
|
+
newKey: {
|
|
2769
|
+
tenantId: row.tenantId,
|
|
2770
|
+
workspaceId: row.workspaceId,
|
|
2771
|
+
sk: newSk
|
|
2772
|
+
},
|
|
2773
|
+
newItem: {
|
|
2774
|
+
...row,
|
|
2775
|
+
sk: newSk,
|
|
2776
|
+
denormalizedUserName: newName
|
|
2777
|
+
},
|
|
2778
|
+
skRewriteRequired: row.sk !== newSk
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
async function pageRoleRename(params) {
|
|
2783
|
+
const { roleId, tenantId, newName, cursors, limit, tableName } = params;
|
|
2784
|
+
if (!tenantId) {
|
|
2785
|
+
throw new Error(
|
|
2786
|
+
"listRenameCascadeTargetsOperation: tenantId is required for Role rename"
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
const service = getDynamoControlService(tableName);
|
|
2790
|
+
const nextCursors = {};
|
|
2791
|
+
const targets = [];
|
|
2792
|
+
const discoveryCursor = cursors.roleDiscovery;
|
|
2793
|
+
if (discoveryCursor !== null) {
|
|
2794
|
+
const page = await service.entities.roleAssignment.query.gsi1({ tenantId, gsi1Shard: "0" }).begins({ gsi1sk: `${roleId}#` }).go({ cursor: discoveryCursor ?? null, limit });
|
|
2795
|
+
for (const row of page.data ?? []) {
|
|
2796
|
+
const userId = extractUserIdFromResource(row.resource);
|
|
2797
|
+
if (userId === void 0) {
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
await collectUserRoleRenameTargets({
|
|
2801
|
+
service,
|
|
2802
|
+
userId,
|
|
2803
|
+
roleId,
|
|
2804
|
+
newName,
|
|
2805
|
+
targets
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
nextCursors.roleDiscovery = page.cursor ?? null;
|
|
2809
|
+
} else {
|
|
2810
|
+
nextCursors.roleDiscovery = null;
|
|
2811
|
+
}
|
|
2812
|
+
nextCursors.roleAssignmentUserProjection = null;
|
|
2813
|
+
nextCursors.roleAssignmentWorkspaceProjection = null;
|
|
2814
|
+
const exhausted = nextCursors.roleDiscovery === null;
|
|
2815
|
+
return { targets, cursors: nextCursors, exhausted };
|
|
2816
|
+
}
|
|
2817
|
+
async function collectUserRoleRenameTargets(params) {
|
|
2818
|
+
const { service, userId, roleId, newName, targets } = params;
|
|
2819
|
+
const userProjPage = await service.entities.roleAssignmentUserProjection.query.record({ userId }).begins({ sk: "ROLEASSIGNMENT#" }).where((attr, op) => op.eq(attr.roleId, roleId)).go({});
|
|
2820
|
+
for (const row of userProjPage.data ?? []) {
|
|
2821
|
+
const isWorkspaceLane = typeof row.workspaceId === "string" && row.workspaceId.length > 0;
|
|
2822
|
+
const newSk = isWorkspaceLane ? buildRoleAssignmentUserProjectionSkWorkspaceLane({
|
|
2823
|
+
tenantId: row.tenantId,
|
|
2824
|
+
workspaceId: row.workspaceId,
|
|
2825
|
+
roleId: row.roleId,
|
|
2826
|
+
roleAssignmentId: row.roleAssignmentId,
|
|
2827
|
+
denormalizedRoleName: newName
|
|
2828
|
+
}) : buildRoleAssignmentUserProjectionSkTenantLane({
|
|
2829
|
+
tenantId: row.tenantId,
|
|
2830
|
+
roleId: row.roleId,
|
|
2831
|
+
roleAssignmentId: row.roleAssignmentId,
|
|
2832
|
+
denormalizedRoleName: newName
|
|
2833
|
+
});
|
|
2834
|
+
targets.push({
|
|
2835
|
+
entity: "roleAssignmentUserProjection",
|
|
2836
|
+
oldKey: { userId: row.userId, sk: row.sk },
|
|
2837
|
+
newKey: { userId: row.userId, sk: newSk },
|
|
2838
|
+
newItem: {
|
|
2839
|
+
...row,
|
|
2840
|
+
sk: newSk,
|
|
2841
|
+
denormalizedRoleName: newName
|
|
2842
|
+
},
|
|
2843
|
+
skRewriteRequired: row.sk !== newSk
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
async function pageTenantRename(params) {
|
|
2848
|
+
const { tenantId, newName, cursors, limit, tableName } = params;
|
|
2849
|
+
const service = getDynamoControlService(tableName);
|
|
2850
|
+
const nextCursors = {};
|
|
2851
|
+
const targets = [];
|
|
2852
|
+
const discoveryCursor = cursors.tenantDiscovery;
|
|
2853
|
+
if (discoveryCursor !== null) {
|
|
2854
|
+
const page = await service.entities.membership.query.gsi1({ tenantId, gsi1Shard: "0" }).go({ cursor: discoveryCursor ?? null, limit });
|
|
2855
|
+
for (const row of page.data ?? []) {
|
|
2856
|
+
const userId = extractUserIdFromResource(row.resource);
|
|
2857
|
+
if (userId === void 0) {
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2860
|
+
const userPage = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: `MEMBERSHIP#TENANT#` }).where((attr, op) => op.eq(attr.tenantId, tenantId)).go({});
|
|
2861
|
+
for (const userRow of userPage.data ?? []) {
|
|
2862
|
+
const newSk = buildMembershipUserProjectionSkTenantLane({
|
|
2863
|
+
tenantId: userRow.tenantId,
|
|
2864
|
+
membershipId: userRow.membershipId,
|
|
2865
|
+
denormalizedTenantName: newName
|
|
2866
|
+
});
|
|
2867
|
+
targets.push({
|
|
2868
|
+
entity: "membershipUserProjection",
|
|
2869
|
+
oldKey: { userId: userRow.userId, sk: userRow.sk },
|
|
2870
|
+
newKey: { userId: userRow.userId, sk: newSk },
|
|
2871
|
+
newItem: {
|
|
2872
|
+
...userRow,
|
|
2873
|
+
sk: newSk,
|
|
2874
|
+
denormalizedTenantName: newName
|
|
2875
|
+
},
|
|
2876
|
+
skRewriteRequired: userRow.sk !== newSk
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
const wsPage = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: `MEMBERSHIP#WORKSPACE#TID#${tenantId}#` }).go({});
|
|
2880
|
+
for (const wsRow of wsPage.data ?? []) {
|
|
2881
|
+
const newSk = buildMembershipUserProjectionSkWorkspaceLane({
|
|
2882
|
+
tenantId: wsRow.tenantId,
|
|
2883
|
+
workspaceId: wsRow.workspaceId,
|
|
2884
|
+
membershipId: wsRow.membershipId,
|
|
2885
|
+
denormalizedWorkspaceName: wsRow.denormalizedWorkspaceName
|
|
2886
|
+
});
|
|
2887
|
+
targets.push({
|
|
2888
|
+
entity: "membershipUserProjection",
|
|
2889
|
+
oldKey: { userId: wsRow.userId, sk: wsRow.sk },
|
|
2890
|
+
newKey: { userId: wsRow.userId, sk: newSk },
|
|
2891
|
+
newItem: {
|
|
2892
|
+
...wsRow,
|
|
2893
|
+
sk: newSk,
|
|
2894
|
+
denormalizedTenantName: newName
|
|
2895
|
+
},
|
|
2896
|
+
skRewriteRequired: wsRow.sk !== newSk
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
nextCursors.tenantDiscovery = page.cursor ?? null;
|
|
2901
|
+
} else {
|
|
2902
|
+
nextCursors.tenantDiscovery = null;
|
|
2903
|
+
}
|
|
2904
|
+
nextCursors.membershipUserProjection = null;
|
|
2905
|
+
nextCursors.roleAssignmentUserProjection = null;
|
|
2906
|
+
const exhausted = nextCursors.tenantDiscovery === null;
|
|
2907
|
+
return { targets, cursors: nextCursors, exhausted };
|
|
2908
|
+
}
|
|
2909
|
+
function extractUserIdFromResource(resource) {
|
|
2910
|
+
if (typeof resource !== "string" || resource.length === 0) {
|
|
2911
|
+
return void 0;
|
|
2912
|
+
}
|
|
2913
|
+
let parsed;
|
|
2914
|
+
try {
|
|
2915
|
+
parsed = JSON.parse(resource);
|
|
2916
|
+
} catch {
|
|
2917
|
+
return void 0;
|
|
2918
|
+
}
|
|
2919
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2920
|
+
return void 0;
|
|
2921
|
+
}
|
|
2922
|
+
return extractReferenceSlug(parsed, "user");
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
// src/data/operations/control/rename-cascade/rename-cascade-rewrite-chunk-operation.ts
|
|
2926
|
+
var RENAME_CASCADE_MAX_TARGETS_PER_CHUNK = 50;
|
|
2927
|
+
function chunkRenameCascadeTargets(targets) {
|
|
2928
|
+
const chunks = [];
|
|
2929
|
+
for (let i = 0; i < targets.length; i += RENAME_CASCADE_MAX_TARGETS_PER_CHUNK) {
|
|
2930
|
+
chunks.push(targets.slice(i, i + RENAME_CASCADE_MAX_TARGETS_PER_CHUNK));
|
|
2931
|
+
}
|
|
2932
|
+
return chunks;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// src/workflows/control-plane/rename-cascade/rename-list-targets.handler.ts
|
|
2936
|
+
var handler = async (input) => {
|
|
2937
|
+
const cursors = {};
|
|
2938
|
+
if (input.cursors) {
|
|
2939
|
+
for (const [key, value] of Object.entries(input.cursors)) {
|
|
2940
|
+
cursors[key] = value;
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
const page = await listRenameCascadeTargetsOperation({
|
|
2944
|
+
entityType: input.entityType,
|
|
2945
|
+
entityId: input.entityId,
|
|
2946
|
+
tenantId: input.tenantId,
|
|
2947
|
+
oldName: input.oldName,
|
|
2948
|
+
newName: input.newName,
|
|
2949
|
+
oldNormalizedName: input.oldNormalizedName,
|
|
2950
|
+
newNormalizedName: input.newNormalizedName,
|
|
2951
|
+
cursors
|
|
2952
|
+
});
|
|
2953
|
+
const chunks = chunkRenameCascadeTargets(
|
|
2954
|
+
page.targets
|
|
2955
|
+
).map((targets) => ({
|
|
2956
|
+
entityType: input.entityType,
|
|
2957
|
+
entityId: input.entityId,
|
|
2958
|
+
tenantId: input.tenantId,
|
|
2959
|
+
targets,
|
|
2960
|
+
chunkToken: (0, import_node_crypto.randomUUID)()
|
|
2961
|
+
}));
|
|
2962
|
+
const priorRewritten = input.itemsRewritten ?? 0;
|
|
2963
|
+
const priorChunks = input.chunkCount ?? 0;
|
|
2964
|
+
const itemsRewritten = priorRewritten + page.targets.length;
|
|
2965
|
+
const chunkCount = priorChunks + chunks.length;
|
|
2966
|
+
return {
|
|
2967
|
+
entityType: input.entityType,
|
|
2968
|
+
entityId: input.entityId,
|
|
2969
|
+
tenantId: input.tenantId,
|
|
2970
|
+
oldName: input.oldName,
|
|
2971
|
+
newName: input.newName,
|
|
2972
|
+
oldNormalizedName: input.oldNormalizedName,
|
|
2973
|
+
newNormalizedName: input.newNormalizedName,
|
|
2974
|
+
cursors: page.cursors,
|
|
2975
|
+
chunks,
|
|
2976
|
+
exhausted: page.exhausted,
|
|
2977
|
+
itemsRewritten,
|
|
2978
|
+
chunkCount
|
|
2979
|
+
};
|
|
2980
|
+
};
|
|
2981
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2982
|
+
0 && (module.exports = {
|
|
2983
|
+
handler
|
|
2984
|
+
});
|
|
2985
|
+
//# sourceMappingURL=rename-list-targets.handler.js.map
|