5
5
DataModel ,
6
6
Expression ,
7
7
InvocationExpr ,
8
- isDataModel ,
9
- isEnumField ,
10
- isThisExpr ,
11
8
LiteralExpr ,
12
9
MemberAccessExpr ,
13
10
NullExpr ,
@@ -16,9 +13,15 @@ import {
16
13
StringLiteral ,
17
14
ThisExpr ,
18
15
UnaryExpr ,
16
+ isArrayExpr ,
17
+ isDataModel ,
18
+ isEnumField ,
19
+ isLiteralExpr ,
20
+ isNullExpr ,
21
+ isThisExpr ,
19
22
} from '@zenstackhq/language/ast' ;
20
23
import { ExpressionContext , getLiteral , isDataModelFieldReference , isFromStdlib , isFutureExpr } from '@zenstackhq/sdk' ;
21
- import { match , P } from 'ts-pattern' ;
24
+ import { P , match } from 'ts-pattern' ;
22
25
import { getIdFields } from './ast-utils' ;
23
26
24
27
export class TypeScriptExpressionTransformerError extends Error {
@@ -168,13 +171,17 @@ export class TypeScriptExpressionTransformer {
168
171
const max = getLiteral < number > ( args [ 2 ] ) ;
169
172
let result : string ;
170
173
if ( min === undefined ) {
171
- result = `( ${ field } ?.length > 0)` ;
174
+ result = this . ensureBooleanTernary ( args [ 0 ] , field , ` ${ field } ?.length > 0` ) ;
172
175
} else if ( max === undefined ) {
173
- result = `( ${ field } ?.length >= ${ min } )` ;
176
+ result = this . ensureBooleanTernary ( args [ 0 ] , field , ` ${ field } ?.length >= ${ min } ` ) ;
174
177
} else {
175
- result = `(${ field } ?.length >= ${ min } && ${ field } ?.length <= ${ max } )` ;
178
+ result = this . ensureBooleanTernary (
179
+ args [ 0 ] ,
180
+ field ,
181
+ `${ field } ?.length >= ${ min } && ${ field } ?.length <= ${ max } `
182
+ ) ;
176
183
}
177
- return this . ensureBoolean ( result ) ;
184
+ return result ;
178
185
}
179
186
180
187
@func ( 'contains' )
@@ -208,25 +215,29 @@ export class TypeScriptExpressionTransformer {
208
215
private _regex ( args : Expression [ ] ) {
209
216
const field = this . transform ( args [ 0 ] , false ) ;
210
217
const pattern = getLiteral < string > ( args [ 1 ] ) ;
211
- return `new RegExp(${ JSON . stringify ( pattern ) } ).test(${ field } )` ;
218
+ return this . ensureBooleanTernary ( args [ 0 ] , field , `new RegExp(${ JSON . stringify ( pattern ) } ).test(${ field } )` ) ;
212
219
}
213
220
214
221
@func ( 'email' )
215
222
private _email ( args : Expression [ ] ) {
216
223
const field = this . transform ( args [ 0 ] , false ) ;
217
- return `z.string().email().safeParse(${ field } ).success` ;
224
+ return this . ensureBooleanTernary ( args [ 0 ] , field , `z.string().email().safeParse(${ field } ).success` ) ;
218
225
}
219
226
220
227
@func ( 'datetime' )
221
228
private _datetime ( args : Expression [ ] ) {
222
229
const field = this . transform ( args [ 0 ] , false ) ;
223
- return `z.string().datetime({ offset: true }).safeParse(${ field } ).success` ;
230
+ return this . ensureBooleanTernary (
231
+ args [ 0 ] ,
232
+ field ,
233
+ `z.string().datetime({ offset: true }).safeParse(${ field } ).success`
234
+ ) ;
224
235
}
225
236
226
237
@func ( 'url' )
227
238
private _url ( args : Expression [ ] ) {
228
239
const field = this . transform ( args [ 0 ] , false ) ;
229
- return `z.string().url().safeParse(${ field } ).success` ;
240
+ return this . ensureBooleanTernary ( args [ 0 ] , field , `z.string().url().safeParse(${ field } ).success` ) ;
230
241
}
231
242
232
243
@func ( 'has' )
@@ -239,22 +250,27 @@ export class TypeScriptExpressionTransformer {
239
250
@func ( 'hasEvery' )
240
251
private _hasEvery ( args : Expression [ ] , normalizeUndefined : boolean ) {
241
252
const field = this . transform ( args [ 0 ] , false ) ;
242
- const result = `${ this . transform ( args [ 1 ] , normalizeUndefined ) } ?.every((item) => ${ field } ?.includes(item))` ;
243
- return this . ensureBoolean ( result ) ;
253
+ return this . ensureBooleanTernary (
254
+ args [ 0 ] ,
255
+ field ,
256
+ `${ this . transform ( args [ 1 ] , normalizeUndefined ) } ?.every((item) => ${ field } ?.includes(item))`
257
+ ) ;
244
258
}
245
259
246
260
@func ( 'hasSome' )
247
261
private _hasSome ( args : Expression [ ] , normalizeUndefined : boolean ) {
248
262
const field = this . transform ( args [ 0 ] , false ) ;
249
- const result = `${ this . transform ( args [ 1 ] , normalizeUndefined ) } ?.some((item) => ${ field } ?.includes(item))` ;
250
- return this . ensureBoolean ( result ) ;
263
+ return this . ensureBooleanTernary (
264
+ args [ 0 ] ,
265
+ field ,
266
+ `${ this . transform ( args [ 1 ] , normalizeUndefined ) } ?.some((item) => ${ field } ?.includes(item))`
267
+ ) ;
251
268
}
252
269
253
270
@func ( 'isEmpty' )
254
271
private _isEmpty ( args : Expression [ ] ) {
255
272
const field = this . transform ( args [ 0 ] , false ) ;
256
- const result = `(!${ field } || ${ field } ?.length === 0)` ;
257
- return this . ensureBoolean ( result ) ;
273
+ return `(!${ field } || ${ field } ?.length === 0)` ;
258
274
}
259
275
260
276
private ensureBoolean ( expr : string ) {
@@ -263,7 +279,22 @@ export class TypeScriptExpressionTransformer {
263
279
// as boolean true
264
280
return `(${ expr } ?? true)` ;
265
281
} else {
266
- return `(${ expr } ?? false)` ;
282
+ return `((${ expr } ) ?? false)` ;
283
+ }
284
+ }
285
+
286
+ private ensureBooleanTernary ( predicate : Expression , transformedPredicate : string , value : string ) {
287
+ if ( isLiteralExpr ( predicate ) || isArrayExpr ( predicate ) ) {
288
+ // these are never undefined
289
+ return value ;
290
+ }
291
+
292
+ if ( this . options . context === ExpressionContext . ValidationRule ) {
293
+ // all fields are optional in a validation context, so we treat undefined
294
+ // as boolean true
295
+ return `((${ transformedPredicate } ) !== undefined ? (${ value } ): true)` ;
296
+ } else {
297
+ return `((${ transformedPredicate } ) !== undefined ? (${ value } ): false)` ;
267
298
}
268
299
}
269
300
@@ -315,7 +346,7 @@ export class TypeScriptExpressionTransformer {
315
346
isDataModelFieldReference ( expr . operand )
316
347
) {
317
348
// in a validation context, we treat unary involving undefined as boolean true
318
- result = `( ${ operand } !== undefined ? ( ${ result } ): true)` ;
349
+ result = this . ensureBooleanTernary ( expr . operand , operand , result ) ;
319
350
}
320
351
return result ;
321
352
}
@@ -336,21 +367,45 @@ export class TypeScriptExpressionTransformer {
336
367
let _default = `(${ left } ${ expr . operator } ${ right } )` ;
337
368
338
369
if ( this . options . context === ExpressionContext . ValidationRule ) {
339
- // in a validation context, we treat binary involving undefined as boolean true
340
- if ( isDataModelFieldReference ( expr . left ) ) {
341
- _default = `(${ left } !== undefined ? (${ _default } ): true)` ;
342
- }
343
- if ( isDataModelFieldReference ( expr . right ) ) {
344
- _default = `(${ right } !== undefined ? (${ _default } ): true)` ;
370
+ const nullComparison = this . extractNullComparison ( expr ) ;
371
+ if ( nullComparison ) {
372
+ // null comparison covers both null and undefined
373
+ const { fieldRef } = nullComparison ;
374
+ const field = this . transform ( fieldRef , normalizeUndefined ) ;
375
+ if ( expr . operator === '==' ) {
376
+ _default = `(${ field } === null || ${ field } === undefined)` ;
377
+ } else if ( expr . operator === '!=' ) {
378
+ _default = `(${ field } !== null && ${ field } !== undefined)` ;
379
+ }
380
+ } else {
381
+ // for other comparisons, in a validation context,
382
+ // we treat binary involving undefined as boolean true
383
+ if ( isDataModelFieldReference ( expr . left ) ) {
384
+ _default = this . ensureBooleanTernary ( expr . left , left , _default ) ;
385
+ }
386
+ if ( isDataModelFieldReference ( expr . right ) ) {
387
+ _default = this . ensureBooleanTernary ( expr . right , right , _default ) ;
388
+ }
345
389
}
346
390
}
347
391
348
392
return match ( expr . operator )
349
- . with ( 'in' , ( ) =>
350
- this . ensureBoolean (
351
- `${ this . transform ( expr . right , false ) } ?.includes(${ this . transform ( expr . left , normalizeUndefined ) } )`
352
- )
353
- )
393
+ . with ( 'in' , ( ) => {
394
+ const left = `${ this . transform ( expr . left , normalizeUndefined ) } ` ;
395
+ const right = `${ this . transform ( expr . right , false ) } ` ;
396
+ let result = `${ right } ?.includes(${ left } )` ;
397
+ if ( this . options . context === ExpressionContext . ValidationRule ) {
398
+ // in a validation context, we treat binary involving undefined as boolean true
399
+ result = this . ensureBooleanTernary (
400
+ expr . left ,
401
+ left ,
402
+ this . ensureBooleanTernary ( expr . right , right , result )
403
+ ) ;
404
+ } else {
405
+ result = this . ensureBoolean ( result ) ;
406
+ }
407
+ return result ;
408
+ } )
354
409
. with ( P . union ( '==' , '!=' ) , ( ) => {
355
410
if ( isThisExpr ( expr . left ) || isThisExpr ( expr . right ) ) {
356
411
// map equality comparison with `this` to id comparison
@@ -376,6 +431,20 @@ export class TypeScriptExpressionTransformer {
376
431
. otherwise ( ( ) => _default ) ;
377
432
}
378
433
434
+ private extractNullComparison ( expr : BinaryExpr ) {
435
+ if ( expr . operator !== '==' && expr . operator !== '!=' ) {
436
+ return undefined ;
437
+ }
438
+
439
+ if ( isDataModelFieldReference ( expr . left ) && isNullExpr ( expr . right ) ) {
440
+ return { fieldRef : expr . left , nullExpr : expr . right } ;
441
+ } else if ( isDataModelFieldReference ( expr . right ) && isNullExpr ( expr . left ) ) {
442
+ return { fieldRef : expr . right , nullExpr : expr . left } ;
443
+ } else {
444
+ return undefined ;
445
+ }
446
+ }
447
+
379
448
private collectionPredicate ( expr : BinaryExpr , operator : '?' | '!' | '^' , normalizeUndefined : boolean ) {
380
449
const operand = this . transform ( expr . left , normalizeUndefined ) ;
381
450
const innerTransformer = new TypeScriptExpressionTransformer ( {
0 commit comments