CRUD group schemas

This commit is contained in:
Alexandre Bodin 2019-06-24 15:31:22 +02:00
parent e9d2c04824
commit f1a1e82b2c
12 changed files with 408 additions and 32 deletions

View File

@ -0,0 +1,6 @@
{
"name": "CTA Facebook",
"connection": "default",
"collectionName": "cta_facebook_aa",
"attributes": {}
}

View File

@ -63,6 +63,30 @@
"config": { "config": {
"policies": [] "policies": []
} }
},
{
"method": "POST",
"path": "/groups",
"handler": "Groups.createGroup",
"config": {
"policies": []
}
},
{
"method": "PUT",
"path": "/groups/:uid",
"handler": "Groups.updateGroup",
"config": {
"policies": []
}
},
{
"method": "DELETE",
"path": "/groups/:uid",
"handler": "Groups.deleteGroup",
"config": {
"policies": []
}
} }
] ]
} }

View File

@ -1,5 +1,21 @@
'use strict'; 'use strict';
const yup = require('yup');
const formatYupErrors = require('./utils/yup-formatter');
const groupSchema = yup.object().shape({
name: yup.string().required(),
connection: yup.string(),
collectionName: yup.string(),
attributes: yup.object().required(),
});
const internals = {
get service() {
return strapi.plugins['content-type-builder'].services.groups;
},
};
/** /**
* Groups controller * Groups controller
*/ */
@ -11,7 +27,7 @@ module.exports = {
*/ */
async getGroups(ctx) { async getGroups(ctx) {
const data = await strapi.groupManager.all(); const data = await strapi.groupManager.all();
ctx.body = { data }; ctx.send({ data });
}, },
/** /**
@ -25,18 +41,74 @@ module.exports = {
const group = await strapi.groupManager.get(uid); const group = await strapi.groupManager.get(uid);
if (!group) { if (!group) {
ctx.status = 404; return ctx.send({ error: 'group.notFound' }, 404);
ctx.body = {
error: 'group.notFound',
};
} }
ctx.body = { data: group }; ctx.send({ data: group });
}, },
async createGroup() {}, async createGroup(ctx) {
const { body } = ctx.request;
async updateGroup() {}, try {
await groupSchema.validate(body, {
strict: true,
abortEarly: false,
});
} catch (error) {
return ctx.send({ error: formatYupErrors(error) }, 400);
}
async deleteGroup() {}, const uid = internals.service.createGroupUID(body.name);
if (strapi.groupManager.get(uid) !== undefined) {
return ctx.send({ error: 'group.alreadyExists' }, 400);
}
const newGroup = await internals.service.createGroup(uid, body);
ctx.send({ data: newGroup }, 201);
},
/**
* Updates a group and return it
* @param {Object} ctx - enhanced koa context
*/
async updateGroup(ctx) {
const { uid } = ctx.params;
const { body } = ctx.request;
const group = await strapi.groupManager.get(uid);
if (!group) {
return ctx.send({ error: 'group.notFound' }, 404);
}
try {
await groupSchema.validate(body, {
strict: true,
abortEarly: false,
});
} catch (error) {
return ctx.send({ error: formatYupErrors(error) }, 400);
}
const updatedGroup = await internals.service.updateGroup(group, body);
ctx.send({ data: updatedGroup }, 200);
},
async deleteGroup(ctx) {
const { uid } = ctx.params;
const group = await strapi.groupManager.get(uid);
if (!group) {
return ctx.send({ error: 'group.notFound' }, 404);
}
await internals.service.deleteGroup(group);
ctx.send({ data: group }, 200);
},
}; };

View File

@ -0,0 +1,74 @@
const yup = require('yup');
const formatYupErrors = require('../yup-formatter');
describe('Format yup errors', () => {
test('Format single errors', async () => {
expect.hasAssertions();
return yup
.object({
name: yup.string().required('name is required'),
})
.validate({})
.catch(err => {
expect(formatYupErrors(err)).toMatchObject({
name: ['name is required'],
});
});
});
test('Format multiple errors', async () => {
expect.hasAssertions();
return yup
.object({
name: yup
.string()
.min(2, 'min length is 2')
.required(),
})
.validate(
{
name: '1',
},
{
strict: true,
abortEarly: false,
}
)
.catch(err => {
expect(formatYupErrors(err)).toMatchObject({
name: ['min length is 2'],
});
});
});
test('Format multiple errors on multiple keys', async () => {
expect.hasAssertions();
return yup
.object({
name: yup
.string()
.min(2, 'min length is 2')
.typeError('name must be a string')
.required(),
price: yup
.number()
.integer()
.required('price is required'),
})
.validate(
{
name: 12,
},
{
strict: true,
abortEarly: false,
}
)
.catch(err => {
expect(formatYupErrors(err)).toMatchObject({
price: ['price is required'],
name: ['name must be a string'],
});
});
});
});

View File

@ -0,0 +1,19 @@
'use strict';
/**
* Returns a formatted error for http responses
* @param {Object} validationError - a Yup ValidationError
*/
const formatYupErrors = validationError => {
if (validationError.inner.length === 0) {
if (validationError.path === undefined) return validationError.errors;
return { [validationError.path]: validationError.errors };
}
return validationError.inner.reduce((acc, err) => {
acc[err.path] = err.errors;
return acc;
}, {});
};
module.exports = formatYupErrors;

View File

@ -8,9 +8,10 @@
"description": "content-type-builder.plugin.description" "description": "content-type-builder.plugin.description"
}, },
"scripts": { "scripts": {
"test": "echo \"no tests yet\"" "test": "jest --verbose controllers"
}, },
"dependencies": { "dependencies": {
"@sindresorhus/slugify": "^0.9.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"fs-extra": "^7.0.0", "fs-extra": "^7.0.0",
"immutable": "^3.8.2", "immutable": "^3.8.2",

View File

@ -1,3 +1,95 @@
'use strict'; 'use strict';
module.exports = {}; const { pick } = require('lodash');
const slugify = require('@sindresorhus/slugify');
const VALID_FIELDS = ['name', 'connection', 'collectionName', 'attributes'];
const createSchema = infos => {
const { name, connection = 'default', collectionName } = infos;
const uid = createGroupUID(name);
return {
name,
connection,
collectionName: collectionName || uid,
attributes: {},
};
};
const updateSchema = (oldSchema, newSchema) =>
pick({ ...oldSchema, ...newSchema }, VALID_FIELDS);
const createGroupUID = str => slugify(str, { separator: '_' });
/**
* Creates a group schema file
* @param {*} uid
* @param {*} infos
*/
async function createGroup(uid, infos) {
const schema = createSchema(infos);
return writeSchema(uid, schema);
}
/**
* Updates a group schema file
* @param {*} group
* @param {*} infos
*/
async function updateGroup(group, infos) {
const { uid } = group;
const updatedSchema = updateSchema(group.schema, infos);
if (infos.name !== group.schema.name) {
await deleteSchema(uid);
const newUid = createGroupUID(infos.name);
return writeSchema(newUid, updatedSchema);
}
return writeSchema(uid, updatedSchema);
}
async function deleteGroup(group) {
await deleteSchema(group.uid);
process.nextTick(() => strapi.reload());
}
/**
* Writes a group schema file
*/
async function writeSchema(uid, schema) {
strapi.reload.isWatching = false;
await strapi.fs.writeAppFile(
`groups/${uid}.json`,
JSON.stringify(schema, null, 2)
);
strapi.reload.isWatching = true;
process.nextTick(() => strapi.reload());
return {
uid,
schema,
};
}
/**
* Deletes a group schema file
* @param {string} ui
*/
async function deleteSchema(uid) {
strapi.reload.isWatching = false;
await strapi.fs.removeAppFile(`groups/${uid}.json`);
strapi.reload.isWatching = true;
}
module.exports = {
createGroup,
createGroupUID,
updateGroup,
deleteGroup,
};

View File

@ -23,13 +23,44 @@ describe.only('Content Type Builder - Groups', () => {
res.body.data.forEach(el => { res.body.data.forEach(el => {
expect(el).toMatchObject({ expect(el).toMatchObject({
id: expect.any(String), uid: expect.any(String),
name: expect.any(String), name: expect.any(String),
icon: expect.any(String),
// later
schema: expect.objectContaining({}), schema: expect.objectContaining({}),
}); });
}); });
}); });
}); });
describe('GET /group/:uid', () => {
test('Returns 404 on not found', async () => {
const res = await rq({
method: 'GET',
url: '/content-type-build/groups/nonexistent-group',
});
expect(res.statusCode).toBe(404);
});
test('Returns correct format', async () => {
const res = await rq({
method: 'GET',
url: '/content-type-build/groups/existing-group',
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
data: {
uid: 'existing-group',
name: 'EXISTING-GROUP',
schema: {
connection: 'default',
collectionName: 'existing_groups',
attributes: {
//...
},
},
},
});
});
});
}); });

View File

@ -1,12 +1,20 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const fs = require('fs-extra'); const fse = require('fs-extra');
/** /**
* create strapi fs layer * create strapi fs layer
*/ */
module.exports = strapi => { module.exports = strapi => {
function normalizePath(optPath) {
const filePath = Array.isArray(optPath) ? optPath.join('/') : optPath;
const normalizedPath = path.normalize(filePath).replace(/^(\/?\.\.?)+/, '');
return path.join(strapi.dir, normalizedPath);
}
const strapiFS = { const strapiFS = {
/** /**
* Writes a file in a strapi app * Writes a file in a strapi app
@ -14,15 +22,10 @@ module.exports = strapi => {
* @param {string} data - content * @param {string} data - content
*/ */
writeAppFile(optPath, data) { writeAppFile(optPath, data) {
const filePath = Array.isArray(optPath) ? optPath.join('/') : optPath; const writePath = normalizePath(optPath);
return fse
const normalizedPath = path .ensureFile(writePath)
.normalize(filePath) .then(() => fse.writeFile(writePath, data));
.replace(/^(\/?\.\.?)+/, '');
const writePath = path.join(strapi.dir, normalizedPath);
return fs.ensureFile(writePath).then(() => fs.writeFile(writePath, data));
}, },
/** /**
@ -35,6 +38,14 @@ module.exports = strapi => {
const newPath = ['extensions', plugin].concat(optPath).join('/'); const newPath = ['extensions', plugin].concat(optPath).join('/');
return strapiFS.writeAppFile(newPath, data); return strapiFS.writeAppFile(newPath, data);
}, },
/**
* Removes a file in strapi app
*/
removeAppFile(optPath) {
const removePath = normalizePath(optPath);
return fse.remove(removePath);
},
}; };
return strapiFS; return strapiFS;

View File

@ -9,6 +9,39 @@ const _ = require('lodash');
const Boom = require('boom'); const Boom = require('boom');
const delegate = require('delegates'); const delegate = require('delegates');
const boomMethods = [
'badRequest',
'unauthorized',
'paymentRequired',
'forbidden',
'notFound',
'methodNotAllowed',
'notAcceptable',
'proxyAuthRequired',
'clientTimeout',
'conflict',
'resourceGone',
'lengthRequired',
'preconditionFailed',
'entityTooLarge',
'uriTooLong',
'unsupportedMediaType',
'rangeNotSatisfiable',
'expectationFailed',
'teapot',
'badData',
'locked',
'failedDependency',
'preconditionRequired',
'tooManyRequests',
'illegal',
'badImplementation',
'notImplemented',
'badGateway',
'serverUnavailable',
'gatewayTimeout',
];
module.exports = strapi => { module.exports = strapi => {
return { return {
/** /**
@ -69,19 +102,19 @@ module.exports = strapi => {
// Custom function to avoid ctx.body repeat // Custom function to avoid ctx.body repeat
createResponses() { createResponses() {
Object.keys(Boom).forEach(key => { boomMethods.forEach(method => {
strapi.app.response[key] = function(...rest) { strapi.app.response[method] = function(...rest) {
const error = Boom[key](...rest) || {}; const error = Boom[method](...rest) || {};
this.status = error.isBoom ? error.output.statusCode : this.status; this.status = error.isBoom ? error.output.statusCode : this.status;
this.body = error; this.body = error;
}; };
this.delegator.method(key); this.delegator.method(method);
}); });
strapi.app.response.send = function(data) { strapi.app.response.send = function(data, status = 200) {
this.status = 200; this.status = status;
this.body = data; this.body = data;
}; };

View File

@ -3,6 +3,7 @@ const initMap = groups => {
Object.keys(groups).forEach(key => { Object.keys(groups).forEach(key => {
const { const {
name,
connection, connection,
collectionName, collectionName,
description, description,
@ -11,8 +12,7 @@ const initMap = groups => {
map.set(key, { map.set(key, {
uid: key, uid: key,
name: key.toUpperCase(), // get the display name som schema: { name, connection, collectionName, description, attributes },
schema: { connection, collectionName, description, attributes },
}); });
}); });

View File

@ -2008,6 +2008,14 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@sindresorhus/slugify@^0.9.1":
version "0.9.1"
resolved "https://registry.yarnpkg.com/@sindresorhus/slugify/-/slugify-0.9.1.tgz#892ad24d70b442c0a14fe519cb4019d59bc5069f"
integrity sha512-b6heYM9dzZD13t2GOiEQTDE0qX+I1GyOotMwKh9VQqzuNiVdPVT8dM43fe9HNb/3ul+Qwd5oKSEDrDIfhq3bnQ==
dependencies:
escape-string-regexp "^1.0.5"
lodash.deburr "^4.1.0"
"@snyk/composer-lockfile-parser@1.0.2": "@snyk/composer-lockfile-parser@1.0.2":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@snyk/composer-lockfile-parser/-/composer-lockfile-parser-1.0.2.tgz#d748e56076bc1c25b130c1f13ed705fa285a1994" resolved "https://registry.yarnpkg.com/@snyk/composer-lockfile-parser/-/composer-lockfile-parser-1.0.2.tgz#d748e56076bc1c25b130c1f13ed705fa285a1994"
@ -10873,6 +10881,11 @@ lodash.clonedeep@^4.3.0, lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.deburr@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
integrity sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s=
lodash.defaults@^4.2.0: lodash.defaults@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"