|
| 1 | +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * Object lifecycle hooks — the showcase's "logic layer". |
| 5 | + * |
| 6 | + * Each hook is a plain object validated by `HookSchema` inside |
| 7 | + * `defineStack({ hooks })` (same authoring style as webhooks). Together they |
| 8 | + * exercise the full hook designer surface so Studio has something real to |
| 9 | + * render for every property: |
| 10 | + * |
| 11 | + * • multi-event targeting (`beforeInsert` + `beforeUpdate`) |
| 12 | + * • an L2 sandboxed-JS `body` (language + source + capabilities) |
| 13 | + * • a CEL `condition` gate |
| 14 | + * • fire-and-forget `async` execution with a `retryPolicy` |
| 15 | + * • `onError` / `priority` tuning across more than one object |
| 16 | + * |
| 17 | + * The bodies are deliberately tiny and side-effect-light — they are read as |
| 18 | + * documentation as much as they run. |
| 19 | + */ |
| 20 | + |
| 21 | +type LifecycleEvent = |
| 22 | + | 'beforeInsert' | 'afterInsert' |
| 23 | + | 'beforeUpdate' | 'afterUpdate' |
| 24 | + | 'beforeDelete' | 'afterDelete'; |
| 25 | + |
| 26 | +/** beforeInsert/beforeUpdate — normalise the task title before it is stored. */ |
| 27 | +export const NormalizeTaskTitleHook = { |
| 28 | + name: 'showcase_normalize_task_title', |
| 29 | + label: 'Normalize Task Title', |
| 30 | + object: 'showcase_task', |
| 31 | + events: ['beforeInsert', 'beforeUpdate'] as LifecycleEvent[], |
| 32 | + body: { |
| 33 | + language: 'js' as const, |
| 34 | + source: "if (ctx.input.title) ctx.input.title = ctx.input.title.trim();", |
| 35 | + }, |
| 36 | + priority: 50, |
| 37 | + onError: 'abort' as const, |
| 38 | + description: 'Trims leading/trailing whitespace from the task title before every write.', |
| 39 | +}; |
| 40 | + |
| 41 | +/** afterUpdate (gated) — log a line whenever a task flips to done. */ |
| 42 | +export const AuditTaskCompletionHook = { |
| 43 | + name: 'showcase_audit_task_completion', |
| 44 | + label: 'Audit Task Completion', |
| 45 | + object: 'showcase_task', |
| 46 | + events: ['afterUpdate'] as LifecycleEvent[], |
| 47 | + condition: "record.done == true", |
| 48 | + body: { |
| 49 | + language: 'js' as const, |
| 50 | + source: "var r = ctx.result || ctx.input || {}; if (typeof ctx.log === 'function') ctx.log('task completed: ' + (r.title || r.id || 'unknown'));", |
| 51 | + capabilities: ['log'] as ('log')[], |
| 52 | + }, |
| 53 | + async: true, |
| 54 | + priority: 90, |
| 55 | + retryPolicy: { maxRetries: 3, backoffMs: 1000 }, |
| 56 | + onError: 'log' as const, |
| 57 | + description: 'Fire-and-forget audit line emitted after a task transitions to done.', |
| 58 | +}; |
| 59 | + |
| 60 | +/** afterUpdate (gated) — warn when a project goes over budget. */ |
| 61 | +export const WarnOverBudgetHook = { |
| 62 | + name: 'showcase_warn_over_budget', |
| 63 | + label: 'Warn On Over-Budget Project', |
| 64 | + object: 'showcase_project', |
| 65 | + events: ['afterUpdate'] as LifecycleEvent[], |
| 66 | + condition: "record.spent > record.budget", |
| 67 | + body: { |
| 68 | + language: 'js' as const, |
| 69 | + source: "var r = ctx.result || ctx.input || {}; if (typeof ctx.log === 'function') ctx.log('project over budget: ' + (r.name || r.id || 'unknown') + ' (' + r.spent + ' / ' + r.budget + ')');", |
| 70 | + capabilities: ['log'] as ('log')[], |
| 71 | + }, |
| 72 | + async: true, |
| 73 | + onError: 'log' as const, |
| 74 | + description: 'Emits a warning when a project’s spend exceeds its budget.', |
| 75 | +}; |
| 76 | + |
| 77 | +export const allHooks = [ |
| 78 | + NormalizeTaskTitleHook, |
| 79 | + AuditTaskCompletionHook, |
| 80 | + WarnOverBudgetHook, |
| 81 | +]; |
0 commit comments