Skip to content

Commit 36e50dd

Browse files
authored
feat(payload-cloud): set up cron jobs on init (#10106)
If the user has tasks configured, we set up cron jobs on init. We also make sure to only run on one instance using a instance identifier stored in a global. This adds a new property to the payloadCloudPlugin: `jobs`.
1 parent 6a262ab commit 36e50dd

File tree

4 files changed

+176
-14
lines changed

4 files changed

+176
-14
lines changed

packages/payload-cloud/src/plugin.spec.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@ import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
55
import nodemailer from 'nodemailer'
66
import { defaults } from 'payload'
77

8-
import { payloadCloudPlugin } from './plugin.js'
8+
// TO-DO: this would be needed for the TO-DO tests below.
9+
// maybe we have to use jest.unstable_mockModule? (already tried)
10+
// jest.mock('./plugin.ts', () => ({
11+
// // generateRandomString: jest.fn<() => string>().mockReturnValue('instance'),
12+
// generateRandomString: jest.fn().mockReturnValue('instance'),
13+
// }))
14+
15+
const mockedPayload: Payload = {
16+
updateGlobal: jest.fn(),
17+
findGlobal: jest.fn().mockReturnValue('instance'),
18+
} as unknown as Payload
919

10-
const mockedPayload: Payload = jest.fn() as unknown as Payload
20+
import { payloadCloudPlugin } from './plugin.js'
1121

1222
describe('plugin', () => {
1323
let createTransportSpy: jest.Spied<any>
@@ -165,6 +175,78 @@ describe('plugin', () => {
165175
})
166176
})
167177
})
178+
179+
describe('autoRun and cronJobs', () => {
180+
beforeEach(() => {
181+
process.env.PAYLOAD_CLOUD = 'true'
182+
process.env.PAYLOAD_CLOUD_EMAIL_API_KEY = 'test-key'
183+
process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN = 'test-domain.com'
184+
})
185+
186+
test('should always set global instance identifier', async () => {
187+
const plugin = payloadCloudPlugin()
188+
const config = await plugin(createConfig())
189+
190+
const globalInstance = config.globals?.find(
191+
(global) => global.slug === 'payload-cloud-instance',
192+
)
193+
194+
expect(globalInstance).toBeDefined()
195+
expect(globalInstance?.fields).toStrictEqual([
196+
{
197+
name: 'instance',
198+
type: 'text',
199+
required: true,
200+
},
201+
]),
202+
expect(globalInstance?.admin?.hidden).toStrictEqual(true)
203+
})
204+
// TO-DO: I managed to mock findGlobal, but not generateRandomString
205+
test.skip('if autoRun is not set, should return default cron job', async () => {
206+
const plugin = payloadCloudPlugin()
207+
const config = await plugin(createConfig())
208+
const DEFAULT_CRON_JOB = {
209+
cron: '* * * * *',
210+
limit: 10,
211+
queue: 'default (every minute)',
212+
}
213+
if (typeof config.jobs?.autoRun !== 'function') {
214+
throw new Error('autoRun should be a function')
215+
}
216+
const cronConfig = await config.jobs!.autoRun!(mockedPayload)
217+
expect(cronConfig).toStrictEqual([DEFAULT_CRON_JOB])
218+
})
219+
// TO-DO: I managed to mock findGlobal, but not generateRandomString
220+
// Either way when mocking the plugin part this test has little if any importance
221+
test.skip('if autoRun is a function, should return the result of the function', async () => {
222+
const plugin = payloadCloudPlugin()
223+
const config = await plugin(
224+
createConfig({
225+
jobs: {
226+
tasks: [],
227+
autoRun: async () => {
228+
return [
229+
{
230+
cron: '1 2 3 4 5',
231+
limit: 5,
232+
queue: 'test-queue',
233+
},
234+
{},
235+
]
236+
},
237+
},
238+
}),
239+
)
240+
expect(config.jobs?.autoRun).toStrictEqual([
241+
{
242+
cron: '1 2 3 4 5',
243+
limit: 5,
244+
queue: 'test-queue',
245+
},
246+
{},
247+
])
248+
})
249+
})
168250
})
169251

170252
function assertCloudStorage(config: Config) {

packages/payload-cloud/src/plugin.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Config } from 'payload'
1+
import type { Config, Payload } from 'payload'
22

33
import type { PluginOptions } from './types.js'
44

@@ -11,6 +11,11 @@ import {
1111
} from './hooks/uploadCache.js'
1212
import { getStaticHandler } from './staticHandler.js'
1313

14+
export const generateRandomString = (): string => {
15+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
16+
return Array.from({ length: 24 }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
17+
}
18+
1419
export const payloadCloudPlugin =
1520
(pluginOptions?: PluginOptions) =>
1621
async (incomingConfig: Config): Promise<Config> => {
@@ -93,5 +98,72 @@ export const payloadCloudPlugin =
9398
})
9499
}
95100

101+
// We make sure to only run cronjobs on one instance using a instance identifier stored in a global.
102+
103+
const DEFAULT_CRON = '* * * * *'
104+
const DEFAULT_LIMIT = 10
105+
const DEFAULT_CRON_JOB = {
106+
cron: DEFAULT_CRON,
107+
limit: DEFAULT_LIMIT,
108+
queue: 'default (every minute)',
109+
}
110+
config.globals = [
111+
...(config.globals || []),
112+
{
113+
slug: 'payload-cloud-instance',
114+
admin: {
115+
hidden: true,
116+
},
117+
fields: [
118+
{
119+
name: 'instance',
120+
type: 'text',
121+
required: true,
122+
},
123+
],
124+
},
125+
]
126+
127+
if (pluginOptions?.enableAutoRun === false || !config.jobs) {
128+
return config
129+
}
130+
131+
const newAutoRun = async (payload: Payload) => {
132+
const instance = generateRandomString()
133+
134+
await payload.updateGlobal({
135+
slug: 'payload-cloud-instance',
136+
data: {
137+
instance,
138+
},
139+
})
140+
141+
await waitRandomTime()
142+
143+
const cloudInstance = await payload.findGlobal({
144+
slug: 'payload-cloud-instance',
145+
})
146+
147+
if (cloudInstance.instance !== instance) {
148+
return []
149+
}
150+
if (!config.jobs?.autoRun) {
151+
return [DEFAULT_CRON_JOB]
152+
}
153+
return typeof config.jobs.autoRun === 'function'
154+
? await config.jobs.autoRun(payload)
155+
: config.jobs.autoRun
156+
}
157+
158+
config.jobs.autoRun = newAutoRun
159+
96160
return config
97161
}
162+
163+
function waitRandomTime(): Promise<void> {
164+
const min = 1000 // 1 second in milliseconds
165+
const max = 5000 // 5 seconds in milliseconds
166+
const randomTime = Math.floor(Math.random() * (max - min + 1)) + min
167+
168+
return new Promise((resolve) => setTimeout(resolve, randomTime))
169+
}

packages/payload-cloud/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ export interface PluginOptions {
6565
}
6666
| false
6767

68+
/**
69+
*
70+
* Configures whether cron jobs defined in config.jobs.autoRun will be run or not
71+
*
72+
* @default true
73+
*/
74+
enableAutoRun?: boolean
75+
6876
/**
6977
* Payload Cloud API endpoint
7078
*

pnpm-lock.yaml

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)