@vuu-ui/vuu-data-test 4.0.0-alpha.2 → 4.0.0-alpha.4

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.
@@ -0,0 +1,412 @@
1
+ import { USER_ADMIN_RPC_CONTRACT, USER_ADMIN_TABLE_SCHEMAS } from "@heswell/user-admin/contracts";
2
+ import { InMemoryUserAdminStore } from "@heswell/user-admin/in-memory";
3
+ import { VuuModule } from "../core/module/VuuModule.js";
4
+ import TableContainer from "../core/table/TableContainer.js";
5
+ import { Table, buildDataColumnMapFromSchema } from "../Table.js";
6
+ import { USER_ADMIN_INITIAL_SNAPSHOT } from "./initialSnapshot.js";
7
+ import { reconcileUserAdminTables, resolveDisplayName } from "./snapshot-projection.js";
8
+ const createTables = ()=>({
9
+ clients: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.clients),
10
+ group_roles: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.group_roles),
11
+ groups: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.groups),
12
+ roles: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.roles),
13
+ user_group_roles: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.user_group_roles),
14
+ user_groups: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.user_groups),
15
+ users: TableContainer.createTable(USER_ADMIN_TABLE_SCHEMAS.users)
16
+ });
17
+ const errorResult = (error)=>({
18
+ errorMessage: error instanceof Error ? error.message : "User admin request failed",
19
+ type: "ERROR_RESULT"
20
+ });
21
+ const isRecord = (value)=>"object" == typeof value && null !== value && !Array.isArray(value);
22
+ const readParams = (request, rpcName)=>{
23
+ if ("RPC_REQUEST" !== request.type || request.rpcName !== rpcName) throw new Error(`Expected ${rpcName} RPC request`);
24
+ if (!isRecord(request.params)) throw new Error(`${rpcName} requires named parameters`);
25
+ const allowed = new Set(USER_ADMIN_RPC_CONTRACT[rpcName]);
26
+ for (const name of Object.keys(request.params))if (!allowed.has(name)) throw new Error(`${rpcName} received an unsupported parameter: ${name}`);
27
+ return request.params;
28
+ };
29
+ const requiredString = (params, name)=>{
30
+ const value = params[name];
31
+ if ("string" != typeof value || 0 === value.length) throw new Error(`${name} must be a non-empty string`);
32
+ return value;
33
+ };
34
+ const optionalString = (params, name)=>{
35
+ const value = params[name];
36
+ if (void 0 !== value && "string" != typeof value) throw new Error(`${name} must be a string`);
37
+ return value;
38
+ };
39
+ const optionalBoolean = (params, name)=>{
40
+ const value = params[name];
41
+ if (void 0 !== value && "boolean" != typeof value) throw new Error(`${name} must be a boolean`);
42
+ return value;
43
+ };
44
+ const optionalStringArray = (params, name)=>{
45
+ const value = params[name];
46
+ if (void 0 !== value && (!Array.isArray(value) || value.some((item)=>"string" != typeof item))) throw new Error(`${name} must be an array of strings`);
47
+ return value;
48
+ };
49
+ const reference = (params, idName, name)=>{
50
+ const id = optionalString(params, idName);
51
+ const label = optionalString(params, name);
52
+ if (!id && !label) throw new Error(`${idName} or ${name} is required`);
53
+ return {
54
+ id,
55
+ label
56
+ };
57
+ };
58
+ const parseAssignments = (value)=>{
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(value);
62
+ } catch {
63
+ throw new Error("assignments must be valid JSON");
64
+ }
65
+ if (!Array.isArray(parsed) || parsed.some((assignment)=>!isRecord(assignment) || "string" != typeof assignment.accessRole || 0 === assignment.accessRole.length || "string" != typeof assignment.groupId || 0 === assignment.groupId.length || Object.keys(assignment).some((name)=>"accessRole" !== name && "groupId" !== name))) throw new Error("assignments must be a JSON array of accessRole and groupId strings");
66
+ return parsed;
67
+ };
68
+ const parseSessionPermissions = (value)=>{
69
+ if ("string" != typeof value) throw new Error("permissions must be a serialized array");
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(value);
73
+ } catch {
74
+ throw new Error("permissions must be valid JSON");
75
+ }
76
+ if (!Array.isArray(parsed)) throw new Error("permissions must contain application group assignments");
77
+ const accessRoles = new Set();
78
+ const permissions = parsed.map((application, index)=>{
79
+ if (!isRecord(application) || Object.keys(application).some((field)=>"clientIdentifier" !== field && "accessRole" !== field && "groupIds" !== field) || "string" != typeof application.clientIdentifier || 0 === application.clientIdentifier.length || "string" != typeof application.accessRole || 0 === application.accessRole.length || !Array.isArray(application.groupIds) || application.groupIds.some((groupId)=>"string" != typeof groupId || 0 === groupId.length)) throw new Error(`permissions[${index}] must contain clientIdentifier, accessRole, and groupIds`);
80
+ if (accessRoles.has(application.accessRole)) throw new Error(`permissions contains duplicate accessRole: ${application.accessRole}`);
81
+ const groupIds = application.groupIds;
82
+ if (new Set(groupIds).size !== groupIds.length) throw new Error(`permissions contains duplicate group IDs for: ${application.accessRole}`);
83
+ accessRoles.add(application.accessRole);
84
+ return {
85
+ clientIdentifier: application.clientIdentifier,
86
+ accessRole: application.accessRole,
87
+ groupIds
88
+ };
89
+ });
90
+ return permissions.flatMap(({ groupIds, accessRole })=>groupIds.map((groupId)=>({
91
+ accessRole,
92
+ groupId
93
+ })));
94
+ };
95
+ class UserAdminModule extends VuuModule {
96
+ #tables;
97
+ store;
98
+ constructor(snapshot = USER_ADMIN_INITIAL_SNAPSHOT){
99
+ super("USER_ADMIN");
100
+ this.store = new InMemoryUserAdminStore(snapshot);
101
+ this.#tables = createTables();
102
+ reconcileUserAdminTables(snapshot, this.#tables);
103
+ }
104
+ get schemas() {
105
+ return USER_ADMIN_TABLE_SCHEMAS;
106
+ }
107
+ get tables() {
108
+ return this.#tables;
109
+ }
110
+ get menus() {
111
+ return {
112
+ clients: void 0,
113
+ group_roles: void 0,
114
+ groups: void 0,
115
+ roles: void 0,
116
+ user_group_roles: void 0,
117
+ user_groups: void 0,
118
+ users: void 0
119
+ };
120
+ }
121
+ get visualLinks() {}
122
+ get menuServices() {}
123
+ get services() {
124
+ const services = [
125
+ {
126
+ rpcName: "addUser",
127
+ service: this.addUser
128
+ },
129
+ {
130
+ rpcName: "updateUser",
131
+ service: this.updateUser
132
+ },
133
+ {
134
+ rpcName: "deleteUser",
135
+ service: this.deleteUser
136
+ },
137
+ {
138
+ rpcName: "addGroup",
139
+ service: this.addGroup
140
+ },
141
+ {
142
+ rpcName: "updateGroup",
143
+ service: this.updateGroup
144
+ },
145
+ {
146
+ rpcName: "deleteGroup",
147
+ service: this.deleteGroup
148
+ },
149
+ {
150
+ rpcName: "addClient",
151
+ service: this.addClient
152
+ },
153
+ {
154
+ rpcName: "updateClient",
155
+ service: this.updateClient
156
+ },
157
+ {
158
+ rpcName: "addRole",
159
+ service: this.addRole
160
+ },
161
+ {
162
+ rpcName: "addClientRole",
163
+ service: this.addClientRole
164
+ },
165
+ {
166
+ rpcName: "updateRole",
167
+ service: this.updateRole
168
+ },
169
+ {
170
+ rpcName: "assignGroupRole",
171
+ service: this.assignGroupRole
172
+ },
173
+ {
174
+ rpcName: "removeGroupRole",
175
+ service: this.removeGroupRole
176
+ },
177
+ {
178
+ rpcName: "assignUserToGroup",
179
+ service: this.assignUserToGroup
180
+ },
181
+ {
182
+ rpcName: "removeUserFromGroup",
183
+ service: this.removeUserFromGroup
184
+ },
185
+ {
186
+ rpcName: "getUserModuleAccessOptions",
187
+ service: this.getUserModuleAccessOptions
188
+ },
189
+ {
190
+ rpcName: "setUserModuleAccess",
191
+ service: this.setUserModuleAccess
192
+ }
193
+ ];
194
+ return {
195
+ clients: services,
196
+ group_roles: services,
197
+ groups: services,
198
+ roles: services,
199
+ user_group_roles: services,
200
+ user_groups: services,
201
+ users: services
202
+ };
203
+ }
204
+ async reconcile() {
205
+ reconcileUserAdminTables(await this.store.snapshot(), this.#tables);
206
+ }
207
+ createSessionTable(sourceTable, sessionTableName, editSessionMode, dataSource, sessionType) {
208
+ const sessionTable = super.createSessionTable(sourceTable, sessionTableName, editSessionMode, dataSource, sessionType);
209
+ if ("users" !== sourceTable.name) return sessionTable;
210
+ const sessionSchema = {
211
+ ...sessionTable.schema,
212
+ columns: sessionTable.schema.columns.concat({
213
+ name: "permissions",
214
+ serverDataType: "string"
215
+ })
216
+ };
217
+ return new Table(sessionSchema, sessionTable.data.map((row)=>row.concat("")), buildDataColumnMapFromSchema(sessionSchema));
218
+ }
219
+ async beforeSessionSave(sourceTable, sessionTable) {
220
+ if ("users" !== sourceTable.name) return;
221
+ const permissionsIndex = sessionTable.map.permissions;
222
+ const actionIndex = sessionTable.map.vuuAction;
223
+ const userIdIndex = sessionTable.map.user_id;
224
+ const usernameIndex = sessionTable.map.username;
225
+ for (const row of sessionTable.data){
226
+ if ("editCell" !== row[actionIndex]) continue;
227
+ const userId = String(row[userIdIndex]);
228
+ const sourceRow = sourceTable.findByKey(userId);
229
+ if (!sourceRow || sourceRow[sourceTable.map.username] !== row[usernameIndex]) throw new Error("username is read-only");
230
+ if ("string" == typeof row[permissionsIndex] && "" !== row[permissionsIndex]) await this.store.setUserModuleAccess(userId, parseSessionPermissions(row[permissionsIndex]));
231
+ }
232
+ }
233
+ async afterSessionSave(sourceTable) {
234
+ if ("users" === sourceTable.name) await this.reconcile();
235
+ }
236
+ mutation = (rpcName, mutate)=>async (request)=>{
237
+ try {
238
+ await mutate(readParams(request, rpcName));
239
+ await this.reconcile();
240
+ return {
241
+ data: void 0,
242
+ type: "SUCCESS_RESULT"
243
+ };
244
+ } catch (error) {
245
+ return errorResult(error);
246
+ }
247
+ };
248
+ addUser = this.mutation("addUser", async (params)=>{
249
+ const groupIds = optionalStringArray(params, "group_ids");
250
+ const username = requiredString(params, "username");
251
+ await this.assertUsernameIsUnique(username);
252
+ await this.store.addUser({
253
+ email: optionalString(params, "email"),
254
+ emailVerified: optionalBoolean(params, "emailVerified"),
255
+ enabled: optionalBoolean(params, "enabled"),
256
+ firstName: optionalString(params, "firstName"),
257
+ lastName: optionalString(params, "lastName"),
258
+ temporary_password: optionalString(params, "temporary_password"),
259
+ username
260
+ });
261
+ if (groupIds) {
262
+ const user = await this.store.findUserByUsername(requiredString(params, "username"));
263
+ if (!user) throw new Error("Created user could not be found");
264
+ await this.store.syncUserGroups(user.id, groupIds);
265
+ }
266
+ });
267
+ updateUser = this.mutation("updateUser", async (params)=>{
268
+ const groupIds = optionalStringArray(params, "group_ids");
269
+ const userId = requiredString(params, "userId");
270
+ const username = optionalString(params, "username");
271
+ if (username) await this.assertUsernameIsUnique(username, userId);
272
+ await this.store.updateUser({
273
+ email: optionalString(params, "email"),
274
+ emailVerified: optionalBoolean(params, "emailVerified"),
275
+ enabled: optionalBoolean(params, "enabled"),
276
+ firstName: optionalString(params, "firstName"),
277
+ lastName: optionalString(params, "lastName"),
278
+ temporary_password: optionalString(params, "temporary_password"),
279
+ userId,
280
+ username
281
+ });
282
+ if (groupIds) await this.store.syncUserGroups(userId, groupIds);
283
+ });
284
+ deleteUser = this.mutation("deleteUser", (params)=>this.store.deleteUser(requiredString(params, "userId")));
285
+ addGroup = this.mutation("addGroup", (params)=>this.store.addGroup({
286
+ name: requiredString(params, "name")
287
+ }));
288
+ updateGroup = this.mutation("updateGroup", (params)=>this.store.updateGroup(requiredString(params, "groupId"), {
289
+ name: requiredString(params, "name")
290
+ }));
291
+ deleteGroup = this.mutation("deleteGroup", (params)=>this.store.deleteGroup(requiredString(params, "groupId")));
292
+ addClient = this.mutation("addClient", (params)=>this.store.addClient({
293
+ clientId: requiredString(params, "clientId"),
294
+ description: optionalString(params, "description"),
295
+ enabled: optionalBoolean(params, "enabled"),
296
+ name: optionalString(params, "name")
297
+ }));
298
+ updateClient = this.mutation("updateClient", (params)=>this.store.updateClient(requiredString(params, "clientId"), {
299
+ description: optionalString(params, "description"),
300
+ enabled: optionalBoolean(params, "enabled"),
301
+ name: optionalString(params, "name")
302
+ }));
303
+ addRole = this.mutation("addRole", (params)=>this.store.addRole({
304
+ description: optionalString(params, "description"),
305
+ name: requiredString(params, "name")
306
+ }));
307
+ addClientRole = this.mutation("addClientRole", (params)=>this.store.addClientRole({
308
+ clientKey: requiredString(params, "clientId")
309
+ }, {
310
+ description: optionalString(params, "description"),
311
+ name: requiredString(params, "name")
312
+ }));
313
+ updateRole = this.mutation("updateRole", async (params)=>{
314
+ const clientId = optionalString(params, "clientId");
315
+ const changes = {
316
+ description: optionalString(params, "description"),
317
+ name: optionalString(params, "name")
318
+ };
319
+ if (clientId) await this.store.updateClientRole(clientId, requiredString(params, "roleName"), changes);
320
+ else await this.store.updateRealmRole(requiredString(params, "roleId"), changes);
321
+ });
322
+ assignGroupRole = this.mutation("assignGroupRole", (params)=>{
323
+ const group = reference(params, "groupId", "groupName");
324
+ const role = reference(params, "roleId", "roleName");
325
+ const clientId = optionalString(params, "clientId");
326
+ return this.store.addRoleToGroup({
327
+ groupId: group.id,
328
+ groupName: group.label
329
+ }, {
330
+ roleId: role.id,
331
+ roleName: role.label
332
+ }, clientId ? {
333
+ clientKey: clientId
334
+ } : void 0);
335
+ });
336
+ removeGroupRole = this.mutation("removeGroupRole", (params)=>{
337
+ const group = reference(params, "groupId", "groupName");
338
+ const role = reference(params, "roleId", "roleName");
339
+ const clientId = optionalString(params, "clientId");
340
+ return this.store.removeRoleFromGroup({
341
+ groupId: group.id,
342
+ groupName: group.label
343
+ }, {
344
+ roleId: role.id,
345
+ roleName: role.label
346
+ }, clientId ? {
347
+ clientKey: clientId
348
+ } : void 0);
349
+ });
350
+ assignUserToGroup = this.mutation("assignUserToGroup", (params)=>{
351
+ const user = reference(params, "userId", "username");
352
+ const group = reference(params, "groupId", "groupName");
353
+ return this.store.addUserToGroup({
354
+ userId: user.id,
355
+ username: user.label
356
+ }, {
357
+ groupId: group.id,
358
+ groupName: group.label
359
+ });
360
+ });
361
+ removeUserFromGroup = this.mutation("removeUserFromGroup", (params)=>{
362
+ const user = reference(params, "userId", "username");
363
+ const group = reference(params, "groupId", "groupName");
364
+ return this.store.removeUserFromGroup({
365
+ userId: user.id,
366
+ username: user.label
367
+ }, {
368
+ groupId: group.id,
369
+ groupName: group.label
370
+ });
371
+ });
372
+ getUserModuleAccessOptions = async (request)=>{
373
+ try {
374
+ const params = readParams(request, "getUserModuleAccessOptions");
375
+ const [options, snapshot] = await Promise.all([
376
+ this.store.getUserModuleAccessOptions(requiredString(params, "userId")),
377
+ this.store.snapshot()
378
+ ]);
379
+ const groupDisplayNames = new Map(snapshot.groups.map(({ groupDisplayName, id, name })=>[
380
+ id,
381
+ resolveDisplayName(name, groupDisplayName)
382
+ ]));
383
+ const roleDisplayNames = new Map(snapshot.clientRoles.map(({ role })=>[
384
+ role.id,
385
+ resolveDisplayName(role.name, role.roleDisplayName)
386
+ ]));
387
+ return {
388
+ data: {
389
+ modules: options.modules.map(({ groups, ...module })=>({
390
+ ...module,
391
+ groups: groups.map((group)=>({
392
+ ...group,
393
+ groupDisplayName: groupDisplayNames.get(group.groupId) ?? resolveDisplayName(group.groupName),
394
+ roleDisplayName: roleDisplayNames.get(group.roleId) ?? resolveDisplayName(group.roleName)
395
+ }))
396
+ }))
397
+ },
398
+ type: "SUCCESS_RESULT"
399
+ };
400
+ } catch (error) {
401
+ return errorResult(error);
402
+ }
403
+ };
404
+ setUserModuleAccess = this.mutation("setUserModuleAccess", (params)=>this.store.setUserModuleAccess(requiredString(params, "userId"), parseAssignments(requiredString(params, "assignments"))));
405
+ async assertUsernameIsUnique(username, userId) {
406
+ const normalizedUsername = username.toLocaleLowerCase();
407
+ const existingUser = (await this.store.snapshot()).users.find((user)=>user.id !== userId && user.username.toLocaleLowerCase() === normalizedUsername);
408
+ if (existingUser) throw new Error("username must be unique");
409
+ }
410
+ }
411
+ const userAdminModule = new UserAdminModule();
412
+ export { UserAdminModule, userAdminModule };
@@ -0,0 +1,3 @@
1
+ export { UserAdminModule, userAdminModule } from "./UserAdminModule.js";
2
+ export { USER_ADMIN_INITIAL_SNAPSHOT } from "./initialSnapshot.js";
3
+ export { projectUserAdminSnapshot, reconcileUserAdminTables } from "./snapshot-projection.js";