Skip to content

goxgen/goxgen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

40 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

goxgen

GitHub license GitHub stars GitHub release (latest by date) Go Report Card codecov

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

  1. Standard gen.go file with go:generate directive
    package main
    
    //go:generate go run -mod=mod github.com/goxgen/goxgen
  2. 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

πŸ“ 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/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
}

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/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.

πŸ–₯️ 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/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.

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

πŸ“ž Contact

For more information, feel free to open an issue in the repository.


Enjoy the power of single-syntax API and domain definitions with goxgen! πŸš€