@@ -38,6 +38,7 @@ import * as BuiltinFunctions from './functions';
3838import { SchemaDbPusher } from './helpers/schema-db-pusher' ;
3939import type { ClientOptions , ProceduresOptions } from './options' ;
4040import type { AnyPlugin } from './plugin' ;
41+ import { getField } from './query-utils' ;
4142import { createZenStackPromise , type ZenStackPromise } from './promise' ;
4243import { ResultProcessor } from './result-processor' ;
4344
@@ -547,6 +548,11 @@ function createModelCrudHandler(
547548 inputValidator : InputValidator < any > ,
548549 resultProcessor : ResultProcessor < any > ,
549550) : ModelOperations < any , any > {
551+ // check if any plugin defines ext result fields
552+ const plugins = client . $options . plugins ?? [ ] ;
553+ const schema = client . $schema ;
554+ const hasAnyExtResult = hasExtResultDefs ( plugins ) ;
555+
550556 const createPromise = (
551557 operation : CoreCrudOperations ,
552558 nominalOperation : AllCrudOperations ,
@@ -557,17 +563,30 @@ function createModelCrudHandler(
557563 ) => {
558564 return createZenStackPromise ( async ( txClient ?: ClientContract < any > ) => {
559565 let proceed = async ( _args : unknown ) => {
566+ // prepare args for ext result: strip ext result field names from select/omit,
567+ // inject needs fields into select (recursively handles nested relations)
568+ const processedArgs =
569+ postProcess && hasAnyExtResult
570+ ? prepareArgsForExtResult ( _args , model , schema , plugins )
571+ : _args ;
572+
560573 const _handler = txClient ? handler . withClient ( txClient ) : handler ;
561- const r = await _handler . handle ( operation , _args ) ;
574+ const r = await _handler . handle ( operation , processedArgs ) ;
562575 if ( ! r && throwIfNoResult ) {
563576 throw createNotFoundError ( model ) ;
564577 }
565578 let result : unknown ;
566579 if ( r && postProcess ) {
567- result = resultProcessor . processResult ( r , model , args ) ;
580+ result = resultProcessor . processResult ( r , model , processedArgs ) ;
568581 } else {
569582 result = r ?? null ;
570583 }
584+
585+ // compute ext result fields (recursively handles nested relations)
586+ if ( result && postProcess && hasAnyExtResult ) {
587+ result = applyExtResult ( result , model , _args , schema , plugins ) ;
588+ }
589+
571590 return result ;
572591 } ;
573592
@@ -823,3 +842,247 @@ function createModelCrudHandler(
823842
824843 return operations as ModelOperations < any , any > ;
825844}
845+
846+ // #region Extended result field helpers
847+
848+ type ExtResultDef = { needs : Record < string , true > ; compute : ( data : any ) => unknown } ;
849+
850+ /**
851+ * Returns true if any plugin defines ext result fields for any model.
852+ */
853+ function hasExtResultDefs ( plugins : AnyPlugin [ ] ) : boolean {
854+ return plugins . some ( ( p ) => p . result && Object . keys ( p . result ) . length > 0 ) ;
855+ }
856+
857+ /**
858+ * Collects extended result field definitions from all plugins for a given model.
859+ */
860+ function collectExtResultDefs ( model : string , plugins : AnyPlugin [ ] ) : Map < string , ExtResultDef > {
861+ const defs = new Map < string , ExtResultDef > ( ) ;
862+ for ( const plugin of plugins ) {
863+ const resultConfig = plugin . result ;
864+ if ( resultConfig ) {
865+ const modelConfig = resultConfig [ model ] ;
866+ if ( modelConfig ) {
867+ for ( const [ fieldName , fieldDef ] of Object . entries ( modelConfig ) ) {
868+ defs . set ( fieldName , fieldDef as ExtResultDef ) ;
869+ }
870+ }
871+ }
872+ }
873+ return defs ;
874+ }
875+
876+ /**
877+ * Prepares query args for extended result fields (recursive):
878+ * - Strips ext result field names from `select` and `omit`
879+ * - Injects `needs` fields into `select` when ext result fields are explicitly selected
880+ * - Recurses into `include` and `select` for nested relation fields
881+ */
882+ function prepareArgsForExtResult (
883+ args : unknown ,
884+ model : string ,
885+ schema : SchemaDef ,
886+ plugins : AnyPlugin [ ] ,
887+ ) : unknown {
888+ if ( ! args || typeof args !== 'object' ) {
889+ return args ;
890+ }
891+
892+ const extResultDefs = collectExtResultDefs ( model , plugins ) ;
893+ const typedArgs = args as Record < string , unknown > ;
894+ let result = typedArgs ;
895+ let changed = false ;
896+
897+ const select = typedArgs [ 'select' ] as Record < string , unknown > | undefined ;
898+ const omit = typedArgs [ 'omit' ] as Record < string , unknown > | undefined ;
899+ const include = typedArgs [ 'include' ] as Record < string , unknown > | undefined ;
900+
901+ if ( select && extResultDefs . size > 0 ) {
902+ const newSelect = { ...select } ;
903+ for ( const [ fieldName , fieldDef ] of extResultDefs ) {
904+ if ( newSelect [ fieldName ] ) {
905+ delete newSelect [ fieldName ] ;
906+ // inject needs fields
907+ for ( const needField of Object . keys ( fieldDef . needs ) ) {
908+ if ( ! newSelect [ needField ] ) {
909+ newSelect [ needField ] = true ;
910+ }
911+ }
912+ }
913+ }
914+ result = { ...result , select : newSelect } ;
915+ changed = true ;
916+ }
917+
918+ if ( omit && extResultDefs . size > 0 ) {
919+ const newOmit = { ...omit } ;
920+ for ( const fieldName of extResultDefs . keys ( ) ) {
921+ if ( newOmit [ fieldName ] ) {
922+ delete newOmit [ fieldName ] ;
923+ }
924+ }
925+ result = { ...result , omit : newOmit } ;
926+ changed = true ;
927+ }
928+
929+ // Recurse into nested relations in `include`
930+ if ( include ) {
931+ const newInclude = { ...include } ;
932+ let includeChanged = false ;
933+ for ( const [ field , value ] of Object . entries ( newInclude ) ) {
934+ if ( value && typeof value === 'object' ) {
935+ const fieldDef = getField ( schema , model , field ) ;
936+ if ( fieldDef ?. relation ) {
937+ const targetModel = fieldDef . type ;
938+ const processed = prepareArgsForExtResult ( value , targetModel , schema , plugins ) ;
939+ if ( processed !== value ) {
940+ newInclude [ field ] = processed ;
941+ includeChanged = true ;
942+ }
943+ }
944+ }
945+ }
946+ if ( includeChanged ) {
947+ result = changed ? { ...result , include : newInclude } : { ...typedArgs , include : newInclude } ;
948+ changed = true ;
949+ }
950+ }
951+
952+ // Recurse into nested relations in `select` (relation fields can have nested args)
953+ if ( select ) {
954+ const currentSelect = ( changed ? ( result as Record < string , unknown > ) [ 'select' ] : select ) as
955+ | Record < string , unknown >
956+ | undefined ;
957+ if ( currentSelect ) {
958+ const newSelect = { ...currentSelect } ;
959+ let selectChanged = false ;
960+ for ( const [ field , value ] of Object . entries ( newSelect ) ) {
961+ if ( value && typeof value === 'object' ) {
962+ const fieldDef = getField ( schema , model , field ) ;
963+ if ( fieldDef ?. relation ) {
964+ const targetModel = fieldDef . type ;
965+ const processed = prepareArgsForExtResult ( value , targetModel , schema , plugins ) ;
966+ if ( processed !== value ) {
967+ newSelect [ field ] = processed ;
968+ selectChanged = true ;
969+ }
970+ }
971+ }
972+ }
973+ if ( selectChanged ) {
974+ result = { ...result , select : newSelect } ;
975+ changed = true ;
976+ }
977+ }
978+ }
979+
980+ return changed ? result : args ;
981+ }
982+
983+ /**
984+ * Applies extended result field computation to query results (recursive).
985+ * Processes the current model's ext result fields, then recurses into nested relation data.
986+ */
987+ function applyExtResult (
988+ result : unknown ,
989+ model : string ,
990+ originalArgs : unknown ,
991+ schema : SchemaDef ,
992+ plugins : AnyPlugin [ ] ,
993+ ) : unknown {
994+ if ( Array . isArray ( result ) ) {
995+ for ( let i = 0 ; i < result . length ; i ++ ) {
996+ result [ i ] = applyExtResultToRow ( result [ i ] , model , originalArgs , schema , plugins ) ;
997+ }
998+ return result ;
999+ } else {
1000+ return applyExtResultToRow ( result , model , originalArgs , schema , plugins ) ;
1001+ }
1002+ }
1003+
1004+ function applyExtResultToRow (
1005+ row : unknown ,
1006+ model : string ,
1007+ originalArgs : unknown ,
1008+ schema : SchemaDef ,
1009+ plugins : AnyPlugin [ ] ,
1010+ ) : unknown {
1011+ if ( ! row || typeof row !== 'object' ) {
1012+ return row ;
1013+ }
1014+
1015+ const data = row as Record < string , unknown > ;
1016+ const extResultDefs = collectExtResultDefs ( model , plugins ) ;
1017+ const typedArgs = ( originalArgs && typeof originalArgs === 'object' ? originalArgs : { } ) as Record < string , unknown > ;
1018+ const select = typedArgs [ 'select' ] as Record < string , unknown > | undefined ;
1019+ const omit = typedArgs [ 'omit' ] as Record < string , unknown > | undefined ;
1020+ const include = typedArgs [ 'include' ] as Record < string , unknown > | undefined ;
1021+
1022+ // Determine which ext result fields were selected/omitted at this level
1023+ const selectedExtResultFields = select ? new Set < string > ( ) : undefined ;
1024+ const omittedExtResultFields = omit ? new Set < string > ( ) : undefined ;
1025+ const injectedNeedsFields = new Set < string > ( ) ;
1026+
1027+ if ( select && extResultDefs . size > 0 ) {
1028+ for ( const [ fieldName , fieldDef ] of extResultDefs ) {
1029+ if ( select [ fieldName ] ) {
1030+ selectedExtResultFields ! . add ( fieldName ) ;
1031+ // Track injected needs: fields that were NOT in the original select
1032+ for ( const needField of Object . keys ( fieldDef . needs ) ) {
1033+ if ( ! select [ needField ] ) {
1034+ injectedNeedsFields . add ( needField ) ;
1035+ }
1036+ }
1037+ }
1038+ }
1039+ }
1040+
1041+ if ( omit && extResultDefs . size > 0 ) {
1042+ for ( const fieldName of extResultDefs . keys ( ) ) {
1043+ if ( omit [ fieldName ] ) {
1044+ omittedExtResultFields ! . add ( fieldName ) ;
1045+ }
1046+ }
1047+ }
1048+
1049+ // Compute ext result fields for the current model
1050+ for ( const [ fieldName , fieldDef ] of extResultDefs ) {
1051+ if ( omittedExtResultFields ?. has ( fieldName ) ) {
1052+ continue ;
1053+ }
1054+ if ( selectedExtResultFields !== undefined && ! selectedExtResultFields . has ( fieldName ) ) {
1055+ continue ;
1056+ }
1057+ const needsSatisfied = Object . keys ( fieldDef . needs ) . every ( ( needField ) => needField in data ) ;
1058+ if ( needsSatisfied ) {
1059+ data [ fieldName ] = fieldDef . compute ( data ) ;
1060+ }
1061+ }
1062+
1063+ // Strip injected needs fields that weren't originally requested
1064+ for ( const field of injectedNeedsFields ) {
1065+ delete data [ field ] ;
1066+ }
1067+
1068+ // Recurse into nested relation data
1069+ const relationSource = include ?? select ;
1070+ if ( relationSource ) {
1071+ for ( const [ field , value ] of Object . entries ( relationSource ) ) {
1072+ if ( data [ field ] == null ) {
1073+ continue ;
1074+ }
1075+ const fieldDef = getField ( schema , model , field ) ;
1076+ if ( ! fieldDef ?. relation ) {
1077+ continue ;
1078+ }
1079+ const targetModel = fieldDef . type ;
1080+ const nestedArgs = value && typeof value === 'object' ? value : undefined ;
1081+ data [ field ] = applyExtResult ( data [ field ] , targetModel , nestedArgs , schema , plugins ) ;
1082+ }
1083+ }
1084+
1085+ return data ;
1086+ }
1087+
1088+ // #endregion
0 commit comments