@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,209 @@
|
|
|
1
|
+
import { FormulaError, toFormulaError } from './errors.js';
|
|
2
|
+
import { FORMULA_LIMITS } from './limits.js';
|
|
3
|
+
import { isKeyword, tokenize } from './tokenize.js';
|
|
4
|
+
export const PRECEDENCE = {
|
|
5
|
+
or: 1,
|
|
6
|
+
and: 2,
|
|
7
|
+
not: 3,
|
|
8
|
+
compare: 4,
|
|
9
|
+
sum: 5,
|
|
10
|
+
product: 6,
|
|
11
|
+
sign: 7,
|
|
12
|
+
power: 8,
|
|
13
|
+
};
|
|
14
|
+
const INFIX = {
|
|
15
|
+
or: { power: PRECEDENCE.or, operator: 'or' },
|
|
16
|
+
and: { power: PRECEDENCE.and, operator: 'and' },
|
|
17
|
+
'==': { power: PRECEDENCE.compare, operator: '==' },
|
|
18
|
+
'=': { power: PRECEDENCE.compare, operator: '==' },
|
|
19
|
+
'!=': { power: PRECEDENCE.compare, operator: '!=' },
|
|
20
|
+
'<': { power: PRECEDENCE.compare, operator: '<' },
|
|
21
|
+
'<=': { power: PRECEDENCE.compare, operator: '<=' },
|
|
22
|
+
'>': { power: PRECEDENCE.compare, operator: '>' },
|
|
23
|
+
'>=': { power: PRECEDENCE.compare, operator: '>=' },
|
|
24
|
+
'+': { power: PRECEDENCE.sum, operator: '+' },
|
|
25
|
+
'-': { power: PRECEDENCE.sum, operator: '-' },
|
|
26
|
+
'*': { power: PRECEDENCE.product, operator: '*' },
|
|
27
|
+
'/': { power: PRECEDENCE.product, operator: '/' },
|
|
28
|
+
'%': { power: PRECEDENCE.product, operator: '%' },
|
|
29
|
+
'^': { power: PRECEDENCE.power, operator: '^', rightAssociative: true },
|
|
30
|
+
};
|
|
31
|
+
const infixOf = (token) => {
|
|
32
|
+
if (token.type !== 'symbol' && token.type !== 'identifier')
|
|
33
|
+
return undefined;
|
|
34
|
+
const key = token.type === 'identifier' ? token.value.toLowerCase() : token.value;
|
|
35
|
+
return Object.hasOwn(INFIX, key) ? INFIX[key] : undefined;
|
|
36
|
+
};
|
|
37
|
+
const unexpected = (token) => token.type === 'eof'
|
|
38
|
+
? new FormulaError('syntax', 'Unexpected end of formula', token.start)
|
|
39
|
+
: new FormulaError('syntax', `Unexpected '${token.type === 'string' ? 'string' : token.type === 'columnId' ? `#${token.value}` : token.value}'`, token.start, token.end);
|
|
40
|
+
const parseTokens = (tokens) => {
|
|
41
|
+
let index = 0;
|
|
42
|
+
const peek = (offset = 0) => tokens[Math.min(index + offset, tokens.length - 1)];
|
|
43
|
+
const isSymbol = (token, value) => token.type === 'symbol' && token.value === value;
|
|
44
|
+
const expect = (value) => {
|
|
45
|
+
const token = peek();
|
|
46
|
+
if (!isSymbol(token, value)) {
|
|
47
|
+
throw new FormulaError('syntax', `Expected '${value}'`, token.start, token.end);
|
|
48
|
+
}
|
|
49
|
+
index++;
|
|
50
|
+
return token;
|
|
51
|
+
};
|
|
52
|
+
const parseCall = (name, depth) => {
|
|
53
|
+
index += 2;
|
|
54
|
+
const target = peek();
|
|
55
|
+
if (name.value === 'prop' && target.type === 'columnId' && isSymbol(peek(1), ')')) {
|
|
56
|
+
index++;
|
|
57
|
+
const end = expect(')').end;
|
|
58
|
+
return {
|
|
59
|
+
kind: 'ref',
|
|
60
|
+
name: '',
|
|
61
|
+
id: target.value,
|
|
62
|
+
form: 'id',
|
|
63
|
+
nameStart: target.start,
|
|
64
|
+
nameEnd: target.end,
|
|
65
|
+
start: name.start,
|
|
66
|
+
end,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const args = [];
|
|
70
|
+
if (!isSymbol(peek(), ')')) {
|
|
71
|
+
for (;;) {
|
|
72
|
+
args.push(parseExpression(0, depth + 1));
|
|
73
|
+
if (!isSymbol(peek(), ','))
|
|
74
|
+
break;
|
|
75
|
+
index++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const end = expect(')').end;
|
|
79
|
+
if (name.value !== 'prop') {
|
|
80
|
+
return { kind: 'call', name: name.value, args, start: name.start, end };
|
|
81
|
+
}
|
|
82
|
+
const [arg] = args;
|
|
83
|
+
if (args.length !== 1 || !arg) {
|
|
84
|
+
throw new FormulaError('arity', 'prop expects 1 argument', name.start, end);
|
|
85
|
+
}
|
|
86
|
+
if (arg.kind !== 'string') {
|
|
87
|
+
throw new FormulaError('syntax', 'prop expects a string literal', arg.start, arg.end);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
kind: 'ref',
|
|
91
|
+
name: arg.value,
|
|
92
|
+
form: 'prop',
|
|
93
|
+
nameStart: arg.start,
|
|
94
|
+
nameEnd: arg.end,
|
|
95
|
+
start: name.start,
|
|
96
|
+
end,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const parseIdentifier = (token, depth) => {
|
|
100
|
+
const keyword = token.value.toLowerCase();
|
|
101
|
+
if (keyword === 'true' || keyword === 'false') {
|
|
102
|
+
index++;
|
|
103
|
+
return { kind: 'boolean', value: keyword === 'true', start: token.start, end: token.end };
|
|
104
|
+
}
|
|
105
|
+
if (keyword === 'not') {
|
|
106
|
+
index++;
|
|
107
|
+
const operand = parseExpression(PRECEDENCE.not, depth + 1);
|
|
108
|
+
return { kind: 'unary', operator: 'not', operand, start: token.start, end: operand.end };
|
|
109
|
+
}
|
|
110
|
+
if (isKeyword(token.value))
|
|
111
|
+
throw unexpected(token);
|
|
112
|
+
if (isSymbol(peek(1), '('))
|
|
113
|
+
return parseCall(token, depth);
|
|
114
|
+
index++;
|
|
115
|
+
return {
|
|
116
|
+
kind: 'ref',
|
|
117
|
+
name: token.value,
|
|
118
|
+
form: 'bare',
|
|
119
|
+
nameStart: token.start,
|
|
120
|
+
nameEnd: token.end,
|
|
121
|
+
start: token.start,
|
|
122
|
+
end: token.end,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const parsePrefix = (depth) => {
|
|
126
|
+
const token = peek();
|
|
127
|
+
if (token.type === 'number') {
|
|
128
|
+
index++;
|
|
129
|
+
const value = Number(token.value);
|
|
130
|
+
if (!Number.isFinite(value)) {
|
|
131
|
+
throw new FormulaError('syntax', 'Number is too large', token.start, token.end);
|
|
132
|
+
}
|
|
133
|
+
return { kind: 'number', value, start: token.start, end: token.end };
|
|
134
|
+
}
|
|
135
|
+
if (token.type === 'string') {
|
|
136
|
+
index++;
|
|
137
|
+
return { kind: 'string', value: token.value, start: token.start, end: token.end };
|
|
138
|
+
}
|
|
139
|
+
if (token.type === 'identifier')
|
|
140
|
+
return parseIdentifier(token, depth);
|
|
141
|
+
if (isSymbol(token, '(')) {
|
|
142
|
+
index++;
|
|
143
|
+
const inner = parseExpression(0, depth + 1);
|
|
144
|
+
expect(')');
|
|
145
|
+
return inner;
|
|
146
|
+
}
|
|
147
|
+
if (isSymbol(token, '-') || isSymbol(token, '+')) {
|
|
148
|
+
index++;
|
|
149
|
+
const operand = parseExpression(PRECEDENCE.sign, depth + 1);
|
|
150
|
+
return {
|
|
151
|
+
kind: 'unary',
|
|
152
|
+
operator: token.value === '-' ? '-' : '+',
|
|
153
|
+
operand,
|
|
154
|
+
start: token.start,
|
|
155
|
+
end: operand.end,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
throw unexpected(token);
|
|
159
|
+
};
|
|
160
|
+
const parseExpression = (minPower, depth) => {
|
|
161
|
+
if (depth > FORMULA_LIMITS.depth) {
|
|
162
|
+
const token = peek();
|
|
163
|
+
throw new FormulaError('depth', 'Formula is nested too deeply', token.start, token.end);
|
|
164
|
+
}
|
|
165
|
+
let left = parsePrefix(depth);
|
|
166
|
+
for (;;) {
|
|
167
|
+
const token = peek();
|
|
168
|
+
const infix = infixOf(token);
|
|
169
|
+
if (!infix || infix.power < minPower)
|
|
170
|
+
return left;
|
|
171
|
+
index++;
|
|
172
|
+
const right = parseExpression(infix.rightAssociative ? infix.power : infix.power + 1, depth + 1);
|
|
173
|
+
left = {
|
|
174
|
+
kind: 'binary',
|
|
175
|
+
operator: infix.operator,
|
|
176
|
+
operatorStart: token.start,
|
|
177
|
+
operatorEnd: token.end,
|
|
178
|
+
left,
|
|
179
|
+
right,
|
|
180
|
+
start: left.start,
|
|
181
|
+
end: right.end,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
if (peek().type === 'eof')
|
|
186
|
+
return { kind: 'empty', start: 0, end: 0 };
|
|
187
|
+
const ast = parseExpression(0, 0);
|
|
188
|
+
if (peek().type !== 'eof')
|
|
189
|
+
throw unexpected(peek());
|
|
190
|
+
return ast;
|
|
191
|
+
};
|
|
192
|
+
const encoder = new TextEncoder();
|
|
193
|
+
export const parseFormula = (source, { maxBytes = FORMULA_LIMITS.sourceBytes } = {}) => {
|
|
194
|
+
try {
|
|
195
|
+
if (typeof source !== 'string') {
|
|
196
|
+
return { error: new FormulaError('syntax', 'Formula must be text', 0) };
|
|
197
|
+
}
|
|
198
|
+
const bytes = encoder.encode(source).byteLength;
|
|
199
|
+
if (bytes > maxBytes) {
|
|
200
|
+
return {
|
|
201
|
+
error: new FormulaError('too_long', `Formula exceeds ${maxBytes} bytes`, 0, source.length),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { ast: parseTokens(tokenize(source)) };
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return { error: toFormulaError(error) };
|
|
208
|
+
}
|
|
209
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FormulaNode } from './ast.js';
|
|
2
|
+
import { FormulaError } from './errors.js';
|
|
3
|
+
import type { FormulaColumn } from './types.js';
|
|
4
|
+
export type FormulaReference = {
|
|
5
|
+
id: string | null;
|
|
6
|
+
name: string | null;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const referencedColumns: (ast: FormulaNode, columns?: readonly Pick<FormulaColumn, 'id' | 'name'>[]) => FormulaReference[];
|
|
11
|
+
export type StoredFormulaResult = {
|
|
12
|
+
source: string;
|
|
13
|
+
} | {
|
|
14
|
+
error: FormulaError;
|
|
15
|
+
};
|
|
16
|
+
export declare const toStoredFormula: (source: string, columns: readonly Pick<FormulaColumn, 'id' | 'name'>[]) => StoredFormulaResult;
|
|
17
|
+
export declare const toDisplayFormula: (source: string, columns: readonly Pick<FormulaColumn, 'id' | 'name'>[]) => string;
|
|
18
|
+
export declare const displayDiagnostics: (display: string, columns: readonly Pick<FormulaColumn, 'id' | 'name'>[]) => FormulaError[];
|
|
19
|
+
export declare const isStoredFormula: (source: string) => boolean;
|
|
20
|
+
export declare const renameColumn: (source: string, oldName: string, newName: string) => string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { FormulaError } from './errors.js';
|
|
2
|
+
import { columnIdReference, columnReference, quoteString } from './format.js';
|
|
3
|
+
import { FORMULA_LIMITS } from './limits.js';
|
|
4
|
+
import { parseFormula } from './parse.js';
|
|
5
|
+
import { refsOf, rewrite } from './rewrite.js';
|
|
6
|
+
const lookup = (columns) => {
|
|
7
|
+
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
8
|
+
const byName = new Map();
|
|
9
|
+
const ambiguous = new Set();
|
|
10
|
+
for (const column of columns) {
|
|
11
|
+
if (byName.has(column.name))
|
|
12
|
+
ambiguous.add(column.name);
|
|
13
|
+
else
|
|
14
|
+
byName.set(column.name, column);
|
|
15
|
+
}
|
|
16
|
+
return { byId, byName, ambiguous };
|
|
17
|
+
};
|
|
18
|
+
export const referencedColumns = (ast, columns) => {
|
|
19
|
+
const { byId, byName } = lookup(columns !== null && columns !== void 0 ? columns : []);
|
|
20
|
+
return refsOf(ast).map((node) => { var _a, _b; var _c, _d; return node.id === undefined
|
|
21
|
+
? { id: (_c = (_a = byName.get(node.name)) === null || _a === void 0 ? void 0 : _a.id) !== null && _c !== void 0 ? _c : null, name: node.name, start: node.start, end: node.end }
|
|
22
|
+
: { id: node.id, name: (_d = (_b = byId.get(node.id)) === null || _b === void 0 ? void 0 : _b.name) !== null && _d !== void 0 ? _d : null, start: node.start, end: node.end }; });
|
|
23
|
+
};
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
export const toStoredFormula = (source, columns) => {
|
|
26
|
+
const { byName } = lookup(columns);
|
|
27
|
+
const stored = rewrite(source, (node) => {
|
|
28
|
+
const column = node.id === undefined ? byName.get(node.name) : undefined;
|
|
29
|
+
return column ? { start: node.start, end: node.end, text: columnIdReference(column.id) } : null;
|
|
30
|
+
});
|
|
31
|
+
if (encoder.encode(stored).byteLength <= FORMULA_LIMITS.sourceBytes)
|
|
32
|
+
return { source: stored };
|
|
33
|
+
return {
|
|
34
|
+
error: new FormulaError('too_long', `Formula exceeds ${FORMULA_LIMITS.sourceBytes} bytes once property names are saved as ids`, 0, source.length),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export const toDisplayFormula = (source, columns) => {
|
|
38
|
+
const { byId, ambiguous } = lookup(columns);
|
|
39
|
+
return rewrite(source, (node) => {
|
|
40
|
+
const column = node.id === undefined ? undefined : byId.get(node.id);
|
|
41
|
+
if (!column || ambiguous.has(column.name))
|
|
42
|
+
return null;
|
|
43
|
+
return { start: node.start, end: node.end, text: columnReference(column.name) };
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
export const displayDiagnostics = (display, columns) => {
|
|
47
|
+
const parsed = parseFormula(display, { maxBytes: FORMULA_LIMITS.draftBytes });
|
|
48
|
+
if ('error' in parsed)
|
|
49
|
+
return [];
|
|
50
|
+
const { byId, ambiguous } = lookup(columns);
|
|
51
|
+
return refsOf(parsed.ast).flatMap((node) => {
|
|
52
|
+
var _a;
|
|
53
|
+
const name = node.id === undefined ? node.name : (_a = byId.get(node.id)) === null || _a === void 0 ? void 0 : _a.name;
|
|
54
|
+
if (name === undefined || !ambiguous.has(name))
|
|
55
|
+
return [];
|
|
56
|
+
const message = node.id === undefined
|
|
57
|
+
? `More than one property is named '${name}'; this uses the first`
|
|
58
|
+
: `More than one property is named '${name}'; shown by id`;
|
|
59
|
+
return [new FormulaError('ambiguous_property', message, node.start, node.end, 'warning')];
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
export const isStoredFormula = (source) => {
|
|
63
|
+
const parsed = parseFormula(source);
|
|
64
|
+
return 'ast' in parsed && refsOf(parsed.ast).every((node) => node.id !== undefined);
|
|
65
|
+
};
|
|
66
|
+
export const renameColumn = (source, oldName, newName) => {
|
|
67
|
+
if (oldName === newName)
|
|
68
|
+
return source;
|
|
69
|
+
return rewrite(source, (node) => {
|
|
70
|
+
if (node.id !== undefined || node.name !== oldName)
|
|
71
|
+
return null;
|
|
72
|
+
return node.form === 'prop'
|
|
73
|
+
? { start: node.nameStart, end: node.nameEnd, text: quoteString(newName) }
|
|
74
|
+
: { start: node.start, end: node.end, text: columnReference(newName) };
|
|
75
|
+
});
|
|
76
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type FormulaNode, type FormulaRefNode } from './ast.js';
|
|
2
|
+
export type Edit = {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const refsOf: (ast: FormulaNode) => (import("./ast.js").Span & {
|
|
8
|
+
kind: 'ref';
|
|
9
|
+
name: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
form: 'bare' | 'prop' | 'id';
|
|
12
|
+
nameStart: number;
|
|
13
|
+
nameEnd: number;
|
|
14
|
+
})[];
|
|
15
|
+
export declare const rewrite: (source: string, edit: (node: FormulaRefNode) => Edit | null) => string;
|
|
16
|
+
export declare const rewriteColumnIds: (source: string, resolve: (id: string) => string) => string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { walkFormula } from './ast.js';
|
|
2
|
+
import { columnIdReference } from './format.js';
|
|
3
|
+
import { FORMULA_LIMITS } from './limits.js';
|
|
4
|
+
import { parseFormula } from './parse.js';
|
|
5
|
+
export const refsOf = (ast) => {
|
|
6
|
+
const refs = [];
|
|
7
|
+
walkFormula(ast, (node) => {
|
|
8
|
+
if (node.kind === 'ref')
|
|
9
|
+
refs.push(node);
|
|
10
|
+
});
|
|
11
|
+
return refs.sort((a, b) => a.start - b.start);
|
|
12
|
+
};
|
|
13
|
+
const applyEdits = (source, edits) => edits
|
|
14
|
+
.sort((a, b) => b.start - a.start)
|
|
15
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end), source);
|
|
16
|
+
export const rewrite = (source, edit) => {
|
|
17
|
+
const parsed = parseFormula(source, { maxBytes: FORMULA_LIMITS.draftBytes });
|
|
18
|
+
if ('error' in parsed)
|
|
19
|
+
return source;
|
|
20
|
+
return applyEdits(source, refsOf(parsed.ast).flatMap((node) => { var _a; return (_a = edit(node)) !== null && _a !== void 0 ? _a : []; }));
|
|
21
|
+
};
|
|
22
|
+
export const rewriteColumnIds = (source, resolve) => rewrite(source, (node) => {
|
|
23
|
+
if (node.id === undefined)
|
|
24
|
+
return null;
|
|
25
|
+
const next = resolve(node.id);
|
|
26
|
+
return next === node.id
|
|
27
|
+
? null
|
|
28
|
+
: { start: node.start, end: node.end, text: columnIdReference(next) };
|
|
29
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Analysis } from './analyze.js';
|
|
2
|
+
import type { FormulaNode } from './ast.js';
|
|
3
|
+
import { type FormulaParseResult } from './parse.js';
|
|
4
|
+
import { type FormulaColumn, type TypeSet } from './types.js';
|
|
5
|
+
export type OptionMaps = {
|
|
6
|
+
idsByLabel: Map<string, string[]>;
|
|
7
|
+
labelById: Map<string, string>;
|
|
8
|
+
};
|
|
9
|
+
export type Analyzer = (ast: FormulaNode, schema: Schema) => Analysis;
|
|
10
|
+
type FormulaInfo = Pick<Analysis, 'type' | 'failed' | 'cyclic' | 'height' | 'volatile' | 'truncated'>;
|
|
11
|
+
export declare class Schema {
|
|
12
|
+
readonly columns: readonly FormulaColumn[];
|
|
13
|
+
private readonly analyzer;
|
|
14
|
+
private readonly overrides;
|
|
15
|
+
readonly byId: Map<string, FormulaColumn>;
|
|
16
|
+
readonly byName: Map<string, FormulaColumn>;
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly parsed;
|
|
19
|
+
private readonly analyses;
|
|
20
|
+
private active;
|
|
21
|
+
constructor(columns: readonly FormulaColumn[], analyzer: Analyzer, overrides?: ReadonlyMap<string, FormulaParseResult>);
|
|
22
|
+
withExpression(columnId: string, parsed: FormulaParseResult): Schema;
|
|
23
|
+
optionsOf(column: FormulaColumn): OptionMaps;
|
|
24
|
+
storedType(column: FormulaColumn): TypeSet;
|
|
25
|
+
parse(column: FormulaColumn): FormulaParseResult;
|
|
26
|
+
formula(column: FormulaColumn): FormulaInfo;
|
|
27
|
+
analysis(column: FormulaColumn): Analysis | undefined;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { FORMULA_LIMITS } from './limits.js';
|
|
2
|
+
import { parseFormula } from './parse.js';
|
|
3
|
+
import { ANY, BOOLEAN, DATE, NUMBER, STRING } from './types.js';
|
|
4
|
+
const STORED_TYPES = {
|
|
5
|
+
number: NUMBER,
|
|
6
|
+
text: STRING,
|
|
7
|
+
url: STRING,
|
|
8
|
+
email: STRING,
|
|
9
|
+
select: STRING,
|
|
10
|
+
multi_select: STRING,
|
|
11
|
+
json: STRING,
|
|
12
|
+
boolean: BOOLEAN,
|
|
13
|
+
date: DATE,
|
|
14
|
+
};
|
|
15
|
+
const FLAGS = { volatile: false, truncated: false };
|
|
16
|
+
const CYCLIC = Object.assign(Object.assign({}, FLAGS), { type: ANY, failed: true, cyclic: true, height: 0 });
|
|
17
|
+
const BROKEN = Object.assign(Object.assign({}, FLAGS), { type: ANY, failed: true, cyclic: false, height: 0 });
|
|
18
|
+
const TOO_DEEP = Object.assign(Object.assign({}, FLAGS), { truncated: true, type: ANY, failed: true, cyclic: false, height: FORMULA_LIMITS.depth });
|
|
19
|
+
const optionMaps = (column) => {
|
|
20
|
+
var _a;
|
|
21
|
+
const idsByLabel = new Map();
|
|
22
|
+
const labelById = new Map();
|
|
23
|
+
for (const option of (_a = column.options) !== null && _a !== void 0 ? _a : []) {
|
|
24
|
+
const { id, label } = typeof option === 'string' ? { id: option, label: option } : option;
|
|
25
|
+
const ids = idsByLabel.get(label);
|
|
26
|
+
if (!ids)
|
|
27
|
+
idsByLabel.set(label, [id]);
|
|
28
|
+
else if (!ids.includes(id))
|
|
29
|
+
ids.push(id);
|
|
30
|
+
labelById.set(id, label);
|
|
31
|
+
}
|
|
32
|
+
return { idsByLabel, labelById };
|
|
33
|
+
};
|
|
34
|
+
export class Schema {
|
|
35
|
+
constructor(columns, analyzer, overrides = new Map()) {
|
|
36
|
+
this.columns = columns;
|
|
37
|
+
this.analyzer = analyzer;
|
|
38
|
+
this.overrides = overrides;
|
|
39
|
+
this.byId = new Map();
|
|
40
|
+
this.byName = new Map();
|
|
41
|
+
this.options = new Map();
|
|
42
|
+
this.parsed = new Map();
|
|
43
|
+
this.analyses = new Map();
|
|
44
|
+
this.active = 0;
|
|
45
|
+
for (const column of columns) {
|
|
46
|
+
if (!this.byId.has(column.id))
|
|
47
|
+
this.byId.set(column.id, column);
|
|
48
|
+
if (!this.byName.has(column.name))
|
|
49
|
+
this.byName.set(column.name, column);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
withExpression(columnId, parsed) {
|
|
53
|
+
return new Schema(this.columns, this.analyzer, new Map([[columnId, parsed]]));
|
|
54
|
+
}
|
|
55
|
+
optionsOf(column) {
|
|
56
|
+
let maps = this.options.get(column.id);
|
|
57
|
+
if (!maps) {
|
|
58
|
+
maps = optionMaps(column);
|
|
59
|
+
this.options.set(column.id, maps);
|
|
60
|
+
}
|
|
61
|
+
return maps;
|
|
62
|
+
}
|
|
63
|
+
storedType(column) {
|
|
64
|
+
return Object.hasOwn(STORED_TYPES, column.type) ? STORED_TYPES[column.type] : ANY;
|
|
65
|
+
}
|
|
66
|
+
parse(column) {
|
|
67
|
+
var _a;
|
|
68
|
+
const override = this.overrides.get(column.id);
|
|
69
|
+
if (override)
|
|
70
|
+
return override;
|
|
71
|
+
let parsed = this.parsed.get(column.id);
|
|
72
|
+
if (!parsed) {
|
|
73
|
+
const expression = (_a = column.config) === null || _a === void 0 ? void 0 : _a.expression;
|
|
74
|
+
parsed = parseFormula(typeof expression === 'string' ? expression : '');
|
|
75
|
+
this.parsed.set(column.id, parsed);
|
|
76
|
+
}
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
formula(column) {
|
|
80
|
+
var _a;
|
|
81
|
+
const known = this.analyses.get(column.id);
|
|
82
|
+
if (known === 'visiting')
|
|
83
|
+
return CYCLIC;
|
|
84
|
+
if (known)
|
|
85
|
+
return known;
|
|
86
|
+
if (this.active > FORMULA_LIMITS.depth)
|
|
87
|
+
return TOO_DEEP;
|
|
88
|
+
return (_a = this.analysis(column)) !== null && _a !== void 0 ? _a : BROKEN;
|
|
89
|
+
}
|
|
90
|
+
analysis(column) {
|
|
91
|
+
const known = this.analyses.get(column.id);
|
|
92
|
+
if (known === 'visiting')
|
|
93
|
+
return undefined;
|
|
94
|
+
if (known)
|
|
95
|
+
return known;
|
|
96
|
+
const parsed = this.parse(column);
|
|
97
|
+
if ('error' in parsed)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (this.active > FORMULA_LIMITS.depth)
|
|
100
|
+
return undefined;
|
|
101
|
+
this.analyses.set(column.id, 'visiting');
|
|
102
|
+
this.active++;
|
|
103
|
+
try {
|
|
104
|
+
const analysis = this.analyzer(parsed.ast, this);
|
|
105
|
+
if (analysis.truncated)
|
|
106
|
+
this.analyses.delete(column.id);
|
|
107
|
+
else
|
|
108
|
+
this.analyses.set(column.id, analysis);
|
|
109
|
+
return analysis;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.active--;
|
|
113
|
+
if (this.analyses.get(column.id) === 'visiting')
|
|
114
|
+
this.analyses.delete(column.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TokenType = 'number' | 'string' | 'identifier' | 'columnId' | 'symbol' | 'eof';
|
|
2
|
+
export type Token = {
|
|
3
|
+
type: TokenType;
|
|
4
|
+
value: string;
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const KEYWORDS: ReadonlySet<string>;
|
|
9
|
+
export declare const isKeyword: (value: string) => boolean;
|
|
10
|
+
export declare const isBareName: (name: string) => boolean;
|
|
11
|
+
export declare const tokenize: (source: string) => Token[];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { FormulaError } from './errors.js';
|
|
2
|
+
const SYMBOLS = [
|
|
3
|
+
'==',
|
|
4
|
+
'!=',
|
|
5
|
+
'<=',
|
|
6
|
+
'>=',
|
|
7
|
+
'+',
|
|
8
|
+
'-',
|
|
9
|
+
'*',
|
|
10
|
+
'/',
|
|
11
|
+
'%',
|
|
12
|
+
'^',
|
|
13
|
+
'<',
|
|
14
|
+
'>',
|
|
15
|
+
'=',
|
|
16
|
+
'(',
|
|
17
|
+
')',
|
|
18
|
+
',',
|
|
19
|
+
];
|
|
20
|
+
const ESCAPES = { n: '\n', t: '\t', r: '\r' };
|
|
21
|
+
export const KEYWORDS = new Set(['and', 'or', 'not', 'true', 'false']);
|
|
22
|
+
export const isKeyword = (value) => KEYWORDS.has(value.toLowerCase());
|
|
23
|
+
export const isBareName = (name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !isKeyword(name) && name !== 'prop';
|
|
24
|
+
const isDigit = (char) => char.length === 1 && char >= '0' && char <= '9';
|
|
25
|
+
const isIdentifierStart = (char) => /^[A-Za-z_]$/.test(char);
|
|
26
|
+
const isIdentifierPart = (char) => /^[A-Za-z0-9_]$/.test(char);
|
|
27
|
+
const isColumnIdPart = (char) => /^[A-Za-z0-9_-]$/.test(char);
|
|
28
|
+
const isWhitespace = (char) => /^\s$/.test(char);
|
|
29
|
+
const readDigits = (source, start) => {
|
|
30
|
+
let index = start;
|
|
31
|
+
while (isDigit(source.charAt(index)))
|
|
32
|
+
index++;
|
|
33
|
+
return index;
|
|
34
|
+
};
|
|
35
|
+
const readNumber = (source, start) => {
|
|
36
|
+
let index = readDigits(source, start);
|
|
37
|
+
if (source.charAt(index) === '.')
|
|
38
|
+
index = readDigits(source, index + 1);
|
|
39
|
+
const exponent = source.charAt(index);
|
|
40
|
+
if (exponent !== 'e' && exponent !== 'E')
|
|
41
|
+
return index;
|
|
42
|
+
let next = index + 1;
|
|
43
|
+
if (source.charAt(next) === '+' || source.charAt(next) === '-')
|
|
44
|
+
next++;
|
|
45
|
+
if (!isDigit(source.charAt(next))) {
|
|
46
|
+
throw new FormulaError('syntax', 'Invalid number', start, next);
|
|
47
|
+
}
|
|
48
|
+
return readDigits(source, next);
|
|
49
|
+
};
|
|
50
|
+
const readString = (source, start) => {
|
|
51
|
+
var _a;
|
|
52
|
+
const quote = source.charAt(start);
|
|
53
|
+
let value = '';
|
|
54
|
+
let index = start + 1;
|
|
55
|
+
while (index < source.length) {
|
|
56
|
+
const char = source.charAt(index);
|
|
57
|
+
if (char === quote)
|
|
58
|
+
return { value, end: index + 1 };
|
|
59
|
+
if (char === '\\' && index + 1 < source.length) {
|
|
60
|
+
const next = source.charAt(index + 1);
|
|
61
|
+
value += (_a = ESCAPES[next]) !== null && _a !== void 0 ? _a : next;
|
|
62
|
+
index += 2;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
value += char;
|
|
66
|
+
index++;
|
|
67
|
+
}
|
|
68
|
+
throw new FormulaError('syntax', 'Unterminated string', start, source.length);
|
|
69
|
+
};
|
|
70
|
+
export const tokenize = (source) => {
|
|
71
|
+
var _a;
|
|
72
|
+
const tokens = [];
|
|
73
|
+
let index = 0;
|
|
74
|
+
while (index < source.length) {
|
|
75
|
+
const char = source.charAt(index);
|
|
76
|
+
if (isWhitespace(char)) {
|
|
77
|
+
index++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const start = index;
|
|
81
|
+
if (isDigit(char) || (char === '.' && isDigit(source.charAt(index + 1)))) {
|
|
82
|
+
index = readNumber(source, start);
|
|
83
|
+
tokens.push({ type: 'number', value: source.slice(start, index), start, end: index });
|
|
84
|
+
}
|
|
85
|
+
else if (char === '"' || char === "'") {
|
|
86
|
+
const { value, end } = readString(source, start);
|
|
87
|
+
index = end;
|
|
88
|
+
tokens.push({ type: 'string', value, start, end });
|
|
89
|
+
}
|
|
90
|
+
else if (char === '#' && isColumnIdPart(source.charAt(index + 1))) {
|
|
91
|
+
index++;
|
|
92
|
+
while (isColumnIdPart(source.charAt(index)))
|
|
93
|
+
index++;
|
|
94
|
+
tokens.push({ type: 'columnId', value: source.slice(start + 1, index), start, end: index });
|
|
95
|
+
}
|
|
96
|
+
else if (isIdentifierStart(char)) {
|
|
97
|
+
index++;
|
|
98
|
+
while (isIdentifierPart(source.charAt(index)))
|
|
99
|
+
index++;
|
|
100
|
+
tokens.push({ type: 'identifier', value: source.slice(start, index), start, end: index });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const symbol = SYMBOLS.find((candidate) => source.startsWith(candidate, start));
|
|
104
|
+
if (!symbol) {
|
|
105
|
+
const unexpected = String.fromCodePoint((_a = source.codePointAt(start)) !== null && _a !== void 0 ? _a : 0);
|
|
106
|
+
throw new FormulaError('syntax', `Unexpected '${unexpected}'`, start, start + unexpected.length);
|
|
107
|
+
}
|
|
108
|
+
index += symbol.length;
|
|
109
|
+
tokens.push({ type: 'symbol', value: symbol, start, end: index });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
tokens.push({ type: 'eof', value: '', start: source.length, end: source.length });
|
|
113
|
+
return tokens;
|
|
114
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FormulaNode } from './ast.js';
|
|
2
|
+
import { type FormulaError } from './errors.js';
|
|
3
|
+
import { type FormulaColumn, type FormulaType } from './types.js';
|
|
4
|
+
export type TypecheckResult = {
|
|
5
|
+
type: FormulaType;
|
|
6
|
+
diagnostics: FormulaError[];
|
|
7
|
+
volatile: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const typecheck: (ast: FormulaNode, columns: readonly FormulaColumn[], options?: {
|
|
10
|
+
columnId?: string;
|
|
11
|
+
}) => TypecheckResult;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { analyzeFor } from './analyze.js';
|
|
2
|
+
import { toFormulaError } from './errors.js';
|
|
3
|
+
import { typeName } from './types.js';
|
|
4
|
+
export const typecheck = (ast, columns, options = {}) => {
|
|
5
|
+
try {
|
|
6
|
+
const { analysis } = analyzeFor(ast, columns, options.columnId);
|
|
7
|
+
return {
|
|
8
|
+
type: typeName(analysis.type),
|
|
9
|
+
diagnostics: [...analysis.diagnostics].sort((a, b) => a.start - b.start),
|
|
10
|
+
volatile: analysis.volatile,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return { type: 'any', diagnostics: [toFormulaError(error)], volatile: false };
|
|
15
|
+
}
|
|
16
|
+
};
|