mirror of
https://github.com/strapi/strapi.git
synced 2025-10-29 17:04:13 +00:00
Single type routes
Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
parent
7509a16152
commit
2bbd47ba2c
@ -86,12 +86,83 @@
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/single-types/:model",
|
||||
"handler": "single-types.find",
|
||||
"config": {
|
||||
"policies": [
|
||||
"routing",
|
||||
"admin::isAuthenticatedAdmin",
|
||||
["plugins::content-manager.hasPermissions", ["plugins::content-manager.explorer.read"]]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/single-types/:model",
|
||||
"handler": "single-types.createOrUpdate",
|
||||
"config": {
|
||||
"policies": [
|
||||
"routing",
|
||||
"admin::isAuthenticatedAdmin",
|
||||
[
|
||||
"plugins::content-manager.hasPermissions",
|
||||
[
|
||||
"plugins::content-manager.explorer.create",
|
||||
"plugins::content-manager.explorer.update"
|
||||
],
|
||||
{ "hasAtLeastOne": true }
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/single-types/:model",
|
||||
"handler": "single-types.delete",
|
||||
"config": {
|
||||
"policies": [
|
||||
"routing",
|
||||
"admin::isAuthenticatedAdmin",
|
||||
["plugins::content-manager.hasPermissions", ["plugins::content-manager.explorer.delete"]]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/single-types/:model/actions/publish",
|
||||
"handler": "single-types.publish",
|
||||
"config": {
|
||||
"policies": [
|
||||
"routing",
|
||||
"plugins::content-manager.has-draft-and-publish",
|
||||
"admin::isAuthenticatedAdmin",
|
||||
["plugins::content-manager.hasPermissions", ["plugins::content-manager.explorer.publish"]]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/single-types/:model/actions/unpublish",
|
||||
"handler": "single-types.unpublish",
|
||||
"config": {
|
||||
"policies": [
|
||||
"routing",
|
||||
"plugins::content-manager.has-draft-and-publish",
|
||||
"admin::isAuthenticatedAdmin",
|
||||
["plugins::content-manager.hasPermissions", ["plugins::content-manager.explorer.publish"]]
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/explorer/:model",
|
||||
"handler": "ContentManager.find",
|
||||
"config": {
|
||||
"policies": ["routing", "admin::isAuthenticatedAdmin"]
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const createContext = require('../../../../test/helpers/create-context');
|
||||
const singleTypes = require('../single-types');
|
||||
const { ACTIONS } = require('../constants');
|
||||
|
||||
describe('Single Types', () => {
|
||||
test('find', async () => {
|
||||
const state = {
|
||||
userAbility: {
|
||||
can: jest.fn(),
|
||||
cannot: jest.fn(() => false),
|
||||
},
|
||||
};
|
||||
|
||||
const notFound = jest.fn();
|
||||
const createPermissionsManager = jest.fn(() => ({
|
||||
ability: state.userAbility,
|
||||
}));
|
||||
|
||||
global.strapi = {
|
||||
admin: {
|
||||
services: {
|
||||
permission: {
|
||||
createPermissionsManager,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'content-manager': {
|
||||
services: {
|
||||
'single-types': {
|
||||
fetchEntitiyWithCreatorRoles() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
entityService: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const modelUid = 'test-model';
|
||||
|
||||
const ctx = createContext(
|
||||
{
|
||||
params: {
|
||||
model: modelUid,
|
||||
},
|
||||
},
|
||||
{ state, notFound }
|
||||
);
|
||||
|
||||
await singleTypes.find(ctx);
|
||||
|
||||
expect(state.userAbility.cannot).toHaveBeenCalledWith(ACTIONS.create, modelUid);
|
||||
expect(notFound).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const ACTIONS = {
|
||||
read: 'plugins::content-manager.explorer.read',
|
||||
create: 'plugins::content-manager.explorer.create',
|
||||
edit: 'plugins::content-manager.explorer.update',
|
||||
delete: 'plugins::content-manager.explorer.delete',
|
||||
publish: 'plugins::content-manager.explorer.publish',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ACTIONS,
|
||||
};
|
||||
@ -0,0 +1,214 @@
|
||||
'use strict';
|
||||
|
||||
const { prop, pipe, assoc, assign } = require('lodash/fp');
|
||||
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
||||
const { getService } = require('../utils');
|
||||
const parseBody = require('../utils/parse-body');
|
||||
const { ACTIONS } = require('./constants');
|
||||
|
||||
const {
|
||||
CREATED_BY_ATTRIBUTE,
|
||||
UPDATED_BY_ATTRIBUTE,
|
||||
PUBLISHED_AT_ATTRIBUTE,
|
||||
} = contentTypesUtils.constants;
|
||||
|
||||
const pickPermittedFields = ({ pm, action, model }) => data => {
|
||||
return pm.pickPermittedFieldsOf(data, { action, subject: model });
|
||||
};
|
||||
|
||||
const setCreatorFields = ({ user, isEdition = false }) => data => {
|
||||
if (isEdition) {
|
||||
return assoc(UPDATED_BY_ATTRIBUTE, user.id, data);
|
||||
}
|
||||
|
||||
return assign(data, {
|
||||
[CREATED_BY_ATTRIBUTE]: user.id,
|
||||
[UPDATED_BY_ATTRIBUTE]: user.id,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async find(ctx) {
|
||||
const { userAbility } = ctx.state;
|
||||
const { model } = ctx.params;
|
||||
|
||||
const pm = strapi.admin.services.permission.createPermissionsManager(
|
||||
userAbility,
|
||||
ACTIONS.read,
|
||||
model
|
||||
);
|
||||
|
||||
const singleTypeService = getService('single-types');
|
||||
|
||||
const entity = await singleTypeService.fetchEntitiyWithCreatorRoles(model);
|
||||
|
||||
// allow user with create permission to know a single type is not created
|
||||
if (!entity) {
|
||||
if (pm.ability.cannot(ACTIONS.create, model)) {
|
||||
return ctx.forbidden();
|
||||
}
|
||||
|
||||
return ctx.notFound();
|
||||
}
|
||||
|
||||
if (pm.ability.cannot(ACTIONS.read, pm.toSubject(entity))) {
|
||||
return ctx.forbidden();
|
||||
}
|
||||
|
||||
ctx.body = pm.sanitize(entity, { action: ACTIONS.read });
|
||||
},
|
||||
|
||||
async createOrUpdate(ctx) {
|
||||
const { user, userAbility } = ctx.state;
|
||||
const { model } = ctx.params;
|
||||
|
||||
const { data, files } = parseBody(ctx);
|
||||
|
||||
const singleTypeService = getService('single-types');
|
||||
const existingEntity = await singleTypeService.fetchEntitiyWithCreatorRoles(model);
|
||||
|
||||
try {
|
||||
if (!existingEntity) {
|
||||
const pm = strapi.admin.services.permission.createPermissionsManager(
|
||||
userAbility,
|
||||
ACTIONS.create,
|
||||
model
|
||||
);
|
||||
|
||||
const sanitizedData = pipe([
|
||||
pickPermittedFields({ pm, action: ACTIONS.create, model }),
|
||||
setCreatorFields({ user }),
|
||||
])(data);
|
||||
|
||||
const entity = await singleTypeService.create({ data: sanitizedData, files }, { model });
|
||||
|
||||
ctx.body = pm.sanitize(entity, { action: ACTIONS.read });
|
||||
return;
|
||||
}
|
||||
|
||||
const pm = strapi.admin.services.permission.createPermissionsManager(
|
||||
userAbility,
|
||||
ACTIONS.edit,
|
||||
model
|
||||
);
|
||||
|
||||
if (pm.ability.cannot(ACTIONS.edit, pm.toSubject(existingEntity))) {
|
||||
return strapi.errors.forbidden();
|
||||
}
|
||||
|
||||
const sanitizedData = pipe([
|
||||
pickPermittedFields({ pm, action: ACTIONS.edit, model: pm.toSubject(existingEntity) }),
|
||||
setCreatorFields({ user, isEdition: true }),
|
||||
])(data);
|
||||
|
||||
const entity = await singleTypeService.update(
|
||||
existingEntity,
|
||||
{ data: sanitizedData, files },
|
||||
{ model }
|
||||
);
|
||||
|
||||
ctx.body = pm.sanitize(entity, { action: ACTIONS.read });
|
||||
} catch (error) {
|
||||
strapi.log.error(error);
|
||||
ctx.badRequest(null, [
|
||||
{
|
||||
messages: [{ id: error.message, message: error.message, field: error.field }],
|
||||
errors: prop('data.errors', error),
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
async delete(ctx) {
|
||||
const { userAbility } = ctx.state;
|
||||
const { model } = ctx.params;
|
||||
|
||||
const singleTypeService = getService('single-types');
|
||||
|
||||
const existingEntity = await singleTypeService.fetchEntitiyWithCreatorRoles(model);
|
||||
|
||||
const pm = strapi.admin.services.permission.createPermissionsManager(
|
||||
userAbility,
|
||||
ACTIONS.delete,
|
||||
model
|
||||
);
|
||||
|
||||
if (pm.ability.cannot(ACTIONS.delete, pm.toSubject(existingEntity))) {
|
||||
return strapi.errors.forbidden();
|
||||
}
|
||||
|
||||
const deletedEntity = await singleTypeService.delete(existingEntity, { userAbility, model });
|
||||
|
||||
ctx.body = pm.sanitize(deletedEntity, { action: ACTIONS.read });
|
||||
},
|
||||
|
||||
async publish(ctx) {
|
||||
const { userAbility } = ctx.state;
|
||||
const { model } = ctx.params;
|
||||
|
||||
const singleTypeService = getService('single-types');
|
||||
|
||||
const existingEntity = await singleTypeService.fetchEntitiyWithCreatorRoles(model);
|
||||
|
||||
if (!existingEntity) {
|
||||
return ctx.notFound();
|
||||
}
|
||||
|
||||
const pm = strapi.admin.services.permission.createPermissionsManager(
|
||||
userAbility,
|
||||
ACTIONS.publish,
|
||||
model
|
||||
);
|
||||
|
||||
if (pm.ability.cannot(ACTIONS.publish, pm.toSubject(existingEntity))) {
|
||||
return ctx.forbidden();
|
||||
}
|
||||
|
||||
await strapi.entityValidator.validateEntityCreation(strapi.getModel(model), existingEntity);
|
||||
|
||||
if (existingEntity[PUBLISHED_AT_ATTRIBUTE]) {
|
||||
return ctx.badRequest('Already published');
|
||||
}
|
||||
|
||||
const publishedEntry = await getService('contentmanager').publish(
|
||||
{ id: existingEntity.id },
|
||||
model
|
||||
);
|
||||
|
||||
ctx.body = pm.sanitize(publishedEntry, { action: ACTIONS.read });
|
||||
},
|
||||
|
||||
async unpublish(ctx) {
|
||||
const { userAbility } = ctx.state;
|
||||
const { model } = ctx.params;
|
||||
|
||||
const singleTypeService = getService('single-types');
|
||||
|
||||
const existingEntity = await singleTypeService.fetchEntitiyWithCreatorRoles(model);
|
||||
|
||||
if (!existingEntity) {
|
||||
return ctx.notFound();
|
||||
}
|
||||
|
||||
const pm = strapi.admin.services.permission.createPermissionsManager(
|
||||
userAbility,
|
||||
ACTIONS.publish,
|
||||
model
|
||||
);
|
||||
|
||||
if (pm.ability.cannot(ACTIONS.publish, pm.toSubject(existingEntity))) {
|
||||
return ctx.forbidden();
|
||||
}
|
||||
|
||||
if (!existingEntity[PUBLISHED_AT_ATTRIBUTE]) {
|
||||
return ctx.badRequest('Already a draft');
|
||||
}
|
||||
|
||||
const unpublishedEntry = await getService('contentmanager').unpublish(
|
||||
{ id: existingEntity.id },
|
||||
model
|
||||
);
|
||||
|
||||
ctx.body = pm.sanitize(unpublishedEntry, { action: ACTIONS.read });
|
||||
},
|
||||
};
|
||||
@ -305,9 +305,7 @@ paths:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: integer
|
||||
$ref: '#/components/schemas/id'
|
||||
'[primaryKey]':
|
||||
oneOf:
|
||||
- type: string
|
||||
@ -338,47 +336,129 @@ paths:
|
||||
tags:
|
||||
- Collection Types content management
|
||||
description: Get one entry
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
put:
|
||||
tags:
|
||||
- Collection Types content management
|
||||
description: Update one entry
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
delete:
|
||||
tags:
|
||||
- Collection Types content management
|
||||
description: Delete one entry
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
/content-manager/collection-type/{model}/{id}/actions/publish:
|
||||
post:
|
||||
tags:
|
||||
- Collection Types content management
|
||||
description: Publish one entry
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
/content-manager/collection-type/{model}/{id}/actions/unpublish:
|
||||
post:
|
||||
tags:
|
||||
- Collection Types content management
|
||||
description: Unpublish one entry
|
||||
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
# Single type
|
||||
/content-manager/single-type/{model}:
|
||||
/content-manager/single-types/{model}:
|
||||
get:
|
||||
tags:
|
||||
- Single Types content management
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
put:
|
||||
tags:
|
||||
- Single Types content management
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
delete:
|
||||
tags:
|
||||
- Single Types content management
|
||||
/content-manager/single-type/{model}/actions/publish:
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
|
||||
/content-manager/single-types/{model}/actions/publish:
|
||||
post:
|
||||
tags:
|
||||
- Single Types content management
|
||||
/content-manager/single-type/{model}/actions/unpublish:
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
|
||||
/content-manager/single-types/{model}/actions/unpublish:
|
||||
post:
|
||||
tags:
|
||||
- Single Types content management
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/entity'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
id:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: integer
|
||||
|
||||
entity:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- created_by
|
||||
- updated_by
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/id'
|
||||
created_by:
|
||||
$ref: '#/components/schemas/user'
|
||||
updated_by:
|
||||
$ref: '#/components/schemas/user'
|
||||
additionalProperties:
|
||||
type: any
|
||||
|
||||
contentType:
|
||||
type: object
|
||||
properties:
|
||||
@ -521,6 +601,20 @@ components:
|
||||
- singleType
|
||||
- collectionType
|
||||
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
oneOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
firstname:
|
||||
type: string
|
||||
lastname:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
|
||||
examples:
|
||||
restaurant:
|
||||
uid: application::restaurant.restaurant
|
||||
|
||||
@ -13,7 +13,7 @@ const { ENTRY_PUBLISH, ENTRY_UNPUBLISH } = webhookUtils.webhookEvents;
|
||||
* A set of functions called "actions" for `ContentManager`
|
||||
*/
|
||||
module.exports = {
|
||||
fetchAll(model, query) {
|
||||
fetchAll(model, query = {}) {
|
||||
const { query: request, populate, ...filters } = query;
|
||||
|
||||
const queryFilter = !_.isEmpty(request)
|
||||
@ -113,6 +113,7 @@ module.exports = {
|
||||
{ params, data: { [PUBLISHED_AT_ATTRIBUTE]: null } },
|
||||
{ model }
|
||||
);
|
||||
|
||||
strapi.eventHub.emit(ENTRY_UNPUBLISH, {
|
||||
model: modelDef.modelName,
|
||||
entry: sanitizeEntity(unpublishedEntry, { model: modelDef }),
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const { omit, prop, has, assoc } = require('lodash/fp');
|
||||
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
||||
const { getService } = require('../utils');
|
||||
|
||||
const { CREATED_BY_ATTRIBUTE } = contentTypesUtils.constants;
|
||||
|
||||
const pickWritableFields = ({ model }) => {
|
||||
return omit(contentTypesUtils.getNonWritableAttributes(strapi.getModel(model)));
|
||||
};
|
||||
|
||||
const fetchCreatorRoles = entity => {
|
||||
const createdByPath = `${CREATED_BY_ATTRIBUTE}.id`;
|
||||
|
||||
if (has(createdByPath, entity)) {
|
||||
const creatorId = prop(createdByPath, entity);
|
||||
return strapi.query('role', 'admin').find({ 'users.id': creatorId }, []);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async fetchEntitiyWithCreatorRoles(model) {
|
||||
const entity = await getService('contentmanager').fetchAll(model);
|
||||
|
||||
if (!entity) {
|
||||
return entity;
|
||||
}
|
||||
|
||||
const roles = await fetchCreatorRoles(entity);
|
||||
return assoc(`${CREATED_BY_ATTRIBUTE}.roles`, roles, entity);
|
||||
},
|
||||
|
||||
async create(body, { model }) {
|
||||
const { files } = body;
|
||||
|
||||
const data = pickWritableFields({ model })(body.data);
|
||||
|
||||
const entity = await getService('contentmanager').create({ data, files }, { model });
|
||||
|
||||
await strapi.telemetry.send('didCreateFirstContentTypeEntry', { model });
|
||||
return entity;
|
||||
},
|
||||
|
||||
async update(existingEntity, body, { model }) {
|
||||
const { files } = body;
|
||||
|
||||
const data = pickWritableFields({ model })(body.data);
|
||||
|
||||
const entity = await getService('contentmanager').edit(
|
||||
{ id: existingEntity.id },
|
||||
{ data, files },
|
||||
{ model }
|
||||
);
|
||||
|
||||
return entity;
|
||||
},
|
||||
|
||||
async delete(existingEntity, { model }) {
|
||||
return getService('contentmanager').delete(model, {
|
||||
id: existingEntity.id,
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -31,7 +31,7 @@ describe('Content Manager single types', () => {
|
||||
|
||||
test('Label is not pluralized', async () => {
|
||||
const res = await rq({
|
||||
url: `/content-manager/schemas/content-types?kind=singleType`,
|
||||
url: `/content-manager/content-types?kind=singleType`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
@ -39,7 +39,9 @@ describe('Content Manager single types', () => {
|
||||
expect(res.body.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
label: 'Single-type-model',
|
||||
info: expect.objectContaining({
|
||||
label: 'Single-type-model',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
@ -47,7 +49,7 @@ describe('Content Manager single types', () => {
|
||||
|
||||
test('find single type content returns 404 when not created', async () => {
|
||||
const res = await rq({
|
||||
url: `/content-manager/explorer/${uid}`,
|
||||
url: `/content-manager/single-types/${uid}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
@ -56,8 +58,8 @@ describe('Content Manager single types', () => {
|
||||
|
||||
test('Create content', async () => {
|
||||
const res = await rq({
|
||||
url: `/content-manager/explorer/${uid}`,
|
||||
method: 'POST',
|
||||
url: `/content-manager/single-types/${uid}`,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
title: 'Title',
|
||||
},
|
||||
@ -72,7 +74,7 @@ describe('Content Manager single types', () => {
|
||||
|
||||
test('find single type content returns an object ', async () => {
|
||||
const res = await rq({
|
||||
url: `/content-manager/explorer/${uid}`,
|
||||
url: `/content-manager/single-types/${uid}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
const { prop } = require('lodash/fp');
|
||||
|
||||
module.exports = {
|
||||
// retrieve a local service from the contet manager plugin to make the code more readable
|
||||
// retrieve a local service
|
||||
getService(name) {
|
||||
return prop(`content-manager.services.${name}`, strapi.plugins);
|
||||
},
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const parseMultipartBody = require('./parse-multipart');
|
||||
|
||||
module.exports = ctx => {
|
||||
const { body } = ctx.request;
|
||||
return ctx.is('multipart') ? parseMultipartBody(ctx) : { data: body };
|
||||
};
|
||||
@ -20,8 +20,8 @@
|
||||
nav-accent-color=""
|
||||
primary-color=""
|
||||
theme="dark"
|
||||
|
||||
schema-style="table"
|
||||
default-schema-tab="example"
|
||||
>
|
||||
</rapi-doc>
|
||||
</body>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user