goxgen

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.
π Features
- π 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.
Schema definition
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
Resource directives is a main directives for domain resource definition.
@Resource
- Your domain resource
@Field
- Field of resource
Action Directives
@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
π Quick Start
π£ Step-by-step guide
π Creating the necessary files
You should create two files in your project
- Standard
gen.go
file with go:generate
directive
package main
//go:generate go run -mod=mod github.com/ikhfa/goxgen
- Xgen config file
xgenc.go
//go:build ignore
// +build ignore
package main
import (
"context"
"fmt"
"github.com/ikhfa/goxgen/plugins/cli"
"github.com/ikhfa/goxgen/projects/basic"
"github.com/ikhfa/goxgen/projects/gorm"
"github.com/ikhfa/goxgen/xgen"
)
func main() {
xg := xgen.NewXgen(
xgen.WithPackageName("github.com/ikhfa/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
π Structure of a generated project
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.
π Providing schema
Check the schema definition section for more information.
You should provide a schema for each project and run go generate
again.
Gorm example
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/ikhfa/goxgen/cmd/internal/integration/gorm_example/generated"
"github.com/ikhfa/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
}
Creating a example schema for resources
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.
"Create User" mutation resolver
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
}
"Browse User" query resolver
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/ikhfa/goxgen/cmd/internal/integration/gorm_example/generated"
"github.com/ikhfa/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.
π₯οΈ CLI plugin usage
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
π GraphQL Playground
To enable the GraphQL playground, you can use the --gql-playground-enabled
flag.
π‘ Environment variables
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/ikhfa/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.
Structure of 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
Available environment variables
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.
Playground and testing
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"
}
]
}
Testing
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.
Available Project Types
Basic Project
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
Gorm project is a project with GORM ORM.
Pagination and Sorting
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
}
π€ Contributing
To configure git hooks, run make init
Contributions, issues, and feature requests are welcome!
Makefile
To simplify the development process, we use Makefile.
make init
- Initialize git hooks
make pre-commit
- Run pre-commit checks
make integrations-generate
- Generate an integration test project
make integrations-run
- Run integration test project
make runtime-generate
- Generate a runtime project that using for goxgen code generation
make build-readme
- Build README.md file from README.gomd
make build
- Build all and prepare release
π¦ Dependencies
π License
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
! π