@vuu-ui/vuu-data-test 4.0.0-alpha.10 → 4.0.0-alpha.11

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/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.0.0-alpha.10",
2
+ "version": "4.0.0-alpha.11",
3
3
  "main": "./src/index.js",
4
4
  "type": "module",
5
5
  "author": "heswell",
@@ -12,14 +12,14 @@
12
12
  "dependencies": {
13
13
  "@heswell/module-admin": "1.0.0",
14
14
  "@heswell/user-admin": "1.0.1",
15
- "@vuu-ui/vuu-data-editing": "4.0.0-alpha.10",
16
- "@vuu-ui/vuu-data-local": "4.0.0-alpha.10",
17
- "@vuu-ui/vuu-utils": "4.0.0-alpha.10"
15
+ "@vuu-ui/vuu-data-editing": "4.0.0-alpha.11",
16
+ "@vuu-ui/vuu-data-local": "4.0.0-alpha.11",
17
+ "@vuu-ui/vuu-utils": "4.0.0-alpha.11"
18
18
  },
19
19
  "devDependencies": {
20
- "@vuu-ui/vuu-data-types": "4.0.0-alpha.10",
21
- "@vuu-ui/vuu-protocol-types": "4.0.0-alpha.10",
22
- "@vuu-ui/vuu-table-types": "4.0.0-alpha.10"
20
+ "@vuu-ui/vuu-data-types": "4.0.0-alpha.11",
21
+ "@vuu-ui/vuu-protocol-types": "4.0.0-alpha.11",
22
+ "@vuu-ui/vuu-table-types": "4.0.0-alpha.11"
23
23
  },
24
24
  "sideEffects": false,
25
25
  "files": [
@@ -65,6 +65,19 @@ const parseAssignments = (value)=>{
65
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
66
  return parsed;
67
67
  };
68
+ const parseSessionRoleAssignments = (value)=>{
69
+ if ("string" != typeof value) throw new Error("role_assignments must be a serialized array");
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(value);
73
+ } catch {
74
+ throw new Error("role_assignments must be a valid JSON array");
75
+ }
76
+ if (!Array.isArray(parsed) || parsed.some((roleId)=>"string" != typeof roleId || !roleId)) throw new Error("role_assignments must be an array of role ID strings");
77
+ return [
78
+ ...new Set(parsed)
79
+ ];
80
+ };
68
81
  const parseSessionPermissions = (value)=>{
69
82
  if ("string" != typeof value) throw new Error("permissions must be a serialized array");
70
83
  let parsed;
@@ -206,17 +219,32 @@ class UserAdminModule extends VuuModule {
206
219
  }
207
220
  createSessionTable(sourceTable, sessionTableName, editSessionMode, dataSource, sessionType) {
208
221
  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({
222
+ const extraColumns = "users" === sourceTable.name ? [
223
+ {
213
224
  name: "permissions",
214
225
  serverDataType: "string"
215
- })
226
+ }
227
+ ] : "groups" === sourceTable.name ? [
228
+ {
229
+ name: "group_name",
230
+ serverDataType: "string"
231
+ },
232
+ {
233
+ name: "role_assignments",
234
+ serverDataType: "string"
235
+ }
236
+ ] : [];
237
+ const columnsToAdd = extraColumns.filter(({ name })=>!sessionTable.schema.columns.some((column)=>column.name === name));
238
+ if (0 === columnsToAdd.length) return sessionTable;
239
+ const sessionSchema = {
240
+ ...sessionTable.schema,
241
+ columns: sessionTable.schema.columns.concat(columnsToAdd)
216
242
  };
217
- return new Table(sessionSchema, sessionTable.data.map((row)=>row.concat("")), buildDataColumnMapFromSchema(sessionSchema));
243
+ return new Table(sessionSchema, sessionTable.data.map((row)=>row.concat(columnsToAdd.map(()=>""))), buildDataColumnMapFromSchema(sessionSchema));
218
244
  }
219
245
  async beforeSessionSave(sourceTable, sessionTable) {
246
+ if ("roles" === sourceTable.name) return void await this.saveClientRolesFromSession(sessionTable);
247
+ if ("groups" === sourceTable.name) return void await this.saveGroupsFromSession(sessionTable);
220
248
  if ("users" !== sourceTable.name) return;
221
249
  const permissionsIndex = sessionTable.map.permissions;
222
250
  const actionIndex = sessionTable.map.vuuAction;
@@ -230,8 +258,135 @@ class UserAdminModule extends VuuModule {
230
258
  if ("string" == typeof row[permissionsIndex] && "" !== row[permissionsIndex]) await this.store.setUserModuleAccess(userId, parseSessionPermissions(row[permissionsIndex]));
231
259
  }
232
260
  }
233
- async afterSessionSave(sourceTable) {
234
- if ("users" === sourceTable.name) await this.reconcile();
261
+ async afterSessionSave(sourceTable, sessionTable) {
262
+ const roleCreationSaved = "roles" === sourceTable.name && sessionTable.data.some((row)=>"addRow" === row[sessionTable.map.vuuAction] && "client role created" === row[sessionTable.map.vuuMsg]);
263
+ const groupChangesSaved = "groups" === sourceTable.name && sessionTable.data.some((row)=>"group created" === row[sessionTable.map.vuuMsg] || "group roles updated" === row[sessionTable.map.vuuMsg]);
264
+ if ("users" === sourceTable.name || roleCreationSaved || groupChangesSaved) await this.reconcile();
265
+ }
266
+ async saveGroupsFromSession(sessionTable) {
267
+ const actionIndex = sessionTable.map.vuuAction;
268
+ const messageIndex = sessionTable.map.vuuMsg;
269
+ const keyIndex = sessionTable.map[sessionTable.schema.key];
270
+ const groupIdIndex = sessionTable.map.group_id;
271
+ const groupNameIndex = sessionTable.map.group_name;
272
+ const roleAssignmentsIndex = sessionTable.map.role_assignments;
273
+ for (const row of sessionTable.data)if ("addRow" !== row[actionIndex] || row[messageIndex]) {
274
+ if ("editCell" === row[actionIndex] && "string" == typeof row[roleAssignmentsIndex] && "" !== row[roleAssignmentsIndex]) {
275
+ const groupId = requiredString({
276
+ group_id: row[groupIdIndex]
277
+ }, "group_id");
278
+ const roleIds = parseSessionRoleAssignments(row[roleAssignmentsIndex]);
279
+ await this.saveGroupRoleAssignments(groupId, roleIds, await this.store.snapshot());
280
+ sessionTable.update(String(row[keyIndex]), "vuuMsg", "group roles updated");
281
+ }
282
+ } else {
283
+ const name = requiredString({
284
+ group_name: row[groupNameIndex]
285
+ }, "group_name").trim();
286
+ if (!name) throw new Error("group_name must be a non-empty string");
287
+ const roleIds = parseSessionRoleAssignments(row[roleAssignmentsIndex]);
288
+ const before = await this.store.snapshot();
289
+ this.resolveGroupRoleAssignments(roleIds, before);
290
+ const existingGroupIds = new Set(before.groups.map(({ id })=>id));
291
+ await this.store.addGroup({
292
+ name
293
+ });
294
+ const snapshot = await this.store.snapshot();
295
+ const group = snapshot.groups.find(({ id, name: groupName })=>!existingGroupIds.has(id) && groupName === name);
296
+ if (!group) throw new Error("Created group could not be found");
297
+ await this.saveGroupRoleAssignments(group.id, roleIds, snapshot);
298
+ sessionTable.update(String(row[keyIndex]), "vuuMsg", "group created");
299
+ }
300
+ }
301
+ async saveGroupRoleAssignments(groupId, desiredRoleIds, snapshot) {
302
+ if (!snapshot.groups.some(({ id })=>id === groupId)) throw new Error(`User admin group not found: ${groupId}`);
303
+ const groupRoles = snapshot.groupRoles.filter(({ group })=>group.id === groupId);
304
+ const roleAssignmentsById = new Map(snapshot.clientRoles.map((assignment)=>[
305
+ assignment.role.id,
306
+ assignment
307
+ ]));
308
+ const desiredRoleSet = new Set(desiredRoleIds);
309
+ const currentAssignments = groupRoles.map(({ role })=>{
310
+ const assignment = roleAssignmentsById.get(role.id);
311
+ if (!assignment) throw new Error(`Assigned user admin role no longer exists: ${role.id}`);
312
+ return assignment;
313
+ });
314
+ const currentRoleSet = new Set(groupRoles.map(({ role })=>role.id));
315
+ const desiredAssignments = this.resolveGroupRoleAssignments(desiredRoleIds, snapshot);
316
+ for (const assignment of currentAssignments)if (!desiredRoleSet.has(assignment.role.id)) {
317
+ const clientRef = assignment.role.clientRole && assignment.client?.clientId ? {
318
+ clientKey: assignment.client.clientId
319
+ } : void 0;
320
+ await this.store.removeRoleFromGroup({
321
+ groupId
322
+ }, {
323
+ roleId: assignment.role.id
324
+ }, clientRef);
325
+ }
326
+ for (const assignment of desiredAssignments)if (!currentRoleSet.has(assignment.role.id)) {
327
+ const clientRef = assignment.role.clientRole && assignment.client?.clientId ? {
328
+ clientKey: assignment.client.clientId
329
+ } : void 0;
330
+ await this.store.addRoleToGroup({
331
+ groupId
332
+ }, {
333
+ roleId: assignment.role.id
334
+ }, clientRef);
335
+ }
336
+ }
337
+ resolveGroupRoleAssignments(roleIds, snapshot) {
338
+ const roleAssignmentsById = new Map(snapshot.clientRoles.map((assignment)=>[
339
+ assignment.role.id,
340
+ assignment
341
+ ]));
342
+ return roleIds.map((roleId)=>{
343
+ const assignment = roleAssignmentsById.get(roleId);
344
+ if (!assignment) throw new Error(`User admin role not found: ${roleId}`);
345
+ if (assignment.role.clientRole && !assignment.client?.clientId) throw new Error(`User admin client role has no owning client: ${roleId}`);
346
+ return assignment;
347
+ });
348
+ }
349
+ async saveClientRolesFromSession(sessionTable) {
350
+ const actionIndex = sessionTable.map.vuuAction;
351
+ const messageIndex = sessionTable.map.vuuMsg;
352
+ const keyIndex = sessionTable.map[sessionTable.schema.key];
353
+ const snapshot = await this.store.snapshot();
354
+ const clientsById = new Map(snapshot.clients.map((client)=>[
355
+ client.id,
356
+ client
357
+ ]));
358
+ const pendingRoles = [
359
+ ...sessionTable.data
360
+ ].flatMap((row)=>{
361
+ if ("addRow" !== row[actionIndex] || row[messageIndex]) return [];
362
+ const params = Object.fromEntries(sessionTable.schema.columns.map(({ name })=>[
363
+ name,
364
+ row[sessionTable.map[name]]
365
+ ]));
366
+ const roleName = requiredString(params, "role_name").trim();
367
+ if (!roleName) throw new Error("role_name must be a non-empty string");
368
+ const clientId = requiredString(params, "client_id");
369
+ const clientIdentifier = requiredString(params, "client_identifier");
370
+ const client = clientsById.get(clientId);
371
+ if (!client || client.clientId !== clientIdentifier) throw new Error("client_id and client_identifier must identify the same client");
372
+ return [
373
+ {
374
+ client,
375
+ description: optionalString(params, "description") ?? "",
376
+ roleName,
377
+ row
378
+ }
379
+ ];
380
+ });
381
+ for (const { client, description, roleName, row } of pendingRoles){
382
+ await this.store.addClientRole({
383
+ clientKey: client.clientId
384
+ }, {
385
+ description,
386
+ name: roleName
387
+ });
388
+ sessionTable.update(String(row[keyIndex]), "vuuMsg", "client role created");
389
+ }
235
390
  }
236
391
  mutation = (rpcName, mutate)=>async (request)=>{
237
392
  try {
@@ -6,6 +6,34 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
6
6
  enabled: true,
7
7
  id: "client-portal",
8
8
  name: "VUU Portal"
9
+ },
10
+ {
11
+ clientId: "vuu-user-admin",
12
+ description: "User administration",
13
+ enabled: true,
14
+ id: "client-user-admin",
15
+ name: "User Admin"
16
+ },
17
+ {
18
+ clientId: "vuu-basket-trading",
19
+ description: "Basket trading",
20
+ enabled: true,
21
+ id: "client-basket-trading",
22
+ name: "Basket Trading"
23
+ },
24
+ {
25
+ clientId: "vuu-module-admin",
26
+ description: "Module administration",
27
+ enabled: true,
28
+ id: "client-module-admin",
29
+ name: "Module Admin"
30
+ },
31
+ {
32
+ clientId: "vuu-feature-filter-table",
33
+ description: "Instrument tables",
34
+ enabled: true,
35
+ id: "client-feature-filter-table",
36
+ name: "Instruments"
9
37
  }
10
38
  ],
11
39
  clientRoles: [
@@ -67,13 +95,13 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
67
95
  },
68
96
  {
69
97
  client: {
70
- clientId: "vuu-portal",
71
- id: "client-portal",
72
- name: "VUU Portal"
98
+ clientId: "vuu-user-admin",
99
+ id: "client-user-admin",
100
+ name: "User Admin"
73
101
  },
74
102
  role: {
75
103
  clientRole: true,
76
- containerId: "client-portal",
104
+ containerId: "client-user-admin",
77
105
  id: "role-user-admin-admin",
78
106
  name: "user-admin-admin",
79
107
  roleDisplayName: "admin"
@@ -81,13 +109,13 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
81
109
  },
82
110
  {
83
111
  client: {
84
- clientId: "vuu-portal",
85
- id: "client-portal",
86
- name: "VUU Portal"
112
+ clientId: "vuu-module-admin",
113
+ id: "client-module-admin",
114
+ name: "Module Admin"
87
115
  },
88
116
  role: {
89
117
  clientRole: true,
90
- containerId: "client-portal",
118
+ containerId: "client-module-admin",
91
119
  id: "role-module-admin-admin",
92
120
  name: "module-admin-admin",
93
121
  roleDisplayName: "admin"
@@ -95,29 +123,17 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
95
123
  },
96
124
  {
97
125
  client: {
98
- clientId: "vuu-portal",
99
- id: "client-portal",
100
- name: "VUU Portal"
126
+ clientId: "vuu-basket-trading",
127
+ id: "client-basket-trading",
128
+ name: "Basket Trading"
101
129
  },
102
130
  role: {
103
131
  clientRole: true,
104
- containerId: "client-portal",
132
+ containerId: "client-basket-trading",
105
133
  id: "role-basket-trading-trade",
106
134
  name: "basket-trading-trade",
107
135
  roleDisplayName: "trade"
108
136
  }
109
- },
110
- {
111
- client: {
112
- clientId: "realm",
113
- id: "realm",
114
- name: "Realm"
115
- },
116
- role: {
117
- id: "role-admin",
118
- name: "admin",
119
- roleDisplayName: "admin"
120
- }
121
137
  }
122
138
  ],
123
139
  groupRoles: [
@@ -225,9 +241,9 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
225
241
  },
226
242
  {
227
243
  client: {
228
- clientId: "vuu-portal",
229
- id: "client-portal",
230
- name: "VUU Portal"
244
+ clientId: "vuu-user-admin",
245
+ id: "client-user-admin",
246
+ name: "User Admin"
231
247
  },
232
248
  group: {
233
249
  id: "group-user-admin-admin",
@@ -261,9 +277,9 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
261
277
  },
262
278
  {
263
279
  client: {
264
- clientId: "vuu-portal",
265
- id: "client-portal",
266
- name: "VUU Portal"
280
+ clientId: "vuu-module-admin",
281
+ id: "client-module-admin",
282
+ name: "Module Admin"
267
283
  },
268
284
  group: {
269
285
  id: "group-module-admin-admin",
@@ -297,9 +313,9 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
297
313
  },
298
314
  {
299
315
  client: {
300
- clientId: "vuu-portal",
301
- id: "client-portal",
302
- name: "VUU Portal"
316
+ clientId: "vuu-basket-trading",
317
+ id: "client-basket-trading",
318
+ name: "Basket Trading"
303
319
  },
304
320
  group: {
305
321
  id: "group-basket-trading-trade",
@@ -312,19 +328,6 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
312
328
  name: "basket-trading-trade",
313
329
  roleDisplayName: "trade"
314
330
  }
315
- },
316
- {
317
- group: {
318
- id: "group-admins",
319
- name: "administrators",
320
- groupDisplayName: "administrators",
321
- path: "/vuu/administrators"
322
- },
323
- role: {
324
- id: "role-admin",
325
- name: "admin",
326
- roleDisplayName: "admin"
327
- }
328
331
  }
329
332
  ],
330
333
  groups: [
@@ -381,12 +384,6 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
381
384
  name: "basket-trading-trade",
382
385
  groupDisplayName: "trade",
383
386
  path: "/vuu/basket-trading-trade"
384
- },
385
- {
386
- id: "group-admins",
387
- name: "administrators",
388
- groupDisplayName: "administrators",
389
- path: "/vuu/administrators"
390
387
  }
391
388
  ],
392
389
  timestamp: 1710000000000,
@@ -438,19 +435,6 @@ const USER_ADMIN_INITIAL_SNAPSHOT = {
438
435
  id: "user-alice",
439
436
  username: "alice"
440
437
  }
441
- },
442
- {
443
- group: {
444
- id: "group-admins",
445
- name: "administrators",
446
- groupDisplayName: "administrators",
447
- path: "/vuu/administrators"
448
- },
449
- user: {
450
- email: "alice@example.com",
451
- id: "user-alice",
452
- username: "alice"
453
- }
454
438
  }
455
439
  ],
456
440
  users: [
@@ -25,7 +25,11 @@ export declare class UserAdminModule extends VuuModule<UserAdminTableName> {
25
25
  reconcile(): Promise<void>;
26
26
  protected createSessionTable(sourceTable: Table, sessionTableName: string, editSessionMode: import("@vuu-ui/vuu-data-types").EditSessionMode | import("@vuu-ui/vuu-data-types").CopyOption, dataSource: import("../TickingArrayDataSource").TickingArrayDataSource, sessionType?: import("@vuu-ui/vuu-data-types").SessionType): Table;
27
27
  protected beforeSessionSave(sourceTable: Table, sessionTable: Table): Promise<void>;
28
- protected afterSessionSave(sourceTable: Table): Promise<void>;
28
+ protected afterSessionSave(sourceTable: Table, sessionTable: Table): Promise<void>;
29
+ private saveGroupsFromSession;
30
+ private saveGroupRoleAssignments;
31
+ private resolveGroupRoleAssignments;
32
+ private saveClientRolesFromSession;
29
33
  private mutation;
30
34
  private addUser;
31
35
  private updateUser;