mirror of
https://github.com/strapi/strapi.git
synced 2025-12-28 15:44:59 +00:00
feat(content-releases): Add new scheduling service (#19414)
* feat(content-releases): Add new scheduling service * apply remi's feedback
This commit is contained in:
parent
287aae0bb4
commit
53caa296b5
@ -185,6 +185,18 @@ Exposes validation functions to run before performing operations on a Release
|
||||
packages/core/content-releases/server/src/services/validation.ts
|
||||
```
|
||||
|
||||
### Scheduling
|
||||
|
||||
:::caution
|
||||
Scheduling is still under development, but you can try it **at your own risk** with future flags. The future flag to enable scheduling is `contentReleasesScheduling`.
|
||||
:::
|
||||
|
||||
Exposes methods to schedule release date for releases.
|
||||
|
||||
```
|
||||
packages/core/content-releases/server/src/services/scheduling.ts
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
We have two migrations that we run every time we sync the content types.
|
||||
|
||||
@ -63,6 +63,7 @@
|
||||
"axios": "1.6.0",
|
||||
"formik": "2.4.0",
|
||||
"lodash": "4.17.21",
|
||||
"node-schedule": "2.1.0",
|
||||
"react-intl": "6.4.1",
|
||||
"react-redux": "8.1.1",
|
||||
"yup": "0.32.9"
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
import { scheduleJob } from 'node-schedule';
|
||||
import createSchedulingService from '../scheduling';
|
||||
|
||||
const baseStrapiMock = {
|
||||
features: {
|
||||
future: {
|
||||
isEnabled: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('node-schedule', () => ({
|
||||
scheduleJob: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Scheduling service', () => {
|
||||
describe('set', () => {
|
||||
it('should throw an error if the release does not exist', async () => {
|
||||
const strapiMock = {
|
||||
...baseStrapiMock,
|
||||
db: {
|
||||
query: jest.fn(() => ({
|
||||
findOne: jest.fn().mockReturnValue(null),
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const schedulingService = createSchedulingService({ strapi: strapiMock });
|
||||
expect(() => schedulingService.set('1', new Date())).rejects.toThrow(
|
||||
'No release found for id 1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should cancel the previous job if it exists and create the new one', async () => {
|
||||
const mockScheduleJob = jest.fn().mockReturnValue({ cancel: jest.fn() });
|
||||
// @ts-expect-error - scheduleJob is a mock
|
||||
scheduleJob.mockImplementation(mockScheduleJob);
|
||||
|
||||
const strapiMock = {
|
||||
...baseStrapiMock,
|
||||
db: {
|
||||
query: jest.fn(() => ({
|
||||
findOne: jest.fn().mockReturnValue({ id: 1 }),
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
const oldJobDate = new Date();
|
||||
const newJobDate = new Date(oldJobDate.getTime() + 1000);
|
||||
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const schedulingService = createSchedulingService({ strapi: strapiMock });
|
||||
const scheduledJobs = await schedulingService.set('1', oldJobDate);
|
||||
expect(scheduledJobs.size).toBe(1);
|
||||
expect(mockScheduleJob).toHaveBeenCalledWith(oldJobDate, expect.any(Function));
|
||||
|
||||
const oldJob = scheduledJobs.get('1')!;
|
||||
|
||||
await schedulingService.set('1', newJobDate);
|
||||
|
||||
expect(oldJob.cancel).toHaveBeenCalled();
|
||||
expect(mockScheduleJob).toHaveBeenCalledWith(newJobDate, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should create a new job', async () => {
|
||||
const mockScheduleJob = jest.fn().mockReturnValue({ cancel: jest.fn() });
|
||||
// @ts-expect-error - scheduleJob is a mock
|
||||
scheduleJob.mockImplementation(mockScheduleJob);
|
||||
|
||||
const strapiMock = {
|
||||
...baseStrapiMock,
|
||||
db: {
|
||||
query: jest.fn(() => ({
|
||||
findOne: jest.fn().mockReturnValue({ id: 1 }),
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
const date = new Date();
|
||||
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const schedulingService = createSchedulingService({ strapi: strapiMock });
|
||||
const scheduledJobs = await schedulingService.set('1', date);
|
||||
expect(scheduledJobs.size).toBe(1);
|
||||
expect(mockScheduleJob).toHaveBeenCalledWith(date, expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', () => {
|
||||
it('should cancel the job if it exists', async () => {
|
||||
const mockScheduleJob = jest.fn().mockReturnValue({ cancel: jest.fn() });
|
||||
// @ts-expect-error - scheduleJob is a mock
|
||||
scheduleJob.mockImplementation(mockScheduleJob);
|
||||
|
||||
const strapiMock = {
|
||||
...baseStrapiMock,
|
||||
db: {
|
||||
query: jest.fn(() => ({
|
||||
findOne: jest.fn().mockReturnValue({ id: 1 }),
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
const date = new Date();
|
||||
|
||||
// @ts-expect-error Ignore missing properties
|
||||
const schedulingService = createSchedulingService({ strapi: strapiMock });
|
||||
const scheduledJobs = await schedulingService.set('1', date);
|
||||
expect(scheduledJobs.size).toBe(1);
|
||||
expect(mockScheduleJob).toHaveBeenCalledWith(date, expect.any(Function));
|
||||
|
||||
schedulingService.cancel('1');
|
||||
expect(scheduledJobs.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,9 @@
|
||||
import release from './release';
|
||||
import releaseValidation from './validation';
|
||||
import scheduling from './scheduling';
|
||||
|
||||
export const services = {
|
||||
release,
|
||||
'release-validation': releaseValidation,
|
||||
...(strapi.features.future.isEnabled('contentReleasesScheduling') ? { scheduling } : {}),
|
||||
};
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { scheduleJob, Job } from 'node-schedule';
|
||||
import { LoadedStrapi } from '@strapi/types';
|
||||
|
||||
import { errors } from '@strapi/utils';
|
||||
import { Release } from '../../../shared/contracts/releases';
|
||||
import { getService } from '../utils';
|
||||
import { RELEASE_MODEL_UID } from '../constants';
|
||||
|
||||
const createSchedulingService = ({ strapi }: { strapi: LoadedStrapi }) => {
|
||||
const scheduledJobs = new Map<Release['id'], Job>();
|
||||
|
||||
return {
|
||||
async set(releaseId: Release['id'], scheduleDate: Date) {
|
||||
const release = await strapi.db
|
||||
.query(RELEASE_MODEL_UID)
|
||||
.findOne({ where: { id: releaseId, releasedAt: null } });
|
||||
|
||||
if (!release) {
|
||||
throw new errors.NotFoundError(`No release found for id ${releaseId}`);
|
||||
}
|
||||
|
||||
const job = scheduleJob(scheduleDate, async () => {
|
||||
try {
|
||||
await getService('release').publish(releaseId);
|
||||
// @TODO: Trigger webhook with success message
|
||||
} catch (error) {
|
||||
// @TODO: Trigger webhook with error message
|
||||
}
|
||||
|
||||
this.cancel(releaseId);
|
||||
});
|
||||
|
||||
if (scheduledJobs.has(releaseId)) {
|
||||
this.cancel(releaseId);
|
||||
}
|
||||
|
||||
scheduledJobs.set(releaseId, job);
|
||||
|
||||
return scheduledJobs;
|
||||
},
|
||||
|
||||
cancel(releaseId: Release['id']) {
|
||||
if (scheduledJobs.has(releaseId)) {
|
||||
scheduledJobs.get(releaseId)!.cancel();
|
||||
scheduledJobs.delete(releaseId);
|
||||
}
|
||||
|
||||
return scheduledJobs;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default createSchedulingService;
|
||||
@ -1,5 +1,5 @@
|
||||
export const getService = (
|
||||
name: 'release' | 'release-validation' | 'release-action' | 'event-manager',
|
||||
name: 'release' | 'release-validation' | 'scheduling' | 'release-action' | 'event-manager',
|
||||
{ strapi } = { strapi: global.strapi }
|
||||
) => {
|
||||
return strapi.plugin('content-releases').service(name);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user