Skip to content

Commit 7970282

Browse files
authored
feat: schedule globals (#10)
* feat: schedule globals * fix: update `publishScheduledPost` for globals * update readme * fix test
1 parent 22786a7 commit 7970282

11 files changed

+197
-65
lines changed

README.md

+13-4
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ import { buildConfig } from 'payload/config'
1919
import { ScheduledPostPlugin } from 'payload-plugin-scheduler'
2020
import Pages from './collections/Pages'
2121
import Posts from './collections/Posts'
22+
import Home from './globals/Home'
2223

2324
export default buildConfig({
2425
collections: [Pages, Posts],
26+
globals: [Home],
2527
plugins: [
2628
ScheduledPostPlugin({
2729
collections: ['pages', 'posts'],
30+
globals: ['home'],
2831
interval: 10,
2932
})
3033
]
@@ -35,10 +38,16 @@ export default buildConfig({
3538

3639
## Options
3740

38-
### `collections: string[]`
41+
At least one collection / global is required.
42+
43+
### `collections?: string[]`
3944

4045
An array of collection slugs. All collections must have drafts enabled.
4146

47+
### `globals?: string[]`
48+
49+
An array of global slugs. All globals must have drafts enabled.
50+
4251
### `interval?: number`
4352

4453
Specify how frequently to check for scheduled posts (in minutes).
@@ -68,8 +77,8 @@ Collection added by the plugin to store pending schedule updates. Can be customi
6877
A configurable timer checks for any posts to be scheduled in the upcoming interval window. For each hit, it creates a separate job that's fired at that document's `publish_date` (via [node-schedule](https://github.com/node-schedule/node-schedule)). The idea here is that you can configure your interval window to avoid super long running tasks that are more prone to flaking.
6978

7079

71-
## Notes
80+
## Caveats
7281

73-
Since the plugin uses cron under the hood, it depends on a long-running server and is incompatible with short-lived/serverless environments like ECS, or Vercel if you're using Payload 3.0 beta.
82+
* This plugin doesn't support Payload 3.0 beta. I intend to update it once 3.0 is stable, but it'll require substantial re-architecting to work in a serverless environment.
7483

75-
I developed this plugin for a project that hasn't gone live yet. It has good test coverage but not in the wild yet -- there's your disclaimer.
84+
* There's no logic in place to dedupe schedules across multiple instances of a single app (see https://github.com/wkentdag/payload-plugin-scheduler/issues/9)

dev/src/globals/Home.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { GlobalConfig } from "payload/types";
2+
3+
const Home: GlobalConfig = {
4+
slug: 'home',
5+
versions: {
6+
drafts: true,
7+
},
8+
fields: [
9+
{
10+
name: 'title',
11+
type: 'text',
12+
}
13+
]
14+
15+
}
16+
17+
export default Home

dev/src/payload.base.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import PagesWithExtraHooks from "./collections/PagesWithExtraHooks";
1010

1111
// @ts-expect-error
1212
import { ScheduledPostPlugin } from '../../src'
13+
import Home from "./globals/Home";
1314

1415
export const INTERVAL = 1
1516

@@ -35,6 +36,7 @@ export const baseConfig: Omit<Config, 'db'> = {
3536
},
3637
editor: slateEditor({}),
3738
collections: [Pages, PagesWithExtraHooks, Posts, Users],
39+
globals: [Home],
3840
typescript: {
3941
outputFile: path.resolve(__dirname, 'payload-types.ts'),
4042
},
@@ -44,6 +46,7 @@ export const baseConfig: Omit<Config, 'db'> = {
4446
plugins: [
4547
(ScheduledPostPlugin({
4648
collections: ['pages', 'posts', 'pageswithextrahooks'],
49+
globals: ['home'],
4750
interval: INTERVAL,
4851
scheduledPosts: {
4952
admin: {

dev/test/plugin.spec.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('Plugin tests', () => {
2727
},
2828
})
2929

30-
it('schedules posts', async () => {
30+
it('schedules collection docs', async () => {
3131
const pubDate = addMinutes(new Date(), 2).toISOString()
3232
const doc = await payload.create({
3333
collection: 'posts',
@@ -51,6 +51,37 @@ describe('Plugin tests', () => {
5151
expect(schedule.status).toBe('queued')
5252
})
5353

54+
it('schedules global docs', async () => {
55+
const pubDate = addMinutes(new Date(), 2).toISOString()
56+
const doc = await payload.updateGlobal({
57+
slug: 'home',
58+
data: {
59+
title: 'hello world',
60+
publish_date: pubDate,
61+
_status: 'draft',
62+
},
63+
})
64+
65+
expect(doc.publish_date).toBe(pubDate)
66+
expect(doc._status).toBe('draft')
67+
68+
const {
69+
totalDocs,
70+
docs: [schedule],
71+
} = await payload.find({
72+
collection: 'scheduled_posts',
73+
where: {
74+
global: {
75+
equals: 'home'
76+
}
77+
}
78+
})
79+
80+
expect(totalDocs).toBe(1)
81+
expect(schedule.date).toBe(doc.publish_date)
82+
expect(schedule.status).toBe('queued')
83+
})
84+
5485
it('bounds publish_date', async () => {
5586
const now = new Date()
5687
const futureDate = addMinutes(now, 10)

src/collections/ScheduledPosts.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@ const ScheduledPosts = (scheduleConfig: ScheduledPostConfig): CollectionConfig =
2222
name: 'post',
2323
type: 'relationship',
2424
unique: true,
25-
required: true,
26-
relationTo: [...scheduleConfig.collections],
25+
relationTo: [...(scheduleConfig.collections || [])],
2726
hasMany: false,
2827
admin: {
2928
readOnly: true,
3029
},
3130
},
31+
{
32+
name: 'global',
33+
type: 'select',
34+
options: [...(scheduleConfig.globals || [])],
35+
unique: true,
36+
},
3237
{
3338
name: 'date',
3439
index: true,

src/hooks/boundPublishDate.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { CollectionBeforeChangeHook } from 'payload/types'
1+
import type { CollectionBeforeChangeHook, GlobalBeforeChangeHook } from 'payload/types'
22
import type { ScheduledPostConfig } from '../types'
33

44
export default function boundPublishDate(
55
// eslint-disable-next-line @typescript-eslint/no-unused-vars
66
scheduleConfig: ScheduledPostConfig,
7-
): CollectionBeforeChangeHook {
8-
return ({ data }) => {
7+
): CollectionBeforeChangeHook | GlobalBeforeChangeHook {
8+
return ({ data }: { data: any }) => {
99
// eslint-disable-next-line no-underscore-dangle
1010
const isPublishing = data?._status === 'published'
1111
const pubDate = data?.publish_date ? new Date(data.publish_date) : undefined

src/hooks/syncSchedule.ts

+57-34
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,55 @@
1-
import { type CollectionAfterChangeHook } from 'payload/types'
1+
import type { GlobalAfterChangeHook, Where, CollectionAfterChangeHook } from 'payload/types'
22
import { type ScheduledPostConfig } from '../types'
33
import { debug } from '../util'
44

5+
type GlobalArgs = Parameters<GlobalAfterChangeHook>[0]
6+
type CollectionArgs = Parameters<CollectionAfterChangeHook>[0]
7+
8+
function isGlobal(args: CollectionArgs| GlobalArgs): args is GlobalArgs {
9+
return (args as GlobalArgs).global !== undefined
10+
}
11+
512
export default function syncSchedule(
613
// eslint-disable-next-line @typescript-eslint/no-unused-vars
714
scheduleConfig: ScheduledPostConfig,
8-
): CollectionAfterChangeHook {
9-
return async ({ collection, doc, previousDoc, req }) => {
15+
): CollectionAfterChangeHook | GlobalAfterChangeHook {
16+
return async (args: GlobalArgs | CollectionArgs) => {
17+
const { doc, previousDoc, req } = args
18+
const slug = isGlobal(args) ? args.global.slug : args.collection.slug
19+
1020
const { payload } = req
11-
debug(`syncSchedule ${collection.slug} ${doc.id}`)
21+
debug(`syncSchedule ${slug} ${doc.id}`)
1222
// eslint-disable-next-line no-underscore-dangle
1323
const isPublishing = doc._status === 'published'
1424
const publishInFuture = doc?.publish_date && new Date(doc.publish_date) > new Date()
1525
const scheduleChanged = doc?.publish_date !== previousDoc?.publish_date
1626
try {
1727
if (isPublishing || scheduleChanged) {
1828
debug('Deleting previous schedule')
19-
// if `publish_date` is modified, remove any pending schedulers.
29+
// if `publish_date` is modified, or the post is being published, remove any pending schedulers.
2030
// there should only ever be a single result here in practice
21-
const deleted = await payload.delete({
22-
collection: 'scheduled_posts',
23-
where: {
24-
and: [
25-
{
26-
'post.value': {
27-
equals: doc.id,
28-
},
31+
const whereClause: Where = isGlobal(args) ? {
32+
global: {
33+
equals: slug,
34+
}
35+
} : {
36+
and: [
37+
{
38+
'post.value': {
39+
equals: doc.id,
2940
},
30-
{
31-
'post.relationTo': {
32-
equals: collection.slug,
33-
},
41+
},
42+
{
43+
'post.relationTo': {
44+
equals: slug,
3445
},
35-
],
36-
},
46+
},
47+
],
48+
}
49+
50+
const deleted = await payload.delete({
51+
collection: 'scheduled_posts',
52+
where: whereClause,
3753
req,
3854
})
3955

@@ -48,29 +64,36 @@ export default function syncSchedule(
4864

4965
// if the publish date has changed and it's in the future, schedule it
5066
if (scheduleChanged && publishInFuture) {
51-
debug('Scheduling post', collection.slug, doc.id)
52-
let dbValue = doc.id
67+
debug('Scheduling post', slug, doc.id)
68+
const data: Record<string, any> = {
69+
date: doc.publish_date,
70+
status: 'queued',
71+
}
5372

54-
// nb without this payload will throw a ValidationError
55-
// seems like a bug
56-
if (payload.db.defaultIDType === 'number') {
57-
dbValue = Number(doc.id)
73+
if (isGlobal(args)) {
74+
data.global = slug
75+
} else {
76+
let dbValue = doc.id
77+
78+
// nb without this payload will throw a ValidationError
79+
// seems like a bug
80+
if (payload.db.defaultIDType === 'number') {
81+
dbValue = Number(doc.id)
82+
}
83+
84+
data.post = {
85+
value: dbValue,
86+
relationTo: slug,
87+
}
5888
}
5989

6090
const res = await payload.create({
6191
collection: 'scheduled_posts',
62-
data: {
63-
post: {
64-
value: dbValue,
65-
relationTo: collection.slug,
66-
},
67-
date: doc.publish_date,
68-
status: 'queued',
69-
},
92+
data,
7093
req,
7194
})
7295

73-
debug(`scheduled ${collection.slug}:${dbValue} ${res.id}`)
96+
debug(`scheduled ${slug}:${doc.id} ${res.id}`)
7497
}
7598
} catch (error: unknown) {
7699
payload.logger.error('[payload-plugin-scheduler] Error scheduling post')

src/init.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const onInit = (config: ScheduledPostConfig, payload: Payload): Job => {
2626
// scheduled to fire at its publish_date
2727
await Promise.all(
2828
queued.map(async schedule => {
29-
const { date, post } = schedule
29+
const { date, post, global } = schedule
3030
const id = schedule.id.toString()
3131
// overwrite any existing job for this same document
3232
if (Object.keys(scheduledJobs).includes(id)) {
@@ -37,7 +37,7 @@ export const onInit = (config: ScheduledPostConfig, payload: Payload): Job => {
3737
}
3838
}
3939

40-
const job = new Job(id, publishScheduledPost({ post }, payload))
40+
const job = new Job(id, publishScheduledPost({ post, global }, payload))
4141

4242
const scheduled = job.schedule(date)
4343

src/lib.ts

+21-13
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,34 @@ export async function getUpcomingPosts(
4242
}
4343

4444
export function publishScheduledPost(
45-
{ post }: Pick<ScheduledPost, 'post'>,
45+
{ post, global }: Pick<ScheduledPost, 'post' | 'global'>,
4646
payload: Payload,
4747
): JobCallback {
4848
return async () => {
49-
payload.logger.info(`Publishing ${post.relationTo} ${post.value}`)
50-
debug(`Publishing ${post.relationTo} ${post.value}`)
49+
const tag = global || `${post!.relationTo} ${post!.value}`
50+
payload.logger.info(`Publishing ${tag}`)
51+
debug(`Publishing ${tag}`)
52+
53+
const updateOp = (): ReturnType<typeof payload.updateGlobal> => global ? payload.updateGlobal({
54+
slug: global,
55+
data: {
56+
_status: 'published'
57+
}
58+
}) : payload.update({
59+
id: post!.value,
60+
collection: post!.relationTo,
61+
data: {
62+
_status: 'published',
63+
},
64+
})
5165

5266
try {
53-
await payload.update({
54-
id: post.value,
55-
collection: post.relationTo,
56-
data: {
57-
_status: 'published',
58-
},
59-
})
60-
payload.logger.info(`[payload-plugin-scheduler] Published ${post.relationTo} ${post.value}`)
67+
await updateOp()
68+
payload.logger.info(`[payload-plugin-scheduler] Published ${tag}`)
6169
} catch (error: unknown) {
62-
debug(`Error publishing ${post.relationTo} ${post.value} ${error?.toString()}`)
70+
debug(`Error publishing ${tag} ${error?.toString()}`)
6371
payload.logger.error(error,
64-
`[payload-plugin-scheduler] Failed to publish ${post.relationTo} ${post.value}`
72+
`[payload-plugin-scheduler] Failed to publish ${tag}`
6573
)
6674
}
6775
}

0 commit comments

Comments
 (0)