├── .deepsource.toml ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .media ├── IDE.png ├── banner.jpg ├── brainyduck.fig ├── divider.png ├── duck.png ├── examples │ ├── basic.gif │ ├── modularized.gif │ └── with-UDF.gif ├── logo-dark.png ├── logo-light.png ├── logo.png ├── npx-vertical.png ├── npx.png ├── schema.png ├── transformation.png └── waves.svg ├── .npmrc ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── cli.js ├── commands ├── build.js ├── deploy-functions.js ├── deploy-indexes.js ├── deploy-roles.js ├── deploy-schemas.js ├── deploy.js ├── dev.js ├── export.js ├── pack.js ├── pull-schema.js └── reset.js ├── docs ├── .nojekyll ├── CNAME ├── README.md ├── _coverpage.md └── index.html ├── examples ├── .gitignore ├── basic-esbuild-bundle │ ├── README.md │ ├── Schema.graphql │ ├── build.mjs │ ├── index.ts │ └── package.json ├── basic │ ├── README.md │ ├── Schema.graphql │ ├── index.ts │ └── package.json ├── modularized-esbuild-bundle │ ├── Query.gql │ ├── README.md │ ├── accounts │ │ ├── User.gql │ │ └── sayHello.udf │ ├── blog │ │ └── Post.gql │ ├── build.js │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── modularized │ ├── Query.gql │ ├── README.md │ ├── accounts │ │ ├── User.gql │ │ └── sayHello.udf │ ├── blog │ │ └── Post.gql │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── with-UDF │ ├── README.md │ ├── Schema.graphql │ ├── index.ts │ ├── package.json │ ├── publicAccess.role │ ├── queries.gql │ ├── sayHello.udf │ └── sayHi.udf ├── with-authentication │ ├── README.md │ ├── domain │ │ ├── Schema.gql │ │ └── accounts │ │ │ ├── Token.gql │ │ │ ├── User.gql │ │ │ ├── actions.gql │ │ │ ├── login.udf │ │ │ ├── logout.udf │ │ │ ├── signUp.udf │ │ │ └── user.role │ ├── lib │ │ ├── accountContainer.js │ │ ├── fetchJson.js │ │ └── withSession.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.js │ │ ├── api │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ ├── signup.ts │ │ │ └── user.ts │ │ └── index.js │ └── tsconfig.json ├── with-proxied-authentication │ ├── README.md │ ├── domain │ │ ├── Schema.gql │ │ └── accounts │ │ │ ├── Token.gql │ │ │ ├── User.gql │ │ │ ├── actions.gql │ │ │ ├── login.udf │ │ │ ├── logout.udf │ │ │ ├── signUp.udf │ │ │ ├── user.role │ │ │ └── whoAmI.udf │ ├── lib │ │ ├── accounts.ts │ │ ├── fetchJson.ts │ │ ├── proxy.ts │ │ └── withSession.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── api │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ ├── proxy.ts │ │ │ └── signup.ts │ │ └── index.js │ └── tsconfig.json └── with-user-defined-functions ├── fetch-ponyfill.cjs ├── locateCache.cjs ├── locateCache.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── protection ├── sdk.cjs ├── sdk.d.ts └── sdk.js ├── scripts ├── reset.collections.fql ├── reset.databases.fql ├── reset.documents.fql ├── reset.functions.fql ├── reset.indexes.fql ├── reset.roles.fql └── reset.schemas.fql ├── tests ├── babel.config.mjs ├── fixtures │ ├── .prettierignore │ ├── basic-esbuild-bundle.output.js │ ├── basic.d.ts │ ├── basic.output.js │ ├── modularized-esbuild-bundle.output.js │ ├── modularized.output.js │ ├── simplified.role │ ├── unmatched.role │ └── unmatched.udf ├── jest.config.mjs ├── package.json ├── run-tests.sh ├── specs │ ├── __snapshots__ │ │ └── build.js.snap │ ├── build.js │ ├── deploy-functions.js │ ├── deploy-roles.js │ ├── deploy-schemas.js │ ├── deploy.js │ ├── dev.js │ ├── examples.js │ └── pull-schema.js ├── storage.js └── testUtils.js ├── tsconfig.json ├── tsup.config.ts └── utils.js /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "shell" 5 | enabled = true 6 | 7 | [[analyzers]] 8 | name = "javascript" 9 | enabled = true -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [**.html] 15 | insert_final_newline = false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: zvictor 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | environment: CI 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Use Node.js 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: '16.x' 15 | - name: Install PNPM 16 | uses: pnpm/action-setup@v2 17 | with: 18 | version: latest 19 | - run: pnpm install 20 | - run: pnpm test -- --ci --reporters='default' --reporters='github-actions' 21 | env: 22 | TESTS_SECRET: ${{ secrets.TESTS_SECRET }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | index.d.ts 3 | 4 | ############################################################################################## 5 | ## From https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore ↓ ## 6 | ############################################################################################## 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | # next.js build output 68 | .next 69 | 70 | 71 | ############################################################################################## 72 | ## from https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore ↓ ## 73 | ############################################################################################## 74 | 75 | .vscode/* 76 | !.vscode/settings.json 77 | !.vscode/tasks.json 78 | !.vscode/launch.json 79 | !.vscode/extensions.json 80 | 81 | 82 | ############################################################################################## 83 | ## from https://github.com/github/gitignore/blob/master/Global/macOS.gitignore ↓ ## 84 | ############################################################################################## 85 | 86 | # General 87 | .DS_Store 88 | .AppleDouble 89 | .LSOverride 90 | 91 | # Icon must end with two \r 92 | Icon 93 | 94 | 95 | # Thumbnails 96 | ._* 97 | 98 | # Files that might appear in the root of a volume 99 | .DocumentRevisions-V100 100 | .fseventsd 101 | .Spotlight-V100 102 | .TemporaryItems 103 | .Trashes 104 | .VolumeIcon.icns 105 | .com.apple.timemachine.donotpresent 106 | 107 | # Directories potentially created on remote AFP share 108 | .AppleDB 109 | .AppleDesktop 110 | Network Trash Folder 111 | Temporary Items 112 | .apdisk 113 | -------------------------------------------------------------------------------- /.media/IDE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/IDE.png -------------------------------------------------------------------------------- /.media/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/banner.jpg -------------------------------------------------------------------------------- /.media/brainyduck.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/brainyduck.fig -------------------------------------------------------------------------------- /.media/divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/divider.png -------------------------------------------------------------------------------- /.media/duck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/duck.png -------------------------------------------------------------------------------- /.media/examples/basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/examples/basic.gif -------------------------------------------------------------------------------- /.media/examples/modularized.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/examples/modularized.gif -------------------------------------------------------------------------------- /.media/examples/with-UDF.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/examples/with-UDF.gif -------------------------------------------------------------------------------- /.media/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/logo-dark.png -------------------------------------------------------------------------------- /.media/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/logo-light.png -------------------------------------------------------------------------------- /.media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/logo.png -------------------------------------------------------------------------------- /.media/npx-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/npx-vertical.png -------------------------------------------------------------------------------- /.media/npx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/npx.png -------------------------------------------------------------------------------- /.media/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/schema.png -------------------------------------------------------------------------------- /.media/transformation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/.media/transformation.png -------------------------------------------------------------------------------- /.media/waves.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "semi": false 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "node debug", 11 | "port": 9229, 12 | "protocol": "inspector", 13 | "restart": true, 14 | "skipFiles": ["//**/*.js", "**//**"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Brainyduck's logo 6 | 7 |

8 | 9 |

10 | A micro "no-backend backend framework" 🤯
11 | Turn any schema into a next-gen backend (BaaS) with a single command! 😮 12 |

13 | 14 |

15 | [ Features 🦆 | Getting started 🐣 | Installation 🚜 | Usage 🍗 | Examples 🌈 | NPM 📦 | Github 🕸 ] 16 |

17 |
18 | 19 |

brainyduck's transformation diagram

20 | 21 | ## Intro 22 | 23 | Brainyduck helps you transition your backend to a top notch serverless environment while keeping the developer experience neat! 🌈🍦🐥 24 | 25 | Worry not about new and complex setup and deployment requisites: The graphql schemas you already have is all you need to build a world-class & reliable endpoint. 26 | 27 | Just run `npx brainyduck` on your schemas and the times in which you had to manually setup your backend will forever be gone! Never find yourself redefining types in multiple files, ever again. 🥹 28 | 29 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png) 30 | 31 | ## Documentation 32 | 33 | Please refer to the [documentation](https://duck.brainy.sh/#/?id=why) in order to [get started](https://duck.brainy.sh/#/?id=getting-started) 🐣. 34 | 35 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png) 36 | 37 | ## Sponsors 38 | 39 |

Brainyduck's logo
40 | Brainyduck needs your support!
41 | Please consider helping us spread the word of the Duck to the world. 🐥🙏 42 |
43 |

44 | 45 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png) 46 | 47 |

48 | Logo edited by zvictor, adapted from an illustration by OpenClipart-Vectors 49 |

50 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import resolve from 'resolve-cwd' 5 | import { program } from 'commander' 6 | import { constantCase } from 'constant-case' 7 | import { fileURLToPath } from 'node:url' 8 | import { patterns } from './utils.js' 9 | 10 | let locallyInstalled = false 11 | try { 12 | locallyInstalled = Boolean(resolve('brainyduck/utils')) 13 | } catch (e) {} 14 | 15 | const findCommandFile = (commandName) => 16 | locallyInstalled 17 | ? resolve(`brainyduck/${commandName}`) 18 | : fileURLToPath(new URL(`./commands/${commandName}.js`, import.meta.url)) 19 | 20 | const prefix = { 21 | FAUNA: 'FAUNA', 22 | BRAINYDUCK: 'BRAINYDUCK', 23 | } 24 | 25 | const pkg = JSON.parse( 26 | await fs.readFileSync(fileURLToPath(new URL('./package.json', import.meta.url))) 27 | ) 28 | 29 | const optionParser = 30 | (key, _prefix = prefix.BRAINYDUCK) => 31 | () => { 32 | let name = constantCase(key) 33 | if (_prefix) { 34 | name = `${_prefix}_${name}` 35 | } 36 | 37 | return (process.env[name] = program.opts()[key]) 38 | } 39 | 40 | program 41 | .version(pkg.version) 42 | 43 | .hook('preSubcommand', (thisCommand, subcommand) => { 44 | if (['build', 'pack', 'export', 'dev'].includes(subcommand._name) && !locallyInstalled) { 45 | console.error( 46 | `Looks like brainyduck is not installed locally and the command '${subcommand._name}' requires a local installation.\nHave you installed brainyduck globally instead?` 47 | ) 48 | throw new Error('You must install brainyduck locally.') 49 | } 50 | }) 51 | 52 | .option( 53 | '-s, --secret ', 54 | `set Fauna's secret key, used to deploy data to your database (defaults to <${prefix.FAUNA}_SECRET>).` 55 | ) 56 | .on('option:secret', optionParser('secret', prefix.FAUNA)) 57 | 58 | .option( 59 | '--domain ', 60 | `FaunaDB server domain (defaults to <${prefix.FAUNA}_DOMAIN or 'db.fauna.com'>).` 61 | ) 62 | .on('option:domain', optionParser('domain', prefix.FAUNA)) 63 | 64 | .option('--port ', `Connection port (defaults to <${prefix.FAUNA}_PORT>).`) 65 | .on('option:port', optionParser('port', prefix.FAUNA)) 66 | 67 | .option( 68 | '--graphql-domain ', 69 | `Graphql server domain (defaults to <${prefix.FAUNA}_GRAPHQL_DOMAIN or 'graphql.fauna.com'>).` 70 | ) 71 | .on('option:graphql-domain', optionParser('graphqlDomain', prefix.FAUNA)) 72 | 73 | .option( 74 | '--graphql-port ', 75 | `Graphql connection port (defaults to <${prefix.FAUNA}_GRAPHQL_PORT>).` 76 | ) 77 | .on('option:graphql-port', optionParser('graphqlPort', prefix.FAUNA)) 78 | 79 | .option( 80 | '--scheme ', 81 | `Connection scheme (defaults to <${prefix.FAUNA}_SCHEME or 'https'>).` 82 | ) 83 | .on('option:scheme', optionParser('scheme', prefix.FAUNA)) 84 | 85 | .option('--overwrite', `wipe out data related to the command before its execution`) 86 | .on('option:overwrite', optionParser('overwrite')) 87 | 88 | .option('--no-operations-generation', `disable the auto-generated operations documents.`) 89 | .on('option:no-operations-generation', function () { 90 | process.env.BRAINYDUCK_NO_OPERATIONS_GENERATION = !this.operationsGeneration 91 | }) 92 | 93 | .option( 94 | '-f, --force ', 95 | `skip prompt confirmations (defaults to ', 101 | `set glob patterns to exclude matches (defaults to ).` 102 | ) 103 | .on('option:ignore', optionParser('ignore')) 104 | 105 | .option('--no-watch', `disable the files watcher (only used in the dev command).`) 106 | .on('option:no-watch', function () { 107 | process.env.BRAINYDUCK_NO_WATCH = !this.watch 108 | }) 109 | 110 | .option( 111 | '--only-changes', 112 | `ignore initial files and watch changes ONLY (only used in the dev command).` 113 | ) 114 | .on('option:only-changes', optionParser('onlyChanges')) 115 | 116 | .option( 117 | '--callback ', 118 | `run external command after every execution completion (only used in the dev command).` 119 | ) 120 | .on('option:callback', optionParser('callback')) 121 | 122 | .option('--tsconfig', `use a custom tsconfig file for the sdk transpilation.`) 123 | .on('option:tsconfig', function () { 124 | process.env.BRAINYDUCK_TSCONFIG = this.tsconfig 125 | }) 126 | 127 | .option('--verbose', `run the command with verbose logging.`) 128 | .on('option:verbose', function () { 129 | process.env.DEBUG = 'brainyduck:*' 130 | }) 131 | 132 | .option('--debug [port]', `run the command with debugging listening on [port].`) 133 | .on('option:debug', function () { 134 | process.env.NODE_OPTIONS = `--inspect=${this.debug || 9229}` 135 | }) 136 | 137 | .option('--debug-brk [port]', `run the command with debugging(-brk) listening on [port].`) 138 | .on('option:debug-brk', function () { 139 | process.env.NODE_OPTIONS = `--inspect-brk=${this['debug-brk'] || 9229}` 140 | }) 141 | 142 | .command( 143 | 'build [schemas-pattern] [documents-pattern] [output]', 144 | 'code generator that creates an easily accessible API. Defaults: [schemas-pattern: **/[A-Z]*.(graphql|gql), documents-pattern: **/[a-z]*.(graphql|gql) output: ]', 145 | { 146 | executableFile: findCommandFile(`build`), 147 | } 148 | ) 149 | 150 | .command('export [destination]', 'export the built module as an independent node package', { 151 | executableFile: findCommandFile(`export`), 152 | }) 153 | 154 | .command('pack', 'create a tarball from the built module', { 155 | executableFile: findCommandFile(`pack`), 156 | }) 157 | 158 | .command('dev [directory]', 'build, deploy and watch for changes. Defaults: [directory: ]', { 159 | executableFile: findCommandFile(`dev`), 160 | isDefault: true, 161 | }) 162 | 163 | .command( 164 | 'deploy [types]', 165 | 'deploy the local folder to your database. Defaults: [types: schemas,functions,indexes,roles]', 166 | { 167 | executableFile: findCommandFile(`deploy`), 168 | } 169 | ) 170 | 171 | .command( 172 | 'deploy-schemas [pattern]', 173 | 'push your schema to faunadb. Defaults: [pattern: **/*.(graphql|gql)]', 174 | { 175 | executableFile: findCommandFile(`deploy-schemas`), 176 | } 177 | ) 178 | 179 | .command( 180 | 'deploy-functions [pattern]', 181 | `upload your User-Defined Functions (UDF) to faunadb. Defaults: [pattern: ${patterns.UDF}]`, 182 | { 183 | executableFile: findCommandFile(`deploy-functions`), 184 | } 185 | ) 186 | 187 | .command( 188 | 'deploy-indexes [pattern]', 189 | `upload your User-Defined Indexes to faunadb. Defaults: [pattern: ${patterns.INDEX}]`, 190 | { 191 | executableFile: findCommandFile(`deploy-indexes`), 192 | } 193 | ) 194 | 195 | .command( 196 | 'deploy-roles [pattern]', 197 | `upload your User-Defined Roles (UDR) to faunadb. Defaults: [pattern: ${patterns.UDR}]`, 198 | { 199 | executableFile: findCommandFile(`deploy-roles`), 200 | } 201 | ) 202 | 203 | .command( 204 | 'pull-schema [output]', 205 | 'load the schema hosted in faunadb. Defaults: [output: ]', 206 | { 207 | executableFile: findCommandFile(`pull-schema`), 208 | } 209 | ) 210 | 211 | .command( 212 | 'reset [types]', 213 | 'wipe out all data in the database {BE CAREFUL!}. Defaults: [types: functions,indexes,roles,documents,collections,databases,schemas]', 214 | { 215 | executableFile: findCommandFile(`reset`), 216 | } 217 | ) 218 | 219 | program.parse(process.argv) 220 | -------------------------------------------------------------------------------- /commands/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import _debug from 'debug' 6 | import { parse } from 'graphql' 7 | import { codegen } from '@graphql-codegen/core' 8 | import { execaSync } from 'execa' 9 | import { fileURLToPath } from 'url' 10 | import * as typescriptPlugin from '@graphql-codegen/typescript' 11 | import * as typescriptOperations from '@graphql-codegen/typescript-operations' 12 | import * as typescriptGraphqlRequest from '@graphql-codegen/typescript-graphql-request' 13 | import { temporaryFile, temporaryDirectory } from 'tempy' 14 | import { findBin, pipeData, patternMatch, locateCache } from '../utils.js' 15 | import push from './deploy-schemas.js' 16 | 17 | const __filename = fileURLToPath(import.meta.url) 18 | const __dirname = path.dirname(__filename) 19 | 20 | const debug = _debug('brainyduck:build') 21 | 22 | const config = { 23 | filename: 'output.ts', 24 | plugins: [ 25 | // Each plugin should be an object 26 | { 27 | typescript: {}, // Here you can pass configuration to the plugin 28 | }, 29 | { ['typescript-operations']: {} }, 30 | { 31 | ['typescript-graphql-request']: {}, 32 | }, 33 | ], 34 | pluginMap: { 35 | typescript: typescriptPlugin, 36 | ['typescript-operations']: typescriptOperations, 37 | ['typescript-graphql-request']: typescriptGraphqlRequest, 38 | }, 39 | } 40 | 41 | const generateOperations = async (schema) => { 42 | debug(`generating operations documents`) 43 | const schemaFile = temporaryFile() 44 | let operationsDir = temporaryDirectory() 45 | 46 | fs.writeFileSync(schemaFile, schema) 47 | const { stdout, stderr, exitCode } = execaSync( 48 | findBin(`gqlg`), 49 | [`--schemaFilePath`, schemaFile, `--destDirPath`, `./output`], 50 | { cwd: operationsDir } 51 | ) 52 | operationsDir = path.join(operationsDir, `./output`) 53 | 54 | if (exitCode) { 55 | console.error(stderr) 56 | throw new Error(`Brainyduck could not generate operations automatically`) 57 | } 58 | 59 | if (stderr) console.warn(stderr) 60 | if (stdout) debug(stdout) 61 | debug(`The operations documents have been auto generated at ${operationsDir}`) 62 | return operationsDir 63 | } 64 | 65 | const generateSdk = async (schema, documentsPattern) => { 66 | debug(`Looking for documents matching '${documentsPattern}'`) 67 | const autoGeneratedDocuments = process.env.BRAINYDUCK_NO_OPERATIONS_GENERATION 68 | ? [] 69 | : await patternMatch(`**/*.gql`, await generateOperations(schema)) 70 | debug(`${autoGeneratedDocuments.length} operations documents have been auto generated`) 71 | 72 | const documents = [ 73 | ...autoGeneratedDocuments, 74 | ...(await patternMatch( 75 | Array.isArray(documentsPattern) ? documentsPattern : documentsPattern.split(',') 76 | )), 77 | ].map((x) => ({ 78 | location: x, 79 | document: parse(fs.readFileSync(path.resolve(x), 'utf8')), 80 | })) 81 | 82 | return await codegen({ 83 | ...config, 84 | documents, 85 | schema: parse(schema), 86 | }) 87 | } 88 | 89 | export default async function main( 90 | schemaPattern, 91 | documentsPattern = '**/[a-z]*.(graphql|gql)', 92 | { cache, output } = { cache: true } 93 | ) { 94 | debug(`called with:`, { schemaPattern, documentsPattern, cache, output }) 95 | 96 | if (cache) { 97 | if (output) throw new Error(`Options 'cache' and 'output' are mutually exclusive`) 98 | 99 | output = locateCache('sdk.ts') 100 | fs.rmSync(locateCache(), { force: true, recursive: true }) 101 | } 102 | 103 | const schema = await push(await schemaPattern, { puke: true }) 104 | 105 | debug(`Generating TypeScript SDK`) 106 | const sdk = `// Temporary workaround for issue microsoft/TypeScript#47663 107 | // Solution found at https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1270716220 108 | import type {} from 'graphql'; 109 | 110 | ${await generateSdk(schema, await documentsPattern)} 111 | 112 | /** 113 | * 114 | * 💸 This schema was generated in the cloud at the expense of the Brainyduck maintainers 📉 115 | * 116 | * 😇 Please kindly consider giving back to the Brainyduck community 😇 117 | * 118 | * 🐥🙏 The DUCK needs your help to spread his word to the world! 🙏🐥 119 | * 120 | * https://duck.brainy.sh 121 | * https://github.com/sponsors/zvictor 122 | * 123 | * 🌟💎🎆 [THIS SPACE IS AVAILABLE FOR ADVERTISING AND SPONSORSHIP] 🎆💎🌟 124 | * 125 | **/ 126 | export default function brainyduck({ 127 | secret = process?.env.FAUNA_SECRET, 128 | endpoint = process?.env.FAUNA_ENDPOINT, 129 | } = {}) { 130 | if (!secret) { 131 | throw new Error('SDK requires a secret to be defined.') 132 | } 133 | 134 | return getSdk( 135 | new GraphQLClient(endpoint || 'https://graphql.fauna.com/graphql', { 136 | headers: { 137 | authorization: secret && \`Bearer \${secret}\`, 138 | }, 139 | }) 140 | ) 141 | } 142 | 143 | export { brainyduck }` 144 | 145 | if (!output) { 146 | return sdk 147 | } 148 | 149 | const outputDir = path.dirname(output) 150 | 151 | if (!fs.existsSync(outputDir)) { 152 | fs.mkdirSync(outputDir, { recursive: true }) 153 | } 154 | 155 | fs.writeFileSync(output, sdk) 156 | debug(`The sdk has been stored at ${output}`) 157 | 158 | if (!cache) { 159 | return output 160 | } 161 | 162 | const tsconfigFile = 163 | process.env.BRAINYDUCK_TSCONFIG || path.join(__dirname, '..', 'tsconfig.json') 164 | const tmpTsconfigFile = locateCache('tsconfig.json') 165 | 166 | debug(`Transpiling sdk with tsconfig at ${tsconfigFile}`) 167 | debug(`Caching files at ${locateCache()}`) 168 | 169 | if (!fs.existsSync(tsconfigFile)) { 170 | throw new Error(`The tsconfig file you specified does not exist.`) 171 | } 172 | 173 | if (!fs.existsSync(locateCache())) { 174 | fs.mkdirSync(locateCache(), { recursive: true }) 175 | } 176 | 177 | fs.writeFileSync( 178 | tmpTsconfigFile, 179 | `{ 180 | "extends": "${tsconfigFile}", "include": ["${output}"], "compilerOptions": { 181 | "outDir": "${locateCache()}", 182 | ${ 183 | /* 184 | Fix for the error TS2742: `The inferred type of "X" cannot be named without a reference to "Y". This is likely not portable. A type annotation is necessary.` 185 | https://github.com/microsoft/TypeScript/issues/42873#issuecomment-1131425209 186 | */ '' 187 | } 188 | "baseUrl": "${path.join(__dirname, '..')}", 189 | "paths": { "*": ["node_modules/*/"]} 190 | } 191 | }` 192 | ) 193 | 194 | const { stdout } = execaSync( 195 | findBin(`tsup`), 196 | [ 197 | output, 198 | '--config', 199 | path.join(__dirname, '..', 'tsup.config.ts'), 200 | '--out-dir', 201 | locateCache(), 202 | '--tsconfig', 203 | tmpTsconfigFile, 204 | ], 205 | { 206 | stdio: ['ignore', 'pipe', process.stderr], 207 | cwd: path.join(__dirname, '..'), 208 | } 209 | ) 210 | debug(stdout) 211 | 212 | debug(`The sdk has been transpiled and cached`) 213 | return output 214 | } 215 | 216 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 217 | const [schemaPattern, documentsPattern, output] = process.argv.slice(2) 218 | 219 | ;(async () => { 220 | const location = await main( 221 | schemaPattern === '-' ? pipeData() : schemaPattern, 222 | documentsPattern === '-' ? pipeData() : documentsPattern, 223 | output && { output } 224 | ) 225 | 226 | console.log(`The sdk has been saved at ${location}`) 227 | process.exit(0) 228 | })() 229 | } 230 | -------------------------------------------------------------------------------- /commands/deploy-functions.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import _debug from 'debug' 6 | import faunadb from 'faunadb' 7 | import figures from 'figures' 8 | import logSymbols from 'log-symbols' 9 | import { fileURLToPath } from 'url' 10 | import { patterns, faunaClient, patternMatch, runFQL } from '../utils.js' 11 | 12 | const { query: q } = faunadb 13 | const debug = _debug('brainyduck:deploy-functions') 14 | 15 | export default async function main(pattern = patterns.UDF) { 16 | debug(`Looking for files matching '${pattern}'`) 17 | const files = await patternMatch(pattern) 18 | 19 | if (!files.length) { 20 | throw new Error(`No matching file could be found`) 21 | } 22 | 23 | return await Promise.all( 24 | files.map(async (file) => { 25 | debug(`\t${figures.pointer} found ${file}`) 26 | const name = path.basename(file, path.extname(file)) 27 | const content = fs.readFileSync(file).toString('utf8') 28 | const replacing = await faunaClient().query(q.IsFunction(q.Function(name))) 29 | 30 | debug(`${replacing ? 'Replacing' : 'Creating'} function '${name}' from file ${file}:`) 31 | 32 | // Remove comments. 33 | // Regex based on: https://stackoverflow.com/a/17791790/599991 34 | // Playground: https://regex101.com/r/IlsODE/3 35 | let query = content.replace( 36 | /(("[^"\\]*(?:\\.[^"\\]*)*")|('[^'\\]*(?:\\.[^'\\]*)*'))|#[^\n]*/gm, 37 | (match, p1, p2, p3, offset, string) => p1 || '' 38 | ) 39 | 40 | // converts simplified definitions into extended definitions 41 | if (!query.match(/^[\s]*\{/)) { 42 | query = `{ name: "${name}", body:\n${query}\n}` 43 | } 44 | 45 | // infer function name only if it has not been declared 46 | // Playground: https://regex101.com/r/iRjGBj/1 47 | if (!query.match(/(? { 69 | if (process.env.BRAINYDUCK_OVERWRITE) { 70 | const { default: reset } = await import('./reset.js') 71 | await reset({ functions: true }) 72 | } 73 | 74 | const refs = await main(pattern) 75 | 76 | console.log( 77 | `User-defined function(s) created or updated:`, 78 | refs.map((x) => x.name) 79 | ) 80 | 81 | process.exit(0) 82 | })() 83 | } 84 | -------------------------------------------------------------------------------- /commands/deploy-indexes.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import _debug from 'debug' 6 | import figures from 'figures' 7 | import faunadb from 'faunadb' 8 | import logSymbols from 'log-symbols' 9 | import { fileURLToPath } from 'url' 10 | import { faunaClient, patternMatch, patterns, runFQL } from '../utils.js' 11 | 12 | const { query: q } = faunadb 13 | const debug = _debug('brainyduck:deploy-indexes') 14 | 15 | export default async function main(pattern = patterns.INDEX) { 16 | debug(`Looking for files matching '${pattern}'`) 17 | const files = await patternMatch(pattern) 18 | 19 | if (!files.length) { 20 | throw new Error(`No matching file could be found`) 21 | } 22 | 23 | return await Promise.all( 24 | files.map(async (file) => { 25 | debug(`\t${figures.pointer} found ${file}`) 26 | const name = path.basename(file, path.extname(file)) 27 | const content = fs.readFileSync(file).toString('utf8') 28 | const replacing = await faunaClient().query(q.IsIndex(q.Index(name))) 29 | 30 | debug(`${replacing ? 'Replacing' : 'Creating'} index '${name}' from file ${file}:`) 31 | 32 | // remove comments 33 | let query = content.replace(/#[^!].*$([\s]*)?/gm, '') 34 | 35 | // forbid simplified definitions (only available for UDFs) 36 | if (!query.match(/^[\s]*\{/)) { 37 | throw new Error(`Incorrect syntax used in index definition`) 38 | } 39 | 40 | // infer index name only if it has not been declared 41 | if (!query.includes('name:')) { 42 | query = query.replace('{', `{ name: "${name}", `) 43 | } 44 | 45 | if (name !== query.match(/name:[\s]*(['"])(.*?)\1/)[2]) { 46 | throw new Error(`File name does not match index name: ${name}`) 47 | } 48 | 49 | query = replacing ? `Update(Index('${name}'), ${query})` : `CreateIndex(${query})` 50 | 51 | const data = runFQL(query) 52 | debug(`${logSymbols.success} index has been created/updated: ${data.name}`) 53 | 54 | return data 55 | }) 56 | ) 57 | } 58 | 59 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 60 | const [pattern] = process.argv.slice(2) 61 | 62 | ;(async () => { 63 | if (process.env.BRAINYDUCK_OVERWRITE) { 64 | const { default: reset } = await import('./reset.js') 65 | await reset({ indexes: true }) 66 | } 67 | 68 | const refs = await main(pattern) 69 | 70 | console.log( 71 | `User-defined index(es) created or updated:`, 72 | refs.map((x) => x.name) 73 | ) 74 | 75 | process.exit(0) 76 | })() 77 | } 78 | -------------------------------------------------------------------------------- /commands/deploy-roles.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import _debug from 'debug' 6 | import figures from 'figures' 7 | import faunadb from 'faunadb' 8 | import logSymbols from 'log-symbols' 9 | import { fileURLToPath } from 'url' 10 | import { faunaClient, patternMatch, patterns, runFQL } from '../utils.js' 11 | 12 | const { query: q } = faunadb 13 | const debug = _debug('brainyduck:deploy-roles') 14 | 15 | export default async function main(pattern = patterns.UDR) { 16 | debug(`Looking for files matching '${pattern}'`) 17 | const files = await patternMatch(pattern) 18 | 19 | if (!files.length) { 20 | throw new Error(`No matching file could be found`) 21 | } 22 | 23 | return await Promise.all( 24 | files.map(async (file) => { 25 | debug(`\t${figures.pointer} found ${file}`) 26 | const name = path.basename(file, path.extname(file)) 27 | const content = fs.readFileSync(file).toString('utf8') 28 | const replacing = await faunaClient().query(q.IsRole(q.Role(name))) 29 | 30 | debug(`${replacing ? 'Replacing' : 'Creating'} role '${name}' from file ${file}:`) 31 | 32 | // remove comments 33 | let query = content.replace(/#[^!].*$([\s]*)?/gm, '') 34 | 35 | // forbid simplified definitions (only available for UDFs) 36 | if (!query.match(/^[\s]*\{/)) { 37 | throw new Error(`Incorrect syntax used in role definition`) 38 | } 39 | 40 | // infer role name only if it has not been declared 41 | if (!query.includes('name:')) { 42 | query = query.replace('{', `{ name: "${name}", `) 43 | } 44 | 45 | if (name !== query.match(/name:[\s]*(['"])(.*?)\1/)[2]) { 46 | throw new Error(`File name does not match role name: ${name}`) 47 | } 48 | 49 | query = replacing ? `Update(Role('${name}'), ${query})` : `CreateRole(${query})` 50 | 51 | const data = runFQL(query) 52 | debug(`${logSymbols.success} role has been created/updated: ${data.name}`) 53 | 54 | return data 55 | }) 56 | ) 57 | } 58 | 59 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 60 | const [pattern] = process.argv.slice(2) 61 | 62 | ;(async () => { 63 | if (process.env.BRAINYDUCK_OVERWRITE) { 64 | const { default: reset } = await import('./reset.js') 65 | await reset({ roles: true }) 66 | } 67 | 68 | const refs = await main(pattern) 69 | 70 | console.log( 71 | `User-defined role(s) created or updated:`, 72 | refs.map((x) => x.name) 73 | ) 74 | 75 | process.exit(0) 76 | })() 77 | } 78 | -------------------------------------------------------------------------------- /commands/deploy-schemas.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import _debug from 'debug' 6 | import figures from 'figures' 7 | import { fileURLToPath } from 'url' 8 | import { patternMatch, importSchema } from '../utils.js' 9 | 10 | const debug = _debug('brainyduck:deploy-schemas') 11 | 12 | const extendTypes = (schema) => { 13 | const regexp = /^[\s]*(?!#)[\s]*extend[\s]+type[\s]+([^\s]+)[\s]*\{([^\}]*)}/gm 14 | 15 | for (const [raw, name, content] of schema.matchAll(regexp)) { 16 | schema = schema 17 | .replace(raw, '') 18 | .replace(new RegExp(`(? { 31 | debug(`Looking for schemas matching '${pattern}'`) 32 | 33 | const files = (await patternMatch(Array.isArray(pattern) ? pattern : pattern.split(','))).map( 34 | (x) => path.resolve(x) 35 | ) 36 | 37 | if (!files.length) { 38 | throw new Error(`No matching file could be found`) 39 | } 40 | 41 | const content = files.map((x) => { 42 | debug(`\t${figures.pointer} found ${x}`) 43 | return fs.readFileSync(x) 44 | }) 45 | 46 | return content.join('\n') 47 | } 48 | 49 | export default async function main(inputPath = '**/[A-Z]*.(graphql|gql)', { override, puke } = {}) { 50 | debug(`called with:`, { inputPath, override, puke }) 51 | const schema = extendTypes(await loadSchema(inputPath)) 52 | 53 | const prettySchema = schema.replace(/^/gm, '\t') 54 | debug(`The resulting merged schema:\n${prettySchema}`) 55 | 56 | try { 57 | return await importSchema(schema, { override, puke }) 58 | } catch (error) { 59 | console.error(`The schema below could not be pushed to remote:\n\n${prettySchema}`) 60 | 61 | throw error 62 | } 63 | } 64 | 65 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 66 | ;(async () => { 67 | const [inputPath] = process.argv.slice(2) 68 | 69 | if (process.env.BRAINYDUCK_OVERWRITE) { 70 | const { default: reset } = await import('./reset.js') 71 | await reset({ collections: true, schemas: true }) 72 | } 73 | 74 | console.log(await main(inputPath)) 75 | process.exit(0) 76 | })() 77 | } 78 | -------------------------------------------------------------------------------- /commands/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import ora from 'ora' 4 | import _debug from 'debug' 5 | import { fileURLToPath } from 'node:url' 6 | import deployFunctions from './deploy-functions.js' 7 | import deployIndexes from './deploy-indexes.js' 8 | import deployRoles from './deploy-roles.js' 9 | import deploySchemas from './deploy-schemas.js' 10 | import { representData } from '../utils.js' 11 | 12 | const ALL_TYPES = { 13 | schemas: deploySchemas, 14 | indexes: deployIndexes, 15 | roles: deployRoles, 16 | functions: deployFunctions, 17 | } 18 | 19 | const debug = _debug('brainyduck:deploy') 20 | 21 | const deploy = async (type) => { 22 | const spinner = ora(`Deploying ${type}...`).start() 23 | 24 | try { 25 | const operation = ALL_TYPES[type] 26 | let data 27 | 28 | try { 29 | data = await operation() 30 | } catch (error) { 31 | if (error.message !== `No matching file could be found`) { 32 | throw error 33 | } 34 | 35 | return spinner.info(`No ${type} to deploy`) 36 | } 37 | 38 | if (!data || !data.length) { 39 | return spinner.fail(`Nothing was deployed of type '${type}'`) 40 | } 41 | 42 | spinner.succeed(`${type} have been deployed!`) 43 | console.log(`${type}:`, type === 'schemas' ? data : representData(data), '\n') 44 | 45 | return data 46 | } catch (e) { 47 | spinner.fail(`${type} deployment has failed`) 48 | throw e 49 | } 50 | } 51 | 52 | export default async function main(types = ALL_TYPES) { 53 | const _types = Object.keys(types).filter((key) => types[key]) 54 | console.log(`The following types are about to be deployed:`, _types) 55 | 56 | for (const type of Object.keys(ALL_TYPES)) { 57 | if (!_types.includes(type)) { 58 | debug(`Skipping ${type}`) 59 | continue 60 | } 61 | 62 | await deploy(type) 63 | } 64 | } 65 | 66 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 67 | ;(async () => { 68 | const types = 69 | process.argv[2] && Object.fromEntries(process.argv[2].split(',').map((type) => [type, true])) 70 | 71 | if (process.env.BRAINYDUCK_OVERWRITE) { 72 | const { default: reset } = await import('./reset.js') 73 | await reset(types) 74 | } 75 | 76 | await main(types) 77 | 78 | console.log(`\n\nAll done! All deployments have been successful 🦆`) 79 | process.exit(0) 80 | })() 81 | } 82 | -------------------------------------------------------------------------------- /commands/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const scream = (e) => { 4 | console.error(e.stack || e) 5 | 6 | if (e.message === `missing brainyduck's secret`) { 7 | process.exit(1) 8 | } 9 | } 10 | 11 | process.on('unhandledRejection', scream) 12 | process.on('uncaughtException', scream) 13 | 14 | import ora from 'ora' 15 | import path from 'path' 16 | import _debug from 'debug' 17 | import PQueue from 'p-queue' 18 | import chokidar from 'chokidar' 19 | import { execaSync } from 'execa' 20 | import { fileURLToPath } from 'url' 21 | import deployFunctions from './deploy-functions.js' 22 | import deployIndexes from './deploy-indexes.js' 23 | import deployRoles from './deploy-roles.js' 24 | import deploySchemas from './deploy-schemas.js' 25 | import build from './build.js' 26 | import { patterns, ignored } from '../utils.js' 27 | 28 | const debug = _debug('brainyduck:watcher') 29 | 30 | const queue = new PQueue({ autoStart: false, concurrency: 1 }) 31 | const lock = {} 32 | 33 | const block = (type, file) => { 34 | lock[type] = lock[type] || [] 35 | lock[type].push(file) 36 | } 37 | 38 | const unblock = (type) => { 39 | lock[type] = false 40 | } 41 | 42 | const runCallback = () => { 43 | if (!process.env.CALLBACK) return 44 | 45 | console.log(`Running callback '${process.env.CALLBACK}':`) 46 | const cmd = process.env.CALLBACK.split(' ') 47 | 48 | execaSync(cmd.shift(), cmd, { 49 | stdio: ['ignore', process.stdout, process.stderr], 50 | cwd: process.cwd(), 51 | }) 52 | 53 | console.log('') 54 | } 55 | 56 | const processor = (type, operation, file, cumulative) => { 57 | if (lock[type]) { 58 | return block(type, file) 59 | } 60 | 61 | if (cumulative) { 62 | block(type, file) 63 | } 64 | 65 | queue.add(async () => { 66 | const filesList = (lock[type] || [file]) 67 | .map((x) => path.relative(process.cwd(), x)) 68 | .sort() 69 | .join(', ') 70 | 71 | unblock(type) 72 | 73 | if (!operation) { 74 | return debug(`Ignoring file(s) ${file} [${type}] (no operation defined)`) 75 | } 76 | 77 | const spinner = ora(`Processing ${filesList} [${type}]\n`).start() 78 | 79 | try { 80 | await operation(file) 81 | spinner.succeed(`Processed ${filesList} [${type}]`) 82 | } catch (e) { 83 | spinner.fail() 84 | console.error(e) 85 | } 86 | }) 87 | } 88 | 89 | const watch = (type, pattern, operation, cumulative) => 90 | new Promise((resolve) => { 91 | const directory = process.cwd() 92 | 93 | if (process.env.BRAINYDUCK_ONLY_CHANGES) { 94 | debug(`Watching ${type} changes but ignoring initial files`) 95 | } 96 | 97 | chokidar 98 | .watch(pattern, { 99 | ignoreInitial: Boolean(process.env.BRAINYDUCK_ONLY_CHANGES), 100 | ignored: [/(^|[\/\\])\../, ...ignored], 101 | persistent: true, 102 | cwd: path.resolve(directory), 103 | }) 104 | .on('error', (error) => debug(`error: ${error}`)) 105 | .on('add', (file) => { 106 | file = path.join(directory, file) 107 | 108 | debug(`Watching ${file} [${type}]`) 109 | operation && processor(type, operation, file, cumulative) 110 | }) 111 | .on('change', (file) => { 112 | file = path.join(directory, file) 113 | 114 | debug(`${file} has been changed [${type}]`) 115 | processor(type, operation, file, cumulative) 116 | }) 117 | .on('ready', resolve) 118 | }) 119 | 120 | export default async function main() { 121 | const ts = await watch('Typescript', patterns.TS, null, true) 122 | 123 | // const schema = await watch('Schema', patterns.SCHEMA, (file) => 124 | // generateTypes(file, file.replace(/(.gql|.graphql)$/, '$1.d.ts')) 125 | // ) 126 | 127 | const schema = await watch( 128 | 'Schema', 129 | patterns.SCHEMA, 130 | async () => { 131 | await build(patterns.SCHEMA, patterns.DOCUMENTS) 132 | await deploySchemas(patterns.SCHEMA, { override: true }) 133 | }, 134 | true 135 | ) 136 | 137 | const index = await watch('Index', patterns.INDEX, deployIndexes) 138 | 139 | const udr = await watch('UDR', patterns.UDR, deployRoles) 140 | 141 | const udf = await watch('UDF', patterns.UDF, deployFunctions) 142 | 143 | const documents = await watch( 144 | 'Document', 145 | patterns.DOCUMENTS, 146 | () => build(patterns.SCHEMA, patterns.DOCUMENTS), 147 | true 148 | ) 149 | 150 | debug('Initial scan complete') 151 | 152 | if (process.env.BRAINYDUCK_NO_WATCH) { 153 | queue.onIdle().then(() => { 154 | runCallback() 155 | 156 | console.log('All operations complete') 157 | process.exit(0) 158 | }) 159 | } else { 160 | let started = false 161 | 162 | const spinner = ora({ 163 | text: `All done! Waiting for new file changes 🦆`, 164 | prefixText: '\n', 165 | spinner: 'bounce', 166 | }) 167 | 168 | queue.on('active', () => { 169 | started = true 170 | spinner.stop() 171 | }) 172 | 173 | queue.on('idle', () => { 174 | if (started) { 175 | runCallback() 176 | } 177 | 178 | spinner.start() 179 | }) 180 | } 181 | 182 | queue.start() 183 | } 184 | 185 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 186 | const [directory] = process.argv.slice(2) 187 | 188 | ;(async () => { 189 | if (process.env.BRAINYDUCK_OVERWRITE) { 190 | const { default: reset } = await import('./reset.js') 191 | await reset() 192 | } 193 | 194 | if (directory) { 195 | process.chdir(directory) 196 | debug(`Changed directory to ${process.cwd()}`) 197 | } 198 | 199 | await main() 200 | })() 201 | } 202 | -------------------------------------------------------------------------------- /commands/export.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs-extra' 4 | import path from 'path' 5 | import _debug from 'debug' 6 | import { fileURLToPath } from 'url' 7 | import { locateCache } from '../utils.js' 8 | 9 | const debug = _debug('brainyduck:export') 10 | 11 | export default async function main(destination) { 12 | debug(`called with:`, { destination }) 13 | 14 | if (!destination) { 15 | throw new Error(`Please provide a destination for your package`) 16 | } 17 | 18 | if (!fs.existsSync(locateCache(`sdk.mjs`))) { 19 | throw new Error(`Please run the 'build' command before running 'export'`) 20 | } 21 | 22 | if (fs.existsSync(destination) && fs.readdirSync(destination).length > 0) { 23 | throw new Error(`Destination '${destination}' already exists and is not empty`) 24 | } 25 | 26 | fs.copySync(locateCache(`.`), destination, { 27 | // filter: (src) => console.log(src) || !src.includes('/.'), 28 | }) 29 | 30 | fs.writeFileSync( 31 | path.join(destination, 'package.json'), 32 | `{ 33 | "name": "brainyduck-sdk", 34 | "version": "1.0.0", 35 | "type": "module", 36 | "exports": { 37 | ".": { 38 | "types": "./sdk.d.ts", 39 | "import": "./sdk.mjs", 40 | "require": "./sdk.cjs" 41 | } 42 | }, 43 | "main": "./sdk.cjs", 44 | "types": "./sdk.d.ts", 45 | "bundleDependencies": true, 46 | "peerDependencies": { 47 | "graphql-request": "latest", 48 | "graphql-tag": "latest" 49 | } 50 | }` 51 | ) 52 | debug(`The sdk has been exported at ${destination}`) 53 | 54 | return destination 55 | } 56 | 57 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 58 | const [destination] = process.argv.slice(2) 59 | 60 | ;(async () => { 61 | const location = await main(destination) 62 | 63 | console.log(`The package has been saved at ${location}`) 64 | process.exit(0) 65 | })() 66 | } 67 | -------------------------------------------------------------------------------- /commands/pack.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'path' 4 | import _debug from 'debug' 5 | import { execaSync } from 'execa' 6 | import { fileURLToPath } from 'url' 7 | import { temporaryDirectory } from 'tempy' 8 | import exportIt from './export.js' 9 | 10 | const debug = _debug('brainyduck:pack') 11 | 12 | export default async function main() { 13 | const destination = temporaryDirectory() 14 | debug(`packing at:`, { destination }, process.cwd()) 15 | 16 | await exportIt(destination) 17 | 18 | execaSync(`npm`, ['pack', '--pack-destination', process.cwd()], { 19 | cwd: destination, 20 | stdio: ['ignore', 'ignore', process.stderr], 21 | }) 22 | 23 | return path.join(process.cwd(), `brainyduck-sdk-1.0.0.tgz`) 24 | } 25 | 26 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 27 | const [destination] = process.argv.slice(2) 28 | 29 | ;(async () => { 30 | const location = await main(destination) 31 | 32 | console.log(`The package has been compressed and saved at ${location}`) 33 | process.exit(0) 34 | })() 35 | } 36 | -------------------------------------------------------------------------------- /commands/pull-schema.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import _debug from 'debug' 6 | import { print } from 'graphql' 7 | import { performance } from 'perf_hooks' 8 | import { fileURLToPath } from 'url' 9 | import { loadTypedefs, OPERATION_KINDS } from '@graphql-tools/load' 10 | import { UrlLoader } from '@graphql-tools/url-loader' 11 | import { mergeTypeDefs } from '@graphql-tools/merge' 12 | import { graphqlEndpoint, loadSecret } from '../utils.js' 13 | 14 | const debug = _debug('brainyduck:pull-schema') 15 | 16 | const options = { 17 | loaders: [new UrlLoader()], 18 | filterKinds: OPERATION_KINDS, 19 | sort: false, 20 | forceGraphQLImport: true, 21 | useSchemaDefinition: false, 22 | headers: { 23 | Authorization: `Bearer ${loadSecret()}`, 24 | }, 25 | } 26 | 27 | const loadSchema = async (url) => { 28 | debug(`Pulling the schema from '${url}'`) 29 | const typeDefs = await loadTypedefs(url, options).catch((err) => { 30 | if ( 31 | err.message.includes('Must provide schema definition with query type or a type named Query.') 32 | ) { 33 | console.warn(`Please make sure you have pushed a valid schema before trying to pull it back.`) 34 | throw new Error(`Invalid schema retrieved: missing type Query`) 35 | } 36 | 37 | throw err 38 | }) 39 | debug(`${typeDefs.length} typeDef(s) found`) 40 | 41 | if (!typeDefs || !typeDefs.length) { 42 | throw new Error('no schema found') 43 | } 44 | 45 | const mergedDocuments = print( 46 | mergeTypeDefs( 47 | typeDefs.map((r) => r.document), 48 | options 49 | ) 50 | ) 51 | 52 | return typeof mergedDocuments === 'string' 53 | ? mergedDocuments 54 | : mergedDocuments && print(mergedDocuments) 55 | } 56 | 57 | export default async function main(outputPath) { 58 | debug(`called with:`, { outputPath }) 59 | const t0 = performance.now() 60 | const schema = await loadSchema(graphqlEndpoint.server) 61 | debug(`The call to fauna took ${performance.now() - t0} milliseconds.`) 62 | 63 | if (outputPath) { 64 | fs.writeFileSync(outputPath, schema) 65 | debug(`The schema has been stored at '${outputPath}'`) 66 | } 67 | 68 | return schema 69 | } 70 | 71 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 72 | const [outputPath] = process.argv.slice(2) 73 | 74 | ;(async () => { 75 | const schema = await main(outputPath && path.resolve(outputPath)) 76 | 77 | if (!outputPath) { 78 | console.log(schema) 79 | } 80 | 81 | process.exit(0) 82 | })() 83 | } 84 | -------------------------------------------------------------------------------- /commands/reset.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs' 4 | import ora from 'ora' 5 | import path from 'path' 6 | import chalk from 'chalk' 7 | import { fileURLToPath } from 'node:url' 8 | import { runFQL, importSchema, representData, question } from '../utils.js' 9 | 10 | const ALL_TYPES = { 11 | functions: true, 12 | indexes: true, 13 | roles: true, 14 | documents: true, 15 | collections: true, 16 | databases: true, 17 | schemas: true, 18 | } 19 | 20 | const readScript = (name) => 21 | fs.readFileSync(new URL(path.join(`../scripts/`, name), import.meta.url), { encoding: 'utf8' }) 22 | 23 | const confirm = async (types = ALL_TYPES) => { 24 | const listOfTypes = chalk.red.bold(Object.keys(types).join(', ')) 25 | 26 | console.warn( 27 | `\n\nYou are about to wipe out all the ${listOfTypes} from the database associated to the key you provided.` 28 | ) 29 | console.warn(`This action is irreversible and might possibly affect production data.\n\n`) 30 | 31 | if (process.env.BRAINYDUCK_FORCE) { 32 | console.warn(`Not asking for confirmation because you are using forced mode`) 33 | return true 34 | } 35 | 36 | const answer = await question( 37 | chalk.bold(`Are you sure you want to delete all the ${listOfTypes}? [y/N] `) 38 | ) 39 | 40 | return answer === 'y' 41 | } 42 | 43 | const reset = (type) => { 44 | const spinner = ora(`Wiping out ${type}...`).start() 45 | 46 | try { 47 | const { data } = runFQL(readScript(`reset.${type}.fql`)) 48 | 49 | if (!data || !data.length) { 50 | return spinner.succeed(`No data was deleted of type '${type}'`) 51 | } 52 | 53 | spinner.succeed(`${type} cleared out`) 54 | console.log('deleted:', representData(data.map((x) => x.data || x))) 55 | 56 | return data 57 | } catch (e) { 58 | spinner.fail(`${type} reset failed`) 59 | throw e 60 | } 61 | } 62 | 63 | export default async function main(types = ALL_TYPES) { 64 | const _types = Object.keys(types).filter((key) => types[key]) 65 | console.log(`The following types are about to be deleted:`, _types) 66 | 67 | if (types.schemas) { 68 | const spinner = ora(`Wiping out the graphql schema...`).start() 69 | 70 | try { 71 | await importSchema(`enum Brainyduck { RESETTING }`) 72 | spinner.succeed(`Graphql schema has been reset.`) 73 | } catch (e) { 74 | spinner.fail() 75 | throw e 76 | } 77 | } 78 | 79 | for (const type of _types) { 80 | reset(type) 81 | } 82 | } 83 | 84 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 85 | ;(async () => { 86 | const types = 87 | process.argv[2] && Object.fromEntries(process.argv[2].split(',').map((type) => [type, true])) 88 | 89 | if (!(await confirm(types))) { 90 | return console.log('Wise decision! 🧑‍🌾') 91 | } 92 | 93 | await main(types) 94 | 95 | console.log(`All reset operations have succeeded.`) 96 | process.exit(0) 97 | })() 98 | } 99 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zvictor/brainyduck/35dc71063f83786dc97082d8583ea8872dd8377e/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | duck.brainy.sh -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Brainyduck helps you transition your backend to a top notch serverless environment while keeping the developer experience neat! 🌈🍦🐥 4 | 5 | Worry not about new and complex setup and deployment requisites: The graphql schemas you already have is all you need to build a world-class & reliable endpoint. 6 | 7 | Just run `npx brainyduck` on your schemas and the times in which you had to manually setup your backend will forever be gone! Never find yourself redefining types in multiple files, ever again. 🥹 8 | 9 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 10 | 11 | ## Features 12 | 13 | #### Code generation 14 | 15 | - ⚡️  Auto generated APIs with small footprint. 16 | - 👮🏼  The generated code is written in TypeScript, with full support for types. 17 | - ⛰  Schemas are [expanded to provide basic CRUD](https://docs.fauna.com/fauna/current/api/graphql/schemas) automatically (_i.e. no need to define resolvers for basic operations!_). 18 | - 🔎  Validation of required and non-nullable fields against provided data. 19 | 20 | #### Backend (by Fauna) 21 | 22 | - 🦄  All the data persists on a [next-gen data backend](https://docs.fauna.com/fauna/current/introduction) 🤘. 23 | - 👨‍👩‍👦‍👦  Support for [relationships between documents](https://docs.fauna.com/fauna/current/learn/tutorials/graphql/relations/), within the schema definition. 24 | - 🔒  [Authentication and access control security](https://docs.fauna.com/fauna/current/security/) at the data level (including [Attribute-based access control (ABAC)](https://docs.fauna.com/fauna/current/security/abac)). 25 | 26 | 27 | #### The library 28 | 29 | - ✅  Well-tested. 30 | - 🐻  Easy to add to your new or existing projects. 31 | - 👀  Quite a few examples in the [./examples](https://github.com/zvictor/brainyduck/tree/master/examples) folder. 32 | 33 |
34 |

Read more

35 | 36 | Given a GraphQL schema looking anything like this: 37 | 38 | ```graphql 39 | type User { 40 | username: String! @unique 41 | } 42 | 43 | type Post { 44 | content: String! 45 | author: User! 46 | } 47 | ``` 48 | 49 | Brainyduck will give you: 50 | 51 | 1. Your schema will be expanded to provide basic CRUD out of the box. Expect it to become something like this: 52 | 53 | ```graphql 54 | type Query { 55 | findPostByID(id: ID!): Post 56 | findUserByID(id: ID!): User 57 | } 58 | 59 | type Mutation { 60 | updateUser(id: ID!, data: UserInput!): User 61 | createUser(data: UserInput!): User! 62 | updatePost(id: ID!, data: PostInput!): Post 63 | deleteUser(id: ID!): User 64 | deletePost(id: ID!): Post 65 | createPost(data: PostInput!): Post! 66 | } 67 | 68 | type Post { 69 | author: User! 70 | _id: ID! 71 | content: String! 72 | title: String! 73 | } 74 | 75 | type User { 76 | _id: ID! 77 | username: String! 78 | } 79 | 80 | input PostInput { 81 | title: String! 82 | content: String! 83 | author: PostAuthorRelation 84 | } 85 | 86 | input UserInput { 87 | username: String! 88 | } 89 | 90 | # ... plus few other less important definitions such as relations and pagination 91 | ``` 92 | 93 | 2. Do you like TypeScript? Your schema will also be exported as TS types. 94 | 95 | ```typescript 96 | export type Query = { 97 | __typename?: 'Query' 98 | /** Find a document from the collection of 'Post' by its id. */ 99 | findPostByID?: Maybe 100 | /** Find a document from the collection of 'User' by its id. */ 101 | findUserByID?: Maybe 102 | } 103 | 104 | export type Mutation = { 105 | __typename?: 'Mutation' 106 | /** Update an existing document in the collection of 'User' */ 107 | updateUser?: Maybe 108 | /** Create a new document in the collection of 'User' */ 109 | createUser: User 110 | /** Update an existing document in the collection of 'Post' */ 111 | updatePost?: Maybe 112 | /** Delete an existing document in the collection of 'User' */ 113 | deleteUser?: Maybe 114 | /** Delete an existing document in the collection of 'Post' */ 115 | deletePost?: Maybe 116 | /** Create a new document in the collection of 'Post' */ 117 | createPost: Post 118 | } 119 | 120 | export type Post = { 121 | __typename?: 'Post' 122 | author: User 123 | /** The document's ID. */ 124 | _id: Scalars['ID'] 125 | content: Scalars['String'] 126 | title: Scalars['String'] 127 | } 128 | 129 | export type User = { 130 | __typename?: 'User' 131 | /** The document's ID. */ 132 | _id: Scalars['ID'] 133 | username: Scalars['String'] 134 | } 135 | 136 | // ... plus few other less important definitions such as relations and pagination 137 | ``` 138 | 139 | 3. You will be able to abstract the GraphQL layer and make calls using a convenient API (with full autocomplete support!) 140 | 141 | ```typescript 142 | import brainyduck from 'brainyduck' // <-- automatically loads the SDK generated exclusively to your schema 143 | 144 | await brainyduck().createUser({ username: `rick-sanchez` }) // <-- TS autocomplete and type checking enabled! 145 | await brainyduck({ secret: 'different-access-token' }).createUser({ username: `morty-smith` }) // <-- Easily handle authentication and sessions by providing different credentials 146 | 147 | const { allUsers } = await brainyduck().allUsers() 148 | 149 | for (const user of allUsers.data) { 150 | console.log(user) 151 | } 152 | 153 | // output: 154 | // 155 | // { username: 'rick-sanchez' } 156 | // { username: 'morty-smith' } 157 | ``` 158 | 159 | 4. The API can be used both on backend and frontend, as long as you are careful enough with your [secrets management](https://forums.fauna.com/t/do-i-need-a-backend-api-between-faunadb-and-my-app-what-are-the-use-cases-of-an-api/95/6?u=zvictor). 160 | 161 | **What else?** 162 | 163 | 1. Brainyduck stiches multiple graphql files together, so your codebase can embrace [modularization](https://github.com/zvictor/brainyduck/tree/master/examples/modularized). 164 | 2. Isn't basic CRUD enough? What about more complex custom resolvers? Brainyduck integrates well with [user-defined functions [UDF]](https://docs.fauna.com/fauna/current/api/graphql/functions), automatically keeping your functions in sync with fauna's backend. 165 | 166 | 167 | For more examples, please check our [examples directory](https://github.com/zvictor/brainyduck/tree/master/examples). 168 |
169 | 170 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 171 | 172 | ## Getting started 173 | 174 | It takes only **3 steps to get started**: 175 | 176 | 1. Create a `.graphql` file defining your desired Graphql schema 177 | 2. Create or reuse a [fauna secret](https://github.com/zvictor/brainyduck/wiki/Fauna-secret) 178 | 3. In the same folder, run `npx brainyduck --secret ` 179 | 180 | That's it! Now you can start importing and consuming your sdk with `import sdk from 'brainyduck'` 🐣🎉 181 | 182 | _Alternatively, you can:_ 183 | 184 | - In any of our [examples](https://github.com/zvictor/brainyduck/tree/master/examples) folder, run `npx brainyduck --secret ` 185 | 186 | | [Basic](https://github.com/zvictor/brainyduck/tree/master/examples/basic) | [Modularized](https://github.com/zvictor/brainyduck/tree/master/examples/modularized) | [with-UDF](https://github.com/zvictor/brainyduck/tree/master/examples/with-UDF) | 187 | | :---------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------: | 188 | | [![Basic example asciicast](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/examples/basic.gif)](https://asciinema.org/a/361576) | [![Modularized example asciicast](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/examples/modularized.gif)](https://asciinema.org/a/361562) | [![with-UDF example asciicast](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/examples/with-UDF.gif)](https://asciinema.org/a/361573) | 189 | | | 190 | 191 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 192 | 193 | ## Installation 194 | 195 | You can install it globally, per project or just run it on demand: 196 | 197 | ```bash 198 | # npm, globally: 199 | $ npm install -g brainyduck 200 | 201 | # npm, project-only: 202 | $ npm i brainyduck -D 203 | 204 | # or run on demand: 205 | $ npx brainyduck 206 | ``` 207 | 208 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 209 | 210 | ## Usage 211 | 212 | ```markup 213 | Usage: brainyduck [options] [command] 214 | 215 | Options: 216 | -V, --version output the version number 217 | -s, --secret set Fauna's secret key, used to deploy data to your database (defaults to ). 218 | --domain FaunaDB server domain (defaults to ). 219 | --port Connection port (defaults to ). 220 | --graphql-domain Graphql server domain (defaults to ). 221 | --graphql-port Graphql connection port (defaults to ). 222 | --scheme Connection scheme (defaults to ). 223 | --overwrite wipe out data related to the command before its execution 224 | --no-operations-generation disable the auto-generated operations documents. 225 | -f, --force skip prompt confirmations (defaults to set glob patterns to exclude matches (defaults to ). 227 | --no-watch disable the files watcher (only used in the dev command). 228 | --only-changes ignore initial files and watch changes ONLY (only used in the dev command). 229 | --callback run external command after every execution completion (only used in the dev command). 230 | --tsconfig use a custom tsconfig file for the sdk transpilation. 231 | --verbose run the command with verbose logging. 232 | --debug [port] run the command with debugging listening on [port]. 233 | --debug-brk [port] run the command with debugging(-brk) listening on [port]. 234 | -h, --help display help for command 235 | 236 | Commands: 237 | build [schemas-pattern] [documents-pattern] [output] code generator that creates an easily accessible API. Defaults: [schemas-pattern: **/[A-Z]*.(graphql|gql), documents-pattern: **/[a-z]*.(graphql|gql) output: ] 238 | export [destination] export the built module as an independent node package 239 | pack create a tarball from the built module 240 | dev [directory] build, deploy and watch for changes. Defaults: [directory: ] 241 | deploy [types] deploy the local folder to your database. Defaults: [types: schemas,functions,indexes,roles] 242 | deploy-schemas [pattern] push your schema to faunadb. Defaults: [pattern: **/*.(graphql|gql)] 243 | deploy-functions [pattern] upload your User-Defined Functions (UDF) to faunadb. Defaults: [pattern: **/*.udf] 244 | deploy-indexes [pattern] upload your User-Defined Indexes to faunadb. Defaults: [pattern: **/*.index] 245 | deploy-roles [pattern] upload your User-Defined Roles (UDR) to faunadb. Defaults: [pattern: **/*.role] 246 | pull-schema [output] load the schema hosted in faunadb. Defaults: [output: ] 247 | reset [types] wipe out all data in the database {BE CAREFUL!}. Defaults: [types: functions,indexes,roles,documents,collections,databases,schemas] 248 | help [command] display help for command 249 | ``` 250 | 251 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 252 | 253 | ## Commands 254 | 255 | Commands with the **operative** badge require a `--secret` value to be passed along and are designed to make changes to the database associated to the given key. 256 | 257 | Using a wrong secret can have unintended and possibly drastic effects on your data. So please **make sure you are using the right secret whenever running a command!** 258 | ### build 259 | 260 | Throw graphql schemas in and get a well typed api back. Simple like that! 261 | 262 | After running `build` you can add `import sdk from 'brainyduck'` statements in your code and run queries against your database directly. 263 | 264 | CLI: 265 | ```shell 266 | npx brainyduck build [schema-pattern] [documents-pattern] [output] 267 | ``` 268 | 269 | Defaults: 270 | * _schema-pattern_: `**/[A-Z]*.(graphql|gql)` 271 | * _documents-pattern_: `**/[a-z]*.(graphql|gql)` 272 | * _output_: `` 273 | 274 | ### export 275 | 276 | Sometimes you want to have the sdk on it's own node package, usually to increase portability or reusability. 277 | 278 | After running `export` you can `npm publish` it or send the files somewhere else. 279 | 280 | CLI: 281 | ```shell 282 | npx brainyduck export [destination] 283 | ``` 284 | ### pack 285 | 286 | Create a tarball from the built module 287 | 288 | CLI: 289 | ```shell 290 | npx brainyduck pack 291 | ``` 292 | 293 | ### dev 294 |
295 | 296 | [Builds](#build), [deploys](#deploy) (override mode), and watches for changes. 297 | 298 | This is usually the command you run when you are developing locally, **never the command you run against production**. 299 | You are recommended to create a secret in a new database, just for the dev environment, before running this command. 300 | 301 | CLI: 302 | ```shell 303 | npx brainyduck dev [directory] 304 | ``` 305 | 306 | Defaults: 307 | * _directory_: `` 308 | 309 | 310 | ### deploy 311 |
312 | 313 | Deploys [schemas](#deploy-schemas) (merge mode), [functions](#deploy-functions), [indexes](#deploy-indexes), and [roles](#deploy-roles). 314 | This is usually the command you run when you have finished developing locally and want to ship to production. Just remember to use the right `--secret` value for that. 315 | 316 | CLI: 317 | ```shell 318 | npx brainyduck deploy [types] 319 | ``` 320 | 321 | Defaults: 322 | * _types_: `schemas,functions,indexes,roles` 323 | 324 | ### deploy-schemas 325 |
326 | 327 | Deploys the selected schemas to your database, creating collections accordingly. 328 | 329 | CLI: 330 | ```shell 331 | npx brainyduck deploy-schemas [pattern] 332 | ``` 333 | 334 | Defaults: 335 | * _pattern_: `**/*.(graphql|gql)` 336 | 337 | ### deploy-functions 338 |
339 | 340 | Deploys your [User-Defined Functions (UDF)](https://docs.fauna.com/fauna/current/build/fql/udfs). 341 | 342 | CLI: 343 | ```shell 344 | npx brainyduck deploy-functions [pattern] 345 | ``` 346 | 347 | Defaults: 348 | * _pattern_: `**/*.udf` 349 | 350 | ### deploy-indexes 351 |
352 | 353 | Deploys your [User-Defined Indexes](https://docs.fauna.com/fauna/current/api/fql/indexes). 354 | 355 | CLI: 356 | ```shell 357 | npx brainyduck deploy-indexes [pattern] 358 | ``` 359 | 360 | Defaults: 361 | * _pattern_: `**/*.index` 362 | 363 | ### deploy-roles 364 |
365 | 366 | Deploys your [User-Defined Roles (UDR)](https://docs.fauna.com/fauna/current/security/roles) to your database. 367 | 368 | CLI: 369 | ```shell 370 | npx brainyduck deploy-roles [pattern] 371 | ``` 372 | 373 | Defaults: 374 | * _pattern_: `**/*.role` 375 | 376 | ### pull-schema 377 | Downloads the schema from Fauna and outputs the result. 378 | Useful only for debugging or inspecting purposes, otherwise used only internally. 379 | 380 | CLI: 381 | ```shell 382 | npx brainyduck pull-schema [output] 383 | ``` 384 | 385 | Defaults: 386 | * _output_: `` 387 | 388 | ### reset 389 |
390 | 391 | The fastest way to restart or get rid of data you don't want to keep anymore. 392 | 393 | **BE CAREFUL, though, as the actions performed by `reset` are irreversible.** Please triple check your `--secret` before running this command! 394 | 395 | CLI: 396 | ```shell 397 | npx brainyduck reset [types] 398 | ``` 399 | 400 | Defaults: 401 | * _types_: `functions,indexes,roles,documents,collections,databases,schemas` 402 | 403 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 404 | 405 | ## CLI and Programmatic Access 406 | 407 | All commands can be accessed in multiple ways. 408 | 409 | Note that all CLI options have an equivalent environment variable you can set directly, as long as you follow the [constant case pattern](https://github.com/zvictor/brainyduck/blob/36f39d9b9e6c50654967b876767abdd905488b7c/cli.js#L18-L27) to do so. 410 | 411 | Fauna options start with `FAUNA_` and all other ones start with `BRAINYDUCK_`. 412 | 413 | E.g `--overwrite` becomes `BRAINYDUCK_OVERWRITE`; `--graphql-domain` becomes `FAUNA_GRAPHQL_DOMAIN`. 414 | 415 | ### Centralized CLI 416 | 417 | Just run `npx brainyduck [options]`. 418 | 419 | For more information, please check [usage](#usage) or run `npx brainyduck --help`. 420 | 421 | ### Programmatically 422 | 423 | Looking for fancy ways to automate your processes? You can import Brainyduck directly into your scripts, using the `import('brainyduck/')` pattern, like shown in the example below: 424 | 425 | ```ts 426 | import build from 'brainyduck/build' 427 | 428 | await build() 429 | ``` 430 | 431 | ### Direct CLI 432 | 433 | You can access each command while skipping the CLI wrapper altogether. 434 | 435 | ```markup 436 | node ./node_modules/brainyduck/commands/ [...args] 437 | ``` 438 | 439 | _The parameters of each script vary from file to file. You will need to [check the signature of each command](https://github.com/zvictor/brainyduck/tree/master/commands) on your own._ 440 | 441 | 442 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 443 | 444 | ## Bundling & Exporting 445 | 446 | Your SDK files will be cached at `./node_modules/brainyduck/.cache`. 447 | 448 | Most of the times you are developing you **don't need to worry about the location of those files as Brainyduck manages them for you** internally. Sometimes, however, (specially when bundling your projects) you might need to think on how to move them around and make sure that they stay available to your code regardless of changes in the environment. 449 | 450 | For such cases, there a few strategies you can choose from: 451 | 452 | ### rebuild 453 | 454 | It's okay to just rebuild your sdk in a new environment. 455 | 456 | ```Dockerfile 457 | FROM node 458 | ... 459 | ADD ./src . 460 | RUN npm install 461 | RUN npx brainyduck build 462 | ``` 463 | 464 | ### clone 465 | 466 | The files in Brainyduck's cache are portable, meaning that you can just copy them around. 467 | 468 | _We wish all ducks could be cloned that easily!_ 🐣🧬🧑‍🔬 469 | 470 | 471 | ```Dockerfile 472 | ... 473 | FROM node 474 | ... 475 | ADD ./src . 476 | RUN npm install 477 | ADD ./node_modules/brainyduck/.cache ./node_modules/brainyduck/.cache 478 | ``` 479 | 480 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 481 | 482 | ## Contributing 483 | 484 | ### Coding Principles 485 | * Whenever possible the commands should be _non-operative_ (meaning that they can be run without altering the database its working with) and require no secret to execute. 486 | 487 | * Separation of concerns: The CLI file is just a wrapper invoking each command file, and that separation must be clear. 488 | 489 | ### Debugging & Testing 490 | 491 | 1. When debugging Brainyduck, it's always a very good idea to run the commands using the `--verbose` (or `DEBUG=brainyduck:*`) flag. 492 | Please make sure you **have that included in your logs before you report any bug**. 493 | 494 | 2. The tests are easy to run and are an important diagnosis tool to understand what could be going on in your environment. 495 | Clone Brainyduck's repository and then run the tests in one of the possible ways: 496 | 497 | **Note: Make sure you use a secret of a DB you create exclusively for these tests, as Brainyduck will potentially wipe all its data out!** 498 | 499 | _Note: `TESTS_SECRET` needs to have `admin` role._ 500 | 501 | ```haskell 502 | -- Run all tests: 503 | TESTS_SECRET=secret_for_an_exclusive_db ./tests/run-tests.sh 504 | ``` 505 | 506 | ```haskell 507 | -- Run only tests of a specific command: 508 | TESTS_SECRET=secret_for_an_exclusive_db ./tests/run-tests.sh specs/.js 509 | ``` 510 | 511 | You can also set `DEBUG=brainyduck:*` when running tests to get deeper insights. 512 | 513 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 514 | 515 | ## Sponsors 516 | 517 |

Brainyduck's logo
518 | Brainyduck needs your support!
519 | Please consider helping us spread the word of the Duck to the world. 🐥🙏 520 |
521 |

522 | 523 | ![divider](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/divider.png ':size=100%') 524 | 525 |

526 | Logo edited by zvictor, adapted from an illustration by OpenClipart-Vectors 527 |

528 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

It's time to say goodbye! 🍗🦴🦴😢

Read the announcement #37 6 |
7 | 8 |

Turn any schema into a next-gen backend (BaaS) with a single command!

9 |

The first micro "no-backend backend framework" 🤯

10 | 11 | divider 12 | 13 | 22 | 23 | 41 | 42 |
43 |

44 | brainyduck's transformation diagram 45 |

46 | 47 | 48 |

49 |
50 | brainyduck's schema diagram 51 |
52 |
53 | brainyduck's npx diagram 54 |
55 |
56 | brainyduck's IDE diagram 57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Brainyduck 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 47 | 48 | 157 | 158 | 159 |
160 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | package-lock.json 3 | yarn.lock 4 | **/dist 5 | **/build 6 | -------------------------------------------------------------------------------- /examples/basic-esbuild-bundle/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Brainyduck's logo 6 | 7 |

8 | 9 | # Basic Esbuild Bundle example 10 | 11 | This example contains: 12 | 13 | - a Graphql schema [Schema.graphql] 14 | - a Graphql operations document [queries.gql] 15 | 16 | By running `npx brainyduck --secret ` you should expect to see: 17 | 18 | - a requests sdk containing all the operations, fully typed and with auto-complete support [accessible through `import sdk from 'brainyduck'`] 19 | 20 | Once brainyduck has been setup, you can run `FAUNA_SECRET= npm start` to execute the operations demonstration [index.ts]. 21 | 22 | [![asciicast](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/examples/basic.gif)](https://asciinema.org/a/361576) 23 | -------------------------------------------------------------------------------- /examples/basic-esbuild-bundle/Schema.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | username: String! @unique 3 | } 4 | 5 | type Query { 6 | allUsers: [User!] 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic-esbuild-bundle/build.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { build } from 'esbuild' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = path.dirname(__filename) 7 | 8 | try { 9 | await build({ 10 | bundle: true, 11 | sourcemap: true, 12 | platform: 'node', 13 | format: 'cjs', 14 | target: 'es6', 15 | entryPoints: [path.join(__dirname, 'index.ts')], 16 | outdir: path.join(__dirname, 'build'), 17 | }) 18 | } catch { 19 | process.exitCode = 1 20 | } 21 | -------------------------------------------------------------------------------- /examples/basic-esbuild-bundle/index.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | 3 | const random = () => Math.random().toString(36).substring(7) 4 | 5 | ;(async () => { 6 | console.log(await sdk().createUser({ data: { username: `rick-sanchez-${random()}` } })) 7 | console.log(await sdk().createUser({ data: { username: `morty-smith-${random()}` } })) 8 | 9 | const { allUsers } = await sdk().allUsers() 10 | 11 | for (const user of allUsers.data) { 12 | console.log(user) 13 | } 14 | })() 15 | 16 | // Expected output of this script: 17 | 18 | // { createUser: { _id: 'xyz', ... } } 19 | // { createUser: { _id: 'xyz', ... } } 20 | 21 | // { username: 'rick-sanchez-xyz', ... } 22 | // { username: 'morty-smith-xyz', ... } 23 | -------------------------------------------------------------------------------- /examples/basic-esbuild-bundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck-example-basic-esbuild", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "brainyduck build && node build.mjs", 6 | "deploy": "brainyduck deploy", 7 | "start": "node ./build/index.js" 8 | }, 9 | "dependencies": { 10 | "brainyduck": "../..", 11 | "graphql-request": "^5.1.0" 12 | }, 13 | "devDependencies": { 14 | "@esbuild-plugins/node-globals-polyfill": "^0.1.1", 15 | "@esbuild-plugins/node-modules-polyfill": "^0.1.4", 16 | "esbuild": "^0.17.3", 17 | "ts-node": "^10.9.1", 18 | "typescript": "^4.9.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Brainyduck's logo 6 | 7 |

8 | 9 | # Basic example 10 | 11 | This example contains: 12 | 13 | - a Graphql schema [Schema.graphql] 14 | - a Graphql operations document [queries.gql] 15 | 16 | By running `npx brainyduck --secret ` you should expect to see: 17 | 18 | - a requests sdk containing all the operations, fully typed and with auto-complete support [accessible through `import sdk from 'brainyduck'`] 19 | 20 | Once brainyduck has been setup, you can run `FAUNA_SECRET= npm start` to execute the operations demonstration [index.ts]. 21 | 22 | [![asciicast](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/examples/basic.gif)](https://asciinema.org/a/361576) 23 | -------------------------------------------------------------------------------- /examples/basic/Schema.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | username: String! @unique 3 | } 4 | 5 | type Query { 6 | allUsers: [User!] 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic/index.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | 3 | const random = () => Math.random().toString(36).substring(7) 4 | 5 | ;(async () => { 6 | console.log(await sdk().createUser({ data: { username: `rick-sanchez-${random()}` } })) 7 | console.log(await sdk().createUser({ data: { username: `morty-smith-${random()}` } })) 8 | 9 | const { allUsers } = await sdk().allUsers() 10 | 11 | for (const user of allUsers.data) { 12 | console.log(user) 13 | } 14 | })() 15 | 16 | // Expected output of this script: 17 | 18 | // { createUser: { _id: 'xyz', ... } } 19 | // { createUser: { _id: 'xyz', ... } } 20 | 21 | // { username: 'rick-sanchez-xyz', ... } 22 | // { username: 'morty-smith-xyz', ... } 23 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck-example-basic", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "brainyduck dev", 6 | "start": "npx ts-node index.ts" 7 | }, 8 | "dependencies": { 9 | "brainyduck": "../..", 10 | "graphql-request": "^5.1.0" 11 | }, 12 | "devDependencies": { 13 | "ts-node": "^10.9.1", 14 | "typescript": "^4.9.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/Query.gql: -------------------------------------------------------------------------------- 1 | type Query {} 2 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Brainyduck's logo 6 | 7 |

8 | 9 | # Modularized Esbuild Bundle example 10 | 11 | This example contains: 12 | 13 | - an [ECMAScript module](https://nodejs.org/api/esm.html) 14 | - multiple graphql schemas and User-Defined Function (UDF) spread in 2 **different folders** 15 | - a root graphql schema containing the Query type 16 | 17 | By running `npx brainyduck --secret ` you should expect to see: 18 | 19 | - all UDF uploaded to the cloud 20 | - a requests sdk containing all the operations, fully typed and with auto-complete support [accessible through `import sdk from 'brainyduck'`] 21 | 22 | Once brainyduck has been setup, you can run `FAUNA_SECRET= npm start` to execute the operations demonstration [index.ts]. 23 | 24 | [![asciicast](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/examples/modularized.gif)](https://asciinema.org/a/361562) 25 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/accounts/User.gql: -------------------------------------------------------------------------------- 1 | type User { 2 | name: String! 3 | } 4 | 5 | extend type Query { 6 | sayHello(name: String!): String! @resolver(name: "sayHello") 7 | } 8 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/accounts/sayHello.udf: -------------------------------------------------------------------------------- 1 | Query(Lambda(["name"], 2 | Concat(["Hello ", Var("name")]) 3 | )) 4 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/blog/Post.gql: -------------------------------------------------------------------------------- 1 | type Post { 2 | title: String! 3 | content: String! 4 | author: User! 5 | } 6 | 7 | extend type Query { 8 | allPosts: [Post!] 9 | } 10 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/build.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { build } from 'esbuild' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = path.dirname(__filename) 7 | 8 | try { 9 | await build({ 10 | bundle: true, 11 | sourcemap: true, 12 | platform: 'node', 13 | format: 'esm', 14 | target: 'es6', 15 | entryPoints: [path.join(__dirname, 'index.ts')], 16 | outdir: path.join(__dirname, 'build'), 17 | external: ['graphql-request', 'graphql-tag'], 18 | }) 19 | } catch { 20 | process.exitCode = 1 21 | } 22 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/index.ts: -------------------------------------------------------------------------------- 1 | import { brainyduck } from 'brainyduck' 2 | const { log } = console 3 | 4 | async function main() { 5 | const mutation = await brainyduck().createPost({ 6 | data: { 7 | title: 'a post title', 8 | content: 'some post content', 9 | author: { create: { name: 'Whatever Name' } }, 10 | }, 11 | }) 12 | 13 | log(`post created with id: ${mutation.createPost._id}\n`) 14 | 15 | log(await brainyduck().findPostByID({ id: mutation.createPost._id })) 16 | } 17 | 18 | main() 19 | 20 | // Expected output of this script: 21 | 22 | // post created with id: 262903814408897042 23 | // 24 | // { 25 | // findPostByID: { 26 | // title: 'a post title', 27 | // content: 'some post content', 28 | // author: { name: 'Whatever Name' } 29 | // } 30 | // } 31 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck-example-modularized-esbuild-bundle", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "brainyduck build && node build.js", 7 | "deploy": "brainyduck deploy", 8 | "start": "node ./build/index.js" 9 | }, 10 | "dependencies": { 11 | "brainyduck": "../..", 12 | "graphql-request": "^5.1.0" 13 | }, 14 | "devDependencies": { 15 | "esbuild": "^0.17.3", 16 | "ts-node": "^10.9.1", 17 | "typescript": "^4.9.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/modularized-esbuild-bundle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["index.ts"], 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "target": "ES5", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "outDir": "./build" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/modularized/Query.gql: -------------------------------------------------------------------------------- 1 | type Query {} 2 | -------------------------------------------------------------------------------- /examples/modularized/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Brainyduck's logo 6 | 7 |

8 | 9 | # Modularized example 10 | 11 | This example contains: 12 | 13 | - an [ECMAScript module](https://nodejs.org/api/esm.html) 14 | - multiple graphql schemas and User-Defined Function (UDF) spread in 2 **different folders** 15 | - a root graphql schema containing the Query type 16 | 17 | By running `npx brainyduck --secret ` you should expect to see: 18 | 19 | - all UDF uploaded to the cloud 20 | - a requests sdk containing all the operations, fully typed and with auto-complete support [accessible through `import sdk from 'brainyduck'`] 21 | 22 | Once brainyduck has been setup, you can run `FAUNA_SECRET= npm start` to execute the operations demonstration [index.ts]. 23 | 24 | [![asciicast](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/examples/modularized.gif)](https://asciinema.org/a/361562) 25 | -------------------------------------------------------------------------------- /examples/modularized/accounts/User.gql: -------------------------------------------------------------------------------- 1 | type User { 2 | name: String! 3 | } 4 | 5 | extend type Query { 6 | sayHello(name: String!): String! @resolver(name: "sayHello") 7 | } 8 | -------------------------------------------------------------------------------- /examples/modularized/accounts/sayHello.udf: -------------------------------------------------------------------------------- 1 | Query(Lambda(["name"], 2 | Concat(["Hello ", Var("name")]) 3 | )) 4 | -------------------------------------------------------------------------------- /examples/modularized/blog/Post.gql: -------------------------------------------------------------------------------- 1 | type Post { 2 | title: String! 3 | content: String! 4 | author: User! 5 | } 6 | 7 | extend type Query { 8 | allPosts: [Post!] 9 | } 10 | -------------------------------------------------------------------------------- /examples/modularized/index.ts: -------------------------------------------------------------------------------- 1 | import { brainyduck } from 'brainyduck' 2 | const { log } = console 3 | 4 | async function main() { 5 | const mutation = await brainyduck().createPost({ 6 | data: { 7 | title: 'a post title', 8 | content: 'some post content', 9 | author: { create: { name: 'Whatever Name' } }, 10 | }, 11 | }) 12 | 13 | log(`post created with id: ${mutation.createPost._id}\n`) 14 | 15 | log(await brainyduck().findPostByID({ id: mutation.createPost._id })) 16 | } 17 | 18 | main() 19 | 20 | // Expected output of this script: 21 | 22 | // post created with id: 262903814408897042 23 | // 24 | // { 25 | // findPostByID: { 26 | // title: 'a post title', 27 | // content: 'some post content', 28 | // author: { name: 'Whatever Name' } 29 | // } 30 | // } 31 | -------------------------------------------------------------------------------- /examples/modularized/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck-example-modularized", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "brainyduck dev", 7 | "start": "node --loader ts-node/esm index.ts" 8 | }, 9 | "dependencies": { 10 | "brainyduck": "../..", 11 | "graphql-request": "^5.1.0" 12 | }, 13 | "devDependencies": { 14 | "ts-node": "^10.9.1", 15 | "typescript": "^4.9.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/modularized/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["index.ts"], 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "target": "ES5", 6 | "moduleResolution": "Node", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "outDir": "./build" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/with-UDF/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Brainyduck's logo 6 | 7 |

8 | 9 | # with user-defined-functions example 10 | 11 | This example contains: 12 | 13 | - a User-Defined Function (UDF) with simplified definition [sayHi.udf] 14 | - a User-Defined Function (UDF) with extended definition [sayHello.udf] 15 | - a User-Defined Role (UDR) [publicAccess.role] 16 | - a Graphql schema [Schema.graphql] 17 | - a Graphql operations document [queries.gql] 18 | 19 | By running `npx brainyduck --secret ` you should expect to see: 20 | 21 | - The UDF and UDR uploaded to the cloud 22 | - a requests sdk containing all the operations, fully typed and with auto-complete support [accessible through `import sdk from 'brainyduck'`] 23 | 24 | Once brainyduck has been setup, you can run `FAUNA_SECRET= npm start` to execute the operations demonstration [index.ts]. 25 | 26 | [![asciicast](https://raw.githubusercontent.com/zvictor/brainyduck/master/.media/examples/with-UDF.gif)](https://asciinema.org/a/361573) 27 | -------------------------------------------------------------------------------- /examples/with-UDF/Schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | sayHi(name: String!): String! @resolver(name: "sayHi") 3 | sayHello(name: String!): String! @resolver(name: "sayHello") 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-UDF/index.ts: -------------------------------------------------------------------------------- 1 | import brainyduck from 'brainyduck' 2 | 3 | brainyduck().sayHi({ name: 'dimension C-137' }).then(console.log) 4 | brainyduck().sayHello({ name: 'dimension C-137' }).then(console.log) 5 | 6 | // Expected output of this script: 7 | 8 | // { sayHi: 'Hi dimension C-137' } 9 | // { sayHello: 'Hello dimension C-137' } 10 | -------------------------------------------------------------------------------- /examples/with-UDF/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck-example-udf", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "brainyduck dev", 6 | "start": "npx ts-node index.ts" 7 | }, 8 | "dependencies": { 9 | "brainyduck": "../..", 10 | "graphql-request": "^5.1.0" 11 | }, 12 | "devDependencies": { 13 | "ts-node": "^10.9.1", 14 | "typescript": "^4.9.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-UDF/publicAccess.role: -------------------------------------------------------------------------------- 1 | { 2 | privileges: [ 3 | { 4 | resource: Function('sayHi'), 5 | actions: { call: true } 6 | }, 7 | { 8 | resource: Function('sayHello'), 9 | actions: { call: true } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-UDF/queries.gql: -------------------------------------------------------------------------------- 1 | query sayHi($name: String!) { 2 | sayHi(name: $name) 3 | } 4 | 5 | query sayHello($name: String!) { 6 | sayHello(name: $name) 7 | } 8 | -------------------------------------------------------------------------------- /examples/with-UDF/sayHello.udf: -------------------------------------------------------------------------------- 1 | # This file contains the full definition of the function. 2 | { 3 | 4 | # The function name is being manually defined here, but, 5 | # if ommited, the function name would be inferred from the file name. 6 | # If defined, it must always match the file name. 7 | name: "sayHello", # (optional) 8 | 9 | body: Query(Lambda(["name"], 10 | Concat(["Hello ", Var("name")]) 11 | )) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/with-UDF/sayHi.udf: -------------------------------------------------------------------------------- 1 | # This file contains only the body of the UDF. 2 | # The function name is taken from the file name. 3 | 4 | Query(Lambda(["name"], 5 | Concat(["Hi ", Var("name")]) 6 | )) 7 | -------------------------------------------------------------------------------- /examples/with-authentication/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Brainyduck's logo 6 | 7 |

8 | 9 | # Authentication & authorization example 10 | 11 | This example shows how to add authentication and authorization to a next.js project backed by brainyduck. 12 | 13 | ## Structure 14 | 15 | The [domain](./domain) folder contains the models, roles and functions. 16 | 17 | Everything else is just regular next.js code, but with special attention to [pages/api](./pages/api): that's where backend and frontend get connected. 18 | 19 | ## Setup 20 | 21 | Execute `brainyduck` inside the domain folder and then start next.js as usual. 22 | 23 | ```bash 24 | export FAUNA_SECRET= 25 | 26 | $ npx brainyduck dev ./domain --no-watch 27 | $ npx next dev 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/with-authentication/domain/Schema.gql: -------------------------------------------------------------------------------- 1 | type Mutation {} 2 | -------------------------------------------------------------------------------- /examples/with-authentication/domain/accounts/Token.gql: -------------------------------------------------------------------------------- 1 | type Token @embedded { 2 | instance: User! 3 | secret: String! 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-authentication/domain/accounts/User.gql: -------------------------------------------------------------------------------- 1 | type User { 2 | email: String! @unique 3 | } 4 | 5 | extend type Mutation { 6 | signUp(email: String!, password: String!): Token! @resolver(name: "signUp") 7 | login(email: String!, password: String!): Token! @resolver(name: "login") 8 | logout: Boolean! @resolver(name: "logout") 9 | } 10 | -------------------------------------------------------------------------------- /examples/with-authentication/domain/accounts/actions.gql: -------------------------------------------------------------------------------- 1 | mutation signUp($email: String!, $password: String!) { 2 | signUp(email: $email, password: $password) { 3 | secret 4 | instance { 5 | email 6 | } 7 | } 8 | } 9 | 10 | mutation login($email: String!, $password: String!) { 11 | login(email: $email, password: $password) { 12 | secret 13 | instance { 14 | email 15 | } 16 | } 17 | } 18 | 19 | mutation logout { 20 | logout 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-authentication/domain/accounts/login.udf: -------------------------------------------------------------------------------- 1 | Query( 2 | Lambda( 3 | ['email', 'password'], 4 | Login(Match(Index('unique_User_email'), Var('email')), { 5 | password: Var('password') 6 | }) 7 | ) 8 | ) 9 | -------------------------------------------------------------------------------- /examples/with-authentication/domain/accounts/logout.udf: -------------------------------------------------------------------------------- 1 | Query(Lambda([], Logout(false))) 2 | -------------------------------------------------------------------------------- /examples/with-authentication/domain/accounts/signUp.udf: -------------------------------------------------------------------------------- 1 | Query( 2 | Lambda( 3 | ['email', 'password'], 4 | Login( 5 | Select('ref', 6 | Create(Collection('User'), { 7 | credentials: { password: Var('password') }, 8 | data: { 9 | email: Var('email'), 10 | }, 11 | }), 12 | ), 13 | { password: Var('password') }, 14 | ) 15 | ) 16 | ) 17 | -------------------------------------------------------------------------------- /examples/with-authentication/domain/accounts/user.role: -------------------------------------------------------------------------------- 1 | { 2 | membership: [{ resource: Collection('User') }], 3 | privileges: [ 4 | { 5 | resource: Function('logout'), 6 | actions: { call: true } 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/with-authentication/lib/accountContainer.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { createContainer } from 'unstated-next' 3 | import fetchJson from '|lib/fetchJson' 4 | 5 | function useAccount() { 6 | const [user, setUser] = useState(null) 7 | useEffect(() => runAction('user')(), []) 8 | 9 | const updateUser = (promise) => { 10 | setUser(promise) 11 | promise.then((user) => setUser(user)) 12 | } 13 | 14 | const runAction = (name) => (body) => 15 | updateUser(fetchJson(`/api/${name}`, { method: 'POST', body })) 16 | 17 | return { 18 | user, 19 | signup: runAction('signup'), 20 | login: runAction('login'), 21 | logout: runAction('logout'), 22 | loading: Promise.resolve(user) == user, 23 | } 24 | } 25 | 26 | export default createContainer(useAccount) 27 | -------------------------------------------------------------------------------- /examples/with-authentication/lib/fetchJson.js: -------------------------------------------------------------------------------- 1 | export default async function fetchJson(url, { body, ...options } = {}) { 2 | const response = await fetch(url, { 3 | headers: { 4 | 'Content-Type': 'application/json', 5 | }, 6 | ...options, 7 | body: body && JSON.stringify(body), 8 | }) 9 | 10 | const data = await response.json() 11 | 12 | if (!response.ok) { 13 | const error = new Error(response.statusText) 14 | error.response = response 15 | error.data = data 16 | 17 | if (data.error) { 18 | error.message = data.error 19 | } 20 | 21 | throw error 22 | } 23 | 24 | return data 25 | } 26 | -------------------------------------------------------------------------------- /examples/with-authentication/lib/withSession.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest } from 'next' 2 | import { withIronSession, Handler, Session } from 'next-iron-session' 3 | 4 | export interface ApiRequestWithSession extends NextApiRequest { 5 | session: Session 6 | } 7 | 8 | export default (handler: Handler) => 9 | withIronSession(handler, { 10 | password: process.env.SECRET_COOKIE_PASSWORD!, 11 | cookieName: 'authorization', 12 | cookieOptions: { 13 | secure: process.env.NODE_ENV === 'production' ? true : false, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /examples/with-authentication/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/with-authentication/next.config.js: -------------------------------------------------------------------------------- 1 | if (!process.env.FAUNA_SECRET) { 2 | throw new Error('Please define `process.env.FAUNA_SECRET`') 3 | } 4 | 5 | module.exports = { 6 | env: { 7 | SECRET_COOKIE_PASSWORD: 'ZBfcR7cmYAg6zD9uYeSVYTaaWbReqzvr', 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /examples/with-authentication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck-example-auth", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "brainyduck": "latest", 6 | "next": "^13.1.2", 7 | "next-iron-session": "^4.1.10", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "unstated-next": "^1.1.0" 11 | }, 12 | "devDependencies": { 13 | "@types/react": "18.0.27", 14 | "typescript": "^4.9.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-authentication/pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import AccountContainer from '|lib/accountContainer' 3 | 4 | const MyApp = ({ Component, pageProps }) => ( 5 | <> 6 | 7 | Brainyduck Authentication / Authorization demo 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | 15 | export default MyApp 16 | -------------------------------------------------------------------------------- /examples/with-authentication/pages/api/login.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | import { NextApiResponse } from 'next' 3 | import withSession, { ApiRequestWithSession } from '|lib/withSession' 4 | 5 | export default withSession(async (req: ApiRequestWithSession, res: NextApiResponse) => { 6 | try { 7 | const { email, password } = await req.body 8 | 9 | if (!email || !password) { 10 | throw new Error('Email and password must be provided.') 11 | } 12 | 13 | const { login } = await sdk().login({ email, password }) 14 | 15 | if (!login.secret) { 16 | throw new Error('No secret present in login query response.') 17 | } 18 | 19 | req.session.set('user', login.instance) 20 | req.session.set('secret', login.secret) 21 | await req.session.save() 22 | 23 | res.status(200).json(login.instance) 24 | } catch (error) { 25 | console.error(error) 26 | res.status(400).json({ error: error.message || error }) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /examples/with-authentication/pages/api/logout.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | import { NextApiResponse } from 'next' 3 | import withSession, { ApiRequestWithSession } from '|lib/withSession' 4 | 5 | export default withSession(async (req: ApiRequestWithSession, res: NextApiResponse) => { 6 | try { 7 | const user = req.session.get('user') 8 | const secret = req.session.get('secret') 9 | req.session.destroy() 10 | 11 | const { logout } = await sdk({ secret }).logout() 12 | 13 | res.json(logout ? 'null' : user) 14 | } catch (error) { 15 | console.error(error) 16 | res.status(400).json({ error: error.message || error }) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /examples/with-authentication/pages/api/signup.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | import { NextApiResponse } from 'next' 3 | import withSession, { ApiRequestWithSession } from '|lib/withSession' 4 | 5 | export default withSession(async (req: ApiRequestWithSession, res: NextApiResponse) => { 6 | try { 7 | const { email, password } = await req.body 8 | 9 | if (!email || !password) { 10 | throw new Error('Email and password must be provided.') 11 | } 12 | 13 | const { signUp } = await sdk().signUp({ email, password }) 14 | 15 | if (!signUp.secret) { 16 | throw new Error('No secret present in signUp query response.') 17 | } 18 | 19 | req.session.set('user', signUp.instance) 20 | req.session.set('secret', signUp.secret) 21 | await req.session.save() 22 | 23 | res.status(200).json(signUp.instance) 24 | } catch (error) { 25 | console.error(error) 26 | res.status(400).json({ error: error.message || error }) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /examples/with-authentication/pages/api/user.ts: -------------------------------------------------------------------------------- 1 | import { NextApiResponse } from 'next' 2 | import withSession, { ApiRequestWithSession } from '|lib/withSession' 3 | 4 | export default withSession(async (req: ApiRequestWithSession, res: NextApiResponse) => { 5 | const user = req.session.get('user') 6 | 7 | res.json(user || 'null') 8 | }) 9 | -------------------------------------------------------------------------------- /examples/with-authentication/pages/index.js: -------------------------------------------------------------------------------- 1 | import AccountContainer from '|lib/accountContainer' 2 | 3 | const askCredentials = () => { 4 | const email = prompt('Please enter your email') 5 | const password = prompt('Please enter your password') 6 | 7 | if (!email || !password) { 8 | throw new Error('invalid value') 9 | } 10 | 11 | return { email, password } 12 | } 13 | 14 | const Page = () => { 15 | const { user, loading, signup, login, logout } = AccountContainer.useContainer() 16 | 17 | if (loading) { 18 | return 'loading...' 19 | } 20 | 21 | if (!user) { 22 | return ( 23 | <> 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | return ( 31 | <> 32 |

User: {JSON.stringify(user)}

33 | 34 | 35 | ) 36 | } 37 | 38 | Page.displayName = 'ProfilePage' 39 | export default Page 40 | -------------------------------------------------------------------------------- /examples/with-authentication/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "|*": ["*"] 6 | }, 7 | "target": "es5", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "exclude": ["node_modules", "domain"], 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Brainyduck's logo 6 | 7 |

8 | 9 | # Proxied Authentication & authorization example 10 | 11 | This example shows how to add authentication and authorization to a next.js project backed by brainyduck, tunneling all database calls through a proxy on the backend. 12 | 13 | ## Structure 14 | 15 | - The [domain](./domain) folder contains the models, roles and functions. 16 | 17 | - The files [pages/api/proxy.ts](./pages/api/proxy.ts) and [lib/proxy.ts](./lib/proxy.ts) handle all the proxy related work. 18 | 19 | - Everything else is just regular next.js code, but with special attention to [pages/api](./pages/api): that's where backend and frontend get connected. 20 | 21 | ## Setup 22 | 23 | Execute `brainyduck` inside the domain folder and then start next.js as usual. 24 | 25 | ```bash 26 | export FAUNA_SECRET= 27 | 28 | $ npx brainyduck dev ./domain --no-watch 29 | $ npx next dev 30 | ``` 31 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/Schema.gql: -------------------------------------------------------------------------------- 1 | type Query {} 2 | type Mutation {} 3 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/accounts/Token.gql: -------------------------------------------------------------------------------- 1 | type Token @embedded { 2 | instance: User! 3 | secret: String! 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/accounts/User.gql: -------------------------------------------------------------------------------- 1 | type User { 2 | email: String! @unique 3 | } 4 | 5 | extend type Mutation { 6 | signUp(email: String!, password: String!): Token! @resolver(name: "signUp") 7 | login(email: String!, password: String!): Token! @resolver(name: "login") 8 | logout: Boolean! @resolver(name: "logout") 9 | } 10 | 11 | extend type Query { 12 | whoAmI: User @resolver(name: "whoAmI") 13 | } 14 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/accounts/actions.gql: -------------------------------------------------------------------------------- 1 | mutation signUp($email: String!, $password: String!) { 2 | signUp(email: $email, password: $password) { 3 | secret 4 | instance { 5 | email 6 | } 7 | } 8 | } 9 | 10 | mutation login($email: String!, $password: String!) { 11 | login(email: $email, password: $password) { 12 | secret 13 | instance { 14 | email 15 | } 16 | } 17 | } 18 | 19 | mutation logout { 20 | logout 21 | } 22 | 23 | query whoAmI { 24 | whoAmI { 25 | email 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/accounts/login.udf: -------------------------------------------------------------------------------- 1 | Query( 2 | Lambda( 3 | ['email', 'password'], 4 | Login(Match(Index('unique_User_email'), Var('email')), { 5 | password: Var('password') 6 | }) 7 | ) 8 | ) 9 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/accounts/logout.udf: -------------------------------------------------------------------------------- 1 | Query(Lambda([], Logout(false))) 2 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/accounts/signUp.udf: -------------------------------------------------------------------------------- 1 | Query( 2 | Lambda( 3 | ['email', 'password'], 4 | Login( 5 | Select('ref', 6 | Create(Collection('User'), { 7 | credentials: { password: Var('password') }, 8 | data: { 9 | email: Var('email'), 10 | }, 11 | }), 12 | ), 13 | { password: Var('password') }, 14 | ) 15 | ) 16 | ) 17 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/accounts/user.role: -------------------------------------------------------------------------------- 1 | { 2 | membership: [{ resource: Collection('User') }], 3 | privileges: [ 4 | { 5 | resource: Collection('User'), 6 | actions: { 7 | read: Query( 8 | Lambda('ref', Equals(CurrentIdentity(), Var('ref'))) 9 | ) 10 | } 11 | }, 12 | { 13 | resource: Function('logout'), 14 | actions: { call: true } 15 | }, 16 | { 17 | resource: Function('whoAmI'), 18 | actions: { call: true } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/domain/accounts/whoAmI.udf: -------------------------------------------------------------------------------- 1 | Query(Lambda([], 2 | Get(CurrentIdentity()) 3 | )) 4 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/lib/accounts.ts: -------------------------------------------------------------------------------- 1 | import proxy from '|lib/proxy' 2 | import fetchJson from '|lib/fetchJson' 3 | 4 | const prepareAction = (name: string) => (body: object) => 5 | fetchJson(`/api/${name}`, { method: 'POST', body }) 6 | 7 | export const signup = prepareAction('signup') 8 | export const login = prepareAction('login') 9 | export const logout = prepareAction('logout') 10 | export const user = () => proxy.whoAmI().then(({ whoAmI }) => whoAmI) 11 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/lib/fetchJson.ts: -------------------------------------------------------------------------------- 1 | interface FetchError extends Error { 2 | response: Response 3 | data: any 4 | } 5 | 6 | export default async function fetchJson( 7 | url: string, 8 | { body, headers, ...options } = <{ [k: string]: any }>{} 9 | ) { 10 | const response = await fetch(url, { 11 | headers: { 12 | Accept: 'application/json', 13 | 'Content-Type': 'application/json', 14 | ...headers, 15 | }, 16 | ...options, 17 | body: body && JSON.stringify(body), 18 | }) 19 | 20 | if (!response.ok) { 21 | const error = new Error(response.statusText) 22 | error.response = response 23 | 24 | try { 25 | error.data = await response.clone().json() 26 | } catch (e) { 27 | error.data = await response.text() 28 | } 29 | 30 | if (error.data.error) { 31 | error.message = error.data.error 32 | } 33 | 34 | throw error 35 | } 36 | 37 | return await response.json() 38 | } 39 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/lib/proxy.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | import fetchJson from './fetchJson' 3 | 4 | const call = async ( 5 | method: string | number | symbol, 6 | args: object 7 | ): Promise> => { 8 | if (typeof window === 'undefined') { 9 | console.error(`Failed attempt to call ${String(method)} with args:`, args) 10 | throw new Error(`A proxied call can only be made on the client side.`) 11 | } 12 | 13 | return await fetchJson('/api/proxy', { 14 | method: 'POST', 15 | body: { method, args }, 16 | }) 17 | } 18 | 19 | const proxy = new Proxy( 20 | {}, 21 | { 22 | get(target, prop, receiver) { 23 | return (args: object) => call(prop, args) 24 | }, 25 | } 26 | ) as ReturnType 27 | 28 | export default proxy 29 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/lib/withSession.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest } from 'next' 2 | import { withIronSession, Handler, Session } from 'next-iron-session' 3 | 4 | export interface ApiRequestWithSession extends NextApiRequest { 5 | session: Session 6 | } 7 | 8 | export default (handler: Handler) => 9 | withIronSession(handler, { 10 | password: process.env.SECRET_COOKIE_PASSWORD!, 11 | cookieName: 'authorization', 12 | cookieOptions: { 13 | secure: process.env.NODE_ENV === 'production' ? true : false, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/next.config.js: -------------------------------------------------------------------------------- 1 | if (!process.env.FAUNA_SECRET) { 2 | throw new Error('Please define `process.env.FAUNA_SECRET`') 3 | } 4 | 5 | module.exports = { 6 | env: { 7 | SECRET_COOKIE_PASSWORD: 'ZBfcR7cmYAg6zD9uYeSVYTaaWbReqzvr', 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck-example-proxy", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "brainyduck": "latest", 6 | "next": "^13.1.2", 7 | "next-iron-session": "^4.1.10", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "valtio": "^1.9.0" 11 | }, 12 | "devDependencies": { 13 | "@types/react": "18.0.27", 14 | "typescript": "^4.9.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/pages/api/login.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | import { NextApiResponse } from 'next' 3 | import withSession, { ApiRequestWithSession } from '|lib/withSession' 4 | 5 | export default withSession(async (req: ApiRequestWithSession, res: NextApiResponse) => { 6 | try { 7 | const { email, password } = await req.body 8 | 9 | if (!email || !password) { 10 | throw new Error('Email and password must be provided.') 11 | } 12 | 13 | const { login } = await sdk().login({ email, password }) 14 | 15 | if (!login.secret) { 16 | throw new Error('No secret present in login query response.') 17 | } 18 | 19 | req.session.set('user', login.instance) 20 | req.session.set('secret', login.secret) 21 | await req.session.save() 22 | 23 | res.status(200).json(login.instance) 24 | } catch (error) { 25 | console.error(error) 26 | res.status(400).json({ error: error.message || error }) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/pages/api/logout.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | import { NextApiResponse } from 'next' 3 | import withSession, { ApiRequestWithSession } from '|lib/withSession' 4 | 5 | export default withSession(async (req: ApiRequestWithSession, res: NextApiResponse) => { 6 | try { 7 | const user = req.session.get('user') 8 | const secret = req.session.get('secret') 9 | req.session.destroy() 10 | 11 | const { logout } = await sdk({ secret }).logout() 12 | 13 | res.json(logout ? 'null' : user) 14 | } catch (error) { 15 | console.error(error) 16 | res.status(400).json({ error: error.message || error }) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/pages/api/proxy.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | import { NextApiResponse } from 'next' 3 | import withSession, { ApiRequestWithSession } from '|lib/withSession' 4 | 5 | type Method = keyof ReturnType 6 | 7 | export default withSession(async (req: ApiRequestWithSession, res: NextApiResponse) => { 8 | try { 9 | const { method, args } = <{ method: Method; args: any }>await req.body 10 | 11 | if (!method) { 12 | throw new Error('Method and arguments must be provided.') 13 | } 14 | 15 | let secret = req.session.get('secret') 16 | 17 | if (!secret) { 18 | return res.status(401).json(new Error(`Unauthorized request from unregistered client`)) 19 | } 20 | 21 | const response = await sdk({ secret })[method](args) 22 | res.status(200).json(response) 23 | } catch (error) { 24 | console.error(error) 25 | res.status(400).json({ error: error.message || error }) 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/pages/api/signup.ts: -------------------------------------------------------------------------------- 1 | import sdk from 'brainyduck' 2 | import { NextApiResponse } from 'next' 3 | import withSession, { ApiRequestWithSession } from '|lib/withSession' 4 | 5 | export default withSession(async (req: ApiRequestWithSession, res: NextApiResponse) => { 6 | try { 7 | const { email, password } = await req.body 8 | 9 | if (!email || !password) { 10 | throw new Error('Email and password must be provided.') 11 | } 12 | 13 | const { signUp } = await sdk().signUp({ email, password }) 14 | 15 | if (!signUp.secret) { 16 | throw new Error('No secret present in signUp query response.') 17 | } 18 | 19 | req.session.set('user', signUp.instance) 20 | req.session.set('secret', signUp.secret) 21 | await req.session.save() 22 | 23 | res.status(200).json(signUp.instance) 24 | } catch (error) { 25 | console.error(error) 26 | res.status(400).json({ error: error.message || error }) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { proxy, useProxy, subscribe } from 'valtio' 3 | import { user, signup, login, logout } from '|lib/accounts' 4 | 5 | const askCredentials = () => { 6 | const email = prompt('Please enter your email') 7 | const password = prompt('Please enter your password') 8 | 9 | if (!email || !password) { 10 | throw new Error('invalid value') 11 | } 12 | 13 | return { email, password } 14 | } 15 | 16 | const state = proxy({ user: null }) 17 | const updateUser = () => 18 | (state.user = user().catch((err) => { 19 | console.error(err) 20 | return null 21 | })) 22 | 23 | subscribe(state, () => console.log('user has changed to', state.user)) 24 | 25 | if (typeof window !== 'undefined') { 26 | updateUser() 27 | } 28 | 29 | const Page = () => { 30 | const snapshot = useProxy(state) 31 | 32 | if (!snapshot.user) { 33 | return ( 34 | <> 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | return ( 42 | <> 43 |

User: {JSON.stringify(snapshot.user)}

44 | 45 | 46 | ) 47 | } 48 | 49 | const Suspense = typeof window !== 'undefined' ? React.Suspense : React.Fragment 50 | 51 | const App = () => ( 52 | loading...}> 53 | 54 | 55 | ) 56 | export default App 57 | -------------------------------------------------------------------------------- /examples/with-proxied-authentication/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "|*": ["*"] 6 | }, 7 | "target": "es5", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "exclude": ["node_modules", "domain"], 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/with-user-defined-functions: -------------------------------------------------------------------------------- 1 | with-UDF -------------------------------------------------------------------------------- /fetch-ponyfill.cjs: -------------------------------------------------------------------------------- 1 | const debug = require('debug') 2 | 3 | if (!globalThis.fetch) { 4 | debug('brainyduck:fetch')(`Native fetch not found. Using node-fetch`) 5 | module.exports = require('node-fetch') 6 | } else { 7 | debug('brainyduck:fetch')(`Using native fetch`) 8 | Object.defineProperty(exports, '__esModule', { value: true }) 9 | 10 | module.exports = globalThis.fetch 11 | module.exports.Headers = globalThis.Headers 12 | module.exports.Request = globalThis.Request 13 | module.exports.Response = globalThis.Response 14 | } 15 | -------------------------------------------------------------------------------- /locateCache.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = (file = '') => path.join(__dirname, `.cache/`, file) 4 | -------------------------------------------------------------------------------- /locateCache.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | const __filename = fileURLToPath(import.meta.url) 5 | const __dirname = path.dirname(__filename) 6 | 7 | export default (file = '') => path.join(__dirname, `.cache/`, file) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck", 3 | "version": "1.0.0", 4 | "description": "Quickly build powerful backends using only your graphql schemas", 5 | "repository": "github:zvictor/brainyduck", 6 | "keywords": [ 7 | "BaaS", 8 | "backend", 9 | "fauna", 10 | "graphql", 11 | "serverless", 12 | "low-code", 13 | "lowcode" 14 | ], 15 | "license": "AGPL-3.0-or-later", 16 | "type": "module", 17 | "bin": "./cli.js", 18 | "exports": { 19 | ".": { 20 | "types": "./.cache/sdk.d.ts", 21 | "import": "./.cache/sdk.mjs", 22 | "require": "./.cache/sdk.cjs" 23 | }, 24 | "./cache": { 25 | "import": "./locateCache.js", 26 | "require": "./locateCache.cjs" 27 | }, 28 | "./utils": "./utils.js", 29 | "./*": "./commands/*.js" 30 | }, 31 | "main": "./.cache/sdk.cjs", 32 | "types": "./.cache/sdk.d.ts", 33 | "files": [ 34 | "/.cache", 35 | "/commands", 36 | "/scripts", 37 | "*.cjs", 38 | "*.js", 39 | "tsconfig.json", 40 | "tsup.config.ts", 41 | "README.md" 42 | ], 43 | "scripts": { 44 | "prepublishOnly": "rm -Rf .cache ; cp -r ./protection ./.cache", 45 | "test": "./tests/run-tests.sh" 46 | }, 47 | "dependencies": { 48 | "@graphql-codegen/core": "4.0.0", 49 | "@graphql-codegen/typescript": "4.0.1", 50 | "@graphql-codegen/typescript-graphql-request": "5.0.0", 51 | "@graphql-codegen/typescript-operations": "4.0.1", 52 | "@graphql-tools/load": "8.0.0", 53 | "@graphql-tools/merge": "9.0.0", 54 | "@graphql-tools/url-loader": "8.0.0", 55 | "@swc/core": "1.3.69", 56 | "@types/node": "20.4.2", 57 | "@types/react": "18.2.15", 58 | "chalk": "^5.3.0", 59 | "chokidar": "3.5.3", 60 | "commander": "11.0.0", 61 | "constant-case": "3.0.4", 62 | "debug": "4.3.4", 63 | "execa": "7.1.1", 64 | "fauna-shell": "0.15.0", 65 | "faunadb": "4.8.0", 66 | "figures": "5.0.0", 67 | "fs-extra": "11.1.1", 68 | "globby": "13.2.2", 69 | "gql-generator": "1.0.19", 70 | "graphql": "16.7.1", 71 | "graphql-request": "6.1.0", 72 | "graphql-tag": "2.12.6", 73 | "log-symbols": "5.1.0", 74 | "ora": "6.3.1", 75 | "p-queue": "7.3.4", 76 | "resolve-cwd": "3.0.0", 77 | "tempy": "3.1.0", 78 | "tsup": "7.1.0", 79 | "typescript": "5.1.6" 80 | }, 81 | "optionalDependencies": { 82 | "node-fetch": "cjs" 83 | }, 84 | "funding": { 85 | "type": "individual", 86 | "url": "https://github.com/sponsors/zvictor" 87 | } 88 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**' 3 | - 'examples/**' 4 | - 'tests' 5 | -------------------------------------------------------------------------------- /protection/sdk.cjs: -------------------------------------------------------------------------------- 1 | if (require.main === module) { 2 | console.error( 3 | `You tried executing brainyduck in some unexpected and unsupported way! 🤷‍🍳\n\nPlease run 'npx brainyduck --help' in your project diretory to get started. 💁🥚\n ↳ or ask for help on https://github.com/zvictor/brainyduck/discussions \n` 4 | ) 5 | 6 | throw new Error('Non executable file') 7 | } 8 | 9 | console.error( 10 | `Project is missing SDK! 🤷‍🐣\n\nPlease run 'npx brainyduck dev' (or 'npx brainyduck build') in your project diretory to get started. 💁🐥\n ↳ read more on https://github.com/zvictor/brainyduck/wiki/Missing-sdk \n` 11 | ) 12 | 13 | console.error(`Debug info: no file could be found at\n ↳ ${filePath}\n`) 14 | throw new Error('SDK could not be found.') 15 | -------------------------------------------------------------------------------- /protection/sdk.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Project is missing SDK! 🤷‍🐣 4 | * 5 | * Please run 'npx brainyduck dev' (or 'npx brainyduck build') in your project diretory to get started. 💁🐥 6 | * 7 | * ↳ read more on https://github.com/zvictor/brainyduck/wiki/Missing-sdk 8 | * 9 | **/ 10 | export default Error = new Error('Project is missing SDK! 🤷‍🐣') 11 | -------------------------------------------------------------------------------- /protection/sdk.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | 3 | if (process.argv[1] === fileURLToPath(import.meta.url)) { 4 | console.error( 5 | `You tried executing brainyduck in some unexpected and unsupported way! 🤷‍🍳\n\nPlease run 'npx brainyduck --help' in your project diretory to get started. 💁🥚\n ↳ or ask for help on https://github.com/zvictor/brainyduck/discussions \n` 6 | ) 7 | 8 | throw new Error('Non executable file') 9 | } 10 | 11 | console.error( 12 | `Project is missing SDK! 🤷‍🐣\n\nPlease run 'npx brainyduck dev' (or 'npx brainyduck build') in your project diretory to get started. 💁🐥\n ↳ read more on https://github.com/zvictor/brainyduck/wiki/Missing-sdk \n` 13 | ) 14 | 15 | console.error(`Debug info: no file could be found at\n ↳ ${filePath}\n`) 16 | throw new Error('SDK could not be found.') 17 | -------------------------------------------------------------------------------- /scripts/reset.collections.fql: -------------------------------------------------------------------------------- 1 | // Delete all Collections 2 | Foreach( 3 | Paginate(Collections(), { size: 100000 }), 4 | Lambda("ref", 5 | Delete(Var("ref")) 6 | ) 7 | ) 8 | -------------------------------------------------------------------------------- /scripts/reset.databases.fql: -------------------------------------------------------------------------------- 1 | // Delete all Databases 2 | Map( 3 | Paginate(Databases(), { size: 100000 }), 4 | Lambda(db => 5 | Delete(db) 6 | ) 7 | ) 8 | -------------------------------------------------------------------------------- /scripts/reset.documents.fql: -------------------------------------------------------------------------------- 1 | // Delete all Documents 2 | Map( 3 | Paginate(Collections(), { size: 100000 }), 4 | Lambda(col => 5 | Map( 6 | Paginate(Documents(col), { size: 100000 }), 7 | Lambda(doc => 8 | Delete(doc) 9 | ) 10 | ) 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /scripts/reset.functions.fql: -------------------------------------------------------------------------------- 1 | // Delete all Functions 2 | Foreach( 3 | Paginate(Functions(), { size: 100000 }), 4 | Lambda("ref", 5 | Delete(Var("ref")) 6 | ) 7 | ) 8 | -------------------------------------------------------------------------------- /scripts/reset.indexes.fql: -------------------------------------------------------------------------------- 1 | // Delete all Indexes 2 | Foreach( 3 | Paginate(Indexes(), { size: 100000 }), 4 | Lambda("ref", 5 | Delete(Var("ref")) 6 | ) 7 | ) 8 | -------------------------------------------------------------------------------- /scripts/reset.roles.fql: -------------------------------------------------------------------------------- 1 | // Delete all Roles 2 | Foreach( 3 | Paginate(Roles(), { size: 100000 }), 4 | Lambda("ref", 5 | Delete(Var("ref")) 6 | ) 7 | ) 8 | -------------------------------------------------------------------------------- /scripts/reset.schemas.fql: -------------------------------------------------------------------------------- 1 | // Remove GraphQL metadata from Collections 2 | Foreach( 3 | Paginate(Collections(), { size: 100000 }), 4 | Lambda("ref", 5 | Update(Var("ref"), 6 | { 7 | "data": { 8 | "gql": null 9 | } 10 | } 11 | ) 12 | ) 13 | ); 14 | 15 | // Remove GraphQL metadata from Functions 16 | Foreach( 17 | Paginate(Functions(), { size: 100000 }), 18 | Lambda("ref", 19 | Update(Var("ref"), 20 | { 21 | "data": { 22 | "gql": null 23 | } 24 | } 25 | ) 26 | ) 27 | ); 28 | 29 | // Remove GraphQL metadata from Indexes 30 | Foreach( 31 | Paginate(Indexes(), { size: 100000 }), 32 | Lambda("ref", 33 | Update(Var("ref"), 34 | { 35 | "data": { 36 | "gql": null 37 | } 38 | } 39 | ) 40 | ) 41 | ); 42 | -------------------------------------------------------------------------------- /tests/babel.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [['@babel/preset-env', { modules: false, targets: { node: 'current' } }]], 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/.prettierignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | -------------------------------------------------------------------------------- /tests/fixtures/basic-esbuild-bundle.output.js: -------------------------------------------------------------------------------- 1 | export default (results, name) => { 2 | console.log(`Basic-esbuild-bundle example ${name ? `- ${name} ` : ''}run:\n`, results) 3 | 4 | const parsedResults = JSON.parse( 5 | `[${results 6 | .replaceAll(`'`, `"`) 7 | .replace(/([\w]+):/gm, `"$1":`) 8 | .replace(/}[\s]*{/gm, '},{')}]` 9 | ) 10 | 11 | expect(parsedResults.length).toBe(4) 12 | 13 | expect(parsedResults[0]).toEqual({ 14 | createUser: { 15 | _id: expect.any(String), 16 | _ts: expect.any(Number), 17 | username: expect.stringContaining('rick-sanchez-'), 18 | }, 19 | }) 20 | 21 | expect(parsedResults[1]).toEqual({ 22 | createUser: { 23 | _id: expect.any(String), 24 | _ts: expect.any(Number), 25 | username: expect.stringContaining('morty-smith-'), 26 | }, 27 | }) 28 | expect(parsedResults[2]).toEqual({ 29 | _id: expect.any(String), 30 | _ts: expect.any(Number), 31 | username: expect.stringContaining('rick-sanchez-'), 32 | }) 33 | expect(parsedResults[3]).toEqual({ 34 | _id: expect.any(String), 35 | _ts: expect.any(Number), 36 | username: expect.stringContaining('morty-smith-'), 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /tests/fixtures/basic.d.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { [K in keyof T]: T[K] }; 4 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 5 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 6 | /** All built-in and custom scalars, mapped to their actual values */ 7 | export type Scalars = { 8 | ID: string; 9 | String: string; 10 | Boolean: boolean; 11 | Int: number; 12 | Float: number; 13 | Date: any; 14 | Time: any; 15 | /** The `Long` scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1. */ 16 | Long: any; 17 | }; 18 | 19 | export enum Brainyduck { 20 | Resetting = 'RESETTING' 21 | } 22 | 23 | export type Mutation = { 24 | __typename?: 'Mutation'; 25 | /** Create a new document in the collection of 'User' */ 26 | createUser: User; 27 | /** Update an existing document in the collection of 'User' */ 28 | updateUser?: Maybe; 29 | /** Delete an existing document in the collection of 'User' */ 30 | deleteUser?: Maybe; 31 | /** Partially updates an existing document in the collection of 'User'. It only modifies the values that are specified in the arguments. During execution, it verifies that required fields are not set to 'null'. */ 32 | partialUpdateUser?: Maybe; 33 | }; 34 | 35 | 36 | export type MutationCreateUserArgs = { 37 | data: UserInput; 38 | }; 39 | 40 | 41 | export type MutationUpdateUserArgs = { 42 | id: Scalars['ID']; 43 | data: UserInput; 44 | }; 45 | 46 | 47 | export type MutationDeleteUserArgs = { 48 | id: Scalars['ID']; 49 | }; 50 | 51 | 52 | export type MutationPartialUpdateUserArgs = { 53 | id: Scalars['ID']; 54 | data: PartialUpdateUserInput; 55 | }; 56 | 57 | /** 'User' input values */ 58 | export type PartialUpdateUserInput = { 59 | username?: InputMaybe; 60 | }; 61 | 62 | /** 'User' input values */ 63 | export type UserInput = { 64 | username: Scalars['String']; 65 | }; 66 | 67 | export type Query = { 68 | __typename?: 'Query'; 69 | /** Find a document from the collection of 'User' by its id. */ 70 | findUserByID?: Maybe; 71 | allUsers: UserPage; 72 | }; 73 | 74 | 75 | export type QueryFindUserByIdArgs = { 76 | id: Scalars['ID']; 77 | }; 78 | 79 | 80 | export type QueryAllUsersArgs = { 81 | _size?: InputMaybe; 82 | _cursor?: InputMaybe; 83 | }; 84 | 85 | export type User = { 86 | __typename?: 'User'; 87 | /** The document's ID. */ 88 | _id: Scalars['ID']; 89 | /** The document's timestamp. */ 90 | _ts: Scalars['Long']; 91 | username: Scalars['String']; 92 | }; 93 | 94 | /** The pagination object for elements of type 'User'. */ 95 | export type UserPage = { 96 | __typename?: 'UserPage'; 97 | /** The elements of type 'User' in this page. */ 98 | data: Array>; 99 | /** A cursor for elements coming after the current page. */ 100 | after?: Maybe; 101 | /** A cursor for elements coming before the current page. */ 102 | before?: Maybe; 103 | }; 104 | -------------------------------------------------------------------------------- /tests/fixtures/basic.output.js: -------------------------------------------------------------------------------- 1 | export default (results, name) => { 2 | console.log(`Basic example ${name ? `- ${name} ` : ''}run:\n`, results) 3 | 4 | const parsedResults = JSON.parse( 5 | `[${results 6 | .replaceAll(`'`, `"`) 7 | .replace(/([\w]+):/gm, `"$1":`) 8 | .replace(/}[\s]*{/gm, '},{')}]` 9 | ) 10 | 11 | expect(parsedResults.length).toBe(4) 12 | 13 | expect(parsedResults[0]).toEqual({ 14 | createUser: { 15 | _id: expect.any(String), 16 | _ts: expect.any(Number), 17 | username: expect.stringContaining('rick-sanchez-'), 18 | }, 19 | }) 20 | 21 | expect(parsedResults[1]).toEqual({ 22 | createUser: { 23 | _id: expect.any(String), 24 | _ts: expect.any(Number), 25 | username: expect.stringContaining('morty-smith-'), 26 | }, 27 | }) 28 | expect(parsedResults[2]).toEqual({ 29 | _id: expect.any(String), 30 | _ts: expect.any(Number), 31 | username: expect.stringContaining('rick-sanchez-'), 32 | }) 33 | expect(parsedResults[3]).toEqual({ 34 | _id: expect.any(String), 35 | _ts: expect.any(Number), 36 | username: expect.stringContaining('morty-smith-'), 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /tests/fixtures/modularized-esbuild-bundle.output.js: -------------------------------------------------------------------------------- 1 | export default (results, name) => { 2 | console.log(`Modularized-esbuild-bundle example ${name ? `- ${name} ` : ''}run:\n`, results) 3 | 4 | const parsedResults = JSON.parse( 5 | results 6 | .split('\n') 7 | .slice(1) 8 | .join('\n') 9 | .replaceAll(`'`, `"`) 10 | .replace(/([\w]+):/gm, `"$1":`) 11 | .replace(/}[\s]*{/gm, '},{') 12 | ) 13 | 14 | expect(parsedResults).toEqual({ 15 | findPostByID: { 16 | author: { 17 | _id: expect.any(String), 18 | _ts: expect.any(Number), 19 | name: 'Whatever Name', 20 | }, 21 | _id: expect.any(String), 22 | _ts: expect.any(Number), 23 | content: 'some post content', 24 | title: 'a post title', 25 | }, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /tests/fixtures/modularized.output.js: -------------------------------------------------------------------------------- 1 | export default (results, name) => { 2 | console.log(`Modularized example ${name ? `- ${name} ` : ''}run:\n`, results) 3 | 4 | const parsedResults = JSON.parse( 5 | results 6 | .split('\n') 7 | .slice(1) 8 | .join('\n') 9 | .replaceAll(`'`, `"`) 10 | .replace(/([\w]+):/gm, `"$1":`) 11 | .replace(/}[\s]*{/gm, '},{') 12 | ) 13 | 14 | expect(parsedResults).toEqual({ 15 | findPostByID: { 16 | author: { 17 | _id: expect.any(String), 18 | _ts: expect.any(Number), 19 | name: 'Whatever Name', 20 | }, 21 | _id: expect.any(String), 22 | _ts: expect.any(Number), 23 | content: 'some post content', 24 | title: 'a post title', 25 | }, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /tests/fixtures/simplified.role: -------------------------------------------------------------------------------- 1 | # Simplified format is not allowed for roles. 2 | [ 3 | { 4 | resource: Function('sayHello'), 5 | actions: { call: true } 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /tests/fixtures/unmatched.role: -------------------------------------------------------------------------------- 1 | { 2 | # The role name does not match the file name [throws ERROR!] 3 | name: "publicAccess", 4 | privileges: [ 5 | { 6 | resource: Function('sayHello'), 7 | actions: { call: true } 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/unmatched.udf: -------------------------------------------------------------------------------- 1 | { 2 | # The function name does not match the file name [throws ERROR!] 3 | name: "sayHello", 4 | body: Query(Lambda(["name"], 5 | Concat(["Hello ", Var("name")]) 6 | )) 7 | } 8 | -------------------------------------------------------------------------------- /tests/jest.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | import path from 'path' 7 | import { createRequire } from 'module' 8 | const require = createRequire(import.meta.url) 9 | 10 | export default { 11 | // All imported modules in your tests should be mocked automatically 12 | // automock: false, 13 | 14 | // Stop running tests after `n` failures 15 | // bail: 0, 16 | 17 | // The directory where Jest should store its cached dependency information 18 | // cacheDirectory: "/private/var/folders/89/ckphm8jd0xxf1wl7131ss7r40000gn/T/jest_dx", 19 | 20 | // Automatically clear mock calls and instances between every test 21 | clearMocks: true, 22 | 23 | // Indicates whether the coverage information should be collected while executing the test 24 | // collectCoverage: false, 25 | 26 | // An array of glob patterns indicating a set of files for which coverage information should be collected 27 | // collectCoverageFrom: undefined, 28 | 29 | // The directory where Jest should output its coverage files 30 | coverageDirectory: 'coverage', 31 | 32 | // An array of regexp pattern strings used to skip coverage collection 33 | // coveragePathIgnorePatterns: [ 34 | // "/node_modules/" 35 | // ], 36 | 37 | // Indicates which provider should be used to instrument code for coverage 38 | coverageProvider: 'v8', 39 | 40 | // A list of reporter names that Jest uses when writing coverage reports 41 | // coverageReporters: [ 42 | // "json", 43 | // "text", 44 | // "lcov", 45 | // "clover" 46 | // ], 47 | 48 | // An object that configures minimum threshold enforcement for coverage results 49 | // coverageThreshold: undefined, 50 | 51 | // A path to a custom dependency extractor 52 | // dependencyExtractor: undefined, 53 | 54 | // Make calling deprecated APIs throw helpful error messages 55 | // errorOnDeprecated: false, 56 | 57 | // Force coverage collection from ignored files using an array of glob patterns 58 | // forceCoverageMatch: [], 59 | 60 | // A path to a module which exports an async function that is triggered once before all test suites 61 | // globalSetup: undefined, 62 | 63 | // A path to a module which exports an async function that is triggered once after all test suites 64 | // globalTeardown: undefined, 65 | 66 | // A set of global variables that need to be available in all test environments 67 | // globals: {}, 68 | 69 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 70 | // maxWorkers: "50%", 71 | 72 | // An array of directory names to be searched recursively up from the requiring module's location 73 | // moduleDirectories: [ 74 | // "node_modules" 75 | // ], 76 | 77 | // An array of file extensions your modules use 78 | // moduleFileExtensions: [ 79 | // "js", 80 | // "json", 81 | // "jsx", 82 | // "ts", 83 | // "tsx", 84 | // "node" 85 | // ], 86 | 87 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 88 | // moduleNameMapper: { 89 | // chalk: require.resolve('chalk'), 90 | // '#ansi-styles': path.join( 91 | // require.resolve('chalk').split('chalk')[0], 92 | // 'chalk/source/vendor/ansi-styles/index.js' 93 | // ), 94 | // '#supports-color': path.join( 95 | // require.resolve('chalk').split('chalk')[0], 96 | // 'chalk/source/vendor/supports-color/index.js' 97 | // ), 98 | // }, 99 | 100 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 101 | // modulePathIgnorePatterns: [], 102 | 103 | // Activates notifications for test results 104 | // notify: false, 105 | 106 | // An enum that specifies notification mode. Requires { notify: true } 107 | // notifyMode: "failure-change", 108 | 109 | // A preset that is used as a base for Jest's configuration 110 | // preset: undefined, 111 | 112 | // Run tests from one or more projects 113 | // projects: undefined, 114 | 115 | // Use this configuration option to add custom reporters to Jest 116 | // reporters: undefined, 117 | 118 | // Automatically reset mock state between every test 119 | // resetMocks: false, 120 | 121 | // Reset the module registry before running each individual test 122 | // resetModules: false, 123 | 124 | // A path to a custom resolver 125 | // resolver: undefined, 126 | 127 | // Automatically restore mock state between every test 128 | // restoreMocks: false, 129 | 130 | // The root directory that Jest should scan for tests and modules within 131 | // rootDir: undefined, 132 | 133 | // A list of paths to directories that Jest should use to search for files in 134 | // roots: [ 135 | // "" 136 | // ], 137 | 138 | // Allows you to use a custom runner instead of Jest's default test runner 139 | // runner: "jest-runner", 140 | 141 | // The paths to modules that run some code to configure or set up the testing environment before each test 142 | // setupFiles: [], 143 | 144 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 145 | // setupFilesAfterEnv: [], 146 | 147 | // The number of seconds after which a test is considered as slow and reported as such in the results. 148 | // slowTestThreshold: 5, 149 | 150 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 151 | // snapshotSerializers: [], 152 | 153 | // The test environment that will be used for testing 154 | testEnvironment: 'node', 155 | 156 | // Options that will be passed to the testEnvironment 157 | // testEnvironmentOptions: {}, 158 | 159 | // Adds a location field to test results 160 | // testLocationInResults: false, 161 | 162 | // The glob patterns Jest uses to detect test files 163 | testMatch: ['**/specs/*.js'], 164 | 165 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 166 | // testPathIgnorePatterns: [ 167 | // "/node_modules/" 168 | // ], 169 | 170 | // The regexp pattern or array of patterns that Jest uses to detect test files 171 | // testRegex: [], 172 | 173 | // This option allows the use of a custom results processor 174 | // testResultsProcessor: undefined, 175 | 176 | // This option allows use of a custom test runner 177 | // testRunner: "jasmine2", 178 | 179 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 180 | // testURL: "http://localhost", 181 | 182 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 183 | // timers: "real", 184 | 185 | // A map from regular expressions to paths to transformers 186 | // transform: undefined, 187 | 188 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 189 | // transformIgnorePatterns: [ 190 | // "/node_modules/", 191 | // "\\.pnp\\.[^\\/]+$" 192 | // ], 193 | 194 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 195 | // unmockedModulePathPatterns: undefined, 196 | 197 | // Indicates whether each individual test should be reported during the run 198 | // verbose: undefined, 199 | 200 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 201 | // watchPathIgnorePatterns: [], 202 | 203 | // Whether to use watchman for file crawling 204 | // watchman: true, 205 | } 206 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brainyduck-tests", 3 | "version": "0.0.3", 4 | "license": "MIT", 5 | "private": true, 6 | "type": "module", 7 | "scripts": { 8 | "start": "./run-tests.sh", 9 | "test": "./run-tests.sh" 10 | }, 11 | "dependencies": { 12 | "@babel/core": "^7.22.9", 13 | "@babel/preset-env": "^7.22.9", 14 | "babel-jest": "29.6.1", 15 | "brainyduck": "link:..", 16 | "esbuild": "0.18.12", 17 | "execa": "^7.1.1", 18 | "jest": "29.6.1", 19 | "param-case": "^3.0.4", 20 | "ts-node": "^10.9.1", 21 | "tsup": "7.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd `dirname "$0"` 4 | 5 | cmd=./node_modules/.bin/jest 6 | 7 | if [ ! -f "$cmd" ] ; then 8 | echo "Jest could not be found. Please run 'npm install' in the tests folder"; 9 | exit 1 10 | fi 11 | 12 | if [ -z ${TESTS_SECRET+x} ]; then 13 | echo "ERROR: TESTS_SECRET is not defined. Expecting the environment to set the TESTS_SECRET variable, ex: 'TESTS_SECRET=klJSDojasd8ojasd $0'"; 14 | exit 1; 15 | fi 16 | 17 | NODE_OPTIONS=--experimental-vm-modules $cmd --silent=false --detectOpenHandles "$@" 18 | -------------------------------------------------------------------------------- /tests/specs/build.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import _debug from 'debug' 3 | import { execaSync } from 'execa' 4 | import { fileURLToPath } from 'url' 5 | import path from 'path' 6 | import { temporaryFile, temporaryDirectory } from 'tempy' 7 | import { findBin } from 'brainyduck/utils' 8 | import { 9 | setupEnvironment, 10 | amountOfCollectionsCreated, 11 | listFiles, 12 | removeRetryMessages, 13 | load, 14 | reset, 15 | clone, 16 | } from '../testUtils.js' 17 | 18 | const debug = _debug('brainyduck:test:build') 19 | const originalCache = fileURLToPath(new URL(`../../.cache`, import.meta.url)) 20 | setupEnvironment(`build`) 21 | 22 | beforeAll(() => 23 | fs.rm(originalCache, { 24 | recursive: true, 25 | force: true, 26 | }) 27 | ) 28 | 29 | const resetBuild = async (cwd) => { 30 | await fs.rm(path.join(cwd, 'build'), { 31 | recursive: true, 32 | force: true, 33 | }) 34 | 35 | reset('documents') 36 | } 37 | 38 | const exportIt = async (cwd, callback) => { 39 | const destination = temporaryDirectory() 40 | debug(`Packing directory ${cwd} into ${destination}`) 41 | 42 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'export', destination], { 43 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: undefined }, 44 | cwd, 45 | }) 46 | 47 | debug(`Packing has finished with exit code ${exitCode}`) 48 | 49 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 50 | expect(stdout).toEqual( 51 | expect.not.stringMatching(/error(?!\('SDK requires a secret to be defined.'\))/i) 52 | ) 53 | 54 | expect(removeRetryMessages(stdout)).toEqual(`The package has been saved at ${destination}`) 55 | 56 | debug(`The Package is valid`) 57 | expect(listFiles(destination)).toEqual( 58 | [ 59 | 'package.json', 60 | 'sdk.d.ts', 61 | 'sdk.d.cts', 62 | 'sdk.mjs', 63 | 'sdk.mjs.map', 64 | 'sdk.cjs', 65 | 'sdk.cjs.map', 66 | 'sdk.ts', 67 | 'tsconfig.json', 68 | ].sort() 69 | ) 70 | 71 | execaSync(`npm`, ['i'], { 72 | cwd: destination, 73 | }) 74 | 75 | const sdk = await import(destination) 76 | debug('Loaded the sdk:', sdk) 77 | 78 | await callback(sdk) 79 | 80 | expect(exitCode).toBe(0) 81 | } 82 | 83 | const packIt = async (cwd) => { 84 | debug(`Packing directory ${cwd}`) 85 | const packName = 'brainyduck-sdk-1.0.0.tgz' 86 | 87 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'pack'], { 88 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: undefined }, 89 | cwd, 90 | }) 91 | 92 | debug(`Packing has finished with exit code ${exitCode}`) 93 | 94 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 95 | expect(stdout).toEqual( 96 | expect.not.stringMatching(/error(?!\('SDK requires a secret to be defined.'\))/i) 97 | ) 98 | 99 | expect(removeRetryMessages(stdout)).toEqual( 100 | `The package has been compressed and saved at ${path.join(cwd, packName)}` 101 | ) 102 | 103 | debug(`The Package is valid`) 104 | expect(listFiles(cwd)).toEqual(expect.arrayContaining([packName])) 105 | 106 | expect(exitCode).toBe(0) 107 | } 108 | 109 | test('build an sdk for basic schema and non-standard cache', async () => { 110 | const root = clone() 111 | const cache = path.join(root, '.cache') 112 | const cwd = path.join(root, 'examples/basic') 113 | const outputCheck = (await import(`../fixtures/basic.output.js`)).default 114 | const tsconfig = temporaryFile({ name: 'tsconfig.json' }) 115 | await fs.writeFile(tsconfig, JSON.stringify({ compilerOptions: { moduleResolution: 'Node' } })) 116 | 117 | debug(`Using temporary directory ${cwd}`) 118 | 119 | const { stdout, stderr, exitCode } = execaSync( 120 | 'node', 121 | ['../../cli.js', 'build', 'Schema.graphql'], 122 | { 123 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: undefined }, 124 | cwd, 125 | } 126 | ) 127 | 128 | debug(`Build of 'basic' has finished with exit code ${exitCode}`) 129 | 130 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 131 | expect(stdout).toEqual( 132 | expect.not.stringMatching(/error(?!\('SDK requires a secret to be defined.'\))/i) 133 | ) 134 | 135 | expect(removeRetryMessages(stdout)).toEqual( 136 | `The sdk has been saved at ${path.join(cache, 'sdk.ts')}` 137 | ) 138 | 139 | expect(await fs.readFile(path.join(cache, 'sdk.ts'), { encoding: 'utf8' })).toMatchSnapshot() 140 | debug(`The SDK is valid`) 141 | 142 | expect(listFiles(originalCache)).toEqual([].sort()) 143 | expect(listFiles(cache)).toEqual( 144 | [ 145 | 'sdk.d.ts', 146 | 'sdk.d.cts', 147 | 'sdk.mjs', 148 | 'sdk.mjs.map', 149 | 'sdk.cjs', 150 | 'sdk.cjs.map', 151 | 'sdk.ts', 152 | 'tsconfig.json', 153 | ].sort() 154 | ) 155 | 156 | expect(exitCode).toBe(0) 157 | expect(await amountOfCollectionsCreated()).toBe(0) 158 | 159 | execaSync('node', ['../../cli.js', 'deploy'], { 160 | env: { 161 | DEBUG: '', 162 | FAUNA_SECRET: load('FAUNA_SECRET'), 163 | FORCE_COLOR: 0, 164 | NODE_OPTIONS: '--no-warnings', 165 | }, 166 | cwd, 167 | }) 168 | 169 | expect(await amountOfCollectionsCreated()).toBe(1) 170 | 171 | await exportIt(cwd, (sdk) => { 172 | expect(Object.keys(sdk)).toEqual([ 173 | 'AllUsersDocument', 174 | 'CreateUserDocument', 175 | 'DeleteUserDocument', 176 | 'FindUserByIdDocument', 177 | 'PartialUpdateUserDocument', 178 | 'UpdateUserDocument', 179 | 'brainyduck', 180 | 'default', 181 | 'getSdk', 182 | ]) 183 | }) 184 | 185 | await packIt(cwd) 186 | 187 | // ts-node tests 188 | outputCheck( 189 | execaSync(findBin('ts-node', './tests'), ['index.ts'], { 190 | env: { FAUNA_SECRET: load('FAUNA_SECRET') }, 191 | cwd, 192 | }).stdout, 193 | 'ts-node' 194 | ) 195 | 196 | // tsc tests 197 | await resetBuild(cwd) 198 | 199 | expect(() => 200 | // When we use a non-standard cache we can't build in strict mode 201 | execaSync(findBin('tsc', './tests'), ['index.ts', '--declaration', '--outDir', './build'], { 202 | env: {}, 203 | stdio: ['ignore', process.stdout, process.stderr], 204 | cwd, 205 | }) 206 | ).not.toThrow() 207 | 208 | outputCheck( 209 | execaSync('node', ['./build/index.js'], { 210 | env: { FAUNA_SECRET: load('FAUNA_SECRET') }, 211 | cwd, 212 | }).stdout, 213 | 'tsc' 214 | ) 215 | 216 | // tsup tests (ESM) 217 | await resetBuild(cwd) 218 | 219 | expect(() => 220 | execaSync( 221 | findBin('tsup', './tests'), 222 | [ 223 | 'index.ts', 224 | '--dts', 225 | '--out-dir', 226 | './build', 227 | '--format', 228 | 'esm', 229 | '--no-config', 230 | '--tsconfig', 231 | tsconfig, 232 | ], 233 | { 234 | env: {}, 235 | stdio: ['ignore', process.stdout, process.stderr], 236 | cwd, 237 | } 238 | ) 239 | ).not.toThrow() 240 | 241 | outputCheck( 242 | execaSync('node', ['./build/index.mjs'], { 243 | env: { FAUNA_SECRET: load('FAUNA_SECRET') }, 244 | cwd, 245 | }).stdout, 246 | 'tsup (ESM)' 247 | ) 248 | 249 | // tsup tests (CJS) 250 | await resetBuild(cwd) 251 | 252 | expect(() => 253 | execaSync( 254 | findBin('tsup', './tests'), 255 | [ 256 | 'index.ts', 257 | '--dts', 258 | '--out-dir', 259 | './build', 260 | '--format', 261 | 'cjs', 262 | '--no-config', 263 | '--tsconfig', 264 | tsconfig, 265 | ], 266 | { 267 | env: {}, 268 | stdio: ['ignore', process.stdout, process.stderr], 269 | cwd, 270 | } 271 | ) 272 | ).not.toThrow() 273 | 274 | outputCheck( 275 | execaSync('node', ['./build/index.js'], { 276 | env: { FAUNA_SECRET: load('FAUNA_SECRET') }, 277 | cwd, 278 | }).stdout, 279 | 'tsup (CJS)' 280 | ) 281 | 282 | // esbuild tests (ESM) 283 | await resetBuild(cwd) 284 | 285 | expect(() => 286 | execaSync( 287 | findBin('esbuild', './tests'), 288 | [ 289 | '--sourcemap', 290 | '--outdir=./build', 291 | '--format=esm', 292 | `--tsconfig=${tsconfig}`, 293 | '--out-extension:.js=.mjs', 294 | 'index.ts', 295 | ], 296 | { 297 | env: {}, 298 | stdio: ['ignore', process.stdout, process.stderr], 299 | cwd, 300 | } 301 | ) 302 | ).not.toThrow() 303 | 304 | outputCheck( 305 | execaSync('node', ['./build/index.mjs'], { 306 | env: { FAUNA_SECRET: load('FAUNA_SECRET') }, 307 | cwd, 308 | }).stdout, 309 | 'esbuild (ESM)' 310 | ) 311 | 312 | // esbuild tests (CJS) 313 | await resetBuild(cwd) 314 | 315 | expect(() => 316 | execaSync( 317 | findBin('esbuild', './tests'), 318 | ['--sourcemap', '--outdir=./build', '--format=cjs', `--tsconfig=${tsconfig}`, 'index.ts'], 319 | { 320 | env: {}, 321 | stdio: ['ignore', process.stdout, process.stderr], 322 | cwd, 323 | } 324 | ) 325 | ).not.toThrow() 326 | 327 | outputCheck( 328 | execaSync('node', ['./build/index.js'], { 329 | env: { FAUNA_SECRET: load('FAUNA_SECRET') }, 330 | cwd, 331 | }).stdout, 332 | 'esbuild (CJS)' 333 | ) 334 | }, 240000) 335 | 336 | test(`build an sdk for the 'modularized' example`, async () => { 337 | const root = clone() 338 | const cache = path.join(root, '.cache') 339 | const cwd = path.join(root, 'examples/modularized') 340 | const outputCheck = (await import(`../fixtures/modularized.output.js`)).default 341 | 342 | debug(`Using temporary directory ${cwd}`) 343 | 344 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'build'], { 345 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: undefined }, 346 | cwd, 347 | }) 348 | 349 | debug(`Build of 'modularized' has finished with exit code ${exitCode}`) 350 | 351 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 352 | expect(stdout).toEqual( 353 | expect.not.stringMatching(/error(?!\('SDK requires a secret to be defined.'\))/i) 354 | ) 355 | 356 | expect(removeRetryMessages(stdout)).toEqual( 357 | `The sdk has been saved at ${path.join(cache, 'sdk.ts')}` 358 | ) 359 | 360 | expect(await fs.readFile(path.join(cache, 'sdk.ts'), { encoding: 'utf8' })).toMatchSnapshot() 361 | debug(`The SDK is valid`) 362 | 363 | expect(listFiles(originalCache)).toEqual([].sort()) 364 | expect(listFiles(cache)).toEqual( 365 | [ 366 | 'sdk.d.ts', 367 | 'sdk.d.cts', 368 | 'sdk.mjs', 369 | 'sdk.mjs.map', 370 | 'sdk.cjs', 371 | 'sdk.cjs.map', 372 | 'sdk.ts', 373 | 'tsconfig.json', 374 | ].sort() 375 | ) 376 | 377 | expect(exitCode).toBe(0) 378 | expect(await amountOfCollectionsCreated()).toBe(0) 379 | 380 | execaSync('node', ['../../cli.js', 'deploy'], { 381 | env: { 382 | DEBUG: '', 383 | FAUNA_SECRET: load('FAUNA_SECRET'), 384 | FORCE_COLOR: 0, 385 | NODE_OPTIONS: '--no-warnings', 386 | }, 387 | cwd, 388 | }) 389 | 390 | expect(await amountOfCollectionsCreated()).toBe(2) 391 | 392 | await exportIt(cwd, (sdk) => 393 | expect(Object.keys(sdk)).toEqual([ 394 | 'AllPostsDocument', 395 | 'CreatePostDocument', 396 | 'CreateUserDocument', 397 | 'DeletePostDocument', 398 | 'DeleteUserDocument', 399 | 'FindPostByIdDocument', 400 | 'FindUserByIdDocument', 401 | 'PartialUpdatePostDocument', 402 | 'PartialUpdateUserDocument', 403 | 'SayHelloDocument', 404 | 'UpdatePostDocument', 405 | 'UpdateUserDocument', 406 | 'brainyduck', 407 | 'default', 408 | 'getSdk', 409 | ]) 410 | ) 411 | 412 | await packIt(cwd) 413 | 414 | // ts-node tests 415 | outputCheck( 416 | execaSync(`node`, [`--loader`, `ts-node/esm`, 'index.ts'], { 417 | env: { 418 | FAUNA_SECRET: load('FAUNA_SECRET'), 419 | }, 420 | cwd, 421 | }).stdout, 422 | 'ts-node' 423 | ) 424 | 425 | // tsc tests 426 | await resetBuild(cwd) 427 | 428 | expect(() => 429 | execaSync(findBin('tsc', './tests'), ['--declaration', '--strict'], { 430 | env: {}, 431 | stdio: ['ignore', process.stdout, process.stderr], 432 | cwd, 433 | }) 434 | ).not.toThrow() 435 | 436 | outputCheck( 437 | execaSync('node', ['./build/index.js'], { 438 | env: { 439 | FAUNA_SECRET: load('FAUNA_SECRET'), 440 | }, 441 | cwd, 442 | }).stdout, 443 | 'tsc' 444 | ) 445 | 446 | // tsup tests (ESM) 447 | await resetBuild(cwd) 448 | 449 | expect(() => 450 | execaSync( 451 | findBin('tsup', './tests'), 452 | ['index.ts', '--dts', '--out-dir', './build', '--format', 'esm', '--no-config'], 453 | { 454 | env: {}, 455 | stdio: ['ignore', process.stdout, process.stderr], 456 | cwd, 457 | } 458 | ) 459 | ).not.toThrow() 460 | 461 | outputCheck( 462 | execaSync('node', ['./build/index.js'], { 463 | env: { 464 | FAUNA_SECRET: load('FAUNA_SECRET'), 465 | }, 466 | cwd, 467 | }).stdout, 468 | 'tsup (ESM)' 469 | ) 470 | 471 | // tsup tests (CJS) 472 | await resetBuild(cwd) 473 | 474 | expect(() => 475 | execaSync( 476 | findBin('tsup', './tests'), 477 | ['index.ts', '--dts', '--out-dir', './build', '--format', 'cjs', '--no-config'], 478 | { 479 | env: {}, 480 | stdio: ['ignore', process.stdout, process.stderr], 481 | cwd, 482 | } 483 | ) 484 | ).not.toThrow() 485 | 486 | outputCheck( 487 | execaSync('node', ['./build/index.cjs'], { 488 | env: { 489 | FAUNA_SECRET: load('FAUNA_SECRET'), 490 | }, 491 | cwd, 492 | }).stdout, 493 | 'tsup (CJS)' 494 | ) 495 | 496 | // esbuild tests (ESM) 497 | await resetBuild(cwd) 498 | 499 | expect(() => 500 | execaSync( 501 | findBin('esbuild', './tests'), 502 | ['--sourcemap', '--outdir=./build', '--format=esm', '--target=es6', 'index.ts'], 503 | { 504 | env: {}, 505 | stdio: ['ignore', process.stdout, process.stderr], 506 | cwd, 507 | } 508 | ) 509 | ).not.toThrow() 510 | 511 | outputCheck( 512 | execaSync('node', ['./build/index.js'], { 513 | env: { 514 | FAUNA_SECRET: load('FAUNA_SECRET'), 515 | }, 516 | cwd, 517 | }).stdout, 518 | 'esbuild (ESM)' 519 | ) 520 | 521 | // esbuild tests (CJS) 522 | await resetBuild(cwd) 523 | 524 | expect(() => 525 | execaSync( 526 | findBin('esbuild', './tests'), 527 | [ 528 | '--sourcemap', 529 | '--outdir=./build', 530 | '--format=cjs', 531 | '--target=es6', 532 | '--out-extension:.js=.cjs', 533 | 'index.ts', 534 | ], 535 | { 536 | env: {}, 537 | stdio: ['ignore', process.stdout, process.stderr], 538 | cwd, 539 | } 540 | ) 541 | ).not.toThrow() 542 | 543 | outputCheck( 544 | execaSync('node', ['./build/index.cjs'], { 545 | env: { 546 | FAUNA_SECRET: load('FAUNA_SECRET'), 547 | }, 548 | cwd, 549 | }).stdout, 550 | 'esbuild (CJS)' 551 | ) 552 | }, 240000) 553 | -------------------------------------------------------------------------------- /tests/specs/deploy-functions.js: -------------------------------------------------------------------------------- 1 | import { execaSync } from 'execa' 2 | import { resolve } from 'path' 3 | import { fileURLToPath } from 'url' 4 | import { setupEnvironment, load, amountOfFunctionsCreated } from '../testUtils.js' 5 | 6 | setupEnvironment(`deploy-functions`) 7 | 8 | test('UDF name should match file name', async () => { 9 | const cwd = resolve(fileURLToPath(new URL(`../fixtures`, import.meta.url))) 10 | 11 | try { 12 | execaSync('node', ['../../cli.js', 'deploy-functions', 'unmatched.udf'], { 13 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 14 | cwd, 15 | }) 16 | 17 | fail('it should not reach here') 18 | } catch (e) { 19 | expect(e.message).toEqual( 20 | expect.stringContaining('Error: File name does not match function name: unmatched') 21 | ) 22 | expect(e.exitCode).toBe(1) 23 | } 24 | 25 | expect(await amountOfFunctionsCreated()).toBe(0) 26 | }) 27 | 28 | test('upload simplified and extended UDFs: sayHi, sayHello', async () => { 29 | const cwd = resolve(fileURLToPath(new URL(`../../examples/with-UDF`, import.meta.url))) 30 | 31 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'deploy-functions'], { 32 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 33 | cwd, 34 | }) 35 | 36 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 37 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 38 | 39 | expect(stdout).toBe(`User-defined function(s) created or updated: [ 'sayHello', 'sayHi' ]`) 40 | expect(exitCode).toBe(0) 41 | 42 | expect(await amountOfFunctionsCreated()).toBe(2) 43 | }, 15000) 44 | -------------------------------------------------------------------------------- /tests/specs/deploy-roles.js: -------------------------------------------------------------------------------- 1 | import { execaSync } from 'execa' 2 | import { resolve } from 'path' 3 | import { fileURLToPath } from 'url' 4 | import { 5 | setupEnvironment, 6 | load, 7 | amountOfFunctionsCreated, 8 | amountOfRolesCreated, 9 | } from '../testUtils.js' 10 | 11 | setupEnvironment(`deploy-roles`) 12 | 13 | test('role definitions should not accept simplified formats', async () => { 14 | const cwd = resolve(fileURLToPath(new URL(`../fixtures`, import.meta.url))) 15 | 16 | try { 17 | execaSync('node', ['../../cli.js', 'deploy-roles', 'simplified.role'], { 18 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 19 | cwd, 20 | }) 21 | 22 | fail('it should not reach here') 23 | } catch (error) { 24 | expect(error.message).toEqual( 25 | expect.stringContaining('Error: Incorrect syntax used in role definition') 26 | ) 27 | expect(error.exitCode).toBe(1) 28 | } 29 | 30 | expect(await amountOfRolesCreated()).toBe(0) 31 | }) 32 | 33 | test('role name should match file name', async () => { 34 | const cwd = resolve(fileURLToPath(new URL(`../fixtures`, import.meta.url))) 35 | 36 | try { 37 | execaSync('node', ['../../cli.js', 'deploy-roles', 'unmatched.role'], { 38 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 39 | cwd, 40 | }) 41 | 42 | fail('it should not reach here') 43 | } catch (error) { 44 | expect(error.message).toEqual( 45 | expect.stringContaining('Error: File name does not match role name: unmatched') 46 | ) 47 | expect(error.exitCode).toBe(1) 48 | } 49 | 50 | expect(await amountOfRolesCreated()).toBe(0) 51 | }) 52 | 53 | test('upload all roles: publicAccess', async () => { 54 | const cwd = resolve(fileURLToPath(new URL(`../../examples/with-UDF`, import.meta.url))) 55 | 56 | // the referred functions needs to be defined first 57 | const functions = execaSync('node', ['../../cli.js', 'deploy-functions'], { 58 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 59 | cwd, 60 | }) 61 | 62 | expect(functions.stderr).toEqual(expect.not.stringMatching(/error/i)) 63 | expect(functions.stdout).toEqual(expect.not.stringMatching(/error/i)) 64 | expect(functions.stdout).toBe( 65 | `User-defined function(s) created or updated: [ 'sayHello', 'sayHi' ]` 66 | ) 67 | 68 | expect(await amountOfFunctionsCreated()).toBe(2) 69 | 70 | // ... and only then their access permission can be defined 71 | const roles = execaSync('node', ['../../cli.js', 'deploy-roles'], { 72 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 73 | cwd, 74 | }) 75 | 76 | expect(roles.stderr).toEqual(expect.not.stringMatching(/error/i)) 77 | expect(roles.stdout).toEqual(expect.not.stringMatching(/error/i)) 78 | 79 | expect(roles.stdout).toBe(`User-defined role(s) created or updated: [ 'publicAccess' ]`) 80 | expect(roles.exitCode).toBe(0) 81 | 82 | expect(await amountOfRolesCreated()).toBe(1) 83 | }, 15000) 84 | -------------------------------------------------------------------------------- /tests/specs/deploy-schemas.js: -------------------------------------------------------------------------------- 1 | import { execaSync } from 'execa' 2 | import { resolve } from 'path' 3 | import { fileURLToPath } from 'url' 4 | import { 5 | setupEnvironment, 6 | load, 7 | amountOfCollectionsCreated, 8 | removeRetryMessages, 9 | } from '../testUtils.js' 10 | 11 | setupEnvironment(`deploy-schemas`) 12 | 13 | test('push a basic schema', async () => { 14 | const cwd = resolve(fileURLToPath(new URL(`../../examples/basic`, import.meta.url))) 15 | 16 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'deploy-schemas'], { 17 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 18 | cwd, 19 | }) 20 | 21 | const mergedSchema = `The resulting merged schema: 22 | \ttype User { 23 | \t username: String! @unique 24 | \t} 25 | \t 26 | \ttype Query { 27 | \t allUsers: [User!] 28 | \t}` 29 | 30 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 31 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 32 | 33 | expect(stderr).toEqual(expect.stringContaining(mergedSchema)) 34 | expect(removeRetryMessages(stdout).split('\n')[0]).toBe(`Schema imported successfully.`) 35 | 36 | expect(exitCode).toBe(0) 37 | 38 | expect(await amountOfCollectionsCreated()).toBe(1) 39 | }, 240000) 40 | 41 | test('push a modular schema', () => { 42 | const cwd = resolve(fileURLToPath(new URL(`../../examples/modularized`, import.meta.url))) 43 | 44 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'deploy-schemas'], { 45 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 46 | cwd, 47 | }) 48 | 49 | const mergedSchema = `The resulting merged schema: 50 | \ttype Query { 51 | \t allPosts: [Post!] 52 | \t 53 | \t 54 | \t sayHello(name: String!): String! @resolver(name: "sayHello") 55 | \t 56 | \t} 57 | \t 58 | \ttype User { 59 | \t name: String! 60 | \t} 61 | \t 62 | \t 63 | \ttype Post { 64 | \t title: String! 65 | \t content: String! 66 | \t author: User! 67 | \t}` 68 | 69 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 70 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 71 | 72 | expect(stderr).toEqual(expect.stringContaining(mergedSchema)) 73 | expect(removeRetryMessages(stdout).split('\n')[0]).toBe(`Schema imported successfully.`) 74 | 75 | expect(exitCode).toBe(0) 76 | }, 240000) 77 | -------------------------------------------------------------------------------- /tests/specs/deploy.js: -------------------------------------------------------------------------------- 1 | import { execaSync } from 'execa' 2 | import { resolve } from 'path' 3 | import { fileURLToPath } from 'url' 4 | import { 5 | setupEnvironment, 6 | load, 7 | amountOfCollectionsCreated, 8 | amountOfRolesCreated, 9 | amountOfFunctionsCreated, 10 | removeRetryMessages, 11 | } from '../testUtils.js' 12 | 13 | setupEnvironment(`deploy`) 14 | 15 | test(`complete all 'deploy' operations for the 'basic' example`, async () => { 16 | const cwd = resolve(fileURLToPath(new URL(`../../examples/basic`, import.meta.url))) 17 | 18 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'deploy'], { 19 | env: { 20 | DEBUG: '', 21 | FAUNA_SECRET: load('FAUNA_SECRET'), 22 | FORCE_COLOR: 0, 23 | NODE_OPTIONS: '--no-warnings', 24 | }, 25 | cwd, 26 | }) 27 | 28 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 29 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 30 | 31 | expect(new Set(stderr.split('\n').sort())).toEqual( 32 | new Set( 33 | [ 34 | '- Deploying functions...', 35 | '- Deploying indexes...', 36 | '- Deploying roles...', 37 | '- Deploying schemas...', 38 | 'ℹ No functions to deploy', 39 | 'ℹ No indexes to deploy', 40 | 'ℹ No roles to deploy', 41 | '✔ schemas have been deployed!', 42 | ].sort() 43 | ) 44 | ) 45 | 46 | expect(new Set(removeRetryMessages(stdout).split('\n'))).toEqual( 47 | new Set([ 48 | '', 49 | `The following types are about to be deployed: \[ 'schemas', 'indexes', 'roles', 'functions' \]`, 50 | `schemas: Schema imported successfully.`, 51 | `Use the following HTTP header to connect to the FaunaDB GraphQL API:`, 52 | expect.stringMatching(/{ "Authorization": "Bearer [\S^"]+" } /), 53 | `All done! All deployments have been successful 🦆`, 54 | ]) 55 | ) 56 | 57 | expect(exitCode).toBe(0) 58 | 59 | expect(await amountOfRolesCreated()).toBe(0) 60 | expect(await amountOfFunctionsCreated()).toBe(0) 61 | expect(await amountOfCollectionsCreated()).toBe(1) 62 | }, 240000) 63 | 64 | test(`complete all 'deploy' operations for the 'modularized' example`, async () => { 65 | const cwd = resolve(fileURLToPath(new URL(`../../examples/modularized`, import.meta.url))) 66 | 67 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'deploy'], { 68 | env: { 69 | DEBUG: '', 70 | FAUNA_SECRET: load('FAUNA_SECRET'), 71 | FORCE_COLOR: 0, 72 | NODE_OPTIONS: '--no-warnings', 73 | }, 74 | cwd, 75 | }) 76 | 77 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 78 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 79 | 80 | expect(new Set(stderr.split('\n').sort())).toEqual( 81 | new Set( 82 | [ 83 | '- Deploying functions...', 84 | '- Deploying indexes...', 85 | '- Deploying roles...', 86 | '- Deploying schemas...', 87 | 'ℹ No indexes to deploy', 88 | 'ℹ No roles to deploy', 89 | '✔ functions have been deployed!', 90 | '✔ schemas have been deployed!', 91 | ].sort() 92 | ) 93 | ) 94 | 95 | expect(new Set(removeRetryMessages(stdout).split('\n'))).toEqual( 96 | new Set([ 97 | `The following types are about to be deployed: \[ 'schemas', 'indexes', 'roles', 'functions' \]`, 98 | `schemas: Schema imported successfully.`, 99 | `Use the following HTTP header to connect to the FaunaDB GraphQL API:`, 100 | expect.stringMatching(/{ "Authorization": "Bearer [\S^"]+" } /), 101 | '', 102 | `functions: [ 'sayHello' ] `, 103 | `All done! All deployments have been successful 🦆`, 104 | ]) 105 | ) 106 | 107 | expect(exitCode).toBe(0) 108 | 109 | expect(await amountOfRolesCreated()).toBe(0) 110 | expect(await amountOfFunctionsCreated()).toBe(1) 111 | expect(await amountOfCollectionsCreated()).toBe(2) 112 | }, 240000) 113 | 114 | test(`complete all 'deploy' operations for the 'with-UDF' example`, async () => { 115 | const cwd = resolve(fileURLToPath(new URL(`../../examples/with-UDF`, import.meta.url))) 116 | 117 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'deploy'], { 118 | env: { 119 | DEBUG: '', 120 | FAUNA_SECRET: load('FAUNA_SECRET'), 121 | FORCE_COLOR: 0, 122 | NODE_OPTIONS: '--no-warnings', 123 | }, 124 | cwd, 125 | }) 126 | 127 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 128 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 129 | 130 | expect(new Set(stderr.split('\n').sort())).toEqual( 131 | new Set( 132 | [ 133 | '- Deploying functions...', 134 | '- Deploying indexes...', 135 | '- Deploying roles...', 136 | '- Deploying schemas...', 137 | 'ℹ No indexes to deploy', 138 | '✔ functions have been deployed!', 139 | '✔ roles have been deployed!', 140 | '✔ schemas have been deployed!', 141 | ].sort() 142 | ) 143 | ) 144 | 145 | expect(new Set(removeRetryMessages(stdout).split('\n'))).toEqual( 146 | new Set([ 147 | '', 148 | `The following types are about to be deployed: \[ 'schemas', 'indexes', 'roles', 'functions' \]`, 149 | `schemas: Schema imported successfully.`, 150 | `Use the following HTTP header to connect to the FaunaDB GraphQL API:`, 151 | expect.stringMatching(/{ "Authorization": "Bearer [\S^"]+" } /), 152 | "functions: [ 'sayHello', 'sayHi' ] ", 153 | "roles: [ 'publicAccess' ] ", 154 | `All done! All deployments have been successful 🦆`, 155 | ]) 156 | ) 157 | 158 | expect(exitCode).toBe(0) 159 | 160 | expect(await amountOfRolesCreated()).toBe(1) 161 | expect(await amountOfFunctionsCreated()).toBe(2) 162 | expect(await amountOfCollectionsCreated()).toBe(0) 163 | }, 240000) 164 | -------------------------------------------------------------------------------- /tests/specs/dev.js: -------------------------------------------------------------------------------- 1 | import { execaSync } from 'execa' 2 | import { resolve } from 'path' 3 | import { fileURLToPath } from 'url' 4 | import { 5 | setupEnvironment, 6 | load, 7 | amountOfCollectionsCreated, 8 | amountOfRolesCreated, 9 | amountOfFunctionsCreated, 10 | removeRetryMessages, 11 | } from '../testUtils.js' 12 | 13 | setupEnvironment(`dev`) 14 | 15 | test(`complete all 'dev' operations for the 'basic' example (default cmd)`, async () => { 16 | const cwd = resolve(fileURLToPath(new URL(`../../examples/basic`, import.meta.url))) 17 | 18 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', '--no-watch'], { 19 | env: { 20 | DEBUG: '', 21 | FAUNA_SECRET: load('FAUNA_SECRET'), 22 | FORCE_COLOR: 0, 23 | NODE_OPTIONS: '--no-warnings', 24 | }, 25 | cwd, 26 | }) 27 | 28 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 29 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 30 | 31 | expect(new Set(stderr.split('\n').sort())).toEqual( 32 | new Set( 33 | ['- Processing Schema.graphql [Schema]', '✔ Processed Schema.graphql [Schema]', ''].sort() 34 | ) 35 | ) 36 | 37 | expect(stdout).toEqual('All operations complete') 38 | expect(exitCode).toBe(0) 39 | 40 | expect(await amountOfRolesCreated()).toBe(0) 41 | expect(await amountOfFunctionsCreated()).toBe(0) 42 | expect(await amountOfCollectionsCreated()).toBe(1) 43 | }, 240000) 44 | 45 | test(`complete all 'dev' operations for the 'modularized' example`, async () => { 46 | // It intentionally runs this test from a different directory in order to test directory changing. 47 | const cwd = resolve(fileURLToPath(new URL(`../../examples`, import.meta.url))) 48 | 49 | const { stdout, stderr, exitCode } = execaSync( 50 | 'node', 51 | ['../cli.js', 'dev', './modularized', '--no-watch'], 52 | { 53 | env: { 54 | DEBUG: '', 55 | FAUNA_SECRET: load('FAUNA_SECRET'), 56 | FORCE_COLOR: 0, 57 | NODE_OPTIONS: '--no-warnings', 58 | }, 59 | cwd, 60 | } 61 | ) 62 | 63 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 64 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 65 | 66 | expect(new Set(stderr.split('\n').sort())).toEqual( 67 | new Set( 68 | [ 69 | '- Processing accounts/sayHello.udf [UDF]', 70 | '✔ Processed accounts/sayHello.udf [UDF]', 71 | '- Processing Query.gql, accounts/User.gql, blog/Post.gql [Schema]', 72 | '✔ Processed Query.gql, accounts/User.gql, blog/Post.gql [Schema]', 73 | '', 74 | ].sort() 75 | ) 76 | ) 77 | 78 | expect(removeRetryMessages(stdout)).toEqual('All operations complete') 79 | expect(exitCode).toBe(0) 80 | 81 | expect(await amountOfRolesCreated()).toBe(0) 82 | expect(await amountOfFunctionsCreated()).toBe(1) 83 | expect(await amountOfCollectionsCreated()).toBe(2) 84 | }, 240000) 85 | 86 | test(`complete all 'dev' operations for the 'with-UDF' example`, async () => { 87 | const cwd = resolve(fileURLToPath(new URL(`../../examples/with-UDF`, import.meta.url))) 88 | 89 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'dev', '--no-watch'], { 90 | env: { 91 | DEBUG: '', 92 | FAUNA_SECRET: load('FAUNA_SECRET'), 93 | FORCE_COLOR: 0, 94 | NODE_OPTIONS: '--no-warnings', 95 | }, 96 | cwd, 97 | }) 98 | 99 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 100 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 101 | 102 | expect(new Set(stderr.split('\n').sort())).toEqual( 103 | new Set( 104 | [ 105 | '- Processing Schema.graphql [Schema]', 106 | '✔ Processed Schema.graphql [Schema]', 107 | '- Processing queries.gql [Document]', 108 | '✔ Processed queries.gql [Document]', 109 | '- Processing sayHello.udf [UDF]', 110 | '✔ Processed sayHello.udf [UDF]', 111 | '- Processing sayHi.udf [UDF]', 112 | '✔ Processed sayHi.udf [UDF]', 113 | '- Processing publicAccess.role [UDR]', 114 | '✔ Processed publicAccess.role [UDR]', 115 | '', 116 | ].sort() 117 | ) 118 | ) 119 | 120 | expect(removeRetryMessages(stdout)).toEqual('All operations complete') 121 | expect(exitCode).toBe(0) 122 | 123 | expect(await amountOfRolesCreated()).toBe(1) 124 | expect(await amountOfFunctionsCreated()).toBe(2) 125 | expect(await amountOfCollectionsCreated()).toBe(0) 126 | }, 240000) 127 | -------------------------------------------------------------------------------- /tests/specs/examples.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import _debug from 'debug' 4 | import { resolve } from 'path' 5 | import { execaSync } from 'execa' 6 | import { fileURLToPath } from 'url' 7 | import { temporaryDirectory } from 'tempy' 8 | import { setupEnvironment, load, clone } from '../testUtils.js' 9 | 10 | const debug = _debug('brainyduck:test:examples') 11 | 12 | setupEnvironment(`examples`) 13 | 14 | // const examples = ( 15 | // await fs.readdir(fileURLToPath(new URL(`../../examples`, import.meta.url)), { 16 | // encoding: 'utf-8', 17 | // withFileTypes: true, 18 | // }) 19 | // ) 20 | // .filter((dirent) => dirent.isDirectory()) 21 | // .map((dirent) => dirent.name) 22 | // TODO: standardize the way examples are built and run, then test them all from here 23 | const examples = ['basic', 'basic-esbuild-bundle', 'modularized', 'modularized-esbuild-bundle'] 24 | 25 | console.log(`Testing the following examples:`, examples) 26 | 27 | for (const name of examples) { 28 | test(`build and run example '${name}'`, async () => { 29 | const root = clone() 30 | const cwd = path.join(root, `examples`, name) 31 | const outputCheck = (await import(`../fixtures/${name}.output.js`)).default 32 | 33 | debug(`Using temporary directory ${cwd}`) 34 | 35 | const { scripts } = JSON.parse(await fs.readFile(path.join(cwd, 'package.json'))) 36 | 37 | if (scripts.build) { 38 | const build = execaSync('npm', ['run', '--silent', 'build'], { 39 | env: { DEBUG: 'brainyduck:*' }, 40 | cwd, 41 | }) 42 | 43 | debug(`Build of '${name}' has finished with exit code ${build.exitCode}`) 44 | 45 | expect(build.stderr).toEqual(expect.not.stringMatching(/error/i)) 46 | expect(build.stdout).toEqual( 47 | expect.not.stringMatching(/error(?!\('SDK requires a secret to be defined.'\))/i) 48 | ) 49 | expect(build.exitCode).toBe(0) 50 | debug(`Build of '${name}' has completed successfully`) 51 | } 52 | 53 | if (scripts.deploy) { 54 | const deploy = execaSync('npm', ['run', '--silent', 'deploy'], { 55 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 56 | cwd, 57 | }) 58 | 59 | expect(deploy.stderr).toEqual(expect.not.stringMatching(/error/i)) 60 | expect(deploy.stdout).toEqual( 61 | expect.not.stringMatching(/error(?!\('SDK requires a secret to be defined.'\))/i) 62 | ) 63 | expect(deploy.exitCode).toBe(0) 64 | debug(`Deployment of '${name}' has completed successfully`) 65 | } 66 | 67 | if (scripts.dev) { 68 | const dev = execaSync('npm', ['run', '--silent', 'dev', '--', '--no-watch'], { 69 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 70 | cwd, 71 | }) 72 | 73 | expect(dev.stderr).toEqual(expect.not.stringMatching(/error/i)) 74 | expect(dev.stdout).toEqual( 75 | expect.not.stringMatching(/error(?!\('SDK requires a secret to be defined.'\))/i) 76 | ) 77 | expect(dev.exitCode).toBe(0) 78 | debug(`Dev preparation of '${name}' has completed successfully`) 79 | } 80 | 81 | const run = execaSync('npm', ['run', '--silent', 'start'], { 82 | env: { 83 | DEBUG: 'brainyduck:*', 84 | FAUNA_SECRET: load('FAUNA_SECRET'), 85 | TS_NODE_TRANSPILE_ONLY: 'true', 86 | }, 87 | cwd, 88 | }) 89 | 90 | expect(run.stderr).toEqual(expect.not.stringMatching(/error/i)) 91 | expect(run.stdout).toEqual( 92 | expect.not.stringMatching(/error(?!\('SDK requires a secret to be defined.'\))/i) 93 | ) 94 | expect(run.exitCode).toBe(0) 95 | 96 | outputCheck(run.stdout, 'npm run start') 97 | }, 240000) 98 | } 99 | -------------------------------------------------------------------------------- /tests/specs/pull-schema.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { execaSync } from 'execa' 3 | import { fileURLToPath } from 'url' 4 | import { importSchema } from 'brainyduck/utils' 5 | import { setupEnvironment, load, amountOfCollectionsCreated } from '../testUtils.js' 6 | 7 | setupEnvironment(`pull-schema`) 8 | 9 | test('fails on empty schema', async () => { 10 | try { 11 | execaSync('node', ['../../cli.js', 'pull-schema'], { 12 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 13 | cwd: path.dirname(fileURLToPath(import.meta.url)), 14 | }) 15 | 16 | fail('it should not reach here') 17 | } catch (error) { 18 | expect(error.message).toEqual( 19 | expect.stringContaining('Error: Invalid schema retrieved: missing type Query') 20 | ) 21 | expect(error.exitCode).toBe(1) 22 | } 23 | }, 240000) 24 | 25 | test('fetch schema from fauna', async () => { 26 | const schema = ` 27 | type User { 28 | username: String! @unique 29 | }` 30 | 31 | // The schema needs to be pre-populated/reset before we can pull it again 32 | await importSchema(schema, { override: true, secret: load('FAUNA_SECRET') }) 33 | 34 | const { stdout, stderr, exitCode } = execaSync('node', ['../../cli.js', 'pull-schema'], { 35 | env: { DEBUG: 'brainyduck:*', FAUNA_SECRET: load('FAUNA_SECRET') }, 36 | cwd: path.dirname(fileURLToPath(import.meta.url)), 37 | }) 38 | 39 | const expectedSchema = `directive @embedded on OBJECT 40 | 41 | directive @collection(name: String!) on OBJECT 42 | 43 | directive @index(name: String!) on FIELD_DEFINITION 44 | 45 | directive @resolver(name: String, paginated: Boolean! = false) on FIELD_DEFINITION 46 | 47 | directive @relation(name: String) on FIELD_DEFINITION 48 | 49 | directive @unique(index: String) on FIELD_DEFINITION 50 | 51 | schema { 52 | query: Query 53 | mutation: Mutation 54 | } 55 | 56 | scalar Date 57 | 58 | type Mutation { 59 | """Create a new document in the collection of 'User'""" 60 | createUser( 61 | """'User' input values""" 62 | data: UserInput! 63 | ): User! 64 | """Update an existing document in the collection of 'User'""" 65 | updateUser( 66 | """The 'User' document's ID""" 67 | id: ID! 68 | """'User' input values""" 69 | data: UserInput! 70 | ): User 71 | """Delete an existing document in the collection of 'User'""" 72 | deleteUser( 73 | """The 'User' document's ID""" 74 | id: ID! 75 | ): User 76 | """ 77 | Partially updates an existing document in the collection of 'User'. It only modifies the values that are specified in the arguments. During execution, it verifies that required fields are not set to 'null'. 78 | """ 79 | partialUpdateUser( 80 | """The 'User' document's ID""" 81 | id: ID! 82 | """'User' input values""" 83 | data: PartialUpdateUserInput! 84 | ): User 85 | } 86 | 87 | """'User' input values""" 88 | input PartialUpdateUserInput { 89 | username: String 90 | } 91 | 92 | scalar Time 93 | 94 | """'User' input values""" 95 | input UserInput { 96 | username: String! 97 | } 98 | 99 | type Query { 100 | """Find a document from the collection of 'User' by its id.""" 101 | findUserByID( 102 | """The 'User' document's ID""" 103 | id: ID! 104 | ): User 105 | } 106 | 107 | type User { 108 | """The document's ID.""" 109 | _id: ID! 110 | """The document's timestamp.""" 111 | _ts: Long! 112 | username: String! 113 | } 114 | 115 | """ 116 | The \`Long\` scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1. 117 | """ 118 | scalar Long` 119 | 120 | expect(stderr).toEqual(expect.not.stringMatching(/error/i)) 121 | expect(stdout).toEqual(expect.not.stringMatching(/error/i)) 122 | 123 | expect( 124 | stdout 125 | .split('\n') 126 | .filter((x) => !x.startsWith('@graphql-tools/load')) 127 | .join('\n') 128 | ).toEqual(expectedSchema) 129 | expect(exitCode).toBe(0) 130 | 131 | expect(await amountOfCollectionsCreated()).toBe(1) 132 | }, 240000) 133 | -------------------------------------------------------------------------------- /tests/storage.js: -------------------------------------------------------------------------------- 1 | const dataStore = {} 2 | 3 | const loadStorage = (suiteLevel = false) => { 4 | const { currentTestName, testPath } = expect.getState() 5 | const suiteStore = (dataStore[testPath] = dataStore[testPath] || {}) 6 | return !suiteLevel && currentTestName 7 | ? (suiteStore[currentTestName] = suiteStore[currentTestName] || {}) 8 | : suiteStore 9 | } 10 | 11 | export const store = (key, value, suiteLevel) => { 12 | const localStore = loadStorage(suiteLevel) 13 | 14 | localStore[key] = value 15 | } 16 | 17 | export const load = (key, suiteLevel = false) => { 18 | const localStore = loadStorage(suiteLevel) 19 | 20 | return localStore[key] 21 | } 22 | -------------------------------------------------------------------------------- /tests/testUtils.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'node:path' 3 | import _debug from 'debug' 4 | import faunadb from 'faunadb' 5 | import { execaSync } from 'execa' 6 | import { paramCase } from 'param-case' 7 | import { fileURLToPath } from 'node:url' 8 | import { temporaryDirectory } from 'tempy' 9 | import { faunaClient, runFQL } from '../utils.js' 10 | import { load, store } from './storage.js' 11 | export { load } 12 | 13 | const { query: q } = faunadb 14 | const debug = _debug('brainyduck:test') 15 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 16 | 17 | export const reset = (types) => 18 | execaSync('node', ['../cli.js', 'reset', types], { 19 | env: { FAUNA_SECRET: load('FAUNA_SECRET'), BRAINYDUCK_FORCE: 1 }, 20 | }) 21 | 22 | export const createDatabase = (name, secret) => 23 | runFQL( 24 | `CreateKey({ 25 | database: Select('ref', CreateDatabase({ name: '${name}' })), 26 | role: 'admin', 27 | })`, 28 | secret 29 | ) 30 | 31 | export const deleteDatabase = (name, secret) => runFQL(`Delete(Database('${name}'))`, secret) 32 | 33 | export const setupEnvironment = (name, options = {}) => { 34 | const timestamp = +new Date() 35 | const start = options.beforeAll ? beforeAll : beforeEach 36 | const end = options.beforeAll ? afterAll : afterEach 37 | let dbName = `${timestamp}_${name}` 38 | 39 | start(() => { 40 | const testName = expect.getState().currentTestName 41 | 42 | if (testName) { 43 | dbName = `${dbName}_${paramCase(testName)}` 44 | } 45 | 46 | const secret = createDatabase(dbName, process.env.TESTS_SECRET).secret 47 | store('FAUNA_SECRET', secret) 48 | debug(`Using database ${timestamp}_${name}`) 49 | }) 50 | 51 | end(() => { 52 | deleteDatabase(dbName, process.env.TESTS_SECRET) 53 | debug(`Deleted database ${timestamp}_${name}`) 54 | }) 55 | } 56 | 57 | const query = async (expression) => { 58 | const client = faunaClient({ secret: load('FAUNA_SECRET') }) 59 | const output = await client.query(expression) 60 | 61 | await client.close() 62 | return output 63 | } 64 | 65 | export const amountOfFunctionsCreated = () => query(q.Count(q.Functions())) 66 | 67 | export const amountOfRolesCreated = () => query(q.Count(q.Roles())) 68 | 69 | export const amountOfCollectionsCreated = () => query(q.Count(q.Collections())) 70 | 71 | export const listDirectory = (directory, filter) => 72 | fs.existsSync(directory) 73 | ? fs 74 | .readdirSync(directory, { withFileTypes: true }) 75 | .filter(filter) 76 | .map((x) => x.name) 77 | : [] 78 | 79 | export const listFiles = (directory) => listDirectory(directory, (dirent) => dirent.isFile()) 80 | export const listSubfolders = (directory) => 81 | listDirectory(directory, (dirent) => dirent.isDirectory()) 82 | export const listSymbolicLinks = (directory) => 83 | listDirectory(directory, (dirent) => dirent.isSymbolicLink()) 84 | 85 | export const removeRetryMessages = (stdout) => 86 | stdout 87 | .split('\n') 88 | .filter( 89 | (x) => 90 | ![ 91 | `Wiped data still found in fauna's cache.`, 92 | `Cooling down for 30s...`, 93 | `Retrying now...`, 94 | ].includes(x) 95 | ) 96 | .join('\n') 97 | 98 | export const clone = () => { 99 | const tempDir = temporaryDirectory({ prefix: 'brainyduck_' }) 100 | const brainyduck = path.join(__dirname, '..') 101 | debug(`Cloning installation into ${tempDir}`) 102 | 103 | fs.copySync(brainyduck, tempDir, { 104 | filter: (src) => 105 | (!src.includes('/.') || src.includes('/.npmrc')) && 106 | !src.includes('node_modules') && 107 | !src.includes('/build/'), 108 | }) 109 | 110 | execaSync('pnpm', ['install'], { cwd: tempDir }) 111 | return tempDir 112 | } 113 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "Node", 4 | "allowSyntheticDefaultImports": true, 5 | "resolveJsonModule": false, 6 | "preserveSymlinks": true, 7 | "skipLibCheck": true, 8 | "outDir": ".cache/", 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": true, 12 | "inlineSources": true, 13 | "isolatedModules": true 14 | }, 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | format: ['esm', 'cjs'], 5 | sourcemap: true, 6 | dts: true, 7 | watch: false, 8 | outExtension: ({ format }) => ({ 9 | js: `.${format === 'esm' ? 'mjs' : format}`, 10 | }), 11 | }) 12 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import debug from 'debug' 4 | import faunadb from 'faunadb' 5 | import readline from 'node:readline' 6 | import { globby } from 'globby' 7 | import { execaSync } from 'execa' 8 | import { performance } from 'node:perf_hooks' 9 | import { fileURLToPath } from 'node:url' 10 | import { temporaryFile } from 'tempy' 11 | import fetch, { Headers } from './fetch-ponyfill.cjs' 12 | import { inspect, promisify } from 'node:util' 13 | 14 | export { default as locateCache } from './locateCache.cjs' 15 | 16 | // Default file extension patterns 17 | export const patterns = { 18 | TS: '**/*.(ts|tsx)', 19 | UDF: '**/*.udf', 20 | SCHEMA: '**/[A-Z]*.(gql|graphql)', 21 | INDEX: '**/*.index', 22 | UDR: '**/*.role', 23 | DOCUMENTS: '**/[a-z]*.(gql|graphql)', 24 | } 25 | 26 | const { Client } = faunadb 27 | const errors = { 28 | CACHE_TIMEOUT: 29 | 'Value is cached. Please wait at least 60 seconds after creating or renaming a collection or index before reusing its name.', 30 | } 31 | 32 | export const ignored = process.env.BRAINYDUCK_IGNORE 33 | ? process.env.BRAINYDUCK_IGNORE.split(',') 34 | : ['**/node_modules/**', '**/.git/**'] 35 | 36 | export const graphqlEndpoint = (() => { 37 | const { 38 | FAUNA_GRAPHQL_DOMAIN = 'graphql.fauna.com', 39 | FAUNA_SCHEME = 'https', 40 | FAUNA_GRAPHQL_PORT, 41 | } = process.env 42 | 43 | const base = `${FAUNA_SCHEME}://${FAUNA_GRAPHQL_DOMAIN}${ 44 | FAUNA_GRAPHQL_PORT ? `:${FAUNA_GRAPHQL_PORT}` : `` 45 | }` 46 | return { 47 | server: `${base}/graphql`, 48 | import: `${base}/import`, 49 | puke: `https://duckpuke.brainy.sh/`, 50 | } 51 | })() 52 | 53 | export const findBin = (name, relative = '.') => { 54 | const local = fileURLToPath( 55 | new URL(path.join(relative, `./node_modules/.bin`, name), import.meta.url) 56 | ) 57 | 58 | if (fs.existsSync(local)) { 59 | return local 60 | } 61 | 62 | if (path.resolve(relative) !== path.resolve('/')) { 63 | return findBin(name, path.join(relative, '..')) 64 | } 65 | 66 | throw new Error(`Binary for '${name}' could not be found.`) 67 | } 68 | 69 | export const question = (...args) => { 70 | const rl = readline.createInterface({ 71 | input: process.stdin, 72 | output: process.stdout, 73 | }) 74 | 75 | const q = promisify(rl.question).bind(rl)(...args) 76 | return q.finally(() => rl.close()) 77 | } 78 | 79 | export const loadSecret = () => { 80 | const secret = process.env.FAUNA_SECRET 81 | 82 | if (!secret) { 83 | console.error( 84 | `The fauna secret is missing! 🤷‍🥚\n\nPlease define a secret to get started. 💁🐣\n ↳ read more on https://github.com/zvictor/brainyduck/wiki/Fauna-secret\n` 85 | ) 86 | 87 | throw new Error(`missing fauna's secret`) 88 | } 89 | 90 | return secret 91 | } 92 | 93 | let _faunaClient 94 | 95 | export const faunaClient = (options) => { 96 | const { FAUNA_DOMAIN, FAUNA_SCHEME, FAUNA_PORT } = process.env 97 | 98 | if (!options && _faunaClient && !_faunaClient._http._adapter._closed) { 99 | return _faunaClient 100 | } 101 | 102 | options = options || {} 103 | 104 | if (!options.secret) { 105 | options.secret = loadSecret() 106 | } 107 | 108 | if (!options.domain && FAUNA_DOMAIN) { 109 | options.domain = process.env.FAUNA_DOMAIN 110 | } 111 | 112 | if (!options.scheme && FAUNA_SCHEME) { 113 | options.scheme = process.env.FAUNA_SCHEME 114 | } 115 | 116 | if (!options.port && FAUNA_PORT) { 117 | options.port = process.env.FAUNA_PORT 118 | } 119 | 120 | _faunaClient = new Client(options) 121 | return _faunaClient 122 | } 123 | 124 | export const patternMatch = async (pattern, cwd = process.cwd()) => 125 | (await globby(pattern, { cwd, ignore: ignored })).map((x) => 126 | x.startsWith('/') ? x : path.join(cwd, x) 127 | ) 128 | 129 | export const runFQL = (query, secret) => { 130 | debug('brainyduck:runFQL')(`Executing query:\n${query}`) 131 | const { FAUNA_DOMAIN, FAUNA_PORT, FAUNA_SCHEME } = process.env 132 | 133 | const tmpFile = temporaryFile() 134 | fs.writeFileSync(tmpFile, query, 'utf8') 135 | 136 | const args = [`eval`, `--secret=${secret || loadSecret()}`, `--file=${tmpFile}`] 137 | 138 | if (FAUNA_DOMAIN) { 139 | args.push('--domain') 140 | args.push(FAUNA_DOMAIN) 141 | } 142 | 143 | if (FAUNA_PORT) { 144 | args.push('--port') 145 | args.push(FAUNA_PORT) 146 | } 147 | 148 | if (FAUNA_SCHEME) { 149 | args.push('--scheme') 150 | args.push(FAUNA_SCHEME) 151 | } 152 | 153 | const { stdout, stderr, exitCode } = execaSync(findBin(`fauna`), args, { 154 | cwd: path.dirname(fileURLToPath(import.meta.url)), 155 | }) 156 | 157 | if (exitCode) { 158 | debug('brainyduck:runFQL')(`The query has failed to execute.`) 159 | console.error(stderr) 160 | 161 | throw new Error(`runFQL failed with exit code ${exitCode}`) 162 | } 163 | 164 | debug('brainyduck:runFQL')(`The query has been executed`) 165 | return JSON.parse(stdout) 166 | } 167 | 168 | export const importSchema = async (schema, { secret, override, puke } = {}) => { 169 | const url = puke ? graphqlEndpoint.puke : graphqlEndpoint.import 170 | 171 | debug('brainyduck:importSchema')( 172 | `Pushing the schema to ${url} in ${override ? 'OVERRIDE' : 'NORMAL'} mode` 173 | ) 174 | 175 | const t0 = performance.now() 176 | const response = await fetch(`${url}${override ? '?mode=override' : ''}`, { 177 | method: 'POST', 178 | body: schema, 179 | headers: puke 180 | ? {} 181 | : new Headers({ 182 | Authorization: `Bearer ${secret || loadSecret()}`, 183 | }), 184 | }) 185 | debug('brainyduck:importSchema')( 186 | `The call to remote took ${performance.now() - t0} milliseconds.` 187 | ) 188 | 189 | const message = await response.text() 190 | if (response.status !== 200) { 191 | if (!message.endsWith(errors.CACHE_TIMEOUT)) { 192 | throw new Error(message) 193 | } 194 | 195 | console.log(`Wiped data still found in fauna's cache.\nCooling down for 30s...`) 196 | await sleep(30000) 197 | console.log(`Retrying now...`) 198 | 199 | return await importSchema(schema, { override, puke }) 200 | } 201 | 202 | debug('brainyduck:importSchema')(`The returned schema is:`, message) 203 | 204 | return message 205 | } 206 | 207 | const _representData = (data) => { 208 | if (typeof data.map === 'function') { 209 | return data.map(_representData) 210 | } 211 | 212 | const deeper = data && (data.name || data.ref || data['@ref']) 213 | 214 | if (deeper) { 215 | return _representData(deeper) 216 | } 217 | 218 | return data 219 | } 220 | 221 | export const representData = (data) => 222 | inspect(_representData(data), { 223 | depth: 5, 224 | colors: process.stdout.hasColors ? process.stdout.hasColors() : false, 225 | }) 226 | 227 | export const sleep = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout)) 228 | 229 | export const pipeData = new Promise((resolve, reject) => { 230 | const stdin = process.openStdin() 231 | let data = '' 232 | 233 | stdin.on('data', function (chunk) { 234 | data += chunk 235 | }) 236 | 237 | stdin.on('error', function (e) { 238 | reject(e) 239 | }) 240 | 241 | stdin.on('end', function () { 242 | resolve(data) 243 | }) 244 | }) 245 | --------------------------------------------------------------------------------