diff --git a/examples/app-showcase/objectstack.config.ts b/examples/app-showcase/objectstack.config.ts index 59b29ebf6..e02be9d37 100644 --- a/examples/app-showcase/objectstack.config.ts +++ b/examples/app-showcase/objectstack.config.ts @@ -1,6 +1,8 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { defineStack } from '@objectstack/spec'; +import { ConnectorRestPlugin } from '@objectstack/connector-rest'; +import { ConnectorSlackPlugin } from '@objectstack/connector-slack'; import * as objects from './src/objects/index.js'; import { TaskViews, ProjectViews } from './src/views/index.js'; @@ -26,6 +28,11 @@ import { allDatasources } from './src/datasources/index.js'; import { allPortals } from './src/portals/index.js'; import { ShowcaseSeedData } from './src/data/index.js'; +// Ambient `process` for the env-var overrides below — the showcase tsconfig +// doesn't pull in `@types/node`, but the CLI provides the real `process` at +// runtime. Keeps `pnpm typecheck` green without widening the type surface. +declare const process: { env: Record }; + /** * Showcase — a kitchen-sink workspace that exercises every metadata type, * every view type, every chart type, and the major end-to-end capability @@ -49,9 +56,35 @@ export default defineStack({ description: 'Kitchen-sink workspace covering all metadata types, all view types, and the major capability chains.', }, - // `approvals` loads ApprovalsServicePlugin so the `approval` flow node - // (ADR-0019) is contributed to the engine — the showcase flows use it. - requires: ['ui', 'automation', 'approvals'], + // Capability tokens the CLI resolves to platform plugins: + // • automation — AutomationServicePlugin (flow engine + node executors). + // • approvals — ApprovalsServicePlugin, so the `approval` flow node + // (ADR-0019) is contributed to the engine. + // • messaging — MessagingServicePlugin, so the `notify` node delivers to + // the inbox channel (`sys_inbox_message` rows) instead of + // degrading to a logged no-op. + // • triggers — record-change + schedule FlowTrigger plugins, so the + // autolaunched / schedule flows below actually auto-fire. + // • job — JobServicePlugin, the timing backend the schedule trigger + // delegates to (interval / cron jobs). + requires: ['ui', 'automation', 'approvals', 'messaging', 'triggers', 'job'], + + // Concrete connectors for the `connector_action` node. The baseline engine + // ships the dispatch node + an empty registry; these plugins populate it. + // • rest → points at the running server itself, so the REST connector + // flow's call + response are observable on the flow run with no + // external dependency. Override the target with SHOWCASE_SELF_URL. + // • slack → registered so TaskCompletedSlackFlow resolves its connector; + // live posting needs a real bot token (set SLACK_BOT_TOKEN). + plugins: [ + new ConnectorRestPlugin({ + name: 'rest', + baseUrl: process.env.SHOWCASE_SELF_URL ?? 'http://127.0.0.1:3000', + }), + new ConnectorSlackPlugin({ + token: process.env.SLACK_BOT_TOKEN ?? 'xoxb-showcase-demo-token', + }), + ], // Infrastructure datasources: allDatasources, diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 9e1453be1..93115fa20 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -20,6 +20,8 @@ "verify": "pnpm typecheck && pnpm test" }, "dependencies": { + "@objectstack/connector-rest": "workspace:*", + "@objectstack/connector-slack": "workspace:*", "@objectstack/runtime": "workspace:*", "@objectstack/spec": "workspace:*" }, diff --git a/examples/app-showcase/src/flows/index.ts b/examples/app-showcase/src/flows/index.ts index 78e0b9cf3..ad5f38a62 100644 --- a/examples/app-showcase/src/flows/index.ts +++ b/examples/app-showcase/src/flows/index.ts @@ -197,8 +197,12 @@ export const BudgetApprovalFlow = defineFlow({ { id: 'e1', source: 'start', target: 'manager_review' }, { id: 'e2', source: 'manager_review', target: 'needs_exec', label: 'approve' }, { id: 'e3', source: 'manager_review', target: 'rejected', label: 'reject' }, - { id: 'e4', source: 'needs_exec', target: 'exec_review', label: 'true' }, - { id: 'e5', source: 'needs_exec', target: 'approved', label: 'false' }, + // Decision branching is edge-condition driven (flow spec): the engine + // routes a decision node by evaluating each out-edge's `condition`. Carry + // the predicate on the edges (the node `config.condition` alone is not + // evaluated by the engine), so budgets ≤ $500k skip the executive step. + { id: 'e4', source: 'needs_exec', target: 'exec_review', label: 'true', condition: 'budget > 500000' }, + { id: 'e5', source: 'needs_exec', target: 'approved', label: 'false', condition: 'budget <= 500000' }, { id: 'e6', source: 'exec_review', target: 'approved', label: 'approve' }, { id: 'e7', source: 'exec_review', target: 'rejected', label: 'reject' }, ], @@ -258,10 +262,112 @@ export const TaskCompletedSlackFlow = defineFlow({ ], }); +/** + * Scheduled Digest — the worked `schedule` trigger example. + * + * A `type: 'schedule'` flow whose start node carries an interval descriptor. + * The automation engine parses that into a schedule binding; the schedule + * trigger plugin (`@objectstack/plugin-trigger-schedule`, paired with the job + * service) registers a job that fires this flow every interval. Each tick runs + * the `notify` node, dropping a fresh `sys_inbox_message` row — so the + * scheduled fire is observable end-to-end with no manual `engine.execute()`. + * + * Install `requires: ['automation', 'triggers', 'job', 'messaging']` and this + * flow auto-launches on the interval. + */ +export const ScheduledDigestFlow = defineFlow({ + name: 'showcase_scheduled_digest', + label: 'Scheduled Project Digest (interval)', + description: 'Fires on a fixed interval and posts a digest to an inbox — demonstrates the schedule trigger.', + type: 'schedule', + nodes: [ + { + id: 'start', + type: 'start', + label: 'Every 20s', + config: { + // Interval keeps the demo observable in-session; production digests + // would use a cron expression, e.g. { type: 'cron', expression: '0 8 * * *' }. + schedule: { type: 'interval', intervalMs: 20000 }, + }, + }, + { + id: 'digest', + type: 'notify', + label: 'Post Digest to Inbox', + config: { + topic: 'project.digest', + recipients: ['admin@objectos.ai'], + channels: ['inbox'], + severity: 'info', + title: 'Scheduled project digest', + message: 'Your periodic project digest is ready — open Projects for the latest health.', + actionUrl: '/showcase_project', + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'digest' }, + { id: 'e2', source: 'digest', target: 'end' }, + ], +}); + +/** + * Task Completed → REST Ping (self) — the worked `connector_action` example on + * the generic `rest` connector. + * + * Where {@link TaskCompletedSlackFlow} targets the `slack` connector (which + * needs a real bot token + channel), this flow dispatches to the `rest` + * connector contributed by `@objectstack/connector-rest`, configured to point + * at the running server itself. On task completion it issues + * `GET /api/v1/health`; the request and its `{ status: 'ok' }` response are + * captured on the flow run, so the connector dispatch is fully observable + * without any external service or credentials. + */ +export const TaskCompletedRestPingFlow = defineFlow({ + name: 'showcase_task_completed_rest_ping', + label: 'REST Ping on Task Completed', + description: 'Calls the local server health endpoint via the rest connector when a task is marked Done.', + type: 'autolaunched', + nodes: [ + { + id: 'start', + type: 'start', + label: 'On Task Update', + config: { + objectName: 'showcase_task', + triggerType: 'record-after-update', + condition: 'status == "done" && previous.status != "done"', + }, + }, + { + id: 'ping', + type: 'connector_action', + label: 'GET /api/v1/health', + connectorConfig: { + connectorId: 'rest', + actionId: 'request', + input: { + method: 'GET', + path: '/api/v1/health', + }, + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'ping' }, + { id: 'e2', source: 'ping', target: 'end' }, + ], +}); + export const allFlows = [ TaskCompletedFlow, ReassignWizardFlow, BudgetApprovalFlow, TaskCompletedSlackFlow, TaskAssignedNotifyFlow, + ScheduledDigestFlow, + TaskCompletedRestPingFlow, ]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7469e20d..b0aafda67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,12 @@ importers: examples/app-showcase: dependencies: + '@objectstack/connector-rest': + specifier: workspace:* + version: link:../../packages/connectors/connector-rest + '@objectstack/connector-slack': + specifier: workspace:* + version: link:../../packages/connectors/connector-slack '@objectstack/runtime': specifier: workspace:* version: link:../../packages/runtime