Skip to content

Commit 8023547

Browse files
committed
add contribution guide for merged graphql schema
1 parent ad548af commit 8023547

File tree

1 file changed

+96
-1
lines changed

1 file changed

+96
-1
lines changed

Diff for: docs/CONTRIBUTING.MD

+96-1
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,99 @@ go mod tidy
197197

198198
Contributors should close conversations when complete. Reviewers may reopen if needed.
199199

200-
For additional help, see our [FAQ](./FAQ.md) or open a GitHub issue.
200+
For additional help, see our [FAQ](./FAQ.md) or open a GitHub issue.
201+
202+
# Contributing to GraphQL Schema and Resolvers
203+
204+
## Overview
205+
206+
Our GraphQL setup uses a **hybrid approach** to schema management. We have individual GraphQL schemas defined within each module (e.g., `modules/core/`, `modules/warehouse/`), but we also generate a **unified schema** at the top level (`graph/`) primarily to **enable correct GraphQL introspection** for development tools like Postman, Insomnia, or Apollo Sandbox.
207+
208+
**Why this approach?**
209+
210+
* **Introspection:** The previous method of registering module schemas only at runtime prevented standard introspection tools from seeing the complete API schema. This made development and testing difficult.
211+
* **Unified View:** The build-time merge creates `graph/schema.merged.graphql` and generates corresponding Go code (`graph/generated.go`, `graph/models_gen.go`) that represents the entire API surface. This allows tools to introspect correctly.
212+
* **Runtime Execution:** Currently, the server *still* uses the original `app.RegisterGraphSchema()` mechanism at runtime. This means the actual query execution relies on the schemas defined and registered within each module individually. *([Note: This might be refactored in the future to use the unified schema directly at runtime, which would simplify the process]).*
213+
214+
## Current Limitations & Workflow
215+
216+
Because we merge schemas via simple concatenation for the build-time generation step, but modules still need their *own* complete schemas for runtime registration, there's a necessary, slightly awkward workflow involving commenting/uncommenting common definitions:
217+
218+
1. **Duplicate Definitions:** Common scalars (like `scalar Time`, `scalar Int64`) and base types (`type Query`, `type Mutation`, `type Subscription`) should ideally be defined *only once* (e.g., in `modules/core/interfaces/graph/base.graphql`).
219+
2. **Before `go generate`:** To allow the top-level `go generate ./graph/...` to succeed using the merged schema, you **MUST temporarily comment out** any re-definitions of these common scalars/types in other modules' `.graphql` files (e.g., comment out `scalar Time` in `modules/warehouse/interfaces/graph/base.graphql`).
220+
3. **After `go generate`:** You **MUST uncomment** those lines back in the module `.graphql` files. This is because the runtime `app.RegisterGraphSchema()` for that module needs the complete schema definition, including those scalars, to work correctly.
221+
222+
**Yes, this comment/uncomment step is cumbersome and error-prone.** It's a known trade-off of this hybrid approach. Adhering strictly to defining common types only once (See Solution 1 / Best Practice mentioned previously) and refactoring the runtime to use the single generated `ExecutableSchema` would eliminate this step.
223+
224+
## Build Process Summary
225+
226+
Running `go generate ./graph/...` from the project root performs these steps:
227+
228+
## How Resolvers Work
229+
230+
We have two "layers" of resolvers:
231+
232+
1. **Module-Specific Resolvers:**
233+
* Located in `modules/<module_name>/interfaces/graph/*.resolvers.go`.
234+
* These contain the **actual business logic** for fetching data, calling services, etc.
235+
* They are associated with the schemas loaded individually at runtime via `app.RegisterGraphSchema`.
236+
* **Example:** `modules/core/interfaces/graph/users.resolvers.go` implements the logic for the `user` and `users` queries defined in `modules/core/interfaces/graph/users.graphql`.
237+
238+
2. **Unified Top-Level Resolvers:**
239+
* Located in `graph/resolver.go` and `graph/*.resolvers.go`.
240+
* These are generated based on the *merged* schema (`schema.merged.graphql`).
241+
* The main `graph/resolver.go` defines a `Resolver` struct that holds references to the *module-specific* resolvers (or the main `app` instance).
242+
```go
243+
// graph/resolver.go
244+
type Resolver struct {
245+
app application.Application
246+
coreResolver *coregraph.Resolver // From modules/core/interfaces/graph
247+
warehouseResolver *warehousegraph.Resolver // From modules/warehouse/interfaces/graph
248+
// ... other module resolvers
249+
}
250+
251+
func NewResolver(app application.Application) *Resolver {
252+
// ... instantiate module resolvers ...
253+
return &Resolver{ /* ... */ }
254+
}
255+
```
256+
* The implementation files (e.g., `graph/schema.merged.resolvers.go`) primarily **delegate** calls to the appropriate module-specific resolver.
257+
```go
258+
// graph/schema.merged.resolvers.go (Illustrative)
259+
func (r *queryResolver) User(ctx context.Context, id int64) (*User, error) { // Uses graph.User model
260+
// Delegate to the core module's resolver
261+
coreUserResult, err := r.coreResolver.Query().User(ctx, id) // Calls core's User resolver
262+
if err != nil {
263+
return nil, err
264+
}
265+
return (*User)(coreUserResult), nil // Direct cast if structs are identical
266+
}
267+
268+
func (r *queryResolver) WarehousePosition(ctx context.Context, id int64) (*WarehousePosition, error) { // Uses graph.WarehousePosition
269+
// Delegate to the warehouse module's resolver
270+
warehousePosResult, err := r.warehouseResolver.Query().WarehousePosition(ctx, id) // Calls warehouse's resolver
271+
if err != nil {
272+
return nil, err
273+
}
274+
// return warehousemappers.PositionToGraphModel(warehousePosResult), nil // Example using mapper
275+
return (*WarehousePosition)(warehousePosResult), nil // Direct cast if structs are identical
276+
}
277+
```
278+
279+
## How to Add/Modify GraphQL Fields
280+
281+
1. **Define Schema:** Add/modify types, queries, or mutations in the relevant module's `.graphql` file(s) (e.g., `modules/warehouse/interfaces/graph/new_feature.graphql`).
282+
2. **Implement Logic:** Add the corresponding resolver method implementation in the module's `*.resolvers.go` file (e.g., `modules/warehouse/interfaces/graph/new_feature.resolvers.go`). This implementation should contain the actual business logic.
283+
3. **Prepare for Generation:** **Temporarily comment out** duplicate scalar/base type definitions in non-core `.graphql` files.
284+
4. **Generate Unified Code:** Run `go generate ./graph/...` from the project root. This updates/creates:
285+
* `graph/generated.go`
286+
* `graph/models_gen.go`
287+
* Stubs for new resolvers in `graph/*.resolvers.go` (e.g., `graph/schema.merged.resolvers.go`).
288+
5. **Restore Source Schemas:** **Uncomment** the lines commented out in Step 3.
289+
6. **Implement Delegation:** Go to the generated stub in the top-level `graph/*.resolvers.go` file. Implement the method by:
290+
* Getting the correct module resolver instance (e.g., `r.warehouseResolver`).
291+
* Calling the corresponding method you implemented in the module resolver (Step 2).
292+
* Mapping the result to the top-level generated model type if necessary (often a simple type cast `(*graph.MyType)(result)` works if the underlying structs are the same, otherwise use a mapper).
293+
7. **Commit:** Commit changes to the module's `.graphql` and `*.resolvers.go` files, the top-level `graph/*.resolvers.go` file containing the delegation, and all updated generated files in `graph/`.
294+
295+
This workflow allows us to have working introspection while keeping the resolver logic located within the relevant module. Remember the manual comment/uncomment steps before and after generation until the runtime is potentially refactored.

0 commit comments

Comments
 (0)