7
7
InvocationExpr ,
8
8
isDataModel ,
9
9
isEnumField ,
10
+ isNullExpr ,
10
11
isThisExpr ,
11
12
LiteralExpr ,
12
13
MemberAccessExpr ,
@@ -168,13 +169,13 @@ export class TypeScriptExpressionTransformer {
168
169
const max = getLiteral < number > ( args [ 2 ] ) ;
169
170
let result : string ;
170
171
if ( min === undefined ) {
171
- result = `( ${ field } ?.length > 0)` ;
172
+ result = this . ensureBooleanTernary ( field , ` ${ field } ?.length > 0` ) ;
172
173
} else if ( max === undefined ) {
173
- result = `( ${ field } ?.length >= ${ min } )` ;
174
+ result = this . ensureBooleanTernary ( field , ` ${ field } ?.length >= ${ min } ` ) ;
174
175
} else {
175
- result = `( ${ field } ?.length >= ${ min } && ${ field } ?.length <= ${ max } )` ;
176
+ result = this . ensureBooleanTernary ( field , ` ${ field } ?.length >= ${ min } && ${ field } ?.length <= ${ max } ` ) ;
176
177
}
177
- return this . ensureBoolean ( result ) ;
178
+ return result ;
178
179
}
179
180
180
181
@func ( 'contains' )
@@ -208,25 +209,25 @@ export class TypeScriptExpressionTransformer {
208
209
private _regex ( args : Expression [ ] ) {
209
210
const field = this . transform ( args [ 0 ] , false ) ;
210
211
const pattern = getLiteral < string > ( args [ 1 ] ) ;
211
- return ` new RegExp(${ JSON . stringify ( pattern ) } ).test( ${ field } )` ;
212
+ return this . ensureBoolean ( ` ${ field } ?.match( new RegExp(${ JSON . stringify ( pattern ) } ))` ) ;
212
213
}
213
214
214
215
@func ( 'email' )
215
216
private _email ( args : Expression [ ] ) {
216
217
const field = this . transform ( args [ 0 ] , false ) ;
217
- return `z.string().email().safeParse(${ field } ).success` ;
218
+ return this . ensureBooleanTernary ( field , `z.string().email().safeParse(${ field } ).success` ) ;
218
219
}
219
220
220
221
@func ( 'datetime' )
221
222
private _datetime ( args : Expression [ ] ) {
222
223
const field = this . transform ( args [ 0 ] , false ) ;
223
- return `z.string().datetime({ offset: true }).safeParse(${ field } ).success` ;
224
+ return this . ensureBooleanTernary ( field , `z.string().datetime({ offset: true }).safeParse(${ field } ).success` ) ;
224
225
}
225
226
226
227
@func ( 'url' )
227
228
private _url ( args : Expression [ ] ) {
228
229
const field = this . transform ( args [ 0 ] , false ) ;
229
- return `z.string().url().safeParse(${ field } ).success` ;
230
+ return this . ensureBooleanTernary ( field , `z.string().url().safeParse(${ field } ).success` ) ;
230
231
}
231
232
232
233
@func ( 'has' )
@@ -239,22 +240,25 @@ export class TypeScriptExpressionTransformer {
239
240
@func ( 'hasEvery' )
240
241
private _hasEvery ( args : Expression [ ] , normalizeUndefined : boolean ) {
241
242
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 ) ;
243
+ return this . ensureBooleanTernary (
244
+ field ,
245
+ `${ this . transform ( args [ 1 ] , normalizeUndefined ) } ?.every((item) => ${ field } ?.includes(item))`
246
+ ) ;
244
247
}
245
248
246
249
@func ( 'hasSome' )
247
250
private _hasSome ( args : Expression [ ] , normalizeUndefined : boolean ) {
248
251
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 ) ;
252
+ return this . ensureBooleanTernary (
253
+ field ,
254
+ `${ this . transform ( args [ 1 ] , normalizeUndefined ) } ?.some((item) => ${ field } ?.includes(item))`
255
+ ) ;
251
256
}
252
257
253
258
@func ( 'isEmpty' )
254
259
private _isEmpty ( args : Expression [ ] ) {
255
260
const field = this . transform ( args [ 0 ] , false ) ;
256
- const result = `(!${ field } || ${ field } ?.length === 0)` ;
257
- return this . ensureBoolean ( result ) ;
261
+ return `(!${ field } || ${ field } ?.length === 0)` ;
258
262
}
259
263
260
264
private ensureBoolean ( expr : string ) {
@@ -263,7 +267,17 @@ export class TypeScriptExpressionTransformer {
263
267
// as boolean true
264
268
return `(${ expr } ?? true)` ;
265
269
} else {
266
- return `(${ expr } ?? false)` ;
270
+ return `((${ expr } ) ?? false)` ;
271
+ }
272
+ }
273
+
274
+ private ensureBooleanTernary ( predicate : string , value : string ) {
275
+ if ( this . options . context === ExpressionContext . ValidationRule ) {
276
+ // all fields are optional in a validation context, so we treat undefined
277
+ // as boolean true
278
+ return `((${ predicate } ) !== undefined ? (${ value } ): true)` ;
279
+ } else {
280
+ return `((${ predicate } ) !== undefined ? (${ value } ): false)` ;
267
281
}
268
282
}
269
283
@@ -315,7 +329,7 @@ export class TypeScriptExpressionTransformer {
315
329
isDataModelFieldReference ( expr . operand )
316
330
) {
317
331
// in a validation context, we treat unary involving undefined as boolean true
318
- result = `( ${ operand } !== undefined ? ( ${ result } ): true)` ;
332
+ result = this . ensureBooleanTernary ( operand , result ) ;
319
333
}
320
334
return result ;
321
335
}
@@ -336,21 +350,39 @@ export class TypeScriptExpressionTransformer {
336
350
let _default = `(${ left } ${ expr . operator } ${ right } )` ;
337
351
338
352
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)` ;
353
+ const nullComparison = this . extractNullComparison ( expr ) ;
354
+ if ( nullComparison ) {
355
+ // null comparison covers both null and undefined
356
+ const { fieldRef } = nullComparison ;
357
+ const field = this . transform ( fieldRef , normalizeUndefined ) ;
358
+ if ( expr . operator === '==' ) {
359
+ _default = `(${ field } === null || ${ field } === undefined)` ;
360
+ } else if ( expr . operator === '!=' ) {
361
+ _default = `(${ field } !== null && ${ field } !== undefined)` ;
362
+ }
363
+ } else {
364
+ // for other comparisons, in a validation context,
365
+ // we treat binary involving undefined as boolean true
366
+ if ( isDataModelFieldReference ( expr . left ) ) {
367
+ _default = this . ensureBooleanTernary ( left , _default ) ;
368
+ }
369
+ if ( isDataModelFieldReference ( expr . right ) ) {
370
+ _default = this . ensureBooleanTernary ( right , _default ) ;
371
+ }
345
372
}
346
373
}
347
374
348
375
return match ( expr . operator )
349
- . with ( 'in' , ( ) =>
350
- this . ensureBoolean (
351
- `${ this . transform ( expr . right , false ) } ?.includes(${ this . transform ( expr . left , normalizeUndefined ) } )`
352
- )
353
- )
376
+ . with ( 'in' , ( ) => {
377
+ const left = `${ this . transform ( expr . left , normalizeUndefined ) } ` ;
378
+ let result = this . ensureBoolean ( `${ this . transform ( expr . right , false ) } ?.includes(${ left } )` ) ;
379
+
380
+ if ( this . options . context === ExpressionContext . ValidationRule ) {
381
+ // in a validation context, we treat binary involving undefined as boolean true
382
+ result = this . ensureBooleanTernary ( left , result ) ;
383
+ }
384
+ return result ;
385
+ } )
354
386
. with ( P . union ( '==' , '!=' ) , ( ) => {
355
387
if ( isThisExpr ( expr . left ) || isThisExpr ( expr . right ) ) {
356
388
// map equality comparison with `this` to id comparison
@@ -376,6 +408,20 @@ export class TypeScriptExpressionTransformer {
376
408
. otherwise ( ( ) => _default ) ;
377
409
}
378
410
411
+ private extractNullComparison ( expr : BinaryExpr ) {
412
+ if ( expr . operator !== '==' && expr . operator !== '!=' ) {
413
+ return undefined ;
414
+ }
415
+
416
+ if ( isDataModelFieldReference ( expr . left ) && isNullExpr ( expr . right ) ) {
417
+ return { fieldRef : expr . left , nullExpr : expr . right } ;
418
+ } else if ( isDataModelFieldReference ( expr . right ) && isNullExpr ( expr . left ) ) {
419
+ return { fieldRef : expr . right , nullExpr : expr . left } ;
420
+ } else {
421
+ return undefined ;
422
+ }
423
+ }
424
+
379
425
private collectionPredicate ( expr : BinaryExpr , operator : '?' | '!' | '^' , normalizeUndefined : boolean ) {
380
426
const operand = this . transform ( expr . left , normalizeUndefined ) ;
381
427
const innerTransformer = new TypeScriptExpressionTransformer ( {
0 commit comments