@mintlify/common 1.0.1310 → 1.0.1312
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/databases/formula/analyze.d.ts +31 -0
- package/dist/databases/formula/analyze.js +296 -0
- package/dist/databases/formula/ast.d.ts +52 -0
- package/dist/databases/formula/ast.js +20 -0
- package/dist/databases/formula/compile.d.ts +10 -0
- package/dist/databases/formula/compile.js +409 -0
- package/dist/databases/formula/dates.d.ts +5 -0
- package/dist/databases/formula/dates.js +55 -0
- package/dist/databases/formula/errors.d.ts +18 -0
- package/dist/databases/formula/errors.js +33 -0
- package/dist/databases/formula/filter.d.ts +9 -0
- package/dist/databases/formula/filter.js +16 -0
- package/dist/databases/formula/format.d.ts +5 -0
- package/dist/databases/formula/format.js +66 -0
- package/dist/databases/formula/function-docs.d.ts +6 -0
- package/dist/databases/formula/function-docs.js +63 -0
- package/dist/databases/formula/functions.d.ts +21 -0
- package/dist/databases/formula/functions.js +161 -0
- package/dist/databases/formula/highlight.d.ts +7 -0
- package/dist/databases/formula/highlight.js +54 -0
- package/dist/databases/formula/index.d.ts +16 -0
- package/dist/databases/formula/index.js +14 -0
- package/dist/databases/formula/limits.d.ts +8 -0
- package/dist/databases/formula/limits.js +8 -0
- package/dist/databases/formula/lower.d.ts +9 -0
- package/dist/databases/formula/lower.js +252 -0
- package/dist/databases/formula/parse.d.ts +21 -0
- package/dist/databases/formula/parse.js +209 -0
- package/dist/databases/formula/references.d.ts +20 -0
- package/dist/databases/formula/references.js +76 -0
- package/dist/databases/formula/rewrite.d.ts +16 -0
- package/dist/databases/formula/rewrite.js +29 -0
- package/dist/databases/formula/schema.d.ts +29 -0
- package/dist/databases/formula/schema.js +117 -0
- package/dist/databases/formula/tokenize.d.ts +11 -0
- package/dist/databases/formula/tokenize.js +114 -0
- package/dist/databases/formula/typecheck.d.ts +11 -0
- package/dist/databases/formula/typecheck.js +16 -0
- package/dist/databases/formula/types.d.ts +26 -0
- package/dist/databases/formula/types.js +27 -0
- package/dist/databases/formula/values.d.ts +8 -0
- package/dist/databases/formula/values.js +100 -0
- package/dist/databases/query/evaluate.d.ts +25 -0
- package/dist/databases/query/evaluate.js +481 -0
- package/dist/databases/query/index.d.ts +4 -0
- package/dist/databases/query/index.js +4 -0
- package/dist/databases/query/types.d.ts +69 -0
- package/dist/databases/query/types.js +20 -0
- package/dist/databases/query/validate.d.ts +8 -0
- package/dist/databases/query/validate.js +138 -0
- package/dist/databases/query/values.d.ts +4 -0
- package/dist/databases/query/values.js +36 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +13 -5
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { compareCodePoints } from '../query/values.js';
|
|
2
|
+
import { analyzeFor, schemaFor } from './analyze.js';
|
|
3
|
+
import { COMPARISONS, walkFormula, } from './ast.js';
|
|
4
|
+
import { FormulaError, isFormulaError, joinText, toFormulaError } from './errors.js';
|
|
5
|
+
import { expectLogical, getFunction } from './functions.js';
|
|
6
|
+
import { FORMULA_LIMITS } from './limits.js';
|
|
7
|
+
import { cellReader, isEmptyScalar, optionIds } from './values.js';
|
|
8
|
+
const typeOf = (value) => value === null
|
|
9
|
+
? 'blank'
|
|
10
|
+
: value instanceof Date
|
|
11
|
+
? 'date'
|
|
12
|
+
: typeof value === 'string'
|
|
13
|
+
? 'text'
|
|
14
|
+
: typeof value;
|
|
15
|
+
const fail = (message, span, code = 'type') => new FormulaError(code, message, span.start, span.end);
|
|
16
|
+
const settle = (value, span) => {
|
|
17
|
+
if (typeof value === 'number') {
|
|
18
|
+
if (!Number.isFinite(value))
|
|
19
|
+
throw fail('Result is not a finite number', span, 'runtime');
|
|
20
|
+
return value === 0 ? 0 : value;
|
|
21
|
+
}
|
|
22
|
+
if (value instanceof Date && Number.isNaN(value.getTime())) {
|
|
23
|
+
throw fail('Date is out of range', span, 'runtime');
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
const holds = (operator, order) => {
|
|
28
|
+
switch (operator) {
|
|
29
|
+
case '==':
|
|
30
|
+
return order === 0;
|
|
31
|
+
case '!=':
|
|
32
|
+
return order !== 0;
|
|
33
|
+
case '<':
|
|
34
|
+
return order < 0;
|
|
35
|
+
case '<=':
|
|
36
|
+
return order <= 0;
|
|
37
|
+
case '>':
|
|
38
|
+
return order > 0;
|
|
39
|
+
default:
|
|
40
|
+
return order >= 0;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const compareValues = (operator, left, right, span) => {
|
|
44
|
+
if (isEmptyScalar(left) || isEmptyScalar(right))
|
|
45
|
+
return operator === '!=';
|
|
46
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
47
|
+
return holds(operator, Math.sign(left - right));
|
|
48
|
+
}
|
|
49
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
50
|
+
return holds(operator, compareCodePoints(left, right));
|
|
51
|
+
}
|
|
52
|
+
if (left instanceof Date && right instanceof Date) {
|
|
53
|
+
return holds(operator, Math.sign(left.getTime() - right.getTime()));
|
|
54
|
+
}
|
|
55
|
+
const equality = operator === '==' || operator === '!=';
|
|
56
|
+
if (typeof left === 'boolean' && typeof right === 'boolean' && equality) {
|
|
57
|
+
return (left === right) === (operator === '==');
|
|
58
|
+
}
|
|
59
|
+
if (equality)
|
|
60
|
+
return operator === '!=';
|
|
61
|
+
throw fail(`Cannot compare ${typeOf(left)} and ${typeOf(right)}`, span);
|
|
62
|
+
};
|
|
63
|
+
const MIRRORED = {
|
|
64
|
+
'==': '==',
|
|
65
|
+
'!=': '!=',
|
|
66
|
+
'<': '>',
|
|
67
|
+
'<=': '>=',
|
|
68
|
+
'>': '<',
|
|
69
|
+
'>=': '<=',
|
|
70
|
+
};
|
|
71
|
+
const withinDay = (operator, time, start, end) => {
|
|
72
|
+
switch (operator) {
|
|
73
|
+
case '==':
|
|
74
|
+
return time >= start && time < end;
|
|
75
|
+
case '!=':
|
|
76
|
+
return time < start || time >= end;
|
|
77
|
+
case '<':
|
|
78
|
+
return time < start;
|
|
79
|
+
case '<=':
|
|
80
|
+
return time < end;
|
|
81
|
+
case '>':
|
|
82
|
+
return time >= end;
|
|
83
|
+
default:
|
|
84
|
+
return time >= start;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const arithmetic = (operator, left, right, span) => {
|
|
88
|
+
switch (operator) {
|
|
89
|
+
case '-':
|
|
90
|
+
return left - right;
|
|
91
|
+
case '*':
|
|
92
|
+
return left * right;
|
|
93
|
+
case '/':
|
|
94
|
+
case '%':
|
|
95
|
+
if (right === 0)
|
|
96
|
+
throw fail('Division by zero', span, 'runtime');
|
|
97
|
+
return operator === '/' ? left / right : left % right;
|
|
98
|
+
default:
|
|
99
|
+
return Math.pow(left, right);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const sizeOf = (ast) => {
|
|
103
|
+
let size = 0;
|
|
104
|
+
walkFormula(ast, () => size++);
|
|
105
|
+
return size;
|
|
106
|
+
};
|
|
107
|
+
const TOO_COMPLEX = (span) => new FormulaError('too_complex', `Formula too complex: more than ${FORMULA_LIMITS.steps.toLocaleString('en-US')} steps for one row`, span.start, span.end);
|
|
108
|
+
const cacheable = (value) => !isFormulaError(value) || value.code !== 'too_complex';
|
|
109
|
+
class Runtime {
|
|
110
|
+
constructor(schema, clock) {
|
|
111
|
+
this.schema = schema;
|
|
112
|
+
this.programs = new Map();
|
|
113
|
+
this.pass = { id: 0, steps: 0, at: null };
|
|
114
|
+
const { pass } = this;
|
|
115
|
+
this.context = { now: () => { var _a; return ((_a = pass.at) !== null && _a !== void 0 ? _a : (pass.at = clock())); }, text: { used: 0, depth: 0 } };
|
|
116
|
+
}
|
|
117
|
+
program(column) {
|
|
118
|
+
let program = this.programs.get(column.id);
|
|
119
|
+
if (program)
|
|
120
|
+
return program;
|
|
121
|
+
const parsed = this.schema.parse(column);
|
|
122
|
+
const analysis = this.schema.analysis(column);
|
|
123
|
+
if ('error' in parsed || !analysis) {
|
|
124
|
+
const error = 'error' in parsed ? parsed.error : fail('Formula is invalid', parsed.ast);
|
|
125
|
+
program = () => error;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const run = this.build(parsed.ast, analysis);
|
|
129
|
+
const { pass } = this;
|
|
130
|
+
if (analysis.volatile) {
|
|
131
|
+
const memo = new WeakMap();
|
|
132
|
+
program = (row) => {
|
|
133
|
+
const known = memo.get(row);
|
|
134
|
+
if ((known === null || known === void 0 ? void 0 : known.pass) === pass.id && this.context.text.depth > 0)
|
|
135
|
+
return known.value;
|
|
136
|
+
const value = run(row);
|
|
137
|
+
if (cacheable(value))
|
|
138
|
+
memo.set(row, { pass: pass.id, value });
|
|
139
|
+
return value;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const memo = new WeakMap();
|
|
144
|
+
program = (row) => {
|
|
145
|
+
const known = memo.get(row);
|
|
146
|
+
if (known !== undefined)
|
|
147
|
+
return known;
|
|
148
|
+
const value = run(row);
|
|
149
|
+
if (cacheable(value))
|
|
150
|
+
memo.set(row, value);
|
|
151
|
+
return value;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
this.programs.set(column.id, program);
|
|
156
|
+
return program;
|
|
157
|
+
}
|
|
158
|
+
build(ast, analysis) {
|
|
159
|
+
const failure = analysis.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
|
|
160
|
+
if (failure)
|
|
161
|
+
return () => failure;
|
|
162
|
+
const closure = new Builder(this, analysis).node(ast);
|
|
163
|
+
const { text } = this.context;
|
|
164
|
+
const { pass } = this;
|
|
165
|
+
const size = sizeOf(ast);
|
|
166
|
+
return (row) => {
|
|
167
|
+
if (text.depth++ === 0) {
|
|
168
|
+
text.used = 0;
|
|
169
|
+
pass.id++;
|
|
170
|
+
pass.steps = 0;
|
|
171
|
+
pass.at = null;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
pass.steps += size;
|
|
175
|
+
if (pass.steps > FORMULA_LIMITS.steps)
|
|
176
|
+
return TOO_COMPLEX(ast);
|
|
177
|
+
return closure(row);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
return toFormulaError(error);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
text.depth--;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
class Builder {
|
|
189
|
+
constructor(runtime, analysis) {
|
|
190
|
+
this.runtime = runtime;
|
|
191
|
+
this.analysis = analysis;
|
|
192
|
+
}
|
|
193
|
+
node(node) {
|
|
194
|
+
switch (node.kind) {
|
|
195
|
+
case 'empty':
|
|
196
|
+
return () => null;
|
|
197
|
+
case 'number':
|
|
198
|
+
case 'string':
|
|
199
|
+
case 'boolean': {
|
|
200
|
+
const { value } = node;
|
|
201
|
+
return () => value;
|
|
202
|
+
}
|
|
203
|
+
case 'ref':
|
|
204
|
+
return this.ref(node);
|
|
205
|
+
case 'unary': {
|
|
206
|
+
const operand = this.node(node.operand);
|
|
207
|
+
if (node.operator === 'not')
|
|
208
|
+
return (row) => !expectLogical('not', operand(row), node);
|
|
209
|
+
const negate = node.operator === '-';
|
|
210
|
+
return (row) => {
|
|
211
|
+
const value = operand(row);
|
|
212
|
+
if (value === null)
|
|
213
|
+
return null;
|
|
214
|
+
if (typeof value !== 'number')
|
|
215
|
+
throw fail(`Expected a number for '${node.operator}'`, node);
|
|
216
|
+
return settle(negate ? -value : value, node);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
case 'binary':
|
|
220
|
+
return this.binary(node);
|
|
221
|
+
case 'call':
|
|
222
|
+
return this.call(node);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
ref(node) {
|
|
226
|
+
var _a;
|
|
227
|
+
const column = this.analysis.columns.get(node);
|
|
228
|
+
if (!column)
|
|
229
|
+
throw fail(`Unknown property '${(_a = node.id) !== null && _a !== void 0 ? _a : node.name}'`, node);
|
|
230
|
+
if (column.type === 'formula') {
|
|
231
|
+
const program = this.runtime.program(column);
|
|
232
|
+
return (row) => {
|
|
233
|
+
const value = program(row);
|
|
234
|
+
if (!isFormulaError(value))
|
|
235
|
+
return value;
|
|
236
|
+
throw new FormulaError(value.code, `${value.description} in '${column.name}'`, node.start, node.end);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const read = cellReader(column, this.runtime.schema.optionsOf(column));
|
|
240
|
+
const { id } = column;
|
|
241
|
+
return (row) => read(Object.hasOwn(row, id) ? row[id] : undefined);
|
|
242
|
+
}
|
|
243
|
+
labels(node) {
|
|
244
|
+
const column = node.kind === 'ref' ? this.analysis.columns.get(node) : undefined;
|
|
245
|
+
if ((column === null || column === void 0 ? void 0 : column.type) !== 'multi_select')
|
|
246
|
+
return undefined;
|
|
247
|
+
const { id } = column;
|
|
248
|
+
const { labelById } = this.runtime.schema.optionsOf(column);
|
|
249
|
+
return (row) => optionIds(Object.hasOwn(row, id) ? row[id] : undefined).map((option) => { var _a; return (_a = labelById.get(option)) !== null && _a !== void 0 ? _a : option; });
|
|
250
|
+
}
|
|
251
|
+
operand(node) {
|
|
252
|
+
const date = this.analysis.dates.get(node);
|
|
253
|
+
if (date)
|
|
254
|
+
return () => date;
|
|
255
|
+
return this.node(node);
|
|
256
|
+
}
|
|
257
|
+
compare(node) {
|
|
258
|
+
var _a;
|
|
259
|
+
const { operator } = node;
|
|
260
|
+
const multi = [node.left, node.right].find((side) => { var _a; return side.kind === 'ref' && ((_a = this.analysis.columns.get(side)) === null || _a === void 0 ? void 0 : _a.type) === 'multi_select'; });
|
|
261
|
+
if (multi) {
|
|
262
|
+
const read = this.labels(multi);
|
|
263
|
+
const other = multi === node.left ? node.right : node.left;
|
|
264
|
+
if (!read || other.kind !== 'string' || other.value === '')
|
|
265
|
+
return () => false;
|
|
266
|
+
const label = other.value;
|
|
267
|
+
return (row) => {
|
|
268
|
+
const labels = read(row);
|
|
269
|
+
const has = labels.includes(label);
|
|
270
|
+
return operator === '==' ? has : !has;
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if ([node.left, node.right].some((side) => side.kind === 'string' && side.value === '')) {
|
|
274
|
+
return () => false;
|
|
275
|
+
}
|
|
276
|
+
const span = { start: node.operatorStart, end: node.operatorEnd };
|
|
277
|
+
const dayOnRight = this.analysis.days.get(node.right);
|
|
278
|
+
const day = dayOnRight !== null && dayOnRight !== void 0 ? dayOnRight : this.analysis.days.get(node.left);
|
|
279
|
+
if (day) {
|
|
280
|
+
const other = this.operand(dayOnRight ? node.left : node.right);
|
|
281
|
+
const op = dayOnRight ? operator : ((_a = MIRRORED[operator]) !== null && _a !== void 0 ? _a : operator);
|
|
282
|
+
const start = day.start.getTime();
|
|
283
|
+
const end = day.end.getTime();
|
|
284
|
+
return (row) => {
|
|
285
|
+
const value = other(row);
|
|
286
|
+
if (isEmptyScalar(value))
|
|
287
|
+
return op === '!=';
|
|
288
|
+
if (!(value instanceof Date))
|
|
289
|
+
throw fail(`Cannot compare ${typeOf(value)} and date`, span);
|
|
290
|
+
return withinDay(op, value.getTime(), start, end);
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const left = this.operand(node.left);
|
|
294
|
+
const right = this.operand(node.right);
|
|
295
|
+
return (row) => compareValues(operator, left(row), right(row), span);
|
|
296
|
+
}
|
|
297
|
+
binary(node) {
|
|
298
|
+
const { operator } = node;
|
|
299
|
+
if (COMPARISONS.has(operator))
|
|
300
|
+
return this.compare(node);
|
|
301
|
+
const left = this.node(node.left);
|
|
302
|
+
const right = this.node(node.right);
|
|
303
|
+
const span = { start: node.operatorStart, end: node.operatorEnd };
|
|
304
|
+
if (operator === 'and') {
|
|
305
|
+
return (row) => expectLogical('and', left(row), node.left) && expectLogical('and', right(row), node.right);
|
|
306
|
+
}
|
|
307
|
+
if (operator === 'or') {
|
|
308
|
+
return (row) => expectLogical('or', left(row), node.left) || expectLogical('or', right(row), node.right);
|
|
309
|
+
}
|
|
310
|
+
if (operator === '+') {
|
|
311
|
+
return (row) => {
|
|
312
|
+
const a = left(row);
|
|
313
|
+
const b = right(row);
|
|
314
|
+
if (a === null || b === null) {
|
|
315
|
+
const other = a !== null && a !== void 0 ? a : b;
|
|
316
|
+
if (other === null || typeof other === 'number')
|
|
317
|
+
return null;
|
|
318
|
+
if (typeof other === 'string')
|
|
319
|
+
return other;
|
|
320
|
+
}
|
|
321
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
322
|
+
return settle(a + b, span);
|
|
323
|
+
if (typeof a === 'string' && typeof b === 'string')
|
|
324
|
+
return joinText([a, b], span.start, span.end, this.runtime.context.text);
|
|
325
|
+
throw fail(`Cannot add ${typeOf(a)} and ${typeOf(b)}`, span);
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return (row) => {
|
|
329
|
+
const a = left(row);
|
|
330
|
+
const b = right(row);
|
|
331
|
+
if (a !== null && typeof a !== 'number')
|
|
332
|
+
throw fail(`Expected a number for '${operator}'`, node.left);
|
|
333
|
+
if (b !== null && typeof b !== 'number')
|
|
334
|
+
throw fail(`Expected a number for '${operator}'`, node.right);
|
|
335
|
+
if (a === null || b === null)
|
|
336
|
+
return null;
|
|
337
|
+
return settle(arithmetic(operator, a, b, span), span);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
call(node) {
|
|
341
|
+
const member = this.analysis.memberships.get(node);
|
|
342
|
+
const [subject, needle] = node.args;
|
|
343
|
+
const read = member && subject ? this.labels(subject) : undefined;
|
|
344
|
+
if (read && (needle === null || needle === void 0 ? void 0 : needle.kind) === 'string') {
|
|
345
|
+
const label = needle.value;
|
|
346
|
+
return (row) => read(row).includes(label);
|
|
347
|
+
}
|
|
348
|
+
const constant = this.analysis.dates.get(node);
|
|
349
|
+
if (constant)
|
|
350
|
+
return () => constant;
|
|
351
|
+
const fn = getFunction(node.name);
|
|
352
|
+
if (!fn)
|
|
353
|
+
throw fail(`Unknown function '${node.name}'`, node);
|
|
354
|
+
const args = node.args.map((arg) => this.node(arg));
|
|
355
|
+
const { context } = this.runtime;
|
|
356
|
+
const { apply } = fn;
|
|
357
|
+
if (apply)
|
|
358
|
+
return (row) => settle(apply(args.map((arg) => arg(row)), node, context), node);
|
|
359
|
+
return (row) => settle(fn.call(args.map((arg) => () => arg(row)), node, context), node);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const runtimes = new WeakMap();
|
|
363
|
+
const runtimeFor = (schema, now) => {
|
|
364
|
+
if (now)
|
|
365
|
+
return new Runtime(schema, now);
|
|
366
|
+
let runtime = runtimes.get(schema);
|
|
367
|
+
if (!runtime) {
|
|
368
|
+
runtime = new Runtime(schema, () => new Date());
|
|
369
|
+
runtimes.set(schema, runtime);
|
|
370
|
+
}
|
|
371
|
+
return runtime;
|
|
372
|
+
};
|
|
373
|
+
export const compile = (ast, columns, options = {}) => {
|
|
374
|
+
try {
|
|
375
|
+
const analyzed = analyzeFor(ast, columns, options.columnId);
|
|
376
|
+
const runtime = runtimeFor(analyzed.schema, options.now);
|
|
377
|
+
if (analyzed.stored)
|
|
378
|
+
return runtime.program(analyzed.stored);
|
|
379
|
+
return runtime.build(analyzed.ast, analyzed.analysis);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
const failure = toFormulaError(error);
|
|
383
|
+
return () => failure;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const programsByColumns = new WeakMap();
|
|
387
|
+
export const compileColumns = (columns, options = {}) => {
|
|
388
|
+
const shared = options.now ? undefined : programsByColumns.get(columns);
|
|
389
|
+
if (shared)
|
|
390
|
+
return shared;
|
|
391
|
+
const runtime = runtimeFor(schemaFor(columns), options.now);
|
|
392
|
+
const programs = (columnId) => {
|
|
393
|
+
const column = runtime.schema.byId.get(columnId);
|
|
394
|
+
if ((column === null || column === void 0 ? void 0 : column.type) !== 'formula') {
|
|
395
|
+
const missing = new FormulaError('unknown_property', `Unknown formula property '#${columnId}'`, 0);
|
|
396
|
+
return () => missing;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
return runtime.program(column);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
const failure = toFormulaError(error);
|
|
403
|
+
return () => failure;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
if (!options.now)
|
|
407
|
+
programsByColumns.set(columns, programs);
|
|
408
|
+
return programs;
|
|
409
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const DATE_UNITS: readonly ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'];
|
|
2
|
+
export type DateUnit = (typeof DATE_UNITS)[number];
|
|
3
|
+
export declare const parseDateUnit: (unit: string, position: number) => DateUnit;
|
|
4
|
+
export declare const addToDate: (date: Date, amount: number, unit: DateUnit) => Date;
|
|
5
|
+
export declare const dateDifference: (first: Date, second: Date, unit: DateUnit) => number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { FormulaError } from './errors.js';
|
|
2
|
+
export const DATE_UNITS = [
|
|
3
|
+
'years',
|
|
4
|
+
'months',
|
|
5
|
+
'weeks',
|
|
6
|
+
'days',
|
|
7
|
+
'hours',
|
|
8
|
+
'minutes',
|
|
9
|
+
'seconds',
|
|
10
|
+
];
|
|
11
|
+
const UNIT_MS = {
|
|
12
|
+
weeks: 604800000,
|
|
13
|
+
days: 86400000,
|
|
14
|
+
hours: 3600000,
|
|
15
|
+
minutes: 60000,
|
|
16
|
+
seconds: 1000,
|
|
17
|
+
};
|
|
18
|
+
export const parseDateUnit = (unit, position) => {
|
|
19
|
+
const normalized = unit.trim().toLowerCase();
|
|
20
|
+
const match = DATE_UNITS.find((candidate) => candidate === normalized || candidate === `${normalized}s`);
|
|
21
|
+
if (!match)
|
|
22
|
+
throw new FormulaError('type', `Unknown date unit '${unit}'`, position);
|
|
23
|
+
return match;
|
|
24
|
+
};
|
|
25
|
+
const addMonths = (date, months) => {
|
|
26
|
+
const year = date.getUTCFullYear();
|
|
27
|
+
const month = date.getUTCMonth() + months;
|
|
28
|
+
const lastDay = new Date(0);
|
|
29
|
+
lastDay.setUTCFullYear(year, month + 1, 0);
|
|
30
|
+
const result = new Date(date.getTime());
|
|
31
|
+
result.setUTCFullYear(year, month, Math.min(date.getUTCDate(), lastDay.getUTCDate()));
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
const monthsBetween = (first, second) => {
|
|
35
|
+
if (first.getTime() < second.getTime())
|
|
36
|
+
return -monthsBetween(second, first);
|
|
37
|
+
const months = (first.getUTCFullYear() - second.getUTCFullYear()) * 12 +
|
|
38
|
+
first.getUTCMonth() -
|
|
39
|
+
second.getUTCMonth();
|
|
40
|
+
return addMonths(second, months).getTime() > first.getTime() ? months - 1 : months;
|
|
41
|
+
};
|
|
42
|
+
export const addToDate = (date, amount, unit) => {
|
|
43
|
+
if (unit === 'years')
|
|
44
|
+
return addMonths(date, Math.trunc(amount) * 12);
|
|
45
|
+
if (unit === 'months')
|
|
46
|
+
return addMonths(date, Math.trunc(amount));
|
|
47
|
+
return new Date(date.getTime() + amount * UNIT_MS[unit]);
|
|
48
|
+
};
|
|
49
|
+
export const dateDifference = (first, second, unit) => {
|
|
50
|
+
if (unit === 'years')
|
|
51
|
+
return Math.trunc(monthsBetween(first, second) / 12);
|
|
52
|
+
if (unit === 'months')
|
|
53
|
+
return monthsBetween(first, second);
|
|
54
|
+
return Math.trunc((first.getTime() - second.getTime()) / UNIT_MS[unit]);
|
|
55
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type FormulaErrorCode = 'syntax' | 'too_long' | 'depth' | 'unknown_property' | 'ambiguous_property' | 'unknown_function' | 'arity' | 'type' | 'cycle' | 'dependency' | 'unknown_option' | 'empty_comparison' | 'text_too_large' | 'too_complex' | 'runtime';
|
|
2
|
+
export type FormulaSeverity = 'error' | 'warning';
|
|
3
|
+
export declare class FormulaError extends Error {
|
|
4
|
+
readonly code: FormulaErrorCode;
|
|
5
|
+
readonly severity: FormulaSeverity;
|
|
6
|
+
readonly description: string;
|
|
7
|
+
readonly start: number;
|
|
8
|
+
readonly end: number;
|
|
9
|
+
constructor(code: FormulaErrorCode, description: string, start: number, end?: number, severity?: FormulaSeverity);
|
|
10
|
+
}
|
|
11
|
+
export declare const isFormulaError: (value: unknown) => value is FormulaError;
|
|
12
|
+
export declare const toFormulaError: (error: unknown) => FormulaError;
|
|
13
|
+
export type TextBudget = {
|
|
14
|
+
used: number;
|
|
15
|
+
depth: number;
|
|
16
|
+
};
|
|
17
|
+
export declare const joinText: (parts: readonly string[], start: number, end: number, budget: TextBudget) => string;
|
|
18
|
+
export declare const limitText: (text: string, start: number, end: number, budget: TextBudget) => string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FORMULA_LIMITS } from './limits.js';
|
|
2
|
+
export class FormulaError extends Error {
|
|
3
|
+
constructor(code, description, start, end = start, severity = 'error') {
|
|
4
|
+
super(`${description} at ${start}`);
|
|
5
|
+
this.name = 'FormulaError';
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.severity = severity;
|
|
8
|
+
this.description = description;
|
|
9
|
+
this.start = start;
|
|
10
|
+
this.end = end;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const isFormulaError = (value) => value instanceof FormulaError;
|
|
14
|
+
export const toFormulaError = (error) => {
|
|
15
|
+
if (error instanceof FormulaError)
|
|
16
|
+
return error;
|
|
17
|
+
if (error instanceof RangeError)
|
|
18
|
+
return new FormulaError('runtime', 'Formula is too complex', 0);
|
|
19
|
+
return new FormulaError('runtime', 'Unexpected error', 0);
|
|
20
|
+
};
|
|
21
|
+
const textBytes = (length) => length * 2;
|
|
22
|
+
export const joinText = (parts, start, end, budget) => {
|
|
23
|
+
const bytes = textBytes(parts.reduce((total, part) => total + part.length, 0));
|
|
24
|
+
if (bytes > FORMULA_LIMITS.textBytes) {
|
|
25
|
+
throw new FormulaError('text_too_large', 'Text result exceeds 100 KB', start, end);
|
|
26
|
+
}
|
|
27
|
+
budget.used += bytes;
|
|
28
|
+
if (budget.used > FORMULA_LIMITS.rowTextBytes) {
|
|
29
|
+
throw new FormulaError('text_too_large', 'Formula builds more than 1 MB of text for this row', start, end);
|
|
30
|
+
}
|
|
31
|
+
return parts.join('');
|
|
32
|
+
};
|
|
33
|
+
export const limitText = (text, start, end, budget) => joinText([text], start, end, budget);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FormulaColumn, FormulaRow } from './types.js';
|
|
2
|
+
export type FormulaFilter = {
|
|
3
|
+
test: (row: {
|
|
4
|
+
columns: FormulaRow;
|
|
5
|
+
}) => boolean;
|
|
6
|
+
} | {
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const formulaFilterEngine: (source: string, columns: readonly FormulaColumn[]) => FormulaFilter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { compile } from './compile.js';
|
|
2
|
+
import { parseFormula } from './parse.js';
|
|
3
|
+
import { typecheck } from './typecheck.js';
|
|
4
|
+
export const formulaFilterEngine = (source, columns) => {
|
|
5
|
+
const parsed = parseFormula(source);
|
|
6
|
+
if ('error' in parsed)
|
|
7
|
+
return { error: parsed.error.message };
|
|
8
|
+
const { type, diagnostics } = typecheck(parsed.ast, columns);
|
|
9
|
+
const failure = diagnostics.find((diagnostic) => diagnostic.severity === 'error');
|
|
10
|
+
if (failure)
|
|
11
|
+
return { error: failure.message };
|
|
12
|
+
if (type !== 'boolean')
|
|
13
|
+
return { error: 'A filter must be true or false' };
|
|
14
|
+
const run = compile(parsed.ast, columns);
|
|
15
|
+
return { test: (row) => run(row.columns) === true };
|
|
16
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FormulaNode } from './ast.js';
|
|
2
|
+
export declare const quoteString: (value: string) => string;
|
|
3
|
+
export declare const columnIdReference: (id: string) => string;
|
|
4
|
+
export declare const columnReference: (name: string) => string;
|
|
5
|
+
export declare const formatFormula: (ast: FormulaNode) => string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PRECEDENCE } from './parse.js';
|
|
2
|
+
import { isBareName } from './tokenize.js';
|
|
3
|
+
const ESCAPED = {
|
|
4
|
+
'\\': '\\\\',
|
|
5
|
+
'"': '\\"',
|
|
6
|
+
'\n': '\\n',
|
|
7
|
+
'\r': '\\r',
|
|
8
|
+
'\t': '\\t',
|
|
9
|
+
};
|
|
10
|
+
export const quoteString = (value) => `"${value.replace(/[\\"\n\r\t]/g, (char) => { var _a; return (_a = ESCAPED[char]) !== null && _a !== void 0 ? _a : char; })}"`;
|
|
11
|
+
export const columnIdReference = (id) => `prop(#${id})`;
|
|
12
|
+
export const columnReference = (name) => isBareName(name) ? name : `prop(${quoteString(name)})`;
|
|
13
|
+
const BINARY_POWER = {
|
|
14
|
+
or: PRECEDENCE.or,
|
|
15
|
+
and: PRECEDENCE.and,
|
|
16
|
+
'==': PRECEDENCE.compare,
|
|
17
|
+
'!=': PRECEDENCE.compare,
|
|
18
|
+
'<': PRECEDENCE.compare,
|
|
19
|
+
'<=': PRECEDENCE.compare,
|
|
20
|
+
'>': PRECEDENCE.compare,
|
|
21
|
+
'>=': PRECEDENCE.compare,
|
|
22
|
+
'+': PRECEDENCE.sum,
|
|
23
|
+
'-': PRECEDENCE.sum,
|
|
24
|
+
'*': PRECEDENCE.product,
|
|
25
|
+
'/': PRECEDENCE.product,
|
|
26
|
+
'%': PRECEDENCE.product,
|
|
27
|
+
'^': PRECEDENCE.power,
|
|
28
|
+
};
|
|
29
|
+
const ATOM = 10;
|
|
30
|
+
const powerOf = (node) => {
|
|
31
|
+
if (node.kind === 'binary')
|
|
32
|
+
return BINARY_POWER[node.operator];
|
|
33
|
+
if (node.kind === 'unary')
|
|
34
|
+
return node.operator === 'not' ? PRECEDENCE.not : PRECEDENCE.sign;
|
|
35
|
+
return ATOM;
|
|
36
|
+
};
|
|
37
|
+
const formatNode = (node, minPower) => {
|
|
38
|
+
const text = formatBare(node);
|
|
39
|
+
return powerOf(node) < minPower ? `(${text})` : text;
|
|
40
|
+
};
|
|
41
|
+
const formatBare = (node) => {
|
|
42
|
+
switch (node.kind) {
|
|
43
|
+
case 'empty':
|
|
44
|
+
return '';
|
|
45
|
+
case 'number':
|
|
46
|
+
return String(node.value);
|
|
47
|
+
case 'string':
|
|
48
|
+
return quoteString(node.value);
|
|
49
|
+
case 'boolean':
|
|
50
|
+
return String(node.value);
|
|
51
|
+
case 'ref':
|
|
52
|
+
return node.id === undefined ? columnReference(node.name) : columnIdReference(node.id);
|
|
53
|
+
case 'unary':
|
|
54
|
+
return node.operator === 'not'
|
|
55
|
+
? `not ${formatNode(node.operand, PRECEDENCE.not)}`
|
|
56
|
+
: `${node.operator}${formatNode(node.operand, PRECEDENCE.sign)}`;
|
|
57
|
+
case 'binary': {
|
|
58
|
+
const power = BINARY_POWER[node.operator];
|
|
59
|
+
const right = node.operator === '^';
|
|
60
|
+
return `${formatNode(node.left, right ? power + 1 : power)} ${node.operator} ${formatNode(node.right, right ? power : power + 1)}`;
|
|
61
|
+
}
|
|
62
|
+
case 'call':
|
|
63
|
+
return `${node.name}(${node.args.map((arg) => formatNode(arg, 0)).join(', ')})`;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
export const formatFormula = (ast) => formatNode(ast, 0);
|