@bedrockio/model 0.1.0 → 0.1.2

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 +110 -44
  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 +135 -44
  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/README.md CHANGED
@@ -378,6 +378,30 @@ Due to ambiguity with the soft delete module, the following methods will throw a
378
378
  - `Model.findOneAndRemove` - Use `Model.findOneAndDelete` instead.
379
379
  - `Model.findByIdAndRemove` - Use `Model.findByIdAndDelete` instead.
380
380
 
381
+ #### Unique Constraints
382
+
383
+ Note that although monogoose allows a `unique` option on fields, this will add a unique index to the mongo collection itself which is incompatible with soft deletion.
384
+
385
+ This package will intercept `unique: true` to create a soft delete compatible validation which will:
386
+
387
+ - Throw an error if other non-deleted documents with the same fields exist when calling:
388
+ - `Document.save`
389
+ - `Document.update`
390
+ - `Document.restore`
391
+ - `Model.updateOne` (see note below)
392
+ - `Model.updateMany` (see note below)
393
+ - `Model.restoreOne`
394
+ - `Model.restoreMany`
395
+ - `Model.insertMany`
396
+ - `Model.replaceOne`
397
+ - Append the same validation to `Model.getCreateSchema` and `Model.getUpdateSchema` to allow this constraint to trickle down to the API.
398
+
399
+ > :warning: updateOne and updateMany
400
+ >
401
+ > Note that calling `Model.updateOne` will throw an error when a unique field exists on any document **including the document being updated**. This is an intentional constraint that allows `updateOne` better peformance by not having to fetch the ids of the documents being updated in order to exclude them. To avoid this call `Document.save` instead.
402
+ >
403
+ > Note also that calling `Model.updateMany` with a unique field passed will always throw an error as the result would inherently be non-unique.
404
+
381
405
  ### Validation
382
406
 
383
407
  Models are extended with methods that allow complex validation that derives from the schema. Bedrock validation is generally used at the API level:
@@ -15,6 +15,10 @@ function hasReadAccess(allowed, options) {
15
15
  function hasWriteAccess(allowed, options) {
16
16
  return hasAccess('write', allowed, options);
17
17
  }
18
+
19
+ /**
20
+ * @param {string|string[]} allowed
21
+ */
18
22
  function hasAccess(type, allowed = 'all', options = {}) {
19
23
  if (allowed === 'all') {
20
24
  return true;
@@ -4,9 +4,8 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.applyAssign = applyAssign;
7
- var _isPlainObject2 = _interopRequireDefault(require("lodash/isPlainObject"));
7
+ var _lodash = require("lodash");
8
8
  var _utils = require("./utils");
9
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
9
  function applyAssign(schema) {
11
10
  schema.method('assign', function assign(fields) {
12
11
  unsetReferenceFields(fields, schema.obj);
@@ -40,7 +39,7 @@ function unsetReferenceFields(fields, schema = {}) {
40
39
  function flattenObject(obj, root = {}, rootPath = []) {
41
40
  for (let [key, val] of Object.entries(obj)) {
42
41
  const path = [...rootPath, key];
43
- if ((0, _isPlainObject2.default)(val)) {
42
+ if ((0, _lodash.isPlainObject)(val)) {
44
43
  flattenObject(val, root, path);
45
44
  } else {
46
45
  root[path.join('.')] = val;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.applyDisallowed = applyDisallowed;
7
+ var _warn = _interopRequireDefault(require("./warn"));
8
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
9
+ function applyDisallowed(schema) {
10
+ schema.method('remove', function () {
11
+ (0, _warn.default)('The "remove" method on documents is disallowed due to ambiguity.', 'To permanently delete a document use "destroy", otherwise "delete".');
12
+ throw new Error('Method not allowed.');
13
+ }, {
14
+ suppressWarning: true
15
+ });
16
+ schema.method('update', function () {
17
+ (0, _warn.default)('The "update" method on documents is deprecated. Use "updateOne" instead.');
18
+ throw new Error('Method not allowed.');
19
+ });
20
+ schema.method('deleteOne', function () {
21
+ (0, _warn.default)('The "deleteOne" method on documents is disallowed due to ambiguity', 'Use either "delete" or "deleteOne" on the model.');
22
+ throw new Error('Method not allowed.');
23
+ });
24
+ schema.static('remove', function () {
25
+ (0, _warn.default)('The "remove" method on models is disallowed due to ambiguity.', 'To permanently delete a document use "destroyMany", otherwise "deleteMany".');
26
+ throw new Error('Method not allowed.');
27
+ });
28
+ schema.static('findOneAndRemove', function () {
29
+ (0, _warn.default)('The "findOneAndRemove" method on models is disallowed due to ambiguity.', 'To permanently delete a document use "findOneAndDestroy", otherwise "findOneAndDelete".');
30
+ throw new Error('Method not allowed.');
31
+ });
32
+ schema.static('findByIdAndRemove', function () {
33
+ (0, _warn.default)('The "findByIdAndRemove" method on models is disallowed due to ambiguity.', 'To permanently delete a document use "findByIdAndDestroy", otherwise "findByIdAndDelete".');
34
+ throw new Error('Method not allowed.');
35
+ });
36
+ schema.static('count', function () {
37
+ (0, _warn.default)('The "count" method on models is deprecated. Use "countDocuments" instead.');
38
+ throw new Error('Method not allowed.');
39
+ });
40
+ }
@@ -6,11 +6,12 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.applyInclude = applyInclude;
7
7
  exports.checkSelects = checkSelects;
8
8
  exports.getIncludes = getIncludes;
9
- var _escapeRegExp2 = _interopRequireDefault(require("lodash/escapeRegExp"));
10
9
  var _mongoose = _interopRequireDefault(require("mongoose"));
10
+ var _lodash = require("lodash");
11
11
  var _utils = require("./utils");
12
12
  var _const = require("./const");
13
13
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
+ // @ts-ignore
14
15
  // Overloading mongoose Query prototype to
15
16
  // allow an "include" method for queries.
16
17
  _mongoose.default.Query.prototype.include = function include(paths) {
@@ -205,7 +206,7 @@ function setNodePath(node, options) {
205
206
  function resolvePaths(schema, str) {
206
207
  let paths;
207
208
  if (str.includes('*')) {
208
- let source = (0, _escapeRegExp2.default)(str);
209
+ let source = (0, _lodash.escapeRegExp)(str);
209
210
  source = source.replaceAll('\\*\\*', '.+');
210
211
  source = source.replaceAll('\\*', '[^.]+');
211
212
  source = `^${source}$`;
package/dist/cjs/load.js CHANGED
@@ -5,12 +5,18 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.loadModel = loadModel;
7
7
  exports.loadModelDir = loadModelDir;
8
- var _startCase2 = _interopRequireDefault(require("lodash/startCase"));
9
8
  var _fs = _interopRequireDefault(require("fs"));
10
9
  var _path = _interopRequireDefault(require("path"));
11
10
  var _mongoose = _interopRequireDefault(require("mongoose"));
11
+ var _lodash = require("lodash");
12
12
  var _schema = require("./schema");
13
13
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
+ /**
15
+ * Loads a single model by definition and name.
16
+ * @param {object} definition
17
+ * @param {string} name
18
+ * @returns mongoose.Model
19
+ */
14
20
  function loadModel(definition, name) {
15
21
  if (!definition.attributes) {
16
22
  throw new Error(`Invalid model definition for ${name}, need attributes`);
@@ -22,15 +28,21 @@ function loadModel(definition, name) {
22
28
  throw new Error(`${err.message} (loading ${name})`);
23
29
  }
24
30
  }
31
+
32
+ /**
33
+ * Loads all model definitions in the given directory.
34
+ * Returns the full loaded model set.
35
+ * @param {string} dirPath
36
+ */
25
37
  function loadModelDir(dirPath) {
26
38
  const files = _fs.default.readdirSync(dirPath);
27
39
  for (const file of files) {
28
40
  const basename = _path.default.basename(file, '.json');
29
41
  if (file.match(/\.json$/)) {
30
42
  const filePath = _path.default.join(dirPath, file);
31
- const data = _fs.default.readFileSync(filePath);
43
+ const data = _fs.default.readFileSync(filePath, 'utf-8');
32
44
  const definition = JSON.parse(data);
33
- const modelName = definition.modelName || (0, _startCase2.default)(basename).replace(/\s/g, '');
45
+ const modelName = definition.modelName || (0, _lodash.startCase)(basename).replace(/\s/g, '');
34
46
  if (!_mongoose.default.models[modelName]) {
35
47
  loadModel(definition, modelName);
36
48
  }
@@ -6,11 +6,8 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.RESERVED_FIELDS = void 0;
7
7
  exports.createSchema = createSchema;
8
8
  exports.normalizeAttributes = normalizeAttributes;
9
- var _camelCase2 = _interopRequireDefault(require("lodash/camelCase"));
10
- var _capitalize2 = _interopRequireDefault(require("lodash/capitalize"));
11
- var _isPlainObject2 = _interopRequireDefault(require("lodash/isPlainObject"));
12
- var _pick2 = _interopRequireDefault(require("lodash/pick"));
13
9
  var _mongoose = _interopRequireDefault(require("mongoose"));
10
+ var _lodash = require("lodash");
14
11
  var _utils = require("./utils");
15
12
  var _serialization = require("./serialization");
16
13
  var _slug = require("./slug");
@@ -19,9 +16,19 @@ var _assign = require("./assign");
19
16
  var _include = require("./include");
20
17
  var _references = require("./references");
21
18
  var _softDelete = require("./soft-delete");
19
+ var _disallowed = require("./disallowed");
22
20
  var _validation = require("./validation");
23
21
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
24
22
  const RESERVED_FIELDS = ['createdAt', 'updatedAt', 'deletedAt', 'deleted'];
23
+
24
+ /**
25
+ * Creates a new Mongoose schema with Bedrock extensions
26
+ * applied. For more about syntax and functionality see
27
+ * [the documentation](https://github.com/bedrockio/model#schemas).
28
+ * @param {object} definition
29
+ * @param {mongoose.SchemaOptions} options
30
+ * @returns mongoose.Schema
31
+ */
25
32
  exports.RESERVED_FIELDS = RESERVED_FIELDS;
26
33
  function createSchema(definition, options = {}) {
27
34
  const schema = new _mongoose.default.Schema(attributesToMongoose(normalizeAttributes({
@@ -42,13 +49,14 @@ function createSchema(definition, options = {}) {
42
49
  toObject: _serialization.serializeOptions,
43
50
  ...options
44
51
  });
45
- (0, _softDelete.applySoftDelete)(schema, definition);
46
52
  (0, _validation.applyValidation)(schema, definition);
47
- (0, _references.applyReferences)(schema, definition);
48
- (0, _include.applyInclude)(schema, definition);
49
53
  (0, _search.applySearch)(schema, definition);
50
- (0, _assign.applyAssign)(schema, definition);
51
- (0, _slug.applySlug)(schema, definition);
54
+ (0, _softDelete.applySoftDelete)(schema);
55
+ (0, _references.applyReferences)(schema);
56
+ (0, _disallowed.applyDisallowed)(schema);
57
+ (0, _include.applyInclude)(schema);
58
+ (0, _assign.applyAssign)(schema);
59
+ (0, _slug.applySlug)(schema);
52
60
  return schema;
53
61
  }
54
62
  function normalizeAttributes(arg, path = []) {
@@ -115,7 +123,7 @@ function attributesToMongoose(attributes) {
115
123
  // Allow custom mongoose validation function that derives from the schema.
116
124
  val = (0, _validation.getNamedValidator)(val);
117
125
  }
118
- } else if ((0, _isPlainObject2.default)(val)) {
126
+ } else if ((0, _lodash.isPlainObject)(val)) {
119
127
  if (isScopeExtension(val)) {
120
128
  applyScopeExtension(val, definition);
121
129
  continue;
@@ -160,7 +168,7 @@ function assertRefs(field, path) {
160
168
  }
161
169
  }
162
170
  function camelUpper(str) {
163
- return (0, _capitalize2.default)((0, _camelCase2.default)(str));
171
+ return (0, _lodash.capitalize)((0, _lodash.camelCase)(str));
164
172
  }
165
173
  function isObjectIdType(type) {
166
174
  return type === 'ObjectId' || type === _mongoose.default.Schema.Types.ObjectId;
@@ -170,6 +178,7 @@ function isMongooseType(type) {
170
178
  }
171
179
  function applyExtensions(typedef) {
172
180
  applySyntaxExtensions(typedef);
181
+ applyUniqueExtension(typedef);
173
182
  applyTupleExtension(typedef);
174
183
  }
175
184
  function applySyntaxExtensions(typedef) {
@@ -192,7 +201,7 @@ function applySyntaxExtensions(typedef) {
192
201
  // Hoist read/write scopes from a nested element.
193
202
  // See the readme for more.
194
203
  function applyOptionHoisting(typedef) {
195
- Object.assign(typedef, (0, _pick2.default)(typedef.type[0], 'readAccess', 'writeAccess'));
204
+ Object.assign(typedef, (0, _lodash.pick)(typedef.type[0], 'readAccess', 'writeAccess'));
196
205
  }
197
206
  function isExtendedSyntax(typedef) {
198
207
  const {
@@ -228,6 +237,14 @@ function applyTupleExtension(typedef) {
228
237
  typedef.validate = (0, _validation.getTupleValidator)(type);
229
238
  }
230
239
  }
240
+
241
+ // Intercepts "unique" options and changes to "softUnique".
242
+ function applyUniqueExtension(typedef) {
243
+ if (typedef.unique === true) {
244
+ typedef.softUnique = true;
245
+ delete typedef.unique;
246
+ }
247
+ }
231
248
  function applyArrayValidators(typedef) {
232
249
  let {
233
250
  minLength,
@@ -5,13 +5,12 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.applySearch = applySearch;
7
7
  exports.searchValidation = searchValidation;
8
- var _isPlainObject2 = _interopRequireDefault(require("lodash/isPlainObject"));
9
- var _isEmpty2 = _interopRequireDefault(require("lodash/isEmpty"));
10
- var _pick2 = _interopRequireDefault(require("lodash/pick"));
11
8
  var _yada = _interopRequireDefault(require("@bedrockio/yada"));
12
9
  var _mongoose = _interopRequireDefault(require("mongoose"));
10
+ var _lodash = require("lodash");
13
11
  var _utils = require("./utils");
14
12
  var _const = require("./const");
13
+ var _validation = require("./validation");
15
14
  var _warn = _interopRequireDefault(require("./warn"));
16
15
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
16
  const {
@@ -20,7 +19,7 @@ const {
20
19
  const SORT_SCHEMA = _yada.default.object({
21
20
  field: _yada.default.string().required(),
22
21
  order: _yada.default.string().allow('desc', 'asc').required()
23
- });
22
+ }).description('An object describing the sort order of results.');
24
23
  function applySearch(schema, definition) {
25
24
  validateDefinition(definition);
26
25
  schema.static('search', function search(body = {}) {
@@ -75,22 +74,23 @@ function applySearch(schema, definition) {
75
74
  });
76
75
  }
77
76
  function searchValidation(definition, options = {}) {
77
+ options = {
78
+ ..._const.SEARCH_DEFAULTS,
79
+ ...(0, _lodash.pick)(definition.search, 'limit', 'sort'),
80
+ ...options
81
+ };
78
82
  const {
79
83
  limit,
80
84
  sort,
81
85
  ...rest
82
- } = {
83
- ..._const.SEARCH_DEFAULTS,
84
- ...(0, _pick2.default)(definition.search, 'limit', 'sort'),
85
- ...options
86
- };
86
+ } = options;
87
87
  return {
88
- ids: _yada.default.array(_yada.default.string().mongo()),
89
- keyword: _yada.default.string(),
90
- include: _yada.default.string(),
91
- skip: _yada.default.number().default(0),
88
+ ids: _yada.default.array(_validation.OBJECT_ID_SCHEMA),
89
+ keyword: _yada.default.string().description('A keyword to perform a text search against.'),
90
+ include: _yada.default.string().description('Fields to be selected or populated.'),
91
+ skip: _yada.default.number().default(0).description('Number of records to skip.'),
92
92
  sort: _yada.default.allow(SORT_SCHEMA, _yada.default.array(SORT_SCHEMA)).default(sort),
93
- limit: _yada.default.number().positive().default(limit),
93
+ limit: _yada.default.number().positive().default(limit).description('Limits the number of results.'),
94
94
  ...rest
95
95
  };
96
96
  }
@@ -202,7 +202,7 @@ function normalizeQuery(query, schema, root = {}, rootPath = []) {
202
202
  for (let [key, value] of Object.entries(query)) {
203
203
  const path = [...rootPath, key];
204
204
  if (isRangeQuery(schema, key, value)) {
205
- if (!(0, _isEmpty2.default)(value)) {
205
+ if (!(0, _lodash.isEmpty)(value)) {
206
206
  root[path.join('.')] = mapOperatorQuery(value);
207
207
  }
208
208
  } else if (isNestedQuery(key, value)) {
@@ -220,7 +220,7 @@ function normalizeQuery(query, schema, root = {}, rootPath = []) {
220
220
  return root;
221
221
  }
222
222
  function isNestedQuery(key, value) {
223
- if (isMongoOperator(key) || !(0, _isPlainObject2.default)(value)) {
223
+ if (isMongoOperator(key) || !(0, _lodash.isPlainObject)(value)) {
224
224
  return false;
225
225
  }
226
226
  return Object.keys(value).every(key => {
@@ -4,11 +4,10 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.serializeOptions = void 0;
7
- var _isPlainObject2 = _interopRequireDefault(require("lodash/isPlainObject"));
7
+ var _lodash = require("lodash");
8
8
  var _include = require("./include");
9
9
  var _access = require("./access");
10
10
  var _utils = require("./utils");
11
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
11
  const serializeOptions = {
13
12
  getters: true,
14
13
  versionKey: false,
@@ -24,7 +23,7 @@ function transformField(obj, field, options) {
24
23
  for (let el of obj) {
25
24
  transformField(el, field, options);
26
25
  }
27
- } else if ((0, _isPlainObject2.default)(obj)) {
26
+ } else if ((0, _lodash.isPlainObject)(obj)) {
28
27
  for (let [key, val] of Object.entries(obj)) {
29
28
  if (!isAllowedField(key, field, options)) {
30
29
  delete obj[key];