@bedrockio/model 0.1.32 → 0.2.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/README.md CHANGED
@@ -16,8 +16,8 @@ Bedrock utilities for model creation.
16
16
  - [Validation](#validation)
17
17
  - [Search](#search)
18
18
  - [Includes](#includes)
19
+ - [Delete Hooks](#delete-hooks)
19
20
  - [Access Control](#access-control)
20
- - [References](#references)
21
21
  - [Assign](#assign)
22
22
  - [Slugs](#slugs)
23
23
  - [Testing](#testing)
@@ -39,14 +39,18 @@ yarn install @bedrockio/yada
39
39
 
40
40
  ## Usage
41
41
 
42
- Bedrock models are defined as flat JSON files to allow static analysis and inspection. They can be further extended to allow more functionality. The most straightforward way to load models is to use `loadModelDir` that points to the directory where JSON definitions exist:
42
+ Bedrock models are defined as flat JSON files to allow static analysis and
43
+ inspection. They can be further extended to allow more functionality. The most
44
+ straightforward way to load models is to use `loadModelDir` that points to the
45
+ directory where JSON definitions exist:
43
46
 
44
47
  ```js
45
48
  const { loadModelDir } = require('@bedrockio/model');
46
49
  model.exports = loadModelDir('path/to/definitions/');
47
50
  ```
48
51
 
49
- Models that need to be extended can use the `createSchema` method with the definition and add to the schema as needed:
52
+ Models that need to be extended can use the `createSchema` method with the
53
+ definition and add to the schema as needed:
50
54
 
51
55
  ```js
52
56
  const mongoose = require('mongoose');
@@ -73,7 +77,8 @@ model.exports = {
73
77
 
74
78
  ## Schemas
75
79
 
76
- The `attributes` field of model definitions can be considered equivalent to Mongoose, but defined in JSON with extended features:
80
+ The `attributes` field of model definitions can be considered equivalent to
81
+ Mongoose, but defined in JSON with extended features:
77
82
 
78
83
  ```js
79
84
  {
@@ -117,7 +122,8 @@ Links:
117
122
 
118
123
  ### Schema Extensions
119
124
 
120
- This package provides a number of extensions to assist schema creation outside the scope of Mongoose.
125
+ This package provides a number of extensions to assist schema creation outside
126
+ the scope of Mongoose.
121
127
 
122
128
  #### Attributes
123
129
 
@@ -132,7 +138,8 @@ Objects are easily defined with their attributes directly on the field:
132
138
  };
133
139
  ```
134
140
 
135
- However it is common to need to add an option like `required` to an object schema. In Mongoose this is technically written as:
141
+ However it is common to need to add an option like `required` to an object
142
+ schema. In Mongoose this is technically written as:
136
143
 
137
144
  ```js
138
145
  {
@@ -146,7 +153,8 @@ However it is common to need to add an option like `required` to an object schem
146
153
  };
147
154
  ```
148
155
 
149
- However in complex cases this can be obtuse and difficult to remember. A more explicit syntax is allowed here:
156
+ However in complex cases this can be obtuse and difficult to remember. A more
157
+ explicit syntax is allowed here:
150
158
 
151
159
  ```js
152
160
  {
@@ -161,7 +169,9 @@ However in complex cases this can be obtuse and difficult to remember. A more ex
161
169
  };
162
170
  ```
163
171
 
164
- The type `Object` and `attributes` is a signal to create the correct schema for the above type. This can also be similarly used for `Array` for an array of objects:
172
+ The type `Object` and `attributes` is a signal to create the correct schema for
173
+ the above type. This can also be similarly used for `Array` for an array of
174
+ objects:
165
175
 
166
176
  ```js
167
177
  {
@@ -176,7 +186,8 @@ The type `Object` and `attributes` is a signal to create the correct schema for
176
186
  };
177
187
  ```
178
188
 
179
- In the above example the `writeAccess` applies to the array itself, not individual fields. Note that for an array of primitives the correct syntax is:
189
+ In the above example the `writeAccess` applies to the array itself, not
190
+ individual fields. Note that for an array of primitives the correct syntax is:
180
191
 
181
192
  ```js
182
193
  {
@@ -189,7 +200,8 @@ In the above example the `writeAccess` applies to the array itself, not individu
189
200
 
190
201
  #### Scopes
191
202
 
192
- One common need is to define multiple fields with the same options. A custom type `Scope` helps make this possible:
203
+ One common need is to define multiple fields with the same options. A custom
204
+ type `Scope` helps make this possible:
193
205
 
194
206
  ```js
195
207
  {
@@ -222,11 +234,13 @@ This syntax expands into the following:
222
234
  };
223
235
  ```
224
236
 
225
- Note that the name `$private` is arbitrary. The `$` helps distinguish it from real fields, but it can be anything as the property is removed.
237
+ Note that the name `$private` is arbitrary. The `$` helps distinguish it from
238
+ real fields, but it can be anything as the property is removed.
226
239
 
227
240
  #### Tuples
228
241
 
229
- Array fields that have more than one element are considered a "tuple". They will enforce an exact length and specific type for each element.
242
+ Array fields that have more than one element are considered a "tuple". They will
243
+ enforce an exact length and specific type for each element.
230
244
 
231
245
  ```js
232
246
  {
@@ -245,13 +259,17 @@ This will map to the following:
245
259
  }
246
260
  ```
247
261
 
248
- Where `validator` is a special validator that enforces both the exact array length and content types.
262
+ Where `validator` is a special validator that enforces both the exact array
263
+ length and content types.
249
264
 
250
- Note that Mongoose [does not provide a way to enforce array elements of specific mixed types](https://github.com/Automattic/mongoose/issues/10894), requiring the `Mixed` type instead.
265
+ Note that Mongoose
266
+ [does not provide a way to enforce array elements of specific mixed types](https://github.com/Automattic/mongoose/issues/10894),
267
+ requiring the `Mixed` type instead.
251
268
 
252
269
  #### Array Extensions
253
270
 
254
- A common need is to validate the length of an array or make it required by enforcing a minimum length of 1. However this does not exist in Mongoose:
271
+ A common need is to validate the length of an array or make it required by
272
+ enforcing a minimum length of 1. However this does not exist in Mongoose:
255
273
 
256
274
  ```js
257
275
  {
@@ -262,7 +280,10 @@ A common need is to validate the length of an array or make it required by enfor
262
280
  };
263
281
  ```
264
282
 
265
- The above syntax will not do anything as the default for arrays is always `[]` so the field will always exist. It also suffers from being ambiguous (is the array required or the elements inside?). An extension is provided here for explicit handling of this case:
283
+ The above syntax will not do anything as the default for arrays is always `[]`
284
+ so the field will always exist. It also suffers from being ambiguous (is the
285
+ array required or the elements inside?). An extension is provided here for
286
+ explicit handling of this case:
266
287
 
267
288
  ```js
268
289
  {
@@ -274,7 +295,8 @@ The above syntax will not do anything as the default for arrays is always `[]` s
274
295
  };
275
296
  ```
276
297
 
277
- A custom validator will be created to enforce the array length, bringing parity with `minLength` and `maxLength` on strings.
298
+ A custom validator will be created to enforce the array length, bringing parity
299
+ with `minLength` and `maxLength` on strings.
278
300
 
279
301
  ### Gotchas
280
302
 
@@ -289,7 +311,8 @@ A custom validator will be created to enforce the array length, bringing parity
289
311
  }
290
312
  ```
291
313
 
292
- Given the above schema, let's say you want to add a default. The appropriate schema would be:
314
+ Given the above schema, let's say you want to add a default. The appropriate
315
+ schema would be:
293
316
 
294
317
  ```js
295
318
  {
@@ -306,7 +329,10 @@ Given the above schema, let's say you want to add a default. The appropriate sch
306
329
  }
307
330
  ```
308
331
 
309
- However this is not a valid definition in Mongoose, which instead sees `type` and `default` as individual fields. A type definition and object schema unfortunately cannot be disambiguated in this case. [Syntax extentsions](#syntax-extensions) provides an escape hatch here:
332
+ However this is not a valid definition in Mongoose, which instead sees `type`
333
+ and `default` as individual fields. A type definition and object schema
334
+ unfortunately cannot be disambiguated in this case.
335
+ [Syntax extentsions](#syntax-extensions) provides an escape hatch here:
310
336
 
311
337
  ```js
312
338
  {
@@ -330,7 +356,10 @@ This will manually create a new nested subschema.
330
356
 
331
357
  ### Soft Delete
332
358
 
333
- The soft delete module ensures that no documents are permanently deleted by default and provides helpful methods to query on and restore deleted documents. "Soft deletion" means that deleted documents have the properties `deleted` and `deletedAt`.
359
+ The soft delete module ensures that no documents are permanently deleted by
360
+ default and provides helpful methods to query on and restore deleted documents.
361
+ "Soft deletion" means that deleted documents have the properties `deleted` and
362
+ `deletedAt`.
334
363
 
335
364
  #### Instance Methods
336
365
 
@@ -345,7 +374,8 @@ The soft delete module ensures that no documents are permanently deleted by defa
345
374
  - `restoreOne` - Restores a single document.
346
375
  - `restoreMany` - Restores multiple documents.
347
376
  - `destroyOne` - Permanently deletes a single document.
348
- - `destroyMany` - Permanently deletes multiple documents. Be careful with this one.
377
+ - `destroyMany` - Permanently deletes multiple documents. Be careful with this
378
+ one.
349
379
 
350
380
  #### Query Deleted Documents
351
381
 
@@ -365,12 +395,17 @@ The soft delete module ensures that no documents are permanently deleted by defa
365
395
 
366
396
  #### Other Static Methods
367
397
 
368
- - `findOneAndDelete` - The soft equivalent of the [Mongoose method](https://mongoosejs.com/docs/api/model.html#model_Model-findOneAndDelete). Fetches the current data before deleting and returns the document.
369
- - `findByIdAndDelete` - The soft equivalent of the [Mongoose method](https://mongoosejs.com/docs/api/model.html#model_Model-findByIdAndDelete). Fetches the current data before deleting and returns the document.
398
+ - `findOneAndDelete` - The soft equivalent of the
399
+ [Mongoose method](https://mongoosejs.com/docs/api/model.html#model_Model-findOneAndDelete).
400
+ Fetches the current data before deleting and returns the document.
401
+ - `findByIdAndDelete` - The soft equivalent of the
402
+ [Mongoose method](https://mongoosejs.com/docs/api/model.html#model_Model-findByIdAndDelete).
403
+ Fetches the current data before deleting and returns the document.
370
404
 
371
405
  #### Disallowed Methods
372
406
 
373
- Due to ambiguity with the soft delete module, the following methods will throw an error:
407
+ Due to ambiguity with the soft delete module, the following methods will throw
408
+ an error:
374
409
 
375
410
  - `Document.remove` - Use `Document.delete` or `Document.destroy` instead.
376
411
  - `Document.deleteOne` - Use `Document.delete` or `Model.deleteOne` instead.
@@ -380,11 +415,15 @@ Due to ambiguity with the soft delete module, the following methods will throw a
380
415
 
381
416
  #### Unique Constraints
382
417
 
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.
418
+ Note that although monogoose allows a `unique` option on fields, this will add a
419
+ unique index to the mongo collection itself which is incompatible with soft
420
+ deletion.
384
421
 
385
- This package will intercept `unique: true` to create a soft delete compatible validation which will:
422
+ This package will intercept `unique: true` to create a soft delete compatible
423
+ validation which will:
386
424
 
387
- - Throw an error if other non-deleted documents with the same fields exist when calling:
425
+ - Throw an error if other non-deleted documents with the same fields exist when
426
+ calling:
388
427
  - `Document.save`
389
428
  - `Document.update`
390
429
  - `Document.restore`
@@ -394,17 +433,24 @@ This package will intercept `unique: true` to create a soft delete compatible va
394
433
  - `Model.restoreMany`
395
434
  - `Model.insertMany`
396
435
  - `Model.replaceOne`
397
- - Append the same validation to `Model.getCreateSchema` and `Model.getUpdateSchema` to allow this constraint to trickle down to the API.
436
+ - Append the same validation to `Model.getCreateSchema` and
437
+ `Model.getUpdateSchema` to allow this constraint to trickle down to the API.
398
438
 
399
439
  > :warning: updateOne and updateMany
400
440
  >
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.
441
+ > Note that calling `Model.updateOne` will throw an error when a unique field
442
+ > exists on any document **including the document being updated**. This is an
443
+ > intentional constraint that allows `updateOne` better peformance by not having
444
+ > to fetch the ids of the documents being updated in order to exclude them. To
445
+ > avoid this call `Document.save` instead.
402
446
  >
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.
447
+ > Note also that calling `Model.updateMany` with a unique field passed will
448
+ > always throw an error as the result would inherently be non-unique.
404
449
 
405
450
  ### Validation
406
451
 
407
- Models are extended with methods that allow complex validation that derives from the schema. Bedrock validation is generally used at the API level:
452
+ Models are extended with methods that allow complex validation that derives from
453
+ the schema. Bedrock validation is generally used at the API level:
408
454
 
409
455
  ```js
410
456
  const Router = require('@koa/router');
@@ -423,16 +469,27 @@ router.post(
423
469
  );
424
470
  ```
425
471
 
426
- In the above example `getCreateValidation` returns a [yada](https://github.com/bedrockio/yada) schema that is validated in the `validateBody` middleware. The `password` field is an additional field that is appended to the create schema.
472
+ In the above example `getCreateValidation` returns a
473
+ [yada](https://github.com/bedrockio/yada) schema that is validated in the
474
+ `validateBody` middleware. The `password` field is an additional field that is
475
+ appended to the create schema.
427
476
 
428
477
  There are 3 main methods to generate schemas:
429
478
 
430
- - `getCreateValidation`: Validates all fields while disallowing reserved fields like `id`, `createdAt`, and `updatedAt`.
431
- - `getUpdateValidation`: Validates all fields as optional (ie. they will not be validated if they don't exist on the object). Additionally will strip out reserved fields to allow created objects to be passed in. Unknown fields will also be stripped out rather than error to allow virtuals to be passed in.
432
- - `getSearchValidation`: Validates fields for use with [search](#search). The generated validation has a number of properties:
433
- - In addition to the base field schemas, arrays or ranges are also allowed. See [search](#search) for more.
434
- - The special fields `limit`, `sort`, `keyword`, `include`, and `ids` are also allowed.
435
- - Array fields are "unwound". This means that for example given an array field `categories`, input may be either a string or an array of strings.
479
+ - `getCreateValidation`: Validates all fields while disallowing reserved fields
480
+ like `id`, `createdAt`, and `updatedAt`.
481
+ - `getUpdateValidation`: Validates all fields as optional (ie. they will not be
482
+ validated if they don't exist on the object). Additionally will strip out
483
+ reserved fields to allow created objects to be passed in. Unknown fields will
484
+ also be stripped out rather than error to allow virtuals to be passed in.
485
+ - `getSearchValidation`: Validates fields for use with [search](#search). The
486
+ generated validation has a number of properties:
487
+ - In addition to the base field schemas, arrays or ranges are also allowed.
488
+ See [search](#search) for more.
489
+ - The special fields `limit`, `sort`, `keyword`, `include`, and `ids` are also
490
+ allowed.
491
+ - Array fields are "unwound". This means that for example given an array field
492
+ `categories`, input may be either a string or an array of strings.
436
493
 
437
494
  #### Named Validations
438
495
 
@@ -447,13 +504,18 @@ Named validations can be specified on the model:
447
504
  }
448
505
  ```
449
506
 
450
- Validator functions are derived from [yada](https://github.com/bedrockio/yada#methods). Note that:
507
+ Validator functions are derived from
508
+ [yada](https://github.com/bedrockio/yada#methods). Note that:
451
509
 
452
510
  - `email` - Will additionally downcase any input.
453
- - `password` - Is not supported as it requires options to be passed and is not a field stored directly in the database.
454
- - `mongo` - Is instead represented in the models as `ObjectId` to have parity with `type`.
455
- - `min` - Defined instead directly on the field with `minLength` for strings and `min` for numbers.
456
- - `max` - Defined instead directly on the field with `maxLength` for strings and `max` for numbers.
511
+ - `password` - Is not supported as it requires options to be passed and is not a
512
+ field stored directly in the database.
513
+ - `mongo` - Is instead represented in the models as `ObjectId` to have parity
514
+ with `type`.
515
+ - `min` - Defined instead directly on the field with `minLength` for strings and
516
+ `min` for numbers.
517
+ - `max` - Defined instead directly on the field with `maxLength` for strings and
518
+ `max` for numbers.
457
519
 
458
520
  ### Search
459
521
 
@@ -466,13 +528,17 @@ const { data, meta } = await User.search();
466
528
  The method takes the following options:
467
529
 
468
530
  - `limit` - Limit for the query. Will be output in `meta`.
469
- - `sort` - The sort for the query as an object containing a `field` and an `order` of `"asc"` or `"desc"`. May also be an array.
531
+ - `sort` - The sort for the query as an object containing a `field` and an
532
+ `order` of `"asc"` or `"desc"`. May also be an array.
470
533
  - `include` - Allows [include](#includes) based population.
471
534
  - `keyword` - A keyword to perform a [keyword search](#keyword-search).
472
535
  - `ids` - An array of document ids to search on.
473
- - `fields` - Used by [keyword search](#keyword-search). Generally for internal use.
536
+ - `fields` - Used by [keyword search](#keyword-search). Generally for internal
537
+ use.
474
538
 
475
- Any other fields passed in will be forwarded to `find`. The return value contains the found documents in `data` and `meta` which contains metadata about the search:
539
+ Any other fields passed in will be forwarded to `find`. The return value
540
+ contains the found documents in `data` and `meta` which contains metadata about
541
+ the search:
476
542
 
477
543
  - `total` The total document count for the query.
478
544
  - `limit` The limit for the query.
@@ -480,10 +546,12 @@ Any other fields passed in will be forwarded to `find`. The return value contain
480
546
 
481
547
  #### Advanced Searching
482
548
 
483
- Input to `search` will execute the optimal mongo query and supports several advanced features:
549
+ Input to `search` will execute the optimal mongo query and supports several
550
+ advanced features:
484
551
 
485
552
  - Array fields will be executed using `$in`.
486
- - Javascript regular expressions will map to `$regex` which allows for [more advanced PCRE compatible features](https://docs.mongodb.com/manual/reference/operator/query/regex/#pcre-vs-javascript).
553
+ - Javascript regular expressions will map to `$regex` which allows for
554
+ [more advanced PCRE compatible features](https://docs.mongodb.com/manual/reference/operator/query/regex/#pcre-vs-javascript).
487
555
  - Nested objects will be automatically flattened to query subdocuments:
488
556
 
489
557
  ```
@@ -513,32 +581,32 @@ age: {
513
581
  }
514
582
  ```
515
583
 
516
- A range query can use `lt`, `gt`, or both. Additionally `lte` and `gte` will query on less/greater than or equal values.
584
+ A range query can use `lt`, `gt`, or both. Additionally `lte` and `gte` will
585
+ query on less/greater than or equal values.
517
586
 
518
587
  #### Keyword Search
519
588
 
520
- Passing `keyword` to the search method will perform a keyword search. To use this feature a `fields` key must be present on the model definition:
589
+ Passing `keyword` to the search method will perform a keyword search. To use
590
+ this feature a `fields` key must be present on the model definition:
521
591
 
522
592
  ```json
523
593
  {
524
594
  "attributes": {
525
595
  "name": {
526
596
  "type": "String"
527
- }
597
+ },
528
598
  "email": {
529
599
  "type": "String"
530
600
  }
531
601
  },
532
602
  "search": {
533
- "fields": [
534
- "name",
535
- "email",
536
- ]
603
+ "fields": ["name", "email"]
537
604
  }
538
605
  }
539
606
  ```
540
607
 
541
- This will use the `$or` operator to search on multiple fields. If `fields` is not defined then a Mongo text query will be attempted:
608
+ This will use the `$or` operator to search on multiple fields. If `fields` is
609
+ not defined then a Mongo text query will be attempted:
542
610
 
543
611
  ```
544
612
  {
@@ -552,18 +620,33 @@ Note that this will fail unless a text index is defined on the model.
552
620
 
553
621
  #### Search Validation
554
622
 
555
- The [validation](#validation) generated for search using `getSearchValidation` is inherently looser and allows more fields to be passed to allow complex searches compatible with the above.
623
+ The [validation](#validation) generated for search using `getSearchValidation`
624
+ is inherently looser and allows more fields to be passed to allow complex
625
+ searches compatible with the above.
556
626
 
557
627
  ### Includes
558
628
 
559
- Populating foreign documents with [populate](https://mongoosejs.com/docs/populate.html) is a powerful feature of mongoose. In the past Bedrock has made use of the [autopopulate](https://plugins.mongoosejs.io/plugins/autopopulate) plugin, however has since moved away from this for two reasons:
629
+ Populating foreign documents with
630
+ [populate](https://mongoosejs.com/docs/populate.html) is a powerful feature of
631
+ mongoose. In the past Bedrock has made use of the
632
+ [autopopulate](https://plugins.mongoosejs.io/plugins/autopopulate) plugin,
633
+ however has since moved away from this for two reasons:
560
634
 
561
- 1. Document population is highly situational. In complex real world applications a document may require deep population or none at all, however autopopulate does not allow this level of control.
562
- 2. Although circular references usually are the result of bad data modeling, in some cases they cannot be avoided. Autopopulate will keep loading these references until it reaches a maximum depth, even when the fetched data is redundant.
635
+ 1. Document population is highly situational. In complex real world applications
636
+ a document may require deep population or none at all, however autopopulate
637
+ does not allow this level of control.
638
+ 2. Although circular references usually are the result of bad data modeling, in
639
+ some cases they cannot be avoided. Autopopulate will keep loading these
640
+ references until it reaches a maximum depth, even when the fetched data is
641
+ redundant.
563
642
 
564
- Both of these issues have major performance implications which result in slower queries and more unneeded data transfer over the wire.
643
+ Both of these issues have major performance implications which result in slower
644
+ queries and more unneeded data transfer over the wire.
565
645
 
566
- For this reason calling `populate` manually is highly preferable, however in complex situations this can easily be a lot of overhead. The include module attempts to greatly streamline this process by adding an `include` method to queries:
646
+ For this reason calling `populate` manually is highly preferable, however in
647
+ complex situations this can easily be a lot of overhead. The include module
648
+ attempts to greatly streamline this process by adding an `include` method to
649
+ queries:
567
650
 
568
651
  ```js
569
652
  const product = await Product.findById(id).include([
@@ -575,7 +658,8 @@ const product = await Product.findById(id).include([
575
658
  ]);
576
659
  ```
577
660
 
578
- This method accepts a string or array of strings that will map to a `populate` call that can be far more complex:
661
+ This method accepts a string or array of strings that will map to a `populate`
662
+ call that can be far more complex:
579
663
 
580
664
  ```js
581
665
  const product = await Product.findById(id).populate([
@@ -602,7 +686,10 @@ const product = await Product.findById(id).populate([
602
686
  ]);
603
687
  ```
604
688
 
605
- In addition to brevity, one major advantage of using `include` is that the caller does not need to know whether the documents are subdocuments or foreign references. As Bedrock has knowledge of the schemas, it is able to build the appropriate call to `populate` for you.
689
+ In addition to brevity, one major advantage of using `include` is that the
690
+ caller does not need to know whether the documents are subdocuments or foreign
691
+ references. As Bedrock has knowledge of the schemas, it is able to build the
692
+ appropriate call to `populate` for you.
606
693
 
607
694
  #### Excluding Fields
608
695
 
@@ -614,8 +701,11 @@ const user = await User.findById(id).include('-profile');
614
701
 
615
702
  The above will return all fields except `profile`. Note that:
616
703
 
617
- - Excluding fields only affects the `select` option. Foreign fields must still be passed, otherwise they will be returned unpopulated.
618
- - An excluded field on a foreign reference will implicitly be populated. This means that passing `-profile.name` where `profile` is a foreign field will populate `profile` but exclude `name`.
704
+ - Excluding fields only affects the `select` option. Foreign fields must still
705
+ be passed, otherwise they will be returned unpopulated.
706
+ - An excluded field on a foreign reference will implicitly be populated. This
707
+ means that passing `-profile.name` where `profile` is a foreign field will
708
+ populate `profile` but exclude `name`.
619
709
 
620
710
  #### Wildcards
621
711
 
@@ -652,9 +742,13 @@ The example above will select both `firstName` and `lastName`.
652
742
  const user = await User.findById(id).include('**.phone');
653
743
  ```
654
744
 
655
- This example above will select both `profile1.address.phone` and `profile2.address.phone`. Compare this to `*` which will not match here.
745
+ This example above will select both `profile1.address.phone` and
746
+ `profile2.address.phone`. Compare this to `*` which will not match here.
656
747
 
657
- Note that wildcards do not implicitly populate foreign fields. For example passing `p*` where `profile` is a foreign field will include all fields matching `p*` but it will not populate the `profile` field. In this case an array must be used instead:
748
+ Note that wildcards do not implicitly populate foreign fields. For example
749
+ passing `p*` where `profile` is a foreign field will include all fields matching
750
+ `p*` but it will not populate the `profile` field. In this case an array must be
751
+ used instead:
658
752
 
659
753
  ```js
660
754
  const user = await User.findById(id).include(['p*', 'profile']);
@@ -672,7 +766,8 @@ const user = await User.search({
672
766
 
673
767
  #### Include as a Filter
674
768
 
675
- Additionally `include` is flagged as a special parameter for filters, allowing the following equivalent syntax on `search` as well as all `find` methods:
769
+ Additionally `include` is flagged as a special parameter for filters, allowing
770
+ the following equivalent syntax on `search` as well as all `find` methods:
676
771
 
677
772
  ```js
678
773
  const user = await User.find({
@@ -683,7 +778,9 @@ const user = await User.find({
683
778
 
684
779
  #### Validation with includes
685
780
 
686
- The [validation](#validation) methods additionally allow `include` as a special field on generated schemas. This allows the client to drive document inclusion on a case by case basis. For example, given a typical Bedrock setup:
781
+ The [validation](#validation) methods additionally allow `include` as a special
782
+ field on generated schemas. This allows the client to drive document inclusion
783
+ on a case by case basis. For example, given a typical Bedrock setup:
687
784
 
688
785
  ```js
689
786
  const Router = require('@koa/router');
@@ -697,7 +794,9 @@ router.post('/', validateBody(User.getSearchValidation()), async (ctx) => {
697
794
  });
698
795
  ```
699
796
 
700
- The `getSearchValidation` will allow the `include` property to be passed, letting the client populate documents as they require. Note that the fields a client is able to include is subject to [access control](#access-control).
797
+ The `getSearchValidation` will allow the `include` property to be passed,
798
+ letting the client populate documents as they require. Note that the fields a
799
+ client is able to include is subject to [access control](#access-control).
701
800
 
702
801
  ### Access Control
703
802
 
@@ -705,11 +804,18 @@ This package applies two forms of access control:
705
804
 
706
805
  #### Read Access
707
806
 
708
- Read access influences how documents are serialized. Fields that have been denied access will be stripped out. Additionally it will influence the validation schema for `getSearchValidation`. Fields that have been denied access are not allowed to be searched on and will throw an error.
807
+ Read access influences how documents are serialized. Fields that have been
808
+ denied access will be stripped out. Additionally it will influence the
809
+ validation schema for `getSearchValidation`. Fields that have been denied access
810
+ are not allowed to be searched on and will throw an error.
709
811
 
710
812
  #### Write Access
711
813
 
712
- Write access influences validation in `getCreateValidation` and `getUpdateValidation`. Fields that have been denied access will throw an error unless they are identical to what is already set on the document. Note that in the case of `getCreateValidation` no document has been created yet so a denied field will always result in an error if passed.
814
+ Write access influences validation in `getCreateValidation` and
815
+ `getUpdateValidation`. Fields that have been denied access will throw an error
816
+ unless they are identical to what is already set on the document. Note that in
817
+ the case of `getCreateValidation` no document has been created yet so a denied
818
+ field will always result in an error if passed.
713
819
 
714
820
  #### Defining Access
715
821
 
@@ -725,7 +831,8 @@ Access is defined in schemas with the `readAccess` and `writeAccess` options:
725
831
  }
726
832
  ```
727
833
 
728
- This may be either a string or an array of strings. For multiple fields with the same access types, use a [scope](#scopes).
834
+ This may be either a string or an array of strings. For multiple fields with the
835
+ same access types, use a [scope](#scopes).
729
836
 
730
837
  ##### Access on Arrays
731
838
 
@@ -742,7 +849,9 @@ Note that on array fields the following schema is often used:
742
849
  };
743
850
  ```
744
851
 
745
- However this is not technically correct as the `readAccess` above is referring to the `tokens` array instead of individual elements. The correct schema is technically written:
852
+ However this is not technically correct as the `readAccess` above is referring
853
+ to the `tokens` array instead of individual elements. The correct schema is
854
+ technically written:
746
855
 
747
856
  ```js
748
857
  {
@@ -753,13 +862,18 @@ However this is not technically correct as the `readAccess` above is referring t
753
862
  }
754
863
  ```
755
864
 
756
- However this is overhead and hard to remember, so `readAccess` and `writeAccess` will be hoisted to the array field itself as a special case. Note that only these two fields will be hoisted as other fields like `validate` and `default` are correctly defined on the string itself.
865
+ However this is overhead and hard to remember, so `readAccess` and `writeAccess`
866
+ will be hoisted to the array field itself as a special case. Note that only
867
+ these two fields will be hoisted as other fields like `validate` and `default`
868
+ are correctly defined on the string itself.
757
869
 
758
870
  #### Access Types
759
871
 
760
- `readAccess` and `writeAccess` can specify any token. However a few special tokens exist:
872
+ `readAccess` and `writeAccess` can specify any token. However a few special
873
+ tokens exist:
761
874
 
762
- - `all` - Allows access to anyone. This token is reserved for clarity but is not required as it is the default.
875
+ - `all` - Allows access to anyone. This token is reserved for clarity but is not
876
+ required as it is the default.
763
877
  - `none` - Allows access to no-one.
764
878
  - `self` - See [document based access](#document-based-access).
765
879
  - `user` - See [document based access](#document-based-access).
@@ -769,7 +883,8 @@ Any other token will use [scope based access](#scope-based-access).
769
883
 
770
884
  ##### Scope Based Access
771
885
 
772
- A non-reserved token specified in `readAccess` or `writeAccess` will test against scopes in the generated validations or when serializing:
886
+ A non-reserved token specified in `readAccess` or `writeAccess` will test
887
+ against scopes in the generated validations or when serializing:
773
888
 
774
889
  ```js
775
890
  // In validation middleware:
@@ -787,7 +902,9 @@ document.toObject({
787
902
  });
788
903
  ```
789
904
 
790
- Note that scopes are just literal strings. For example a route already checking that the user is admin may simply pass `.toObject({ scope: 'admin' })`. However for more complex cases scopes are typically derived from the authUser's roles.
905
+ Note that scopes are just literal strings. For example a route already checking
906
+ that the user is admin may simply pass `.toObject({ scope: 'admin' })`. However
907
+ for more complex cases scopes are typically derived from the authUser's roles.
791
908
 
792
909
  ##### Document Based Access
793
910
 
@@ -801,16 +918,19 @@ Document based access allows 3 different tokens:
801
918
 
802
919
  Using document based access comes with some requirements:
803
920
 
804
- 1. Read access must use `.toObject({ authUser })`. Note that the document is not required here as a reference is already kept.
921
+ 1. Read access must use `.toObject({ authUser })`. Note that the document is not
922
+ required here as a reference is already kept.
805
923
  2. Write access must use `schema.validate(body, { authUser, document })`.
806
924
 
807
925
  #### Examples
808
926
 
809
- For clarity, here are a few examples about how document based access control should be used:
927
+ For clarity, here are a few examples about how document based access control
928
+ should be used:
810
929
 
811
930
  ##### Example 1
812
931
 
813
- A user is allowed to update their own date of birth, but not their email which is set after verification:
932
+ A user is allowed to update their own date of birth, but not their email which
933
+ is set after verification:
814
934
 
815
935
  ```js
816
936
  // user.json
@@ -828,7 +948,8 @@ A user is allowed to update their own date of birth, but not their email which i
828
948
 
829
949
  ##### Example 2
830
950
 
831
- A user is allowed to update the name of their own shop and admins can as well. However, only admins can set the owner of the shop:
951
+ A user is allowed to update the name of their own shop and admins can as well.
952
+ However, only admins can set the owner of the shop:
832
953
 
833
954
  ```json
834
955
  // shop.json
@@ -847,9 +968,13 @@ A user is allowed to update the name of their own shop and admins can as well. H
847
968
 
848
969
  ##### Example 3
849
970
 
850
- A user is allowed to update the fact that they have received their medical report, but nothing else. The medical report is received externally so even admins are not allowed to change the user they belong to.
971
+ A user is allowed to update the fact that they have received their medical
972
+ report, but nothing else. The medical report is received externally so even
973
+ admins are not allowed to change the user they belong to.
851
974
 
852
- The difference with `owner` here is the name only, however both options exist as a `user` defined on a schema does not necessarily represent the document's owner, as this example illustrates:
975
+ The difference with `owner` here is the name only, however both options exist as
976
+ a `user` defined on a schema does not necessarily represent the document's
977
+ owner, as this example illustrates:
853
978
 
854
979
  ```js
855
980
  // medical-report.json
@@ -868,33 +993,111 @@ The difference with `owner` here is the name only, however both options exist as
868
993
 
869
994
  #### Notes on Read Access
870
995
 
871
- Note that all forms of read access require that `.toObject` is called on the document with special parameters, however this method is called on internal serialization including both `JSON.stringify` and logging to the console. For this reason it will never fail even if it cannot perform the correct access checks. Instead any fields with `readAccess` defined on them will be stripped out.
996
+ Note that all forms of read access require that `.toObject` is called on the
997
+ document with special parameters, however this method is called on internal
998
+ serialization including both `JSON.stringify` and logging to the console. For
999
+ this reason it will never fail even if it cannot perform the correct access
1000
+ checks. Instead any fields with `readAccess` defined on them will be stripped
1001
+ out.
872
1002
 
873
1003
  #### Notes on Write Access
874
1004
 
875
- Note that `self` is generally only meaningful on a User model as it will always check the document is the same as `authUser`.
1005
+ Note that `self` is generally only meaningful on a User model as it will always
1006
+ check the document is the same as `authUser`.
876
1007
 
877
- ### References
1008
+ ### Delete Hooks
878
1009
 
879
- This module adds a single method `assertNoReferences` to the schema. This is useful on delete operations to throw an error if there are external references to the document. It takes an options object with a single option `except` as an array:
1010
+ Delete hooks are a powerful way to define what actions are taken on document
1011
+ deletion. They are defined in the `onDelete` field of the model definition file:
880
1012
 
881
- ```js
882
- router.delete('/:id', async (ctx) => {
883
- const { shop } = ctx.state;
884
- try {
885
- await shop.assertNoReferences({
886
- except: [AuditEntry],
887
- });
888
- } catch (err) {
889
- console.info(err.references);
890
- ctx.throw(400, err.message);
1013
+ ```json
1014
+ // user.json
1015
+ {
1016
+ "attributes": {
1017
+ "name": "String",
1018
+ "profile": {
1019
+ "type": "ObjectId",
1020
+ "ref": "UserProfile"
1021
+ }
1022
+ },
1023
+ "onDelete": {
1024
+ "clean": {
1025
+ "local": "profile",
1026
+ "foreign": {
1027
+ Shop: "owner"
1028
+ },
1029
+ }
1030
+ "errorOnReferenced": {
1031
+ "except": ["AuditEntry"]
1032
+ }
891
1033
  }
892
- await user.delete();
893
- ctx.status = 204;
1034
+ }
1035
+ ```
1036
+
1037
+ #### Clean
1038
+
1039
+ `clean` determines other associated documents that will be deleted when the main
1040
+ document is deleted.
1041
+
1042
+ #### Local References
1043
+
1044
+ `clean.local` specifies local refs to delete. It may be a string or array of
1045
+ strings. In the above example:
1046
+
1047
+ ```js
1048
+ user.delete();
1049
+
1050
+ // Will implicitly run:
1051
+ await user.populate('profile');
1052
+ await user.profile.delete();
1053
+ ```
1054
+
1055
+ #### Foreign Reference Cleanup
1056
+
1057
+ `clean.foreign` specifies foreign refs to delete. It is defined as an object
1058
+ that maps foreign `ref` names to their referencing field. In the above example:
1059
+
1060
+ ```js
1061
+ user.delete();
1062
+
1063
+ // Will implicitly run:
1064
+ const shop = await Shop.find({
1065
+ owner: user,
894
1066
  });
1067
+ await shop.delete();
1068
+ ```
1069
+
1070
+ #### Erroring on Delete
1071
+
1072
+ The `errorOnReferenced` field helps to prevent orphaned references by defining
1073
+ if and how the `delete` method will error if it is being referenced by another
1074
+ foreign document. In the above example:
1075
+
1076
+ ```js
1077
+ user.delete();
1078
+
1079
+ // Will error if referenced by any document other than:
1080
+ // 1. AuditEntry - Explicitly allowed by "except".
1081
+ // 2. Shop - Implicitly allowed as it will be deleted.
895
1082
  ```
896
1083
 
897
- The above example will throw an error if shop is referenced externally (for example by a a `Product`), however it will not count `AuditEntry` references. Note that a `references` property on the thrown error will contain the found references.
1084
+ In this case, "referenced by" means any other model that explicitly uses "User"
1085
+ as a `ref` for type `ObjectId`. `errorOnReference` may also be simply `true`,
1086
+ which will error on any foreign references of any kind.
1087
+
1088
+ `only` may be passed instead of `except`, which will only error when the
1089
+ document is referenced by referenced by specific models.
1090
+
1091
+ #### Restoring Deleted Documents
1092
+
1093
+ Models that have delete hooks defined on them will keep a reference of the
1094
+ documents that were deleted. Calling `.restore()` on the document will also
1095
+ restore these references.
1096
+
1097
+ > [!IMPORTANT]
1098
+ > Delete hooks are **only** run on a single document (`.delete` or `.restore`).
1099
+ > They will not be run when using model methods like `deleteOne` or
1100
+ > `deleteMany`.
898
1101
 
899
1102
  ### Assign
900
1103
 
@@ -906,13 +1109,20 @@ user.assign(ctx.request.body);
906
1109
  Object.assign(user, ctx.request.body);
907
1110
  ```
908
1111
 
909
- This is functionally identical to `Object.assign` with the exception that `ObjectId` reference fields can be unset by passing falsy values. This method is provided as `undefined` cannot be represented in JSON which requires using either a `null` or empty string, both of which would be stored in the database if naively assigned with `Object.assign`.
1112
+ This is functionally identical to `Object.assign` with the exception that
1113
+ `ObjectId` reference fields can be unset by passing falsy values. This method is
1114
+ provided as `undefined` cannot be represented in JSON which requires using
1115
+ either a `null` or empty string, both of which would be stored in the database
1116
+ if naively assigned with `Object.assign`.
910
1117
 
911
1118
  ### Slugs
912
1119
 
913
- A common requirement is to allow slugs on documents to serve as ids for human readable URLs. To load a single document this way the naive approach would be to run a search on all documents matching the `slug` then pull the first one off.
1120
+ A common requirement is to allow slugs on documents to serve as ids for human
1121
+ readable URLs. To load a single document this way the naive approach would be to
1122
+ run a search on all documents matching the `slug` then pull the first one off.
914
1123
 
915
- This module simplifies this by assuming a `slug` field on a model and adding a `findByIdOrSlug` method that allows searching on both:
1124
+ This module simplifies this by assuming a `slug` field on a model and adding a
1125
+ `findByIdOrSlug` method that allows searching on both:
916
1126
 
917
1127
  ```js
918
1128
  const post = await Post.findByIdOrSlug(str);
@@ -923,7 +1133,8 @@ Note that soft delete methods are also applied:
923
1133
  - `findByIdOrSlugDeleted`
924
1134
  - `findByIdOrSlugWithDeleted`
925
1135
 
926
- Also note that as Mongo ids are represented as 24 byte hexadecimal a collision is possible:
1136
+ Also note that as Mongo ids are represented as 24 byte hexadecimal a collision
1137
+ is possible:
927
1138
 
928
1139
  - `deadbeefdeadbeefdeadbeef`
929
1140
  - `cafecafecafecafecafecafe`
@@ -932,7 +1143,8 @@ However the likelyhood of such collisions on a slug are acceptably small.
932
1143
 
933
1144
  ## Testing
934
1145
 
935
- A helper `createTestModel` is exported to allow quickly building models for testing:
1146
+ A helper `createTestModel` is exported to allow quickly building models for
1147
+ testing:
936
1148
 
937
1149
  ```js
938
1150
  const { createTestModel } = require('@bedrockio/model');
@@ -943,7 +1155,9 @@ const User = createTestModel({
943
1155
  mk;
944
1156
  ```
945
1157
 
946
- Note that a unique model name will be generated to prevent clashing with other models. This can be accessed with `Model.modelName` or to make tests more readable it can be overridden:
1158
+ Note that a unique model name will be generated to prevent clashing with other
1159
+ models. This can be accessed with `Model.modelName` or to make tests more
1160
+ readable it can be overridden:
947
1161
 
948
1162
  ```js
949
1163
  const { createTestModel } = require('@bedrockio/model');