@strapi/utils 4.0.0-next.9 → 4.0.3

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.
@@ -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: 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 = (serverConfig, forAdminBuild = false) => {
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(serverConfig, 'admin.url', '/admin');
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.get('server'), forAdminBuild);
66
+ const { serverUrl, adminUrl } = getConfigUrls(config, forAdminBuild);
64
67
  let url = adminOrServer === 'server' ? serverUrl : adminUrl;
65
68
 
66
69
  if (url.startsWith('http')) {
@@ -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 = 'published_at';
10
- const CREATED_BY_ATTRIBUTE = 'created_by';
11
- const UPDATED_BY_ATTRIBUTE = 'updated_by';
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 = 'created_at';
14
- const UPDATED_AT_ATTRIBUTE = 'updated_at';
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
- return [CREATED_AT_ATTRIBUTE, UPDATED_AT_ATTRIBUTE];
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,12 +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/documentation/developer-docs/latest/developer-resources/content-api/content-api.html#filters
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
+ const parseType = require('./parse-type');
10
+ const contentTypesUtils = require('./content-types');
9
11
 
10
- const QUERY_OPERATORS = ['_where', '_or', '_and'];
12
+ const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
11
13
 
12
14
  class InvalidOrderError extends Error {
13
15
  constructor() {
@@ -29,6 +31,10 @@ const validateOrder = order => {
29
31
  }
30
32
  };
31
33
 
34
+ const convertCountQueryParams = countQuery => {
35
+ return parseType({ type: 'boolean', value: countQuery });
36
+ };
37
+
32
38
  /**
33
39
  * Sort query parser
34
40
  * @param {string} sortQuery - ex: id:asc,price:desc
@@ -104,6 +110,8 @@ const convertLimitQueryParams = limitQuery => {
104
110
  throw new Error(`convertLimitQueryParams expected a positive integer got ${limitAsANumber}`);
105
111
  }
106
112
 
113
+ if (limitAsANumber === -1) return null;
114
+
107
115
  return limitAsANumber;
108
116
  };
109
117
 
@@ -127,13 +135,15 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
127
135
 
128
136
  if (Array.isArray(populate)) {
129
137
  // map convert
130
- return populate.flatMap(value => {
131
- if (typeof value !== 'string') {
132
- throw new InvalidPopulateError();
133
- }
138
+ return _.uniq(
139
+ populate.flatMap(value => {
140
+ if (typeof value !== 'string') {
141
+ throw new InvalidPopulateError();
142
+ }
134
143
 
135
- return value.split(',').map(value => _.trim(value));
136
- });
144
+ return value.split(',').map(value => _.trim(value));
145
+ })
146
+ );
137
147
  }
138
148
 
139
149
  if (_.isPlainObject(populate)) {
@@ -141,6 +151,7 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
141
151
  for (const key in populate) {
142
152
  transformedPopulate[key] = convertNestedPopulate(populate[key]);
143
153
  }
154
+
144
155
  return transformedPopulate;
145
156
  }
146
157
 
@@ -161,7 +172,7 @@ const convertNestedPopulate = subPopulate => {
161
172
  }
162
173
 
163
174
  // TODO: We will need to consider a way to add limitation / pagination
164
- const { sort, filters, fields, populate } = subPopulate;
175
+ const { sort, filters, fields, populate, count } = subPopulate;
165
176
 
166
177
  const query = {};
167
178
 
@@ -181,6 +192,10 @@ const convertNestedPopulate = subPopulate => {
181
192
  query.populate = convertPopulateQueryParams(populate);
182
193
  }
183
194
 
195
+ if (count) {
196
+ query.count = convertCountQueryParams(count);
197
+ }
198
+
184
199
  return query;
185
200
  };
186
201
 
@@ -203,25 +218,30 @@ const convertFieldsQueryParams = (fields, depth = 0) => {
203
218
  throw new Error('Invalid fields parameter. Expected a string or an array of strings');
204
219
  };
205
220
 
206
- // NOTE: We could validate the parameters are on existing / non private attributes
207
221
  const convertFiltersQueryParams = filters => filters;
208
222
 
209
- // TODO: migrate
210
- const VALID_REST_OPERATORS = [
211
- 'eq',
212
- 'ne',
213
- 'in',
214
- 'nin',
215
- 'contains',
216
- 'ncontains',
217
- 'containss',
218
- 'ncontainss',
219
- 'lt',
220
- 'lte',
221
- 'gt',
222
- 'gte',
223
- 'null',
224
- ];
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
+ };
225
245
 
226
246
  module.exports = {
227
247
  convertSortQueryParams,
@@ -230,6 +250,5 @@ module.exports = {
230
250
  convertPopulateQueryParams,
231
251
  convertFiltersQueryParams,
232
252
  convertFieldsQueryParams,
233
- VALID_REST_OPERATORS,
234
- QUERY_OPERATORS,
253
+ convertPublicationStateParams,
235
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: handler => {
27
+ register(handler) {
28
28
  state.handlers.push(handler);
29
+ return this;
29
30
  },
30
31
 
31
- delete: handler => {
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,13 +4,12 @@
4
4
  * Export shared utilities
5
5
  */
6
6
  const { buildQuery, hasDeepFilters } = require('./build-query');
7
- const { VALID_REST_OPERATORS, 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, formatYupErrors } = require('./validators');
11
+ const { yup, handleYupError, validateYupSchema, validateYupSchemaSync } = require('./validators');
12
+ const errors = require('./errors');
14
13
  const {
15
14
  nameToSlug,
16
15
  nameToCollectionName,
@@ -31,18 +30,21 @@ const relations = require('./relations');
31
30
  const setCreatorFields = require('./set-creator-fields');
32
31
  const hooks = require('./hooks');
33
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');
34
37
 
35
38
  module.exports = {
36
39
  yup,
37
- formatYupErrors,
40
+ handleYupError,
38
41
  policy,
39
42
  templateConfiguration,
40
- VALID_REST_OPERATORS,
41
- QUERY_OPERATORS,
42
43
  buildQuery,
43
44
  hasDeepFilters,
44
45
  parseMultipartData,
45
- sanitizeEntity,
46
+ sanitize,
47
+ traverseEntity,
46
48
  parseType,
47
49
  nameToSlug,
48
50
  nameToCollectionName,
@@ -64,4 +66,9 @@ module.exports = {
64
66
  setCreatorFields,
65
67
  hooks,
66
68
  providerFactory,
69
+ pagination,
70
+ pipeAsync,
71
+ errors,
72
+ validateYupSchema,
73
+ validateYupSchemaSync,
67
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 };
@@ -10,7 +10,7 @@ module.exports = ctx => {
10
10
  const { body = {}, files = {} } = ctx.request;
11
11
 
12
12
  if (!body.data) {
13
- throw strapi.errors.badRequest(
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
- throw strapi.errors.badRequest(`Invalid 'data' field. 'data' should be a valid JSON.`);
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
- throw strapi.errors.badRequest(
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
  );
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ module.exports = (...methods) => async data => {
4
+ let res = data;
5
+
6
+ for (const method of methods) {
7
+ res = await method(res);
8
+ }
9
+
10
+ return res;
11
+ };