@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
package/plugins/index.mjs
CHANGED
|
@@ -51,6 +51,121 @@ var resolveFilter = (value, document) => {
|
|
|
51
51
|
}
|
|
52
52
|
return result;
|
|
53
53
|
};
|
|
54
|
+
var isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
55
|
+
var isEmptyFilterObject = (value) => Object.keys(value).length === 0;
|
|
56
|
+
var EMPTY_FULL_FILTER_MESSAGE = "cascadeDeletePlugin rejects empty foreignFilter by default because {} would match every document in the dependent collection; provide a non-empty filter object. Broad deletes are not implicitly enabled.";
|
|
57
|
+
var DEFAULT_MAX_CONCURRENCY = 8;
|
|
58
|
+
var DEFAULT_BATCH_SIZE = 100;
|
|
59
|
+
var assertPositiveIntOption = (value, name, modelName) => {
|
|
60
|
+
if (value === void 0) return;
|
|
61
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`cascadeDeletePlugin for model "${modelName}" requires \`${name}\` to be an integer >= 1 when provided`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var assertPluginOptions = (options) => {
|
|
68
|
+
if (!isPlainObject2(options)) {
|
|
69
|
+
throw new Error("cascadeDeletePlugin requires options with a non-empty `model` name");
|
|
70
|
+
}
|
|
71
|
+
const { model, localField, foreignField, foreignFilter, extraForeignFilter } = options;
|
|
72
|
+
if (!isNonEmptyString(model)) {
|
|
73
|
+
throw new Error("cascadeDeletePlugin requires a non-empty `model` name");
|
|
74
|
+
}
|
|
75
|
+
if (localField !== void 0 && !isNonEmptyString(localField)) {
|
|
76
|
+
throw new Error("cascadeDeletePlugin requires `localField` to be a non-empty string when provided");
|
|
77
|
+
}
|
|
78
|
+
assertPositiveIntOption(options.maxConcurrency, "maxConcurrency", model);
|
|
79
|
+
assertPositiveIntOption(options.batchSize, "batchSize", model);
|
|
80
|
+
if (foreignField !== void 0 && !isNonEmptyString(foreignField)) {
|
|
81
|
+
throw new Error("cascadeDeletePlugin requires `foreignField` to be a non-empty string when provided");
|
|
82
|
+
}
|
|
83
|
+
if (foreignFilter !== void 0) {
|
|
84
|
+
if (!isFunction2(foreignFilter) && !isPlainObject2(foreignFilter)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`cascadeDeletePlugin invalid \`foreignFilter\` for model "${model}": expected a plain filter object or a resolver function`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (isPlainObject2(foreignFilter) && isEmptyFilterObject(foreignFilter)) {
|
|
90
|
+
throw new Error(`cascadeDeletePlugin ${EMPTY_FULL_FILTER_MESSAGE}`);
|
|
91
|
+
}
|
|
92
|
+
} else if (!isNonEmptyString(localField) || !isNonEmptyString(foreignField)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`cascadeDeletePlugin for model "${model}" requires either \`foreignFilter\` or both \`localField\` and \`foreignField\``
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (extraForeignFilter !== void 0) {
|
|
98
|
+
if (!isFunction2(extraForeignFilter) && !isPlainObject2(extraForeignFilter)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`cascadeDeletePlugin invalid \`extraForeignFilter\` for model "${model}": expected a plain filter object or a resolver function`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var assertFullFilterNotEmpty = (filter, modelName) => {
|
|
106
|
+
if (isEmptyFilterObject(filter)) {
|
|
107
|
+
throw new Error(`cascadeDeletePlugin for model "${modelName}" ${EMPTY_FULL_FILTER_MESSAGE}`);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var isMissingScalarKey = (value) => {
|
|
111
|
+
if (value === void 0 || value === null) return true;
|
|
112
|
+
if (typeof value === "string" && value.trim().length === 0) return true;
|
|
113
|
+
return false;
|
|
114
|
+
};
|
|
115
|
+
var unwrapKeyValue = (value) => {
|
|
116
|
+
if (isPlainObject2(value)) {
|
|
117
|
+
const candidate = value._id;
|
|
118
|
+
if (candidate !== void 0 && candidate !== null) return candidate;
|
|
119
|
+
}
|
|
120
|
+
if (isObject(value)) {
|
|
121
|
+
try {
|
|
122
|
+
const candidate = value._id;
|
|
123
|
+
if (candidate !== void 0 && candidate !== null) return candidate;
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
};
|
|
129
|
+
var normalizeKeyList = (localValue) => {
|
|
130
|
+
if (Array.isArray(localValue)) {
|
|
131
|
+
const keys = [];
|
|
132
|
+
for (const entry of localValue) {
|
|
133
|
+
const unwrapped2 = unwrapKeyValue(entry);
|
|
134
|
+
if (!isMissingScalarKey(unwrapped2)) keys.push(unwrapped2);
|
|
135
|
+
}
|
|
136
|
+
return keys.length > 0 ? keys : null;
|
|
137
|
+
}
|
|
138
|
+
const unwrapped = unwrapKeyValue(localValue);
|
|
139
|
+
if (isMissingScalarKey(unwrapped)) return null;
|
|
140
|
+
return [unwrapped];
|
|
141
|
+
};
|
|
142
|
+
var isPathDeselected = (owner, path) => {
|
|
143
|
+
try {
|
|
144
|
+
const candidate = owner;
|
|
145
|
+
if (typeof candidate.$isSelected === "function") {
|
|
146
|
+
const result = candidate.$isSelected(path);
|
|
147
|
+
if (typeof result === "boolean") return result === false;
|
|
148
|
+
}
|
|
149
|
+
if (typeof candidate.isSelected === "function") {
|
|
150
|
+
const result = candidate.isSelected(path);
|
|
151
|
+
if (typeof result === "boolean") return result === false;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
};
|
|
157
|
+
var buildRelationshipPredicate = (foreignField, localValue) => {
|
|
158
|
+
const keys = normalizeKeyList(localValue);
|
|
159
|
+
if (!keys) return null;
|
|
160
|
+
if (Array.isArray(localValue)) {
|
|
161
|
+
return { [foreignField]: { $in: keys } };
|
|
162
|
+
}
|
|
163
|
+
return { [foreignField]: keys[0] };
|
|
164
|
+
};
|
|
165
|
+
var combineWithRelationship = (relationship, extra) => {
|
|
166
|
+
if (!extra || isEmptyFilterObject(extra)) return relationship;
|
|
167
|
+
return { $and: [relationship, extra] };
|
|
168
|
+
};
|
|
54
169
|
var applyQueryOptions = (query, options) => {
|
|
55
170
|
let builder = query;
|
|
56
171
|
if (options.select) {
|
|
@@ -65,8 +180,81 @@ var applyQueryOptions = (query, options) => {
|
|
|
65
180
|
if (options.lean) {
|
|
66
181
|
builder = builder.lean();
|
|
67
182
|
}
|
|
183
|
+
if (options.session) {
|
|
184
|
+
builder = builder.session(options.session);
|
|
185
|
+
}
|
|
68
186
|
return builder;
|
|
69
187
|
};
|
|
188
|
+
var getOwningConnection = (owner) => {
|
|
189
|
+
const candidate = owner?.collection?.conn ?? owner?.constructor?.collection?.conn ?? null;
|
|
190
|
+
if (candidate && typeof candidate.model === "function") {
|
|
191
|
+
return candidate;
|
|
192
|
+
}
|
|
193
|
+
return mongoose2;
|
|
194
|
+
};
|
|
195
|
+
var getBoundSession = (owner) => {
|
|
196
|
+
try {
|
|
197
|
+
const session = owner?.$session?.() ?? null;
|
|
198
|
+
return session ?? null;
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var resolveExecutionContext = (owner, targetModelName, explicitSession) => {
|
|
204
|
+
const session = explicitSession ?? getBoundSession(owner) ?? null;
|
|
205
|
+
const connection = getOwningConnection(owner);
|
|
206
|
+
const Target = connection.model(targetModelName);
|
|
207
|
+
return { connection, Target, session };
|
|
208
|
+
};
|
|
209
|
+
var isInTransaction = (session) => {
|
|
210
|
+
try {
|
|
211
|
+
return typeof session?.inTransaction === "function" ? session.inTransaction() : false;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var getSessionClient = (session) => {
|
|
217
|
+
if (!session) return null;
|
|
218
|
+
return session.client ?? null;
|
|
219
|
+
};
|
|
220
|
+
var getConnectionClient = (connection) => {
|
|
221
|
+
try {
|
|
222
|
+
if (connection === mongoose2) {
|
|
223
|
+
return mongoose2.connection.getClient?.() ?? null;
|
|
224
|
+
}
|
|
225
|
+
return connection.getClient?.() ?? null;
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var getTopologyType = (client) => {
|
|
231
|
+
try {
|
|
232
|
+
const topology = client?.topology;
|
|
233
|
+
const type = topology?.description?.type;
|
|
234
|
+
return typeof type === "string" ? type : null;
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
var assertTransactionSupported = (context) => {
|
|
240
|
+
const { connection, session } = context;
|
|
241
|
+
if (!session) return;
|
|
242
|
+
const sessionClient = getSessionClient(session);
|
|
243
|
+
const connectionClient = getConnectionClient(connection);
|
|
244
|
+
if (sessionClient && connectionClient && sessionClient !== connectionClient) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"cascadeDeletePlugin does not support sessions bound to a different connection/client; pass a session from the owning model connection"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (isInTransaction(session)) {
|
|
250
|
+
const topologyType = getTopologyType(sessionClient);
|
|
251
|
+
if (topologyType === "Single") {
|
|
252
|
+
throw new Error(
|
|
253
|
+
"cascadeDeletePlugin does not support transactions on a standalone MongoDB deployment; use a replica set or sharded cluster, or delete without a transaction"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
70
258
|
var supportsDeleteOne = (value) => isObject(value) && typeof value.deleteOne === "function";
|
|
71
259
|
var supportsRemove = (value) => isObject(value) && typeof value.remove === "function";
|
|
72
260
|
var mergeResults = (previousResults, modelName, currentResults) => {
|
|
@@ -79,56 +267,189 @@ var mergeResults = (previousResults, modelName, currentResults) => {
|
|
|
79
267
|
[modelName]: currentResults
|
|
80
268
|
};
|
|
81
269
|
};
|
|
270
|
+
var resolveDependentFilter = (owner, config) => {
|
|
271
|
+
const { model, localField, foreignField, foreignFilter, extraForeignFilter } = config;
|
|
272
|
+
const document = owner.toObject({ virtuals: false });
|
|
273
|
+
if (foreignFilter !== void 0) {
|
|
274
|
+
const resolved = resolveFilter(foreignFilter, document);
|
|
275
|
+
if (resolved === null) return null;
|
|
276
|
+
assertFullFilterNotEmpty(resolved, model);
|
|
277
|
+
return resolved;
|
|
278
|
+
}
|
|
279
|
+
if (localField && foreignField) {
|
|
280
|
+
const localValue = owner.get(localField);
|
|
281
|
+
if (normalizeKeyList(localValue) === null) {
|
|
282
|
+
if (isPathDeselected(owner, localField)) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`cascadeDeletePlugin for model "${model}" cannot resolve dependents: localField "${localField}" was not selected on the document; load the document with that path projected instead of assuming an empty relation`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const relationship = buildRelationshipPredicate(foreignField, localValue);
|
|
290
|
+
if (!relationship) return null;
|
|
291
|
+
const extraFilter = resolveFilter(extraForeignFilter, document);
|
|
292
|
+
return combineWithRelationship(relationship, extraFilter);
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
};
|
|
296
|
+
var resolveTraversalLimits = (options) => ({
|
|
297
|
+
maxConcurrency: typeof options.maxConcurrency === "number" ? options.maxConcurrency : DEFAULT_MAX_CONCURRENCY,
|
|
298
|
+
batchSize: typeof options.batchSize === "number" ? options.batchSize : DEFAULT_BATCH_SIZE
|
|
299
|
+
});
|
|
300
|
+
var extractIdList = (rows) => rows.map((row) => {
|
|
301
|
+
if (isPlainObject2(row)) return row._id;
|
|
302
|
+
if (isObject(row)) {
|
|
303
|
+
try {
|
|
304
|
+
const candidate = row._id;
|
|
305
|
+
if (candidate !== void 0) return candidate;
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return row;
|
|
310
|
+
});
|
|
311
|
+
var fetchDependentIdPage = async (Target, filter, session, lastId, batchSize) => {
|
|
312
|
+
const paged = lastId === void 0 ? filter : { $and: [filter, { _id: { $gt: lastId } }] };
|
|
313
|
+
let query = Target.find(paged).select("_id").sort({ _id: 1 }).limit(batchSize).lean();
|
|
314
|
+
if (session) query = query.session(session);
|
|
315
|
+
const rows = await query;
|
|
316
|
+
return extractIdList(Array.isArray(rows) ? rows : []);
|
|
317
|
+
};
|
|
318
|
+
var loadHydratedDependent = async (Target, id, session) => {
|
|
319
|
+
const query = Target.findById(id);
|
|
320
|
+
const result = session ? await query.session(session) : await query;
|
|
321
|
+
return result ?? null;
|
|
322
|
+
};
|
|
323
|
+
var runWithConcurrencyLimit = async (items, limit, task) => {
|
|
324
|
+
if (items.length === 0) return;
|
|
325
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
326
|
+
let next = 0;
|
|
327
|
+
let failed = false;
|
|
328
|
+
let firstError = null;
|
|
329
|
+
const workers = Array.from({ length: workerCount }, () => {
|
|
330
|
+
const run = async () => {
|
|
331
|
+
while (true) {
|
|
332
|
+
if (failed) return;
|
|
333
|
+
const index = next;
|
|
334
|
+
next += 1;
|
|
335
|
+
if (index >= items.length) return;
|
|
336
|
+
try {
|
|
337
|
+
await task(items[index]);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (!failed) {
|
|
340
|
+
failed = true;
|
|
341
|
+
firstError = error;
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
return run();
|
|
348
|
+
});
|
|
349
|
+
try {
|
|
350
|
+
await Promise.all(workers);
|
|
351
|
+
} catch {
|
|
352
|
+
throw firstError;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
82
355
|
function cascadeDeletePlugin(schema, options) {
|
|
356
|
+
assertPluginOptions(options);
|
|
83
357
|
const { model, localField, foreignField, foreignFilter, extraForeignFilter } = options ?? {};
|
|
358
|
+
const filterConfig = {
|
|
359
|
+
model,
|
|
360
|
+
localField,
|
|
361
|
+
foreignField,
|
|
362
|
+
foreignFilter,
|
|
363
|
+
extraForeignFilter
|
|
364
|
+
};
|
|
365
|
+
const traversalLimits = resolveTraversalLimits(options ?? {});
|
|
84
366
|
const findDependents = async function(queryOptions) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
if (foreignFilter) {
|
|
89
|
-
query = resolveFilter(foreignFilter, document);
|
|
90
|
-
} else if (localField && foreignField) {
|
|
91
|
-
const localValue = this.get(localField);
|
|
92
|
-
const extraFilter = resolveFilter(extraForeignFilter, document) ?? {};
|
|
93
|
-
query = {
|
|
94
|
-
[foreignField]: Array.isArray(localValue) ? { $in: localValue } : localValue,
|
|
95
|
-
...extraFilter
|
|
96
|
-
};
|
|
97
|
-
}
|
|
367
|
+
const context = resolveExecutionContext(this, model, queryOptions?.session ?? null);
|
|
368
|
+
const { Target, session } = context;
|
|
369
|
+
const query = resolveDependentFilter(this, filterConfig);
|
|
98
370
|
if (!query || !isPlainObject2(query)) {
|
|
99
371
|
return [];
|
|
100
372
|
}
|
|
101
|
-
|
|
373
|
+
const mergedOptions = session ? { ...queryOptions, session } : queryOptions;
|
|
374
|
+
return await applyQueryOptions(Target.find(query), mergedOptions);
|
|
102
375
|
};
|
|
103
376
|
const findOrphans = async function(queryOptions) {
|
|
104
377
|
if (!localField || !foreignField) return null;
|
|
105
|
-
const
|
|
378
|
+
const context = resolveExecutionContext(this, model, queryOptions?.session ?? null);
|
|
379
|
+
const { Target, session } = context;
|
|
106
380
|
const schemaValue = Target.schema.obj[foreignField];
|
|
107
381
|
if (!schemaValue) return null;
|
|
108
382
|
const isMyRef = isReference(schemaValue, this.modelName);
|
|
109
383
|
if (!isMyRef) return null;
|
|
110
|
-
const ids = await this.distinct("_id");
|
|
111
|
-
const extraFilter = resolveFilter(extraForeignFilter)
|
|
112
|
-
const
|
|
113
|
-
[foreignField]: { $not: { $in: ids } }
|
|
114
|
-
...extraFilter
|
|
384
|
+
const ids = session != null ? await this.distinct("_id", {}, { session }) : await this.distinct("_id");
|
|
385
|
+
const extraFilter = resolveFilter(extraForeignFilter);
|
|
386
|
+
const relationship = {
|
|
387
|
+
[foreignField]: { $not: { $in: ids } }
|
|
115
388
|
};
|
|
116
|
-
|
|
389
|
+
const query = combineWithRelationship(
|
|
390
|
+
relationship,
|
|
391
|
+
extraFilter
|
|
392
|
+
);
|
|
393
|
+
const mergedOptions = session ? { ...queryOptions, session } : queryOptions;
|
|
394
|
+
return await applyQueryOptions(Target.find(query), mergedOptions);
|
|
117
395
|
};
|
|
118
|
-
const deleteDependents = async function() {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
396
|
+
const deleteDependents = async function(operationOptions) {
|
|
397
|
+
const context = resolveExecutionContext(this, model, operationOptions?.session ?? null);
|
|
398
|
+
assertTransactionSupported(context);
|
|
399
|
+
const { Target, session } = context;
|
|
400
|
+
const filter = resolveDependentFilter(this, filterConfig);
|
|
401
|
+
if (!filter || !isPlainObject2(filter)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const deleteSingle = async (document) => {
|
|
405
|
+
if (session != null && typeof document.$session === "function") {
|
|
406
|
+
try {
|
|
407
|
+
await document.$session?.(session);
|
|
408
|
+
} catch {
|
|
127
409
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
410
|
+
}
|
|
411
|
+
if (supportsDeleteOne(document)) {
|
|
412
|
+
return session != null ? document.deleteOne({ session }) : document.deleteOne();
|
|
413
|
+
}
|
|
414
|
+
if (supportsRemove(document)) {
|
|
415
|
+
return session != null ? document.remove({ session }) : document.remove();
|
|
416
|
+
}
|
|
417
|
+
return Promise.resolve();
|
|
418
|
+
};
|
|
419
|
+
const deleteById = async (id) => {
|
|
420
|
+
const hydrated = await loadHydratedDependent(Target, id, session);
|
|
421
|
+
if (!hydrated) return;
|
|
422
|
+
await deleteSingle(hydrated);
|
|
423
|
+
};
|
|
424
|
+
const effectiveConcurrency = session != null && isInTransaction(session) ? 1 : traversalLimits.maxConcurrency;
|
|
425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
426
|
+
let lastId;
|
|
427
|
+
let firstPage = true;
|
|
428
|
+
for (; ; ) {
|
|
429
|
+
const ids = await fetchDependentIdPage(
|
|
430
|
+
Target,
|
|
431
|
+
filter,
|
|
432
|
+
session,
|
|
433
|
+
firstPage ? void 0 : lastId,
|
|
434
|
+
traversalLimits.batchSize
|
|
435
|
+
);
|
|
436
|
+
firstPage = false;
|
|
437
|
+
if (ids.length === 0) return;
|
|
438
|
+
const fresh = ids.filter((id) => {
|
|
439
|
+
const key = typeof id === "object" && id !== null ? String(id) : `${typeof id}:${String(id)}`;
|
|
440
|
+
if (seen.has(key)) return false;
|
|
441
|
+
seen.add(key);
|
|
442
|
+
return true;
|
|
443
|
+
});
|
|
444
|
+
await runWithConcurrencyLimit(fresh, effectiveConcurrency, deleteById);
|
|
445
|
+
lastId = ids[ids.length - 1];
|
|
446
|
+
if (ids.length < traversalLimits.batchSize) return;
|
|
447
|
+
}
|
|
131
448
|
};
|
|
449
|
+
schema.pre("deleteOne", { document: true, query: false }, async function preDeleteOne(...args) {
|
|
450
|
+
const explicitSession = args[1]?.session ?? args[0]?.session ?? null;
|
|
451
|
+
assertTransactionSupported(resolveExecutionContext(this, model, explicitSession));
|
|
452
|
+
});
|
|
132
453
|
schema.post("deleteOne", { document: true, query: false }, async function postDeleteOne() {
|
|
133
454
|
await deleteDependents.call(this);
|
|
134
455
|
});
|
|
@@ -25,10 +25,23 @@ interface KeycloakUserSyncErrorContext {
|
|
|
25
25
|
interface KeycloakUserSyncSensitiveErrorContext extends KeycloakUserSyncErrorContext {
|
|
26
26
|
document: unknown;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Allowlisted logger error summary. The logger boundary never receives the
|
|
30
|
+
* original error, its message/stack/cause, transport request/response data,
|
|
31
|
+
* or arbitrary enumerable properties — only a sanitized name plus optional
|
|
32
|
+
* machine codes. `onError` still receives the original error for private
|
|
33
|
+
* processing.
|
|
34
|
+
*/
|
|
35
|
+
interface KeycloakUserSyncLoggedError {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly code?: string;
|
|
38
|
+
readonly status?: number;
|
|
39
|
+
}
|
|
40
|
+
interface KeycloakUserSyncLogContext extends KeycloakUserSyncErrorContext {
|
|
41
|
+
readonly error: KeycloakUserSyncLoggedError;
|
|
42
|
+
}
|
|
28
43
|
interface KeycloakUserSyncLogger {
|
|
29
|
-
error(message: string, context:
|
|
30
|
-
error: unknown;
|
|
31
|
-
}): void | Promise<void>;
|
|
44
|
+
error(message: string, context: KeycloakUserSyncLogContext): void | Promise<void>;
|
|
32
45
|
}
|
|
33
46
|
interface KeycloakUserSyncDocument {
|
|
34
47
|
get(path: string): unknown;
|
|
@@ -54,6 +67,21 @@ interface KeycloakUserSyncPluginOptions {
|
|
|
54
67
|
mapPassword?: (document: KeycloakUserSyncDocument) => string | null | undefined;
|
|
55
68
|
/** Mongoose paths that should trigger attribute syncing when mapAttributes reads dynamic fields. */
|
|
56
69
|
attributePaths?: readonly string[];
|
|
70
|
+
/**
|
|
71
|
+
* Mongoose paths that should trigger role syncing when mapRoles reads
|
|
72
|
+
* fields beyond the configured roles path (e.g. a tier or plan field).
|
|
73
|
+
* Honored only while `syncFields.roles` is enabled. The configured roles
|
|
74
|
+
* path always triggers; these are additional explicit dependencies.
|
|
75
|
+
*/
|
|
76
|
+
rolePaths?: readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* Mongoose paths that should trigger password syncing when mapPassword
|
|
79
|
+
* reads fields beyond the configured password path (e.g. a virtual
|
|
80
|
+
* backing field or pending-password input). Honored only while
|
|
81
|
+
* `syncFields.password` is enabled. The mapper itself is still evaluated
|
|
82
|
+
* only for creation or an actual password-sync intent.
|
|
83
|
+
*/
|
|
84
|
+
passwordPaths?: readonly string[];
|
|
57
85
|
/** Attribute keys owned by this plugin. Only managed keys are removed when missing from the mapper result. */
|
|
58
86
|
managedAttributes?: readonly string[];
|
|
59
87
|
/** Role names this plugin owns. Role removal is limited to this set; without it, role sync is additive-only. */
|
|
@@ -87,4 +115,4 @@ interface KeycloakUserSyncPluginOptions {
|
|
|
87
115
|
*/
|
|
88
116
|
declare function keycloakUserSyncPlugin(schema: Schema, rawOptions: KeycloakUserSyncPluginOptions): void;
|
|
89
117
|
|
|
90
|
-
export { type KeycloakUserIdentityField, type KeycloakUserSyncDocument, type KeycloakUserSyncErrorContext, type KeycloakUserSyncField, type KeycloakUserSyncLogger, type KeycloakUserSyncPaths, type KeycloakUserSyncPluginOptions, type KeycloakUserSyncSensitiveErrorContext, type ManagedKeycloakClientSecret, keycloakUserSyncPlugin };
|
|
118
|
+
export { type KeycloakUserIdentityField, type KeycloakUserSyncDocument, type KeycloakUserSyncErrorContext, type KeycloakUserSyncField, type KeycloakUserSyncLogContext, type KeycloakUserSyncLoggedError, type KeycloakUserSyncLogger, type KeycloakUserSyncPaths, type KeycloakUserSyncPluginOptions, type KeycloakUserSyncSensitiveErrorContext, type ManagedKeycloakClientSecret, keycloakUserSyncPlugin };
|
|
@@ -25,10 +25,23 @@ interface KeycloakUserSyncErrorContext {
|
|
|
25
25
|
interface KeycloakUserSyncSensitiveErrorContext extends KeycloakUserSyncErrorContext {
|
|
26
26
|
document: unknown;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Allowlisted logger error summary. The logger boundary never receives the
|
|
30
|
+
* original error, its message/stack/cause, transport request/response data,
|
|
31
|
+
* or arbitrary enumerable properties — only a sanitized name plus optional
|
|
32
|
+
* machine codes. `onError` still receives the original error for private
|
|
33
|
+
* processing.
|
|
34
|
+
*/
|
|
35
|
+
interface KeycloakUserSyncLoggedError {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly code?: string;
|
|
38
|
+
readonly status?: number;
|
|
39
|
+
}
|
|
40
|
+
interface KeycloakUserSyncLogContext extends KeycloakUserSyncErrorContext {
|
|
41
|
+
readonly error: KeycloakUserSyncLoggedError;
|
|
42
|
+
}
|
|
28
43
|
interface KeycloakUserSyncLogger {
|
|
29
|
-
error(message: string, context:
|
|
30
|
-
error: unknown;
|
|
31
|
-
}): void | Promise<void>;
|
|
44
|
+
error(message: string, context: KeycloakUserSyncLogContext): void | Promise<void>;
|
|
32
45
|
}
|
|
33
46
|
interface KeycloakUserSyncDocument {
|
|
34
47
|
get(path: string): unknown;
|
|
@@ -54,6 +67,21 @@ interface KeycloakUserSyncPluginOptions {
|
|
|
54
67
|
mapPassword?: (document: KeycloakUserSyncDocument) => string | null | undefined;
|
|
55
68
|
/** Mongoose paths that should trigger attribute syncing when mapAttributes reads dynamic fields. */
|
|
56
69
|
attributePaths?: readonly string[];
|
|
70
|
+
/**
|
|
71
|
+
* Mongoose paths that should trigger role syncing when mapRoles reads
|
|
72
|
+
* fields beyond the configured roles path (e.g. a tier or plan field).
|
|
73
|
+
* Honored only while `syncFields.roles` is enabled. The configured roles
|
|
74
|
+
* path always triggers; these are additional explicit dependencies.
|
|
75
|
+
*/
|
|
76
|
+
rolePaths?: readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* Mongoose paths that should trigger password syncing when mapPassword
|
|
79
|
+
* reads fields beyond the configured password path (e.g. a virtual
|
|
80
|
+
* backing field or pending-password input). Honored only while
|
|
81
|
+
* `syncFields.password` is enabled. The mapper itself is still evaluated
|
|
82
|
+
* only for creation or an actual password-sync intent.
|
|
83
|
+
*/
|
|
84
|
+
passwordPaths?: readonly string[];
|
|
57
85
|
/** Attribute keys owned by this plugin. Only managed keys are removed when missing from the mapper result. */
|
|
58
86
|
managedAttributes?: readonly string[];
|
|
59
87
|
/** Role names this plugin owns. Role removal is limited to this set; without it, role sync is additive-only. */
|
|
@@ -87,4 +115,4 @@ interface KeycloakUserSyncPluginOptions {
|
|
|
87
115
|
*/
|
|
88
116
|
declare function keycloakUserSyncPlugin(schema: Schema, rawOptions: KeycloakUserSyncPluginOptions): void;
|
|
89
117
|
|
|
90
|
-
export { type KeycloakUserIdentityField, type KeycloakUserSyncDocument, type KeycloakUserSyncErrorContext, type KeycloakUserSyncField, type KeycloakUserSyncLogger, type KeycloakUserSyncPaths, type KeycloakUserSyncPluginOptions, type KeycloakUserSyncSensitiveErrorContext, type ManagedKeycloakClientSecret, keycloakUserSyncPlugin };
|
|
118
|
+
export { type KeycloakUserIdentityField, type KeycloakUserSyncDocument, type KeycloakUserSyncErrorContext, type KeycloakUserSyncField, type KeycloakUserSyncLogContext, type KeycloakUserSyncLoggedError, type KeycloakUserSyncLogger, type KeycloakUserSyncPaths, type KeycloakUserSyncPluginOptions, type KeycloakUserSyncSensitiveErrorContext, type ManagedKeycloakClientSecret, keycloakUserSyncPlugin };
|