@@ -20,6 +20,7 @@ import {
20
20
isNullExpr ,
21
21
isThisExpr ,
22
22
} from '@zenstackhq/language/ast' ;
23
+ import { getContainerOfType } from 'langium' ;
23
24
import { P , match } from 'ts-pattern' ;
24
25
import { ExpressionContext } from './constants' ;
25
26
import { getEntityCheckerFunctionName } from './names' ;
@@ -40,6 +41,8 @@ type Options = {
40
41
operationContext ?: 'read' | 'create' | 'update' | 'postUpdate' | 'delete' ;
41
42
} ;
42
43
44
+ type Casing = 'original' | 'upper' | 'lower' | 'capitalize' | 'uncapitalize' ;
45
+
43
46
// a registry of function handlers marked with @func
44
47
const functionHandlers = new Map < string , PropertyDescriptor > ( ) ;
45
48
@@ -150,7 +153,7 @@ export class TypeScriptExpressionTransformer {
150
153
}
151
154
152
155
const args = expr . args . map ( ( arg ) => arg . value ) ;
153
- return handler . value . call ( this , args , normalizeUndefined ) ;
156
+ return handler . value . call ( this , expr , args , normalizeUndefined ) ;
154
157
}
155
158
156
159
// #region function invocation handlers
@@ -168,7 +171,7 @@ export class TypeScriptExpressionTransformer {
168
171
}
169
172
170
173
@func ( 'length' )
171
- private _length ( args : Expression [ ] ) {
174
+ private _length ( _invocation : InvocationExpr , args : Expression [ ] ) {
172
175
const field = this . transform ( args [ 0 ] , false ) ;
173
176
const min = getLiteral < number > ( args [ 1 ] ) ;
174
177
const max = getLiteral < number > ( args [ 2 ] ) ;
@@ -188,7 +191,7 @@ export class TypeScriptExpressionTransformer {
188
191
}
189
192
190
193
@func ( 'contains' )
191
- private _contains ( args : Expression [ ] , normalizeUndefined : boolean ) {
194
+ private _contains ( _invocation : InvocationExpr , args : Expression [ ] , normalizeUndefined : boolean ) {
192
195
const field = this . transform ( args [ 0 ] , false ) ;
193
196
const caseInsensitive = getLiteral < boolean > ( args [ 2 ] ) === true ;
194
197
let result : string ;
@@ -201,34 +204,34 @@ export class TypeScriptExpressionTransformer {
201
204
}
202
205
203
206
@func ( 'startsWith' )
204
- private _startsWith ( args : Expression [ ] , normalizeUndefined : boolean ) {
207
+ private _startsWith ( _invocation : InvocationExpr , args : Expression [ ] , normalizeUndefined : boolean ) {
205
208
const field = this . transform ( args [ 0 ] , false ) ;
206
209
const result = `${ field } ?.startsWith(${ this . transform ( args [ 1 ] , normalizeUndefined ) } )` ;
207
210
return this . ensureBoolean ( result ) ;
208
211
}
209
212
210
213
@func ( 'endsWith' )
211
- private _endsWith ( args : Expression [ ] , normalizeUndefined : boolean ) {
214
+ private _endsWith ( _invocation : InvocationExpr , args : Expression [ ] , normalizeUndefined : boolean ) {
212
215
const field = this . transform ( args [ 0 ] , false ) ;
213
216
const result = `${ field } ?.endsWith(${ this . transform ( args [ 1 ] , normalizeUndefined ) } )` ;
214
217
return this . ensureBoolean ( result ) ;
215
218
}
216
219
217
220
@func ( 'regex' )
218
- private _regex ( args : Expression [ ] ) {
221
+ private _regex ( _invocation : InvocationExpr , args : Expression [ ] ) {
219
222
const field = this . transform ( args [ 0 ] , false ) ;
220
223
const pattern = getLiteral < string > ( args [ 1 ] ) ;
221
224
return this . ensureBooleanTernary ( args [ 0 ] , field , `new RegExp(${ JSON . stringify ( pattern ) } ).test(${ field } )` ) ;
222
225
}
223
226
224
227
@func ( 'email' )
225
- private _email ( args : Expression [ ] ) {
228
+ private _email ( _invocation : InvocationExpr , args : Expression [ ] ) {
226
229
const field = this . transform ( args [ 0 ] , false ) ;
227
230
return this . ensureBooleanTernary ( args [ 0 ] , field , `z.string().email().safeParse(${ field } ).success` ) ;
228
231
}
229
232
230
233
@func ( 'datetime' )
231
- private _datetime ( args : Expression [ ] ) {
234
+ private _datetime ( _invocation : InvocationExpr , args : Expression [ ] ) {
232
235
const field = this . transform ( args [ 0 ] , false ) ;
233
236
return this . ensureBooleanTernary (
234
237
args [ 0 ] ,
@@ -238,20 +241,20 @@ export class TypeScriptExpressionTransformer {
238
241
}
239
242
240
243
@func ( 'url' )
241
- private _url ( args : Expression [ ] ) {
244
+ private _url ( _invocation : InvocationExpr , args : Expression [ ] ) {
242
245
const field = this . transform ( args [ 0 ] , false ) ;
243
246
return this . ensureBooleanTernary ( args [ 0 ] , field , `z.string().url().safeParse(${ field } ).success` ) ;
244
247
}
245
248
246
249
@func ( 'has' )
247
- private _has ( args : Expression [ ] , normalizeUndefined : boolean ) {
250
+ private _has ( _invocation : InvocationExpr , args : Expression [ ] , normalizeUndefined : boolean ) {
248
251
const field = this . transform ( args [ 0 ] , false ) ;
249
252
const result = `${ field } ?.includes(${ this . transform ( args [ 1 ] , normalizeUndefined ) } )` ;
250
253
return this . ensureBoolean ( result ) ;
251
254
}
252
255
253
256
@func ( 'hasEvery' )
254
- private _hasEvery ( args : Expression [ ] , normalizeUndefined : boolean ) {
257
+ private _hasEvery ( _invocation : InvocationExpr , args : Expression [ ] , normalizeUndefined : boolean ) {
255
258
const field = this . transform ( args [ 0 ] , false ) ;
256
259
return this . ensureBooleanTernary (
257
260
args [ 0 ] ,
@@ -261,7 +264,7 @@ export class TypeScriptExpressionTransformer {
261
264
}
262
265
263
266
@func ( 'hasSome' )
264
- private _hasSome ( args : Expression [ ] , normalizeUndefined : boolean ) {
267
+ private _hasSome ( _invocation : InvocationExpr , args : Expression [ ] , normalizeUndefined : boolean ) {
265
268
const field = this . transform ( args [ 0 ] , false ) ;
266
269
return this . ensureBooleanTernary (
267
270
args [ 0 ] ,
@@ -271,13 +274,13 @@ export class TypeScriptExpressionTransformer {
271
274
}
272
275
273
276
@func ( 'isEmpty' )
274
- private _isEmpty ( args : Expression [ ] ) {
277
+ private _isEmpty ( _invocation : InvocationExpr , args : Expression [ ] ) {
275
278
const field = this . transform ( args [ 0 ] , false ) ;
276
279
return `(!${ field } || ${ field } ?.length === 0)` ;
277
280
}
278
281
279
282
@func ( 'check' )
280
- private _check ( args : Expression [ ] ) {
283
+ private _check ( _invocation : InvocationExpr , args : Expression [ ] ) {
281
284
if ( ! isDataModelFieldReference ( args [ 0 ] ) ) {
282
285
throw new TypeScriptExpressionTransformerError ( `First argument of check() must be a field` ) ;
283
286
}
@@ -309,6 +312,52 @@ export class TypeScriptExpressionTransformer {
309
312
return `${ entityCheckerFunc } (input.${ fieldRef . target . $refText } , context)` ;
310
313
}
311
314
315
+ private toStringWithCaseChange ( value : string , casing : Casing ) {
316
+ if ( ! value ) {
317
+ return "''" ;
318
+ }
319
+ return match ( casing )
320
+ . with ( 'original' , ( ) => `'${ value } '` )
321
+ . with ( 'upper' , ( ) => `'${ value . toUpperCase ( ) } '` )
322
+ . with ( 'lower' , ( ) => `'${ value . toLowerCase ( ) } '` )
323
+ . with ( 'capitalize' , ( ) => `'${ value . charAt ( 0 ) . toUpperCase ( ) + value . slice ( 1 ) } '` )
324
+ . with ( 'uncapitalize' , ( ) => `'${ value . charAt ( 0 ) . toLowerCase ( ) + value . slice ( 1 ) } '` )
325
+ . exhaustive ( ) ;
326
+ }
327
+
328
+ @func ( 'currentModel' )
329
+ private _currentModel ( invocation : InvocationExpr , args : Expression [ ] ) {
330
+ let casing : Casing = 'original' ;
331
+ if ( args [ 0 ] ) {
332
+ casing = getLiteral < string > ( args [ 0 ] ) as Casing ;
333
+ }
334
+
335
+ const containingModel = getContainerOfType ( invocation , isDataModel ) ;
336
+ if ( ! containingModel ) {
337
+ throw new TypeScriptExpressionTransformerError ( 'currentModel() must be called inside a model' ) ;
338
+ }
339
+ return this . toStringWithCaseChange ( containingModel . name , casing ) ;
340
+ }
341
+
342
+ @func ( 'currentOperation' )
343
+ private _currentOperation ( _invocation : InvocationExpr , args : Expression [ ] ) {
344
+ let casing : Casing = 'original' ;
345
+ if ( args [ 0 ] ) {
346
+ casing = getLiteral < string > ( args [ 0 ] ) as Casing ;
347
+ }
348
+
349
+ if ( ! this . options . operationContext ) {
350
+ throw new TypeScriptExpressionTransformerError (
351
+ 'currentOperation() must be called inside an access policy rule'
352
+ ) ;
353
+ }
354
+ let contextOperation = this . options . operationContext ;
355
+ if ( contextOperation === 'postUpdate' ) {
356
+ contextOperation = 'update' ;
357
+ }
358
+ return this . toStringWithCaseChange ( contextOperation , casing ) ;
359
+ }
360
+
312
361
private ensureBoolean ( expr : string ) {
313
362
if ( this . options . context === ExpressionContext . ValidationRule ) {
314
363
// all fields are optional in a validation context, so we treat undefined
0 commit comments