diff --git a/examples/getstarted/api/restaurant/models/Restaurant.settings.json b/examples/getstarted/api/restaurant/models/Restaurant.settings.json index cddbe8bf9a..33c0086e40 100755 --- a/examples/getstarted/api/restaurant/models/Restaurant.settings.json +++ b/examples/getstarted/api/restaurant/models/Restaurant.settings.json @@ -18,7 +18,8 @@ "type": "string" }, "slug": { - "type": "uid" + "type": "uid", + "targetField": "name" }, "price_range": { "enum": ["very_cheap", "cheap", "average", "expensive", "very_expensive"], diff --git a/packages/strapi-plugin-content-manager/config/routes.json b/packages/strapi-plugin-content-manager/config/routes.json index 46ad143bad..6c7aafdf78 100644 --- a/packages/strapi-plugin-content-manager/config/routes.json +++ b/packages/strapi-plugin-content-manager/config/routes.json @@ -48,6 +48,22 @@ "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", "path": "/explorer/:model", diff --git a/packages/strapi-plugin-content-manager/controllers/ContentManager.js b/packages/strapi-plugin-content-manager/controllers/ContentManager.js index f9bc3cf723..d6c3cbe991 100644 --- a/packages/strapi-plugin-content-manager/controllers/ContentManager.js +++ b/packages/strapi-plugin-content-manager/controllers/ContentManager.js @@ -1,27 +1,57 @@ 'use strict'; const _ = require('lodash'); + const parseMultipartBody = require('../utils/parse-multipart'); +const { + validateGenerateUIDInput, + validateCheckUIDAvailabilityInput, + validateUIDField, +} = require('./validation'); 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 */ async find(ctx) { - const contentManagerService = - strapi.plugins['content-manager'].services.contentmanager; + const contentManagerService = strapi.plugins['content-manager'].services.contentmanager; let entities = []; if (_.has(ctx.request.query, '_q')) { - entities = await contentManagerService.search( - ctx.params, - ctx.request.query - ); + entities = await contentManagerService.search(ctx.params, ctx.request.query); } else { - entities = await contentManagerService.fetchAll( - ctx.params, - ctx.request.query - ); + entities = await contentManagerService.fetchAll(ctx.params, ctx.request.query); } ctx.body = entities; @@ -31,8 +61,7 @@ module.exports = { * Returns an entity of a content type by id */ async findOne(ctx) { - const contentManagerService = - strapi.plugins['content-manager'].services.contentmanager; + const contentManagerService = strapi.plugins['content-manager'].services.contentmanager; 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 */ async count(ctx) { - const contentManagerService = - strapi.plugins['content-manager'].services.contentmanager; + const contentManagerService = strapi.plugins['content-manager'].services.contentmanager; let count; if (_.has(ctx.request.query, '_q')) { - count = await contentManagerService.countSearch( - ctx.params, - ctx.request.query - ); + count = await contentManagerService.countSearch(ctx.params, ctx.request.query); } else { count = await contentManagerService.count(ctx.params, ctx.request.query); } @@ -70,36 +95,24 @@ module.exports = { * Creates an entity of a content type */ async create(ctx) { - const contentManagerService = - strapi.plugins['content-manager'].services.contentmanager; + const contentManagerService = strapi.plugins['content-manager'].services.contentmanager; const { model } = ctx.params; - try { - if (ctx.is('multipart')) { - const { data, files } = parseMultipartBody(ctx); - ctx.body = await contentManagerService.create(data, { - files, - model, - }); - } else { - // Create an entry using `queries` system - ctx.body = await contentManagerService.create(ctx.request.body, { - model, - }); - } - - strapi.emit('didCreateFirstContentTypeEntry', ctx.params); - } catch (error) { - strapi.log.error(error); - ctx.badRequest(null, [ - { - messages: [ - { id: error.message, message: error.message, field: error.field }, - ], - }, - ]); + if (ctx.is('multipart')) { + const { data, files } = parseMultipartBody(ctx); + ctx.body = await contentManagerService.create(data, { + files, + model, + }); + } else { + // Create an entry using `queries` system + ctx.body = await contentManagerService.create(ctx.request.body, { + model, + }); } + + strapi.emit('didCreateFirstContentTypeEntry', ctx.params); }, /** @@ -108,31 +121,19 @@ module.exports = { async update(ctx) { const { id, model } = ctx.params; - const contentManagerService = - strapi.plugins['content-manager'].services.contentmanager; + const contentManagerService = strapi.plugins['content-manager'].services.contentmanager; - try { - if (ctx.is('multipart')) { - const { data, files } = parseMultipartBody(ctx); - ctx.body = await contentManagerService.edit({ id }, data, { - files, - model, - }); - } else { - // Return the last one which is the current model. - ctx.body = await contentManagerService.edit({ id }, ctx.request.body, { - model, - }); - } - } catch (error) { - strapi.log.error(error); - ctx.badRequest(null, [ - { - messages: [ - { id: error.message, message: error.message, field: error.field }, - ], - }, - ]); + if (ctx.is('multipart')) { + const { data, files } = parseMultipartBody(ctx); + ctx.body = await contentManagerService.edit({ id }, data, { + files, + model, + }); + } else { + // Return the last one which is the current model. + ctx.body = await contentManagerService.edit({ id }, ctx.request.body, { + model, + }); } }, @@ -140,8 +141,7 @@ module.exports = { * Deletes one entity of a content type matching a query */ async delete(ctx) { - const contentManagerService = - strapi.plugins['content-manager'].services.contentmanager; + const contentManagerService = strapi.plugins['content-manager'].services.contentmanager; ctx.body = await contentManagerService.delete(ctx.params); }, @@ -150,12 +150,8 @@ module.exports = { * Deletes multiple entities of a content type matching a query */ async deleteMany(ctx) { - const contentManagerService = - strapi.plugins['content-manager'].services.contentmanager; + const contentManagerService = strapi.plugins['content-manager'].services.contentmanager; - ctx.body = await contentManagerService.deleteMany( - ctx.params, - ctx.request.query - ); + ctx.body = await contentManagerService.deleteMany(ctx.params, ctx.request.query); }, }; diff --git a/packages/strapi-plugin-content-manager/controllers/validation/index.js b/packages/strapi-plugin-content-manager/controllers/validation/index.js index b683b9edaa..3ec10bbb66 100644 --- a/packages/strapi-plugin-content-manager/controllers/validation/index.js +++ b/packages/strapi-plugin-content-manager/controllers/validation/index.js @@ -1,7 +1,7 @@ 'use strict'; -const yup = require('yup'); -const { formatYupErrors } = require('strapi-utils'); +const _ = require('lodash'); +const { yup, formatYupErrors } = require('strapi-utils'); const createModelConfigurationSchema = require('./model-configuration'); @@ -19,7 +19,62 @@ const validateKind = kind => { .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 = { createModelConfigurationSchema, validateKind, + validateGenerateUIDInput, + validateCheckUIDAvailabilityInput, + validateUIDField, }; diff --git a/packages/strapi-plugin-content-manager/controllers/validation/model-configuration.js b/packages/strapi-plugin-content-manager/controllers/validation/model-configuration.js index 016b6ea2d6..4bcef4cc49 100644 --- a/packages/strapi-plugin-content-manager/controllers/validation/model-configuration.js +++ b/packages/strapi-plugin-content-manager/controllers/validation/model-configuration.js @@ -1,6 +1,6 @@ 'use strict'; -const yup = require('yup'); +const { yup } = require('strapi-utils'); const { isListable, hasRelationAttribute, @@ -26,9 +26,7 @@ module.exports = (model, schema, opts = {}) => .noUnknown(); const createSettingsSchema = (model, schema) => { - const validAttributes = Object.keys(schema.attributes).filter(key => - isListable(schema, key) - ); + const validAttributes = Object.keys(schema.attributes).filter(key => isListable(schema, key)); return yup .object() @@ -98,14 +96,11 @@ const createMetadasSchema = (model, schema) => { const createArrayTest = ({ allowUndefined = false } = {}) => ({ name: 'isArray', message: '${path} is required and must be an array', - test: val => - allowUndefined === true && val === undefined ? true : Array.isArray(val), + test: val => (allowUndefined === true && val === undefined ? true : Array.isArray(val)), }); const createLayoutsSchema = (model, schema, opts = {}) => { - const validAttributes = Object.keys(schema.attributes).filter(key => - isListable(schema, key) - ); + const validAttributes = Object.keys(schema.attributes).filter(key => isListable(schema, key)); const editAttributes = Object.keys(schema.attributes).filter(key => hasEditableAttribute(schema, key) diff --git a/packages/strapi-plugin-content-manager/package.json b/packages/strapi-plugin-content-manager/package.json index 8f8564f151..8de01bbdf9 100644 --- a/packages/strapi-plugin-content-manager/package.json +++ b/packages/strapi-plugin-content-manager/package.json @@ -9,6 +9,7 @@ "required": true }, "dependencies": { + "@sindresorhus/slugify": "0.9.1", "classnames": "^2.2.6", "codemirror": "^5.46.0", "draft-js": "^0.10.5", diff --git a/packages/strapi-plugin-content-manager/services/__tests__/uid.test.js b/packages/strapi-plugin-content-manager/services/__tests__/uid.test.js new file mode 100644 index 0000000000..302cd50cf8 --- /dev/null +++ b/packages/strapi-plugin-content-manager/services/__tests__/uid.test.js @@ -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); + }); + }); +}); diff --git a/packages/strapi-plugin-content-manager/services/uid.js b/packages/strapi-plugin-content-manager/services/uid.js new file mode 100644 index 0000000000..b900cd0ae2 --- /dev/null +++ b/packages/strapi-plugin-content-manager/services/uid.js @@ -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; + }, +}; diff --git a/packages/strapi-plugin-content-manager/test/content-manager/uid.test.e2e.js b/packages/strapi-plugin-content-manager/test/content-manager/uid.test.e2e.js new file mode 100644 index 0000000000..8eaa9f7c06 --- /dev/null +++ b/packages/strapi-plugin-content-manager/test/content-manager/uid.test.e2e.js @@ -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', + }); + }); + }); +}); diff --git a/packages/strapi-plugin-content-manager/test/single-type.test.e2e.js b/packages/strapi-plugin-content-manager/test/single-type.test.e2e.js index 0fabc679ce..9411fe5935 100644 --- a/packages/strapi-plugin-content-manager/test/single-type.test.e2e.js +++ b/packages/strapi-plugin-content-manager/test/single-type.test.e2e.js @@ -25,7 +25,7 @@ describe('Content Manager single types', () => { }); }, 60000); - afterAll(() => modelsUtils.deleteContentType('single-type'), 60000); + afterAll(() => modelsUtils.deleteContentType('single-type-model'), 60000); test('Label is not pluralized', async () => { const res = await rq({ diff --git a/packages/strapi-plugin-content-type-builder/package.json b/packages/strapi-plugin-content-type-builder/package.json index a6af49fcb2..2e99b02939 100644 --- a/packages/strapi-plugin-content-type-builder/package.json +++ b/packages/strapi-plugin-content-type-builder/package.json @@ -8,7 +8,7 @@ "description": "content-type-builder.plugin.description" }, "dependencies": { - "@sindresorhus/slugify": "^0.9.1", + "@sindresorhus/slugify": "0.9.1", "classnames": "^2.2.6", "fs-extra": "^7.0.0", "immutable": "^3.8.2",