@bedrockio/model 0.1.0 → 0.1.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.
Files changed (73) hide show
  1. package/README.md +24 -0
  2. package/dist/cjs/access.js +4 -0
  3. package/dist/cjs/assign.js +2 -3
  4. package/dist/cjs/disallowed.js +40 -0
  5. package/dist/cjs/include.js +3 -2
  6. package/dist/cjs/load.js +15 -3
  7. package/dist/cjs/schema.js +29 -12
  8. package/dist/cjs/search.js +16 -16
  9. package/dist/cjs/serialization.js +2 -3
  10. package/dist/cjs/soft-delete.js +234 -55
  11. package/dist/cjs/testing.js +8 -0
  12. package/dist/cjs/validation.js +109 -43
  13. package/package.json +9 -7
  14. package/src/access.js +3 -0
  15. package/src/disallowed.js +63 -0
  16. package/src/include.js +1 -0
  17. package/src/load.js +12 -1
  18. package/src/schema.js +25 -5
  19. package/src/search.js +21 -10
  20. package/src/soft-delete.js +238 -85
  21. package/src/testing.js +7 -0
  22. package/src/validation.js +134 -43
  23. package/types/access.d.ts +7 -0
  24. package/types/access.d.ts.map +1 -0
  25. package/types/assign.d.ts +2 -0
  26. package/types/assign.d.ts.map +1 -0
  27. package/types/const.d.ts +9 -0
  28. package/types/const.d.ts.map +1 -0
  29. package/types/disallowed.d.ts +2 -0
  30. package/types/disallowed.d.ts.map +1 -0
  31. package/types/errors.d.ts +9 -0
  32. package/types/errors.d.ts.map +1 -0
  33. package/types/include.d.ts +4 -0
  34. package/types/include.d.ts.map +1 -0
  35. package/types/index.d.ts +6 -0
  36. package/types/index.d.ts.map +1 -0
  37. package/types/load.d.ts +15 -0
  38. package/types/load.d.ts.map +1 -0
  39. package/types/references.d.ts +2 -0
  40. package/types/references.d.ts.map +1 -0
  41. package/types/schema.d.ts +71 -0
  42. package/types/schema.d.ts.map +1 -0
  43. package/types/search.d.ts +303 -0
  44. package/types/search.d.ts.map +1 -0
  45. package/types/serialization.d.ts +6 -0
  46. package/types/serialization.d.ts.map +1 -0
  47. package/types/slug.d.ts +2 -0
  48. package/types/slug.d.ts.map +1 -0
  49. package/types/soft-delete.d.ts +4 -0
  50. package/types/soft-delete.d.ts.map +1 -0
  51. package/types/testing.d.ts +11 -0
  52. package/types/testing.d.ts.map +1 -0
  53. package/types/utils.d.ts +8 -0
  54. package/types/utils.d.ts.map +1 -0
  55. package/types/validation.d.ts +13 -0
  56. package/types/validation.d.ts.map +1 -0
  57. package/types/warn.d.ts +2 -0
  58. package/types/warn.d.ts.map +1 -0
  59. package/babel.config.cjs +0 -41
  60. package/jest.config.js +0 -8
  61. package/test/assign.test.js +0 -225
  62. package/test/definitions/custom-model.json +0 -9
  63. package/test/definitions/special-category.json +0 -18
  64. package/test/include.test.js +0 -896
  65. package/test/load.test.js +0 -47
  66. package/test/references.test.js +0 -71
  67. package/test/schema.test.js +0 -919
  68. package/test/search.test.js +0 -652
  69. package/test/serialization.test.js +0 -748
  70. package/test/setup.js +0 -27
  71. package/test/slug.test.js +0 -112
  72. package/test/soft-delete.test.js +0 -333
  73. package/test/validation.test.js +0 -1925
package/src/validation.js CHANGED
@@ -6,16 +6,40 @@ import { get, omit, lowerFirst } from 'lodash';
6
6
  import { hasAccess } from './access';
7
7
  import { searchValidation } from './search';
8
8
  import { PermissionsError } from './errors';
9
+ import { hasUniqueConstraints, assertUnique } from './soft-delete';
9
10
  import { isMongooseSchema, isSchemaTypedef } from './utils';
10
11
  import { RESERVED_FIELDS } from './schema';
11
12
 
13
+ const DATE_SCHEMA = yd.date().iso().tag({
14
+ 'x-schema': 'DateTime',
15
+ 'x-description':
16
+ 'A `string` in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format.',
17
+ });
18
+
19
+ const OBJECT_ID_DESCRIPTION = `
20
+ A 24 character hexadecimal string representing a Mongo [ObjectId](https://bit.ly/3YPtGlU).
21
+ An object with an \`id\` field may also be passed, which will be converted into a string.
22
+ `;
23
+
24
+ export const OBJECT_ID_SCHEMA = yd
25
+ .custom(async (val) => {
26
+ const id = String(val.id || val);
27
+ await namedSchemas.objectId.validate(id);
28
+ return id;
29
+ })
30
+ .tag({
31
+ type: 'ObjectId',
32
+ 'x-schema': 'ObjectId',
33
+ 'x-description': OBJECT_ID_DESCRIPTION.trim(),
34
+ });
35
+
12
36
  const namedSchemas = {
13
37
  // Email is special as we are assuming that in
14
38
  // all cases lowercase should be allowed but coerced.
15
39
  email: yd.string().lowercase().email(),
16
- // Force "ObjectId" to have parity with refs.
40
+ // Force "objectId" to have parity with refs.
17
41
  // "mongo" is notably excluded here for this reason.
18
- ObjectId: yd.string().mongo(),
42
+ objectId: yd.string().mongo(),
19
43
 
20
44
  ascii: yd.string().ascii(),
21
45
  base64: yd.string().base64(),
@@ -44,14 +68,22 @@ export function addValidators(schemas) {
44
68
  }
45
69
 
46
70
  export function applyValidation(schema, definition) {
71
+ const hasUnique = hasUniqueConstraints(schema);
72
+
47
73
  schema.static(
48
74
  'getCreateValidation',
49
75
  function getCreateValidation(appendSchema) {
50
76
  return getSchemaFromMongoose(schema, {
77
+ model: this,
51
78
  appendSchema,
52
79
  stripReserved: true,
53
80
  requireWriteAccess: true,
54
- modelName: this.modelName,
81
+ ...(hasUnique && {
82
+ assertUniqueOptions: {
83
+ schema,
84
+ operation: 'create',
85
+ },
86
+ }),
55
87
  });
56
88
  }
57
89
  );
@@ -60,12 +92,18 @@ export function applyValidation(schema, definition) {
60
92
  'getUpdateValidation',
61
93
  function getUpdateValidation(appendSchema) {
62
94
  return getSchemaFromMongoose(schema, {
95
+ model: this,
63
96
  appendSchema,
64
97
  skipRequired: true,
65
98
  stripReserved: true,
66
99
  stripUnknown: true,
67
100
  requireWriteAccess: true,
68
- modelName: this.modelName,
101
+ ...(hasUnique && {
102
+ assertUniqueOptions: {
103
+ schema,
104
+ operation: 'update',
105
+ },
106
+ }),
69
107
  });
70
108
  }
71
109
  );
@@ -74,16 +112,19 @@ export function applyValidation(schema, definition) {
74
112
  'getSearchValidation',
75
113
  function getSearchValidation(searchOptions) {
76
114
  return getSchemaFromMongoose(schema, {
77
- allowRanges: true,
115
+ allowSearch: true,
78
116
  skipRequired: true,
79
- allowMultiple: true,
80
117
  unwindArrayFields: true,
81
118
  requireReadAccess: true,
82
119
  appendSchema: searchValidation(definition, searchOptions),
83
- modelName: this.modelName,
120
+ model: this,
84
121
  });
85
122
  }
86
123
  );
124
+
125
+ schema.static('getBaseSchema', function getBaseSchema() {
126
+ return getSchemaFromMongoose(schema);
127
+ });
87
128
  }
88
129
 
89
130
  // Yada schemas
@@ -98,8 +139,16 @@ function getSchemaFromMongoose(schema, options) {
98
139
 
99
140
  // Exported for testing
100
141
  export function getValidationSchema(attributes, options = {}) {
101
- const { appendSchema } = options;
142
+ const { appendSchema, assertUniqueOptions } = options;
102
143
  let schema = getObjectSchema(attributes, options);
144
+ if (assertUniqueOptions) {
145
+ schema = yd.custom(async (obj) => {
146
+ await assertUnique(obj, {
147
+ model: options.model,
148
+ ...assertUniqueOptions,
149
+ });
150
+ });
151
+ }
103
152
  if (appendSchema) {
104
153
  schema = schema.append(appendSchema);
105
154
  }
@@ -169,7 +218,10 @@ function getSchemaForTypedef(typedef, options = {}) {
169
218
  }
170
219
  if (typedef.validate?.schema) {
171
220
  schema = schema.append(typedef.validate.schema);
221
+ } else if (typeof typedef.validate === 'function') {
222
+ schema = schema.custom(wrapMongooseValidator(typedef.validate));
172
223
  }
224
+
173
225
  if (typedef.enum) {
174
226
  schema = schema.allow(...typedef.enum);
175
227
  }
@@ -182,11 +234,8 @@ function getSchemaForTypedef(typedef, options = {}) {
182
234
  if (typedef.max != null || typedef.maxLength != null) {
183
235
  schema = schema.max(typedef.max ?? typedef.maxLength);
184
236
  }
185
- if (options.allowRanges) {
186
- schema = getRangeSchema(schema, type);
187
- }
188
- if (options.allowMultiple) {
189
- schema = yd.allow(schema, yd.array(schema));
237
+ if (options.allowSearch) {
238
+ schema = getSearchSchema(schema, type);
190
239
  }
191
240
  if (typedef.readAccess && options.requireReadAccess) {
192
241
  schema = validateReadAccess(schema, typedef.readAccess, options);
@@ -206,46 +255,79 @@ function getSchemaForType(type) {
206
255
  case 'Boolean':
207
256
  return yd.boolean();
208
257
  case 'Date':
209
- return yd.date().iso();
258
+ return DATE_SCHEMA;
210
259
  case 'Mixed':
211
260
  case 'Object':
212
261
  return yd.object();
213
262
  case 'Array':
214
263
  return yd.array();
215
264
  case 'ObjectId':
216
- return yd.custom(async (val) => {
217
- const id = String(val.id || val);
218
- await namedSchemas['ObjectId'].validate(id);
219
- return id;
220
- });
265
+ return OBJECT_ID_SCHEMA;
221
266
  default:
222
267
  throw new TypeError(`Unknown schema type ${type}`);
223
268
  }
224
269
  }
225
270
 
226
- function getRangeSchema(schema, type) {
271
+ function getSearchSchema(schema, type) {
227
272
  if (type === 'Number') {
228
- schema = yd.allow(
229
- schema,
230
- yd.object({
231
- lt: yd.number(),
232
- gt: yd.number(),
233
- lte: yd.number(),
234
- gte: yd.number(),
235
- })
236
- );
273
+ return yd
274
+ .allow(
275
+ schema,
276
+ yd.array(schema),
277
+ yd
278
+ .object({
279
+ lt: yd.number().description('Select values less than.'),
280
+ gt: yd.number().description('Select values greater than.'),
281
+ lte: yd.number().description('Select values less than or equal.'),
282
+ gte: yd
283
+ .number()
284
+ .description('Select values greater than or equal.'),
285
+ })
286
+ .tag({
287
+ 'x-schema': 'NumberRange',
288
+ 'x-description':
289
+ 'An object representing numbers falling within a range.',
290
+ })
291
+ )
292
+ .description(
293
+ 'Allows searching by a value, array of values, or a numeric range.'
294
+ );
237
295
  } else if (type === 'Date') {
238
- return yd.allow(
239
- schema,
240
- yd.object({
241
- lt: yd.date().iso(),
242
- gt: yd.date().iso(),
243
- lte: yd.date().iso(),
244
- gte: yd.date().iso(),
245
- })
246
- );
296
+ return yd
297
+ .allow(
298
+ schema,
299
+ yd.array(schema),
300
+ yd
301
+ .object({
302
+ lt: yd.date().iso().tag({
303
+ 'x-ref': 'DateTime',
304
+ description: 'Select dates occurring before.',
305
+ }),
306
+ gt: yd.date().iso().tag({
307
+ 'x-ref': 'DateTime',
308
+ description: 'Select dates occurring after.',
309
+ }),
310
+ lte: yd.date().iso().tag({
311
+ 'x-ref': 'DateTime',
312
+ description: 'Select dates occurring on or before.',
313
+ }),
314
+ gte: yd.date().iso().tag({
315
+ 'x-ref': 'DateTime',
316
+ description: 'Select dates occurring on or after.',
317
+ }),
318
+ })
319
+ .tag({
320
+ 'x-schema': 'DateRange',
321
+ 'x-description':
322
+ 'An object representing dates falling within a range.',
323
+ })
324
+ )
325
+ .description('Allows searching by a date, array of dates, or a range.');
326
+ } else if (type === 'String' || type === 'ObjectId') {
327
+ return yd.allow(schema, yd.array(schema));
328
+ } else {
329
+ return schema;
247
330
  }
248
- return schema;
249
331
  }
250
332
 
251
333
  function isRequired(typedef, options) {
@@ -270,7 +352,7 @@ function validateWriteAccess(schema, allowed, options) {
270
352
  }
271
353
 
272
354
  function validateAccess(type, schema, allowed, options) {
273
- const { modelName } = options;
355
+ const { modelName } = options.model;
274
356
  return schema.custom((val, options) => {
275
357
  const document = options[lowerFirst(modelName)] || options['document'];
276
358
  const isAllowed = hasAccess(type, allowed, {
@@ -289,14 +371,14 @@ function validateAccess(type, schema, allowed, options) {
289
371
  // Mongoose Validators
290
372
 
291
373
  export function getNamedValidator(name) {
292
- return wrapMongooseValidator(getNamedSchema(name));
374
+ return wrapSchemaAsValidator(getNamedSchema(name));
293
375
  }
294
376
 
295
377
  export function getTupleValidator(types) {
296
378
  types = types.map((type) => {
297
379
  return getSchemaForTypedef(type);
298
380
  });
299
- return wrapMongooseValidator(yd.array(types).length(types.length));
381
+ return wrapSchemaAsValidator(yd.array(types).length(types.length));
300
382
  }
301
383
 
302
384
  // Returns an async function that will error on failure.
@@ -312,7 +394,7 @@ export function getTupleValidator(types) {
312
394
  // the first style here.
313
395
  //
314
396
  // https://mongoosejs.com/docs/api/schematype.html#schematype_SchemaType-validate
315
- function wrapMongooseValidator(schema) {
397
+ function wrapSchemaAsValidator(schema) {
316
398
  const validator = async (val) => {
317
399
  await schema.validate(val);
318
400
  };
@@ -320,6 +402,15 @@ function wrapMongooseValidator(schema) {
320
402
  return validator;
321
403
  }
322
404
 
405
+ function wrapMongooseValidator(validator) {
406
+ return async (val) => {
407
+ const result = await validator(val);
408
+ if (!result && result !== undefined) {
409
+ throw new Error('Validation failed.');
410
+ }
411
+ };
412
+ }
413
+
323
414
  function getNamedSchema(name) {
324
415
  const schema = namedSchemas[name];
325
416
  if (!schema) {
@@ -0,0 +1,7 @@
1
+ export function hasReadAccess(allowed: any, options: any): boolean;
2
+ export function hasWriteAccess(allowed: any, options: any): boolean;
3
+ /**
4
+ * @param {string|string[]} allowed
5
+ */
6
+ export function hasAccess(type: any, allowed?: string | string[], options?: {}): boolean;
7
+ //# sourceMappingURL=access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.js"],"names":[],"mappings":"AAGA,mEAEC;AAED,oEAEC;AAED;;GAEG;AACH,+CAFW,MAAM,GAAC,MAAM,EAAE,yBA4BzB"}
@@ -0,0 +1,2 @@
1
+ export function applyAssign(schema: any): void;
2
+ //# sourceMappingURL=assign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assign.d.ts","sourceRoot":"","sources":["../src/assign.js"],"names":[],"mappings":"AAIA,+CAYC"}
@@ -0,0 +1,9 @@
1
+ export namespace SEARCH_DEFAULTS {
2
+ const limit: number;
3
+ namespace sort {
4
+ const field: string;
5
+ const order: string;
6
+ }
7
+ }
8
+ export const POPULATE_MAX_DEPTH: 5;
9
+ //# sourceMappingURL=const.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../src/const.js"],"names":[],"mappings":";;;;;;;AAQA,mCAAoC"}
@@ -0,0 +1,2 @@
1
+ export function applyDisallowed(schema: any): void;
2
+ //# sourceMappingURL=disallowed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disallowed.d.ts","sourceRoot":"","sources":["../src/disallowed.js"],"names":[],"mappings":"AAEA,mDA4DC"}
@@ -0,0 +1,9 @@
1
+ export class PermissionsError extends Error {
2
+ }
3
+ export class ImplementationError extends Error {
4
+ }
5
+ export class ReferenceError extends Error {
6
+ constructor(message: any, references: any);
7
+ references: any;
8
+ }
9
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.js"],"names":[],"mappings":"AAAA;CAA8C;AAC9C;CAAiD;AAEjD;IACE,2CAGC;IADC,gBAA4B;CAE/B"}
@@ -0,0 +1,4 @@
1
+ export function applyInclude(schema: any): void;
2
+ export function checkSelects(doc: any, ret: any): void;
3
+ export function getIncludes(modelName: any, arg: any): any;
4
+ //# sourceMappingURL=include.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../src/include.js"],"names":[],"mappings":"AAeA,gDAoCC;AAMD,uDA4BC;AAGD,2DAIC"}
@@ -0,0 +1,6 @@
1
+ export { createSchema } from "./schema";
2
+ export { addValidators } from "./validation";
3
+ export * from "./testing";
4
+ export * from "./errors";
5
+ export { loadModel, loadModelDir } from "./load";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Loads a single model by definition and name.
3
+ * @param {object} definition
4
+ * @param {string} name
5
+ * @returns mongoose.Model
6
+ */
7
+ export function loadModel(definition: object, name: string): any;
8
+ /**
9
+ * Loads all model definitions in the given directory.
10
+ * Returns the full loaded model set.
11
+ * @param {string} dirPath
12
+ */
13
+ export function loadModelDir(dirPath: string): mongoose.Models;
14
+ import mongoose from "mongoose";
15
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,2 @@
1
+ export function applyReferences(schema: any): void;
2
+ //# sourceMappingURL=references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"references.d.ts","sourceRoot":"","sources":["../src/references.js"],"names":[],"mappings":"AAMA,mDAoDC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Creates a new Mongoose schema with Bedrock extensions
3
+ * applied. For more about syntax and functionality see
4
+ * [the documentation](https://github.com/bedrockio/model#schemas).
5
+ * @param {object} definition
6
+ * @param {mongoose.SchemaOptions} options
7
+ * @returns mongoose.Schema
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
+ 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?: import("mongodb").CollationOptions;
20
+ timeseries?: import("mongodb").TimeSeriesCollectionOptions;
21
+ expireAfterSeconds?: number;
22
+ expires?: string | number;
23
+ collection?: string;
24
+ discriminatorKey?: string;
25
+ excludeIndexes?: boolean;
26
+ id?: boolean;
27
+ _id?: boolean;
28
+ minimize?: boolean;
29
+ optimisticConcurrency?: boolean;
30
+ pluginTags?: string[];
31
+ read?: string;
32
+ writeConcern?: import("mongodb").WriteConcern;
33
+ safe?: boolean | {
34
+ w?: string | number;
35
+ wtimeout?: number;
36
+ j?: boolean;
37
+ };
38
+ shardKey?: Record<string, unknown>;
39
+ strict?: boolean | "throw";
40
+ strictQuery?: boolean | "throw";
41
+ toJSON: {
42
+ getters: boolean;
43
+ versionKey: boolean;
44
+ transform: (doc: any, ret: any, options: any) => void;
45
+ } | mongoose.ToObjectOptions;
46
+ toObject: {
47
+ getters: boolean;
48
+ versionKey: boolean;
49
+ transform: (doc: any, ret: any, options: any) => void;
50
+ } | mongoose.ToObjectOptions;
51
+ typeKey?: string;
52
+ validateBeforeSave?: boolean;
53
+ versionKey?: string | boolean;
54
+ selectPopulatedPaths?: boolean;
55
+ skipVersioning?: {
56
+ [key: string]: boolean;
57
+ };
58
+ storeSubdocValidationError?: boolean;
59
+ timestamps: boolean | mongoose.SchemaTimestampsConfig;
60
+ supressReservedKeysWarning?: boolean;
61
+ statics?: any;
62
+ methods?: any;
63
+ query?: any;
64
+ castNonArrays?: boolean;
65
+ virtuals?: mongoose.SchemaOptionsVirtualsPropertyType<any, any, any>;
66
+ overwriteModels?: boolean;
67
+ }, any>;
68
+ export function normalizeAttributes(arg: any, path?: any[]): any;
69
+ export const RESERVED_FIELDS: string[];
70
+ import mongoose from "mongoose";
71
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.js"],"names":[],"mappings":"AA2BA;;;;;;;GAOG;AACH,yCAJW,MAAM,YACN,SAAS,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAoChC;AAED,iEAuBC;AAzED,uCAKE"}