feat(content-releases): Add new scheduling service (#19414)

* feat(content-releases): Add new scheduling service

* apply remi's feedback
This commit is contained in:
Fernando Chávez 2024-02-06 10:08:59 +01:00 committed by GitHub
parent 287aae0bb4
commit 53caa296b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 187 additions and 1 deletions

View File

@ -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.

View File

@ -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"

View File

@ -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);
});
});
});

View File

@ -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 } : {}),
};

View File

@ -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;

View File

@ -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);

View File

@ -7924,6 +7924,7 @@ __metadata:
koa: "npm:2.13.4"
lodash: "npm:4.17.21"
msw: "npm:1.3.0"
node-schedule: "npm:2.1.0"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-intl: "npm:6.4.1"