mirror of
https://github.com/strapi/strapi.git
synced 2025-12-24 21:54:24 +00:00
Init new validation schemas for CT and compo
This commit is contained in:
parent
a1d3c3abb5
commit
489b653e1b
@ -11,13 +11,10 @@ const path = require('path');
|
||||
// Public node modules.
|
||||
const _ = require('lodash');
|
||||
const pluralize = require('pluralize');
|
||||
const slugify = require('@sindresorhus/slugify');
|
||||
|
||||
const toSlug = str => slugify(str, { separator: '-' });
|
||||
|
||||
// Fetch stub attribute template on initial load.
|
||||
const attributeTemplate = fs.readFileSync(
|
||||
path.resolve(__dirname, '..', 'templates', 'attribute.template'),
|
||||
'utf8'
|
||||
);
|
||||
/* eslint-disable prefer-template */
|
||||
/**
|
||||
* This `before` function is run before generating targets.
|
||||
* Validate, configure defaults, get extra dependencies, etc.
|
||||
@ -35,113 +32,95 @@ module.exports = (scope, cb) => {
|
||||
const parent = scope.args.api || scope.args.plugin;
|
||||
|
||||
// Format `id`.
|
||||
scope.id = _.trim(_.camelCase(scope.id));
|
||||
const name = toSlug(scope.id);
|
||||
const environment = process.env.NODE_ENV || 'development';
|
||||
|
||||
// `scope.args` are the raw command line arguments.
|
||||
_.defaults(scope, {
|
||||
idPluralized: pluralize.plural(_.trim(_.camelCase(scope.id))),
|
||||
idPluralized: pluralize(name),
|
||||
parentId: _.isEmpty(parent) ? undefined : _.trim(_.deburr(parent)),
|
||||
parentIdPluralized: _.isEmpty(scope.parentId)
|
||||
? undefined
|
||||
: pluralize.plural(_.trim(_.camelCase(scope.parentId))),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
});
|
||||
|
||||
// Determine default values based on the available scope.
|
||||
_.defaults(scope, {
|
||||
globalID: _.upperFirst(_.camelCase(scope.id)),
|
||||
ext: '.js',
|
||||
globalID: name,
|
||||
});
|
||||
|
||||
// Take another pass to take advantage of the defaults absorbed in previous passes.
|
||||
_.defaults(scope, {
|
||||
rootPath: scope.rootPath,
|
||||
filename: `${scope.globalID}${scope.ext}`,
|
||||
filenameSettings: scope.globalID + '.settings.json',
|
||||
folderPrefix: !scope.args.api && scope.args.plugin ? 'plugins' : 'api',
|
||||
folderName: _.camelCase(scope.parentId || scope.id).toLowerCase(),
|
||||
filename: `${name}.js`,
|
||||
filenameSettings: `${name}.settings.json`,
|
||||
folderPrefix: !scope.args.api && scope.args.plugin ? 'extensions' : 'api',
|
||||
folderName: scope.parentId || name,
|
||||
});
|
||||
|
||||
// Humanize output.
|
||||
_.defaults(scope, {
|
||||
humanizeId: _.camelCase(scope.id).toLowerCase(),
|
||||
humanizeIdPluralized: pluralize.plural(_.camelCase(scope.id).toLowerCase()),
|
||||
humanizedPath: `\`./${scope.folderPrefix}/${
|
||||
scope.parentId ? '' + scope.folderName : ''
|
||||
}\``,
|
||||
humanizedPath: `./${scope.folderPrefix}/${scope.folderName}`,
|
||||
});
|
||||
|
||||
// Validate optional attribute arguments.
|
||||
const invalidAttributes = [];
|
||||
|
||||
// Map attributes and split them for CLI.
|
||||
scope.attributes = scope.args.attributes.map(attribute => {
|
||||
if (_.isString(attribute)) {
|
||||
const parts = attribute.split(':');
|
||||
if (_.isPlainObject(scope.args.attributes)) {
|
||||
scope.attributes = scope.args.attributes;
|
||||
} else {
|
||||
// Map attributes and split them for CLI.
|
||||
scope.attributes = scope.args.attributes.map(attribute => {
|
||||
if (_.isString(attribute)) {
|
||||
const parts = attribute.split(':');
|
||||
|
||||
parts[1] = parts[1] || 'string';
|
||||
parts[1] = parts[1] || 'string';
|
||||
|
||||
// Handle invalid attributes.
|
||||
if (!parts[1] || !parts[0]) {
|
||||
invalidAttributes.push(
|
||||
'Error: Invalid attribute notation `' + attribute + '`.'
|
||||
);
|
||||
return;
|
||||
// Handle invalid attributes.
|
||||
if (!parts[1] || !parts[0]) {
|
||||
invalidAttributes.push(
|
||||
'Error: Invalid attribute notation `' + attribute + '`.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
name: _.trim(_.deburr(parts[0].toLowerCase())),
|
||||
params: {
|
||||
type: _.trim(_.deburr(parts[1].toLowerCase())),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return _.has(attribute, 'params.type') ? attribute : undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name: _.trim(_.deburr(parts[0].toLowerCase())),
|
||||
params: {
|
||||
type: _.trim(_.deburr(parts[1].toLowerCase())),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return _.has(attribute, 'params.type') ? attribute : undefined;
|
||||
scope.attributes = _.compact(scope.attributes);
|
||||
|
||||
// Handle invalid action arguments.
|
||||
// Send back invalidActions.
|
||||
if (invalidAttributes.length) {
|
||||
return cb.invalid(invalidAttributes);
|
||||
}
|
||||
});
|
||||
|
||||
scope.attributes = _.compact(scope.attributes);
|
||||
// Make sure there aren't duplicates.
|
||||
if (
|
||||
_(scope.attributes.map(attribute => attribute.name))
|
||||
.uniq()
|
||||
.valueOf().length !== scope.attributes.length
|
||||
) {
|
||||
return cb.invalid('Duplicate attributes not allowed!');
|
||||
}
|
||||
|
||||
// Handle invalid action arguments.
|
||||
// Send back invalidActions.
|
||||
if (invalidAttributes.length) {
|
||||
return cb.invalid(invalidAttributes);
|
||||
// Render some stringified code from the action template
|
||||
// and make it available in our scope for use later on.
|
||||
scope.attributes = scope.attributes.reduce((acc, attribute) => {
|
||||
acc[attribute.name] = attribute.params;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Make sure there aren't duplicates.
|
||||
if (
|
||||
_(scope.attributes.map(attribute => attribute.name))
|
||||
.uniq()
|
||||
.valueOf().length !== scope.attributes.length
|
||||
) {
|
||||
return cb.invalid('Duplicate attributes not allowed!');
|
||||
}
|
||||
|
||||
// Render some stringified code from the action template
|
||||
// and make it available in our scope for use later on.
|
||||
scope.attributes = scope.attributes
|
||||
.map(attribute => {
|
||||
const compiled = _.template(attributeTemplate);
|
||||
return _.trimEnd(
|
||||
_.unescape(
|
||||
compiled({
|
||||
name: attribute.name,
|
||||
params: attribute.params,
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
// Set collectionName
|
||||
scope.collectionName = _.has(scope.args, 'collectionName')
|
||||
? scope.args.collectionName
|
||||
: undefined;
|
||||
: pluralize(name);
|
||||
|
||||
// Set description
|
||||
scope.description = _.has(scope.args, 'description')
|
||||
? scope.args.description
|
||||
: undefined;
|
||||
: '';
|
||||
|
||||
// Get default connection
|
||||
try {
|
||||
@ -153,7 +132,7 @@ module.exports = (scope, cb) => {
|
||||
scope.rootPath,
|
||||
'config',
|
||||
'environments',
|
||||
scope.environment,
|
||||
environment,
|
||||
'database.json'
|
||||
)
|
||||
)
|
||||
@ -163,6 +142,25 @@ module.exports = (scope, cb) => {
|
||||
return cb.invalid(err);
|
||||
}
|
||||
|
||||
scope.schema = JSON.stringify(
|
||||
{
|
||||
connection: scope.connection,
|
||||
collectionName: scope.collectionName,
|
||||
info: {
|
||||
name: scope.args.name || scope.id,
|
||||
description: scope.description,
|
||||
},
|
||||
options: {
|
||||
increments: true,
|
||||
timestamps: true,
|
||||
comment: '',
|
||||
},
|
||||
attributes: scope.attributes,
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
// Trigger callback with no error to proceed.
|
||||
return cb.success();
|
||||
};
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
// Node.js core.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Local dependencies.
|
||||
@ -16,45 +15,34 @@ const routesJSON = require('../json/routes.json.js');
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
templatesDirectory: scope => {
|
||||
try {
|
||||
// Try to reach the path. If it fail, throw an error.
|
||||
fs.accessSync(path.resolve(__dirname, '..', 'templates', scope.args.tpl), fs.constants.R_OK | fs.constants.W_OK);
|
||||
|
||||
return path.resolve(__dirname, '..', 'templates', scope.args.tpl);
|
||||
} catch (e) {
|
||||
// Default template is Mongoose
|
||||
return path.resolve(__dirname, '..', 'templates', 'mongoose');
|
||||
}
|
||||
},
|
||||
templatesDirectory: path.resolve(__dirname, '..', 'templates'),
|
||||
before: require('./before'),
|
||||
targets: {
|
||||
|
||||
// Use the default `controller` file as a template for
|
||||
// every generated controller.
|
||||
':folderPrefix/:folderName/controllers/:filename': {
|
||||
template: 'controller.template'
|
||||
template: 'controller.template',
|
||||
},
|
||||
|
||||
// every generated controller.
|
||||
':folderPrefix/:folderName/services/:filename': {
|
||||
template: 'service.template'
|
||||
template: 'service.template',
|
||||
},
|
||||
|
||||
// Copy an empty JavaScript model where every functions will be.
|
||||
':folderPrefix/:folderName/models/:filename': {
|
||||
template: 'model.template'
|
||||
template: 'model.template',
|
||||
},
|
||||
|
||||
// Copy the generated JSON model for the connection,
|
||||
// schema and attributes.
|
||||
':folderPrefix/:folderName/models/:filenameSettings': {
|
||||
template: 'model.settings.template'
|
||||
template: 'model.settings.template',
|
||||
},
|
||||
|
||||
// Generate routes.
|
||||
':folderPrefix/:folderName/config/routes.json': {
|
||||
jsonfile: routesJSON
|
||||
}
|
||||
}
|
||||
jsonfile: routesJSON,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1 +0,0 @@
|
||||
"<%= name %>": <%= JSON.stringify(params, null, 2) %>
|
||||
@ -1,16 +0,0 @@
|
||||
{
|
||||
"connection": "<%= connection %>",
|
||||
"collectionName": "<%= collectionName || idPluralized %>",
|
||||
"info": {
|
||||
"name": "<%= id %>",
|
||||
"description": "<%= description %>"
|
||||
},
|
||||
"options": {
|
||||
"increments": true,
|
||||
"timestamps": true,
|
||||
"comment": ""
|
||||
},
|
||||
"attributes": {
|
||||
<%= attributes %>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
<%= schema %>
|
||||
@ -1,7 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Read the documentation () to implement custom controller functions
|
||||
*/
|
||||
|
||||
module.exports = {};
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"connection": "<%= connection %>",
|
||||
"collectionName": "<%= collectionName || '' %>",
|
||||
"info": {
|
||||
"name": "<%= id %>",
|
||||
"description": "<%= description %>"
|
||||
},
|
||||
"options": {
|
||||
"timestamps": true
|
||||
},
|
||||
"attributes": {
|
||||
<%= attributes %>
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Lifecycle callbacks for the `<%= globalID %>` model.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// Before saving a value.
|
||||
// Fired before an `insert` or `update` query.
|
||||
// beforeSave: async (model) => {},
|
||||
|
||||
// After saving a value.
|
||||
// Fired after an `insert` or `update` query.
|
||||
// afterSave: async (model, result) => {},
|
||||
|
||||
// Before fetching all values.
|
||||
// Fired before a `fetchAll` operation.
|
||||
// beforeFetchAll: async (model) => {},
|
||||
|
||||
// After fetching all values.
|
||||
// Fired after a `fetchAll` operation.
|
||||
// afterFetchAll: async (model, results) => {},
|
||||
|
||||
// Fired before a `fetch` operation.
|
||||
// beforeFetch: async (model) => {},
|
||||
|
||||
// After fetching a value.
|
||||
// Fired after a `fetch` operation.
|
||||
// afterFetch: async (model, result) => {},
|
||||
|
||||
// Before creating a value.
|
||||
// Fired before an `insert` query.
|
||||
// beforeCreate: async (model) => {},
|
||||
|
||||
// After creating a value.
|
||||
// Fired after an `insert` query.
|
||||
// afterCreate: async (model, result) => {},
|
||||
|
||||
// Before updating a value.
|
||||
// Fired before an `update` query.
|
||||
// beforeUpdate: async (model) => {},
|
||||
|
||||
// After updating a value.
|
||||
// Fired after an `update` query.
|
||||
// afterUpdate: async (model, result) => {},
|
||||
|
||||
// Before destroying a value.
|
||||
// Fired before a `delete` query.
|
||||
// beforeDestroy: async (model) => {},
|
||||
|
||||
// After destroying a value.
|
||||
// Fired after a `delete` query.
|
||||
// afterDestroy: async (model, result) => {}
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Read the documentation () to implement custom service functions
|
||||
*/
|
||||
|
||||
module.exports = {};
|
||||
@ -3,31 +3,31 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/connections",
|
||||
"handler": "ContentTypeBuilder.getConnections",
|
||||
"handler": "Connections.getConnections",
|
||||
"config": {
|
||||
"policies": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/models",
|
||||
"handler": "ContentTypeBuilder.getModels",
|
||||
"path": "/content-types",
|
||||
"handler": "ContentTypes.getContentTypes",
|
||||
"config": {
|
||||
"policies": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/models/:model",
|
||||
"handler": "ContentTypeBuilder.getModel",
|
||||
"path": "/content-types/:uid",
|
||||
"handler": "ContentTypes.getContentType",
|
||||
"config": {
|
||||
"policies": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/models",
|
||||
"handler": "ContentTypeBuilder.createModel",
|
||||
"path": "/content-types",
|
||||
"handler": "ContentTypes.createContentType",
|
||||
"config": {
|
||||
"policies": []
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async getConnections(ctx) {
|
||||
ctx.send({
|
||||
connections: Object.keys(
|
||||
strapi.config.currentEnvironment.database.connections
|
||||
),
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,128 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const pluralize = require('pluralize');
|
||||
|
||||
const generator = require('strapi-generate');
|
||||
const { formatAttributes, convertAttributes } = require('../utils/attributes');
|
||||
const { nameToSlug } = require('../utils/helpers');
|
||||
const { validateContentTypeInput } = require('./validation/contentType');
|
||||
|
||||
module.exports = {
|
||||
getContentTypes(ctx) {
|
||||
const contentTypes = Object.keys(strapi.contentTypes)
|
||||
.filter(uid => {
|
||||
if (uid.startsWith('strapi::')) return false;
|
||||
if (uid === 'plugins::upload.file') return false; // TODO: add a flag in the content type instead
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(uid => formatContentType(strapi.contentTypes[uid]));
|
||||
|
||||
ctx.send({
|
||||
data: contentTypes,
|
||||
});
|
||||
},
|
||||
|
||||
getContentType(ctx) {
|
||||
const { uid } = ctx.params;
|
||||
|
||||
const contentType = strapi.contentTypes[uid];
|
||||
|
||||
if (!contentType) {
|
||||
return ctx.send({ error: 'contentType.notFound' }, 404);
|
||||
}
|
||||
|
||||
ctx.send({ data: formatContentType(contentType) });
|
||||
},
|
||||
|
||||
async createContentType(ctx) {
|
||||
const { body } = ctx.request;
|
||||
|
||||
strapi.reload.isWatching = false;
|
||||
|
||||
try {
|
||||
await validateContentTypeInput(body);
|
||||
} catch (error) {
|
||||
return ctx.send({ error }, 400);
|
||||
}
|
||||
|
||||
const slug = nameToSlug(body.name);
|
||||
const uid = `application::${slug}.${slug}`;
|
||||
|
||||
if (_.has(strapi.contentTypes, uid)) {
|
||||
return ctx.send({ error: 'contentType.alreadyExists' }, 400);
|
||||
}
|
||||
|
||||
const contentType = createContentTypeSchema(body);
|
||||
|
||||
await generateAPI(slug, contentType);
|
||||
|
||||
// create relations
|
||||
strapi.reload();
|
||||
|
||||
ctx.send({
|
||||
data: {
|
||||
uid,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const formatContentType = contentType => {
|
||||
const { uid, plugin, connection, collectionName, info } = contentType;
|
||||
|
||||
return {
|
||||
uid,
|
||||
plugin,
|
||||
schema: {
|
||||
icon: _.get(info, 'icon'),
|
||||
name: _.get(info, 'name') || _.upperFirst(pluralize(uid)),
|
||||
description: _.get(info, 'description', ''),
|
||||
connection,
|
||||
collectionName,
|
||||
attributes: formatAttributes(contentType),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createContentTypeSchema = infos => ({
|
||||
connection:
|
||||
infos.connection ||
|
||||
_.get(
|
||||
strapi,
|
||||
['config', 'currentEnvironment', 'database', 'defaultConnection'],
|
||||
'default'
|
||||
),
|
||||
collectionName:
|
||||
infos.collectionName || `${nameToSlug(pluralize(infos.name))}`,
|
||||
info: {
|
||||
name: infos.name,
|
||||
description: infos.description,
|
||||
},
|
||||
attributes: convertAttributes(infos.attributes),
|
||||
});
|
||||
|
||||
const generateAPI = (name, contentType) => {
|
||||
// create api
|
||||
return new Promise((resolve, reject) => {
|
||||
const scope = {
|
||||
generatorType: 'api',
|
||||
id: name,
|
||||
rootPath: strapi.dir,
|
||||
args: {
|
||||
api: name,
|
||||
name: contentType.info.name,
|
||||
description: contentType.info.description,
|
||||
connection: contentType.connection,
|
||||
collectionName: contentType.collectionName,
|
||||
attributes: contentType.attributes,
|
||||
},
|
||||
};
|
||||
|
||||
generator(scope, {
|
||||
success: () => resolve(),
|
||||
error: err => reject(err),
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -2,26 +2,6 @@
|
||||
|
||||
const yup = require('yup');
|
||||
|
||||
const VALID_TYPES = [
|
||||
// advanced types
|
||||
'media',
|
||||
|
||||
// scalar types
|
||||
'string',
|
||||
'text',
|
||||
'richtext',
|
||||
'json',
|
||||
'enumeration',
|
||||
'password',
|
||||
'email',
|
||||
'integer',
|
||||
'biginteger',
|
||||
'float',
|
||||
'decimal',
|
||||
'date',
|
||||
'boolean',
|
||||
];
|
||||
|
||||
const validators = {
|
||||
required: yup.boolean(),
|
||||
unique: yup.boolean(),
|
||||
@ -63,6 +43,4 @@ module.exports = {
|
||||
isValidName,
|
||||
isValidKey,
|
||||
isValidEnum,
|
||||
|
||||
VALID_TYPES,
|
||||
};
|
||||
|
||||
@ -5,9 +5,33 @@ const _ = require('lodash');
|
||||
const formatYupErrors = require('./yup-formatter');
|
||||
|
||||
const { isValidName, isValidKey } = require('./common');
|
||||
const getTypeValidator = require('./types');
|
||||
const { getTypeShape } = require('./types');
|
||||
const getRelationValidator = require('./relations');
|
||||
|
||||
const VALID_COMPONENT_RELATIONS = ['oneWay', 'manyWay'];
|
||||
const VALID_COMPONENT_TYPES = [
|
||||
// advanced types
|
||||
'media',
|
||||
|
||||
// scalar types
|
||||
'string',
|
||||
'text',
|
||||
'richtext',
|
||||
'json',
|
||||
'enumeration',
|
||||
'password',
|
||||
'email',
|
||||
'integer',
|
||||
'biginteger',
|
||||
'float',
|
||||
'decimal',
|
||||
'date',
|
||||
'boolean',
|
||||
|
||||
// nested component
|
||||
'component',
|
||||
];
|
||||
|
||||
const validateComponentInput = data => {
|
||||
return componentSchema
|
||||
.validate(data, {
|
||||
@ -64,9 +88,15 @@ const componentSchema = yup
|
||||
return yup.lazy(obj => {
|
||||
let shape;
|
||||
if (_.has(obj, 'type')) {
|
||||
shape = getTypeValidator(obj);
|
||||
shape = {
|
||||
type: yup
|
||||
.string()
|
||||
.oneOf(VALID_COMPONENT_TYPES)
|
||||
.required(),
|
||||
...getTypeShape(obj),
|
||||
};
|
||||
} else if (_.has(obj, 'target')) {
|
||||
shape = getRelationValidator(obj);
|
||||
shape = getRelationValidator(VALID_COMPONENT_RELATIONS);
|
||||
} else {
|
||||
return yup.object().test({
|
||||
name: 'mustHaveTypeOrTarget',
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
'use strict';
|
||||
|
||||
const yup = require('yup');
|
||||
const _ = require('lodash');
|
||||
const formatYupErrors = require('./yup-formatter');
|
||||
|
||||
const { isValidName, isValidKey } = require('./common');
|
||||
const { getTypeShape } = require('./types');
|
||||
const getRelationValidator = require('./relations');
|
||||
|
||||
const VALID_COMPONENT_RELATIONS = [
|
||||
'oneWay',
|
||||
'manyWay',
|
||||
'oneToOne',
|
||||
'oneToMany',
|
||||
'manyToOne',
|
||||
'manyToMany',
|
||||
];
|
||||
const VALID_COMPONENT_TYPES = [
|
||||
// advanced types
|
||||
'media',
|
||||
|
||||
// scalar types
|
||||
'string',
|
||||
'text',
|
||||
'richtext',
|
||||
'json',
|
||||
'enumeration',
|
||||
'password',
|
||||
'email',
|
||||
'integer',
|
||||
'biginteger',
|
||||
'float',
|
||||
'decimal',
|
||||
'date',
|
||||
'boolean',
|
||||
|
||||
// nested component
|
||||
'component',
|
||||
'dynamiczone',
|
||||
];
|
||||
|
||||
const validateContentTypeInput = data => {
|
||||
return componentSchema
|
||||
.validate(data, {
|
||||
strict: true,
|
||||
abortEarly: false,
|
||||
})
|
||||
.catch(error => Promise.reject(formatYupErrors(error)));
|
||||
};
|
||||
|
||||
const validateUpdateContentTypeInput = data => {
|
||||
// convert zero length string on default attributes to undefined
|
||||
if (_.has(data, 'attributes')) {
|
||||
Object.keys(data.attributes).forEach(attribute => {
|
||||
if (data.attributes[attribute].default === '') {
|
||||
data.attributes[attribute].default = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return componentSchema
|
||||
.validate(data, {
|
||||
strict: true,
|
||||
abortEarly: false,
|
||||
})
|
||||
.catch(error => Promise.reject(formatYupErrors(error)));
|
||||
};
|
||||
|
||||
const componentSchema = yup
|
||||
.object({
|
||||
name: yup
|
||||
.string()
|
||||
.min(1)
|
||||
.required('name.required'),
|
||||
description: yup.string(),
|
||||
connection: yup.string(),
|
||||
collectionName: yup
|
||||
.string()
|
||||
.nullable()
|
||||
.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 = {
|
||||
type: yup
|
||||
.string()
|
||||
.oneOf(VALID_COMPONENT_TYPES)
|
||||
.required(),
|
||||
...getTypeShape(obj),
|
||||
};
|
||||
} else if (_.has(obj, 'target')) {
|
||||
shape = getRelationValidator(VALID_COMPONENT_RELATIONS);
|
||||
} 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();
|
||||
|
||||
module.exports = {
|
||||
validateContentTypeInput,
|
||||
validateUpdateContentTypeInput,
|
||||
};
|
||||
@ -4,9 +4,7 @@ const yup = require('yup');
|
||||
const _ = require('lodash');
|
||||
const { validators } = require('./common');
|
||||
|
||||
const VALID_NATURES = ['oneWay', 'manyWay'];
|
||||
|
||||
module.exports = () => {
|
||||
module.exports = validNatures => {
|
||||
return {
|
||||
target: yup
|
||||
.mixed()
|
||||
@ -29,7 +27,7 @@ module.exports = () => {
|
||||
.required(),
|
||||
nature: yup
|
||||
.string()
|
||||
.oneOf(VALID_NATURES)
|
||||
.oneOf(validNatures)
|
||||
.required(),
|
||||
plugin: yup.string().oneOf(['', ...Object.keys(strapi.plugins)]),
|
||||
unique: validators.unique,
|
||||
|
||||
@ -1,22 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const yup = require('yup');
|
||||
const {
|
||||
validators,
|
||||
VALID_TYPES,
|
||||
isValidName,
|
||||
isValidEnum,
|
||||
} = require('./common');
|
||||
|
||||
module.exports = obj => {
|
||||
return {
|
||||
type: yup
|
||||
.string()
|
||||
.oneOf(VALID_TYPES)
|
||||
.required(),
|
||||
...getTypeShape(obj),
|
||||
};
|
||||
};
|
||||
const { validators, isValidName, isValidEnum } = require('./common');
|
||||
|
||||
const getTypeShape = obj => {
|
||||
switch (obj.type) {
|
||||
@ -149,8 +134,36 @@ const getTypeShape = obj => {
|
||||
unique: validators.unique,
|
||||
};
|
||||
}
|
||||
|
||||
case 'component': {
|
||||
return {
|
||||
required: validators.required,
|
||||
repeatable: yup.boolean(),
|
||||
component: yup.string().required(),
|
||||
min: yup.number(),
|
||||
max: yup.number(),
|
||||
};
|
||||
}
|
||||
|
||||
case 'dynamiczone': {
|
||||
return {
|
||||
required: validators.required,
|
||||
components: yup
|
||||
.array()
|
||||
.of(yup.string())
|
||||
.min(1)
|
||||
.required(),
|
||||
min: yup.number(),
|
||||
max: yup.number(),
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getTypeShape,
|
||||
};
|
||||
|
||||
@ -4,7 +4,9 @@ const path = require('path');
|
||||
const _ = require('lodash');
|
||||
const fse = require('fs-extra');
|
||||
const pluralize = require('pluralize');
|
||||
const slugify = require('@sindresorhus/slugify');
|
||||
|
||||
const { formatAttributes, convertAttributes } = require('../utils/attributes');
|
||||
const { nameToSlug } = require('../utils/helpers');
|
||||
|
||||
/**
|
||||
* Returns a list of all available components with formatted attributes
|
||||
@ -32,7 +34,7 @@ const getComponent = uid => {
|
||||
* @param {Object} component - strapi component model
|
||||
*/
|
||||
const formatComponent = (uid, component) => {
|
||||
const { connection, collectionName, attributes, info, category } = component;
|
||||
const { connection, collectionName, info, category } = component;
|
||||
|
||||
return {
|
||||
uid,
|
||||
@ -43,65 +45,11 @@ const formatComponent = (uid, component) => {
|
||||
description: _.get(info, 'description', ''),
|
||||
connection,
|
||||
collectionName,
|
||||
attributes: formatAttributes(attributes, { component }),
|
||||
attributes: formatAttributes(component),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a component's attributes
|
||||
* @param {Object} attributes - the attributes map
|
||||
* @param {Object} context - function context
|
||||
* @param {Object} context.component - the associated component
|
||||
*/
|
||||
const formatAttributes = (attributes, { component }) => {
|
||||
return Object.keys(attributes).reduce((acc, key) => {
|
||||
acc[key] = formatAttribute(key, attributes[key], { component });
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fromats a component attribute
|
||||
* @param {string} key - the attribute key
|
||||
* @param {Object} attribute - the attribute
|
||||
* @param {Object} context - function context
|
||||
* @param {Object} context.component - the associated component
|
||||
*/
|
||||
const formatAttribute = (key, attribute, { component }) => {
|
||||
if (_.has(attribute, 'type')) return attribute;
|
||||
|
||||
// format relations
|
||||
const relation = (component.associations || []).find(
|
||||
assoc => assoc.alias === key
|
||||
);
|
||||
const { plugin } = attribute;
|
||||
let targetEntity = attribute.model || attribute.collection;
|
||||
|
||||
if (plugin === 'upload' && targetEntity === 'file') {
|
||||
return {
|
||||
type: 'media',
|
||||
multiple: attribute.collection ? true : false,
|
||||
required: attribute.required ? true : false,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
nature: relation.nature,
|
||||
target: targetEntity,
|
||||
plugin: plugin || undefined,
|
||||
dominant: attribute.dominant ? true : false,
|
||||
key: attribute.via || undefined,
|
||||
columnName: attribute.columnName || undefined,
|
||||
targetColumnName: _.get(
|
||||
strapi.getModel(targetEntity, plugin),
|
||||
['attributes', attribute.via, 'columnName'],
|
||||
undefined
|
||||
),
|
||||
unique: attribute.unique ? true : false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a component schema file
|
||||
* @param {string} uid
|
||||
@ -197,48 +145,6 @@ const createSchema = infos => {
|
||||
};
|
||||
};
|
||||
|
||||
const convertAttributes = attributes => {
|
||||
return Object.keys(attributes).reduce((acc, key) => {
|
||||
const attribute = attributes[key];
|
||||
|
||||
if (_.has(attribute, 'type')) {
|
||||
if (attribute.type === 'media') {
|
||||
const fileModel = strapi.getModel('file', 'upload');
|
||||
if (!fileModel) return acc;
|
||||
|
||||
const via = _.findKey(fileModel.attributes, { collection: '*' });
|
||||
acc[key] = {
|
||||
[attribute.multiple ? 'collection' : 'model']: 'file',
|
||||
via,
|
||||
plugin: 'upload',
|
||||
required: attribute.required ? true : false,
|
||||
};
|
||||
} else {
|
||||
acc[key] = attribute;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (_.has(attribute, 'target')) {
|
||||
const { target, nature, unique, plugin } = attribute;
|
||||
|
||||
// ingore relation which aren't oneWay or manyWay
|
||||
if (!['oneWay', 'manyWay'].includes(nature)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[key] = {
|
||||
[nature === 'oneWay' ? 'model' : 'collection']: target,
|
||||
plugin: plugin ? _.trim(plugin) : undefined,
|
||||
unique: unique === true ? true : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a uid from a string
|
||||
* @param {string} str - string to slugify
|
||||
@ -246,12 +152,6 @@ const convertAttributes = attributes => {
|
||||
const createComponentUID = ({ category, name }) =>
|
||||
`${category}.${nameToSlug(name)}`;
|
||||
|
||||
/**
|
||||
* Converts a name to a slug
|
||||
* @param {string} name a name to convert
|
||||
*/
|
||||
const nameToSlug = name => slugify(name, { separator: '_' });
|
||||
|
||||
/**
|
||||
* Deletes a component
|
||||
* @param {Object} component
|
||||
|
||||
126
packages/strapi-plugin-content-type-builder/utils/attributes.js
Normal file
126
packages/strapi-plugin-content-type-builder/utils/attributes.js
Normal file
@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const MODEL_RELATIONS = ['oneWay', 'oneToOne', 'manyToOne'];
|
||||
const COLLECTION_RELATIONS = ['manyWay', 'manyToMany', 'oneToMany'];
|
||||
|
||||
/**
|
||||
* Formats a component's attributes
|
||||
* @param {Object} attributes - the attributes map
|
||||
* @param {Object} context - function context
|
||||
* @param {Object} context.component - the associated component
|
||||
*/
|
||||
const formatAttributes = model => {
|
||||
return Object.keys(model.attributes).reduce((acc, key) => {
|
||||
acc[key] = formatAttribute(key, model.attributes[key], { model });
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fromats a component attribute
|
||||
* @param {string} key - the attribute key
|
||||
* @param {Object} attribute - the attribute
|
||||
* @param {Object} context - function context
|
||||
* @param {Object} context.component - the associated component
|
||||
*/
|
||||
const formatAttribute = (key, attribute, { model }) => {
|
||||
if (_.has(attribute, 'type')) return attribute;
|
||||
|
||||
// format relations
|
||||
const relation = (model.associations || []).find(
|
||||
assoc => assoc.alias === key
|
||||
);
|
||||
const { plugin } = attribute;
|
||||
let targetEntity = attribute.model || attribute.collection;
|
||||
|
||||
if (plugin === 'upload' && targetEntity === 'file') {
|
||||
return {
|
||||
type: 'media',
|
||||
multiple: attribute.collection ? true : false,
|
||||
required: attribute.required ? true : false,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
nature: relation.nature,
|
||||
target: targetEntity,
|
||||
plugin: plugin || undefined,
|
||||
dominant: attribute.dominant ? true : false,
|
||||
key: attribute.via || undefined,
|
||||
columnName: attribute.columnName || undefined,
|
||||
targetColumnName: _.get(
|
||||
strapi.getModel(targetEntity, plugin),
|
||||
['attributes', attribute.via, 'columnName'],
|
||||
undefined
|
||||
),
|
||||
unique: attribute.unique ? true : false,
|
||||
required: attribute.required ? true : false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const convertAttributes = attributes => {
|
||||
return Object.keys(attributes).reduce((acc, key) => {
|
||||
const attribute = attributes[key];
|
||||
|
||||
if (_.has(attribute, 'type')) {
|
||||
if (attribute.type === 'media') {
|
||||
const fileModel = strapi.getModel('file', 'upload');
|
||||
if (!fileModel) return acc;
|
||||
|
||||
const via = _.findKey(fileModel.attributes, { collection: '*' });
|
||||
acc[key] = {
|
||||
[attribute.multiple ? 'collection' : 'model']: 'file',
|
||||
via,
|
||||
plugin: 'upload',
|
||||
required: attribute.required ? true : false,
|
||||
};
|
||||
} else {
|
||||
acc[key] = attribute;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (_.has(attribute, 'target')) {
|
||||
const {
|
||||
target,
|
||||
nature,
|
||||
unique,
|
||||
plugin,
|
||||
required,
|
||||
key,
|
||||
columnName,
|
||||
dominant,
|
||||
} = attribute;
|
||||
|
||||
const attr = {
|
||||
plugin: plugin ? _.trim(plugin) : undefined,
|
||||
unique: unique === true ? true : undefined,
|
||||
dominant,
|
||||
required,
|
||||
columnName,
|
||||
};
|
||||
|
||||
if (MODEL_RELATIONS.includes(nature)) {
|
||||
attr.model = target;
|
||||
} else if (COLLECTION_RELATIONS.includes(nature)) {
|
||||
attr.collection = target;
|
||||
}
|
||||
|
||||
if (!['manyWay', 'oneWay'].includes(nature)) {
|
||||
attr.via = key;
|
||||
}
|
||||
|
||||
acc[key] = attr;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
formatAttributes,
|
||||
convertAttributes,
|
||||
};
|
||||
@ -1,3 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const slugify = require('@sindresorhus/slugify');
|
||||
|
||||
const escapeNewlines = (content = '', placeholder = '\n') => {
|
||||
return content.replace(/[\r\n]+/g, placeholder);
|
||||
};
|
||||
@ -18,7 +22,14 @@ const deepTrimObject = attribute => {
|
||||
return typeof attribute === 'string' ? attribute.trim() : attribute;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a name to a slug
|
||||
* @param {string} name a name to convert
|
||||
*/
|
||||
const nameToSlug = name => slugify(name, { separator: '-' });
|
||||
|
||||
module.exports = {
|
||||
escapeNewlines,
|
||||
deepTrimObject,
|
||||
nameToSlug,
|
||||
};
|
||||
|
||||
@ -119,9 +119,9 @@ program
|
||||
// `$ strapi generate:api`
|
||||
program
|
||||
.command('generate:api <id> [attributes...]')
|
||||
.option('-t, --tpl <template>', 'template name')
|
||||
.option('-a, --api <api>', 'API name to generate a sub API')
|
||||
.option('-p, --plugin <plugin>', 'plugin name to generate a sub API')
|
||||
.option('-p, --plugin <api>', 'plugin name')
|
||||
.option('-c, --connection <connection>', 'The name of the connection to use')
|
||||
.description('generate a basic API')
|
||||
.action((id, attributes, cliArguments) => {
|
||||
cliArguments.attributes = attributes;
|
||||
|
||||
14
packages/strapi/lib/core/bootstrap.js
vendored
14
packages/strapi/lib/core/bootstrap.js
vendored
@ -37,6 +37,8 @@ module.exports = function(strapi) {
|
||||
);
|
||||
}
|
||||
|
||||
strapi.contentTypes = {};
|
||||
|
||||
Object.keys(strapi.components).forEach(key => {
|
||||
const component = strapi.components[key];
|
||||
|
||||
@ -46,7 +48,7 @@ module.exports = function(strapi) {
|
||||
if (!component.collectionName)
|
||||
throw new Error(`Component ${key} is missing a collectionName attribute`);
|
||||
|
||||
return Object.assign(component, {
|
||||
Object.assign(component, {
|
||||
uid: key,
|
||||
modelType: 'component',
|
||||
globalId:
|
||||
@ -61,13 +63,15 @@ module.exports = function(strapi) {
|
||||
|
||||
Object.assign(model, {
|
||||
modelType: 'contentType',
|
||||
uid: `app::${key}.${index}`,
|
||||
uid: `application::${key}.${index}`,
|
||||
apiName: key,
|
||||
globalId: model.globalId || _.upperFirst(_.camelCase(index)),
|
||||
collectionName: model.collectionName || `${index}`.toLocaleLowerCase(),
|
||||
connection: model.connection || defaultConnection,
|
||||
});
|
||||
|
||||
strapi.contentTypes[model.uid] = model;
|
||||
|
||||
// find corresponding service and controller
|
||||
const userService = _.get(strapi.api[key], ['services', index], {});
|
||||
const userController = _.get(strapi.api[key], ['controllers', index], {});
|
||||
@ -129,13 +133,15 @@ module.exports = function(strapi) {
|
||||
|
||||
Object.assign(model, {
|
||||
modelType: 'contentType',
|
||||
uid: `admin::${key}`,
|
||||
uid: `strapi::${key}`,
|
||||
identity: model.identity || _.upperFirst(key),
|
||||
globalId: model.globalId || _.upperFirst(_.camelCase(`admin-${key}`)),
|
||||
connection:
|
||||
model.connection ||
|
||||
strapi.config.currentEnvironment.database.defaultConnection,
|
||||
});
|
||||
|
||||
strapi.contentTypes[model.uid] = model;
|
||||
});
|
||||
|
||||
Object.keys(strapi.plugins).forEach(pluginName => {
|
||||
@ -169,6 +175,8 @@ module.exports = function(strapi) {
|
||||
model.connection ||
|
||||
strapi.config.currentEnvironment.database.defaultConnection,
|
||||
});
|
||||
|
||||
strapi.contentTypes[model.uid] = model;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user