mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 16:29:34 +00:00
feat(content-releases): publish a release (#18973)
* feat(content-releases): publish a release * feat(content-releases): publish is enabled if you have releases publish permissions * chore(content-releases): Mark's feedback * Update packages/core/content-releases/server/src/services/release.ts Co-authored-by: markkaylor <mark.kaylor@strapi.io> --------- Co-authored-by: markkaylor <mark.kaylor@strapi.io>
This commit is contained in:
parent
f803c8a5b6
commit
1cab7003f9
@ -83,6 +83,11 @@ packages/core/content-releases/server/src/routes/release.ts
|
||||
}
|
||||
```
|
||||
|
||||
**Publish a release**:
|
||||
|
||||
- method: `POST`
|
||||
- endpoint: `/content-releases/:id/publish`
|
||||
|
||||
### Release Action
|
||||
|
||||
**Create a release action**
|
||||
|
@ -5,6 +5,7 @@ import { validateRelease } from './validation/release';
|
||||
import type {
|
||||
CreateRelease,
|
||||
UpdateRelease,
|
||||
PublishRelease,
|
||||
GetRelease,
|
||||
Release,
|
||||
} from '../../../shared/contracts/releases';
|
||||
@ -134,6 +135,18 @@ const releaseController = {
|
||||
data: await permissionsManager.sanitizeOutput(release),
|
||||
};
|
||||
},
|
||||
|
||||
async publish(ctx: Koa.Context) {
|
||||
const user: PublishRelease.Request['state']['user'] = ctx.state.user;
|
||||
const id: PublishRelease.Request['params']['id'] = ctx.params.id;
|
||||
|
||||
const releaseService = getService('release', { strapi });
|
||||
const release = await releaseService.publish(id, { user });
|
||||
|
||||
ctx.body = {
|
||||
data: release,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default releaseController;
|
||||
|
@ -65,5 +65,21 @@ export default {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/:id/publish',
|
||||
handler: 'release.publish',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['plugin::content-releases.publish'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import createReleaseService from "../release";
|
||||
import createReleaseService from '../release';
|
||||
|
||||
const baseStrapiMock = {
|
||||
utils: {
|
||||
@ -55,7 +55,97 @@ describe('release service', () => {
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const releaseService = createReleaseService({ strapi: strapiMock });
|
||||
|
||||
expect(() => releaseService.findActions(1, ['api::contentType.contentType'], {})).rejects.toThrow('No release found for id 1');
|
||||
expect(() =>
|
||||
releaseService.findActions(1, ['api::contentType.contentType'], {})
|
||||
).rejects.toThrow('No release found for id 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('publish', () => {
|
||||
it('throws an error if the release does not exist', () => {
|
||||
const strapiMock = {
|
||||
...baseStrapiMock,
|
||||
entityService: {
|
||||
findOne: jest.fn().mockReturnValue(null),
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const releaseService = createReleaseService({ strapi: strapiMock });
|
||||
|
||||
expect(() => releaseService.publish(1)).rejects.toThrow('No release found for id 1');
|
||||
});
|
||||
|
||||
it('throws an error if the release is already published', () => {
|
||||
const strapiMock = {
|
||||
...baseStrapiMock,
|
||||
entityService: {
|
||||
findOne: jest.fn().mockReturnValue({ releasedAt: new Date() }),
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const releaseService = createReleaseService({ strapi: strapiMock });
|
||||
|
||||
expect(() => releaseService.publish(1)).rejects.toThrow('Release already published');
|
||||
});
|
||||
|
||||
it('throws an error if the release have 0 actions', () => {
|
||||
const strapiMock = {
|
||||
...baseStrapiMock,
|
||||
entityService: {
|
||||
findOne: jest.fn().mockReturnValue({ releasedAt: null, actions: [] }),
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const releaseService = createReleaseService({ strapi: strapiMock });
|
||||
|
||||
expect(() => releaseService.publish(1)).rejects.toThrow('No entries to publish');
|
||||
});
|
||||
|
||||
it('calls publishMany for each contentType with the right actions', async () => {
|
||||
const mockPublishMany = jest.fn();
|
||||
const mockUnpublishMany = jest.fn();
|
||||
|
||||
const strapiMock = {
|
||||
...baseStrapiMock,
|
||||
db: {
|
||||
transaction: jest.fn().mockImplementation((cb) => cb()),
|
||||
},
|
||||
plugin: jest.fn().mockReturnValue({
|
||||
service: jest.fn().mockReturnValue({
|
||||
publishMany: mockPublishMany,
|
||||
unpublishMany: mockUnpublishMany,
|
||||
}),
|
||||
}),
|
||||
entityService: {
|
||||
findOne: jest.fn().mockReturnValue({
|
||||
releasedAt: null,
|
||||
actions: [
|
||||
{
|
||||
contentType: 'contentType',
|
||||
type: 'publish',
|
||||
entry: { id: 1 },
|
||||
},
|
||||
{
|
||||
contentType: 'contentType',
|
||||
type: 'unpublish',
|
||||
entry: { id: 2 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
update: jest.fn().mockReturnValue({}),
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const releaseService = createReleaseService({ strapi: strapiMock });
|
||||
|
||||
await releaseService.publish(1);
|
||||
|
||||
expect(mockPublishMany).toHaveBeenCalledWith([{ id: 1 }], 'contentType');
|
||||
expect(mockUnpublishMany).toHaveBeenCalledWith([{ id: 2 }], 'contentType');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,12 +5,14 @@ import type {
|
||||
GetReleases,
|
||||
CreateRelease,
|
||||
UpdateRelease,
|
||||
PublishRelease,
|
||||
GetRelease,
|
||||
Release,
|
||||
} from '../../../shared/contracts/releases';
|
||||
import type {
|
||||
CreateReleaseAction,
|
||||
GetReleaseActions,
|
||||
ReleaseAction,
|
||||
UpdateReleaseAction,
|
||||
DeleteReleaseAction,
|
||||
} from '../../../shared/contracts/release-actions';
|
||||
@ -125,7 +127,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
||||
async countActions(query: EntityService.Params.Pick<typeof RELEASE_ACTION_MODEL_UID, 'filters'>) {
|
||||
return strapi.entityService.count(RELEASE_ACTION_MODEL_UID, query);
|
||||
},
|
||||
async findReleaseContentTypesMainFields(releaseId: Release['id']) {
|
||||
async findReleaseContentTypes(releaseId: Release['id']) {
|
||||
const contentTypesFromReleaseActions: { contentType: UID.ContentType }[] = await strapi.db
|
||||
.queryBuilder(RELEASE_ACTION_MODEL_UID)
|
||||
.select('content_type')
|
||||
@ -139,9 +141,10 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
||||
.groupBy('content_type')
|
||||
.execute();
|
||||
|
||||
const contentTypesUids = contentTypesFromReleaseActions.map(
|
||||
({ contentType: contentTypeUid }) => contentTypeUid
|
||||
);
|
||||
return contentTypesFromReleaseActions.map(({ contentType: contentTypeUid }) => contentTypeUid);
|
||||
},
|
||||
async findReleaseContentTypesMainFields(releaseId: Release['id']) {
|
||||
const contentTypesUids = await this.findReleaseContentTypes(releaseId);
|
||||
|
||||
const contentManagerContentTypeService = strapi
|
||||
.plugin('content-manager')
|
||||
@ -162,6 +165,90 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
||||
|
||||
return contentTypesMeta;
|
||||
},
|
||||
async publish(releaseId: PublishRelease.Request['params']['id']) {
|
||||
// We need to pass the type because entityService.findOne is not returning the correct type
|
||||
const releaseWithPopulatedActionEntries = (await strapi.entityService.findOne(
|
||||
RELEASE_MODEL_UID,
|
||||
releaseId,
|
||||
{
|
||||
populate: {
|
||||
actions: {
|
||||
populate: {
|
||||
entry: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)) as unknown as Release;
|
||||
|
||||
if (!releaseWithPopulatedActionEntries) {
|
||||
throw new errors.NotFoundError(`No release found for id ${releaseId}`);
|
||||
}
|
||||
|
||||
if (releaseWithPopulatedActionEntries.releasedAt) {
|
||||
throw new errors.ValidationError('Release already published');
|
||||
}
|
||||
|
||||
if (releaseWithPopulatedActionEntries.actions.length === 0) {
|
||||
throw new errors.ValidationError('No entries to publish');
|
||||
}
|
||||
|
||||
/**
|
||||
* We separate publish and unpublish actions group by content type
|
||||
*/
|
||||
const actions: {
|
||||
[key: UID.ContentType]: {
|
||||
publish: ReleaseAction['entry'][];
|
||||
unpublish: ReleaseAction['entry'][];
|
||||
};
|
||||
} = {};
|
||||
for (const action of releaseWithPopulatedActionEntries.actions) {
|
||||
const contentTypeUid = action.contentType;
|
||||
|
||||
if (!actions[contentTypeUid]) {
|
||||
actions[contentTypeUid] = {
|
||||
publish: [],
|
||||
unpublish: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'publish') {
|
||||
actions[contentTypeUid].publish.push(action.entry);
|
||||
} else {
|
||||
actions[contentTypeUid].unpublish.push(action.entry);
|
||||
}
|
||||
}
|
||||
|
||||
const entityManagerService = strapi.plugin('content-manager').service('entity-manager');
|
||||
|
||||
// Only publish the release if all action updates are applied successfully to their entry, otherwise leave everything as is
|
||||
await strapi.db.transaction(async () => {
|
||||
for (const contentTypeUid of Object.keys(actions)) {
|
||||
const { publish, unpublish } = actions[contentTypeUid as UID.ContentType];
|
||||
|
||||
if (publish.length > 0) {
|
||||
await entityManagerService.publishMany(publish, contentTypeUid);
|
||||
}
|
||||
|
||||
if (unpublish.length > 0) {
|
||||
await entityManagerService.unpublishMany(unpublish, contentTypeUid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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, {
|
||||
data: {
|
||||
/*
|
||||
* The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
|
||||
*/
|
||||
// @ts-expect-error see above
|
||||
releasedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return release;
|
||||
},
|
||||
async updateAction(
|
||||
actionId: UpdateReleaseAction.Request['params']['actionId'],
|
||||
releaseId: UpdateReleaseAction.Request['params']['releaseId'],
|
||||
|
@ -100,3 +100,22 @@ export declare namespace UpdateRelease {
|
||||
error?: errors.ApplicationError | errors.ValidationError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /content-releases/:releaseId/publish - Publish a release
|
||||
*/
|
||||
export declare namespace PublishRelease {
|
||||
export interface Request {
|
||||
state: {
|
||||
user: UserInfo;
|
||||
};
|
||||
params: {
|
||||
id: Release['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
data: ReleaseDataResponse;
|
||||
error?: errors.ApplicationError | errors.ValidationError;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user