@strapi/utils 4.0.0-next.9 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -127,13 +133,15 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
127
133
 
128
134
  if (Array.isArray(populate)) {
129
135
  // map convert
130
- return populate.flatMap(value => {
131
- if (typeof value !== 'string') {
132
- throw new InvalidPopulateError();
133
- }
136
+ return _.uniq(
137
+ populate.flatMap(value => {
138
+ if (typeof value !== 'string') {
139
+ throw new InvalidPopulateError();
140
+ }
134
141
 
135
- return value.split(',').map(value => _.trim(value));
136
- });
142
+ return value.split(',').map(value => _.trim(value));
143
+ })
144
+ );
137
145
  }
138
146
 
139
147
  if (_.isPlainObject(populate)) {
@@ -141,6 +149,7 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
141
149
  for (const key in populate) {
142
150
  transformedPopulate[key] = convertNestedPopulate(populate[key]);
143
151
  }
152
+
144
153
  return transformedPopulate;
145
154
  }
146
155
 
@@ -161,7 +170,7 @@ const convertNestedPopulate = subPopulate => {
161
170
  }
162
171
 
163
172
  // TODO: We will need to consider a way to add limitation / pagination
164
- const { sort, filters, fields, populate } = subPopulate;
173
+ const { sort, filters, fields, populate, count } = subPopulate;
165
174
 
166
175
  const query = {};
167
176
 
@@ -181,6 +190,10 @@ const convertNestedPopulate = subPopulate => {
181
190
  query.populate = convertPopulateQueryParams(populate);
182
191
  }
183
192
 
193
+ if (count) {
194
+ query.count = convertCountQueryParams(count);
195
+ }
196
+
184
197
  return query;
185
198
  };
186
199
 
@@ -203,25 +216,30 @@ const convertFieldsQueryParams = (fields, depth = 0) => {
203
216
  throw new Error('Invalid fields parameter. Expected a string or an array of strings');
204
217
  };
205
218
 
206
- // NOTE: We could validate the parameters are on existing / non private attributes
207
219
  const convertFiltersQueryParams = filters => filters;
208
220
 
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
- ];
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
+ };
225
243
 
226
244
  module.exports = {
227
245
  convertSortQueryParams,
@@ -230,6 +248,5 @@ module.exports = {
230
248
  convertPopulateQueryParams,
231
249
  convertFiltersQueryParams,
232
250
  convertFieldsQueryParams,
233
- VALID_REST_OPERATORS,
234
- QUERY_OPERATORS,
251
+ convertPublicationStateParams,
235
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
@@ -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,88 @@
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: Math.max(limit, 1),
31
+ });
32
+
33
+ const ensureMaxValues = (maxLimit = -1) => ({ start, limit }) => ({
34
+ start,
35
+ limit: withMaxLimit(limit, maxLimit),
36
+ });
37
+
38
+ const withDefaultPagination = (args, { defaults = {}, maxLimit = -1 } = {}) => {
39
+ const defaultValues = merge(STRAPI_DEFAULTS, defaults);
40
+
41
+ const usePagePagination = !isNil(args.page) || !isNil(args.pageSize);
42
+ const useOffsetPagination = !isNil(args.start) || !isNil(args.limit);
43
+
44
+ const ensureValidValues = pipe(
45
+ ensureMinValues,
46
+ ensureMaxValues(maxLimit)
47
+ );
48
+
49
+ // If there is no pagination attribute, don't modify the payload
50
+ if (!usePagePagination && !useOffsetPagination) {
51
+ return merge(args, ensureValidValues(defaultValues.offset));
52
+ }
53
+
54
+ // If there is page & offset pagination attributes, throw an error
55
+ if (usePagePagination && useOffsetPagination) {
56
+ throw new PaginationError('Cannot use both page & offset pagination in the same query');
57
+ }
58
+
59
+ const pagination = {};
60
+
61
+ // Start / Limit
62
+ if (useOffsetPagination) {
63
+ const { start, limit } = merge(defaultValues.offset, args);
64
+
65
+ Object.assign(pagination, { start, limit });
66
+ }
67
+
68
+ // Page / PageSize
69
+ if (usePagePagination) {
70
+ const { page, pageSize } = merge(defaultValues.page, args);
71
+
72
+ Object.assign(pagination, {
73
+ start: (page - 1) * pageSize,
74
+ limit: pageSize,
75
+ });
76
+ }
77
+
78
+ const replacePaginationAttributes = pipe(
79
+ // Remove pagination attributes
80
+ omit(paginationAttributes),
81
+ // Merge the object with the new pagination + ensure minimum & maximum values
82
+ merge(ensureValidValues(pagination))
83
+ );
84
+
85
+ return replacePaginationAttributes(args);
86
+ };
87
+
88
+ 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
+ };