@bedrockio/model 0.2.17 → 0.2.19

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/src/search.js CHANGED
@@ -1,7 +1,15 @@
1
1
  import yd from '@bedrockio/yada';
2
2
  import logger from '@bedrockio/logger';
3
3
  import mongoose from 'mongoose';
4
- import { pick, isEmpty, escapeRegExp, isPlainObject } from 'lodash';
4
+ import {
5
+ get,
6
+ pick,
7
+ isEmpty,
8
+ camelCase,
9
+ upperFirst,
10
+ escapeRegExp,
11
+ isPlainObject,
12
+ } from 'lodash';
5
13
 
6
14
  import { isDateField, isNumberField, getField } from './utils';
7
15
  import { SEARCH_DEFAULTS } from './const';
@@ -11,19 +19,21 @@ import { wrapQuery } from './query';
11
19
 
12
20
  import warn from './warn';
13
21
 
22
+ const { SchemaTypes } = mongoose;
14
23
  const { ObjectId } = mongoose.Types;
15
24
 
16
25
  export function applySearch(schema, definition) {
17
26
  validateDefinition(definition);
27
+ applySearchCache(schema, definition);
18
28
 
19
29
  schema.static('search', function search(body = {}) {
20
30
  const options = {
21
31
  ...SEARCH_DEFAULTS,
22
- ...definition.search,
32
+ ...definition.search?.query,
23
33
  ...body,
24
34
  };
25
35
 
26
- const { ids, keyword, skip = 0, limit, sort, fields, ...rest } = options;
36
+ const { ids, keyword, skip = 0, limit, sort, ...rest } = options;
27
37
 
28
38
  const query = {};
29
39
 
@@ -32,7 +42,10 @@ export function applySearch(schema, definition) {
32
42
  }
33
43
 
34
44
  if (keyword) {
35
- Object.assign(query, buildKeywordQuery(schema, keyword, fields));
45
+ Object.assign(
46
+ query,
47
+ buildKeywordQuery(schema, keyword, definition.search?.fields)
48
+ );
36
49
  }
37
50
 
38
51
  Object.assign(query, normalizeQuery(rest, schema.obj));
@@ -172,7 +185,7 @@ function buildKeywordQuery(schema, keyword, fields) {
172
185
  } else if (hasTextIndex(schema)) {
173
186
  queries = [getTextQuery(keyword)];
174
187
  } else {
175
- queries = [];
188
+ throw new Error('No keyword fields defined.');
176
189
  }
177
190
 
178
191
  if (ObjectId.isValid(keyword)) {
@@ -300,3 +313,217 @@ function parseRegexQuery(str) {
300
313
  $options,
301
314
  };
302
315
  }
316
+
317
+ // Search field caching
318
+
319
+ function applySearchCache(schema, definition) {
320
+ normalizeCacheFields(schema, definition);
321
+
322
+ if (!definition.search?.cache) {
323
+ return;
324
+ }
325
+
326
+ createCacheFields(schema, definition);
327
+ applyCacheHook(schema, definition);
328
+
329
+ schema.static(
330
+ 'syncSearchFields',
331
+ async function syncSearchFields(options = {}) {
332
+ assertIncludeModule(this);
333
+
334
+ const { force } = options;
335
+ const { cache = {} } = definition.search || {};
336
+
337
+ const paths = getCachePaths(definition);
338
+
339
+ const cachedFields = Object.keys(cache);
340
+
341
+ if (!cachedFields.length) {
342
+ throw new Error('No search fields to sync.');
343
+ }
344
+
345
+ const query = {};
346
+
347
+ if (!force) {
348
+ const $or = Object.entries(cache).map((entry) => {
349
+ const [cachedField, def] = entry;
350
+ const { base } = def;
351
+ return {
352
+ [base]: {
353
+ $exists: true,
354
+ },
355
+ [cachedField]: {
356
+ $exists: false,
357
+ },
358
+ };
359
+ });
360
+ query.$or = $or;
361
+ }
362
+
363
+ const docs = await this.find(query).include(paths);
364
+
365
+ const ops = docs.map((doc) => {
366
+ return {
367
+ updateOne: {
368
+ filter: {
369
+ _id: doc._id,
370
+ },
371
+ update: {
372
+ $set: getUpdates(doc, paths, definition),
373
+ },
374
+ },
375
+ };
376
+ });
377
+
378
+ return await this.bulkWrite(ops);
379
+ }
380
+ );
381
+ }
382
+
383
+ function normalizeCacheFields(schema, definition) {
384
+ const { fields, cache = {} } = definition.search || {};
385
+ if (!fields) {
386
+ return;
387
+ }
388
+
389
+ const normalized = [];
390
+
391
+ for (let path of fields) {
392
+ if (isForeignField(schema, path)) {
393
+ const cacheName = generateCacheFieldName(path);
394
+ const type = resolveSchemaType(schema, path);
395
+ const base = getRefBase(schema, path);
396
+ cache[cacheName] = {
397
+ type,
398
+ base,
399
+ path: path,
400
+ };
401
+ normalized.push(cacheName);
402
+ } else {
403
+ normalized.push(path);
404
+ }
405
+ }
406
+
407
+ definition.search.cache = cache;
408
+ definition.search.fields = normalized;
409
+ }
410
+
411
+ function createCacheFields(schema, definition) {
412
+ for (let [cachedField, def] of Object.entries(definition.search.cache)) {
413
+ // Fall back to string type for virtuals or not defined.
414
+ const { type = 'String' } = def;
415
+ schema.add({
416
+ [cachedField]: type,
417
+ });
418
+ schema.obj[cachedField] = {
419
+ type,
420
+ readAccess: 'none',
421
+ };
422
+ }
423
+ }
424
+
425
+ function applyCacheHook(schema, definition) {
426
+ schema.pre('save', async function () {
427
+ assertIncludeModule(this.constructor);
428
+ assertAssignModule(this.constructor);
429
+
430
+ const doc = this;
431
+ const paths = getCachePaths(definition, (cachedField, def) => {
432
+ if (def.lazy) {
433
+ return !get(doc, cachedField);
434
+ } else {
435
+ return true;
436
+ }
437
+ });
438
+
439
+ await this.include(paths);
440
+ this.assign(getUpdates(this, paths, definition));
441
+ });
442
+ }
443
+
444
+ function resolveSchemaType(schema, path) {
445
+ if (!path.includes('.')) {
446
+ return get(schema.obj, path)?.type;
447
+ }
448
+ const field = getRefField(schema, path);
449
+ if (field) {
450
+ const { type, rest } = field;
451
+ const Model = mongoose.models[type.options.ref];
452
+ return resolveSchemaType(Model.schema, rest.join('.'));
453
+ }
454
+ }
455
+
456
+ function isForeignField(schema, path) {
457
+ if (!path.includes('.')) {
458
+ return false;
459
+ }
460
+ return !!getRefField(schema, path);
461
+ }
462
+
463
+ function getRefBase(schema, path) {
464
+ const field = getRefField(schema, path);
465
+ if (field) {
466
+ return field.base.join('.');
467
+ }
468
+ }
469
+
470
+ function getRefField(schema, path) {
471
+ const split = path.split('.');
472
+ for (let i = 1; i < split.length; i++) {
473
+ const base = split.slice(0, i);
474
+ const rest = split.slice(i);
475
+ const type = schema.path(base);
476
+ if (type instanceof SchemaTypes.ObjectId) {
477
+ return {
478
+ type,
479
+ base,
480
+ rest,
481
+ };
482
+ }
483
+ }
484
+ }
485
+
486
+ function getUpdates(doc, paths, definition) {
487
+ const updates = {};
488
+
489
+ const entries = Object.entries(definition.search.cache).filter((entry) => {
490
+ return paths.includes(entry[1].path);
491
+ });
492
+ for (let [cachedField, def] of entries) {
493
+ // doc.get will not return virtuals (even with specified options),
494
+ // so use lodash to ensure they are included here.
495
+ // https://mongoosejs.com/docs/api/document.html#Document.prototype.get()
496
+ updates[cachedField] = get(doc, def.path);
497
+ }
498
+ return updates;
499
+ }
500
+
501
+ function getCachePaths(definition, filter) {
502
+ filter ||= () => true;
503
+ const { cache } = definition.search || {};
504
+ return Object.entries(cache)
505
+ .filter((entry) => {
506
+ return filter(...entry);
507
+ })
508
+ .map((entry) => {
509
+ return entry[1].path;
510
+ });
511
+ }
512
+
513
+ function generateCacheFieldName(field) {
514
+ return `cached${upperFirst(camelCase(field))}`;
515
+ }
516
+
517
+ // Assertions
518
+
519
+ function assertIncludeModule(Model) {
520
+ if (!Model.schema.methods.include) {
521
+ throw new Error('Include module is required for cached search fields.');
522
+ }
523
+ }
524
+
525
+ function assertAssignModule(Model) {
526
+ if (!Model.schema.methods.assign) {
527
+ throw new Error('Assign module is required for cached search fields.');
528
+ }
529
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"delete-hooks.d.ts","sourceRoot":"","sources":["../src/delete-hooks.js"],"names":[],"mappings":"AAQA,qEAkDC"}
1
+ {"version":3,"file":"delete-hooks.d.ts","sourceRoot":"","sources":["../src/delete-hooks.js"],"names":[],"mappings":"AAQA,qEA8CC"}
package/types/load.d.ts CHANGED
@@ -4,73 +4,7 @@
4
4
  * @param {string} name
5
5
  * @returns mongoose.Model
6
6
  */
7
- export function loadModel(definition: object, name: string): mongoose.Model<any, any, any, any, any, mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, any, any, any, {
8
- [x: string]: any;
9
- }, {
10
- autoIndex?: boolean;
11
- autoCreate?: boolean;
12
- bufferCommands?: boolean;
13
- bufferTimeoutMS?: number;
14
- capped?: number | boolean | {
15
- size?: number;
16
- max?: number;
17
- autoIndexId?: boolean;
18
- };
19
- collation?: mongoose.mongo.CollationOptions;
20
- collectionOptions?: mongoose.mongo.CreateCollectionOptions;
21
- timeseries?: mongoose.mongo.TimeSeriesCollectionOptions;
22
- expireAfterSeconds?: number;
23
- expires?: string | number;
24
- collection?: string;
25
- discriminatorKey?: string;
26
- excludeIndexes?: boolean;
27
- id?: boolean;
28
- _id?: boolean;
29
- minimize?: boolean;
30
- optimisticConcurrency?: boolean;
31
- pluginTags?: string[];
32
- read?: string;
33
- writeConcern?: mongoose.mongo.WriteConcern;
34
- safe?: boolean | {
35
- w?: string | number;
36
- wtimeout?: number;
37
- j?: boolean;
38
- };
39
- shardKey?: Record<string, unknown>;
40
- strict?: boolean | "throw";
41
- strictQuery?: boolean | "throw";
42
- toJSON: {
43
- getters: boolean;
44
- versionKey: boolean;
45
- transform: (doc: any, ret: any, options: any) => void;
46
- } | mongoose.ToObjectOptions<any>;
47
- toObject: {
48
- getters: boolean;
49
- versionKey: boolean;
50
- transform: (doc: any, ret: any, options: any) => void;
51
- } | mongoose.ToObjectOptions<any>;
52
- typeKey?: string;
53
- validateBeforeSave?: boolean;
54
- validateModifiedOnly?: boolean;
55
- versionKey?: string | boolean;
56
- selectPopulatedPaths?: boolean;
57
- skipVersioning?: {
58
- [key: string]: boolean;
59
- };
60
- storeSubdocValidationError?: boolean;
61
- timestamps: boolean | mongoose.SchemaTimestampsConfig;
62
- suppressReservedKeysWarning?: boolean;
63
- statics?: {
64
- [x: string]: any;
65
- };
66
- methods?: any;
67
- query?: any;
68
- castNonArrays?: boolean;
69
- virtuals?: mongoose.SchemaOptionsVirtualsPropertyType<any, any, any>;
70
- overwriteModels?: boolean;
71
- }, any, any>> & {
72
- [x: string]: any;
73
- };
7
+ export function loadModel(definition: object, name: string): any;
74
8
  /**
75
9
  * Loads all model definitions in the given directory.
76
10
  * Returns the full loaded model set.
@@ -1 +1 @@
1
- {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,sCAJW,MAAM,QACN,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAahB;AAED;;;;GAIG;AACH,sCAFW,MAAM,mBAkBhB"}
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,sCAJW,MAAM,QACN,MAAM,OAahB;AAED;;;;GAIG;AACH,sCAFW,MAAM,mBAkBhB"}
package/types/schema.d.ts CHANGED
@@ -6,9 +6,7 @@
6
6
  * @param {mongoose.SchemaOptions} options
7
7
  * @returns mongoose.Schema
8
8
  */
9
- export function createSchema(definition: object, options?: mongoose.SchemaOptions): mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, any, any, any, {
10
- [x: string]: any;
11
- }, {
9
+ export function createSchema(definition: object, options?: mongoose.SchemaOptions): mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, any, any, any, any, {
12
10
  autoIndex?: boolean;
13
11
  autoCreate?: boolean;
14
12
  bufferCommands?: boolean;
@@ -62,10 +60,8 @@ export function createSchema(definition: object, options?: mongoose.SchemaOption
62
60
  storeSubdocValidationError?: boolean;
63
61
  timestamps: boolean | mongoose.SchemaTimestampsConfig;
64
62
  suppressReservedKeysWarning?: boolean;
65
- statics?: {
66
- [x: string]: any;
67
- };
68
- methods?: any;
63
+ statics?: mongoose.AddThisParameter<any, mongoose.Model<any, {}, {}, {}, any, any>>;
64
+ methods?: mongoose.AddThisParameter<any, any> & mongoose.AnyObject;
69
65
  query?: any;
70
66
  castNonArrays?: boolean;
71
67
  virtuals?: mongoose.SchemaOptionsVirtualsPropertyType<any, any, any>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.js"],"names":[],"mappings":"AAoBA;;;;;;;GAOG;AACH,yCAJW,MAAM,YACN,SAAS,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAuChC;AAED,iEAsBC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.js"],"names":[],"mappings":"AAoBA;;;;;;;GAOG;AACH,yCAJW,MAAM,YACN,SAAS,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAuChC;AAED,iEAsBC"}
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.js"],"names":[],"mappings":"AAeA,gEAmDC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBC"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.js"],"names":[],"mappings":"AAwBA,gEAuDC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBC"}