@bedrockio/model 0.7.6 → 0.8.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.
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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 !!getRefField(schema, path);
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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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, query) {
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
- if (!query && isObjectId) {
35
- return Model.findById(str, ...args);
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
  }
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
13
  function applySoftDelete(schema) {
14
14
  applyQueries(schema);
15
15
  applyUniqueConstraints(schema);
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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"
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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('{field} must be an id.').tag({
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('{field} must be an id or object containing "id" field.').tag({
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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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.7.6",
3
+ "version": "0.8.1",
4
4
  "description": "Bedrock utilities for model creation.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -26,31 +26,31 @@
26
26
  "url": "https://github.com/bedrockio/model"
27
27
  },
28
28
  "dependencies": {
29
- "@bedrockio/logger": "^1.0.6",
29
+ "@bedrockio/logger": "^1.1.1",
30
30
  "lodash": "^4.17.21"
31
31
  },
32
32
  "peerDependencies": {
33
- "@bedrockio/yada": "^1.2.3",
33
+ "@bedrockio/yada": "^1.2.7",
34
34
  "mongoose": "^8.6.2"
35
35
  },
36
36
  "devDependencies": {
37
- "@babel/cli": "^7.20.7",
38
- "@babel/core": "^7.20.12",
39
- "@babel/preset-env": "^7.20.2",
37
+ "@babel/cli": "^7.26.4",
38
+ "@babel/core": "^7.26.0",
39
+ "@babel/preset-env": "^7.26.0",
40
40
  "@bedrockio/prettier-config": "^1.0.2",
41
- "@bedrockio/yada": "^1.2.3",
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",
45
- "jest": "^29.4.1",
46
- "jest-environment-node": "^29.4.1",
47
- "mongodb": "^6.5.0",
48
- "mongoose": "^8.6.2",
49
- "prettier-eslint": "^15.0.1",
50
- "typescript": "^4.9.5"
45
+ "jest": "^29.7.0",
46
+ "jest-environment-node": "^29.7.0",
47
+ "mongodb": "^6.12.0",
48
+ "mongoose": "^8.8.4",
49
+ "prettier-eslint": "^16.3.0",
50
+ "typescript": "^5.7.2"
51
51
  },
52
52
  "volta": {
53
- "node": "18.14.0",
54
- "yarn": "1.22.19"
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 = Array.isArray(arg) ? arg : [arg];
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);