Your One-Stop Solution for GraphQL Application Generation
goxgen is a powerful library designed to simplify the creation of GraphQL applications. By defining your domain and API interface through a single syntax, You can quickly generate a fully-functional GraphQL server. Beyond that, goxgen also provides support for ORM(GORM) and a Command-Line Interface for server operations.
Built upon the
gqlgen
framework,goxgen
extends its capabilities to offer a more streamlined developer experience.
- π Single Syntax for Domain and API: Define your domain and API interface in GraphQL schema language.
- π GraphQL: Schema-based application generation
- ποΈ ORM Support: Seamlessly integrates with various ORM systems like GORM and ENT.
- βοΈ CLI Support: Comes with a CLI tool to spin up your server application in no time.
- π Domain Driven Design: Extensible project structure
- π‘οΈ Future-Ready: Plans to roll out UI for admin back-office, along with comprehensive authentication and authorization features.
goxgen using a directives for business logic and domain definition.
All schema files in xgen has this format schema.{some_name}.graphql
, for example schema.user.graphql
Resource directives is a main directives for domain resource definition.
@Resource
- Your domain resource@Field
- Field of resource
@Action
- Action that can be done for single resource@ListAction
- Action that can be done for bulk resources@ActionField
- Field of action or list action
You should create two files in your project
- Standard
gen.go
file withgo:generate
directivepackage main //go:generate go run -mod=mod github.com/goxgen/goxgen
- Xgen config file
xgenc.go
//go:build ignore // +build ignore package main import ( "context" "fmt" "github.com/goxgen/goxgen/plugins/cli" "github.com/goxgen/goxgen/projects/basic" "github.com/goxgen/goxgen/projects/gorm" "github.com/goxgen/goxgen/xgen" ) func main() { xg := xgen.NewXgen( xgen.WithPackageName("github.com/goxgen/goxgen/cmd/internal/integration"), xgen.WithProject( "myproject", basic.NewProject(), ), xgen.WithProject( "gorm_advanced", gorm.NewProject( gorm.WithBasicProjectOption(basic.WithTestDir("tests")), ), ), xgen.WithProject( "gorm_example", gorm.NewProject( gorm.WithBasicProjectOption(basic.WithTestDir("tests")), ), ), xgen.WithPlugin(cli.NewPlugin()), ) err := xg.Generate(context.Background()) if err != nil { fmt.Println(err) } }
Then run go generate
command, and goxgen will generate project structure
go generate
After running go generate
command, goxgen will generate project structure like this
βββ gorm_advanced
β βββ generated
β β βββ server
β β βββ generated_gqlgen.go
β β βββ generated_gqlgen_models.go
β β βββ generated_xgen_directives.graphql
β β βββ generated_xgen_gorm.go
β β βββ generated_xgen_introspection.go
β β βββ generated_xgen_introspection.graphql
β β βββ generated_xgen_mappers.go
β β βββ generated_xgen_sortable.go
β βββ tests
β β βββ default-tests.yaml
β β βββ user-lifecycle.yaml
β β βββ user-pagination.yaml
β βββ graphql.config.yml
β βββ resolver.go
β βββ schema.main.graphql
β βββ schema.resolver.go
βββ gorm_example
β βββ generated
β β βββ server
β β βββ generated_gqlgen.go
β β βββ generated_gqlgen_models.go
β β βββ generated_xgen_directives.graphql
β β βββ generated_xgen_gorm.go
β β βββ generated_xgen_introspection.go
β β βββ generated_xgen_introspection.graphql
β β βββ generated_xgen_mappers.go
β β βββ generated_xgen_sortable.go
β βββ tests
β β βββ default-tests.yaml
β β βββ user-lifecycle.yaml
β βββ graphql.config.yml
β βββ resolver.go
β βββ schema.phone.graphql
β βββ schema.resolver.go
β βββ schema.user.graphql
βββ myproject
β βββ generated
β β βββ server
β β βββ generated_gqlgen.go
β β βββ generated_gqlgen_models.go
β β βββ generated_xgen_directives.graphql
β β βββ generated_xgen_introspection.go
β β βββ generated_xgen_introspection.graphql
β β βββ generated_xgen_mappers.go
β β βββ generated_xgen_sortable.go
β βββ tests
β β βββ default-tests.yaml
β βββ graphql.config.yml
β βββ resolver.go
β βββ schema.main.graphql
β βββ schema.resolver.go
β βββ schema.todo.graphql
β βββ schema.users.graphql
βββ .env
βββ .env.default
βββ .gitignore
βββ gen.go
βββ generated_xgen_cli.go
βββ gorm_advanced.db
βββ gorm_example.db
βββ gormproj.db
βββ xgenc.go
Note:
generated
directories can be ignored in git. But you can add it to git if you want.
Check the schema definition section for more information.
You should provide a schema for each project and run go generate
again.
Let's focus on gorm_example
, which uses the GORM ORM.
The connection to the GORM database can be configured from the gqlgen standard resolver.go
file in the gorm_example
directory.
resolver.go
is designed to support your custom dependency injection (DI) and any services you've provided.
package gorm_example
import (
"github.com/goxgen/goxgen/cmd/internal/integration/gorm_example/generated"
"github.com/goxgen/goxgen/plugins/cli/settings"
"gorm.io/gorm"
"embed"
"fmt"
)
//go:embed tests/*
var TestsFS embed.FS
type Resolver struct {
DB *gorm.DB
}
func NewResolver(sts *settings.EnvironmentSettings) (*Resolver, error) {
r := &Resolver{}
db, err := generated.NewGormDB(sts)
if err != nil {
return nil, fmt.Errorf("failed to create gorm db: %w", err)
}
r.DB = db
return r, nil
}
schema.user.graphql
# Define the User resource(entity) and its fields
# Enable DB mapping for the resource
type User
@Resource(Name: "user", DB: {Table: "user"})
{
id: ID! @Field(Label: "ID", DB: {Column: "id", PrimaryKey: true})
name: String! @Field(Label: "Text", DB: {Column: "name", Unique: true})
phoneNumbers: [Phone!]! @Field(Label: "Phone Numbers", DB: {})
}
# User input type for create and update actions
# Define the actions for the resource
input UserInput
@Action(Resource: "user", Action: CREATE_MUTATION, Route: "new")
@Action(Resource: "user", Action: UPDATE_MUTATION, Route: "update")
{
id: ID @ActionField(Label: "ID", MapTo: ["User.ID"])
name: String @ActionField(Label: "Name", MapTo: ["User.Name"])
phones: [PhoneNumberInput!] @ActionField(Label: "Phone Numbers", MapTo: ["User.PhoneNumbers"])
}
# User input type for browse action
input BrowseUserInput
@ListAction(Resource: "user", Action: BROWSE_QUERY, Route: "list", Pagination: true, Sort: {Default: [{by: "name", direction: ASC}]})
{
id: ID @ActionField(Label: "ID", MapTo: ["User.ID"])
name: String @ActionField(Label: "Name", MapTo: ["User.Name"])
}
schema.phone.graphql
type Phone
@Resource(Name: "phone_number", DB: {Table: "phone_number"})
{
id: ID! @Field(Label: "ID", DB: {Column: "id", PrimaryKey: true})
number: String! @Field(Label: "Number", DB: {Column: "number"})
user: User! @Field(Label: "User", DB: {})
}
input PhoneNumberInput
@Action(Resource: "phone_number", Action: CREATE_MUTATION, Route: "new")
@Action(Resource: "phone_number", Action: UPDATE_MUTATION, Route: "update")
{
id: ID @ActionField(Label: "ID", MapTo: ["Phone.ID"])
number: String @ActionField(Label: "Name", MapTo: ["Phone.Number"])
user: UserInput @ActionField(Label: "User", MapTo: ["Phone.User"])
}
After writing a custom schema You should run again gogen
command.
go generate
After regenerating the code, the schema.resolver.go
file will be updated based on your schema.
You can find the resolver functions for each field in the schema.resolver.go
file.
func (r *mutationResolver) UserCreate(ctx context.Context, input *generated.UserInput) (*generated.User, error) {
u, err := input.ToUserModel(ctx)
if err != nil {
return nil, err
}
res := r.DB.Preload(clause.Associations).Create(u)
if res.Error != nil {
return nil, res.Error
}
return u, nil
}
func (r *queryResolver) UserBrowse(ctx context.Context, where *generated.BrowseUserInput, pagination *generated.XgenPaginationInput, sort *generated.UserSortInput) ([]*generated.User, error) {
var users []*generated.User
u, err := where.ToUserModel(ctx)
if err != nil {
return nil, err
}
res := r.DB.
Preload(clause.Associations).
Scopes(
generated.Paginate(pagination), // passing `pagination` to the xgen `generated.Paginate` scope
generated.Sort(sort), // passing `sort` to the xgen `generated.Sort` scope
).
Where(&[]*generated.User{u}).
Find(&users)
return users, res.Error
}
etc.
You can add your own implementation for each function in the updated schema.resolver.go
file.
For more information,
You can read the gqlgen documentation.
In those functions, you can see that the r.DB
instance is used,
which is provided from the resolver.go
file.
package gorm_example
import (
"github.com/goxgen/goxgen/cmd/internal/integration/gorm_example/generated"
"github.com/goxgen/goxgen/plugins/cli/settings"
"gorm.io/gorm"
"embed"
"fmt"
)
//go:embed tests/*
var TestsFS embed.FS
type Resolver struct {
DB *gorm.DB
}
func NewResolver(sts *settings.EnvironmentSettings) (*Resolver, error) {
r := &Resolver{}
db, err := generated.NewGormDB(sts)
if err != nil {
return nil, fmt.Errorf("failed to create gorm db: %w", err)
}
r.DB = db
return r, nil
}
Great, you're all set to launch your GraphQL application.
To start the server using the xgen CLI plugin, you can run the following command:
go run generated_xgen_cli.go run --gql-playground-enabled
This will initialize and start your all projects GraphQL servers together, making it ready to handle incoming requests.
The output from the xgen CLI will provide information about the server endpoints. Additionally, logs will be written to this output during the server's runtime, giving you insights into its operation.
2023-10-09T00:46:43.600+0400 INFO server/server.go:77 Serving graphql playground {"project": "gorm_example", "url": "http://localhost:8080/playground"}
2023-10-09T00:46:43.600+0400 INFO server/server.go:88 Serving graphql {"project": "gorm_example", "url": "http://localhost:8080/query"}
If You have a more then one project, and you want to run only one or some projects, you can use --project
flag
go run generated_xgen_cli.go run --gql-playground-enabled --project gorm_example
Or for multiple projects
go run generated_xgen_cli.go run --gql-playground-enabled --project gorm_example --project otherproj
To enable the GraphQL playground, you can use the --gql-playground-enabled
flag.
By default, the xgen generating two dotenv files in your root directory - .env
and .env.default
.
.env.default
file is auto-generated and contains necessary environment variables for your project. Do not edit this file because it will be overwritten on each generation.# Auto generated by goxgen, do not edit manually # This is default environment variables for github.com/goxgen/goxgen/cmd/internal/integration project # gorm_advanced project default environment variables GORM_ADVANCED_PORT=8080 GORM_ADVANCED_DB_DRIVER=sqlite GORM_ADVANCED_DB_DSN=file:gorm_advanced.db?mode=rwc&cache=shared&_fk=1 # gorm_example project default environment variables GORM_EXAMPLE_PORT=8081 GORM_EXAMPLE_DB_DRIVER=sqlite GORM_EXAMPLE_DB_DSN=file:gorm_example.db?mode=rwc&cache=shared&_fk=1 # myproject project default environment variables MYPROJECT_PORT=8082 MYPROJECT_DB_DRIVER=sqlite MYPROJECT_DB_DSN=file:myproject.db?mode=rwc&cache=shared&_fk=1
.env
file is a file that you can edit and add your own environment variables. This file is not overwritten on each generation.
You can also use .env.local file for local environment variables.
Xgen CLI has a special structure for environment variables. You can define default environment variables for all projects and override them for each project with project name prefix.
{ENVIRONMENT_VARIABLE_NAME}={VALUE}
{PROJECT_NAME}_{ENVIRONMENT_VARIABLE_NAME}={VALUE}
e.g.
# Default environment variable for all projects
DB_DSN=sqllite://file.db
# Environment variable for gorm_example project
GORM_EXAMPLE_DB_DSN=postgres://user:pass@localhost:5432/gorm_example?sslmode=disable
To see all available environment variables, you can run the following command:
go run generated_xgen_cli.go run --help
For more information about the xgen CLI, you can run main help command:
go run generated_xgen_cli.go help
This will display a list of available commands, options, and descriptions to help you navigate the xgen CLI more effectively.
You can copy the URL http://localhost:80/playground
from the logs
and open it in your browser to access the GraphQL playground.
This interface will allow you to test queries, mutations, and subscriptions in real-time.
Then we see graphql playground, let's run some mutation query to add two new users
mutation{
user1: user_create(input: {name: "My user 1"}){
id
name
}
user2: user_create(input: {name: "My user 2"}){
id
name
}
}
After execution of this mutation, graphql should be return result like this
{
"user1": {
"id": 1,
"name": "My user 1"
},
"user2": {
"id": 2,
"name": "My user 2"
}
}
One more example, let's list our new users by query
query{
user_browse(where: {}){
id
name
}
}
The result of this query should be like this
{
"user_browse": [
{
"id": 1,
"name": "My user 1"
},
{
"id": 2,
"name": "My user 2"
}
]
}
Xgen has a support for custom api tests. You can write your own tests in yaml format and run it CLI command.
In generated project directory you can find tests
directory. Xgen also generates a default test file tests/default-tests.yaml
.
name: "Default tests"
tests:
- name: "Healthcheck"
query: |
query{
__schema{
__typename
}
}
expectedResult: |
{
"__schema": {
"__typename": "__Schema"
}
}
You can create your own test file and run it with CLI command.
go run generated_xgen_cli.go run --test
This command will run all tests in all projects. If you want to run tests only for one project, you can use --project
flag.
Basic project is a project without any ORM. It's a simple project with a simple structure. You can use it for your own custom implementation.
Gorm project is a project with GORM ORM.
Resolver method UserBrowse
has a Pagination
and Sort
arguments. This arguments is a set of standard pagination and sort parameters.
Xgen provides a special GORM scopes for pagination and sort functionalities. You can use it in your custom implementation.
func (r *queryResolver) UserBrowse(ctx context.Context, where *generated.BrowseUserInput, pagination *generated.XgenPaginationInput, sort *generated.UserSortInput) ([]*generated.User, error) {
var users []*generated.User
u, err := where.ToUserModel(ctx)
if err != nil {
return nil, err
}
res := r.DB.
Preload(clause.Associations).
Scopes(
generated.Paginate(pagination), // passing `pagination` to the xgen `generated.Paginate` scope
generated.Sort(sort), // passing `sort` to the xgen `generated.Sort` scope
).
Where(&[]*generated.User{u}).
Find(&users)
return users, res.Error
}
To configure git hooks, run
make init
Contributions, issues, and feature requests are welcome!
To simplify the development process, we use Makefile.
make init
- Initialize git hooksmake pre-commit
- Run pre-commit checksmake integrations-generate
- Generate an integration test projectmake integrations-run
- Run integration test projectmake runtime-generate
- Generate a runtime project that using for goxgen code generationmake build-readme
- Build README.md file from README.gomdmake build
- Build all and prepare release
Apache 2.0
For more information, feel free to open an issue in the repository.
Enjoy the power of single-syntax API and domain definitions with goxgen
! π