-
Notifications
You must be signed in to change notification settings - Fork 13.3k
refactor: move index creation to onServerVersionChange #39253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
2eb28d2
f2cec7f
45865a5
cba9a5b
0f10160
16de861
3182c2f
1c27f60
96ffc6a
a44a92c
8d8e9f5
6403c15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -43,68 +43,89 @@ export const upsertPermissions = async (): Promise<void> => { | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const createSettingPermission = async function ( | ||||||||||||||||||||||||||||||||||||||||
| setting: ISetting, | ||||||||||||||||||||||||||||||||||||||||
| previousSettingPermissions: { | ||||||||||||||||||||||||||||||||||||||||
| [key: string]: IPermission; | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| previousSettingPermissions: Record<string, IPermission>, | ||||||||||||||||||||||||||||||||||||||||
| ): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||
| const { _id: permissionId, doc } = buildSettingPermissionDoc(setting, previousSettingPermissions); | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| await Permissions.updateOne({ _id: permissionId }, { $set: doc }, { upsert: true }); | ||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||
| if (!(e as Error).message.includes('E11000')) { | ||||||||||||||||||||||||||||||||||||||||
| await Permissions.updateOne({ _id: permissionId }, { $set: doc }, { upsert: true }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| delete previousSettingPermissions[permissionId]; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const buildSettingPermissionDoc = function ( | ||||||||||||||||||||||||||||||||||||||||
| setting: ISetting, | ||||||||||||||||||||||||||||||||||||||||
| previousSettingPermissions: Record<string, IPermission>, | ||||||||||||||||||||||||||||||||||||||||
| ): { _id: string; doc: Omit<IPermission, '_id'> } { | ||||||||||||||||||||||||||||||||||||||||
| const permissionId = getSettingPermissionId(setting._id); | ||||||||||||||||||||||||||||||||||||||||
| const permission: Omit<IPermission, '_id' | '_updatedAt'> = { | ||||||||||||||||||||||||||||||||||||||||
| level: CONSTANTS.SETTINGS_LEVEL as 'settings' | undefined, | ||||||||||||||||||||||||||||||||||||||||
| // copy those setting-properties which are needed to properly publish the setting-based permissions | ||||||||||||||||||||||||||||||||||||||||
| settingId: setting._id, | ||||||||||||||||||||||||||||||||||||||||
| group: setting.group, | ||||||||||||||||||||||||||||||||||||||||
| section: setting.section ?? undefined, | ||||||||||||||||||||||||||||||||||||||||
| sorter: setting.sorter, | ||||||||||||||||||||||||||||||||||||||||
| roles: [], | ||||||||||||||||||||||||||||||||||||||||
| roles: previousSettingPermissions[permissionId]?.roles ?? [], | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| // copy previously assigned roles if available | ||||||||||||||||||||||||||||||||||||||||
| if (previousSettingPermissions[permissionId]?.roles) { | ||||||||||||||||||||||||||||||||||||||||
| permission.roles = previousSettingPermissions[permissionId].roles; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if (setting.group) { | ||||||||||||||||||||||||||||||||||||||||
| permission.groupPermissionId = getSettingPermissionId(setting.group); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if (setting.section) { | ||||||||||||||||||||||||||||||||||||||||
| permission.sectionPermissionId = getSettingPermissionId(setting.section); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return { _id: permissionId, doc: { ...permission, _updatedAt: new Date() } }; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const existent = await Permissions.findOne( | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| _id: permissionId, | ||||||||||||||||||||||||||||||||||||||||
| ...permission, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| { projection: { _id: 1 } }, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| const BULK_WRITE_BATCH_SIZE = 500; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!existent) { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| await Permissions.updateOne({ _id: permissionId }, { $set: permission }, { upsert: true }); | ||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||
| if (!(e as Error).message.includes('E11000')) { | ||||||||||||||||||||||||||||||||||||||||
| // E11000 refers to a MongoDB error that can occur when using unique indexes for upserts | ||||||||||||||||||||||||||||||||||||||||
| // https://docs.mongodb.com/manual/reference/method/db.collection.update/#use-unique-indexes | ||||||||||||||||||||||||||||||||||||||||
| await Permissions.updateOne({ _id: permissionId }, { $set: permission }, { upsert: true }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| delete previousSettingPermissions[permissionId]; | ||||||||||||||||||||||||||||||||||||||||
| type SettingPermissionUpdateOp = { | ||||||||||||||||||||||||||||||||||||||||
| updateOne: { | ||||||||||||||||||||||||||||||||||||||||
| filter: { _id: string }; | ||||||||||||||||||||||||||||||||||||||||
| update: { $set: Omit<IPermission, '_id'> }; | ||||||||||||||||||||||||||||||||||||||||
| upsert: true; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const createPermissionsForExistingSettings = async function (): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||
| const previousSettingPermissions = await getPreviousPermissions(); | ||||||||||||||||||||||||||||||||||||||||
| const settingsList = await Settings.findNotHidden().toArray(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const settings = await Settings.findNotHidden().toArray(); | ||||||||||||||||||||||||||||||||||||||||
| for await (const setting of settings) { | ||||||||||||||||||||||||||||||||||||||||
| await createSettingPermission(setting, previousSettingPermissions); | ||||||||||||||||||||||||||||||||||||||||
| const updateOps: SettingPermissionUpdateOp[] = []; | ||||||||||||||||||||||||||||||||||||||||
| for (const setting of settingsList) { | ||||||||||||||||||||||||||||||||||||||||
| const { _id: permissionId, doc } = buildSettingPermissionDoc(setting, previousSettingPermissions); | ||||||||||||||||||||||||||||||||||||||||
| updateOps.push({ | ||||||||||||||||||||||||||||||||||||||||
| updateOne: { | ||||||||||||||||||||||||||||||||||||||||
| filter: { _id: permissionId }, | ||||||||||||||||||||||||||||||||||||||||
| update: { $set: doc }, | ||||||||||||||||||||||||||||||||||||||||
| upsert: true, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| delete previousSettingPermissions[permissionId]; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // remove permissions for non-existent settings | ||||||||||||||||||||||||||||||||||||||||
| for await (const obsoletePermission of Object.keys(previousSettingPermissions)) { | ||||||||||||||||||||||||||||||||||||||||
| if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) { | ||||||||||||||||||||||||||||||||||||||||
| await Permissions.deleteOne({ _id: obsoletePermission }); | ||||||||||||||||||||||||||||||||||||||||
| // Batches run sequentially so E11000 retry applies per batch | ||||||||||||||||||||||||||||||||||||||||
| /* eslint-disable no-await-in-loop */ | ||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < updateOps.length; i += BULK_WRITE_BATCH_SIZE) { | ||||||||||||||||||||||||||||||||||||||||
| const batch = updateOps.slice(i, i + BULK_WRITE_BATCH_SIZE); | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| await Permissions.col.bulkWrite(batch, { ordered: false }); | ||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||
| if ((e as Error).message.includes('E11000')) { | ||||||||||||||||||||||||||||||||||||||||
| // E11000 duplicate key: retry without upsert for this batch (doc already exists) | ||||||||||||||||||||||||||||||||||||||||
| await Promise.all(batch.map((op) => Permissions.updateOne(op.updateOne.filter, op.updateOne.update))); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+108
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove new inline implementation comments in the batching path. Line 108 and Line 116 add implementation comments; this repo guideline asks to keep TS/JS implementation comment-free. As per coding guidelines, "Avoid code comments in the implementation". 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| throw e; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+112
to
+120
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In the MongoDB Node.js driver, To detect duplicate key failures from the bulk, check for error code import { MongoBulkWriteError } from "mongodb";
try {
const res = await collection.bulkWrite(ops, { ordered: false });
} catch (err) {
if (err instanceof MongoBulkWriteError) {
const dupKeyErrors = (err.writeErrors ?? []).filter(e => e.code === 11000);
// each error typically includes: e.index (op index), e.code, e.errmsg, e.op
for (const e of dupKeyErrors) {
console.log("dup key at op index:", e.index, "msg:", e.errmsg);
}
} else {
throw err;
}
}Note: If you’re using the driver’s newer “client bulk write” API, Sources: Citations:
🏁 Script executed: # First, let's examine the actual code in the file
find . -type f -name "upsertPermissions.ts" | head -5Repository: RocketChat/Rocket.Chat Length of output: 135 🏁 Script executed: # Check the actual code around lines 112-120
sed -n '105,125p' ./apps/meteor/app/authorization/server/functions/upsertPermissions.tsRepository: RocketChat/Rocket.Chat Length of output: 841 Avoid relying on error message string matching to detect duplicate-key-only failures in unordered On unordered bulks, multiple distinct errors can occur. If any error message happens to contain Correct pattern try {
await Permissions.col.bulkWrite(batch, { ordered: false });
} catch (e) {
- if ((e as Error).message.includes('E11000')) {
- // E11000 duplicate key: retry without upsert for this batch (doc already exists)
- await Promise.all(batch.map((op) => Permissions.updateOne(op.updateOne.filter, op.updateOne.update)));
- } else {
- throw e;
- }
+ const writeErrors = (e as { writeErrors?: Array<{ code?: number }> }).writeErrors ?? [];
+ const duplicateOnly = writeErrors.length > 0 && writeErrors.every((we) => we.code === 11000);
+ if (!duplicateOnly) {
+ throw e;
+ }
+ await Promise.all(batch.map((op) => Permissions.updateOne(op.updateOne.filter, op.updateOne.update)));
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| /* eslint-enable no-await-in-loop */ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const obsoleteIds = Object.keys(previousSettingPermissions); | ||||||||||||||||||||||||||||||||||||||||
| if (obsoleteIds.length > 0) { | ||||||||||||||||||||||||||||||||||||||||
| await Permissions.deleteMany({ _id: { $in: obsoleteIds } }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // for each setting which already exists, create a permission to allow changing just this one setting | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,11 @@ | ||
| import type { ISetting } from '@rocket.chat/core-typings'; | ||
| import type { Settings } from '@rocket.chat/models'; | ||
|
|
||
| import type { ICachedSettings } from './CachedSettings'; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| export async function initializeSettings({ model, settings }: { model: typeof Settings; settings: ICachedSettings }): Promise<void> { | ||
| await model.find().forEach((record: ISetting) => { | ||
| const records = await model.find().toArray(); | ||
| for (const record of records) { | ||
| settings.set(record); | ||
| }); | ||
|
|
||
| } | ||
| settings.initialized(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,24 +191,30 @@ export class AppServerOrchestrator { | |
| return; | ||
| } | ||
|
|
||
| const loadStart = Date.now(); | ||
| await this.getManager().load(); | ||
|
|
||
| // Before enabling each app we verify if there is still room for it | ||
| const apps = await this.getManager().get(); | ||
|
|
||
| // This needs to happen sequentially to keep track of app limits | ||
| for await (const app of apps) { | ||
| try { | ||
| await canEnableApp(app.getStorageItem()); | ||
|
|
||
| await this.getManager().loadOne(app.getID(), true); | ||
| } catch (error) { | ||
| this._rocketchatLogger.warn({ | ||
| msg: 'App could not be enabled', | ||
| appName: app.getInfo().name, | ||
| err: error, | ||
| }); | ||
| } | ||
| const CONCURRENCY_LIMIT = 4; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Race condition: parallelizing app enablement can exceed license app limits. The removed comment explicitly stated "This needs to happen sequentially to keep track of app limits." (Based on your team's feedback about questioning behavioral changes that affect concurrency.) Prompt for AI agents |
||
| for (let i = 0; i < apps.length; i += CONCURRENCY_LIMIT) { | ||
| const chunk = apps.slice(i, i + CONCURRENCY_LIMIT); | ||
| // eslint-disable-next-line no-await-in-loop | ||
| await Promise.all( | ||
| chunk.map(async (app) => { | ||
| try { | ||
| await canEnableApp(app.getStorageItem()); | ||
| await this.getManager().loadOne(app.getID(), true); | ||
| } catch (error) { | ||
| this._rocketchatLogger.warn({ | ||
| msg: 'App could not be enabled', | ||
| appName: app.getInfo().name, | ||
| err: error, | ||
| }); | ||
| } | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| await this.getBridges().getSchedulerBridge().startScheduler(); | ||
|
|
@@ -218,6 +224,7 @@ export class AppServerOrchestrator { | |
| this._rocketchatLogger.info({ | ||
| msg: 'Loaded the Apps Framework and apps', | ||
| appCount, | ||
| durationMs: Date.now() - loadStart, | ||
| }); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| import { TrashRaw } from '@rocket.chat/models'; | ||
| import { registerModel, TrashRaw } from '@rocket.chat/models'; | ||
|
|
||
| import { db } from './utils'; | ||
|
|
||
| const Trash = new TrashRaw(db); | ||
| export const trashCollection = Trash.col; | ||
|
|
||
| registerModel('ITrashModel', Trash); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import type { RocketChatRecordDeleted } from '@rocket.chat/core-typings'; | ||
| import { Emitter } from '@rocket.chat/emitter'; | ||
| import type { IBaseModel, DefaultFields, ResultFields, FindPaginated, InsertionModel } from '@rocket.chat/model-typings'; | ||
| import { traceInstanceMethods } from '@rocket.chat/tracing'; | ||
| import { ObjectId } from 'mongodb'; | ||
|
|
@@ -46,6 +47,30 @@ type ModelOptions = { | |
| collection?: CollectionOptions; | ||
| }; | ||
|
|
||
| export type IndexRegisterFn = () => Promise<void>; | ||
| const ee = new Emitter<{ | ||
| added: IndexRegisterFn; | ||
| }>(); | ||
| // The idea is to accumulate the indexes that should be created in a set, and then create them all at once. | ||
| // in case of a lazy model, we need to create the indexes when the model is instantiated. | ||
|
|
||
| const indexesThatShouldBeCreated = new Set<IndexRegisterFn>(); | ||
| const onAdded = (fn: IndexRegisterFn) => indexesThatShouldBeCreated.add(fn); | ||
| const onAddedExecute = (fn: IndexRegisterFn) => fn(); | ||
| ee.on('added', onAdded); | ||
| export const indexes = { | ||
| ensureIndexes: () => { | ||
| indexesThatShouldBeCreated.forEach((fn) => fn()); | ||
| indexesThatShouldBeCreated.clear(); | ||
| ee.off('added', onAdded); | ||
| ee.on('added', onAddedExecute); | ||
| }, | ||
| cancel: () => { | ||
| ee.off('added', onAdded); | ||
| indexesThatShouldBeCreated.clear(); | ||
| }, | ||
|
Comment on lines
+57
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Index registration currently accumulates duplicates and can multiply executions. At Line 109, every instantiation emits a new closure, so the 💡 Proposed fix (dedupe by collection key + idempotent listener lifecycle) export type IndexRegisterFn = () => Promise<void>;
+type IndexRegistration = { key: string; fn: IndexRegisterFn };
const ee = new Emitter<{
- added: IndexRegisterFn;
+ added: IndexRegistration;
}>();
-const indexesThatShouldBeCreated = new Set<IndexRegisterFn>();
-const onAdded = (fn: IndexRegisterFn) => indexesThatShouldBeCreated.add(fn);
-const onAddedExecute = (fn: IndexRegisterFn) => fn();
+const indexesThatShouldBeCreated = new Map<string, IndexRegisterFn>();
+const onAdded = ({ key, fn }: IndexRegistration) => indexesThatShouldBeCreated.set(key, fn);
+const onAddedExecute = ({ fn }: IndexRegistration) => void fn();
+let isExecutingMode = false;
ee.on('added', onAdded);
export const indexes = {
ensureIndexes: () => {
- indexesThatShouldBeCreated.forEach((fn) => fn());
+ if (isExecutingMode) {
+ return;
+ }
+ for (const fn of indexesThatShouldBeCreated.values()) {
+ void fn();
+ }
indexesThatShouldBeCreated.clear();
ee.off('added', onAdded);
+ ee.off('added', onAddedExecute);
ee.on('added', onAddedExecute);
+ isExecutingMode = true;
},
cancel: () => {
ee.off('added', onAdded);
+ ee.off('added', onAddedExecute);
indexesThatShouldBeCreated.clear();
+ isExecutingMode = false;
},
} as const;- void ee.emit('added', () => this.createIndexes());
+ void ee.emit('added', { key: this.collectionName, fn: () => this.createIndexes() });Also applies to: 109-109 🧰 Tools🪛 Biome (2.4.4)[error] 63-63: This callback passed to forEach() iterable method should not return a value. (lint/suspicious/useIterableCallbackReturn) 🤖 Prompt for AI Agents |
||
| } as const; | ||
|
|
||
| export abstract class BaseRaw< | ||
| T extends { _id: string }, | ||
| C extends DefaultFields<T> = undefined, | ||
|
|
@@ -79,10 +104,10 @@ export abstract class BaseRaw< | |
|
|
||
| this.col = this.db.collection(this.collectionName, options?.collection || {}); | ||
|
|
||
| void this.createIndexes(); | ||
|
|
||
| this.preventSetUpdatedAt = options?.preventSetUpdatedAt ?? false; | ||
|
|
||
| void ee.emit('added', () => this.createIndexes()); | ||
|
|
||
| return traceInstanceMethods(this); | ||
| } | ||
|
|
||
|
|
@@ -363,7 +388,7 @@ export abstract class BaseRaw< | |
| throw e; | ||
| } | ||
|
|
||
| return doc as WithId<T>; | ||
| return doc; | ||
| } | ||
|
|
||
| async deleteMany(filter: Filter<T>, options?: DeleteOptions & { onTrash?: (record: ResultFields<T, C>) => void }): Promise<DeleteResult> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix inverted duplicate-key retry logic in
createSettingPermission.At Line 52, the condition is reversed. E11000 is currently ignored, while non-E11000 is retried. This can leave the permission doc stale after duplicate-key races.
🐛 Proposed fix
try { await Permissions.updateOne({ _id: permissionId }, { $set: doc }, { upsert: true }); } catch (e) { - if (!(e as Error).message.includes('E11000')) { - await Permissions.updateOne({ _id: permissionId }, { $set: doc }, { upsert: true }); - } + if ((e as Error).message.includes('E11000')) { + await Permissions.updateOne({ _id: permissionId }, { $set: doc }); + } else { + throw e; + } }🤖 Prompt for AI Agents