Init new validation schemas for CT and compo

This commit is contained in:
Alexandre Bodin 2019-10-29 18:33:38 +01:00
parent a1d3c3abb5
commit 489b653e1b
25 changed files with 576 additions and 362 deletions

View File

@ -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();
};

View File

@ -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,
},
},
};

View File

@ -1 +0,0 @@
"<%= name %>": <%= JSON.stringify(params, null, 2) %>

View File

@ -1,16 +0,0 @@
{
"connection": "<%= connection %>",
"collectionName": "<%= collectionName || idPluralized %>",
"info": {
"name": "<%= id %>",
"description": "<%= description %>"
},
"options": {
"increments": true,
"timestamps": true,
"comment": ""
},
"attributes": {
<%= attributes %>
}
}

View File

@ -0,0 +1 @@
<%= schema %>

View File

@ -1,7 +0,0 @@
'use strict';
/**
* Read the documentation () to implement custom controller functions
*/
module.exports = {};

View File

@ -1,14 +0,0 @@
{
"connection": "<%= connection %>",
"collectionName": "<%= collectionName || '' %>",
"info": {
"name": "<%= id %>",
"description": "<%= description %>"
},
"options": {
"timestamps": true
},
"attributes": {
<%= attributes %>
}
}

View File

@ -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) => {}
};

View File

@ -1,7 +0,0 @@
'use strict';
/**
* Read the documentation () to implement custom service functions
*/
module.exports = {};

View File

@ -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": []
}

View File

@ -0,0 +1,11 @@
'use strict';
module.exports = {
async getConnections(ctx) {
ctx.send({
connections: Object.keys(
strapi.config.currentEnvironment.database.connections
),
});
},
};

View File

@ -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),
});
});
};

View File

@ -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,
};

View File

@ -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',

View File

@ -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,
};

View File

@ -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,

View File

@ -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,
};

View File

@ -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

View 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,
};

View File

@ -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,
};

View File

@ -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;

View File

@ -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;
});
});