@@ -3,70 +3,68 @@ package service
33import (
44 "context"
55 "fmt"
6- "io/fs"
76 "sync"
7+ "time"
88
99 "github.com/graphql-go/graphql"
10+ "github.com/graphql-go/graphql/gqlerrors"
1011 "golang.org/x/sync/singleflight"
1112
12- "github.com/operator-framework/operator-registry/alpha/declcfg"
13-
1413 gql "github.com/operator-framework/operator-controller/internal/catalogd/graphql"
1514)
1615
16+ // CatalogDataProvider provides access to catalog data for GraphQL schema building.
17+ // Implemented by the storage layer.
18+ type CatalogDataProvider interface {
19+ LoadCatalogSchema (catalog string ) (* gql.CatalogSchema , error )
20+ NewObjectLoader (catalog string ) (gql.ObjectLoader , error )
21+ }
22+
1723// GraphQLService handles GraphQL schema generation and query execution for catalogs
1824type GraphQLService interface {
19- // GetSchema returns the GraphQL schema for a catalog, using cache if available
20- GetSchema (catalog string , catalogFS fs.FS ) (* gql.DynamicSchema , error )
21-
22- // ExecuteQuery executes a GraphQL query against a catalog
23- ExecuteQuery (catalog string , catalogFS fs.FS , query string ) (* graphql.Result , error )
24-
25- // InvalidateCache removes the cached schema for a catalog
25+ GetSchema (ctx context.Context , catalog string ) (* gql.DynamicSchema , error )
26+ ExecuteQuery (ctx context.Context , catalog string , query string ) (* graphql.Result , error )
2627 InvalidateCache (catalog string )
2728}
2829
29- // CachedGraphQLService implements GraphQLService with an in-memory schema cache
30+ // CachedGraphQLService implements GraphQLService with an in-memory schema cache.
31+ // The cached DynamicSchema contains only the GraphQL type system and schema metadata
32+ // (a few KB). Object data is loaded from disk at query time via the ObjectLoader.
3033type CachedGraphQLService struct {
34+ provider CatalogDataProvider
3135 schemaMux sync.RWMutex
3236 schemaCache map [string ]* gql.DynamicSchema
33- buildGroup singleflight.Group // Prevents duplicate concurrent schema builds
37+ buildGroup singleflight.Group
3438}
3539
36- // NewCachedGraphQLService creates a new GraphQL service with caching
37- func NewCachedGraphQLService () * CachedGraphQLService {
40+ func NewCachedGraphQLService (provider CatalogDataProvider ) * CachedGraphQLService {
3841 return & CachedGraphQLService {
42+ provider : provider ,
3943 schemaCache : make (map [string ]* gql.DynamicSchema ),
4044 }
4145}
4246
43- // GetSchema returns the GraphQL schema for a catalog, using cache if available
44- func (s * CachedGraphQLService ) GetSchema (catalog string , catalogFS fs.FS ) (* gql.DynamicSchema , error ) {
45- // Check cache first (read lock)
47+ func (s * CachedGraphQLService ) GetSchema (ctx context.Context , catalog string ) (* gql.DynamicSchema , error ) {
4648 s .schemaMux .RLock ()
4749 if cachedSchema , ok := s .schemaCache [catalog ]; ok {
4850 s .schemaMux .RUnlock ()
4951 return cachedSchema , nil
5052 }
5153 s .schemaMux .RUnlock ()
5254
53- // Use singleflight to prevent duplicate concurrent builds for the same catalog
5455 result , err , _ := s .buildGroup .Do (catalog , func () (interface {}, error ) {
55- // Double-check cache after acquiring singleflight lock
5656 s .schemaMux .RLock ()
5757 if cachedSchema , ok := s .schemaCache [catalog ]; ok {
5858 s .schemaMux .RUnlock ()
5959 return cachedSchema , nil
6060 }
6161 s .schemaMux .RUnlock ()
6262
63- // Schema not in cache, build it
64- dynamicSchema , err := buildSchemaFromFS (catalogFS )
63+ dynamicSchema , err := s .buildSchema (catalog )
6564 if err != nil {
6665 return nil , err
6766 }
6867
69- // Cache the result (write lock)
7068 s .schemaMux .Lock ()
7169 s .schemaCache [catalog ] = dynamicSchema
7270 s .schemaMux .Unlock ()
@@ -81,80 +79,50 @@ func (s *CachedGraphQLService) GetSchema(catalog string, catalogFS fs.FS) (*gql.
8179 return result .(* gql.DynamicSchema ), nil
8280}
8381
84- // ExecuteQuery executes a GraphQL query against a catalog
85- func (s * CachedGraphQLService ) ExecuteQuery (catalog string , catalogFS fs.FS , query string ) (* graphql.Result , error ) {
86- // Get or build the schema (uses cache and singleflight)
87- dynamicSchema , err := s .GetSchema (catalog , catalogFS )
82+ func (s * CachedGraphQLService ) ExecuteQuery (ctx context.Context , catalog string , query string ) (* graphql.Result , error ) {
83+ if err := gql .ValidateQueryComplexity (query ); err != nil {
84+ return & graphql.Result {
85+ Errors : []gqlerrors.FormattedError {
86+ gqlerrors .FormatError (err ),
87+ },
88+ }, nil
89+ }
90+
91+ dynamicSchema , err := s .GetSchema (ctx , catalog )
8892 if err != nil {
8993 return nil , fmt .Errorf ("failed to get GraphQL schema: %w" , err )
9094 }
9195
92- // Execute the query
96+ queryCtx , cancel := context .WithTimeout (ctx , 5 * time .Second )
97+ defer cancel ()
98+
9399 result := graphql .Do (graphql.Params {
94100 Schema : dynamicSchema .Schema ,
95101 RequestString : query ,
102+ Context : queryCtx ,
96103 })
97104
98105 return result , nil
99106}
100107
101- // InvalidateCache removes the cached schema for a catalog
102108func (s * CachedGraphQLService ) InvalidateCache (catalog string ) {
103109 s .schemaMux .Lock ()
104110 delete (s .schemaCache , catalog )
105111 s .schemaMux .Unlock ()
106112}
107113
108- // buildSchemaFromFS builds a GraphQL schema from a catalog filesystem
109- func buildSchemaFromFS (catalogFS fs.FS ) (* gql.DynamicSchema , error ) {
110- var metas []* declcfg.Meta
111- var metasMux sync.Mutex
112- var walkErr error
113-
114- // Collect all metas from the catalog filesystem
115- // WalkMetasFS walks the filesystem concurrently, so we need to protect the metas slice and error
116- err := declcfg .WalkMetasFS (context .Background (), catalogFS , func (path string , meta * declcfg.Meta , err error ) error {
117- metasMux .Lock ()
118- defer metasMux .Unlock ()
119-
120- if err != nil {
121- // Set shared error so other goroutines can check
122- if walkErr == nil {
123- walkErr = err
124- }
125- return err
126- }
127-
128- // If an error has already occurred, skip further mutation
129- if walkErr != nil {
130- return walkErr
131- }
132-
133- if meta != nil {
134- metas = append (metas , meta )
135- }
136- return nil
137- })
114+ func (s * CachedGraphQLService ) buildSchema (catalog string ) (* gql.DynamicSchema , error ) {
115+ catalogSchema , err := s .provider .LoadCatalogSchema (catalog )
138116 if err != nil {
139- return nil , fmt .Errorf ("error walking catalog metas : %w" , err )
117+ return nil , fmt .Errorf ("error loading catalog schema : %w" , err )
140118 }
141119
142- // Discover schema from collected metas
143- catalogSchema , err := gql .DiscoverSchemaFromMetas (metas )
120+ loader , err := s .provider .NewObjectLoader (catalog )
144121 if err != nil {
145- return nil , fmt .Errorf ("error discovering schema: %w" , err )
146- }
147-
148- // Organize metas by schema for resolvers
149- metasBySchema := make (map [string ][]* declcfg.Meta )
150- for _ , meta := range metas {
151- if meta .Schema != "" {
152- metasBySchema [meta .Schema ] = append (metasBySchema [meta .Schema ], meta )
153- }
122+ return nil , fmt .Errorf ("error creating object loader: %w" , err )
154123 }
155124
156- // Build dynamic GraphQL schema
157- dynamicSchema , err := gql .BuildDynamicGraphQLSchema (catalogSchema , metasBySchema )
125+ dynamicSchema , err := gql .BuildDynamicGraphQLSchema (catalogSchema , loader )
158126 if err != nil {
159127 return nil , fmt .Errorf ("error building GraphQL schema: %w" , err )
160128 }
0 commit comments