@strapi/strapi 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.
Files changed (89) hide show
  1. package/README.md +12 -12
  2. package/bin/strapi.js +6 -1
  3. package/lib/Strapi.js +33 -21
  4. package/lib/commands/build.js +1 -1
  5. package/lib/commands/content-types/list.js +3 -5
  6. package/lib/commands/develop.js +8 -10
  7. package/lib/commands/generate-template.js +4 -5
  8. package/lib/commands/hooks/list.js +3 -5
  9. package/lib/commands/middlewares/list.js +3 -5
  10. package/lib/commands/new.js +3 -1
  11. package/lib/commands/policies/list.js +3 -5
  12. package/lib/commands/services/list.js +22 -0
  13. package/lib/commands/watchAdmin.js +4 -4
  14. package/lib/core/app-configuration/index.js +5 -10
  15. package/lib/core/bootstrap.js +2 -2
  16. package/lib/core/domain/module/index.js +3 -1
  17. package/lib/core/domain/module/validation.js +1 -4
  18. package/lib/core/loaders/admin.js +2 -2
  19. package/lib/core/loaders/apis.js +3 -1
  20. package/lib/core/loaders/plugins/get-enabled-plugins.js +25 -9
  21. package/lib/core/loaders/plugins/index.js +21 -7
  22. package/lib/core/loaders/src-index.js +1 -0
  23. package/lib/core/registries/apis.js +2 -16
  24. package/lib/core/registries/content-types.js +50 -6
  25. package/lib/core/registries/controllers.d.ts +7 -0
  26. package/lib/core/registries/controllers.js +74 -3
  27. package/lib/core/registries/hooks.d.ts +20 -0
  28. package/lib/core/registries/hooks.js +57 -7
  29. package/lib/core/registries/middlewares.d.ts +5 -0
  30. package/lib/core/registries/middlewares.js +61 -2
  31. package/lib/core/registries/policies.d.ts +9 -0
  32. package/lib/core/registries/policies.js +57 -6
  33. package/lib/core/registries/services.d.ts +7 -0
  34. package/lib/core/registries/services.js +67 -11
  35. package/lib/core-api/controller/collection-type.js +38 -11
  36. package/lib/core-api/controller/index.d.ts +25 -0
  37. package/lib/core-api/controller/index.js +30 -11
  38. package/lib/core-api/controller/single-type.js +26 -7
  39. package/lib/core-api/routes/index.js +71 -0
  40. package/lib/core-api/service/collection-type.js +8 -12
  41. package/lib/core-api/service/index.d.ts +21 -0
  42. package/lib/core-api/service/index.js +9 -19
  43. package/lib/core-api/service/pagination.js +7 -2
  44. package/lib/core-api/service/single-type.js +12 -11
  45. package/lib/factories.d.ts +48 -0
  46. package/lib/factories.js +84 -0
  47. package/lib/index.d.ts +1 -0
  48. package/lib/index.js +5 -1
  49. package/lib/middlewares/body.js +33 -0
  50. package/lib/middlewares/compression.js +1 -1
  51. package/lib/middlewares/cors.js +3 -2
  52. package/lib/middlewares/errors.js +24 -119
  53. package/lib/middlewares/favicon.js +3 -3
  54. package/lib/middlewares/index.d.ts +2 -1
  55. package/lib/middlewares/index.js +4 -2
  56. package/lib/middlewares/ip.js +1 -1
  57. package/lib/middlewares/logger.js +1 -1
  58. package/lib/middlewares/powered-by.js +4 -2
  59. package/lib/middlewares/public/index.js +4 -7
  60. package/lib/middlewares/query.js +46 -0
  61. package/lib/middlewares/responses.js +2 -2
  62. package/lib/middlewares/security.js +29 -3
  63. package/lib/middlewares/session/index.js +1 -1
  64. package/lib/services/auth/index.js +1 -6
  65. package/lib/services/entity-service/attributes/index.js +31 -0
  66. package/lib/services/entity-service/attributes/transforms.js +20 -0
  67. package/lib/services/entity-service/components.js +2 -3
  68. package/lib/services/entity-service/index.d.ts +1 -1
  69. package/lib/services/entity-service/index.js +83 -27
  70. package/lib/services/entity-service/params.js +37 -87
  71. package/lib/services/entity-validator/index.js +76 -43
  72. package/lib/services/entity-validator/validators.js +129 -43
  73. package/lib/services/errors.js +77 -0
  74. package/lib/services/metrics/index.js +37 -35
  75. package/lib/services/server/compose-endpoint.js +39 -11
  76. package/lib/services/server/content-api.js +1 -1
  77. package/lib/services/server/index.js +4 -9
  78. package/lib/services/server/koa.js +64 -0
  79. package/lib/services/server/middleware.js +8 -1
  80. package/lib/services/server/policy.js +8 -10
  81. package/lib/services/server/register-middlewares.js +7 -2
  82. package/lib/services/server/register-routes.js +1 -3
  83. package/lib/services/server/routing.js +13 -0
  84. package/lib/utils/get-dirs.js +1 -0
  85. package/lib/utils/signals.js +24 -0
  86. package/package.json +22 -17
  87. package/lib/core/app-configuration/load-functions.js +0 -28
  88. package/lib/core-api/index.js +0 -39
  89. package/lib/middlewares/request.js +0 -74
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const { getOr, toNumber, isString, isBuffer } = require('lodash/fp');
4
+ const bcrypt = require('bcryptjs');
5
+
6
+ const transforms = {
7
+ password(value, context) {
8
+ const { attribute } = context;
9
+
10
+ if (!isString(value) && !isBuffer(value)) {
11
+ return value;
12
+ }
13
+
14
+ const rounds = toNumber(getOr(10, 'encryption.rounds', attribute));
15
+
16
+ return bcrypt.hashSync(value, rounds);
17
+ },
18
+ };
19
+
20
+ module.exports = transforms;
@@ -4,6 +4,7 @@ const _ = require('lodash');
4
4
  const { has, prop, omit, toString } = require('lodash/fp');
5
5
 
6
6
  const { contentTypes: contentTypesUtils } = require('@strapi/utils');
7
+ const { ApplicationError } = require('@strapi/utils').errors;
7
8
 
8
9
  const omitComponentData = (contentType, data) => {
9
10
  const { attributes } = contentType;
@@ -208,11 +209,9 @@ const deleteOldComponents = async (
208
209
 
209
210
  idsToKeep.forEach(id => {
210
211
  if (!allIds.includes(id)) {
211
- const err = new Error(
212
+ throw new ApplicationError(
212
213
  `Some of the provided components in ${attributeName} are not related to the entity`
213
214
  );
214
- err.status = 400;
215
- throw err;
216
215
  }
217
216
  });
218
217
 
@@ -35,7 +35,7 @@ type Params<T> = {
35
35
  files?: any;
36
36
  };
37
37
 
38
- interface EntityService {
38
+ export interface EntityService {
39
39
  uploadFiles<K extends keyof AllTypes, T extends AllTypes[K]>(uid: K, entity, files);
40
40
  wrapParams<K extends keyof AllTypes, T extends AllTypes[K]>(
41
41
  params: Params<T>,
@@ -1,13 +1,18 @@
1
1
  'use strict';
2
2
 
3
+ const _ = require('lodash');
3
4
  const delegate = require('delegates');
4
- const { pipe } = require('lodash/fp');
5
-
6
5
  const {
7
- sanitizeEntity,
6
+ InvalidTimeError,
7
+ InvalidDateError,
8
+ InvalidDateTimeError,
9
+ } = require('@strapi/database').errors;
10
+ const {
8
11
  webhook: webhookUtils,
9
12
  contentTypes: contentTypesUtils,
13
+ sanitize,
10
14
  } = require('@strapi/utils');
15
+ const { ValidationError } = require('@strapi/utils').errors;
11
16
  const uploadFiles = require('../utils/upload-files');
12
17
 
13
18
  const {
@@ -16,16 +21,22 @@ const {
16
21
  updateComponents,
17
22
  deleteComponents,
18
23
  } = require('./components');
19
- const {
20
- transformCommonParams,
21
- transformPaginationParams,
22
- transformParamsToQuery,
23
- pickSelectionParams,
24
- } = require('./params');
24
+ const { transformParamsToQuery, pickSelectionParams } = require('./params');
25
+ const { applyTransforms } = require('./attributes');
25
26
 
26
27
  // TODO: those should be strapi events used by the webhooks not the other way arround
27
28
  const { ENTRY_CREATE, ENTRY_UPDATE, ENTRY_DELETE } = webhookUtils.webhookEvents;
28
29
 
30
+ const databaseErrorsToTransform = [InvalidTimeError, InvalidDateTimeError, InvalidDateError];
31
+
32
+ const creationPipeline = (data, context) => {
33
+ return applyTransforms(data, context);
34
+ };
35
+
36
+ const updatePipeline = (data, context) => {
37
+ return applyTransforms(data, context);
38
+ };
39
+
29
40
  module.exports = ctx => {
30
41
  const implementation = createDefaultImplementation(ctx);
31
42
 
@@ -46,6 +57,28 @@ module.exports = ctx => {
46
57
  // delegate every method in implementation
47
58
  Object.keys(service.implementation).forEach(key => delegator.method(key));
48
59
 
60
+ // wrap methods to handle Database Errors
61
+ service.decorate(oldService => {
62
+ const newService = _.mapValues(
63
+ oldService,
64
+ (method, methodName) =>
65
+ async function(...args) {
66
+ try {
67
+ return await oldService[methodName].call(this, ...args);
68
+ } catch (error) {
69
+ if (
70
+ databaseErrorsToTransform.some(errorToTransform => error instanceof errorToTransform)
71
+ ) {
72
+ throw new ValidationError(error.message);
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+ );
78
+
79
+ return newService;
80
+ });
81
+
49
82
  return service;
50
83
  };
51
84
 
@@ -59,12 +92,13 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
59
92
  return options;
60
93
  },
61
94
 
62
- emitEvent(uid, event, entity) {
95
+ async emitEvent(uid, event, entity) {
63
96
  const model = strapi.getModel(uid);
97
+ const sanitizedEntity = await sanitize.sanitizers.defaultSanitizeOutput(model, entity);
64
98
 
65
99
  eventHub.emit(event, {
66
100
  model: model.modelName,
67
- entry: sanitizeEntity(entity, { model }),
101
+ entry: sanitizedEntity,
68
102
  });
69
103
  },
70
104
 
@@ -137,7 +171,9 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
137
171
 
138
172
  let entity = await db.query(uid).create({
139
173
  ...query,
140
- data: Object.assign(omitComponentData(model, validData), componentData),
174
+ data: creationPipeline(Object.assign(omitComponentData(model, validData), componentData), {
175
+ contentType: model,
176
+ }),
141
177
  });
142
178
 
143
179
  // TODO: upload the files then set the links in the entity like with compo to avoid making too many queries
@@ -147,7 +183,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
147
183
  entity = await this.findOne(uid, entity.id, wrappedParams);
148
184
  }
149
185
 
150
- this.emitEvent(uid, ENTRY_CREATE, entity);
186
+ await this.emitEvent(uid, ENTRY_CREATE, entity);
151
187
 
152
188
  return entity;
153
189
  },
@@ -166,9 +202,14 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
166
202
 
167
203
  const isDraft = contentTypesUtils.isDraft(entityToUpdate, model);
168
204
 
169
- const validData = await entityValidator.validateEntityUpdate(model, data, {
170
- isDraft,
171
- });
205
+ const validData = await entityValidator.validateEntityUpdate(
206
+ model,
207
+ data,
208
+ {
209
+ isDraft,
210
+ },
211
+ entityToUpdate
212
+ );
172
213
 
173
214
  const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams));
174
215
 
@@ -178,7 +219,9 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
178
219
  let entity = await db.query(uid).update({
179
220
  ...query,
180
221
  where: { id: entityId },
181
- data: Object.assign(omitComponentData(model, validData), componentData),
222
+ data: updatePipeline(Object.assign(omitComponentData(model, validData), componentData), {
223
+ contentType: model,
224
+ }),
182
225
  });
183
226
 
184
227
  // TODO: upload the files then set the links in the entity like with compo to avoid making too many queries
@@ -188,7 +231,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
188
231
  entity = await this.findOne(uid, entity.id, wrappedParams);
189
232
  }
190
233
 
191
- this.emitEvent(uid, ENTRY_UPDATE, entity);
234
+ await this.emitEvent(uid, ENTRY_UPDATE, entity);
192
235
 
193
236
  return entity;
194
237
  },
@@ -211,7 +254,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
211
254
  await deleteComponents(uid, entityToDelete);
212
255
  await db.query(uid).delete({ where: { id: entityToDelete.id } });
213
256
 
214
- this.emitEvent(uid, ENTRY_DELETE, entityToDelete);
257
+ await this.emitEvent(uid, ENTRY_DELETE, entityToDelete);
215
258
 
216
259
  return entityToDelete;
217
260
  },
@@ -226,18 +269,31 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
226
269
  return db.query(uid).deleteMany(query);
227
270
  },
228
271
 
229
- load(uid, entity, field, params) {
272
+ load(uid, entity, field, params = {}) {
230
273
  const { attributes } = strapi.getModel(uid);
231
274
 
232
275
  const attribute = attributes[field];
233
276
 
234
- const loadParams =
235
- attribute.type === 'relation'
236
- ? transformParamsToQuery(attribute.target, params)
237
- : pipe(
238
- transformCommonParams,
239
- transformPaginationParams
240
- )(params);
277
+ const loadParams = {};
278
+
279
+ switch (attribute.type) {
280
+ case 'relation': {
281
+ Object.assign(loadParams, transformParamsToQuery(attribute.target, params));
282
+ break;
283
+ }
284
+ case 'component': {
285
+ Object.assign(loadParams, transformParamsToQuery(attribute.component, params));
286
+ break;
287
+ }
288
+ case 'dynamiczone': {
289
+ Object.assign(loadParams, transformParamsToQuery(null, params));
290
+ break;
291
+ }
292
+ case 'media': {
293
+ Object.assign(loadParams, transformParamsToQuery('plugin::upload.file', params));
294
+ break;
295
+ }
296
+ }
241
297
 
242
298
  return db.query(uid).load(entity, field, loadParams);
243
299
  },
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { pick, pipe, isNil } = require('lodash/fp');
3
+ const { pick, isNil, toNumber, isInteger } = require('lodash/fp');
4
+ const { PaginationError } = require('@strapi/utils').errors;
4
5
 
5
6
  const {
6
7
  convertSortQueryParams,
@@ -9,137 +10,86 @@ const {
9
10
  convertPopulateQueryParams,
10
11
  convertFiltersQueryParams,
11
12
  convertFieldsQueryParams,
13
+ convertPublicationStateParams,
12
14
  } = require('@strapi/utils/lib/convert-query-params');
13
15
 
14
- const { contentTypes: contentTypesUtils } = require('@strapi/utils');
16
+ const pickSelectionParams = pick(['fields', 'populate']);
15
17
 
16
- const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
18
+ const transformParamsToQuery = (uid, params) => {
19
+ // NOTE: can be a CT, a Compo or nothing in the case of polymorphism (DZ & morph relations)
20
+ const type = strapi.getModel(uid);
17
21
 
18
- // TODO: to remove once the front is migrated
19
- const convertOldQuery = params => {
20
22
  const query = {};
21
23
 
22
- Object.keys(params).forEach(key => {
23
- if (key.startsWith('_')) {
24
- query[key.slice(1)] = params[key];
25
- } else {
26
- query[key] = params[key];
27
- }
28
- });
24
+ const { _q, sort, filters, fields, populate, page, pageSize, start, limit } = params;
29
25
 
30
- return query;
31
- };
32
-
33
- const transformCommonParams = (params = {}) => {
34
- const { _q, sort, filters, _where, fields, populate, ...query } = params;
35
-
36
- if (_q) {
26
+ if (!isNil(_q)) {
37
27
  query._q = _q;
38
28
  }
39
29
 
40
- if (sort) {
30
+ if (!isNil(sort)) {
41
31
  query.orderBy = convertSortQueryParams(sort);
42
32
  }
43
33
 
44
- if (filters) {
34
+ if (!isNil(filters)) {
45
35
  query.where = convertFiltersQueryParams(filters);
46
36
  }
47
37
 
48
- if (_where) {
49
- query.where = {
50
- $and: [_where].concat(query.where || []),
51
- };
52
- }
53
-
54
- if (fields) {
38
+ if (!isNil(fields)) {
55
39
  query.select = convertFieldsQueryParams(fields);
56
40
  }
57
41
 
58
- if (populate) {
42
+ if (!isNil(populate)) {
59
43
  query.populate = convertPopulateQueryParams(populate);
60
44
  }
61
45
 
62
- return { ...convertOldQuery(query), ...query };
63
- };
64
-
65
- const transformPaginationParams = (params = {}) => {
66
- const { page, pageSize, start, limit, ...query } = params;
67
-
68
46
  const isPagePagination = !isNil(page) || !isNil(pageSize);
69
47
  const isOffsetPagination = !isNil(start) || !isNil(limit);
70
48
 
71
49
  if (isPagePagination && isOffsetPagination) {
72
- throw new Error(
50
+ throw new PaginationError(
73
51
  'Invalid pagination attributes. You cannot use page and offset pagination in the same query'
74
52
  );
75
53
  }
76
54
 
77
- if (page) {
78
- query.page = Number(page);
79
- }
80
-
81
- if (pageSize) {
82
- query.pageSize = Number(pageSize);
83
- }
55
+ if (!isNil(page)) {
56
+ const pageVal = toNumber(page);
84
57
 
85
- if (start) {
86
- query.offset = convertStartQueryParams(start);
87
- }
58
+ if (!isInteger(pageVal) || pageVal <= 0) {
59
+ throw new PaginationError(
60
+ `Invalid 'page' parameter. Expected an integer > 0, received: ${page}`
61
+ );
62
+ }
88
63
 
89
- if (limit) {
90
- query.limit = convertLimitQueryParams(limit);
64
+ query.page = pageVal;
91
65
  }
92
66
 
93
- return { ...convertOldQuery(query), ...query };
94
- };
67
+ if (!isNil(pageSize)) {
68
+ const pageSizeVal = toNumber(pageSize);
95
69
 
96
- const transformPublicationStateParams = uid => (params = {}) => {
97
- const contentType = strapi.getModel(uid);
70
+ if (!isInteger(pageSizeVal) || pageSizeVal <= 0) {
71
+ throw new PaginationError(
72
+ `Invalid 'pageSize' parameter. Expected an integer > 0, received: ${page}`
73
+ );
74
+ }
98
75
 
99
- if (!contentType) {
100
- return params;
76
+ query.pageSize = pageSizeVal;
101
77
  }
102
78
 
103
- const { publicationState, ...query } = params;
104
-
105
- if (publicationState && contentTypesUtils.hasDraftAndPublish(contentType)) {
106
- const { publicationState = 'live' } = params;
107
-
108
- const liveClause = {
109
- [PUBLISHED_AT_ATTRIBUTE]: {
110
- $notNull: true,
111
- },
112
- };
113
-
114
- if (publicationState === 'live') {
115
- query.where = {
116
- $and: [liveClause].concat(query.where || []),
117
- };
118
-
119
- // TODO: propagate nested publicationState filter somehow
120
- }
79
+ if (!isNil(start)) {
80
+ query.offset = convertStartQueryParams(start);
121
81
  }
122
82
 
123
- return { ...convertOldQuery(query), ...query };
124
- };
83
+ if (!isNil(limit)) {
84
+ query.limit = convertLimitQueryParams(limit);
85
+ }
125
86
 
126
- const pickSelectionParams = pick(['fields', 'populate']);
87
+ convertPublicationStateParams(type, params, query);
127
88
 
128
- const transformParamsToQuery = (uid, params) => {
129
- return pipe(
130
- // _q, _where, filters, etc...
131
- transformCommonParams,
132
- // page, pageSize, start, limit
133
- transformPaginationParams,
134
- // publicationState
135
- transformPublicationStateParams(uid)
136
- )(params);
89
+ return query;
137
90
  };
138
91
 
139
92
  module.exports = {
140
- transformCommonParams,
141
- transformPublicationStateParams,
142
- transformPaginationParams,
143
93
  transformParamsToQuery,
144
94
  pickSelectionParams,
145
95
  };
@@ -4,15 +4,19 @@
4
4
  */
5
5
  'use strict';
6
6
 
7
- const { has, assoc, prop } = require('lodash/fp');
7
+ const { has, assoc, prop, isObject } = require('lodash/fp');
8
8
  const strapiUtils = require('@strapi/utils');
9
9
  const validators = require('./validators');
10
10
 
11
- const { yup, formatYupErrors } = strapiUtils;
11
+ const { yup, validateYupSchema } = strapiUtils;
12
12
  const { isMediaAttribute, isScalarAttribute, getWritableAttributes } = strapiUtils.contentTypes;
13
+ const { ValidationError } = strapiUtils.errors;
13
14
 
14
- const addMinMax = (attr, validator, data) => {
15
- if (Number.isInteger(attr.min) && (attr.required || (Array.isArray(data) && data.length > 0))) {
15
+ const addMinMax = (validator, { attr, updatedAttribute }) => {
16
+ if (
17
+ Number.isInteger(attr.min) &&
18
+ (attr.required || (Array.isArray(updatedAttribute.value) && updatedAttribute.value.length > 0))
19
+ ) {
16
20
  validator = validator.min(attr.min);
17
21
  }
18
22
  if (Number.isInteger(attr.max)) {
@@ -21,7 +25,7 @@ const addMinMax = (attr, validator, data) => {
21
25
  return validator;
22
26
  };
23
27
 
24
- const addRequiredValidation = createOrUpdate => (required, validator) => {
28
+ const addRequiredValidation = createOrUpdate => (validator, { attr: { required } }) => {
25
29
  if (required) {
26
30
  if (createOrUpdate === 'creation') {
27
31
  validator = validator.notNil();
@@ -34,7 +38,7 @@ const addRequiredValidation = createOrUpdate => (required, validator) => {
34
38
  return validator;
35
39
  };
36
40
 
37
- const addDefault = createOrUpdate => (attr, validator) => {
41
+ const addDefault = createOrUpdate => (validator, { attr }) => {
38
42
  if (createOrUpdate === 'creation') {
39
43
  if (
40
44
  ((attr.type === 'component' && attr.repeatable) || attr.type === 'dynamiczone') &&
@@ -53,7 +57,7 @@ const addDefault = createOrUpdate => (attr, validator) => {
53
57
 
54
58
  const preventCast = validator => validator.transform((val, originalVal) => originalVal);
55
59
 
56
- const createComponentValidator = createOrUpdate => (attr, data, { isDraft }) => {
60
+ const createComponentValidator = createOrUpdate => ({ attr, updatedAttribute }, { isDraft }) => {
57
61
  let validator;
58
62
 
59
63
  const model = strapi.getModel(attr.component);
@@ -65,19 +69,23 @@ const createComponentValidator = createOrUpdate => (attr, data, { isDraft }) =>
65
69
  validator = yup
66
70
  .array()
67
71
  .of(
68
- yup.lazy(item => createModelValidator(createOrUpdate)(model, item, { isDraft }).notNull())
72
+ yup.lazy(item =>
73
+ createModelValidator(createOrUpdate)({ model, data: item }, { isDraft }).notNull()
74
+ )
69
75
  );
70
- validator = addRequiredValidation(createOrUpdate)(true, validator);
71
- validator = addMinMax(attr, validator, data);
76
+ validator = addRequiredValidation(createOrUpdate)(validator, { attr: { required: true } });
77
+ validator = addMinMax(validator, { attr, updatedAttribute });
72
78
  } else {
73
- validator = createModelValidator(createOrUpdate)(model, data, { isDraft });
74
- validator = addRequiredValidation(createOrUpdate)(!isDraft && attr.required, validator);
79
+ validator = createModelValidator(createOrUpdate)({ model, updatedAttribute }, { isDraft });
80
+ validator = addRequiredValidation(createOrUpdate)(validator, {
81
+ attr: { required: !isDraft && attr.required },
82
+ });
75
83
  }
76
84
 
77
85
  return validator;
78
86
  };
79
87
 
80
- const createDzValidator = createOrUpdate => (attr, data, { isDraft }) => {
88
+ const createDzValidator = createOrUpdate => ({ attr, updatedAttribute }, { isDraft }) => {
81
89
  let validator;
82
90
 
83
91
  validator = yup.array().of(
@@ -94,76 +102,85 @@ const createDzValidator = createOrUpdate => (attr, data, { isDraft }) => {
94
102
  .notNull();
95
103
 
96
104
  return model
97
- ? schema.concat(createModelValidator(createOrUpdate)(model, item, { isDraft }))
105
+ ? schema.concat(createModelValidator(createOrUpdate)({ model, data: item }, { isDraft }))
98
106
  : schema;
99
107
  })
100
108
  );
101
- validator = addRequiredValidation(createOrUpdate)(true, validator);
102
- validator = addMinMax(attr, validator, data);
109
+ validator = addRequiredValidation(createOrUpdate)(validator, { attr: { required: true } });
110
+ validator = addMinMax(validator, { attr, updatedAttribute });
103
111
 
104
112
  return validator;
105
113
  };
106
114
 
107
- const createRelationValidator = createOrUpdate => (attr, data, { isDraft }) => {
115
+ const createRelationValidator = createOrUpdate => ({ attr, updatedAttribute }, { isDraft }) => {
108
116
  let validator;
109
117
 
110
- if (Array.isArray(data)) {
118
+ if (Array.isArray(updatedAttribute.value)) {
111
119
  validator = yup.array().of(yup.mixed());
112
120
  } else {
113
121
  validator = yup.mixed();
114
122
  }
115
- validator = addRequiredValidation(createOrUpdate)(!isDraft && attr.required, validator);
123
+
124
+ validator = addRequiredValidation(createOrUpdate)(validator, {
125
+ attr: { required: !isDraft && attr.required },
126
+ });
116
127
 
117
128
  return validator;
118
129
  };
119
130
 
120
- const createScalarAttributeValidator = createOrUpdate => (attr, { isDraft }) => {
131
+ const createScalarAttributeValidator = createOrUpdate => (metas, options) => {
121
132
  let validator;
122
133
 
123
- if (has(attr.type, validators)) {
124
- validator = validators[attr.type](attr, { isDraft });
134
+ if (has(metas.attr.type, validators)) {
135
+ validator = validators[metas.attr.type](metas, options);
125
136
  } else {
126
137
  // No validators specified - fall back to mixed
127
138
  validator = yup.mixed();
128
139
  }
129
140
 
130
- validator = addRequiredValidation(createOrUpdate)(!isDraft && attr.required, validator);
141
+ validator = addRequiredValidation(createOrUpdate)(validator, {
142
+ attr: { required: !options.isDraft && metas.attr.required },
143
+ });
131
144
 
132
145
  return validator;
133
146
  };
134
147
 
135
- const createAttributeValidator = createOrUpdate => (attr, data, { isDraft }) => {
148
+ const createAttributeValidator = createOrUpdate => (metas, options) => {
136
149
  let validator;
137
150
 
138
- if (isMediaAttribute(attr)) {
151
+ if (isMediaAttribute(metas.attr)) {
139
152
  validator = yup.mixed();
140
- } else if (isScalarAttribute(attr)) {
141
- validator = createScalarAttributeValidator(createOrUpdate)(attr, { isDraft });
153
+ } else if (isScalarAttribute(metas.attr)) {
154
+ validator = createScalarAttributeValidator(createOrUpdate)(metas, options);
142
155
  } else {
143
- if (attr.type === 'component') {
144
- validator = createComponentValidator(createOrUpdate)(attr, data, { isDraft });
145
- } else if (attr.type === 'dynamiczone') {
146
- validator = createDzValidator(createOrUpdate)(attr, data, { isDraft });
156
+ if (metas.attr.type === 'component') {
157
+ validator = createComponentValidator(createOrUpdate)(metas, options);
158
+ } else if (metas.attr.type === 'dynamiczone') {
159
+ validator = createDzValidator(createOrUpdate)(metas, options);
147
160
  } else {
148
- validator = createRelationValidator(createOrUpdate)(attr, data, { isDraft });
161
+ validator = createRelationValidator(createOrUpdate)(metas, options);
149
162
  }
150
163
 
151
164
  validator = preventCast(validator);
152
165
  }
153
166
 
154
- validator = addDefault(createOrUpdate)(attr, validator);
167
+ validator = addDefault(createOrUpdate)(validator, metas);
155
168
 
156
169
  return validator;
157
170
  };
158
171
 
159
- const createModelValidator = createOrUpdate => (model, data, { isDraft }) => {
172
+ const createModelValidator = createOrUpdate => ({ model, data, entity }, options) => {
160
173
  const writableAttributes = model ? getWritableAttributes(model) : [];
161
174
 
162
175
  const schema = writableAttributes.reduce((validators, attributeName) => {
163
176
  const validator = createAttributeValidator(createOrUpdate)(
164
- model.attributes[attributeName],
165
- prop(attributeName, data),
166
- { isDraft }
177
+ {
178
+ attr: model.attributes[attributeName],
179
+ updatedAttribute: { name: attributeName, value: prop(attributeName, data) },
180
+ model,
181
+ entity,
182
+ },
183
+ options
167
184
  );
168
185
 
169
186
  return assoc(attributeName, validator)(validators);
@@ -172,13 +189,29 @@ const createModelValidator = createOrUpdate => (model, data, { isDraft }) => {
172
189
  return yup.object().shape(schema);
173
190
  };
174
191
 
175
- const createValidateEntity = createOrUpdate => async (model, data, { isDraft = false } = {}) => {
176
- try {
177
- const validator = createModelValidator(createOrUpdate)(model, data, { isDraft }).required();
178
- return await validator.validate(data, { abortEarly: false });
179
- } catch (e) {
180
- throw strapi.errors.badRequest('ValidationError', { errors: formatYupErrors(e) });
192
+ const createValidateEntity = createOrUpdate => async (
193
+ model,
194
+ data,
195
+ { isDraft = false } = {},
196
+ entity = null
197
+ ) => {
198
+ if (!isObject(data)) {
199
+ const { displayName } = model.info;
200
+
201
+ throw new ValidationError(
202
+ `Invalid payload submitted for the ${createOrUpdate} of an entity of type ${displayName}. Expected an object, but got ${typeof data}`
203
+ );
181
204
  }
205
+
206
+ const validator = createModelValidator(createOrUpdate)(
207
+ {
208
+ model,
209
+ data,
210
+ entity,
211
+ },
212
+ { isDraft }
213
+ ).required();
214
+ return validateYupSchema(validator, { strict: false, abortEarly: false })(data);
182
215
  };
183
216
 
184
217
  module.exports = {