hasura-ndc-go

command module
v1.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2024 License: Apache-2.0 Imports: 32 Imported by: 0

README

Native Data Connector code generator

The NDC code generator provides a set of tools to develop data connectors quickly. It's suitable for developers who create connectors for business logic functions (or action in GraphQL Engine v2).

The generator is inspired by ndc-typescript-deno and ndc-nodejs-lambda that automatically infer TypeScript functions as NDC functions/procedures for use at runtime. It's possible to do this with Go via reflection. However, code generation is better for performance, type-safe, and no magic.

Installation

Download

Get a release here.

Build from source

To install with Go 1.21+:

go install github.com/hasura/ndc-sdk-go/cmd/hasura-ndc-go@latest

How to Use

❯ hasura-ndc-go -h
Usage: hasura-ndc-go <command>

Flags:
  -h, --help    Show context-sensitive help.

Commands:
  new --name=STRING --module=STRING
    Initialize an NDC connector boilerplate. For example:

        hasura-ndc-go new -n example -m github.com/foo/example

  update
    Generate schema and implementation for the connector from functions.

  test snapshots
    Generate test snapshots.
Initialize connector project

The new command generates a boilerplate project for connector development from template with the following folder structure:

  • functions: the folder contains query and mutation functions. The update command will parse .go files in this folder.
  • types: the folder contains reusable types such as RawConfiguration, Configuration and State.
  • connector.go: parts of Connector methods, except GetSchema, Query and Mutation methods that will be generated by the update command.
  • main.go: the main function that runs the connector CLI.
  • go.mod: the module file with required dependencies.
  • README.md: the index README file.
  • Dockerfile: the build template for Docker image.

The command requires names of the connector and module. By default, the tool creates a new folder with the connector name. If you want to customize the path or generate files in the current folder. use --output (-o) argument.

hasura-ndc-go new -n example -m github.com/foo/example -o .
Generate queries and mutations

The update command parses code in the functions folder, finds functions and types that are allowed to be exposed and generates the following files:

  • schema.generated.json: the generated connector schema in JSON format.
  • connector.generated.go: implement GetSchema, Query and Mutation methods with exposed functions.
hasura-ndc-go update

How it works

Functions

Functions that are allowed to be exposed as queries or mutations need to have a Function or Procedure prefix in the name. For example:

// FunctionHello sends a hello message
func FunctionHello(ctx context.Context, state *types.State, arguments *HelloArguments) (*HelloResult, error)

// ProcedureCreateAuthor creates an author
func ProcedureCreateAuthor(ctx context.Context, state *types.State, arguments *CreateAuthorArguments) (*CreateAuthorResult, error)

Or use @function or @procedure comment tag:

[!IMPORTANT] The first word of the comment must be the function name. Without it the parser can not find the exact comment of the function:

// Hello sends a hello message
// @function
func Hello(ctx context.Context, state *types.State, arguments *HelloArguments) (*HelloResult, error)

// CreateAuthor creates an author
// @procedure
func CreateAuthor(ctx context.Context, state *types.State, arguments *CreateAuthorArguments) (*CreateAuthorResult, error)

// you also can set the alias after the tag:

// Foo a bar
// @function bar
func Foo(ctx context.Context, state *types.State) (*FooResult, error)

Function and Procedure names will be formatted to camelCase by default.

The generator detects comments by the nearby code position. It isn't perfectly accurate in some use cases. Prefixing name in the function is highly recommended.

A function must have 2 (no argument) or 3 parameters. Context and State are always present as 2 first parameters. The result is a tuple with an expected output and error.

Function is a type of Query and Procedure is a type of mutation. Collection is usually used for database queries so it isn't used for business logic.

Types

The tool only infers arguments and result types of exposed functions to generate object type schemas:

  • Argument type must be a struct with serializable properties.
  • Result type can be a scalar, slice, or struct.
Object Types

The tool can infer properties of the struct and generate Object Type schema. The json tags will be read as properties name to be consistent with JSON Marshaller and Unmarshaller. For example, with the following type:

type CreateAuthorResult struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

// auto-generated
// func (j CreateAuthorResult) ToMap() map[string]any {
//   return map[string]any{
//     "id": j.ID,
//     "name": j.Name,
//   }
// }

the schema will be:

{
  "CreateAuthorResult": {
    "fields": {
      "id": {
        "type": {
          "name": "Int",
          "type": "named"
        }
      },
      "name": {
        "type": {
          "name": "String",
          "type": "named"
        }
      }
    }
  }
}
Arguments

Arguments must be defined as struct types. The generator automatically infers argument types in functions to generate schemas and Encoder and Decoder methods. Value fields are treated as required and pointer fields are optional.

type HelloArguments struct {
	Greeting string `json:"greeting"` // value argument will be required
	Count    *int   `json:"count"`    // pointer arguments are optional
}
Scalar Types

Supported types

The basic scalar types supported are:

Name Native type Description JSON representation
Boolean bool A 8-bit signed integer with a minimum value of -2^7 and a maximum value of 2^7 - 1 boolean
String string String string
Int8 int8, uin8 A 8-bit signed integer with a minimum value of -2^7 and a maximum value of 2^7 - 1 int8
Int16 int16, uint16 A 16-bit signed integer with a minimum value of -2^15 and a maximum value of 2^15 - 1 int16
Int32 int32, uint32 A 32-bit signed integer with a minimum value of -2^31 and a maximum value of 2^31 - 1 int32
Int64 int64, uint64 A 64-bit signed integer with a minimum value of -2^63 and a maximum value of 2^63 - 1 int64
Bigint scalar.BigInt A 64-bit signed integer with a minimum value of -2^63 and a maximum value of 2^63 - 1 string
Float32 float32 An IEEE-754 single-precision floating-point number float32
Float64 float64 An IEEE-754 double-precision floating-point number float64
UUID github.com/google/uuid.UUID UUID string (8-4-4-4-12) uuid
Date scalar.Date ISO 8601 date string
TimestampTZ time.Time ISO 8601 timestamp-with-timezone string
Enum Enumeration values string
Bytes scalar.Bytes Base64-encoded bytes string
JSON any, map[K]V Arbitrary JSON json
RawJSON json.RawMessage Raw arbitrary JSON json

[!NOTE] We don't recommend to use the RawJSON scalar for function arguments because the decoder will re-encode the value to []byte that isn't performance-wise.

Alias scalar types will be inferred to the origin type in the schema.

// the scalar type in schema is still a `String`.
type Text string

If you want to define a custom scalar type, the type name must have a Scalar prefix or @scalar tag in the comment. The generator doesn't care about the underlying type even if it is a struct.

[!IMPORTANT] Require the type name at the head of the comment if using comment tags.

type ScalarFoo struct {
  bar string
}
// output: Foo
// auto-generated
// func (j ScalarFoo) ScalarName() string {
//   return "Foo"
// }


// Tag a tag scalar
// @scalar
type Tag struct {
  tag string
}
// output: Tag

// Foo a foo scalar
// @scalar Bar
type Foo struct {}
// output: Bar

For custom scalar, you must implement a method to decode any value so its data can be set when resolving request query arguments. UnmarshalJSON is also used when encoding results.

func (c *ScalarFoo) FromValue(value any) (err error) {
	c.Bar, err = utils.DecodeString(value)
	return
}

func (c *ScalarFoo) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}

	c.bar = s

	return nil
}
Enum

You can define the enum type with @enum comment tag. The data type must be an alias of string.

// @enum <values separated by commas>
//
// example:
//
// SomeEnum some enum
// @enum foo, bar
type SomeEnum string

The tool will help generate schema, constants and helper methods for it.

{
  "scalar_types": {
    "SomeEnum": {
      "aggregate_functions": {},
      "comparison_operators": {},
      "representation": {
        "one_of": ["foo", "bar"],
        "type": "enum"
      }
    }
  }
}

Enum scalar

Limitation

Comments of types from third-party packages can't be parsed. If you want to use scalars from 3rd dependencies, you must wrap them with type alias.

Documentation

The tool parses comments of functions and types by the nearby code position to describe properties in the schema. For example:

// Creates an author
func ProcedureCreateAuthor(ctx context.Context, state *types.State, arguments *CreateAuthorArguments) (*CreateAuthorResult, error)

// {
//   "name": "createAuthor",
//   "description": "Creates an author",
//   ...
// }

Example

See example/codegen.

Test Snapshots

The tool supports test snapshots generation for query and mutation requests and responses that are compatible with ndc-test replay command. See generated snapshots at the codegen example.

Usage: hasura-ndc-go test snapshots

Generate test snapshots.

Flags:
  -h, --help                     Show context-sensitive help.
      --log-level="info"         Log level.

      --schema=STRING            NDC schema file path. Use either endpoint or schema path
      --endpoint=STRING          The endpoint of the connector. Use either endpoint or schema path
      --dir=STRING               The directory of test snapshots.
      --depth=10                 The selection depth of nested fields in result types.
      --seed=SEED                Using a fixed seed will produce the same output on every run.
      --query=QUERY,...          Specify individual queries to be generated. Separated by commas, or 'all' for all queries
      --mutation=MUTATION,...    Specify individual mutations to be generated. Separated by commas, or 'all' for all mutations
      --strategy="none"          Decide the strategy to do when the snapshot file exists.

The command accepts either a connector --endpoint or a JSON --schema file.

Endpoint

hasura-ndc-go test snapshots --endpoint http://localhost:8080 --dir testdata

NDC Schema

hasura-ndc-go test snapshots --schema schema.generated.json --dir testdata

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
Package version implements cli handling.
Package version implements cli handling.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL