@ng-forge/dynamic-forms 1.0.0-rc.3 → 1.0.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 (30) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -34
  3. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-D5qCzJsL.mjs → ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs} +26 -20
  4. package/fesm2022/ng-forge-dynamic-forms-array-field.component-CIheZh11.mjs.map +1 -0
  5. package/fesm2022/{ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-D6ntLtW9.mjs → ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs} +39 -14
  6. package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-C9aVachF.mjs.map +1 -0
  7. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-nB6KFqVs.mjs → ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs} +5 -5
  8. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-nB6KFqVs.mjs.map → ng-forge-dynamic-forms-container-field.component-DNafCSO8.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-AScQAJTQ.mjs → ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs} +4 -4
  10. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-AScQAJTQ.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-BmHGlmES.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Bq6Sgd6Z.mjs → ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs} +5 -5
  12. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-Bq6Sgd6Z.mjs.map → ng-forge-dynamic-forms-group-field.component-CKVIw2K5.mjs.map} +1 -1
  13. package/fesm2022/ng-forge-dynamic-forms-integration.mjs +34 -34
  14. package/fesm2022/ng-forge-dynamic-forms-internal.mjs +1397 -1401
  15. package/fesm2022/ng-forge-dynamic-forms-internal.mjs.map +1 -1
  16. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-C59pvc26.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs} +70 -57
  17. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-CBawLbtp.mjs.map +1 -0
  18. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DRaxbiqA.mjs → ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs} +5 -5
  19. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-DRaxbiqA.mjs.map → ng-forge-dynamic-forms-page-field.component-BRixkgyS.mjs.map} +1 -1
  20. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-BlVa801t.mjs → ng-forge-dynamic-forms-row-wrapper.component-BBWmqQA_.mjs} +4 -4
  21. package/fesm2022/{ng-forge-dynamic-forms-row-wrapper.component-BlVa801t.mjs.map → ng-forge-dynamic-forms-row-wrapper.component-BBWmqQA_.mjs.map} +1 -1
  22. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CNCpmf5_.mjs → ng-forge-dynamic-forms-text-field.component-C-f80sbk.mjs} +4 -4
  23. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-CNCpmf5_.mjs.map → ng-forge-dynamic-forms-text-field.component-C-f80sbk.mjs.map} +1 -1
  24. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  25. package/package.json +6 -6
  26. package/types/ng-forge-dynamic-forms-integration.d.ts +1 -1
  27. package/types/ng-forge-dynamic-forms-internal.d.ts +21 -200
  28. package/fesm2022/ng-forge-dynamic-forms-array-field.component-D5qCzJsL.mjs.map +0 -1
  29. package/fesm2022/ng-forge-dynamic-forms-bootstrap-derivation-orchestrator-D6ntLtW9.mjs.map +0 -1
  30. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-C59pvc26.mjs.map +0 -1
@@ -3,7 +3,7 @@ import { InjectionToken, computed, isSignal, inject, Injector, input, signal, Ch
3
3
  import { NgComponentOutlet } from '@angular/common';
4
4
  import { explicitEffect } from 'ngxtension/explicit-effect';
5
5
  import { toSignal, toObservable, rxResource } from '@angular/core/rxjs-interop';
6
- import { isObservable, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, of, from, map, catchError, tap, startWith, Subject, filter, TimeoutError, throwError } from 'rxjs';
6
+ import { isObservable, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, map, tap, catchError, of, from, startWith, Subject, filter, TimeoutError, throwError } from 'rxjs';
7
7
  import { HttpClient } from '@angular/common/http';
8
8
  import { email, min, max, minLength, maxLength, pattern, required, validate, validateAsync, validateHttp } from '@angular/forms/signals';
9
9
  import { scan, map as map$1, filter as filter$1, take, switchMap, timeout, catchError as catchError$1, shareReplay } from 'rxjs/operators';
@@ -155,14 +155,14 @@ class DfAddonSlot {
155
155
  });
156
156
  });
157
157
  }
158
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: DfAddonSlot, deps: [], target: i0.ɵɵFactoryTarget.Component });
159
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", type: DfAddonSlot, isStandalone: true, selector: "df-addon-slot", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.slot": "slotAttr()", "class": "className()", "style.display": "isHidden() ? \"none\" : null" } }, ngImport: i0, template: `
158
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DfAddonSlot, deps: [], target: i0.ɵɵFactoryTarget.Component });
159
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: DfAddonSlot, isStandalone: true, selector: "df-addon-slot", inputs: { addon: { classPropertyName: "addon", publicName: "addon", isSignal: true, isRequired: true, transformFunction: null }, fieldInputs: { classPropertyName: "fieldInputs", publicName: "fieldInputs", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.slot": "slotAttr()", "class": "className()", "style.display": "isHidden() ? \"none\" : null" } }, ngImport: i0, template: `
160
160
  @if (resolvedComponent(); as cmp) {
161
161
  <ng-container *ngComponentOutlet="cmp; inputs: typeInputs()" />
162
162
  }
163
163
  `, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
164
164
  }
165
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: DfAddonSlot, decorators: [{
165
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DfAddonSlot, decorators: [{
166
166
  type: Component,
167
167
  args: [{
168
168
  selector: 'df-addon-slot',
@@ -475,50 +475,6 @@ class AsyncConditionFunctionCacheService {
475
475
  asyncConditionFunctionCache = new Map();
476
476
  }
477
477
 
478
- function stableStringify(value) {
479
- if (value === null || value === undefined) {
480
- return String(value);
481
- }
482
- if (typeof value !== 'object') {
483
- return JSON.stringify(value);
484
- }
485
- if (Array.isArray(value)) {
486
- return '[' + value.map(stableStringify).join(',') + ']';
487
- }
488
- const obj = value;
489
- const sortedKeys = Object.keys(obj).sort();
490
- const pairs = sortedKeys.map((key) => JSON.stringify(key) + ':' + stableStringify(obj[key]));
491
- return '{' + pairs.join(',') + '}';
492
- }
493
-
494
- const DEFAULT_WRAPPERS = new InjectionToken('DEFAULT_WRAPPERS');
495
- const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT');
496
- function injectFieldSignalContext() {
497
- const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });
498
- if (!ctx) {
499
- throw new DynamicFormError('FIELD_SIGNAL_CONTEXT is not provided. This consumer must run inside a <df-dynamic-form> component tree.');
500
- }
501
- return ctx;
502
- }
503
- const ARRAY_CONTEXT = new InjectionToken('ARRAY_CONTEXT');
504
- const GROUP_CONTEXT = new InjectionToken('GROUP_CONTEXT');
505
- const DEFAULT_PROPS = new InjectionToken('DEFAULT_PROPS');
506
- const DEFAULT_VALIDATION_MESSAGES = new InjectionToken('DEFAULT_VALIDATION_MESSAGES');
507
- const FORM_OPTIONS = new InjectionToken('FORM_OPTIONS');
508
- const EXTERNAL_DATA = new InjectionToken('EXTERNAL_DATA');
509
- const FORM_ID_PREFIX = new InjectionToken('FORM_ID_PREFIX');
510
-
511
- class RootFormRegistryService {
512
- formValue;
513
- rootForm;
514
- constructor(formValue, rootForm) {
515
- this.formValue = formValue;
516
- this.rootForm = rootForm;
517
- }
518
- }
519
-
520
- const DEPRECATION_WARNING_TRACKER = new InjectionToken('DeprecationWarningTracker');
521
-
522
478
  function compareValues(actual, expected, operator) {
523
479
  switch (operator) {
524
480
  case 'equals':
@@ -572,1445 +528,1352 @@ function hasNestedProperty(obj, path) {
572
528
  return keys[keys.length - 1] in current;
573
529
  }
574
530
 
575
- function safeReadPathKeys(fieldContext) {
576
- if (!('pathKeys' in fieldContext) || typeof fieldContext.pathKeys !== 'function') {
577
- return [];
531
+ var TokenType;
532
+ (function (TokenType) {
533
+ TokenType["NUMBER"] = "NUMBER";
534
+ TokenType["STRING"] = "STRING";
535
+ TokenType["TRUE"] = "TRUE";
536
+ TokenType["FALSE"] = "FALSE";
537
+ TokenType["NULL"] = "NULL";
538
+ TokenType["UNDEFINED"] = "UNDEFINED";
539
+ TokenType["IDENTIFIER"] = "IDENTIFIER";
540
+ TokenType["PLUS"] = "PLUS";
541
+ TokenType["MINUS"] = "MINUS";
542
+ TokenType["MULTIPLY"] = "MULTIPLY";
543
+ TokenType["DIVIDE"] = "DIVIDE";
544
+ TokenType["MODULO"] = "MODULO";
545
+ TokenType["EQUAL"] = "EQUAL";
546
+ TokenType["NOT_EQUAL"] = "NOT_EQUAL";
547
+ TokenType["GREATER"] = "GREATER";
548
+ TokenType["LESS"] = "LESS";
549
+ TokenType["GREATER_EQUAL"] = "GREATER_EQUAL";
550
+ TokenType["LESS_EQUAL"] = "LESS_EQUAL";
551
+ TokenType["AND"] = "AND";
552
+ TokenType["OR"] = "OR";
553
+ TokenType["NOT"] = "NOT";
554
+ TokenType["DOT"] = "DOT";
555
+ TokenType["COMMA"] = "COMMA";
556
+ TokenType["COLON"] = "COLON";
557
+ TokenType["LPAREN"] = "LPAREN";
558
+ TokenType["RPAREN"] = "RPAREN";
559
+ TokenType["LBRACKET"] = "LBRACKET";
560
+ TokenType["RBRACKET"] = "RBRACKET";
561
+ TokenType["LBRACE"] = "LBRACE";
562
+ TokenType["RBRACE"] = "RBRACE";
563
+ TokenType["ARROW"] = "ARROW";
564
+ TokenType["QUESTION"] = "QUESTION";
565
+ TokenType["OPTIONAL_DOT"] = "OPTIONAL_DOT";
566
+ TokenType["EOF"] = "EOF";
567
+ })(TokenType || (TokenType = {}));
568
+ class ExpressionParserError extends DynamicFormError {
569
+ position;
570
+ expression;
571
+ constructor(message, position, expression) {
572
+ super(`${message} at position ${position} in expression: ${expression}`);
573
+ this.position = position;
574
+ this.expression = expression;
575
+ this.name = 'ExpressionParserError';
578
576
  }
579
- return untracked(() => fieldContext.pathKeys());
580
577
  }
581
578
 
582
- function isChildFieldContext(context) {
583
- return 'key' in context && isSignal(context.key);
584
- }
585
- function extractFieldState(fieldContext) {
586
- return untracked(() => {
587
- if (!fieldContext || !('state' in fieldContext))
588
- return undefined;
589
- return fieldContext.state;
590
- });
591
- }
592
- function detectArrayScope(pathKeys) {
593
- if (pathKeys.length < 3)
594
- return undefined;
595
- for (let i = pathKeys.length - 2; i >= 1; i--) {
596
- const maybeIndex = Number(pathKeys[i]);
597
- if (Number.isInteger(maybeIndex) && maybeIndex >= 0) {
598
- return {
599
- arrayKey: pathKeys.slice(0, i).join('.'),
600
- index: maybeIndex,
601
- localKey: pathKeys[pathKeys.length - 1],
602
- };
579
+ class Tokenizer {
580
+ position = 0;
581
+ expression;
582
+ constructor(expression) {
583
+ this.expression = expression;
584
+ }
585
+ tokenize() {
586
+ const tokens = [];
587
+ while (this.position < this.expression.length) {
588
+ this.skipWhitespace();
589
+ if (this.position >= this.expression.length) {
590
+ break;
591
+ }
592
+ const token = this.nextToken();
593
+ tokens.push(token);
603
594
  }
595
+ tokens.push({ type: TokenType.EOF, value: '', position: this.position });
596
+ return tokens;
604
597
  }
605
- return undefined;
606
- }
607
- class FieldContextRegistryService {
608
- rootFormRegistry = inject(RootFormRegistryService);
609
- logger = inject(DynamicFormLogger);
610
- deprecationTracker = inject(DEPRECATION_WARNING_TRACKER, { optional: true });
611
- externalDataSignal = inject(EXTERNAL_DATA, { optional: true });
612
- createEvaluationContext(fieldContext, customFunctions) {
613
- const fieldValue = untracked(() => fieldContext.value());
614
- const rootFormValue = untracked(() => this.rootFormRegistry.formValue());
615
- const localKey = this.extractFieldPath(fieldContext);
616
- const pathKeys = safeReadPathKeys(fieldContext);
617
- const arrayScope = detectArrayScope(pathKeys);
618
- if (arrayScope) {
619
- return this.buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, false, fieldContext);
598
+ skipWhitespace() {
599
+ while (this.position < this.expression.length && /\s/.test(this.expression[this.position])) {
600
+ this.position++;
620
601
  }
621
- const rootFormSignal = this.rootFormRegistry.rootForm;
622
- return {
623
- fieldValue,
624
- formValue: rootFormValue,
625
- fieldPath: localKey,
626
- customFunctions: customFunctions || {},
627
- externalData: this.resolveExternalData(false),
628
- logger: this.logger,
629
- deprecationTracker: this.deprecationTracker ?? undefined,
630
- get fieldState() {
631
- return readFieldStateInfo(extractFieldState(fieldContext), false);
632
- },
633
- get formFieldState() {
634
- return createFormFieldStateMap(untracked(rootFormSignal), false);
635
- },
636
- };
637
602
  }
638
- extractFieldPath(fieldContext) {
639
- if (isChildFieldContext(fieldContext)) {
640
- try {
641
- return String(fieldContext.key());
603
+ nextToken() {
604
+ const char = this.expression[this.position];
605
+ if (char === '"' || char === "'") {
606
+ return this.readString(char);
607
+ }
608
+ if (/[0-9]/.test(char)) {
609
+ return this.readNumber();
610
+ }
611
+ if (/[a-zA-Z_$]/.test(char)) {
612
+ return this.readIdentifierOrKeyword();
613
+ }
614
+ return this.readOperatorOrPunctuation();
615
+ }
616
+ readString(quote) {
617
+ const start = this.position;
618
+ this.position++; let value = '';
619
+ while (this.position < this.expression.length) {
620
+ const char = this.expression[this.position];
621
+ if (char === quote) {
622
+ this.position++; return { type: TokenType.STRING, value, position: start };
642
623
  }
643
- catch (error) {
644
- this.logger.warn('Unable to extract field key:', error);
624
+ if (char === '\\' && this.position + 1 < this.expression.length) {
625
+ this.position++;
626
+ const nextChar = this.expression[this.position];
627
+ switch (nextChar) {
628
+ case 'n':
629
+ value += '\n';
630
+ break;
631
+ case 't':
632
+ value += '\t';
633
+ break;
634
+ case 'r':
635
+ value += '\r';
636
+ break;
637
+ case '\\':
638
+ value += '\\';
639
+ break;
640
+ case quote:
641
+ value += quote;
642
+ break;
643
+ default:
644
+ value += nextChar;
645
+ }
646
+ this.position++;
647
+ }
648
+ else {
649
+ value += char;
650
+ this.position++;
645
651
  }
646
652
  }
647
- return '';
653
+ throw new ExpressionParserError('Unterminated string literal', start, this.expression);
648
654
  }
649
- buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, reactive, fieldContext) {
650
- const { arrayKey, index, localKey } = arrayScope;
651
- const arrayData = getNestedValue(rootFormValue, arrayKey);
652
- let scopedFormValue;
653
- if (Array.isArray(arrayData) && index >= 0 && index < arrayData.length) {
654
- const item = arrayData[index];
655
- if (item != null && typeof item === 'object') {
656
- scopedFormValue = item;
657
- }
655
+ readNumber() {
656
+ const start = this.position;
657
+ let value = '';
658
+ while (this.position < this.expression.length && /[0-9.]/.test(this.expression[this.position])) {
659
+ value += this.expression[this.position];
660
+ this.position++;
658
661
  }
659
- const rootFormSignal = this.rootFormRegistry.rootForm;
660
- const fieldStateGetter = fieldContext ? () => readFieldStateInfo(extractFieldState(fieldContext), reactive) : () => undefined;
661
- const formFieldStateGetter = () => createFormFieldStateMap((reactive ? rootFormSignal() : untracked(rootFormSignal)), reactive);
662
- if (!scopedFormValue) {
663
- return {
664
- fieldValue,
665
- formValue: rootFormValue,
666
- fieldPath: localKey,
667
- customFunctions: customFunctions || {},
668
- externalData: this.resolveExternalData(reactive),
669
- logger: this.logger,
670
- deprecationTracker: this.deprecationTracker ?? undefined,
671
- get fieldState() {
672
- return fieldStateGetter();
673
- },
674
- get formFieldState() {
675
- return formFieldStateGetter();
676
- },
677
- };
662
+ if (!/^\d+(\.\d+)?$/.test(value)) {
663
+ throw new ExpressionParserError(`Invalid number: ${value}`, start, this.expression);
678
664
  }
679
- return {
680
- fieldValue,
681
- formValue: scopedFormValue,
682
- rootFormValue,
683
- arrayIndex: index,
684
- arrayPath: arrayKey,
685
- fieldPath: `${arrayKey}.${index}.${localKey}`,
686
- customFunctions: customFunctions || {},
687
- externalData: this.resolveExternalData(reactive),
688
- logger: this.logger,
689
- deprecationTracker: this.deprecationTracker ?? undefined,
690
- get fieldState() {
691
- return fieldStateGetter();
692
- },
693
- get formFieldState() {
694
- return formFieldStateGetter();
695
- },
696
- };
665
+ return { type: TokenType.NUMBER, value, position: start };
697
666
  }
698
- resolveExternalData(reactive) {
699
- const externalDataSignal = this.externalDataSignal;
700
- if (!externalDataSignal)
701
- return undefined;
702
- const externalDataRecord = reactive ? externalDataSignal() : untracked(() => externalDataSignal());
703
- if (!externalDataRecord) {
704
- return undefined;
705
- }
706
- const resolved = {};
707
- for (const [key, value] of Object.entries(externalDataRecord)) {
708
- if (!isSignal(value)) {
709
- throw new DynamicFormError(`externalData["${key}"] must be a Signal. Got: ${typeof value}. Wrap it with signal(yourValue).`);
710
- }
711
- resolved[key] = reactive ? value() : untracked(() => value());
667
+ readIdentifierOrKeyword() {
668
+ const start = this.position;
669
+ let value = '';
670
+ while (this.position < this.expression.length && /[a-zA-Z0-9_$]/.test(this.expression[this.position])) {
671
+ value += this.expression[this.position];
672
+ this.position++;
712
673
  }
713
- return resolved;
674
+ const type = this.getKeywordType(value);
675
+ return { type, value, position: start };
714
676
  }
715
- createReactiveEvaluationContext(fieldContext, customFunctions) {
716
- const fieldValue = fieldContext.value();
717
- const rootFormValue = this.rootFormRegistry.formValue();
718
- const pathKeys = safeReadPathKeys(fieldContext);
719
- const arrayScope = detectArrayScope(pathKeys);
720
- if (arrayScope) {
721
- return this.buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, true, fieldContext);
677
+ getKeywordType(value) {
678
+ switch (value) {
679
+ case 'true':
680
+ return TokenType.TRUE;
681
+ case 'false':
682
+ return TokenType.FALSE;
683
+ case 'null':
684
+ return TokenType.NULL;
685
+ case 'undefined':
686
+ return TokenType.UNDEFINED;
687
+ default:
688
+ return TokenType.IDENTIFIER;
722
689
  }
723
- const localKey = this.extractFieldPath(fieldContext);
724
- const rootFormSignal = this.rootFormRegistry.rootForm;
725
- return {
726
- fieldValue,
727
- formValue: rootFormValue,
728
- fieldPath: localKey,
729
- customFunctions: customFunctions || {},
730
- externalData: this.resolveExternalData(true),
731
- logger: this.logger,
732
- deprecationTracker: this.deprecationTracker ?? undefined,
733
- get fieldState() {
734
- return readFieldStateInfo(extractFieldState(fieldContext), true);
735
- },
736
- get formFieldState() {
737
- return createFormFieldStateMap(rootFormSignal(), true);
738
- },
739
- };
740
690
  }
741
- createDisplayOnlyContext(fieldPath, customFunctions) {
742
- const formValue = this.rootFormRegistry.formValue();
743
- const rootFormSignal = this.rootFormRegistry.rootForm;
744
- return {
745
- fieldValue: undefined,
746
- formValue,
747
- fieldPath,
748
- customFunctions: customFunctions || {},
749
- externalData: this.resolveExternalData(true),
750
- logger: this.logger,
751
- deprecationTracker: this.deprecationTracker ?? undefined,
752
- get formFieldState() {
753
- return createFormFieldStateMap(rootFormSignal(), true);
754
- },
755
- };
691
+ readOperatorOrPunctuation() {
692
+ const start = this.position;
693
+ const char = this.expression[this.position];
694
+ const nextChar = this.position + 1 < this.expression.length ? this.expression[this.position + 1] : '';
695
+ const thirdChar = this.position + 2 < this.expression.length ? this.expression[this.position + 2] : '';
696
+ const threeChar = char + nextChar + thirdChar;
697
+ if (threeChar === '===') {
698
+ this.position += 3;
699
+ return { type: TokenType.EQUAL, value: '===', position: start };
700
+ }
701
+ if (threeChar === '!==') {
702
+ this.position += 3;
703
+ return { type: TokenType.NOT_EQUAL, value: '!==', position: start };
704
+ }
705
+ const twoChar = char + nextChar;
706
+ switch (twoChar) {
707
+ case '==':
708
+ this.position += 2;
709
+ return { type: TokenType.EQUAL, value: '==', position: start };
710
+ case '!=':
711
+ this.position += 2;
712
+ return { type: TokenType.NOT_EQUAL, value: '!=', position: start };
713
+ case '>=':
714
+ this.position += 2;
715
+ return { type: TokenType.GREATER_EQUAL, value: '>=', position: start };
716
+ case '<=':
717
+ this.position += 2;
718
+ return { type: TokenType.LESS_EQUAL, value: '<=', position: start };
719
+ case '&&':
720
+ this.position += 2;
721
+ return { type: TokenType.AND, value: '&&', position: start };
722
+ case '||':
723
+ this.position += 2;
724
+ return { type: TokenType.OR, value: '||', position: start };
725
+ case '=>':
726
+ this.position += 2;
727
+ return { type: TokenType.ARROW, value: '=>', position: start };
728
+ case '?.':
729
+ this.position += 2;
730
+ return { type: TokenType.OPTIONAL_DOT, value: '?.', position: start };
731
+ }
732
+ this.position++;
733
+ switch (char) {
734
+ case '+':
735
+ return { type: TokenType.PLUS, value: '+', position: start };
736
+ case '-':
737
+ return { type: TokenType.MINUS, value: '-', position: start };
738
+ case '*':
739
+ return { type: TokenType.MULTIPLY, value: '*', position: start };
740
+ case '/':
741
+ return { type: TokenType.DIVIDE, value: '/', position: start };
742
+ case '%':
743
+ return { type: TokenType.MODULO, value: '%', position: start };
744
+ case '>':
745
+ return { type: TokenType.GREATER, value: '>', position: start };
746
+ case '<':
747
+ return { type: TokenType.LESS, value: '<', position: start };
748
+ case '!':
749
+ return { type: TokenType.NOT, value: '!', position: start };
750
+ case '.':
751
+ return { type: TokenType.DOT, value: '.', position: start };
752
+ case ',':
753
+ return { type: TokenType.COMMA, value: ',', position: start };
754
+ case ':':
755
+ return { type: TokenType.COLON, value: ':', position: start };
756
+ case '(':
757
+ return { type: TokenType.LPAREN, value: '(', position: start };
758
+ case ')':
759
+ return { type: TokenType.RPAREN, value: ')', position: start };
760
+ case '[':
761
+ return { type: TokenType.LBRACKET, value: '[', position: start };
762
+ case ']':
763
+ return { type: TokenType.RBRACKET, value: ']', position: start };
764
+ case '{':
765
+ return { type: TokenType.LBRACE, value: '{', position: start };
766
+ case '}':
767
+ return { type: TokenType.RBRACE, value: '}', position: start };
768
+ case '?':
769
+ return { type: TokenType.QUESTION, value: '?', position: start };
770
+ default:
771
+ throw new ExpressionParserError(`Unexpected character: ${char}`, start, this.expression);
772
+ }
756
773
  }
757
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
758
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: FieldContextRegistryService });
759
774
  }
760
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: FieldContextRegistryService, decorators: [{
761
- type: Injectable
762
- }] });
763
775
 
764
- class FunctionRegistryService {
765
- customFunctions = new Map();
766
- customFunctionScopes = new Map();
767
- derivationFunctions = new Map();
768
- asyncDerivationFunctions = new Map();
769
- asyncConditionFunctions = new Map();
770
- validators = new Map();
771
- asyncValidators = new Map();
772
- httpValidators = new Map();
773
- registerCustomFunction(name, fn, options) {
774
- this.customFunctions.set(name, fn);
775
- this.customFunctionScopes.set(name, options?.scope ?? 'form');
776
- }
777
- getCustomFunctionScope(name) {
778
- return this.customFunctionScopes.get(name);
779
- }
780
- isFieldScopedFunction(name) {
781
- return this.customFunctionScopes.get(name) === 'field';
782
- }
783
- getCustomFunctions() {
784
- return Object.fromEntries(this.customFunctions);
785
- }
786
- clearCustomFunctions() {
787
- this.customFunctions.clear();
788
- this.customFunctionScopes.clear();
789
- }
790
- registerDerivationFunction(name, fn) {
791
- this.derivationFunctions.set(name, fn);
792
- }
793
- getDerivationFunction(name) {
794
- return this.derivationFunctions.get(name);
795
- }
796
- getDerivationFunctions() {
797
- return Object.fromEntries(this.derivationFunctions);
798
- }
799
- setDerivationFunctions(derivations) {
800
- this.setRegistryIfChanged(this.derivationFunctions, derivations);
801
- }
802
- clearDerivationFunctions() {
803
- this.derivationFunctions.clear();
804
- }
805
- registerAsyncDerivationFunction(name, fn) {
806
- this.asyncDerivationFunctions.set(name, fn);
807
- }
808
- getAsyncDerivationFunction(name) {
809
- return this.asyncDerivationFunctions.get(name);
810
- }
811
- getAsyncDerivationFunctions() {
812
- return Object.fromEntries(this.asyncDerivationFunctions);
813
- }
814
- setAsyncDerivationFunctions(fns) {
815
- this.setRegistryIfChanged(this.asyncDerivationFunctions, fns);
816
- }
817
- clearAsyncDerivationFunctions() {
818
- this.asyncDerivationFunctions.clear();
819
- }
820
- registerAsyncConditionFunction(name, fn) {
821
- this.asyncConditionFunctions.set(name, fn);
822
- }
823
- getAsyncConditionFunction(name) {
824
- return this.asyncConditionFunctions.get(name);
825
- }
826
- getAsyncConditionFunctions() {
827
- return Object.fromEntries(this.asyncConditionFunctions);
828
- }
829
- setAsyncConditionFunctions(fns) {
830
- this.setRegistryIfChanged(this.asyncConditionFunctions, fns);
831
- }
832
- clearAsyncConditionFunctions() {
833
- this.asyncConditionFunctions.clear();
834
- }
835
- registerValidator(name, fn) {
836
- this.validators.set(name, fn);
837
- }
838
- getValidator(name) {
839
- return this.validators.get(name);
840
- }
841
- clearValidators() {
842
- this.validators.clear();
843
- }
844
- registerAsyncValidator(name, fn) {
845
- this.asyncValidators.set(name, fn);
846
- }
847
- getAsyncValidator(name) {
848
- return this.asyncValidators.get(name);
849
- }
850
- clearAsyncValidators() {
851
- this.asyncValidators.clear();
852
- }
853
- registerHttpValidator(name, config) {
854
- this.httpValidators.set(name, config);
855
- }
856
- getHttpValidator(name) {
857
- return this.httpValidators.get(name);
858
- }
859
- clearHttpValidators() {
860
- this.httpValidators.clear();
776
+ class Parser {
777
+ tokens = [];
778
+ current = 0;
779
+ expression;
780
+ constructor(expression) {
781
+ this.expression = expression;
861
782
  }
862
- setRegistryIfChanged(registry, values) {
863
- if (!values)
864
- return;
865
- const entries = Object.entries(values);
866
- const hasChanges = entries.some(([name, value]) => registry.get(name) !== value);
867
- if (hasChanges) {
868
- entries.forEach(([name, value]) => registry.set(name, value));
783
+ parse() {
784
+ const tokenizer = new Tokenizer(this.expression);
785
+ this.tokens = tokenizer.tokenize();
786
+ this.current = 0;
787
+ if (this.isAtEnd()) {
788
+ throw new ExpressionParserError('Empty expression', 0, this.expression);
869
789
  }
790
+ const result = this.parseExpression();
791
+ if (!this.isAtEnd()) {
792
+ const token = this.peek();
793
+ throw new ExpressionParserError(`Unexpected token '${token.value}' at position ${token.position} — expression has trailing content`, token.position, this.expression);
794
+ }
795
+ return result;
870
796
  }
871
- setValidators(validators) {
872
- this.setRegistryIfChanged(this.validators, validators);
873
- }
874
- setAsyncValidators(asyncValidators) {
875
- this.setRegistryIfChanged(this.asyncValidators, asyncValidators);
876
- }
877
- setHttpValidators(httpValidators) {
878
- this.setRegistryIfChanged(this.httpValidators, httpValidators);
797
+ parseExpression() {
798
+ const arrow = this.tryParseArrowFunction();
799
+ if (arrow)
800
+ return arrow;
801
+ return this.parseConditional();
879
802
  }
880
- clearAll() {
881
- this.clearCustomFunctions();
882
- this.clearDerivationFunctions();
883
- this.clearAsyncDerivationFunctions();
884
- this.clearAsyncConditionFunctions();
885
- this.clearValidators();
886
- this.clearAsyncValidators();
887
- this.clearHttpValidators();
803
+ parseConditional() {
804
+ const test = this.parseLogicalOr();
805
+ if (!this.match(TokenType.QUESTION))
806
+ return test;
807
+ const consequent = this.parseExpression();
808
+ this.consume(TokenType.COLON, 'Expected ":" between ternary branches');
809
+ const alternate = this.parseExpression();
810
+ return { type: 'Conditional', test, consequent, alternate };
888
811
  }
889
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
890
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: FunctionRegistryService });
891
- }
892
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: FunctionRegistryService, decorators: [{
893
- type: Injectable
894
- }] });
895
-
896
- function isSignalArray(value) {
897
- return Array.isArray(value) && value.every((item) => isSignal(item));
898
- }
899
- function isSignalRecord(value) {
900
- return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));
901
- }
902
- function derivedFromDeferred(source, operator, options) {
903
- const deferred$ = defer(() => {
904
- if (isSignal(source)) {
905
- return toObservable(source, { injector: options.injector });
906
- }
907
- if (isSignalArray(source)) {
908
- const observables = source.map((s) => toObservable(s, { injector: options.injector }));
909
- return combineLatest(observables);
910
- }
911
- if (isSignalRecord(source)) {
912
- const keys = Object.keys(source);
913
- const observables = keys.map((key) => toObservable(source[key], { injector: options.injector }));
914
- return combineLatest(observables, (...values) => {
915
- const result = {};
916
- keys.forEach((key, index) => {
917
- result[key] = values[index];
918
- });
919
- return result;
920
- });
812
+ tryParseArrowFunction() {
813
+ const savedPos = this.current;
814
+ if (this.check(TokenType.IDENTIFIER) && this.checkAt(this.current + 1, TokenType.ARROW)) {
815
+ const param = this.advance().value;
816
+ this.advance(); const body = this.parseExpression();
817
+ return { type: 'ArrowFunction', params: [param], body };
921
818
  }
922
- throw new Error('Invalid source type. Expected a Signal, array of Signals, or object of Signals.');
923
- }).pipe(operator);
924
- return toSignal(deferred$, { initialValue: options.initialValue, injector: options.injector });
925
- }
926
-
927
- function withPreviousValue(input) {
928
- const derived = linkedSignal({ ...(ngDevMode ? { debugName: "derived" } : /* istanbul ignore next */ {}), source: input.snapshot,
929
- computation: (snapshot, previous) => {
930
- if ((snapshot.status === 'loading' || snapshot.status === 'reloading') &&
931
- previous !== undefined &&
932
- previous.value.status !== 'error') {
933
- return { status: snapshot.status, value: previous.value.value };
819
+ if (this.check(TokenType.LPAREN)) {
820
+ this.advance(); const params = [];
821
+ if (this.check(TokenType.RPAREN)) {
822
+ this.advance(); }
823
+ else if (this.check(TokenType.IDENTIFIER)) {
824
+ params.push(this.advance().value);
825
+ while (this.check(TokenType.COMMA)) {
826
+ this.advance(); if (!this.check(TokenType.IDENTIFIER)) {
827
+ this.current = savedPos;
828
+ return null;
829
+ }
830
+ params.push(this.advance().value);
831
+ }
832
+ if (!this.check(TokenType.RPAREN)) {
833
+ this.current = savedPos;
834
+ return null;
835
+ }
836
+ this.advance(); }
837
+ else {
838
+ this.current = savedPos;
839
+ return null;
934
840
  }
935
- return snapshot;
936
- } });
937
- return resourceFromSnapshots(derived);
938
- }
939
-
940
- function createDebouncedResourceLogicFn(config) {
941
- const perFunctionSignalStore = new Map();
942
- return (ctx) => {
943
- const contextKey = safeReadPathKeys(ctx).join('.');
944
- let handle = perFunctionSignalStore.get(contextKey);
945
- if (!handle) {
946
- const trigger = signal(undefined, /* @ts-ignore */
947
- ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
948
- const resultResource = untracked(() => {
949
- const debouncedTrigger = derivedFromDeferred(trigger, pipe(debounceTime(config.debounceMs),
950
- distinctUntilChanged((a, b) => a?.key === b?.key)), { initialValue: undefined, injector: config.injector });
951
- const resource = rxResource({
952
- params: () => debouncedTrigger(),
953
- stream: ({ params }) => config.buildStream(params.payload),
954
- defaultValue: config.pendingValue,
955
- injector: config.injector,
956
- });
957
- return withPreviousValue(resource);
958
- });
959
- handle = { trigger, resultResource, refetchVersion: 0 };
960
- perFunctionSignalStore.set(contextKey, handle);
841
+ if (!this.check(TokenType.ARROW)) {
842
+ this.current = savedPos;
843
+ return null;
844
+ }
845
+ this.advance(); const body = this.parseExpression();
846
+ return { type: 'ArrowFunction', params, body };
961
847
  }
962
- const resolved = config.resolve(ctx);
963
- if (resolved.kind === 'returnEarly') {
964
- handle.refetchVersion++;
965
- return resolved.value;
848
+ return null;
849
+ }
850
+ checkAt(index, type) {
851
+ if (index < 0 || index >= this.tokens.length)
852
+ return false;
853
+ return this.tokens[index].type === type;
854
+ }
855
+ parseLogicalOr() {
856
+ let node = this.parseLogicalAnd();
857
+ while (this.match(TokenType.OR)) {
858
+ const operator = this.previous().value;
859
+ const right = this.parseLogicalAnd();
860
+ node = { type: 'BinaryOp', operator, left: node, right };
861
+ }
862
+ return node;
863
+ }
864
+ parseLogicalAnd() {
865
+ let node = this.parseEquality();
866
+ while (this.match(TokenType.AND)) {
867
+ const operator = this.previous().value;
868
+ const right = this.parseEquality();
869
+ node = { type: 'BinaryOp', operator, left: node, right };
966
870
  }
967
- if (resolved.kind === 'holdPrevious') {
968
- return handle.resultResource.value();
871
+ return node;
872
+ }
873
+ parseEquality() {
874
+ let node = this.parseComparison();
875
+ while (this.match(TokenType.EQUAL, TokenType.NOT_EQUAL)) {
876
+ const operator = this.previous().value;
877
+ const right = this.parseComparison();
878
+ node = { type: 'BinaryOp', operator, left: node, right };
969
879
  }
970
- const dedupKey = `${resolved.key}::${handle.refetchVersion}`;
971
- const newTrigger = { key: dedupKey, payload: resolved.payload };
972
- untracked(() => {
973
- const current = handle.trigger();
974
- if (current?.key !== newTrigger.key) {
975
- handle.trigger.set(newTrigger);
976
- }
977
- });
978
- return handle.resultResource.value();
979
- };
980
- }
981
-
982
- function createAsyncConditionLogicFunction(condition) {
983
- const fieldContextRegistry = inject(FieldContextRegistryService);
984
- const functionRegistry = inject(FunctionRegistryService);
985
- const injector = inject(Injector);
986
- const logger = inject(DynamicFormLogger);
987
- const cacheService = inject(AsyncConditionFunctionCacheService);
988
- const pendingValue = condition.pendingValue ?? false;
989
- const debounceMs = condition.debounceMs ?? 300;
990
- if (condition.asyncFn && condition.asyncFunctionName) {
991
- logger.warn('Both "asyncFn" and "asyncFunctionName" are set on async condition. Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.');
880
+ return node;
992
881
  }
993
- const cacheKey = condition.asyncFn ? undefined : stableStringify(condition);
994
- if (cacheKey !== undefined) {
995
- const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);
996
- if (cached) {
997
- return cached;
882
+ parseComparison() {
883
+ let node = this.parseAddition();
884
+ while (this.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {
885
+ const operator = this.previous().value;
886
+ const right = this.parseAddition();
887
+ node = { type: 'BinaryOp', operator, left: node, right };
998
888
  }
889
+ return node;
999
890
  }
1000
- const fn = createDebouncedResourceLogicFn({
1001
- pendingValue,
1002
- debounceMs,
1003
- injector,
1004
- resolve: (ctx) => {
1005
- const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
1006
- const key = stableStringify({
1007
- formValue: evaluationContext.formValue,
1008
- fieldValue: evaluationContext.fieldValue,
1009
- externalData: evaluationContext.externalData,
1010
- });
1011
- return { kind: 'trigger', key, payload: ctx };
1012
- },
1013
- buildStream: (capturedCtx) => {
1014
- const asyncFn = condition.asyncFn ??
1015
- (condition.asyncFunctionName ? functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName) : undefined);
1016
- if (!asyncFn) {
1017
- logger.warn(`Async Condition - function '${condition.asyncFunctionName}' not found. ` + `Register it in customFnConfig.asyncConditions.`);
1018
- return of(pendingValue);
1019
- }
1020
- const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(capturedCtx, functionRegistry.getCustomFunctions());
1021
- return from(asyncFn(evaluationContext)).pipe(map((result) => !!result), catchError((error) => {
1022
- logger.warn('Async Condition - function failed:', error);
1023
- return of(pendingValue);
1024
- }));
1025
- },
1026
- });
1027
- if (cacheKey !== undefined) {
1028
- cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
891
+ parseAddition() {
892
+ let node = this.parseMultiplication();
893
+ while (this.match(TokenType.PLUS, TokenType.MINUS)) {
894
+ const operator = this.previous().value;
895
+ const right = this.parseMultiplication();
896
+ node = { type: 'BinaryOp', operator, left: node, right };
897
+ }
898
+ return node;
1029
899
  }
1030
- return fn;
1031
- }
1032
-
1033
- var TokenType;
1034
- (function (TokenType) {
1035
- TokenType["NUMBER"] = "NUMBER";
1036
- TokenType["STRING"] = "STRING";
1037
- TokenType["TRUE"] = "TRUE";
1038
- TokenType["FALSE"] = "FALSE";
1039
- TokenType["NULL"] = "NULL";
1040
- TokenType["UNDEFINED"] = "UNDEFINED";
1041
- TokenType["IDENTIFIER"] = "IDENTIFIER";
1042
- TokenType["PLUS"] = "PLUS";
1043
- TokenType["MINUS"] = "MINUS";
1044
- TokenType["MULTIPLY"] = "MULTIPLY";
1045
- TokenType["DIVIDE"] = "DIVIDE";
1046
- TokenType["MODULO"] = "MODULO";
1047
- TokenType["EQUAL"] = "EQUAL";
1048
- TokenType["NOT_EQUAL"] = "NOT_EQUAL";
1049
- TokenType["GREATER"] = "GREATER";
1050
- TokenType["LESS"] = "LESS";
1051
- TokenType["GREATER_EQUAL"] = "GREATER_EQUAL";
1052
- TokenType["LESS_EQUAL"] = "LESS_EQUAL";
1053
- TokenType["AND"] = "AND";
1054
- TokenType["OR"] = "OR";
1055
- TokenType["NOT"] = "NOT";
1056
- TokenType["DOT"] = "DOT";
1057
- TokenType["COMMA"] = "COMMA";
1058
- TokenType["COLON"] = "COLON";
1059
- TokenType["LPAREN"] = "LPAREN";
1060
- TokenType["RPAREN"] = "RPAREN";
1061
- TokenType["LBRACKET"] = "LBRACKET";
1062
- TokenType["RBRACKET"] = "RBRACKET";
1063
- TokenType["LBRACE"] = "LBRACE";
1064
- TokenType["RBRACE"] = "RBRACE";
1065
- TokenType["ARROW"] = "ARROW";
1066
- TokenType["QUESTION"] = "QUESTION";
1067
- TokenType["OPTIONAL_DOT"] = "OPTIONAL_DOT";
1068
- TokenType["EOF"] = "EOF";
1069
- })(TokenType || (TokenType = {}));
1070
- class ExpressionParserError extends DynamicFormError {
1071
- position;
1072
- expression;
1073
- constructor(message, position, expression) {
1074
- super(`${message} at position ${position} in expression: ${expression}`);
1075
- this.position = position;
1076
- this.expression = expression;
1077
- this.name = 'ExpressionParserError';
900
+ parseMultiplication() {
901
+ let node = this.parseUnary();
902
+ while (this.match(TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO)) {
903
+ const operator = this.previous().value;
904
+ const right = this.parseUnary();
905
+ node = { type: 'BinaryOp', operator, left: node, right };
906
+ }
907
+ return node;
1078
908
  }
1079
- }
1080
-
1081
- class Tokenizer {
1082
- position = 0;
1083
- expression;
1084
- constructor(expression) {
1085
- this.expression = expression;
909
+ parseUnary() {
910
+ if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.PLUS)) {
911
+ const operator = this.previous().value;
912
+ const operand = this.parseUnary();
913
+ return { type: 'UnaryOp', operator, operand };
914
+ }
915
+ return this.parsePostfix();
1086
916
  }
1087
- tokenize() {
1088
- const tokens = [];
1089
- while (this.position < this.expression.length) {
1090
- this.skipWhitespace();
1091
- if (this.position >= this.expression.length) {
917
+ parsePostfix() {
918
+ let node = this.parsePrimary();
919
+ while (true) {
920
+ if (this.match(TokenType.DOT)) {
921
+ if (!this.check(TokenType.IDENTIFIER)) {
922
+ throw new ExpressionParserError('Expected property name after "."', this.peek().position, this.expression);
923
+ }
924
+ const property = this.advance().value;
925
+ node = { type: 'MemberAccess', object: node, property };
926
+ }
927
+ else if (this.match(TokenType.OPTIONAL_DOT)) {
928
+ if (this.match(TokenType.LBRACKET)) {
929
+ const property = this.parseExpression();
930
+ this.consume(TokenType.RBRACKET, 'Expected "]" after computed property');
931
+ node = { type: 'ComputedMemberAccess', object: node, property, optional: true };
932
+ }
933
+ else if (this.match(TokenType.LPAREN)) {
934
+ const args = this.parseArgumentList();
935
+ this.consume(TokenType.RPAREN, 'Expected ")" after arguments');
936
+ node = { type: 'CallExpression', callee: node, arguments: args, optional: true };
937
+ }
938
+ else if (this.check(TokenType.IDENTIFIER)) {
939
+ const property = this.advance().value;
940
+ node = { type: 'MemberAccess', object: node, property, optional: true };
941
+ }
942
+ else {
943
+ throw new ExpressionParserError('Expected property name, "[", or "(" after "?."', this.peek().position, this.expression);
944
+ }
945
+ }
946
+ else if (this.match(TokenType.LBRACKET)) {
947
+ const property = this.parseExpression();
948
+ this.consume(TokenType.RBRACKET, 'Expected "]" after computed property');
949
+ node = { type: 'ComputedMemberAccess', object: node, property };
950
+ }
951
+ else if (this.match(TokenType.LPAREN)) {
952
+ const args = this.parseArgumentList();
953
+ this.consume(TokenType.RPAREN, 'Expected ")" after arguments');
954
+ node = { type: 'CallExpression', callee: node, arguments: args };
955
+ }
956
+ else {
1092
957
  break;
1093
958
  }
1094
- const token = this.nextToken();
1095
- tokens.push(token);
1096
959
  }
1097
- tokens.push({ type: TokenType.EOF, value: '', position: this.position });
1098
- return tokens;
960
+ return node;
1099
961
  }
1100
- skipWhitespace() {
1101
- while (this.position < this.expression.length && /\s/.test(this.expression[this.position])) {
1102
- this.position++;
962
+ parsePrimary() {
963
+ if (this.match(TokenType.TRUE)) {
964
+ return { type: 'Literal', value: true };
1103
965
  }
1104
- }
1105
- nextToken() {
1106
- const char = this.expression[this.position];
1107
- if (char === '"' || char === "'") {
1108
- return this.readString(char);
966
+ if (this.match(TokenType.FALSE)) {
967
+ return { type: 'Literal', value: false };
1109
968
  }
1110
- if (/[0-9]/.test(char)) {
1111
- return this.readNumber();
969
+ if (this.match(TokenType.NULL)) {
970
+ return { type: 'Literal', value: null };
971
+ }
972
+ if (this.match(TokenType.UNDEFINED)) {
973
+ return { type: 'Literal', value: undefined };
974
+ }
975
+ if (this.match(TokenType.NUMBER)) {
976
+ return { type: 'Literal', value: parseFloat(this.previous().value) };
977
+ }
978
+ if (this.match(TokenType.STRING)) {
979
+ return { type: 'Literal', value: this.previous().value };
980
+ }
981
+ if (this.match(TokenType.LBRACKET)) {
982
+ const elements = this.parseArrayElements();
983
+ this.consume(TokenType.RBRACKET, 'Expected "]" after array elements');
984
+ return { type: 'ArrayLiteral', elements };
985
+ }
986
+ if (this.match(TokenType.LBRACE)) {
987
+ const properties = this.parseObjectProperties();
988
+ this.consume(TokenType.RBRACE, 'Expected "}" after object properties');
989
+ return { type: 'ObjectLiteral', properties };
990
+ }
991
+ if (this.match(TokenType.IDENTIFIER)) {
992
+ return { type: 'Identifier', name: this.previous().value };
993
+ }
994
+ if (this.match(TokenType.LPAREN)) {
995
+ const expr = this.parseExpression();
996
+ this.consume(TokenType.RPAREN, 'Expected ")" after expression');
997
+ return expr;
998
+ }
999
+ throw new ExpressionParserError(`Unexpected token: ${this.peek().value}`, this.peek().position, this.expression);
1000
+ }
1001
+ parseArgumentList() {
1002
+ const args = [];
1003
+ if (!this.check(TokenType.RPAREN)) {
1004
+ do {
1005
+ args.push(this.parseExpression());
1006
+ } while (this.match(TokenType.COMMA));
1112
1007
  }
1113
- if (/[a-zA-Z_$]/.test(char)) {
1114
- return this.readIdentifierOrKeyword();
1008
+ return args;
1009
+ }
1010
+ parseArrayElements() {
1011
+ const elements = [];
1012
+ if (!this.check(TokenType.RBRACKET)) {
1013
+ do {
1014
+ elements.push(this.parseExpression());
1015
+ } while (this.match(TokenType.COMMA));
1115
1016
  }
1116
- return this.readOperatorOrPunctuation();
1017
+ return elements;
1117
1018
  }
1118
- readString(quote) {
1119
- const start = this.position;
1120
- this.position++; let value = '';
1121
- while (this.position < this.expression.length) {
1122
- const char = this.expression[this.position];
1123
- if (char === quote) {
1124
- this.position++; return { type: TokenType.STRING, value, position: start };
1125
- }
1126
- if (char === '\\' && this.position + 1 < this.expression.length) {
1127
- this.position++;
1128
- const nextChar = this.expression[this.position];
1129
- switch (nextChar) {
1130
- case 'n':
1131
- value += '\n';
1132
- break;
1133
- case 't':
1134
- value += '\t';
1135
- break;
1136
- case 'r':
1137
- value += '\r';
1138
- break;
1139
- case '\\':
1140
- value += '\\';
1141
- break;
1142
- case quote:
1143
- value += quote;
1144
- break;
1145
- default:
1146
- value += nextChar;
1147
- }
1148
- this.position++;
1019
+ parseObjectProperties() {
1020
+ const properties = [];
1021
+ if (this.check(TokenType.RBRACE)) {
1022
+ return properties;
1023
+ }
1024
+ do {
1025
+ if (this.check(TokenType.RBRACE))
1026
+ break;
1027
+ let key;
1028
+ if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.STRING)) {
1029
+ key = this.advance().value;
1149
1030
  }
1150
1031
  else {
1151
- value += char;
1152
- this.position++;
1032
+ throw new ExpressionParserError(`Expected property name (identifier or string) in object literal, got '${this.peek().value}'`, this.peek().position, this.expression);
1153
1033
  }
1154
- }
1155
- throw new ExpressionParserError('Unterminated string literal', start, this.expression);
1034
+ this.consume(TokenType.COLON, 'Expected ":" after property name in object literal');
1035
+ const value = this.parseExpression();
1036
+ properties.push({ key, value });
1037
+ } while (this.match(TokenType.COMMA));
1038
+ return properties;
1156
1039
  }
1157
- readNumber() {
1158
- const start = this.position;
1159
- let value = '';
1160
- while (this.position < this.expression.length && /[0-9.]/.test(this.expression[this.position])) {
1161
- value += this.expression[this.position];
1162
- this.position++;
1163
- }
1164
- if (!/^\d+(\.\d+)?$/.test(value)) {
1165
- throw new ExpressionParserError(`Invalid number: ${value}`, start, this.expression);
1040
+ match(...types) {
1041
+ for (const type of types) {
1042
+ if (this.check(type)) {
1043
+ this.advance();
1044
+ return true;
1045
+ }
1166
1046
  }
1167
- return { type: TokenType.NUMBER, value, position: start };
1047
+ return false;
1168
1048
  }
1169
- readIdentifierOrKeyword() {
1170
- const start = this.position;
1171
- let value = '';
1172
- while (this.position < this.expression.length && /[a-zA-Z0-9_$]/.test(this.expression[this.position])) {
1173
- value += this.expression[this.position];
1174
- this.position++;
1175
- }
1176
- const type = this.getKeywordType(value);
1177
- return { type, value, position: start };
1049
+ check(type) {
1050
+ if (this.isAtEnd())
1051
+ return false;
1052
+ return this.peek().type === type;
1178
1053
  }
1179
- getKeywordType(value) {
1180
- switch (value) {
1181
- case 'true':
1182
- return TokenType.TRUE;
1183
- case 'false':
1184
- return TokenType.FALSE;
1185
- case 'null':
1186
- return TokenType.NULL;
1187
- case 'undefined':
1188
- return TokenType.UNDEFINED;
1189
- default:
1190
- return TokenType.IDENTIFIER;
1191
- }
1054
+ advance() {
1055
+ if (!this.isAtEnd())
1056
+ this.current++;
1057
+ return this.previous();
1192
1058
  }
1193
- readOperatorOrPunctuation() {
1194
- const start = this.position;
1195
- const char = this.expression[this.position];
1196
- const nextChar = this.position + 1 < this.expression.length ? this.expression[this.position + 1] : '';
1197
- const thirdChar = this.position + 2 < this.expression.length ? this.expression[this.position + 2] : '';
1198
- const threeChar = char + nextChar + thirdChar;
1199
- if (threeChar === '===') {
1200
- this.position += 3;
1201
- return { type: TokenType.EQUAL, value: '===', position: start };
1202
- }
1203
- if (threeChar === '!==') {
1204
- this.position += 3;
1205
- return { type: TokenType.NOT_EQUAL, value: '!==', position: start };
1206
- }
1207
- const twoChar = char + nextChar;
1208
- switch (twoChar) {
1209
- case '==':
1210
- this.position += 2;
1211
- return { type: TokenType.EQUAL, value: '==', position: start };
1212
- case '!=':
1213
- this.position += 2;
1214
- return { type: TokenType.NOT_EQUAL, value: '!=', position: start };
1215
- case '>=':
1216
- this.position += 2;
1217
- return { type: TokenType.GREATER_EQUAL, value: '>=', position: start };
1218
- case '<=':
1219
- this.position += 2;
1220
- return { type: TokenType.LESS_EQUAL, value: '<=', position: start };
1221
- case '&&':
1222
- this.position += 2;
1223
- return { type: TokenType.AND, value: '&&', position: start };
1224
- case '||':
1225
- this.position += 2;
1226
- return { type: TokenType.OR, value: '||', position: start };
1227
- case '=>':
1228
- this.position += 2;
1229
- return { type: TokenType.ARROW, value: '=>', position: start };
1230
- case '?.':
1231
- this.position += 2;
1232
- return { type: TokenType.OPTIONAL_DOT, value: '?.', position: start };
1233
- }
1234
- this.position++;
1235
- switch (char) {
1236
- case '+':
1237
- return { type: TokenType.PLUS, value: '+', position: start };
1238
- case '-':
1239
- return { type: TokenType.MINUS, value: '-', position: start };
1240
- case '*':
1241
- return { type: TokenType.MULTIPLY, value: '*', position: start };
1242
- case '/':
1243
- return { type: TokenType.DIVIDE, value: '/', position: start };
1244
- case '%':
1245
- return { type: TokenType.MODULO, value: '%', position: start };
1246
- case '>':
1247
- return { type: TokenType.GREATER, value: '>', position: start };
1248
- case '<':
1249
- return { type: TokenType.LESS, value: '<', position: start };
1250
- case '!':
1251
- return { type: TokenType.NOT, value: '!', position: start };
1252
- case '.':
1253
- return { type: TokenType.DOT, value: '.', position: start };
1254
- case ',':
1255
- return { type: TokenType.COMMA, value: ',', position: start };
1256
- case ':':
1257
- return { type: TokenType.COLON, value: ':', position: start };
1258
- case '(':
1259
- return { type: TokenType.LPAREN, value: '(', position: start };
1260
- case ')':
1261
- return { type: TokenType.RPAREN, value: ')', position: start };
1262
- case '[':
1263
- return { type: TokenType.LBRACKET, value: '[', position: start };
1264
- case ']':
1265
- return { type: TokenType.RBRACKET, value: ']', position: start };
1266
- case '{':
1267
- return { type: TokenType.LBRACE, value: '{', position: start };
1268
- case '}':
1269
- return { type: TokenType.RBRACE, value: '}', position: start };
1270
- case '?':
1271
- return { type: TokenType.QUESTION, value: '?', position: start };
1272
- default:
1273
- throw new ExpressionParserError(`Unexpected character: ${char}`, start, this.expression);
1274
- }
1059
+ isAtEnd() {
1060
+ return this.peek().type === TokenType.EOF;
1061
+ }
1062
+ peek() {
1063
+ return this.tokens[this.current];
1064
+ }
1065
+ previous() {
1066
+ return this.tokens[this.current - 1];
1067
+ }
1068
+ consume(type, message) {
1069
+ if (this.check(type))
1070
+ return this.advance();
1071
+ throw new ExpressionParserError(message, this.peek().position, this.expression);
1275
1072
  }
1276
1073
  }
1277
1074
 
1278
- class Parser {
1279
- tokens = [];
1280
- current = 0;
1075
+ const STRING_SAFE_METHODS = [
1076
+ 'charAt',
1077
+ 'charCodeAt',
1078
+ 'concat',
1079
+ 'endsWith',
1080
+ 'includes',
1081
+ 'indexOf',
1082
+ 'lastIndexOf',
1083
+ 'match',
1084
+ 'padEnd',
1085
+ 'padStart',
1086
+ 'repeat',
1087
+ 'replace',
1088
+ 'search',
1089
+ 'slice',
1090
+ 'split',
1091
+ 'startsWith',
1092
+ 'substring',
1093
+ 'toLowerCase',
1094
+ 'toUpperCase',
1095
+ 'trim',
1096
+ 'trimEnd',
1097
+ 'trimStart',
1098
+ 'toString',
1099
+ ];
1100
+ const NUMBER_SAFE_METHODS = ['toExponential', 'toFixed', 'toPrecision', 'toString'];
1101
+ const ARRAY_SAFE_METHODS = [
1102
+ 'concat',
1103
+ 'every',
1104
+ 'filter',
1105
+ 'find',
1106
+ 'findIndex',
1107
+ 'flat',
1108
+ 'flatMap',
1109
+ 'includes',
1110
+ 'indexOf',
1111
+ 'join',
1112
+ 'lastIndexOf',
1113
+ 'map',
1114
+ 'reduce',
1115
+ 'reduceRight',
1116
+ 'slice',
1117
+ 'some',
1118
+ 'toString',
1119
+ 'entries',
1120
+ 'keys',
1121
+ 'values',
1122
+ ];
1123
+ const DATE_SAFE_METHODS = [
1124
+ 'getDate',
1125
+ 'getDay',
1126
+ 'getFullYear',
1127
+ 'getHours',
1128
+ 'getMilliseconds',
1129
+ 'getMinutes',
1130
+ 'getMonth',
1131
+ 'getSeconds',
1132
+ 'getTime',
1133
+ 'getTimezoneOffset',
1134
+ 'getUTCDate',
1135
+ 'getUTCDay',
1136
+ 'getUTCFullYear',
1137
+ 'getUTCHours',
1138
+ 'getUTCMilliseconds',
1139
+ 'getUTCMinutes',
1140
+ 'getUTCMonth',
1141
+ 'getUTCSeconds',
1142
+ 'toDateString',
1143
+ 'toISOString',
1144
+ 'toJSON',
1145
+ 'toString',
1146
+ 'toTimeString',
1147
+ 'toUTCString',
1148
+ ];
1149
+ const SAFE_METHODS = {
1150
+ string: new Set(STRING_SAFE_METHODS),
1151
+ number: new Set(NUMBER_SAFE_METHODS),
1152
+ array: new Set(ARRAY_SAFE_METHODS),
1153
+ date: new Set(DATE_SAFE_METHODS),
1154
+ };
1155
+ const BLOCKED_PROPERTIES = new Set([
1156
+ 'constructor',
1157
+ '__proto__',
1158
+ 'prototype',
1159
+ '__defineGetter__',
1160
+ '__defineSetter__',
1161
+ '__lookupGetter__',
1162
+ '__lookupSetter__',
1163
+ ]);
1164
+ class Evaluator {
1165
+ scope;
1281
1166
  expression;
1282
- constructor(expression) {
1167
+ constructor(scope, expression) {
1168
+ this.scope = scope;
1283
1169
  this.expression = expression;
1284
1170
  }
1285
- parse() {
1286
- const tokenizer = new Tokenizer(this.expression);
1287
- this.tokens = tokenizer.tokenize();
1288
- this.current = 0;
1289
- if (this.isAtEnd()) {
1290
- throw new ExpressionParserError('Empty expression', 0, this.expression);
1291
- }
1292
- const result = this.parseExpression();
1293
- if (!this.isAtEnd()) {
1294
- const token = this.peek();
1295
- throw new ExpressionParserError(`Unexpected token '${token.value}' at position ${token.position} — expression has trailing content`, token.position, this.expression);
1171
+ evaluate(node) {
1172
+ switch (node.type) {
1173
+ case 'Literal':
1174
+ return node.value;
1175
+ case 'Identifier':
1176
+ return this.evaluateIdentifier(node.name);
1177
+ case 'MemberAccess':
1178
+ return this.evaluateMemberAccess(node);
1179
+ case 'ComputedMemberAccess':
1180
+ return this.evaluateComputedMemberAccess(node);
1181
+ case 'BinaryOp':
1182
+ return this.evaluateBinaryOp(node);
1183
+ case 'UnaryOp':
1184
+ return this.evaluateUnaryOp(node);
1185
+ case 'CallExpression':
1186
+ return this.evaluateCallExpression(node);
1187
+ case 'Conditional':
1188
+ return this.evaluate(node.test) ? this.evaluate(node.consequent) : this.evaluate(node.alternate);
1189
+ case 'ArrayLiteral':
1190
+ return this.evaluateArrayLiteral(node);
1191
+ case 'ObjectLiteral':
1192
+ return this.evaluateObjectLiteral(node);
1193
+ case 'ArrowFunction':
1194
+ return this.evaluateArrowFunction(node);
1195
+ default:
1196
+ throw new ExpressionParserError(`Unknown node type: ${node.type}`, 0, this.expression);
1296
1197
  }
1297
- return result;
1298
- }
1299
- parseExpression() {
1300
- const arrow = this.tryParseArrowFunction();
1301
- if (arrow)
1302
- return arrow;
1303
- return this.parseConditional();
1304
1198
  }
1305
- parseConditional() {
1306
- const test = this.parseLogicalOr();
1307
- if (!this.match(TokenType.QUESTION))
1308
- return test;
1309
- const consequent = this.parseExpression();
1310
- this.consume(TokenType.COLON, 'Expected ":" between ternary branches');
1311
- const alternate = this.parseExpression();
1312
- return { type: 'Conditional', test, consequent, alternate };
1199
+ evaluateIdentifier(name) {
1200
+ if (name in this.scope) {
1201
+ return this.scope[name];
1202
+ }
1203
+ return undefined;
1313
1204
  }
1314
- tryParseArrowFunction() {
1315
- const savedPos = this.current;
1316
- if (this.check(TokenType.IDENTIFIER) && this.checkAt(this.current + 1, TokenType.ARROW)) {
1317
- const param = this.advance().value;
1318
- this.advance(); const body = this.parseExpression();
1319
- return { type: 'ArrowFunction', params: [param], body };
1205
+ evaluateMemberAccess(node) {
1206
+ const obj = this.evaluate(node.object);
1207
+ if (obj === null || obj === undefined) {
1208
+ return undefined;
1320
1209
  }
1321
- if (this.check(TokenType.LPAREN)) {
1322
- this.advance(); const params = [];
1323
- if (this.check(TokenType.RPAREN)) {
1324
- this.advance(); }
1325
- else if (this.check(TokenType.IDENTIFIER)) {
1326
- params.push(this.advance().value);
1327
- while (this.check(TokenType.COMMA)) {
1328
- this.advance(); if (!this.check(TokenType.IDENTIFIER)) {
1329
- this.current = savedPos;
1330
- return null;
1331
- }
1332
- params.push(this.advance().value);
1333
- }
1334
- if (!this.check(TokenType.RPAREN)) {
1335
- this.current = savedPos;
1336
- return null;
1337
- }
1338
- this.advance(); }
1339
- else {
1340
- this.current = savedPos;
1341
- return null;
1342
- }
1343
- if (!this.check(TokenType.ARROW)) {
1344
- this.current = savedPos;
1345
- return null;
1346
- }
1347
- this.advance(); const body = this.parseExpression();
1348
- return { type: 'ArrowFunction', params, body };
1210
+ if (BLOCKED_PROPERTIES.has(node.property)) {
1211
+ throw new ExpressionParserError(`Property "${node.property}" is not accessible for security reasons`, 0, this.expression);
1349
1212
  }
1350
- return null;
1351
- }
1352
- checkAt(index, type) {
1353
- if (index < 0 || index >= this.tokens.length)
1354
- return false;
1355
- return this.tokens[index].type === type;
1356
- }
1357
- parseLogicalOr() {
1358
- let node = this.parseLogicalAnd();
1359
- while (this.match(TokenType.OR)) {
1360
- const operator = this.previous().value;
1361
- const right = this.parseLogicalAnd();
1362
- node = { type: 'BinaryOp', operator, left: node, right };
1213
+ if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {
1214
+ return obj[node.property];
1363
1215
  }
1364
- return node;
1216
+ return undefined;
1365
1217
  }
1366
- parseLogicalAnd() {
1367
- let node = this.parseEquality();
1368
- while (this.match(TokenType.AND)) {
1369
- const operator = this.previous().value;
1370
- const right = this.parseEquality();
1371
- node = { type: 'BinaryOp', operator, left: node, right };
1218
+ evaluateComputedMemberAccess(node) {
1219
+ const obj = this.evaluate(node.object);
1220
+ if (obj === null || obj === undefined) {
1221
+ return undefined;
1372
1222
  }
1373
- return node;
1374
- }
1375
- parseEquality() {
1376
- let node = this.parseComparison();
1377
- while (this.match(TokenType.EQUAL, TokenType.NOT_EQUAL)) {
1378
- const operator = this.previous().value;
1379
- const right = this.parseComparison();
1380
- node = { type: 'BinaryOp', operator, left: node, right };
1223
+ const rawKey = this.evaluate(node.property);
1224
+ if (rawKey === null || rawKey === undefined)
1225
+ return undefined;
1226
+ const key = String(rawKey);
1227
+ if (BLOCKED_PROPERTIES.has(key)) {
1228
+ throw new ExpressionParserError(`Property "${key}" is not accessible for security reasons`, 0, this.expression);
1381
1229
  }
1382
- return node;
1383
- }
1384
- parseComparison() {
1385
- let node = this.parseAddition();
1386
- while (this.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {
1387
- const operator = this.previous().value;
1388
- const right = this.parseAddition();
1389
- node = { type: 'BinaryOp', operator, left: node, right };
1230
+ if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {
1231
+ return obj[key];
1390
1232
  }
1391
- return node;
1233
+ return undefined;
1392
1234
  }
1393
- parseAddition() {
1394
- let node = this.parseMultiplication();
1395
- while (this.match(TokenType.PLUS, TokenType.MINUS)) {
1396
- const operator = this.previous().value;
1397
- const right = this.parseMultiplication();
1398
- node = { type: 'BinaryOp', operator, left: node, right };
1235
+ evaluateBinaryOp(node) {
1236
+ if (node.operator === '&&') {
1237
+ const left = this.evaluate(node.left);
1238
+ return left ? this.evaluate(node.right) : left;
1399
1239
  }
1400
- return node;
1401
- }
1402
- parseMultiplication() {
1403
- let node = this.parseUnary();
1404
- while (this.match(TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO)) {
1405
- const operator = this.previous().value;
1406
- const right = this.parseUnary();
1407
- node = { type: 'BinaryOp', operator, left: node, right };
1240
+ if (node.operator === '||') {
1241
+ const left = this.evaluate(node.left);
1242
+ return left ? left : this.evaluate(node.right);
1243
+ }
1244
+ const left = this.evaluate(node.left);
1245
+ const right = this.evaluate(node.right);
1246
+ switch (node.operator) {
1247
+ case '+':
1248
+ return left + right;
1249
+ case '-':
1250
+ return left - right;
1251
+ case '*':
1252
+ return left * right;
1253
+ case '/':
1254
+ if (right === 0)
1255
+ return null;
1256
+ return left / right;
1257
+ case '%':
1258
+ if (right === 0)
1259
+ return null;
1260
+ return left % right;
1261
+ case '==':
1262
+ return left == right;
1263
+ case '===':
1264
+ return left === right;
1265
+ case '!=':
1266
+ return left != right;
1267
+ case '!==':
1268
+ return left !== right;
1269
+ case '>':
1270
+ return left > right;
1271
+ case '<':
1272
+ return left < right;
1273
+ case '>=':
1274
+ return left >= right;
1275
+ case '<=':
1276
+ return left <= right;
1277
+ default:
1278
+ throw new ExpressionParserError(`Unknown binary operator: ${node.operator}`, 0, this.expression);
1408
1279
  }
1409
- return node;
1410
1280
  }
1411
- parseUnary() {
1412
- if (this.match(TokenType.NOT, TokenType.MINUS, TokenType.PLUS)) {
1413
- const operator = this.previous().value;
1414
- const operand = this.parseUnary();
1415
- return { type: 'UnaryOp', operator, operand };
1281
+ evaluateUnaryOp(node) {
1282
+ const operand = this.evaluate(node.operand);
1283
+ switch (node.operator) {
1284
+ case '!':
1285
+ return !operand;
1286
+ case '-':
1287
+ return -operand;
1288
+ case '+':
1289
+ return +operand;
1290
+ default:
1291
+ throw new ExpressionParserError(`Unknown unary operator: ${node.operator}`, 0, this.expression);
1416
1292
  }
1417
- return this.parsePostfix();
1418
1293
  }
1419
- parsePostfix() {
1420
- let node = this.parsePrimary();
1421
- while (true) {
1422
- if (this.match(TokenType.DOT)) {
1423
- if (!this.check(TokenType.IDENTIFIER)) {
1424
- throw new ExpressionParserError('Expected property name after "."', this.peek().position, this.expression);
1425
- }
1426
- const property = this.advance().value;
1427
- node = { type: 'MemberAccess', object: node, property };
1428
- }
1429
- else if (this.match(TokenType.OPTIONAL_DOT)) {
1430
- if (this.match(TokenType.LBRACKET)) {
1431
- const property = this.parseExpression();
1432
- this.consume(TokenType.RBRACKET, 'Expected "]" after computed property');
1433
- node = { type: 'ComputedMemberAccess', object: node, property, optional: true };
1434
- }
1435
- else if (this.match(TokenType.LPAREN)) {
1436
- const args = this.parseArgumentList();
1437
- this.consume(TokenType.RPAREN, 'Expected ")" after arguments');
1438
- node = { type: 'CallExpression', callee: node, arguments: args, optional: true };
1439
- }
1440
- else if (this.check(TokenType.IDENTIFIER)) {
1441
- const property = this.advance().value;
1442
- node = { type: 'MemberAccess', object: node, property, optional: true };
1443
- }
1444
- else {
1445
- throw new ExpressionParserError('Expected property name, "[", or "(" after "?."', this.peek().position, this.expression);
1446
- }
1447
- }
1448
- else if (this.match(TokenType.LBRACKET)) {
1449
- const property = this.parseExpression();
1450
- this.consume(TokenType.RBRACKET, 'Expected "]" after computed property');
1451
- node = { type: 'ComputedMemberAccess', object: node, property };
1452
- }
1453
- else if (this.match(TokenType.LPAREN)) {
1454
- const args = this.parseArgumentList();
1455
- this.consume(TokenType.RPAREN, 'Expected ")" after arguments');
1456
- node = { type: 'CallExpression', callee: node, arguments: args };
1457
- }
1458
- else {
1459
- break;
1460
- }
1294
+ evaluateCallExpression(node) {
1295
+ if (node.callee.type !== 'MemberAccess') {
1296
+ throw new ExpressionParserError('Only method calls are allowed, not arbitrary function calls', 0, this.expression);
1461
1297
  }
1462
- return node;
1463
- }
1464
- parsePrimary() {
1465
- if (this.match(TokenType.TRUE)) {
1466
- return { type: 'Literal', value: true };
1298
+ const obj = this.evaluate(node.callee.object);
1299
+ if (obj === null || obj === undefined) {
1300
+ return undefined;
1467
1301
  }
1468
- if (this.match(TokenType.FALSE)) {
1469
- return { type: 'Literal', value: false };
1302
+ const methodName = node.callee.property;
1303
+ if (BLOCKED_PROPERTIES.has(methodName)) {
1304
+ throw new ExpressionParserError(`Property "${methodName}" is not accessible for security reasons`, 0, this.expression);
1470
1305
  }
1471
- if (this.match(TokenType.NULL)) {
1472
- return { type: 'Literal', value: null };
1306
+ if (!this.isMethodSafe(obj, methodName)) {
1307
+ throw new ExpressionParserError(`Method "${methodName}" is not allowed for security reasons`, 0, this.expression);
1473
1308
  }
1474
- if (this.match(TokenType.UNDEFINED)) {
1475
- return { type: 'Literal', value: undefined };
1309
+ const args = node.arguments.map((arg) => this.evaluate(arg));
1310
+ const method = obj[methodName];
1311
+ if (typeof method === 'function') {
1312
+ return method.call(obj, ...args);
1476
1313
  }
1477
- if (this.match(TokenType.NUMBER)) {
1478
- return { type: 'Literal', value: parseFloat(this.previous().value) };
1314
+ return undefined;
1315
+ }
1316
+ evaluateArrayLiteral(node) {
1317
+ return node.elements.map((element) => this.evaluate(element));
1318
+ }
1319
+ evaluateObjectLiteral(node) {
1320
+ const result = {};
1321
+ for (const { key, value } of node.properties) {
1322
+ if (BLOCKED_PROPERTIES.has(key)) {
1323
+ throw new ExpressionParserError(`Property "${key}" is not allowed in object literals for security reasons`, 0, this.expression);
1324
+ }
1325
+ result[key] = this.evaluate(value);
1479
1326
  }
1480
- if (this.match(TokenType.STRING)) {
1481
- return { type: 'Literal', value: this.previous().value };
1327
+ return result;
1328
+ }
1329
+ evaluateArrowFunction(node) {
1330
+ const parentScope = this.scope;
1331
+ const expression = this.expression;
1332
+ return (...args) => {
1333
+ const childScope = { ...parentScope };
1334
+ for (let i = 0; i < node.params.length; i++) {
1335
+ childScope[node.params[i]] = args[i];
1336
+ }
1337
+ const childEvaluator = new Evaluator(childScope, expression);
1338
+ return childEvaluator.evaluate(node.body);
1339
+ };
1340
+ }
1341
+ isMethodSafe(obj, methodName) {
1342
+ if (obj === null || obj === undefined) {
1343
+ return false;
1482
1344
  }
1483
- if (this.match(TokenType.LBRACKET)) {
1484
- const elements = this.parseArrayElements();
1485
- this.consume(TokenType.RBRACKET, 'Expected "]" after array elements');
1486
- return { type: 'ArrayLiteral', elements };
1345
+ const type = Array.isArray(obj) ? 'array' : obj instanceof Date ? 'date' : typeof obj;
1346
+ const safeMethods = SAFE_METHODS[type];
1347
+ if (!safeMethods) {
1348
+ return false;
1487
1349
  }
1488
- if (this.match(TokenType.LBRACE)) {
1489
- const properties = this.parseObjectProperties();
1490
- this.consume(TokenType.RBRACE, 'Expected "}" after object properties');
1491
- return { type: 'ObjectLiteral', properties };
1350
+ return safeMethods.has(methodName);
1351
+ }
1352
+ }
1353
+
1354
+ const MAX_AST_CACHE_SIZE = 1000;
1355
+ class LRUCache {
1356
+ cache = new Map();
1357
+ maxSize;
1358
+ constructor(maxSize) {
1359
+ this.maxSize = maxSize;
1360
+ }
1361
+ get(key) {
1362
+ const value = this.cache.get(key);
1363
+ if (value !== undefined) {
1364
+ this.cache.delete(key);
1365
+ this.cache.set(key, value);
1492
1366
  }
1493
- if (this.match(TokenType.IDENTIFIER)) {
1494
- return { type: 'Identifier', name: this.previous().value };
1367
+ return value;
1368
+ }
1369
+ set(key, value) {
1370
+ if (this.cache.has(key)) {
1371
+ this.cache.delete(key);
1495
1372
  }
1496
- if (this.match(TokenType.LPAREN)) {
1497
- const expr = this.parseExpression();
1498
- this.consume(TokenType.RPAREN, 'Expected ")" after expression');
1499
- return expr;
1373
+ this.cache.set(key, value);
1374
+ if (this.cache.size > this.maxSize) {
1375
+ const firstKey = this.cache.keys().next().value;
1376
+ if (firstKey !== undefined) {
1377
+ this.cache.delete(firstKey);
1378
+ }
1500
1379
  }
1501
- throw new ExpressionParserError(`Unexpected token: ${this.peek().value}`, this.peek().position, this.expression);
1502
1380
  }
1503
- parseArgumentList() {
1504
- const args = [];
1505
- if (!this.check(TokenType.RPAREN)) {
1506
- do {
1507
- args.push(this.parseExpression());
1508
- } while (this.match(TokenType.COMMA));
1509
- }
1510
- return args;
1381
+ clear() {
1382
+ this.cache.clear();
1511
1383
  }
1512
- parseArrayElements() {
1513
- const elements = [];
1514
- if (!this.check(TokenType.RBRACKET)) {
1515
- do {
1516
- elements.push(this.parseExpression());
1517
- } while (this.match(TokenType.COMMA));
1384
+ get size() {
1385
+ return this.cache.size;
1386
+ }
1387
+ }
1388
+ const astCache = new LRUCache(MAX_AST_CACHE_SIZE);
1389
+ class ExpressionParser {
1390
+ static parse(expression) {
1391
+ const cached = astCache.get(expression);
1392
+ if (cached) {
1393
+ return cached;
1518
1394
  }
1519
- return elements;
1395
+ const parser = new Parser(expression);
1396
+ const ast = parser.parse();
1397
+ astCache.set(expression, ast);
1398
+ return ast;
1520
1399
  }
1521
- parseObjectProperties() {
1522
- const properties = [];
1523
- if (this.check(TokenType.RBRACE)) {
1524
- return properties;
1400
+ static evaluate(expression, scope) {
1401
+ try {
1402
+ const ast = this.parse(expression);
1403
+ const evaluator = new Evaluator(scope, expression);
1404
+ return evaluator.evaluate(ast);
1525
1405
  }
1526
- do {
1527
- if (this.check(TokenType.RBRACE))
1528
- break;
1529
- let key;
1530
- if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.STRING)) {
1531
- key = this.advance().value;
1406
+ catch (error) {
1407
+ if (error instanceof ExpressionParserError) {
1408
+ throw error;
1532
1409
  }
1533
- else {
1534
- throw new ExpressionParserError(`Expected property name (identifier or string) in object literal, got '${this.peek().value}'`, this.peek().position, this.expression);
1410
+ throw new ExpressionParserError(`Error evaluating expression: ${error instanceof Error ? error.message : String(error)}`, 0, expression);
1411
+ }
1412
+ }
1413
+ static clearCache() {
1414
+ astCache.clear();
1415
+ }
1416
+ static getCacheStats() {
1417
+ return {
1418
+ size: astCache.size,
1419
+ maxSize: MAX_AST_CACHE_SIZE,
1420
+ };
1421
+ }
1422
+ }
1423
+
1424
+ function isThenableOrObservable(value) {
1425
+ if (value === null || typeof value !== 'object')
1426
+ return false;
1427
+ const candidate = value;
1428
+ return typeof candidate.then === 'function' || typeof candidate.subscribe === 'function';
1429
+ }
1430
+ function evaluateCondition(expression, context) {
1431
+ switch (expression.type) {
1432
+ case 'fieldValue':
1433
+ return evaluateFieldValueCondition(expression, context);
1434
+ case 'javascript':
1435
+ return evaluateJavaScriptExpression(expression, context);
1436
+ case 'custom': {
1437
+ const bothSet = expression;
1438
+ const inlineFn = bothSet.fn;
1439
+ const registeredName = bothSet.functionName;
1440
+ if (inlineFn && registeredName) {
1441
+ context.logger.warn('Both "fn" and "functionName" are set on custom condition. Inline "fn" takes precedence; "functionName" is ignored.');
1442
+ }
1443
+ const customFn = inlineFn ?? (registeredName ? context.customFunctions?.[registeredName] : undefined);
1444
+ if (!customFn) {
1445
+ context.logger.error('Custom function not found:', registeredName);
1446
+ return false;
1447
+ }
1448
+ try {
1449
+ const result = customFn(context);
1450
+ if (isThenableOrObservable(result)) {
1451
+ context.logger.warn(`Custom condition '${registeredName ?? '<inline>'}' returned a Promise or Observable. ` +
1452
+ `Synchronous conditions must return a value; use an 'async' or 'http' condition type for asynchronous logic. Treating as false.`);
1453
+ return false;
1454
+ }
1455
+ return !!result;
1535
1456
  }
1536
- this.consume(TokenType.COLON, 'Expected ":" after property name in object literal');
1537
- const value = this.parseExpression();
1538
- properties.push({ key, value });
1539
- } while (this.match(TokenType.COMMA));
1540
- return properties;
1541
- }
1542
- match(...types) {
1543
- for (const type of types) {
1544
- if (this.check(type)) {
1545
- this.advance();
1546
- return true;
1457
+ catch (error) {
1458
+ context.logger.error('Error executing custom function:', registeredName ?? '<inline>', error);
1459
+ return false;
1547
1460
  }
1548
1461
  }
1462
+ case 'and':
1463
+ return evaluateAndCondition(expression, context);
1464
+ case 'or':
1465
+ return evaluateOrCondition(expression, context);
1466
+ case 'http':
1467
+ context.logger.warn('HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +
1468
+ 'When used inside and/or composites, the HTTP result is not available synchronously. Returning false.');
1469
+ return false;
1470
+ case 'async':
1471
+ context.logger.warn('Async Condition - resolved asynchronously via createAsyncConditionLogicFunction(). ' +
1472
+ 'When used inside and/or composites, the async result is not available synchronously. Returning false.');
1473
+ return false;
1474
+ default:
1475
+ return false;
1476
+ }
1477
+ }
1478
+ function evaluateFieldValueCondition(expression, context) {
1479
+ if (!expression.fieldPath)
1549
1480
  return false;
1481
+ const fieldValue = hasNestedProperty(context.formValue, expression.fieldPath)
1482
+ ? getNestedValue(context.formValue, expression.fieldPath)
1483
+ : context.rootFormValue
1484
+ ? getNestedValue(context.rootFormValue, expression.fieldPath)
1485
+ : undefined;
1486
+ return compareValues(fieldValue, expression.value, expression.operator);
1487
+ }
1488
+ function evaluateJavaScriptExpression(expression, context) {
1489
+ try {
1490
+ const result = ExpressionParser.evaluate(expression.expression, context);
1491
+ return !!result;
1550
1492
  }
1551
- check(type) {
1552
- if (this.isAtEnd())
1553
- return false;
1554
- return this.peek().type === type;
1493
+ catch (error) {
1494
+ context.logger.error('Error evaluating JavaScript expression:', expression.expression, error);
1495
+ return false;
1555
1496
  }
1556
- advance() {
1557
- if (!this.isAtEnd())
1558
- this.current++;
1559
- return this.previous();
1497
+ }
1498
+ function evaluateAndCondition(expression, context) {
1499
+ if (expression.conditions.length === 0)
1500
+ return false;
1501
+ return expression.conditions.every((condition) => evaluateCondition(condition, context));
1502
+ }
1503
+ function evaluateOrCondition(expression, context) {
1504
+ if (expression.conditions.length === 0)
1505
+ return false;
1506
+ return expression.conditions.some((condition) => evaluateCondition(condition, context));
1507
+ }
1508
+
1509
+ class HttpConditionFunctionCacheService {
1510
+ httpConditionFunctionCache = new Map();
1511
+ }
1512
+
1513
+ function stableStringify(value) {
1514
+ if (value === null || value === undefined) {
1515
+ return String(value);
1560
1516
  }
1561
- isAtEnd() {
1562
- return this.peek().type === TokenType.EOF;
1517
+ if (typeof value !== 'object') {
1518
+ return JSON.stringify(value);
1563
1519
  }
1564
- peek() {
1565
- return this.tokens[this.current];
1520
+ if (Array.isArray(value)) {
1521
+ return '[' + value.map(stableStringify).join(',') + ']';
1566
1522
  }
1567
- previous() {
1568
- return this.tokens[this.current - 1];
1523
+ const obj = value;
1524
+ const sortedKeys = Object.keys(obj).sort();
1525
+ const pairs = sortedKeys.map((key) => JSON.stringify(key) + ':' + stableStringify(obj[key]));
1526
+ return '{' + pairs.join(',') + '}';
1527
+ }
1528
+
1529
+ const DEFAULT_WRAPPERS = new InjectionToken('DEFAULT_WRAPPERS');
1530
+ const FIELD_SIGNAL_CONTEXT = new InjectionToken('FIELD_SIGNAL_CONTEXT');
1531
+ function injectFieldSignalContext() {
1532
+ const ctx = inject(FIELD_SIGNAL_CONTEXT, { optional: true });
1533
+ if (!ctx) {
1534
+ throw new DynamicFormError('FIELD_SIGNAL_CONTEXT is not provided. This consumer must run inside a <df-dynamic-form> component tree.');
1569
1535
  }
1570
- consume(type, message) {
1571
- if (this.check(type))
1572
- return this.advance();
1573
- throw new ExpressionParserError(message, this.peek().position, this.expression);
1536
+ return ctx;
1537
+ }
1538
+ const ARRAY_CONTEXT = new InjectionToken('ARRAY_CONTEXT');
1539
+ const GROUP_CONTEXT = new InjectionToken('GROUP_CONTEXT');
1540
+ const DEFAULT_PROPS = new InjectionToken('DEFAULT_PROPS');
1541
+ const DEFAULT_VALIDATION_MESSAGES = new InjectionToken('DEFAULT_VALIDATION_MESSAGES');
1542
+ const FORM_OPTIONS = new InjectionToken('FORM_OPTIONS');
1543
+ const EXTERNAL_DATA = new InjectionToken('EXTERNAL_DATA');
1544
+ const FORM_ID_PREFIX = new InjectionToken('FORM_ID_PREFIX');
1545
+
1546
+ class RootFormRegistryService {
1547
+ formValue;
1548
+ rootForm;
1549
+ constructor(formValue, rootForm) {
1550
+ this.formValue = formValue;
1551
+ this.rootForm = rootForm;
1574
1552
  }
1575
1553
  }
1576
1554
 
1577
- const STRING_SAFE_METHODS = [
1578
- 'charAt',
1579
- 'charCodeAt',
1580
- 'concat',
1581
- 'endsWith',
1582
- 'includes',
1583
- 'indexOf',
1584
- 'lastIndexOf',
1585
- 'match',
1586
- 'padEnd',
1587
- 'padStart',
1588
- 'repeat',
1589
- 'replace',
1590
- 'search',
1591
- 'slice',
1592
- 'split',
1593
- 'startsWith',
1594
- 'substring',
1595
- 'toLowerCase',
1596
- 'toUpperCase',
1597
- 'trim',
1598
- 'trimEnd',
1599
- 'trimStart',
1600
- 'toString',
1601
- ];
1602
- const NUMBER_SAFE_METHODS = ['toExponential', 'toFixed', 'toPrecision', 'toString'];
1603
- const ARRAY_SAFE_METHODS = [
1604
- 'concat',
1605
- 'every',
1606
- 'filter',
1607
- 'find',
1608
- 'findIndex',
1609
- 'flat',
1610
- 'flatMap',
1611
- 'includes',
1612
- 'indexOf',
1613
- 'join',
1614
- 'lastIndexOf',
1615
- 'map',
1616
- 'reduce',
1617
- 'reduceRight',
1618
- 'slice',
1619
- 'some',
1620
- 'toString',
1621
- 'entries',
1622
- 'keys',
1623
- 'values',
1624
- ];
1625
- const DATE_SAFE_METHODS = [
1626
- 'getDate',
1627
- 'getDay',
1628
- 'getFullYear',
1629
- 'getHours',
1630
- 'getMilliseconds',
1631
- 'getMinutes',
1632
- 'getMonth',
1633
- 'getSeconds',
1634
- 'getTime',
1635
- 'getTimezoneOffset',
1636
- 'getUTCDate',
1637
- 'getUTCDay',
1638
- 'getUTCFullYear',
1639
- 'getUTCHours',
1640
- 'getUTCMilliseconds',
1641
- 'getUTCMinutes',
1642
- 'getUTCMonth',
1643
- 'getUTCSeconds',
1644
- 'toDateString',
1645
- 'toISOString',
1646
- 'toJSON',
1647
- 'toString',
1648
- 'toTimeString',
1649
- 'toUTCString',
1650
- ];
1651
- const SAFE_METHODS = {
1652
- string: new Set(STRING_SAFE_METHODS),
1653
- number: new Set(NUMBER_SAFE_METHODS),
1654
- array: new Set(ARRAY_SAFE_METHODS),
1655
- date: new Set(DATE_SAFE_METHODS),
1656
- };
1657
- const BLOCKED_PROPERTIES = new Set([
1658
- 'constructor',
1659
- '__proto__',
1660
- 'prototype',
1661
- '__defineGetter__',
1662
- '__defineSetter__',
1663
- '__lookupGetter__',
1664
- '__lookupSetter__',
1665
- ]);
1666
- class Evaluator {
1667
- scope;
1668
- expression;
1669
- constructor(scope, expression) {
1670
- this.scope = scope;
1671
- this.expression = expression;
1555
+ const DEPRECATION_WARNING_TRACKER = new InjectionToken('DeprecationWarningTracker');
1556
+
1557
+ function safeReadPathKeys(fieldContext) {
1558
+ if (!('pathKeys' in fieldContext) || typeof fieldContext.pathKeys !== 'function') {
1559
+ return [];
1672
1560
  }
1673
- evaluate(node) {
1674
- switch (node.type) {
1675
- case 'Literal':
1676
- return node.value;
1677
- case 'Identifier':
1678
- return this.evaluateIdentifier(node.name);
1679
- case 'MemberAccess':
1680
- return this.evaluateMemberAccess(node);
1681
- case 'ComputedMemberAccess':
1682
- return this.evaluateComputedMemberAccess(node);
1683
- case 'BinaryOp':
1684
- return this.evaluateBinaryOp(node);
1685
- case 'UnaryOp':
1686
- return this.evaluateUnaryOp(node);
1687
- case 'CallExpression':
1688
- return this.evaluateCallExpression(node);
1689
- case 'Conditional':
1690
- return this.evaluate(node.test) ? this.evaluate(node.consequent) : this.evaluate(node.alternate);
1691
- case 'ArrayLiteral':
1692
- return this.evaluateArrayLiteral(node);
1693
- case 'ObjectLiteral':
1694
- return this.evaluateObjectLiteral(node);
1695
- case 'ArrowFunction':
1696
- return this.evaluateArrowFunction(node);
1697
- default:
1698
- throw new ExpressionParserError(`Unknown node type: ${node.type}`, 0, this.expression);
1561
+ return untracked(() => fieldContext.pathKeys());
1562
+ }
1563
+
1564
+ function isChildFieldContext(context) {
1565
+ return 'key' in context && isSignal(context.key);
1566
+ }
1567
+ function extractFieldState(fieldContext) {
1568
+ return untracked(() => {
1569
+ if (!fieldContext || !('state' in fieldContext))
1570
+ return undefined;
1571
+ return fieldContext.state;
1572
+ });
1573
+ }
1574
+ function detectArrayScope(pathKeys) {
1575
+ if (pathKeys.length < 3)
1576
+ return undefined;
1577
+ for (let i = pathKeys.length - 2; i >= 1; i--) {
1578
+ const maybeIndex = Number(pathKeys[i]);
1579
+ if (Number.isInteger(maybeIndex) && maybeIndex >= 0) {
1580
+ return {
1581
+ arrayKey: pathKeys.slice(0, i).join('.'),
1582
+ index: maybeIndex,
1583
+ localKey: pathKeys[pathKeys.length - 1],
1584
+ };
1699
1585
  }
1700
1586
  }
1701
- evaluateIdentifier(name) {
1702
- if (name in this.scope) {
1703
- return this.scope[name];
1587
+ return undefined;
1588
+ }
1589
+ class FieldContextRegistryService {
1590
+ rootFormRegistry = inject(RootFormRegistryService);
1591
+ logger = inject(DynamicFormLogger);
1592
+ deprecationTracker = inject(DEPRECATION_WARNING_TRACKER, { optional: true });
1593
+ externalDataSignal = inject(EXTERNAL_DATA, { optional: true });
1594
+ createEvaluationContext(fieldContext, customFunctions) {
1595
+ const fieldValue = untracked(() => fieldContext.value());
1596
+ const rootFormValue = untracked(() => this.rootFormRegistry.formValue());
1597
+ const localKey = this.extractFieldPath(fieldContext);
1598
+ const pathKeys = safeReadPathKeys(fieldContext);
1599
+ const arrayScope = detectArrayScope(pathKeys);
1600
+ if (arrayScope) {
1601
+ return this.buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, false, fieldContext);
1704
1602
  }
1705
- return undefined;
1603
+ const rootFormSignal = this.rootFormRegistry.rootForm;
1604
+ return {
1605
+ fieldValue,
1606
+ formValue: rootFormValue,
1607
+ fieldPath: localKey,
1608
+ customFunctions: customFunctions || {},
1609
+ externalData: this.resolveExternalData(false),
1610
+ logger: this.logger,
1611
+ deprecationTracker: this.deprecationTracker ?? undefined,
1612
+ get fieldState() {
1613
+ return readFieldStateInfo(extractFieldState(fieldContext), false);
1614
+ },
1615
+ get formFieldState() {
1616
+ return createFormFieldStateMap(untracked(rootFormSignal), false);
1617
+ },
1618
+ };
1706
1619
  }
1707
- evaluateMemberAccess(node) {
1708
- const obj = this.evaluate(node.object);
1709
- if (obj === null || obj === undefined) {
1710
- return undefined;
1620
+ extractFieldPath(fieldContext) {
1621
+ if (isChildFieldContext(fieldContext)) {
1622
+ try {
1623
+ return String(fieldContext.key());
1624
+ }
1625
+ catch (error) {
1626
+ this.logger.warn('Unable to extract field key:', error);
1627
+ }
1711
1628
  }
1712
- if (BLOCKED_PROPERTIES.has(node.property)) {
1713
- throw new ExpressionParserError(`Property "${node.property}" is not accessible for security reasons`, 0, this.expression);
1629
+ return '';
1630
+ }
1631
+ buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, reactive, fieldContext) {
1632
+ const { arrayKey, index, localKey } = arrayScope;
1633
+ const arrayData = getNestedValue(rootFormValue, arrayKey);
1634
+ let scopedFormValue;
1635
+ if (Array.isArray(arrayData) && index >= 0 && index < arrayData.length) {
1636
+ const item = arrayData[index];
1637
+ if (item != null && typeof item === 'object') {
1638
+ scopedFormValue = item;
1639
+ }
1714
1640
  }
1715
- if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {
1716
- return obj[node.property];
1641
+ const rootFormSignal = this.rootFormRegistry.rootForm;
1642
+ const fieldStateGetter = fieldContext ? () => readFieldStateInfo(extractFieldState(fieldContext), reactive) : () => undefined;
1643
+ const formFieldStateGetter = () => createFormFieldStateMap((reactive ? rootFormSignal() : untracked(rootFormSignal)), reactive);
1644
+ if (!scopedFormValue) {
1645
+ return {
1646
+ fieldValue,
1647
+ formValue: rootFormValue,
1648
+ fieldPath: localKey,
1649
+ customFunctions: customFunctions || {},
1650
+ externalData: this.resolveExternalData(reactive),
1651
+ logger: this.logger,
1652
+ deprecationTracker: this.deprecationTracker ?? undefined,
1653
+ get fieldState() {
1654
+ return fieldStateGetter();
1655
+ },
1656
+ get formFieldState() {
1657
+ return formFieldStateGetter();
1658
+ },
1659
+ };
1717
1660
  }
1718
- return undefined;
1661
+ return {
1662
+ fieldValue,
1663
+ formValue: scopedFormValue,
1664
+ rootFormValue,
1665
+ arrayIndex: index,
1666
+ arrayPath: arrayKey,
1667
+ fieldPath: `${arrayKey}.${index}.${localKey}`,
1668
+ customFunctions: customFunctions || {},
1669
+ externalData: this.resolveExternalData(reactive),
1670
+ logger: this.logger,
1671
+ deprecationTracker: this.deprecationTracker ?? undefined,
1672
+ get fieldState() {
1673
+ return fieldStateGetter();
1674
+ },
1675
+ get formFieldState() {
1676
+ return formFieldStateGetter();
1677
+ },
1678
+ };
1719
1679
  }
1720
- evaluateComputedMemberAccess(node) {
1721
- const obj = this.evaluate(node.object);
1722
- if (obj === null || obj === undefined) {
1723
- return undefined;
1724
- }
1725
- const rawKey = this.evaluate(node.property);
1726
- if (rawKey === null || rawKey === undefined)
1680
+ resolveExternalData(reactive) {
1681
+ const externalDataSignal = this.externalDataSignal;
1682
+ if (!externalDataSignal)
1683
+ return undefined;
1684
+ const externalDataRecord = reactive ? externalDataSignal() : untracked(() => externalDataSignal());
1685
+ if (!externalDataRecord) {
1727
1686
  return undefined;
1728
- const key = String(rawKey);
1729
- if (BLOCKED_PROPERTIES.has(key)) {
1730
- throw new ExpressionParserError(`Property "${key}" is not accessible for security reasons`, 0, this.expression);
1731
1687
  }
1732
- if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {
1733
- return obj[key];
1688
+ const resolved = {};
1689
+ for (const [key, value] of Object.entries(externalDataRecord)) {
1690
+ if (!isSignal(value)) {
1691
+ throw new DynamicFormError(`externalData["${key}"] must be a Signal. Got: ${typeof value}. Wrap it with signal(yourValue).`);
1692
+ }
1693
+ resolved[key] = reactive ? value() : untracked(() => value());
1734
1694
  }
1735
- return undefined;
1695
+ return resolved;
1736
1696
  }
1737
- evaluateBinaryOp(node) {
1738
- if (node.operator === '&&') {
1739
- const left = this.evaluate(node.left);
1740
- return left ? this.evaluate(node.right) : left;
1741
- }
1742
- if (node.operator === '||') {
1743
- const left = this.evaluate(node.left);
1744
- return left ? left : this.evaluate(node.right);
1745
- }
1746
- const left = this.evaluate(node.left);
1747
- const right = this.evaluate(node.right);
1748
- switch (node.operator) {
1749
- case '+':
1750
- return left + right;
1751
- case '-':
1752
- return left - right;
1753
- case '*':
1754
- return left * right;
1755
- case '/':
1756
- if (right === 0)
1757
- return null;
1758
- return left / right;
1759
- case '%':
1760
- if (right === 0)
1761
- return null;
1762
- return left % right;
1763
- case '==':
1764
- return left == right;
1765
- case '===':
1766
- return left === right;
1767
- case '!=':
1768
- return left != right;
1769
- case '!==':
1770
- return left !== right;
1771
- case '>':
1772
- return left > right;
1773
- case '<':
1774
- return left < right;
1775
- case '>=':
1776
- return left >= right;
1777
- case '<=':
1778
- return left <= right;
1779
- default:
1780
- throw new ExpressionParserError(`Unknown binary operator: ${node.operator}`, 0, this.expression);
1697
+ createReactiveEvaluationContext(fieldContext, customFunctions) {
1698
+ const fieldValue = fieldContext.value();
1699
+ const rootFormValue = this.rootFormRegistry.formValue();
1700
+ const pathKeys = safeReadPathKeys(fieldContext);
1701
+ const arrayScope = detectArrayScope(pathKeys);
1702
+ if (arrayScope) {
1703
+ return this.buildArrayScopedContext(rootFormValue, arrayScope, fieldValue, customFunctions, true, fieldContext);
1781
1704
  }
1705
+ const localKey = this.extractFieldPath(fieldContext);
1706
+ const rootFormSignal = this.rootFormRegistry.rootForm;
1707
+ return {
1708
+ fieldValue,
1709
+ formValue: rootFormValue,
1710
+ fieldPath: localKey,
1711
+ customFunctions: customFunctions || {},
1712
+ externalData: this.resolveExternalData(true),
1713
+ logger: this.logger,
1714
+ deprecationTracker: this.deprecationTracker ?? undefined,
1715
+ get fieldState() {
1716
+ return readFieldStateInfo(extractFieldState(fieldContext), true);
1717
+ },
1718
+ get formFieldState() {
1719
+ return createFormFieldStateMap(rootFormSignal(), true);
1720
+ },
1721
+ };
1722
+ }
1723
+ createDisplayOnlyContext(fieldPath, customFunctions) {
1724
+ const formValue = this.rootFormRegistry.formValue();
1725
+ const rootFormSignal = this.rootFormRegistry.rootForm;
1726
+ return {
1727
+ fieldValue: undefined,
1728
+ formValue,
1729
+ fieldPath,
1730
+ customFunctions: customFunctions || {},
1731
+ externalData: this.resolveExternalData(true),
1732
+ logger: this.logger,
1733
+ deprecationTracker: this.deprecationTracker ?? undefined,
1734
+ get formFieldState() {
1735
+ return createFormFieldStateMap(rootFormSignal(), true);
1736
+ },
1737
+ };
1738
+ }
1739
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FieldContextRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1740
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FieldContextRegistryService });
1741
+ }
1742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FieldContextRegistryService, decorators: [{
1743
+ type: Injectable
1744
+ }] });
1745
+
1746
+ class FunctionRegistryService {
1747
+ customFunctions = new Map();
1748
+ customFunctionScopes = new Map();
1749
+ derivationFunctions = new Map();
1750
+ asyncDerivationFunctions = new Map();
1751
+ asyncConditionFunctions = new Map();
1752
+ validators = new Map();
1753
+ asyncValidators = new Map();
1754
+ httpValidators = new Map();
1755
+ registerCustomFunction(name, fn, options) {
1756
+ this.customFunctions.set(name, fn);
1757
+ this.customFunctionScopes.set(name, options?.scope ?? 'form');
1758
+ }
1759
+ getCustomFunctionScope(name) {
1760
+ return this.customFunctionScopes.get(name);
1761
+ }
1762
+ isFieldScopedFunction(name) {
1763
+ return this.customFunctionScopes.get(name) === 'field';
1764
+ }
1765
+ getCustomFunctions() {
1766
+ return Object.fromEntries(this.customFunctions);
1767
+ }
1768
+ clearCustomFunctions() {
1769
+ this.customFunctions.clear();
1770
+ this.customFunctionScopes.clear();
1771
+ }
1772
+ registerDerivationFunction(name, fn) {
1773
+ this.derivationFunctions.set(name, fn);
1782
1774
  }
1783
- evaluateUnaryOp(node) {
1784
- const operand = this.evaluate(node.operand);
1785
- switch (node.operator) {
1786
- case '!':
1787
- return !operand;
1788
- case '-':
1789
- return -operand;
1790
- case '+':
1791
- return +operand;
1792
- default:
1793
- throw new ExpressionParserError(`Unknown unary operator: ${node.operator}`, 0, this.expression);
1794
- }
1775
+ getDerivationFunction(name) {
1776
+ return this.derivationFunctions.get(name);
1795
1777
  }
1796
- evaluateCallExpression(node) {
1797
- if (node.callee.type !== 'MemberAccess') {
1798
- throw new ExpressionParserError('Only method calls are allowed, not arbitrary function calls', 0, this.expression);
1799
- }
1800
- const obj = this.evaluate(node.callee.object);
1801
- if (obj === null || obj === undefined) {
1802
- return undefined;
1803
- }
1804
- const methodName = node.callee.property;
1805
- if (BLOCKED_PROPERTIES.has(methodName)) {
1806
- throw new ExpressionParserError(`Property "${methodName}" is not accessible for security reasons`, 0, this.expression);
1807
- }
1808
- if (!this.isMethodSafe(obj, methodName)) {
1809
- throw new ExpressionParserError(`Method "${methodName}" is not allowed for security reasons`, 0, this.expression);
1810
- }
1811
- const args = node.arguments.map((arg) => this.evaluate(arg));
1812
- const method = obj[methodName];
1813
- if (typeof method === 'function') {
1814
- return method.call(obj, ...args);
1815
- }
1816
- return undefined;
1778
+ getDerivationFunctions() {
1779
+ return Object.fromEntries(this.derivationFunctions);
1817
1780
  }
1818
- evaluateArrayLiteral(node) {
1819
- return node.elements.map((element) => this.evaluate(element));
1781
+ setDerivationFunctions(derivations) {
1782
+ this.setRegistryIfChanged(this.derivationFunctions, derivations);
1820
1783
  }
1821
- evaluateObjectLiteral(node) {
1822
- const result = {};
1823
- for (const { key, value } of node.properties) {
1824
- if (BLOCKED_PROPERTIES.has(key)) {
1825
- throw new ExpressionParserError(`Property "${key}" is not allowed in object literals for security reasons`, 0, this.expression);
1826
- }
1827
- result[key] = this.evaluate(value);
1828
- }
1829
- return result;
1784
+ clearDerivationFunctions() {
1785
+ this.derivationFunctions.clear();
1830
1786
  }
1831
- evaluateArrowFunction(node) {
1832
- const parentScope = this.scope;
1833
- const expression = this.expression;
1834
- return (...args) => {
1835
- const childScope = { ...parentScope };
1836
- for (let i = 0; i < node.params.length; i++) {
1837
- childScope[node.params[i]] = args[i];
1838
- }
1839
- const childEvaluator = new Evaluator(childScope, expression);
1840
- return childEvaluator.evaluate(node.body);
1841
- };
1787
+ registerAsyncDerivationFunction(name, fn) {
1788
+ this.asyncDerivationFunctions.set(name, fn);
1842
1789
  }
1843
- isMethodSafe(obj, methodName) {
1844
- if (obj === null || obj === undefined) {
1845
- return false;
1846
- }
1847
- const type = Array.isArray(obj) ? 'array' : obj instanceof Date ? 'date' : typeof obj;
1848
- const safeMethods = SAFE_METHODS[type];
1849
- if (!safeMethods) {
1850
- return false;
1851
- }
1852
- return safeMethods.has(methodName);
1790
+ getAsyncDerivationFunction(name) {
1791
+ return this.asyncDerivationFunctions.get(name);
1853
1792
  }
1854
- }
1855
-
1856
- const MAX_AST_CACHE_SIZE = 1000;
1857
- class LRUCache {
1858
- cache = new Map();
1859
- maxSize;
1860
- constructor(maxSize) {
1861
- this.maxSize = maxSize;
1793
+ getAsyncDerivationFunctions() {
1794
+ return Object.fromEntries(this.asyncDerivationFunctions);
1862
1795
  }
1863
- get(key) {
1864
- const value = this.cache.get(key);
1865
- if (value !== undefined) {
1866
- this.cache.delete(key);
1867
- this.cache.set(key, value);
1868
- }
1869
- return value;
1796
+ setAsyncDerivationFunctions(fns) {
1797
+ this.setRegistryIfChanged(this.asyncDerivationFunctions, fns);
1870
1798
  }
1871
- set(key, value) {
1872
- if (this.cache.has(key)) {
1873
- this.cache.delete(key);
1874
- }
1875
- this.cache.set(key, value);
1876
- if (this.cache.size > this.maxSize) {
1877
- const firstKey = this.cache.keys().next().value;
1878
- if (firstKey !== undefined) {
1879
- this.cache.delete(firstKey);
1880
- }
1881
- }
1799
+ clearAsyncDerivationFunctions() {
1800
+ this.asyncDerivationFunctions.clear();
1882
1801
  }
1883
- clear() {
1884
- this.cache.clear();
1802
+ registerAsyncConditionFunction(name, fn) {
1803
+ this.asyncConditionFunctions.set(name, fn);
1885
1804
  }
1886
- get size() {
1887
- return this.cache.size;
1805
+ getAsyncConditionFunction(name) {
1806
+ return this.asyncConditionFunctions.get(name);
1888
1807
  }
1889
- }
1890
- const astCache = new LRUCache(MAX_AST_CACHE_SIZE);
1891
- class ExpressionParser {
1892
- static parse(expression) {
1893
- const cached = astCache.get(expression);
1894
- if (cached) {
1895
- return cached;
1896
- }
1897
- const parser = new Parser(expression);
1898
- const ast = parser.parse();
1899
- astCache.set(expression, ast);
1900
- return ast;
1808
+ getAsyncConditionFunctions() {
1809
+ return Object.fromEntries(this.asyncConditionFunctions);
1901
1810
  }
1902
- static evaluate(expression, scope) {
1903
- try {
1904
- const ast = this.parse(expression);
1905
- const evaluator = new Evaluator(scope, expression);
1906
- return evaluator.evaluate(ast);
1907
- }
1908
- catch (error) {
1909
- if (error instanceof ExpressionParserError) {
1910
- throw error;
1911
- }
1912
- throw new ExpressionParserError(`Error evaluating expression: ${error instanceof Error ? error.message : String(error)}`, 0, expression);
1913
- }
1811
+ setAsyncConditionFunctions(fns) {
1812
+ this.setRegistryIfChanged(this.asyncConditionFunctions, fns);
1914
1813
  }
1915
- static clearCache() {
1916
- astCache.clear();
1814
+ clearAsyncConditionFunctions() {
1815
+ this.asyncConditionFunctions.clear();
1917
1816
  }
1918
- static getCacheStats() {
1919
- return {
1920
- size: astCache.size,
1921
- maxSize: MAX_AST_CACHE_SIZE,
1922
- };
1817
+ registerValidator(name, fn) {
1818
+ this.validators.set(name, fn);
1923
1819
  }
1924
- }
1925
-
1926
- function isThenableOrObservable(value) {
1927
- if (value === null || typeof value !== 'object')
1928
- return false;
1929
- const candidate = value;
1930
- return typeof candidate.then === 'function' || typeof candidate.subscribe === 'function';
1931
- }
1932
- function evaluateCondition(expression, context) {
1933
- switch (expression.type) {
1934
- case 'fieldValue':
1935
- return evaluateFieldValueCondition(expression, context);
1936
- case 'javascript':
1937
- return evaluateJavaScriptExpression(expression, context);
1938
- case 'custom': {
1939
- const bothSet = expression;
1940
- const inlineFn = bothSet.fn;
1941
- const registeredName = bothSet.functionName;
1942
- if (inlineFn && registeredName) {
1943
- context.logger.warn('Both "fn" and "functionName" are set on custom condition. Inline "fn" takes precedence; "functionName" is ignored.');
1944
- }
1945
- const customFn = inlineFn ?? (registeredName ? context.customFunctions?.[registeredName] : undefined);
1946
- if (!customFn) {
1947
- context.logger.error('Custom function not found:', registeredName);
1948
- return false;
1949
- }
1950
- try {
1951
- const result = customFn(context);
1952
- if (isThenableOrObservable(result)) {
1953
- context.logger.warn(`Custom condition '${registeredName ?? '<inline>'}' returned a Promise or Observable. ` +
1954
- `Synchronous conditions must return a value; use an 'async' or 'http' condition type for asynchronous logic. Treating as false.`);
1955
- return false;
1956
- }
1957
- return !!result;
1958
- }
1959
- catch (error) {
1960
- context.logger.error('Error executing custom function:', registeredName ?? '<inline>', error);
1961
- return false;
1962
- }
1820
+ getValidator(name) {
1821
+ return this.validators.get(name);
1822
+ }
1823
+ clearValidators() {
1824
+ this.validators.clear();
1825
+ }
1826
+ registerAsyncValidator(name, fn) {
1827
+ this.asyncValidators.set(name, fn);
1828
+ }
1829
+ getAsyncValidator(name) {
1830
+ return this.asyncValidators.get(name);
1831
+ }
1832
+ clearAsyncValidators() {
1833
+ this.asyncValidators.clear();
1834
+ }
1835
+ registerHttpValidator(name, config) {
1836
+ this.httpValidators.set(name, config);
1837
+ }
1838
+ getHttpValidator(name) {
1839
+ return this.httpValidators.get(name);
1840
+ }
1841
+ clearHttpValidators() {
1842
+ this.httpValidators.clear();
1843
+ }
1844
+ setRegistryIfChanged(registry, values) {
1845
+ if (!values)
1846
+ return;
1847
+ const entries = Object.entries(values);
1848
+ const hasChanges = entries.some(([name, value]) => registry.get(name) !== value);
1849
+ if (hasChanges) {
1850
+ entries.forEach(([name, value]) => registry.set(name, value));
1963
1851
  }
1964
- case 'and':
1965
- return evaluateAndCondition(expression, context);
1966
- case 'or':
1967
- return evaluateOrCondition(expression, context);
1968
- case 'http':
1969
- context.logger.warn('HTTP conditions are resolved asynchronously via createHttpConditionLogicFunction(). ' +
1970
- 'When used inside and/or composites, the HTTP result is not available synchronously. Returning false.');
1971
- return false;
1972
- case 'async':
1973
- context.logger.warn('Async Condition - resolved asynchronously via createAsyncConditionLogicFunction(). ' +
1974
- 'When used inside and/or composites, the async result is not available synchronously. Returning false.');
1975
- return false;
1976
- default:
1977
- return false;
1978
1852
  }
1979
- }
1980
- function evaluateFieldValueCondition(expression, context) {
1981
- if (!expression.fieldPath)
1982
- return false;
1983
- const fieldValue = hasNestedProperty(context.formValue, expression.fieldPath)
1984
- ? getNestedValue(context.formValue, expression.fieldPath)
1985
- : context.rootFormValue
1986
- ? getNestedValue(context.rootFormValue, expression.fieldPath)
1987
- : undefined;
1988
- return compareValues(fieldValue, expression.value, expression.operator);
1989
- }
1990
- function evaluateJavaScriptExpression(expression, context) {
1991
- try {
1992
- const result = ExpressionParser.evaluate(expression.expression, context);
1993
- return !!result;
1853
+ setValidators(validators) {
1854
+ this.setRegistryIfChanged(this.validators, validators);
1994
1855
  }
1995
- catch (error) {
1996
- context.logger.error('Error evaluating JavaScript expression:', expression.expression, error);
1997
- return false;
1856
+ setAsyncValidators(asyncValidators) {
1857
+ this.setRegistryIfChanged(this.asyncValidators, asyncValidators);
1998
1858
  }
1859
+ setHttpValidators(httpValidators) {
1860
+ this.setRegistryIfChanged(this.httpValidators, httpValidators);
1861
+ }
1862
+ clearAll() {
1863
+ this.clearCustomFunctions();
1864
+ this.clearDerivationFunctions();
1865
+ this.clearAsyncDerivationFunctions();
1866
+ this.clearAsyncConditionFunctions();
1867
+ this.clearValidators();
1868
+ this.clearAsyncValidators();
1869
+ this.clearHttpValidators();
1870
+ }
1871
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FunctionRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1872
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FunctionRegistryService });
1999
1873
  }
2000
- function evaluateAndCondition(expression, context) {
2001
- if (expression.conditions.length === 0)
2002
- return false;
2003
- return expression.conditions.every((condition) => evaluateCondition(condition, context));
2004
- }
2005
- function evaluateOrCondition(expression, context) {
2006
- if (expression.conditions.length === 0)
2007
- return false;
2008
- return expression.conditions.some((condition) => evaluateCondition(condition, context));
2009
- }
2010
-
2011
- class HttpConditionFunctionCacheService {
2012
- httpConditionFunctionCache = new Map();
2013
- }
1874
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FunctionRegistryService, decorators: [{
1875
+ type: Injectable
1876
+ }] });
2014
1877
 
2015
1878
  class HttpConditionCache {
2016
1879
  cache = new Map();
@@ -2094,6 +1957,92 @@ function resolveHttpRequest(config, context) {
2094
1957
  return request;
2095
1958
  }
2096
1959
 
1960
+ function isSignalArray(value) {
1961
+ return Array.isArray(value) && value.every((item) => isSignal(item));
1962
+ }
1963
+ function isSignalRecord(value) {
1964
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((item) => isSignal(item));
1965
+ }
1966
+ function derivedFromDeferred(source, operator, options) {
1967
+ const deferred$ = defer(() => {
1968
+ if (isSignal(source)) {
1969
+ return toObservable(source, { injector: options.injector });
1970
+ }
1971
+ if (isSignalArray(source)) {
1972
+ const observables = source.map((s) => toObservable(s, { injector: options.injector }));
1973
+ return combineLatest(observables);
1974
+ }
1975
+ if (isSignalRecord(source)) {
1976
+ const keys = Object.keys(source);
1977
+ const observables = keys.map((key) => toObservable(source[key], { injector: options.injector }));
1978
+ return combineLatest(observables, (...values) => {
1979
+ const result = {};
1980
+ keys.forEach((key, index) => {
1981
+ result[key] = values[index];
1982
+ });
1983
+ return result;
1984
+ });
1985
+ }
1986
+ throw new Error('Invalid source type. Expected a Signal, array of Signals, or object of Signals.');
1987
+ }).pipe(operator);
1988
+ return toSignal(deferred$, { initialValue: options.initialValue, injector: options.injector });
1989
+ }
1990
+
1991
+ function withPreviousValue(input) {
1992
+ const derived = linkedSignal({ ...(ngDevMode ? { debugName: "derived" } : /* istanbul ignore next */ {}), source: input.snapshot,
1993
+ computation: (snapshot, previous) => {
1994
+ if ((snapshot.status === 'loading' || snapshot.status === 'reloading') &&
1995
+ previous !== undefined &&
1996
+ previous.value.status !== 'error') {
1997
+ return { status: snapshot.status, value: previous.value.value };
1998
+ }
1999
+ return snapshot;
2000
+ } });
2001
+ return resourceFromSnapshots(derived);
2002
+ }
2003
+
2004
+ function createDebouncedResourceLogicFn(config) {
2005
+ const perFunctionSignalStore = new Map();
2006
+ return (ctx) => {
2007
+ const contextKey = safeReadPathKeys(ctx).join('.');
2008
+ let handle = perFunctionSignalStore.get(contextKey);
2009
+ if (!handle) {
2010
+ const trigger = signal(undefined, /* @ts-ignore */
2011
+ ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
2012
+ const resultResource = untracked(() => {
2013
+ const debouncedTrigger = derivedFromDeferred(trigger, pipe(debounceTime(config.debounceMs),
2014
+ distinctUntilChanged((a, b) => a?.key === b?.key)), { initialValue: undefined, injector: config.injector });
2015
+ const resource = rxResource({
2016
+ params: () => debouncedTrigger(),
2017
+ stream: ({ params }) => config.buildStream(params.payload),
2018
+ defaultValue: config.pendingValue,
2019
+ injector: config.injector,
2020
+ });
2021
+ return withPreviousValue(resource);
2022
+ });
2023
+ handle = { trigger, resultResource, refetchVersion: 0 };
2024
+ perFunctionSignalStore.set(contextKey, handle);
2025
+ }
2026
+ const resolved = config.resolve(ctx);
2027
+ if (resolved.kind === 'returnEarly') {
2028
+ handle.refetchVersion++;
2029
+ return resolved.value;
2030
+ }
2031
+ if (resolved.kind === 'holdPrevious') {
2032
+ return handle.resultResource.value();
2033
+ }
2034
+ const dedupKey = `${resolved.key}::${handle.refetchVersion}`;
2035
+ const newTrigger = { key: dedupKey, payload: resolved.payload };
2036
+ untracked(() => {
2037
+ const current = handle.trigger();
2038
+ if (current?.key !== newTrigger.key) {
2039
+ handle.trigger.set(newTrigger);
2040
+ }
2041
+ });
2042
+ return handle.resultResource.value();
2043
+ };
2044
+ }
2045
+
2097
2046
  function extractBoolean(response, responseExpression, pendingValue, logger) {
2098
2047
  if (responseExpression) {
2099
2048
  try {
@@ -2164,6 +2113,57 @@ function createHttpConditionLogicFunction(condition) {
2164
2113
  return fn;
2165
2114
  }
2166
2115
 
2116
+ function createAsyncConditionLogicFunction(condition) {
2117
+ const fieldContextRegistry = inject(FieldContextRegistryService);
2118
+ const functionRegistry = inject(FunctionRegistryService);
2119
+ const injector = inject(Injector);
2120
+ const logger = inject(DynamicFormLogger);
2121
+ const cacheService = inject(AsyncConditionFunctionCacheService);
2122
+ const pendingValue = condition.pendingValue ?? false;
2123
+ const debounceMs = condition.debounceMs ?? 300;
2124
+ if (condition.asyncFn && condition.asyncFunctionName) {
2125
+ logger.warn('Both "asyncFn" and "asyncFunctionName" are set on async condition. Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.');
2126
+ }
2127
+ const cacheKey = condition.asyncFn ? undefined : stableStringify(condition);
2128
+ if (cacheKey !== undefined) {
2129
+ const cached = cacheService.asyncConditionFunctionCache.get(cacheKey);
2130
+ if (cached) {
2131
+ return cached;
2132
+ }
2133
+ }
2134
+ const fn = createDebouncedResourceLogicFn({
2135
+ pendingValue,
2136
+ debounceMs,
2137
+ injector,
2138
+ resolve: (ctx) => {
2139
+ const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(ctx, functionRegistry.getCustomFunctions());
2140
+ const key = stableStringify({
2141
+ formValue: evaluationContext.formValue,
2142
+ fieldValue: evaluationContext.fieldValue,
2143
+ externalData: evaluationContext.externalData,
2144
+ });
2145
+ return { kind: 'trigger', key, payload: ctx };
2146
+ },
2147
+ buildStream: (capturedCtx) => {
2148
+ const asyncFn = condition.asyncFn ??
2149
+ (condition.asyncFunctionName ? functionRegistry.getAsyncConditionFunction(condition.asyncFunctionName) : undefined);
2150
+ if (!asyncFn) {
2151
+ logger.warn(`Async Condition - function '${condition.asyncFunctionName}' not found. ` + `Register it in customFnConfig.asyncConditions.`);
2152
+ return of(pendingValue);
2153
+ }
2154
+ const evaluationContext = fieldContextRegistry.createReactiveEvaluationContext(capturedCtx, functionRegistry.getCustomFunctions());
2155
+ return from(asyncFn(evaluationContext)).pipe(map((result) => !!result), catchError((error) => {
2156
+ logger.warn('Async Condition - function failed:', error);
2157
+ return of(pendingValue);
2158
+ }));
2159
+ },
2160
+ });
2161
+ if (cacheKey !== undefined) {
2162
+ cacheService.asyncConditionFunctionCache.set(cacheKey, fn);
2163
+ }
2164
+ return fn;
2165
+ }
2166
+
2167
2167
  class LogicFunctionCacheService {
2168
2168
  logicFunctionCache = new Map();
2169
2169
  debouncedLogicFunctionCache = new Map();
@@ -2294,35 +2294,6 @@ function toReadonlyFieldTreeCached(cache, field) {
2294
2294
  return view;
2295
2295
  }
2296
2296
 
2297
- function evaluateHttpValidationResponse(response, mapping, logger) {
2298
- const scope = { response };
2299
- try {
2300
- const result = ExpressionParser.evaluate(mapping.validWhen, scope);
2301
- if (typeof result !== 'boolean') {
2302
- logger.warn(`validWhen expression "${mapping.validWhen}" returned non-boolean value:`, result, 'Expected true or false.');
2303
- }
2304
- if (result === true) {
2305
- return null;
2306
- }
2307
- const error = { kind: mapping.errorKind };
2308
- if (mapping.errorParams) {
2309
- for (const [key, expression] of Object.entries(mapping.errorParams)) {
2310
- try {
2311
- error[key] = ExpressionParser.evaluate(expression, scope);
2312
- }
2313
- catch (err) {
2314
- logger.warn(`Error evaluating errorParam "${key}":`, expression, err);
2315
- }
2316
- }
2317
- }
2318
- return error;
2319
- }
2320
- catch (err) {
2321
- logger.warn('Error evaluating validWhen expression:', mapping.validWhen, err);
2322
- return { kind: mapping.errorKind };
2323
- }
2324
- }
2325
-
2326
2297
  const noOpLogger = {
2327
2298
  debug: () => { },
2328
2299
  info: () => { },
@@ -2471,6 +2442,35 @@ function isFunctionHttpValidator(config) {
2471
2442
  return typeof c.functionName === 'string' || (c.fn !== null && typeof c.fn === 'object');
2472
2443
  }
2473
2444
 
2445
+ function evaluateHttpValidationResponse(response, mapping, logger) {
2446
+ const scope = { response };
2447
+ try {
2448
+ const result = ExpressionParser.evaluate(mapping.validWhen, scope);
2449
+ if (typeof result !== 'boolean') {
2450
+ logger.warn(`validWhen expression "${mapping.validWhen}" returned non-boolean value:`, result, 'Expected true or false.');
2451
+ }
2452
+ if (result === true) {
2453
+ return null;
2454
+ }
2455
+ const error = { kind: mapping.errorKind };
2456
+ if (mapping.errorParams) {
2457
+ for (const [key, expression] of Object.entries(mapping.errorParams)) {
2458
+ try {
2459
+ error[key] = ExpressionParser.evaluate(expression, scope);
2460
+ }
2461
+ catch (err) {
2462
+ logger.warn(`Error evaluating errorParam "${key}":`, expression, err);
2463
+ }
2464
+ }
2465
+ }
2466
+ return error;
2467
+ }
2468
+ catch (err) {
2469
+ logger.warn('Error evaluating validWhen expression:', mapping.validWhen, err);
2470
+ return { kind: mapping.errorKind };
2471
+ }
2472
+ }
2473
+
2474
2474
  class DynamicValueFunctionCacheService {
2475
2475
  dynamicValueFunctionCache = new Map();
2476
2476
  }
@@ -2620,9 +2620,7 @@ function applyCustomValidator(config, fieldPath) {
2620
2620
  validatorFn = createFunctionValidator(config);
2621
2621
  }
2622
2622
  else {
2623
- const logger = inject(DynamicFormLogger);
2624
- logger.warn('Custom validator must have one of "expression", "functionName", or "fn"');
2625
- return;
2623
+ throw new DynamicFormError('Custom validator requires one of "expression", "functionName", or "fn".');
2626
2624
  }
2627
2625
  const whenLogic = createConditionalLogic(config.when);
2628
2626
  validate(fieldPath, (ctx) => {
@@ -2642,8 +2640,7 @@ function createFunctionValidator(config) {
2642
2640
  }
2643
2641
  const functionName = config.functionName;
2644
2642
  if (!functionName) {
2645
- logger.warn('Custom validator missing functionName');
2646
- return () => null;
2643
+ throw new DynamicFormError('Custom validator requires one of "functionName" or "fn".');
2647
2644
  }
2648
2645
  const registry = inject(FunctionRegistryService);
2649
2646
  const validatorFn = registry.getValidator(functionName);
@@ -2656,8 +2653,7 @@ function createExpressionValidator(config) {
2656
2653
  const logger = inject(DynamicFormLogger);
2657
2654
  const expression = config.expression;
2658
2655
  if (!expression) {
2659
- logger.warn('Custom validator missing expression');
2660
- return () => null;
2656
+ throw new DynamicFormError('Custom validator requires a non-empty "expression".');
2661
2657
  }
2662
2658
  const fieldContextRegistry = inject(FieldContextRegistryService);
2663
2659
  const functionRegistry = inject(FunctionRegistryService);
@@ -2940,10 +2936,10 @@ class EventBus {
2940
2936
  }
2941
2937
  return this.pipeline$.pipe(filter((event) => event.type === eventType));
2942
2938
  }
2943
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2944
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: EventBus });
2939
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EventBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2940
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EventBus });
2945
2941
  }
2946
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: EventBus, decorators: [{
2942
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EventBus, decorators: [{
2947
2943
  type: Injectable
2948
2944
  }] });
2949
2945
 
@@ -2973,18 +2969,6 @@ function resolveTokens(args, context) {
2973
2969
  });
2974
2970
  }
2975
2971
 
2976
- function applyHiddenLogic(inputs, fieldDef, rootFormRegistry) {
2977
- const rootForm = rootFormRegistry.rootForm();
2978
- if (rootForm && (fieldDef.hidden === true || fieldDef.logic?.some((l) => l.type === 'hidden'))) {
2979
- inputs['hidden'] = evaluateNonFieldHidden({
2980
- form: rootForm,
2981
- fieldLogic: fieldDef.logic,
2982
- explicitValue: fieldDef.hidden,
2983
- formValue: rootFormRegistry.formValue(),
2984
- });
2985
- }
2986
- }
2987
-
2988
2972
  function getGridClassString(fieldDef) {
2989
2973
  const col = fieldDef.col;
2990
2974
  if (typeof col === 'number' && Number.isInteger(col) && col > 0 && col <= 12) {
@@ -3005,6 +2989,18 @@ function buildClassName(fieldDef) {
3005
2989
  return classes.length > 0 ? classes.join(' ') : undefined;
3006
2990
  }
3007
2991
 
2992
+ function applyHiddenLogic(inputs, fieldDef, rootFormRegistry) {
2993
+ const rootForm = rootFormRegistry.rootForm();
2994
+ if (rootForm && (fieldDef.hidden === true || fieldDef.logic?.some((l) => l.type === 'hidden'))) {
2995
+ inputs['hidden'] = evaluateNonFieldHidden({
2996
+ form: rootForm,
2997
+ fieldLogic: fieldDef.logic,
2998
+ explicitValue: fieldDef.hidden,
2999
+ formValue: rootFormRegistry.formValue(),
3000
+ });
3001
+ }
3002
+ }
3003
+
3008
3004
  function arrayFieldMapper(fieldDef) {
3009
3005
  const rootFormRegistry = inject(RootFormRegistryService);
3010
3006
  const className = buildClassName(fieldDef);
@@ -3276,10 +3272,10 @@ class DynamicTextPipe {
3276
3272
  }
3277
3273
  return of(value || '');
3278
3274
  }
3279
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
3280
- static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.0", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
3275
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DynamicTextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
3276
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: DynamicTextPipe, isStandalone: true, name: "dynamicText" });
3281
3277
  }
3282
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: DynamicTextPipe, decorators: [{
3278
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DynamicTextPipe, decorators: [{
3283
3279
  type: Pipe,
3284
3280
  args: [{
3285
3281
  name: 'dynamicText',
@@ -3609,5 +3605,5 @@ function createWarningTracker() {
3609
3605
 
3610
3606
 
3611
3607
 
3612
- export { ADDON_ACTION_HANDLERS, ADDON_ACTION_REGISTRY, ADDON_TYPE_COMPONENT_CACHE, ADDON_TYPE_DEFINITIONS, ADDON_TYPE_REGISTRY, ARRAY_CONTEXT, AsyncConditionFunctionCacheService, ComponentInitializedEvent, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DEFAULT_WRAPPERS, DEPRECATION_WARNING_TRACKER, DF_FIELD_TEMPLATES, DfAddonSlot, DynamicFormError, DynamicFormLogger, DynamicTextPipe, DynamicValueFunctionCacheService, EMIT_FORM_VALUE_ON_EVENTS, EXTERNAL_DATA, Evaluator, EventBus, ExpressionParser, ExpressionParserError, FIELD_REGISTRY, FIELD_SIGNAL_CONTEXT, FORM_ID_PREFIX, FORM_OPTIONS, FieldContextRegistryService, FunctionRegistryService, GROUP_CONTEXT, HTTP_CONDITION_CACHE, HttpConditionCache, HttpConditionFunctionCacheService, INITIALIZATION_TIMEOUT_MS, LogicFunctionCacheService, NoopLogger, Parser, READONLY_FIELD_TREE_CACHE, RootFormRegistryService, TokenType, Tokenizer, WRAPPER_AUTO_ASSOCIATIONS, WRAPPER_COMPONENT_CACHE, WRAPPER_REGISTRY, applyHiddenLogic, applyMetaToElement, applyValidator, applyValidators, arrayFieldMapper, assertNever, baseFieldMapper, buildBaseInputs, buildClassName, compareValues, containerFieldMapper, createAddonActionRegistry, createAsyncConditionLogicFunction, createDebouncedLogicFunction, createDebouncedResourceLogicFn, createDefaultDerivationLogConfig, createDynamicValueFunction, createFormFieldStateMap, createHttpConditionLogicFunction, createInitializationTracker, createLogicFunction, createWarningTracker, deepMergeDefaults, derivedFromDeferred, detectFormMode, dynamicTextToObservable, evaluateCondition, evaluateHttpValidationResponse, evaluateNonFieldDisabled, evaluateNonFieldHidden, extractExpressionDependencies, extractStringDependencies, getArrayLength, getChangedKeys, getFieldValueHandling, getGridClassString, getNestedValue, groupFieldMapper, hasChildFields, hasCrossFieldWhenCondition, hasFormValue, hasNestedProperty, hasTargetProperty, injectAddonTypeRegistry, injectFieldSignalContext, injectFieldsSupportingAddons, interpolateParams, isArrayField, isCheckedField, isContainerField, isCrossFieldBuiltInValidator, isCrossFieldExpression, isCrossFieldSchema, isCrossFieldStateLogic, isCrossFieldValidator, isDerivationLogicConfig, isDisplayOnlyField, isEqual, isFieldBoundContext, isFormStateCondition, isFunctionHttpValidator, isGenericContainerField, isGroupField, isHiddenField, isLeafField, isNonPagedForm, isPageField, isPagedForm, isResourceBasedValidator, isRowField, isSimplifiedArrayField, isStateLogicConfig, isValidNonPagedForm, isValidPagedForm, isValueBearingField, isValueField, isWrapperTypeDefinition, keyBy, mapValues, memoize, normalizeFieldsArray, omit, pageFieldMapper, readFieldStateInfo, resolveDynamicValue, resolveHttpRequest, resolveNextButtonDisabled, resolveNonFieldDisabled, resolveNonFieldHidden, resolveSubmitButtonDisabled, resolveTokens, rowFieldMapper, runPresetAction, safeReadPathKeys, setupInitializationTracking, shouldLog, stableStringify, textFieldMapper, toReadonlyFieldTree, toReadonlyFieldTreeCached, validatePageNesting, withPreviousValue, writeToFieldValue };
3608
+ export { ADDON_ACTION_HANDLERS, ADDON_ACTION_REGISTRY, ADDON_TYPE_COMPONENT_CACHE, ADDON_TYPE_DEFINITIONS, ADDON_TYPE_REGISTRY, ARRAY_CONTEXT, AsyncConditionFunctionCacheService, ComponentInitializedEvent, DEFAULT_PROPS, DEFAULT_VALIDATION_MESSAGES, DEFAULT_WRAPPERS, DEPRECATION_WARNING_TRACKER, DF_FIELD_TEMPLATES, DfAddonSlot, DynamicFormError, DynamicFormLogger, DynamicTextPipe, DynamicValueFunctionCacheService, EMIT_FORM_VALUE_ON_EVENTS, EXTERNAL_DATA, EventBus, ExpressionParser, ExpressionParserError, FIELD_REGISTRY, FIELD_SIGNAL_CONTEXT, FORM_ID_PREFIX, FORM_OPTIONS, FieldContextRegistryService, FunctionRegistryService, GROUP_CONTEXT, HTTP_CONDITION_CACHE, HttpConditionCache, HttpConditionFunctionCacheService, INITIALIZATION_TIMEOUT_MS, LogicFunctionCacheService, NoopLogger, READONLY_FIELD_TREE_CACHE, RootFormRegistryService, TokenType, WRAPPER_AUTO_ASSOCIATIONS, WRAPPER_COMPONENT_CACHE, WRAPPER_REGISTRY, applyMetaToElement, applyValidator, applyValidators, arrayFieldMapper, assertNever, baseFieldMapper, buildBaseInputs, compareValues, containerFieldMapper, createAddonActionRegistry, createDebouncedLogicFunction, createDefaultDerivationLogConfig, createDynamicValueFunction, createFormFieldStateMap, createInitializationTracker, createLogicFunction, createWarningTracker, deepMergeDefaults, derivedFromDeferred, detectFormMode, dynamicTextToObservable, evaluateCondition, evaluateNonFieldDisabled, evaluateNonFieldHidden, extractExpressionDependencies, extractStringDependencies, getArrayLength, getChangedKeys, getFieldValueHandling, getNestedValue, groupFieldMapper, hasChildFields, hasCrossFieldWhenCondition, hasFormValue, hasNestedProperty, hasTargetProperty, injectAddonTypeRegistry, injectFieldSignalContext, injectFieldsSupportingAddons, interpolateParams, isArrayField, isCheckedField, isContainerField, isCrossFieldBuiltInValidator, isCrossFieldExpression, isCrossFieldSchema, isCrossFieldStateLogic, isCrossFieldValidator, isDerivationLogicConfig, isDisplayOnlyField, isEqual, isFieldBoundContext, isFormStateCondition, isFunctionHttpValidator, isGenericContainerField, isGroupField, isHiddenField, isLeafField, isNonPagedForm, isPageField, isPagedForm, isResourceBasedValidator, isRowField, isSimplifiedArrayField, isStateLogicConfig, isValidNonPagedForm, isValidPagedForm, isValueBearingField, isValueField, isWrapperTypeDefinition, keyBy, mapValues, memoize, normalizeFieldsArray, omit, pageFieldMapper, readFieldStateInfo, resolveDynamicValue, resolveHttpRequest, resolveNextButtonDisabled, resolveNonFieldDisabled, resolveNonFieldHidden, resolveSubmitButtonDisabled, resolveTokens, rowFieldMapper, runPresetAction, setupInitializationTracking, shouldLog, textFieldMapper, toReadonlyFieldTree, toReadonlyFieldTreeCached, validatePageNesting, withPreviousValue, writeToFieldValue };
3613
3609
  //# sourceMappingURL=ng-forge-dynamic-forms-internal.mjs.map