@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 CHANGED
@@ -47,7 +47,7 @@ npm install @vulkano/core
47
47
  PORT=8000
48
48
  MONGO_URI=mongodb://localhost:27017/myapp
49
49
  SALT_KEY=random-string
50
- JWT_SECRET=supersecret
50
+ JWT_SECRET_KEY=supersecret
51
51
  ```
52
52
 
53
53
  ## Quick Start
package/bin/setup.js CHANGED
@@ -422,7 +422,7 @@ module.exports = {
422
422
  ' .env ← fill in values',
423
423
  '',
424
424
  ' Next steps:',
425
- ' 1. Set MONGO_URI, JWT_SECRET, etc.',
425
+ ' 1. Set MONGO_URI, JWT_SECRET_KEY, etc.',
426
426
  '',
427
427
  ].join('\n'));
428
428
 
@@ -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
- return this.findOne(toSearch)
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
- const populate = [];
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
- return Example.findOne({ _id })
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vulkano/core",
3
- "version": "1.23.1",
3
+ "version": "1.24.0",
4
4
  "description": "A MVC framework using Express 4",
5
5
  "license": "MIT",
6
6
  "author": "Vulkano Team",