@web-ts-toolkit/moo 0.42.2 → 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 +208 -45
- package/plugins/keycloak-user-sync.mjs +208 -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,33 +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({
|
|
618
|
+
username,
|
|
473
619
|
...payload,
|
|
474
|
-
...
|
|
475
|
-
password:
|
|
620
|
+
...creationPassword && {
|
|
621
|
+
password: creationPassword,
|
|
476
622
|
passwordTemporary: options.passwordTemporary ?? false
|
|
477
623
|
}
|
|
478
624
|
}) ?? null;
|
|
479
625
|
created = true;
|
|
480
626
|
} catch (error) {
|
|
481
|
-
if (
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
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;
|
|
485
632
|
}
|
|
486
633
|
}
|
|
487
634
|
if (!user?.id) throw new Error("Keycloak did not return a user ID");
|
|
@@ -496,6 +643,8 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
496
643
|
currentEmail: readDocumentValue(document, "email"),
|
|
497
644
|
remoteEmail: user.email
|
|
498
645
|
});
|
|
646
|
+
const desiredPassword = !created && !passwordAlreadyApplied && (recoveredPartial || state.passwordChanged) ? readDesiredPassword() : cachedPassword ?? null;
|
|
647
|
+
const willResetPassword = Boolean(desiredPassword) && !created && !passwordAlreadyApplied && (recoveredPartial || state.passwordChanged);
|
|
499
648
|
if (!created) {
|
|
500
649
|
if (emailVerificationPlan.initialLinkSameEmail) delete payload.emailVerified;
|
|
501
650
|
if (emailVerificationPlan.changed) {
|
|
@@ -504,21 +653,35 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
504
653
|
payload.emailVerified = false;
|
|
505
654
|
}
|
|
506
655
|
if (shouldSyncAttributes) payload.attributes = mergeAttributes(user, document, options);
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
}
|
|
510
672
|
}
|
|
511
673
|
}
|
|
512
|
-
if (!created && state.passwordChanged && desiredPassword) {
|
|
513
|
-
await realmHandle.userById(userId).resetPassword(desiredPassword, { temporary: options.passwordTemporary ?? false });
|
|
514
|
-
}
|
|
515
674
|
if (created || state.changedFields.has("roles")) await syncRoles(user, document);
|
|
516
675
|
if (emailVerificationPlan.changed && options.sendVerificationEmailOnChange !== false) {
|
|
517
676
|
await realmHandle.userById(userId).sendVerifyEmail();
|
|
518
677
|
}
|
|
519
678
|
};
|
|
520
679
|
const deleteDocument = async (document) => {
|
|
521
|
-
|
|
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);
|
|
522
685
|
if (user?.id) await realmHandle.userById(user.id).delete();
|
|
523
686
|
};
|
|
524
687
|
schema.pre("save", async function keycloakUserSyncPreSave() {
|
|
@@ -533,7 +696,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
|
|
|
533
696
|
const state = { ...changePlan, wasNew: this.isNew };
|
|
534
697
|
this.$locals[syncStateKey] = state;
|
|
535
698
|
if (!state.shouldSync || this.isNew) return;
|
|
536
|
-
state.previous = await
|
|
699
|
+
state.previous = await readPersistedSnapshot(this);
|
|
537
700
|
if (providerIdChanged) {
|
|
538
701
|
const previousProviderId = stringValue(getPreviousValue(state.previous, "providerId"));
|
|
539
702
|
const currentProviderId = stringValue(readDocumentValue(this, "providerId"));
|