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 ```