-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathstdlib.ts
More file actions
128 lines (118 loc) · 4.64 KB
/
Copy pathstdlib.ts
File metadata and controls
128 lines (118 loc) · 4.64 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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* ObjectStack standard CEL function library.
*
* Registered into the per-evaluation `Environment` by the CEL engine. All
* functions are pure given a pinned `now` — that determinism is what makes
* `objectstack build` artifacts byte-stable across runs.
*
* Function naming intentionally avoids the `os.` prefix because cel-js binds
* dotted names to receiver types. Instead, the `os` namespace in CEL holds
* *data* (`os.user`, `os.org`, `os.env`) supplied by the caller's
* {@link EvalContext}.
*/
import type { Environment } from '@marcbachmann/cel-js';
import type { EvalContext } from './types';
/** Truncate a Date to start-of-day in UTC. */
function startOfDayUtc(d: Date): Date {
const out = new Date(d.getTime());
out.setUTCHours(0, 0, 0, 0);
return out;
}
/** Add `n` days to a Date in UTC; returns a new Date. */
function addDaysUtc(d: Date, n: number): Date {
const out = new Date(d.getTime());
out.setUTCDate(out.getUTCDate() + n);
return out;
}
/**
* Register the ObjectStack standard library into a CEL environment.
*
* The `now` resolver is closed over so each call uses the pinned
* `EvalContext.now` (or wall-clock fallback). Implementations are kept tiny
* and dependency-free — they're the contract surface for AI authors and must
* stay legible.
*/
export function registerStdLib(
env: Environment,
now: () => Date,
): Environment {
return env
.registerFunction('now(): google.protobuf.Timestamp', () => now())
.registerFunction(
'today(): google.protobuf.Timestamp',
() => startOfDayUtc(now()),
)
.registerFunction(
'daysFromNow(int): google.protobuf.Timestamp',
(n: bigint | number) => addDaysUtc(now(), Number(n)),
)
.registerFunction(
'daysAgo(int): google.protobuf.Timestamp',
(n: bigint | number) => addDaysUtc(now(), -Number(n)),
)
// Returns true when `value` is null, undefined, empty string, or empty list.
// Matches the intent of legacy `ISBLANK()` while staying CEL-idiomatic.
.registerFunction(
'isBlank(dyn): bool',
(value: unknown) => {
if (value === null || value === undefined) return true;
if (typeof value === 'string') return value.length === 0;
if (Array.isArray(value)) return value.length === 0;
return false;
},
)
// Returns `value` when not null/undefined, otherwise the `fallback`.
// Use this to safely concatenate optional string fields:
// coalesce(record.salutation, '') + ' ' + coalesce(record.first_name, '')
.registerFunction(
'coalesce(dyn, dyn): dyn',
(value: unknown, fallback: unknown) =>
(value === null || value === undefined) ? fallback : value,
)
// Trim leading/trailing ASCII whitespace from a string. Returns '' for
// null/undefined so it composes cleanly with `coalesce`.
.registerFunction(
'trim(dyn): string',
(value: unknown) => {
if (value === null || value === undefined) return '';
return String(value).trim();
},
)
// Join a list of values with `sep`, dropping null/undefined/empty entries
// first. Designed for display-name formulas like:
// joinNonEmpty([record.salutation, record.first_name, record.last_name], ' ')
// which produces 'Alice Martinez' (no leading/trailing/internal extra
// spaces) when `salutation` is null.
.registerFunction(
'joinNonEmpty(list, string): string',
(list: unknown, sep: unknown) => {
const arr = Array.isArray(list) ? list : [];
const separator = typeof sep === 'string' ? sep : ' ';
const parts: string[] = [];
for (const item of arr) {
if (item === null || item === undefined) continue;
const s = String(item).trim();
if (s.length > 0) parts.push(s);
}
return parts.join(separator);
},
);
}
/**
* Build the variable scope for a single evaluation. Absent fields are simply
* not bound — CEL macros (`has(record.foo)`) handle missing-key safely.
*/
export function buildScope(ctx: EvalContext): Record<string, unknown> {
const scope: Record<string, unknown> = {};
if (ctx.record !== undefined) scope.record = ctx.record;
if (ctx.previous !== undefined) scope.previous = ctx.previous;
if (ctx.input !== undefined) scope.input = ctx.input;
// Namespaced data — written as `os.user.id`, `os.env`, etc. in CEL.
const os: Record<string, unknown> = {};
if (ctx.user !== undefined) os.user = ctx.user;
if (ctx.org !== undefined) os.org = ctx.org;
if (ctx.env !== undefined) os.env = ctx.env;
if (Object.keys(os).length > 0) scope.os = os;
if (ctx.extra !== undefined) Object.assign(scope, ctx.extra);
return scope;
}