Skip to content

Commit

Permalink
feat(schema+mongoose): add pluginTags to allow applying global plug…
Browse files Browse the repository at this point in the history
…ins to only schemas with matching tags

Fix #9780
  • Loading branch information
vkarpov15 committed Jan 24, 2022
1 parent 2bd5c56 commit 48dd9d2
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 24 deletions.
45 changes: 21 additions & 24 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1158,40 +1158,37 @@ const schema = Schema({
});
```

<h3 id="useNestedStrict"><a href="#useNestedStrict">option: useNestedStrict</a></h3>
<h3 id="pluginTags"><a href="#pluginTags">option: pluginTags</a></h3>

Write operations like `update()`, `updateOne()`, `updateMany()`,
and `findOneAndUpdate()` only check the top-level
schema's strict mode setting.
Mongoose supports defining global plugins, plugins that apply to all schemas.

```javascript
const childSchema = new Schema({}, { strict: false });
const parentSchema = new Schema({ child: childSchema }, { strict: 'throw' });
const Parent = mongoose.model('Parent', parentSchema);
Parent.update({}, { 'child.name': 'Luke Skywalker' }, (error) => {
// Error because parentSchema has `strict: throw`, even though
// `childSchema` has `strict: false`
// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
schema.add({ meta: {} });
});
```

Sometimes, you may only want to apply a given plugin to some schemas.
In that case, you can add `pluginTags` to a schema:

const update = { 'child.name': 'Luke Skywalker' };
const opts = { strict: false };
Parent.update({}, update, opts, function(error) {
// This works because passing `strict: false` to `update()` overwrites
// the parent schema.
```javascript
const schema1 = new Schema({
name: String
}, { pluginTags: ['useMetaPlugin'] });

const schema2 = new Schema({
name: String
});
```

If you set `useNestedStrict` to true, mongoose will use the child schema's
`strict` option for casting updates.
If you call `plugin()` with a `tags` option, Mongoose will only apply that plugin to schemas that have a matching entry in `pluginTags`.

```javascript
const childSchema = new Schema({}, { strict: false });
const parentSchema = new Schema({ child: childSchema },
{ strict: 'throw', useNestedStrict: true });
const Parent = mongoose.model('Parent', parentSchema);
Parent.update({}, { 'child.name': 'Luke Skywalker' }, error => {
// Works!
});
// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
schema.add({ meta: {} });
}, { tags: ['useMetaPlugin'] });
```

<h3 id="selectPopulatedPaths">
Expand Down
5 changes: 5 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,11 @@ declare module 'mongoose' {
* optimistic concurrency.
*/
optimisticConcurrency?: boolean;
/**
* If `plugin()` called with tags, Mongoose will only apply plugins to schemas that have
* a matching tag in `pluginTags`
*/
pluginTags?: string[];
/**
* Allows setting query#read options at the schema level, providing us a way to apply default ReadPreferences
* to all queries derived from a model.
Expand Down
11 changes: 11 additions & 0 deletions lib/helpers/schema/applyPlugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ module.exports = function applyPlugins(schema, plugins, options, cacheKey) {
schema[cacheKey] = true;

if (!options || !options.skipTopLevel) {
let pluginTags = null;
for (const plugin of plugins) {
const tags = plugin[1] == null ? null : plugin[1].tags;
if (!Array.isArray(tags)) {
schema.plugin(plugin[0], plugin[1]);
continue;
}

pluginTags = pluginTags || new Set(schema.options.pluginTags || []);
if (!tags.find(tag => pluginTags.has(tag))) {
continue;
}
schema.plugin(plugin[0], plugin[1]);
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ let id = 0;
* - [selectPopulatedPaths](/docs/guide.html#selectPopulatedPaths): boolean - defaults to `true`
* - [skipVersioning](/docs/guide.html#skipVersioning): object - paths to exclude from versioning
* - [timestamps](/docs/guide.html#timestamps): object or boolean - defaults to `false`. If true, Mongoose adds `createdAt` and `updatedAt` properties to your schema and manages those properties for you.
* - [pluginTags](/docs/guide.html#pluginTags): array of strings - defaults to `undefined`. If set and plugin called with `tags` option, will only apply that plugin to schemas with a matching tag.
*
* ####Options for Nested Schemas:
* - `excludeIndexes`: bool - defaults to `false`. If `true`, skip building indexes on this schema's paths.
Expand Down
35 changes: 35 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,41 @@ describe('mongoose module:', function() {
await mong.disconnect();
});

it('declaring global plugins with tags (gh-9780)', async function() {
const mong = new Mongoose();
const schema1 = new Schema({}, { pluginTags: ['tag1'] });
const schema2 = new Schema({}, { pluginTags: ['tag2'] });
const schema3 = new Schema({});

mong.plugin(function(s) {
s.add({ prop1: String });
}, { tags: ['tag1'] });

mong.plugin(function(s) {
s.add({ prop2: String });
}, { tags: ['tag1', 'tag2'] });

mong.plugin(function(s) {
s.add({ prop3: String });
});

const Test1 = mong.model('Test1', schema1);
const Test2 = mong.model('Test2', schema2);
const Test3 = mong.model('Test3', schema3);

assert.ok(Test1.schema.path('prop1'));
assert.ok(Test1.schema.path('prop2'));
assert.ok(Test1.schema.path('prop3'));

assert.ok(!Test2.schema.path('prop1'));
assert.ok(Test2.schema.path('prop2'));
assert.ok(Test2.schema.path('prop3'));

assert.ok(!Test3.schema.path('prop1'));
assert.ok(!Test3.schema.path('prop2'));
assert.ok(Test3.schema.path('prop3'));
});

it('global plugins on nested schemas underneath embedded discriminators (gh-7370)', function() {
const m = new Mongoose();

Expand Down

0 comments on commit 48dd9d2

Please sign in to comment.