@zentto/studio-core 0.1.0
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/__tests__/data-binding.test.d.ts +2 -0
- package/dist/__tests__/data-binding.test.d.ts.map +1 -0
- package/dist/__tests__/data-binding.test.js +86 -0
- package/dist/__tests__/data-binding.test.js.map +1 -0
- package/dist/__tests__/event-bus.test.d.ts +2 -0
- package/dist/__tests__/event-bus.test.d.ts.map +1 -0
- package/dist/__tests__/event-bus.test.js +70 -0
- package/dist/__tests__/event-bus.test.js.map +1 -0
- package/dist/__tests__/expression.test.d.ts +2 -0
- package/dist/__tests__/expression.test.d.ts.map +1 -0
- package/dist/__tests__/expression.test.js +173 -0
- package/dist/__tests__/expression.test.js.map +1 -0
- package/dist/__tests__/schema.test.d.ts +2 -0
- package/dist/__tests__/schema.test.d.ts.map +1 -0
- package/dist/__tests__/schema.test.js +91 -0
- package/dist/__tests__/schema.test.js.map +1 -0
- package/dist/__tests__/validation.test.d.ts +2 -0
- package/dist/__tests__/validation.test.d.ts.map +1 -0
- package/dist/__tests__/validation.test.js +70 -0
- package/dist/__tests__/validation.test.js.map +1 -0
- package/dist/app-types.d.ts +184 -0
- package/dist/app-types.d.ts.map +1 -0
- package/dist/app-types.js +4 -0
- package/dist/app-types.js.map +1 -0
- package/dist/data/data-source.d.ts +16 -0
- package/dist/data/data-source.d.ts.map +1 -0
- package/dist/data/data-source.js +139 -0
- package/dist/data/data-source.js.map +1 -0
- package/dist/data/data-transformer.d.ts +15 -0
- package/dist/data/data-transformer.d.ts.map +1 -0
- package/dist/data/data-transformer.js +58 -0
- package/dist/data/data-transformer.js.map +1 -0
- package/dist/engine/action-engine.d.ts +18 -0
- package/dist/engine/action-engine.d.ts.map +1 -0
- package/dist/engine/action-engine.js +97 -0
- package/dist/engine/action-engine.js.map +1 -0
- package/dist/engine/data-binding.d.ts +46 -0
- package/dist/engine/data-binding.d.ts.map +1 -0
- package/dist/engine/data-binding.js +198 -0
- package/dist/engine/data-binding.js.map +1 -0
- package/dist/engine/expression.d.ts +39 -0
- package/dist/engine/expression.d.ts.map +1 -0
- package/dist/engine/expression.js +630 -0
- package/dist/engine/expression.js.map +1 -0
- package/dist/engine/rule-engine.d.ts +23 -0
- package/dist/engine/rule-engine.d.ts.map +1 -0
- package/dist/engine/rule-engine.js +91 -0
- package/dist/engine/rule-engine.js.map +1 -0
- package/dist/engine/validation.d.ts +16 -0
- package/dist/engine/validation.d.ts.map +1 -0
- package/dist/engine/validation.js +116 -0
- package/dist/engine/validation.js.map +1 -0
- package/dist/events/event-bus.d.ts +16 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +51 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/i18n/en.d.ts +29 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +29 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/es.d.ts +29 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +29 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/i18n.d.ts +14 -0
- package/dist/i18n/i18n.d.ts.map +1 -0
- package/dist/i18n/i18n.js +32 -0
- package/dist/i18n/i18n.js.map +1 -0
- package/dist/i18n/pt.d.ts +29 -0
- package/dist/i18n/pt.d.ts.map +1 -0
- package/dist/i18n/pt.js +29 -0
- package/dist/i18n/pt.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/grid-solver.d.ts +18 -0
- package/dist/layout/grid-solver.d.ts.map +1 -0
- package/dist/layout/grid-solver.js +72 -0
- package/dist/layout/grid-solver.js.map +1 -0
- package/dist/persistence/flavor-manager.d.ts +17 -0
- package/dist/persistence/flavor-manager.d.ts.map +1 -0
- package/dist/persistence/flavor-manager.js +63 -0
- package/dist/persistence/flavor-manager.js.map +1 -0
- package/dist/persistence/schema-store.d.ts +35 -0
- package/dist/persistence/schema-store.d.ts.map +1 -0
- package/dist/persistence/schema-store.js +142 -0
- package/dist/persistence/schema-store.js.map +1 -0
- package/dist/registry/field-registry.d.ts +12 -0
- package/dist/registry/field-registry.d.ts.map +1 -0
- package/dist/registry/field-registry.js +91 -0
- package/dist/registry/field-registry.js.map +1 -0
- package/dist/schema/studio-schema.d.ts +1196 -0
- package/dist/schema/studio-schema.d.ts.map +1 -0
- package/dist/schema/studio-schema.js +191 -0
- package/dist/schema/studio-schema.js.map +1 -0
- package/dist/templates/app-templates.d.ts +35 -0
- package/dist/templates/app-templates.d.ts.map +1 -0
- package/dist/templates/app-templates.js +306 -0
- package/dist/templates/app-templates.js.map +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// @zentto/studio-core — Expression engine
|
|
2
|
+
// Fork of @zentto/report-core expression engine, adapted for form/UI context
|
|
3
|
+
// Complete formula language with recursive descent parser
|
|
4
|
+
// ─── Token Types ───────────────────────────────────────────────────
|
|
5
|
+
var TokenType;
|
|
6
|
+
(function (TokenType) {
|
|
7
|
+
TokenType[TokenType["Number"] = 0] = "Number";
|
|
8
|
+
TokenType[TokenType["String"] = 1] = "String";
|
|
9
|
+
TokenType[TokenType["Boolean"] = 2] = "Boolean";
|
|
10
|
+
TokenType[TokenType["Null"] = 3] = "Null";
|
|
11
|
+
TokenType[TokenType["Identifier"] = 4] = "Identifier";
|
|
12
|
+
TokenType[TokenType["FieldRef"] = 5] = "FieldRef";
|
|
13
|
+
TokenType[TokenType["LParen"] = 6] = "LParen";
|
|
14
|
+
TokenType[TokenType["RParen"] = 7] = "RParen";
|
|
15
|
+
TokenType[TokenType["Comma"] = 8] = "Comma";
|
|
16
|
+
TokenType[TokenType["Plus"] = 9] = "Plus";
|
|
17
|
+
TokenType[TokenType["Minus"] = 10] = "Minus";
|
|
18
|
+
TokenType[TokenType["Star"] = 11] = "Star";
|
|
19
|
+
TokenType[TokenType["Slash"] = 12] = "Slash";
|
|
20
|
+
TokenType[TokenType["Percent"] = 13] = "Percent";
|
|
21
|
+
TokenType[TokenType["Caret"] = 14] = "Caret";
|
|
22
|
+
TokenType[TokenType["Ampersand"] = 15] = "Ampersand";
|
|
23
|
+
TokenType[TokenType["Eq"] = 16] = "Eq";
|
|
24
|
+
TokenType[TokenType["Neq"] = 17] = "Neq";
|
|
25
|
+
TokenType[TokenType["Lt"] = 18] = "Lt";
|
|
26
|
+
TokenType[TokenType["Gt"] = 19] = "Gt";
|
|
27
|
+
TokenType[TokenType["Lte"] = 20] = "Lte";
|
|
28
|
+
TokenType[TokenType["Gte"] = 21] = "Gte";
|
|
29
|
+
TokenType[TokenType["And"] = 22] = "And";
|
|
30
|
+
TokenType[TokenType["Or"] = 23] = "Or";
|
|
31
|
+
TokenType[TokenType["Not"] = 24] = "Not";
|
|
32
|
+
TokenType[TokenType["EOF"] = 25] = "EOF";
|
|
33
|
+
})(TokenType || (TokenType = {}));
|
|
34
|
+
// ─── Lexer ─────────────────────────────────────────────────────────
|
|
35
|
+
class Lexer {
|
|
36
|
+
input;
|
|
37
|
+
tokens = [];
|
|
38
|
+
constructor(input) {
|
|
39
|
+
this.input = input;
|
|
40
|
+
this.tokenize();
|
|
41
|
+
}
|
|
42
|
+
tokenize() {
|
|
43
|
+
const src = this.input;
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < src.length) {
|
|
46
|
+
if (/\s/.test(src[i])) {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Field reference: {field} or {ds.field}
|
|
51
|
+
if (src[i] === '{') {
|
|
52
|
+
const start = i;
|
|
53
|
+
i++;
|
|
54
|
+
let ref = '';
|
|
55
|
+
while (i < src.length && src[i] !== '}') {
|
|
56
|
+
ref += src[i];
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
if (i < src.length)
|
|
60
|
+
i++;
|
|
61
|
+
this.tokens.push({ type: TokenType.FieldRef, value: ref, pos: start });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// String literal
|
|
65
|
+
if (src[i] === '"' || src[i] === "'") {
|
|
66
|
+
const quote = src[i];
|
|
67
|
+
const start = i;
|
|
68
|
+
i++;
|
|
69
|
+
let str = '';
|
|
70
|
+
while (i < src.length && src[i] !== quote) {
|
|
71
|
+
if (src[i] === '\\' && i + 1 < src.length) {
|
|
72
|
+
i++;
|
|
73
|
+
if (src[i] === 'n')
|
|
74
|
+
str += '\n';
|
|
75
|
+
else if (src[i] === 't')
|
|
76
|
+
str += '\t';
|
|
77
|
+
else if (src[i] === '\\')
|
|
78
|
+
str += '\\';
|
|
79
|
+
else if (src[i] === quote)
|
|
80
|
+
str += quote;
|
|
81
|
+
else
|
|
82
|
+
str += src[i];
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
str += src[i];
|
|
86
|
+
}
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
if (i < src.length)
|
|
90
|
+
i++;
|
|
91
|
+
this.tokens.push({ type: TokenType.String, value: str, pos: start });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Number literal
|
|
95
|
+
if (/\d/.test(src[i]) || (src[i] === '.' && i + 1 < src.length && /\d/.test(src[i + 1]))) {
|
|
96
|
+
const start = i;
|
|
97
|
+
let num = '';
|
|
98
|
+
while (i < src.length && /[\d.]/.test(src[i])) {
|
|
99
|
+
num += src[i];
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
this.tokens.push({ type: TokenType.Number, value: num, pos: start });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// Two-character operators
|
|
106
|
+
if (i + 1 < src.length) {
|
|
107
|
+
const two = src[i] + src[i + 1];
|
|
108
|
+
if (two === '==') {
|
|
109
|
+
this.tokens.push({ type: TokenType.Eq, value: '==', pos: i });
|
|
110
|
+
i += 2;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (two === '!=') {
|
|
114
|
+
this.tokens.push({ type: TokenType.Neq, value: '!=', pos: i });
|
|
115
|
+
i += 2;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (two === '<=') {
|
|
119
|
+
this.tokens.push({ type: TokenType.Lte, value: '<=', pos: i });
|
|
120
|
+
i += 2;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (two === '>=') {
|
|
124
|
+
this.tokens.push({ type: TokenType.Gte, value: '>=', pos: i });
|
|
125
|
+
i += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (two === '<>') {
|
|
129
|
+
this.tokens.push({ type: TokenType.Neq, value: '<>', pos: i });
|
|
130
|
+
i += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Single-character operators
|
|
135
|
+
const ch = src[i];
|
|
136
|
+
const singles = {
|
|
137
|
+
'(': TokenType.LParen, ')': TokenType.RParen, ',': TokenType.Comma,
|
|
138
|
+
'+': TokenType.Plus, '-': TokenType.Minus, '*': TokenType.Star,
|
|
139
|
+
'/': TokenType.Slash, '%': TokenType.Percent, '^': TokenType.Caret,
|
|
140
|
+
'&': TokenType.Ampersand, '<': TokenType.Lt, '>': TokenType.Gt,
|
|
141
|
+
'=': TokenType.Eq, '!': TokenType.Not,
|
|
142
|
+
};
|
|
143
|
+
if (ch in singles) {
|
|
144
|
+
this.tokens.push({ type: singles[ch], value: ch, pos: i });
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Identifiers and keywords
|
|
149
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
150
|
+
const start = i;
|
|
151
|
+
let id = '';
|
|
152
|
+
while (i < src.length && /[a-zA-Z0-9_]/.test(src[i])) {
|
|
153
|
+
id += src[i];
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
const upper = id.toUpperCase();
|
|
157
|
+
if (upper === 'TRUE') {
|
|
158
|
+
this.tokens.push({ type: TokenType.Boolean, value: 'true', pos: start });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (upper === 'FALSE') {
|
|
162
|
+
this.tokens.push({ type: TokenType.Boolean, value: 'false', pos: start });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (upper === 'NULL' || upper === 'NIL') {
|
|
166
|
+
this.tokens.push({ type: TokenType.Null, value: 'null', pos: start });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (upper === 'AND') {
|
|
170
|
+
this.tokens.push({ type: TokenType.And, value: 'AND', pos: start });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (upper === 'OR') {
|
|
174
|
+
this.tokens.push({ type: TokenType.Or, value: 'OR', pos: start });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (upper === 'NOT') {
|
|
178
|
+
this.tokens.push({ type: TokenType.Not, value: 'NOT', pos: start });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
this.tokens.push({ type: TokenType.Identifier, value: id, pos: start });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
i++; // skip unknown
|
|
185
|
+
}
|
|
186
|
+
this.tokens.push({ type: TokenType.EOF, value: '', pos: i });
|
|
187
|
+
}
|
|
188
|
+
getTokens() { return this.tokens; }
|
|
189
|
+
}
|
|
190
|
+
// ─── Parser ────────────────────────────────────────────────────────
|
|
191
|
+
class Parser {
|
|
192
|
+
pos = 0;
|
|
193
|
+
tokens;
|
|
194
|
+
constructor(tokens) { this.tokens = tokens; }
|
|
195
|
+
parse() {
|
|
196
|
+
const result = this.parseOr();
|
|
197
|
+
if (this.peek().type !== TokenType.EOF) {
|
|
198
|
+
throw new ExprError(`Token inesperado: "${this.peek().value}" en posicion ${this.peek().pos}`);
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
peek() {
|
|
203
|
+
return this.tokens[this.pos] || { type: TokenType.EOF, value: '', pos: -1 };
|
|
204
|
+
}
|
|
205
|
+
advance() { return this.tokens[this.pos++]; }
|
|
206
|
+
expect(type) {
|
|
207
|
+
const t = this.peek();
|
|
208
|
+
if (t.type !== type) {
|
|
209
|
+
throw new ExprError(`Se esperaba ${TokenType[type]}, se encontro "${t.value}" en posicion ${t.pos}`);
|
|
210
|
+
}
|
|
211
|
+
return this.advance();
|
|
212
|
+
}
|
|
213
|
+
parseOr() {
|
|
214
|
+
let left = this.parseAnd();
|
|
215
|
+
while (this.peek().type === TokenType.Or) {
|
|
216
|
+
this.advance();
|
|
217
|
+
left = { kind: 'binary', op: 'OR', left, right: this.parseAnd() };
|
|
218
|
+
}
|
|
219
|
+
return left;
|
|
220
|
+
}
|
|
221
|
+
parseAnd() {
|
|
222
|
+
let left = this.parseNot();
|
|
223
|
+
while (this.peek().type === TokenType.And) {
|
|
224
|
+
this.advance();
|
|
225
|
+
left = { kind: 'binary', op: 'AND', left, right: this.parseNot() };
|
|
226
|
+
}
|
|
227
|
+
return left;
|
|
228
|
+
}
|
|
229
|
+
parseNot() {
|
|
230
|
+
if (this.peek().type === TokenType.Not) {
|
|
231
|
+
this.advance();
|
|
232
|
+
return { kind: 'unary', op: 'NOT', operand: this.parseNot() };
|
|
233
|
+
}
|
|
234
|
+
return this.parseComparison();
|
|
235
|
+
}
|
|
236
|
+
parseComparison() {
|
|
237
|
+
let left = this.parseConcatenation();
|
|
238
|
+
const compOps = [TokenType.Eq, TokenType.Neq, TokenType.Lt, TokenType.Gt, TokenType.Lte, TokenType.Gte];
|
|
239
|
+
while (compOps.includes(this.peek().type)) {
|
|
240
|
+
const op = this.advance().value;
|
|
241
|
+
left = { kind: 'binary', op, left, right: this.parseConcatenation() };
|
|
242
|
+
}
|
|
243
|
+
return left;
|
|
244
|
+
}
|
|
245
|
+
parseConcatenation() {
|
|
246
|
+
let left = this.parseAddSub();
|
|
247
|
+
while (this.peek().type === TokenType.Ampersand) {
|
|
248
|
+
this.advance();
|
|
249
|
+
left = { kind: 'binary', op: '&', left, right: this.parseAddSub() };
|
|
250
|
+
}
|
|
251
|
+
return left;
|
|
252
|
+
}
|
|
253
|
+
parseAddSub() {
|
|
254
|
+
let left = this.parseMulDiv();
|
|
255
|
+
while (this.peek().type === TokenType.Plus || this.peek().type === TokenType.Minus) {
|
|
256
|
+
const op = this.advance().value;
|
|
257
|
+
left = { kind: 'binary', op, left, right: this.parseMulDiv() };
|
|
258
|
+
}
|
|
259
|
+
return left;
|
|
260
|
+
}
|
|
261
|
+
parseMulDiv() {
|
|
262
|
+
let left = this.parsePower();
|
|
263
|
+
while (this.peek().type === TokenType.Star || this.peek().type === TokenType.Slash || this.peek().type === TokenType.Percent) {
|
|
264
|
+
const op = this.advance().value;
|
|
265
|
+
left = { kind: 'binary', op, left, right: this.parsePower() };
|
|
266
|
+
}
|
|
267
|
+
return left;
|
|
268
|
+
}
|
|
269
|
+
parsePower() {
|
|
270
|
+
let left = this.parseUnary();
|
|
271
|
+
while (this.peek().type === TokenType.Caret) {
|
|
272
|
+
this.advance();
|
|
273
|
+
left = { kind: 'binary', op: '^', left, right: this.parseUnary() };
|
|
274
|
+
}
|
|
275
|
+
return left;
|
|
276
|
+
}
|
|
277
|
+
parseUnary() {
|
|
278
|
+
if (this.peek().type === TokenType.Minus) {
|
|
279
|
+
this.advance();
|
|
280
|
+
return { kind: 'unary', op: '-', operand: this.parseUnary() };
|
|
281
|
+
}
|
|
282
|
+
if (this.peek().type === TokenType.Plus) {
|
|
283
|
+
this.advance();
|
|
284
|
+
return this.parseUnary();
|
|
285
|
+
}
|
|
286
|
+
return this.parsePrimary();
|
|
287
|
+
}
|
|
288
|
+
parsePrimary() {
|
|
289
|
+
const t = this.peek();
|
|
290
|
+
if (t.type === TokenType.Number) {
|
|
291
|
+
this.advance();
|
|
292
|
+
return { kind: 'number', value: parseFloat(t.value) };
|
|
293
|
+
}
|
|
294
|
+
if (t.type === TokenType.String) {
|
|
295
|
+
this.advance();
|
|
296
|
+
return { kind: 'string', value: t.value };
|
|
297
|
+
}
|
|
298
|
+
if (t.type === TokenType.Boolean) {
|
|
299
|
+
this.advance();
|
|
300
|
+
return { kind: 'boolean', value: t.value === 'true' };
|
|
301
|
+
}
|
|
302
|
+
if (t.type === TokenType.Null) {
|
|
303
|
+
this.advance();
|
|
304
|
+
return { kind: 'null' };
|
|
305
|
+
}
|
|
306
|
+
if (t.type === TokenType.FieldRef) {
|
|
307
|
+
this.advance();
|
|
308
|
+
return { kind: 'fieldRef', ref: t.value };
|
|
309
|
+
}
|
|
310
|
+
if (t.type === TokenType.Identifier) {
|
|
311
|
+
this.advance();
|
|
312
|
+
if (this.peek().type === TokenType.LParen) {
|
|
313
|
+
this.advance();
|
|
314
|
+
const args = [];
|
|
315
|
+
if (this.peek().type !== TokenType.RParen) {
|
|
316
|
+
args.push(this.parseOr());
|
|
317
|
+
while (this.peek().type === TokenType.Comma) {
|
|
318
|
+
this.advance();
|
|
319
|
+
args.push(this.parseOr());
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
this.expect(TokenType.RParen);
|
|
323
|
+
return { kind: 'call', name: t.value.toUpperCase(), args };
|
|
324
|
+
}
|
|
325
|
+
return { kind: 'identifier', name: t.value };
|
|
326
|
+
}
|
|
327
|
+
if (t.type === TokenType.LParen) {
|
|
328
|
+
this.advance();
|
|
329
|
+
const expr = this.parseOr();
|
|
330
|
+
this.expect(TokenType.RParen);
|
|
331
|
+
return expr;
|
|
332
|
+
}
|
|
333
|
+
throw new ExprError(`Token inesperado: "${t.value}" en posicion ${t.pos}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// ─── Error ─────────────────────────────────────────────────────────
|
|
337
|
+
export class ExprError extends Error {
|
|
338
|
+
constructor(message) {
|
|
339
|
+
super(message);
|
|
340
|
+
this.name = 'ExprError';
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// ─── Type Coercion Helpers ─────────────────────────────────────────
|
|
344
|
+
function toNumber(val) {
|
|
345
|
+
if (typeof val === 'number')
|
|
346
|
+
return val;
|
|
347
|
+
if (typeof val === 'string') {
|
|
348
|
+
const n = parseFloat(val);
|
|
349
|
+
return isNaN(n) ? 0 : n;
|
|
350
|
+
}
|
|
351
|
+
if (typeof val === 'boolean')
|
|
352
|
+
return val ? 1 : 0;
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
function toBool(val) {
|
|
356
|
+
if (typeof val === 'boolean')
|
|
357
|
+
return val;
|
|
358
|
+
if (typeof val === 'number')
|
|
359
|
+
return val !== 0;
|
|
360
|
+
if (typeof val === 'string') {
|
|
361
|
+
const lower = val.toLowerCase();
|
|
362
|
+
return lower === 'true' || lower === 'yes' || lower === 'si' || lower === '1';
|
|
363
|
+
}
|
|
364
|
+
return val != null;
|
|
365
|
+
}
|
|
366
|
+
function toString(val) {
|
|
367
|
+
if (val == null)
|
|
368
|
+
return '';
|
|
369
|
+
if (val instanceof Date)
|
|
370
|
+
return val.toISOString();
|
|
371
|
+
return String(val);
|
|
372
|
+
}
|
|
373
|
+
function toDate(val) {
|
|
374
|
+
if (val instanceof Date)
|
|
375
|
+
return val;
|
|
376
|
+
if (typeof val === 'string') {
|
|
377
|
+
const d = new Date(val);
|
|
378
|
+
return isNaN(d.getTime()) ? null : d;
|
|
379
|
+
}
|
|
380
|
+
if (typeof val === 'number')
|
|
381
|
+
return new Date(val);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
// ─── Deep Value Resolution ─────────────────────────────────────────
|
|
385
|
+
function resolveDeep(obj, path) {
|
|
386
|
+
const parts = path.split('.');
|
|
387
|
+
let current = obj;
|
|
388
|
+
for (const part of parts) {
|
|
389
|
+
if (current == null || typeof current !== 'object')
|
|
390
|
+
return undefined;
|
|
391
|
+
// Handle array notation: items[0]
|
|
392
|
+
const arrMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
393
|
+
if (arrMatch) {
|
|
394
|
+
const arr = current[arrMatch[1]];
|
|
395
|
+
if (!Array.isArray(arr))
|
|
396
|
+
return undefined;
|
|
397
|
+
current = arr[parseInt(arrMatch[2], 10)];
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
current = current[part];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return current;
|
|
404
|
+
}
|
|
405
|
+
const FUNCTIONS = {};
|
|
406
|
+
function registerFn(name, fn) {
|
|
407
|
+
FUNCTIONS[name.toUpperCase()] = fn;
|
|
408
|
+
}
|
|
409
|
+
// String functions
|
|
410
|
+
registerFn('LEFT', (args) => toString(args[0]).slice(0, toNumber(args[1])));
|
|
411
|
+
registerFn('RIGHT', (args) => { const s = toString(args[0]); const n = toNumber(args[1]); return s.slice(Math.max(0, s.length - n)); });
|
|
412
|
+
registerFn('MID', (args) => { const s = toString(args[0]); const start = toNumber(args[1]) - 1; const len = args.length > 2 ? toNumber(args[2]) : s.length - start; return s.slice(Math.max(0, start), start + len); });
|
|
413
|
+
registerFn('LEN', (args) => toString(args[0]).length);
|
|
414
|
+
registerFn('TRIM', (args) => toString(args[0]).trim());
|
|
415
|
+
registerFn('UPPER', (args) => toString(args[0]).toUpperCase());
|
|
416
|
+
registerFn('LOWER', (args) => toString(args[0]).toLowerCase());
|
|
417
|
+
registerFn('REPLACE', (args) => toString(args[0]).split(toString(args[1])).join(toString(args[2])));
|
|
418
|
+
registerFn('CONTAINS', (args) => toString(args[0]).toLowerCase().includes(toString(args[1]).toLowerCase()));
|
|
419
|
+
registerFn('STARTSWITH', (args) => toString(args[0]).toLowerCase().startsWith(toString(args[1]).toLowerCase()));
|
|
420
|
+
registerFn('ENDSWITH', (args) => toString(args[0]).toLowerCase().endsWith(toString(args[1]).toLowerCase()));
|
|
421
|
+
registerFn('CONCAT', (args) => args.map(toString).join(''));
|
|
422
|
+
registerFn('JOIN', (args) => { const delim = toString(args[0]); return args.slice(1).map(toString).join(delim); });
|
|
423
|
+
// Math functions
|
|
424
|
+
registerFn('ABS', (args) => Math.abs(toNumber(args[0])));
|
|
425
|
+
registerFn('ROUND', (args) => { const val = toNumber(args[0]); const dec = args.length > 1 ? toNumber(args[1]) : 0; const f = Math.pow(10, dec); return Math.round(val * f) / f; });
|
|
426
|
+
registerFn('FLOOR', (args) => Math.floor(toNumber(args[0])));
|
|
427
|
+
registerFn('CEIL', (args) => Math.ceil(toNumber(args[0])));
|
|
428
|
+
registerFn('MIN', (args) => Math.min(...args.map(toNumber)));
|
|
429
|
+
registerFn('MAX', (args) => Math.max(...args.map(toNumber)));
|
|
430
|
+
registerFn('SUM', (args) => args.reduce((acc, v) => acc + toNumber(v), 0));
|
|
431
|
+
registerFn('AVG', (args) => args.length === 0 ? 0 : args.reduce((acc, v) => acc + toNumber(v), 0) / args.length);
|
|
432
|
+
registerFn('SQRT', (args) => Math.sqrt(toNumber(args[0])));
|
|
433
|
+
registerFn('POW', (args) => Math.pow(toNumber(args[0]), toNumber(args[1])));
|
|
434
|
+
// Logic functions
|
|
435
|
+
registerFn('IF', (args) => toBool(args[0]) ? args[1] : (args.length > 2 ? args[2] : null));
|
|
436
|
+
registerFn('IIF', (args) => toBool(args[0]) ? args[1] : (args.length > 2 ? args[2] : null));
|
|
437
|
+
registerFn('SWITCH', (args) => {
|
|
438
|
+
const val = args[0];
|
|
439
|
+
for (let i = 1; i + 1 < args.length; i += 2) {
|
|
440
|
+
if (val === args[i] || toString(val) === toString(args[i]))
|
|
441
|
+
return args[i + 1];
|
|
442
|
+
}
|
|
443
|
+
return args.length % 2 === 0 ? args[args.length - 1] : null; // default case
|
|
444
|
+
});
|
|
445
|
+
registerFn('COALESCE', (args) => args.find(a => a != null && a !== '') ?? null);
|
|
446
|
+
registerFn('ISNULL', (args) => args[0] == null);
|
|
447
|
+
registerFn('ISEMPTY', (args) => { const v = args[0]; return v == null || v === '' || (Array.isArray(v) && v.length === 0); });
|
|
448
|
+
registerFn('NOT', (args) => !toBool(args[0]));
|
|
449
|
+
// Type conversion
|
|
450
|
+
registerFn('TONUMBER', (args) => toNumber(args[0]));
|
|
451
|
+
registerFn('TOTEXT', (args) => toString(args[0]));
|
|
452
|
+
registerFn('TOBOOLEAN', (args) => toBool(args[0]));
|
|
453
|
+
registerFn('TODATE', (args) => { const d = toDate(args[0]); return d ? d.toISOString() : null; });
|
|
454
|
+
// Date functions
|
|
455
|
+
registerFn('NOW', () => new Date().toISOString());
|
|
456
|
+
registerFn('TODAY', () => new Date().toISOString().slice(0, 10));
|
|
457
|
+
registerFn('YEAR', (args) => { const d = toDate(args[0]); return d ? d.getFullYear() : 0; });
|
|
458
|
+
registerFn('MONTH', (args) => { const d = toDate(args[0]); return d ? d.getMonth() + 1 : 0; });
|
|
459
|
+
registerFn('DAY', (args) => { const d = toDate(args[0]); return d ? d.getDate() : 0; });
|
|
460
|
+
registerFn('DATEADD', (args) => {
|
|
461
|
+
const d = toDate(args[0]);
|
|
462
|
+
if (!d)
|
|
463
|
+
return null;
|
|
464
|
+
const unit = toString(args[1]).toLowerCase();
|
|
465
|
+
const amount = toNumber(args[2]);
|
|
466
|
+
const result = new Date(d);
|
|
467
|
+
if (unit === 'day' || unit === 'days')
|
|
468
|
+
result.setDate(result.getDate() + amount);
|
|
469
|
+
else if (unit === 'month' || unit === 'months')
|
|
470
|
+
result.setMonth(result.getMonth() + amount);
|
|
471
|
+
else if (unit === 'year' || unit === 'years')
|
|
472
|
+
result.setFullYear(result.getFullYear() + amount);
|
|
473
|
+
else if (unit === 'hour' || unit === 'hours')
|
|
474
|
+
result.setHours(result.getHours() + amount);
|
|
475
|
+
return result.toISOString();
|
|
476
|
+
});
|
|
477
|
+
registerFn('DATEDIFF', (args) => {
|
|
478
|
+
const d1 = toDate(args[0]);
|
|
479
|
+
const d2 = toDate(args[1]);
|
|
480
|
+
if (!d1 || !d2)
|
|
481
|
+
return 0;
|
|
482
|
+
const unit = args.length > 2 ? toString(args[2]).toLowerCase() : 'days';
|
|
483
|
+
const diffMs = d2.getTime() - d1.getTime();
|
|
484
|
+
if (unit === 'day' || unit === 'days')
|
|
485
|
+
return Math.floor(diffMs / 86400000);
|
|
486
|
+
if (unit === 'hour' || unit === 'hours')
|
|
487
|
+
return Math.floor(diffMs / 3600000);
|
|
488
|
+
if (unit === 'minute' || unit === 'minutes')
|
|
489
|
+
return Math.floor(diffMs / 60000);
|
|
490
|
+
return diffMs;
|
|
491
|
+
});
|
|
492
|
+
// Studio-specific: form context functions
|
|
493
|
+
registerFn('FIELD', (args, ctx) => resolveDeep(ctx.formData, toString(args[0])));
|
|
494
|
+
registerFn('DATASOURCE', (args, ctx) => {
|
|
495
|
+
const dsId = toString(args[0]);
|
|
496
|
+
const path = args.length > 1 ? toString(args[1]) : '';
|
|
497
|
+
const ds = ctx.dataSources[dsId];
|
|
498
|
+
if (!path || !ds)
|
|
499
|
+
return ds;
|
|
500
|
+
if (typeof ds === 'object' && ds !== null)
|
|
501
|
+
return resolveDeep(ds, path);
|
|
502
|
+
return ds;
|
|
503
|
+
});
|
|
504
|
+
registerFn('ROLE_IS', (args, ctx) => {
|
|
505
|
+
const role = toString(args[0]);
|
|
506
|
+
return ctx.user?.roles?.includes(role) ?? false;
|
|
507
|
+
});
|
|
508
|
+
registerFn('HAS_ROLE', (args, ctx) => {
|
|
509
|
+
const role = toString(args[0]);
|
|
510
|
+
return ctx.user?.roles?.includes(role) ?? false;
|
|
511
|
+
});
|
|
512
|
+
registerFn('CHANGED', (args) => {
|
|
513
|
+
// In runtime, this is resolved by the data-binding layer before reaching here
|
|
514
|
+
// Default: true (field has been modified)
|
|
515
|
+
return args[0] != null;
|
|
516
|
+
});
|
|
517
|
+
// ─── Evaluator ─────────────────────────────────────────────────────
|
|
518
|
+
function evaluate(node, ctx) {
|
|
519
|
+
switch (node.kind) {
|
|
520
|
+
case 'number': return node.value;
|
|
521
|
+
case 'string': return node.value;
|
|
522
|
+
case 'boolean': return node.value;
|
|
523
|
+
case 'null': return null;
|
|
524
|
+
case 'fieldRef': return resolveDeep(ctx.formData, node.ref);
|
|
525
|
+
case 'identifier': {
|
|
526
|
+
// Check formData first, then dataSources, then parameters
|
|
527
|
+
const fromForm = resolveDeep(ctx.formData, node.name);
|
|
528
|
+
if (fromForm !== undefined)
|
|
529
|
+
return fromForm;
|
|
530
|
+
if (ctx.parameters && node.name in ctx.parameters)
|
|
531
|
+
return ctx.parameters[node.name];
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
case 'unary': {
|
|
535
|
+
const operand = evaluate(node.operand, ctx);
|
|
536
|
+
if (node.op === '-')
|
|
537
|
+
return -toNumber(operand);
|
|
538
|
+
if (node.op === 'NOT')
|
|
539
|
+
return !toBool(operand);
|
|
540
|
+
return operand;
|
|
541
|
+
}
|
|
542
|
+
case 'binary': {
|
|
543
|
+
const left = evaluate(node.left, ctx);
|
|
544
|
+
const right = evaluate(node.right, ctx);
|
|
545
|
+
switch (node.op) {
|
|
546
|
+
case '+': return toNumber(left) + toNumber(right);
|
|
547
|
+
case '-': return toNumber(left) - toNumber(right);
|
|
548
|
+
case '*': return toNumber(left) * toNumber(right);
|
|
549
|
+
case '/': {
|
|
550
|
+
const d = toNumber(right);
|
|
551
|
+
return d === 0 ? 0 : toNumber(left) / d;
|
|
552
|
+
}
|
|
553
|
+
case '%': {
|
|
554
|
+
const d = toNumber(right);
|
|
555
|
+
return d === 0 ? 0 : toNumber(left) % d;
|
|
556
|
+
}
|
|
557
|
+
case '^': return Math.pow(toNumber(left), toNumber(right));
|
|
558
|
+
case '&': return toString(left) + toString(right);
|
|
559
|
+
case '==':
|
|
560
|
+
case '=': return left === right || toString(left) === toString(right);
|
|
561
|
+
case '!=':
|
|
562
|
+
case '<>': return left !== right && toString(left) !== toString(right);
|
|
563
|
+
case '<': return toNumber(left) < toNumber(right);
|
|
564
|
+
case '>': return toNumber(left) > toNumber(right);
|
|
565
|
+
case '<=': return toNumber(left) <= toNumber(right);
|
|
566
|
+
case '>=': return toNumber(left) >= toNumber(right);
|
|
567
|
+
case 'AND': return toBool(left) && toBool(right);
|
|
568
|
+
case 'OR': return toBool(left) || toBool(right);
|
|
569
|
+
default: return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
case 'call': {
|
|
573
|
+
const fn = FUNCTIONS[node.name];
|
|
574
|
+
if (!fn)
|
|
575
|
+
throw new ExprError(`Funcion desconocida: ${node.name}`);
|
|
576
|
+
const args = node.args.map(a => evaluate(a, ctx));
|
|
577
|
+
return fn(args, ctx);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// ─── Public API ────────────────────────────────────────────────────
|
|
582
|
+
/**
|
|
583
|
+
* Evaluate an expression string against a StudioBindingContext.
|
|
584
|
+
* Returns the computed value. Throws ExprError on syntax errors.
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* evaluateExpression('{precio} * {cantidad}', { formData: { precio: 10, cantidad: 5 } })
|
|
588
|
+
* // → 50
|
|
589
|
+
*
|
|
590
|
+
* evaluateExpression('{role} == "admin" AND {total} > 1000', {
|
|
591
|
+
* formData: { role: 'admin', total: 1500 }
|
|
592
|
+
* })
|
|
593
|
+
* // → true
|
|
594
|
+
*/
|
|
595
|
+
export function evaluateExpression(expression, context = { formData: {}, dataSources: {} }) {
|
|
596
|
+
if (!expression || expression.trim() === '')
|
|
597
|
+
return null;
|
|
598
|
+
const lexer = new Lexer(expression);
|
|
599
|
+
const parser = new Parser(lexer.getTokens());
|
|
600
|
+
const ast = parser.parse();
|
|
601
|
+
return evaluate(ast, context);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Safe evaluation — returns { ok, value } or { ok, error }.
|
|
605
|
+
* Never throws.
|
|
606
|
+
*/
|
|
607
|
+
export function safeEvaluateExpression(expression, context = { formData: {}, dataSources: {} }) {
|
|
608
|
+
try {
|
|
609
|
+
const value = evaluateExpression(expression, context);
|
|
610
|
+
return { ok: true, value };
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Evaluate an expression and coerce to boolean.
|
|
618
|
+
* Useful for visibility rules and conditions.
|
|
619
|
+
*/
|
|
620
|
+
export function evaluateCondition(expression, context) {
|
|
621
|
+
const result = safeEvaluateExpression(expression, context);
|
|
622
|
+
if (!result.ok)
|
|
623
|
+
return false;
|
|
624
|
+
return toBool(result.value);
|
|
625
|
+
}
|
|
626
|
+
/** Register a custom function for use in expressions */
|
|
627
|
+
export function registerFunction(name, fn) {
|
|
628
|
+
registerFn(name, fn);
|
|
629
|
+
}
|
|
630
|
+
//# sourceMappingURL=expression.js.map
|