@@ -617,61 +617,82 @@ public JsonDocument Execute(string query, JsonElement? variables = null)
617617 // Handle different relation types
618618 if ( relationColumn . RelationType == RelationType . OneToMany || relationColumn . IsList )
619619 {
620- // Many-to-many or one-to-many: return array of related records
621- // The foreign key value should be an array of IDs
622- var results = new List < Dictionary < string , object ? > > ( ) ;
620+ // Many-to-many or one-to-many: return Connection object
621+ // Check if query is asking for 'items' subfield (Connection pattern)
622+ var itemsField = field . SelectionSet ? . Selections ? . OfType < Field > ( )
623+ . FirstOrDefault ( f => f . Name == "items" ) ;
623624
624- if ( foreignKeyValue . ValueKind == JsonValueKind . Array )
625+ if ( itemsField != null )
625626 {
626- // Many-to-many: foreign key is an array of IDs
627- // Collect all IDs first for batch loading
628- var ids = new List < string > ( ) ;
629- foreach ( var idElement in foreignKeyValue . EnumerateArray ( ) )
627+ // Connection pattern: resolve the items with the nested field selections
628+ var results = new List < Dictionary < string , object ? > > ( ) ;
629+
630+ if ( foreignKeyValue . ValueKind == JsonValueKind . Array )
630631 {
631- var foreignId = idElement . GetString ( ) ;
632- if ( ! string . IsNullOrEmpty ( foreignId ) )
632+ // Many-to-many: foreign key is an array of IDs
633+ var ids = new List < string > ( ) ;
634+ foreach ( var idElement in foreignKeyValue . EnumerateArray ( ) )
633635 {
634- ids . Add ( foreignId ) ;
636+ var foreignId = idElement . GetString ( ) ;
637+ if ( ! string . IsNullOrEmpty ( foreignId ) )
638+ {
639+ ids . Add ( foreignId ) ;
640+ }
635641 }
636- }
637-
638- // Batch load all related records at once (fixes N+1 problem)
639- foreach ( var foreignId in ids )
640- {
641- var relatedRecord = relatedTable . Find ( foreignId ) ;
642- if ( relatedRecord != null )
642+
643+ // Batch load all related records at once (fixes N+1 problem)
644+ foreach ( var foreignId in ids )
643645 {
644- var recordData = JsonSerializer . Deserialize < Dictionary < string , JsonElement > > ( relatedRecord ) ;
645- if ( recordData != null )
646+ var relatedRecord = relatedTable . Find ( foreignId ) ;
647+ if ( relatedRecord != null )
646648 {
647- results . Add ( ProjectFields ( recordData , field , fragments , relationColumn . RelatedTable ) ) ;
649+ var recordData = JsonSerializer . Deserialize < Dictionary < string , JsonElement > > ( relatedRecord ) ;
650+ if ( recordData != null )
651+ {
652+ results . Add ( ProjectFields ( recordData , itemsField , fragments , relationColumn . RelatedTable ) ) ;
653+ }
648654 }
649655 }
650656 }
651- }
652- else
653- {
654- // One-to-many: need to scan the related table for records that reference this record
655- // This is the reverse direction (e.g., a Character's films where Film has characterId)
656- var currentId = data . TryGetValue ( "id" , out var idElement ) ? idElement . GetString ( ) : null ;
657- if ( currentId != null )
657+ else
658658 {
659- var allRecords = relatedTable . SelectAll ( ) ;
660- foreach ( var ( key , value ) in allRecords )
659+ // One-to-many: scan related table for records that reference this record
660+ var currentId = data . TryGetValue ( "id" , out var idElement ) ? idElement . GetString ( ) : null ;
661+ if ( currentId != null )
661662 {
662- var recordData = JsonSerializer . Deserialize < Dictionary < string , JsonElement > > ( value ) ;
663- if ( recordData != null && recordData . TryGetValue ( foreignKeyField , out var recordFk ) )
663+ var allRecords = relatedTable . SelectAll ( ) ;
664+ foreach ( var ( key , value ) in allRecords )
664665 {
665- if ( recordFk . GetString ( ) == currentId )
666+ var recordData = JsonSerializer . Deserialize < Dictionary < string , JsonElement > > ( value ) ;
667+ if ( recordData != null && recordData . TryGetValue ( foreignKeyField , out var recordFk ) )
666668 {
667- results . Add ( ProjectFields ( recordData , field , fragments , relationColumn . RelatedTable ) ) ;
669+ if ( recordFk . GetString ( ) == currentId )
670+ {
671+ results . Add ( ProjectFields ( recordData , itemsField , fragments , relationColumn . RelatedTable ) ) ;
672+ }
668673 }
669674 }
670675 }
671676 }
677+
678+ // Apply filtering, ordering, and pagination from items field arguments
679+ var filteredResults = ApplyFiltersAndPagination ( results , itemsField , relationColumn . RelatedTable ) ;
680+
681+ // Return Connection object
682+ return new Dictionary < string , object ? >
683+ {
684+ [ "items" ] = filteredResults
685+ } ;
686+ }
687+ else
688+ {
689+ // Legacy: No items field requested, return empty Connection
690+ // This handles the case where the query just asks for "friends" without "items { ... }"
691+ return new Dictionary < string , object ? >
692+ {
693+ [ "items" ] = new List < Dictionary < string , object ? > > ( )
694+ } ;
672695 }
673-
674- return results ;
675696 }
676697 else
677698 {
@@ -691,6 +712,74 @@ public JsonDocument Execute(string query, JsonElement? variables = null)
691712 return null ;
692713 }
693714
715+ private List < Dictionary < string , object ? > > ApplyFiltersAndPagination ( List < Dictionary < string , object ? > > data , Field field , string tableName )
716+ {
717+ // Convert data to JsonElement-based records for filtering
718+ var jsonRecords = data . Select ( item =>
719+ JsonSerializer . Deserialize < Dictionary < string , JsonElement > > (
720+ JsonSerializer . Serialize ( item ) ) ?? new Dictionary < string , JsonElement > ( )
721+ ) . ToList ( ) ;
722+
723+ var filteredRecords = jsonRecords ;
724+
725+ // Apply WHERE filtering using existing FilterEvaluator
726+ var whereArg = field . Arguments . FirstOrDefault ( a => a . Name == "where" ) ;
727+ if ( whereArg != null )
728+ {
729+ var whereValue = ResolveValue ( whereArg . Value , null ) ;
730+ if ( whereValue is JsonElement whereElement )
731+ {
732+ // Use the existing FilterEvaluator for consistency
733+ filteredRecords = FilterEvaluator . ApplyFilters ( filteredRecords , whereElement ) ;
734+ }
735+ }
736+
737+ // Apply ORDER BY using existing FilterEvaluator
738+ var orderByArg = field . Arguments . FirstOrDefault ( a => a . Name == "orderBy" ) ;
739+ if ( orderByArg != null )
740+ {
741+ var orderByValue = ResolveValue ( orderByArg . Value , null ) ;
742+ if ( orderByValue is JsonElement orderByElement )
743+ {
744+ filteredRecords = FilterEvaluator . ApplySorting ( filteredRecords , orderByElement ) ;
745+ }
746+ }
747+
748+ // Apply SKIP/TAKE pagination
749+ int ? skip = null ;
750+ int ? take = null ;
751+
752+ var skipArg = field . Arguments . FirstOrDefault ( a => a . Name == "skip" ) ;
753+ if ( skipArg != null )
754+ {
755+ var skipValue = ResolveValue ( skipArg . Value , null ) ;
756+ if ( skipValue is JsonElement skipElement && skipElement . TryGetInt32 ( out var skipInt ) )
757+ {
758+ skip = skipInt ;
759+ }
760+ }
761+
762+ var takeArg = field . Arguments . FirstOrDefault ( a => a . Name == "take" ) ;
763+ if ( takeArg != null )
764+ {
765+ var takeValue = ResolveValue ( takeArg . Value , null ) ;
766+ if ( takeValue is JsonElement takeElement && takeElement . TryGetInt32 ( out var takeInt ) )
767+ {
768+ take = takeInt ;
769+ }
770+ }
771+
772+ filteredRecords = FilterEvaluator . ApplyPagination ( filteredRecords , skip , take ) ;
773+
774+ // Convert back to Dictionary<string, object?>
775+ return filteredRecords . Select ( record =>
776+ record . ToDictionary (
777+ kvp => kvp . Key ,
778+ kvp => ( object ? ) ConvertJsonElement ( kvp . Value )
779+ )
780+ ) . ToList ( ) ;
781+ }
782+
694783 private object ? ConvertJsonElement ( JsonElement element )
695784 {
696785 return element . ValueKind switch
@@ -1633,7 +1722,7 @@ private string GetBaseTypeName(TypeNode typeNode)
16331722 [ "kind" ] = "OBJECT" ,
16341723 [ "name" ] = name ,
16351724 [ "description" ] = $ "{ name } type",
1636- [ "fields" ] = BuildFields ( typeDef , field ) ,
1725+ [ "fields" ] = BuildFields ( name , typeDef , field ) ,
16371726 [ "inputFields" ] = null ,
16381727 [ "interfaces" ] = new List < object > ( ) ,
16391728 [ "enumValues" ] = null ,
@@ -1701,21 +1790,70 @@ private string GetBaseTypeName(TypeNode typeNode)
17011790 } ;
17021791 }
17031792
1704- private List < Dictionary < string , object ? > > ? BuildFields ( TypeDefinition typeDef , Field field )
1793+ private List < Dictionary < string , object ? > > ? BuildFields ( string typeName , TypeDefinition typeDef , Field field )
17051794 {
17061795 if ( typeDef . Fields . Count == 0 )
17071796 return new List < Dictionary < string , object ? > > ( ) ;
17081797
17091798 var fields = new List < Dictionary < string , object ? > > ( ) ;
17101799
1800+ // Try to get table metadata for relationship checking
1801+ TableMetadata ? tableMetadata = null ;
1802+ if ( _tables . TryGetValue ( typeName , out var table ) )
1803+ {
1804+ tableMetadata = table . GetMetadata ( ) ;
1805+ }
1806+
17111807 foreach ( var fieldDef in typeDef . Fields )
17121808 {
1809+ // Check if this field is a relationship that returns a list
1810+ bool isListRelationship = false ;
1811+ string ? relatedTypeName = null ;
1812+
1813+ if ( tableMetadata != null )
1814+ {
1815+ var relationColumn = tableMetadata . Columns ? . FirstOrDefault ( c => c . Name == fieldDef . Name ) ;
1816+ if ( relationColumn != null && ! string . IsNullOrEmpty ( relationColumn . RelatedTable ) )
1817+ {
1818+ isListRelationship = relationColumn . IsList || relationColumn . RelationType == RelationType . OneToMany ;
1819+ relatedTypeName = relationColumn . RelatedTable ;
1820+ }
1821+ }
1822+
1823+ // If it's a list relationship, wrap it as a Connection type
1824+ Dictionary < string , object ? > fieldType ;
1825+ List < object > args = new List < object > ( ) ;
1826+
1827+ if ( isListRelationship && relatedTypeName != null )
1828+ {
1829+ // Return ConnectionType with filtering/pagination arguments
1830+ fieldType = new Dictionary < string , object ? >
1831+ {
1832+ [ "kind" ] = "NON_NULL" ,
1833+ [ "name" ] = null ,
1834+ [ "ofType" ] = new Dictionary < string , object ? >
1835+ {
1836+ [ "kind" ] = "OBJECT" ,
1837+ [ "name" ] = $ "{ relatedTypeName } Connection",
1838+ [ "ofType" ] = null
1839+ }
1840+ } ;
1841+
1842+ // Add standard Connection arguments (these are handled by the Connection type's items field)
1843+ // No arguments needed at the Connection level
1844+ }
1845+ else
1846+ {
1847+ // Regular field - use its defined type
1848+ fieldType = BuildTypeRef ( fieldDef . Type , field ) ;
1849+ }
1850+
17131851 var fieldInfo = new Dictionary < string , object ? >
17141852 {
17151853 [ "name" ] = fieldDef . Name ,
17161854 [ "description" ] = null ,
1717- [ "args" ] = new List < object > ( ) ,
1718- [ "type" ] = BuildTypeRef ( fieldDef . Type , field ) ,
1855+ [ "args" ] = args ,
1856+ [ "type" ] = fieldType ,
17191857 [ "isDeprecated" ] = false ,
17201858 [ "deprecationReason" ] = null
17211859 } ;
0 commit comments