@bedrockio/model 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -32
- package/dist/cjs/include.js +83 -30
- package/dist/cjs/search.js +1 -1
- package/package.json +3 -3
- package/src/include.js +86 -28
- package/src/search.js +1 -1
- package/types/load.d.ts +1 -67
- package/types/load.d.ts.map +1 -1
- package/types/schema.d.ts +7 -7
package/README.md
CHANGED
|
@@ -756,13 +756,7 @@ complex situations this can easily be a lot of overhead. The include module
|
|
|
756
756
|
attempts to streamline this process by adding an `include` method to queries:
|
|
757
757
|
|
|
758
758
|
```js
|
|
759
|
-
const product = await Product.findById(id).include(
|
|
760
|
-
'name',
|
|
761
|
-
'shop.email',
|
|
762
|
-
'shop.user.name',
|
|
763
|
-
'shop.user.address.line1',
|
|
764
|
-
'shop.customers.tags',
|
|
765
|
-
]);
|
|
759
|
+
const product = await Product.findById(id).include('shop.user.customers');
|
|
766
760
|
```
|
|
767
761
|
|
|
768
762
|
This method accepts a string or array of strings that will map to a `populate`
|
|
@@ -771,20 +765,21 @@ call that can be far more complex:
|
|
|
771
765
|
```js
|
|
772
766
|
const product = await Product.findById(id).populate([
|
|
773
767
|
{
|
|
774
|
-
select: [
|
|
768
|
+
select: [],
|
|
775
769
|
populate: [
|
|
776
770
|
{
|
|
777
771
|
path: 'shop',
|
|
778
|
-
select: [
|
|
772
|
+
select: [],
|
|
779
773
|
populate: [
|
|
780
774
|
{
|
|
781
775
|
path: 'user',
|
|
782
|
-
select: [
|
|
776
|
+
select: [],
|
|
783
777
|
populate: [],
|
|
784
778
|
},
|
|
785
779
|
{
|
|
786
780
|
path: 'customers',
|
|
787
|
-
select: [
|
|
781
|
+
select: [],
|
|
782
|
+
populate: [],
|
|
788
783
|
},
|
|
789
784
|
],
|
|
790
785
|
},
|
|
@@ -794,11 +789,131 @@ const product = await Product.findById(id).populate([
|
|
|
794
789
|
```
|
|
795
790
|
|
|
796
791
|
In addition to brevity, one major advantage of using `include` is that the
|
|
797
|
-
caller does not need to know whether the
|
|
792
|
+
caller does not need to know whether the path contains subdocuments or foreign
|
|
798
793
|
references. As Bedrock has knowledge of the schemas, it is able to build the
|
|
799
794
|
appropriate call to `populate` for you.
|
|
800
795
|
|
|
801
|
-
####
|
|
796
|
+
#### Exclusive Fields
|
|
797
|
+
|
|
798
|
+
By default, arguments to `include` are for population. However often field
|
|
799
|
+
projection (selection) is also desired to avoid excessive data transfer. The `^`
|
|
800
|
+
token can be used here to build the `select` option to populates:
|
|
801
|
+
|
|
802
|
+
```js
|
|
803
|
+
const product = await Product.findById(id).include([
|
|
804
|
+
'^name',
|
|
805
|
+
'^shop.name',
|
|
806
|
+
'^shop.user.name',
|
|
807
|
+
]);
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
This will map to a selective inclusion of fields in the `populate` call:
|
|
811
|
+
|
|
812
|
+
```js
|
|
813
|
+
const product = await Product.findById(id).populate([
|
|
814
|
+
{
|
|
815
|
+
select: ['name', 'shop'],
|
|
816
|
+
populate: [
|
|
817
|
+
{
|
|
818
|
+
path: 'shop',
|
|
819
|
+
select: ['name', 'user'],
|
|
820
|
+
populate: [
|
|
821
|
+
{
|
|
822
|
+
path: 'user',
|
|
823
|
+
select: ['name'],
|
|
824
|
+
populate: [],
|
|
825
|
+
},
|
|
826
|
+
],
|
|
827
|
+
},
|
|
828
|
+
],
|
|
829
|
+
},
|
|
830
|
+
]);
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
The resulting data will include only the specified fields:
|
|
834
|
+
|
|
835
|
+
```json
|
|
836
|
+
{
|
|
837
|
+
"name": "Product Name",
|
|
838
|
+
"shop": {
|
|
839
|
+
"name": "Shop Name",
|
|
840
|
+
"user": {
|
|
841
|
+
"name": "User Name"
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Note that the exclusive operator can be used anywhere in the path. The exclusion
|
|
848
|
+
(select) will be applied at the depth in which it is specified:
|
|
849
|
+
|
|
850
|
+
Example 1:
|
|
851
|
+
|
|
852
|
+
- Top level exclusion
|
|
853
|
+
- Only exact field returned.
|
|
854
|
+
|
|
855
|
+
```js
|
|
856
|
+
await Product.findById(id).include('^shop.user.name').
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
```json
|
|
860
|
+
{
|
|
861
|
+
"shop": {
|
|
862
|
+
"user": {
|
|
863
|
+
"name": "User Name"
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Example 2:
|
|
870
|
+
|
|
871
|
+
- Mid-level exclusion.
|
|
872
|
+
- Top level fields included, mid-level begins exclusion.
|
|
873
|
+
|
|
874
|
+
```js
|
|
875
|
+
await Product.findById(id).include('shop.^user.name').
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
```json
|
|
879
|
+
{
|
|
880
|
+
"name": "Product Name",
|
|
881
|
+
"cost": 10,
|
|
882
|
+
// etc
|
|
883
|
+
"shop": {
|
|
884
|
+
"user": {
|
|
885
|
+
"name": "User Name"
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
Example 3:
|
|
892
|
+
|
|
893
|
+
- Final level exclusion.
|
|
894
|
+
- All fields returned except in final `user` population.
|
|
895
|
+
|
|
896
|
+
```js
|
|
897
|
+
await Product.findById(id).include('shop.user.^name').
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
```json
|
|
901
|
+
{
|
|
902
|
+
"name": "Product Name",
|
|
903
|
+
"cost": 10,
|
|
904
|
+
// etc
|
|
905
|
+
"shop": {
|
|
906
|
+
"name": "Shop Name",
|
|
907
|
+
"rating": 5,
|
|
908
|
+
// etc
|
|
909
|
+
"user": {
|
|
910
|
+
"name": "User Name"
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
#### Excluded Fields
|
|
802
917
|
|
|
803
918
|
Fields can be excluded rather than included using `-`:
|
|
804
919
|
|
|
@@ -813,6 +928,7 @@ The above will return all fields except `profile`. Note that:
|
|
|
813
928
|
- An excluded field on a foreign reference will implicitly be populated. This
|
|
814
929
|
means that passing `-profile.name` where `profile` is a foreign field will
|
|
815
930
|
populate `profile` but exclude `name`.
|
|
931
|
+
- Note that `-` can only be used at the beginning of the path.
|
|
816
932
|
|
|
817
933
|
#### Wildcards
|
|
818
934
|
|
|
@@ -820,35 +936,43 @@ Multiple fields can be selected using wildcards:
|
|
|
820
936
|
|
|
821
937
|
- `*` - Matches anything except `.`.
|
|
822
938
|
- `**` - Matches anything including `.`.
|
|
939
|
+
- Note that the use of wildcards implies that other fields are excluded.
|
|
940
|
+
|
|
941
|
+
Example 1: Single wildcard
|
|
823
942
|
|
|
824
943
|
```js
|
|
825
|
-
// Assuming a schema of:
|
|
826
|
-
// {
|
|
827
|
-
// "firstName": "String"
|
|
828
|
-
// "lastName": "String"
|
|
829
|
-
// }
|
|
830
944
|
const user = await User.findById(id).include('*Name');
|
|
831
945
|
```
|
|
832
946
|
|
|
833
|
-
|
|
947
|
+
```json
|
|
948
|
+
{
|
|
949
|
+
"firstName": "Frank",
|
|
950
|
+
"lastName": "Reynolds"
|
|
951
|
+
// Other fields excluded.
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Example 2: Double wildcard
|
|
834
956
|
|
|
835
957
|
```js
|
|
836
|
-
// Assuming a schema of:
|
|
837
|
-
// {
|
|
838
|
-
// "profile1": {
|
|
839
|
-
// "address": {
|
|
840
|
-
// "phone": "String"
|
|
841
|
-
// }
|
|
842
|
-
// },
|
|
843
|
-
// "profile2": {
|
|
844
|
-
// "address": {
|
|
845
|
-
// "phone": "String"
|
|
846
|
-
// }
|
|
847
|
-
// }
|
|
848
|
-
// }
|
|
849
958
|
const user = await User.findById(id).include('**.phone');
|
|
850
959
|
```
|
|
851
960
|
|
|
961
|
+
```json
|
|
962
|
+
{
|
|
963
|
+
"profile1": {
|
|
964
|
+
"address": {
|
|
965
|
+
"phone": "String"
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
"profile2": {
|
|
969
|
+
"address": {
|
|
970
|
+
"phone": "String"
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
```
|
|
975
|
+
|
|
852
976
|
This example above will select both `profile1.address.phone` and
|
|
853
977
|
`profile2.address.phone`. Compare this to `*` which will not match here.
|
|
854
978
|
|
package/dist/cjs/include.js
CHANGED
|
@@ -186,13 +186,19 @@ function nodeToPopulates(node) {
|
|
|
186
186
|
const select = [];
|
|
187
187
|
const populate = [];
|
|
188
188
|
for (let [key, value] of Object.entries(node)) {
|
|
189
|
+
if (key.startsWith('-')) {
|
|
190
|
+
select.push(key);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (key.startsWith('^')) {
|
|
194
|
+
key = key.slice(1);
|
|
195
|
+
select.push(key);
|
|
196
|
+
}
|
|
189
197
|
if (value) {
|
|
190
198
|
populate.push({
|
|
191
199
|
path: key,
|
|
192
200
|
...nodeToPopulates(value)
|
|
193
201
|
});
|
|
194
|
-
} else {
|
|
195
|
-
select.push(key);
|
|
196
202
|
}
|
|
197
203
|
}
|
|
198
204
|
return {
|
|
@@ -212,24 +218,17 @@ function pathsToNode(paths, modelName) {
|
|
|
212
218
|
if (typeof str !== 'string') {
|
|
213
219
|
throw new Error('Provided include path was not as string.');
|
|
214
220
|
}
|
|
215
|
-
let exclude = false;
|
|
216
|
-
if (str.startsWith('-')) {
|
|
217
|
-
exclude = true;
|
|
218
|
-
str = str.slice(1);
|
|
219
|
-
}
|
|
220
221
|
setNodePath(node, {
|
|
221
222
|
path: str.split('.'),
|
|
222
|
-
modelName
|
|
223
|
-
exclude
|
|
223
|
+
modelName
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
226
|
return node;
|
|
227
227
|
}
|
|
228
228
|
function setNodePath(node, options) {
|
|
229
229
|
const {
|
|
230
|
-
path,
|
|
231
230
|
modelName,
|
|
232
|
-
|
|
231
|
+
path: fullPath,
|
|
233
232
|
depth = 0
|
|
234
233
|
} = options;
|
|
235
234
|
if (depth > _const.POPULATE_MAX_DEPTH) {
|
|
@@ -239,50 +238,104 @@ function setNodePath(node, options) {
|
|
|
239
238
|
if (!schema) {
|
|
240
239
|
throw new Error(`Could not derive schema for ${modelName}.`);
|
|
241
240
|
}
|
|
241
|
+
let {
|
|
242
|
+
excluded = false,
|
|
243
|
+
exclusive = false
|
|
244
|
+
} = options;
|
|
242
245
|
const parts = [];
|
|
243
|
-
for (let part of
|
|
246
|
+
for (let part of fullPath) {
|
|
247
|
+
if (part.startsWith('-')) {
|
|
248
|
+
// Field is excluded. Note that this occurs only at
|
|
249
|
+
// top level and should take precedence:
|
|
250
|
+
// -name -> "name" is excluded
|
|
251
|
+
// -user.name -> "user" is populated but "name" is
|
|
252
|
+
// excluded
|
|
253
|
+
excluded = true;
|
|
254
|
+
part = part.slice(1);
|
|
255
|
+
} else if (!excluded && part.startsWith('^')) {
|
|
256
|
+
// Field is exclusive. Note that this can happen at
|
|
257
|
+
// any part of the path:
|
|
258
|
+
// ^name -> "name" is exclusively selected
|
|
259
|
+
// user.^name -> "user" is populated with "name"
|
|
260
|
+
// exclusively selected within
|
|
261
|
+
// ^user.name -> "user" is exclusively selected
|
|
262
|
+
// ("name" is redundant)
|
|
263
|
+
exclusive = true;
|
|
264
|
+
part = part.slice(1);
|
|
265
|
+
} else if (!excluded && part.includes('*')) {
|
|
266
|
+
// Wildcards in field implies exclusion, but only
|
|
267
|
+
// if path is not already excluded:
|
|
268
|
+
// *name -> "firstName" and "lastName" are exclusively
|
|
269
|
+
// selected
|
|
270
|
+
// user.*name -> "user" is populated, "user.firstName"
|
|
271
|
+
// and "user.lastName" are exclusively selected
|
|
272
|
+
// -*name -> "firstName" and "lastName" are excluded
|
|
273
|
+
// -user.*name -> "user" is populated but "user.firstName"
|
|
274
|
+
// and "user.lastName" are excluded
|
|
275
|
+
exclusive = true;
|
|
276
|
+
}
|
|
244
277
|
parts.push(part);
|
|
245
|
-
const
|
|
246
|
-
const isExact = parts.length === path.length;
|
|
278
|
+
const isExact = parts.length === fullPath.length;
|
|
247
279
|
let halt = false;
|
|
248
|
-
for (let [
|
|
280
|
+
for (let [path, type] of resolvePaths(schema, parts.join('.'))) {
|
|
281
|
+
// The exclusive key.
|
|
282
|
+
const eKey = '^' + path;
|
|
283
|
+
// The negative (excluded) key.
|
|
284
|
+
const nKey = '-' + path;
|
|
285
|
+
let key = path;
|
|
286
|
+
if (exclusive && !node[key]) {
|
|
287
|
+
// Add the exclusive flag if the node
|
|
288
|
+
// has not already been included.
|
|
289
|
+
key = eKey;
|
|
290
|
+
} else if (!exclusive && node[eKey]) {
|
|
291
|
+
// If the node has already been marked exclusive
|
|
292
|
+
// and another include overrides it, then we need
|
|
293
|
+
// to move that node over to be inclusive. This
|
|
294
|
+
// step is needed as includes should always take
|
|
295
|
+
// priority over exclusion regardless of the order.
|
|
296
|
+
node[key] = node[eKey];
|
|
297
|
+
delete node[eKey];
|
|
298
|
+
} else if (excluded && isExact) {
|
|
299
|
+
// Only flag the node as excluded if the path is an
|
|
300
|
+
// exact match:
|
|
301
|
+
// -name -> Exclude "name" field.
|
|
302
|
+
// -user.name -> Exclude "user.name" field, however this
|
|
303
|
+
// implies that we must populate "user" so
|
|
304
|
+
// continue traversing and flag for include.
|
|
305
|
+
key = nKey;
|
|
306
|
+
}
|
|
249
307
|
if (type === 'real') {
|
|
250
|
-
const field = (0, _utils.getInnerField)(schema.obj,
|
|
251
|
-
|
|
252
|
-
// -name - Exclude "name"
|
|
253
|
-
// -user.name - Implies population of "user" but exclude "user.name",
|
|
254
|
-
// so continue traversing into object when part is "user".
|
|
255
|
-
if (isExact && exclude) {
|
|
256
|
-
node['-' + key] = LEAF_NODE;
|
|
257
|
-
} else if (field.ref) {
|
|
308
|
+
const field = (0, _utils.getInnerField)(schema.obj, path);
|
|
309
|
+
if (field.ref) {
|
|
258
310
|
node[key] ||= {};
|
|
259
311
|
setNodePath(node[key], {
|
|
260
312
|
modelName: field.ref,
|
|
261
|
-
path:
|
|
313
|
+
path: fullPath.slice(parts.length),
|
|
262
314
|
depth: depth + 1,
|
|
263
|
-
|
|
315
|
+
excluded,
|
|
316
|
+
exclusive
|
|
264
317
|
});
|
|
265
318
|
halt = true;
|
|
266
319
|
} else if ((0, _utils.isSchemaTypedef)(field)) {
|
|
267
320
|
node[key] = LEAF_NODE;
|
|
268
321
|
}
|
|
269
322
|
} else if (type === 'virtual') {
|
|
270
|
-
const virtual = schema.virtual(
|
|
323
|
+
const virtual = schema.virtual(path);
|
|
271
324
|
// @ts-ignore
|
|
272
325
|
const ref = virtual.options.ref;
|
|
273
326
|
if (ref) {
|
|
274
327
|
node[key] ||= {};
|
|
275
328
|
setNodePath(node[key], {
|
|
276
|
-
// @ts-ignore
|
|
277
329
|
modelName: ref,
|
|
278
|
-
path:
|
|
330
|
+
path: fullPath.slice(parts.length),
|
|
279
331
|
depth: depth + 1,
|
|
280
|
-
|
|
332
|
+
excluded,
|
|
333
|
+
exclusive
|
|
281
334
|
});
|
|
282
335
|
}
|
|
283
336
|
halt = true;
|
|
284
337
|
} else if (type !== 'nested') {
|
|
285
|
-
throw new Error(`Unknown path on ${modelName}: ${
|
|
338
|
+
throw new Error(`Unknown path on ${modelName}: ${path}.`);
|
|
286
339
|
}
|
|
287
340
|
}
|
|
288
341
|
if (halt) {
|
package/dist/cjs/search.js
CHANGED
|
@@ -441,7 +441,7 @@ function getRefField(schema, path) {
|
|
|
441
441
|
for (let i = 1; i < split.length; i++) {
|
|
442
442
|
const base = split.slice(0, i);
|
|
443
443
|
const rest = split.slice(i);
|
|
444
|
-
const type = schema.path(base);
|
|
444
|
+
const type = schema.path(base.join('.'));
|
|
445
445
|
if (type instanceof SchemaTypes.ObjectId) {
|
|
446
446
|
return {
|
|
447
447
|
type,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrockio/model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Bedrock utilities for model creation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@bedrockio/yada": "^1.1.2",
|
|
34
|
-
"mongoose": "^
|
|
34
|
+
"mongoose": "^8.5.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@babel/cli": "^7.20.7",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"jest": "^29.4.1",
|
|
46
46
|
"jest-environment-node": "^29.4.1",
|
|
47
47
|
"mongodb": "^6.5.0",
|
|
48
|
-
"mongoose": "
|
|
48
|
+
"mongoose": "^8.5.1",
|
|
49
49
|
"prettier-eslint": "^15.0.1",
|
|
50
50
|
"typescript": "^4.9.5"
|
|
51
51
|
},
|
package/src/include.js
CHANGED
|
@@ -175,13 +175,19 @@ function nodeToPopulates(node) {
|
|
|
175
175
|
const select = [];
|
|
176
176
|
const populate = [];
|
|
177
177
|
for (let [key, value] of Object.entries(node)) {
|
|
178
|
+
if (key.startsWith('-')) {
|
|
179
|
+
select.push(key);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (key.startsWith('^')) {
|
|
183
|
+
key = key.slice(1);
|
|
184
|
+
select.push(key);
|
|
185
|
+
}
|
|
178
186
|
if (value) {
|
|
179
187
|
populate.push({
|
|
180
188
|
path: key,
|
|
181
189
|
...nodeToPopulates(value),
|
|
182
190
|
});
|
|
183
|
-
} else {
|
|
184
|
-
select.push(key);
|
|
185
191
|
}
|
|
186
192
|
}
|
|
187
193
|
return {
|
|
@@ -202,75 +208,127 @@ function pathsToNode(paths, modelName) {
|
|
|
202
208
|
if (typeof str !== 'string') {
|
|
203
209
|
throw new Error('Provided include path was not as string.');
|
|
204
210
|
}
|
|
205
|
-
let exclude = false;
|
|
206
|
-
if (str.startsWith('-')) {
|
|
207
|
-
exclude = true;
|
|
208
|
-
str = str.slice(1);
|
|
209
|
-
}
|
|
210
211
|
setNodePath(node, {
|
|
211
212
|
path: str.split('.'),
|
|
212
213
|
modelName,
|
|
213
|
-
exclude,
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
return node;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
function setNodePath(node, options) {
|
|
220
|
-
const {
|
|
220
|
+
const { modelName, path: fullPath, depth = 0 } = options;
|
|
221
221
|
if (depth > POPULATE_MAX_DEPTH) {
|
|
222
222
|
throw new Error(`Cannot populate more than ${POPULATE_MAX_DEPTH} levels.`);
|
|
223
223
|
}
|
|
224
|
+
|
|
224
225
|
const schema = mongoose.models[modelName]?.schema;
|
|
225
226
|
if (!schema) {
|
|
226
227
|
throw new Error(`Could not derive schema for ${modelName}.`);
|
|
227
228
|
}
|
|
229
|
+
|
|
230
|
+
let { excluded = false, exclusive = false } = options;
|
|
231
|
+
|
|
228
232
|
const parts = [];
|
|
229
|
-
for (let part of
|
|
233
|
+
for (let part of fullPath) {
|
|
234
|
+
if (part.startsWith('-')) {
|
|
235
|
+
// Field is excluded. Note that this occurs only at
|
|
236
|
+
// top level and should take precedence:
|
|
237
|
+
// -name -> "name" is excluded
|
|
238
|
+
// -user.name -> "user" is populated but "name" is
|
|
239
|
+
// excluded
|
|
240
|
+
excluded = true;
|
|
241
|
+
part = part.slice(1);
|
|
242
|
+
} else if (!excluded && part.startsWith('^')) {
|
|
243
|
+
// Field is exclusive. Note that this can happen at
|
|
244
|
+
// any part of the path:
|
|
245
|
+
// ^name -> "name" is exclusively selected
|
|
246
|
+
// user.^name -> "user" is populated with "name"
|
|
247
|
+
// exclusively selected within
|
|
248
|
+
// ^user.name -> "user" is exclusively selected
|
|
249
|
+
// ("name" is redundant)
|
|
250
|
+
exclusive = true;
|
|
251
|
+
part = part.slice(1);
|
|
252
|
+
} else if (!excluded && part.includes('*')) {
|
|
253
|
+
// Wildcards in field implies exclusion, but only
|
|
254
|
+
// if path is not already excluded:
|
|
255
|
+
// *name -> "firstName" and "lastName" are exclusively
|
|
256
|
+
// selected
|
|
257
|
+
// user.*name -> "user" is populated, "user.firstName"
|
|
258
|
+
// and "user.lastName" are exclusively selected
|
|
259
|
+
// -*name -> "firstName" and "lastName" are excluded
|
|
260
|
+
// -user.*name -> "user" is populated but "user.firstName"
|
|
261
|
+
// and "user.lastName" are excluded
|
|
262
|
+
exclusive = true;
|
|
263
|
+
}
|
|
264
|
+
|
|
230
265
|
parts.push(part);
|
|
231
|
-
const
|
|
232
|
-
|
|
266
|
+
const isExact = parts.length === fullPath.length;
|
|
267
|
+
|
|
233
268
|
let halt = false;
|
|
234
269
|
|
|
235
|
-
for (let [
|
|
270
|
+
for (let [path, type] of resolvePaths(schema, parts.join('.'))) {
|
|
271
|
+
// The exclusive key.
|
|
272
|
+
const eKey = '^' + path;
|
|
273
|
+
// The negative (excluded) key.
|
|
274
|
+
const nKey = '-' + path;
|
|
275
|
+
|
|
276
|
+
let key = path;
|
|
277
|
+
if (exclusive && !node[key]) {
|
|
278
|
+
// Add the exclusive flag if the node
|
|
279
|
+
// has not already been included.
|
|
280
|
+
key = eKey;
|
|
281
|
+
} else if (!exclusive && node[eKey]) {
|
|
282
|
+
// If the node has already been marked exclusive
|
|
283
|
+
// and another include overrides it, then we need
|
|
284
|
+
// to move that node over to be inclusive. This
|
|
285
|
+
// step is needed as includes should always take
|
|
286
|
+
// priority over exclusion regardless of the order.
|
|
287
|
+
node[key] = node[eKey];
|
|
288
|
+
delete node[eKey];
|
|
289
|
+
} else if (excluded && isExact) {
|
|
290
|
+
// Only flag the node as excluded if the path is an
|
|
291
|
+
// exact match:
|
|
292
|
+
// -name -> Exclude "name" field.
|
|
293
|
+
// -user.name -> Exclude "user.name" field, however this
|
|
294
|
+
// implies that we must populate "user" so
|
|
295
|
+
// continue traversing and flag for include.
|
|
296
|
+
key = nKey;
|
|
297
|
+
}
|
|
298
|
+
|
|
236
299
|
if (type === 'real') {
|
|
237
|
-
const field = getInnerField(schema.obj,
|
|
238
|
-
|
|
239
|
-
// -name - Exclude "name"
|
|
240
|
-
// -user.name - Implies population of "user" but exclude "user.name",
|
|
241
|
-
// so continue traversing into object when part is "user".
|
|
242
|
-
if (isExact && exclude) {
|
|
243
|
-
node['-' + key] = LEAF_NODE;
|
|
244
|
-
} else if (field.ref) {
|
|
300
|
+
const field = getInnerField(schema.obj, path);
|
|
301
|
+
if (field.ref) {
|
|
245
302
|
node[key] ||= {};
|
|
246
303
|
setNodePath(node[key], {
|
|
247
304
|
modelName: field.ref,
|
|
248
|
-
path:
|
|
305
|
+
path: fullPath.slice(parts.length),
|
|
249
306
|
depth: depth + 1,
|
|
250
|
-
|
|
307
|
+
excluded,
|
|
308
|
+
exclusive,
|
|
251
309
|
});
|
|
252
310
|
halt = true;
|
|
253
311
|
} else if (isSchemaTypedef(field)) {
|
|
254
312
|
node[key] = LEAF_NODE;
|
|
255
313
|
}
|
|
256
314
|
} else if (type === 'virtual') {
|
|
257
|
-
const virtual = schema.virtual(
|
|
315
|
+
const virtual = schema.virtual(path);
|
|
258
316
|
// @ts-ignore
|
|
259
317
|
const ref = virtual.options.ref;
|
|
260
318
|
|
|
261
319
|
if (ref) {
|
|
262
320
|
node[key] ||= {};
|
|
263
321
|
setNodePath(node[key], {
|
|
264
|
-
// @ts-ignore
|
|
265
322
|
modelName: ref,
|
|
266
|
-
path:
|
|
323
|
+
path: fullPath.slice(parts.length),
|
|
267
324
|
depth: depth + 1,
|
|
268
|
-
|
|
325
|
+
excluded,
|
|
326
|
+
exclusive,
|
|
269
327
|
});
|
|
270
328
|
}
|
|
271
329
|
halt = true;
|
|
272
330
|
} else if (type !== 'nested') {
|
|
273
|
-
throw new Error(`Unknown path on ${modelName}: ${
|
|
331
|
+
throw new Error(`Unknown path on ${modelName}: ${path}.`);
|
|
274
332
|
}
|
|
275
333
|
}
|
|
276
334
|
|
package/src/search.js
CHANGED
|
@@ -473,7 +473,7 @@ function getRefField(schema, path) {
|
|
|
473
473
|
for (let i = 1; i < split.length; i++) {
|
|
474
474
|
const base = split.slice(0, i);
|
|
475
475
|
const rest = split.slice(i);
|
|
476
|
-
const type = schema.path(base);
|
|
476
|
+
const type = schema.path(base.join('.'));
|
|
477
477
|
if (type instanceof SchemaTypes.ObjectId) {
|
|
478
478
|
return {
|
|
479
479
|
type,
|
package/types/load.d.ts
CHANGED
|
@@ -4,73 +4,7 @@
|
|
|
4
4
|
* @param {string} name
|
|
5
5
|
* @returns mongoose.Model
|
|
6
6
|
*/
|
|
7
|
-
export function loadModel(definition: object, name: string):
|
|
8
|
-
[x: string]: any;
|
|
9
|
-
}, {
|
|
10
|
-
autoIndex?: boolean;
|
|
11
|
-
autoCreate?: boolean;
|
|
12
|
-
bufferCommands?: boolean;
|
|
13
|
-
bufferTimeoutMS?: number;
|
|
14
|
-
capped?: number | boolean | {
|
|
15
|
-
size?: number;
|
|
16
|
-
max?: number;
|
|
17
|
-
autoIndexId?: boolean;
|
|
18
|
-
};
|
|
19
|
-
collation?: mongoose.mongo.CollationOptions;
|
|
20
|
-
collectionOptions?: mongoose.mongo.CreateCollectionOptions;
|
|
21
|
-
timeseries?: mongoose.mongo.TimeSeriesCollectionOptions;
|
|
22
|
-
expireAfterSeconds?: number;
|
|
23
|
-
expires?: string | number;
|
|
24
|
-
collection?: string;
|
|
25
|
-
discriminatorKey?: string;
|
|
26
|
-
excludeIndexes?: boolean;
|
|
27
|
-
id?: boolean;
|
|
28
|
-
_id?: boolean;
|
|
29
|
-
minimize?: boolean;
|
|
30
|
-
optimisticConcurrency?: boolean;
|
|
31
|
-
pluginTags?: string[];
|
|
32
|
-
read?: string;
|
|
33
|
-
writeConcern?: mongoose.mongo.WriteConcern;
|
|
34
|
-
safe?: boolean | {
|
|
35
|
-
w?: string | number;
|
|
36
|
-
wtimeout?: number;
|
|
37
|
-
j?: boolean;
|
|
38
|
-
};
|
|
39
|
-
shardKey?: Record<string, unknown>;
|
|
40
|
-
strict?: boolean | "throw";
|
|
41
|
-
strictQuery?: boolean | "throw";
|
|
42
|
-
toJSON: {
|
|
43
|
-
getters: boolean;
|
|
44
|
-
versionKey: boolean;
|
|
45
|
-
transform: (doc: any, ret: any, options: any) => void;
|
|
46
|
-
} | mongoose.ToObjectOptions<any>;
|
|
47
|
-
toObject: {
|
|
48
|
-
getters: boolean;
|
|
49
|
-
versionKey: boolean;
|
|
50
|
-
transform: (doc: any, ret: any, options: any) => void;
|
|
51
|
-
} | mongoose.ToObjectOptions<any>;
|
|
52
|
-
typeKey?: string;
|
|
53
|
-
validateBeforeSave?: boolean;
|
|
54
|
-
validateModifiedOnly?: boolean;
|
|
55
|
-
versionKey?: string | boolean;
|
|
56
|
-
selectPopulatedPaths?: boolean;
|
|
57
|
-
skipVersioning?: {
|
|
58
|
-
[key: string]: boolean;
|
|
59
|
-
};
|
|
60
|
-
storeSubdocValidationError?: boolean;
|
|
61
|
-
timestamps: boolean | mongoose.SchemaTimestampsConfig;
|
|
62
|
-
suppressReservedKeysWarning?: boolean;
|
|
63
|
-
statics?: {
|
|
64
|
-
[x: string]: any;
|
|
65
|
-
};
|
|
66
|
-
methods?: any;
|
|
67
|
-
query?: any;
|
|
68
|
-
castNonArrays?: boolean;
|
|
69
|
-
virtuals?: mongoose.SchemaOptionsVirtualsPropertyType<any, any, any>;
|
|
70
|
-
overwriteModels?: boolean;
|
|
71
|
-
}, any, any>> & {
|
|
72
|
-
[x: string]: any;
|
|
73
|
-
};
|
|
7
|
+
export function loadModel(definition: object, name: string): any;
|
|
74
8
|
/**
|
|
75
9
|
* Loads all model definitions in the given directory.
|
|
76
10
|
* Returns the full loaded model set.
|
package/types/load.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,sCAJW,MAAM,QACN,MAAM
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,sCAJW,MAAM,QACN,MAAM,OAahB;AAED;;;;GAIG;AACH,sCAFW,MAAM,mBAkBhB"}
|
package/types/schema.d.ts
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* @param {mongoose.SchemaOptions} options
|
|
7
7
|
* @returns mongoose.Schema
|
|
8
8
|
*/
|
|
9
|
-
export function createSchema(definition: object, options?: mongoose.SchemaOptions): mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, any, any, any, {
|
|
10
|
-
[x: string]: any;
|
|
11
|
-
}, {
|
|
9
|
+
export function createSchema(definition: object, options?: mongoose.SchemaOptions): mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, any, any, any, any, {
|
|
12
10
|
autoIndex?: boolean;
|
|
11
|
+
autoSearchIndex?: boolean;
|
|
13
12
|
autoCreate?: boolean;
|
|
14
13
|
bufferCommands?: boolean;
|
|
15
14
|
bufferTimeoutMS?: number;
|
|
@@ -32,6 +31,9 @@ export function createSchema(definition: object, options?: mongoose.SchemaOption
|
|
|
32
31
|
optimisticConcurrency?: boolean;
|
|
33
32
|
pluginTags?: string[];
|
|
34
33
|
read?: string;
|
|
34
|
+
readConcern?: {
|
|
35
|
+
level: "local" | "available" | "majority" | "snapshot" | "linearizable";
|
|
36
|
+
};
|
|
35
37
|
writeConcern?: mongoose.mongo.WriteConcern;
|
|
36
38
|
safe?: boolean | {
|
|
37
39
|
w?: string | number;
|
|
@@ -62,10 +64,8 @@ export function createSchema(definition: object, options?: mongoose.SchemaOption
|
|
|
62
64
|
storeSubdocValidationError?: boolean;
|
|
63
65
|
timestamps: boolean | mongoose.SchemaTimestampsConfig;
|
|
64
66
|
suppressReservedKeysWarning?: boolean;
|
|
65
|
-
statics?: {
|
|
66
|
-
|
|
67
|
-
};
|
|
68
|
-
methods?: any;
|
|
67
|
+
statics?: mongoose.AddThisParameter<any, mongoose.Model<any, {}, {}, {}, any, any>>;
|
|
68
|
+
methods?: mongoose.AddThisParameter<any, any> & mongoose.AnyObject;
|
|
69
69
|
query?: any;
|
|
70
70
|
castNonArrays?: boolean;
|
|
71
71
|
virtuals?: mongoose.SchemaOptionsVirtualsPropertyType<any, any, any>;
|