@memberjunction/server 5.29.0 → 5.30.1

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.
@@ -1,6 +1,6 @@
1
1
  import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
- import { BaseEntity, CompositeKey, EntityDeleteOptions, EntitySaveOptions, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
3
+ import { BaseEntity, CompositeKey, DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
6
6
  import { MJDatasetItemEntity } from '@memberjunction/core-entities';
@@ -213,30 +213,40 @@ export class SyncDataResolver {
213
213
 
214
214
  protected async SyncSingleItemDeleteWithFilter(entityName: string, filter: string, result: ActionItemOutputType, user: UserInfo, userPayload: UserPayload) {
215
215
  try {
216
- // here we will iterate through the result of a RunView on the entityname/filter and delete each matching record
217
- let overallSuccess: boolean = true;
218
- let combinedErrorMessage: string = "";
216
+ // Run the view to find matching records, then delete them all atomically
217
+ // any single failure rolls back the entire batch so the dataset stays consistent.
219
218
  const rv = new RunView();
220
219
  const data = await rv.RunView<BaseEntity>({
221
220
  EntityName: entityName,
222
221
  ExtraFilter: filter,
223
222
  ResultType: 'entity_object'
224
223
  }, user);
225
- if (data && data.Success) {
224
+
225
+ if (!data || !data.Success) {
226
+ result.Success = false;
227
+ result.ErrorMessage = 'Failed to run the view to get the list of items to delete for entity: ' + entityName + ' with filter: ' + filter + '\n';
228
+ return;
229
+ }
230
+
231
+ if (data.Results.length === 0) {
232
+ result.Success = true;
233
+ return;
234
+ }
235
+
236
+ const provider = Metadata.Provider as DatabaseProviderBase;
237
+ await provider.BeginTransaction();
238
+ try {
226
239
  for (const entityObject of data.Results) {
227
240
  if (!await entityObject.Delete()) {
228
- overallSuccess = false;
229
- combinedErrorMessage += 'Failed to delete the item :' + entityObject.LatestResult.CompleteMessage + '\n';
241
+ throw new Error('Failed to delete the item: ' + entityObject.LatestResult.CompleteMessage);
230
242
  }
231
243
  }
232
- result.Success = overallSuccess
233
- if (!overallSuccess) {
234
- result.ErrorMessage = combinedErrorMessage
235
- }
236
- }
237
- else {
244
+ await provider.CommitTransaction();
245
+ result.Success = true;
246
+ } catch (txErr) {
247
+ await provider.RollbackTransaction();
238
248
  result.Success = false;
239
- result.ErrorMessage = 'Failed to run the view to get the list of items to delete for entity: ' + entityName + ' with filter: ' + filter + '\n';
249
+ result.ErrorMessage = typeof txErr === 'string' ? txErr : (txErr as Error).message;
240
250
  }
241
251
  }
242
252
  catch (e) {
@@ -1,6 +1,6 @@
1
1
  import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
- import { EntityDeleteOptions, EntitySaveOptions, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
3
+ import { DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
4
  import { UUIDsEqual } from '@memberjunction/global';
5
5
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
6
6
  import { MJRoleEntity, MJUserEntity, MJUserRoleEntity } from '@memberjunction/core-entities';
@@ -87,24 +87,36 @@ export class SyncRolesAndUsersResolver {
87
87
  @Arg('data', () => RolesAndUsersInputType ) data: RolesAndUsersInputType,
88
88
  @Ctx() context: AppContext
89
89
  ) {
90
+ // Wrap roles + users in one DB transaction so a user-sync failure
91
+ // rolls back any role changes made earlier in this call. Prior code
92
+ // attempted this with a TransactionGroup but ran into nesting issues —
93
+ // direct DB transactions per the plan doc avoid that.
90
94
  try {
91
- // first we sync the roles, then the users
92
- const roleResult = await this.SyncRoles(data.Roles, context);
93
- if (roleResult?.Success) {
94
- const usersResult = await this.SyncUsers(data.Users, context);
95
- if (usersResult?.Success) {
96
- // refresh the user cache, don't set an auto-refresh
97
- // interval here becuase that is alreayd done at startup
98
- // and will keep going on its own as per the config. This is a
99
- // special one-time refresh since we made changes here.
100
- await UserCache.Instance.Refresh(context.dataSource);
95
+ const provider = Metadata.Provider as DatabaseProviderBase;
96
+ await provider.BeginTransaction();
97
+ try {
98
+ const roleResult = await this.DoSyncRoles(data.Roles, context.userPayload.userRecord, context.userPayload);
99
+ if (!roleResult.Success) {
100
+ await provider.RollbackTransaction();
101
+ return roleResult;
101
102
  }
103
+
104
+ const usersResult = await this.DoSyncUsers(data.Users, context.userPayload.userRecord, context.userPayload);
105
+ if (!usersResult.Success) {
106
+ await provider.RollbackTransaction();
107
+ return usersResult;
108
+ }
109
+
110
+ await provider.CommitTransaction();
111
+
112
+ // refresh the user cache — one-time, since the normal auto-refresh is already scheduled
113
+ await UserCache.Instance.Refresh(context.dataSource);
102
114
  return usersResult;
115
+ } catch (txErr) {
116
+ await provider.RollbackTransaction();
117
+ throw txErr;
103
118
  }
104
- else {
105
- return roleResult;
106
- }
107
- }
119
+ }
108
120
  catch (err) {
109
121
  LogError(err);
110
122
  throw new Error('Error syncing roles and users\n\n' + err);
@@ -121,76 +133,93 @@ export class SyncRolesAndUsersResolver {
121
133
  @Arg('roles', () => [RoleInputType]) roles: RoleInputType[],
122
134
  @Ctx() context: AppContext
123
135
  ) : Promise<SyncRolesAndUsersResultType> {
136
+ // Wrap delete + add + update of roles in one DB transaction so any failure
137
+ // rolls back the whole batch. Keeps the sync idempotent across retries.
124
138
  try {
125
- // we iterate through the provided roles and we remove roles that are not in the input and add roles that are new
126
- // and update roles that already exist
127
- const rv = new RunView();
128
- const result = await rv.RunView<MJRoleEntity>({
129
- EntityName: "MJ: Roles",
130
- ResultType: 'entity_object'
131
- }, context.userPayload.userRecord);
132
-
133
- if (result && result.Success) {
134
- const currentRoles = result.Results;
135
- if (await this.DeleteRemovedRoles(currentRoles, roles, context.userPayload.userRecord, context.userPayload)) {
136
- if ( await this.AddNewRoles(currentRoles, roles, context.userPayload.userRecord, context.userPayload)) {
137
- return await this.UpdateExistingRoles(currentRoles, roles, context.userPayload);
138
- }
139
+ const provider = Metadata.Provider as DatabaseProviderBase;
140
+ await provider.BeginTransaction();
141
+ try {
142
+ const result = await this.DoSyncRoles(roles, context.userPayload.userRecord, context.userPayload);
143
+ if (result.Success) {
144
+ await provider.CommitTransaction();
145
+ } else {
146
+ await provider.RollbackTransaction();
139
147
  }
148
+ return result;
149
+ } catch (txErr) {
150
+ await provider.RollbackTransaction();
151
+ throw txErr;
140
152
  }
141
-
142
- return { Success: false }; // if we get here, something went wrong
143
153
  } catch (err) {
144
154
  LogError(err);
145
155
  throw new Error('Error syncing roles and users\n\n' + err);
146
156
  }
147
157
  }
148
158
 
149
- protected async UpdateExistingRoles(currentRoles: MJRoleEntity[], futureRoles: RoleInputType[], userPayload: UserPayload): Promise<SyncRolesAndUsersResultType> {
150
- // go through the future roles and update any that are in the current roles
151
- const md = new Metadata();
152
- let ok: boolean = true;
159
+ /**
160
+ * Transaction-free core of SyncRoles expected to be invoked inside an outer transaction.
161
+ * Throws on any Save/Delete failure so the outer transaction rolls back. A returned
162
+ * `{ Success: true }` means every operation succeeded.
163
+ */
164
+ protected async DoSyncRoles(roles: RoleInputType[], user: UserInfo, userPayload: UserPayload): Promise<SyncRolesAndUsersResultType> {
165
+ const rv = new RunView();
166
+ const result = await rv.RunView<MJRoleEntity>({
167
+ EntityName: "MJ: Roles",
168
+ ResultType: 'entity_object'
169
+ }, user);
170
+
171
+ if (!result || !result.Success) {
172
+ return { Success: false };
173
+ }
174
+
175
+ const currentRoles = result.Results;
176
+ await this.DeleteRemovedRoles(currentRoles, roles, user, userPayload);
177
+ await this.AddNewRoles(currentRoles, roles, user, userPayload);
178
+ await this.UpdateExistingRoles(currentRoles, roles, userPayload);
179
+ return { Success: true };
180
+ }
153
181
 
182
+ protected async UpdateExistingRoles(currentRoles: MJRoleEntity[], futureRoles: RoleInputType[], userPayload: UserPayload): Promise<void> {
183
+ // go through the future roles and update any that are in the current roles
154
184
  for (const update of futureRoles) {
155
185
  const currentRole = currentRoles.find(r => r.Name.trim().toLowerCase() === update.Name.trim().toLowerCase());
156
186
  if (currentRole) {
157
187
  currentRole.Description = update.Description;
158
- ok = ok && await currentRole.Save();
188
+ if (!await currentRole.Save()) {
189
+ throw new Error(`Failed to update role '${currentRole.Name}': ${currentRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);
190
+ }
159
191
  }
160
192
  }
161
- return { Success: ok };
162
193
  }
163
194
 
164
- protected async AddNewRoles(currentRoles: MJRoleEntity[], futureRoles: RoleInputType[], user: UserInfo, userPayload: UserPayload): Promise<boolean> {
195
+ protected async AddNewRoles(currentRoles: MJRoleEntity[], futureRoles: RoleInputType[], user: UserInfo, userPayload: UserPayload): Promise<void> {
165
196
  // go through the future roles and add any that are not in the current roles
166
197
  const md = new Metadata();
167
- let ok: boolean = true;
168
198
 
169
199
  for (const add of futureRoles) {
170
200
  if (!currentRoles.find(r => r.Name.trim().toLowerCase() === add.Name.trim().toLowerCase())) {
171
201
  const role = await md.GetEntityObject<MJRoleEntity>("MJ: Roles", user);
172
202
  role.Name = add.Name;
173
203
  role.Description = add.Description;
174
- ok = ok && await role.Save();
204
+ if (!await role.Save()) {
205
+ throw new Error(`Failed to create role '${add.Name}': ${role.LatestResult?.CompleteMessage ?? 'unknown error'}`);
206
+ }
175
207
  }
176
208
  }
177
- return ok;
178
209
  }
179
210
 
180
211
 
181
- protected async DeleteRemovedRoles(currentRoles: MJRoleEntity[], futureRoles: RoleInputType[], user: UserInfo, userPayload: UserPayload): Promise<boolean> {
212
+ protected async DeleteRemovedRoles(currentRoles: MJRoleEntity[], futureRoles: RoleInputType[], user: UserInfo, userPayload: UserPayload): Promise<void> {
182
213
  const rv = new RunView();
183
- let ok: boolean = true;
184
214
 
185
215
  // iterate through the existing roles and remove any that are not in the input
186
216
  for (const remove of currentRoles) {
187
217
  if (!this.IsStandardRole(remove.Name)) {
188
218
  if (!futureRoles.find(r => r.Name.trim().toLowerCase() === remove.Name.trim().toLowerCase())) {
189
- ok = ok && await this.DeleteSingleRole(remove, rv, user, userPayload);
190
- }
219
+ await this.DeleteSingleRole(remove, rv, user, userPayload);
220
+ }
191
221
  }
192
222
  }
193
- return ok;
194
223
  }
195
224
 
196
225
  public get StandardRoles(): string[] {
@@ -200,10 +229,8 @@ export class SyncRolesAndUsersResolver {
200
229
  return this.StandardRoles.find(r => r.toLowerCase() === roleName.toLowerCase()) !== undefined;
201
230
  }
202
231
 
203
- protected async DeleteSingleRole(role: MJRoleEntity, rv: RunView, user: UserInfo, userPayload: UserPayload): Promise<boolean> {
232
+ protected async DeleteSingleRole(role: MJRoleEntity, rv: RunView, user: UserInfo, userPayload: UserPayload): Promise<void> {
204
233
  // first, remove all the UserRole records that match this role
205
- let ok: boolean = true;
206
-
207
234
  const r2 = await rv.RunView<MJUserRoleEntity>({
208
235
  EntityName: "MJ: User Roles",
209
236
  ExtraFilter: "RoleID = '" + role.ID + "'",
@@ -211,11 +238,15 @@ export class SyncRolesAndUsersResolver {
211
238
  }, user);
212
239
  if (r2.Success) {
213
240
  for (const ur of r2.Results) {
214
- ok = ok && await ur.Delete(); // remove the user role
241
+ if (!await ur.Delete()) {
242
+ throw new Error(`Failed to delete user-role link for role '${role.Name}': ${ur.LatestResult?.CompleteMessage ?? 'unknown error'}`);
243
+ }
215
244
  }
216
245
  }
217
246
 
218
- return ok && role.Delete(); // remove the role
247
+ if (!await role.Delete()) {
248
+ throw new Error(`Failed to delete role '${role.Name}': ${role.LatestResult?.CompleteMessage ?? 'unknown error'}`);
249
+ }
219
250
  }
220
251
 
221
252
  /**
@@ -229,39 +260,53 @@ export class SyncRolesAndUsersResolver {
229
260
  @Arg('users', () => [UserInputType]) users: UserInputType[],
230
261
  @Ctx() context: AppContext
231
262
  ) : Promise<SyncRolesAndUsersResultType> {
263
+ // Wrap delete + add + update + role-sync of users in one DB transaction.
232
264
  try {
233
- // first, we sync up the users and then the user roles.
234
- // for syncing users we first remove users that are no longer in the input, then we add new users and update existing users
235
- const rv = new RunView();
236
- const result = await rv.RunView<MJUserEntity>({
237
- EntityName: "MJ: Users",
238
- ResultType: 'entity_object'
239
- }, context.userPayload.userRecord);
240
- if (result && result.Success) {
241
- // go through current users and remove those that are not in the input
242
- const currentUsers = result.Results;
243
- if (await this.DeleteRemovedUsers(currentUsers, users, context.userPayload.userRecord, context.userPayload)) {
244
- if (await this.AddNewUsers(currentUsers, users, context.userPayload)) {
245
- if (await this.UpdateExistingUsers(currentUsers, users, context.userPayload)) {
246
- if (await this.SyncUserRoles(users, context.userPayload.userRecord, context.userPayload)) {
247
- return { Success: true };
248
- }
249
- }
250
- }
265
+ const provider = Metadata.Provider as DatabaseProviderBase;
266
+ await provider.BeginTransaction();
267
+ try {
268
+ const result = await this.DoSyncUsers(users, context.userPayload.userRecord, context.userPayload);
269
+ if (result.Success) {
270
+ await provider.CommitTransaction();
271
+ } else {
272
+ await provider.RollbackTransaction();
251
273
  }
274
+ return result;
275
+ } catch (txErr) {
276
+ await provider.RollbackTransaction();
277
+ throw txErr;
252
278
  }
253
-
254
- return { Success: false }; // if we get here, something went wrong
255
279
  } catch (err) {
256
280
  LogError(err);
257
281
  throw new Error('Error syncing roles and users\n\n' + err);
258
282
  }
259
283
  }
260
284
 
261
- protected async UpdateExistingUsers(currentUsers: MJUserEntity[], futureUsers: UserInputType[], userPayload: UserPayload): Promise<boolean> {
262
- // go through the future users and update any that are in the current users
263
- let ok: boolean = true;
285
+ /**
286
+ * Transaction-free core of SyncUsers expected to be invoked inside an outer transaction.
287
+ * Throws on any Save/Delete failure so the outer transaction rolls back.
288
+ */
289
+ protected async DoSyncUsers(users: UserInputType[], user: UserInfo, userPayload: UserPayload): Promise<SyncRolesAndUsersResultType> {
290
+ const rv = new RunView();
291
+ const result = await rv.RunView<MJUserEntity>({
292
+ EntityName: "MJ: Users",
293
+ ResultType: 'entity_object'
294
+ }, user);
264
295
 
296
+ if (!result || !result.Success) {
297
+ return { Success: false };
298
+ }
299
+
300
+ const currentUsers = result.Results;
301
+ await this.DeleteRemovedUsers(currentUsers, users, user, userPayload);
302
+ await this.AddNewUsers(currentUsers, users, userPayload);
303
+ await this.UpdateExistingUsers(currentUsers, users, userPayload);
304
+ await this.SyncUserRoles(users, user, userPayload);
305
+ return { Success: true };
306
+ }
307
+
308
+ protected async UpdateExistingUsers(currentUsers: MJUserEntity[], futureUsers: UserInputType[], userPayload: UserPayload): Promise<void> {
309
+ // go through the future users and update any that are in the current users
265
310
  for (const update of futureUsers) {
266
311
  const current = currentUsers.find(c => c.Email?.trim().toLowerCase() === update.Email?.trim().toLowerCase());
267
312
  if (current) {
@@ -270,23 +315,26 @@ export class SyncRolesAndUsersResolver {
270
315
  current.FirstName = update.FirstName;
271
316
  current.LastName = update.LastName;
272
317
  current.Title = update.Title;
273
- ok = ok && await current.Save();
318
+ if (!await current.Save()) {
319
+ throw new Error(`Failed to update user '${current.Email}': ${current.LatestResult?.CompleteMessage ?? 'unknown error'}`);
320
+ }
274
321
  }
275
322
  }
276
- return ok;
277
323
  }
278
- protected async AddNewUsers(currentUsers: MJUserEntity[], futureUsers: UserInputType[], userPayload: UserPayload): Promise<boolean> {
324
+
325
+ protected async AddNewUsers(currentUsers: MJUserEntity[], futureUsers: UserInputType[], userPayload: UserPayload): Promise<void> {
279
326
  // add users that are not in the current users
280
327
  const md = new Metadata();
281
- let ok: boolean = true;
282
328
 
283
329
  for (const add of futureUsers) {
284
330
  const match = currentUsers.find(currentUser => currentUser.Email?.trim().toLowerCase() === add.Email?.trim().toLowerCase());
285
331
  if (match) {
286
332
  // make sure the IsActive bit is set to true
287
333
  match.IsActive = true;
288
- ok = ok && await match.Save();
289
- }
334
+ if (!await match.Save()) {
335
+ throw new Error(`Failed to reactivate user '${match.Email}': ${match.LatestResult?.CompleteMessage ?? 'unknown error'}`);
336
+ }
337
+ }
290
338
  else {
291
339
  const user = await md.GetEntityObject<MJUserEntity>("MJ: Users", userPayload.userRecord);
292
340
  user.Name = add.Name;
@@ -297,33 +345,27 @@ export class SyncRolesAndUsersResolver {
297
345
  user.Title = add.Title;
298
346
  user.IsActive = true;
299
347
 
300
- ok = ok && await user.Save();
348
+ if (!await user.Save()) {
349
+ throw new Error(`Failed to create user '${add.Email}': ${user.LatestResult?.CompleteMessage ?? 'unknown error'}`);
350
+ }
301
351
  }
302
352
  }
303
- return ok;
304
353
  }
305
354
 
306
- protected async DeleteRemovedUsers(currentUsers: MJUserEntity[], futureUsers: UserInputType[], u: UserInfo, userPayload: UserPayload): Promise<boolean> {
355
+ protected async DeleteRemovedUsers(currentUsers: MJUserEntity[], futureUsers: UserInputType[], u: UserInfo, userPayload: UserPayload): Promise<void> {
307
356
  // remove users that are not in the future users
308
357
  const rv = new RunView();
309
- const md = new Metadata();
310
-
311
- let ok: boolean = true;
312
- //const tg = await md.CreateTransactionGroup(); HAVING PROBLEMS with this, so skipping for now, I think the entire thing is wrapped in a transaction and that's causing issues with two styles of trans wrappers
313
358
  for (const remove of currentUsers) {
314
359
  if (remove.Type.trim().toLowerCase() !== 'owner') {
315
360
  if (!futureUsers.find(r => r.Email.trim().toLowerCase() === remove.Email.trim().toLowerCase())) {
316
- ok = ok && await this.DeleteSingleUser(remove, rv, u, userPayload);
361
+ await this.DeleteSingleUser(remove, rv, u, userPayload);
317
362
  }
318
363
  }
319
364
  }
320
- return ok;
321
365
  }
322
366
 
323
- protected async DeleteSingleUser(user: MJUserEntity, rv: RunView, u: UserInfo, userPayload: UserPayload): Promise<boolean> {
367
+ protected async DeleteSingleUser(user: MJUserEntity, rv: RunView, u: UserInfo, userPayload: UserPayload): Promise<void> {
324
368
  // first, remove all the UserRole records that match this user
325
- let ok: boolean = true;
326
-
327
369
  const r2 = await rv.RunView<MJUserRoleEntity>({
328
370
  EntityName: "MJ: User Roles",
329
371
  ExtraFilter: "UserID = '" + user.ID + "'",
@@ -331,21 +373,23 @@ export class SyncRolesAndUsersResolver {
331
373
  }, u);
332
374
  if (r2.Success) {
333
375
  for (const ur of r2.Results) {
334
- //ur.TransactionGroup = tg;
335
- ok = ok && await ur.Delete(); // remove the user role
376
+ if (!await ur.Delete()) {
377
+ throw new Error(`Failed to delete user-role link for user '${user.Email}': ${ur.LatestResult?.CompleteMessage ?? 'unknown error'}`);
378
+ }
336
379
  }
337
380
  }
338
381
  if (await user.Delete()) {
339
- return ok;
382
+ return;
340
383
  }
341
- else {
342
- // in some cases there are a lot of fkey constraints that prevent the user from being deleted, so we mark the user as inactive instead
343
- user.IsActive = false;
344
- return await user.Save() && ok;
384
+ // FK constraint fallback: when the user has dependent records that block hard delete,
385
+ // we mark the user inactive instead. A failure to soft-delete IS a real error and rolls back.
386
+ user.IsActive = false;
387
+ if (!await user.Save()) {
388
+ throw new Error(`Failed to delete or deactivate user '${user.Email}': ${user.LatestResult?.CompleteMessage ?? 'unknown error'}`);
345
389
  }
346
390
  }
347
391
 
348
- protected async SyncUserRoles(users: UserInputType[], u: UserInfo, userPayload: UserPayload): Promise<boolean> {
392
+ protected async SyncUserRoles(users: UserInputType[], u: UserInfo, userPayload: UserPayload): Promise<void> {
349
393
  // for each user in the users array, make sure there is a User Role that matches. First, get a list of all DATABASE user and roels so we have that for fast lookup in memory
350
394
  const rv = new RunView();
351
395
  const md = new Metadata();
@@ -363,51 +407,43 @@ export class SyncRolesAndUsersResolver {
363
407
  ResultType: 'entity_object'
364
408
  }, u);
365
409
 
366
- // await both
367
- const [uResult,rResult, urResult] = await Promise.all([p1, p2, p3]);
368
-
369
- if (uResult.Success && rResult.Success && urResult.Success) {
370
- // we have the DB users and roles, and user roles
371
- const dbUsers = uResult.Results;
372
- const dbRoles = rResult.Results;
373
- const dbUserRoles = urResult.Results;
374
- let ok: boolean = true;
375
-
376
- // now, we can do lookups in memory from those DB roles and Users for their ID values
377
- // now we will iterate through the users input type and for each role, make sure it is in there
378
- //const tg = await md.CreateTransactionGroup();
379
- for (const user of users) {
380
- const dbUser = dbUsers.find(u => u.Email.trim().toLowerCase() === user.Email.trim().toLowerCase());
381
- if (dbUser) {
382
- for (const role of user.Roles) {
383
- const dbRole = dbRoles.find(r => r.Name.trim().toLowerCase() === role.Name.trim().toLowerCase());
384
- if (dbRole) {
385
- // now we need to make sure there is a user role that matches this user and role
386
- if (!dbUserRoles.find(ur => UUIDsEqual(ur.UserID, dbUser.ID) && UUIDsEqual(ur.RoleID, dbRole.ID))) {
387
- // we need to add a user role
388
- const ur = await md.GetEntityObject<MJUserRoleEntity>("MJ: User Roles", u);
389
- ur.UserID = dbUser.ID;
390
- ur.RoleID = dbRole.ID;
391
- ok = ok && await ur.Save();
392
- }
393
- }
410
+ const [uResult, rResult, urResult] = await Promise.all([p1, p2, p3]);
411
+
412
+ if (!uResult.Success || !rResult.Success || !urResult.Success) {
413
+ throw new Error(`Failed to load users/roles/user-roles for SyncUserRoles`);
414
+ }
415
+
416
+ const dbUsers = uResult.Results;
417
+ const dbRoles = rResult.Results;
418
+ const dbUserRoles = urResult.Results;
419
+
420
+ // Saves/Deletes below run inside the outer SyncUsers/SyncRolesAndUsers transaction.
421
+ for (const user of users) {
422
+ const dbUser = dbUsers.find(u => u.Email.trim().toLowerCase() === user.Email.trim().toLowerCase());
423
+ if (!dbUser) continue;
424
+
425
+ for (const role of user.Roles) {
426
+ const dbRole = dbRoles.find(r => r.Name.trim().toLowerCase() === role.Name.trim().toLowerCase());
427
+ if (dbRole && !dbUserRoles.find(ur => UUIDsEqual(ur.UserID, dbUser.ID) && UUIDsEqual(ur.RoleID, dbRole.ID))) {
428
+ const ur = await md.GetEntityObject<MJUserRoleEntity>("MJ: User Roles", u);
429
+ ur.UserID = dbUser.ID;
430
+ ur.RoleID = dbRole.ID;
431
+ if (!await ur.Save()) {
432
+ throw new Error(`Failed to assign role '${dbRole.Name}' to user '${dbUser.Email}': ${ur.LatestResult?.CompleteMessage ?? 'unknown error'}`);
394
433
  }
395
- // now, we check for DB user roles that are NOT in the user.Roles property as they are no longer part of the user's roles
396
- const thisUserDBRoles = dbUserRoles.filter(ur => UUIDsEqual(ur.UserID, dbUser.ID));
397
- for (const dbUserRole of thisUserDBRoles) {
398
- const role = user.Roles.find(r => r.Name.trim().toLowerCase() === dbRoles.find(rr => UUIDsEqual(rr.ID, dbUserRole.RoleID))?.Name.trim().toLowerCase());
399
- if (!role && !this.IsStandardRole(dbUserRole.Role)) {
400
- // this user role is no longer in the user's roles, we need to remove it
401
- //dbUserRole.TransactionGroup = tg;
402
- ok = ok && await dbUserRole.Delete(); // remove the user role - we use await for the DELETE, not the save
403
- }
434
+ }
435
+ }
436
+
437
+ // Check for DB user roles that are NOT in the user.Roles input those need to be removed
438
+ const thisUserDBRoles = dbUserRoles.filter(ur => UUIDsEqual(ur.UserID, dbUser.ID));
439
+ for (const dbUserRole of thisUserDBRoles) {
440
+ const role = user.Roles.find(r => r.Name.trim().toLowerCase() === dbRoles.find(rr => UUIDsEqual(rr.ID, dbUserRole.RoleID))?.Name.trim().toLowerCase());
441
+ if (!role && !this.IsStandardRole(dbUserRole.Role)) {
442
+ if (!await dbUserRole.Delete()) {
443
+ throw new Error(`Failed to remove role from user '${dbUser.Email}': ${dbUserRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);
404
444
  }
405
445
  }
406
446
  }
407
- return ok;
408
- }
409
- else {
410
- return false;
411
447
  }
412
448
  }
413
449
  }