Documentation
¶
Overview ¶
Package gqldomainresolver is a gqlgen plugin that splits the generated resolver package into per-domain Go packages.
The standard gqlgen layout puts every resolver method in a single graph/resolver package, which forces every file to import graph/generated. In large schemas this collapses the build cache: any edit to a resolver invalidates the whole generated artifact (often hundreds of MB) and rebuilds slow to a crawl. gqldomainresolver keeps the original generated package intact for wiring and moves the resolver bodies out into per-domain packages that have no dependency on graph/generated. The per-domain packages satisfy the gqlgen interfaces by Go duck typing (matching method names and signatures), and the root resolver wires them together via Go method promotion.
Domain extraction ¶
A domain is the parent directory name of a .graphqls schema file, e.g.
graph/schema/todos/todo.graphqls → domain "todos" graph/schema/business-process/x.graphqls → domain "business-process" graph/schema/schema.graphqls → no domain (stays in root)
The directory name is normalized to a Go package identifier using the strip-only lowercase rule (see normalizeDomain). Names that collide with a Go keyword, equal "schema", or start with a digit get a configurable prefix prepended (default "gql", override with WithKeywordPrefix).
Greenfield vs. migration ¶
With no options (New alone) every domain in the schema is migrated. This is the greenfield default.
To migrate an existing project incrementally, pass WithEnabledDomains with the subset of domains to move first. Calling WithEnabledDomains() with no arguments produces an explicit empty allowlist — the plugin becomes a no-op, useful as a bootstrap step that wires the plugin into a project without producing any diff.
Wiring ¶
Construct the plugin and pass it to api.Generate alongside the standard gqlgen plugins. The plugin must run after resolvergen because it relies on resolvergen's prevImpl handling for first-time migrations.
plugin, err := gqldomainresolver.New(gqldomainresolver.WithEnabledDomains("todos"))
if err != nil {
log.Fatal(err)
}
if err := api.Generate(cfg, api.AddPlugin(plugin)); err != nil {
log.Fatal(err)
}
The plugin injects its own safety-net resolver template via gqlgen's ConfigMutator hook, so non-migrated fields get a panic stub without any resolver_template entry in gqlgen.yml. Setting resolver_template explicitly is still honoured and overrides the bundled template.
See the package README for a full integration walkthrough.
Index ¶
Examples ¶
Constants ¶
const DefaultKeywordPrefix = "gql"
DefaultKeywordPrefix is the prefix used by normalizeDomain when a domain name collides with a Go keyword, equals "schema", or starts with a digit.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Option ¶
type Option func(*Plugin)
Option configures a Plugin at construction time.
func WithEnabledDomains ¶
WithEnabledDomains restricts migration to the listed schema-directory names. Names are matched against the *raw* directory name (e.g. "business-process", not the normalized "businessprocess") and are case-sensitive. Duplicates are deduplicated; empty entries are dropped.
Names that don't correspond to any directory in the schema cause codegen to fail with a clear error — typos and case mismatches can't silently degrade to a no-op. This is checked at GenerateCode time, after gqlgen has loaded the schema.
Calling this option — even with no arguments — switches the plugin out of the default "all domains" mode into an explicit allowlist. WithEnabledDomains() with no arguments produces an empty allowlist, which makes the plugin a no-op (useful as a bootstrap step in an incremental migration).
Example ¶
Restrict migration to a subset of domains. Used during incremental migration of an existing project, where domains move to Tier-2 packages one at a time. Names are matched against the *raw* schema-directory name (case-sensitive). Names that are not present in the schema cause codegen to fail with a clear error so typos can't silently degrade to a no-op.
package main
import (
"fmt"
"github.com/prusov/gqldomainresolver"
)
func main() {
plugin, err := gqldomainresolver.New(
gqldomainresolver.WithEnabledDomains("todos", "users", "business-process"),
)
if err != nil {
panic(err)
}
fmt.Println(plugin.Name())
}
Output: gqldomainresolver
Example (Bootstrap) ¶
Migration bootstrap: WithEnabledDomains() with no arguments produces an explicit empty allowlist, so the plugin is a no-op. This lets a project wire the plugin into its build before migrating any domain — the first PR introduces zero diff in the existing resolvers.
package main
import (
"fmt"
"github.com/prusov/gqldomainresolver"
)
func main() {
plugin, err := gqldomainresolver.New(
gqldomainresolver.WithEnabledDomains(),
)
if err != nil {
panic(err)
}
fmt.Println(plugin.Name())
}
Output: gqldomainresolver
func WithExcludedDomains ¶ added in v0.2.0
WithExcludedDomains excludes the listed schema-directory names from migration. Like WithEnabledDomains, names are matched against the *raw* directory name (case-sensitive); duplicates are deduplicated and empty entries are dropped.
Names that don't correspond to any directory in the schema cause codegen to fail with a clear error — mirrors WithEnabledDomains so typos surface loudly rather than degrading to a silent no-op.
Can be combined with WithEnabledDomains: the allowlist is applied first, then the exclude list subtracts from the result. Used standalone (without WithEnabledDomains) it is the natural fit for a project that wants to migrate every domain *except* a known large or in-flight one — pair it with the greenfield default. WithExcludedDomains() with no arguments is allowed but a no-op.
func WithKeywordPrefix ¶
WithKeywordPrefix overrides the prefix used to disambiguate domain names that collide with Go keywords, "schema", or that start with a digit. The default is DefaultKeywordPrefix ("gql"), so e.g. an "import" directory produces package "gqlimport" and "2fa" produces "gql2fa".
The prefix must be a non-empty valid Go identifier prefix: it must start with an ASCII lowercase letter and may contain only lowercase letters and digits afterwards. Invalid prefixes cause New() to return an error.
Example ¶
Override the prefix used when a domain name collides with a Go keyword, equals "schema", or starts with a digit. The default is "gql" (DefaultKeywordPrefix), so the directory "import" produces package "gqlimport". Passing "dom" produces "domimport" instead.
package main
import (
"fmt"
"github.com/prusov/gqldomainresolver"
)
func main() {
plugin, err := gqldomainresolver.New(
gqldomainresolver.WithKeywordPrefix("dom"),
gqldomainresolver.WithEnabledDomains("import"),
)
if err != nil {
panic(err)
}
fmt.Println(plugin.Name())
}
Output: gqldomainresolver
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin implements gqlgen's ResolverImplementer + CodeGenerator.
The import path for generated domain packages is derived at codegen time from the resolver section of gqlgen.yml — the plugin is module-agnostic.
By default — New() with no options — every domain in the schema is migrated. This is the greenfield configuration.
To restrict migration to a subset of domains, pass WithEnabledDomains. This is used during incremental migration of an existing project, where domains are moved to their own packages one at a time. An explicit empty allowlist (WithEnabledDomains() with no arguments) makes the plugin a no-op — useful as a bootstrap step in a migration so the plugin can be wired up without producing any diff.
To exclude specific domains from migration — typically large or experimental domains that aren't ready to move yet — pass WithExcludedDomains. It can be combined with WithEnabledDomains: the allowlist is applied first, then the exclude list subtracts from the result.
Plugin is not safe for concurrent use. Construct one instance per api.Generate() call; gqlgen runs single-threaded today, but Implement() mutates internal state and parallel codegen would race.
func New ¶
New constructs the plugin. With no options every domain is migrated (greenfield default). Pass WithEnabledDomains to restrict migration to a specific subset — typically during incremental migration of an existing project.
Returns an error if WithKeywordPrefix was passed an invalid prefix.
Example ¶
Greenfield default. With no options every domain in the schema is migrated to its own Tier-2 package — typically what a new project wants.
package main
import (
"fmt"
"github.com/prusov/gqldomainresolver"
)
func main() {
plugin, err := gqldomainresolver.New()
if err != nil {
panic(err)
}
fmt.Println(plugin.Name())
}
Output: gqldomainresolver
func (*Plugin) GenerateCode ¶
GenerateCode generates files in domain packages. Called by api.Generate() AFTER resolvergen.
func (*Plugin) Implement ¶
Implement is called by resolvergen for each resolver field. Returns the method body string written into .resolvers.go in the root package.
Returning "" signals the safety-net template (resolver.gotpl) to skip emitting a method declaration entirely. We rely on Go method promotion: each wrapper (mutationResolver / queryResolver / subscriptionResolver) embeds the kind-specific DomainMutationResolvers / DomainQueryResolvers / DomainSubscriptionResolvers, which embed the per-domain Mixin*Mutation/Query/ Subscription structs, whose methods are promoted up through the wrapper.
Behavior, in evaluation order:
- field whose domain is enabled → "" (template skips; method lives in the domain package). Applies to root and non-root fields alike — non-root methods are reached via the per-object constructor returning the domain-package resolver.
- prevImpl != "" → preserve hand-written code in the root file. Critical for the gradual migration case: a project's existing field resolvers (e.g. (r *todoResolver) User) must survive regen until their domain is enabled.
- otherwise → panic stub.
func (*Plugin) MutateConfig ¶ added in v1.1.0
MutateConfig materializes the embedded safety-net resolver template to a temp file and points cfg.Resolver.ResolverTemplate at it, so consumers do not need to set resolver_template themselves or rely on `go mod vendor` to surface the file at a stable path.
An explicit cfg.Resolver.ResolverTemplate set by the consumer is left untouched — the plugin yields to a custom template path.
The temp file is intentionally not cleaned up: codegen is a short-lived process, the plugin cannot observe the end of api.Generate, and the OS rotates /tmp on its own. Not worth complicating the lifecycle for.