Merge pull request #19778 from strapi/feat/create-many-release-actions

feat(content-releases): new create many release actions endpoint
This commit is contained in:
Fernando Chávez 2024-03-14 17:36:33 +01:00 committed by GitHub
commit e6eaa3d056
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 178 additions and 1 deletions

View File

@ -1,8 +1,10 @@
import { AlreadyOnReleaseError } from '../../services/validation';
import releaseActionController from '../release-action';
const mockSanitizedQueryRead = jest.fn().mockResolvedValue({});
const mockFindActions = jest.fn().mockResolvedValue({ results: [], pagination: {} });
const mockSanitizeOutput = jest.fn((entry: { id: number; name: string }) => ({ id: entry.id }));
const mockCreateAction = jest.fn();
jest.mock('../../utils', () => ({
getService: jest.fn(() => ({
@ -18,6 +20,7 @@ jest.mock('../../utils', () => ({
displayName: 'contentTypeB',
},
})),
createAction: mockCreateAction,
})),
getPermissionsChecker: jest.fn(() => ({
sanitizedQuery: {
@ -92,6 +95,85 @@ describe('Release Action controller', () => {
});
});
describe('createMany', () => {
beforeEach(() => {
global.strapi = {
db: {
transaction: jest.fn((cb) => cb()),
},
};
jest.clearAllMocks();
});
it('creates multiple release actions', async () => {
mockCreateAction.mockResolvedValue({ id: 1 });
const ctx: any = {
params: {
releaseId: 1,
},
request: {
body: [
{
entry: {
id: 1,
contentType: 'api::contentTypeA.contentTypeA',
},
type: 'publish',
},
{
entry: {
id: 2,
contentType: 'api::contentTypeB.contentTypeB',
},
type: 'unpublish',
},
],
},
};
await releaseActionController.createMany(ctx);
expect(mockCreateAction).toHaveBeenCalledTimes(2);
expect(ctx.body.data).toHaveLength(2);
expect(ctx.body.meta.totalEntries).toBe(2);
expect(ctx.body.meta.entriesAlreadyInRelease).toBe(0);
});
it('should count already added entries and dont throw an error', async () => {
mockCreateAction.mockRejectedValue(
new AlreadyOnReleaseError(
'Entry with id 1 and contentType api::contentTypeA.contentTypeA already exists in release with id 1'
)
);
const ctx: any = {
params: {
releaseId: 1,
},
request: {
body: [
{
entry: {
id: 1,
contentType: 'api::contentTypeA.contentTypeA',
},
type: 'publish',
},
],
},
};
await releaseActionController.createMany(ctx);
expect(mockCreateAction).toHaveBeenCalledTimes(1);
expect(ctx.body.data).toHaveLength(0);
expect(ctx.body.meta.totalEntries).toBe(1);
expect(ctx.body.meta.entriesAlreadyInRelease).toBe(1);
});
});
describe('update', () => {
it('throws an error given bad request arguments', () => {
const ctx = {

View File

@ -7,12 +7,14 @@ import {
} from './validation/release-action';
import type {
CreateReleaseAction,
CreateManyReleaseActions,
GetReleaseActions,
UpdateReleaseAction,
DeleteReleaseAction,
} from '../../../shared/contracts/release-actions';
import { getService } from '../utils';
import { RELEASE_ACTION_MODEL_UID } from '../constants';
import { AlreadyOnReleaseError } from '../services/validation';
const releaseActionController = {
async create(ctx: Koa.Context) {
@ -29,6 +31,48 @@ const releaseActionController = {
};
},
async createMany(ctx: Koa.Context) {
const releaseId: CreateManyReleaseActions.Request['params']['releaseId'] = ctx.params.releaseId;
const releaseActionsArgs: CreateManyReleaseActions.Request['body'] = ctx.request.body;
await Promise.all(
releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
);
const releaseService = getService('release', { strapi });
const releaseActions = await strapi.db.transaction(async () => {
const releaseActions = await Promise.all(
releaseActionsArgs.map(async (releaseActionArgs) => {
try {
const action = await releaseService.createAction(releaseId, releaseActionArgs);
return action;
} catch (error) {
// If the entry is already in the release, we don't want to throw an error, so we catch and ignore it
if (error instanceof AlreadyOnReleaseError) {
return null;
}
throw error;
}
})
);
return releaseActions;
});
const newReleaseActions = releaseActions.filter((action) => action !== null);
ctx.body = {
data: newReleaseActions,
meta: {
entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
totalEntries: releaseActions.length,
},
};
},
async findMany(ctx: Koa.Context) {
const releaseId: GetReleaseActions.Request['params']['releaseId'] = ctx.params.releaseId;
const permissionsManager = strapi.admin.services.permission.createPermissionsManager({

View File

@ -17,6 +17,22 @@ export default {
],
},
},
{
method: 'POST',
path: '/:releaseId/actions/bulk',
handler: 'release-action.createMany',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
config: {
actions: ['plugin::content-releases.create-action'],
},
},
],
},
},
{
method: 'GET',
path: '/:releaseId/actions',

View File

@ -5,6 +5,13 @@ import type { Release, CreateRelease, UpdateRelease } from '../../../shared/cont
import type { CreateReleaseAction } from '../../../shared/contracts/release-actions';
import { RELEASE_MODEL_UID } from '../constants';
export class AlreadyOnReleaseError extends errors.ApplicationError<'AlreadyOnReleaseError'> {
constructor(message: string) {
super(message);
this.name = 'AlreadyOnReleaseError';
}
}
const createReleaseValidationService = ({ strapi }: { strapi: LoadedStrapi }) => ({
async validateUniqueEntry(
releaseId: CreateReleaseAction.Request['params']['releaseId'],
@ -29,7 +36,7 @@ const createReleaseValidationService = ({ strapi }: { strapi: LoadedStrapi }) =>
);
if (isEntryInRelease) {
throw new errors.ValidationError(
throw new AlreadyOnReleaseError(
`Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
);
}

View File

@ -59,6 +59,34 @@ export declare namespace CreateReleaseAction {
}
}
/**
* POST /content-releases/:releaseId/actions/bulk - Create multiple release actions
*/
export declare namespace CreateManyReleaseActions {
export interface Request {
params: {
releaseId: Release['id'];
};
body: Array<{
type: ReleaseAction['type'];
entry: {
id: ReleaseActionEntry['id'];
locale?: ReleaseActionEntry['locale'];
contentType: Common.UID.ContentType;
};
}>;
}
export interface Response {
data: Array<ReleaseAction>;
meta: {
totalEntries: number;
entriesAlreadyInRelease: number;
};
error?: errors.ApplicationError | errors.ValidationError | errors.NotFoundError;
}
}
/**
* GET /content-releases/:id/actions - Get all release actions
*/