diff --git a/examples/getstarted/api/article/models/Article.settings.json b/examples/getstarted/api/article/models/Article.settings.json index d674ddbf53..951a16e76b 100644 --- a/examples/getstarted/api/article/models/Article.settings.json +++ b/examples/getstarted/api/article/models/Article.settings.json @@ -7,7 +7,7 @@ }, "options": { "increments": true, - "timestamps": true, + "timestamps": ["created_at", "updated_at"], "comment": "" }, "attributes": { @@ -29,6 +29,15 @@ "number": { "type": "integer" }, + "big_number": { + "type": "biginteger" + }, + "float_number": { + "type": "float" + }, + "decimal_number": { + "type": "decimal" + }, "date": { "type": "date" }, @@ -57,18 +66,16 @@ }, "manyTags": { "collection": "tag", - "dominant": true, - "via": "linkedArticles" + "via": "linkedArticles", + "dominant": true }, "fb_cta": { "type": "group", - "group": "cta_facebook", - "repeatable": false + "group": "cta_facebook" }, "mainIngredient": { "type": "group", - "group": "ingredients", - "repeatable": false + "group": "ingredients" }, "ingredients": { "type": "group", diff --git a/examples/getstarted/api/tag/models/Tag.settings.json b/examples/getstarted/api/tag/models/Tag.settings.json index d5d26d71a2..0a83136ac3 100644 --- a/examples/getstarted/api/tag/models/Tag.settings.json +++ b/examples/getstarted/api/tag/models/Tag.settings.json @@ -18,4 +18,4 @@ "via": "manyTags" } } -} +} \ No newline at end of file diff --git a/packages/strapi-plugin-content-manager/admin/src/components/AddDropdown/index.js b/packages/strapi-plugin-content-manager/admin/src/components/AddDropdown/index.js index df26f90f9b..ceffc66ae3 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/AddDropdown/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/AddDropdown/index.js @@ -12,15 +12,15 @@ import { import { Wrapper } from './components'; function Add({ data, onClick, pStyle, style }) { - const [state, setState] = useState(false); + const [isOpen, setIsOpen] = useState(false); return ( - + { if (data.length > 0) { - setState(prevState => !prevState); + setIsOpen(prevState => !prevState); } }} > diff --git a/packages/strapi-plugin-content-manager/admin/src/components/Group/index.js b/packages/strapi-plugin-content-manager/admin/src/components/Group/index.js index afc38e2c23..7914c3dbd0 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/Group/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/Group/index.js @@ -211,7 +211,6 @@ Group.defaultProps = { max: Infinity, min: -Infinity, modifiedData: {}, - onChange: () => {}, }; Group.propTypes = { @@ -227,7 +226,6 @@ Group.propTypes = { moveGroupField: PropTypes.func.isRequired, name: PropTypes.string.isRequired, pathname: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, removeField: PropTypes.func.isRequired, }; diff --git a/packages/strapi-plugin-content-manager/admin/src/components/SelectMany/ListItem.js b/packages/strapi-plugin-content-manager/admin/src/components/SelectMany/ListItem.js index e95228bb3e..17a49e0c39 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/SelectMany/ListItem.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/SelectMany/ListItem.js @@ -59,6 +59,7 @@ function ListItem({ ListItem.defaultProps = { findRelation: () => {}, + moveRelation: () => {}, nextSearch: '', onRemove: () => {}, targetModel: '', diff --git a/packages/strapi-plugin-content-type-builder/controllers/Groups.js b/packages/strapi-plugin-content-type-builder/controllers/Groups.js index a6eb5d6cae..2033ca73ac 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/Groups.js +++ b/packages/strapi-plugin-content-type-builder/controllers/Groups.js @@ -1,26 +1,6 @@ 'use strict'; -const yup = require('yup'); -const formatYupErrors = require('./utils/yup-formatter'); - -const groupSchema = yup - .object({ - name: yup.string().required('name.required'), - description: yup.string(), - connection: yup.string(), - collectionName: yup.string(), - attributes: yup.object().required('attributes.required'), - }) - .noUnknown(); - -const validateGroupInput = async data => - groupSchema - .validate(data, { - strict: true, - abortEarly: false, - }) - .catch(error => Promise.reject(formatYupErrors(error))); - +const validateGroupInput = require('./validation/group'); /** * Groups controller */ diff --git a/packages/strapi-plugin-content-type-builder/controllers/utils/__tests__/yup-formatter.test.js b/packages/strapi-plugin-content-type-builder/controllers/validation/__tests__/yup-formatter.test.js similarity index 100% rename from packages/strapi-plugin-content-type-builder/controllers/utils/__tests__/yup-formatter.test.js rename to packages/strapi-plugin-content-type-builder/controllers/validation/__tests__/yup-formatter.test.js diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/common.js b/packages/strapi-plugin-content-type-builder/controllers/validation/common.js new file mode 100644 index 0000000000..3fa218bcfb --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/common.js @@ -0,0 +1,58 @@ +'use strict'; + +const yup = require('yup'); + +const VALID_TYPES = [ + // advanced types + 'media', + + // scalar types + 'string', + 'text', + 'richtext', + 'json', + 'enumeration', + 'password', + 'email', + 'integer', + 'float', + 'decimal', + 'date', + 'boolean', +]; + +const validators = { + required: yup.boolean(), + unique: yup.boolean(), + minLength: yup + .number() + .integer() + .positive(), + maxLength: yup + .number() + .integer() + .positive(), +}; + +const NAME_REGEX = new RegExp('^[A-Za-z][_0-9A-Za-z]*$'); + +const isValidName = { + name: 'isValidName', + message: '${path} must match the following regex: /^[_A-Za-z][_0-9A-Za-z]*/^', + test: val => NAME_REGEX.test(val), +}; + +const isValidKey = key => ({ + name: 'isValidKey', + message: `Attribute name '${key}' must match the following regex: /^[_A-Za-z][_0-9A-Za-z]*/^`, + test: () => NAME_REGEX.test(key), +}); + +module.exports = { + validators, + + isValidName, + isValidKey, + + VALID_TYPES, +}; diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/group.js b/packages/strapi-plugin-content-type-builder/controllers/validation/group.js new file mode 100644 index 0000000000..b597f8d350 --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/group.js @@ -0,0 +1,60 @@ +'use strict'; + +const yup = require('yup'); +const _ = require('lodash'); +const formatYupErrors = require('./yup-formatter'); + +const { isValidName, isValidKey } = require('./common'); +const getTypeValidator = require('./types'); +const getRelationValidator = require('./relations'); + +module.exports = data => { + return groupSchema + .validate(data, { + strict: true, + abortEarly: false, + }) + .catch(error => Promise.reject(formatYupErrors(error))); +}; + +const groupSchema = yup + .object({ + name: yup + .string() + .min(1) + .test(isValidName) + .required('name.required'), + description: yup.string(), + connection: yup.string(), + collectionName: yup.string().test(isValidName), + attributes: yup.lazy(obj => { + return yup + .object() + .shape( + _.mapValues(obj, (value, key) => { + return yup.lazy(obj => { + let shape; + if (_.has(obj, 'type')) { + shape = getTypeValidator(obj); + } else if (_.has(obj, 'target')) { + shape = getRelationValidator(obj); + } else { + return yup.object().test({ + name: 'mustHaveTypeOrTarget', + message: 'Attribute must have either a type or a target', + test: () => false, + }); + } + + return yup + .object() + .shape(shape) + .test(isValidKey(key)) + .noUnknown(); + }); + }) + ) + .required('attributes.required'); + }), + }) + .noUnknown(); diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/relations.js b/packages/strapi-plugin-content-type-builder/controllers/validation/relations.js new file mode 100644 index 0000000000..418470bcdd --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/relations.js @@ -0,0 +1,42 @@ +'use strict'; + +const yup = require('yup'); +const _ = require('lodash'); +const { validators } = require('./common'); + +const VALID_NATURES = ['oneWay', 'manyWay']; + +module.exports = () => { + return { + target: yup + .mixed() + .when('plugin', plugin => { + if (!plugin) + return yup + .string() + .oneOf( + Object.keys(strapi.models).filter(name => name !== 'core_store') + ); + + if (plugin === 'admin') + return yup.string().oneOf(Object.keys(strapi.admin.models)); + + if (plugin) + return yup + .string() + .oneOf(Object.keys(_.get(strapi.plugins, [plugin, 'models'], {}))); + }) + .required(), + nature: yup + .string() + .oneOf(VALID_NATURES) + .required(), + plugin: yup.string().oneOf(Object.keys(strapi.plugins)), + unique: validators.unique, + + // TODO: remove once front-end stop sending them even if useless + columnName: yup.string(), + key: yup.string(), + targetColumnName: yup.string(), + }; +}; diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/types.js b/packages/strapi-plugin-content-type-builder/controllers/validation/types.js new file mode 100644 index 0000000000..dafc32feb6 --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/types.js @@ -0,0 +1,126 @@ +'use strict'; + +const yup = require('yup'); +const { validators, VALID_TYPES, isValidName } = require('./common'); + +module.exports = obj => { + return { + type: yup + .string() + .oneOf(VALID_TYPES) + .required(), + ...getTypeShape(obj), + }; +}; + +const getTypeShape = obj => { + switch (obj.type) { + /** + * complexe types + */ + + case 'media': { + return { + multiple: yup.boolean(), + required: validators.required, + unique: validators.unique, + }; + } + + /** + * scalar types + */ + case 'string': + case 'text': + case 'richtext': { + return { + default: yup.string(), + required: validators.required, + unique: validators.unique, + min: validators.minLength, + max: validators.maxLength, + }; + } + case 'json': { + return { + required: validators.required, + unique: validators.unique, + }; + } + case 'enumeration': { + return { + enum: yup + .array() + .of(yup.string().test(isValidName)) + .min(1) + .required(), + default: yup + .string() + .when('enum', enumVal => yup.string().oneOf(enumVal)), + enumName: yup.string().test(isValidName), + required: validators.required, + unique: validators.unique, + }; + } + case 'password': { + return { + required: validators.required, + min: validators.minLength, + max: validators.maxLength, + }; + } + case 'email': { + return { + default: yup.string().email(), + required: validators.required, + unique: validators.unique, + min: validators.minLength, + max: validators.maxLength, + }; + } + case 'integer': { + return { + default: yup.number().integer(), + required: validators.required, + unique: validators.unique, + min: yup.number().integer(), + max: yup.number().integer(), + }; + } + case 'float': { + return { + default: yup.number(), + required: validators.required, + unique: validators.unique, + min: yup.number(), + max: yup.number(), + }; + } + case 'decimal': { + return { + default: yup.number(), + required: validators.required, + unique: validators.unique, + min: yup.number(), + max: yup.number(), + }; + } + case 'date': { + return { + default: yup.date(), + required: validators.required, + unique: validators.unique, + }; + } + case 'boolean': { + return { + default: yup.boolean(), + required: validators.required, + unique: validators.unique, + }; + } + default: { + return {}; + } + } +}; diff --git a/packages/strapi-plugin-content-type-builder/controllers/utils/yup-formatter.js b/packages/strapi-plugin-content-type-builder/controllers/validation/yup-formatter.js similarity index 100% rename from packages/strapi-plugin-content-type-builder/controllers/utils/yup-formatter.js rename to packages/strapi-plugin-content-type-builder/controllers/validation/yup-formatter.js diff --git a/packages/strapi-plugin-content-type-builder/services/Groups.js b/packages/strapi-plugin-content-type-builder/services/Groups.js index 998961bd21..7eda30b306 100644 --- a/packages/strapi-plugin-content-type-builder/services/Groups.js +++ b/packages/strapi-plugin-content-type-builder/services/Groups.js @@ -201,9 +201,9 @@ const convertAttributes = attributes => { } if (_.has(attribute, 'target')) { - const { target, nature, required, unique, plugin } = attribute; + const { target, nature, unique, plugin } = attribute; - // ingore relation which aren't oneWay or manyWay (except for images) + // ingore relation which aren't oneWay or manyWay if (!['oneWay', 'manyWay'].includes(nature)) { return acc; } @@ -211,7 +211,6 @@ const convertAttributes = attributes => { acc[key] = { [nature === 'oneWay' ? 'model' : 'collection']: target, plugin: plugin ? _.trim(plugin) : undefined, - required: required === true ? true : undefined, unique: unique === true ? true : undefined, }; } diff --git a/packages/strapi-plugin-content-type-builder/test/groups.test.e2e.js b/packages/strapi-plugin-content-type-builder/test/groups.test.e2e.js index b1e65eb5c6..58537fff11 100644 --- a/packages/strapi-plugin-content-type-builder/test/groups.test.e2e.js +++ b/packages/strapi-plugin-content-type-builder/test/groups.test.e2e.js @@ -31,7 +31,7 @@ describe.only('Content Type Builder - Groups', () => { method: 'POST', url: '/content-type-builder/groups', body: { - name: 'some-group', + name: 'SomeGroup', attributes: { title: { type: 'string', @@ -58,7 +58,7 @@ describe.only('Content Type Builder - Groups', () => { method: 'POST', url: '/content-type-builder/groups', body: { - name: 'some-group', + name: 'someGroup', attributes: {}, }, }); @@ -121,7 +121,7 @@ describe.only('Content Type Builder - Groups', () => { data: { uid: 'some_group', schema: { - name: 'some-group', + name: 'SomeGroup', description: '', connection: 'default', collectionName: 'groups_some_groups', @@ -176,7 +176,7 @@ describe.only('Content Type Builder - Groups', () => { method: 'PUT', url: '/content-type-builder/groups/some_group', body: { - name: 'New Group', + name: 'NewGroup', attributes: {}, }, }); diff --git a/packages/strapi-plugin-upload/controllers/Upload.js b/packages/strapi-plugin-upload/controllers/Upload.js index 87e5c75cd3..19e7144084 100644 --- a/packages/strapi-plugin-upload/controllers/Upload.js +++ b/packages/strapi-plugin-upload/controllers/Upload.js @@ -10,6 +10,8 @@ const _ = require('lodash'); module.exports = { async upload(ctx) { + const uploadService = strapi.plugins.upload.services.upload; + // Retrieve provider configuration. const config = await strapi .store({ @@ -30,8 +32,8 @@ module.exports = { } // Extract optional relational data. - const { refId, ref, source, field, path } = ctx.request.body.fields || {}; - const { files = {} } = ctx.request.body.files || {}; + const { refId, ref, source, field, path } = ctx.request.body || {}; + const { files = {} } = ctx.request.files || {}; if (_.isEmpty(files)) { return ctx.badRequest( @@ -43,9 +45,8 @@ module.exports = { } // Transform stream files to buffer - const buffers = await strapi.plugins.upload.services.upload.bufferize( - ctx.request.body.files.files - ); + const buffers = await uploadService.bufferize(files); + const enhancedFiles = buffers.map(file => { if (file.size > config.sizeLimit) { return ctx.badRequest( @@ -94,10 +95,7 @@ module.exports = { return; } - const uploadedFiles = await strapi.plugins.upload.services.upload.upload( - enhancedFiles, - config - ); + const uploadedFiles = await uploadService.upload(enhancedFiles, config); // Send 200 `ok` ctx.send( diff --git a/packages/strapi/package.json b/packages/strapi/package.json index 294b304001..66d9d3b054 100644 --- a/packages/strapi/package.json +++ b/packages/strapi/package.json @@ -24,7 +24,7 @@ "inquirer": "^6.2.1", "kcors": "^2.2.0", "koa": "^2.1.0", - "koa-body": "^2.5.0", + "koa-body": "^4.1.0", "koa-compose": "^4.0.0", "koa-compress": "^2.0.0", "koa-convert": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index 2c4105c881..7eabdea4d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2236,6 +2236,14 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" +"@types/formidable@^1.0.31": + version "1.0.31" + resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-1.0.31.tgz#274f9dc2d0a1a9ce1feef48c24ca0859e7ec947b" + integrity sha512-dIhM5t8lRP0oWe2HF8MuPvdd1TpPTjhDMAqemcq6oIZQCBQTovhBAdTQ5L5veJB4pdQChadmHuxtB0YzqvfU3Q== + dependencies: + "@types/events" "*" + "@types/node" "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -10468,11 +10476,12 @@ knex@^0.19.0: uuid "^3.3.2" v8flags "^3.1.3" -koa-body@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/koa-body/-/koa-body-2.6.0.tgz#8ed7a192a64a38df610a986342d1801855641a1d" - integrity sha512-8i9ti3TRxelsnPUct0xY8toTFj5gTzGWW45ePBkT8fnzZP75y5woisVpziIdqcnqtt1lMNBD30p+tkiSC+NfjQ== +koa-body@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-body/-/koa-body-4.1.0.tgz#99295ee2e9543884e5730ae696780930b3821c44" + integrity sha512-rWkMfMaCjFmIAMohtjlrg4BqDzcotK5BdZhiwJu1ONuR1ceoFUjnH3wp0hEV39HuBlc9tI3eUUFMK4Cp6ccFtA== dependencies: + "@types/formidable" "^1.0.31" co-body "^5.1.1" formidable "^1.1.1"