@bedrockio/model 0.7.6 → 0.8.0
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/CHANGELOG.md +15 -0
- package/README.md +96 -111
- package/dist/cjs/access.js +1 -1
- package/dist/cjs/assign.js +1 -1
- package/dist/cjs/cache.js +240 -0
- package/dist/cjs/delete-hooks.js +1 -1
- package/dist/cjs/disallowed.js +1 -1
- package/dist/cjs/include.js +11 -2
- package/dist/cjs/load.js +1 -1
- package/dist/cjs/schema.js +3 -1
- package/dist/cjs/search.js +2 -159
- package/dist/cjs/slug.js +10 -13
- package/dist/cjs/soft-delete.js +1 -1
- package/dist/cjs/testing.js +1 -1
- package/dist/cjs/utils.js +31 -1
- package/dist/cjs/validation.js +3 -3
- package/dist/cjs/warn.js +1 -1
- package/package.json +6 -6
- package/src/cache.js +245 -0
- package/src/include.js +11 -1
- package/src/schema.js +2 -0
- package/src/search.js +10 -176
- package/src/slug.js +12 -10
- package/src/utils.js +27 -0
- package/src/validation.js +2 -2
- package/types/cache.d.ts +2 -0
- package/types/cache.d.ts.map +1 -0
- package/types/const.d.ts +3 -3
- package/types/const.d.ts.map +1 -1
- package/types/include.d.ts +22 -30
- package/types/include.d.ts.map +1 -1
- package/types/load.d.ts +1 -1
- package/types/load.d.ts.map +1 -1
- package/types/schema.d.ts +5 -5
- package/types/schema.d.ts.map +1 -1
- package/types/search.d.ts +22 -30
- package/types/search.d.ts.map +1 -1
- package/types/serialization.d.ts +2 -2
- package/types/testing.d.ts +1 -2
- package/types/testing.d.ts.map +1 -1
- package/types/utils.d.ts +7 -1
- package/types/utils.d.ts.map +1 -1
- package/types/validation.d.ts +49 -57
- package/types/validation.d.ts.map +1 -1
package/dist/cjs/search.js
CHANGED
|
@@ -15,18 +15,13 @@ var _validation = require("./validation");
|
|
|
15
15
|
var _env = require("./env");
|
|
16
16
|
var _query = require("./query");
|
|
17
17
|
var _warn = _interopRequireDefault(require("./warn"));
|
|
18
|
-
function _interopRequireDefault(
|
|
19
|
-
const {
|
|
20
|
-
SchemaTypes
|
|
21
|
-
} = _mongoose.default;
|
|
18
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
22
19
|
const {
|
|
23
20
|
ObjectId
|
|
24
21
|
} = _mongoose.default.Types;
|
|
25
22
|
function applySearch(schema, definition) {
|
|
26
23
|
validateDefinition(definition);
|
|
27
24
|
validateSearchFields(schema, definition);
|
|
28
|
-
applySearchCache(schema, definition);
|
|
29
|
-
applySearchSync(schema, definition);
|
|
30
25
|
const {
|
|
31
26
|
query: searchQuery,
|
|
32
27
|
fields: searchFields
|
|
@@ -305,50 +300,6 @@ function parseRegexQuery(str) {
|
|
|
305
300
|
|
|
306
301
|
// Search field caching
|
|
307
302
|
|
|
308
|
-
function applySearchCache(schema, definition) {
|
|
309
|
-
if (!definition.search?.cache) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
createCacheFields(schema, definition);
|
|
313
|
-
applyCacheHook(schema, definition);
|
|
314
|
-
schema.static('syncCacheFields', async function syncCacheFields(options = {}) {
|
|
315
|
-
assertIncludeModule(this);
|
|
316
|
-
const {
|
|
317
|
-
force
|
|
318
|
-
} = options;
|
|
319
|
-
const {
|
|
320
|
-
cache = {}
|
|
321
|
-
} = definition.search || {};
|
|
322
|
-
const paths = getCachePaths(definition);
|
|
323
|
-
const cachedFields = Object.keys(cache);
|
|
324
|
-
if (!cachedFields.length) {
|
|
325
|
-
throw new Error('No search fields to sync.');
|
|
326
|
-
}
|
|
327
|
-
const query = {};
|
|
328
|
-
if (!force) {
|
|
329
|
-
const $or = Object.keys(cache).map(cachedField => {
|
|
330
|
-
return {
|
|
331
|
-
[cachedField]: null
|
|
332
|
-
};
|
|
333
|
-
});
|
|
334
|
-
query.$or = $or;
|
|
335
|
-
}
|
|
336
|
-
const docs = await this.find(query).include(paths);
|
|
337
|
-
const ops = docs.map(doc => {
|
|
338
|
-
return {
|
|
339
|
-
updateOne: {
|
|
340
|
-
filter: {
|
|
341
|
-
_id: doc._id
|
|
342
|
-
},
|
|
343
|
-
update: {
|
|
344
|
-
$set: getUpdates(doc, paths, definition)
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
});
|
|
349
|
-
return await this.bulkWrite(ops);
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
303
|
function validateSearchFields(schema, definition) {
|
|
353
304
|
const {
|
|
354
305
|
fields
|
|
@@ -362,62 +313,6 @@ function validateSearchFields(schema, definition) {
|
|
|
362
313
|
}
|
|
363
314
|
}
|
|
364
315
|
}
|
|
365
|
-
function createCacheFields(schema, definition) {
|
|
366
|
-
for (let [cachedField, def] of Object.entries(definition.search.cache)) {
|
|
367
|
-
// Fall back to string type for virtuals or not defined.
|
|
368
|
-
const {
|
|
369
|
-
type = 'String',
|
|
370
|
-
path,
|
|
371
|
-
...rest
|
|
372
|
-
} = def;
|
|
373
|
-
schema.add({
|
|
374
|
-
[cachedField]: type
|
|
375
|
-
});
|
|
376
|
-
schema.obj[cachedField] = {
|
|
377
|
-
type,
|
|
378
|
-
...rest
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
function applyCacheHook(schema, definition) {
|
|
383
|
-
schema.pre('save', async function () {
|
|
384
|
-
assertIncludeModule(this.constructor);
|
|
385
|
-
assertAssignModule(this.constructor);
|
|
386
|
-
const doc = this;
|
|
387
|
-
const paths = getCachePaths(definition, (cachedField, def) => {
|
|
388
|
-
if (def.lazy) {
|
|
389
|
-
return !(0, _lodash.get)(doc, cachedField);
|
|
390
|
-
} else {
|
|
391
|
-
return true;
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
await this.include(paths);
|
|
395
|
-
this.assign(getUpdates(this, paths, definition));
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Search field syncing
|
|
400
|
-
|
|
401
|
-
function applySearchSync(schema, definition) {
|
|
402
|
-
if (!definition.search?.sync) {
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
schema.post('save', async function postSave() {
|
|
406
|
-
for (let entry of definition.search.sync) {
|
|
407
|
-
const {
|
|
408
|
-
ref,
|
|
409
|
-
path
|
|
410
|
-
} = entry;
|
|
411
|
-
const Model = _mongoose.default.models[ref];
|
|
412
|
-
const docs = await Model.find({
|
|
413
|
-
[path]: this.id
|
|
414
|
-
});
|
|
415
|
-
await Promise.all(docs.map(async doc => {
|
|
416
|
-
await doc.save();
|
|
417
|
-
}));
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
316
|
|
|
422
317
|
// Utils
|
|
423
318
|
|
|
@@ -425,57 +320,5 @@ function isForeignField(schema, path) {
|
|
|
425
320
|
if (!path.includes('.')) {
|
|
426
321
|
return false;
|
|
427
322
|
}
|
|
428
|
-
return !!
|
|
429
|
-
}
|
|
430
|
-
function getRefField(schema, path) {
|
|
431
|
-
const split = path.split('.');
|
|
432
|
-
for (let i = 1; i < split.length; i++) {
|
|
433
|
-
const base = split.slice(0, i);
|
|
434
|
-
const rest = split.slice(i);
|
|
435
|
-
const type = schema.path(base.join('.'));
|
|
436
|
-
if (type instanceof SchemaTypes.ObjectId) {
|
|
437
|
-
return {
|
|
438
|
-
type,
|
|
439
|
-
base,
|
|
440
|
-
rest
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
function getUpdates(doc, paths, definition) {
|
|
446
|
-
const updates = {};
|
|
447
|
-
const entries = Object.entries(definition.search.cache).filter(entry => {
|
|
448
|
-
return paths.includes(entry[1].path);
|
|
449
|
-
});
|
|
450
|
-
for (let [cachedField, def] of entries) {
|
|
451
|
-
// doc.get will not return virtuals (even with specified options),
|
|
452
|
-
// so use lodash to ensure they are included here.
|
|
453
|
-
// https://mongoosejs.com/docs/api/document.html#Document.prototype.get()
|
|
454
|
-
updates[cachedField] = (0, _lodash.get)(doc, def.path);
|
|
455
|
-
}
|
|
456
|
-
return updates;
|
|
457
|
-
}
|
|
458
|
-
function getCachePaths(definition, filter) {
|
|
459
|
-
filter ||= () => true;
|
|
460
|
-
const {
|
|
461
|
-
cache
|
|
462
|
-
} = definition.search || {};
|
|
463
|
-
return Object.entries(cache).filter(entry => {
|
|
464
|
-
return filter(...entry);
|
|
465
|
-
}).map(entry => {
|
|
466
|
-
return entry[1].path;
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Assertions
|
|
471
|
-
|
|
472
|
-
function assertIncludeModule(Model) {
|
|
473
|
-
if (!Model.schema.methods.include) {
|
|
474
|
-
throw new Error('Include module is required for cached search fields.');
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function assertAssignModule(Model) {
|
|
478
|
-
if (!Model.schema.methods.assign) {
|
|
479
|
-
throw new Error('Assign module is required for cached search fields.');
|
|
480
|
-
}
|
|
323
|
+
return !!(0, _utils.resolveRefPath)(schema, path);
|
|
481
324
|
}
|
package/dist/cjs/slug.js
CHANGED
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.applySlug = applySlug;
|
|
7
7
|
var _mongoose = _interopRequireDefault(require("mongoose"));
|
|
8
|
-
function _interopRequireDefault(
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
9
|
const {
|
|
10
10
|
ObjectId
|
|
11
11
|
} = _mongoose.default.Types;
|
|
@@ -26,22 +26,19 @@ function applySlug(schema) {
|
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
function find(Model, str, args,
|
|
29
|
+
function find(Model, str, args, deleted) {
|
|
30
30
|
const isObjectId = str.length === 24 && ObjectId.isValid(str);
|
|
31
31
|
// There is a non-zero chance of a slug colliding with an ObjectId but
|
|
32
32
|
// is exceedingly rare (run of exactly 24 [a-f0-9] chars together
|
|
33
33
|
// without a hyphen) so this should be acceptable.
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const query = {};
|
|
35
|
+
if (isObjectId) {
|
|
36
|
+
query._id = str;
|
|
36
37
|
} else {
|
|
37
|
-
query =
|
|
38
|
-
...query
|
|
39
|
-
};
|
|
40
|
-
if (isObjectId) {
|
|
41
|
-
query._id = str;
|
|
42
|
-
} else {
|
|
43
|
-
query.slug = str;
|
|
44
|
-
}
|
|
45
|
-
return Model.findOne(query, ...args);
|
|
38
|
+
query.slug = str;
|
|
46
39
|
}
|
|
40
|
+
return Model.findOne({
|
|
41
|
+
...deleted,
|
|
42
|
+
...query
|
|
43
|
+
}, ...args);
|
|
47
44
|
}
|
package/dist/cjs/soft-delete.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.hasUniqueConstraints = hasUniqueConstraints;
|
|
|
9
9
|
var _mongoose = _interopRequireDefault(require("mongoose"));
|
|
10
10
|
var _lodash = require("lodash");
|
|
11
11
|
var _query = require("./query");
|
|
12
|
-
function _interopRequireDefault(
|
|
12
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
13
|
function applySoftDelete(schema) {
|
|
14
14
|
applyQueries(schema);
|
|
15
15
|
applyUniqueConstraints(schema);
|
package/dist/cjs/testing.js
CHANGED
|
@@ -8,7 +8,7 @@ exports.getTestModelName = getTestModelName;
|
|
|
8
8
|
var _mongoose = _interopRequireDefault(require("mongoose"));
|
|
9
9
|
var _schema = require("./schema");
|
|
10
10
|
var _utils = require("./utils");
|
|
11
|
-
function _interopRequireDefault(
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
12
|
let counter = 0;
|
|
13
13
|
|
|
14
14
|
/**
|
package/dist/cjs/utils.js
CHANGED
|
@@ -12,8 +12,13 @@ exports.isMongooseSchema = isMongooseSchema;
|
|
|
12
12
|
exports.isNumberField = isNumberField;
|
|
13
13
|
exports.isReferenceField = isReferenceField;
|
|
14
14
|
exports.isSchemaTypedef = isSchemaTypedef;
|
|
15
|
+
exports.resolveRefPath = resolveRefPath;
|
|
15
16
|
var _mongoose = _interopRequireDefault(require("mongoose"));
|
|
16
|
-
function _interopRequireDefault(
|
|
17
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
|
+
const {
|
|
19
|
+
SchemaTypes
|
|
20
|
+
} = _mongoose.default;
|
|
21
|
+
|
|
17
22
|
// Mongoose provides an "equals" method on both documents and
|
|
18
23
|
// ObjectIds, however it does not provide a static method to
|
|
19
24
|
// compare two unknown values that may be either, so provide
|
|
@@ -95,6 +100,31 @@ function getField(obj, path) {
|
|
|
95
100
|
return field || {};
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
// Finds a reference field in a schema and splits
|
|
104
|
+
// the provided path into the local and foreign
|
|
105
|
+
// components of the full path.
|
|
106
|
+
function resolveRefPath(schema, path) {
|
|
107
|
+
const split = path.split('.');
|
|
108
|
+
for (let i = 1; i < split.length; i++) {
|
|
109
|
+
const base = split.slice(0, i);
|
|
110
|
+
const rest = split.slice(i);
|
|
111
|
+
let type = schema.path(base.join('.'));
|
|
112
|
+
if (type instanceof SchemaTypes.Array) {
|
|
113
|
+
type = type.caster;
|
|
114
|
+
}
|
|
115
|
+
if (type instanceof SchemaTypes.ObjectId) {
|
|
116
|
+
const {
|
|
117
|
+
ref
|
|
118
|
+
} = type.options;
|
|
119
|
+
return {
|
|
120
|
+
ref,
|
|
121
|
+
local: base.join('.'),
|
|
122
|
+
foreign: rest.join('.')
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
// The same as getField but traverses into the final field
|
|
99
129
|
// as well. In the above example this will return:
|
|
100
130
|
// { type: 'Number' }, given "product.inventory"
|
package/dist/cjs/validation.js
CHANGED
|
@@ -18,12 +18,12 @@ var _errors = require("./errors");
|
|
|
18
18
|
var _softDelete = require("./soft-delete");
|
|
19
19
|
var _utils = require("./utils");
|
|
20
20
|
var _include = require("./include");
|
|
21
|
-
function _interopRequireDefault(
|
|
21
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
22
22
|
const DATE_TAGS = {
|
|
23
23
|
'x-schema': 'DateTime',
|
|
24
24
|
'x-description': 'A `string` in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format.'
|
|
25
25
|
};
|
|
26
|
-
const OBJECT_ID_SCHEMA = exports.OBJECT_ID_SCHEMA = _yada.default.string().mongo().message('
|
|
26
|
+
const OBJECT_ID_SCHEMA = exports.OBJECT_ID_SCHEMA = _yada.default.string().mongo().message('Must be an id.').tag({
|
|
27
27
|
'x-schema': 'ObjectId',
|
|
28
28
|
'x-description': 'A 24 character hexadecimal string representing a Mongo [ObjectId](https://bit.ly/3YPtGlU).'
|
|
29
29
|
});
|
|
@@ -33,7 +33,7 @@ const REFERENCE_SCHEMA = _yada.default.allow(OBJECT_ID_SCHEMA, _yada.default.obj
|
|
|
33
33
|
stripUnknown: true
|
|
34
34
|
}).custom(obj => {
|
|
35
35
|
return obj.id;
|
|
36
|
-
})).message('
|
|
36
|
+
})).message('Must be an id or object containing an "id" field.').tag({
|
|
37
37
|
'x-schema': 'Reference',
|
|
38
38
|
'x-description': `
|
|
39
39
|
A 24 character hexadecimal string representing a Mongo [ObjectId](https://bit.ly/3YPtGlU).
|
package/dist/cjs/warn.js
CHANGED
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = warn;
|
|
7
7
|
var _logger = _interopRequireDefault(require("@bedrockio/logger"));
|
|
8
|
-
function _interopRequireDefault(
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
9
|
function warn(...lines) {
|
|
10
10
|
if (process.env.ENV_NAME !== 'test') {
|
|
11
11
|
_logger.default.warn(lines.join('\n'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrockio/model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Bedrock utilities for model creation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"lodash": "^4.17.21"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@bedrockio/yada": "^1.2.
|
|
33
|
+
"@bedrockio/yada": "^1.2.7",
|
|
34
34
|
"mongoose": "^8.6.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@babel/core": "^7.20.12",
|
|
39
39
|
"@babel/preset-env": "^7.20.2",
|
|
40
40
|
"@bedrockio/prettier-config": "^1.0.2",
|
|
41
|
-
"@bedrockio/yada": "^1.2.
|
|
41
|
+
"@bedrockio/yada": "^1.2.7",
|
|
42
42
|
"@shelf/jest-mongodb": "^4.3.2",
|
|
43
43
|
"eslint": "^8.33.0",
|
|
44
44
|
"eslint-plugin-bedrock": "^1.0.26",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"mongodb": "^6.5.0",
|
|
48
48
|
"mongoose": "^8.6.2",
|
|
49
49
|
"prettier-eslint": "^15.0.1",
|
|
50
|
-
"typescript": "^
|
|
50
|
+
"typescript": "^5.7.2"
|
|
51
51
|
},
|
|
52
52
|
"volta": {
|
|
53
|
-
"node": "
|
|
54
|
-
"yarn": "1.22.
|
|
53
|
+
"node": "22.12.0",
|
|
54
|
+
"yarn": "1.22.22"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/cache.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
import { get, once, groupBy } from 'lodash';
|
|
3
|
+
|
|
4
|
+
import { resolveRefPath } from './utils';
|
|
5
|
+
|
|
6
|
+
const definitionMap = new Map();
|
|
7
|
+
|
|
8
|
+
mongoose.plugin(cacheSyncPlugin);
|
|
9
|
+
|
|
10
|
+
export function applyCache(schema, definition) {
|
|
11
|
+
definitionMap.set(schema, definition);
|
|
12
|
+
|
|
13
|
+
if (!definition.cache) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
createCacheFields(schema, definition);
|
|
18
|
+
applyStaticMethods(schema, definition);
|
|
19
|
+
applyCacheHook(schema, definition);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createCacheFields(schema, definition) {
|
|
23
|
+
for (let [cachedField, def] of Object.entries(definition.cache)) {
|
|
24
|
+
const { type, path, ...rest } = def;
|
|
25
|
+
|
|
26
|
+
schema.add({
|
|
27
|
+
[cachedField]: type,
|
|
28
|
+
});
|
|
29
|
+
schema.obj[cachedField] = {
|
|
30
|
+
...rest,
|
|
31
|
+
type,
|
|
32
|
+
writeAccess: 'none',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function applyStaticMethods(schema, definition) {
|
|
38
|
+
schema.static('syncCacheFields', async function syncCacheFields() {
|
|
39
|
+
assertIncludeModule(this);
|
|
40
|
+
|
|
41
|
+
const fields = resolveCachedFields(schema, definition);
|
|
42
|
+
|
|
43
|
+
const hasSynced = fields.some((entry) => {
|
|
44
|
+
return entry.sync;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const query = {};
|
|
48
|
+
|
|
49
|
+
if (!hasSynced) {
|
|
50
|
+
const $or = fields.map((field) => {
|
|
51
|
+
return {
|
|
52
|
+
[field.name]: null,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
query.$or = $or;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const includes = getIncludes(fields);
|
|
59
|
+
const docs = await this.find(query).include(includes);
|
|
60
|
+
|
|
61
|
+
const ops = docs.flatMap((doc) => {
|
|
62
|
+
return fields.map((field) => {
|
|
63
|
+
const { name, sync } = field;
|
|
64
|
+
const updates = getUpdates(doc, [field]);
|
|
65
|
+
const filter = {
|
|
66
|
+
_id: doc._id,
|
|
67
|
+
};
|
|
68
|
+
if (!sync) {
|
|
69
|
+
filter[name] = null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
updateOne: {
|
|
73
|
+
filter,
|
|
74
|
+
update: {
|
|
75
|
+
$set: updates,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return await this.bulkWrite(ops);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function applyCacheHook(schema, definition) {
|
|
87
|
+
const fields = resolveCachedFields(schema, definition);
|
|
88
|
+
schema.pre('save', async function () {
|
|
89
|
+
assertIncludeModule(this.constructor);
|
|
90
|
+
assertAssignModule(this.constructor);
|
|
91
|
+
|
|
92
|
+
const doc = this;
|
|
93
|
+
|
|
94
|
+
const changes = fields.filter((field) => {
|
|
95
|
+
const { sync, local, name } = field;
|
|
96
|
+
if (sync || doc.isModified(local)) {
|
|
97
|
+
// Always update if we are actively syncing
|
|
98
|
+
// or if the field has been changed.
|
|
99
|
+
return true;
|
|
100
|
+
} else {
|
|
101
|
+
// Otherwise only update if the value does
|
|
102
|
+
// not exist yet.
|
|
103
|
+
const value = get(doc, name);
|
|
104
|
+
return Array.isArray(value) ? !value.length : !value;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await this.include(getIncludes(changes));
|
|
109
|
+
this.assign(getUpdates(doc, changes));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Syncing
|
|
114
|
+
|
|
115
|
+
const syncOperations = {};
|
|
116
|
+
const compiledModels = new Set();
|
|
117
|
+
|
|
118
|
+
function cacheSyncPlugin(schema) {
|
|
119
|
+
// Compile sync fields each time a new schema
|
|
120
|
+
// is registered but only do it one time for.
|
|
121
|
+
const initialize = once(compileSyncOperations);
|
|
122
|
+
|
|
123
|
+
schema.pre('save', async function () {
|
|
124
|
+
this.$locals.modifiedPaths = this.modifiedPaths();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
schema.post('save', async function () {
|
|
128
|
+
initialize();
|
|
129
|
+
|
|
130
|
+
// @ts-ignore
|
|
131
|
+
const { modelName } = this.constructor;
|
|
132
|
+
|
|
133
|
+
const ops = syncOperations[modelName] || [];
|
|
134
|
+
for (let op of ops) {
|
|
135
|
+
await op(this);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function compileSyncOperations() {
|
|
140
|
+
for (let Model of Object.values(mongoose.models)) {
|
|
141
|
+
const { schema } = Model;
|
|
142
|
+
|
|
143
|
+
if (compiledModels.has(Model)) {
|
|
144
|
+
// Model has already been compiled so skip.
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const definition = definitionMap.get(schema);
|
|
148
|
+
const fields = resolveCachedFields(schema, definition);
|
|
149
|
+
|
|
150
|
+
for (let [ref, group] of Object.entries(groupBy(fields, 'ref'))) {
|
|
151
|
+
const hasSynced = group.some((entry) => {
|
|
152
|
+
return entry.sync;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!hasSynced) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fn = async (doc) => {
|
|
160
|
+
const { modifiedPaths } = doc.$locals;
|
|
161
|
+
const changes = group.filter((entry) => {
|
|
162
|
+
return entry.sync && modifiedPaths.includes(entry.foreign);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (changes.length) {
|
|
166
|
+
const $or = changes.map((change) => {
|
|
167
|
+
const { local } = change;
|
|
168
|
+
return {
|
|
169
|
+
[local]: doc.id,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const docs = await Model.find({
|
|
174
|
+
$or,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await Promise.all(docs.map((doc) => doc.save()));
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
syncOperations[ref] ||= [];
|
|
181
|
+
syncOperations[ref].push(fn);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
compiledModels.add(Model);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Utils
|
|
189
|
+
|
|
190
|
+
function resolveCachedFields(schema, definition) {
|
|
191
|
+
const { cache = {} } = definition;
|
|
192
|
+
return Object.entries(cache).map(([name, def]) => {
|
|
193
|
+
const { path, sync = false } = def;
|
|
194
|
+
const resolved = resolveRefPath(schema, path);
|
|
195
|
+
if (!resolved) {
|
|
196
|
+
throw new Error(`Could not resolve path ${path}.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...resolved,
|
|
201
|
+
name,
|
|
202
|
+
path,
|
|
203
|
+
sync,
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getUpdates(doc, fields) {
|
|
209
|
+
const updates = {};
|
|
210
|
+
|
|
211
|
+
for (let field of fields) {
|
|
212
|
+
const { name, path } = field;
|
|
213
|
+
|
|
214
|
+
// doc.get will not return virtuals (even with specified options),
|
|
215
|
+
// so fall back to lodash to ensure they are included here.
|
|
216
|
+
// https://mongoosejs.com/docs/api/document.html#Document.prototype.get()
|
|
217
|
+
const value = doc.get(path) ?? get(doc, path);
|
|
218
|
+
|
|
219
|
+
updates[name] = value;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return updates;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getIncludes(fields) {
|
|
226
|
+
const includes = new Set();
|
|
227
|
+
for (let field of fields) {
|
|
228
|
+
includes.add(field.local);
|
|
229
|
+
}
|
|
230
|
+
return includes;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Assertions
|
|
234
|
+
|
|
235
|
+
function assertIncludeModule(Model) {
|
|
236
|
+
if (!Model.schema.methods.include) {
|
|
237
|
+
throw new Error('Include module is required for cached fields.');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function assertAssignModule(Model) {
|
|
242
|
+
if (!Model.schema.methods.assign) {
|
|
243
|
+
throw new Error('Assign module is required for cached fields.');
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/include.js
CHANGED
|
@@ -134,11 +134,21 @@ export function checkSelects(doc, ret) {
|
|
|
134
134
|
|
|
135
135
|
// Exported for testing.
|
|
136
136
|
export function getParams(modelName, arg) {
|
|
137
|
-
const paths =
|
|
137
|
+
const paths = resolvePathsArg(arg);
|
|
138
138
|
const node = pathsToNode(paths, modelName);
|
|
139
139
|
return nodeToPopulates(node);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
function resolvePathsArg(arg) {
|
|
143
|
+
if (Array.isArray(arg)) {
|
|
144
|
+
return arg;
|
|
145
|
+
} else if (arg instanceof Set) {
|
|
146
|
+
return Array.from(arg);
|
|
147
|
+
} else {
|
|
148
|
+
return [arg];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
// Exported for testing.
|
|
143
153
|
export function getDocumentParams(doc, arg, options = {}) {
|
|
144
154
|
const params = getParams(doc.constructor.modelName, arg);
|
package/src/schema.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isSchemaTypedef } from './utils';
|
|
|
5
5
|
|
|
6
6
|
import { serializeOptions } from './serialization';
|
|
7
7
|
import { applySlug } from './slug';
|
|
8
|
+
import { applyCache } from './cache';
|
|
8
9
|
import { applySearch } from './search';
|
|
9
10
|
import { applyAssign } from './assign';
|
|
10
11
|
import { applyUpsert } from './upsert';
|
|
@@ -58,6 +59,7 @@ export function createSchema(definition, options = {}) {
|
|
|
58
59
|
applyValidation(schema, definition);
|
|
59
60
|
applyDeleteHooks(schema, definition);
|
|
60
61
|
applySearch(schema, definition);
|
|
62
|
+
applyCache(schema, definition);
|
|
61
63
|
applyDisallowed(schema);
|
|
62
64
|
applyInclude(schema);
|
|
63
65
|
applyHydrate(schema);
|