merge develop and regex validation

This commit is contained in:
Virginie Ky 2019-08-01 17:42:20 +02:00
commit e6589ce6c6
32 changed files with 649 additions and 350 deletions

View File

@ -8,47 +8,36 @@ module.exports = {
// Before saving a value.
// Fired before an `insert` or `update` query.
// beforeSave: async (model, attrs, options) => {},
// After saving a value.
// Fired after an `insert` or `update` query.
// afterSave: async (model, response, options) => {},
// Before fetching a value.
// Fired before a `fetch` operation.
// beforeFetch: async (model, columns, options) => {},
// After fetching a value.
// Fired after a `fetch` operation.
// afterFetch: async (model, response, options) => {},
// Before fetching all values.
// Fired before a `fetchAll` operation.
// beforeFetchAll: async (model, columns, options) => {},
// After fetching all values.
// Fired after a `fetchAll` operation.
// afterFetchAll: async (model, response, options) => {},
// Before creating a value.
// Fired before an `insert` query.
// beforeCreate: async (model, attrs, options) => {},
// After creating a value.
// Fired after an `insert` query.
// afterCreate: async (model, attrs, options) => {},
// Before updating a value.
// Fired before an `update` query.
// beforeUpdate: async (model, attrs, options) => {},
// After updating a value.
// Fired after an `update` query.
// afterUpdate: async (model, attrs, options) => {},
// Before destroying a value.
// Fired before a `delete` query.
// beforeDestroy: async (model, attrs, options) => {},
// After destroying a value.
// Fired after a `delete` query.
// afterDestroy: async (model, attrs, options) => {}

View File

@ -26,6 +26,15 @@
"number": {
"type": "integer"
},
"big_number": {
"type": "biginteger"
},
"float_number": {
"type": "float"
},
"decimal_number": {
"type": "decimal"
},
"date": {
"type": "date"
},
@ -58,7 +67,6 @@
"dominant": true
},
"fb_cta": {
"required": true,
"type": "group",
"group": "facebook_cta"
},

View File

@ -113,7 +113,7 @@
"components.Input.error.validation.regex": "値が正規表現と一致しません",
"components.Input.error.validation.required": "値は必須項目です",
"components.ListRow.empty": "表示するデータがありません",
"components.OverlayBlocker.description": "サーバーのスタートが必要な機能を使用しています。サーバーが起動するまでお待ち下さい",
"components.OverlayBlocker.description": "サーバーのスタートが必要な機能を使用しています。サーバーが起動するまでお待ち下さい",
"components.OverlayBlocker.title": "リスタートを待っています...",
"components.PageFooter.select": "ページ毎に表示する投稿数",
"components.ProductionBlocker.description": "このプラグインは、安全のため、他の環境では無効する必要があります",

View File

@ -15,9 +15,9 @@ const createGroupModels = async ({ model, definition, ORM, GLOBALS }) => {
const joinColumn = `${pluralize.singular(collectionName)}_${primaryKey}`;
const joinModel = ORM.Model.extend({
tableName: joinTable,
slice() {
group() {
return this.morphTo(
'slice',
'group',
...groupAttributes.map(key => {
const groupKey = definition.attributes[key].group;
return GLOBALS[strapi.groups[groupKey].globalId];
@ -59,8 +59,8 @@ const createGroupJoinTables = async ({ definition, ORM }) => {
.integer('order')
.unsigned()
.notNullable();
table.string('slice_type').notNullable();
table.integer('slice_id').notNullable();
table.string('group_type').notNullable();
table.integer('group_id').notNullable();
table.integer(joinColumn).notNullable();
table

View File

@ -436,6 +436,15 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
});
};
// Extract association except polymorphic.
const associations = definition.associations.filter(
association => association.nature.toLowerCase().indexOf('morph') === -1
);
// Extract polymorphic association.
const polymorphicAssociations = definition.associations.filter(
association => association.nature.toLowerCase().indexOf('morph') !== -1
);
// Update serialize to reformat data for polymorphic associations.
loadedModel.serialize = function(options) {
const attrs = _.clone(this.attributes);
@ -447,27 +456,14 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
const relations = this.relations;
groupAttributes.forEach(key => {
const { repeatable } = definition.attributes[key];
if (relations[key]) {
const groups = relations[key].toJSON().map(el => el.slice);
const groups = relations[key].toJSON().map(el => el.group);
attrs[key] =
definition.attributes[key].repeatable === true
? groups
: _.first(groups) || null;
attrs[key] = repeatable === true ? groups : _.first(groups) || null;
}
});
// Extract association except polymorphic.
const associations = definition.associations.filter(
association =>
association.nature.toLowerCase().indexOf('morph') === -1
);
// Extract polymorphic association.
const polymorphicAssociations = definition.associations.filter(
association =>
association.nature.toLowerCase().indexOf('morph') !== -1
);
polymorphicAssociations.map(association => {
// Retrieve relation Bookshelf object.
const relation = relations[association.alias];
@ -489,7 +485,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
switch (association.nature) {
case 'oneToManyMorph':
attrs[association.alias] =
attrs[association.alias][model.collectionName];
attrs[association.alias][model.collectionName] || null;
break;
case 'manyToManyMorph':
attrs[association.alias] = attrs[association.alias].map(
@ -497,7 +493,8 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
);
break;
case 'oneMorphToOne':
attrs[association.alias] = attrs[association.alias].related;
attrs[association.alias] =
attrs[association.alias].related || null;
break;
case 'manyMorphToOne':
case 'manyMorphToMany':
@ -524,6 +521,176 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
return attrs;
};
const findModelByAssoc = ({ assoc }) => {
const target = assoc.collection || assoc.model;
return assoc.plugin === 'admin'
? strapi.admin.models[target]
: assoc.plugin
? strapi.plugins[assoc.plugin].models[target]
: strapi.models[target];
};
const isPolymorphic = ({ assoc }) => {
return assoc.nature.toLowerCase().indexOf('morph') !== -1;
};
const formatPolymorphicPopulate = ({ assoc, path, prefix = '' }) => {
if (_.isString(path) && path === assoc.via) {
return { [`related.${assoc.via}`]: () => {} };
} else if (_.isString(path) && path === assoc.alias) {
// MorphTo side.
if (assoc.related) {
return { [`${prefix}${assoc.alias}.related`]: () => {} };
}
// oneToMorph or manyToMorph side.
// Retrieve collection name because we are using it to build our hidden model.
const model = findModelByAssoc({ assoc });
return {
[`${prefix}${assoc.alias}.${model.collectionName}`]: function(
query
) {
query.orderBy('created_at', 'desc');
},
};
}
};
const createAssociationPopulate = () => {
return definition.associations
.filter(ast => ast.autoPopulate !== false)
.map(assoc => {
if (isPolymorphic({ assoc })) {
return formatPolymorphicPopulate({
assoc,
path: assoc.alias,
});
}
let path = assoc.alias;
let extraAssocs = [];
if (assoc) {
const assocModel = findModelByAssoc({ assoc });
extraAssocs = assocModel.associations
.filter(assoc => isPolymorphic({ assoc }))
.map(assoc =>
formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix: `${path}.`,
})
);
}
return [assoc.alias, ...extraAssocs];
})
.reduce((acc, val) => acc.concat(val), []);
};
const populateGroup = key => {
let paths = [];
const group = strapi.groups[definition.attributes[key].group];
const assocs = (group.associations || []).filter(
assoc => assoc.autoPopulate === true
);
// paths.push(`${key}.group`);
assocs.forEach(assoc => {
if (isPolymorphic({ assoc })) {
const rel = formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix: `${key}.group.`,
});
paths.push(rel);
} else {
paths.push(`${key}.group.${assoc.alias}`);
}
});
return paths;
};
const createGroupsPopulate = () => {
const groupsToPopulate = groupAttributes.reduce((acc, key) => {
const attribute = definition.attributes[key];
const autoPopulate = _.get(attribute, ['autoPopulate'], true);
if (autoPopulate === true) {
return acc.concat(populateGroup(key));
}
return acc;
}, []);
return groupsToPopulate;
};
const isGroup = (def, key) =>
_.get(def, ['attributes', key, 'type']) === 'group';
const formatPopulateOptions = withRelated => {
if (!Array.isArray(withRelated)) withRelated = [withRelated];
const obj = withRelated.reduce((acc, key) => {
if (_.isString(key)) {
acc[key] = () => {};
return acc;
}
return _.extend(acc, key);
}, {});
// if groups are no
const finalObj = Object.keys(obj).reduce((acc, key) => {
// check the key path and update it if necessary nothing more
const parts = key.split('.');
let newKey;
let prefix = '';
let tmpModel = definition;
for (let part of parts) {
if (isGroup(tmpModel, part)) {
tmpModel = strapi.groups[tmpModel.attributes[part].group];
// add group path and there relations / images
const path = `${prefix}${part}.group`;
newKey = path;
prefix = `${path}.`;
continue;
}
const assoc = tmpModel.associations.find(
association => association.alias === part
);
if (!assoc) return acc;
tmpModel = findModelByAssoc({ assoc });
if (isPolymorphic({ assoc })) {
const path = formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix,
});
return _.extend(acc, path);
}
newKey = `${prefix}${part}`;
prefix = `${newKey}.`;
}
acc[newKey] = obj[key];
return acc;
}, {});
return [finalObj];
};
// Initialize lifecycle callbacks.
loadedModel.initialize = function() {
const lifecycle = {
@ -547,84 +714,19 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
}
});
const findModelByAssoc = ({ assoc }) => {
return assoc.plugin
? strapi.plugins[assoc.plugin].models[
assoc.collection || assoc.model
]
: strapi.models[assoc.collection || assoc.model];
};
const isPolymorphic = ({ assoc }) => {
return assoc.nature.toLowerCase().indexOf('morph') !== -1;
};
const formatPolymorphicPopulate = ({ assoc, path, prefix = '' }) => {
if (_.isString(path) && path === assoc.via) {
return `related.${assoc.via}`;
} else if (_.isString(path) && path === assoc.alias) {
// MorphTo side.
if (assoc.related) {
return `${prefix}${assoc.alias}.related`;
}
// oneToMorph or manyToMorph side.
// Retrieve collection name because we are using it to build our hidden model.
const model = findModelByAssoc({ assoc });
return {
[`${prefix}${assoc.alias}.${model.collectionName}`]: function(
query
) {
query.orderBy('created_at', 'desc');
},
};
}
};
const addPolymorphicRelated = path => {
const assoc = definition.associations.find(
assoc => assoc.alias === path || assoc.via === path
);
if (assoc && isPolymorphic({ assoc })) {
return formatPolymorphicPopulate({
assoc,
path,
});
}
let extraAssocs = [];
if (assoc) {
const assocModel = findModelByAssoc({ assoc });
extraAssocs = assocModel.associations
.filter(assoc => isPolymorphic({ assoc }))
.map(assoc =>
formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix: `${path}.`,
})
);
}
return [path, ...extraAssocs];
};
// Update withRelated level to bypass many-to-many association for polymorphic relationshiips.
// Apply only during fetching.
this.on('fetching fetching:collection', (instance, attrs, options) => {
if (_.isArray(options.withRelated)) {
options.withRelated = options.withRelated
.concat(groupAttributes.map(key => `${key}.slice`))
.map(addPolymorphicRelated)
.reduce((acc, paths) => acc.concat(paths), []);
// do not populate anything
if (options.withRelated === false) return;
if (options.isEager === true) return;
if (_.isNil(options.withRelated)) {
options.withRelated = []
.concat(createGroupsPopulate())
.concat(createAssociationPopulate());
} else {
options.withRelated = groupAttributes
.map(key => `${key}.slice`)
.map(addPolymorphicRelated)
.reduce((acc, paths) => acc.concat(paths), []);
options.withRelated = formatPopulateOptions(options.withRelated);
}
return _.isFunction(target[model.toLowerCase()]['beforeFetchAll'])

View File

@ -19,11 +19,6 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
return model.attributes[key].type === 'group';
});
// default relations to populate
const defaultPopulate = model.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
// Returns an object with relation keys only to create relations in DB
const pickRelations = values => {
return _.pick(values, assocKeys);
@ -58,7 +53,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
}
const entry = await model.forge(params).fetch({
withRelated: populate || defaultPopulate,
withRelated: populate,
});
return entry ? entry.toJSON() : null;
@ -72,7 +67,10 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
return model
.query(buildQuery({ model, filters }))
.fetchAll({ withRelated: populate || defaultPopulate, transacting })
.fetchAll({
withRelated: populate,
transacting,
})
.then(results => results.toJSON());
}
@ -196,9 +194,6 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
// Convert `params` object to filters compatible with Bookshelf.
const filters = modelUtils.convertParams(modelKey, params);
// Select field to populate.
const withRelated = populate || defaultPopulate;
return model
.query(qb => {
buildSearchQuery(qb, model, params);
@ -216,7 +211,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
}
})
.fetchAll({
withRelated,
withRelated: populate,
});
}
@ -248,8 +243,8 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
return joinModel.forge().save(
{
[foreignKey]: entry.id,
slice_type: groupModel.collectionName,
slice_id: group.id,
group_type: groupModel.collectionName,
group_id: group.id,
field: key,
order,
},
@ -317,8 +312,8 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
.query({
where: {
[foreignKey]: entry.id,
slice_type: groupModel.collectionName,
slice_id: group.id,
group_type: groupModel.collectionName,
group_id: group.id,
field: key,
},
})
@ -338,8 +333,8 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
return joinModel.forge().save(
{
[foreignKey]: entry.id,
slice_type: groupModel.collectionName,
slice_id: group.id,
group_type: groupModel.collectionName,
group_id: group.id,
field: key,
order,
},
@ -396,7 +391,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
qb.where(joinModel.foreignKey, entry.id).andWhere('field', key);
})
.fetchAll({ transacting })
.map(el => el.get('slice_id').toString());
.map(el => el.get('group_id').toString());
// verify the provided ids are realted to this entity.
idsToKeep.forEach(id => {
@ -413,7 +408,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
if (idsToDelete.length > 0) {
await joinModel
.forge()
.query(qb => qb.whereIn('slice_id', idsToDelete))
.query(qb => qb.whereIn('group_id', idsToDelete))
.destroy({ transacting, require: false });
await strapi
@ -442,12 +437,12 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
.query({
where: {
[foreignKey]: entry.id,
slice_type: groupModel.collectionName,
group_type: groupModel.collectionName,
field: key,
},
})
.fetchAll({ transacting })
.map(el => el.get('slice_id'));
.map(el => el.get('group_id'));
await strapi
.query(groupModel.uid)
@ -458,7 +453,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
.query({
where: {
[foreignKey]: entry.id,
slice_type: groupModel.collectionName,
group_type: groupModel.collectionName,
field: key,
},
})

View File

@ -51,7 +51,7 @@ module.exports = {
[this.primaryKey]: getValuePrimaryKey(params, this.primaryKey),
}).fetch({
transacting,
withRelated: populate || this.associations.map(x => x.alias),
withRelated: populate,
});
const data = record ? record.toJSON() : record;
@ -395,7 +395,6 @@ module.exports = {
[this.primaryKey]: getValuePrimaryKey(params, this.primaryKey),
}).fetch({
transacting,
withRelated: this.associations.map(x => x.alias),
});
},
@ -410,7 +409,6 @@ module.exports = {
})
.fetch({
transacting,
withRelated: this.associations.map(x => x.alias),
});
const entry = record ? record.toJSON() : record;

View File

@ -24,59 +24,10 @@ module.exports = async (ctx, next) => {
'actions',
ctx.request.route.action,
],
[],
[]
).split('.');
if (controller && action) {
// Redirect to specific controller.
if (
ctx.request.body.hasOwnProperty('fields') &&
ctx.request.body.hasOwnProperty('files')
) {
let { files, fields } = ctx.request.body;
const parser = value => {
try {
value = JSON.parse(value);
} catch (e) {
// Silent.
}
return _.isArray(value) ? value.map(obj => parser(obj)) : value;
};
fields = Object.keys(fields).reduce((acc, current) => {
acc[current] = parser(fields[current]);
return acc;
}, {});
ctx.request.body = fields;
await target.controllers[controller.toLowerCase()][action](ctx);
const resBody = ctx.body;
await Promise.all(
Object.keys(files).map(async field => {
ctx.request.body = {
files: {
files: files[field],
},
fields: {
refId: resBody.id || resBody._id,
ref: ctx.params.model,
source,
field,
},
};
return strapi.plugins.upload.controllers.upload.upload(ctx);
}),
);
return ctx.send(resBody);
}
return await target.controllers[controller.toLowerCase()][action](ctx);
}
}

View File

@ -64,6 +64,14 @@
"policies": []
}
},
{
"method": "POST",
"path": "/explorer/upload",
"handler": "ContentManager.uploadFile",
"config": {
"policies": []
}
},
{
"method": "GET",
"path": "/explorer/:model",

View File

@ -144,4 +144,15 @@ module.exports = {
ctx.request.query
);
},
/**
* Handle uploads in the explorer
*/
async uploadFile(ctx) {
if (!strapi.plugins.upload) {
ctx.send({ error: 'uploadPlugin.notInstalled' }, 400);
}
return strapi.plugins.upload.controllers.upload.upload(ctx);
},
};

View File

@ -5,27 +5,6 @@ const _ = require('lodash');
/**
* A set of functions called "actions" for `ContentManager`
*/
const parseFormInput = value => {
try {
const parsed = JSON.parse(value);
// do not modify initial value if it is string except 'null'
if (typeof parsed !== 'string') {
value = parsed;
}
} catch (e) {
// Silent.
}
return _.isArray(value) ? value.map(parseFormInput) : value;
};
const parseFormData = fields =>
Object.keys(fields).reduce((acc, current) => {
acc[current] = parseFormInput(fields[current]);
return acc;
}, {});
module.exports = {
fetch(params, source, populate) {
return strapi
@ -53,61 +32,10 @@ module.exports = {
},
async add(params, values, source) {
// Multipart/form-data.
if (values.hasOwnProperty('fields') && values.hasOwnProperty('files')) {
const data = parseFormData(values.fields);
const entry = await strapi.query(params.model, source).create(data);
// Then, request plugin upload.
if (strapi.plugins.upload && Object.keys(values.files).length > 0) {
// Upload new files and attach them to this entity.
await strapi.plugins.upload.services.upload.uploadToEntity(
{
id: entry.id || entry._id,
model: params.model,
},
values.files,
source
);
}
return entry;
}
// Create an entry using `queries` system
return await strapi.query(params.model, source).create(values);
},
async edit(params, values, source) {
// Multipart/form-data.
if (values.hasOwnProperty('fields') && values.hasOwnProperty('files')) {
// set empty attributes if old values was cleared
_.difference(
Object.keys(values.files),
Object.keys(values.fields)
).forEach(attr => {
values.fields[attr] = [];
});
const data = parseFormData(values.fields);
const updatedEntity = await strapi
.query(params.model, source)
.update({ id: params.id }, data);
// Then, request plugin upload.
if (strapi.plugins.upload) {
// Upload new files and attach them to this entity.
await strapi.plugins.upload.services.upload.uploadToEntity(
params,
values.files,
source
);
}
return updatedEntity;
}
// Raw JSON.
return strapi.query(params.model, source).update({ id: params.id }, values);
},

View File

@ -12,6 +12,8 @@ const isSortable = (schema, name) => {
return false;
}
if (schema.modelType === 'group' && name === 'id') return false;
const attribute = schema.attributes[name];
if (NON_SORTABLES.includes(attribute.type)) {
return false;

View File

@ -9,7 +9,7 @@ async function createDefaultConfiguration(model) {
const schema = formatContentTypeSchema(model);
return {
settings: await createDefaultSettings(),
settings: await createDefaultSettings(schema),
metadatas: await createDefaultMetadatas(schema),
layouts: await createDefaultLayouts(schema),
};

View File

@ -3,18 +3,32 @@
const _ = require('lodash');
const { isSortable } = require('./attributes');
const getDefaultMainField = schema => {
if (schema.modelType == 'group') {
// find first group attribute that is sortable
return (
Object.keys(schema.attributes).find(key => isSortable(schema, key)) ||
'id'
);
}
return 'id';
};
/**
* Retunrs a configuration default settings
*/
async function createDefaultSettings() {
async function createDefaultSettings(schema) {
const generalSettings = await strapi.plugins[
'content-manager'
].services.generalsettings.getGeneralSettings();
let defaultField = getDefaultMainField(schema);
return {
...generalSettings,
mainField: 'id',
defaultSortBy: 'id',
mainField: defaultField,
defaultSortBy: defaultField,
defaultSortOrder: 'ASC',
};
}
@ -24,13 +38,17 @@ async function createDefaultSettings() {
async function syncSettings(configuration, schema) {
if (_.isEmpty(configuration.settings)) return createDefaultSettings(schema);
const { mainField = 'id', defaultSortBy = 'id' } =
let defaultField = getDefaultMainField(schema);
const { mainField = defaultField, defaultSortBy = defaultField } =
configuration.settings || {};
return {
...configuration.settings,
mainField: isSortable(schema, mainField) ? mainField : 'id',
defaultSortBy: isSortable(schema, defaultSortBy) ? defaultSortBy : 'id',
mainField: isSortable(schema, mainField) ? mainField : defaultField,
defaultSortBy: isSortable(schema, defaultSortBy)
? defaultSortBy
: defaultField,
};
}

View File

@ -57,7 +57,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/tag/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'tag1',
},
});
@ -73,7 +73,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/tag/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'tag2',
},
});
@ -89,7 +89,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/tag/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'tag3',
},
});
@ -110,7 +110,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/article/?source=content-manager',
method: 'POST',
formData: entry,
body: entry,
});
data.articles.push(body);
@ -132,7 +132,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/article/?source=content-manager',
method: 'POST',
formData: entry,
body: entry,
});
data.articles.push(body);
@ -155,7 +155,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry,
body: entry,
});
data.articles[0] = body;
@ -178,7 +178,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry,
body: entry,
});
data.articles[0] = body;
@ -199,7 +199,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry,
body: entry,
});
data.articles[0] = body;
@ -221,7 +221,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry,
body: entry,
});
data.articles[0] = body;
@ -237,7 +237,7 @@ describe('Content Manager End to End', () => {
const { body: createdTag } = await rq({
url: '/content-manager/explorer/tag/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'tag11',
},
});
@ -245,7 +245,7 @@ describe('Content Manager End to End', () => {
const { body: article12 } = await rq({
url: '/content-manager/explorer/article/?source=content-manager',
method: 'POST',
formData: {
body: {
title: 'article12',
content: 'Content',
tags: [createdTag],
@ -260,7 +260,7 @@ describe('Content Manager End to End', () => {
const { body: article13 } = await rq({
url: '/content-manager/explorer/article/?source=content-manager',
method: 'POST',
formData: {
body: {
title: 'article13',
content: 'Content',
tags: [updatedTag],
@ -308,7 +308,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/articlewithtag/?source=content-manager',
method: 'POST',
formData: entry,
body: entry,
});
expect(body.id);
@ -330,7 +330,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/category/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'cat1',
},
});
@ -346,7 +346,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/category/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'cat2',
},
});
@ -368,7 +368,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/article/?source=content-manager',
method: 'POST',
formData: entry,
body: entry,
});
data.articles.push(body);
@ -390,7 +390,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry,
body: entry,
});
data.articles[0] = body;
@ -411,7 +411,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/article?source=content-manager',
method: 'POST',
formData: entry,
body: entry,
});
data.articles.push(body);
@ -432,7 +432,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry,
body: entry,
});
data.articles[1] = body;
@ -453,7 +453,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: `/content-manager/explorer/category/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry,
body: entry,
});
data.categories[0] = body;
@ -473,7 +473,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/category/?source=content-manager',
method: 'POST',
formData: entry,
body: entry,
});
data.categories.push(body);
@ -549,7 +549,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/reference/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'ref1',
},
});
@ -569,7 +569,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/article?source=content-manager',
method: 'POST',
formData: entry,
body: entry,
});
data.articles.push(body);
@ -589,7 +589,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry,
body: entry,
});
data.articles[0] = body;
@ -610,7 +610,7 @@ describe('Content Manager End to End', () => {
let { body } = await rq({
url: '/content-manager/explorer/article?source=content-manager',
method: 'POST',
formData: entry,
body: entry,
});
data.articles.push(body);
@ -627,7 +627,7 @@ describe('Content Manager End to End', () => {
const { body: tagToCreate } = await rq({
url: '/content-manager/explorer/tag/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'tag111',
},
});
@ -635,7 +635,7 @@ describe('Content Manager End to End', () => {
const { body: referenceToCreate } = await rq({
url: '/content-manager/explorer/reference/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'cat111',
tag: tagToCreate,
},
@ -648,7 +648,7 @@ describe('Content Manager End to End', () => {
const { body: tagToCreate } = await rq({
url: '/content-manager/explorer/tag/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'tag111',
},
});
@ -656,7 +656,7 @@ describe('Content Manager End to End', () => {
const { body: referenceToCreate } = await rq({
url: '/content-manager/explorer/reference/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'cat111',
tag: tagToCreate,
},
@ -667,7 +667,7 @@ describe('Content Manager End to End', () => {
const { body: referenceToUpdate } = await rq({
url: `/content-manager/explorer/reference/${referenceToCreate.id}?source=content-manager`,
method: 'PUT',
formData: {
body: {
tag: null,
},
});
@ -679,7 +679,7 @@ describe('Content Manager End to End', () => {
const { body: tagToCreate } = await rq({
url: '/content-manager/explorer/tag/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'tag111',
},
});
@ -687,7 +687,7 @@ describe('Content Manager End to End', () => {
const { body: referenceToCreate } = await rq({
url: '/content-manager/explorer/reference/?source=content-manager',
method: 'POST',
formData: {
body: {
name: 'cat111',
tag: tagToCreate,
},

View File

@ -82,6 +82,25 @@ class AttributeForm extends React.Component {
acc[current] = [{ id: `${pluginId}.error.validation.required` }];
}
if (
current === 'name' &&
!new RegExp('^[A-Za-z][_0-9A-Za-z]*$').test(value)
) {
acc[current] = [{ id: `${pluginId}.error.validation.regex.name` }];
}
if (current === 'enum' && !!value) {
const split = value.split('\n');
const hasEnumFormatError = split.filter(
v => !new RegExp('^[A-Za-z][_0-9A-Za-z]*$').test(v)
);
if (hasEnumFormatError.length > 0) {
acc[current] = [{ id: `${pluginId}.error.validation.regex.values` }];
}
}
return acc;
}, formErrors);

View File

@ -104,6 +104,12 @@ class ModelForm extends React.Component {
formErrors = { name: [{ id: `${pluginId}.error.validation.required` }] };
}
if (!new RegExp('^[A-Za-z][_0-9A-Za-z]*$').test(modifiedData.name)) {
formErrors = {
name: [{ id: `${pluginId}.error.validation.regex.name` }],
};
}
this.setState(prevState => ({
formErrors,
didCheckErrors: !prevState.didCheckErrors,

View File

@ -35,6 +35,8 @@
"error.validation.minLength": "The value is too short.",
"error.validation.minSupMax": "Can't be superior",
"error.validation.regex": "The value does not match the regex.",
"error.validation.regex.name": "The name should not start with a number or a special character.",
"error.validation.regex.values": "Values should not start with a number or a special character.",
"error.validation.required": "This value input is required.",
"form.attribute.info.no-space-allowed": "No space is allowed for the name of the attribute",
"form.attribute.item.appearance.description": "Otherwise, the value will be editable through a basic textarea field",

View File

@ -34,6 +34,8 @@
"error.validation.minLength": "La valeur est trop courte.",
"error.validation.minSupMax": "Ne peut pas être plus grand",
"error.validation.regex": "La valeur ne correspond pas au format attendu.",
"error.validation.regex.name": "Le nom ne peut pas commencer par un nombre ou un caractère spécial",
"error.validation.regex.values": "Les valeurs ne peuvent pas commencer par un nombre ou un caractère spécial",
"error.validation.required": "Ce champ est obligatoire.",
"form.attribute.info.no-space-allowed": "Les espaces ne sont pas autorisés pour les noms du champ",
"form.attribute.item.appearance.description": "Sinon, il sera editable à partir d'une simple zone de texte",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,13 @@
const request = require('request-promise-native');
const createRequest = (defaults = {}) => {
const client = request.defaults({
return request.defaults({
baseUrl: 'http://localhost:1337',
json: true,
resolveWithFullResponse: true,
simple: false,
...defaults,
});
return async options => {
const params = JSON.parse(JSON.stringify(options));
for (let key in params.formData) {
if (typeof params.formData[key] === 'object') {
params.formData[key] = JSON.stringify(params.formData[key]);
}
}
return client(params);
};
};
const createAuthRequest = token => {

View File

@ -2235,6 +2235,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"
@ -10490,11 +10498,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"