@via-profit/ability 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -526,17 +526,6 @@ order.total
526
526
  | **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains the element | array |
527
527
  | **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain the element | array |
528
528
 
529
- **String Operators**
530
-
531
- | DSL Operator | Synonyms | Example | Description | Types |
532
- |--------------|----------|---------|-------------|-------|
533
- | **starts with** | `begins with` | `email starts with 'admin@'` | String starts with | string |
534
- | **not starts with** | — | `email not starts with 'test'` | String does not start with | string |
535
- | **ends with** | — | `email ends with '.ru'` | String ends with | string |
536
- | **not ends with** | — | `email not ends with '.com'` | String does not end with | string |
537
- | **includes** | `contains substring` | `name includes 'lex'` | String contains substring | string |
538
- | **not includes** | — | `name not includes 'test'` | String does not contain substring | string |
539
-
540
529
  **Boolean Operators**
541
530
 
542
531
  | DSL Operator | Synonyms | Example | Description | Types |
@@ -552,6 +541,34 @@ order.total
552
541
  | **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
553
542
  | **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
554
543
 
544
+ Here is the English version, keeping the structure and tone consistent with your documentation style.
545
+
546
+ **Special Operators**
547
+
548
+ | DSL Operator | Synonyms | Example | Description | Types |
549
+ |--------------|----------|---------|-------------|--------|
550
+ | **always** | — | `always` | The condition is always true. Used for global allow rules or simplifying logic. | special operator |
551
+ | **never** | — | `never` | The condition is always false. Used for global deny rules or disabling a rule. | special operator |
552
+
553
+
554
+ **always**
555
+ An operator that always returns `true`.
556
+ Used for:
557
+
558
+ - global allow (`permit permission.* if all: always`)
559
+ - testing
560
+ - disabling complex conditions
561
+ - creating fallback rules
562
+
563
+ **never**
564
+ An operator that always returns `false`.
565
+ Used for:
566
+
567
+ - global deny (`deny permission.* if all: never`)
568
+ - temporarily disabling a rule
569
+ - explicit unconditional rejection
570
+
571
+
555
572
  #### Value
556
573
 
557
574
  Supported values:
@@ -1,6 +1,6 @@
1
1
  import AbilityCode from './AbilityCode';
2
- export type AbilityConditionCodeType = '=' | '<>' | '>' | '<' | '>=' | '<=' | 'in' | 'not in' | 'contains' | 'not contains' | 'length greater than' | 'length less than' | 'length equals';
3
- export type AbilityConditionLiteralType = 'equals' | 'not_equals' | 'contains' | 'no_contains' | 'in' | 'not_in' | 'greater_than' | 'less_than' | 'less_or_equal' | 'greater_or_equal' | 'length_greater_than' | 'length_less_than' | 'length_equals';
2
+ export type AbilityConditionCodeType = '=' | '<>' | '>' | '<' | '>=' | '<=' | 'in' | 'not in' | 'contains' | 'not contains' | 'length greater than' | 'length less than' | 'length equals' | 'always' | 'never';
3
+ export type AbilityConditionLiteralType = 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'in' | 'not_in' | 'greater_than' | 'less_than' | 'less_or_equal' | 'greater_or_equal' | 'length_greater_than' | 'length_less_than' | 'length_equals' | 'always' | 'never';
4
4
  export declare class AbilityCondition extends AbilityCode<AbilityConditionCodeType> {
5
5
  static equals: AbilityCondition;
6
6
  static not_equals: AbilityCondition;
@@ -15,6 +15,8 @@ export declare class AbilityCondition extends AbilityCode<AbilityConditionCodeTy
15
15
  static length_greater_than: AbilityCondition;
16
16
  static length_less_than: AbilityCondition;
17
17
  static length_equals: AbilityCondition;
18
+ static always: AbilityCondition;
19
+ static never: AbilityCondition;
18
20
  static fromLiteral(literal: AbilityConditionLiteralType): AbilityCondition;
19
21
  get literal(): AbilityConditionLiteralType;
20
22
  }
@@ -1,6 +1,6 @@
1
1
  export declare class AbilityError extends Error {
2
- constructor(message: string);
2
+ constructor(message: string, options?: ErrorOptions);
3
3
  }
4
4
  export declare class AbilityParserError extends Error {
5
- constructor(message: string);
5
+ constructor(message: string, options?: ErrorOptions);
6
6
  }
@@ -1,5 +1,5 @@
1
1
  import AbilityMatch from './AbilityMatch';
2
- import AbilityCondition, { AbilityConditionCodeType } from './AbilityCondition';
2
+ import { AbilityCondition, AbilityConditionCodeType } from './AbilityCondition';
3
3
  export type AbilityRuleConfig = {
4
4
  readonly id?: string | null;
5
5
  readonly name?: string | null;
@@ -42,6 +42,11 @@ export declare class AbilityRule<Resources extends object = object, Environment
42
42
  * @param params
43
43
  */
44
44
  constructor(params: AbilityRuleConstructorProps);
45
+ private isPrimitive;
46
+ private isNumber;
47
+ private isString;
48
+ private valueLen;
49
+ private operatorHandlers;
45
50
  /**
46
51
  * Check if the rule is matched
47
52
  * @param resource - The resource to check
package/dist/index.js CHANGED
@@ -22,69 +22,116 @@ class AbilityCompare extends AbilityCode {
22
22
  }
23
23
 
24
24
  class AbilityError extends Error {
25
- constructor(message) {
26
- super(message);
25
+ constructor(message, options) {
26
+ super(message, options);
27
+ this.name = 'AbilityError';
28
+ if (Error.captureStackTrace) {
29
+ Error.captureStackTrace(this, this.constructor);
30
+ }
27
31
  }
28
32
  }
29
33
  class AbilityParserError extends Error {
30
- constructor(message) {
31
- super(message);
34
+ constructor(message, options) {
35
+ super(message, options);
36
+ this.name = 'AbilityParserError';
37
+ if (Error.captureStackTrace) {
38
+ Error.captureStackTrace(this, this.constructor);
39
+ }
32
40
  }
33
41
  }
34
42
 
35
43
  class AbilityCondition extends AbilityCode {
36
- static equals = new AbilityCondition('=');
37
- static not_equals = new AbilityCondition('<>');
38
- static greater_than = new AbilityCondition('>');
39
- static less_than = new AbilityCondition('<');
40
- static less_or_equal = new AbilityCondition('<=');
41
- static greater_or_equal = new AbilityCondition('>=');
42
- static in = new AbilityCondition('in');
43
- static not_in = new AbilityCondition('not in');
44
- static contains = new AbilityCondition('contains');
45
- static not_contains = new AbilityCondition('not contains');
46
- static length_greater_than = new AbilityCondition('length greater than');
47
- static length_less_than = new AbilityCondition('length less than');
48
- static length_equals = new AbilityCondition('length equals');
44
+ static equals;
45
+ static not_equals;
46
+ static greater_than;
47
+ static less_than;
48
+ static less_or_equal;
49
+ static greater_or_equal;
50
+ static in;
51
+ static not_in;
52
+ static contains;
53
+ static not_contains;
54
+ static length_greater_than;
55
+ static length_less_than;
56
+ static length_equals;
57
+ static always;
58
+ static never;
59
+ static {
60
+ this.equals = new AbilityCondition('=');
61
+ this.not_equals = new AbilityCondition('<>');
62
+ this.greater_than = new AbilityCondition('>');
63
+ this.less_than = new AbilityCondition('<');
64
+ this.less_or_equal = new AbilityCondition('<=');
65
+ this.greater_or_equal = new AbilityCondition('>=');
66
+ this.in = new AbilityCondition('in');
67
+ this.not_in = new AbilityCondition('not in');
68
+ this.contains = new AbilityCondition('contains');
69
+ this.not_contains = new AbilityCondition('not contains');
70
+ this.length_greater_than = new AbilityCondition('length greater than');
71
+ this.length_less_than = new AbilityCondition('length less than');
72
+ this.length_equals = new AbilityCondition('length equals');
73
+ this.always = new AbilityCondition('always');
74
+ this.never = new AbilityCondition('never');
75
+ }
49
76
  static fromLiteral(literal) {
50
- switch (literal) {
51
- case 'equals':
52
- return this.equals;
53
- case 'not_equals':
54
- return this.not_equals;
55
- case 'greater_than':
56
- return this.greater_than;
57
- case 'less_than':
58
- return this.less_than;
59
- case 'less_or_equal':
60
- return this.less_or_equal;
61
- case 'greater_or_equal':
62
- return this.greater_or_equal;
63
- case 'contains':
64
- return this.contains;
65
- case 'no_contains':
66
- return this.not_contains;
67
- case 'in':
68
- return this.in;
69
- case 'not_in':
70
- return this.not_in;
71
- case 'length_greater_than':
72
- return this.length_greater_than;
73
- case 'length_equals':
74
- return this.length_equals;
75
- default:
76
- throw new AbilityParserError(`Literal ${literal} does not found in AbilityCondition class`);
77
+ const map = {
78
+ equals: this.equals,
79
+ not_equals: this.not_equals,
80
+ greater_than: this.greater_than,
81
+ less_than: this.less_than,
82
+ less_or_equal: this.less_or_equal,
83
+ greater_or_equal: this.greater_or_equal,
84
+ in: this.in,
85
+ not_in: this.not_in,
86
+ contains: this.contains,
87
+ not_contains: this.not_contains,
88
+ length_greater_than: this.length_greater_than,
89
+ length_equals: this.length_equals,
90
+ always: this.always,
91
+ never: this.never,
92
+ length_less_than: this.length_less_than,
93
+ };
94
+ const condition = map[literal];
95
+ if (!condition) {
96
+ throw new AbilityParserError(`Literal "${literal}" does not found in AbilityCondition class`);
77
97
  }
98
+ return condition;
78
99
  }
79
100
  get literal() {
80
- const literal = Object.keys(AbilityCondition).find(member => {
81
- const val = AbilityCondition[member];
82
- return val.code === this.code;
83
- });
84
- if (typeof literal === 'undefined') {
85
- throw new Error(`Literal value does not found in class AbilityCondition`);
101
+ switch (this.code) {
102
+ case '=':
103
+ return 'equals';
104
+ case '<>':
105
+ return 'not_equals';
106
+ case '>':
107
+ return 'greater_than';
108
+ case '<':
109
+ return 'less_than';
110
+ case '>=':
111
+ return 'greater_or_equal';
112
+ case '<=':
113
+ return 'less_or_equal';
114
+ case 'in':
115
+ return 'in';
116
+ case 'not in':
117
+ return 'not_in';
118
+ case 'contains':
119
+ return 'contains';
120
+ case 'not contains':
121
+ return 'not_contains';
122
+ case 'length greater than':
123
+ return 'length_greater_than';
124
+ case 'length less than':
125
+ return 'length_less_than';
126
+ case 'length equals':
127
+ return 'length_equals';
128
+ case 'always':
129
+ return 'always';
130
+ case 'never':
131
+ return 'never';
132
+ default:
133
+ throw new Error(`Unknown condition code: ${this.code}`);
86
134
  }
87
- return literal;
88
135
  }
89
136
  }
90
137
 
@@ -618,145 +665,122 @@ class AbilityRule {
618
665
  this.resource = resource;
619
666
  this.condition = condition;
620
667
  }
621
- /**
622
- * Check if the rule is matched
623
- * @param resource - The resource to check
624
- * @param environment
625
- */
626
- async check(resource, environment) {
627
- let is = false;
628
- const [subjectValue, resourceValue] = this.extractValues(resource, environment);
629
- const isValue = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
630
- // equals
631
- if (AbilityCondition.equals.isEqual(this.condition)) {
632
- is = subjectValue === resourceValue;
633
- }
634
- // not equals
635
- if (AbilityCondition.not_equals.isEqual(this.condition)) {
636
- is = subjectValue !== resourceValue;
637
- }
638
- // less than
639
- if (AbilityCondition.less_than.isEqual(this.condition)) {
640
- if (typeof subjectValue === 'number' && typeof resourceValue === 'number') {
641
- is = subjectValue < resourceValue;
642
- }
643
- }
644
- // less or equal
645
- if (AbilityCondition.less_or_equal.isEqual(this.condition)) {
646
- if (typeof subjectValue === 'number' && typeof resourceValue === 'number') {
647
- is = subjectValue <= resourceValue;
648
- }
649
- }
650
- // more than
651
- if (AbilityCondition.greater_than.isEqual(this.condition)) {
652
- if (typeof subjectValue === 'number' && typeof resourceValue === 'number') {
653
- is = subjectValue > resourceValue;
654
- }
655
- }
656
- // more or equal
657
- if (AbilityCondition.greater_or_equal.isEqual(this.condition)) {
658
- if (typeof subjectValue === 'number' && typeof resourceValue === 'number') {
659
- is = subjectValue >= resourceValue;
660
- }
661
- }
662
- // in
663
- if (AbilityCondition.in.isEqual(this.condition)) {
664
- // value in array
665
- if (isValue(subjectValue) && Array.isArray(resourceValue)) {
666
- is = resourceValue.includes(subjectValue);
667
- }
668
- // array intersects array
669
- else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
670
- is = subjectValue.some(v => resourceValue.includes(v));
671
- }
672
- }
673
- // not in
674
- if (AbilityCondition.not_in.isEqual(this.condition)) {
675
- if (isValue(subjectValue) && Array.isArray(resourceValue)) {
676
- is = !resourceValue.includes(subjectValue);
677
- }
678
- else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
679
- is = !subjectValue.some(v => resourceValue.includes(v));
680
- }
681
- }
682
- // contains
683
- if (AbilityCondition.contains.isEqual(this.condition)) {
684
- // array contains value
685
- if (Array.isArray(subjectValue) && isValue(resourceValue)) {
686
- is = subjectValue.includes(resourceValue);
687
- }
688
- // array intersects array
689
- else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
690
- is = subjectValue.some(v => resourceValue.includes(v));
668
+ isPrimitive(v) {
669
+ return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
670
+ }
671
+ isNumber(v) {
672
+ return typeof v === 'number';
673
+ }
674
+ isString(v) {
675
+ return typeof v === 'string';
676
+ }
677
+ valueLen = (v) => this.isString(v) || Array.isArray(v) ? v.length : null;
678
+ operatorHandlers = {
679
+ [AbilityCondition.always.literal]: () => true,
680
+ [AbilityCondition.never.literal]: () => false,
681
+ [AbilityCondition.equals.literal]: (a, b) => a === b,
682
+ [AbilityCondition.not_equals.literal]: (a, b) => a !== b,
683
+ [AbilityCondition.contains.literal]: (a, b) => {
684
+ if (Array.isArray(a) && this.isPrimitive(b)) {
685
+ return a.includes(b);
691
686
  }
692
- }
693
- // not contains
694
- if (AbilityCondition.not_contains.isEqual(this.condition)) {
695
- if (Array.isArray(subjectValue) && isValue(resourceValue)) {
696
- is = !subjectValue.includes(resourceValue);
687
+ if (Array.isArray(a) && Array.isArray(b)) {
688
+ return a.some(v => b.includes(v));
697
689
  }
698
- else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
699
- is = !subjectValue.some(v => resourceValue.includes(v));
690
+ return false;
691
+ },
692
+ [AbilityCondition.not_contains.literal]: (a, b) => {
693
+ if (Array.isArray(a) && this.isPrimitive(b)) {
694
+ return !a.includes(b);
700
695
  }
701
- }
702
- // length equals
703
- if (AbilityCondition.length_equals.isEqual(this.condition)) {
704
- // foo.bar == n
705
- if (isValue(subjectValue) && typeof resourceValue === 'number') {
706
- is = String(subjectValue).length === resourceValue;
696
+ if (Array.isArray(a) && Array.isArray(b)) {
697
+ return !a.some(v => b.includes(v));
707
698
  }
708
- // ['foo', 'bar'] = n
709
- else if (Array.isArray(subjectValue) && typeof resourceValue === 'number') {
710
- is = subjectValue.length === resourceValue;
699
+ return false;
700
+ },
701
+ [AbilityCondition.in.literal]: (a, b) => {
702
+ if (this.isPrimitive(a) && Array.isArray(b)) {
703
+ return b.includes(a);
711
704
  }
712
- // ['foo', 'bar'] = ['baz', 'taz']
713
- else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
714
- is = subjectValue.length === resourceValue.length;
705
+ if (Array.isArray(a) && Array.isArray(b)) {
706
+ return a.some(v => b.includes(v));
715
707
  }
716
- // 'foo' = 'bar'
717
- else if (typeof subjectValue === 'string' && typeof resourceValue === 'string') {
718
- is = subjectValue.length === resourceValue.length;
708
+ return false;
709
+ },
710
+ [AbilityCondition.not_in.literal]: (a, b) => {
711
+ if (this.isPrimitive(a) && Array.isArray(b)) {
712
+ return !b.includes(a);
719
713
  }
720
- }
721
- // length greater than
722
- if (AbilityCondition.length_greater_than.isEqual(this.condition)) {
723
- // foo.bar > n
724
- if (isValue(subjectValue) && typeof resourceValue === 'number') {
725
- is = String(subjectValue).length > resourceValue;
714
+ if (Array.isArray(a) && Array.isArray(b)) {
715
+ return !a.some(v => b.includes(v));
726
716
  }
727
- // ['foo', 'bar'] > n
728
- else if (Array.isArray(subjectValue) && typeof resourceValue === 'number') {
729
- is = subjectValue.length > resourceValue;
717
+ return false;
718
+ },
719
+ [AbilityCondition.greater_than.literal]: (a, b) => {
720
+ return this.isNumber(a) && this.isNumber(b) ? a > b : false;
721
+ },
722
+ [AbilityCondition.less_than.literal]: (a, b) => {
723
+ return this.isNumber(a) && this.isNumber(b) ? a < b : false;
724
+ },
725
+ [AbilityCondition.greater_or_equal.literal]: (a, b) => {
726
+ return this.isNumber(a) && this.isNumber(b) ? a >= b : false;
727
+ },
728
+ [AbilityCondition.less_or_equal.literal]: (a, b) => {
729
+ return this.isNumber(a) && this.isNumber(b) ? a <= b : false;
730
+ },
731
+ [AbilityCondition.length_greater_than.literal]: (a, b) => {
732
+ const alen = this.valueLen(a);
733
+ if (alen === null) {
734
+ return false;
735
+ }
736
+ if (this.isNumber(b)) {
737
+ return alen > b;
738
+ }
739
+ const bLen = this.valueLen(b);
740
+ if (bLen !== null) {
741
+ return alen > bLen;
730
742
  }
731
- // ['foo', 'bar'] > ['baz', 'taz']
732
- else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
733
- is = subjectValue.length > resourceValue.length;
743
+ return false;
744
+ },
745
+ [AbilityCondition.length_less_than.literal]: (a, b) => {
746
+ const alen = this.valueLen(a);
747
+ if (alen === null) {
748
+ return false;
734
749
  }
735
- // 'foo' > 'bar'
736
- else if (typeof subjectValue === 'string' && typeof resourceValue === 'string') {
737
- is = subjectValue.length > resourceValue.length;
750
+ if (this.isNumber(b)) {
751
+ return alen < b;
738
752
  }
739
- }
740
- // length greater than
741
- if (AbilityCondition.length_less_than.isEqual(this.condition)) {
742
- // foo.bar < n
743
- if (isValue(subjectValue) && typeof resourceValue === 'number') {
744
- is = String(subjectValue).length < resourceValue;
753
+ const bLen = this.valueLen(b);
754
+ if (bLen !== null) {
755
+ return alen < bLen;
745
756
  }
746
- // ['foo', 'bar'] < n
747
- else if (Array.isArray(subjectValue) && typeof resourceValue === 'number') {
748
- is = subjectValue.length < resourceValue;
757
+ return false;
758
+ },
759
+ [AbilityCondition.length_equals.literal]: (a, b) => {
760
+ const alen = this.valueLen(a);
761
+ if (alen === null) {
762
+ return false;
749
763
  }
750
- // ['foo', 'bar'] < ['baz', 'taz']
751
- else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
752
- is = subjectValue.length < resourceValue.length;
764
+ if (this.isNumber(b)) {
765
+ return alen === b;
753
766
  }
754
- // 'foo' < 'bar'
755
- else if (typeof subjectValue === 'string' && typeof resourceValue === 'string') {
756
- is = subjectValue.length < resourceValue.length;
767
+ const bLen = this.valueLen(b);
768
+ if (bLen !== null) {
769
+ return alen === bLen;
757
770
  }
758
- }
759
- this.state = is ? AbilityMatch.match : AbilityMatch.mismatch;
771
+ return false;
772
+ },
773
+ };
774
+ /**
775
+ * Check if the rule is matched
776
+ * @param resource - The resource to check
777
+ * @param environment
778
+ */
779
+ async check(resource, environment) {
780
+ const [subjectValue, resourceValue] = this.extractValues(resource, environment);
781
+ const handler = this.operatorHandlers[this.condition.literal];
782
+ const result = handler(subjectValue, resourceValue);
783
+ this.state = result ? AbilityMatch.match : AbilityMatch.mismatch;
760
784
  return this.state;
761
785
  }
762
786
  /**
@@ -1221,6 +1245,8 @@ class AbilityDSLToken extends AbilityCode {
1221
1245
  static LEN_LT = 'LEN_LT';
1222
1246
  static LEN_EQ = 'LEN_EQ';
1223
1247
  static NOT_EQ = 'NOT_EQ';
1248
+ static ALWAYS = 'ALWAYS';
1249
+ static NEVER = 'NEVER';
1224
1250
  static STRING = 'STRING';
1225
1251
  static NUMBER = 'NUMBER';
1226
1252
  static BOOLEAN = 'BOOLEAN';
@@ -1263,6 +1289,8 @@ class AbilityDSLLexer {
1263
1289
  'is',
1264
1290
  'or',
1265
1291
  'than',
1292
+ 'always',
1293
+ 'never',
1266
1294
  ]);
1267
1295
  constructor(input) {
1268
1296
  this.input = input;
@@ -1408,6 +1436,12 @@ class AbilityDSLLexer {
1408
1436
  }
1409
1437
  }
1410
1438
  const word = this.input.slice(start, this.pos);
1439
+ if (word === 'always') {
1440
+ return new AbilityDSLToken(AbilityDSLToken.ALWAYS, word, startLine, startColumn);
1441
+ }
1442
+ if (word === 'never') {
1443
+ return new AbilityDSLToken(AbilityDSLToken.NEVER, word, startLine, startColumn);
1444
+ }
1411
1445
  // Если есть точка — это путь (identifier или permission)
1412
1446
  if (word.includes('.')) {
1413
1447
  const last = this.tokens[this.tokens.length - 1];
@@ -1670,7 +1704,9 @@ class AbilityDSLParser {
1670
1704
  if (this.isStartOfGroup() || this.isStartOfPolicy()) {
1671
1705
  break;
1672
1706
  }
1673
- if (this.check(AbilityDSLToken.IDENTIFIER)) {
1707
+ if (this.check(AbilityDSLToken.IDENTIFIER) ||
1708
+ this.check(AbilityDSLToken.ALWAYS) ||
1709
+ this.check(AbilityDSLToken.NEVER)) {
1674
1710
  group.addRule(this.parseRule());
1675
1711
  }
1676
1712
  else {
@@ -1687,7 +1723,7 @@ class AbilityDSLParser {
1687
1723
  parseGroup() {
1688
1724
  this.consumeLeadingComments();
1689
1725
  const meta = this.takeAnnotations();
1690
- const compareToken = this.consumeOneOf([AbilityDSLToken.ALL, AbilityDSLToken.ANY], 'Expected "all" or "any"');
1726
+ const compareToken = this.consumeOneOf([AbilityDSLToken.ALL, AbilityDSLToken.ANY, AbilityDSLToken.ALWAYS, AbilityDSLToken.NEVER], 'Expected "all" or "any" or "always" or "never"');
1691
1727
  const compareMethod = compareToken.code === AbilityDSLToken.ALL ? AbilityCompare.and : AbilityCompare.or;
1692
1728
  if (this.check(AbilityDSLToken.OF)) {
1693
1729
  this.advance();
@@ -1717,11 +1753,26 @@ class AbilityDSLParser {
1717
1753
  parseRule() {
1718
1754
  this.consumeLeadingComments();
1719
1755
  const meta = this.takeAnnotations();
1720
- if (!this.check(AbilityDSLToken.IDENTIFIER)) {
1721
- this.syntaxError(`Expected identifier, got ${this.peek().code}`, this.peek());
1756
+ // if (this.check(AbilityDSLToken.ALWAYS) || this.check(AbilityDSLToken.NEVER)) {
1757
+ // // Checking that there are no extra tokens after the value
1758
+ // // (skip comments)
1759
+ // this.consumeLeadingComments();
1760
+ // const specOperator = this.consume();
1761
+ // // return new AbilityRule({
1762
+ // // subject: '',
1763
+ // // resource,
1764
+ // // condition,
1765
+ // // name: meta.name,
1766
+ // // });
1767
+ // }
1768
+ const isNeverAlways = this.check(AbilityDSLToken.ALWAYS) || this.check(AbilityDSLToken.NEVER);
1769
+ if (!isNeverAlways && !this.check(AbilityDSLToken.IDENTIFIER)) {
1770
+ this.syntaxError(`Expected identifier, but got ${this.peek().code}`, this.peek());
1722
1771
  }
1723
1772
  // Subject (e.g., "user.roles")
1724
- const subject = this.consume(AbilityDSLToken.IDENTIFIER, 'Expected field').value;
1773
+ const subject = isNeverAlways
1774
+ ? ''
1775
+ : this.consume(AbilityDSLToken.IDENTIFIER, 'Expected field').value;
1725
1776
  // Operator (e.g., "contains", "equals", "is not null")
1726
1777
  const { condition, operator } = this.parseConditionOperator();
1727
1778
  let resource;
@@ -1729,7 +1780,9 @@ class AbilityDSLParser {
1729
1780
  // Special operators that don't consume a value token.
1730
1781
  if (operator === AbilityDSLToken.EQ_NULL ||
1731
1782
  operator === AbilityDSLToken.NOT_EQ_NULL ||
1732
- operator === AbilityDSLToken.NULL) {
1783
+ operator === AbilityDSLToken.NULL ||
1784
+ operator === AbilityDSLToken.ALWAYS ||
1785
+ operator === AbilityDSLToken.NEVER) {
1733
1786
  resource = null;
1734
1787
  }
1735
1788
  else {
@@ -1761,6 +1814,16 @@ class AbilityDSLParser {
1761
1814
  */
1762
1815
  parseConditionOperator() {
1763
1816
  const savedPos = this.pos;
1817
+ // "always"
1818
+ if (this.matchWord('always')) {
1819
+ return { condition: AbilityCondition.always, operator: AbilityDSLToken.ALWAYS };
1820
+ }
1821
+ this.pos = savedPos;
1822
+ // "never"
1823
+ if (this.matchWord('never')) {
1824
+ return { condition: AbilityCondition.never, operator: AbilityDSLToken.NEVER };
1825
+ }
1826
+ this.pos = savedPos;
1764
1827
  // "length equals"
1765
1828
  if (this.matchWord('length') && this.matchWord('equals')) {
1766
1829
  return { condition: AbilityCondition.length_equals, operator: AbilityDSLToken.LEN_EQ };
@@ -1956,7 +2019,10 @@ class AbilityDSLParser {
1956
2019
  return false;
1957
2020
  }
1958
2021
  const token = this.peek();
1959
- if ((token.code === AbilityDSLToken.KEYWORD || token.code === AbilityDSLToken.IDENTIFIER) &&
2022
+ if ((token.code === AbilityDSLToken.KEYWORD ||
2023
+ token.code === AbilityDSLToken.IDENTIFIER ||
2024
+ token.code === AbilityDSLToken.ALWAYS ||
2025
+ token.code === AbilityDSLToken.NEVER) &&
1960
2026
  token.value === word) {
1961
2027
  this.advance();
1962
2028
  return true;
@@ -2101,9 +2167,6 @@ class AbilityDSLParser {
2101
2167
  }
2102
2168
  throw new AbilityDSLSyntaxError(token.line, token.column, context + '\n', finalDetails);
2103
2169
  }
2104
- getLine(lineNumber) {
2105
- return this.dsl.split(/\r?\n/)[lineNumber - 1] ?? '';
2106
- }
2107
2170
  suggest(actual, expectedTypes) {
2108
2171
  const candidates = [];
2109
2172
  for (const type of expectedTypes) {
@@ -72,7 +72,6 @@ export declare class AbilityDSLParser<Resource extends ResourceObject = Record<s
72
72
  private processCommentToken;
73
73
  private takeAnnotations;
74
74
  private syntaxError;
75
- private getLine;
76
75
  private suggest;
77
76
  private levenshteinDistance;
78
77
  private consumeOneOf;
@@ -1,5 +1,5 @@
1
1
  import AbilityCode from '../../core/AbilityCode';
2
- export type TokenType = 'EFFECT' | 'IF' | 'PERMISSION' | 'IDENTIFIER' | 'COLON' | 'COMMA' | 'DOT' | 'LBRACKET' | 'RBRACKET' | 'ALL' | 'ANY' | 'OF' | 'EOF' | 'COMMENT' | 'EQ' | 'CONTAINS' | 'IN' | 'NOT_IN' | 'NOT_CONTAINS' | 'GT' | 'GTE' | 'LT' | 'LTE' | 'NULL' | 'EQ_NULL' | 'NOT_EQ_NULL' | 'NOT_EQ' | 'LEN_GT' | 'LEN_LT' | 'LEN_EQ' | 'STRING' | 'NUMBER' | 'BOOLEAN' | 'SYMBOL' | 'KEYWORD' | 'UNKNOWN';
2
+ export type TokenType = 'EFFECT' | 'IF' | 'PERMISSION' | 'IDENTIFIER' | 'COLON' | 'COMMA' | 'DOT' | 'LBRACKET' | 'RBRACKET' | 'ALL' | 'ANY' | 'OF' | 'EOF' | 'COMMENT' | 'EQ' | 'CONTAINS' | 'IN' | 'NOT_IN' | 'NOT_CONTAINS' | 'GT' | 'GTE' | 'LT' | 'LTE' | 'NULL' | 'EQ_NULL' | 'NOT_EQ_NULL' | 'NOT_EQ' | 'LEN_GT' | 'LEN_LT' | 'LEN_EQ' | 'ALWAYS' | 'NEVER' | 'STRING' | 'NUMBER' | 'BOOLEAN' | 'SYMBOL' | 'KEYWORD' | 'UNKNOWN';
3
3
  /**
4
4
  * Represents a single token produced by the Ability DSL lexer.
5
5
  * Each token carries a type (e.g., EFFECT, IDENTIFIER, STRING) and its raw string value.
@@ -47,6 +47,8 @@ export declare class AbilityDSLToken<Code extends TokenType = TokenType> extends
47
47
  static readonly LEN_LT: TokenType;
48
48
  static readonly LEN_EQ: TokenType;
49
49
  static readonly NOT_EQ: TokenType;
50
+ static readonly ALWAYS: TokenType;
51
+ static readonly NEVER: TokenType;
50
52
  static readonly STRING: TokenType;
51
53
  static readonly NUMBER: TokenType;
52
54
  static readonly BOOLEAN: TokenType;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@via-profit/ability",
3
3
  "support": "https://via-profit.ru",
4
- "version": "3.3.0",
4
+ "version": "3.4.1",
5
5
  "description": "Via-Profit Ability service",
6
6
  "keywords": [
7
7
  "ability",
@@ -1,61 +0,0 @@
1
- import AbilityPolicy from './AbilityPolicy';
2
- export type Primitive = string | number | boolean | null | undefined;
3
- export type NestedDict<T = Primitive> = {
4
- [key: string]: NestedDict<T> | T;
5
- };
6
- export type ResourceObject = Record<string, unknown>;
7
- export type ResourcesMap = Record<string, ResourceObject>;
8
- export declare class AbilityParser {
9
- /**
10
- * Sets a value in a nested object structure based on a dot/bracket notation path.
11
- * @param object - The target object to modify.
12
- * @param path - The path to the property in dot/bracket notation.
13
- * @param value - The value to set at the specified path.
14
- */
15
- static setValueDotValue<T extends Primitive>(object: NestedDict<T>, path: string, value: T): void;
16
- /**
17
- * Generates TypeScript type definitions based on the provided policies.
18
- * @param policies - An array of AbilityPolicy instances.
19
- * @returns A generated type definitions.
20
- */
21
- static generateTypeDefs(policies: readonly AbilityPolicy[]): string;
22
- /**
23
- * Determines TypeScript type based on the rule
24
- * @param rule - The rule to analyze
25
- * @returns TypeScript type as string
26
- */
27
- private static determineTypeFromRule;
28
- /**
29
- * Gets TypeScript type for array values
30
- * @param resource - The resource value to analyze
31
- * @returns TypeScript array type as string
32
- */
33
- private static getArrayType;
34
- /**
35
- * Gets primitive TypeScript type for a value
36
- * @param value - The value to analyze
37
- * @returns TypeScript primitive type as string
38
- */
39
- private static getPrimitiveType;
40
- /**
41
- * Builds nested structure from flat paths
42
- * Example: 'user.profile.name' -> { user: { profile: { name: 'string' } } }
43
- * @param flatStructure - Flat structure with dot notation paths
44
- * @returns Nested object structure
45
- */
46
- private static buildNestedStructure;
47
- /**
48
- * Formats type structure into a string
49
- * @param structure - Nested type structure
50
- * @returns Formatted TypeScript type definition string
51
- */
52
- private static formatTypeDefinitions;
53
- /**
54
- * Recursively formats nested object
55
- * @param obj - Object to format
56
- * @param indent - Current indentation level
57
- * @returns Formatted string
58
- */
59
- private static formatNestedObject;
60
- }
61
- export default AbilityParser;