mirror of
https://github.com/strapi/strapi.git
synced 2025-06-27 00:41:25 +00:00
Add UID validation routes for CTM livechecks
Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
parent
a6708c3e46
commit
db03d9d07d
@ -18,7 +18,8 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"slug": {
|
"slug": {
|
||||||
"type": "uid"
|
"type": "uid",
|
||||||
|
"targetField": "name"
|
||||||
},
|
},
|
||||||
"price_range": {
|
"price_range": {
|
||||||
"enum": ["very_cheap", "cheap", "average", "expensive", "very_expensive"],
|
"enum": ["very_cheap", "cheap", "average", "expensive", "very_expensive"],
|
||||||
|
@ -48,6 +48,22 @@
|
|||||||
"policies": []
|
"policies": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/explorer/uid/generate",
|
||||||
|
"handler": "ContentManager.generateUID",
|
||||||
|
"config": {
|
||||||
|
"policies": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/explorer/uid/check-availability",
|
||||||
|
"handler": "ContentManager.checkUIDAvailability",
|
||||||
|
"config": {
|
||||||
|
"policies": []
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/explorer/:model",
|
"path": "/explorer/:model",
|
||||||
|
@ -1,27 +1,57 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const parseMultipartBody = require('../utils/parse-multipart');
|
const parseMultipartBody = require('../utils/parse-multipart');
|
||||||
|
const {
|
||||||
|
validateGenerateUIDInput,
|
||||||
|
validateCheckUIDAvailabilityInput,
|
||||||
|
validateUIDField,
|
||||||
|
} = require('./validation');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
async generateUID(ctx) {
|
||||||
|
const { contentTypeUID, field, data } = await validateGenerateUIDInput(ctx.request.body);
|
||||||
|
|
||||||
|
await validateUIDField(contentTypeUID, field);
|
||||||
|
|
||||||
|
const uidService = strapi.plugins['content-manager'].services.uid;
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: await uidService.generateUIDField({ contentTypeUID, field, data }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkUIDAvailability(ctx) {
|
||||||
|
const { contentTypeUID, field, value } = await validateCheckUIDAvailabilityInput(
|
||||||
|
ctx.request.body
|
||||||
|
);
|
||||||
|
|
||||||
|
await validateUIDField(contentTypeUID, field);
|
||||||
|
|
||||||
|
const uidService = strapi.plugins['content-manager'].services.uid;
|
||||||
|
|
||||||
|
const isAvailable = await uidService.checkUIDAvailability({ contentTypeUID, field, value });
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
isAvailable,
|
||||||
|
suggestion: !isAvailable
|
||||||
|
? await uidService.findUniqueUID({ contentTypeUID, field, value })
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of entities of a content-type matching the query parameters
|
* Returns a list of entities of a content-type matching the query parameters
|
||||||
*/
|
*/
|
||||||
async find(ctx) {
|
async find(ctx) {
|
||||||
const contentManagerService =
|
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||||
strapi.plugins['content-manager'].services.contentmanager;
|
|
||||||
|
|
||||||
let entities = [];
|
let entities = [];
|
||||||
if (_.has(ctx.request.query, '_q')) {
|
if (_.has(ctx.request.query, '_q')) {
|
||||||
entities = await contentManagerService.search(
|
entities = await contentManagerService.search(ctx.params, ctx.request.query);
|
||||||
ctx.params,
|
|
||||||
ctx.request.query
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
entities = await contentManagerService.fetchAll(
|
entities = await contentManagerService.fetchAll(ctx.params, ctx.request.query);
|
||||||
ctx.params,
|
|
||||||
ctx.request.query
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = entities;
|
ctx.body = entities;
|
||||||
@ -31,8 +61,7 @@ module.exports = {
|
|||||||
* Returns an entity of a content type by id
|
* Returns an entity of a content type by id
|
||||||
*/
|
*/
|
||||||
async findOne(ctx) {
|
async findOne(ctx) {
|
||||||
const contentManagerService =
|
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||||
strapi.plugins['content-manager'].services.contentmanager;
|
|
||||||
|
|
||||||
const entry = await contentManagerService.fetch(ctx.params);
|
const entry = await contentManagerService.fetch(ctx.params);
|
||||||
|
|
||||||
@ -48,15 +77,11 @@ module.exports = {
|
|||||||
* Returns a count of entities of a content type matching query parameters
|
* Returns a count of entities of a content type matching query parameters
|
||||||
*/
|
*/
|
||||||
async count(ctx) {
|
async count(ctx) {
|
||||||
const contentManagerService =
|
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||||
strapi.plugins['content-manager'].services.contentmanager;
|
|
||||||
|
|
||||||
let count;
|
let count;
|
||||||
if (_.has(ctx.request.query, '_q')) {
|
if (_.has(ctx.request.query, '_q')) {
|
||||||
count = await contentManagerService.countSearch(
|
count = await contentManagerService.countSearch(ctx.params, ctx.request.query);
|
||||||
ctx.params,
|
|
||||||
ctx.request.query
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
count = await contentManagerService.count(ctx.params, ctx.request.query);
|
count = await contentManagerService.count(ctx.params, ctx.request.query);
|
||||||
}
|
}
|
||||||
@ -70,12 +95,10 @@ module.exports = {
|
|||||||
* Creates an entity of a content type
|
* Creates an entity of a content type
|
||||||
*/
|
*/
|
||||||
async create(ctx) {
|
async create(ctx) {
|
||||||
const contentManagerService =
|
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||||
strapi.plugins['content-manager'].services.contentmanager;
|
|
||||||
|
|
||||||
const { model } = ctx.params;
|
const { model } = ctx.params;
|
||||||
|
|
||||||
try {
|
|
||||||
if (ctx.is('multipart')) {
|
if (ctx.is('multipart')) {
|
||||||
const { data, files } = parseMultipartBody(ctx);
|
const { data, files } = parseMultipartBody(ctx);
|
||||||
ctx.body = await contentManagerService.create(data, {
|
ctx.body = await contentManagerService.create(data, {
|
||||||
@ -90,16 +113,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
strapi.emit('didCreateFirstContentTypeEntry', ctx.params);
|
strapi.emit('didCreateFirstContentTypeEntry', ctx.params);
|
||||||
} catch (error) {
|
|
||||||
strapi.log.error(error);
|
|
||||||
ctx.badRequest(null, [
|
|
||||||
{
|
|
||||||
messages: [
|
|
||||||
{ id: error.message, message: error.message, field: error.field },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,10 +121,8 @@ module.exports = {
|
|||||||
async update(ctx) {
|
async update(ctx) {
|
||||||
const { id, model } = ctx.params;
|
const { id, model } = ctx.params;
|
||||||
|
|
||||||
const contentManagerService =
|
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||||
strapi.plugins['content-manager'].services.contentmanager;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (ctx.is('multipart')) {
|
if (ctx.is('multipart')) {
|
||||||
const { data, files } = parseMultipartBody(ctx);
|
const { data, files } = parseMultipartBody(ctx);
|
||||||
ctx.body = await contentManagerService.edit({ id }, data, {
|
ctx.body = await contentManagerService.edit({ id }, data, {
|
||||||
@ -124,24 +135,13 @@ module.exports = {
|
|||||||
model,
|
model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
strapi.log.error(error);
|
|
||||||
ctx.badRequest(null, [
|
|
||||||
{
|
|
||||||
messages: [
|
|
||||||
{ id: error.message, message: error.message, field: error.field },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes one entity of a content type matching a query
|
* Deletes one entity of a content type matching a query
|
||||||
*/
|
*/
|
||||||
async delete(ctx) {
|
async delete(ctx) {
|
||||||
const contentManagerService =
|
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||||
strapi.plugins['content-manager'].services.contentmanager;
|
|
||||||
|
|
||||||
ctx.body = await contentManagerService.delete(ctx.params);
|
ctx.body = await contentManagerService.delete(ctx.params);
|
||||||
},
|
},
|
||||||
@ -150,12 +150,8 @@ module.exports = {
|
|||||||
* Deletes multiple entities of a content type matching a query
|
* Deletes multiple entities of a content type matching a query
|
||||||
*/
|
*/
|
||||||
async deleteMany(ctx) {
|
async deleteMany(ctx) {
|
||||||
const contentManagerService =
|
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||||
strapi.plugins['content-manager'].services.contentmanager;
|
|
||||||
|
|
||||||
ctx.body = await contentManagerService.deleteMany(
|
ctx.body = await contentManagerService.deleteMany(ctx.params, ctx.request.query);
|
||||||
ctx.params,
|
|
||||||
ctx.request.query
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const yup = require('yup');
|
const _ = require('lodash');
|
||||||
const { formatYupErrors } = require('strapi-utils');
|
const { yup, formatYupErrors } = require('strapi-utils');
|
||||||
|
|
||||||
const createModelConfigurationSchema = require('./model-configuration');
|
const createModelConfigurationSchema = require('./model-configuration');
|
||||||
|
|
||||||
@ -19,7 +19,62 @@ const validateKind = kind => {
|
|||||||
.catch(error => Promise.reject(formatYupErrors(error)));
|
.catch(error => Promise.reject(formatYupErrors(error)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateGenerateUIDInput = data => {
|
||||||
|
return yup
|
||||||
|
.object({
|
||||||
|
contentTypeUID: yup.string().required(),
|
||||||
|
field: yup.string().required(),
|
||||||
|
data: yup.object().required(),
|
||||||
|
})
|
||||||
|
.validate(data, {
|
||||||
|
strict: true,
|
||||||
|
abortEarly: false,
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
throw strapi.errors.badRequest('ValidationError', formatYupErrors(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateCheckUIDAvailabilityInput = data => {
|
||||||
|
return yup
|
||||||
|
.object({
|
||||||
|
contentTypeUID: yup.string().required(),
|
||||||
|
field: yup.string().required(),
|
||||||
|
value: yup
|
||||||
|
.string()
|
||||||
|
.matches(new RegExp('^[A-Za-z0-9-_.~]*$'))
|
||||||
|
.required(),
|
||||||
|
})
|
||||||
|
.validate(data, {
|
||||||
|
strict: true,
|
||||||
|
abortEarly: false,
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
throw strapi.errors.badRequest('ValidationError', formatYupErrors(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateUIDField = (contentTypeUID, field) => {
|
||||||
|
const model = strapi.contentTypes[contentTypeUID];
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
throw strapi.errors.badRequest('ValidationError', ['ContentType not found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!_.has(model, ['attributes', field]) ||
|
||||||
|
_.get(model, ['attributes', field, 'type']) !== 'uid'
|
||||||
|
) {
|
||||||
|
throw strapi.errors.badRequest('ValidationError', {
|
||||||
|
field: ['field must be a valid `uid` attribute'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createModelConfigurationSchema,
|
createModelConfigurationSchema,
|
||||||
validateKind,
|
validateKind,
|
||||||
|
validateGenerateUIDInput,
|
||||||
|
validateCheckUIDAvailabilityInput,
|
||||||
|
validateUIDField,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const yup = require('yup');
|
const { yup } = require('strapi-utils');
|
||||||
const {
|
const {
|
||||||
isListable,
|
isListable,
|
||||||
hasRelationAttribute,
|
hasRelationAttribute,
|
||||||
@ -26,9 +26,7 @@ module.exports = (model, schema, opts = {}) =>
|
|||||||
.noUnknown();
|
.noUnknown();
|
||||||
|
|
||||||
const createSettingsSchema = (model, schema) => {
|
const createSettingsSchema = (model, schema) => {
|
||||||
const validAttributes = Object.keys(schema.attributes).filter(key =>
|
const validAttributes = Object.keys(schema.attributes).filter(key => isListable(schema, key));
|
||||||
isListable(schema, key)
|
|
||||||
);
|
|
||||||
|
|
||||||
return yup
|
return yup
|
||||||
.object()
|
.object()
|
||||||
@ -98,14 +96,11 @@ const createMetadasSchema = (model, schema) => {
|
|||||||
const createArrayTest = ({ allowUndefined = false } = {}) => ({
|
const createArrayTest = ({ allowUndefined = false } = {}) => ({
|
||||||
name: 'isArray',
|
name: 'isArray',
|
||||||
message: '${path} is required and must be an array',
|
message: '${path} is required and must be an array',
|
||||||
test: val =>
|
test: val => (allowUndefined === true && val === undefined ? true : Array.isArray(val)),
|
||||||
allowUndefined === true && val === undefined ? true : Array.isArray(val),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const createLayoutsSchema = (model, schema, opts = {}) => {
|
const createLayoutsSchema = (model, schema, opts = {}) => {
|
||||||
const validAttributes = Object.keys(schema.attributes).filter(key =>
|
const validAttributes = Object.keys(schema.attributes).filter(key => isListable(schema, key));
|
||||||
isListable(schema, key)
|
|
||||||
);
|
|
||||||
|
|
||||||
const editAttributes = Object.keys(schema.attributes).filter(key =>
|
const editAttributes = Object.keys(schema.attributes).filter(key =>
|
||||||
hasEditableAttribute(schema, key)
|
hasEditableAttribute(schema, key)
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sindresorhus/slugify": "0.9.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"codemirror": "^5.46.0",
|
"codemirror": "^5.46.0",
|
||||||
"draft-js": "^0.10.5",
|
"draft-js": "^0.10.5",
|
||||||
|
@ -0,0 +1,238 @@
|
|||||||
|
const uidService = require('../uid');
|
||||||
|
|
||||||
|
describe('Test uid service', () => {
|
||||||
|
describe('generateUIDField', () => {
|
||||||
|
test('Uses modelName if no targetField specified or set', async () => {
|
||||||
|
global.strapi = {
|
||||||
|
contentTypes: {
|
||||||
|
'my-model': {
|
||||||
|
modelName: 'myTestModel',
|
||||||
|
attributes: {
|
||||||
|
slug: {
|
||||||
|
type: 'uid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
find: async () => [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uid = await uidService.generateUIDField({
|
||||||
|
contentTypeUID: 'my-model',
|
||||||
|
field: 'slug',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(uid).toBe('my-test-model');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Calls findUniqueUID', async () => {
|
||||||
|
const tmpFn = uidService.findUniqueUID;
|
||||||
|
uidService.findUniqueUID = jest.fn(v => v);
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
contentTypes: {
|
||||||
|
'my-model': {
|
||||||
|
modelName: 'myTestModel',
|
||||||
|
attributes: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
slug: {
|
||||||
|
type: 'uid',
|
||||||
|
targetField: 'title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
find: async () => [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await uidService.generateUIDField({
|
||||||
|
contentTypeUID: 'my-model',
|
||||||
|
field: 'slug',
|
||||||
|
data: {
|
||||||
|
title: 'Test title',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await uidService.generateUIDField({
|
||||||
|
contentTypeUID: 'my-model',
|
||||||
|
field: 'slug',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(uidService.findUniqueUID).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
uidService.findUniqueUID = tmpFn;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Uses targetField value for generation', async () => {
|
||||||
|
const find = jest.fn(async () => {
|
||||||
|
return [{ slug: 'test-title' }];
|
||||||
|
});
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
contentTypes: {
|
||||||
|
'my-model': {
|
||||||
|
modelName: 'myTestModel',
|
||||||
|
attributes: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
slug: {
|
||||||
|
type: 'uid',
|
||||||
|
targetField: 'title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
find,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uid = await uidService.generateUIDField({
|
||||||
|
contentTypeUID: 'my-model',
|
||||||
|
field: 'slug',
|
||||||
|
data: {
|
||||||
|
title: 'Test title',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(uid).toBe('test-title-1');
|
||||||
|
|
||||||
|
// change find response
|
||||||
|
global.strapi.db.query = () => ({ find: jest.fn(async () => []) });
|
||||||
|
|
||||||
|
const uidWithEmptyTarget = await uidService.generateUIDField({
|
||||||
|
contentTypeUID: 'my-model',
|
||||||
|
field: 'slug',
|
||||||
|
data: {
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(uidWithEmptyTarget).toBe('my-test-model');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findUniqueUID', () => {
|
||||||
|
test('Finds closest match', async () => {
|
||||||
|
const find = jest.fn(async () => {
|
||||||
|
return [
|
||||||
|
{ slug: 'my-test-model' },
|
||||||
|
{ slug: 'my-test-model-1' },
|
||||||
|
// it finds the quickest match possible
|
||||||
|
{ slug: 'my-test-model-4' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
contentTypes: {
|
||||||
|
'my-model': {
|
||||||
|
modelName: 'myTestModel',
|
||||||
|
attributes: {
|
||||||
|
slug: {
|
||||||
|
type: 'uid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
find,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uid = await uidService.findUniqueUID({
|
||||||
|
contentTypeUID: 'my-model',
|
||||||
|
field: 'slug',
|
||||||
|
value: 'my-test-model',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(uid).toBe('my-test-model-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Calls db find', async () => {
|
||||||
|
const find = jest.fn(async () => {
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
contentTypes: {
|
||||||
|
'my-model': {
|
||||||
|
modelName: 'myTestModel',
|
||||||
|
attributes: {
|
||||||
|
slug: {
|
||||||
|
type: 'uid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
find,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await uidService.findUniqueUID({
|
||||||
|
contentTypeUID: 'my-model',
|
||||||
|
field: 'slug',
|
||||||
|
value: 'my-test-model',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(find).toHaveBeenCalledWith({
|
||||||
|
slug_contains: 'my-test-model',
|
||||||
|
_limit: -1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CheckUIDAvailability', () => {
|
||||||
|
test('Counts the data in db', async () => {
|
||||||
|
const count = jest.fn(async () => 0);
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
db: {
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAvailable = await uidService.checkUIDAvailability({
|
||||||
|
contentTypeUID: 'my-model',
|
||||||
|
field: 'slug',
|
||||||
|
value: 'my-test-model',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(count).toHaveBeenCalledWith({ slug: 'my-test-model' });
|
||||||
|
expect(isAvailable).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
63
packages/strapi-plugin-content-manager/services/uid.js
Normal file
63
packages/strapi-plugin-content-manager/services/uid.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
const slugify = require('@sindresorhus/slugify');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async generateUIDField({ contentTypeUID, field, data }) {
|
||||||
|
const contentType = strapi.contentTypes[contentTypeUID];
|
||||||
|
const { attributes } = contentType;
|
||||||
|
|
||||||
|
const targetField = _.get(attributes, [field, 'targetField']);
|
||||||
|
const targetValue = _.get(data, targetField);
|
||||||
|
|
||||||
|
if (!_.isEmpty(targetValue)) {
|
||||||
|
return this.findUniqueUID({
|
||||||
|
contentTypeUID,
|
||||||
|
field,
|
||||||
|
value: slugify(targetValue),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.findUniqueUID({
|
||||||
|
contentTypeUID,
|
||||||
|
field,
|
||||||
|
value: slugify(contentType.modelName),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async findUniqueUID({ contentTypeUID, field, value }) {
|
||||||
|
const query = strapi.db.query(contentTypeUID);
|
||||||
|
|
||||||
|
const possibleColisions = await query
|
||||||
|
.find({
|
||||||
|
[`${field}_contains`]: value,
|
||||||
|
_limit: -1,
|
||||||
|
})
|
||||||
|
.then(results => results.map(result => result[field]));
|
||||||
|
|
||||||
|
if (possibleColisions.length === 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 1;
|
||||||
|
let tmpUId = `${value}-${i}`;
|
||||||
|
while (possibleColisions.includes(tmpUId)) {
|
||||||
|
i += 1;
|
||||||
|
tmpUId = `${value}-${i}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpUId;
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkUIDAvailability({ contentTypeUID, field, value }) {
|
||||||
|
const query = strapi.db.query(contentTypeUID);
|
||||||
|
|
||||||
|
const count = await query.count({
|
||||||
|
[field]: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count > 0) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,367 @@
|
|||||||
|
// Helpers.
|
||||||
|
const { registerAndLogin } = require('../../../../test/helpers/auth');
|
||||||
|
const createModelsUtils = require('../../../../test/helpers/models');
|
||||||
|
const { createAuthRequest } = require('../../../../test/helpers/request');
|
||||||
|
|
||||||
|
let modelsUtils;
|
||||||
|
let rq;
|
||||||
|
let uid = 'application::uid-model.uid-model';
|
||||||
|
|
||||||
|
describe('Content Manager single types', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const token = await registerAndLogin();
|
||||||
|
rq = createAuthRequest(token);
|
||||||
|
|
||||||
|
modelsUtils = createModelsUtils({ rq });
|
||||||
|
|
||||||
|
await modelsUtils.createContentType({
|
||||||
|
kind: 'collectionType',
|
||||||
|
name: 'uid-model',
|
||||||
|
attributes: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
slug: {
|
||||||
|
type: 'uid',
|
||||||
|
targetField: 'title',
|
||||||
|
},
|
||||||
|
otherField: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
afterAll(() => modelsUtils.deleteContentType('uid-model'), 60000);
|
||||||
|
|
||||||
|
describe('Generate UID', () => {
|
||||||
|
test('Throws if input is not provided', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'ValidationError',
|
||||||
|
data: {
|
||||||
|
contentTypeUID: expect.arrayContaining([expect.stringMatching('required field')]),
|
||||||
|
field: expect.arrayContaining([expect.stringMatching('required field')]),
|
||||||
|
data: expect.arrayContaining([expect.stringMatching('required field')]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Throws when contentType is not found', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: 'non-existent',
|
||||||
|
field: 'slug',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'ValidationError',
|
||||||
|
data: ['ContentType not found'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Throws when field is not a uid field', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'otherField',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'ValidationError',
|
||||||
|
data: {
|
||||||
|
field: [expect.stringMatching('must be a valid `uid` attribute')],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Generates a unique field when not targetField', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body.data).toBe('uid-model');
|
||||||
|
|
||||||
|
await rq({
|
||||||
|
url: `/content-manager/explorer/${uid}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
slug: res.body.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondRes = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(secondRes.statusCode).toBe(200);
|
||||||
|
expect(secondRes.body.data).toBe('uid-model-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Generates a unique field based on targetField', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
data: {
|
||||||
|
title: 'This is a super title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body.data).toBe('this-is-a-super-title');
|
||||||
|
|
||||||
|
await rq({
|
||||||
|
url: `/content-manager/explorer/${uid}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
slug: res.body.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondRes = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
data: {
|
||||||
|
title: 'This is a super title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(secondRes.statusCode).toBe(200);
|
||||||
|
expect(secondRes.body.data).toBe('this-is-a-super-title-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Avoids colisions with already generated uids', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
data: {
|
||||||
|
title: 'My title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body.data).toBe('my-title');
|
||||||
|
|
||||||
|
await rq({
|
||||||
|
url: `/content-manager/explorer/${uid}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
slug: res.body.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondRes = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
data: {
|
||||||
|
title: 'My title',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(secondRes.statusCode).toBe(200);
|
||||||
|
expect(secondRes.body.data).toBe('my-title-1');
|
||||||
|
|
||||||
|
await rq({
|
||||||
|
url: `/content-manager/explorer/${uid}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
slug: secondRes.body.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const thridRes = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/generate`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
data: {
|
||||||
|
title: 'My title 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(thridRes.statusCode).toBe(200);
|
||||||
|
expect(thridRes.body.data).toBe('my-title-1-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check UID availability', () => {
|
||||||
|
test('Throws if input is not provided', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/check-availability`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'ValidationError',
|
||||||
|
data: {
|
||||||
|
contentTypeUID: expect.arrayContaining([expect.stringMatching('required field')]),
|
||||||
|
field: expect.arrayContaining([expect.stringMatching('required field')]),
|
||||||
|
value: expect.arrayContaining([expect.stringMatching('required field')]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Throws on invalid uid value', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/check-availability`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
value: 'Invalid UID valuéééé',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
data: {
|
||||||
|
value: expect.arrayContaining([expect.stringMatching('must match')]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Throws when contentType is not found', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/check-availability`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: 'non-existent',
|
||||||
|
field: 'slug',
|
||||||
|
value: 'some-slug',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'ValidationError',
|
||||||
|
data: ['ContentType not found'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Throws when field is not a uid field', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/check-availability`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'otherField',
|
||||||
|
value: 'some-slug',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'ValidationError',
|
||||||
|
data: {
|
||||||
|
field: [expect.stringMatching('must be a valid `uid` attribute')],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Checks availability', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/check-availability`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
value: 'some-available-slug',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
isAvailable: true,
|
||||||
|
suggestion: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Gives a suggestion when not available', async () => {
|
||||||
|
// create data
|
||||||
|
await rq({
|
||||||
|
url: `/content-manager/explorer/${uid}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
slug: 'custom-slug',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await rq({
|
||||||
|
url: `/content-manager/explorer/uid/check-availability`,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
contentTypeUID: uid,
|
||||||
|
field: 'slug',
|
||||||
|
value: 'custom-slug',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
isAvailable: false,
|
||||||
|
suggestion: 'custom-slug-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -25,7 +25,7 @@ describe('Content Manager single types', () => {
|
|||||||
});
|
});
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
afterAll(() => modelsUtils.deleteContentType('single-type'), 60000);
|
afterAll(() => modelsUtils.deleteContentType('single-type-model'), 60000);
|
||||||
|
|
||||||
test('Label is not pluralized', async () => {
|
test('Label is not pluralized', async () => {
|
||||||
const res = await rq({
|
const res = await rq({
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"description": "content-type-builder.plugin.description"
|
"description": "content-type-builder.plugin.description"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sindresorhus/slugify": "^0.9.1",
|
"@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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user