diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a80b7a2..7c60a30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: - node-version: '14.x' + node-version: '16.x' registry-url: 'https://registry.npmjs.org' - run: yarn install - run: yarn build diff --git a/README.md b/README.md index eba69fa..e1cccd4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Payload Action Scheduler (in development) +# Payload Action Scheduler (in alpha) Add scheduled tasks to your Payload app effortlessly. Whether you need to postpone a task to a specific future time, set up periodic tasks, or offload tasks to run in the background, Payload Action Scheduler has you covered. With this plugin, you can monitor your task queues, see execution results, and track execution timestamps seamlessly. ![scheduled-actions-view.png](readme/scheduled-actions-view.png) diff --git a/dev/src/payload.config.ts b/dev/src/payload.config.ts index 57dbf9b..1feb469 100644 --- a/dev/src/payload.config.ts +++ b/dev/src/payload.config.ts @@ -19,6 +19,19 @@ export default buildConfig({ plugins: [actionScheduler({ enabled: true, actions: [ + { + endpoint: 'test', + async handler(payload, args) { + console.log('Running test action', args, typeof args) + return 'some message in return'; + } + }, + { + endpoint: 'test2', + handler: async (payload, args) => { + console.log('Running test2 action', args, typeof args) + } + }, { endpoint: 'long-running-action', handler: async (payload, args) => { @@ -26,7 +39,18 @@ export default buildConfig({ await new Promise(resolve => setTimeout(resolve, 25000)); } } - ] + ], + errorHooks: [ + async ({ payload, action, message, code }) => { + console.error('ERROR LOG 1:', { action, message, code }) + }, + async ({ payload, action, message, code }) => { + console.error('ERROR LOG 2:', { action, message, code }) + } + ], + debug: true, + timeoutSeconds: 10, + runtime: 'serverless', })], graphQL: { disable: true, diff --git a/src/collections/ScheduledActions.ts b/src/collections/ScheduledActions.ts index 2383300..51057c0 100644 --- a/src/collections/ScheduledActions.ts +++ b/src/collections/ScheduledActions.ts @@ -9,7 +9,7 @@ import { RecurringCell, } from '../components/Cells' import {parseExpression} from 'cron-parser' -import {AddActionType, PluginTypes} from "../types"; +import {AddActionType, PluginTypes, ScheduledAction} from "../types"; import {MAX_NODE_TIMEOUT_SECONDS, pluginDefaults} from "../defaults"; import {sortObjectKeys} from "../helpers"; import { @@ -22,7 +22,7 @@ import { import {stringifyDiff} from "../helpers/time-server"; import {GeneratedTypes} from "payload"; -export const SCHEDULED_ACTIONS_COLLECTION_SLUG = 'scheduled-actions' // do not put in config to change the collection slug +export const SCHEDULED_ACTIONS_COLLECTION_SLUG = 'scheduled-actions' as const // do not put in config to change the collection slug export const SCHEDULED_ACTIONS_ENDPOINT = '/run' // export const RECURRING_INTERVAL_SECONDS = 60 // not needed @@ -40,6 +40,7 @@ export const ScheduledActions = (pluginConfig: PluginTypes) : CollectionConfig = timeoutSeconds: timeoutSecondsConst, errorHooks, apiURL, + collectionGroup, } = { ...pluginDefaults, ...pluginConfig @@ -59,7 +60,7 @@ export const ScheduledActions = (pluginConfig: PluginTypes) : CollectionConfig = slug: SCHEDULED_ACTIONS_COLLECTION_SLUG, access: { create: createAccess, - delete: () => false, + delete: () => true, update: () => false, read: readAccess, }, @@ -76,6 +77,7 @@ export const ScheduledActions = (pluginConfig: PluginTypes) : CollectionConfig = components: { BeforeListTable: [BeforeScheduledActionsList], }, + group: collectionGroup }, disableDuplicate: true, hooks: { @@ -107,7 +109,6 @@ export const ScheduledActions = (pluginConfig: PluginTypes) : CollectionConfig = method: 'get', async handler(request) { const {payload} = request - const startTime = new Date() let duration: number let totalDocs = 0 @@ -130,7 +131,7 @@ export const ScheduledActions = (pluginConfig: PluginTypes) : CollectionConfig = successCount = await processActionsQueue({ payload: payload, - actions: results.docs as GeneratedTypes["collections"]["scheduled-actions"][], + actions: results.docs as ScheduledAction[], actionHandlers: actions!, timeoutSeconds: timeoutSeconds, errorHooks: errorHooks!, diff --git a/src/defaults.ts b/src/defaults.ts index 227eab3..442300e 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -12,4 +12,5 @@ export const pluginDefaults : PluginTypes = { errorHooks: [], enabled: true, apiURL: '/api', + collectionGroup: 'Collections' } diff --git a/src/newCollection.ts b/src/newCollection.ts deleted file mode 100644 index f2e985f..0000000 --- a/src/newCollection.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CollectionConfig } from 'payload/types'; - -const newCollection: CollectionConfig = { - slug: 'new-collection', - admin: { - useAsTitle: 'title', - }, - fields: [ - { - name: 'title', - type: 'text', - }, - ], -} - -export default newCollection; diff --git a/src/plugin.ts b/src/plugin.ts index 872992c..53d7520 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -21,7 +21,7 @@ export const actionScheduler = config.collections = [ ...(config.collections || []), // Add additional collections here - ScheduledActions(config), + ScheduledActions(pluginOptions), ] config.globals = [ diff --git a/src/types.ts b/src/types.ts index b0f1647..1a3a332 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,11 +51,17 @@ export interface PluginTypes { * @default '/api' */ apiURL?: string, + + /** + * Collection group admin dashboard for the scheduled actions + * @default 'Collections' + */ + collectionGroup?: string, } export interface ErrorLogFunctionArgs{ payload: Payload, - action: GeneratedTypes["collections"]["scheduled-actions"], + action: ScheduledAction, message: string, code?: number, } @@ -75,10 +81,11 @@ export type AddActionType = { scheduledAt?: Date, cronExpression?: string, priority?: number, + payload: Payload } -export type GetActionType = AddActionType & { - status?: GeneratedTypes["collections"]["scheduled-actions"]['status'], +export type GetActionType = Omit & { + status?: ScheduledAction['status'], } export type ActionDefinition = { @@ -88,7 +95,7 @@ export type ActionDefinition = { export interface RunActionArgs { payload: BasePayload; - action: GeneratedTypes["collections"]["scheduled-actions"]; + action: ScheduledAction; timeoutSeconds: number; actionHandlers: ActionDefinition[]; errorHooks: ((args: ErrorLogFunctionArgs) => Promise)[]; diff --git a/src/utilities/scheduled-actions.ts b/src/utilities/scheduled-actions.ts index 36233e3..a8a6838 100644 --- a/src/utilities/scheduled-actions.ts +++ b/src/utilities/scheduled-actions.ts @@ -3,12 +3,11 @@ import { AddActionType, GetActionType, ErrorLogFunctionArgs, - RunActionArgs, TimeoutError + RunActionArgs, TimeoutError, ScheduledAction } from "../types"; import {parseExpression} from "cron-parser"; import {SCHEDULED_ACTIONS_COLLECTION_SLUG} from "../collections/ScheduledActions"; import {normalizeJSONString, sortObjectKeys} from "../helpers"; -import {PaginatedDocs} from "payload/database"; import {stringifyDiff} from "../helpers/time-server"; import {generateSignature} from "./security"; @@ -28,7 +27,7 @@ export const addAction = async (props: AddActionType) => { log, } = action - const payload = await getPayload({config: configPromise}) + const {payload} = props; const isScheduled = await isActionScheduled(payload, endpoint, scheduledDateTime, 'pending') if (isScheduled) @@ -106,7 +105,7 @@ export const constructNewAction = (props: AddActionType) => { } -export const isActionScheduled = async (payload: BasePayload, endpoint: string, scheduledAt?: Date | string, status?: GeneratedTypes["collections"]["scheduled-actions"]['status']) => { +export const isActionScheduled = async (payload: BasePayload, endpoint: string, scheduledAt?: Date | string, status?: ScheduledAction['status']) => { if (typeof scheduledAt === 'string') { scheduledAt = new Date(scheduledAt) @@ -206,7 +205,7 @@ export const processActionsQueue = async ({ errorHooks, apiURL }: Omit & { - actions: GeneratedTypes["collections"]["scheduled-actions"][] + actions: ScheduledAction[] }) => { if (actions.length === 0) { return 0 @@ -240,7 +239,7 @@ export const processActionsQueue = async ({ } } -export const updateActionsToRunningStatus = async (payload: BasePayload, actions: GeneratedTypes["collections"]["scheduled-actions"][]) => { +export const updateActionsToRunningStatus = async (payload: BasePayload, actions: ScheduledAction[]) => { await payload.update({ collection: SCHEDULED_ACTIONS_COLLECTION_SLUG, where: { @@ -268,13 +267,11 @@ export const runAction = async ({ // first determine the action let actionPromise: Promise - // @ts-ignore if (action.endpoint.startsWith('@')) { let registeredAction = actionHandlers.find(a => a.endpoint === action.endpoint) - // @ts-ignore + if (!registeredAction && action.endpoint.startsWith('@')) { - // @ts-ignore registeredAction = actionHandlers.find(a => a.endpoint === action.endpoint.slice(1)) } @@ -290,11 +287,9 @@ export const runAction = async ({ return Promise.reject(new Error(`Action ${action.endpoint} is not a function`)) } - // @ts-ignore actionPromise = registeredAction.handler(payload, action.args) } else { - // @ts-ignore const endPoint: string = action.endpoint.startsWith('http') ? action.endpoint : `${apiURL}${action.endpoint}` actionPromise = fetch(endPoint, { @@ -327,7 +322,7 @@ export const runAction = async ({ throw error }) - let status: GeneratedTypes["collections"]["scheduled-actions"]['status'] = 'failed' + let status: ScheduledAction['status'] = 'failed' let message: string = 'Unknown error' let code: number = 500 @@ -371,7 +366,7 @@ export const runAction = async ({ } } -const logStartAction = (action: GeneratedTypes["collections"]["scheduled-actions"]) => { +const logStartAction = (action: ScheduledAction) => { // @ts-ignore action.log?.push({ date: new Date().toISOString(), @@ -382,8 +377,8 @@ const logStartAction = (action: GeneratedTypes["collections"]["scheduled-actions const actionCleanup = async ( payload: BasePayload, errorHooks: ((args: ErrorLogFunctionArgs) => Promise)[], - action: GeneratedTypes["collections"]["scheduled-actions"], - status: GeneratedTypes["collections"]["scheduled-actions"]['status'], + action: ScheduledAction, + status: ScheduledAction['status'], message: string, code?: number, ) => { @@ -402,8 +397,8 @@ const actionCleanup = async ( export const logAction = async ( payload: BasePayload, - action: GeneratedTypes["collections"]["scheduled-actions"], - status: GeneratedTypes["collections"]["scheduled-actions"]["status"], + action: ScheduledAction, + status: ScheduledAction["status"], message: string, code?: number, calculateTimeDiff = false @@ -454,7 +449,7 @@ export const logAction = async ( const executeErrorLog = async ( payload: BasePayload, errorHooks: ((args: ErrorLogFunctionArgs) => Promise)[], - action: GeneratedTypes["collections"]["scheduled-actions"], + action: ScheduledAction, message: string, code?: number ) => { @@ -470,7 +465,7 @@ const executeErrorLog = async ( } -const rescheduleAction = async (payload: BasePayload, action: GeneratedTypes["collections"]["scheduled-actions"]) => { +const rescheduleAction = async (payload: BasePayload, action: ScheduledAction) => { if (!action.cronExpression) { throw new Error('Cannot reschedule an action without a cron expression')