@vulkano/core 1.23.1 → 1.24.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 +1 -1
- package/bin/setup.js +1 -1
- package/database/scaffold.js +125 -5
- package/examples/models/Example.js +26 -6
- package/package.json +1 -1
package/README.md
CHANGED
package/bin/setup.js
CHANGED
package/database/scaffold.js
CHANGED
|
@@ -1,9 +1,122 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Parse the populate param into one entry per relation: `name` (trimmed,
|
|
5
|
+
* lowercased) plus an optional `fields` list. Syntax: relations are
|
|
6
|
+
* comma-separated; a relation can carry its own field list after a colon,
|
|
7
|
+
* with `|` separating multiple fields — e.g.
|
|
8
|
+
* `?populate=school:name|address,teacher` populates `school` (selecting
|
|
9
|
+
* only name+address) and `teacher` (full doc). Kept as its own param
|
|
10
|
+
* (not `?school=...`) so it can never collide with a real filter/field
|
|
11
|
+
* query param that happens to share the relation's name.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} props (populate)
|
|
14
|
+
* @returns {Array} [{ name, fields }]
|
|
15
|
+
*/
|
|
16
|
+
_parsePopulateEntries(props) {
|
|
17
|
+
|
|
18
|
+
return ((props || {}).populate || '')
|
|
19
|
+
.split(',')
|
|
20
|
+
.map((raw) => {
|
|
21
|
+
|
|
22
|
+
const [rawName, rawFields] = raw.split(':');
|
|
23
|
+
const name = (rawName || '').trim().toLowerCase();
|
|
24
|
+
|
|
25
|
+
if (!name) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fields = rawFields
|
|
30
|
+
? rawFields.split('|').map((item) => item.trim()).filter(Boolean)
|
|
31
|
+
: null;
|
|
32
|
+
|
|
33
|
+
return { name, fields };
|
|
34
|
+
|
|
35
|
+
})
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sanitize populate param: trim + lowercase each relation name
|
|
42
|
+
* (field-selection suffix, if any, is dropped — see _parsePopulateEntries)
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} props (populate)
|
|
45
|
+
* @returns {Array}
|
|
46
|
+
*/
|
|
47
|
+
_getSanitizedPopulate(props) {
|
|
48
|
+
|
|
49
|
+
return this._parsePopulateEntries(props).map((entry) => entry.name);
|
|
50
|
+
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build populate array by auto-detecting the model's own relations that
|
|
55
|
+
* were opted in — either via `autopopulate: true` on the attribute
|
|
56
|
+
* definition, or via the `extra` allowlist passed here for a one-off call.
|
|
57
|
+
* A ref field with neither is never populated, no matter what the caller
|
|
58
|
+
* asks for. This is the security gate: a relation only becomes reachable
|
|
59
|
+
* through ?populate= when its attribute or the calling code says so.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} props (populate — see _parsePopulateEntries for its syntax)
|
|
62
|
+
* @param {Array} [extra] field names to allow even without autopopulate: true
|
|
63
|
+
* @returns {Array}
|
|
64
|
+
*/
|
|
65
|
+
_buildPopulate(props, extra) {
|
|
66
|
+
|
|
67
|
+
const entries = this._parsePopulateEntries(props);
|
|
68
|
+
|
|
69
|
+
if (entries.length === 0) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const extraLower = (Array.isArray(extra) ? extra : []).map((item) => item.toLowerCase());
|
|
74
|
+
|
|
75
|
+
const { paths } = this.schema;
|
|
76
|
+
|
|
77
|
+
const fieldByLowerName = {};
|
|
78
|
+
Object.keys(paths).forEach((field) => {
|
|
79
|
+
fieldByLowerName[field.toLowerCase()] = field;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return entries
|
|
83
|
+
.map((entry) => {
|
|
84
|
+
|
|
85
|
+
const field = fieldByLowerName[entry.name];
|
|
86
|
+
|
|
87
|
+
if (!field) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const path = paths[field];
|
|
92
|
+
const opts = path.options || {};
|
|
93
|
+
const casterOpts = (path.caster && path.caster.options) || {};
|
|
94
|
+
const ref = opts.ref || casterOpts.ref;
|
|
95
|
+
const autopopulate = opts.autopopulate
|
|
96
|
+
|| casterOpts.autopopulate
|
|
97
|
+
|| extraLower.includes(entry.name);
|
|
98
|
+
|
|
99
|
+
if (!ref || !autopopulate) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const populateProps = { path: field };
|
|
104
|
+
|
|
105
|
+
if (entry.fields && entry.fields.length > 0) {
|
|
106
|
+
populateProps.select = entry.fields.join(' ');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return populateProps;
|
|
110
|
+
|
|
111
|
+
})
|
|
112
|
+
.filter(Boolean);
|
|
113
|
+
|
|
114
|
+
},
|
|
115
|
+
|
|
3
116
|
/**
|
|
4
117
|
* Method to get all records by page
|
|
5
118
|
*
|
|
6
|
-
* @param {Object} props (page, perPage, search, sort)
|
|
119
|
+
* @param {Object} props (page, perPage, search, sort, populate)
|
|
7
120
|
* @returns {Promise}
|
|
8
121
|
*/
|
|
9
122
|
getAll(props) {
|
|
@@ -13,15 +126,17 @@ module.exports = {
|
|
|
13
126
|
sort: 'createdAt|DESC',
|
|
14
127
|
searchBy: [],
|
|
15
128
|
filter: {
|
|
16
|
-
active: true
|
|
129
|
+
active: true // soft-delete
|
|
17
130
|
},
|
|
18
131
|
};
|
|
19
132
|
|
|
133
|
+
const populate = this._buildPopulate(props);
|
|
134
|
+
|
|
20
135
|
// Query to Run
|
|
21
136
|
const query = Paginate.serializeQuery(defaultProps, props);
|
|
22
137
|
|
|
23
138
|
// Pagination
|
|
24
|
-
return Paginate.get(this, query);
|
|
139
|
+
return Paginate.get(this, query, populate);
|
|
25
140
|
|
|
26
141
|
},
|
|
27
142
|
|
|
@@ -29,9 +144,10 @@ module.exports = {
|
|
|
29
144
|
* Method to get a record by id
|
|
30
145
|
*
|
|
31
146
|
* @param {ObjectID} id
|
|
147
|
+
* @param {Object} props (populate)
|
|
32
148
|
* @returns {Promise}
|
|
33
149
|
*/
|
|
34
|
-
getByField(value, field) {
|
|
150
|
+
getByField(value, field, props) {
|
|
35
151
|
|
|
36
152
|
// This is to prevent error while run the findOne
|
|
37
153
|
if (!(/^[a-fA-F0-9]{24}$/).test(value) && !field) {
|
|
@@ -41,7 +157,11 @@ module.exports = {
|
|
|
41
157
|
const toSearch = { active: true };
|
|
42
158
|
toSearch[field || '_id'] = value;
|
|
43
159
|
|
|
44
|
-
|
|
160
|
+
const query = this.findOne(toSearch);
|
|
161
|
+
|
|
162
|
+
this._buildPopulate(props).forEach((p) => query.populate(p));
|
|
163
|
+
|
|
164
|
+
return query
|
|
45
165
|
.then( (r) => {
|
|
46
166
|
|
|
47
167
|
if (!r) {
|
|
@@ -18,6 +18,13 @@ module.exports = {
|
|
|
18
18
|
type: String,
|
|
19
19
|
required: true
|
|
20
20
|
},
|
|
21
|
+
school: {
|
|
22
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
23
|
+
ref: 'School',
|
|
24
|
+
// Opts this relation into ?populate=school — a ref field without this
|
|
25
|
+
// flag is never populated, no matter what the caller asks for
|
|
26
|
+
autopopulate: true
|
|
27
|
+
},
|
|
21
28
|
age: {
|
|
22
29
|
type: Number,
|
|
23
30
|
required: false,
|
|
@@ -44,7 +51,7 @@ module.exports = {
|
|
|
44
51
|
/**
|
|
45
52
|
* Method to get all records by page
|
|
46
53
|
*
|
|
47
|
-
* @param {Object} props (page, perPage, search, sort)
|
|
54
|
+
* @param {Object} props (page, perPage, search, sort, populate — see _buildPopulate below)
|
|
48
55
|
* @returns {Promise}
|
|
49
56
|
*/
|
|
50
57
|
getAll(props) {
|
|
@@ -53,14 +60,21 @@ module.exports = {
|
|
|
53
60
|
const defaultProps = {
|
|
54
61
|
sort: 'createdAt|DESC',
|
|
55
62
|
searchBy: ['name'],
|
|
56
|
-
fields: ['name', 'age', 'active', 'createdAt', 'updatedAt'],
|
|
63
|
+
fields: ['name', 'school', 'age', 'active', 'createdAt', 'updatedAt'],
|
|
57
64
|
filter: {
|
|
58
65
|
active: true
|
|
59
66
|
},
|
|
60
67
|
};
|
|
61
68
|
|
|
62
|
-
// Populate
|
|
63
|
-
|
|
69
|
+
// Populate: only relations opted in via `autopopulate: true` on the
|
|
70
|
+
// attribute (see `school` above) ever get expanded, and only when asked
|
|
71
|
+
// for through ?populate=. Syntax (see database/scaffold.js#_buildPopulate):
|
|
72
|
+
// ?populate=school → full School doc
|
|
73
|
+
// ?populate=school:name → only { _id, name }
|
|
74
|
+
// ?populate=school:name|address → only { _id, name, address }
|
|
75
|
+
// A relation NOT marked autopopulate can still be opened for one call by
|
|
76
|
+
// passing it as a second arg: this._buildPopulate(props, ['someRef'])
|
|
77
|
+
const populate = this._buildPopulate(props);
|
|
64
78
|
|
|
65
79
|
// Query to Run
|
|
66
80
|
const query = Paginate.serializeQuery(defaultProps, props);
|
|
@@ -74,16 +88,22 @@ module.exports = {
|
|
|
74
88
|
* Method to get a record by id
|
|
75
89
|
*
|
|
76
90
|
* @param {ObjectID} id
|
|
91
|
+
* @param {Object} _props (populate — see getAll above for the ?populate= syntax)
|
|
77
92
|
* @returns {Promise}
|
|
78
93
|
*/
|
|
79
|
-
getExample(_id) {
|
|
94
|
+
getExample(_id, _props) {
|
|
80
95
|
|
|
81
96
|
// This is to prevent error while run the findOne
|
|
82
97
|
if (!(/^[a-fA-F0-9]{24}$/).test(_id)) {
|
|
83
98
|
return VSError.reject('Invalid ID. Record not found', 404);
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
|
|
101
|
+
const query = Example.findOne({ _id });
|
|
102
|
+
|
|
103
|
+
// Populate (only relations marked autopopulate: true in attributes)
|
|
104
|
+
this._buildPopulate(_props).forEach((p) => query.populate(p));
|
|
105
|
+
|
|
106
|
+
return query
|
|
87
107
|
.then( (r) => {
|
|
88
108
|
|
|
89
109
|
if (!r) {
|