From e204d1241ae24be49d91924ae5d216a1040e5630 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Mon, 8 Sep 2025 20:54:49 +0200 Subject: [PATCH] Refactor trigger helpers to single simplified function - Replace multiple helper functions with single createTriggerField function - createTriggerField takes a standard PayloadCMS field and adds virtual storage hooks - Simplify trigger presets to use the new createTrigger helper - Update exports to match new simplified API - Cleaner, more maintainable code with less boilerplate --- src/exports/helpers.ts | 39 +++---- src/utils/trigger-helpers.ts | 219 +++++++++++++++++------------------ src/utils/trigger-presets.ts | 197 +++++++++++++++---------------- 3 files changed, 221 insertions(+), 234 deletions(-) diff --git a/src/exports/helpers.ts b/src/exports/helpers.ts index f1adf45..ca3720c 100644 --- a/src/exports/helpers.ts +++ b/src/exports/helpers.ts @@ -3,31 +3,29 @@ * * @example * ```typescript - * import { createTrigger, webhookTrigger } from '@xtr-dev/payload-automation/helpers' + * import { createTrigger, createTriggerField, webhookTrigger } from '@xtr-dev/payload-automation/helpers' * - * // Simple trigger - * const myTrigger = createTrigger('my-trigger').parameters({ - * apiKey: { type: 'text', required: true }, - * timeout: { type: 'number', defaultValue: 30 } - * }) + * // Simple trigger with array of fields + * const myTrigger = createTrigger('my-trigger', [ + * { name: 'apiKey', type: 'text', required: true }, + * { name: 'timeout', type: 'number', defaultValue: 30 } + * ]) * - * // Webhook trigger with presets + * // Single field with virtual storage + * const field = createTriggerField( + * { name: 'webhookUrl', type: 'text', required: true }, + * 'my-trigger' + * ) + * + * // Webhook trigger preset * const orderWebhook = webhookTrigger('order-webhook') - * .parameter('orderTypes', { - * type: 'select', - * hasMany: true, - * options: ['regular', 'subscription'] - * }) - * .build() * ``` */ // Core helpers export { - createTriggerParameter, - createTriggerParameters, - createTrigger, - createAdvancedTrigger + createTriggerField, + createTrigger } from '../utils/trigger-helpers.js' // Preset builders @@ -37,11 +35,4 @@ export { eventTrigger, manualTrigger, apiTrigger -} from '../utils/trigger-presets.js' - -// Common parameter sets for extending -export { - webhookParameters, - cronParameters, - eventParameters } from '../utils/trigger-presets.js' \ No newline at end of file diff --git a/src/utils/trigger-helpers.ts b/src/utils/trigger-helpers.ts index 2d34486..dfff1b1 100644 --- a/src/utils/trigger-helpers.ts +++ b/src/utils/trigger-helpers.ts @@ -2,137 +2,132 @@ import type { Field } from 'payload' import type { CustomTriggerConfig } from '../plugin/config-types.js' /** - * Helper function to create a virtual trigger parameter field - * Handles the boilerplate for storing/reading from the parameters JSON field + * Creates a virtual field for a trigger parameter that stores its value in the parameters JSON field + * + * @param field - Standard PayloadCMS field configuration (must be a data field with a name) + * @param triggerSlug - The slug of the trigger this field belongs to + * @returns Modified field with virtual storage hooks and proper naming + * + * @example + * ```typescript + * const myTrigger: CustomTriggerConfig = { + * slug: 'my-trigger', + * inputs: [ + * createTriggerField({ + * name: 'webhookUrl', + * type: 'text', + * required: true, + * admin: { + * description: 'URL to call when triggered' + * } + * }, 'my-trigger') + * ] + * } + * ``` */ -export function createTriggerParameter( - name: string, - fieldConfig: any, // Use any to allow flexible field configurations - triggerSlug: string -): Field { +export function createTriggerField(field: any, triggerSlug: string): Field { + const originalName = field.name + if (!originalName) { + throw new Error('Field must have a name property') + } + // Create a unique field name by prefixing with trigger slug - const uniqueFieldName = `__trigger_${triggerSlug}_${name}` + const uniqueFieldName = `__trigger_${triggerSlug}_${originalName}` - return { - ...fieldConfig, + const resultField: any = { + ...field, name: uniqueFieldName, virtual: true, admin: { - ...fieldConfig.admin, - condition: (_, siblingData) => siblingData?.type === triggerSlug && ( - fieldConfig.admin?.condition ? - fieldConfig.admin.condition(_, siblingData) : - true - ) + ...(field.admin || {}), + condition: (data: any, siblingData: any) => { + // Only show this field when the trigger type matches + const triggerMatches = siblingData?.type === triggerSlug + + // If the original field had a condition, combine it with our trigger condition + if (field.admin?.condition) { + return triggerMatches && field.admin.condition(data, siblingData) + } + + return triggerMatches + } }, hooks: { - ...fieldConfig.hooks, + ...(field.hooks || {}), afterRead: [ - ...(fieldConfig.hooks?.afterRead || []), - ({ siblingData }) => siblingData?.parameters?.[name] || fieldConfig.defaultValue + ...(field.hooks?.afterRead || []), + ({ siblingData }: any) => { + // Read the value from the parameters JSON field + return siblingData?.parameters?.[originalName] ?? field.defaultValue + } ], beforeChange: [ - ...(fieldConfig.hooks?.beforeChange || []), - ({ value, siblingData }) => { - if (!siblingData.parameters) siblingData.parameters = {} - siblingData.parameters[name] = value + ...(field.hooks?.beforeChange || []), + ({ value, siblingData }: any) => { + // Store the value in the parameters JSON field + if (!siblingData.parameters) { + siblingData.parameters = {} + } + siblingData.parameters[originalName] = value return undefined // Virtual field, don't store directly } ] - }, - validate: fieldConfig.validate || fieldConfig.required ? - (value: any, args: any) => { - const paramValue = value ?? args.siblingData?.parameters?.[name] - - // Check required - if (fieldConfig.required && args.siblingData?.type === triggerSlug && !paramValue) { - return `${fieldConfig.admin?.description || name} is required for ${triggerSlug}` - } - - // Run original validation if present - return fieldConfig.validate?.(paramValue, args) ?? true - } : - undefined - } as Field + } + } + + // Only add validate if the field supports it (data fields) + if (field.validate || field.required) { + resultField.validate = (value: any, args: any) => { + const paramValue = value ?? args.siblingData?.parameters?.[originalName] + + // Check required validation + if (field.required && args.siblingData?.type === triggerSlug && !paramValue) { + const label = field.label || field.admin?.description || originalName + return `${label} is required for ${triggerSlug}` + } + + // Run original validation if present + if (field.validate) { + return field.validate(paramValue, args) + } + + return true + } + } + + return resultField as Field } /** - * Helper to create multiple trigger parameter fields at once + * Creates a custom trigger configuration with the provided fields + * + * @param slug - Unique identifier for the trigger + * @param fields - Array of PayloadCMS fields that will be shown as trigger parameters + * @returns Complete trigger configuration + * + * @example + * ```typescript + * const webhookTrigger = createTrigger('webhook', [ + * { + * name: 'url', + * type: 'text', + * required: true, + * admin: { + * description: 'Webhook URL' + * } + * }, + * { + * name: 'method', + * type: 'select', + * options: ['GET', 'POST', 'PUT', 'DELETE'], + * defaultValue: 'POST' + * } + * ]) + * ``` */ -export function createTriggerParameters( - triggerSlug: string, - parameters: Record -): Field[] { - return Object.entries(parameters).map(([name, fieldConfig]) => - createTriggerParameter(name, fieldConfig, triggerSlug) - ) -} - -/** - * Main trigger builder function that creates a fluent API for defining triggers - */ -export function createTrigger(slug: TSlug) { +export function createTrigger(slug: string, fields: Field[]): CustomTriggerConfig { return { - /** - * Define parameters for this trigger using a clean object syntax - * @param paramConfig - Object where keys are parameter names and values are Field configs - * @returns Complete CustomTriggerConfig ready for use - */ - parameters(paramConfig: Record): CustomTriggerConfig { - return { - slug, - inputs: Object.entries(paramConfig).map(([name, fieldConfig]) => - createTriggerParameter(name, fieldConfig, slug) - ) - } - } - } -} - -/** - * Advanced trigger builder with chainable methods for more complex scenarios - */ -export function createAdvancedTrigger(slug: TSlug) { - const builder = { slug, - _parameters: {} as Record, - - /** - * Set all parameters at once - */ - parameters(paramConfig: Record) { - this._parameters = paramConfig - return this - }, - - /** - * Add a single parameter - */ - parameter(name: string, fieldConfig: any) { - this._parameters[name] = fieldConfig - return this - }, - - /** - * Extend with existing parameter sets (useful for common patterns) - */ - extend(baseParameters: Record) { - this._parameters = { ...baseParameters, ...this._parameters } - return this - }, - - /** - * Build the final trigger configuration - */ - build(): CustomTriggerConfig { - return { - slug: this.slug, - inputs: Object.entries(this._parameters).map(([name, fieldConfig]) => - createTriggerParameter(name, fieldConfig, this.slug) - ) - } - } + inputs: fields.map(field => createTriggerField(field, slug)) } - - return builder } \ No newline at end of file diff --git a/src/utils/trigger-presets.ts b/src/utils/trigger-presets.ts index 857836e..479c291 100644 --- a/src/utils/trigger-presets.ts +++ b/src/utils/trigger-presets.ts @@ -1,88 +1,5 @@ -import { createAdvancedTrigger } from './trigger-helpers.js' - -/** - * Common parameter sets for reuse across different triggers - */ - -export const webhookParameters: Record = { - path: { - type: 'text', - required: true, - admin: { - description: 'URL path for the webhook endpoint (e.g., "my-webhook")' - }, - validate: (value: any) => { - if (typeof value === 'string' && value.includes(' ')) { - return 'Webhook path cannot contain spaces' - } - return true - } - }, - secret: { - type: 'text', - admin: { - description: 'Secret key for webhook signature validation (optional but recommended)' - } - }, - headers: { - type: 'json', - admin: { - description: 'Expected HTTP headers for validation (JSON object)' - } - } -} - -export const cronParameters: Record = { - expression: { - type: 'text', - required: true, - admin: { - description: 'Cron expression for scheduling (e.g., "0 9 * * 1" for every Monday at 9 AM)', - placeholder: '0 9 * * 1' - } - }, - timezone: { - type: 'text', - defaultValue: 'UTC', - admin: { - description: 'Timezone for cron execution (e.g., "America/New_York", "Europe/London")', - placeholder: 'UTC' - }, - validate: (value: any) => { - if (value) { - try { - new Intl.DateTimeFormat('en', { timeZone: value as string }) - return true - } catch { - return `Invalid timezone: ${value}. Please use a valid IANA timezone identifier` - } - } - return true - } - } -} - -export const eventParameters: Record = { - eventTypes: { - type: 'select', - hasMany: true, - options: [ - { label: 'User Created', value: 'user.created' }, - { label: 'User Updated', value: 'user.updated' }, - { label: 'Document Published', value: 'document.published' }, - { label: 'Payment Completed', value: 'payment.completed' } - ], - admin: { - description: 'Event types that should trigger this workflow' - } - }, - filters: { - type: 'json', - admin: { - description: 'JSON filters to apply to event data (e.g., {"status": "active"})' - } - } -} +import { createTrigger } from './trigger-helpers.js' +import type { CustomTriggerConfig } from '../plugin/config-types.js' /** * Preset trigger builders for common patterns @@ -91,28 +8,109 @@ export const eventParameters: Record = { /** * Create a webhook trigger with common webhook parameters pre-configured */ -export function webhookTrigger(slug: TSlug) { - return createAdvancedTrigger(slug).extend(webhookParameters) +export function webhookTrigger(slug: string): CustomTriggerConfig { + return createTrigger(slug, [ + { + name: 'path', + type: 'text', + required: true, + admin: { + description: 'URL path for the webhook endpoint (e.g., "my-webhook")' + }, + validate: (value: any) => { + if (typeof value === 'string' && value.includes(' ')) { + return 'Webhook path cannot contain spaces' + } + return true + } + }, + { + name: 'secret', + type: 'text', + admin: { + description: 'Secret key for webhook signature validation (optional but recommended)' + } + }, + { + name: 'headers', + type: 'json', + admin: { + description: 'Expected HTTP headers for validation (JSON object)' + } + } + ]) } /** * Create a scheduled/cron trigger with timing parameters pre-configured */ -export function cronTrigger(slug: TSlug) { - return createAdvancedTrigger(slug).extend(cronParameters) +export function cronTrigger(slug: string): CustomTriggerConfig { + return createTrigger(slug, [ + { + name: 'expression', + type: 'text', + required: true, + admin: { + description: 'Cron expression for scheduling (e.g., "0 9 * * 1" for every Monday at 9 AM)', + placeholder: '0 9 * * 1' + } + }, + { + name: 'timezone', + type: 'text', + defaultValue: 'UTC', + admin: { + description: 'Timezone for cron execution (e.g., "America/New_York", "Europe/London")', + placeholder: 'UTC' + }, + validate: (value: any) => { + if (value) { + try { + new Intl.DateTimeFormat('en', { timeZone: value as string }) + return true + } catch { + return `Invalid timezone: ${value}. Please use a valid IANA timezone identifier` + } + } + return true + } + } + ]) } /** * Create an event-driven trigger with event filtering parameters */ -export function eventTrigger(slug: TSlug) { - return createAdvancedTrigger(slug).extend(eventParameters) +export function eventTrigger(slug: string): CustomTriggerConfig { + return createTrigger(slug, [ + { + name: 'eventTypes', + type: 'select', + hasMany: true, + options: [ + { label: 'User Created', value: 'user.created' }, + { label: 'User Updated', value: 'user.updated' }, + { label: 'Document Published', value: 'document.published' }, + { label: 'Payment Completed', value: 'payment.completed' } + ], + admin: { + description: 'Event types that should trigger this workflow' + } + }, + { + name: 'filters', + type: 'json', + admin: { + description: 'JSON filters to apply to event data (e.g., {"status": "active"})' + } + } + ]) } /** * Create a simple manual trigger (no parameters needed) */ -export function manualTrigger(slug: TSlug) { +export function manualTrigger(slug: string): CustomTriggerConfig { return { slug, inputs: [] @@ -122,16 +120,18 @@ export function manualTrigger(slug: TSlug) { /** * Create an API trigger for external systems to call */ -export function apiTrigger(slug: TSlug) { - return createAdvancedTrigger(slug).extend({ - endpoint: { +export function apiTrigger(slug: string): CustomTriggerConfig { + return createTrigger(slug, [ + { + name: 'endpoint', type: 'text', required: true, admin: { description: 'API endpoint path (e.g., "/api/triggers/my-trigger")' } }, - method: { + { + name: 'method', type: 'select', options: ['GET', 'POST', 'PUT', 'PATCH'], defaultValue: 'POST', @@ -139,7 +139,8 @@ export function apiTrigger(slug: TSlug) { description: 'HTTP method for the API endpoint' } }, - authentication: { + { + name: 'authentication', type: 'select', options: [ { label: 'None', value: 'none' }, @@ -152,5 +153,5 @@ export function apiTrigger(slug: TSlug) { description: 'Authentication method for the API endpoint' } } - }) + ]) } \ No newline at end of file