@strapi/utils 4.0.0-next.8 → 4.0.2

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/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
- * Returns a formatted error for http responses
61
- * @param {Object} validationError - a Yup ValidationError
62
- */
63
- const formatYupErrors = validationError => {
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
- if (validationError.inner.length === 0) {
69
- if (validationError.path === undefined) return validationError.errors;
70
- return { [validationError.path]: validationError.errors };
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
- return validationError.inner.reduce((acc, err) => {
74
- acc[err.path] = err.errors;
75
- return acc;
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
- formatYupErrors,
108
+ handleYupError,
109
+ validateYupSchema,
110
+ validateYupSchemaSync,
82
111
  };
package/package.json CHANGED
@@ -1,49 +1,49 @@
1
1
  {
2
2
  "name": "@strapi/utils",
3
- "version": "4.0.0-next.8",
3
+ "version": "4.0.2",
4
4
  "description": "Shared utilities for the Strapi packages",
5
- "homepage": "https://strapi.io",
6
5
  "keywords": [
7
6
  "strapi",
8
- "utilities",
9
- "utils",
10
- "winston"
7
+ "utils"
11
8
  ],
12
- "directories": {
13
- "lib": "./lib"
9
+ "homepage": "https://strapi.io",
10
+ "bugs": {
11
+ "url": "https://github.com/strapi/strapi/issues"
14
12
  },
15
- "main": "./lib",
16
- "dependencies": {
17
- "@sindresorhus/slugify": "1.1.0",
18
- "date-fns": "^2.19.0",
19
- "lodash": "4.17.21",
20
- "pino": "^4.7.1",
21
- "pluralize": "^8.0.0",
22
- "yup": "^0.32.9"
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git://github.com/strapi/strapi.git"
23
16
  },
17
+ "license": "SEE LICENSE IN LICENSE",
24
18
  "author": {
19
+ "name": "Strapi Solutions SAS",
25
20
  "email": "hi@strapi.io",
26
- "name": "Strapi team",
27
21
  "url": "https://strapi.io"
28
22
  },
29
23
  "maintainers": [
30
24
  {
31
- "name": "Strapi team",
25
+ "name": "Strapi Solutions SAS",
32
26
  "email": "hi@strapi.io",
33
27
  "url": "https://strapi.io"
34
28
  }
35
29
  ],
36
- "repository": {
37
- "type": "git",
38
- "url": "git://github.com/strapi/strapi.git"
30
+ "main": "./lib",
31
+ "directories": {
32
+ "lib": "./lib"
39
33
  },
40
- "bugs": {
41
- "url": "https://github.com/strapi/strapi/issues"
34
+ "scripts": {
35
+ "test:unit": "jest --verbose"
36
+ },
37
+ "dependencies": {
38
+ "@sindresorhus/slugify": "1.1.0",
39
+ "date-fns": "2.24.0",
40
+ "http-errors": "1.8.0",
41
+ "lodash": "4.17.21",
42
+ "yup": "0.32.9"
42
43
  },
43
44
  "engines": {
44
45
  "node": ">=12.x.x <=16.x.x",
45
46
  "npm": ">=6.0.0"
46
47
  },
47
- "license": "SEE LICENSE IN LICENSE",
48
- "gitHead": "e3452f6662a45a4ba96e96861e076e313b297666"
48
+ "gitHead": "fd656a47698e0a33aae42abd4330410c8cba1d08"
49
49
  }
@@ -1,245 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Converts the standard Strapi REST query params to a moe usable format for querying
5
- * You can read more here: https://strapi.io/documentation/developer-docs/latest/developer-resources/content-api/content-api.html#filters
6
- */
7
-
8
- const _ = require('lodash');
9
- const {
10
- constants: { DP_PUB_STATES },
11
- } = require('./content-types');
12
-
13
- const BOOLEAN_OPERATORS = ['or', 'and'];
14
- const QUERY_OPERATORS = ['_where', '_or', '_and'];
15
-
16
- /**
17
- * Global converter
18
- * @param {Object} params
19
- * @param defaults
20
- */
21
- const convertRestQueryParams = (params = {}, defaults = {}) => {
22
- if (typeof params !== 'object' || params === null) {
23
- throw new Error(
24
- `convertRestQueryParams expected an object got ${params === null ? 'null' : typeof params}`
25
- );
26
- }
27
-
28
- let finalParams = Object.assign({ start: 0, limit: 100 }, defaults);
29
-
30
- if (Object.keys(params).length === 0) {
31
- return finalParams;
32
- }
33
-
34
- if (_.has(params, 'sort')) {
35
- finalParams.sort = convertSortQueryParams(params.sort);
36
- }
37
-
38
- if (_.has(params, '_start')) {
39
- finalParams.start = convertStartQueryParams(params._start);
40
- }
41
-
42
- if (_.has(params, '_limit')) {
43
- finalParams.limit = convertLimitQueryParams(params._limit);
44
- }
45
-
46
- if (_.has(params, '_publicationState')) {
47
- Object.assign(finalParams, convertPublicationStateParams(params._publicationState));
48
- }
49
-
50
- const whereParams = convertExtraRootParams(
51
- _.omit(params, ['sort', '_start', '_limit', '_where', '_publicationState'])
52
- );
53
-
54
- const whereClauses = [];
55
-
56
- if (_.keys(whereParams).length > 0) {
57
- whereClauses.push(...convertWhereParams(whereParams));
58
- }
59
-
60
- if (_.has(params, '_where')) {
61
- whereClauses.push(...convertWhereParams(params._where));
62
- }
63
-
64
- Object.assign(finalParams, { where: whereClauses });
65
-
66
- return finalParams;
67
- };
68
-
69
- /**
70
- * Convert params prefixed with _ by removing the prefix after we have handle the internal params
71
- * NOTE: This is only a temporary patch for v3 to handle extra params coming from plugins
72
- * @param {object} params
73
- * @returns {object}
74
- */
75
- const convertExtraRootParams = params => {
76
- return Object.entries(params).reduce((acc, [key, value]) => {
77
- if (_.startsWith(key, '_') && !QUERY_OPERATORS.includes(key)) {
78
- acc[key.slice(1)] = value;
79
- } else {
80
- acc[key] = value;
81
- }
82
-
83
- return acc;
84
- }, {});
85
- };
86
-
87
- /**
88
- * Sort query parser
89
- * @param {string} sortQuery - ex: id:asc,price:desc
90
- */
91
- const convertSortQueryParams = sortQuery => {
92
- if (Array.isArray(sortQuery)) {
93
- return sortQuery.flatMap(sortValue => convertSortQueryParams(sortValue));
94
- }
95
-
96
- if (typeof sortQuery !== 'string') {
97
- throw new Error(`convertSortQueryParams expected a string, got ${typeof sortQuery}`);
98
- }
99
-
100
- const sortKeys = [];
101
-
102
- sortQuery.split(',').forEach(part => {
103
- // split field and order param with default order to ascending
104
- const [field, order = 'asc'] = part.split(':');
105
-
106
- if (field.length === 0) {
107
- throw new Error('Field cannot be empty');
108
- }
109
-
110
- if (!['asc', 'desc'].includes(order.toLocaleLowerCase())) {
111
- throw new Error('order can only be one of asc|desc|ASC|DESC');
112
- }
113
-
114
- sortKeys.push(_.set({}, field, order.toLowerCase()));
115
- });
116
-
117
- return sortKeys;
118
- };
119
-
120
- /**
121
- * Start query parser
122
- * @param {string} startQuery - ex: id:asc,price:desc
123
- */
124
- const convertStartQueryParams = startQuery => {
125
- const startAsANumber = _.toNumber(startQuery);
126
-
127
- if (!_.isInteger(startAsANumber) || startAsANumber < 0) {
128
- throw new Error(`convertStartQueryParams expected a positive integer got ${startAsANumber}`);
129
- }
130
-
131
- return startAsANumber;
132
- };
133
-
134
- /**
135
- * Limit query parser
136
- * @param {string} limitQuery - ex: id:asc,price:desc
137
- */
138
- const convertLimitQueryParams = limitQuery => {
139
- const limitAsANumber = _.toNumber(limitQuery);
140
-
141
- if (!_.isInteger(limitAsANumber) || (limitAsANumber !== -1 && limitAsANumber < 0)) {
142
- throw new Error(`convertLimitQueryParams expected a positive integer got ${limitAsANumber}`);
143
- }
144
-
145
- return limitAsANumber;
146
- };
147
-
148
- /**
149
- * publicationState query parser
150
- * @param {string} publicationState - eg: 'live', 'preview'
151
- */
152
- const convertPublicationStateParams = publicationState => {
153
- if (!DP_PUB_STATES.includes(publicationState)) {
154
- throw new Error(
155
- `convertPublicationStateParams expected a value from: ${DP_PUB_STATES.join(
156
- ', '
157
- )}. Got ${publicationState} instead`
158
- );
159
- }
160
-
161
- return { publicationState };
162
- };
163
-
164
- // List of all the possible filters
165
- const VALID_REST_OPERATORS = [
166
- 'eq',
167
- 'ne',
168
- 'in',
169
- 'nin',
170
- 'contains',
171
- 'ncontains',
172
- 'containss',
173
- 'ncontainss',
174
- 'lt',
175
- 'lte',
176
- 'gt',
177
- 'gte',
178
- 'null',
179
- ];
180
-
181
- /**
182
- * Parse where params
183
- */
184
- const convertWhereParams = whereParams => {
185
- let finalWhere = [];
186
-
187
- if (Array.isArray(whereParams)) {
188
- return whereParams.reduce((acc, whereParam) => {
189
- return acc.concat(convertWhereParams(whereParam));
190
- }, []);
191
- }
192
-
193
- Object.keys(whereParams).forEach(whereClause => {
194
- const { field, operator = 'eq', value } = convertWhereClause(
195
- whereClause,
196
- whereParams[whereClause]
197
- );
198
-
199
- finalWhere.push({
200
- field,
201
- operator,
202
- value,
203
- });
204
- });
205
-
206
- return finalWhere;
207
- };
208
-
209
- /**
210
- * Parse single where param
211
- * @param {string} whereClause - Any possible where clause e.g: id_ne text_ncontains
212
- * @param {string} value - the value of the where clause e.g id_ne=value
213
- */
214
- const convertWhereClause = (whereClause, value) => {
215
- const separatorIndex = whereClause.lastIndexOf('_');
216
-
217
- // eq operator
218
- if (separatorIndex === -1) {
219
- return { field: whereClause, value };
220
- }
221
-
222
- // split field and operator
223
- const field = whereClause.substring(0, separatorIndex);
224
- const operator = whereClause.slice(separatorIndex + 1);
225
-
226
- if (BOOLEAN_OPERATORS.includes(operator) && field === '') {
227
- return { field: null, operator, value: [].concat(value).map(convertWhereParams) };
228
- }
229
-
230
- // the field as underscores
231
- if (!VALID_REST_OPERATORS.includes(operator)) {
232
- return { field: whereClause, value };
233
- }
234
-
235
- return { field, operator, value };
236
- };
237
-
238
- module.exports = {
239
- convertRestQueryParams,
240
- convertSortQueryParams,
241
- convertStartQueryParams,
242
- convertLimitQueryParams,
243
- VALID_REST_OPERATORS,
244
- QUERY_OPERATORS,
245
- };
package/lib/finder.js DELETED
@@ -1,29 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Module dependencies
5
- */
6
-
7
- const _ = require('lodash');
8
-
9
- /**
10
- * Find controller's location
11
- */
12
-
13
- module.exports = (api, controller) => {
14
- if (!_.isObject(api)) {
15
- throw new Error('Should be an object');
16
- }
17
- if (_.isString(controller)) {
18
- controller = controller.toLowerCase();
19
- } else {
20
- throw new Error('Should be an object or a string');
21
- }
22
-
23
- const where = _.findKey(api, o => {
24
- return _.get(o, `controllers.${controller}`);
25
- });
26
-
27
- // Return the API's name where the controller is located
28
- return where;
29
- };
@@ -1,172 +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 relation = attribute && (attribute.model || attribute.collection || attribute.component);
52
- if (relation) {
53
- if (_.isNil(value)) {
54
- return { ...acc, [key]: value };
55
- }
56
-
57
- const [nextFields, isAllowed] = includeFields
58
- ? getNextFields(allowedFields, key, { allowedFieldsHasKey })
59
- : [null, true];
60
-
61
- if (!isAllowed) {
62
- return acc;
63
- }
64
-
65
- const baseOptions = {
66
- withPrivate,
67
- isOutput,
68
- includeFields: nextFields,
69
- };
70
-
71
- let sanitizeFn;
72
- if (relation === '*') {
73
- sanitizeFn = entity => {
74
- if (_.isNil(entity) || !_.has(entity, '__contentType')) {
75
- return entity;
76
- }
77
-
78
- return sanitizeEntity(entity, {
79
- model: strapi.getModel(entity.__contentType),
80
- ...baseOptions,
81
- });
82
- };
83
- } else {
84
- sanitizeFn = entity =>
85
- sanitizeEntity(entity, {
86
- model: strapi.getModel(relation, attribute.plugin),
87
- ...baseOptions,
88
- });
89
- }
90
-
91
- const nextVal = Array.isArray(value) ? value.map(sanitizeFn) : sanitizeFn(value);
92
-
93
- return { ...acc, [key]: nextVal };
94
- }
95
-
96
- const isAllowedField = !includeFields || allowedFieldsHasKey;
97
-
98
- // Dynamic zones
99
- if (attribute && attribute.type === 'dynamiczone' && value !== null && isAllowedField) {
100
- const nextVal = value.map(elem =>
101
- sanitizeEntity(elem, {
102
- model: strapi.getModel(elem.__component),
103
- withPrivate,
104
- isOutput,
105
- })
106
- );
107
- return { ...acc, [key]: nextVal };
108
- }
109
-
110
- // Other fields
111
- if (isAllowedField) {
112
- return { ...acc, [key]: value };
113
- }
114
-
115
- return acc;
116
- };
117
-
118
- return _.reduce(data, reducerFn, {});
119
- };
120
-
121
- const parseOriginalData = data => (_.isFunction(data.toJSON) ? data.toJSON() : data);
122
-
123
- const COMPONENT_FIELDS = ['__component'];
124
- const STATIC_FIELDS = [ID_ATTRIBUTE];
125
-
126
- const getAllowedFields = ({ includeFields, model, isOutput }) => {
127
- const nonWritableAttributes = getNonWritableAttributes(model);
128
- const nonVisibleAttributes = getNonVisibleAttributes(model);
129
-
130
- const writableAttributes = getWritableAttributes(model);
131
-
132
- const nonVisibleWritableAttributes = _.intersection(writableAttributes, nonVisibleAttributes);
133
-
134
- return _.concat(
135
- includeFields || [],
136
- ...(isOutput
137
- ? [
138
- STATIC_FIELDS,
139
- CREATED_AT_ATTRIBUTE,
140
- UPDATED_AT_ATTRIBUTE,
141
- COMPONENT_FIELDS,
142
- ...nonWritableAttributes,
143
- ...nonVisibleAttributes,
144
- ]
145
- : [STATIC_FIELDS, COMPONENT_FIELDS, ...nonVisibleWritableAttributes])
146
- );
147
- };
148
-
149
- const getNextFields = (fields, key, { allowedFieldsHasKey }) => {
150
- const searchStr = `${key}.`;
151
-
152
- const transformedFields = (fields || [])
153
- .filter(field => field.startsWith(searchStr))
154
- .map(field => field.replace(searchStr, ''));
155
-
156
- const isAllowed = allowedFieldsHasKey || transformedFields.length > 0;
157
- const nextFields = allowedFieldsHasKey ? null : transformedFields;
158
-
159
- return [nextFields, isAllowed];
160
- };
161
-
162
- const shouldRemoveAttribute = (model, key, attribute = {}, { withPrivate, isOutput }) => {
163
- const isPassword = attribute.type === 'password';
164
- const isPrivate = isPrivateAttribute(model, key);
165
-
166
- const shouldRemovePassword = isOutput;
167
- const shouldRemovePrivate = !withPrivate && isOutput;
168
-
169
- return !!((isPassword && shouldRemovePassword) || (isPrivate && shouldRemovePrivate));
170
- };
171
-
172
- module.exports = sanitizeEntity;