11import { IStrings } from '../../../constants/strings' ;
2+ import { ParsedNode } from '../../../query-formats/sql/sql-token.types' ;
3+ import { getBuilderValidationMessage } from '../../../utils/validation/get-builder-validation-message.util' ;
24import { getValidationString } from '../../../utils/validation/get-validation-string.util' ;
35import { IBuilderFieldProps } from '../../types' ;
6+ import {
7+ resolveBuilderFieldUsageLimitKey ,
8+ resolveBuilderFieldUsageLimitScope ,
9+ } from '../../utils/resolve-builder-field-usage.util' ;
410import { ITextModeDiagnostic } from '../types/text-mode-diagnostic' ;
5- import { ParsedNode } from '../../../query-formats/sql/sql-token.types' ;
6- import { collectParsedSqlRules } from '../../../query-formats/sql/utils/collect-parsed-sql-rules' ;
711import { resolveFieldAllowedValues } from './resolve-field-allowed-values' ;
812
13+ interface IParsedRuleEntry {
14+ parentScopeId : string ;
15+ rule : Exclude < ParsedNode , { kind : 'group' } > ;
16+ }
17+
918const createFieldNotFoundDiagnostic = (
1019 fieldName : string ,
1120 start : number ,
@@ -56,15 +65,55 @@ const createOperatorNotAllowedDiagnostic = (
5665 end,
5766} ) ;
5867
68+ const createUsageLimitExceededDiagnostic = (
69+ field : IBuilderFieldProps ,
70+ start : number ,
71+ end : number ,
72+ strings : IStrings
73+ ) : ITextModeDiagnostic => ( {
74+ code : 'usage_limit_exceeded' ,
75+ message : getBuilderValidationMessage (
76+ field . usageLimit ?. message ,
77+ getValidationString (
78+ strings . validation ,
79+ 'usageLimitExceeded' ,
80+ `Field "${ field . field } " can appear at most ${ field . usageLimit ?. max } times in this scope` ,
81+ {
82+ field : field . label || field . field ,
83+ max : field . usageLimit ?. max ,
84+ }
85+ ) ,
86+ {
87+ field,
88+ usageLimit : field . usageLimit ,
89+ }
90+ ) ,
91+ start,
92+ end,
93+ } ) ;
94+
95+ const collectParsedRuleEntries = (
96+ nodes : ParsedNode [ ] ,
97+ parentScopeId = '__root__'
98+ ) : IParsedRuleEntry [ ] =>
99+ nodes . flatMap ( ( node , index ) => {
100+ if ( 'kind' in node ) {
101+ return collectParsedRuleEntries ( node . children , `${ parentScopeId } .${ index } ` ) ;
102+ }
103+
104+ return [ { rule : node , parentScopeId } ] ;
105+ } ) ;
106+
59107export const validateBuilderSqlTextSemantics = (
60108 nodes : ParsedNode [ ] ,
61109 fields : IBuilderFieldProps [ ] ,
62110 strings : IStrings
63111) : ITextModeDiagnostic [ ] => {
64112 const diagnostics : ITextModeDiagnostic [ ] = [ ] ;
65- const parsedRules = collectParsedSqlRules ( nodes ) ;
113+ const parsedRules = collectParsedRuleEntries ( nodes ) ;
114+ const usageCounts = new Map < string , number > ( ) ;
66115
67- parsedRules . forEach ( ( rule ) => {
116+ parsedRules . forEach ( ( { rule, parentScopeId } ) => {
68117 const field = fields . find ( ( fieldItem ) => fieldItem . field === rule . field ) ;
69118 const operator = rule . operator ;
70119
@@ -80,6 +129,27 @@ export const validateBuilderSqlTextSemantics = (
80129 return ;
81130 }
82131
132+ if ( field . usageLimit ) {
133+ const scope = resolveBuilderFieldUsageLimitScope ( field ) ;
134+ const usageBucketKey = `${ scope } :${ resolveBuilderFieldUsageLimitKey ( field ) } :${
135+ scope === 'parent' ? parentScopeId : 'all'
136+ } `;
137+ const nextUsageCount = ( usageCounts . get ( usageBucketKey ) || 0 ) + 1 ;
138+
139+ usageCounts . set ( usageBucketKey , nextUsageCount ) ;
140+
141+ if ( nextUsageCount > field . usageLimit . max ) {
142+ diagnostics . push (
143+ createUsageLimitExceededDiagnostic (
144+ field ,
145+ rule . source . field . start ,
146+ rule . source . field . end ,
147+ strings
148+ )
149+ ) ;
150+ }
151+ }
152+
83153 if (
84154 operator &&
85155 field . operators &&
@@ -121,9 +191,7 @@ export const validateBuilderSqlTextSemantics = (
121191 return ;
122192 }
123193
124- diagnostics . push (
125- createOneOfDiagnostic ( range . start , range . end , strings )
126- ) ;
194+ diagnostics . push ( createOneOfDiagnostic ( range . start , range . end , strings ) ) ;
127195 } ) ;
128196
129197 return ;
0 commit comments