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