diff --git a/examples/getstarted/api/article/models/Article.settings.json b/examples/getstarted/api/article/models/Article.settings.json index 956c779006..d31add02f2 100644 --- a/examples/getstarted/api/article/models/Article.settings.json +++ b/examples/getstarted/api/article/models/Article.settings.json @@ -7,7 +7,10 @@ }, "options": { "increments": true, - "timestamps": true, + "timestamps": [ + "created_at", + "updated_at" + ], "comment": "" }, "attributes": { @@ -24,14 +27,17 @@ "type": "json" }, "number": { - "type": "integer" + "type": "biginteger" }, "date": { "type": "date" }, "enum": { "type": "enumeration", - "enum": ["morning,", "noon"] + "enum": [ + "morning,", + "noon" + ] }, "bool": { "type": "boolean" @@ -54,18 +60,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", @@ -73,6 +77,30 @@ "repeatable": true, "min": 1, "max": 10 + }, + "blabla": { + "enum": [ + "one", + "two", + "three" + ], + "type": "enumeration", + "unique": true, + "enumName": "azd", + "default": "azd", + "required": true + }, + "article": { + "columnName": "azdazd", + "unique": true, + "model": "article", + "via": "articlea" + }, + "articlea": { + "columnName": "azdazd", + "unique": true, + "model": "article", + "via": "article" } } -} +} \ No newline at end of file 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-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..2d423558b5 --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/types.js @@ -0,0 +1,132 @@ +'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() + .positive(), + max: yup + .number() + .integer() + .positive(), + }; + } + case 'float': { + return { + default: yup.number(), + required: validators.required, + unique: validators.unique, + min: yup.number().positive(), + max: yup.number().positive(), + }; + } + case 'decimal': { + return { + default: yup.number(), + required: validators.required, + unique: validators.unique, + min: yup.number().positive(), + max: yup.number().positive(), + }; + } + 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: {}, }, });