Skip to content

Commit 0b01142

Browse files
committed
functional, caching dynamic graphql server
1 parent 559b18f commit 0b01142

File tree

2 files changed

+79
-10
lines changed

2 files changed

+79
-10
lines changed

internal/catalogd/graphql/graphql.go

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,44 @@ func remapFieldName(name string) string {
5353
re := regexp.MustCompile(`[^a-zA-Z0-9_]`)
5454
clean := re.ReplaceAllString(name, "_")
5555

56+
// Collapse multiple consecutive underscores
57+
clean = regexp.MustCompile(`_+`).ReplaceAllString(clean, "_")
58+
59+
// Trim leading underscores only (keep trailing to detect them)
60+
clean = strings.TrimLeft(clean, "_")
61+
5662
// Split on underscores and camelCase
5763
parts := strings.Split(clean, "_")
5864
result := ""
65+
hasContent := false
5966
for i, part := range parts {
6067
if part == "" {
68+
// If we have an empty part after having content, it means there was a trailing separator
69+
// Add a capitalized version of the last word
70+
if hasContent && i == len(parts)-1 {
71+
// Get the base word (first non-empty part)
72+
for _, p := range parts {
73+
if p != "" {
74+
result += strings.ToUpper(string(p[0])) + strings.ToLower(p[1:])
75+
break
76+
}
77+
}
78+
}
6179
continue
6280
}
63-
if i == 0 {
64-
result = strings.ToLower(part)
81+
hasContent = true
82+
if i == 0 || result == "" {
83+
// For the first part, check if it's all uppercase
84+
if strings.ToUpper(part) == part {
85+
// If all uppercase, convert entirely to lowercase
86+
result = strings.ToLower(part)
87+
} else {
88+
// Otherwise, make only the first character lowercase
89+
result = strings.ToLower(string(part[0])) + part[1:]
90+
}
6591
} else {
66-
result += strings.Title(strings.ToLower(part))
92+
// For subsequent parts, capitalize first letter, lowercase rest
93+
result += strings.ToUpper(string(part[0])) + strings.ToLower(part[1:])
6794
}
6895
}
6996

@@ -330,7 +357,7 @@ func buildGraphQLObjectType(schemaName string, info *SchemaInfo) *graphql.Object
330357
}
331358

332359
return graphql.NewObject(graphql.ObjectConfig{
333-
Name: strings.Title(strings.ToLower(schemaName)),
360+
Name: sanitizeTypeName(schemaName),
334361
Fields: fields,
335362
})
336363
}
@@ -414,12 +441,16 @@ func sanitizeTypeName(propType string) string {
414441
// Remove dots and other invalid characters, capitalize words
415442
re := regexp.MustCompile(`[^a-zA-Z0-9]`)
416443
clean := re.ReplaceAllString(propType, "_")
444+
445+
// Strip leading digits
446+
clean = regexp.MustCompile(`^[0-9]+`).ReplaceAllString(clean, "")
447+
417448
parts := strings.Split(clean, "_")
418449

419450
result := ""
420451
for _, part := range parts {
421452
if part != "" {
422-
result += strings.Title(strings.ToLower(part))
453+
result += strings.ToUpper(string(part[0])) + strings.ToLower(part[1:])
423454
}
424455
}
425456

@@ -443,7 +474,9 @@ func BuildDynamicGraphQLSchema(catalogSchema *CatalogSchema, metasBySchema map[s
443474
queryFields := graphql.Fields{}
444475

445476
for schemaName, objectType := range objectTypes {
446-
fieldName := strings.ToLower(schemaName) + "s" // e.g., "bundles", "packages"
477+
// Sanitize schema name by removing dots and special characters for GraphQL field name
478+
sanitized := regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(schemaName, "")
479+
fieldName := strings.ToLower(sanitized) + "s" // e.g., "olmbundles", "olmpackages"
447480

448481
queryFields[fieldName] = &graphql.Field{
449482
Type: graphql.NewList(objectType),
@@ -463,7 +496,8 @@ func BuildDynamicGraphQLSchema(catalogSchema *CatalogSchema, metasBySchema map[s
463496
// Get the schema name from the field name
464497
currentSchemaName := ""
465498
for sn := range catalogSchema.Schemas {
466-
if strings.ToLower(sn)+"s" == p.Info.FieldName {
499+
sanitized := regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(sn, "")
500+
if strings.ToLower(sanitized)+"s" == p.Info.FieldName {
467501
currentSchemaName = sn
468502
break
469503
}

internal/catalogd/storage/localdir.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ type LocalDirV1 struct {
4141
// the loaded index. This avoids lots of unnecessary open/decode/close cycles when concurrent
4242
// requests are being handled, which improves overall performance and decreases response latency.
4343
sf singleflight.Group
44+
45+
// GraphQL schema cache: maps catalog name to its dynamically generated schema
46+
// This cache is invalidated when a catalog is updated via Store()
47+
schemaCacheMux sync.RWMutex
48+
schemaCache map[string]*gql.DynamicSchema
4449
}
4550

4651
var (
@@ -102,10 +107,20 @@ func (s *LocalDirV1) Store(ctx context.Context, catalog string, fsys fs.FS) erro
102107
}
103108

104109
catalogDir := s.catalogDir(catalog)
105-
return errors.Join(
110+
err = errors.Join(
106111
os.RemoveAll(catalogDir),
107112
os.Rename(tmpCatalogDir, catalogDir),
108113
)
114+
if err != nil {
115+
return err
116+
}
117+
118+
// Invalidate GraphQL schema cache for this catalog
119+
s.schemaCacheMux.Lock()
120+
delete(s.schemaCache, catalog)
121+
s.schemaCacheMux.Unlock()
122+
123+
return nil
109124
}
110125

111126
func (s *LocalDirV1) Delete(catalog string) error {
@@ -312,7 +327,7 @@ func (s *LocalDirV1) handleV1GraphQL(w http.ResponseWriter, r *http.Request) {
312327
}
313328

314329
// Build dynamic GraphQL schema for this catalog
315-
dynamicSchema, err := s.buildCatalogGraphQLSchema(catalogFS)
330+
dynamicSchema, err := s.buildCatalogGraphQLSchema(catalog, catalogFS)
316331
if err != nil {
317332
httpError(w, err)
318333
return
@@ -355,6 +370,8 @@ func httpError(w http.ResponseWriter, err error) {
355370
default:
356371
code = http.StatusInternalServerError
357372
}
373+
// Log the actual error for debugging
374+
fmt.Printf("HTTP Error %d: %v\n", code, err)
358375
http.Error(w, fmt.Sprintf("%d %s", code, http.StatusText(code)), code)
359376
}
360377

@@ -398,7 +415,17 @@ func (s *LocalDirV1) createCatalogFS(catalog string) (fs.FS, error) {
398415
}
399416

400417
// buildCatalogGraphQLSchema builds a dynamic GraphQL schema for the given catalog
401-
func (s *LocalDirV1) buildCatalogGraphQLSchema(catalogFS fs.FS) (*gql.DynamicSchema, error) {
418+
// Uses a cache to avoid rebuilding the schema on every request
419+
func (s *LocalDirV1) buildCatalogGraphQLSchema(catalog string, catalogFS fs.FS) (*gql.DynamicSchema, error) {
420+
// Check cache first (read lock)
421+
s.schemaCacheMux.RLock()
422+
if cachedSchema, ok := s.schemaCache[catalog]; ok {
423+
s.schemaCacheMux.RUnlock()
424+
return cachedSchema, nil
425+
}
426+
s.schemaCacheMux.RUnlock()
427+
428+
// Schema not in cache, build it
402429
var metas []*declcfg.Meta
403430

404431
// Collect all metas from the catalog filesystem
@@ -435,5 +462,13 @@ func (s *LocalDirV1) buildCatalogGraphQLSchema(catalogFS fs.FS) (*gql.DynamicSch
435462
return nil, fmt.Errorf("error building GraphQL schema: %w", err)
436463
}
437464

465+
// Cache the result (write lock)
466+
s.schemaCacheMux.Lock()
467+
if s.schemaCache == nil {
468+
s.schemaCache = make(map[string]*gql.DynamicSchema)
469+
}
470+
s.schemaCache[catalog] = dynamicSchema
471+
s.schemaCacheMux.Unlock()
472+
438473
return dynamicSchema, nil
439474
}

0 commit comments

Comments
 (0)