@web-ts-toolkit/moo 0.43.0 → 0.47.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.
- package/README.md +122 -7
- package/index.d.mts +2 -2
- package/index.d.ts +2 -2
- package/index.js +386 -57
- package/index.mjs +386 -57
- package/is.d.mts +2 -2
- package/is.d.ts +2 -2
- package/is.js +8 -7
- package/is.mjs +8 -7
- package/llms.txt +33 -5
- package/package.json +3 -3
- package/plugins/cascade-delete.d.mts +151 -4
- package/plugins/cascade-delete.d.ts +151 -4
- package/plugins/cascade-delete.js +354 -33
- package/plugins/cascade-delete.mjs +354 -33
- package/plugins/index.d.mts +1 -1
- package/plugins/index.d.ts +1 -1
- package/plugins/index.js +354 -33
- package/plugins/index.mjs +354 -33
- package/plugins/keycloak-user-sync.d.mts +32 -4
- package/plugins/keycloak-user-sync.d.ts +32 -4
- package/plugins/keycloak-user-sync.js +207 -45
- package/plugins/keycloak-user-sync.mjs +207 -45
- package/plugins/model-function.d.mts +46 -0
- package/plugins/model-function.d.ts +46 -0
- package/plugins/new-document.d.mts +21 -2
- package/plugins/new-document.d.ts +21 -2
- package/schema.d.mts +34 -5
- package/schema.d.ts +34 -5
- package/schema.js +24 -17
- package/schema.mjs +24 -17
|
@@ -77,12 +77,24 @@ var normalizeAttributes = (attributes) => {
|
|
|
77
77
|
return normalizedAttributes;
|
|
78
78
|
};
|
|
79
79
|
var getDocumentValue = (document, paths, field) => document.get(paths[field]);
|
|
80
|
+
var dependencyPathsFor = (options, field) => {
|
|
81
|
+
if (!options.syncFields[field]) return [];
|
|
82
|
+
if (field === "attributes") return options.attributePaths ?? [];
|
|
83
|
+
if (field === "roles") return options.rolePaths ?? [];
|
|
84
|
+
return options.passwordPaths ?? [];
|
|
85
|
+
};
|
|
86
|
+
var hasDependencyChanged = (options, field, isModified) => dependencyPathsFor(options, field).some(isModified);
|
|
87
|
+
var collectDependencyPaths = (options) => [
|
|
88
|
+
...dependencyPathsFor(options, "attributes"),
|
|
89
|
+
...dependencyPathsFor(options, "roles"),
|
|
90
|
+
...dependencyPathsFor(options, "password")
|
|
91
|
+
];
|
|
80
92
|
var buildTrackedPaths = (options) => uniqueStrings([
|
|
81
93
|
options.paths.providerId,
|
|
82
94
|
...Object.entries(options.syncFields).filter(([, enabled]) => enabled).flatMap(
|
|
83
95
|
([field]) => field === "enabled" ? [options.paths.enabled, options.paths.archived] : [options.paths[field]]
|
|
84
96
|
),
|
|
85
|
-
...options
|
|
97
|
+
...collectDependencyPaths(options)
|
|
86
98
|
]);
|
|
87
99
|
var planChangedFields = (options, isNew, isModified, currentProviderId) => {
|
|
88
100
|
const changedFields = /* @__PURE__ */ new Set();
|
|
@@ -92,7 +104,9 @@ var planChangedFields = (options, isNew, isModified, currentProviderId) => {
|
|
|
92
104
|
const paths = field === "enabled" ? [options.paths.enabled, options.paths.archived] : [options.paths[field]];
|
|
93
105
|
if (paths.some(isModified)) changedFields.add(field);
|
|
94
106
|
}
|
|
95
|
-
if (options
|
|
107
|
+
if (hasDependencyChanged(options, "attributes", isModified)) changedFields.add("attributes");
|
|
108
|
+
if (hasDependencyChanged(options, "roles", isModified)) changedFields.add("roles");
|
|
109
|
+
if (hasDependencyChanged(options, "password", isModified)) changedFields.add("password");
|
|
96
110
|
const shouldPersistMissingProviderId = stringValue(currentProviderId) === null;
|
|
97
111
|
const shouldSync = isNew || shouldPersistMissingProviderId || changedFields.size > 0;
|
|
98
112
|
return {
|
|
@@ -311,6 +325,8 @@ var normalizeOptions = (schema, options) => {
|
|
|
311
325
|
paths,
|
|
312
326
|
syncFields,
|
|
313
327
|
attributePaths: normalizeOptionalStringList(options.attributePaths, "attributePaths"),
|
|
328
|
+
rolePaths: normalizeOptionalStringList(options.rolePaths, "rolePaths"),
|
|
329
|
+
passwordPaths: normalizeOptionalStringList(options.passwordPaths, "passwordPaths"),
|
|
314
330
|
managedAttributes: normalizeOptionalStringList(options.managedAttributes, "managedAttributes"),
|
|
315
331
|
managedRoles: normalizeOptionalStringList(options.managedRoles, "managedRoles"),
|
|
316
332
|
maxRolesPerSync: normalizeMaxRolesPerSync(options.maxRolesPerSync)
|
|
@@ -334,6 +350,26 @@ var normalizeOptions = (schema, options) => {
|
|
|
334
350
|
return normalized;
|
|
335
351
|
};
|
|
336
352
|
var getState = (document) => document.$locals[syncStateKey];
|
|
353
|
+
var loggerTokenPattern = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
354
|
+
var toLoggerToken = (value) => {
|
|
355
|
+
if (typeof value !== "string") return void 0;
|
|
356
|
+
if (value.length === 0 || value.length > 64) return void 0;
|
|
357
|
+
if (!loggerTokenPattern.test(value)) return void 0;
|
|
358
|
+
return value;
|
|
359
|
+
};
|
|
360
|
+
var toLoggedError = (error) => {
|
|
361
|
+
if (typeof error !== "object" || error === null) return { name: "UnknownError" };
|
|
362
|
+
try {
|
|
363
|
+
const candidate = error;
|
|
364
|
+
const name = toLoggerToken(candidate.name) ?? "Error";
|
|
365
|
+
const code = toLoggerToken(candidate.code);
|
|
366
|
+
const rawStatus = candidate.status ?? candidate.statusCode;
|
|
367
|
+
const status = typeof rawStatus === "number" && Number.isSafeInteger(rawStatus) && rawStatus >= 0 && rawStatus <= 999 ? rawStatus : void 0;
|
|
368
|
+
return { name, ...code === void 0 ? {} : { code }, ...status === void 0 ? {} : { status } };
|
|
369
|
+
} catch {
|
|
370
|
+
return { name: "Error" };
|
|
371
|
+
}
|
|
372
|
+
};
|
|
337
373
|
var getDocumentSession = (document) => document.$session?.() ?? null;
|
|
338
374
|
var assertNoDocumentSession = (document, operation) => {
|
|
339
375
|
if (getDocumentSession(document)) {
|
|
@@ -354,6 +390,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
354
390
|
const identities = options.identifyBy;
|
|
355
391
|
const realmHandle = options.client.realm(options.realm);
|
|
356
392
|
const trackedPaths = buildTrackedPaths(options);
|
|
393
|
+
const identitySnapshotPaths = uniqueStrings([...trackedPaths, paths.providerId, paths.username, paths.email]);
|
|
357
394
|
const buildSafeErrorContext = (operation, document) => ({
|
|
358
395
|
operation,
|
|
359
396
|
localDocumentId: document._id === void 0 || document._id === null ? void 0 : String(document._id)
|
|
@@ -362,7 +399,10 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
362
399
|
const safeContext = buildSafeErrorContext(operation, document);
|
|
363
400
|
const logger = options.logger === false ? null : options.logger ?? console;
|
|
364
401
|
try {
|
|
365
|
-
await logger?.error(`Keycloak user sync failed during ${operation}`, {
|
|
402
|
+
await logger?.error(`Keycloak user sync failed during ${operation}`, {
|
|
403
|
+
...safeContext,
|
|
404
|
+
error: toLoggedError(error)
|
|
405
|
+
});
|
|
366
406
|
} catch {
|
|
367
407
|
}
|
|
368
408
|
try {
|
|
@@ -375,7 +415,8 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
375
415
|
const readDocumentValue = (document, field) => getDocumentValue(document, paths, field);
|
|
376
416
|
const getPreviousValue = (previous, field) => getPathValue(previous, paths[field]);
|
|
377
417
|
const getUsername = (document) => stringValue(readDocumentValue(document, "username")) ?? stringValue(readDocumentValue(document, "email"));
|
|
378
|
-
const
|
|
418
|
+
const readPersistedSnapshot = async (document) => document.constructor.findById(document._id).select(identitySnapshotPaths).session(getDocumentSession(document)).lean();
|
|
419
|
+
const resolveUser = async (document, snapshot, wasNew) => {
|
|
379
420
|
const realm = await realmHandle.get();
|
|
380
421
|
if (!realm) throw new Error(`Keycloak realm "${options.realm}" was not found`);
|
|
381
422
|
const duplicateEmailsAllowed = realm.duplicateEmailsAllowed === true;
|
|
@@ -384,40 +425,133 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
384
425
|
if (!user.id) return user;
|
|
385
426
|
return await realmHandle.userById(user.id).get() ?? user;
|
|
386
427
|
};
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const user = await realmHandle.userById(providerId).get();
|
|
394
|
-
if (user) return { user, duplicateEmailsAllowed };
|
|
395
|
-
}
|
|
428
|
+
const findByProviderId = async (providerId) => realmHandle.userById(providerId).get();
|
|
429
|
+
const findByUsername = async (username) => realmHandle.user(username).get();
|
|
430
|
+
const findByEmail = async (email) => {
|
|
431
|
+
const matches = (await realmHandle.searchUsers(email, { attribute: "email", exact: true, first: 0, max: 2 })).filter((user) => normalizeEmail(user.email) === normalizeEmail(email));
|
|
432
|
+
if (matches.length > 1) {
|
|
433
|
+
throw new Error("Cannot identify a unique Keycloak user for the configured email identity");
|
|
396
434
|
}
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
435
|
+
if (matches.length === 1) return getCompleteUserById(matches[0]);
|
|
436
|
+
return null;
|
|
437
|
+
};
|
|
438
|
+
const findByIdentity = async (identity, value) => {
|
|
439
|
+
if (identity === "providerId") return findByProviderId(value);
|
|
440
|
+
if (identity === "username") return findByUsername(value);
|
|
441
|
+
return findByEmail(value);
|
|
442
|
+
};
|
|
443
|
+
const identityConflict = () => new Error(
|
|
444
|
+
"keycloakUserSyncPlugin detected a Keycloak identity conflict; refusing to relink to a different remote user"
|
|
445
|
+
);
|
|
446
|
+
if (wasNew) {
|
|
447
|
+
for (const identity of orderedIdentities) {
|
|
448
|
+
for (const value of uniqueStrings([
|
|
449
|
+
readDocumentValue(document, identity),
|
|
450
|
+
getPreviousValue(snapshot, identity)
|
|
401
451
|
])) {
|
|
402
|
-
const user = await
|
|
452
|
+
const user = await findByIdentity(identity, value);
|
|
403
453
|
if (user) return { user, duplicateEmailsAllowed };
|
|
404
454
|
}
|
|
405
455
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
456
|
+
return { user: null, duplicateEmailsAllowed };
|
|
457
|
+
}
|
|
458
|
+
if (!snapshot) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
"keycloakUserSyncPlugin cannot verify the persisted Keycloak identity for this document; refusing to sync a possibly relinked account"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const persistedValue = (identity) => stringValue(getPreviousValue(snapshot, identity));
|
|
464
|
+
const currentValue = (identity) => stringValue(readDocumentValue(document, identity));
|
|
465
|
+
const persistedProviderId = persistedValue("providerId");
|
|
466
|
+
const currentProviderId = currentValue("providerId");
|
|
467
|
+
if (persistedProviderId && currentProviderId && persistedProviderId !== currentProviderId) {
|
|
468
|
+
throw new Error("keycloakUserSyncPlugin providerId is server-controlled and cannot be changed after persistence");
|
|
469
|
+
}
|
|
470
|
+
const isRelinkProtected = (identity) => identity === "username" || identity === "email" && !duplicateEmailsAllowed;
|
|
471
|
+
const checkCurrentDoesNotRelink = async (identity, boundId) => {
|
|
472
|
+
if (!identities.includes(identity) || !isRelinkProtected(identity)) return;
|
|
473
|
+
const current = currentValue(identity);
|
|
474
|
+
if (!current) return;
|
|
475
|
+
const persisted = persistedValue(identity);
|
|
476
|
+
const unchanged = identity === "email" ? persisted !== null && normalizeEmail(persisted) === normalizeEmail(current) : persisted === current;
|
|
477
|
+
if (persisted !== null && unchanged) return;
|
|
478
|
+
const user = await findByIdentity(identity, current);
|
|
479
|
+
if (user?.id && user.id !== boundId) throw identityConflict();
|
|
480
|
+
};
|
|
481
|
+
if (persistedProviderId) {
|
|
482
|
+
const bound2 = await findByProviderId(persistedProviderId);
|
|
483
|
+
if (!bound2?.id) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
"keycloakUserSyncPlugin persisted Keycloak identity no longer exists; refusing to relink to a different remote user"
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
await checkCurrentDoesNotRelink("username", bound2.id);
|
|
489
|
+
await checkCurrentDoesNotRelink("email", bound2.id);
|
|
490
|
+
return { user: bound2, duplicateEmailsAllowed };
|
|
491
|
+
}
|
|
492
|
+
let bound = null;
|
|
493
|
+
for (const identity of orderedIdentities) {
|
|
494
|
+
if (identity === "providerId") continue;
|
|
495
|
+
const persisted = persistedValue(identity);
|
|
496
|
+
if (!persisted) continue;
|
|
497
|
+
const user = await findByIdentity(identity, persisted);
|
|
498
|
+
if (!user?.id) continue;
|
|
499
|
+
if (bound?.id && bound.id !== user.id) throw identityConflict();
|
|
500
|
+
bound ??= user;
|
|
501
|
+
}
|
|
502
|
+
if (bound?.id) {
|
|
503
|
+
await checkCurrentDoesNotRelink("username", bound.id);
|
|
504
|
+
await checkCurrentDoesNotRelink("email", bound.id);
|
|
505
|
+
if (currentProviderId) {
|
|
506
|
+
const owner = await findByProviderId(currentProviderId);
|
|
507
|
+
if (owner?.id && owner.id !== bound.id) throw identityConflict();
|
|
417
508
|
}
|
|
509
|
+
return { user: bound, duplicateEmailsAllowed };
|
|
510
|
+
}
|
|
511
|
+
if (currentProviderId) {
|
|
512
|
+
const owner = await findByProviderId(currentProviderId);
|
|
513
|
+
if (owner?.id) throw identityConflict();
|
|
514
|
+
}
|
|
515
|
+
for (const identity of orderedIdentities) {
|
|
516
|
+
if (identity === "providerId") continue;
|
|
517
|
+
const current = currentValue(identity);
|
|
518
|
+
if (!current) continue;
|
|
519
|
+
const user = await findByIdentity(identity, current);
|
|
520
|
+
if (user?.id) throw identityConflict();
|
|
418
521
|
}
|
|
419
522
|
return { user: null, duplicateEmailsAllowed };
|
|
420
523
|
};
|
|
524
|
+
const getProvisioningFlags = (error) => {
|
|
525
|
+
if (typeof error !== "object" || error === null) return null;
|
|
526
|
+
const candidate = error;
|
|
527
|
+
if (candidate.name !== "UserPasswordProvisioningError" && !("accountPersists" in candidate) && !("initialProvisioning" in candidate)) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
return candidate;
|
|
531
|
+
};
|
|
532
|
+
const isAlreadyExistsError = (error) => error instanceof Error && /already exists in realm/.test(error.message);
|
|
533
|
+
const readIntendedEnabled = (document) => {
|
|
534
|
+
if (!syncFields.enabled) return void 0;
|
|
535
|
+
const archived = readDocumentValue(document, "archived");
|
|
536
|
+
if (typeof archived === "boolean") return !archived;
|
|
537
|
+
const enabled = readDocumentValue(document, "enabled");
|
|
538
|
+
if (typeof enabled === "boolean") return enabled;
|
|
539
|
+
return void 0;
|
|
540
|
+
};
|
|
541
|
+
const recoverPartialCreation = async (document, username, creationError, desiredPassword) => {
|
|
542
|
+
if (identities.length === 0) throw creationError;
|
|
543
|
+
if (isAlreadyExistsError(creationError)) throw creationError;
|
|
544
|
+
const flags = getProvisioningFlags(creationError);
|
|
545
|
+
if (flags && flags.accountPersists === false) throw creationError;
|
|
546
|
+
if (desiredPassword && !(flags && flags.accountPersists === true)) throw creationError;
|
|
547
|
+
const recoveredUser = await resolveCreatedUser(document, username);
|
|
548
|
+
if (!recoveredUser?.id) throw creationError;
|
|
549
|
+
if (desiredPassword) {
|
|
550
|
+
const candidateUsername = stringValue(recoveredUser.username);
|
|
551
|
+
if (!candidateUsername || candidateUsername !== username) throw creationError;
|
|
552
|
+
}
|
|
553
|
+
return { user: recoveredUser, passwordAlreadyApplied: flags?.passwordApplied === true };
|
|
554
|
+
};
|
|
421
555
|
const resolveCreatedUser = async (document, username) => {
|
|
422
556
|
const userByUsername = await realmHandle.user(username).get();
|
|
423
557
|
if (userByUsername) return userByUsername;
|
|
@@ -455,34 +589,46 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
455
589
|
document.unmarkModified(paths.providerId);
|
|
456
590
|
};
|
|
457
591
|
const syncDocument = async (document, state) => {
|
|
458
|
-
const resolved = await resolveUser(document, state.previous);
|
|
592
|
+
const resolved = await resolveUser(document, state.previous, state.wasNew);
|
|
459
593
|
let user = resolved.user;
|
|
460
594
|
let created = false;
|
|
595
|
+
let recoveredPartial = false;
|
|
596
|
+
let passwordAlreadyApplied = false;
|
|
461
597
|
const shouldSyncProfile = state.wasNew || ["username", "email", "emailVerified", "firstName", "lastName", "enabled"].some(
|
|
462
598
|
(field) => state.changedFields.has(field)
|
|
463
599
|
);
|
|
464
600
|
const shouldSyncAttributes = state.wasNew || state.changedFields.has("attributes");
|
|
465
601
|
const payload = shouldSyncProfile ? buildProfilePayload(document, options, Boolean(resolved.user), state.wasNew ? void 0 : state.changedFields) : {};
|
|
466
|
-
|
|
602
|
+
let cachedPassword;
|
|
603
|
+
let passwordEvaluated = false;
|
|
604
|
+
const readDesiredPassword = () => {
|
|
605
|
+
if (!passwordEvaluated) {
|
|
606
|
+
cachedPassword = getDesiredPassword(document, options);
|
|
607
|
+
passwordEvaluated = true;
|
|
608
|
+
}
|
|
609
|
+
return cachedPassword ?? null;
|
|
610
|
+
};
|
|
467
611
|
if (!user) {
|
|
468
612
|
const username = getUsername(document);
|
|
469
613
|
if (!username) throw new Error("Cannot create a Keycloak user without a username or email");
|
|
470
614
|
payload.attributes = mergeAttributes(null, document, options);
|
|
615
|
+
const creationPassword = readDesiredPassword();
|
|
471
616
|
try {
|
|
472
617
|
user = await realmHandle.user(username).create({
|
|
473
618
|
username,
|
|
474
619
|
...payload,
|
|
475
|
-
...
|
|
476
|
-
password:
|
|
620
|
+
...creationPassword && {
|
|
621
|
+
password: creationPassword,
|
|
477
622
|
passwordTemporary: options.passwordTemporary ?? false
|
|
478
623
|
}
|
|
479
624
|
}) ?? null;
|
|
480
625
|
created = true;
|
|
481
626
|
} catch (error) {
|
|
482
|
-
if (
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
627
|
+
if (!state.wasNew) throw error;
|
|
628
|
+
const recovered = await recoverPartialCreation(document, username, error, creationPassword);
|
|
629
|
+
user = recovered.user;
|
|
630
|
+
recoveredPartial = true;
|
|
631
|
+
passwordAlreadyApplied = recovered.passwordAlreadyApplied;
|
|
486
632
|
}
|
|
487
633
|
}
|
|
488
634
|
if (!user?.id) throw new Error("Keycloak did not return a user ID");
|
|
@@ -497,6 +643,8 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
497
643
|
currentEmail: readDocumentValue(document, "email"),
|
|
498
644
|
remoteEmail: user.email
|
|
499
645
|
});
|
|
646
|
+
const desiredPassword = !created && !passwordAlreadyApplied && (recoveredPartial || state.passwordChanged) ? readDesiredPassword() : cachedPassword ?? null;
|
|
647
|
+
const willResetPassword = Boolean(desiredPassword) && !created && !passwordAlreadyApplied && (recoveredPartial || state.passwordChanged);
|
|
500
648
|
if (!created) {
|
|
501
649
|
if (emailVerificationPlan.initialLinkSameEmail) delete payload.emailVerified;
|
|
502
650
|
if (emailVerificationPlan.changed) {
|
|
@@ -505,21 +653,35 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
505
653
|
payload.emailVerified = false;
|
|
506
654
|
}
|
|
507
655
|
if (shouldSyncAttributes) payload.attributes = mergeAttributes(user, document, options);
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
656
|
+
if (willResetPassword) {
|
|
657
|
+
const { enabled: deferredEnabled, ...profileWithoutEnabled } = payload;
|
|
658
|
+
const intendedEnabled = typeof deferredEnabled === "boolean" ? deferredEnabled : readIntendedEnabled(document) ?? true;
|
|
659
|
+
if (Object.keys(profileWithoutEnabled).length > 0) {
|
|
660
|
+
await realmHandle.userById(userId).update(profileWithoutEnabled);
|
|
661
|
+
user = { ...user, ...profileWithoutEnabled };
|
|
662
|
+
}
|
|
663
|
+
await realmHandle.userById(userId).resetPassword(desiredPassword, { temporary: options.passwordTemporary ?? false });
|
|
664
|
+
user = { ...user };
|
|
665
|
+
await realmHandle.userById(userId).update({ enabled: intendedEnabled });
|
|
666
|
+
user = { ...user, enabled: intendedEnabled };
|
|
667
|
+
} else {
|
|
668
|
+
if (Object.keys(payload).length > 0) {
|
|
669
|
+
await realmHandle.userById(userId).update(payload);
|
|
670
|
+
user = { ...user, ...payload };
|
|
671
|
+
}
|
|
511
672
|
}
|
|
512
673
|
}
|
|
513
|
-
if (!created && state.passwordChanged && desiredPassword) {
|
|
514
|
-
await realmHandle.userById(userId).resetPassword(desiredPassword, { temporary: options.passwordTemporary ?? false });
|
|
515
|
-
}
|
|
516
674
|
if (created || state.changedFields.has("roles")) await syncRoles(user, document);
|
|
517
675
|
if (emailVerificationPlan.changed && options.sendVerificationEmailOnChange !== false) {
|
|
518
676
|
await realmHandle.userById(userId).sendVerifyEmail();
|
|
519
677
|
}
|
|
520
678
|
};
|
|
521
679
|
const deleteDocument = async (document) => {
|
|
522
|
-
|
|
680
|
+
if (document.isNew) {
|
|
681
|
+
throw new Error("keycloakUserSyncPlugin cannot delete a Keycloak user for a document that was never persisted");
|
|
682
|
+
}
|
|
683
|
+
const snapshot = await readPersistedSnapshot(document);
|
|
684
|
+
const { user } = await resolveUser(document, snapshot, false);
|
|
523
685
|
if (user?.id) await realmHandle.userById(user.id).delete();
|
|
524
686
|
};
|
|
525
687
|
schema.pre("save", async function keycloakUserSyncPreSave() {
|
|
@@ -534,7 +696,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
534
696
|
const state = { ...changePlan, wasNew: this.isNew };
|
|
535
697
|
this.$locals[syncStateKey] = state;
|
|
536
698
|
if (!state.shouldSync || this.isNew) return;
|
|
537
|
-
state.previous = await
|
|
699
|
+
state.previous = await readPersistedSnapshot(this);
|
|
538
700
|
if (providerIdChanged) {
|
|
539
701
|
const previousProviderId = stringValue(getPreviousValue(state.previous, "providerId"));
|
|
540
702
|
const currentProviderId = stringValue(readDocumentValue(this, "providerId"));
|