@strapi/utils 4.0.0-next.7 → 4.0.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/lib/build-query.js +1 -1
- package/lib/config.js +6 -3
- package/lib/content-types.js +21 -9
- package/lib/convert-query-params.js +254 -0
- package/lib/errors.js +82 -0
- package/lib/format-yup-error.js +20 -0
- package/lib/hooks.js +5 -3
- package/lib/index.js +14 -14
- package/lib/pagination.js +100 -0
- package/lib/parse-multipart.js +7 -3
- package/lib/pipe-async.js +11 -0
- package/lib/policy.js +74 -119
- package/lib/print-value.js +51 -0
- package/lib/sanitize/index.js +51 -0
- package/lib/sanitize/sanitizers.js +26 -0
- package/lib/sanitize/visitors/allowed-fields.js +92 -0
- package/lib/sanitize/visitors/index.js +9 -0
- package/lib/sanitize/visitors/remove-password.js +7 -0
- package/lib/sanitize/visitors/remove-private.js +11 -0
- package/lib/sanitize/visitors/remove-restricted-relations.js +67 -0
- package/lib/sanitize/visitors/restricted-fields.js +31 -0
- package/lib/traverse-entity.js +100 -0
- package/lib/validators.js +45 -16
- package/package.json +24 -24
- package/lib/convert-rest-query-params.js +0 -245
- package/lib/finder.js +0 -29
- package/lib/sanitize-entity.js +0 -172
package/lib/build-query.js
CHANGED
|
@@ -60,7 +60,7 @@ const getAssociationFromFieldKey = ({ model, field }) => {
|
|
|
60
60
|
const castInput = ({ type, value, operator }) => {
|
|
61
61
|
return Array.isArray(value)
|
|
62
62
|
? value.map(val => castValue({ type, operator, value: val }))
|
|
63
|
-
: castValue({ type, operator, value
|
|
63
|
+
: castValue({ type, operator, value });
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
/**
|
package/lib/config.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const { getCommonPath } = require('./string-formatting');
|
|
5
5
|
|
|
6
|
-
const getConfigUrls = (
|
|
6
|
+
const getConfigUrls = (config, forAdminBuild = false) => {
|
|
7
|
+
const serverConfig = config.get('server');
|
|
8
|
+
const adminConfig = config.get('admin');
|
|
9
|
+
|
|
7
10
|
// Defines serverUrl value
|
|
8
11
|
let serverUrl = _.get(serverConfig, 'url', '');
|
|
9
12
|
serverUrl = _.trim(serverUrl, '/ ');
|
|
@@ -23,7 +26,7 @@ const getConfigUrls = (serverConfig, forAdminBuild = false) => {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
// Defines adminUrl value
|
|
26
|
-
let adminUrl = _.get(
|
|
29
|
+
let adminUrl = _.get(adminConfig, 'url', '/admin');
|
|
27
30
|
adminUrl = _.trim(adminUrl, '/ ');
|
|
28
31
|
if (typeof adminUrl !== 'string') {
|
|
29
32
|
throw new Error('Invalid admin url config. Make sure the url is a non-empty string.');
|
|
@@ -60,7 +63,7 @@ const getConfigUrls = (serverConfig, forAdminBuild = false) => {
|
|
|
60
63
|
};
|
|
61
64
|
|
|
62
65
|
const getAbsoluteUrl = adminOrServer => (config, forAdminBuild = false) => {
|
|
63
|
-
const { serverUrl, adminUrl } = getConfigUrls(config
|
|
66
|
+
const { serverUrl, adminUrl } = getConfigUrls(config, forAdminBuild);
|
|
64
67
|
let url = adminOrServer === 'server' ? serverUrl : adminUrl;
|
|
65
68
|
|
|
66
69
|
if (url.startsWith('http')) {
|
package/lib/content-types.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
|
+
const { has } = require('lodash/fp');
|
|
4
5
|
|
|
5
6
|
const SINGLE_TYPE = 'singleType';
|
|
6
7
|
const COLLECTION_TYPE = 'collectionType';
|
|
7
8
|
|
|
8
9
|
const ID_ATTRIBUTE = 'id';
|
|
9
|
-
const PUBLISHED_AT_ATTRIBUTE = '
|
|
10
|
-
const CREATED_BY_ATTRIBUTE = '
|
|
11
|
-
const UPDATED_BY_ATTRIBUTE = '
|
|
10
|
+
const PUBLISHED_AT_ATTRIBUTE = 'publishedAt';
|
|
11
|
+
const CREATED_BY_ATTRIBUTE = 'createdBy';
|
|
12
|
+
const UPDATED_BY_ATTRIBUTE = 'updatedBy';
|
|
12
13
|
|
|
13
|
-
const CREATED_AT_ATTRIBUTE = '
|
|
14
|
-
const UPDATED_AT_ATTRIBUTE = '
|
|
14
|
+
const CREATED_AT_ATTRIBUTE = 'createdAt';
|
|
15
|
+
const UPDATED_AT_ATTRIBUTE = 'updatedAt';
|
|
15
16
|
|
|
16
17
|
const DP_PUB_STATE_LIVE = 'live';
|
|
17
18
|
const DP_PUB_STATE_PREVIEW = 'preview';
|
|
@@ -31,8 +32,18 @@ const constants = {
|
|
|
31
32
|
COLLECTION_TYPE,
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
const getTimestamps =
|
|
35
|
-
|
|
35
|
+
const getTimestamps = model => {
|
|
36
|
+
const attributes = [];
|
|
37
|
+
|
|
38
|
+
if (has(CREATED_AT_ATTRIBUTE, model.attributes)) {
|
|
39
|
+
attributes.push(CREATED_AT_ATTRIBUTE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (has(UPDATED_AT_ATTRIBUTE, model.attributes)) {
|
|
43
|
+
attributes.push(UPDATED_AT_ATTRIBUTE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return attributes;
|
|
36
47
|
};
|
|
37
48
|
|
|
38
49
|
const getNonWritableAttributes = (model = {}) => {
|
|
@@ -42,7 +53,7 @@ const getNonWritableAttributes = (model = {}) => {
|
|
|
42
53
|
[]
|
|
43
54
|
);
|
|
44
55
|
|
|
45
|
-
return _.uniq([ID_ATTRIBUTE, ...getTimestamps(), ...nonWritableAttributes]);
|
|
56
|
+
return _.uniq([ID_ATTRIBUTE, ...getTimestamps(model), ...nonWritableAttributes]);
|
|
46
57
|
};
|
|
47
58
|
|
|
48
59
|
const getWritableAttributes = (model = {}) => {
|
|
@@ -60,7 +71,7 @@ const getNonVisibleAttributes = model => {
|
|
|
60
71
|
[]
|
|
61
72
|
);
|
|
62
73
|
|
|
63
|
-
return _.uniq([ID_ATTRIBUTE, ...getTimestamps(), ...nonVisibleAttributes]);
|
|
74
|
+
return _.uniq([ID_ATTRIBUTE, ...getTimestamps(model), ...nonVisibleAttributes]);
|
|
64
75
|
};
|
|
65
76
|
|
|
66
77
|
const getVisibleAttributes = model => {
|
|
@@ -137,6 +148,7 @@ module.exports = {
|
|
|
137
148
|
isWritableAttribute,
|
|
138
149
|
getNonVisibleAttributes,
|
|
139
150
|
getVisibleAttributes,
|
|
151
|
+
getTimestamps,
|
|
140
152
|
isVisibleAttribute,
|
|
141
153
|
hasDraftAndPublish,
|
|
142
154
|
isDraft,
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts the standard Strapi REST query params to a more usable format for querying
|
|
5
|
+
* You can read more here: https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#filters
|
|
6
|
+
*/
|
|
7
|
+
const { has } = require('lodash/fp');
|
|
8
|
+
const _ = require('lodash');
|
|
9
|
+
const parseType = require('./parse-type');
|
|
10
|
+
const contentTypesUtils = require('./content-types');
|
|
11
|
+
|
|
12
|
+
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
|
13
|
+
|
|
14
|
+
class InvalidOrderError extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.message = 'Invalid order. order can only be one of asc|desc|ASC|DESC';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class InvalidSortError extends Error {
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this.message =
|
|
24
|
+
'Invalid sort parameter. Expected a string, an array of strings, a sort object or an array of sort objects';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const validateOrder = order => {
|
|
29
|
+
if (!['asc', 'desc'].includes(order.toLocaleLowerCase())) {
|
|
30
|
+
throw new InvalidOrderError();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const convertCountQueryParams = countQuery => {
|
|
35
|
+
return parseType({ type: 'boolean', value: countQuery });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sort query parser
|
|
40
|
+
* @param {string} sortQuery - ex: id:asc,price:desc
|
|
41
|
+
*/
|
|
42
|
+
const convertSortQueryParams = sortQuery => {
|
|
43
|
+
if (typeof sortQuery === 'string') {
|
|
44
|
+
return sortQuery.split(',').map(value => convertSingleSortQueryParam(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(sortQuery)) {
|
|
48
|
+
return sortQuery.flatMap(sortValue => convertSortQueryParams(sortValue));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (_.isPlainObject(sortQuery)) {
|
|
52
|
+
return convertNestedSortQueryParam(sortQuery);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new InvalidSortError();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const convertSingleSortQueryParam = sortQuery => {
|
|
59
|
+
// split field and order param with default order to ascending
|
|
60
|
+
const [field, order = 'asc'] = sortQuery.split(':');
|
|
61
|
+
|
|
62
|
+
if (field.length === 0) {
|
|
63
|
+
throw new Error('Field cannot be empty');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
validateOrder(order);
|
|
67
|
+
|
|
68
|
+
return _.set({}, field, order);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const convertNestedSortQueryParam = sortQuery => {
|
|
72
|
+
const transformedSort = {};
|
|
73
|
+
for (const field in sortQuery) {
|
|
74
|
+
const order = sortQuery[field];
|
|
75
|
+
|
|
76
|
+
// this is a deep sort
|
|
77
|
+
if (_.isPlainObject(order)) {
|
|
78
|
+
transformedSort[field] = convertNestedSortQueryParam(order);
|
|
79
|
+
} else {
|
|
80
|
+
validateOrder(order);
|
|
81
|
+
transformedSort[field] = order;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return transformedSort;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Start query parser
|
|
90
|
+
* @param {string} startQuery
|
|
91
|
+
*/
|
|
92
|
+
const convertStartQueryParams = startQuery => {
|
|
93
|
+
const startAsANumber = _.toNumber(startQuery);
|
|
94
|
+
|
|
95
|
+
if (!_.isInteger(startAsANumber) || startAsANumber < 0) {
|
|
96
|
+
throw new Error(`convertStartQueryParams expected a positive integer got ${startAsANumber}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return startAsANumber;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Limit query parser
|
|
104
|
+
* @param {string} limitQuery
|
|
105
|
+
*/
|
|
106
|
+
const convertLimitQueryParams = limitQuery => {
|
|
107
|
+
const limitAsANumber = _.toNumber(limitQuery);
|
|
108
|
+
|
|
109
|
+
if (!_.isInteger(limitAsANumber) || (limitAsANumber !== -1 && limitAsANumber < 0)) {
|
|
110
|
+
throw new Error(`convertLimitQueryParams expected a positive integer got ${limitAsANumber}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (limitAsANumber === -1) return null;
|
|
114
|
+
|
|
115
|
+
return limitAsANumber;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
class InvalidPopulateError extends Error {
|
|
119
|
+
constructor() {
|
|
120
|
+
super();
|
|
121
|
+
this.message =
|
|
122
|
+
'Invalid populate parameter. Expected a string, an array of strings, a populate object';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// NOTE: we could support foo.* or foo.bar.* etc later on
|
|
127
|
+
const convertPopulateQueryParams = (populate, depth = 0) => {
|
|
128
|
+
if (depth === 0 && populate === '*') {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof populate === 'string') {
|
|
133
|
+
return populate.split(',').map(value => _.trim(value));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (Array.isArray(populate)) {
|
|
137
|
+
// map convert
|
|
138
|
+
return _.uniq(
|
|
139
|
+
populate.flatMap(value => {
|
|
140
|
+
if (typeof value !== 'string') {
|
|
141
|
+
throw new InvalidPopulateError();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return value.split(',').map(value => _.trim(value));
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (_.isPlainObject(populate)) {
|
|
150
|
+
const transformedPopulate = {};
|
|
151
|
+
for (const key in populate) {
|
|
152
|
+
transformedPopulate[key] = convertNestedPopulate(populate[key]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return transformedPopulate;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw new InvalidPopulateError();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const convertNestedPopulate = subPopulate => {
|
|
162
|
+
if (subPopulate === '*') {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (_.isBoolean(subPopulate)) {
|
|
167
|
+
return subPopulate;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!_.isPlainObject(subPopulate)) {
|
|
171
|
+
throw new Error(`Invalid nested populate. Expected '*' or an object`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// TODO: We will need to consider a way to add limitation / pagination
|
|
175
|
+
const { sort, filters, fields, populate, count } = subPopulate;
|
|
176
|
+
|
|
177
|
+
const query = {};
|
|
178
|
+
|
|
179
|
+
if (sort) {
|
|
180
|
+
query.orderBy = convertSortQueryParams(sort);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (filters) {
|
|
184
|
+
query.where = convertFiltersQueryParams(filters);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (fields) {
|
|
188
|
+
query.select = convertFieldsQueryParams(fields);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (populate) {
|
|
192
|
+
query.populate = convertPopulateQueryParams(populate);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (count) {
|
|
196
|
+
query.count = convertCountQueryParams(count);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return query;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const convertFieldsQueryParams = (fields, depth = 0) => {
|
|
203
|
+
if (depth === 0 && fields === '*') {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (typeof fields === 'string') {
|
|
208
|
+
const fieldsValues = fields.split(',').map(value => _.trim(value));
|
|
209
|
+
return _.uniq(['id', ...fieldsValues]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (Array.isArray(fields)) {
|
|
213
|
+
// map convert
|
|
214
|
+
const fieldsValues = fields.flatMap(value => convertFieldsQueryParams(value, depth + 1));
|
|
215
|
+
return _.uniq(['id', ...fieldsValues]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
throw new Error('Invalid fields parameter. Expected a string or an array of strings');
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const convertFiltersQueryParams = filters => filters;
|
|
222
|
+
|
|
223
|
+
const convertPublicationStateParams = (type, params = {}, query = {}) => {
|
|
224
|
+
if (!type) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { publicationState } = params;
|
|
229
|
+
|
|
230
|
+
if (!_.isNil(publicationState)) {
|
|
231
|
+
if (!contentTypesUtils.constants.DP_PUB_STATES.includes(publicationState)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Invalid publicationState. Expected one of 'preview','live' received: ${publicationState}.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// NOTE: this is the query layer filters not the entity service filters
|
|
238
|
+
query.filters = ({ meta }) => {
|
|
239
|
+
if (publicationState === 'live' && has(PUBLISHED_AT_ATTRIBUTE, meta.attributes)) {
|
|
240
|
+
return { [PUBLISHED_AT_ATTRIBUTE]: { $notNull: true } };
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
convertSortQueryParams,
|
|
248
|
+
convertStartQueryParams,
|
|
249
|
+
convertLimitQueryParams,
|
|
250
|
+
convertPopulateQueryParams,
|
|
251
|
+
convertFiltersQueryParams,
|
|
252
|
+
convertFieldsQueryParams,
|
|
253
|
+
convertPublicationStateParams,
|
|
254
|
+
};
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { HttpError } = require('http-errors');
|
|
4
|
+
const { formatYupErrors } = require('./format-yup-error');
|
|
5
|
+
|
|
6
|
+
/* ApplicationError */
|
|
7
|
+
class ApplicationError extends Error {
|
|
8
|
+
constructor(message, details = {}) {
|
|
9
|
+
super();
|
|
10
|
+
this.name = 'ApplicationError';
|
|
11
|
+
this.message = message || 'An application error occured';
|
|
12
|
+
this.details = details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class ValidationError extends ApplicationError {
|
|
17
|
+
constructor(message, details) {
|
|
18
|
+
super(message, details);
|
|
19
|
+
this.name = 'ValidationError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class YupValidationError extends ValidationError {
|
|
24
|
+
constructor(yupError, message) {
|
|
25
|
+
super();
|
|
26
|
+
const { errors, message: yupMessage } = formatYupErrors(yupError);
|
|
27
|
+
this.message = message || yupMessage;
|
|
28
|
+
this.details = { errors };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class PaginationError extends ApplicationError {
|
|
33
|
+
constructor(message, details) {
|
|
34
|
+
super(message, details);
|
|
35
|
+
this.name = 'PaginationError';
|
|
36
|
+
this.message = message || 'Invalid pagination';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class NotFoundError extends ApplicationError {
|
|
41
|
+
constructor(message, details) {
|
|
42
|
+
super(message, details);
|
|
43
|
+
this.name = 'NotFoundError';
|
|
44
|
+
this.message = message || 'Entity not found';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class ForbiddenError extends ApplicationError {
|
|
49
|
+
constructor(message, details) {
|
|
50
|
+
super(message, details);
|
|
51
|
+
this.name = 'ForbiddenError';
|
|
52
|
+
this.message = message || 'Forbidden access';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class PayloadTooLargeError extends ApplicationError {
|
|
57
|
+
constructor(message, details) {
|
|
58
|
+
super(message, details);
|
|
59
|
+
this.name = 'PayloadTooLargeError';
|
|
60
|
+
this.message = message || 'Entity too large';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class UnauthorizedError extends ApplicationError {
|
|
65
|
+
constructor(message, details) {
|
|
66
|
+
super(message, details);
|
|
67
|
+
this.name = 'UnauthorizedError';
|
|
68
|
+
this.message = message || 'Unauthorized';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
HttpError,
|
|
74
|
+
ApplicationError,
|
|
75
|
+
ValidationError,
|
|
76
|
+
YupValidationError,
|
|
77
|
+
PaginationError,
|
|
78
|
+
NotFoundError,
|
|
79
|
+
ForbiddenError,
|
|
80
|
+
PayloadTooLargeError,
|
|
81
|
+
UnauthorizedError,
|
|
82
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isEmpty, toPath } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const formatYupInnerError = yupError => ({
|
|
6
|
+
path: toPath(yupError.path),
|
|
7
|
+
message: yupError.message,
|
|
8
|
+
name: yupError.name,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const formatYupErrors = yupError => ({
|
|
12
|
+
errors: isEmpty(yupError.inner)
|
|
13
|
+
? [formatYupInnerError(yupError)]
|
|
14
|
+
: yupError.inner.map(formatYupInnerError),
|
|
15
|
+
message: yupError.message,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
formatYupErrors,
|
|
20
|
+
};
|
package/lib/hooks.js
CHANGED
|
@@ -24,12 +24,14 @@ const createHook = () => {
|
|
|
24
24
|
return state.handlers;
|
|
25
25
|
},
|
|
26
26
|
|
|
27
|
-
register
|
|
27
|
+
register(handler) {
|
|
28
28
|
state.handlers.push(handler);
|
|
29
|
+
return this;
|
|
29
30
|
},
|
|
30
31
|
|
|
31
|
-
delete
|
|
32
|
+
delete(handler) {
|
|
32
33
|
state.handlers = remove(eq(handler), state.handlers);
|
|
34
|
+
return this;
|
|
33
35
|
},
|
|
34
36
|
|
|
35
37
|
call() {
|
|
@@ -80,7 +82,7 @@ const createAsyncSeriesWaterfallHook = () => ({
|
|
|
80
82
|
const createAsyncParallelHook = () => ({
|
|
81
83
|
...createHook(),
|
|
82
84
|
|
|
83
|
-
call(context) {
|
|
85
|
+
async call(context) {
|
|
84
86
|
const promises = this.handlers.map(handler => handler(cloneDeep(context)));
|
|
85
87
|
|
|
86
88
|
return Promise.all(promises);
|
package/lib/index.js
CHANGED
|
@@ -4,18 +4,12 @@
|
|
|
4
4
|
* Export shared utilities
|
|
5
5
|
*/
|
|
6
6
|
const { buildQuery, hasDeepFilters } = require('./build-query');
|
|
7
|
-
const {
|
|
8
|
-
convertRestQueryParams,
|
|
9
|
-
VALID_REST_OPERATORS,
|
|
10
|
-
QUERY_OPERATORS,
|
|
11
|
-
} = require('./convert-rest-query-params');
|
|
12
7
|
const parseMultipartData = require('./parse-multipart');
|
|
13
|
-
const sanitizeEntity = require('./sanitize-entity');
|
|
14
8
|
const parseType = require('./parse-type');
|
|
15
|
-
const finder = require('./finder');
|
|
16
9
|
const policy = require('./policy');
|
|
17
10
|
const templateConfiguration = require('./template-configuration');
|
|
18
|
-
const { yup,
|
|
11
|
+
const { yup, handleYupError, validateYupSchema, validateYupSchemaSync } = require('./validators');
|
|
12
|
+
const errors = require('./errors');
|
|
19
13
|
const {
|
|
20
14
|
nameToSlug,
|
|
21
15
|
nameToCollectionName,
|
|
@@ -36,20 +30,21 @@ const relations = require('./relations');
|
|
|
36
30
|
const setCreatorFields = require('./set-creator-fields');
|
|
37
31
|
const hooks = require('./hooks');
|
|
38
32
|
const providerFactory = require('./provider-factory');
|
|
33
|
+
const pagination = require('./pagination');
|
|
34
|
+
const sanitize = require('./sanitize');
|
|
35
|
+
const traverseEntity = require('./traverse-entity');
|
|
36
|
+
const pipeAsync = require('./pipe-async');
|
|
39
37
|
|
|
40
38
|
module.exports = {
|
|
41
39
|
yup,
|
|
42
|
-
|
|
43
|
-
finder,
|
|
40
|
+
handleYupError,
|
|
44
41
|
policy,
|
|
45
42
|
templateConfiguration,
|
|
46
|
-
convertRestQueryParams,
|
|
47
|
-
VALID_REST_OPERATORS,
|
|
48
|
-
QUERY_OPERATORS,
|
|
49
43
|
buildQuery,
|
|
50
44
|
hasDeepFilters,
|
|
51
45
|
parseMultipartData,
|
|
52
|
-
|
|
46
|
+
sanitize,
|
|
47
|
+
traverseEntity,
|
|
53
48
|
parseType,
|
|
54
49
|
nameToSlug,
|
|
55
50
|
nameToCollectionName,
|
|
@@ -71,4 +66,9 @@ module.exports = {
|
|
|
71
66
|
setCreatorFields,
|
|
72
67
|
hooks,
|
|
73
68
|
providerFactory,
|
|
69
|
+
pagination,
|
|
70
|
+
pipeAsync,
|
|
71
|
+
errors,
|
|
72
|
+
validateYupSchema,
|
|
73
|
+
validateYupSchemaSync,
|
|
74
74
|
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { merge, pipe, omit, isNil } = require('lodash/fp');
|
|
4
|
+
const { PaginationError } = require('./errors');
|
|
5
|
+
|
|
6
|
+
const STRAPI_DEFAULTS = {
|
|
7
|
+
offset: {
|
|
8
|
+
start: 0,
|
|
9
|
+
limit: 10,
|
|
10
|
+
},
|
|
11
|
+
page: {
|
|
12
|
+
page: 1,
|
|
13
|
+
pageSize: 10,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const paginationAttributes = ['start', 'limit', 'page', 'pageSize'];
|
|
18
|
+
|
|
19
|
+
const withMaxLimit = (limit, maxLimit = -1) => {
|
|
20
|
+
if (maxLimit === -1 || limit < maxLimit) {
|
|
21
|
+
return limit;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return maxLimit;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Ensure minimum page & pageSize values (page >= 1, pageSize >= 0, start >= 0, limit >= 0)
|
|
28
|
+
const ensureMinValues = ({ start, limit }) => ({
|
|
29
|
+
start: Math.max(start, 0),
|
|
30
|
+
limit: limit === -1 ? limit : Math.max(limit, 1),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ensureMaxValues = (maxLimit = -1) => ({ start, limit }) => ({
|
|
34
|
+
start,
|
|
35
|
+
limit: withMaxLimit(limit, maxLimit),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Apply maxLimit as the limit when limit is -1
|
|
39
|
+
const withNoLimit = (pagination, maxLimit = -1) => ({
|
|
40
|
+
...pagination,
|
|
41
|
+
limit: pagination.limit === -1 ? maxLimit : pagination.limit,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const withDefaultPagination = (args, { defaults = {}, maxLimit = -1 } = {}) => {
|
|
45
|
+
const defaultValues = merge(STRAPI_DEFAULTS, defaults);
|
|
46
|
+
|
|
47
|
+
const usePagePagination = !isNil(args.page) || !isNil(args.pageSize);
|
|
48
|
+
const useOffsetPagination = !isNil(args.start) || !isNil(args.limit);
|
|
49
|
+
|
|
50
|
+
const ensureValidValues = pipe(
|
|
51
|
+
ensureMinValues,
|
|
52
|
+
ensureMaxValues(maxLimit)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// If there is no pagination attribute, don't modify the payload
|
|
56
|
+
if (!usePagePagination && !useOffsetPagination) {
|
|
57
|
+
return merge(args, ensureValidValues(defaultValues.offset));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If there is page & offset pagination attributes, throw an error
|
|
61
|
+
if (usePagePagination && useOffsetPagination) {
|
|
62
|
+
throw new PaginationError('Cannot use both page & offset pagination in the same query');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pagination = {};
|
|
66
|
+
|
|
67
|
+
// Start / Limit
|
|
68
|
+
if (useOffsetPagination) {
|
|
69
|
+
const { start, limit } = merge(defaultValues.offset, args);
|
|
70
|
+
|
|
71
|
+
Object.assign(pagination, { start, limit });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Page / PageSize
|
|
75
|
+
if (usePagePagination) {
|
|
76
|
+
const { page, pageSize } = merge(defaultValues.page, {
|
|
77
|
+
...args,
|
|
78
|
+
pageSize: Math.max(1, args.pageSize),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
Object.assign(pagination, {
|
|
82
|
+
start: (page - 1) * pageSize,
|
|
83
|
+
limit: pageSize,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle -1 limit
|
|
88
|
+
Object.assign(pagination, withNoLimit(pagination, maxLimit));
|
|
89
|
+
|
|
90
|
+
const replacePaginationAttributes = pipe(
|
|
91
|
+
// Remove pagination attributes
|
|
92
|
+
omit(paginationAttributes),
|
|
93
|
+
// Merge the object with the new pagination + ensure minimum & maximum values
|
|
94
|
+
merge(ensureValidValues(pagination))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return replacePaginationAttributes(args);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
module.exports = { withDefaultPagination };
|
package/lib/parse-multipart.js
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
|
|
5
5
|
module.exports = ctx => {
|
|
6
|
+
if (!ctx.is('multipart')) {
|
|
7
|
+
return { data: ctx.request.body, files: {} };
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
const { body = {}, files = {} } = ctx.request;
|
|
7
11
|
|
|
8
12
|
if (!body.data) {
|
|
9
|
-
|
|
13
|
+
return ctx.badRequest(
|
|
10
14
|
`When using multipart/form-data you need to provide your data in a JSON 'data' field.`
|
|
11
15
|
);
|
|
12
16
|
}
|
|
@@ -15,14 +19,14 @@ module.exports = ctx => {
|
|
|
15
19
|
try {
|
|
16
20
|
data = JSON.parse(body.data);
|
|
17
21
|
} catch (error) {
|
|
18
|
-
|
|
22
|
+
return ctx.badRequest(`Invalid 'data' field. 'data' should be a valid JSON.`);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
const filesToUpload = Object.keys(files).reduce((acc, key) => {
|
|
22
26
|
const fullPath = _.toPath(key);
|
|
23
27
|
|
|
24
28
|
if (fullPath.length <= 1 || fullPath[0] !== 'files') {
|
|
25
|
-
|
|
29
|
+
return ctx.badRequest(
|
|
26
30
|
`When using multipart/form-data you need to provide your files by prefixing them with the 'files'.
|
|
27
31
|
For example, when a media file is named "avatar", make sure the form key name is "files.avatar"`
|
|
28
32
|
);
|