@fieldia/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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/package.json +40 -0
  4. package/schema/page.schema.json +2142 -0
  5. package/src/index.d.mts +15 -0
  6. package/src/index.d.ts +15 -0
  7. package/src/index.js +46 -0
  8. package/src/index.js.map +1 -0
  9. package/src/index.mjs +14 -0
  10. package/src/index.mjs.map +1 -0
  11. package/src/lib/expression/evaluateModifier.d.mts +55 -0
  12. package/src/lib/expression/evaluateModifier.d.ts +55 -0
  13. package/src/lib/expression/evaluateModifier.js +121 -0
  14. package/src/lib/expression/evaluateModifier.js.map +1 -0
  15. package/src/lib/expression/evaluateModifier.mjs +117 -0
  16. package/src/lib/expression/evaluateModifier.mjs.map +1 -0
  17. package/src/lib/expression/evaluator.d.mts +23 -0
  18. package/src/lib/expression/evaluator.d.ts +23 -0
  19. package/src/lib/expression/evaluator.js +122 -0
  20. package/src/lib/expression/evaluator.js.map +1 -0
  21. package/src/lib/expression/evaluator.mjs +118 -0
  22. package/src/lib/expression/evaluator.mjs.map +1 -0
  23. package/src/lib/expression/extractFieldNames.d.mts +41 -0
  24. package/src/lib/expression/extractFieldNames.d.ts +41 -0
  25. package/src/lib/expression/extractFieldNames.js +97 -0
  26. package/src/lib/expression/extractFieldNames.js.map +1 -0
  27. package/src/lib/expression/extractFieldNames.mjs +93 -0
  28. package/src/lib/expression/extractFieldNames.mjs.map +1 -0
  29. package/src/lib/expression/modifier.d.mts +14 -0
  30. package/src/lib/expression/modifier.d.ts +14 -0
  31. package/src/lib/expression/modifier.js +55 -0
  32. package/src/lib/expression/modifier.js.map +1 -0
  33. package/src/lib/expression/modifier.mjs +52 -0
  34. package/src/lib/expression/modifier.mjs.map +1 -0
  35. package/src/lib/expression/parser.d.mts +109 -0
  36. package/src/lib/expression/parser.d.ts +109 -0
  37. package/src/lib/expression/parser.js +317 -0
  38. package/src/lib/expression/parser.js.map +1 -0
  39. package/src/lib/expression/parser.mjs +313 -0
  40. package/src/lib/expression/parser.mjs.map +1 -0
  41. package/src/lib/expression/tokenizer.d.mts +31 -0
  42. package/src/lib/expression/tokenizer.d.ts +31 -0
  43. package/src/lib/expression/tokenizer.js +100 -0
  44. package/src/lib/expression/tokenizer.js.map +1 -0
  45. package/src/lib/expression/tokenizer.mjs +96 -0
  46. package/src/lib/expression/tokenizer.mjs.map +1 -0
  47. package/src/lib/format/field.d.mts +967 -0
  48. package/src/lib/format/field.d.ts +967 -0
  49. package/src/lib/format/field.js +147 -0
  50. package/src/lib/format/field.js.map +1 -0
  51. package/src/lib/format/field.mjs +144 -0
  52. package/src/lib/format/field.mjs.map +1 -0
  53. package/src/lib/format/json-schema.d.mts +6 -0
  54. package/src/lib/format/json-schema.d.ts +6 -0
  55. package/src/lib/format/json-schema.js +14 -0
  56. package/src/lib/format/json-schema.js.map +1 -0
  57. package/src/lib/format/json-schema.mjs +11 -0
  58. package/src/lib/format/json-schema.mjs.map +1 -0
  59. package/src/lib/format/json.d.mts +7 -0
  60. package/src/lib/format/json.d.ts +7 -0
  61. package/src/lib/format/json.js +7 -0
  62. package/src/lib/format/json.js.map +1 -0
  63. package/src/lib/format/json.mjs +4 -0
  64. package/src/lib/format/json.mjs.map +1 -0
  65. package/src/lib/format/layout.d.mts +521 -0
  66. package/src/lib/format/layout.d.ts +521 -0
  67. package/src/lib/format/layout.js +150 -0
  68. package/src/lib/format/layout.js.map +1 -0
  69. package/src/lib/format/layout.mjs +147 -0
  70. package/src/lib/format/layout.mjs.map +1 -0
  71. package/src/lib/format/layout.types.d.mts +22 -0
  72. package/src/lib/format/layout.types.d.ts +22 -0
  73. package/src/lib/format/layout.types.js +3 -0
  74. package/src/lib/format/layout.types.js.map +1 -0
  75. package/src/lib/format/layout.types.mjs +2 -0
  76. package/src/lib/format/layout.types.mjs.map +1 -0
  77. package/src/lib/format/page.d.mts +511 -0
  78. package/src/lib/format/page.d.ts +511 -0
  79. package/src/lib/format/page.js +27 -0
  80. package/src/lib/format/page.js.map +1 -0
  81. package/src/lib/format/page.mjs +24 -0
  82. package/src/lib/format/page.mjs.map +1 -0
  83. package/src/lib/format/validate.d.mts +23 -0
  84. package/src/lib/format/validate.d.ts +23 -0
  85. package/src/lib/format/validate.js +210 -0
  86. package/src/lib/format/validate.js.map +1 -0
  87. package/src/lib/format/validate.mjs +207 -0
  88. package/src/lib/format/validate.mjs.map +1 -0
  89. package/src/lib/record/check.d.mts +14 -0
  90. package/src/lib/record/check.d.ts +14 -0
  91. package/src/lib/record/check.js +167 -0
  92. package/src/lib/record/check.js.map +1 -0
  93. package/src/lib/record/check.mjs +163 -0
  94. package/src/lib/record/check.mjs.map +1 -0
  95. package/src/lib/record/data-source.d.mts +109 -0
  96. package/src/lib/record/data-source.d.ts +109 -0
  97. package/src/lib/record/data-source.js +3 -0
  98. package/src/lib/record/data-source.js.map +1 -0
  99. package/src/lib/record/data-source.mjs +2 -0
  100. package/src/lib/record/data-source.mjs.map +1 -0
  101. package/src/lib/record/form.d.mts +106 -0
  102. package/src/lib/record/form.d.ts +106 -0
  103. package/src/lib/record/form.js +545 -0
  104. package/src/lib/record/form.js.map +1 -0
  105. package/src/lib/record/form.mjs +542 -0
  106. package/src/lib/record/form.mjs.map +1 -0
  107. package/src/lib/record/memory-data-source.d.mts +28 -0
  108. package/src/lib/record/memory-data-source.d.ts +28 -0
  109. package/src/lib/record/memory-data-source.js +132 -0
  110. package/src/lib/record/memory-data-source.js.map +1 -0
  111. package/src/lib/record/memory-data-source.mjs +129 -0
  112. package/src/lib/record/memory-data-source.mjs.map +1 -0
  113. package/src/lib/record/messages.d.mts +35 -0
  114. package/src/lib/record/messages.d.ts +35 -0
  115. package/src/lib/record/messages.js +111 -0
  116. package/src/lib/record/messages.js.map +1 -0
  117. package/src/lib/record/messages.mjs +107 -0
  118. package/src/lib/record/messages.mjs.map +1 -0
  119. package/src/lib/record/scheduler.d.mts +12 -0
  120. package/src/lib/record/scheduler.d.ts +12 -0
  121. package/src/lib/record/scheduler.js +16 -0
  122. package/src/lib/record/scheduler.js.map +1 -0
  123. package/src/lib/record/scheduler.mjs +12 -0
  124. package/src/lib/record/scheduler.mjs.map +1 -0
  125. package/src/lib/record/values.d.mts +54 -0
  126. package/src/lib/record/values.d.ts +54 -0
  127. package/src/lib/record/values.js +78 -0
  128. package/src/lib/record/values.js.map +1 -0
  129. package/src/lib/record/values.mjs +71 -0
  130. package/src/lib/record/values.mjs.map +1 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * AST evaluator for modifiers, ported from the React form engine.
3
+ *
4
+ * Python semantics where the React version used JavaScript's: an empty list
5
+ * or string is false, and an empty value is never smaller or larger than
6
+ * anything — modifiers come from backends that think in Python.
7
+ */
8
+ /**
9
+ * Evaluate an AST node against a context object
10
+ *
11
+ * @param ast - The AST node to evaluate
12
+ * @param context - Object containing field values
13
+ * @returns The result of evaluation
14
+ *
15
+ * @example
16
+ * const ast = { type: 'BinaryOp', operator: '==', left: {...}, right: {...} };
17
+ * const context = { state: 'draft', amount: 100 };
18
+ * evaluate(ast, context); // Returns boolean result
19
+ */
20
+ export function evaluate(ast, context) {
21
+ switch (ast.type) {
22
+ case 'BinaryOp':
23
+ return evaluateBinaryOp(ast, context);
24
+ case 'UnaryOp':
25
+ return evaluateUnaryOp(ast, context);
26
+ case 'Literal':
27
+ return ast.value;
28
+ case 'Identifier':
29
+ return getNestedValue(context, ast.name);
30
+ default:
31
+ throw new Error(`Unknown AST node type: ${ast.type}`);
32
+ }
33
+ }
34
+ /**
35
+ * Evaluate binary operations
36
+ */
37
+ function evaluateBinaryOp(node, context) {
38
+ const left = normalize(evaluate(node.left, context));
39
+ const right = normalize(evaluate(node.right, context));
40
+ const operator = node.operator.toLowerCase();
41
+ const ordered = left !== null && right !== null;
42
+ switch (operator) {
43
+ // Comparison operators
44
+ case '==':
45
+ return left === right;
46
+ case '!=':
47
+ case '<>':
48
+ return left !== right;
49
+ case '<':
50
+ return ordered && left < right;
51
+ case '>':
52
+ return ordered && left > right;
53
+ case '<=':
54
+ return ordered && left <= right;
55
+ case '>=':
56
+ return ordered && left >= right;
57
+ // In operators
58
+ case 'in':
59
+ return Array.isArray(right) && right.includes(left);
60
+ case 'not in':
61
+ return Array.isArray(right) && !right.includes(left);
62
+ // Logical operators (use truthiness)
63
+ case 'and':
64
+ return truthy(left) && truthy(right);
65
+ case 'or':
66
+ return truthy(left) || truthy(right);
67
+ default:
68
+ throw new Error(`Unknown binary operator: ${node.operator}`);
69
+ }
70
+ }
71
+ /**
72
+ * Evaluate unary operations
73
+ */
74
+ function evaluateUnaryOp(node, context) {
75
+ const operand = evaluate(node.operand, context);
76
+ const operator = node.operator.toLowerCase();
77
+ switch (operator) {
78
+ case 'not':
79
+ return !truthy(operand);
80
+ default:
81
+ throw new Error(`Unknown unary operator: ${node.operator}`);
82
+ }
83
+ }
84
+ /** A missing value compares like None. */
85
+ function normalize(value) {
86
+ return value === undefined ? null : value;
87
+ }
88
+ /** Python's truth test: empty lists, strings and objects, 0 and None are false. */
89
+ export function truthy(value) {
90
+ if (Array.isArray(value))
91
+ return value.length > 0;
92
+ if (value !== null && typeof value === 'object')
93
+ return Object.keys(value).length > 0;
94
+ return Boolean(value);
95
+ }
96
+ /**
97
+ * Get a nested value from an object using dot notation
98
+ *
99
+ * @param obj - The object to traverse
100
+ * @param path - Dot-separated path (e.g., 'partner_id.country_id.name')
101
+ * @returns The value at the path, or undefined if not found
102
+ *
103
+ * @example
104
+ * getNestedValue({ a: { b: { c: 5 } } }, 'a.b.c') // Returns 5
105
+ * getNestedValue({ a: null }, 'a.b.c') // Returns undefined
106
+ */
107
+ function getNestedValue(obj, path) {
108
+ const parts = path.split('.');
109
+ let current = obj;
110
+ for (const part of parts) {
111
+ if (current === null || current === undefined) {
112
+ return undefined;
113
+ }
114
+ current = current[part];
115
+ }
116
+ return current;
117
+ }
118
+ //# sourceMappingURL=evaluator.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluator.mjs","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/evaluator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Extract field names from modifier expressions
3
+ *
4
+ * Used to determine which form fields need to be watched for modifier evaluation.
5
+ * This enables targeted subscriptions instead of watching all form values.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * extractFieldNames("state == 'draft'") // ['state']
10
+ * extractFieldNames("state == 'draft' and amount > 100") // ['state', 'amount']
11
+ * extractFieldNames("partner_id.country_id.code == 'US'") // ['partner_id']
12
+ * extractFieldNames(true) // []
13
+ * extractFieldNames(undefined) // []
14
+ * ```
15
+ */
16
+ /**
17
+ * Extract all field names referenced in a modifier expression
18
+ *
19
+ * @param expression - Modifier expression (string, boolean, or undefined)
20
+ * @returns Array of root field names to watch
21
+ */
22
+ export declare function extractFieldNames(expression: string | boolean | undefined): string[];
23
+ /**
24
+ * Extract all field names from a modifiers object
25
+ *
26
+ * @param modifiers - Object containing invisible/readonly/required expressions
27
+ * @returns Array of unique root field names to watch
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * extractFieldNamesFromModifiers({
32
+ * invisible: "state == 'draft'",
33
+ * readonly: "amount > 100"
34
+ * }) // ['state', 'amount']
35
+ * ```
36
+ */
37
+ export declare function extractFieldNamesFromModifiers(modifiers: Record<string, string | boolean | undefined> | {
38
+ invisible?: string | boolean;
39
+ readonly?: string | boolean;
40
+ required?: string | boolean;
41
+ }): string[];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Extract field names from modifier expressions
3
+ *
4
+ * Used to determine which form fields need to be watched for modifier evaluation.
5
+ * This enables targeted subscriptions instead of watching all form values.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * extractFieldNames("state == 'draft'") // ['state']
10
+ * extractFieldNames("state == 'draft' and amount > 100") // ['state', 'amount']
11
+ * extractFieldNames("partner_id.country_id.code == 'US'") // ['partner_id']
12
+ * extractFieldNames(true) // []
13
+ * extractFieldNames(undefined) // []
14
+ * ```
15
+ */
16
+ /**
17
+ * Extract all field names referenced in a modifier expression
18
+ *
19
+ * @param expression - Modifier expression (string, boolean, or undefined)
20
+ * @returns Array of root field names to watch
21
+ */
22
+ export declare function extractFieldNames(expression: string | boolean | undefined): string[];
23
+ /**
24
+ * Extract all field names from a modifiers object
25
+ *
26
+ * @param modifiers - Object containing invisible/readonly/required expressions
27
+ * @returns Array of unique root field names to watch
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * extractFieldNamesFromModifiers({
32
+ * invisible: "state == 'draft'",
33
+ * readonly: "amount > 100"
34
+ * }) // ['state', 'amount']
35
+ * ```
36
+ */
37
+ export declare function extractFieldNamesFromModifiers(modifiers: Record<string, string | boolean | undefined> | {
38
+ invisible?: string | boolean;
39
+ readonly?: string | boolean;
40
+ required?: string | boolean;
41
+ }): string[];
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Extract field names from modifier expressions
4
+ *
5
+ * Used to determine which form fields need to be watched for modifier evaluation.
6
+ * This enables targeted subscriptions instead of watching all form values.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * extractFieldNames("state == 'draft'") // ['state']
11
+ * extractFieldNames("state == 'draft' and amount > 100") // ['state', 'amount']
12
+ * extractFieldNames("partner_id.country_id.code == 'US'") // ['partner_id']
13
+ * extractFieldNames(true) // []
14
+ * extractFieldNames(undefined) // []
15
+ * ```
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.extractFieldNames = extractFieldNames;
19
+ exports.extractFieldNamesFromModifiers = extractFieldNamesFromModifiers;
20
+ const tokenizer_1 = require("./tokenizer");
21
+ /**
22
+ * Extract all field names referenced in a modifier expression
23
+ *
24
+ * @param expression - Modifier expression (string, boolean, or undefined)
25
+ * @returns Array of root field names to watch
26
+ */
27
+ function extractFieldNames(expression) {
28
+ // Non-string expressions don't reference fields
29
+ if (typeof expression !== 'string' || expression.trim() === '') {
30
+ return [];
31
+ }
32
+ try {
33
+ const tokens = (0, tokenizer_1.tokenize)(expression);
34
+ const fields = new Set();
35
+ // Keywords that look like identifiers but aren't field names
36
+ const keywords = new Set(['null', 'true', 'false', 'True', 'False', 'None']);
37
+ tokens.forEach(token => {
38
+ if (token.type === 'IDENTIFIER') {
39
+ // Skip literal keywords
40
+ if (keywords.has(token.value)) {
41
+ return;
42
+ }
43
+ // For nested fields like 'partner_id.country_id.name',
44
+ // we only need to watch the root field 'partner_id'
45
+ const rootField = token.value.split('.')[0];
46
+ fields.add(rootField);
47
+ }
48
+ });
49
+ return Array.from(fields);
50
+ }
51
+ catch {
52
+ // An expression that cannot be read reads no fields. validatePage reports
53
+ // it, so a validated page never reaches this branch.
54
+ return [];
55
+ }
56
+ }
57
+ // CRITICAL: Cache to prevent creating new arrays when the result is the same
58
+ // WeakMap ensures memory is freed when modifiers object is garbage collected
59
+ const extractionCache = new WeakMap();
60
+ // Stable empty array for when there are no fields to extract
61
+ const EMPTY_FIELDS_ARRAY = Object.freeze([]);
62
+ /**
63
+ * Extract all field names from a modifiers object
64
+ *
65
+ * @param modifiers - Object containing invisible/readonly/required expressions
66
+ * @returns Array of unique root field names to watch
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * extractFieldNamesFromModifiers({
71
+ * invisible: "state == 'draft'",
72
+ * readonly: "amount > 100"
73
+ * }) // ['state', 'amount']
74
+ * ```
75
+ */
76
+ function extractFieldNamesFromModifiers(modifiers) {
77
+ // Check cache first for stable reference
78
+ const cached = extractionCache.get(modifiers);
79
+ if (cached !== undefined) {
80
+ return cached;
81
+ }
82
+ const allFields = new Set();
83
+ Object.values(modifiers).forEach(expression => {
84
+ const fields = extractFieldNames(expression);
85
+ fields.forEach(field => allFields.add(field));
86
+ });
87
+ // Return stable empty array if no fields
88
+ if (allFields.size === 0) {
89
+ extractionCache.set(modifiers, EMPTY_FIELDS_ARRAY);
90
+ return EMPTY_FIELDS_ARRAY;
91
+ }
92
+ // Freeze the result array to ensure immutability
93
+ const result = Object.freeze(Array.from(allFields));
94
+ extractionCache.set(modifiers, result);
95
+ return result;
96
+ }
97
+ //# sourceMappingURL=extractFieldNames.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractFieldNames.js","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/extractFieldNames.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAUH,8CAiCC;AAuBD,wEA0BC;AA1FD,2CAAuC;AAEvC;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,UAAwC;IACxE,gDAAgD;IAChD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/D,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,oBAAQ,EAAC,UAAU,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;QAEjC,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAE7E,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;YACrB,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAChC,wBAAwB;gBACxB,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC9B,OAAO;gBACT,CAAC;gBAED,uDAAuD;gBACvD,oDAAoD;gBACpD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC5C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;QAC1E,qDAAqD;QACrD,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,6EAA6E;AAC7E,MAAM,eAAe,GAAG,IAAI,OAAO,EAAoB,CAAC;AAExD,6DAA6D;AAC7D,MAAM,kBAAkB,GAAa,MAAM,CAAC,MAAM,CAAC,EAAE,CAAwB,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,SAAgB,8BAA8B,CAC5C,SAAoJ;IAEpJ,yCAAyC;IACzC,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IAEpC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;QAC5C,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC7C,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,yCAAyC;IACzC,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACzB,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;QACnD,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,iDAAiD;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAa,CAAC;IAChE,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Extract field names from modifier expressions
3
+ *
4
+ * Used to determine which form fields need to be watched for modifier evaluation.
5
+ * This enables targeted subscriptions instead of watching all form values.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * extractFieldNames("state == 'draft'") // ['state']
10
+ * extractFieldNames("state == 'draft' and amount > 100") // ['state', 'amount']
11
+ * extractFieldNames("partner_id.country_id.code == 'US'") // ['partner_id']
12
+ * extractFieldNames(true) // []
13
+ * extractFieldNames(undefined) // []
14
+ * ```
15
+ */
16
+ import { tokenize } from './tokenizer.mjs';
17
+ /**
18
+ * Extract all field names referenced in a modifier expression
19
+ *
20
+ * @param expression - Modifier expression (string, boolean, or undefined)
21
+ * @returns Array of root field names to watch
22
+ */
23
+ export function extractFieldNames(expression) {
24
+ // Non-string expressions don't reference fields
25
+ if (typeof expression !== 'string' || expression.trim() === '') {
26
+ return [];
27
+ }
28
+ try {
29
+ const tokens = tokenize(expression);
30
+ const fields = new Set();
31
+ // Keywords that look like identifiers but aren't field names
32
+ const keywords = new Set(['null', 'true', 'false', 'True', 'False', 'None']);
33
+ tokens.forEach(token => {
34
+ if (token.type === 'IDENTIFIER') {
35
+ // Skip literal keywords
36
+ if (keywords.has(token.value)) {
37
+ return;
38
+ }
39
+ // For nested fields like 'partner_id.country_id.name',
40
+ // we only need to watch the root field 'partner_id'
41
+ const rootField = token.value.split('.')[0];
42
+ fields.add(rootField);
43
+ }
44
+ });
45
+ return Array.from(fields);
46
+ }
47
+ catch {
48
+ // An expression that cannot be read reads no fields. validatePage reports
49
+ // it, so a validated page never reaches this branch.
50
+ return [];
51
+ }
52
+ }
53
+ // CRITICAL: Cache to prevent creating new arrays when the result is the same
54
+ // WeakMap ensures memory is freed when modifiers object is garbage collected
55
+ const extractionCache = new WeakMap();
56
+ // Stable empty array for when there are no fields to extract
57
+ const EMPTY_FIELDS_ARRAY = Object.freeze([]);
58
+ /**
59
+ * Extract all field names from a modifiers object
60
+ *
61
+ * @param modifiers - Object containing invisible/readonly/required expressions
62
+ * @returns Array of unique root field names to watch
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * extractFieldNamesFromModifiers({
67
+ * invisible: "state == 'draft'",
68
+ * readonly: "amount > 100"
69
+ * }) // ['state', 'amount']
70
+ * ```
71
+ */
72
+ export function extractFieldNamesFromModifiers(modifiers) {
73
+ // Check cache first for stable reference
74
+ const cached = extractionCache.get(modifiers);
75
+ if (cached !== undefined) {
76
+ return cached;
77
+ }
78
+ const allFields = new Set();
79
+ Object.values(modifiers).forEach(expression => {
80
+ const fields = extractFieldNames(expression);
81
+ fields.forEach(field => allFields.add(field));
82
+ });
83
+ // Return stable empty array if no fields
84
+ if (allFields.size === 0) {
85
+ extractionCache.set(modifiers, EMPTY_FIELDS_ARRAY);
86
+ return EMPTY_FIELDS_ARRAY;
87
+ }
88
+ // Freeze the result array to ensure immutability
89
+ const result = Object.freeze(Array.from(allFields));
90
+ extractionCache.set(modifiers, result);
91
+ return result;
92
+ }
93
+ //# sourceMappingURL=extractFieldNames.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractFieldNames.mjs","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/extractFieldNames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAwC;IACxE,gDAAgD;IAChD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/D,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;QAEjC,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAE7E,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;YACrB,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAChC,wBAAwB;gBACxB,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC9B,OAAO;gBACT,CAAC;gBAED,uDAAuD;gBACvD,oDAAoD;gBACpD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC5C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;QAC1E,qDAAqD;QACrD,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,6EAA6E;AAC7E,MAAM,eAAe,GAAG,IAAI,OAAO,EAAoB,CAAC;AAExD,6DAA6D;AAC7D,MAAM,kBAAkB,GAAa,MAAM,CAAC,MAAM,CAAC,EAAE,CAAwB,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,8BAA8B,CAC5C,SAAoJ;IAEpJ,yCAAyC;IACzC,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IAEpC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;QAC5C,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC7C,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,yCAAyC;IACzC,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACzB,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;QACnD,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,iDAAiD;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAa,CAAC;IAChE,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Modifier } from '../format/layout.mjs';
2
+ /** A modifier read once, ready to evaluate against a record's values. */
3
+ export interface CompiledModifier {
4
+ readonly source: Modifier | undefined;
5
+ /** The fields it reads, by their first name, in order of appearance. */
6
+ readonly fields: readonly string[];
7
+ evaluate(values: Readonly<Record<string, unknown>>): boolean;
8
+ }
9
+ /**
10
+ * Read a modifier. `true`, `false` and a missing modifier become constants;
11
+ * an expression is parsed now, so a mistake surfaces here and not on the
12
+ * first keystroke. Throws when the expression cannot be read.
13
+ */
14
+ export declare function compileModifier(modifier: Modifier | undefined): CompiledModifier;
@@ -0,0 +1,14 @@
1
+ import type { Modifier } from '../format/layout';
2
+ /** A modifier read once, ready to evaluate against a record's values. */
3
+ export interface CompiledModifier {
4
+ readonly source: Modifier | undefined;
5
+ /** The fields it reads, by their first name, in order of appearance. */
6
+ readonly fields: readonly string[];
7
+ evaluate(values: Readonly<Record<string, unknown>>): boolean;
8
+ }
9
+ /**
10
+ * Read a modifier. `true`, `false` and a missing modifier become constants;
11
+ * an expression is parsed now, so a mistake surfaces here and not on the
12
+ * first keystroke. Throws when the expression cannot be read.
13
+ */
14
+ export declare function compileModifier(modifier: Modifier | undefined): CompiledModifier;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compileModifier = compileModifier;
4
+ const evaluator_1 = require("./evaluator");
5
+ const parser_1 = require("./parser");
6
+ const tokenizer_1 = require("./tokenizer");
7
+ /**
8
+ * Read a modifier. `true`, `false` and a missing modifier become constants;
9
+ * an expression is parsed now, so a mistake surfaces here and not on the
10
+ * first keystroke. Throws when the expression cannot be read.
11
+ */
12
+ function compileModifier(modifier) {
13
+ if (typeof modifier !== 'string') {
14
+ const value = modifier === true;
15
+ return { source: modifier, fields: [], evaluate: () => value };
16
+ }
17
+ const ast = new parser_1.ExpressionParser((0, tokenizer_1.tokenize)(modifier)).parse();
18
+ return {
19
+ source: modifier,
20
+ fields: fieldsRead(ast),
21
+ evaluate(values) {
22
+ try {
23
+ return (0, evaluator_1.truthy)((0, evaluator_1.evaluate)(ast, values));
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ },
29
+ };
30
+ }
31
+ function fieldsRead(ast) {
32
+ const names = [];
33
+ const visit = (node) => {
34
+ switch (node.type) {
35
+ case 'Identifier': {
36
+ const root = node.name.split('.')[0];
37
+ if (!names.includes(root))
38
+ names.push(root);
39
+ return;
40
+ }
41
+ case 'BinaryOp':
42
+ visit(node.left);
43
+ visit(node.right);
44
+ return;
45
+ case 'UnaryOp':
46
+ visit(node.operand);
47
+ return;
48
+ default:
49
+ return;
50
+ }
51
+ };
52
+ visit(ast);
53
+ return names;
54
+ }
55
+ //# sourceMappingURL=modifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modifier.js","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/modifier.ts"],"names":[],"mappings":";;AAkBA,0CAiBC;AAlCD,2CAA+C;AAC/C,qCAA0D;AAC1D,2CAAuC;AAUvC;;;;GAIG;AACH,SAAgB,eAAe,CAAC,QAA8B;IAC5D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,QAAQ,KAAK,IAAI,CAAC;QAChC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;IACjE,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,yBAAgB,CAAC,IAAA,oBAAQ,EAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;IAC7D,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC;QACvB,QAAQ,CAAC,MAAM;YACb,IAAI,CAAC;gBACH,OAAO,IAAA,kBAAM,EAAC,IAAA,oBAAQ,EAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,GAAY;IAC9B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QACpC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAClB,KAAK,YAAY,CAAC,CAAC,CAAC;gBAClB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;YACD,KAAK,UAAU;gBACb,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,OAAO;YACT,KAAK,SAAS;gBACZ,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACpB,OAAO;YACT;gBACE,OAAO;QACX,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,GAAG,CAAC,CAAC;IACX,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,52 @@
1
+ import { evaluate, truthy } from './evaluator.mjs';
2
+ import { ExpressionParser } from './parser.mjs';
3
+ import { tokenize } from './tokenizer.mjs';
4
+ /**
5
+ * Read a modifier. `true`, `false` and a missing modifier become constants;
6
+ * an expression is parsed now, so a mistake surfaces here and not on the
7
+ * first keystroke. Throws when the expression cannot be read.
8
+ */
9
+ export function compileModifier(modifier) {
10
+ if (typeof modifier !== 'string') {
11
+ const value = modifier === true;
12
+ return { source: modifier, fields: [], evaluate: () => value };
13
+ }
14
+ const ast = new ExpressionParser(tokenize(modifier)).parse();
15
+ return {
16
+ source: modifier,
17
+ fields: fieldsRead(ast),
18
+ evaluate(values) {
19
+ try {
20
+ return truthy(evaluate(ast, values));
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ },
26
+ };
27
+ }
28
+ function fieldsRead(ast) {
29
+ const names = [];
30
+ const visit = (node) => {
31
+ switch (node.type) {
32
+ case 'Identifier': {
33
+ const root = node.name.split('.')[0];
34
+ if (!names.includes(root))
35
+ names.push(root);
36
+ return;
37
+ }
38
+ case 'BinaryOp':
39
+ visit(node.left);
40
+ visit(node.right);
41
+ return;
42
+ case 'UnaryOp':
43
+ visit(node.operand);
44
+ return;
45
+ default:
46
+ return;
47
+ }
48
+ };
49
+ visit(ast);
50
+ return names;
51
+ }
52
+ //# sourceMappingURL=modifier.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modifier.mjs","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/expression/modifier.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAgB,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAUvC;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,QAA8B;IAC5D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,QAAQ,KAAK,IAAI,CAAC;QAChC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;IACjE,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;IAC7D,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC;QACvB,QAAQ,CAAC,MAAM;YACb,IAAI,CAAC;gBACH,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,GAAY;IAC9B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QACpC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAClB,KAAK,YAAY,CAAC,CAAC,CAAC;gBAClB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;YACD,KAAK,UAAU;gBACb,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,OAAO;YACT,KAAK,SAAS;gBACZ,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACpB,OAAO;YACT;gBACE,OAAO;QACX,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,GAAG,CAAC,CAAC;IACX,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Expression parser for modifiers, ported from the React form engine.
3
+ *
4
+ * Recursive descent parser that converts tokens into an Abstract Syntax Tree (AST).
5
+ * Implements Python-like operator precedence:
6
+ * 1. OR (lowest precedence)
7
+ * 2. AND
8
+ * 3. NOT
9
+ * 4. Comparison operators (==, !=, <, >, <=, >=, in, not in)
10
+ * 5. Values/Identifiers (highest precedence)
11
+ */
12
+ import { type Token } from './tokenizer.mjs';
13
+ /**
14
+ * AST Node Types
15
+ */
16
+ export type ASTNode = BinaryOpNode | UnaryOpNode | LiteralNode | IdentifierNode;
17
+ export interface BinaryOpNode {
18
+ type: 'BinaryOp';
19
+ operator: string;
20
+ left: ASTNode;
21
+ right: ASTNode;
22
+ }
23
+ export interface UnaryOpNode {
24
+ type: 'UnaryOp';
25
+ operator: string;
26
+ operand: ASTNode;
27
+ }
28
+ export interface LiteralNode {
29
+ type: 'Literal';
30
+ value: unknown;
31
+ }
32
+ export interface IdentifierNode {
33
+ type: 'Identifier';
34
+ name: string;
35
+ }
36
+ /**
37
+ * Recursive Descent Expression Parser
38
+ */
39
+ export declare class ExpressionParser {
40
+ private tokens;
41
+ private position;
42
+ constructor(tokens: Token[]);
43
+ /**
44
+ * Parse tokens into an AST
45
+ */
46
+ parse(): ASTNode;
47
+ /**
48
+ * Parse OR expressions (lowest precedence)
49
+ * or_expr: and_expr ('or' and_expr)*
50
+ */
51
+ private parseOr;
52
+ /**
53
+ * Parse AND expressions
54
+ * and_expr: not_expr ('and' not_expr)*
55
+ */
56
+ private parseAnd;
57
+ /**
58
+ * Parse NOT expressions
59
+ * not_expr: 'not' comparison | comparison
60
+ */
61
+ private parseNot;
62
+ /**
63
+ * Parse comparison expressions
64
+ * comparison: value (comp_op value)?
65
+ * comp_op: '==' | '!=' | '<>' | '<' | '>' | '<=' | '>=' | 'in' | 'not in'
66
+ */
67
+ private parseComparison;
68
+ /**
69
+ * Parse value expressions (highest precedence)
70
+ * value: '(' or_expr ')' | literal | identifier
71
+ */
72
+ private parseValue;
73
+ /**
74
+ * Check if current token matches any of the given types/values
75
+ */
76
+ private match;
77
+ /**
78
+ * Match logical operators (case-insensitive)
79
+ */
80
+ private matchLogical;
81
+ /**
82
+ * Match comparison operators (case-insensitive)
83
+ */
84
+ private matchOperator;
85
+ /**
86
+ * Check if current token has given type
87
+ */
88
+ private check;
89
+ /**
90
+ * Consume current token and advance
91
+ */
92
+ private advance;
93
+ /**
94
+ * Get current token without advancing
95
+ */
96
+ private peek;
97
+ /**
98
+ * Get previous token
99
+ */
100
+ private previous;
101
+ /**
102
+ * Check if we've consumed all tokens
103
+ */
104
+ private isAtEnd;
105
+ /**
106
+ * Consume a token of specific type/value or throw error
107
+ */
108
+ private consume;
109
+ }