@@ -41,6 +41,12 @@ import * as BuiltinFunctions from './functions';
4141import { SchemaDbPusher } from './helpers/schema-db-pusher' ;
4242import type { ClientOptions , ProceduresOptions } from './options' ;
4343import type { AnyPlugin } from './plugin' ;
44+
45+ type ExtResultFieldDef = {
46+ needs : Record < string , true > ;
47+ compute : ( data : Record < string , any > ) => unknown ;
48+ } ;
49+ import { getField } from './query-utils' ;
4450import { createZenStackPromise , type ZenStackPromise } from './promise' ;
4551import { ResultProcessor } from './result-processor' ;
4652
@@ -582,6 +588,11 @@ function createModelCrudHandler(
582588 inputValidator : InputValidator < any > ,
583589 resultProcessor : ResultProcessor < any > ,
584590) : ModelOperations < any , any > {
591+ // check if any plugin defines ext result fields
592+ const plugins = client . $options . plugins ?? [ ] ;
593+ const schema = client . $schema ;
594+ const hasAnyExtResult = hasExtResultFieldDefs ( plugins ) ;
595+
585596 const createPromise = (
586597 operation : CoreCrudOperations ,
587598 nominalOperation : AllCrudOperations ,
@@ -592,17 +603,30 @@ function createModelCrudHandler(
592603 ) => {
593604 return createZenStackPromise ( async ( txClient ?: ClientContract < any > ) => {
594605 let proceed = async ( _args : unknown ) => {
606+ // prepare args for ext result: strip ext result field names from select/omit,
607+ // inject needs fields into select (recursively handles nested relations)
608+ const shouldApplyExtResult = hasAnyExtResult && EXT_RESULT_OPERATIONS . has ( operation ) ;
609+ const processedArgs = shouldApplyExtResult
610+ ? prepareArgsForExtResult ( _args , model , schema , plugins )
611+ : _args ;
612+
595613 const _handler = txClient ? handler . withClient ( txClient ) : handler ;
596- const r = await _handler . handle ( operation , _args ) ;
614+ const r = await _handler . handle ( operation , processedArgs ) ;
597615 if ( ! r && throwIfNoResult ) {
598616 throw createNotFoundError ( model ) ;
599617 }
600618 let result : unknown ;
601619 if ( r && postProcess ) {
602- result = resultProcessor . processResult ( r , model , args ) ;
620+ result = resultProcessor . processResult ( r , model , processedArgs ) ;
603621 } else {
604622 result = r ?? null ;
605623 }
624+
625+ // compute ext result fields (recursively handles nested relations)
626+ if ( result && shouldApplyExtResult ) {
627+ result = applyExtResult ( result , model , _args , schema , plugins ) ;
628+ }
629+
606630 return result ;
607631 } ;
608632
@@ -858,3 +882,269 @@ function createModelCrudHandler(
858882
859883 return operations as ModelOperations < any , any > ;
860884}
885+
886+ // #region Extended result field helpers
887+
888+ // operations that return model rows and should have ext result fields applied
889+ const EXT_RESULT_OPERATIONS = new Set < CoreCrudOperations > ( [
890+ 'findMany' ,
891+ 'findUnique' ,
892+ 'findFirst' ,
893+ 'create' ,
894+ 'createManyAndReturn' ,
895+ 'update' ,
896+ 'updateManyAndReturn' ,
897+ 'upsert' ,
898+ 'delete' ,
899+ ] ) ;
900+
901+ /**
902+ * Returns true if any plugin defines ext result fields for any model.
903+ */
904+ function hasExtResultFieldDefs ( plugins : AnyPlugin [ ] ) : boolean {
905+ return plugins . some ( ( p ) => p . result && Object . keys ( p . result ) . length > 0 ) ;
906+ }
907+
908+ /**
909+ * Collects extended result field definitions from all plugins for a given model.
910+ */
911+ function collectExtResultFieldDefs (
912+ model : string ,
913+ schema : SchemaDef ,
914+ plugins : AnyPlugin [ ] ,
915+ ) : Map < string , ExtResultFieldDef > {
916+ const defs = new Map < string , ExtResultFieldDef > ( ) ;
917+ for ( const plugin of plugins ) {
918+ const resultConfig = plugin . result ;
919+ if ( resultConfig ) {
920+ const modelConfig = resultConfig [ lowerCaseFirst ( model ) ] ;
921+ if ( modelConfig ) {
922+ for ( const [ fieldName , fieldDef ] of Object . entries ( modelConfig ) ) {
923+ if ( getField ( schema , model , fieldName ) ) {
924+ throw new Error (
925+ `Plugin "${ plugin . id } " registers ext result field "${ fieldName } " on model "${ model } " which conflicts with an existing model field` ,
926+ ) ;
927+ }
928+ for ( const needField of Object . keys ( ( fieldDef as ExtResultFieldDef ) . needs ?? { } ) ) {
929+ const needDef = getField ( schema , model , needField ) ;
930+ if ( ! needDef || needDef . relation ) {
931+ throw new Error (
932+ `Plugin "${ plugin . id } " registers ext result field "${ fieldName } " on model "${ model } " with invalid need "${ needField } "` ,
933+ ) ;
934+ }
935+ }
936+ defs . set ( fieldName , fieldDef as ExtResultFieldDef ) ;
937+ }
938+ }
939+ }
940+ }
941+ return defs ;
942+ }
943+
944+ /**
945+ * Prepares query args for extended result fields (recursive):
946+ * - Strips ext result field names from `select` and `omit`
947+ * - Injects `needs` fields into `select` when ext result fields are explicitly selected
948+ * - Recurses into `include` and `select` for nested relation fields
949+ */
950+ function prepareArgsForExtResult (
951+ args : unknown ,
952+ model : string ,
953+ schema : SchemaDef ,
954+ plugins : AnyPlugin [ ] ,
955+ ) : unknown {
956+ if ( ! args || typeof args !== 'object' ) {
957+ return args ;
958+ }
959+
960+ const extResultDefs = collectExtResultFieldDefs ( model , schema , plugins ) ;
961+ const typedArgs = args as Record < string , unknown > ;
962+ let result = typedArgs ;
963+ let changed = false ;
964+
965+ const select = typedArgs [ 'select' ] as Record < string , unknown > | undefined ;
966+ const omit = typedArgs [ 'omit' ] as Record < string , unknown > | undefined ;
967+ const include = typedArgs [ 'include' ] as Record < string , unknown > | undefined ;
968+
969+ if ( select && extResultDefs . size > 0 ) {
970+ const newSelect = { ...select } ;
971+ for ( const [ fieldName , fieldDef ] of extResultDefs ) {
972+ if ( newSelect [ fieldName ] ) {
973+ delete newSelect [ fieldName ] ;
974+ // inject needs fields
975+ for ( const needField of Object . keys ( fieldDef . needs ) ) {
976+ if ( ! newSelect [ needField ] ) {
977+ newSelect [ needField ] = true ;
978+ }
979+ }
980+ }
981+ }
982+ result = { ...result , select : newSelect } ;
983+ changed = true ;
984+ }
985+
986+ if ( omit && extResultDefs . size > 0 ) {
987+ const newOmit = { ...omit } ;
988+ for ( const [ fieldName , fieldDef ] of extResultDefs ) {
989+ if ( newOmit [ fieldName ] ) {
990+ // strip ext result field names from omit (they don't exist in the DB)
991+ delete newOmit [ fieldName ] ;
992+ } else {
993+ // this ext result field is active — ensure its needs are not omitted
994+ for ( const needField of Object . keys ( fieldDef . needs ) ) {
995+ if ( newOmit [ needField ] ) {
996+ delete newOmit [ needField ] ;
997+ }
998+ }
999+ }
1000+ }
1001+ result = { ...result , omit : newOmit } ;
1002+ changed = true ;
1003+ }
1004+
1005+ // Recurse into nested relations in `include`
1006+ if ( include ) {
1007+ const newInclude = { ...include } ;
1008+ let includeChanged = false ;
1009+ for ( const [ field , value ] of Object . entries ( newInclude ) ) {
1010+ if ( value && typeof value === 'object' ) {
1011+ const fieldDef = getField ( schema , model , field ) ;
1012+ if ( fieldDef ?. relation ) {
1013+ const targetModel = fieldDef . type ;
1014+ const processed = prepareArgsForExtResult ( value , targetModel , schema , plugins ) ;
1015+ if ( processed !== value ) {
1016+ newInclude [ field ] = processed ;
1017+ includeChanged = true ;
1018+ }
1019+ }
1020+ }
1021+ }
1022+ if ( includeChanged ) {
1023+ result = changed ? { ...result , include : newInclude } : { ...typedArgs , include : newInclude } ;
1024+ changed = true ;
1025+ }
1026+ }
1027+
1028+ // Recurse into nested relations in `select` (relation fields can have nested args)
1029+ if ( select ) {
1030+ const currentSelect = ( changed ? ( result as Record < string , unknown > ) [ 'select' ] : select ) as
1031+ | Record < string , unknown >
1032+ | undefined ;
1033+ if ( currentSelect ) {
1034+ const newSelect = { ...currentSelect } ;
1035+ let selectChanged = false ;
1036+ for ( const [ field , value ] of Object . entries ( newSelect ) ) {
1037+ if ( value && typeof value === 'object' ) {
1038+ const fieldDef = getField ( schema , model , field ) ;
1039+ if ( fieldDef ?. relation ) {
1040+ const targetModel = fieldDef . type ;
1041+ const processed = prepareArgsForExtResult ( value , targetModel , schema , plugins ) ;
1042+ if ( processed !== value ) {
1043+ newSelect [ field ] = processed ;
1044+ selectChanged = true ;
1045+ }
1046+ }
1047+ }
1048+ }
1049+ if ( selectChanged ) {
1050+ result = { ...result , select : newSelect } ;
1051+ changed = true ;
1052+ }
1053+ }
1054+ }
1055+
1056+ return changed ? result : args ;
1057+ }
1058+
1059+ /**
1060+ * Applies extended result field computation to query results (recursive).
1061+ * Processes the current model's ext result fields, then recurses into nested relation data.
1062+ */
1063+ function applyExtResult (
1064+ result : unknown ,
1065+ model : string ,
1066+ originalArgs : unknown ,
1067+ schema : SchemaDef ,
1068+ plugins : AnyPlugin [ ] ,
1069+ ) : unknown {
1070+ const extResultDefs = collectExtResultFieldDefs ( model , schema , plugins ) ;
1071+ if ( Array . isArray ( result ) ) {
1072+ for ( let i = 0 ; i < result . length ; i ++ ) {
1073+ result [ i ] = applyExtResultToRow ( result [ i ] , model , originalArgs , schema , plugins , extResultDefs ) ;
1074+ }
1075+ return result ;
1076+ } else {
1077+ return applyExtResultToRow ( result , model , originalArgs , schema , plugins , extResultDefs ) ;
1078+ }
1079+ }
1080+
1081+ function applyExtResultToRow (
1082+ row : unknown ,
1083+ model : string ,
1084+ originalArgs : unknown ,
1085+ schema : SchemaDef ,
1086+ plugins : AnyPlugin [ ] ,
1087+ extResultDefs : Map < string , ExtResultFieldDef > ,
1088+ ) : unknown {
1089+ if ( ! row || typeof row !== 'object' ) {
1090+ return row ;
1091+ }
1092+
1093+ const data = row as Record < string , unknown > ;
1094+ const typedArgs = ( originalArgs && typeof originalArgs === 'object' ? originalArgs : { } ) as Record < string , unknown > ;
1095+ const select = typedArgs [ 'select' ] as Record < string , unknown > | undefined ;
1096+ const omit = typedArgs [ 'omit' ] as Record < string , unknown > | undefined ;
1097+ const include = typedArgs [ 'include' ] as Record < string , unknown > | undefined ;
1098+
1099+ // Compute ext result fields for the current model
1100+ for ( const [ fieldName , fieldDef ] of extResultDefs ) {
1101+ if ( select && ! select [ fieldName ] ) {
1102+ continue ;
1103+ }
1104+ if ( omit ?. [ fieldName ] ) {
1105+ continue ;
1106+ }
1107+ const needsSatisfied = Object . keys ( fieldDef . needs ) . every ( ( needField ) => needField in data ) ;
1108+ if ( needsSatisfied ) {
1109+ data [ fieldName ] = fieldDef . compute ( data ) ;
1110+ }
1111+ }
1112+
1113+ // Strip fields that shouldn't be in the result: when `select` was used,
1114+ // drop any field not in the original select and not a computed ext result field;
1115+ // when `omit` was used, re-delete any field the user originally omitted.
1116+ if ( select ) {
1117+ for ( const key of Object . keys ( data ) ) {
1118+ if ( ! select [ key ] && ! extResultDefs . has ( key ) ) {
1119+ delete data [ key ] ;
1120+ }
1121+ }
1122+ } else if ( omit ) {
1123+ for ( const key of Object . keys ( omit ) ) {
1124+ if ( omit [ key ] && ! extResultDefs . has ( key ) ) {
1125+ delete data [ key ] ;
1126+ }
1127+ }
1128+ }
1129+
1130+ // Recurse into nested relation data
1131+ const relationSource = include ?? select ;
1132+ if ( relationSource ) {
1133+ for ( const [ field , value ] of Object . entries ( relationSource ) ) {
1134+ if ( data [ field ] == null ) {
1135+ continue ;
1136+ }
1137+ const fieldDef = getField ( schema , model , field ) ;
1138+ if ( ! fieldDef ?. relation ) {
1139+ continue ;
1140+ }
1141+ const targetModel = fieldDef . type ;
1142+ const nestedArgs = value && typeof value === 'object' ? value : undefined ;
1143+ data [ field ] = applyExtResult ( data [ field ] , targetModel , nestedArgs , schema , plugins ) ;
1144+ }
1145+ }
1146+
1147+ return data ;
1148+ }
1149+
1150+ // #endregion
0 commit comments