@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 +323 -109
- package/dist/cjs/const.js +2 -4
- package/dist/cjs/delete-hooks.js +351 -0
- package/dist/cjs/disallowed.js +0 -14
- package/dist/cjs/env.js +1 -2
- package/dist/cjs/include.js +11 -3
- package/dist/cjs/query.js +21 -0
- package/dist/cjs/schema.js +6 -3
- package/dist/cjs/search.js +12 -23
- package/dist/cjs/serialization.js +5 -3
- package/dist/cjs/soft-delete.js +226 -51
- package/dist/cjs/validation.js +1 -2
- package/package.json +5 -5
- package/src/delete-hooks.js +343 -0
- package/src/disallowed.js +0 -29
- package/src/include.js +10 -1
- package/src/query.js +15 -0
- package/src/schema.js +9 -4
- package/src/search.js +15 -27
- package/src/serialization.js +5 -1
- package/src/soft-delete.js +248 -55
- package/types/delete-hooks.d.ts +2 -0
- package/types/delete-hooks.d.ts.map +1 -0
- package/types/disallowed.d.ts.map +1 -1
- package/types/load.d.ts +67 -1
- package/types/load.d.ts.map +1 -1
- package/types/query.d.ts +2 -0
- package/types/query.d.ts.map +1 -0
- package/types/schema.d.ts +15 -9
- package/types/schema.d.ts.map +1 -1
- package/types/search.d.ts.map +1 -1
- package/types/serialization.d.ts.map +1 -1
- package/types/soft-delete.d.ts.map +1 -1
- package/types/testing.d.ts +1 -1
- package/types/testing.d.ts.map +1 -1
- package/dist/cjs/references.js +0 -104
- package/src/references.js +0 -101
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
262
|
+
Where `validator` is a special validator that enforces both the exact array
|
|
263
|
+
length and content types.
|
|
249
264
|
|
|
250
|
-
Note that Mongoose
|
|
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
|
|
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 `[]`
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
369
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
431
|
-
|
|
432
|
-
- `
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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
|
|
454
|
-
|
|
455
|
-
- `
|
|
456
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
562
|
-
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
618
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
###
|
|
1008
|
+
### Delete Hooks
|
|
878
1009
|
|
|
879
|
-
|
|
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
|
-
```
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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');
|