├── README.md ├── builder-kit ├── .gitignore ├── README.md ├── assets │ ├── codegen-cli.png │ └── codegen-tab.png ├── package.json ├── src │ ├── CodegenOutput │ │ └── .gitkeep │ ├── debug.ts │ ├── derivedAndWholeSchemaCodegenNotes.ts1 │ ├── exampleDerivePayload.ts │ ├── index.ts │ ├── languages-functional │ │ ├── go.ts │ │ ├── index.ts │ │ ├── java.ts │ │ ├── jsdoc.ts │ │ ├── kotlin.ts │ │ ├── python.ts │ │ └── typescript.ts │ ├── nodemon.json │ ├── schemaTools.ts │ ├── templates │ │ ├── goServeMux.codegen.ts │ │ ├── goServeMux.ts │ │ ├── http4kContractRoutes.todo.kt │ │ ├── http4kServerlessLambda.todo.kt │ │ ├── index.ts │ │ ├── javaSpringBoot.codegen.ts │ │ ├── javaSpringBoot.ts │ │ ├── javascriptJSDocExpress.codegen.ts │ │ ├── javascriptJSDocExpress.ts │ │ ├── kotlinHttp4k.codegen.ts │ │ ├── kotlinHttp4k.ts │ │ ├── kotlinKtor.codegen.ts │ │ ├── kotlinKtor.ts │ │ ├── pythonFastAPI.codegen.ts │ │ ├── pythonFastAPI.ts │ │ ├── pythonFlask.codegen.ts │ │ ├── pythonFlask.ts │ │ ├── rubyRails.codegen.ts │ │ ├── rubyRails.ts │ │ ├── typescriptExpress.codegen.ts │ │ └── typescriptExpress.ts │ ├── types.ts │ └── utils.ts ├── tsconfig.json ├── webpack.config.ts └── yarn.lock ├── frameworks.json ├── go-serve-mux └── actions-codegen.js ├── http4k-basic └── actions-codegen.js ├── java-spring-boot └── actions-codegen.js ├── javascript-express └── actions-codegen.js ├── javascript-js-doc-express └── actions-codegen.js ├── kotlin-http4k └── actions-codegen.js ├── kotlin-ktor └── actions-codegen.js ├── nodejs-azure-function └── actions-codegen.js ├── nodejs-express ├── actions-codegen.js ├── nodejs-express.zip └── starter-kit │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ └── server.js ├── nodejs-zeit ├── actions-codegen.js ├── nodejs-zeit.zip └── starter-kit │ ├── .gitignore │ ├── README.md │ ├── api │ └── hello.js │ ├── package-lock.json │ └── package.json ├── python-fast-api └── actions-codegen.js ├── python-flask └── actions-codegen.js ├── ruby-rails └── actions-codegen.js ├── typescript-express └── actions-codegen.js └── typescript-zeit ├── actions-codegen.js ├── starter-kit ├── .gitignore ├── README.md ├── api │ └── hello.ts ├── package-lock.json └── package.json └── typescript-zeit.zip /README.md: -------------------------------------------------------------------------------- 1 | # Codegens for Hasura 2 | 3 | This repo contains a list of codegens for Hasura (CLI & Console). 4 | This repo also contains a builder-kit for building your own codegen. 5 | 6 | ## What is codegen? 7 | 8 | Codegen for Hasura generates working boilerplate code in your language/framework for action handlers (currently, we may do more in the future!). 9 | 10 | ## Using a codegen 11 | 12 | To use Hasura codegen, head to the Hasura docs. It can be used via the CLI or the console. 13 | 14 | ## Building a codegen 15 | 16 | To build and publish a codegen for your favourite language and framework (thank you ❤️!) head to the [builder-kit docs](./builder-kit/README.md)! 17 | -------------------------------------------------------------------------------- /builder-kit/.gitignore: -------------------------------------------------------------------------------- 1 | ./src/CodegenOutput/* 2 | node_modules 3 | -------------------------------------------------------------------------------- /builder-kit/README.md: -------------------------------------------------------------------------------- 1 | # Contributing to Hasura codegen 2 | 3 | ## Introduction 4 | 5 | The specification for building a codegen, at it's most basic form, is an exposed function called `templater()` which takes `actionName`, `actionSdl`, and optional/nullable metadata if the action is derived (`derive`) and returns an array of objects with keys `name` and `content`. These objects are used to create each file entry generated by the codegen. 6 | 7 | This codegen would create a Markdown file with the content of the Action's GraphQL schema document: 8 | 9 | ```ts 10 | interface DeriveParams { 11 | // The SDL for the Derived operation 12 | operation: string 13 | // Endpoint to the Hasura instance 14 | endpoint: string 15 | } 16 | ``` 17 | 18 | ```ts 19 | const templater = ( 20 | actionName: string, 21 | actionSdl: string, 22 | derive: DeriveParams | null 23 | ) => { 24 | const response = [ 25 | { 26 | name: actionName + '.md', 27 | content: JSON.stringify(actionSdl), 28 | }, 29 | ] 30 | return response 31 | } 32 | ``` 33 | 34 | In order to make the process of creating new codegens as easy as possible, a higher-level API has been constructed which provides convenience utilities and most of the information you would likely want to use. 35 | 36 | **There are two parts to these utility API's:** 37 | 38 | 1. A `buildActionTypes()` function, which takes the `actionName` and `actionSdl`, and returns the following type: 39 | 40 | ```ts 41 | /** 42 | * An interface for the paramaters of Action codegen functions 43 | * The type provides the name, return type, list of action arguments 44 | * and a type-map of all types in the Action SDL 45 | */ 46 | export interface ActionParams { 47 | actionName: string 48 | actionArgs: InputValueApi[] 49 | returnType: string 50 | typeMap: ITypeMap 51 | } 52 | ``` 53 | 54 | The `actionArgs` here is an array of `InputValueAPI` types from the [graphql-extra](https://github.com/vadistic/graphql-extra) library (which powers most of the codegen library). This tool provides typed, high-level bindings for manipulating and working with GraphQL AST's and Document SDL's. The `ITypeMap` type is an object of enum and field/input types with their corresponding `graphql-extra` API values. 55 | 56 | Using `buildActionTypes()` from a codegen looks like this: 57 | 58 | ```ts 59 | import { buildActionTypes } from '../schemaTools' 60 | import { DeriveParams } from '../types' 61 | 62 | const templater = ( 63 | actionName: string, 64 | actionSdl: string, 65 | derive: DeriveParams | null 66 | ) => { 67 | const actionParams = buildActionTypes(actionName, actionSdl) 68 | // .... 69 | } 70 | ``` 71 | 72 | 2. Custom-made **GraphQL-Schema-to-Language-Type converters** for common languages that allow us to provide you with matching type definitions based on the Action SDL for your codegen. 73 | 74 | These are created by taking a GraphQL schema, and passing it's `typeMap` through language-specific converters to generate the equivalent representations of the types in the corresponding language. Using a type converter in a codegen looks like this: 75 | 76 | ```ts 77 | import { graphqlSchemaToTypescript } from '../languages-functional' 78 | import { typescriptExpressTemplate } from '../templates' 79 | import { buildActionTypes } from '../schemaTools' 80 | import { DeriveParams } from '../types' 81 | 82 | const templater = ( 83 | actionName: string, 84 | actionSdl: string, 85 | derive: DeriveParams | null 86 | ) => { 87 | const typeDefs = graphqlSchemaToTypescript(actionSdl) 88 | // ... 89 | } 90 | ``` 91 | 92 | ## Architecture of a Codegen Template 93 | 94 | Putting these two together, `buildActionTypes()` and optionally a language type-converter, makes our job really easy. You will notice a standard pattern among the codegen templates, they all look something like this: 95 | 96 | ```ts 97 | const templater = ( 98 | actionName: string, 99 | actionSdl: string, 100 | derive: DeriveParams | null 101 | ) => { 102 | const actionParams = buildActionTypes(actionName, actionSdl) 103 | const templateParams = { ...actionParams, derive } 104 | 105 | const codegen = typescriptExpressTemplate({ 106 | ...templateParams, 107 | typeDefs: graphqlSchemaToTypescript(actionSdl), 108 | }) 109 | 110 | const response = [ 111 | { 112 | name: actionName + 'TypescriptExpress.ts', 113 | content: codegen, 114 | }, 115 | ] 116 | 117 | return response 118 | } 119 | 120 | // In Typescript, this is needed to expose templater() to global namespace 121 | globalThis.templater = templater 122 | ``` 123 | 124 | The piece that changes betwen codegens, is which _template function_ the action information and language types are passed to. A "template function" is a pattern which emerged out of this architecture, and is a method containing an ES6 template-string which takes a type of `CodegenTemplateParams`. This type is an extension of `ActionParams` which optionally includes type-definition strings, and derived operation info: 125 | 126 | ```ts 127 | interface CodegenTemplateParams extends ActionParams { 128 | typeDefs?: string 129 | derive: DeriveParams | null 130 | } 131 | ``` 132 | 133 | Let's have a look at the Typescript + Express codegen template: 134 | 135 | ```ts 136 | import { html as template } from 'common-tags' 137 | import { CodegenTemplateParams } from '../types' 138 | 139 | const sampleValues = { 140 | Int: 1111, 141 | String: '""', 142 | Boolean: false, 143 | Float: 11.11, 144 | ID: 1111, 145 | } 146 | 147 | export const typescriptExpressTemplate = (params: CodegenTemplateParams) => { 148 | const { 149 | actionArgs, 150 | actionName, 151 | returnType, 152 | typeDefs, 153 | derive, 154 | typeMap, 155 | } = params 156 | 157 | const returnTypeDef = typeMap.types[returnType] 158 | 159 | const baseTemplate = template` 160 | import { Request, Response } from 'express' 161 | ${typeDefs} 162 | 163 | function ${actionName}Handler(args: ${actionName}Args): ${returnType} { 164 | return { 165 | ${returnTypeDef 166 | .map((f) => { 167 | return `${f.getName()}: ${ 168 | sampleValues[f.getType().getTypename()] || sampleValues['String'] 169 | }` 170 | }) 171 | .join(',\n')}, 172 | } 173 | } 174 | 175 | // Request Handler 176 | app.post('/${actionName}', async (req: Request, res: Response) => { 177 | // get request input 178 | const params: ${actionName}Args = req.body.input 179 | 180 | // run some business logic 181 | const result = ${actionName}Handler(params) 182 | 183 | /* 184 | // In case of errors: 185 | return res.status(400).json({ 186 | message: "error happened" 187 | }) 188 | */ 189 | 190 | // success 191 | return res.json(result) 192 | }) 193 | ` 194 | 195 | const hasuraOperation = ' `' + derive?.operation + '`\n\n' 196 | 197 | const derivedTemplate = 198 | template` 199 | import { Request, Response } from 'express' 200 | import fetch from 'node-fetch' 201 | ${typeDefs} 202 | const HASURA_OPERATION =` + 203 | hasuraOperation + 204 | template` 205 | 206 | const execute = async (variables) => { 207 | const fetchResponse = await fetch('http://localhost:8080/v1/graphql', { 208 | method: 'POST', 209 | body: JSON.stringify({ 210 | query: HASURA_OPERATION, 211 | variables, 212 | }), 213 | }) 214 | const data = await fetchResponse.json() 215 | console.log('DEBUG: ', data) 216 | return data 217 | } 218 | 219 | // Request Handler 220 | app.post('/${actionName}', async (req: Request, res: Response) => { 221 | // get request input 222 | const params: ${actionName}Args = req.body.input 223 | // execute the parent operation in Hasura 224 | const { data, errors } = await execute(params) 225 | if (errors) return res.status(400).json(errors[0]) 226 | // run some business logic 227 | 228 | // success 229 | return res.json(data) 230 | }) 231 | ` 232 | 233 | if (derive?.operation) return derivedTemplate 234 | else return baseTemplate 235 | } 236 | ``` 237 | 238 | A codegen is nothing more than a function which takes some information about an Action (it's arguments, return type, name, and sometimes a derived operation) and returns a string of code! Each template should return a `baseTemplate`, which is the code for a non-derived Action, and a `derivedTemplate`, which contains the code to perform an HTTP request back to Hasura containing the original derived mutation if it's derived. 239 | 240 | ## Build Process 241 | 242 | There is a `fuse.ts` in the project root, which uses Fusebox as a build tool to compile the codegens. You can run the build process with `yarn build`. A walkthrough of the `fuse.ts` file can help to explain what happens: 243 | 244 | A path to template files from `fuse.ts` file is configured, and an array of codegen templates is defined. **If you add a new codegen, it needs to go here.** 245 | 246 | ```ts 247 | // Path to codegen template files 248 | const templatePath = './src/templates' 249 | // List of codegen templates to generate 250 | //prettier-ignore 251 | const codegenTemplates = [ 252 | { file: 'goServeMux.codegen.ts', folder: 'go-serve-mux', starterKit: false, }, 253 | { file: 'http4kBasic.codegen.ts', folder: 'kotlin-http4k', starterKit: false }, 254 | { file: 'javascriptExpress.codegen.ts', folder: 'node-express-jsdoc', starterKit: false }, 255 | { file: 'kotlinKtor.codegen.ts', folder: 'kotlin-ktor', starterKit: false }, 256 | { file: 'javaSpringBoot.codegen.ts', folder: 'java-spring', starterKit: false }, 257 | { file: 'pythonFastAPI.codegen.ts', folder: 'python-fast-api', starterKit: true }, 258 | { file: 'typescriptExpress.codegen.ts', folder: 'typescript-express',starterKit: false }, 259 | ] 260 | ``` 261 | 262 | For each codegen template, task functions are defined which will be called later: 263 | 264 | ```ts 265 | // This just creates the task definitions for each codegen template 266 | for (const { file, folder } of codegenTemplates) { 267 | task(`prebuild:${file}`, () => { 268 | rm(`${projectRoot}/${folder}`) 269 | }) 270 | 271 | task(`postbuild-clean:${file}`, () => { 272 | rm(`${projectRoot}/${folder}/actions-codegen.js.map`) 273 | rm(`${projectRoot}/${folder}/manifest-server.json`) 274 | }) 275 | 276 | task(`build:${file}`, async (ctx) => { 277 | await ctx.getConfig(templatePath, file).runDev({ 278 | target: 'browser', 279 | bundles: { 280 | app: './actions-codegen.js', 281 | distRoot: `${projectRoot}/${folder}`, 282 | }, 283 | }) 284 | }) 285 | 286 | task(`browserify:${file}`, () => { 287 | const path = `${projectRoot}/${folder}/actions-codegen.js` 288 | browserify(path).bundle((err, buffer) => { 289 | const data = buffer.toString() 290 | if (err) console.log('BROWSERIFY ERR:', err) 291 | else fs.writeFileSync(path, data) 292 | }) 293 | }) 294 | 295 | task(`update-framework:${folder}`, async () => { 296 | src(`${projectRoot}/**`) 297 | .contentsOf('frameworks.json', (current) => { 298 | const frameworks = JSON.parse(current) 299 | let entry = frameworks.find((x) => x.name == folder) 300 | const template = codegenTemplates.find((x) => x.folder == folder) 301 | const values = { name: folder, hasStarterKit: template.starterKit } 302 | if (!entry) frameworks.push(values) 303 | else Object.assign(entry, values) 304 | return JSON.stringify(frameworks, null, 2) 305 | }) 306 | .write() 307 | .exec() 308 | }) 309 | } 310 | ``` 311 | 312 | Finally, the previously defined tasks are executed: 313 | 314 | ```ts 315 | // This invokes the generated tasks for each of the templates 316 | task(`build`, async () => { 317 | for (const { file, folder } of codegenTemplates) { 318 | // Delete old version 319 | await exec(`prebuild:${file}`) 320 | // Generate new bundle 321 | await exec(`build:${file}`) 322 | // Browserify it so that it works in Browser + Node 323 | await exec(`browserify:${file}`) 324 | // Remove 'actions-codegen.js.map' and 'manifest-server.json' autogenerated files 325 | await exec(`postbuild-clean:${file}`) 326 | // Update 'frameworks.json' 327 | await exec(`update-framework:${folder}`) 328 | } 329 | }) 330 | ``` 331 | 332 | This series of build steps will: 333 | 334 | - Remove the old codegen files 335 | - Transpile the new version of each codegen into a single-file Javascript bundle in root-level folders of the repo 336 | - Run `browserify` on each of the bundles, so that they are isomorphic and work in both browser and Node environments 337 | - Remove some autogenerated manifests entries from the build step 338 | - Update the `frameworks.json` repo root to reflect any changes 339 | 340 | You can contribute to codegen in one or more of the following ways: 341 | 342 | ### Creating a type convertor for a new language 343 | 344 | We have type convertors for a bunch of languages [here](https://github.com/hasura/codegen-assets/tree/master/source/src/languages-functional). You can add a type convertor for a new language that can be levaraged in future to add codegen for different framworks/runtimes for that language. 345 | 346 | TODO (elaborate instructions) 347 | 348 | ### Creating a templates for a framework 349 | 350 | We have a templaters for different frameworks and runtimes [here](https://github.com/hasura/codegen-assets/tree/master/source/src/templates). 351 | 352 | TODO (elaborate instructions) 353 | 354 | ### Bugs and improvements to existing assets 355 | 356 | If you see some bugs in the codegen or if you feel that something could be done better, please [open an issue] about it. If you wish to work on a particular bug/enhancement, please comment on the issue and we will assign you accordingly. 357 | 358 | > If you are working on an issue or wanting to work on an issue, please make sure that you are on the same page with the maintainers about it. This is to avoid duplicate or unnecessary work. 359 | -------------------------------------------------------------------------------- /builder-kit/assets/codegen-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/codegen-assets/36f4d0f3103dd912db389ef33da6fa182672ec67/builder-kit/assets/codegen-cli.png -------------------------------------------------------------------------------- /builder-kit/assets/codegen-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/codegen-assets/36f4d0f3103dd912db389ef33da6fa182672ec67/builder-kit/assets/codegen-tab.png -------------------------------------------------------------------------------- /builder-kit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratchpad", 3 | "version": "1.0.2", 4 | "main": "dist/app.js", 5 | "ts:main": "src/index.ts", 6 | "typings": "src/index.ts", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "yarn webpack --config webpack.config.ts", 10 | "debug": "cd src && nodemon debug.ts" 11 | }, 12 | "dependencies": { 13 | "common-tags": "^1.8.0", 14 | "graphql": "^15.0.0", 15 | "graphql-extra": "0.3.0-alpha.3" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^13.9.0", 19 | "@types/webpack": "^4.41.12", 20 | "glob": "^7.1.6", 21 | "nodemon": "^2.0.3", 22 | "ts-loader": "^7.0.2", 23 | "ts-node": "^8.9.0", 24 | "typescript": "^3.8.3", 25 | "webpack": "^5.0.0-beta.16", 26 | "webpack-cli": "^3.3.11" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /builder-kit/src/CodegenOutput/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/codegen-assets/36f4d0f3103dd912db389ef33da6fa182672ec67/builder-kit/src/CodegenOutput/.gitkeep -------------------------------------------------------------------------------- /builder-kit/src/debug.ts: -------------------------------------------------------------------------------- 1 | import { 2 | graphqlSchemaToGo, 3 | graphqlSchemaToJSDoc, 4 | graphqlSchemaToKotlin, 5 | graphqlSchemaToPython, 6 | graphqlSchemaToTypescript, 7 | } from './languages-functional' 8 | 9 | import { 10 | javascriptExpressTemplate, 11 | typescriptExpressTemplate, 12 | kotlinHttp4kTemplate, 13 | kotlinKtorTemplate, 14 | pythonFastAPITemplate, 15 | goServeMuxTemplate, 16 | } from './templates' 17 | 18 | import { buildActionTypes } from './schemaTools' 19 | import { customInsertUserDerive } from './exampleDerivePayload' 20 | 21 | import fs from 'fs' 22 | import { DeriveParams } from './types' 23 | 24 | interface CodegenFile { 25 | name: string 26 | content: string 27 | } 28 | 29 | /** 30 | * Example Non-Derived Schema, InsertUserAction is non-Derived Action 31 | */ 32 | const nonDerivedSDL = ` 33 | type Mutation { 34 | InsertUserAction(user_info: UserInfo!): UserOutput 35 | } 36 | 37 | enum SOME_ENUM { 38 | TYPE_A 39 | TYPE_B 40 | TYPE_C 41 | } 42 | 43 | scalar Email 44 | 45 | input UserInfo { 46 | username: String! 47 | password: String! 48 | email: Email! 49 | age: Int 50 | birthDate: timestamptz! 51 | enum_field: SOME_ENUM! 52 | nullable_field: Float 53 | nullable_list: [Int] 54 | } 55 | 56 | type UserOutput { 57 | accessToken: String! 58 | age: Int 59 | email: Email! 60 | birthDate: timestamptz 61 | } 62 | ` 63 | 64 | /** 65 | * Example Derived Schema, CustomInsertUser is a Derived Action 66 | * CustomInsertUser has the Derive payload exported from exampleDerivePayload.ts 67 | * as customInsertUserDerive 68 | */ 69 | const derivedSDL = ` 70 | 71 | scalar Email 72 | 73 | type Mutation { 74 | CustomInsertUser(email: Email!, name: String! birthDate: timestamptz!): CustomInsertUserOutput 75 | } 76 | 77 | enum SOME_ENUM { 78 | TYPE_A 79 | TYPE_B 80 | TYPE_C 81 | } 82 | 83 | type CustomInsertUserOutput { 84 | id: Int! 85 | name: String! 86 | email: Email! 87 | birthDate: timestamptz! 88 | enum_value: SOME_ENUM 89 | nullable_field: Float 90 | nullable_list: [Int] 91 | arbit_field: [[String!]!] 92 | } 93 | ` 94 | 95 | const realSdl = ` 96 | extend type Mutation { 97 | Login ( 98 | not_email: String! 99 | password: String! 100 | ): JsonWebToken 101 | } 102 | 103 | extend type Query { 104 | Signup ( 105 | email: String! 106 | password: String! 107 | ): CreateUserOutput 108 | } 109 | 110 | type CreateUserOutput { 111 | id : Int! 112 | email : String! 113 | password : String! 114 | } 115 | 116 | type JsonWebToken { 117 | token : String! 118 | } 119 | ` 120 | 121 | const templater = ( 122 | actionName: string, 123 | actionSdl: string, 124 | derive: DeriveParams | null 125 | ) => { 126 | const actionParams = buildActionTypes(actionName, actionSdl) 127 | const templateParams = { ...actionParams, derive } 128 | /** 129 | * Javascript 130 | */ 131 | const jsCodegen = javascriptExpressTemplate({ 132 | ...templateParams, 133 | typeDefs: graphqlSchemaToJSDoc(actionSdl), 134 | }) 135 | 136 | /** 137 | * Typescript 138 | */ 139 | const tsCodegen = typescriptExpressTemplate({ 140 | ...templateParams, 141 | typeDefs: graphqlSchemaToTypescript(actionSdl), 142 | }) 143 | 144 | /** 145 | * Go 146 | */ 147 | const goCodegen = goServeMuxTemplate({ 148 | ...templateParams, 149 | typeDefs: graphqlSchemaToGo(actionSdl), 150 | }) 151 | 152 | /** 153 | * Python 154 | */ 155 | const pythonCodeGen = pythonFastAPITemplate({ 156 | ...templateParams, 157 | typeDefs: graphqlSchemaToPython(actionSdl), 158 | }) 159 | 160 | /** 161 | * Kotlin 162 | */ 163 | const kotlinTypes = graphqlSchemaToKotlin(actionSdl) 164 | const kotlinHttp4kCodegen = kotlinHttp4kTemplate({ 165 | ...templateParams, 166 | typeDefs: kotlinTypes, 167 | }) 168 | const kotlinKtorCodegen = kotlinKtorTemplate({ 169 | ...templateParams, 170 | typeDefs: kotlinTypes, 171 | }) 172 | 173 | /** 174 | * Response 175 | */ 176 | const response = [ 177 | { 178 | name: actionName + 'TypescriptExpress.ts', 179 | content: tsCodegen, 180 | }, 181 | { 182 | name: actionName + 'JavascriptJSDocExpress.js', 183 | content: jsCodegen, 184 | }, 185 | { 186 | name: actionName + 'GoServeMux.go', 187 | content: goCodegen, 188 | }, 189 | { 190 | name: actionName + 'PythonFastAPI.py', 191 | content: pythonCodeGen, 192 | }, 193 | { 194 | name: actionName + 'KotlinHttp4k.kt', 195 | content: kotlinHttp4kCodegen, 196 | }, 197 | { 198 | name: actionName + 'KotlinKtor.kt', 199 | content: kotlinKtorCodegen, 200 | }, 201 | ] 202 | return response 203 | } 204 | 205 | const writeCodegenTemplate = (input: CodegenFile) => { 206 | const fd = fs.openSync(`./CodegenOutput/${input.name}`, 'w') 207 | fs.writeSync(fd, input.content) 208 | } 209 | 210 | const codegenNonDerived = templater('InsertUserAction', nonDerivedSDL, null) 211 | const codegenDerived = templater( 212 | 'CustomInsertUser', 213 | derivedSDL, 214 | customInsertUserDerive 215 | ) 216 | 217 | for (let codegen of codegenNonDerived) { 218 | writeCodegenTemplate(codegen) 219 | } 220 | 221 | for (let codegen of codegenDerived) { 222 | codegen.name = 'Derived' + codegen.name 223 | writeCodegenTemplate(codegen) 224 | } 225 | -------------------------------------------------------------------------------- /builder-kit/src/derivedAndWholeSchemaCodegenNotes.ts1: -------------------------------------------------------------------------------- 1 | import { buildClientSchema, printSchema } from 'graphql' 2 | import { documentApi } from 'graphql-extra' 3 | /** 4 | * Notes on possibilites for "Derive" and future work with codegen: 5 | * 6 | * It is possible to take the introspection schema from Derive, and codegen 7 | * all of Hasura's types with it. We can do it like this: 8 | * 9 | * Call buildClientSchema() on derive.introspection_schema to create a GraphQL Schema object 10 | */ 11 | const schema = buildClientSchema(derive.introspection_schema) 12 | /** 13 | * 14 | * We need this function to skirt graphql-extra not parsing the root fields of a schema 15 | */ 16 | const removeSchemaRoot = (schemaString: string) => 17 | schemaString.replace( 18 | `schema { 19 | query: query_root 20 | mutation: mutation_root 21 | subscription: subscription_root 22 | }`, 23 | '' 24 | ) 25 | /** 26 | * Now use printSchema() to convert the JSON AST format to a string schema 27 | * and cut the root definitions off of it so that we can pass it to graphql-extra 28 | */ 29 | const schemaString = printSchema(schema) 30 | const schemaWithoutRoot = removeSchemaRoot(schemaString) 31 | /** 32 | * Pass the root-less schema string SDL to documentApi() from graphql-extra 33 | * Now we have a powerful API object for interacting with the schema 34 | * and can re-use our codegen functions because we have the proper API 35 | */ 36 | const document = documentApi().addSDL(schemaWithoutRoot) 37 | /** 38 | * Convert ALL OF HASURA'S SCHEMA to language types: 39 | */ 40 | const typescriptConverter = new TSTypeConverter({ 41 | schema: document.toSDLString(), 42 | isAction: false, 43 | }) 44 | const pythonConverter = new PythonTypeConverter({ 45 | schema: document.toSDLString(), 46 | isAction: false, 47 | }) 48 | console.log(typescriptConverter.generateTypes()) 49 | console.log(typescriptConverter.generateTypes()) 50 | /** 51 | * We can grab the Action's root field 52 | */ 53 | const getActionRoot = (actionType: ActionType) => { 54 | switch (actionType) { 55 | case 'Mutation': 56 | return 'mutation_root' 57 | case 'Query': 58 | return 'query_root' 59 | } 60 | } 61 | const actionType = getActionType(documentApi().addSDL(actionSdl)) 62 | const actionRoot = document.getObjectType(getActionRoot(actionType)) 63 | /** 64 | * Now we can peek into the root of Mutation/Query for the Derived field 65 | */ 66 | const userInsert = actionRoot.getField('insert_user') 67 | console.log(serializeField(userInsert)) 68 | /** 69 | * Or all the fields 70 | */ 71 | const actionRootFields = mapSerializeFields(actionRoot) 72 | -------------------------------------------------------------------------------- /builder-kit/src/exampleDerivePayload.ts: -------------------------------------------------------------------------------- 1 | import { DeriveParams } from './types' 2 | 3 | /** 4 | * A sample Derive payload for a database with a table of User (id: int, name: string, email: string) 5 | * 6 | * type Mutation { 7 | * # Derived mutation 8 | * CustomInsertUser(email: String!, name: String!): CustomInsertUserOutput 9 | * } 10 | * 11 | * type Mutation { 12 | * InsertUserAction(user_info: UserInfo!): TokenOutput 13 | * } 14 | * 15 | * input UserInfo { 16 | * username: String! 17 | * password: String! 18 | * } 19 | * 20 | * type TokenOutput { 21 | * accessToken: String! 22 | * } 23 | * 24 | * type CustomInsertUserOutput { 25 | * email: String! 26 | * id: Int! 27 | * name: String! 28 | * } 29 | * 30 | */ 31 | export const customInsertUserDerive: DeriveParams = { 32 | endpoint: "http://localhost:8080/v1/graphql", 33 | operation: 34 | 'mutation CustomInsertUser($email: String!, $name: String!) {\n insert_user_one(object: {email: $email, name: $name}) {\n id\n name\n email\n }\n}', 35 | }; 36 | -------------------------------------------------------------------------------- /builder-kit/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | graphqlSchemaToJSDoc, 3 | graphqlSchemaToTypescript, 4 | graphqlSchemaToGo, 5 | graphqlSchemaToPython, 6 | graphqlSchemaToKotlin, 7 | } from './languages-functional' 8 | 9 | import { 10 | javascriptExpressTemplate, 11 | typescriptExpressTemplate, 12 | kotlinHttp4kTemplate, 13 | kotlinKtorTemplate, 14 | pythonFastAPITemplate, 15 | goServeMuxTemplate, 16 | } from './templates' 17 | 18 | import { buildActionTypes } from './schemaTools' 19 | import { DeriveParams } from './types' 20 | 21 | const templater = ( 22 | actionName: string, 23 | actionSdl: string, 24 | derive: DeriveParams | null 25 | ) => { 26 | const actionParams = buildActionTypes(actionName, actionSdl) 27 | const templateParams = { ...actionParams, derive } 28 | /** 29 | * Javascript 30 | */ 31 | const jsCodegen = javascriptExpressTemplate({ 32 | ...templateParams, 33 | typeDefs: graphqlSchemaToJSDoc(actionSdl), 34 | }) 35 | 36 | /** 37 | * Typescript 38 | */ 39 | const tsCodegen = typescriptExpressTemplate({ 40 | ...templateParams, 41 | typeDefs: graphqlSchemaToTypescript(actionSdl), 42 | }) 43 | 44 | /** 45 | * Go 46 | */ 47 | const goCodegen = goServeMuxTemplate({ 48 | ...templateParams, 49 | typeDefs: graphqlSchemaToGo(actionSdl), 50 | }) 51 | 52 | /** 53 | * Python 54 | */ 55 | const pythonCodeGen = pythonFastAPITemplate({ 56 | ...templateParams, 57 | typeDefs: graphqlSchemaToPython(actionSdl), 58 | }) 59 | 60 | /** 61 | * Kotlin 62 | */ 63 | const kotlinTypes = graphqlSchemaToKotlin(actionSdl) 64 | const kotlinHttp4kCodegen = kotlinHttp4kTemplate({ 65 | ...templateParams, 66 | typeDefs: kotlinTypes, 67 | }) 68 | const kotlinKtorCodegen = kotlinKtorTemplate({ 69 | ...templateParams, 70 | typeDefs: kotlinTypes, 71 | }) 72 | 73 | /** 74 | * Response 75 | */ 76 | const response = [ 77 | { 78 | name: actionName + 'TypescriptExpress.ts', 79 | content: tsCodegen, 80 | }, 81 | { 82 | name: actionName + 'JavascriptJSDocExpress.js', 83 | content: jsCodegen, 84 | }, 85 | { 86 | name: actionName + 'GoServeMux.go', 87 | content: goCodegen, 88 | }, 89 | { 90 | name: actionName + 'PythonFastAPI.py', 91 | content: pythonCodeGen, 92 | }, 93 | { 94 | name: actionName + 'KotlinHttp4k.kt', 95 | content: kotlinHttp4kCodegen, 96 | }, 97 | { 98 | name: actionName + 'KotlinKtor.kt', 99 | content: kotlinKtorCodegen, 100 | }, 101 | ] 102 | return response 103 | } 104 | 105 | globalThis.templater = templater 106 | -------------------------------------------------------------------------------- /builder-kit/src/languages-functional/go.ts: -------------------------------------------------------------------------------- 1 | import { ScalarTypes, Fieldlike, ITypeMap } from '../types' 2 | import { serialize, isScalar, capitalize } from '../utils' 3 | import { buildBaseTypes } from '../schemaTools' 4 | import { html as template } from 'common-tags' 5 | import { EnumValueDefinitionApi, ScalarTypeApi } from 'graphql-extra' 6 | 7 | const scalarMap = { 8 | [ScalarTypes.ID]: `int`, 9 | [ScalarTypes.INT]: `int`, 10 | [ScalarTypes.FLOAT]: `float32`, 11 | [ScalarTypes.STRING]: `string`, 12 | [ScalarTypes.BOOLEAN]: `bool`, 13 | } 14 | 15 | const fieldFormatter = (field: Fieldlike) => { 16 | let { name, required, list, type } = serialize(field) 17 | let T = isScalar(type) ? scalarMap[type] : type 18 | if (!required) T = `*${T}` 19 | if (list) T = `[]${T}` 20 | return { name, type: T } 21 | } 22 | const goTypeDef = (typeName, fields: Fieldlike[]) => { 23 | const fieldDefs = fields 24 | .map(fieldFormatter) 25 | .map(({ name, type }) => `${capitalize(name)} ${type}`) 26 | .join('\n') 27 | 28 | return template` 29 | type ${typeName} struct { 30 | ${fieldDefs} 31 | } 32 | ` 33 | } 34 | 35 | const typeMapToGoTypes = (typeMap: ITypeMap) => 36 | Object.entries(typeMap.types) 37 | .map(([typeName, fields]) => goTypeDef(typeName, fields)) 38 | .join('\n\n') 39 | 40 | // type LeaveType string 41 | // const( 42 | // AnnualLeave LeaveType = "AnnualLeave" 43 | // Sick = "Sick" 44 | // BankHoliday = "BankHoliday" 45 | // Other = "Other" 46 | // ) 47 | const goEnumDef = (typeName, fields: EnumValueDefinitionApi[]) => { 48 | const fieldDefs = fields 49 | .map((field, idx) => 50 | idx == 0 51 | ? `${capitalize(field.getName())} ${typeName} = "${capitalize( 52 | field.getName() 53 | )}"` 54 | : `${capitalize(field.getName())} = "${capitalize(field.getName())}"` 55 | ) 56 | .join('\n') 57 | 58 | return template` 59 | type ${typeName} string 60 | 61 | const( 62 | ${fieldDefs} 63 | ) 64 | ` 65 | } 66 | 67 | const goScalarDef = (scalarType: ScalarTypeApi) => 68 | `type ${capitalize(scalarType.getName())} string` 69 | 70 | const typeMapToGoEnums = (typeMap: ITypeMap) => 71 | Object.entries(typeMap.enums) 72 | .map(([typeName, fields]) => goEnumDef(typeName, fields)) 73 | .join('\n\n') 74 | 75 | const typeMapToGoScalars = (typeMap: ITypeMap) => 76 | Object.entries(typeMap.scalars) 77 | .map(([_, scalarType]) => goScalarDef(scalarType)) 78 | .join('\n\n') 79 | 80 | const typeMapToGo = (typeMap: ITypeMap) => 81 | typeMapToGoScalars(typeMap) + 82 | '\n\n' + 83 | typeMapToGoEnums(typeMap) + 84 | '\n\n' + 85 | typeMapToGoTypes(typeMap) 86 | 87 | export const graphqlSchemaToGo = (schema: string) => 88 | typeMapToGo(buildBaseTypes(schema)) 89 | -------------------------------------------------------------------------------- /builder-kit/src/languages-functional/index.ts: -------------------------------------------------------------------------------- 1 | export * from './go' 2 | export * from './jsdoc' 3 | export * from './kotlin' 4 | export * from './python' 5 | export * from './typescript' 6 | export * from './java' 7 | -------------------------------------------------------------------------------- /builder-kit/src/languages-functional/java.ts: -------------------------------------------------------------------------------- 1 | import { ScalarTypes, ITypeMap, Fieldlike } from '../types' 2 | import {capitalize, isScalar, serialize} from '../utils' 3 | import { buildBaseTypes } from '../schemaTools' 4 | import { html as template } from 'common-tags' 5 | import { EnumValueDefinitionApi } from 'graphql-extra' 6 | 7 | const scalarMap = { 8 | [ScalarTypes.ID]: `Integer`, 9 | [ScalarTypes.INT]: `Integer`, 10 | [ScalarTypes.FLOAT]: `Float`, 11 | [ScalarTypes.STRING]: `String`, 12 | [ScalarTypes.BOOLEAN]: `Boolean`, 13 | } 14 | 15 | const fieldFormatter = (field: Fieldlike) => { 16 | let { name, required, list, type } = serialize(field) 17 | let T = isScalar(type) ? scalarMap[type] : type 18 | // String? -> List 19 | if (list) T = `Iterable<${T}>` 20 | return { name, type: T } 21 | } 22 | 23 | 24 | const javaTypeDef = (typeName: string, fields: Fieldlike[]): string => { 25 | const fieldDefs = fields 26 | .map(fieldFormatter) 27 | .map( 28 | ({ name, type }) => template` 29 | private ${type} _${name}; 30 | public void set${capitalize(name)}(${type} ${name}) { this._${name} = ${name}; } 31 | public ${type} get${capitalize(name)}() { return this._${name}; } 32 | ` 33 | ) 34 | .join('\n\n') 35 | 36 | return template` 37 | class ${capitalize(typeName)} { 38 | ${fieldDefs} 39 | }` 40 | } 41 | 42 | const typeMapTojavaTypes = (typeMap: ITypeMap) => 43 | Object.entries(typeMap.types) 44 | .map(([typeName, fields]) => javaTypeDef(typeName, fields)) 45 | .join('\n\n') 46 | 47 | // const javaEnumDef = (typeName: string, fields: EnumValueDefinitionApi[]): string => { 48 | // const fieldDefs = fields.map((field) => field.getName()).join(', ') 49 | 50 | // return template` 51 | // enum class ${typeName} { 52 | // ${fieldDefs} 53 | // }` 54 | // } 55 | 56 | // const typeMapTojavaEnums = (typeMap: ITypeMap) => 57 | // Object.entries(typeMap.enums) 58 | // .map(([typeName, fields]) => javaEnumDef(typeName, fields)) 59 | // .join('\n\n') 60 | 61 | // const typeMapTojava = (typeMap: ITypeMap) => 62 | // typeMapTojavaTypes(typeMap) + '\n\n' + typeMapTojavaEnums(typeMap) 63 | 64 | export const graphqlSchemaTojava = (schema: string) => 65 | typeMapTojavaTypes(buildBaseTypes(schema)) 66 | -------------------------------------------------------------------------------- /builder-kit/src/languages-functional/jsdoc.ts: -------------------------------------------------------------------------------- 1 | import { ScalarTypes, Fieldlike, ITypeMap } from '../types' 2 | import { serialize, isScalar } from '../utils' 3 | import { buildBaseTypes } from '../schemaTools' 4 | import { html as template } from 'common-tags' 5 | import { EnumValueDefinitionApi, ScalarTypeApi } from 'graphql-extra' 6 | 7 | const scalarMap = { 8 | [ScalarTypes.ID]: `number`, 9 | [ScalarTypes.INT]: `number`, 10 | [ScalarTypes.FLOAT]: `number`, 11 | [ScalarTypes.STRING]: `string`, 12 | [ScalarTypes.BOOLEAN]: `boolean`, 13 | } 14 | 15 | const fieldFormatter = (field: Fieldlike) => { 16 | let { name, required, list, type } = serialize(field) 17 | let T = isScalar(type) ? scalarMap[type] : type 18 | if (!required) T = `[${T}]` 19 | if (list) T = `Array<${T}>` 20 | return { name, type: T } 21 | } 22 | const jsdocTypeDef = (typeName, fields: Fieldlike[]) => { 23 | const fieldDefs = fields 24 | .map(fieldFormatter) 25 | .map(({ name, type }) => `* @property {${type}} ${name}`) 26 | .join('\n') 27 | 28 | return template` 29 | /** 30 | * @typedef {Object} ${typeName} 31 | ${fieldDefs} 32 | */ 33 | ` 34 | } 35 | 36 | const typeMapToJSDocTypes = (typeMap: ITypeMap) => 37 | Object.entries(typeMap.types) 38 | .map(([typeName, fields]) => jsdocTypeDef(typeName, fields)) 39 | .join('\n\n') 40 | 41 | const jsdocEnumDef = (typeName, fields: EnumValueDefinitionApi[]) => { 42 | const fieldDefs = fields 43 | .map((field) => `* @property {string} ${field.getName()}`) 44 | .join('\n') 45 | 46 | return template` 47 | /** 48 | * @enum {Object} ${typeName} 49 | ${fieldDefs} 50 | */ 51 | ` 52 | } 53 | const jsdocScalarDef = (scalarType: ScalarTypeApi) => 54 | template` 55 | /** 56 | * @typedef {string} ${scalarType.getName()} 57 | */ 58 | ` 59 | 60 | const typeMapToJSDocEnums = (typeMap: ITypeMap) => 61 | Object.entries(typeMap.enums) 62 | .map(([typeName, fields]) => jsdocEnumDef(typeName, fields)) 63 | .join('\n\n') 64 | 65 | const typeMapToJSDocScalars = (typeMap: ITypeMap) => 66 | Object.entries(typeMap.scalars) 67 | .map(([_, scalarType]) => jsdocScalarDef(scalarType)) 68 | .join('\n\n') 69 | 70 | const typeMapToJSDoc = (typeMap: ITypeMap) => 71 | typeMapToJSDocScalars(typeMap) + 72 | '\n\n' + 73 | typeMapToJSDocEnums(typeMap) + 74 | '\n\n' + 75 | typeMapToJSDocTypes(typeMap) 76 | 77 | export const graphqlSchemaToJSDoc = (schema: string) => 78 | typeMapToJSDoc(buildBaseTypes(schema)) 79 | -------------------------------------------------------------------------------- /builder-kit/src/languages-functional/kotlin.ts: -------------------------------------------------------------------------------- 1 | import { ScalarTypes, ITypeMap, Fieldlike } from '../types' 2 | import { isScalar, serialize } from '../utils' 3 | import { buildBaseTypes } from '../schemaTools' 4 | import { html as template } from 'common-tags' 5 | import { EnumValueDefinitionApi, ScalarTypeApi } from 'graphql-extra' 6 | 7 | const scalarMap = { 8 | [ScalarTypes.ID]: `Int`, 9 | [ScalarTypes.INT]: `Int`, 10 | [ScalarTypes.FLOAT]: `Float`, 11 | [ScalarTypes.STRING]: `String`, 12 | [ScalarTypes.BOOLEAN]: `Boolean`, 13 | } 14 | 15 | const fieldFormatter = (field: Fieldlike) => { 16 | let { name, required, list, type } = serialize(field) 17 | let T = isScalar(type) ? scalarMap[type] : type 18 | // String -> String? 19 | if (!required) T = `${T}?` 20 | // String? -> List 21 | if (list) T = `List<${T}>` 22 | // List -> List? 23 | if (!required && list) T = `${T}?` 24 | return { name, type: T } 25 | } 26 | 27 | const kotlinTypeDef = (typeName: string, fields: Fieldlike[]): string => { 28 | const fieldDefs = fields 29 | .map(fieldFormatter) 30 | .map(({ name, type }) => `var ${name}: ${type}`) 31 | .join(', ') 32 | 33 | return `data class ${typeName}(${fieldDefs})` 34 | } 35 | 36 | const kotlinScalarDef = (scalarType: ScalarTypeApi): string => template` 37 | typealias ${scalarType.getName()} = Any 38 | ` 39 | 40 | const kotlinEnumDef = ( 41 | typeName: string, 42 | fields: EnumValueDefinitionApi[] 43 | ): string => { 44 | const fieldDefs = fields.map((field) => field.getName()).join(', ') 45 | return template` 46 | enum class ${typeName} { 47 | ${fieldDefs} 48 | }` 49 | } 50 | 51 | const typeMapToKotlinScalars = (typeMap: ITypeMap) => 52 | Object.entries(typeMap.scalars) 53 | .map(([_, scalarType]) => kotlinScalarDef(scalarType)) 54 | .join('\n\n') 55 | 56 | const typeMapToKotlinTypes = (typeMap: ITypeMap) => 57 | Object.entries(typeMap.types) 58 | .map(([typeName, fields]) => kotlinTypeDef(typeName, fields)) 59 | .join('\n\n') 60 | 61 | const typeMapToKotlinEnums = (typeMap: ITypeMap) => 62 | Object.entries(typeMap.enums) 63 | .map(([typeName, fields]) => kotlinEnumDef(typeName, fields)) 64 | .join('\n\n') 65 | 66 | const typeMapToKotlin = (typeMap: ITypeMap) => 67 | typeMapToKotlinScalars(typeMap) + 68 | '\n\n' + 69 | typeMapToKotlinTypes(typeMap) + 70 | '\n\n' + 71 | typeMapToKotlinEnums(typeMap) 72 | 73 | export const graphqlSchemaToKotlin = (schema: string) => 74 | typeMapToKotlin(buildBaseTypes(schema)) 75 | -------------------------------------------------------------------------------- /builder-kit/src/languages-functional/python.ts: -------------------------------------------------------------------------------- 1 | import { ScalarTypes, Fieldlike, ITypeMap } from '../types' 2 | import { indent, serialize, isScalar } from '../utils' 3 | import { buildBaseTypes } from '../schemaTools' 4 | import { html as template } from 'common-tags' 5 | import { EnumValueDefinitionApi } from 'graphql-extra' 6 | 7 | const scalarMap = { 8 | [ScalarTypes.ID]: `int`, 9 | [ScalarTypes.INT]: `int`, 10 | [ScalarTypes.FLOAT]: `float`, 11 | [ScalarTypes.STRING]: `str`, 12 | [ScalarTypes.BOOLEAN]: `bool`, 13 | } 14 | 15 | const baseTypes = template` 16 | from pydantic import BaseModel 17 | from enum import Enum, auto 18 | ` 19 | const fieldFormatter = (field: Fieldlike) => { 20 | let { name, required, list, type } = serialize(field) 21 | let T = isScalar(type) ? scalarMap[type] : type 22 | // str -> List[str] 23 | if (list) T = `list[${T}]` 24 | // List[str] -> Optional[List[str]] 25 | if (!required) T = `${T} | None` 26 | return { name, type: T } 27 | } 28 | const pythonTypeDef = (typeName: string, fields: Fieldlike[]): string => { 29 | const fieldDefs = fields 30 | .map(fieldFormatter) 31 | .map(({ name, type }) => indent(`${name}: ${type}`)) 32 | .join('\n') 33 | 34 | return template` 35 | class ${typeName}(BaseModel): 36 | ${fieldDefs} 37 | ` 38 | } 39 | 40 | const typeMapToPythonTypes = (typeMap: ITypeMap) => 41 | Object.entries(typeMap.types) 42 | .map(([typeName, fields]) => pythonTypeDef(typeName, fields)) 43 | .join('\n\n') 44 | 45 | const pythonEnumDef = ( 46 | typeName: string, 47 | fields: EnumValueDefinitionApi[] 48 | ): string => { 49 | const fieldDefs = fields 50 | .map((field) => indent(`${field.getName()} = auto()`)) 51 | .join('\n') 52 | 53 | return template` 54 | class ${typeName}(Enum): 55 | ${fieldDefs} 56 | ` 57 | } 58 | 59 | const typeMapToPythonEnums = (typeMap: ITypeMap) => 60 | Object.entries(typeMap.enums) 61 | .map(([typeName, fields]) => pythonEnumDef(typeName, fields)) 62 | .join('\n\n') 63 | 64 | const typeMapToPython = (typeMap: ITypeMap) => { 65 | let typeDefs = 66 | baseTypes + 67 | '\n\n' + 68 | typeMapToPythonTypes(typeMap) + 69 | '\n\n' + 70 | typeMapToPythonEnums(typeMap) 71 | 72 | // Python can't handle type-aliases or standalone type-defs, so we need to replace them post-fact 73 | Object.keys(typeMap.scalars).forEach((scalar) => { 74 | typeDefs = typeDefs.replace(new RegExp(scalar, 'g'), 'Any') 75 | }) 76 | 77 | return typeDefs 78 | } 79 | 80 | export const graphqlSchemaToPython = (schema: string) => 81 | typeMapToPython(buildBaseTypes(schema)) 82 | -------------------------------------------------------------------------------- /builder-kit/src/languages-functional/typescript.ts: -------------------------------------------------------------------------------- 1 | import { ScalarTypes, Fieldlike, ITypeMap } from '../types' 2 | import { serialize, isScalar } from '../utils' 3 | import { buildBaseTypes } from '../schemaTools' 4 | import { html as template } from 'common-tags' 5 | import { EnumValueDefinitionApi, ScalarTypeApi } from 'graphql-extra' 6 | 7 | const scalarMap = { 8 | [ScalarTypes.ID]: 'number', 9 | [ScalarTypes.INT]: 'number', 10 | [ScalarTypes.FLOAT]: 'number', 11 | [ScalarTypes.STRING]: 'string', 12 | [ScalarTypes.BOOLEAN]: 'boolean', 13 | } 14 | 15 | const baseTypes = template` 16 | type Maybe = T | null 17 | ` 18 | const fieldFormatter = (field: Fieldlike) => { 19 | let { name, required, list, type } = serialize(field) 20 | // let { required, type, name } = field 21 | let T = isScalar(type) ? scalarMap[type] : type 22 | // string -> Maybe 23 | if (!required) T = `Maybe<${T}>` 24 | // Maybe -> Array> 25 | if (list) T = `Array<${T}>` 26 | // Array> -> Maybe>> 27 | if (!required && list) T = `Maybe<${T}>` 28 | // username: string -> username?: string 29 | if (!required) name = `${name}?` 30 | return { name, type: T } 31 | } 32 | 33 | const tsTypeDef = (typeName: string, fields: Fieldlike[]): string => { 34 | const fieldDefs = fields 35 | .map(fieldFormatter) 36 | .map(({ name, type }) => `${name}: ${type}`) 37 | .join('\n') 38 | 39 | return template` 40 | type ${typeName} = { 41 | ${fieldDefs} 42 | }` 43 | } 44 | 45 | const typeMapToTSTypes = (typeMap: ITypeMap) => 46 | Object.entries(typeMap.types) 47 | .map(([typeName, fields]) => tsTypeDef(typeName, fields)) 48 | .join('\n\n') 49 | 50 | const tsEnumDef = ( 51 | typeName: string, 52 | fields: EnumValueDefinitionApi[] 53 | ): string => { 54 | const fieldDefs = fields 55 | .map((field) => `${field.getName()} = '${field.getName()}'`) 56 | .join(',\n') 57 | 58 | return template` 59 | enum ${typeName} { 60 | ${fieldDefs} 61 | }` 62 | } 63 | 64 | const tsScalarDef = (scalarType: ScalarTypeApi): string => { 65 | return template` 66 | type ${scalarType.getName()} = string` 67 | } 68 | 69 | const typeMapToTSEnums = (typeMap: ITypeMap) => 70 | Object.entries(typeMap.enums) 71 | .map(([typeName, fields]) => tsEnumDef(typeName, fields)) 72 | .join('\n\n') 73 | 74 | const typeMapToTSScalars = (typeMap: ITypeMap) => 75 | Object.entries(typeMap.scalars) 76 | .map(([_, scalarType]) => tsScalarDef(scalarType)) 77 | .join('\n\n') 78 | 79 | export const typeMapToTypescript = (typeMap: ITypeMap) => 80 | baseTypes + 81 | '\n\n' + 82 | typeMapToTSScalars(typeMap) + 83 | '\n\n' + 84 | typeMapToTSEnums(typeMap) + 85 | '\n\n' + 86 | typeMapToTSTypes(typeMap) 87 | 88 | export const graphqlSchemaToTypescript = (schema: string) => 89 | typeMapToTypescript(buildBaseTypes(schema)) 90 | -------------------------------------------------------------------------------- /builder-kit/src/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["CodegenOutput/*"] 3 | } 4 | -------------------------------------------------------------------------------- /builder-kit/src/schemaTools.ts: -------------------------------------------------------------------------------- 1 | import { ObjectTypeDefinitionNode } from 'graphql' 2 | import { ActionParams, ITypeMap } from './types' 3 | import { pipe, isScalar } from './utils' 4 | import { 5 | t, 6 | documentApi, 7 | FieldDefinitionApi, 8 | DocumentApi, 9 | InputTypeApi, 10 | ObjectTypeApi, 11 | } from 'graphql-extra' 12 | 13 | /** 14 | * Takes an argument from Action field in Schema 15 | * and converts it to an object type + adds to document 16 | * To codegen the Action's input parameter types later 17 | */ 18 | const _makeActionArgType = ( 19 | field: FieldDefinitionApi 20 | ): ObjectTypeDefinitionNode => 21 | t.objectType({ 22 | name: field.getName() + 'Args', 23 | fields: field.getArguments().map((arg) => arg.toField().node), 24 | }) 25 | 26 | /** 27 | * Maps through the Mutation fields to grab Action and creates types 28 | * in the schema document for each of them for codegen 29 | */ 30 | export function addArgumentTypesToSchema(document: DocumentApi) { 31 | document 32 | .getAllObjectTypes() 33 | .filter((type) => type.getName() == 'Mutation' || type.getName() == 'Query') 34 | .forEach((type) => { 35 | type.getFields().forEach((field) => { 36 | document.createObjectType(_makeActionArgType(field)) 37 | }) 38 | }) 39 | return document 40 | } 41 | 42 | const _addMissingScalarsForType = ( 43 | document: DocumentApi, 44 | type: ObjectTypeApi | InputTypeApi 45 | ) => 46 | type.getFields().forEach((f) => { 47 | const fieldTypename = f.getTypename() 48 | if (document.hasType(fieldTypename) || isScalar(fieldTypename)) return 49 | document.createScalarType({ name: fieldTypename }) 50 | }) 51 | 52 | const populateCustomScalars = (document: DocumentApi) => { 53 | document 54 | .getAllObjectTypes() 55 | .forEach((type) => _addMissingScalarsForType(document, type)) 56 | document 57 | .getAllInputTypes() 58 | .forEach((type) => _addMissingScalarsForType(document, type)) 59 | return document 60 | } 61 | /** 62 | * Takes a Document API object and builds a map of it's types and their fields 63 | */ 64 | export function buildTypeMap(document: DocumentApi): ITypeMap { 65 | let res: ITypeMap = { types: {}, enums: {}, scalars: {} } 66 | 67 | for (let type of document.getAllTypes()) { 68 | const name = type.getName() 69 | switch (true) { 70 | case type.isInputType(): 71 | const inputFields = type.assertInputType().getFields() 72 | res.types[name] = inputFields 73 | break 74 | case type.isObjectType(): 75 | const objectFields = type.assertObjectType().getFields() 76 | res.types[name] = objectFields 77 | break 78 | case type.isEnumType(): 79 | res.enums[name] = type.assertEnumType().getValues() 80 | break 81 | case type.isScalarType(): 82 | res.scalars[name] = type.assertScalarType() 83 | break 84 | } 85 | } 86 | 87 | return res 88 | } 89 | 90 | /** 91 | * 92 | * @param {string} actionName 93 | * @param {string} actionSdl 94 | * @returns {ActionParams} actionParams 95 | */ 96 | export function buildActionTypes( 97 | actionName: string, 98 | sdl: string 99 | ): ActionParams { 100 | const document = processSchema(sdl) 101 | 102 | // The current Action is found by: 103 | // - Iterating through all the Object Extension types 104 | // (since we use "extend type Query/Mutation") 105 | // - Finding the one that has a field with current Action name 106 | // - Then returning that field from the Operation (Mutation/Query) type 107 | const action = document 108 | .getAllObjectTypes() 109 | .find((type) => type._fields.has(actionName)) 110 | .getField(actionName) 111 | 112 | let actionParams: ActionParams = { 113 | actionName: actionName, 114 | returnType: action.getTypename(), 115 | actionArgs: action.getArguments(), 116 | typeMap: buildTypeMap(document), 117 | } 118 | 119 | return actionParams 120 | } 121 | 122 | /** 123 | * Converts "extend type Mutation" and "extend type Query" definitions 124 | * into fields on root Mutation/Query types in the Document 125 | */ 126 | function convertExtendedQueriesAndMutations(document: DocumentApi) { 127 | document.upsertObjectType(t.queryType({})) 128 | document.upsertObjectType(t.mutationType({})) 129 | 130 | const queries = document.getObjectType('Query') 131 | const mutations = document.getObjectType('Mutation') 132 | 133 | document.getAllObjectExts().forEach((extendedType) => { 134 | extendedType.getFields().forEach((field) => { 135 | const operation = extendedType.getName() 136 | if (operation == 'Query') queries.upsertField(field.node) 137 | if (operation == 'Mutation') mutations.upsertField(field.node) 138 | }) 139 | }) 140 | 141 | const queryFields = queries.getFields() 142 | const mutationFields = mutations.getFields() 143 | 144 | if (!queryFields.length) document.removeObjectType('Query') 145 | if (!mutationFields.length) document.removeObjectType('Mutation') 146 | 147 | return document 148 | } 149 | 150 | /** 151 | * Function that allows TypeConverters to generate TypeMap's for non-Hasura Action SDL 152 | * In TypeConverter constructor, it checks whether it should generate 153 | */ 154 | export const buildBaseTypes = (sdl: string) => buildTypeMap(processSchema(sdl)) 155 | 156 | /** 157 | * Converts a GraphQL Schema SDL string into a Document API 158 | * while also processing it. 159 | * 160 | * Converts `extend type Query/Mutation` into regular types, 161 | * creates types for each operation's inputs/arguments, and 162 | * creates definitions for any custom scalars not in the schema. 163 | */ 164 | export function processSchema(sdl: string): DocumentApi { 165 | const document = documentApi().addSDL(sdl) 166 | const process = pipe( 167 | convertExtendedQueriesAndMutations, 168 | addArgumentTypesToSchema, 169 | populateCustomScalars 170 | ) 171 | return process(document) 172 | } 173 | -------------------------------------------------------------------------------- /builder-kit/src/templates/goServeMux.codegen.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSchemaToGo } from '../languages-functional/go' 2 | import { goServeMuxTemplate } from './goServeMux' 3 | import { buildActionTypes } from '../schemaTools' 4 | import { DeriveParams } from '../types' 5 | 6 | const templater = ( 7 | actionName: string, 8 | actionSdl: string, 9 | derive: DeriveParams | null 10 | ) => { 11 | const actionParams = buildActionTypes(actionName, actionSdl) 12 | const codegen = goServeMuxTemplate({ ...actionParams, derive }) 13 | const response = [ 14 | { 15 | name: actionName + '.go', 16 | content: codegen, 17 | }, 18 | { 19 | name: actionName + 'Types.go', 20 | content: graphqlSchemaToGo(actionSdl), 21 | }, 22 | ] 23 | return response 24 | } 25 | 26 | globalThis.templater = templater 27 | -------------------------------------------------------------------------------- /builder-kit/src/templates/goServeMux.ts: -------------------------------------------------------------------------------- 1 | import { html as template } from 'common-tags' 2 | import { ITypeNode, CodegenTemplateParams } from '../types' 3 | import { getRootFieldName, capitalize } from '../utils' 4 | 5 | const sampleValues = { 6 | Int: 1111, 7 | String: '""', 8 | Boolean: false, 9 | Float: 11.11, 10 | ID: 1111, 11 | } 12 | 13 | export const goServeMuxTemplate = (params: CodegenTemplateParams) => { 14 | const { actionName, returnType, typeDefs, typeMap, derive } = params 15 | 16 | const returnTypeDef = typeMap.types[returnType] 17 | 18 | let delegationTypedefs = derive?.operation 19 | ? template` 20 | 21 | type GraphQLRequest struct { 22 | Query string \`json:"query"\` 23 | Variables ${actionName}Args \`json:"variables"\` 24 | } 25 | type GraphQLData struct { 26 | ${getRootFieldName( 27 | derive.operation, 28 | true 29 | )} ${returnType} \`json:"${getRootFieldName(derive.operation)}"\` 30 | } 31 | type GraphQLResponse struct { 32 | Data GraphQLData \`json:"data,omitempty"\` 33 | Errors []GraphQLError \`json:"errors,omitempty"\` 34 | } 35 | ` 36 | : '' 37 | 38 | let executeFunc = derive?.operation 39 | ? template` 40 | func execute(variables ${actionName}Args) (response GraphQLResponse, err error) { 41 | 42 | // build the request body 43 | reqBody := GraphQLRequest{ 44 | Query: "${derive.operation}", 45 | Variables: variables, 46 | } 47 | reqBytes, err := json.Marshal(reqBody) 48 | if err != nil { 49 | return 50 | } 51 | 52 | // make request to Hasura 53 | resp, err := http.Post("http://localhost:8080/v1/graphql", "application/json", bytes.NewBuffer(reqBytes)) 54 | if err != nil { 55 | return 56 | } 57 | 58 | // parse the response 59 | respBytes, err := ioutil.ReadAll(resp.Body) 60 | if err != nil { 61 | return 62 | } 63 | err = json.Unmarshal(respBytes, &response) 64 | if err != nil { 65 | return 66 | } 67 | 68 | // return the response 69 | return 70 | } 71 | 72 | ` 73 | : '' 74 | 75 | let handlerFunc = derive?.operation 76 | ? template` 77 | // Auto-generated function that takes the Action parameters and must return it's response type 78 | func ${actionName}(args ${actionName}Args) (response ${returnType}, err error) { 79 | 80 | hasuraResponse, err := execute(args) 81 | 82 | // throw if any unexpected error happens 83 | if err != nil { 84 | return 85 | } 86 | 87 | // delegate Hasura error 88 | if len(hasuraResponse.Errors) != 0 { 89 | err = errors.New(hasuraResponse.Errors[0].Message) 90 | return 91 | } 92 | 93 | response = hasuraResponse.Data.${getRootFieldName(derive.operation, true)} 94 | return 95 | 96 | } 97 | 98 | ` 99 | : template` 100 | // Auto-generated function that takes the Action parameters and must return it's response type 101 | func ${actionName}(args ${actionName}Args) (response ${returnType}, err error) { 102 | response = ${returnType} { 103 | ${returnTypeDef 104 | .map((f) => { 105 | return `${capitalize(f.getName())}: ${ 106 | sampleValues[f.getType().getTypename()] || sampleValues['String'] 107 | }` 108 | }) 109 | .join(',\n')}, 110 | } 111 | return response, nil 112 | } 113 | ` 114 | 115 | return template` 116 | 117 | package main 118 | 119 | import ( 120 | "bytes" 121 | "encoding/json" 122 | "io/ioutil" 123 | "log" 124 | "net/http" 125 | ) 126 | 127 | type ActionPayload struct { 128 | SessionVariables map[string]interface{} \`json:"session_variables"\` 129 | Input ${actionName}Args \`json:"input"\` 130 | } 131 | 132 | type GraphQLError struct { 133 | Message string \`json:"message"\` 134 | } 135 | 136 | ${delegationTypedefs} 137 | 138 | func handler(w http.ResponseWriter, r *http.Request) { 139 | 140 | // set the response header as JSON 141 | w.Header().Set("Content-Type", "application/json") 142 | 143 | // read request body 144 | reqBody, err := ioutil.ReadAll(r.Body) 145 | if err != nil { 146 | http.Error(w, "invalid payload", http.StatusBadRequest) 147 | return 148 | } 149 | 150 | // parse the body as action payload 151 | var actionPayload ActionPayload 152 | err = json.Unmarshal(reqBody, &actionPayload) 153 | if err != nil { 154 | http.Error(w, "invalid payload", http.StatusBadRequest) 155 | return 156 | } 157 | 158 | // Send the request params to the Action's generated handler function 159 | result, err := ${actionName}(actionPayload.Input) 160 | 161 | // throw if an error happens 162 | if err != nil { 163 | errorObject := GraphQLError{ 164 | Message: err.Error(), 165 | } 166 | errorBody, _ := json.Marshal(errorObject) 167 | w.WriteHeader(http.StatusBadRequest) 168 | w.Write(errorBody) 169 | return 170 | } 171 | 172 | // Write the response as JSON 173 | data, _ := json.Marshal(result) 174 | w.Write(data) 175 | 176 | } 177 | 178 | ${handlerFunc} 179 | ${executeFunc} 180 | 181 | // HTTP server for the handler 182 | func main() { 183 | mux := http.NewServeMux() 184 | mux.HandleFunc("/${actionName}", handler) 185 | 186 | err := http.ListenAndServe(":3000", mux) 187 | log.Fatal(err) 188 | } 189 | ` 190 | } 191 | -------------------------------------------------------------------------------- /builder-kit/src/templates/http4kContractRoutes.todo.kt: -------------------------------------------------------------------------------- 1 | package guide.modules.contracts 2 | 3 | // for this example we're using Jackson - note that the auto method imported is an extension 4 | // function that is defined on the Jackson instance 5 | 6 | import org.http4k.contract.ContractRoute 7 | import org.http4k.contract.bind 8 | import org.http4k.contract.contract 9 | import org.http4k.contract.div 10 | import org.http4k.contract.meta 11 | import org.http4k.contract.openapi.ApiInfo 12 | import org.http4k.contract.openapi.v3.OpenApi3 13 | import org.http4k.contract.security.ApiKeySecurity 14 | import org.http4k.core.Body 15 | import org.http4k.core.ContentType.Companion.TEXT_PLAIN 16 | import org.http4k.core.HttpHandler 17 | import org.http4k.core.Method.GET 18 | import org.http4k.core.Method.POST 19 | import org.http4k.core.Request 20 | import org.http4k.core.Response 21 | import org.http4k.core.Status.Companion.OK 22 | import org.http4k.core.with 23 | import org.http4k.format.Jackson 24 | import org.http4k.format.Jackson.auto 25 | import org.http4k.lens.Path 26 | import org.http4k.lens.Query 27 | import org.http4k.lens.int 28 | import org.http4k.lens.string 29 | import org.http4k.routing.routes 30 | 31 | // this route has a dynamic path segment 32 | fun greetRoute(): ContractRoute { 33 | 34 | // these lenses define the dynamic parts of the request that will be used in processing 35 | val ageQuery = Query.int().required("age") 36 | val stringBody = Body.string(TEXT_PLAIN).toLens() 37 | 38 | // this specifies the route contract, with the desired contract of path, headers, queries and body parameters. 39 | val spec = "/greet" / Path.of("name") meta { 40 | summary = "tells the user hello!" 41 | queries += ageQuery 42 | receiving(stringBody) 43 | } bindContract GET 44 | 45 | // the this function will dynamically supply a new HttpHandler for each call. The number of parameters 46 | // matches the number of dynamic sections in the path (1) 47 | fun greet(nameFromPath: String): HttpHandler = { request: Request -> 48 | val age = ageQuery(request) 49 | val sentMessage = stringBody(request) 50 | 51 | Response(OK).with(stringBody of "hello $nameFromPath you are $age. You sent $sentMessage") 52 | } 53 | 54 | return spec to ::greet 55 | } 56 | 57 | data class NameAndMessage(val name: String, val message: String) 58 | 59 | // this route uses auto-marshalling to convert the JSON body directly to/from a data class instance 60 | fun echoRoute(): ContractRoute { 61 | 62 | // the body lens here is imported as an extension function from the Jackson instance 63 | val body = Body.auto().toLens() 64 | 65 | // this specifies the route contract, including examples of the input and output body objects - they will 66 | // get exploded into JSON schema in the OpenAPI docs 67 | val spec = "/echo" meta { 68 | summary = "echoes the name and message sent to it" 69 | receiving(body to NameAndMessage("jim", "hello!")) 70 | returning(OK, body to NameAndMessage("jim", "hello!")) 71 | } bindContract POST 72 | 73 | // note that because we don't have any dynamic parameters, we can use a HttpHandler instance instead of a function 74 | val echo: HttpHandler = { request: Request -> 75 | val received: NameAndMessage = body(request) 76 | Response(OK).with(body of received) 77 | } 78 | 79 | return spec to echo 80 | } 81 | 82 | // use another Lens to set up the API-key - the answer is 42! 83 | val mySecurity = ApiKeySecurity(Query.int().required("api"), { it == 42 }) 84 | 85 | // Combine the Routes into a contract and bind to a context, defining a renderer (in this example 86 | // OpenApi/Swagger) and a security model (in this case an API-Key): 87 | val contract = contract { 88 | renderer = OpenApi3(ApiInfo("My great API", "v1.0"), Jackson) 89 | descriptionPath = "/swagger.json" 90 | security = mySecurity 91 | routes += greetRoute() 92 | routes += echoRoute() 93 | } 94 | 95 | val handler: HttpHandler = routes("/api/v1" bind contract) 96 | 97 | // by default, the OpenAPI docs live at the root of the contract context, but we can override it.. 98 | fun main() { 99 | println(handler(Request(GET, "/api/v1/swagger.json"))) 100 | 101 | println(handler(Request(POST, "/api/v1/echo") 102 | .query("api", "42") 103 | .body("""{"name":"Bob","message":"Hello"}"""))) 104 | } 105 | -------------------------------------------------------------------------------- /builder-kit/src/templates/http4kServerlessLambda.todo.kt: -------------------------------------------------------------------------------- 1 | package guide.modules.serverless 2 | 3 | import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent 4 | import org.http4k.client.ApacheClient 5 | import org.http4k.core.HttpHandler 6 | import org.http4k.core.Method.GET 7 | import org.http4k.core.Request 8 | import org.http4k.core.Response 9 | import org.http4k.core.Status.Companion.OK 10 | import org.http4k.server.SunHttp 11 | import org.http4k.server.asServer 12 | import org.http4k.serverless.AppLoader 13 | import org.http4k.serverless.BootstrapAppLoader 14 | import org.http4k.serverless.lambda.LambdaFunction 15 | 16 | // This AppLoader is responsible for building our HttpHandler which is supplied to AWS 17 | // It is the only actual piece of code that needs to be written. 18 | object TweetEchoLambda : AppLoader { 19 | override fun invoke(env: Map): HttpHandler = { 20 | Response(OK).body(it.bodyString().take(20)) 21 | } 22 | } 23 | 24 | fun main() { 25 | 26 | // Launching your Lambda Function locally - by simply providing the operating ENVIRONMENT map as would 27 | // be configured on AWS. 28 | fun runLambdaLocally() { 29 | val app: HttpHandler = TweetEchoLambda(mapOf()) 30 | val localLambda = app.asServer(SunHttp(8000)).start() 31 | 32 | println(ApacheClient()(Request(GET, "http://localhost:8000/").body("hello hello hello, i suppose this isn't 140 characters anymore.."))) 33 | localLambda.stop() 34 | } 35 | 36 | // the following code is purely here for demonstration purposes, to explain exactly what is happening at AWS. 37 | fun runLambdaAsAwsWould() { 38 | val lambda = LambdaFunction(mapOf(BootstrapAppLoader.HTTP4K_BOOTSTRAP_CLASS to TweetEchoLambda::class.java.name)) 39 | val response = lambda.handle(APIGatewayProxyRequestEvent().apply { 40 | path = "/" 41 | body = "hello hello hello, i suppose this isn't 140 characters anymore.." 42 | httpMethod = "GET" 43 | headers = mapOf() 44 | queryStringParameters = mapOf() 45 | }) 46 | println(response.statusCode) 47 | println(response.headers) 48 | println(response.body) 49 | } 50 | 51 | runLambdaLocally() 52 | runLambdaAsAwsWould() 53 | 54 | } -------------------------------------------------------------------------------- /builder-kit/src/templates/index.ts: -------------------------------------------------------------------------------- 1 | export { typescriptExpressTemplate } from './typescriptExpress' 2 | export { javascriptExpressTemplate } from './javascriptJSDocExpress' 3 | export { goServeMuxTemplate } from './goServeMux' 4 | export { kotlinHttp4kTemplate } from './kotlinHttp4k' 5 | export { kotlinKtorTemplate } from './kotlinKtor' 6 | export { pythonFastAPITemplate } from './pythonFastAPI' 7 | -------------------------------------------------------------------------------- /builder-kit/src/templates/javaSpringBoot.codegen.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSchemaTojava } from '../languages-functional' 2 | import { buildActionTypes } from '../schemaTools' 3 | import { DeriveParams } from '../types' 4 | import { javaSpringBootTemplate } from './javaSpringBoot' 5 | 6 | const templater = ( 7 | actionName: string, 8 | actionSdl: string, 9 | derive: DeriveParams | null 10 | ) => { 11 | const actionParams = buildActionTypes(actionName, actionSdl) 12 | const codegen = javaSpringBootTemplate({ ...actionParams, derive }) 13 | const response = [ 14 | { 15 | name: actionName + '.java', 16 | content: codegen, 17 | }, 18 | { 19 | name: actionName + 'Types.java', 20 | content: graphqlSchemaTojava(actionSdl), 21 | }, 22 | ] 23 | return response 24 | } 25 | 26 | globalThis.templater = templater 27 | -------------------------------------------------------------------------------- /builder-kit/src/templates/javaSpringBoot.ts: -------------------------------------------------------------------------------- 1 | import { html as template } from 'common-tags' 2 | import { CodegenTemplateParams } from '../types' 3 | import {capitalize, kebabCase} from "../utils"; 4 | 5 | export const javaSpringBootTemplate = (params: CodegenTemplateParams) => { 6 | const { actionArgs, actionName, typeDefs, returnType } = params 7 | return template` 8 | package my_action; 9 | 10 | import java.util.List; 11 | 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | @RestController 18 | class ActionController { 19 | @PostMapping("/${kebabCase(actionName)}") 20 | ${returnType} ${actionName}Handler(@RequestBody ${capitalize(actionName)}Args actionArgs) { 21 | // logic 22 | } 23 | } 24 | ` 25 | } 26 | -------------------------------------------------------------------------------- /builder-kit/src/templates/javascriptJSDocExpress.codegen.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSchemaToJSDoc } from '../languages-functional' 2 | import { javascriptExpressTemplate } from '../templates' 3 | import { buildActionTypes } from '../schemaTools' 4 | import { DeriveParams } from '../types' 5 | 6 | const templater = ( 7 | actionName: string, 8 | actionSdl: string, 9 | derive: DeriveParams | null 10 | ) => { 11 | const actionParams = buildActionTypes(actionName, actionSdl) 12 | const codegen = javascriptExpressTemplate({ ...actionParams, derive }) 13 | const response = [ 14 | { 15 | name: actionName + '.js', 16 | content: codegen, 17 | }, 18 | { 19 | name: actionName + 'Types.js', 20 | content: graphqlSchemaToJSDoc(actionSdl), 21 | }, 22 | ] 23 | return response 24 | } 25 | 26 | globalThis.templater = templater 27 | -------------------------------------------------------------------------------- /builder-kit/src/templates/javascriptJSDocExpress.ts: -------------------------------------------------------------------------------- 1 | import { html as template } from 'common-tags' 2 | import { CodegenTemplateParams } from '../types' 3 | 4 | const sampleValues = { 5 | Int: 1111, 6 | String: '""', 7 | Boolean: false, 8 | Float: 11.11, 9 | ID: 1111, 10 | } 11 | 12 | // Use JSDoc Types for typeDefs args 13 | export const javascriptExpressTemplate = (params: CodegenTemplateParams) => { 14 | const { actionName, returnType, derive, typeMap } = params 15 | 16 | const returnTypeDef = typeMap.types[returnType] 17 | 18 | const baseTemplate = template` 19 | function ${actionName}Handler(args) { 20 | return { 21 | ${returnTypeDef 22 | .map((f) => { 23 | return `${f.getName()}: ${ 24 | sampleValues[f.getTypename()] || sampleValues['String'] 25 | }` 26 | }) 27 | .join(',\n')}, 28 | } 29 | } 30 | 31 | // Request Handler 32 | app.post('/${actionName}', async (req, res) => { 33 | // get request input 34 | const params = req.body.input 35 | 36 | // run some business logic 37 | const result = ${actionName}Handler(params) 38 | 39 | /* 40 | // In case of errors: 41 | return res.status(400).json({ 42 | message: "error happened" 43 | }) 44 | */ 45 | 46 | // success 47 | return res.json(result) 48 | }) 49 | ` 50 | 51 | // This is horrendous, but that chunk in the middle is the only way the GraphQL backtick-quoted multiline string will format properly 52 | const hasuraOperation = ' `' + derive?.operation + '`\n\n' 53 | 54 | const derivedTemplate = 55 | template` 56 | import fetch from 'node-fetch' 57 | 58 | const HASURA_OPERATION =` + 59 | hasuraOperation + 60 | template` 61 | 62 | const execute = async (variables) => { 63 | const fetchResponse = await fetch('http://localhost:8080/v1/graphql', { 64 | method: 'POST', 65 | body: JSON.stringify({ 66 | query: HASURA_OPERATION, 67 | variables, 68 | }), 69 | }) 70 | const data = await fetchResponse.json() 71 | console.log('DEBUG: ', data) 72 | return data 73 | } 74 | 75 | // Request Handler 76 | app.post('/${actionName}', async (req, res) => { 77 | // get request input 78 | const params = req.body.input 79 | // execute the parent operation in Hasura 80 | const { data, errors } = await execute(params) 81 | if (errors) return res.status(400).json(errors[0]) 82 | // run some business logic 83 | 84 | // success 85 | return res.json(data) 86 | }) 87 | ` 88 | 89 | if (derive?.operation) return derivedTemplate 90 | else return baseTemplate 91 | } 92 | -------------------------------------------------------------------------------- /builder-kit/src/templates/kotlinHttp4k.codegen.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSchemaToKotlin } from '../languages-functional/kotlin' 2 | import { kotlinHttp4kTemplate } from '.' 3 | import { buildActionTypes } from '../schemaTools' 4 | import { DeriveParams } from '../types' 5 | 6 | const templater = ( 7 | actionName: string, 8 | actionSdl: string, 9 | derive: DeriveParams | null 10 | ) => { 11 | const actionParams = buildActionTypes(actionName, actionSdl) 12 | const codegen = kotlinHttp4kTemplate({ ...actionParams, derive }) 13 | const response = [ 14 | { 15 | name: actionName + '.kt', 16 | content: codegen, 17 | }, 18 | { 19 | name: actionName + 'Types.kt', 20 | content: graphqlSchemaToKotlin(actionSdl), 21 | }, 22 | ] 23 | return response 24 | } 25 | 26 | globalThis.templater = templater 27 | -------------------------------------------------------------------------------- /builder-kit/src/templates/kotlinHttp4k.ts: -------------------------------------------------------------------------------- 1 | import { html as template } from 'common-tags' 2 | import { CodegenTemplateParams } from '../types' 3 | 4 | // I am unsure of this one, need to have a Kotlin dev look this over probably 5 | export const kotlinHttp4kTemplate = (params: CodegenTemplateParams) => { 6 | const { actionArgs, actionName, returnType, typeDefs } = params 7 | 8 | return template` 9 | package org.hasura.my_action_handler 10 | 11 | import org.http4k.core.Body 12 | import org.http4k.core.Method.DELETE 13 | import org.http4k.core.Method.GET 14 | import org.http4k.core.Method.OPTIONS 15 | import org.http4k.core.Method.PATCH 16 | import org.http4k.core.Method.POST 17 | import org.http4k.core.Request 18 | import org.http4k.core.Response 19 | import org.http4k.core.Status.Companion.NOT_FOUND 20 | import org.http4k.core.Status.Companion.OK 21 | import org.http4k.core.then 22 | import org.http4k.core.with 23 | import org.http4k.filter.CorsPolicy.Companion.UnsafeGlobalPermissive 24 | import org.http4k.filter.DebuggingFilters 25 | import org.http4k.filter.ServerFilters.CatchLensFailure 26 | import org.http4k.filter.ServerFilters.Cors 27 | import org.http4k.format.Jackson.auto 28 | import org.http4k.lens.Path 29 | import org.http4k.lens.string 30 | import org.http4k.routing.bind 31 | import org.http4k.routing.routes 32 | import org.http4k.server.Jetty 33 | import org.http4k.server.asServer 34 | 35 | fun main(args: Array) { 36 | val port = if (args.isNotEmpty()) args[0] else "5000" 37 | val baseUrl = if (args.size > 1) args[1] else "http://localhost:$port" 38 | 39 | val ${actionName}ArgsLens = Body.auto<${actionName}Args>().toLens() 40 | 41 | fun ${actionName}Handler(${actionName}Args: ${actionName}ArgsLens): HttpHandler = { request: Request -> 42 | // Business logic here 43 | Response(OK).with(stringBody of "$${actionName}Args") 44 | } 45 | 46 | DebuggingFilters 47 | .PrintRequestAndResponse() 48 | .then(Cors(UnsafeGlobalPermissive)) 49 | .then(CatchLensFailure) 50 | .then(routes( 51 | "/{any:.*}" bind OPTIONS to { _: Request -> Response(OK) }, 52 | "/" bind POST to ${actionName}Handler(${actionName}ArgsLens) }, 53 | )) 54 | .asServer(Jetty(port.toInt())).start().block() 55 | } 56 | ` 57 | } 58 | -------------------------------------------------------------------------------- /builder-kit/src/templates/kotlinKtor.codegen.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSchemaToKotlin } from '../languages-functional' 2 | import { kotlinKtorTemplate } from '../templates' 3 | import { buildActionTypes } from '../schemaTools' 4 | import { DeriveParams } from '../types' 5 | 6 | const templater = ( 7 | actionName: string, 8 | actionSdl: string, 9 | derive: DeriveParams | null 10 | ) => { 11 | const actionParams = buildActionTypes(actionName, actionSdl) 12 | const codegen = kotlinKtorTemplate({ ...actionParams, derive }) 13 | const response = [ 14 | { 15 | name: actionName + '.kt', 16 | content: codegen, 17 | }, 18 | { 19 | name: actionName + 'Types.kt', 20 | content: graphqlSchemaToKotlin(actionSdl), 21 | }, 22 | ] 23 | return response 24 | } 25 | 26 | globalThis.templater = templater 27 | -------------------------------------------------------------------------------- /builder-kit/src/templates/kotlinKtor.ts: -------------------------------------------------------------------------------- 1 | import { html as template } from 'common-tags' 2 | import { CodegenTemplateParams } from '../types' 3 | 4 | export const kotlinKtorTemplate = (params: CodegenTemplateParams) => { 5 | const { actionName } = params 6 | 7 | return template` 8 | package org.hasura.my_action_handler 9 | 10 | import io.ktor.application.* 11 | import io.ktor.response.* 12 | import io.ktor.request.* 13 | import io.ktor.routing.* 14 | import io.ktor.http.* 15 | import com.fasterxml.jackson.databind.* 16 | import io.ktor.jackson.* 17 | import io.ktor.features.* 18 | 19 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) 20 | 21 | @Suppress("unused") // Referenced in application.conf 22 | @kotlin.jvm.JvmOverloads 23 | fun Application.module(testing: Boolean = false) { 24 | install(ContentNegotiation) { 25 | jackson { 26 | enable(SerializationFeature.INDENT_OUTPUT) 27 | } 28 | } 29 | 30 | install(CORS) { 31 | method(HttpMethod.Options) 32 | method(HttpMethod.Put) 33 | method(HttpMethod.Delete) 34 | method(HttpMethod.Patch) 35 | header(HttpHeaders.Authorization) 36 | header("MyCustomHeader") 37 | allowCredentials = true 38 | anyHost() // @TODO: Don't do this in production if possible. Try to limit it. 39 | } 40 | 41 | routing { 42 | post("/${actionName}") { 43 | val input = call.receive<${actionName}Args>() 44 | // Business logic here 45 | call.respond(mapOf("hello" to "world")) 46 | } 47 | } 48 | }` 49 | } 50 | -------------------------------------------------------------------------------- /builder-kit/src/templates/pythonFastAPI.codegen.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSchemaToPython } from '../languages-functional' 2 | import { pythonFastAPITemplate } from '../templates' 3 | import { buildActionTypes } from '../schemaTools' 4 | import { DeriveParams } from '../types' 5 | 6 | const templater = ( 7 | actionName: string, 8 | actionSdl: string, 9 | derive: DeriveParams | null 10 | ) => { 11 | const actionParams = buildActionTypes(actionName, actionSdl) 12 | const codegen = pythonFastAPITemplate({ ...actionParams, derive }) 13 | const response = [ 14 | { 15 | name: actionName + '.py', 16 | content: codegen, 17 | }, 18 | { 19 | name: actionName + 'Types.py', 20 | content: graphqlSchemaToPython(actionSdl), 21 | }, 22 | ] 23 | return response 24 | } 25 | 26 | globalThis.templater = templater 27 | -------------------------------------------------------------------------------- /builder-kit/src/templates/pythonFastAPI.ts: -------------------------------------------------------------------------------- 1 | import { html as template } from 'common-tags' 2 | import { CodegenTemplateParams } from '../types' 3 | 4 | export const pythonFastAPITemplate = (params: CodegenTemplateParams) => { 5 | const { actionName, returnType } = params 6 | 7 | let baseTemplate: string = template` 8 | from fastapi import FastAPI 9 | from typing import Generic, TypeVar 10 | from pydantic import BaseModel 11 | from pydantic.generics import GenericModel 12 | from ${actionName}Types import ${actionName}Args, ${returnType} 13 | 14 | 15 | ActionInput = TypeVar("ActionInput", bound=BaseModel | None) 16 | 17 | 18 | class ActionName(BaseModel): 19 | name: str 20 | 21 | 22 | class ActionPayload(GenericModel, Generic[ActionInput]): 23 | action: ActionName 24 | input: ActionInput 25 | request_query: str 26 | session_variables: dict[str, str] 27 | 28 | 29 | app = FastAPI() 30 | 31 | 32 | @app.post("/${actionName}") 33 | async def ${actionName}Handler(action: ActionPayload[${actionName}Args]) -> ${returnType}: 34 | # business logic here 35 | return ${returnType}() 36 | ` 37 | 38 | return baseTemplate 39 | } 40 | -------------------------------------------------------------------------------- /builder-kit/src/templates/pythonFlask.codegen.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSchemaToPython } from '../languages-functional' 2 | import { pythonFlaskTemplate } from './pythonFlask' 3 | import { buildActionTypes } from '../schemaTools' 4 | import { DeriveParams } from '../types' 5 | 6 | const templater = ( 7 | actionName: string, 8 | actionSdl: string, 9 | derive: DeriveParams | null 10 | ) => { 11 | const actionParams = buildActionTypes(actionName, actionSdl) 12 | const codegen = pythonFlaskTemplate({ ...actionParams, derive }) 13 | const response = [ 14 | { 15 | name: actionName + '.py', 16 | content: codegen, 17 | }, 18 | { 19 | name: actionName + 'Types.py', 20 | content: graphqlSchemaToPython(actionSdl), 21 | }, 22 | ] 23 | return response 24 | } 25 | 26 | globalThis.templater = templater 27 | -------------------------------------------------------------------------------- /builder-kit/src/templates/pythonFlask.ts: -------------------------------------------------------------------------------- 1 | import { html as template } from 'common-tags' 2 | import { CodegenTemplateParams } from '../types' 3 | 4 | export const pythonFlaskTemplate = (params: CodegenTemplateParams) => { 5 | const { actionName, returnType } = params 6 | 7 | let baseTemplate: string = template` 8 | from flask import Flask 9 | from flask_pydantic import validate 10 | from typing import Generic, TypeVar 11 | from pydantic import BaseModel 12 | from pydantic.generics import GenericModel 13 | from ${actionName}Types import ${actionName}Args, ${returnType} 14 | 15 | 16 | ActionInput = TypeVar("ActionInput", bound=BaseModel | None) 17 | 18 | 19 | class ActionName(BaseModel): 20 | name: str 21 | 22 | 23 | class ActionPayload(GenericModel, Generic[ActionInput]): 24 | action: ActionName 25 | input: ActionInput 26 | request_query: str 27 | session_variables: dict[str, str] 28 | 29 | 30 | app = Flask(__name__) 31 | 32 | 33 | @app.route('/${actionName}', methods=['POST']) 34 | @validate() 35 | def ${actionName}Handler(action: ActionPayload[${actionName}Args]) -> ${returnType}: 36 | # business logic here 37 | return ${returnType}() 38 | ` 39 | 40 | return baseTemplate 41 | } 42 | -------------------------------------------------------------------------------- /builder-kit/src/templates/rubyRails.codegen.ts: -------------------------------------------------------------------------------- 1 | import { rubyRailsTemplate } from './rubyRails' 2 | import { buildActionTypes } from '../schemaTools' 3 | import { DeriveParams } from '../types' 4 | 5 | const templater = ( 6 | actionName: string, 7 | actionSdl: string, 8 | derive: DeriveParams | null 9 | ) => { 10 | const actionParams = buildActionTypes(actionName, actionSdl) 11 | const codegen = rubyRailsTemplate({ ...actionParams, derive }) 12 | const response = [ 13 | { 14 | name: actionName + '.rb', 15 | content: codegen, 16 | }, 17 | ] 18 | return response 19 | } 20 | 21 | globalThis.templater = templater 22 | -------------------------------------------------------------------------------- /builder-kit/src/templates/rubyRails.ts: -------------------------------------------------------------------------------- 1 | import { snakeCase } from '../utils' 2 | import { html as template } from 'common-tags' 3 | import { CodegenTemplateParams } from '../types' 4 | 5 | export const rubyRailsTemplate = (params: CodegenTemplateParams) => { 6 | const { actionName } = params 7 | 8 | let baseTemplate: string = template` 9 | begin 10 | require "bundler/inline" 11 | rescue LoadError => e 12 | $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" 13 | raise e 14 | end 15 | 16 | gemfile(true) do 17 | source "https://rubygems.org" 18 | gem 'rails', '~> 6.0.0' 19 | end 20 | 21 | require "action_controller/railtie" 22 | 23 | class App < Rails::Application 24 | routes.append do 25 | post "/${actionName}" => "hasura#${snakeCase(actionName)}_handler" 26 | end 27 | 28 | config.consider_all_requests_local = true # display errors 29 | end 30 | 31 | class HasuraController < ActionController::API 32 | def ${snakeCase(actionName)}_handler 33 | request_data = params[:input] 34 | puts request_data 35 | render json: request_data 36 | end 37 | end 38 | 39 | App.initialize! 40 | Rack::Server.new(app: App, Port: 3000).start 41 | ` 42 | 43 | return baseTemplate 44 | } 45 | -------------------------------------------------------------------------------- /builder-kit/src/templates/typescriptExpress.codegen.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSchemaToTypescript } from '../languages-functional' 2 | import { typescriptExpressTemplate } from '../templates' 3 | import { buildActionTypes } from '../schemaTools' 4 | import { DeriveParams } from '../types' 5 | 6 | const templater = ( 7 | actionName: string, 8 | actionSdl: string, 9 | derive: DeriveParams | null 10 | ) => { 11 | const actionParams = buildActionTypes(actionName, actionSdl) 12 | const codegen = typescriptExpressTemplate({ ...actionParams, derive }) 13 | const response = [ 14 | { 15 | name: actionName + '.ts', 16 | content: codegen, 17 | }, 18 | { 19 | name: actionName + 'Types.ts', 20 | content: graphqlSchemaToTypescript(actionSdl), 21 | }, 22 | ] 23 | return response 24 | } 25 | 26 | globalThis.templater = templater 27 | -------------------------------------------------------------------------------- /builder-kit/src/templates/typescriptExpress.ts: -------------------------------------------------------------------------------- 1 | import { html as template } from 'common-tags' 2 | import { CodegenTemplateParams } from '../types' 3 | import { NEWLINE } from '../utils' 4 | 5 | const sampleValues = { 6 | Int: 1111, 7 | String: '""', 8 | Boolean: false, 9 | Float: 11.11, 10 | ID: 1111, 11 | } 12 | 13 | export const typescriptExpressTemplate = (params: CodegenTemplateParams) => { 14 | const { actionName, returnType, derive, typeMap } = params 15 | 16 | const returnTypeDef = typeMap.types[returnType] 17 | 18 | const baseTemplate = template` 19 | import { Request, Response } from 'express' 20 | 21 | function ${actionName}Handler(args: ${actionName}Args): ${returnType} { 22 | return { 23 | ${returnTypeDef 24 | .map((f) => { 25 | return `${f.getName()}: ${ 26 | sampleValues[f.getType().getTypename()] || sampleValues['String'] 27 | }` 28 | }) 29 | .join(',\n')}, 30 | } 31 | } 32 | 33 | // Request Handler 34 | app.post('/${actionName}', async (req: Request, res: Response) => { 35 | // get request input 36 | const params: ${actionName}Args = req.body.input 37 | 38 | // run some business logic 39 | const result = ${actionName}Handler(params) 40 | 41 | /* 42 | // In case of errors: 43 | return res.status(400).json({ 44 | message: "error happened" 45 | }) 46 | */ 47 | 48 | // success 49 | return res.json(result) 50 | }) 51 | ` 52 | 53 | // This is horrendous, but that chunk in the middle is the only way the GraphQL backtick-quoted multiline string will format properly 54 | const hasuraOperation = ' `' + derive?.operation + '`\n\n' 55 | 56 | const derivedTemplate = 57 | template` 58 | import { Request, Response } from 'express' 59 | import fetch from 'node-fetch' 60 | const HASURA_OPERATION =` + 61 | hasuraOperation + 62 | template` 63 | 64 | const execute = async (variables) => { 65 | const fetchResponse = await fetch('http://localhost:8080/v1/graphql', { 66 | method: 'POST', 67 | body: JSON.stringify({ 68 | query: HASURA_OPERATION, 69 | variables, 70 | }), 71 | }) 72 | const data = await fetchResponse.json() 73 | console.log('DEBUG: ', data) 74 | return data 75 | } 76 | 77 | // Request Handler 78 | app.post('/${actionName}', async (req: Request, res: Response) => { 79 | // get request input 80 | const params: ${actionName}Args = req.body.input 81 | // execute the parent operation in Hasura 82 | const { data, errors } = await execute(params) 83 | if (errors) return res.status(400).json(errors[0]) 84 | // run some business logic 85 | 86 | // success 87 | return res.json(data) 88 | }) 89 | ` 90 | 91 | if (derive?.operation) return derivedTemplate 92 | else return baseTemplate 93 | } 94 | -------------------------------------------------------------------------------- /builder-kit/src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldDefinitionApi, 3 | ScalarTypeApi, 4 | ArgumentApi, 5 | InputValueDefinitionApi, 6 | ScalarTypeDefinitionNodeProps, 7 | EnumValueDefinitionApi, 8 | } from 'graphql-extra' 9 | 10 | /** 11 | * LanguageTypeConverterConfigs are stripped-down BaseTypeConverterConfig that require 12 | * the user provide only the schema and Action name, because in the constructor the rest of 13 | * values for scalarMap, type, and field formatting have already been pre-filled. 14 | */ 15 | export interface LanguageTypeConverterConfig { 16 | schema: string 17 | isAction?: boolean 18 | } 19 | 20 | /** 21 | * An Enum for GraphQL scalars 22 | * Used to compose ScalarMaps for language-specific types codegen 23 | */ 24 | export enum ScalarTypes { 25 | ID = 'ID', 26 | INT = 'Int', 27 | FLOAT = 'Float', 28 | STRING = 'String', 29 | BOOLEAN = 'Boolean', 30 | } 31 | /** 32 | * An interface that TypeConverters must implement for how to 33 | * map GraphQL scalars to their corresponding language types 34 | */ 35 | export type ScalarMap = { 36 | [key in ScalarTypes]: string 37 | } 38 | 39 | /** 40 | * @param name the original type/name of the type 41 | * @param type the converted language-specific type (if scalar) 42 | * @param required whether the type is required or not 43 | * @param list whether the type is a list/array 44 | */ 45 | export interface ITypeNode { 46 | name: string 47 | type: string 48 | required: boolean 49 | list: boolean 50 | } 51 | 52 | export interface IEnumNode { 53 | value: string 54 | } 55 | 56 | /** 57 | * A convenience interface for the reprensentation of a field type in GraphQL 58 | * Provides formatted information about name, type, nullability, and is list 59 | * @interface IField 60 | */ 61 | export interface IField { 62 | name: string 63 | type: ITypeNode 64 | required: boolean 65 | } 66 | 67 | /** 68 | * An interface for the paramaters of Action codegen functions 69 | * The type provides the name, return type, list of action arguments 70 | * and a type-map of all types in the Action SDL 71 | * @interface ActionParams 72 | */ 73 | export interface ActionParams { 74 | actionName: string 75 | actionArgs: InputValueDefinitionApi[] 76 | returnType: string 77 | typeMap: ITypeMap 78 | } 79 | 80 | export interface CodegenTemplateParams extends ActionParams { 81 | typeDefs?: string 82 | derive: DeriveParams | null 83 | } 84 | 85 | export type Fieldlike = FieldDefinitionApi | InputValueDefinitionApi 86 | 87 | export interface ITypeMap { 88 | types: Record 89 | enums: Record 90 | scalars: Record 91 | } 92 | 93 | /** 94 | * A type for the "derive" parameter of codegen templates 95 | * @interface DeriveParams 96 | */ 97 | export interface DeriveParams { 98 | operation: string 99 | endpoint: string 100 | } 101 | -------------------------------------------------------------------------------- /builder-kit/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ScalarTypes, Fieldlike } from './types' 2 | import { parse } from 'graphql' 3 | 4 | export const NEWLINE = '\n' 5 | const SPACE = ' ' 6 | 7 | export const indent = (string, tabSize = 2) => SPACE.repeat(tabSize) + string 8 | 9 | /** 10 | * Checks if type string exists in ScalarMap 11 | */ 12 | export const isScalar = (type: string) => { 13 | return type.toUpperCase() in ScalarTypes 14 | } 15 | 16 | /** 17 | * Capitalizes a string 18 | */ 19 | export const capitalize = (str: string) => { 20 | if (!str.length) return str 21 | return str[0].toUpperCase() + str.substring(1) 22 | } 23 | 24 | /** 25 | * Function which creates letter-case converters 26 | * Example: caseConverter("_") -> "SomeString" -> "some_string" 27 | */ 28 | const caseConverter = (symbol) => (string) => 29 | string 30 | .match(/[A-Z]{2,}(?=[A-Z][a-z0-9]*|\b)|[A-Z]?[a-z0-9]*|[A-Z]|[0-9]+/g) 31 | .filter(Boolean) 32 | .map((x) => x.toLowerCase()) 33 | .join(symbol) 34 | 35 | export const kebabCase = caseConverter('-') 36 | export const snakeCase = caseConverter('_') 37 | /** 38 | * Returns the first root field from an operation 39 | * Ex: Returns "user" if the operation is "query { user { id name } articles { title content } }" 40 | */ 41 | export const getRootFieldName = ( 42 | operationString: string, 43 | shouldCapitalize = false 44 | ) => { 45 | try { 46 | const doc = parse(operationString) 47 | const operation: any = doc.definitions[0] 48 | const selection = operation.selectionSet.selections[0] 49 | const name: string = selection.alias 50 | ? selection.alias.value 51 | : selection.name.value 52 | return shouldCapitalize ? capitalize(name) : name 53 | } catch (err) { 54 | console.error('Got error in getRootFieldName:', err) 55 | } 56 | } 57 | 58 | /** 59 | * Takes a Field from graphql-extra's FieldDefinitionApi and serializes it 60 | * to extract and format the important information: 61 | * Name, Type, Nullability, and whether it's a list 62 | */ 63 | export const serialize = (field: Fieldlike) => ({ 64 | name: field.getName(), 65 | required: field.isNonNullType(), 66 | list: field.isListType(), 67 | type: field.getTypename(), 68 | }) 69 | 70 | const _pipe = (f, g) => (...args) => g(f(...args)) 71 | export const pipe = (...fns) => fns.reduce(_pipe) 72 | -------------------------------------------------------------------------------- /builder-kit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | "lib": [] /* Specify library files to be included in the compilation. */, 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true /* Generates corresponding '.map' file. */, 14 | // "outFile": "" /* Concatenate and emit output to single file. */, 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": false /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /builder-kit/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | import * as path from 'path' 3 | import * as webpack from 'webpack' 4 | 5 | const kebabCase = (string) => 6 | string 7 | .match(/[A-Z]{2,}(?=[A-Z][a-z0-9]*|\b)|[A-Z]?[a-z0-9]*|[A-Z]|[0-9]+/g) 8 | .filter(Boolean) 9 | .map((x) => x.toLowerCase()) 10 | .join('-') 11 | 12 | const codegenFiles = glob 13 | .sync('./src/templates/**.codegen.ts') 14 | .reduce((res, path) => { 15 | const [fileName] = path.replace('./src/templates/', '').split('.') 16 | const folderName = kebabCase(fileName) 17 | res[folderName] = path 18 | return res 19 | }, {}) 20 | 21 | const config: webpack.Configuration = { 22 | entry: codegenFiles, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.ts?$/, 27 | use: 'ts-loader', 28 | exclude: /node_modules/, 29 | }, 30 | ], 31 | }, 32 | resolve: { 33 | extensions: ['.ts', '.js'], 34 | }, 35 | output: { 36 | filename: '[name]/actions-codegen.js', 37 | path: path.resolve(__dirname, '../'), 38 | }, 39 | } 40 | 41 | export default config 42 | -------------------------------------------------------------------------------- /frameworks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "nodejs-express", 4 | "hasStarterKit": true 5 | }, 6 | { 7 | "name": "nodejs-zeit", 8 | "hasStarterKit": true 9 | }, 10 | { 11 | "name": "typescript-zeit", 12 | "hasStarterKit": true 13 | }, 14 | { 15 | "name": "nodejs-azure-function", 16 | "hasStarterKit": false 17 | }, 18 | { 19 | "name": "go-serve-mux", 20 | "hasStarterKit": false 21 | }, 22 | { 23 | "name": "javascript-js-doc-express", 24 | "hasStarterKit": false 25 | }, 26 | { 27 | "name": "kotlin-http4k", 28 | "hasStarterKit": false 29 | }, 30 | { 31 | "name": "kotlin-ktor", 32 | "hasStarterKit": false 33 | }, 34 | { 35 | "name": "python-fast-api", 36 | "hasStarterKit": false 37 | }, 38 | { 39 | "name": "python-flask", 40 | "hasStarterKit": false 41 | }, 42 | { 43 | "name": "ruby-rails", 44 | "hasStarterKit": false 45 | }, 46 | { 47 | "name": "typescript-express", 48 | "hasStarterKit": false 49 | }, 50 | { 51 | "name": "java-spring-boot", 52 | "hasStarterKit": false 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /nodejs-azure-function/actions-codegen.js: -------------------------------------------------------------------------------- 1 | /* Files to be generated: handler and types (optional) 2 | 3 | - Take input from request JSON body 4 | - Extract Hasura payload into an object. 5 | { 6 | session_variables: { 7 | 'x-hasura-user-id': <>, 8 | 'x-hasura-role': <>, 9 | 'x-hasura-xxxx': <> 10 | }, 11 | input: { 12 | arg1: <>, 13 | arg2: <> 14 | } 15 | } 16 | - If this is a typed language: 17 | - codegen the types in the second file & 18 | - parse into the right type if this is a typed language 19 | - If this is derived from a hasura operation 20 | - Take the inputs and use them to create variables 21 | - Pass these variables and fire the underlying Hasura operation 22 | - If Hasura operation returns error, return error 23 | - Else return the exact response that the Hasura operation returns 24 | - If this is not derived: 25 | - Have a small error block showing what an error response would look like 26 | - Return a success with a JSON object, the keys set to the return type 27 | of the action 28 | */ 29 | 30 | // Generate the basic handler 31 | // For azure 32 | const generateHandler = (extractArgsFromBody, outputTypeSpread) => { 33 | const handlerBeginningCode = ` 34 | module.exports = async function (context, req) { 35 | 36 | ${extractArgsFromBody} 37 | 38 | // Write business logic that deals with inputs here... 39 | 40 | `; 41 | 42 | const errorSuccessHandlerResponseCode = ` 43 | // If error: 44 | // context.res = { 45 | // status: 400, 46 | // body: { code: "internal-error", message: "Internal error." } 47 | // }; 48 | // return; 49 | 50 | context.res = { 51 | headers: { 'Content-Type': 'application/json' }, 52 | body: ${outputTypeSpread} 53 | }; 54 | };` 55 | return {handlerBeginningCode, errorSuccessHandlerResponseCode}; 56 | }; 57 | 58 | // Generate all the custom types 59 | // - Input types for all actions (including the current one this is invoked for) 60 | // - Output types for all actions (including the current one this is invoked for) 61 | 62 | 63 | // Template code for generating a fetch API call 64 | // Only used for derive 65 | const generateFetch = (actionName, rawQuery, variables, queryRootFieldName, derive) => { 66 | const queryName = 'HASURA_' + actionName.toUpperCase(); 67 | let actionNameUpper = actionName[0].toUpperCase() + actionName.slice(1); 68 | 69 | const fetchExecuteCode = ` 70 | const fetch = require ('node-fetch'); 71 | 72 | const ${queryName} = \` 73 | ${rawQuery} 74 | \`; 75 | 76 | const execute${actionNameUpper} = async (variables) => { 77 | const result = await fetch ("${derive.endpoint || 'http://localhost:8080/v1/graphql'}", { 78 | method: 'POST', 79 | body: JSON.stringify({ 80 | query: ${queryName}, 81 | variables 82 | }) 83 | }); 84 | 85 | const data = await result.json(); 86 | console.log('DEBUG: ', data); 87 | return data; 88 | }; 89 | ` 90 | 91 | const runExecuteInHandlerCode = ` 92 | // Execute the Hasura query 93 | const {data, errors} = await execute${actionNameUpper}(${variables}, headers); 94 | 95 | // If there's an error in the running the Hasura query 96 | if (errors) { 97 | context.res = { 98 | headers: { 'Content-Type': 'application/json' }, 99 | status: 400, 100 | body: errors[0] 101 | }; 102 | return; 103 | } 104 | 105 | // If success 106 | context.res = { 107 | headers: { 'Content-Type': 'application/json' }, 108 | body: { 109 | ...data.${queryRootFieldName} 110 | } 111 | }; 112 | };`; 113 | 114 | return {fetchExecuteCode, runExecuteInHandlerCode}; 115 | }; 116 | 117 | // actionName: Name of the action 118 | // actionsSdl: GraphQL SDL string that has the action and dependent types 119 | // derive: Whether this action was asked to be derived from a Hasura operation 120 | // derive.operation contains the operation string 121 | const templater = (actionName, actionsSdl, derive) => { 122 | 123 | console.log('Running the codegen for: ' + actionName); 124 | 125 | // Parse the actions SDL into an AST 126 | let ast; 127 | ast = parse(actionsSdl); 128 | 129 | // Find the type for this action 130 | let actionDef; 131 | for (var i = ast.definitions.length - 1; i >= 0; i--) { 132 | const typeDef = ast.definitions[i]; 133 | if (typeDef.name.value === 'Mutation' || typeDef.name.value === 'Query') { 134 | actionDef = typeDef 135 | .fields 136 | .find(def => (def.name.value === actionName)); 137 | if (!!actionDef) { 138 | break; 139 | } 140 | } 141 | } 142 | 143 | // If the input arguments are {name, age, email} 144 | // then we want to generate: const {name, age, email} = req.body 145 | console.log(actionDef); 146 | const inputArgumentsNames = actionDef.arguments.map(i => i.name.value); 147 | console.log('Input arguments: ' + inputArgumentsNames); 148 | 149 | const extractArgsFromBody = `const {${inputArgumentsNames.join(', ')}} = req.body.input;`; 150 | 151 | // If the output type is type ActionResult {field1: <>, field2: <>} 152 | // we want to template the response of the handler to be: 153 | // { 154 | // field1: "", 155 | // field2: "" 156 | // } 157 | const actionOutputType = ast.definitions 158 | .find(def => (def.name.value === actionDef.type.name.value)) 159 | console.log('Output type: ' + actionOutputType.name.value); 160 | const outputTypeFieldNames = actionOutputType.fields.map(f => f.name.value); 161 | console.log('Output type fields: ' + outputTypeFieldNames); 162 | 163 | let outputTypeSpread = '{\n '; 164 | outputTypeFieldNames.forEach((n, i) => { 165 | outputTypeSpread += n + ': ""'; 166 | if (i === outputTypeFieldNames.length - 1) { 167 | outputTypeSpread += '\n }' 168 | } else { 169 | outputTypeSpread += ',\n '; 170 | } 171 | }); 172 | 173 | console.log('Generating base hanlder...'); 174 | const basicHandlerCode = generateHandler(extractArgsFromBody, outputTypeSpread); 175 | 176 | console.log(basicHandlerCode); 177 | // If this action is being derived for an existing operation 178 | // then we'll add a fetch API call 179 | let deriveCode; 180 | let isDerivation = derive && derive.operation; 181 | if (isDerivation) { 182 | const operationAST = parse(derive.operation); 183 | const queryRootField = operationAST.definitions[0].selectionSet 184 | .selections 185 | .find(f => (!f.name.value.startsWith('__'))) 186 | const queryRootFieldName = queryRootField.alias ? 187 | queryRootField.alias.value : 188 | queryRootField.name.value; 189 | const variableNames = operationAST.definitions[0] 190 | .variableDefinitions 191 | .map(vdef => vdef.variable.name.value); 192 | 193 | console.log('Derive:: \n' + derive.operation); 194 | console.log('Derive:: root field name: ' + queryRootFieldName); 195 | console.log('Derive:: variable names: ' + variableNames); 196 | 197 | deriveCode = generateFetch( 198 | actionName, 199 | derive.operation, 200 | `{ ${variableNames.join(', ')} }`, 201 | queryRootFieldName, 202 | derive 203 | ); 204 | } 205 | 206 | // Render the handler! 207 | console.log('Rendering handler'); 208 | let finalHandlerCode = '' 209 | if (!isDerivation) { 210 | finalHandlerCode += 211 | basicHandlerCode.handlerBeginningCode + 212 | basicHandlerCode.errorSuccessHandlerResponseCode; 213 | } else { 214 | finalHandlerCode += 215 | deriveCode.fetchExecuteCode + 216 | basicHandlerCode.handlerBeginningCode + 217 | deriveCode.runExecuteInHandlerCode; 218 | } 219 | 220 | console.log(finalHandlerCode); 221 | 222 | return [ 223 | { 224 | name: actionName + '.js', 225 | content: finalHandlerCode 226 | } 227 | ]; 228 | }; 229 | -------------------------------------------------------------------------------- /nodejs-express/actions-codegen.js: -------------------------------------------------------------------------------- 1 | const templater = (actionName, actionsSdl, derive) => { 2 | 3 | const ast = parse(`${actionsSdl}`); 4 | 5 | let actionDef; 6 | 7 | const actionAst = { 8 | ...ast, 9 | definitions: ast.definitions.filter(d => { 10 | if ((d.name.value === 'Mutation' || d.name.value === 'Query') && (d.kind === 'ObjectTypeDefinition' || d.kind === 'ObjectTypeExtension')) { 11 | if (actionDef) return false 12 | actionDef = d.fields.find(f => f.name.value === actionName); 13 | if (!actionDef) { 14 | return false; 15 | } else { 16 | return true; 17 | } 18 | } 19 | return false; 20 | }) 21 | } 22 | 23 | const actionArguments = actionDef.arguments; 24 | let actionOutputType = actionDef.type; 25 | 26 | while (actionOutputType.kind !== 'NamedType') { 27 | actionOutputType = actionOutputType.type; 28 | } 29 | const outputType = ast.definitions.find(d => { 30 | return (d.kind === 'ObjectTypeDefinition' && d.name.value === actionOutputType.name.value) 31 | }); 32 | 33 | const outputTypeFields = outputType.fields.map(f => f.name.value); 34 | 35 | let graphqlClientCode = ''; 36 | let operationCodegen = ''; 37 | let validateFunction = ''; 38 | let errorSnippet = ''; 39 | let successSnippet = ''; 40 | let executeFunction = ''; 41 | 42 | const requestInputDestructured = `{ ${actionDef.arguments.map(a => a.name.value).join(', ')} }`; 43 | 44 | const shouldDerive = !!(derive && derive.operation); 45 | const hasuraEndpoint = derive && derive.endpoint ? derive.endpoint : 'http://localhost:8080/v1/graphql'; 46 | if (shouldDerive) { 47 | 48 | const operationDoc = parse(derive.operation); 49 | const operationName = operationDoc.definitions[0].selectionSet.selections.filter(s => s.name.value.indexOf('__') !== 0)[0].name.value; 50 | 51 | operationCodegen = ` 52 | const HASURA_OPERATION = \` 53 | ${derive.operation} 54 | \`;`; 55 | 56 | executeFunction = ` 57 | // execute the parent operation in Hasura 58 | const execute = async (variables) => { 59 | const fetchResponse = await fetch( 60 | "${hasuraEndpoint}", 61 | { 62 | method: 'POST', 63 | body: JSON.stringify({ 64 | query: HASURA_OPERATION, 65 | variables 66 | }) 67 | } 68 | ); 69 | const data = await fetchResponse.json(); 70 | console.log('DEBUG: ', data); 71 | return data; 72 | }; 73 | ` 74 | 75 | graphqlClientCode = ` 76 | // execute the Hasura operation 77 | const { data, errors } = await execute(${requestInputDestructured});` 78 | 79 | errorSnippet = ` // if Hasura operation errors, then throw error 80 | if (errors) { 81 | return res.status(400).json(errors[0]) 82 | }`; 83 | 84 | successSnippet = ` // success 85 | return res.json({ 86 | ...data.${operationName} 87 | })` 88 | 89 | } 90 | 91 | if (!errorSnippet) { 92 | errorSnippet = ` /* 93 | // In case of errors: 94 | return res.status(400).json({ 95 | message: "error happened" 96 | }) 97 | */` 98 | } 99 | 100 | if (!successSnippet) { 101 | successSnippet = ` // success 102 | return res.json({ 103 | ${outputTypeFields.map(f => ` ${f}: ""`).join(',\n')} 104 | })`; 105 | } 106 | 107 | const handlerContent = ` 108 | ${shouldDerive ? 'const fetch = require("node-fetch")\n' : ''}${shouldDerive ? `${operationCodegen}\n` : ''}${shouldDerive ? `${executeFunction}\n` : ''} 109 | // Request Handler 110 | app.post('/${actionName}', async (req, res) => { 111 | 112 | // get request input 113 | const ${requestInputDestructured} = req.body.input; 114 | 115 | // run some business logic 116 | ${shouldDerive ? graphqlClientCode : ''} 117 | 118 | ${errorSnippet} 119 | 120 | ${successSnippet} 121 | 122 | }); 123 | `; 124 | 125 | const handlerFile = { 126 | name: `${actionName}.js`, 127 | content: handlerContent 128 | } 129 | 130 | return [handlerFile]; 131 | 132 | } 133 | -------------------------------------------------------------------------------- /nodejs-express/nodejs-express.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/codegen-assets/36f4d0f3103dd912db389ef33da6fa182672ec67/nodejs-express/nodejs-express.zip -------------------------------------------------------------------------------- /nodejs-express/starter-kit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /nodejs-express/starter-kit/README.md: -------------------------------------------------------------------------------- 1 | # nodejs-express 2 | 3 | This is a starter kit for `nodejs` with `express`. To get started: 4 | 5 | Firstly, [download the starter-kit](https://github.com/hasura/codegen-assets/raw/master/nodejs-express/nodejs-express.zip) and `cd` into it. 6 | 7 | ``` 8 | npm ci 9 | npm start 10 | ``` 11 | 12 | ## Development 13 | 14 | The entrypoint for the server lives in `src/server.js`. 15 | 16 | If you wish to add a new route (say `/greet`) , you can add it directly in the `server.js` as: 17 | 18 | ```js 19 | app.post('/greet', (req, res) => { 20 | return res.json({ 21 | "greeting": "have a nice day" 22 | }); 23 | }); 24 | ``` 25 | 26 | ### Throwing errors 27 | 28 | You can throw an error object or a list of error objects from your handler. The response must be 4xx and the error object must have a string field called `message`. 29 | 30 | ```js 31 | retun res.status(400).json({ 32 | message: 'invalid email' 33 | }); 34 | ``` 35 | -------------------------------------------------------------------------------- /nodejs-express/starter-kit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-express-actions", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "nodemon -r esm src/server.js" 8 | }, 9 | "dependencies": { 10 | "body-parser": "1.19.0", 11 | "express": "4.16.4", 12 | "node-fetch": "2.6.0" 13 | }, 14 | "devDependencies": { 15 | "esm": "^3.2.25", 16 | "nodemon": "1.18.4" 17 | }, 18 | "keywords": [] 19 | } 20 | -------------------------------------------------------------------------------- /nodejs-express/starter-kit/src/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const bodyParser = require("body-parser"); 3 | 4 | const app = express(); 5 | 6 | const PORT = process.env.PORT || 3000; 7 | 8 | app.use(bodyParser.json()); 9 | 10 | app.post('/hello', async (req, res) => { 11 | return res.json({ 12 | hello: "world" 13 | }); 14 | }); 15 | 16 | app.listen(PORT); 17 | -------------------------------------------------------------------------------- /nodejs-zeit/actions-codegen.js: -------------------------------------------------------------------------------- 1 | const templater = (actionName, actionsSdl, derive) => { 2 | 3 | const ast = parse(`${actionsSdl}`); 4 | 5 | let actionDef; 6 | 7 | const actionAst = { 8 | ...ast, 9 | definitions: ast.definitions.filter(d => { 10 | if ((d.name.value === 'Mutation' || d.name.value === 'Query') && (d.kind === 'ObjectTypeDefinition' || d.kind === 'ObjectTypeExtension')) { 11 | if (actionDef) return false 12 | actionDef = d.fields.find(f => f.name.value === actionName); 13 | if (!actionDef) { 14 | return false; 15 | } else { 16 | return true; 17 | } 18 | } 19 | return false; 20 | }) 21 | } 22 | 23 | const actionArguments = actionDef.arguments; 24 | let actionOutputType = actionDef.type; 25 | 26 | while (actionOutputType.kind !== 'NamedType') { 27 | actionOutputType = actionOutputType.type; 28 | } 29 | const outputType = ast.definitions.find(d => { 30 | return (d.kind === 'ObjectTypeDefinition' && d.name.value === actionOutputType.name.value) 31 | }); 32 | 33 | const outputTypeFields = outputType.fields.map(f => f.name.value); 34 | 35 | let graphqlClientCode = ''; 36 | let operationCodegen = ''; 37 | let validateFunction = ''; 38 | let errorSnippet = ''; 39 | let successSnippet = ''; 40 | let executeFunction = ''; 41 | 42 | const requestInputDestructured = `{ ${actionDef.arguments.map(a => a.name.value).join(', ')} }`; 43 | 44 | const shouldDerive = !!(derive && derive.operation); 45 | const hasuraEndpoint = derive && derive.endpoint ? derive.endpoint : 'http://localhost:8080/v1/graphql'; 46 | if (shouldDerive) { 47 | 48 | const operationDoc = parse(derive.operation); 49 | const operationName = operationDoc.definitions[0].selectionSet.selections.filter(s => s.name.value.indexOf('__') !== 0)[0].name.value; 50 | 51 | operationCodegen = ` 52 | const HASURA_OPERATION = \` 53 | ${derive.operation} 54 | \`;`; 55 | 56 | executeFunction = ` 57 | // execute the parent operation in Hasura 58 | const execute = async (variables) => { 59 | const fetchResponse = await fetch( 60 | "${hasuraEndpoint}", 61 | { 62 | method: 'POST', 63 | body: JSON.stringify({ 64 | query: HASURA_OPERATION, 65 | variables 66 | }) 67 | } 68 | ); 69 | const data = await fetchResponse.json(); 70 | console.log('DEBUG: ', data); 71 | return data; 72 | }; 73 | ` 74 | 75 | graphqlClientCode = ` 76 | // execute the Hasura operation 77 | const { data, errors } = await execute(${requestInputDestructured});` 78 | 79 | errorSnippet = ` // if Hasura operation errors, then throw error 80 | if (errors) { 81 | return res.status(400).json(errors[0]) 82 | }`; 83 | 84 | successSnippet = ` // success 85 | return res.json({ 86 | ...data.${operationName} 87 | })` 88 | 89 | } 90 | 91 | if (!errorSnippet) { 92 | errorSnippet = ` /* 93 | // In case of errors: 94 | return res.status(400).json({ 95 | message: "error happened" 96 | }) 97 | */` 98 | } 99 | 100 | if (!successSnippet) { 101 | successSnippet = ` // success 102 | return res.json({ 103 | ${outputTypeFields.map(f => ` ${f}: ""`).join(',\n')} 104 | })`; 105 | } 106 | 107 | const handlerContent = ` 108 | ${shouldDerive ? 'const fetch = require("node-fetch")\n' : ''}${shouldDerive ? `${operationCodegen}\n` : ''}${shouldDerive ? `${executeFunction}\n` : ''} 109 | // Request Handler 110 | const handler = async (req, res) => { 111 | 112 | // get request input 113 | const ${requestInputDestructured} = req.body.input; 114 | 115 | // run some business logic 116 | ${shouldDerive ? graphqlClientCode : ''} 117 | 118 | ${errorSnippet} 119 | 120 | ${successSnippet} 121 | 122 | }; 123 | 124 | module.exports = handler; 125 | `; 126 | 127 | const handlerFile = { 128 | name: `${actionName}.js`, 129 | content: handlerContent 130 | } 131 | 132 | return [handlerFile]; 133 | 134 | } 135 | -------------------------------------------------------------------------------- /nodejs-zeit/nodejs-zeit.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/codegen-assets/36f4d0f3103dd912db389ef33da6fa182672ec67/nodejs-zeit/nodejs-zeit.zip -------------------------------------------------------------------------------- /nodejs-zeit/starter-kit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /nodejs-zeit/starter-kit/README.md: -------------------------------------------------------------------------------- 1 | # nodejs-zeit 2 | 3 | This is a starter kit for `nodejs` with `zeit`. To get started: 4 | 5 | 1. Firstly, [download the starter-kit](https://github.com/hasura/codegen-assets/raw/master/nodejs-zeit/nodejs-zeit.zip) and `cd` into it. 6 | 7 | 2. Install the `now-cli`. 8 | 9 | ```bash 10 | npm install -g now 11 | ``` 12 | 13 | 3. Install the dependencies and start the now dev server: 14 | 15 | ```bash 16 | npm ci 17 | now dev 18 | ``` 19 | 20 | ## Development 21 | 22 | If you want to add a route (say `/greet`), you can just add a new file called `greet.js` in the `api` directory. This file must have a default export function that behaves as a request handler. 23 | 24 | Example of `greet.js` 25 | 26 | ```js 27 | const greetHandler = (req, res) => { 28 | return res.json({ 29 | "greeting" 30 | }) 31 | } 32 | 33 | export default greetHandler; 34 | ``` 35 | 36 | ### Throwing erros 37 | 38 | You can throw an error object or a list of error objects from your handler. The response must be 4xx and the error object must have a string field called `message`. 39 | 40 | ```js 41 | retun res.status(400).json({ 42 | message: 'invalid email' 43 | }); 44 | ``` 45 | 46 | ## Deployment 47 | 48 | ```bash 49 | now 50 | ``` 51 | -------------------------------------------------------------------------------- /nodejs-zeit/starter-kit/api/hello.js: -------------------------------------------------------------------------------- 1 | const handler = (req, resp) => { 2 | // You can access ther request body at req.body 3 | return resp.json({ "hello": "world" }); 4 | }; 5 | 6 | export default handler; 7 | -------------------------------------------------------------------------------- /nodejs-zeit/starter-kit/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-zeit", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "node-fetch": { 8 | "version": "2.6.0", 9 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 10 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 11 | }, 12 | "now-cli": { 13 | "version": "0.0.0", 14 | "resolved": "https://registry.npmjs.org/now-cli/-/now-cli-0.0.0.tgz", 15 | "integrity": "sha1-6w5jQmmZgVs0QxO6PqmaT3z43As=", 16 | "dev": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nodejs-zeit/starter-kit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-zeit", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "node-fetch": "^2.6.0" 8 | }, 9 | "scripts": { 10 | "develop": "now dev", 11 | "deploy": "now" 12 | }, 13 | "devDependencies": { 14 | "now-cli": "^0.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /typescript-zeit/actions-codegen.js: -------------------------------------------------------------------------------- 1 | /*Utils*/ 2 | 3 | const getWrappingTypeMetadata = (_type) => { 4 | let type = JSON.parse(JSON.stringify(_type)); 5 | const wrapperStack = []; 6 | while (type.kind !== 'NamedType') { 7 | if (type.kind === 'ListType') { 8 | wrapperStack.push('l'); 9 | } 10 | if(type.kind === 'NonNullType') { 11 | wrapperStack.push('n'); 12 | } 13 | type = type.type; 14 | } 15 | const typename = type.name.value; 16 | return { 17 | typename, 18 | stack: wrapperStack.reverse() 19 | } 20 | } 21 | 22 | const getTypescriptTypename = (_typename, wrapperStack) => { 23 | let typename = _typename; 24 | if (!wrapperStack.length || wrapperStack[0] === 'l') { 25 | typename = `Maybe<${typename}>` 26 | } 27 | wrapperStack.forEach((w, i) => { 28 | if (w === 'l') { 29 | if (wrapperStack[i+1] === 'n') { 30 | typename = `Array <${typename}>` 31 | } else { 32 | typename = `Maybe >` 33 | } 34 | } 35 | }); 36 | return typename; 37 | } 38 | 39 | const getHasuraScalars = (ast) => { 40 | let hasuraScalars = {}; 41 | ast.definitions.forEach(d => { 42 | if (d.fields) { 43 | d.fields.forEach(f => { 44 | const fieldTypeMetadata = getWrappingTypeMetadata(f.type); 45 | if (!ast.definitions.some(dd => dd.name.value === fieldTypeMetadata.typename)) { 46 | hasuraScalars[fieldTypeMetadata.typename] = true; 47 | } 48 | if (f.arguments) { 49 | f.arguments.forEach(a => { 50 | const argTypeMetadata = getWrappingTypeMetadata(a.type); 51 | if (!ast.definitions.some(dd => dd.name.value === argTypeMetadata.typename)) { 52 | hasuraScalars[argTypeMetadata.typename] = true; 53 | } 54 | }) 55 | } 56 | }) 57 | } 58 | }); 59 | return Object.keys(hasuraScalars) 60 | }; 61 | 62 | const templater = async (actionName, actionsSdl, derive) => { 63 | 64 | const ast = parse(`${actionsSdl}`); 65 | 66 | const typesAst = { 67 | ...ast, 68 | definitions: ast.definitions.filter(d => (d.name.value !== 'Mutation' && d.name.value !== 'Query')) 69 | }; 70 | 71 | const allMutationActionDefs = ast.definitions.filter(d => (d.name.value === 'Mutation')); 72 | const allQueryActionDefs = ast.definitions.filter(d => (d.name.value === 'Query')); 73 | let allMutationActionFields = []; 74 | allMutationActionDefs.forEach(md => { 75 | allMutationActionFields = [...allMutationActionFields, ...md.fields] 76 | }); 77 | let allQueryActionFields = []; 78 | allQueryActionDefs.forEach(qd => { 79 | allQueryActionFields = [...allQueryActionFields, ...qd.fields] 80 | }); 81 | 82 | // TODO (this is a temporary hack, must be fixed by accepting hasura scalars as a parameter) 83 | const hasuraScalars = getHasuraScalars(ast); 84 | const scalarsSdl = hasuraScalars.map(s => { 85 | return `scalar ${s}`; 86 | }).join('\n'); 87 | const scalarsAst = parse(scalarsSdl); 88 | if (hasuraScalars.length) { 89 | typesAst.definitions.push(...scalarsAst.definitions); 90 | } 91 | 92 | const mutationRootDef = ast.definitions.find(d => d.name.value === 'Mutation'); 93 | const queryRootDef = ast.definitions.find(d => d.name.value === 'Query'); 94 | 95 | if (mutationRootDef) { 96 | mutationRootDef.kind = 'ObjectTypeDefinition'; 97 | mutationRootDef.fields = allMutationActionFields; 98 | typesAst.definitions.push(mutationRootDef); 99 | } 100 | 101 | if (queryRootDef) { 102 | queryRootDef.kind = 'ObjectTypeDefinition'; 103 | queryRootDef.fields = allQueryActionFields; 104 | typesAst.definitions.push(queryRootDef); 105 | } 106 | 107 | const codegenConfig = { 108 | schema: typesAst, 109 | plugins: [ 110 | { 111 | typescript: {}, 112 | }, 113 | ], 114 | pluginMap: { 115 | typescript: typescriptPlugin 116 | } 117 | } 118 | const typesCodegen = await codegen(codegenConfig); 119 | const typesFileMetadata = { 120 | content: typesCodegen, 121 | name: `hasuraCustomTypes.ts` 122 | } 123 | 124 | let actionDef; 125 | let actionType = ''; 126 | const actionAst = { 127 | ...ast, 128 | definitions: ast.definitions.filter(d => { 129 | if (d.name.value === 'Mutation' || d.name.value === 'Query') { 130 | if (actionDef) return false 131 | actionDef = d.fields.find(f => f.name.value === actionName); 132 | actionType = d.name.value; 133 | if (!actionDef) { 134 | return false; 135 | } else { 136 | return true; 137 | } 138 | } 139 | return false; 140 | }) 141 | } 142 | 143 | const actionArgType = (`${actionType}${camelize(actionName)}Args`) 144 | 145 | const actionArguments = actionDef.arguments; 146 | let actionOutputType = actionDef.type; 147 | 148 | while (actionOutputType.kind !== 'NamedType') { 149 | actionOutputType = actionOutputType.type; 150 | } 151 | const outputType = ast.definitions.find(d => { 152 | return (d.kind === 'ObjectTypeDefinition' && d.name.value === actionOutputType.name.value) 153 | }); 154 | 155 | const outputTypeFields = outputType.fields.map(f => f.name.value); 156 | 157 | let graphqlClientCode = ''; 158 | let operationCodegen = ''; 159 | let validateFunction = ''; 160 | let errorSnippet = ''; 161 | let successSnippet = ''; 162 | let executeFunction = ''; 163 | 164 | const requestInputDestructured = `{ ${actionDef.arguments.map(a => a.name.value).join(', ')} }`; 165 | 166 | const shouldDerive = !!(derive && derive.operation) 167 | const hasuraEndpoint = derive && derive.endpoint ? derive.endpoint : 'http://localhost:8080/v1/graphql'; 168 | if (shouldDerive) { 169 | 170 | const operationDoc = parse(derive.operation); 171 | const operationName = operationDoc.definitions[0].selectionSet.selections.filter(s => s.name.value.indexOf('__') !== 0)[0].name.value; 172 | 173 | operationCodegen = ` 174 | const HASURA_OPERATION = \`${derive.operation}\`;`; 175 | 176 | executeFunction = ` 177 | // execute the parent operation in Hasura 178 | const execute = async (variables) => { 179 | const fetchResponse = await fetch( 180 | "${hasuraEndpoint}", 181 | { 182 | method: 'POST', 183 | body: JSON.stringify({ 184 | query: HASURA_OPERATION, 185 | variables 186 | }) 187 | } 188 | ); 189 | const data = await fetchResponse.json(); 190 | console.log('DEBUG: ', data); 191 | return data; 192 | }; 193 | ` 194 | 195 | graphqlClientCode = ` 196 | // execute the Hasura operation 197 | const { data, errors } = await execute(${requestInputDestructured});` 198 | 199 | errorSnippet = ` // if Hasura operation errors, then throw error 200 | if (errors) { 201 | return res.status(400).json(errors[0]) 202 | }`; 203 | 204 | successSnippet = ` // success 205 | return res.json({ 206 | ...data.${operationName} 207 | })` 208 | 209 | } 210 | 211 | if (!errorSnippet) { 212 | errorSnippet = ` /* 213 | // In case of errors: 214 | return res.status(400).json({ 215 | message: "error happened" 216 | }) 217 | */` 218 | } 219 | 220 | if (!successSnippet) { 221 | successSnippet = ` // success 222 | return res.json({ 223 | ${outputTypeFields.map(f => ` ${f}: ""`).join(',\n')} 224 | })`; 225 | } 226 | 227 | const handlerContent = `import { NowRequest, NowResponse } from '@now/node'; 228 | import { ${actionArgType} } from './hasuraCustomTypes'; 229 | ${derive ? 'import fetch from "node-fetch"\n' : ''}${derive ? `${operationCodegen}\n` : ''}${derive ? `${executeFunction}\n` : ''} 230 | // Request Handler 231 | const handler = async (req: NowRequest, res: NowResponse) => { 232 | 233 | // get request input 234 | const ${requestInputDestructured}: ${actionArgType} = req.body.input; 235 | 236 | // run some business logic 237 | ${derive ? graphqlClientCode : ''} 238 | 239 | ${errorSnippet} 240 | 241 | ${successSnippet} 242 | 243 | }; 244 | 245 | export default handler; 246 | `; 247 | 248 | const handlerFileMetadata = { 249 | name: `${actionName}.ts`, 250 | content: handlerContent 251 | } 252 | 253 | return [handlerFileMetadata, typesFileMetadata]; 254 | } 255 | -------------------------------------------------------------------------------- /typescript-zeit/starter-kit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /typescript-zeit/starter-kit/README.md: -------------------------------------------------------------------------------- 1 | # typescript-zeit 2 | 3 | This is a starter kit for `typescript` with `zeit`. To get started: 4 | 5 | 1. Firstly, [download the starter-kit](https://github.com/hasura/codegen-assets/raw/master/typescript-zeit/typescript-zeit.zip) and `cd` into it. 6 | 7 | 2. You first need to install the `now-cli`. 8 | 9 | ```bash 10 | npm install -g now 11 | ``` 12 | 13 | 3. Install the dependencies and start the now dev server: 14 | 15 | ```bash 16 | npm ci 17 | now dev 18 | ``` 19 | 20 | ## Development 21 | 22 | If you want to add a route (say `/greet`), you can just add a new file called `greet.js` in the `api` directory. This file must have a default export function that behaves as a request handler. 23 | 24 | Note: Make sure you import `NowRequest, NowResponse` from `@now/node` 25 | 26 | Example of `greet.js` 27 | 28 | ```typescript 29 | import { NowRequest, NowResponse } from '@now/node' 30 | 31 | const greetHandler = (req: NowRequest, res: NowResponse) => { 32 | return res.json({ 33 | "greeting" 34 | }) 35 | } 36 | 37 | export default greetHandler; 38 | ``` 39 | 40 | ### Throwing erros 41 | 42 | You can throw an error object or a list of error objects from your handler. The response must be 4xx and the error object must have a string field called `message`. 43 | 44 | ```js 45 | retun res.status(400).json({ 46 | message: 'invalid email' 47 | }); 48 | ``` 49 | 50 | ## Deployment 51 | 52 | ```bash 53 | now 54 | ``` 55 | -------------------------------------------------------------------------------- /typescript-zeit/starter-kit/api/hello.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from '@now/node' 2 | 3 | const handler = (req: NowRequest, resp: NowResponse) => { 4 | // You can access the request body at req.body 5 | return resp.json({ "hello": "world" }); 6 | }; 7 | 8 | export default handler; 9 | -------------------------------------------------------------------------------- /typescript-zeit/starter-kit/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-zeit", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@now/node": { 8 | "version": "1.4.1", 9 | "resolved": "https://registry.npmjs.org/@now/node/-/node-1.4.1.tgz", 10 | "integrity": "sha512-EjP/pdBMKsEMCGQ1OLLmBGnjA3QZG1erYTrMqmDVqypeQsY1UUFTY4h1C4d6WNq33qk/nMxpcJzuAhxt+nLQyg==", 11 | "requires": { 12 | "@types/node": "*" 13 | } 14 | }, 15 | "@types/node": { 16 | "version": "13.7.1", 17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.1.tgz", 18 | "integrity": "sha512-Zq8gcQGmn4txQEJeiXo/KiLpon8TzAl0kmKH4zdWctPj05nWwp1ClMdAVEloqrQKfaC48PNLdgN/aVaLqUrluA==" 19 | }, 20 | "node-fetch": { 21 | "version": "2.6.0", 22 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 23 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 24 | }, 25 | "now-cli": { 26 | "version": "0.0.0", 27 | "resolved": "https://registry.npmjs.org/now-cli/-/now-cli-0.0.0.tgz", 28 | "integrity": "sha1-6w5jQmmZgVs0QxO6PqmaT3z43As=", 29 | "dev": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /typescript-zeit/starter-kit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-zeit", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@now/node": "^1.3.4", 8 | "node-fetch": "^2.6.0" 9 | }, 10 | "scripts": { 11 | "develop": "now dev", 12 | "deploy": "now" 13 | }, 14 | "devDependencies": { 15 | "now-cli": "^0.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /typescript-zeit/typescript-zeit.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/codegen-assets/36f4d0f3103dd912db389ef33da6fa182672ec67/typescript-zeit/typescript-zeit.zip --------------------------------------------------------------------------------