diff --git a/integrations/shopify-admin/integration.definition.ts b/integrations/shopify-admin/integration.definition.ts index a95aafb2b12..02460fed2b1 100644 --- a/integrations/shopify-admin/integration.definition.ts +++ b/integrations/shopify-admin/integration.definition.ts @@ -3,7 +3,7 @@ import { actions, events, states, configuration, secrets } from './definitions' export default new IntegrationDefinition({ name: 'shopify-admin', - version: '0.1.2', + version: '0.1.3', title: 'Shopify Admin', description: 'Connect your Shopify store via the Admin GraphQL API to manage products, customers, and orders via OAuth 2.0.', @@ -14,4 +14,8 @@ export default new IntegrationDefinition({ events, states, secrets, + attributes: { + category: 'E-commerce & Payments', + repo: 'botpress', + }, }) diff --git a/integrations/shopify-storefront/integration.definition.ts b/integrations/shopify-storefront/integration.definition.ts index c0f8b280c53..3dd864ee7f3 100644 --- a/integrations/shopify-storefront/integration.definition.ts +++ b/integrations/shopify-storefront/integration.definition.ts @@ -3,7 +3,7 @@ import { actions, states, configuration, secrets } from './definitions' export default new IntegrationDefinition({ name: 'shopify-storefront', - version: '0.1.2', + version: '0.1.3', title: 'Shopify Storefront', description: 'Connect your Shopify store via the Storefront API to power buyer-facing product browsing, collections, and cart/checkout flows via OAuth 2.0.', @@ -13,4 +13,8 @@ export default new IntegrationDefinition({ actions, states, secrets, + attributes: { + category: 'E-commerce & Payments', + repo: 'botpress', + }, }) diff --git a/packages/cli/package.json b/packages/cli/package.json index 6c1173965f4..a8255e98fc5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "6.8.6", + "version": "6.8.7", "description": "Botpress CLI", "scripts": { "build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen", @@ -28,7 +28,7 @@ "@apidevtools/json-schema-ref-parser": "^11.7.0", "@botpress/chat": "0.5.5", "@botpress/client": "1.46.0", - "@botpress/sdk": "6.11.2", + "@botpress/sdk": "6.12.0", "@bpinternal/const": "^0.1.0", "@bpinternal/tunnel": "^0.1.1", "@bpinternal/verel": "^0.2.0", diff --git a/packages/cli/templates/empty-bot/package.json b/packages/cli/templates/empty-bot/package.json index c450effa358..2f1d5773d8d 100644 --- a/packages/cli/templates/empty-bot/package.json +++ b/packages/cli/templates/empty-bot/package.json @@ -6,7 +6,7 @@ "private": true, "dependencies": { "@botpress/client": "1.46.0", - "@botpress/sdk": "6.11.2" + "@botpress/sdk": "6.12.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-integration/package.json b/packages/cli/templates/empty-integration/package.json index d28248d8642..cfa1d8314d7 100644 --- a/packages/cli/templates/empty-integration/package.json +++ b/packages/cli/templates/empty-integration/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.46.0", - "@botpress/sdk": "6.11.2" + "@botpress/sdk": "6.12.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-plugin/package.json b/packages/cli/templates/empty-plugin/package.json index daa5d291e0b..fc9afea3a76 100644 --- a/packages/cli/templates/empty-plugin/package.json +++ b/packages/cli/templates/empty-plugin/package.json @@ -6,7 +6,7 @@ }, "private": true, "dependencies": { - "@botpress/sdk": "6.11.2" + "@botpress/sdk": "6.12.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/hello-world/package.json b/packages/cli/templates/hello-world/package.json index 56efb4f776e..8f32a4e3d19 100644 --- a/packages/cli/templates/hello-world/package.json +++ b/packages/cli/templates/hello-world/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.46.0", - "@botpress/sdk": "6.11.2" + "@botpress/sdk": "6.12.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/webhook-message/package.json b/packages/cli/templates/webhook-message/package.json index c3661c67b0e..0f83cd60cf5 100644 --- a/packages/cli/templates/webhook-message/package.json +++ b/packages/cli/templates/webhook-message/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.46.0", - "@botpress/sdk": "6.11.2", + "@botpress/sdk": "6.12.0", "axios": "^1.6.8" }, "devDependencies": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 1e0128a45ce..2c39f359e28 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/sdk", - "version": "6.11.2", + "version": "6.12.0", "description": "Botpress SDK", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/sdk/src/bot/definition.test.ts b/packages/sdk/src/bot/definition.test.ts index 03fe7a23137..ae86f0841ea 100644 --- a/packages/sdk/src/bot/definition.test.ts +++ b/packages/sdk/src/bot/definition.test.ts @@ -326,3 +326,97 @@ test('addPlugin falls back to raw config when schema validation fails', () => { // Falls back to raw config; default for sneaky is not applied since safeParse failed expect(storedConfig).toEqual({ preciousEmail: '$GOLLUM_EMAIL' }) }) + +// BotDefinition constructor — recurrence field handling + +test('BotDefinition strips recurrence from events', () => { + const bot = new BotDefinition({ + events: { + heartbeat: { + schema: z.object({}), + recurrence: { cron: '*/5 * * * *', payload: {} }, + }, + }, + }) + + expect(bot.events?.heartbeat).not.toHaveProperty('recurrence') +}) + +test('BotDefinition converts inline recurrence to a recurringEvents entry', () => { + const bot = new BotDefinition({ + events: { + heartbeat: { + schema: z.object({}), + recurrence: { cron: '*/5 * * * *', payload: {} }, + }, + }, + }) + + expect(bot.recurringEvents?.heartbeat).toEqual({ + type: 'heartbeat', + schedule: { cron: '*/5 * * * *' }, + payload: {}, + }) +}) + +test('BotDefinition recurringEvents is undefined when no recurring events are defined', () => { + const bot = new BotDefinition({ + events: { + plain: { schema: z.object({}) }, + }, + }) + + expect(bot.recurringEvents).toBeUndefined() +}) + +test('BotDefinition: explicit recurringEvents overrides inline recurrence for the same key', () => { + const bot = new BotDefinition({ + events: { + heartbeat: { + schema: z.object({}), + recurrence: { cron: '*/5 * * * *', payload: { foo: 'foo' } }, + }, + }, + recurringEvents: { + heartbeat: { type: 'heartbeat', schedule: { cron: '0 * * * *' }, payload: { bar: 'bar' } }, + }, + }) + + expect(bot.recurringEvents?.heartbeat).toEqual({ + type: 'heartbeat', + schedule: { cron: '0 * * * *' }, + payload: { bar: 'bar' }, + }) +}) + +test('BotDefinition: two recurringEvents entries with different keys but same type both survive', () => { + const bot = new BotDefinition({ + events: { + foo: { schema: z.object({}) }, + }, + recurringEvents: { + fooEvery6: { type: 'foo', schedule: { cron: '*/6 * * * *' }, payload: {} }, + fooEvery7: { type: 'foo', schedule: { cron: '*/7 * * * *' }, payload: {} }, + }, + }) + + expect(bot.recurringEvents?.fooEvery6).toEqual({ type: 'foo', schedule: { cron: '*/6 * * * *' }, payload: {} }) + expect(bot.recurringEvents?.fooEvery7).toEqual({ type: 'foo', schedule: { cron: '*/7 * * * *' }, payload: {} }) +}) + +test('BotDefinition: explicit recurringEvents with no inline counterpart is preserved', () => { + const bot = new BotDefinition({ + events: { + heartbeat: { schema: z.object({}) }, + }, + recurringEvents: { + dailyDigest: { type: 'heartbeat', schedule: { cron: '0 9 * * *' }, payload: {} }, + }, + }) + + expect(bot.recurringEvents?.dailyDigest).toEqual({ + type: 'heartbeat', + schedule: { cron: '0 9 * * *' }, + payload: {}, + }) +}) diff --git a/packages/sdk/src/bot/definition.ts b/packages/sdk/src/bot/definition.ts index 4fc64ab6b92..738bd6f5a9c 100644 --- a/packages/sdk/src/bot/definition.ts +++ b/packages/sdk/src/bot/definition.ts @@ -1,4 +1,5 @@ import { Table } from '@botpress/client' +import { stripRecurringFromEvents, resolveRecurringEvents } from '../common/recurring-events' import { SchemaTransformOptions } from '../common/types' import * as consts from '../consts' import { DefinitionError } from '../errors' @@ -29,6 +30,9 @@ export type StateDefinition = { [K in keyof TEvents]: { type: K @@ -38,6 +42,10 @@ export type RecurringEventDefinition = }[keyof TEvents] export type EventDefinition = SchemaDefinition & { + recurrence?: { + cron: string + payload: z.input + } attributes?: Record } @@ -178,6 +186,7 @@ export type BotDefinitionProps< events?: { [K in keyof TEvents]: EventDefinition } + /** @deprecated Use the `recurrence` field on each event in `events` instead. */ recurringEvents?: Record> actions?: { [K in keyof TActions]: ActionDefinition @@ -231,6 +240,14 @@ export class BotDefinition< > public constructor(public readonly props: BotDefinitionProps) { + const events = stripRecurringFromEvents( + props.events as Record | undefined + ) as this['props']['events'] + const recurringEvents = resolveRecurringEvents( + props.events as Record, + props.recurringEvents as BotDefinitionProps['recurringEvents'] + ) + this.integrations = props.integrations this.plugins = props.plugins this.user = props.user @@ -238,8 +255,8 @@ export class BotDefinition< this.message = props.message this.states = props.states this.configuration = props.configuration - this.events = props.events - this.recurringEvents = props.recurringEvents + this.events = events + this.recurringEvents = recurringEvents this.actions = props.actions this.tables = props.tables this.secrets = props.secrets @@ -252,8 +269,8 @@ export class BotDefinition< conversation: props.conversation, message: props.message, states: props.states, - events: props.events, - recurringEvents: props.recurringEvents, + events, + recurringEvents, actions: props.actions, tables: props.tables, workflows: props.workflows, diff --git a/packages/sdk/src/common/recurring-events.test.ts b/packages/sdk/src/common/recurring-events.test.ts new file mode 100644 index 00000000000..d737412a572 --- /dev/null +++ b/packages/sdk/src/common/recurring-events.test.ts @@ -0,0 +1,101 @@ +import { test, expect } from 'vitest' +import { stripRecurringFromEvents, resolveRecurringEvents } from './recurring-events' +import { z } from '../zui' + +// stripRecurringFromEvents + +test('stripRecurringFromEvents returns undefined when events is undefined', () => { + expect(stripRecurringFromEvents(undefined)).toBeUndefined() +}) + +test('stripRecurringFromEvents removes the recurrence field from events', () => { + const result = stripRecurringFromEvents({ + heartbeat: { schema: z.object({}), recurrence: { cron: '*/5 * * * *', payload: {} } }, + }) + expect(result?.heartbeat).not.toHaveProperty('recurrence') +}) + +test('stripRecurringFromEvents preserves other event fields', () => { + const result = stripRecurringFromEvents({ + heartbeat: { + schema: z.object({}), + attributes: { foo: 'bar' }, + recurrence: { cron: '*/5 * * * *', payload: {} }, + }, + }) + expect(result?.heartbeat).toHaveProperty('attributes', { foo: 'bar' }) + expect(result?.heartbeat).toHaveProperty('schema') +}) + +test('stripRecurringFromEvents handles events without a recurrence field', () => { + const input = { plain: { schema: z.object({}) } } + const result = stripRecurringFromEvents(input) + expect(result?.plain).not.toHaveProperty('recurrence') + expect(result?.plain?.schema).toBe(input.plain.schema) +}) + +// resolveRecurringEvents + +test('resolveRecurringEvents returns undefined when there are no events and no explicit recurringEvents', () => { + expect(resolveRecurringEvents(undefined, undefined)).toBeUndefined() +}) + +test('resolveRecurringEvents returns undefined when events have no recurrence field and no explicit recurringEvents', () => { + expect(resolveRecurringEvents({ plain: { schema: z.object({}) } }, undefined)).toBeUndefined() +}) + +test('resolveRecurringEvents derives a recurringEvents entry from an inline recurrence field', () => { + const result = resolveRecurringEvents( + { heartbeat: { schema: z.object({}), recurrence: { cron: '*/5 * * * *', payload: {} } } }, + undefined + ) + expect(result).toEqual({ + heartbeat: { type: 'heartbeat', schedule: { cron: '*/5 * * * *' }, payload: {} }, + }) +}) + +test('resolveRecurringEvents only derives entries for events that have a recurrence field', () => { + const result = resolveRecurringEvents( + { + heartbeat: { schema: z.object({}), recurrence: { cron: '*/5 * * * *', payload: {} } }, + plain: { schema: z.object({}) }, + }, + undefined + ) + expect(Object.keys(result ?? {})).toEqual(['heartbeat']) +}) + +test('resolveRecurringEvents preserves explicit recurringEvents when there are no inline recurrence events', () => { + const result = resolveRecurringEvents( + { plain: { schema: z.object({}) } }, + { dailyDigest: { type: 'plain', schedule: { cron: '0 9 * * *' }, payload: {} } } + ) + expect(result).toEqual({ + dailyDigest: { type: 'plain', schedule: { cron: '0 9 * * *' }, payload: {} }, + }) +}) + +test('resolveRecurringEvents: explicit recurringEvents overrides inline recurrence for the same key', () => { + const result = resolveRecurringEvents( + { + heartbeat: { + schema: z.object({}), + recurrence: { cron: '*/5 * * * *', payload: { foo: 'foo' } }, + }, + }, + { heartbeat: { type: 'heartbeat', schedule: { cron: '0 * * * *' }, payload: { bar: 'bar' } } } + ) + expect(result?.heartbeat).toEqual({ + type: 'heartbeat', + schedule: { cron: '0 * * * *' }, + payload: { bar: 'bar' }, + }) +}) + +test('resolveRecurringEvents merges derived and explicit entries with different keys', () => { + const result = resolveRecurringEvents( + { heartbeat: { schema: z.object({}), recurrence: { cron: '*/5 * * * *', payload: {} } } }, + { dailyDigest: { type: 'heartbeat', schedule: { cron: '0 9 * * *' }, payload: {} } } + ) + expect(Object.keys(result ?? {}).sort()).toEqual(['dailyDigest', 'heartbeat']) +}) diff --git a/packages/sdk/src/common/recurring-events.ts b/packages/sdk/src/common/recurring-events.ts new file mode 100644 index 00000000000..ec31f1ea41b --- /dev/null +++ b/packages/sdk/src/common/recurring-events.ts @@ -0,0 +1,41 @@ +import { BotDefinitionProps, EventDefinition, RecurringEventDefinition } from '../bot/definition' +import { SafeOmit } from '../utils/type-utils' + +type StrippedEvents> = { + [K in keyof TEvents]: SafeOmit +} + +export function stripRecurringFromEvents>( + events: TEvents +): StrippedEvents +export function stripRecurringFromEvents>( + events: TEvents | undefined +): StrippedEvents | undefined +export function stripRecurringFromEvents>( + events: TEvents | undefined +): StrippedEvents | undefined { + if (!events) return undefined + return Object.fromEntries( + Object.entries(events).map(([key, { recurrence: _, ...rest }]) => [key, rest]) + ) as StrippedEvents +} + +export function resolveRecurringEvents( + events: Record | undefined, + explicitRecurringEvents: BotDefinitionProps['recurringEvents'] +): BotDefinitionProps['recurringEvents'] { + const derived: NonNullable = Object.fromEntries( + Object.entries(events ?? {}) + .map(([eventName, event]): [string, RecurringEventDefinition] | null => + event.recurrence + ? [ + eventName, + { type: eventName, payload: event.recurrence.payload, schedule: { cron: event.recurrence.cron } }, + ] + : null + ) + .filter((x) => x !== null) + ) + const merged = { ...derived, ...explicitRecurringEvents } + return Object.keys(merged).length ? merged : undefined +} diff --git a/packages/sdk/src/plugin/definition.test.ts b/packages/sdk/src/plugin/definition.test.ts new file mode 100644 index 00000000000..b31216f2c96 --- /dev/null +++ b/packages/sdk/src/plugin/definition.test.ts @@ -0,0 +1,107 @@ +import { test, expect } from 'vitest' +import { PluginDefinition } from './definition' +import { z } from '../zui' + +test('PluginDefinition strips recurrence from events', () => { + const plugin = new PluginDefinition({ + name: 'myplugin', + version: '1.0.0', + events: { + heartbeat: { + schema: z.object({}), + recurrence: { cron: '*/5 * * * *', payload: {} }, + }, + }, + }) + + expect(plugin.events?.heartbeat).not.toHaveProperty('recurrence') +}) + +test('PluginDefinition converts inline recurrence to a recurringEvents entry', () => { + const plugin = new PluginDefinition({ + name: 'myplugin', + version: '1.0.0', + events: { + heartbeat: { + schema: z.object({}), + recurrence: { cron: '*/5 * * * *', payload: {} }, + }, + }, + }) + + expect(plugin.recurringEvents?.heartbeat).toEqual({ + type: 'heartbeat', + schedule: { cron: '*/5 * * * *' }, + payload: {}, + }) +}) + +test('PluginDefinition recurringEvents is undefined when no recurring events are defined', () => { + const plugin = new PluginDefinition({ + name: 'myplugin', + version: '1.0.0', + events: { + plain: { schema: z.object({}) }, + }, + }) + + expect(plugin.recurringEvents).toBeUndefined() +}) + +test('PluginDefinition: explicit recurringEvents overrides inline recurrence for the same key', () => { + const plugin = new PluginDefinition({ + name: 'myplugin', + version: '1.0.0', + events: { + heartbeat: { + schema: z.object({}), + recurrence: { cron: '*/5 * * * *', payload: { foo: 'foo' } }, + }, + }, + recurringEvents: { + heartbeat: { type: 'heartbeat', schedule: { cron: '0 * * * *' }, payload: { bar: 'bar' } }, + }, + }) + + expect(plugin.recurringEvents?.heartbeat).toEqual({ + type: 'heartbeat', + schedule: { cron: '0 * * * *' }, + payload: { bar: 'bar' }, + }) +}) + +test('PluginDefinition: explicit recurringEvents with no inline counterpart is preserved', () => { + const plugin = new PluginDefinition({ + name: 'myplugin', + version: '1.0.0', + events: { + heartbeat: { schema: z.object({}) }, + }, + recurringEvents: { + dailyDigest: { type: 'heartbeat', schedule: { cron: '0 9 * * *' }, payload: {} }, + }, + }) + + expect(plugin.recurringEvents?.dailyDigest).toEqual({ + type: 'heartbeat', + schedule: { cron: '0 9 * * *' }, + payload: {}, + }) +}) + +test('PluginDefinition: two explicit recurringEvents entries with different keys both survive', () => { + const plugin = new PluginDefinition({ + name: 'myplugin', + version: '1.0.0', + events: { + tick: { schema: z.object({}) }, + }, + recurringEvents: { + tickEvery6: { type: 'tick', schedule: { cron: '*/6 * * * *' }, payload: {} }, + tickEvery7: { type: 'tick', schedule: { cron: '*/7 * * * *' }, payload: {} }, + }, + }) + + expect(plugin.recurringEvents?.tickEvery6).toEqual({ type: 'tick', schedule: { cron: '*/6 * * * *' }, payload: {} }) + expect(plugin.recurringEvents?.tickEvery7).toEqual({ type: 'tick', schedule: { cron: '*/7 * * * *' }, payload: {} }) +}) diff --git a/packages/sdk/src/plugin/definition.ts b/packages/sdk/src/plugin/definition.ts index 7c5be58d924..689446054c1 100644 --- a/packages/sdk/src/plugin/definition.ts +++ b/packages/sdk/src/plugin/definition.ts @@ -1,4 +1,5 @@ import { + BotDefinitionProps, StateDefinition as BotStateDefinition, EventDefinition as BotEventDefinition, ConfigurationDefinition as BotConfigurationDefinition, @@ -9,6 +10,7 @@ import { TableDefinition as BotTableDefinition, WorkflowDefinition, } from '../bot/definition' +import { stripRecurringFromEvents, resolveRecurringEvents } from '../common/recurring-events' import { SchemaTransformOptions } from '../common/types' import { DefinitionError } from '../errors' import { IntegrationPackage, InterfacePackage } from '../package' @@ -63,6 +65,9 @@ export type ActionDefinition +/* + * @deprecated Use the `recurring` field on `EventDefinition` instead. + */ export type RecurringEventDefinition = { [K in keyof TEvents]: { type: K @@ -140,6 +145,7 @@ export type PluginDefinitionProps< events?: { [K in keyof TEvents]: GenericDefinition> } + /** @deprecated Use the `recurrence` field on each event in `events` instead. */ recurringEvents?: Record> actions?: { [K in keyof TActions]: GenericNestedDefinition, 'input' | 'output'> @@ -246,7 +252,6 @@ export class PluginDefinition< this.user = props.user this.conversation = props.conversation this.message = props.message - this.recurringEvents = props.recurringEvents this.workflows = props.workflows this.attributes = props.attributes this.__advanced = props.__advanced @@ -285,7 +290,7 @@ export class PluginDefinition< ) ) as { [K in keyof TStates]: StateDefinition } - this.events = Object.fromEntries( + const resolvedEvents = Object.fromEntries( Object.entries(props.events ?? {}).map( ([eventName, eventDef]: [keyof TEvents, NonNullable<(typeof props)['events']>[keyof TEvents]]) => [ eventName, @@ -297,6 +302,12 @@ export class PluginDefinition< ) ) as { [K in keyof TEvents]: EventDefinition } + this.events = stripRecurringFromEvents(resolvedEvents) + this.recurringEvents = resolveRecurringEvents( + resolvedEvents as Record, + props.recurringEvents as BotDefinitionProps['recurringEvents'] + ) + this.actions = Object.fromEntries( Object.entries(props.actions ?? {}).map( ([actionName, actionDef]: [keyof TActions, NonNullable<(typeof props)['actions']>[keyof TActions]]) => [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2454443de73..1d0cc8f50bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2832,7 +2832,7 @@ importers: specifier: 1.46.0 version: link:../client '@botpress/sdk': - specifier: 6.11.2 + specifier: 6.12.0 version: link:../sdk '@bpinternal/const': specifier: ^0.1.0 @@ -2956,7 +2956,7 @@ importers: specifier: 1.46.0 version: link:../../../client '@botpress/sdk': - specifier: 6.11.2 + specifier: 6.12.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2972,7 +2972,7 @@ importers: specifier: 1.46.0 version: link:../../../client '@botpress/sdk': - specifier: 6.11.2 + specifier: 6.12.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2985,7 +2985,7 @@ importers: packages/cli/templates/empty-plugin: dependencies: '@botpress/sdk': - specifier: 6.11.2 + specifier: 6.12.0 version: link:../../../sdk devDependencies: '@types/node': @@ -3001,7 +3001,7 @@ importers: specifier: 1.46.0 version: link:../../../client '@botpress/sdk': - specifier: 6.11.2 + specifier: 6.12.0 version: link:../../../sdk devDependencies: '@types/node': @@ -3017,7 +3017,7 @@ importers: specifier: 1.46.0 version: link:../../../client '@botpress/sdk': - specifier: 6.11.2 + specifier: 6.12.0 version: link:../../../sdk axios: specifier: ^1.6.8