@@ -524,16 +524,57 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
524524 private buildJsonFilter ( lhs : Expression < any > , payload : any ) : any {
525525 const clauses : Expression < SqlBool > [ ] = [ ] ;
526526 invariant ( payload && typeof payload === 'object' , 'Json filter payload must be an object' ) ;
527+
528+ const path = payload . path && Array . isArray ( payload . path ) ? payload . path : [ ] ;
529+ const receiver = this . buildJsonPathSelection ( lhs , path , 'json' ) ;
530+ const stringReceiver = this . buildJsonPathSelection ( lhs , path , 'string' ) ;
531+
532+ const mode = payload . mode ?? 'default' ;
533+ invariant ( mode === 'default' || mode === 'insensitive' , 'Invalid JSON filter mode' ) ;
534+
527535 for ( const [ key , value ] of Object . entries ( payload ) ) {
528536 switch ( key ) {
529537 case 'equals' : {
530- clauses . push ( this . buildJsonValueFilterClause ( lhs , value ) ) ;
538+ clauses . push ( this . buildJsonValueFilterClause ( receiver , value ) ) ;
531539 break ;
532540 }
533541 case 'not' : {
534- clauses . push ( this . eb . not ( this . buildJsonValueFilterClause ( lhs , value ) ) ) ;
542+ clauses . push ( this . eb . not ( this . buildJsonValueFilterClause ( receiver , value ) ) ) ;
543+ break ;
544+ }
545+ case 'string_contains' : {
546+ invariant ( typeof value === 'string' , 'string_contains value must be a string' ) ;
547+ clauses . push ( this . buildJsonStringFilter ( stringReceiver , key , value , mode ) ) ;
548+ break ;
549+ }
550+ case 'string_starts_with' : {
551+ invariant ( typeof value === 'string' , 'string_starts_with value must be a string' ) ;
552+ clauses . push ( this . buildJsonStringFilter ( stringReceiver , key , value , mode ) ) ;
553+ break ;
554+ }
555+ case 'string_ends_with' : {
556+ invariant ( typeof value === 'string' , 'string_ends_with value must be a string' ) ;
557+ clauses . push ( this . buildJsonStringFilter ( stringReceiver , key , value , mode ) ) ;
558+ break ;
559+ }
560+ case 'array_contains' : {
561+ clauses . push ( this . buildJsonArrayFilter ( receiver , key , value ) ) ;
562+ break ;
563+ }
564+ case 'array_starts_with' : {
565+ clauses . push ( this . buildJsonArrayFilter ( receiver , key , value ) ) ;
566+ break ;
567+ }
568+ case 'array_ends_with' : {
569+ clauses . push ( this . buildJsonArrayFilter ( receiver , key , value ) ) ;
535570 break ;
536571 }
572+ case 'path' :
573+ case 'mode' :
574+ // already handled
575+ break ;
576+ default :
577+ invariant ( false , `Invalid JSON filter key: ${ key } ` ) ;
537578 }
538579 }
539580 return this . and ( ...clauses ) ;
@@ -552,6 +593,24 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
552593 }
553594 }
554595
596+ private buildJsonStringFilter (
597+ lhs : Expression < any > ,
598+ operation : 'string_contains' | 'string_starts_with' | 'string_ends_with' ,
599+ value : string ,
600+ mode : 'default' | 'insensitive' ,
601+ ) {
602+ // build LIKE pattern based on operation
603+ const pattern = match ( operation )
604+ . with ( 'string_contains' , ( ) => `%${ value } %` )
605+ . with ( 'string_starts_with' , ( ) => `${ value } %` )
606+ . with ( 'string_ends_with' , ( ) => `%${ value } ` )
607+ . exhaustive ( ) ;
608+
609+ // use appropriate operator based on database capabilities
610+ const { supportsILike } = this . getStringCasingBehavior ( ) ;
611+ return this . eb ( lhs , mode === 'insensitive' && supportsILike ? 'ilike' : 'like' , sql . val ( pattern ) ) ;
612+ }
613+
555614 private buildLiteralFilter ( lhs : Expression < any > , type : BuiltinType , rhs : unknown ) {
556615 return this . eb ( lhs , '=' , rhs !== null && rhs !== undefined ? this . transformPrimitive ( rhs , type , false ) : rhs ) ;
557616 }
@@ -1245,5 +1304,24 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
12451304 */
12461305 abstract getStringCasingBehavior ( ) : { supportsILike : boolean ; likeCaseSensitive : boolean } ;
12471306
1307+ /**
1308+ * Builds a JSON path selection expression.
1309+ * @param asType 'string' | 'json', when 'string', the result is stripped with text quotes if it's a string
1310+ */
1311+ protected abstract buildJsonPathSelection (
1312+ receiver : Expression < any > ,
1313+ path : string [ ] ,
1314+ asType : 'string' | 'json' ,
1315+ ) : Expression < any > ;
1316+
1317+ /**
1318+ * Builds a JSON array filter expression.
1319+ */
1320+ protected abstract buildJsonArrayFilter (
1321+ lhs : Expression < any > ,
1322+ operation : 'array_contains' | 'array_starts_with' | 'array_ends_with' ,
1323+ value : unknown ,
1324+ ) : Expression < SqlBool > ;
1325+
12481326 // #endregion
12491327}
0 commit comments