Merge remote-tracking branch 'origin/main' into webhooks/edit-view

This commit is contained in:
Jamie Howard 2023-06-02 11:14:18 +01:00
commit 96bfeca3db
20 changed files with 141 additions and 66 deletions

View File

@ -71,6 +71,9 @@ const strapiMock = {
return null;
}
},
webhookStore: {
addAllowedEvent: jest.fn(),
},
};
const reviewWorkflowsService = reviewWorkflowsServiceFactory({ strapi: strapiMock });

View File

@ -108,12 +108,16 @@ function persistStagesJoinTables({ strapi }) {
};
}
const registerWebhookEvents = async ({ strapi }) =>
strapi.webhookStore.addAllowedEvent('WORKFLOW_UPDATE_STAGE', 'workflow.updateEntryStage');
module.exports = ({ strapi }) => {
const workflowsService = getService('workflows', { strapi });
const stagesService = getService('stages', { strapi });
return {
async bootstrap() {
await registerWebhookEvents({ strapi });
await initDefaultWorkflow({ workflowsService, stagesService, strapi });
},
async register() {

View File

@ -1,7 +1,7 @@
'use strict';
const _ = require('lodash');
const { yup, webhook: webhookUtils, validateYupSchema } = require('@strapi/utils');
const { yup, validateYupSchema } = require('@strapi/utils');
const urlRegex =
/^(?:([a-z0-9+.-]+):\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9_]-*)*[a-z\u00a1-\uffff0-9_]+)(?:\.(?:[a-z\u00a1-\uffff0-9_]-*)*[a-z\u00a1-\uffff0-9_]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/;
@ -23,7 +23,7 @@ const webhookValidator = yup
)
.required();
}),
events: yup.array().of(yup.string().oneOf(_.values(webhookUtils.webhookEvents)).required()),
events: yup.array().of(yup.string()).required(),
})
.noUnknown();
@ -111,11 +111,11 @@ module.exports = {
for (const id of ids) {
const webhook = await strapi.webhookStore.findWebhook(id);
if (!webhook) continue;
if (webhook) {
await strapi.webhookStore.deleteWebhook(id);
strapi.webhookRunner.remove(webhook);
}
}
ctx.send({ data: ids });
},

View File

@ -1,8 +1,13 @@
'use strict';
const { getService } = require('./utils');
const { ALLOWED_WEBHOOK_EVENTS } = require('./constants');
module.exports = async () => {
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
strapi.webhookStore.addAllowedEvent(key, value);
});
await getService('components').syncConfigurations();
await getService('content-types').syncConfigurations();
await getService('permission').registerPermissions();

View File

@ -0,0 +1,10 @@
'use strict';
const ALLOWED_WEBHOOK_EVENTS = {
ENTRY_PUBLISH: 'entry.publish',
ENTRY_UNPUBLISH: 'entry.unpublish',
};
module.exports = {
ALLOWED_WEBHOOK_EVENTS,
};

View File

@ -382,7 +382,7 @@ describe('Single Types', () => {
await singleTypes.publish(ctx);
expect(publishFn).toHaveBeenCalledWith(entity, { updatedBy: state.user.id }, modelUid);
expect(publishFn).toHaveBeenCalledWith(entity, modelUid, { updatedBy: state.user.id });
expect(permissionChecker.cannot.publish).toHaveBeenCalledWith(entity);
expect(permissionChecker.sanitizeOutput).toHaveBeenCalled();
});
@ -479,7 +479,7 @@ describe('Single Types', () => {
await singleTypes.unpublish(ctx);
expect(unpublishFn).toHaveBeenCalledWith(entity, { updatedBy: state.user.id }, modelUid);
expect(unpublishFn).toHaveBeenCalledWith(entity, modelUid, { updatedBy: state.user.id });
expect(permissionChecker.cannot.unpublish).toHaveBeenCalledWith(entity);
expect(permissionChecker.sanitizeOutput).toHaveBeenCalled();
});

View File

@ -174,8 +174,8 @@ module.exports = {
const result = await entityManager.publish(
entity,
setCreatorFields({ user, isEdition: true })({}),
model
model,
setCreatorFields({ user, isEdition: true })({})
);
ctx.body = await permissionChecker.sanitizeOutput(result);
@ -204,8 +204,8 @@ module.exports = {
const result = await entityManager.unpublish(
entity,
setCreatorFields({ user, isEdition: true })({}),
model
model,
setCreatorFields({ user, isEdition: true })({})
);
ctx.body = await permissionChecker.sanitizeOutput(result);

View File

@ -148,8 +148,8 @@ module.exports = {
const publishedEntity = await entityManager.publish(
entity,
setCreatorFields({ user, isEdition: true })({}),
model
model,
setCreatorFields({ user, isEdition: true })({})
);
ctx.body = await permissionChecker.sanitizeOutput(publishedEntity);
@ -181,8 +181,8 @@ module.exports = {
const unpublishedEntity = await entityManager.unpublish(
entity,
setCreatorFields({ user, isEdition: true })({}),
model
model,
setCreatorFields({ user, isEdition: true })({})
);
ctx.body = await permissionChecker.sanitizeOutput(unpublishedEntity);

View File

@ -26,6 +26,9 @@ describe('Content-Manager', () => {
config: {
get: (path, defaultValue) => _.get(defaultConfig, path, defaultValue),
},
webhookStore: {
allowedEvents: new Map([['ENTRY_PUBLISH', 'entry.publish']]),
},
};
entityManager = entityManagerLoader({ strapi });
});
@ -37,7 +40,7 @@ describe('Content-Manager', () => {
test('Publish a content-type', async () => {
const uid = 'api::test.test';
const entity = { id: 1, publishedAt: null };
await entityManager.publish(entity, {}, uid);
await entityManager.publish(entity, uid, {});
expect(strapi.entityService.update).toBeCalledWith(uid, entity.id, {
data: { publishedAt: expect.any(Date) },
@ -58,6 +61,9 @@ describe('Content-Manager', () => {
config: {
get: (path, defaultValue) => _.get(defaultConfig, path, defaultValue),
},
webhookStore: {
allowedEvents: new Map([['ENTRY_UNPUBLISH', 'entry.unpublish']]),
},
};
entityManager = entityManagerLoader({ strapi });
});
@ -69,7 +75,7 @@ describe('Content-Manager', () => {
test('Unpublish a content-type', async () => {
const uid = 'api::test.test';
const entity = { id: 1, publishedAt: new Date() };
await entityManager.unpublish(entity, {}, uid);
await entityManager.unpublish(entity, uid, {});
expect(strapi.entityService.update).toHaveBeenCalledWith(uid, entity.id, {
data: { publishedAt: null },

View File

@ -7,15 +7,17 @@ const { ApplicationError } = require('@strapi/utils').errors;
const { getDeepPopulate, getDeepPopulateDraftCount } = require('./utils/populate');
const { getDeepRelationsCount } = require('./utils/count');
const { sumDraftCounts } = require('./utils/draft');
const {
ALLOWED_WEBHOOK_EVENTS: { ENTRY_PUBLISH, ENTRY_UNPUBLISH },
} = require('../constants');
const { hasDraftAndPublish } = strapiUtils.contentTypes;
const { PUBLISHED_AT_ATTRIBUTE, CREATED_BY_ATTRIBUTE } = strapiUtils.contentTypes.constants;
const { ENTRY_PUBLISH, ENTRY_UNPUBLISH } = strapiUtils.webhook.webhookEvents;
const omitPublishedAtField = omit(PUBLISHED_AT_ATTRIBUTE);
const emitEvent = async (event, entity, modelUid) => {
const modelDef = strapi.getModel(modelUid);
const emitEvent = async (uid, event, entity) => {
const modelDef = strapi.getModel(uid);
const sanitizedEntity = await strapiUtils.sanitize.sanitizers.defaultSanitizeOutput(
modelDef,
entity
@ -229,7 +231,7 @@ module.exports = ({ strapi }) => ({
return strapi.entityService.deleteMany(uid, params);
},
async publish(entity, body = {}, uid) {
async publish(entity, uid, body = {}) {
if (entity[PUBLISHED_AT_ATTRIBUTE]) {
throw new ApplicationError('already.published');
}
@ -254,7 +256,7 @@ module.exports = ({ strapi }) => ({
const updatedEntity = await strapi.entityService.update(uid, entity.id, params);
await emitEvent(ENTRY_PUBLISH, updatedEntity, uid);
await emitEvent(uid, ENTRY_PUBLISH, updatedEntity);
const mappedEntity = await this.mapEntity(updatedEntity, uid);
@ -266,7 +268,7 @@ module.exports = ({ strapi }) => ({
return mappedEntity;
},
async unpublish(entity, body = {}, uid) {
async unpublish(entity, uid, body = {}) {
if (!entity[PUBLISHED_AT_ATTRIBUTE]) {
throw new ApplicationError('already.draft');
}
@ -283,7 +285,7 @@ module.exports = ({ strapi }) => ({
const updatedEntity = await strapi.entityService.update(uid, entity.id, params);
await emitEvent(ENTRY_UNPUBLISH, updatedEntity, uid);
await emitEvent(uid, ENTRY_UNPUBLISH, updatedEntity);
const mappedEntity = await this.mapEntity(updatedEntity, uid);

View File

@ -20,6 +20,9 @@ describe('Entity service triggers webhooks', () => {
instance = createEntityService({
strapi: {
getModel: () => model,
webhookStore: {
addAllowedEvent: jest.fn(),
},
},
db: {
transaction: (cb) => cb(),
@ -39,9 +42,6 @@ describe('Entity service triggers webhooks', () => {
global.strapi = {
getModel: () => model,
config: {
get: () => [],
},
};
});

View File

@ -18,6 +18,10 @@ describe('Entity service', () => {
},
},
query: jest.fn(() => ({})),
webhookStore: {
allowedEvents: new Map([['ENTRY_CREATE', 'entry.create']]),
addAllowedEvent: jest.fn(),
},
};
describe('Decorator', () => {
@ -25,7 +29,7 @@ describe('Entity service', () => {
'Can decorate',
async (method) => {
const instance = createEntityService({
strapi: {},
strapi: global.strapi,
db: {},
eventHub: new EventEmitter(),
});
@ -61,6 +65,7 @@ describe('Entity service', () => {
};
const fakeStrapi = {
...global.strapi,
getModel: jest.fn(() => {
return { kind: 'singleType' };
}),
@ -98,12 +103,15 @@ describe('Entity service', () => {
global.strapi.getModel.mockImplementation((modelName) => fakeModels[modelName]);
global.strapi.query.mockImplementation(() => fakeQuery);
});
beforeEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
global.strapi.getModel.mockImplementation(() => ({}));
});
describe('assign default values', () => {
let instance;
const entityUID = 'api::entity.entity';
@ -373,10 +381,13 @@ describe('Entity service', () => {
},
};
global.strapi = fakeStrapi;
global.strapi = {
...global.strapi,
...fakeStrapi,
};
instance = createEntityService({
strapi: fakeStrapi,
strapi: global.strapi,
db: fakeDB,
eventHub: new EventEmitter(),
entityValidator,
@ -522,6 +533,7 @@ describe('Entity service', () => {
};
global.strapi = {
...global.strapi,
getModel: jest.fn((uid) => {
return fakeModels[uid];
}),

View File

@ -4,11 +4,7 @@ const _ = require('lodash');
const delegate = require('delegates');
const { InvalidTimeError, InvalidDateError, InvalidDateTimeError, InvalidRelationError } =
require('@strapi/database').errors;
const {
webhook: webhookUtils,
contentTypes: contentTypesUtils,
sanitize,
} = require('@strapi/utils');
const { contentTypes: contentTypesUtils, sanitize } = require('@strapi/utils');
const { ValidationError } = require('@strapi/utils').errors;
const { isAnyToMany } = require('@strapi/utils').relations;
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
@ -31,9 +27,6 @@ const transformLoadParamsToQuery = (uid, field, params = {}, pagination = {}) =>
};
};
// TODO: those should be strapi events used by the webhooks not the other way arround
const { ENTRY_CREATE, ENTRY_UPDATE, ENTRY_DELETE } = webhookUtils.webhookEvents;
const databaseErrorsToTransform = [
InvalidTimeError,
InvalidDateTimeError,
@ -49,6 +42,12 @@ const updatePipeline = (data, context) => {
return applyTransforms(data, context);
};
const ALLOWED_WEBHOOK_EVENTS = {
ENTRY_CREATE: 'entry.create',
ENTRY_UPDATE: 'entry.update',
ENTRY_DELETE: 'entry.delete',
};
/**
* @type {import('.').default}
*/
@ -180,6 +179,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
entity = await this.wrapResult(entity, { uid, action: 'create' });
const { ENTRY_CREATE } = ALLOWED_WEBHOOK_EVENTS;
await this.emitEvent(uid, ENTRY_CREATE, entity);
return entity;
@ -233,6 +233,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
entity = await this.wrapResult(entity, { uid, action: 'update' });
const { ENTRY_UPDATE } = ALLOWED_WEBHOOK_EVENTS;
await this.emitEvent(uid, ENTRY_UPDATE, entity);
return entity;
@ -260,6 +261,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
entityToDelete = await this.wrapResult(entityToDelete, { uid, action: 'delete' });
const { ENTRY_DELETE } = ALLOWED_WEBHOOK_EVENTS;
await this.emitEvent(uid, ENTRY_DELETE, entityToDelete);
return entityToDelete;
@ -290,6 +292,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
entitiesToDelete = await this.wrapResult(entitiesToDelete, { uid, action: 'delete' });
// Trigger webhooks. One for each entity
const { ENTRY_DELETE } = ALLOWED_WEBHOOK_EVENTS;
await Promise.all(entitiesToDelete.map((entity) => this.emitEvent(uid, ENTRY_DELETE, entity)));
return deletedEntities;
@ -331,6 +334,10 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
});
module.exports = (ctx) => {
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
ctx.strapi.webhookStore.addAllowedEvent(key, value);
});
const implementation = createDefaultImplementation(ctx);
const service = {

View File

@ -4,6 +4,9 @@
'use strict';
const { mapAsync } = require('@strapi/utils');
const { ValidationError } = require('@strapi/utils').errors;
const webhookModel = {
uid: 'webhook',
collectionName: 'strapi_webhooks',
@ -47,30 +50,56 @@ const fromDBObject = (row) => {
};
};
const webhookEventValidator = async (allowedEvents, events) => {
const allowedValues = Array.from(allowedEvents.values());
await mapAsync(events, (event) => {
if (allowedValues.includes(event)) {
return;
}
throw new ValidationError(`Webhook event ${event} is not supported`);
});
};
const createWebhookStore = ({ db }) => {
const webhookQueries = db.query('webhook');
return {
allowedEvents: new Map([]),
addAllowedEvent(key, value) {
this.allowedEvents.set(key, value);
},
removeAllowedEvent(key) {
this.allowedEvents.delete(key);
},
listAllowedEvents() {
return Array.from(this.allowedEvents.keys());
},
getAllowedEvent(key) {
return this.allowedEvents.get(key);
},
async findWebhooks() {
const results = await webhookQueries.findMany();
return results.map(fromDBObject);
},
async findWebhook(id) {
const result = await webhookQueries.findOne({ where: { id } });
return result ? fromDBObject(result) : null;
},
async createWebhook(data) {
await webhookEventValidator(this.allowedEvents, data.events);
createWebhook(data) {
return webhookQueries
.create({
data: toDBObject({ ...data, isEnabled: true }),
})
.then(fromDBObject);
},
async updateWebhook(id, data) {
await webhookEventValidator(this.allowedEvents, data.events);
const webhook = await webhookQueries.update({
where: { id },
data: toDBObject(data),
@ -78,7 +107,6 @@ const createWebhookStore = ({ db }) => {
return webhook ? fromDBObject(webhook) : null;
},
async deleteWebhook(id) {
const webhook = await webhookQueries.delete({ where: { id } });
return webhook ? fromDBObject(webhook) : null;

View File

@ -68,6 +68,9 @@ describe('Upload plugin bootstrap function', () => {
set: setStore,
};
},
webhookStore: {
addAllowedEvent: jest.fn(),
},
};
await bootstrap({ strapi });

View File

@ -1,7 +1,7 @@
'use strict';
const { getService } = require('./utils');
const { ALLOWED_SORT_STRINGS } = require('./constants');
const { ALLOWED_SORT_STRINGS, ALLOWED_WEBHOOK_EVENTS } = require('./constants');
module.exports = async ({ strapi }) => {
const defaultConfig = {
@ -36,6 +36,7 @@ module.exports = async ({ strapi }) => {
}
await registerPermissionActions();
await registerWebhookEvents();
await getService('weeklyMetrics').registerCron();
getService('metrics').sendUploadPluginMetrics();
@ -47,6 +48,11 @@ module.exports = async ({ strapi }) => {
}
};
const registerWebhookEvents = async () =>
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
strapi.webhookStore.addAllowedEvent(key, value);
});
const registerPermissionActions = async () => {
const actions = [
{

View File

@ -19,10 +19,17 @@ const ALLOWED_SORT_STRINGS = [
'updatedAt:ASC',
];
const ALLOWED_WEBHOOK_EVENTS = {
MEDIA_CREATE: 'media.create',
MEDIA_UPDATE: 'media.update',
MEDIA_DELETE: 'media.delete',
};
module.exports = {
ACTIONS,
FOLDER_MODEL_UID: 'plugin::upload.folder',
FILE_MODEL_UID: 'plugin::upload.file',
API_UPLOAD_FOLDER_BASE_NAME: 'API Uploads',
ALLOWED_SORT_STRINGS,
ALLOWED_WEBHOOK_EVENTS,
};

View File

@ -17,14 +17,14 @@ const {
sanitize,
nameToSlug,
contentTypes: contentTypesUtils,
webhook: webhookUtils,
errors: { ApplicationError, NotFoundError },
file: { bytesToKbytes },
} = require('@strapi/utils');
const { MEDIA_UPDATE, MEDIA_CREATE, MEDIA_DELETE } = webhookUtils.webhookEvents;
const { FILE_MODEL_UID } = require('../constants');
const {
FILE_MODEL_UID,
ALLOWED_WEBHOOK_EVENTS: { MEDIA_CREATE, MEDIA_UPDATE, MEDIA_DELETE },
} = require('../constants');
const { getService } = require('../utils');
const { UPDATED_BY_ATTRIBUTE, CREATED_BY_ATTRIBUTE } = contentTypesUtils.constants;

View File

@ -28,7 +28,6 @@ const { removeUndefined, keysDeep } = require('./object-formatting');
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
const { generateTimestampCode } = require('./code-generator');
const contentTypes = require('./content-types');
const webhook = require('./webhook');
const env = require('./env-helper');
const relations = require('./relations');
const setCreatorFields = require('./set-creator-fields');
@ -75,7 +74,6 @@ module.exports = {
isCamelCase,
toKebabCase,
contentTypes,
webhook,
env,
relations,
setCreatorFields,

View File

@ -1,16 +0,0 @@
'use strict';
const webhookEvents = {
ENTRY_CREATE: 'entry.create',
ENTRY_UPDATE: 'entry.update',
ENTRY_DELETE: 'entry.delete',
ENTRY_PUBLISH: 'entry.publish',
ENTRY_UNPUBLISH: 'entry.unpublish',
MEDIA_CREATE: 'media.create',
MEDIA_UPDATE: 'media.update',
MEDIA_DELETE: 'media.delete',
};
module.exports = {
webhookEvents,
};