@memberjunction/server 5.29.0 → 5.30.0
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/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +63 -70
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/generated/generated.d.ts +126 -8
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +708 -61
- package/dist/generated/generated.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +100 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +532 -41
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +20 -12
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +20 -9
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +153 -116
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +78 -79
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +66 -66
- package/src/auth/newUsers.ts +65 -74
- package/src/generated/generated.ts +503 -50
- package/src/resolvers/IntegrationDiscoveryResolver.ts +543 -43
- package/src/resolvers/SyncDataResolver.ts +24 -14
- package/src/resolvers/SyncRolesUsersResolver.ts +177 -141
- package/src/services/TaskOrchestrator.ts +86 -93
|
@@ -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
|
-
//
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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 =
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
335
|
-
|
|
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
|
|
382
|
+
return;
|
|
340
383
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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<
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
}
|