@@ -31,6 +31,12 @@ export class QueryProcessor {
3131 }
3232
3333 if ( r . include || r . select ) {
34+ if ( r . include && r . select ) {
35+ throw Error (
36+ 'Passing both "include" and "select" at the same level of query is not supported'
37+ ) ;
38+ }
39+
3440 // "include" and "select" are mutually exclusive
3541 const selector = r . include ? 'include' : 'select' ;
3642 for ( const [ field , value ] of Object . entries ( r [ selector ] ) ) {
@@ -59,11 +65,129 @@ export class QueryProcessor {
5965 return r ;
6066 }
6167
68+ private async getToOneFieldInfo (
69+ model : string ,
70+ fieldName : string ,
71+ fieldValue : any
72+ ) {
73+ if (
74+ ! ! fieldValue &&
75+ ! Array . isArray ( fieldValue ) &&
76+ typeof fieldValue === 'object' &&
77+ typeof fieldValue . id == 'string'
78+ ) {
79+ return null ;
80+ }
81+
82+ const fieldInfo = await this . service . resolveField ( model , fieldName ) ;
83+ if ( ! fieldInfo || fieldInfo . isArray ) {
84+ return null ;
85+ }
86+
87+ return fieldInfo ;
88+ }
89+
90+ private async collectRelationFields (
91+ model : string ,
92+ data : any ,
93+ map : Map < string , string [ ] >
94+ ) {
95+ for ( const [ fieldName , fieldValue ] of Object . entries ( data ) ) {
96+ const val : any = fieldValue ;
97+ const fieldInfo = await this . getToOneFieldInfo (
98+ model ,
99+ fieldName ,
100+ fieldValue
101+ ) ;
102+ if ( ! fieldInfo ) {
103+ continue ;
104+ }
105+
106+ if ( ! map . has ( fieldInfo . type ) ) {
107+ map . set ( fieldInfo . type , [ ] ) ;
108+ }
109+ map . get ( fieldInfo . type ) ! . push ( val . id ) ;
110+
111+ // recurse into field value
112+ this . collectRelationFields ( fieldInfo . type , val , map ) ;
113+ }
114+ }
115+
116+ private async checkIdsAgainstPolicy (
117+ relationFieldMap : Map < string , string [ ] > ,
118+ operation : PolicyOperationKind ,
119+ context : QueryContext
120+ ) {
121+ const promises = Array . from ( relationFieldMap . entries ( ) ) . map (
122+ async ( [ model , ids ] ) => {
123+ const args = {
124+ select : { id : true } ,
125+ where : {
126+ id : { in : ids } ,
127+ } ,
128+ } ;
129+
130+ const processedArgs = this . processQueryArgs (
131+ model ,
132+ args ,
133+ operation ,
134+ context ,
135+ true
136+ ) ;
137+
138+ const checkedIds : Array < { id : string } > = await this . service . db [
139+ model
140+ ] . findMany ( processedArgs ) ;
141+ return [ model , checkedIds . map ( ( r ) => r . id ) ] as [
142+ string ,
143+ string [ ]
144+ ] ;
145+ }
146+ ) ;
147+ return new Map < string , string [ ] > ( await Promise . all ( promises ) ) ;
148+ }
149+
150+ private async sanitizeData (
151+ model : string ,
152+ data : any ,
153+ validatedIds : Map < string , string [ ] >
154+ ) {
155+ for ( const [ fieldName , fieldValue ] of Object . entries ( data ) ) {
156+ const fieldInfo = await this . getToOneFieldInfo (
157+ model ,
158+ fieldName ,
159+ fieldValue
160+ ) ;
161+ if ( ! fieldInfo ) {
162+ continue ;
163+ }
164+ const fv = fieldValue as { id : string } ;
165+ const valIds = validatedIds . get ( fieldInfo . type ) ;
166+
167+ if ( ! valIds || ! valIds . includes ( fv . id ) ) {
168+ console . log (
169+ `Deleting field ${ fieldName } from ${ model } #${ data . id } , because field value #${ fv . id } failed policy check`
170+ ) ;
171+ delete data [ fieldName ] ;
172+ }
173+
174+ await this . sanitizeData ( fieldInfo . type , fieldValue , validatedIds ) ;
175+ }
176+ }
177+
62178 async postProcess (
63179 model : string ,
64- queryArgs : any ,
65180 data : any ,
66181 operation : PolicyOperationKind ,
67182 context : QueryContext
68- ) { }
183+ ) {
184+ const relationFieldMap = new Map < string , string [ ] > ( ) ;
185+ await this . collectRelationFields ( model , data , relationFieldMap ) ;
186+ const validatedIds = await this . checkIdsAgainstPolicy (
187+ relationFieldMap ,
188+ operation ,
189+ context
190+ ) ;
191+ await this . sanitizeData ( model , data , validatedIds ) ;
192+ }
69193}
0 commit comments