github.com/hasura/ndc-sdk-go/cmd/hasura-ndc-go@v0.0.0-20240508172728-e960be013ca2/README.md (about)

     1  # Native Data Connector code generator
     2  
     3  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).
     4  
     5  The generator is inspired by [ndc-typescript-deno](https://github.com/hasura/ndc-typescript-deno) and [ndc-nodejs-lambda](https://github.com/hasura/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.
     6  
     7  ## Installation
     8  
     9  ### Download
    10  
    11  Get a release [here](https://github.com/hasura/ndc-sdk-go/releases).
    12  
    13  ### Build from source
    14  
    15  To install with Go 1.21+:
    16  
    17  ```bash
    18  go install github.com/hasura/ndc-sdk-go/cmd/hasura-ndc-go@latest
    19  ```
    20  
    21  ## How to Use
    22  
    23  ```bash
    24  ❯ hasura-ndc-go -h
    25  Usage: hasura-ndc-go <command>
    26  
    27  Flags:
    28    -h, --help    Show context-sensitive help.
    29  
    30  Commands:
    31    new --name=STRING --module=STRING
    32      Initialize an NDC connector boilerplate. For example:
    33  
    34          hasura-ndc-go new -n example -m github.com/foo/example
    35  
    36    generate
    37      Generate schema and implementation for the connector from functions.
    38  
    39    test snapshots
    40      Generate test snapshots.
    41  ```
    42  
    43  ### Initialize connector project
    44  
    45  The `new` command generates a boilerplate project for connector development from [template](templates/new) with the following folder structure:
    46  
    47  - `functions`: the folder contains query and mutation functions. The `generate` command will parse `.go` files in this folder.
    48  - `types`: the folder contains reusable types such as `RawConfiguration`, `Configuration` and `State`.
    49  - `connector.go`: parts of Connector methods, except `GetSchema`, `Query` and `Mutation` methods that will be generated by the `generate` command.
    50  - `main.go`: the main function that runs the connector CLI.
    51  - `go.mod`: the module file with required dependencies.
    52  - `README.md`: the index README file.
    53  - `Dockerfile`: the build template for Docker image.
    54  
    55  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.
    56  
    57  ```bash
    58  hasura-ndc-go new -n example -m github.com/foo/example -o .
    59  ```
    60  
    61  ### Generate queries and mutations
    62  
    63  The `generate` command parses code in the `functions` folder, finds functions and types that are allowed to be exposed and generates the following files:
    64  
    65  - `schema.generated.json`: the generated connector schema in JSON format.
    66  - `connector.generated.go`: implement `GetSchema`, `Query` and `Mutation` methods with exposed functions.
    67  
    68  ```bash
    69  hasura-ndc-go generate
    70  ```
    71  
    72  ## How it works
    73  
    74  ### Functions
    75  
    76  Functions that are allowed to be exposed as queries or mutations need to have a `Function` or `Procedure` prefix in the name. For example:
    77  
    78  ```go
    79  // FunctionHello sends a hello message
    80  func FunctionHello(ctx context.Context, state *types.State, arguments *HelloArguments) (*HelloResult, error)
    81  
    82  // ProcedureCreateAuthor creates an author
    83  func ProcedureCreateAuthor(ctx context.Context, state *types.State, arguments *CreateAuthorArguments) (*CreateAuthorResult, error)
    84  ```
    85  
    86  Or use `@function` or `@procedure` comment tag:
    87  
    88  > [!IMPORTANT]
    89  > The first word of the comment must be the function name. Without it the parser can not find the exact comment of the function:
    90  
    91  ```go
    92  // Hello sends a hello message
    93  // @function
    94  func Hello(ctx context.Context, state *types.State, arguments *HelloArguments) (*HelloResult, error)
    95  
    96  // CreateAuthor creates an author
    97  // @procedure
    98  func CreateAuthor(ctx context.Context, state *types.State, arguments *CreateAuthorArguments) (*CreateAuthorResult, error)
    99  
   100  // you also can set the alias after the tag:
   101  
   102  // Foo a bar
   103  // @function bar
   104  func Foo(ctx context.Context, state *types.State) (*FooResult, error)
   105  ```
   106  
   107  Function and Procedure names will be formatted to `camelCase` by default.
   108  
   109  > 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.
   110  
   111  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`.
   112  
   113  > [Function](https://hasura.github.io/ndc-spec/specification/schema/functions.html) is a type of Query and [Procedure](https://hasura.github.io/ndc-spec/specification/schema/procedures.html) is a type of mutation. [Collection](https://hasura.github.io/ndc-spec/specification/schema/collections.html) is usually used for database queries so it isn't used for business logic.
   114  
   115  ### Types
   116  
   117  The tool only infers arguments and result types of exposed functions to generate object type schemas:
   118  
   119  - Argument type must be a struct with serializable properties.
   120  - Result type can be a scalar, slice, or struct.
   121  
   122  #### Object Types
   123  
   124  The tool can infer properties of the struct and generate [Object Type](https://hasura.github.io/ndc-spec/specification/schema/object-types.html) schema. The `json` tags will be read as properties name to be consistent with `JSON Marshaller` and `Unmarshaller`. For example, with the following type:
   125  
   126  ```go
   127  type CreateAuthorResult struct {
   128  	ID   int    `json:"id"`
   129  	Name string `json:"name"`
   130  }
   131  
   132  // auto-generated
   133  // func (j CreateAuthorResult) ToMap() map[string]any {
   134  //   return map[string]any{
   135  //     "id": j.ID,
   136  //     "name": j.Name,
   137  //   }
   138  // }
   139  ```
   140  
   141  the schema will be:
   142  
   143  ```json
   144  {
   145    "CreateAuthorResult": {
   146      "fields": {
   147        "id": {
   148          "type": {
   149            "name": "Int",
   150            "type": "named"
   151          }
   152        },
   153        "name": {
   154          "type": {
   155            "name": "String",
   156            "type": "named"
   157          }
   158        }
   159      }
   160    }
   161  }
   162  ```
   163  
   164  #### Arguments
   165  
   166  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.
   167  
   168  ```go
   169  type HelloArguments struct {
   170  	Greeting string `json:"greeting"` // value argument will be required
   171  	Count    *int   `json:"count"`    // pointer arguments are optional
   172  }
   173  ```
   174  
   175  #### Scalar Types
   176  
   177  **Supported types**
   178  
   179  The basic scalar types supported are:
   180  
   181  | Name        | Native type                 | Description                                                                           | JSON representation |
   182  | ----------- | --------------------------- | ------------------------------------------------------------------------------------- | ------------------- |
   183  | Boolean     | bool                        | A 8-bit signed integer with a minimum value of -2^7 and a maximum value of 2^7 - 1    | boolean             |
   184  | String      | string                      | String                                                                                | string              |
   185  | Int8        | int8, uin8                  | A 8-bit signed integer with a minimum value of -2^7 and a maximum value of 2^7 - 1    | int8                |
   186  | Int16       | int16, uint16               | A 16-bit signed integer with a minimum value of -2^15 and a maximum value of 2^15 - 1 | int16               |
   187  | Int32       | int32, uint32               | A 32-bit signed integer with a minimum value of -2^31 and a maximum value of 2^31 - 1 | int32               |
   188  | Int64       | int64, uint64               | A 64-bit signed integer with a minimum value of -2^63 and a maximum value of 2^63 - 1 | int64               |
   189  | Bigint      | scalar.BigInt               | A 64-bit signed integer with a minimum value of -2^63 and a maximum value of 2^63 - 1 | string              |
   190  | Float32     | float32                     | An IEEE-754 single-precision floating-point number                                    | float32             |
   191  | Float64     | float64                     | An IEEE-754 double-precision floating-point number                                    | float64             |
   192  | UUID        | github.com/google/uuid.UUID | UUID string (8-4-4-4-12)                                                              | uuid                |
   193  | Date        | scalar.Date                 | ISO 8601 date                                                                         | string              |
   194  | TimestampTZ | time.Time                   | ISO 8601 timestamp-with-timezone                                                      | string              |
   195  | Enum        |                             | Enumeration values                                                                    | string              |
   196  | Bytes       | scalar.Bytes                | Base64-encoded bytes                                                                  | string              |
   197  | JSON        | any, map[K]V                | Arbitrary JSON                                                                        | json                |
   198  | RawJSON     | json.RawMessage             | Raw arbitrary JSON                                                                    | json                |
   199  
   200  > [!NOTE]
   201  > 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.
   202  
   203  Alias scalar types will be inferred to the origin type in the schema.
   204  
   205  ```go
   206  // the scalar type in schema is still a `String`.
   207  type Text string
   208  ```
   209  
   210  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.
   211  
   212  > [!IMPORTANT]
   213  > Require the type name at the head of the comment if using comment tags.
   214  
   215  ```go
   216  type ScalarFoo struct {
   217    bar string
   218  }
   219  // output: Foo
   220  // auto-generated
   221  // func (j ScalarFoo) ScalarName() string {
   222  //   return "Foo"
   223  // }
   224  
   225  
   226  // Tag a tag scalar
   227  // @scalar
   228  type Tag struct {
   229    tag string
   230  }
   231  // output: Tag
   232  
   233  // Foo a foo scalar
   234  // @scalar Bar
   235  type Foo struct {}
   236  // output: Bar
   237  ```
   238  
   239  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.
   240  
   241  ```go
   242  func (c *ScalarFoo) FromValue(value any) (err error) {
   243  	c.Bar, err = utils.DecodeString(value)
   244  	return
   245  }
   246  
   247  func (c *ScalarFoo) UnmarshalJSON(b []byte) error {
   248  	var s string
   249  	if err := json.Unmarshal(b, &s); err != nil {
   250  		return err
   251  	}
   252  
   253  	c.bar = s
   254  
   255  	return nil
   256  }
   257  ```
   258  
   259  #### Enum
   260  
   261  You can define the enum type with `@enum` comment tag. The data type must be an alias of string.
   262  
   263  ```go
   264  // @enum <values separated by commas>
   265  //
   266  // example:
   267  //
   268  // SomeEnum some enum
   269  // @enum foo, bar
   270  type SomeEnum string
   271  ```
   272  
   273  The tool will help generate schema, constants and helper methods for it.
   274  
   275  ```json
   276  {
   277    "scalar_types": {
   278      "SomeEnum": {
   279        "aggregate_functions": {},
   280        "comparison_operators": {},
   281        "representation": {
   282          "one_of": ["foo", "bar"],
   283          "type": "enum"
   284        }
   285      }
   286    }
   287  }
   288  ```
   289  
   290  ![Enum scalar](../../assets/scalar-enum.gif)
   291  
   292  #### Limitation
   293  
   294  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.
   295  
   296  ### Documentation
   297  
   298  The tool parses comments of functions and types by the nearby code position to describe properties in the schema. For example:
   299  
   300  ```go
   301  // Creates an author
   302  func ProcedureCreateAuthor(ctx context.Context, state *types.State, arguments *CreateAuthorArguments) (*CreateAuthorResult, error)
   303  
   304  // {
   305  //   "name": "createAuthor",
   306  //   "description": "Creates an author",
   307  //   ...
   308  // }
   309  ```
   310  
   311  ## Example
   312  
   313  See [example/codegen](../../example/codegen).
   314  
   315  ## Test Snapshots
   316  
   317  The tool supports test snapshots generation for query and mutation requests and responses that are compatible with [ndc-test replay](https://github.com/hasura/ndc-spec/tree/main/ndc-test#custom-tests) command. See generated snapshots at [the codegen example](../../example/codegen/testdata).
   318  
   319  ```bash
   320  Usage: hasura-ndc-go test snapshots
   321  
   322  Generate test snapshots.
   323  
   324  Flags:
   325    -h, --help                     Show context-sensitive help.
   326        --log-level="info"         Log level.
   327  
   328        --schema=STRING            NDC schema file path. Use either endpoint or schema path
   329        --endpoint=STRING          The endpoint of the connector. Use either endpoint or schema path
   330        --dir=STRING               The directory of test snapshots.
   331        --depth=10                 The selection depth of nested fields in result types.
   332        --seed=SEED                Using a fixed seed will produce the same output on every run.
   333        --query=QUERY,...          Specify individual queries to be generated. Separated by commas, or 'all' for all queries
   334        --mutation=MUTATION,...    Specify individual mutations to be generated. Separated by commas, or 'all' for all mutations
   335        --strategy="none"          Decide the strategy to do when the snapshot file exists.
   336  ```
   337  
   338  The command accepts either a connector `--endpoint` or a JSON `--schema` file.
   339  
   340  **Endpoint**
   341  
   342  ```bash
   343  hasura-ndc-go test snapshots --endpoint http://localhost:8080 --dir testdata
   344  ```
   345  
   346  **NDC Schema**
   347  
   348  ```bash
   349  hasura-ndc-go test snapshots --schema schema.generated.json --dir testdata
   350  ```