Skip to content

Commit c0f593d

Browse files
committed
functional, caching dynamic graphql server
1 parent f26b98e commit c0f593d

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 (
@@ -103,10 +108,20 @@ func (s *LocalDirV1) Store(ctx context.Context, catalog string, fsys fs.FS) erro
103108
}
104109

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

112127
func (s *LocalDirV1) Delete(catalog string) error {
@@ -313,7 +328,7 @@ func (s *LocalDirV1) handleV1GraphQL(w http.ResponseWriter, r *http.Request) {
313328
}
314329

315330
// Build dynamic GraphQL schema for this catalog
316-
dynamicSchema, err := s.buildCatalogGraphQLSchema(catalogFS)
331+
dynamicSchema, err := s.buildCatalogGraphQLSchema(catalog, catalogFS)
317332
if err != nil {
318333
httpError(w, err)
319334
return
@@ -356,6 +371,8 @@ func httpError(w http.ResponseWriter, err error) {
356371
default:
357372
code = http.StatusInternalServerError
358373
}
374+
// Log the actual error for debugging
375+
fmt.Printf("HTTP Error %d: %v\n", code, err)
359376
http.Error(w, fmt.Sprintf("%d %s", code, http.StatusText(code)), code)
360377
}
361378

@@ -399,7 +416,17 @@ func (s *LocalDirV1) createCatalogFS(catalog string) (fs.FS, error) {
399416
}
400417

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

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

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

0 commit comments

Comments
 (0)