@@ -170,6 +170,74 @@ function matchesFilter(item: any, filter: Record<string, any>): boolean {
170170 return true ;
171171}
172172
173+ /**
174+ * Build Prisma include object with access control filters
175+ * This allows us to filter relationships at the database level instead of in memory
176+ */
177+ export async function buildIncludeWithAccessControl (
178+ fieldConfigs : Record < string , FieldConfig > ,
179+ args : {
180+ session : Session ;
181+ context : AccessContext ;
182+ } ,
183+ config : OpenSaaSConfig ,
184+ depth : number = 0 ,
185+ ) : Promise < Record < string , any > | undefined > {
186+ const MAX_DEPTH = 5 ;
187+ if ( depth >= MAX_DEPTH ) {
188+ return undefined ;
189+ }
190+
191+ const include : Record < string , any > = { } ;
192+ let hasRelationships = false ;
193+
194+ for ( const [ fieldName , fieldConfig ] of Object . entries ( fieldConfigs ) ) {
195+ if ( fieldConfig ?. type === "relationship" ) {
196+ hasRelationships = true ;
197+ const relatedConfig = getRelatedListConfig ( fieldConfig . ref , config ) ;
198+
199+ if ( relatedConfig ) {
200+ // Check query access for the related list
201+ const queryAccess = relatedConfig . listConfig . access ?. operation ?. query ;
202+ const accessResult = await checkAccess ( queryAccess , {
203+ session : args . session ,
204+ context : args . context ,
205+ } ) ;
206+
207+ // If access is completely denied, exclude this relationship
208+ if ( accessResult === false ) {
209+ continue ;
210+ }
211+
212+ // Build the include entry
213+ const includeEntry : Record < string , any > = { } ;
214+
215+ // If access returns a filter, add it to the where clause
216+ if ( typeof accessResult === "object" ) {
217+ includeEntry . where = accessResult ;
218+ }
219+
220+ // Recursively build nested includes
221+ const nestedInclude = await buildIncludeWithAccessControl (
222+ relatedConfig . listConfig . fields ,
223+ args ,
224+ config ,
225+ depth + 1 ,
226+ ) ;
227+
228+ if ( nestedInclude && Object . keys ( nestedInclude ) . length > 0 ) {
229+ includeEntry . include = nestedInclude ;
230+ }
231+
232+ // Add to include object
233+ include [ fieldName ] = Object . keys ( includeEntry ) . length > 0 ? includeEntry : true ;
234+ }
235+ }
236+ }
237+
238+ return hasRelationships ? include : undefined ;
239+ }
240+
173241/**
174242 * Filter fields from an object based on read access
175243 * Recursively applies access control to nested relationships
@@ -206,7 +274,9 @@ export async function filterReadableFields<T extends Record<string, any>>(
206274 continue ;
207275 }
208276
209- // Handle relationship fields with nested access control
277+ // Handle relationship fields - recursively filter fields within related items
278+ // Note: Access control filtering is now done at database level via buildIncludeWithAccessControl
279+ // This only handles field-level access (hiding sensitive fields)
210280 if (
211281 config &&
212282 fieldConfig ?. type === "relationship" &&
@@ -217,84 +287,29 @@ export async function filterReadableFields<T extends Record<string, any>>(
217287 const relatedConfig = getRelatedListConfig ( fieldConfig . ref , config ) ;
218288
219289 if ( relatedConfig ) {
220- // For many relationships (arrays)
290+ // For many relationships (arrays) - recursively filter fields in each item
221291 if ( Array . isArray ( value ) ) {
222- const filteredArray = await Promise . all (
223- value . map ( async ( relatedItem ) => {
224- // Check query access for the related list
225- const queryAccess = relatedConfig . listConfig . access ?. operation ?. query ;
226- const accessResult = await checkAccess ( queryAccess , {
227- session : args . session ,
228- item : relatedItem ,
229- context : args . context ,
230- } ) ;
231-
232- // If access denied, filter out this item
233- if ( accessResult === false ) {
234- return null ;
235- }
236-
237- // If access returns a filter, check if item matches
238- if ( typeof accessResult === "object" ) {
239- if ( ! matchesFilter ( relatedItem , accessResult ) ) {
240- return null ;
241- }
242- }
243-
244- // Recursively filter readable fields on the related item
245- return await filterReadableFields (
292+ filtered [ fieldName ] = await Promise . all (
293+ value . map ( ( relatedItem ) =>
294+ filterReadableFields (
246295 relatedItem ,
247296 relatedConfig . listConfig . fields ,
248297 args ,
249298 config ,
250299 depth + 1 ,
251- ) ;
252- } ) ,
300+ ) ,
301+ ) ,
253302 ) ;
254-
255- // Remove null entries (items that were filtered out)
256- filtered [ fieldName ] = filteredArray . filter ( ( item ) => item !== null ) ;
257303 }
258- // For single relationships (objects)
304+ // For single relationships (objects) - recursively filter fields
259305 else if ( typeof value === "object" ) {
260- // Check query access for the related list
261- const queryAccess = relatedConfig . listConfig . access ?. operation ?. query ;
262- const accessResult = await checkAccess ( queryAccess , {
263- session : args . session ,
264- item : value ,
265- context : args . context ,
266- } ) ;
267-
268- // If access denied, set to null
269- if ( accessResult === false ) {
270- filtered [ fieldName ] = null ;
271- }
272- // If access returns a filter, check if item matches
273- else if ( typeof accessResult === "object" ) {
274- if ( ! matchesFilter ( value , accessResult ) ) {
275- filtered [ fieldName ] = null ;
276- } else {
277- // Recursively filter readable fields on the related item
278- filtered [ fieldName ] = await filterReadableFields (
279- value ,
280- relatedConfig . listConfig . fields ,
281- args ,
282- config ,
283- depth + 1 ,
284- ) ;
285- }
286- }
287- // Access granted (true)
288- else {
289- // Recursively filter readable fields on the related item
290- filtered [ fieldName ] = await filterReadableFields (
291- value ,
292- relatedConfig . listConfig . fields ,
293- args ,
294- config ,
295- depth + 1 ,
296- ) ;
297- }
306+ filtered [ fieldName ] = await filterReadableFields (
307+ value ,
308+ relatedConfig . listConfig . fields ,
309+ args ,
310+ config ,
311+ depth + 1 ,
312+ ) ;
298313 }
299314 } else {
300315 // Related config not found, include the value as-is
0 commit comments