feat(content-releases): add publish webhook (#19515)

* feat(content-releases): add publish webhook

* apply Marks feedback
This commit is contained in:
Fernando Chávez 2024-02-20 14:38:27 +01:00 committed by GitHub
parent 8d3598965d
commit 4af8963f68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 675 additions and 597 deletions

View File

@ -1 +1,5 @@
module.exports = ({ env }) => ({}); module.exports = ({ env }) => ({
future: {
contentReleasesScheduling: env('STRAPI_FUTURE_CONTENT_RELEASES_SCHEDULING', false),
},
});

View File

@ -1,11 +1,21 @@
import { Events } from '../../../../../../../../admin/src/pages/Settings/pages/Webhooks/components/Events'; import { Events } from '../../../../../../../../admin/src/pages/Settings/pages/Webhooks/components/Events';
const events = { const eeTables = {
'review-workflows': {
'review-workflows': ['review-workflows.updateEntryStage'], 'review-workflows': ['review-workflows.updateEntryStage'],
},
releases: {
releases: ['releases.publish'],
},
}; };
const getHeaders = () => { const getHeaders = (table: keyof typeof eeTables) => {
return [{ id: 'review-workflows.updateEntryStage', defaultMessage: 'Stage Change' }]; switch (table) {
case 'review-workflows':
return () => [{ id: 'review-workflows.updateEntryStage', defaultMessage: 'Stage Change' }];
case 'releases':
return () => [{ id: 'releases.publish', defaultMessage: 'Publish' }];
}
}; };
const EventsTableEE = () => { const EventsTableEE = () => {
@ -13,8 +23,12 @@ const EventsTableEE = () => {
<Events.Root> <Events.Root>
<Events.Headers /> <Events.Headers />
<Events.Body /> <Events.Body />
<Events.Headers getHeaders={getHeaders} /> {(Object.keys(eeTables) as Array<keyof typeof eeTables>).map((table) => (
<Events.Body providedEvents={events} /> <>
<Events.Headers getHeaders={getHeaders(table)} />
<Events.Body providedEvents={eeTables[table]} />
</>
))}
</Events.Root> </Events.Root>
); );
}; };

View File

@ -84,6 +84,9 @@ describe('bootstrap', () => {
log: { log: {
error: jest.fn(), error: jest.fn(),
}, },
webhookStore: {
addAllowedEvent: jest.fn(),
},
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import type { LoadedStrapi, Entity as StrapiEntity } from '@strapi/types'; import type { LoadedStrapi, Entity as StrapiEntity } from '@strapi/types';
import { RELEASE_ACTION_MODEL_UID } from './constants'; import { ALLOWED_WEBHOOK_EVENTS, RELEASE_ACTION_MODEL_UID } from './constants';
import { getService } from './utils'; import { getService } from './utils';
const { features } = require('@strapi/strapi/dist/utils/ee'); const { features } = require('@strapi/strapi/dist/utils/ee');
@ -69,6 +69,10 @@ export const bootstrap = async ({ strapi }: { strapi: LoadedStrapi }) => {
throw err; throw err;
}); });
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
strapi.webhookStore.addAllowedEvent(key, value);
});
} }
} }
}; };

View File

@ -45,3 +45,7 @@ export const ACTIONS = [
pluginName: 'content-releases', pluginName: 'content-releases',
}, },
]; ];
export const ALLOWED_WEBHOOK_EVENTS = {
RELEASES_PUBLISH: 'releases.publish',
};

View File

@ -26,6 +26,9 @@ const baseStrapiMock = {
isEnabled: jest.fn().mockReturnValue(true), isEnabled: jest.fn().mockReturnValue(true),
}, },
}, },
eventHub: {
emit: jest.fn(),
},
telemetry: { telemetry: {
send: jest.fn().mockReturnValue(true), send: jest.fn().mockReturnValue(true),
}, },

View File

@ -4,7 +4,7 @@ import type { LoadedStrapi, EntityService, UID, Schema } from '@strapi/types';
import _ from 'lodash/fp'; import _ from 'lodash/fp';
import { RELEASE_ACTION_MODEL_UID, RELEASE_MODEL_UID } from '../constants'; import { ALLOWED_WEBHOOK_EVENTS, RELEASE_ACTION_MODEL_UID, RELEASE_MODEL_UID } from '../constants';
import type { import type {
GetReleases, GetReleases,
CreateRelease, CreateRelease,
@ -48,7 +48,19 @@ const getGroupName = (queryValue?: ReleaseActionGroupBy) => {
} }
}; };
const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => {
const dispatchWebhook = (
event: string,
{ isPublished, release, error }: { isPublished: boolean; release?: Release; error?: unknown }
) => {
strapi.eventHub.emit(event, {
isPublished,
error,
release,
});
};
return {
async create(releaseData: CreateRelease.Request['body'], { user }: { user: UserInfo }) { async create(releaseData: CreateRelease.Request['body'], { user }: { user: UserInfo }) {
const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData); const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
@ -201,7 +213,9 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
releaseData: UpdateRelease.Request['body'], releaseData: UpdateRelease.Request['body'],
{ user }: { user: UserInfo } { user }: { user: UserInfo }
) { ) {
const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData); const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(
releaseData
);
const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService( const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
'release-validation', 'release-validation',
@ -315,7 +329,9 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
}); });
}, },
async countActions(query: EntityService.Params.Pick<typeof RELEASE_ACTION_MODEL_UID, 'filters'>) { async countActions(
query: EntityService.Params.Pick<typeof RELEASE_ACTION_MODEL_UID, 'filters'>
) {
return strapi.entityService.count(RELEASE_ACTION_MODEL_UID, query); return strapi.entityService.count(RELEASE_ACTION_MODEL_UID, query);
}, },
@ -409,12 +425,17 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
}, },
async getAllComponents() { async getAllComponents() {
const contentManagerComponentsService = strapi.plugin('content-manager').service('components'); const contentManagerComponentsService = strapi
.plugin('content-manager')
.service('components');
const components = await contentManagerComponentsService.findAllComponents(); const components = await contentManagerComponentsService.findAllComponents();
const componentsMap = components.reduce( const componentsMap = components.reduce(
(acc: { [key: Schema.Component['uid']]: Schema.Component }, component: Schema.Component) => { (
acc: { [key: Schema.Component['uid']]: Schema.Component },
component: Schema.Component
) => {
acc[component.uid] = component; acc[component.uid] = component;
return acc; return acc;
@ -466,6 +487,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
}, },
async publish(releaseId: PublishRelease.Request['params']['id']) { async publish(releaseId: PublishRelease.Request['params']['id']) {
try {
// We need to pass the type because entityService.findOne is not returning the correct type // We need to pass the type because entityService.findOne is not returning the correct type
const releaseWithPopulatedActionEntries = (await strapi.entityService.findOne( const releaseWithPopulatedActionEntries = (await strapi.entityService.findOne(
RELEASE_MODEL_UID, RELEASE_MODEL_UID,
@ -615,7 +637,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
}); });
// When the transaction fails it throws an error, when it is successful proceed to updating the release // When the transaction fails it throws an error, when it is successful proceed to updating the release
const release = await strapi.entityService.update(RELEASE_MODEL_UID, releaseId, { const release = (await strapi.entityService.update(RELEASE_MODEL_UID, releaseId, {
data: { data: {
/* /*
* The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
@ -623,11 +645,34 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
// @ts-expect-error see above // @ts-expect-error see above
releasedAt: new Date(), releasedAt: new Date(),
}, },
populate: {
actions: {
// @ts-expect-error is not expecting count but it is working
count: true,
},
},
})) as Release;
if (strapi.features.future.isEnabled('contentReleasesScheduling')) {
dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
isPublished: true,
release,
}); });
}
strapi.telemetry.send('didPublishContentRelease'); strapi.telemetry.send('didPublishContentRelease');
return release; return release;
} catch (error) {
if (strapi.features.future.isEnabled('contentReleasesScheduling')) {
dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
isPublished: false,
error,
});
}
throw error;
}
}, },
async updateAction( async updateAction(
@ -681,6 +726,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
return deletedAction; return deletedAction;
}, },
}); };
};
export default createReleaseService; export default createReleaseService;