@strapi/utils 4.0.0-beta.2 → 4.0.0-beta.20
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/config.js +6 -3
- package/lib/content-types.js +16 -4
- package/lib/convert-query-params.js +37 -11
- package/lib/errors.js +82 -0
- package/lib/format-yup-error.js +20 -0
- package/lib/hooks.js +1 -1
- package/lib/index.js +12 -6
- package/lib/pagination.js +2 -1
- package/lib/parse-multipart.js +3 -3
- package/lib/pipe-async.js +11 -0
- package/lib/policy.js +52 -52
- 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 +20 -19
- package/lib/sanitize-entity.js +0 -171
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,6 +1,7 @@
|
|
|
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';
|
|
@@ -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,
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Converts the standard Strapi REST query params to a more usable format for querying
|
|
5
|
-
* You can read more here: https://strapi.io/
|
|
5
|
+
* You can read more here: https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#filters
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
const { has } = require('lodash/fp');
|
|
8
8
|
const _ = require('lodash');
|
|
9
9
|
const parseType = require('./parse-type');
|
|
10
|
+
const contentTypesUtils = require('./content-types');
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
|
12
13
|
|
|
13
14
|
class InvalidOrderError extends Error {
|
|
14
15
|
constructor() {
|
|
@@ -132,13 +133,15 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
|
|
|
132
133
|
|
|
133
134
|
if (Array.isArray(populate)) {
|
|
134
135
|
// map convert
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
return _.uniq(
|
|
137
|
+
populate.flatMap(value => {
|
|
138
|
+
if (typeof value !== 'string') {
|
|
139
|
+
throw new InvalidPopulateError();
|
|
140
|
+
}
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
return value.split(',').map(value => _.trim(value));
|
|
143
|
+
})
|
|
144
|
+
);
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
if (_.isPlainObject(populate)) {
|
|
@@ -146,6 +149,7 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
|
|
|
146
149
|
for (const key in populate) {
|
|
147
150
|
transformedPopulate[key] = convertNestedPopulate(populate[key]);
|
|
148
151
|
}
|
|
152
|
+
|
|
149
153
|
return transformedPopulate;
|
|
150
154
|
}
|
|
151
155
|
|
|
@@ -212,9 +216,31 @@ const convertFieldsQueryParams = (fields, depth = 0) => {
|
|
|
212
216
|
throw new Error('Invalid fields parameter. Expected a string or an array of strings');
|
|
213
217
|
};
|
|
214
218
|
|
|
215
|
-
// NOTE: We could validate the parameters are on existing / non private attributes
|
|
216
219
|
const convertFiltersQueryParams = filters => filters;
|
|
217
220
|
|
|
221
|
+
const convertPublicationStateParams = (type, params = {}, query = {}) => {
|
|
222
|
+
if (!type) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const { publicationState } = params;
|
|
227
|
+
|
|
228
|
+
if (!_.isNil(publicationState)) {
|
|
229
|
+
if (!contentTypesUtils.constants.DP_PUB_STATES.includes(publicationState)) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Invalid publicationState. Expected one of 'preview','live' received: ${publicationState}.`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// NOTE: this is the query layer filters not the entity service filters
|
|
236
|
+
query.filters = ({ meta }) => {
|
|
237
|
+
if (publicationState === 'live' && has(PUBLISHED_AT_ATTRIBUTE, meta.attributes)) {
|
|
238
|
+
return { [PUBLISHED_AT_ATTRIBUTE]: { $notNull: true } };
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
218
244
|
module.exports = {
|
|
219
245
|
convertSortQueryParams,
|
|
220
246
|
convertStartQueryParams,
|
|
@@ -222,5 +248,5 @@ module.exports = {
|
|
|
222
248
|
convertPopulateQueryParams,
|
|
223
249
|
convertFiltersQueryParams,
|
|
224
250
|
convertFieldsQueryParams,
|
|
225
|
-
|
|
251
|
+
convertPublicationStateParams,
|
|
226
252
|
};
|
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
|
@@ -82,7 +82,7 @@ const createAsyncSeriesWaterfallHook = () => ({
|
|
|
82
82
|
const createAsyncParallelHook = () => ({
|
|
83
83
|
...createHook(),
|
|
84
84
|
|
|
85
|
-
call(context) {
|
|
85
|
+
async call(context) {
|
|
86
86
|
const promises = this.handlers.map(handler => handler(cloneDeep(context)));
|
|
87
87
|
|
|
88
88
|
return Promise.all(promises);
|
package/lib/index.js
CHANGED
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
* Export shared utilities
|
|
5
5
|
*/
|
|
6
6
|
const { buildQuery, hasDeepFilters } = require('./build-query');
|
|
7
|
-
const { QUERY_OPERATORS } = require('./convert-query-params');
|
|
8
7
|
const parseMultipartData = require('./parse-multipart');
|
|
9
|
-
const sanitizeEntity = require('./sanitize-entity');
|
|
10
8
|
const parseType = require('./parse-type');
|
|
11
9
|
const policy = require('./policy');
|
|
12
10
|
const templateConfiguration = require('./template-configuration');
|
|
13
|
-
const { yup,
|
|
11
|
+
const { yup, handleYupError, validateYupSchema, validateYupSchemaSync } = require('./validators');
|
|
12
|
+
const errors = require('./errors');
|
|
14
13
|
const {
|
|
15
14
|
nameToSlug,
|
|
16
15
|
nameToCollectionName,
|
|
@@ -32,17 +31,20 @@ const setCreatorFields = require('./set-creator-fields');
|
|
|
32
31
|
const hooks = require('./hooks');
|
|
33
32
|
const providerFactory = require('./provider-factory');
|
|
34
33
|
const pagination = require('./pagination');
|
|
34
|
+
const sanitize = require('./sanitize');
|
|
35
|
+
const traverseEntity = require('./traverse-entity');
|
|
36
|
+
const pipeAsync = require('./pipe-async');
|
|
35
37
|
|
|
36
38
|
module.exports = {
|
|
37
39
|
yup,
|
|
38
|
-
|
|
40
|
+
handleYupError,
|
|
39
41
|
policy,
|
|
40
42
|
templateConfiguration,
|
|
41
|
-
QUERY_OPERATORS,
|
|
42
43
|
buildQuery,
|
|
43
44
|
hasDeepFilters,
|
|
44
45
|
parseMultipartData,
|
|
45
|
-
|
|
46
|
+
sanitize,
|
|
47
|
+
traverseEntity,
|
|
46
48
|
parseType,
|
|
47
49
|
nameToSlug,
|
|
48
50
|
nameToCollectionName,
|
|
@@ -65,4 +67,8 @@ module.exports = {
|
|
|
65
67
|
hooks,
|
|
66
68
|
providerFactory,
|
|
67
69
|
pagination,
|
|
70
|
+
pipeAsync,
|
|
71
|
+
errors,
|
|
72
|
+
validateYupSchema,
|
|
73
|
+
validateYupSchemaSync,
|
|
68
74
|
};
|
package/lib/pagination.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { merge, pipe, omit, isNil } = require('lodash/fp');
|
|
4
|
+
const { PaginationError } = require('./errors');
|
|
4
5
|
|
|
5
6
|
const STRAPI_DEFAULTS = {
|
|
6
7
|
offset: {
|
|
@@ -52,7 +53,7 @@ const withDefaultPagination = (args, { defaults = {}, maxLimit = -1 } = {}) => {
|
|
|
52
53
|
|
|
53
54
|
// If there is page & offset pagination attributes, throw an error
|
|
54
55
|
if (usePagePagination && useOffsetPagination) {
|
|
55
|
-
throw new
|
|
56
|
+
throw new PaginationError('Cannot use both page & offset pagination in the same query');
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
const pagination = {};
|
package/lib/parse-multipart.js
CHANGED
|
@@ -10,7 +10,7 @@ module.exports = ctx => {
|
|
|
10
10
|
const { body = {}, files = {} } = ctx.request;
|
|
11
11
|
|
|
12
12
|
if (!body.data) {
|
|
13
|
-
|
|
13
|
+
return ctx.badRequest(
|
|
14
14
|
`When using multipart/form-data you need to provide your data in a JSON 'data' field.`
|
|
15
15
|
);
|
|
16
16
|
}
|
|
@@ -19,14 +19,14 @@ module.exports = ctx => {
|
|
|
19
19
|
try {
|
|
20
20
|
data = JSON.parse(body.data);
|
|
21
21
|
} catch (error) {
|
|
22
|
-
|
|
22
|
+
return ctx.badRequest(`Invalid 'data' field. 'data' should be a valid JSON.`);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const filesToUpload = Object.keys(files).reduce((acc, key) => {
|
|
26
26
|
const fullPath = _.toPath(key);
|
|
27
27
|
|
|
28
28
|
if (fullPath.length <= 1 || fullPath[0] !== 'files') {
|
|
29
|
-
|
|
29
|
+
return ctx.badRequest(
|
|
30
30
|
`When using multipart/form-data you need to provide your files by prefixing them with the 'files'.
|
|
31
31
|
For example, when a media file is named "avatar", make sure the form key name is "files.avatar"`
|
|
32
32
|
);
|
package/lib/policy.js
CHANGED
|
@@ -9,36 +9,22 @@ const { eq } = require('lodash/fp');
|
|
|
9
9
|
const PLUGIN_PREFIX = 'plugin::';
|
|
10
10
|
const API_PREFIX = 'api::';
|
|
11
11
|
|
|
12
|
-
const createPolicy = (policyName, args) => ({ policyName, args });
|
|
13
|
-
|
|
14
|
-
const resolveHandler = policy => {
|
|
15
|
-
return _.has('handler', policy) ? policy.handler : policy;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
12
|
const parsePolicy = policy => {
|
|
19
13
|
if (typeof policy === 'string') {
|
|
20
|
-
return
|
|
14
|
+
return { policyName: policy, config: {} };
|
|
21
15
|
}
|
|
22
16
|
|
|
23
|
-
const { name,
|
|
24
|
-
return
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const resolvePolicy = policyName => {
|
|
28
|
-
const policy = strapi.policy(policyName);
|
|
29
|
-
|
|
30
|
-
return resolveHandler(policy);
|
|
17
|
+
const { name, config } = policy;
|
|
18
|
+
return { policyName: name, config };
|
|
31
19
|
};
|
|
32
20
|
|
|
33
21
|
const searchLocalPolicy = (policyName, { pluginName, apiName }) => {
|
|
34
22
|
if (pluginName) {
|
|
35
|
-
|
|
36
|
-
return resolveHandler(policy);
|
|
23
|
+
return strapi.policy(`${PLUGIN_PREFIX}${pluginName}.${policyName}`);
|
|
37
24
|
}
|
|
38
25
|
|
|
39
26
|
if (apiName) {
|
|
40
|
-
|
|
41
|
-
return resolveHandler(policy);
|
|
27
|
+
return strapi.policy(`${API_PREFIX}${apiName}.${policyName}`);
|
|
42
28
|
}
|
|
43
29
|
};
|
|
44
30
|
|
|
@@ -56,53 +42,68 @@ const globalPolicy = ({ method, endpoint, controller, action, plugin }) => {
|
|
|
56
42
|
};
|
|
57
43
|
};
|
|
58
44
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
45
|
+
const resolvePolicies = (config, { pluginName, apiName } = {}) => {
|
|
46
|
+
return config.map(policyConfig => {
|
|
47
|
+
return {
|
|
48
|
+
handler: getPolicy(policyConfig, { pluginName, apiName }),
|
|
49
|
+
config: policyConfig.config || {},
|
|
50
|
+
};
|
|
51
|
+
});
|
|
65
52
|
};
|
|
66
53
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
return policy;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const { policyName, args } = parsePolicy(policy);
|
|
73
|
-
|
|
74
|
-
const resolvedPolicy = resolvePolicy(policyName);
|
|
54
|
+
const findPolicy = (name, { pluginName, apiName } = {}) => {
|
|
55
|
+
const resolvedPolicy = strapi.policy(name);
|
|
75
56
|
|
|
76
57
|
if (resolvedPolicy !== undefined) {
|
|
77
|
-
return
|
|
58
|
+
return resolvedPolicy;
|
|
78
59
|
}
|
|
79
60
|
|
|
80
|
-
const localPolicy = searchLocalPolicy(
|
|
61
|
+
const localPolicy = searchLocalPolicy(name, { pluginName, apiName });
|
|
81
62
|
|
|
82
63
|
if (localPolicy !== undefined) {
|
|
83
64
|
return localPolicy;
|
|
84
65
|
}
|
|
85
66
|
|
|
86
|
-
throw new Error(`Could not find policy "${
|
|
67
|
+
throw new Error(`Could not find policy "${name}"`);
|
|
87
68
|
};
|
|
88
69
|
|
|
89
|
-
const
|
|
90
|
-
|
|
70
|
+
const getPolicy = (policyConfig, { pluginName, apiName } = {}) => {
|
|
71
|
+
if (typeof policyConfig === 'function') {
|
|
72
|
+
return policyConfig;
|
|
73
|
+
}
|
|
91
74
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
75
|
+
const { policyName, config } = parsePolicy(policyConfig);
|
|
76
|
+
|
|
77
|
+
const policy = findPolicy(policyName, { pluginName, apiName });
|
|
78
|
+
|
|
79
|
+
if (typeof policy === 'function') {
|
|
80
|
+
return policy;
|
|
81
|
+
}
|
|
99
82
|
|
|
100
|
-
|
|
83
|
+
if (policy.validator) {
|
|
84
|
+
policy.validator(config);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return policy.handler;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const createPolicy = options => {
|
|
91
|
+
const { name = 'unnamed', validator, handler } = options;
|
|
92
|
+
|
|
93
|
+
const wrappedValidator = config => {
|
|
101
94
|
if (validator) {
|
|
102
|
-
|
|
95
|
+
try {
|
|
96
|
+
validator(config);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
throw new Error(`Invalid config passed to "${name}" policy.`);
|
|
99
|
+
}
|
|
103
100
|
}
|
|
101
|
+
};
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
return {
|
|
104
|
+
name,
|
|
105
|
+
validator: wrappedValidator,
|
|
106
|
+
handler,
|
|
106
107
|
};
|
|
107
108
|
};
|
|
108
109
|
|
|
@@ -110,7 +111,6 @@ const createPolicyContext = (type, ctx) => {
|
|
|
110
111
|
return Object.assign(
|
|
111
112
|
{
|
|
112
113
|
is: eq(type),
|
|
113
|
-
|
|
114
114
|
get type() {
|
|
115
115
|
return type;
|
|
116
116
|
},
|
|
@@ -120,9 +120,9 @@ const createPolicyContext = (type, ctx) => {
|
|
|
120
120
|
};
|
|
121
121
|
|
|
122
122
|
module.exports = {
|
|
123
|
-
get,
|
|
123
|
+
get: getPolicy,
|
|
124
|
+
resolve: resolvePolicies,
|
|
124
125
|
globalPolicy,
|
|
125
|
-
|
|
126
|
-
createPolicyFactory,
|
|
126
|
+
createPolicy,
|
|
127
127
|
createPolicyContext,
|
|
128
128
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Code copied from the yup library (https://github.com/jquense/yup)
|
|
4
|
+
// https://github.com/jquense/yup/blob/2778b88bdacd5260d593c6468793da2e77daf21f/src/util/printValue.ts
|
|
5
|
+
|
|
6
|
+
const toString = Object.prototype.toString;
|
|
7
|
+
const errorToString = Error.prototype.toString;
|
|
8
|
+
const regExpToString = RegExp.prototype.toString;
|
|
9
|
+
const symbolToString = typeof Symbol !== 'undefined' ? Symbol.prototype.toString : () => '';
|
|
10
|
+
|
|
11
|
+
const SYMBOL_REGEXP = /^Symbol\((.*)\)(.*)$/;
|
|
12
|
+
|
|
13
|
+
function printNumber(val) {
|
|
14
|
+
if (val != +val) return 'NaN';
|
|
15
|
+
const isNegativeZero = val === 0 && 1 / val < 0;
|
|
16
|
+
return isNegativeZero ? '-0' : '' + val;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function printSimpleValue(val, quoteStrings = false) {
|
|
20
|
+
if (val == null || val === true || val === false) return '' + val;
|
|
21
|
+
|
|
22
|
+
const typeOf = typeof val;
|
|
23
|
+
if (typeOf === 'number') return printNumber(val);
|
|
24
|
+
if (typeOf === 'string') return quoteStrings ? `"${val}"` : val;
|
|
25
|
+
if (typeOf === 'function') return '[Function ' + (val.name || 'anonymous') + ']';
|
|
26
|
+
if (typeOf === 'symbol') return symbolToString.call(val).replace(SYMBOL_REGEXP, 'Symbol($1)');
|
|
27
|
+
|
|
28
|
+
const tag = toString.call(val).slice(8, -1);
|
|
29
|
+
if (tag === 'Date') return isNaN(val.getTime()) ? '' + val : val.toISOString(val);
|
|
30
|
+
if (tag === 'Error' || val instanceof Error) return '[' + errorToString.call(val) + ']';
|
|
31
|
+
if (tag === 'RegExp') return regExpToString.call(val);
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printValue(value, quoteStrings) {
|
|
37
|
+
let result = printSimpleValue(value, quoteStrings);
|
|
38
|
+
if (result !== null) return result;
|
|
39
|
+
|
|
40
|
+
return JSON.stringify(
|
|
41
|
+
value,
|
|
42
|
+
function(key, value) {
|
|
43
|
+
let result = printSimpleValue(this[key], quoteStrings);
|
|
44
|
+
if (result !== null) return result;
|
|
45
|
+
return value;
|
|
46
|
+
},
|
|
47
|
+
2
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = printValue;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isArray } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const traverseEntity = require('../traverse-entity');
|
|
6
|
+
const { getNonWritableAttributes } = require('../content-types');
|
|
7
|
+
const pipeAsync = require('../pipe-async');
|
|
8
|
+
|
|
9
|
+
const visitors = require('./visitors');
|
|
10
|
+
const sanitizers = require('./sanitizers');
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
contentAPI: {
|
|
14
|
+
input(data, schema, { auth } = {}) {
|
|
15
|
+
if (isArray(data)) {
|
|
16
|
+
return Promise.all(data.map(entry => this.input(entry, schema, { auth })));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const nonWritableAttributes = getNonWritableAttributes(schema);
|
|
20
|
+
|
|
21
|
+
const transforms = [
|
|
22
|
+
// Remove non writable attributes
|
|
23
|
+
traverseEntity(visitors.restrictedFields(nonWritableAttributes), { schema }),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
if (auth) {
|
|
27
|
+
// Remove restricted relations
|
|
28
|
+
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return pipeAsync(...transforms)(data);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
output(data, schema, { auth } = {}) {
|
|
35
|
+
if (isArray(data)) {
|
|
36
|
+
return Promise.all(data.map(entry => this.output(entry, schema, { auth })));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const transforms = [sanitizers.defaultSanitizeOutput(schema)];
|
|
40
|
+
|
|
41
|
+
if (auth) {
|
|
42
|
+
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return pipeAsync(...transforms)(data);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
sanitizers,
|
|
50
|
+
visitors,
|
|
51
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { curry } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const pipeAsync = require('../pipe-async');
|
|
6
|
+
const traverseEntity = require('../traverse-entity');
|
|
7
|
+
|
|
8
|
+
const { removePassword, removePrivate } = require('./visitors');
|
|
9
|
+
|
|
10
|
+
const sanitizePasswords = curry((schema, entity) => {
|
|
11
|
+
return traverseEntity(removePassword, { schema }, entity);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const sanitizePrivates = curry((schema, entity) => {
|
|
15
|
+
return traverseEntity(removePrivate, { schema }, entity);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const defaultSanitizeOutput = curry((schema, entity) => {
|
|
19
|
+
return pipeAsync(sanitizePrivates(schema), sanitizePasswords(schema))(entity);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
sanitizePasswords,
|
|
24
|
+
sanitizePrivates,
|
|
25
|
+
defaultSanitizeOutput,
|
|
26
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isArray, toPath } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
module.exports = (allowedFields = null) => ({ key, path }, { remove }) => {
|
|
6
|
+
// All fields are allowed
|
|
7
|
+
if (allowedFields === null) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Ignore invalid formats
|
|
12
|
+
if (!isArray(allowedFields)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const containedPaths = getContainedPaths(path);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tells if the current path should be kept or not based
|
|
20
|
+
* on the success of the check functions for any of the allowed paths.
|
|
21
|
+
*
|
|
22
|
+
* The check functions are defined as follow:
|
|
23
|
+
*
|
|
24
|
+
* `containedPaths.includes(p)`
|
|
25
|
+
* @example
|
|
26
|
+
* ```js
|
|
27
|
+
* const path = 'foo.bar.field';
|
|
28
|
+
* const p = 'foo.bar';
|
|
29
|
+
* // it should match
|
|
30
|
+
*
|
|
31
|
+
* const path = 'foo.bar.field';
|
|
32
|
+
* const p = 'bar.foo';
|
|
33
|
+
* // it shouldn't match
|
|
34
|
+
*
|
|
35
|
+
* const path = 'foo.bar';
|
|
36
|
+
* const p = 'foo.bar.field';
|
|
37
|
+
* // it should match but isn't handled by this check
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* `p.startsWith(`${path}.`)`
|
|
41
|
+
* @example
|
|
42
|
+
* ```js
|
|
43
|
+
* const path = 'foo.bar';
|
|
44
|
+
* const p = 'foo.bar.field';
|
|
45
|
+
* // it should match
|
|
46
|
+
*
|
|
47
|
+
* const path = 'foo.bar.field';
|
|
48
|
+
* const p = 'bar.foo';
|
|
49
|
+
* // it shouldn't match
|
|
50
|
+
*
|
|
51
|
+
* const path = 'foo.bar.field';
|
|
52
|
+
* const p = 'foo.bar';
|
|
53
|
+
* // it should match but isn't handled by this check
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
const isPathAllowed = allowedFields.some(
|
|
57
|
+
p => containedPaths.includes(p) || p.startsWith(`${path}.`)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (isPathAllowed) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Remove otherwise
|
|
65
|
+
remove(key);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve the list of allowed paths based on the given path
|
|
70
|
+
*
|
|
71
|
+
* @param {string} path
|
|
72
|
+
* @return {string[]}
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```js
|
|
76
|
+
* const containedPaths = getContainedPaths('foo');
|
|
77
|
+
* // ['foo']
|
|
78
|
+
*
|
|
79
|
+
* * const containedPaths = getContainedPaths('foo.bar');
|
|
80
|
+
* // ['foo', 'foo.bar']
|
|
81
|
+
*
|
|
82
|
+
* * const containedPaths = getContainedPaths('foo.bar.field');
|
|
83
|
+
* // ['foo', 'foo.bar', 'foo.bar.field']
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
const getContainedPaths = path => {
|
|
87
|
+
const parts = toPath(path);
|
|
88
|
+
|
|
89
|
+
return parts.reduce((acc, value, index, list) => {
|
|
90
|
+
return [...acc, list.slice(0, index + 1).join('.')];
|
|
91
|
+
}, []);
|
|
92
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
removePassword: require('./remove-password'),
|
|
5
|
+
removePrivate: require('./remove-private'),
|
|
6
|
+
removeRestrictedRelations: require('./remove-restricted-relations'),
|
|
7
|
+
allowedFields: require('./allowed-fields'),
|
|
8
|
+
restrictedFields: require('./restricted-fields'),
|
|
9
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ACTIONS_TO_VERIFY = ['find'];
|
|
4
|
+
|
|
5
|
+
module.exports = auth => async ({ data, key, attribute }, { remove, set }) => {
|
|
6
|
+
const isRelation = attribute.type === 'relation';
|
|
7
|
+
|
|
8
|
+
if (!isRelation) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const handleMorphRelation = async () => {
|
|
13
|
+
const newMorphValue = [];
|
|
14
|
+
|
|
15
|
+
for (const element of data[key]) {
|
|
16
|
+
const scopes = ACTIONS_TO_VERIFY.map(action => `${element.__type}.${action}`);
|
|
17
|
+
const isAllowed = await hasAccessToSomeScopes(scopes, auth);
|
|
18
|
+
|
|
19
|
+
if (isAllowed) {
|
|
20
|
+
newMorphValue.push(element);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If the new value is empty, remove the relation completely
|
|
25
|
+
if (newMorphValue.length === 0) {
|
|
26
|
+
remove(key);
|
|
27
|
+
} else {
|
|
28
|
+
set(key, newMorphValue);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleRegularRelation = async () => {
|
|
33
|
+
const scopes = ACTIONS_TO_VERIFY.map(action => `${attribute.target}.${action}`);
|
|
34
|
+
|
|
35
|
+
const isAllowed = await hasAccessToSomeScopes(scopes, auth);
|
|
36
|
+
|
|
37
|
+
// If the authenticated user don't have access to any of the scopes, then remove the field
|
|
38
|
+
if (!isAllowed) {
|
|
39
|
+
remove(key);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
|
44
|
+
|
|
45
|
+
// Polymorphic relations
|
|
46
|
+
if (isMorphRelation) {
|
|
47
|
+
await handleMorphRelation();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Regular relations
|
|
51
|
+
else {
|
|
52
|
+
await handleRegularRelation();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const hasAccessToSomeScopes = async (scopes, auth) => {
|
|
57
|
+
for (const scope of scopes) {
|
|
58
|
+
try {
|
|
59
|
+
await strapi.auth.verify(auth, { scope });
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isArray } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
module.exports = (restrictedFields = null) => ({ key, path }, { remove }) => {
|
|
6
|
+
// Remove all fields
|
|
7
|
+
if (restrictedFields === null) {
|
|
8
|
+
remove(key);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Ignore invalid formats
|
|
13
|
+
if (!isArray(restrictedFields)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Remove if an exact match was found
|
|
18
|
+
if (restrictedFields.includes(path)) {
|
|
19
|
+
remove(key);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Remove nested matches
|
|
24
|
+
const isRestrictedNested = restrictedFields.some(allowedPath =>
|
|
25
|
+
path.startsWith(`${allowedPath}.`)
|
|
26
|
+
);
|
|
27
|
+
if (isRestrictedNested) {
|
|
28
|
+
remove(key);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { cloneDeep, isObject, isArray, isNil, curry } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const traverseEntity = async (visitor, options, entity) => {
|
|
6
|
+
const { path = null, schema } = options;
|
|
7
|
+
|
|
8
|
+
// End recursion
|
|
9
|
+
if (!isObject(entity) || isNil(schema)) {
|
|
10
|
+
return entity;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Don't mutate the original entity object
|
|
14
|
+
const copy = cloneDeep(entity);
|
|
15
|
+
|
|
16
|
+
for (const key of Object.keys(copy)) {
|
|
17
|
+
// Retrieve the attribute definition associated to the key from the schema
|
|
18
|
+
const attribute = schema.attributes[key];
|
|
19
|
+
|
|
20
|
+
// If the attribute doesn't exist within the schema, ignore it
|
|
21
|
+
if (isNil(attribute)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
26
|
+
|
|
27
|
+
// Visit the current attribute
|
|
28
|
+
const visitorOptions = { data: copy, schema, key, value: copy[key], attribute, path: newPath };
|
|
29
|
+
const visitorUtils = createVisitorUtils({ data: copy });
|
|
30
|
+
|
|
31
|
+
await visitor(visitorOptions, visitorUtils);
|
|
32
|
+
|
|
33
|
+
// Extract the value for the current key (after calling the visitor)
|
|
34
|
+
const value = copy[key];
|
|
35
|
+
|
|
36
|
+
// Ignore Nil values
|
|
37
|
+
if (isNil(value)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isRelation = attribute.type === 'relation';
|
|
42
|
+
const isComponent = attribute.type === 'component';
|
|
43
|
+
const isDynamicZone = attribute.type === 'dynamiczone';
|
|
44
|
+
|
|
45
|
+
if (isRelation) {
|
|
46
|
+
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
|
47
|
+
|
|
48
|
+
const traverseTarget = entry => {
|
|
49
|
+
// Handle polymorphic relationships
|
|
50
|
+
const targetSchemaUID = isMorphRelation ? entry.__type : attribute.target;
|
|
51
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
52
|
+
|
|
53
|
+
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
54
|
+
|
|
55
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// need to update copy
|
|
59
|
+
copy[key] = isArray(value)
|
|
60
|
+
? await Promise.all(value.map(traverseTarget))
|
|
61
|
+
: await traverseTarget(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isComponent) {
|
|
65
|
+
const targetSchema = strapi.getModel(attribute.component);
|
|
66
|
+
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
67
|
+
|
|
68
|
+
const traverseComponent = entry => traverseEntity(visitor, traverseOptions, entry);
|
|
69
|
+
|
|
70
|
+
copy[key] = isArray(value)
|
|
71
|
+
? await Promise.all(value.map(traverseComponent))
|
|
72
|
+
: await traverseComponent(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isDynamicZone && isArray(value)) {
|
|
76
|
+
const visitDynamicZoneEntry = entry => {
|
|
77
|
+
const targetSchema = strapi.getModel(entry.__component);
|
|
78
|
+
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
79
|
+
|
|
80
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
copy[key] = await Promise.all(value.map(visitDynamicZoneEntry));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return copy;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const createVisitorUtils = ({ data }) => ({
|
|
91
|
+
remove(key) {
|
|
92
|
+
delete data[key];
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
set(key, value) {
|
|
96
|
+
data[key] = value;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
module.exports = curry(traverseEntity);
|
package/lib/validators.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const yup = require('yup');
|
|
4
4
|
const _ = require('lodash');
|
|
5
|
+
const { defaults } = require('lodash/fp');
|
|
5
6
|
const utils = require('./string-formatting');
|
|
7
|
+
const { YupValidationError } = require('./errors');
|
|
8
|
+
const printValue = require('./print-value');
|
|
6
9
|
|
|
7
10
|
const MixedSchemaType = yup.mixed;
|
|
8
11
|
|
|
@@ -56,27 +59,53 @@ class StrapiIDSchema extends MixedSchemaType {
|
|
|
56
59
|
|
|
57
60
|
yup.strapiID = () => new StrapiIDSchema();
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
if (!validationError.inner) {
|
|
65
|
-
throw new Error('invalid.input');
|
|
66
|
-
}
|
|
62
|
+
const handleYupError = (error, errorMessage) => {
|
|
63
|
+
throw new YupValidationError(error, errorMessage);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const defaultValidationParam = { strict: true, abortEarly: false };
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const validateYupSchema = (schema, options = {}) => async (body, errorMessage) => {
|
|
69
|
+
try {
|
|
70
|
+
const optionsWithDefaults = defaults(defaultValidationParam, options);
|
|
71
|
+
return await schema.validate(body, optionsWithDefaults);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
handleYupError(e, errorMessage);
|
|
71
74
|
}
|
|
75
|
+
};
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
const validateYupSchemaSync = (schema, options = {}) => (body, errorMessage) => {
|
|
78
|
+
try {
|
|
79
|
+
const optionsWithDefaults = defaults(defaultValidationParam, options);
|
|
80
|
+
return schema.validateSync(body, optionsWithDefaults);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
handleYupError(e, errorMessage);
|
|
83
|
+
}
|
|
77
84
|
};
|
|
78
85
|
|
|
86
|
+
// Temporary fix of this issue : https://github.com/jquense/yup/issues/616
|
|
87
|
+
yup.setLocale({
|
|
88
|
+
mixed: {
|
|
89
|
+
notType({ path, type, value, originalValue }) {
|
|
90
|
+
let isCast = originalValue != null && originalValue !== value;
|
|
91
|
+
let msg =
|
|
92
|
+
`${path} must be a \`${type}\` type, ` +
|
|
93
|
+
`but the final value was: \`${printValue(value, true)}\`` +
|
|
94
|
+
(isCast ? ` (cast from the value \`${printValue(originalValue, true)}\`).` : '.');
|
|
95
|
+
|
|
96
|
+
/* Remove comment that is not supposed to be seen by the enduser
|
|
97
|
+
if (value === null) {
|
|
98
|
+
msg += `\n If "null" is intended as an empty value be sure to mark the schema as \`.nullable()\``;
|
|
99
|
+
}
|
|
100
|
+
*/
|
|
101
|
+
return msg;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
79
106
|
module.exports = {
|
|
80
107
|
yup,
|
|
81
|
-
|
|
108
|
+
handleYupError,
|
|
109
|
+
validateYupSchema,
|
|
110
|
+
validateYupSchemaSync,
|
|
82
111
|
};
|
package/package.json
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/utils",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.20",
|
|
4
4
|
"description": "Shared utilities for the Strapi packages",
|
|
5
|
-
"homepage": "https://strapi.io",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"strapi",
|
|
8
7
|
"utils"
|
|
9
8
|
],
|
|
10
|
-
"
|
|
11
|
-
|
|
9
|
+
"homepage": "https://strapi.io",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/strapi/strapi/issues"
|
|
12
12
|
},
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"date-fns": "2.24.0",
|
|
17
|
-
"lodash": "4.17.21",
|
|
18
|
-
"yup": "0.32.9"
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git://github.com/strapi/strapi.git"
|
|
19
16
|
},
|
|
17
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
20
18
|
"author": {
|
|
19
|
+
"name": "Strapi Solutions SAS",
|
|
21
20
|
"email": "hi@strapi.io",
|
|
22
|
-
"name": "Strapi team",
|
|
23
21
|
"url": "https://strapi.io"
|
|
24
22
|
},
|
|
25
23
|
"maintainers": [
|
|
26
24
|
{
|
|
27
|
-
"name": "Strapi
|
|
25
|
+
"name": "Strapi Solutions SAS",
|
|
28
26
|
"email": "hi@strapi.io",
|
|
29
27
|
"url": "https://strapi.io"
|
|
30
28
|
}
|
|
31
29
|
],
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
"
|
|
30
|
+
"main": "./lib",
|
|
31
|
+
"directories": {
|
|
32
|
+
"lib": "./lib"
|
|
35
33
|
},
|
|
36
|
-
"
|
|
37
|
-
"
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@sindresorhus/slugify": "1.1.0",
|
|
36
|
+
"date-fns": "2.24.0",
|
|
37
|
+
"http-errors": "1.8.0",
|
|
38
|
+
"lodash": "4.17.21",
|
|
39
|
+
"yup": "0.32.9"
|
|
38
40
|
},
|
|
39
41
|
"engines": {
|
|
40
42
|
"node": ">=12.x.x <=16.x.x",
|
|
41
43
|
"npm": ">=6.0.0"
|
|
42
44
|
},
|
|
43
|
-
"
|
|
44
|
-
"gitHead": "020680cbb38b6281b3e16df264b9388a087b2b49"
|
|
45
|
+
"gitHead": "b4993dab9f6dbc583709167f459b6f00e0b4baa6"
|
|
45
46
|
}
|
package/lib/sanitize-entity.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const _ = require('lodash');
|
|
4
|
-
const {
|
|
5
|
-
constants,
|
|
6
|
-
isPrivateAttribute,
|
|
7
|
-
getNonWritableAttributes,
|
|
8
|
-
getNonVisibleAttributes,
|
|
9
|
-
getWritableAttributes,
|
|
10
|
-
} = require('./content-types');
|
|
11
|
-
|
|
12
|
-
const { ID_ATTRIBUTE, CREATED_AT_ATTRIBUTE, UPDATED_AT_ATTRIBUTE } = constants;
|
|
13
|
-
|
|
14
|
-
const sanitizeEntity = (dataSource, options) => {
|
|
15
|
-
const { model, withPrivate = false, isOutput = true, includeFields = null } = options;
|
|
16
|
-
|
|
17
|
-
if (typeof dataSource !== 'object' || _.isNil(dataSource)) {
|
|
18
|
-
return dataSource;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const data = parseOriginalData(dataSource);
|
|
22
|
-
|
|
23
|
-
if (typeof data !== 'object' || _.isNil(data)) {
|
|
24
|
-
return data;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (_.isArray(data)) {
|
|
28
|
-
return data.map(entity => sanitizeEntity(entity, options));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (_.isNil(model)) {
|
|
32
|
-
if (isOutput) {
|
|
33
|
-
return null;
|
|
34
|
-
} else {
|
|
35
|
-
return data;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const { attributes } = model;
|
|
40
|
-
const allowedFields = getAllowedFields({ includeFields, model, isOutput });
|
|
41
|
-
|
|
42
|
-
const reducerFn = (acc, value, key) => {
|
|
43
|
-
const attribute = attributes[key];
|
|
44
|
-
const allowedFieldsHasKey = allowedFields.includes(key);
|
|
45
|
-
|
|
46
|
-
if (shouldRemoveAttribute(model, key, attribute, { withPrivate, isOutput })) {
|
|
47
|
-
return acc;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Relations
|
|
51
|
-
const isRelation = attribute && ['relation', 'component'].includes(attribute.type);
|
|
52
|
-
if (isRelation) {
|
|
53
|
-
const relation = attribute && (attribute.target || attribute.component);
|
|
54
|
-
|
|
55
|
-
if (_.isNil(value)) {
|
|
56
|
-
return { ...acc, [key]: value };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const [nextFields, isAllowed] = includeFields
|
|
60
|
-
? getNextFields(allowedFields, key, { allowedFieldsHasKey })
|
|
61
|
-
: [null, true];
|
|
62
|
-
|
|
63
|
-
if (!isAllowed) {
|
|
64
|
-
return acc;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const baseOptions = {
|
|
68
|
-
withPrivate,
|
|
69
|
-
isOutput,
|
|
70
|
-
includeFields: nextFields,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
let sanitizeFn;
|
|
74
|
-
if (attribute.relation && attribute.relation.toLowerCase().includes('morph')) {
|
|
75
|
-
sanitizeFn = entity => {
|
|
76
|
-
if (_.isNil(entity) || !_.has(entity, '__type')) {
|
|
77
|
-
return entity;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return sanitizeEntity(entity, { model: strapi.getModel(entity.__type), ...baseOptions });
|
|
81
|
-
};
|
|
82
|
-
} else {
|
|
83
|
-
sanitizeFn = entity => {
|
|
84
|
-
return sanitizeEntity(entity, { model: strapi.getModel(relation), ...baseOptions });
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const nextVal = Array.isArray(value) ? value.map(sanitizeFn) : sanitizeFn(value);
|
|
89
|
-
|
|
90
|
-
return { ...acc, [key]: nextVal };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const isAllowedField = !includeFields || allowedFieldsHasKey;
|
|
94
|
-
|
|
95
|
-
// Dynamic zones
|
|
96
|
-
if (attribute && attribute.type === 'dynamiczone' && value !== null && isAllowedField) {
|
|
97
|
-
const nextVal = value.map(elem =>
|
|
98
|
-
sanitizeEntity(elem, {
|
|
99
|
-
model: strapi.getModel(elem.__component),
|
|
100
|
-
withPrivate,
|
|
101
|
-
isOutput,
|
|
102
|
-
})
|
|
103
|
-
);
|
|
104
|
-
return { ...acc, [key]: nextVal };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Other fields
|
|
108
|
-
if (isAllowedField) {
|
|
109
|
-
return { ...acc, [key]: value };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return acc;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
return _.reduce(data, reducerFn, {});
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const parseOriginalData = data => (_.isFunction(data.toJSON) ? data.toJSON() : data);
|
|
119
|
-
|
|
120
|
-
const COMPONENT_FIELDS = ['__component'];
|
|
121
|
-
const STATIC_FIELDS = [ID_ATTRIBUTE];
|
|
122
|
-
|
|
123
|
-
const getAllowedFields = ({ includeFields, model, isOutput }) => {
|
|
124
|
-
const nonWritableAttributes = getNonWritableAttributes(model);
|
|
125
|
-
const nonVisibleAttributes = getNonVisibleAttributes(model);
|
|
126
|
-
|
|
127
|
-
const writableAttributes = getWritableAttributes(model);
|
|
128
|
-
|
|
129
|
-
const nonVisibleWritableAttributes = _.intersection(writableAttributes, nonVisibleAttributes);
|
|
130
|
-
|
|
131
|
-
return _.uniq(
|
|
132
|
-
_.concat(
|
|
133
|
-
includeFields || [],
|
|
134
|
-
...(isOutput
|
|
135
|
-
? [
|
|
136
|
-
STATIC_FIELDS,
|
|
137
|
-
CREATED_AT_ATTRIBUTE,
|
|
138
|
-
UPDATED_AT_ATTRIBUTE,
|
|
139
|
-
COMPONENT_FIELDS,
|
|
140
|
-
...nonWritableAttributes,
|
|
141
|
-
...nonVisibleAttributes,
|
|
142
|
-
]
|
|
143
|
-
: [STATIC_FIELDS, COMPONENT_FIELDS, ...nonVisibleWritableAttributes])
|
|
144
|
-
)
|
|
145
|
-
);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const getNextFields = (fields, key, { allowedFieldsHasKey }) => {
|
|
149
|
-
const searchStr = `${key}.`;
|
|
150
|
-
|
|
151
|
-
const transformedFields = (fields || [])
|
|
152
|
-
.filter(field => field.startsWith(searchStr))
|
|
153
|
-
.map(field => field.replace(searchStr, ''));
|
|
154
|
-
|
|
155
|
-
const isAllowed = allowedFieldsHasKey || transformedFields.length > 0;
|
|
156
|
-
const nextFields = allowedFieldsHasKey ? null : transformedFields;
|
|
157
|
-
|
|
158
|
-
return [nextFields, isAllowed];
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const shouldRemoveAttribute = (model, key, attribute = {}, { withPrivate, isOutput }) => {
|
|
162
|
-
const isPassword = attribute.type === 'password';
|
|
163
|
-
const isPrivate = isPrivateAttribute(model, key);
|
|
164
|
-
|
|
165
|
-
const shouldRemovePassword = isOutput;
|
|
166
|
-
const shouldRemovePrivate = !withPrivate && isOutput;
|
|
167
|
-
|
|
168
|
-
return !!((isPassword && shouldRemovePassword) || (isPrivate && shouldRemovePrivate));
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
module.exports = sanitizeEntity;
|