├── .circleci ├── README.md └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .markdownlint.json ├── .nvmrc ├── .prettierignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .yarnrc ├── LICENSE ├── README.md ├── bin └── warthog ├── codecov.yml ├── commitlint.config.js ├── examples ├── 01-simple-model │ ├── .env │ ├── README.md │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── server.ts │ │ ├── user.model.ts │ │ └── user.resolver.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 02-complex-example │ ├── .env │ ├── README.md │ ├── db │ │ └── migrations │ │ │ └── 1562027331912-UserModel.ts │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── modules │ │ │ ├── post │ │ │ │ └── post.model.ts │ │ │ └── user │ │ │ │ ├── user.model.ts │ │ │ │ └── user.resolver.ts │ │ └── server.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 03-one-to-many-relationship │ ├── .env │ ├── README.md │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── post.model.ts │ │ ├── post.resolver.ts │ │ ├── server.ts │ │ ├── user.model.ts │ │ └── user.resolver.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 04-many-to-many-relationship │ ├── .env │ ├── README.md │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── join-with-metadata │ │ │ ├── role.model.ts │ │ │ ├── role.resolver.ts │ │ │ ├── user-role.model.ts │ │ │ ├── user-role.resolver.ts │ │ │ ├── user.model.ts │ │ │ └── user.resolver.ts │ │ ├── server.ts │ │ └── simple-join-table │ │ │ ├── author.model.ts │ │ │ ├── author.resolver.ts │ │ │ ├── post.model.ts │ │ │ └── post.resolver.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 05-migrations │ ├── .env │ ├── README.md │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── server.ts │ │ ├── user.model.ts │ │ └── user.resolver.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 06-base-service │ ├── .env │ ├── README.md │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── server.ts │ │ ├── user.model.ts │ │ ├── user.resolver.ts │ │ └── user.service.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 07-feature-flags │ ├── .env │ ├── .vscode │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── README.md │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── package.json │ ├── src │ │ ├── environment │ │ │ ├── environment.model.ts │ │ │ ├── environment.resolver.ts │ │ │ └── environment.service.ts │ │ ├── feature-flag-segment │ │ │ ├── feature-flag-segment.model.ts │ │ │ ├── feature-flag-segment.resolver.ts │ │ │ └── feature-flag-segment.service.ts │ │ ├── feature-flag-user │ │ │ ├── feature-flag-user.model.ts │ │ │ ├── feature-flag-user.resolver.ts │ │ │ └── feature-flag-user.service.ts │ │ ├── feature-flag │ │ │ ├── feature-flag.model.ts │ │ │ ├── feature-flag.resolver.ts │ │ │ └── feature-flag.service.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── models.ts │ │ ├── project │ │ │ ├── project.model.ts │ │ │ ├── project.resolver.ts │ │ │ └── project.service.ts │ │ ├── segment │ │ │ ├── segment.model.ts │ │ │ ├── segment.resolver.ts │ │ │ └── segment.service.ts │ │ ├── server.ts │ │ ├── user-segment │ │ │ ├── user-segment.model.ts │ │ │ ├── user-segment.resolver.ts │ │ │ └── user-segment.service.ts │ │ └── user │ │ │ ├── user.model.ts │ │ │ ├── user.resolver.ts │ │ │ └── user.service.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 08-performance │ ├── .env │ ├── .vscode │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── README.md │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── models.ts │ │ ├── modules │ │ │ ├── post │ │ │ │ ├── post.model.ts │ │ │ │ ├── post.resolver.ts │ │ │ │ └── post.service.ts │ │ │ └── user │ │ │ │ ├── user.model.ts │ │ │ │ ├── user.resolver.ts │ │ │ │ └── user.service.ts │ │ └── server.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 09-production │ ├── .env │ ├── .vscode │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── README.md │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── models.ts │ │ ├── modules │ │ │ ├── post │ │ │ │ ├── post.model.ts │ │ │ │ ├── post.resolver.ts │ │ │ │ └── post.service.ts │ │ │ └── user │ │ │ │ ├── user.model.ts │ │ │ │ ├── user.resolver.ts │ │ │ │ └── user.service.ts │ │ └── server.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 10-subscriptions │ ├── .env │ ├── README.md │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── server.ts │ │ ├── user.model.ts │ │ ├── user.resolver.ts │ │ └── user.service.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 11-transactions │ ├── .env │ ├── README.md │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── server.ts │ │ ├── user.model.ts │ │ ├── user.resolver.ts │ │ ├── user.service.ts │ │ └── user.test.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock ├── 12-relay-connection │ ├── .env │ ├── README.md │ ├── examples.gql │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── server.ts │ │ ├── user.model.ts │ │ ├── user.resolver.ts │ │ ├── user.service.ts │ │ └── user.test.ts │ ├── tools │ │ └── seed.ts │ ├── tsconfig.json │ ├── warthog.config.js │ └── yarn.lock └── README.md ├── img └── warthog-logo.png ├── package.json ├── src ├── cli │ ├── README.md │ ├── cli.ts │ ├── commands │ │ ├── codegen.ts │ │ ├── dbCreate.ts │ │ ├── dbDrop.ts │ │ ├── generate.ts │ │ ├── migrate.ts │ │ ├── migrationGenerate.ts │ │ ├── new.ts │ │ ├── playground.ts │ │ └── warthog.ts │ ├── docs │ │ ├── commands.md │ │ └── plugins.md │ ├── extensions │ │ ├── config-extension.ts │ │ ├── db-extension.ts │ │ └── string-extension.ts │ ├── templates │ │ ├── generate │ │ │ ├── model.ts.ejs │ │ │ ├── resolver.ts.ejs │ │ │ └── service.ts.ejs │ │ └── new │ │ │ ├── _env.ejs │ │ │ ├── _gitignore.ejs │ │ │ ├── env.yml.ejs │ │ │ ├── package.json.ejs │ │ │ ├── src │ │ │ ├── config.ts.ejs │ │ │ ├── index.ts.ejs │ │ │ ├── logger.ts.ejs │ │ │ └── server.ts.ejs │ │ │ ├── tsconfig.json.ejs │ │ │ └── warthog.config.js.ejs │ └── types.ts ├── core │ ├── BaseModel.ts │ ├── BaseService.test.ts │ ├── BaseService.ts │ ├── Context.ts │ ├── GraphQLInfoService.ts │ ├── RelayService.test.ts │ ├── RelayService.ts │ ├── code-generator.ts │ ├── config.test.ts │ ├── config.ts │ ├── encoding.ts │ ├── http.ts │ ├── index.ts │ ├── logger.ts │ ├── server.test.ts │ ├── server.ts │ ├── tests │ │ ├── dotenv-files │ │ │ ├── .env │ │ │ ├── .env.local │ │ │ ├── .env.local.development │ │ │ ├── .env.local.production │ │ │ └── dotenv.test.ts │ │ ├── entity │ │ │ └── MyBase.model.ts │ │ ├── invalid-config-file │ │ │ ├── .warthogrc.json │ │ │ └── config.test.ts │ │ └── valid-config-file │ │ │ ├── .warthogrc.json │ │ │ └── config.test.ts │ └── types.ts ├── decorators │ ├── BooleanField.ts │ ├── CustomField.ts │ ├── DateField.ts │ ├── DateOnlyField.ts │ ├── DateTimeField.ts │ ├── EmailField.ts │ ├── EnumField.test.ts │ ├── EnumField.ts │ ├── Fields.ts │ ├── FloatField.ts │ ├── ForeignKeyField.ts │ ├── IdField.ts │ ├── IntField.ts │ ├── InterfaceType.ts │ ├── JSONField.ts │ ├── ManyToMany.ts │ ├── ManyToManyJoin.ts │ ├── ManyToOne.ts │ ├── Model.ts │ ├── NumericField.ts │ ├── ObjectType.ts │ ├── OneToMany.ts │ ├── StringField.ts │ ├── UpdateDateField.ts.bak │ ├── UserId.ts │ ├── WarthogField.ts │ ├── debug.ts │ ├── getCombinedDecorator.ts │ └── index.ts ├── gql │ ├── binding.ts │ └── index.ts ├── index.ts ├── metadata │ ├── index.ts │ └── metadata-storage.ts ├── middleware │ ├── DataLoaderMiddleware.ts │ ├── ErrorMiddleware.ts.bak │ ├── HealthMiddleware.ts │ └── index.ts ├── schema │ ├── SchemaGenerator.ts │ ├── TypeORMConverter.ts │ ├── getSchemaInfo.ts │ ├── index.ts │ └── type-conversion.ts ├── test │ ├── codegen-test-files.sh │ ├── examples │ │ ├── create-id-model.ts │ │ └── create-id-model │ │ │ └── generated │ │ │ ├── classes.ts │ │ │ ├── index.ts │ │ │ └── ormconfig.ts │ ├── functional │ │ ├── __snapshots__ │ │ │ ├── schema.test.ts.snap │ │ │ └── server.test.ts.snap │ │ ├── cli.test.ts │ │ ├── fixtures.ts │ │ ├── schema.test.ts │ │ └── server.test.ts │ ├── generated │ │ ├── binding.ts │ │ ├── classes.ts │ │ ├── index.ts │ │ ├── ormconfig.ts │ │ └── schema.graphql │ ├── helpers.ts │ ├── modules │ │ ├── api-only │ │ │ ├── api-only.model.ts │ │ │ ├── api-only.resolver.ts │ │ │ └── api-only.service.ts │ │ ├── db-only │ │ │ └── db-only.model.ts │ │ ├── dish │ │ │ ├── dish.model.ts │ │ │ ├── dish.resolver.ts │ │ │ └── dish.service.ts │ │ ├── index.ts │ │ ├── kitchen-sink │ │ │ ├── kitchen-sink.model.ts │ │ │ ├── kitchen-sink.resolver.ts │ │ │ └── kitchen-sink.service.ts │ │ └── shared.ts │ ├── server-vars.ts │ ├── setupFiles.ts │ ├── setupFilesAfterEnv.ts │ ├── test-server.ts │ └── utils.ts ├── tgql │ ├── BaseResolver.ts │ ├── BaseWhereInput.ts │ ├── DeleteResponse.ts │ ├── PageInfo.ts │ ├── PaginationArgs.ts │ ├── authChecker.ts │ ├── index.ts │ └── loadGlobs.ts ├── torm │ ├── EverythingSubscriber.ts.bak │ ├── SnakeNamingStrategy.ts │ ├── createConnection.ts │ ├── index.ts │ ├── operators.ts │ └── types.ts └── utils │ ├── decoratorComposer.test.ts │ ├── decoratorComposer.ts │ ├── generatedFolder.ts │ ├── index.ts │ ├── object.test.ts │ ├── object.ts │ ├── string.test.ts │ └── string.ts ├── tools ├── bootstrap-all.sh └── test.sh ├── tsconfig.eslint.json ├── tsconfig.json ├── tsconfig.test.json ├── typings └── typings.d.ts └── yarn.lock /.circleci/README.md: -------------------------------------------------------------------------------- 1 | ## CircleCI 2 | 3 | ### Troubleshoot CircleCI Issues 4 | 5 | To troubleshoot things happening in Circle without having to kick off a bunch of builds, you can use the Circle CLI. To install, run: 6 | 7 | - `brew install circleci` 8 | - `circleci setup` 9 | 10 | Then run `circle:build` to run the build job via the Circle CLI. 11 | 12 | ### Configuring CodeCov Integration 13 | 14 | See [CodeCov Commit Status](https://docs.codecov.io/docs/commit-status) 15 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | codecov: codecov/codecov@1.0.4 4 | references: 5 | yarn_cache_key: &yarn_cache_key yarn-v1-{{ checksum "yarn.lock" }} 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/node:10.13 10 | environment: 11 | PGUSER: postgres 12 | PG_USER: postgres 13 | PG_HOST: localhost 14 | - image: postgis/postgis:12-3.0-alpine 15 | environment: 16 | PGUSER: postgres 17 | POSTGRES_USER: postgres 18 | POSTGRES_DB: warthog-test 19 | POSTGRES_PASSWORD: postgres 20 | steps: 21 | - checkout 22 | - run: 23 | name: Waiting for Postgres to be ready 24 | command: | 25 | # Wait for 10 seconds for Postgres to respond 26 | for i in `seq 1 10`; 27 | do 28 | nc -z localhost 5432 && echo Success && exit 0 29 | echo -n . 30 | sleep 1 31 | done 32 | echo Failed waiting for Postgress && exit 1 33 | - restore_cache: 34 | keys: 35 | - *yarn_cache_key 36 | - yarn-v1- 37 | - run: 38 | name: dependencies 39 | command: yarn install --frozen-lockfile 40 | - run: 41 | name: build 42 | command: yarn build 43 | - run: 44 | name: test 45 | command: | 46 | yarn test 47 | - codecov/upload: 48 | file: coverage/*.json 49 | flags: backend 50 | - run: 51 | name: deploy 52 | command: DEBUG=condition* yarn run semantic-release --debug || true 53 | - save_cache: 54 | key: *yarn_cache_key 55 | paths: 56 | - ~/circleci/node_modules 57 | - node_modules 58 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | # markdown files require trailing double whitespace for newline 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /**/node_modules/**/* 2 | /examples/**/coverage/**/* 3 | /examples/**/node_modules/**/* 4 | /**/generated/**/* 5 | !.eslintrc.js 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Coverage directory used by tools like istanbul 9 | coverage 10 | 11 | # Dependency directories 12 | node_modules/ 13 | 14 | # Yarn Integrity file 15 | .yarn-integrity 16 | 17 | dist 18 | 19 | .npmrc 20 | 21 | # Generated files 22 | tmp 23 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "first-line-h1": { 3 | "level": 2 4 | }, 5 | "line-length": false, 6 | "no-inline-html": { 7 | "allowed_elements": ["a", "img", "p", "details", "summary"] 8 | }, 9 | "no-trailing-punctuation": { 10 | "punctuation": ".,;:" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/generated/**/* 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Jest", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 9 | "stopOnEntry": false, 10 | "args": ["--runInBand", "${file}"], 11 | "cwd": "${workspaceRoot}", 12 | "internalConsoleOptions": "openOnSessionStart", 13 | "runtimeArgs": ["--nolazy"], 14 | "sourceMaps": true, 15 | "outFiles": ["${workspaceRoot}/dist/**/*.js"], 16 | "env": { 17 | "DEBUG": "*" 18 | } 19 | }, 20 | { 21 | "name": "Run from index.ts", 22 | "type": "node", 23 | "request": "launch", 24 | "args": ["src/index.ts"], 25 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 26 | "sourceMaps": true, 27 | "cwd": "${workspaceRoot}", 28 | "protocol": "inspector" 29 | }, 30 | { 31 | "name": "ts-node", 32 | "type": "node", 33 | "request": "launch", 34 | "args": ["${relativeFile}"], 35 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 36 | "sourceMaps": true, 37 | "cwd": "${workspaceRoot}", 38 | "protocol": "inspector", 39 | "env": { 40 | "TS_NODE_IGNORE": "false", 41 | "DEBUG": "*" 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "editor.formatOnSave": true, 4 | // Don't auto-format ejs templates 5 | "[html]": { 6 | "editor.formatOnSave": false 7 | }, 8 | "eslint.validate": [ 9 | "javascript", 10 | { 11 | "language": "typescript", 12 | "autoFix": true 13 | } 14 | ], 15 | "search.exclude": { 16 | "dist/": true, 17 | "node_modules/": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "command": "yarn jest --verbose ${file}", 6 | "type": "shell", 7 | "label": "jest-current-file", 8 | "problemMatcher": [] 9 | }, 10 | { 11 | "label": "build", 12 | "type": "typescript", 13 | "tsconfig": "tsconfig.json", 14 | "problemMatcher": ["$tsc"], 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dan Caddigan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/warthog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); // eslint-disable-line @typescript-eslint/no-var-requires 3 | const fs = require('fs'); 4 | var devMode = process.env.WARTHOG_CLI_DEV_MODE === 'true'; 5 | 6 | var tsCommands = ['codegen']; 7 | var runCommand = process.argv[2]; 8 | var commandRequiresTS = tsCommands.indexOf(runCommand) > -1; 9 | var srcFolder = `${__dirname}/../src/cli`; 10 | var srcFileExists = fs.existsSync(`${srcFolder}/cli.ts`); 11 | 12 | if (commandRequiresTS) { 13 | try { 14 | require.resolve('ts-node'); 15 | } catch (e) { 16 | console.error('Error: ts-node is required.'); 17 | process.exit(1); 18 | } 19 | 20 | require('ts-node').register({ project: path.join(process.cwd(), 'tsconfig.json') }); 21 | } 22 | 23 | if (devMode || (commandRequiresTS && srcFileExists)) { 24 | // this runs from the typescript source (for dev only) 25 | // hook into ts-node so we can run typescript on the fly 26 | // run the CLI with the current process arguments 27 | require(`${srcFolder}/cli`).run(process.argv); 28 | } else { 29 | require(`${__dirname}/../dist/cli/cli`).run(process.argv); 30 | } 31 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /examples/01-simple-model/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-example-1 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | -------------------------------------------------------------------------------- /examples/01-simple-model/README.md: -------------------------------------------------------------------------------- 1 | # Example 1 - User Model 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | 14 | ## Running the App 15 | 16 | To run the project, run `yarn start`. This will: 17 | 18 | - Run the API server 19 | - Open GraphQL Playground 20 | 21 | ## Example Queries/Mutations 22 | 23 | You can find some examples in [examples.gql](./examples.gql) 24 | -------------------------------------------------------------------------------- /examples/01-simple-model/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | users(orderBy: createdAt_DESC) { 3 | id 4 | lastName 5 | createdAt 6 | } 7 | } 8 | 9 | mutation { 10 | createUser(data: { firstName: "Test", email: "test@fakeemail.com" }) { 11 | id 12 | firstName 13 | lastName 14 | createdAt 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/01-simple-model/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/01-simple-model/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/01-simple-model/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["dist/*", "generated/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/01-simple-model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example1", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn codegen && yarn db:create", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "db:seed:dev": "ts-node tools/seed.ts", 11 | "playground": "yarn warthog playground", 12 | "start": "yarn start:ts", 13 | "start:ts": "DEBUG=warthog* ts-node-dev --type-check src/index.ts", 14 | "start:prod": "ts-node src/index.ts", 15 | "//": "Allows us to use the local warthog CLI in commands above", 16 | "warthog": "../../bin/warthog" 17 | }, 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "@types/isomorphic-fetch": "^0.0.34", 21 | "@types/node": "^10.17.5", 22 | "ts-jest": "^23.10.5", 23 | "ts-node": "^8.10.2", 24 | "ts-node-dev": "^1.0.0-pre.44" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/01-simple-model/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/01-simple-model/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Server } from '../../../src'; 4 | 5 | export function getServer(AppOptions = {}, dbOptions = {}) { 6 | return new Server( 7 | { 8 | context: () => { 9 | return { 10 | user: { 11 | id: 'abc123' 12 | } 13 | }; 14 | }, 15 | ...AppOptions 16 | }, 17 | dbOptions 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/01-simple-model/src/user.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseModel, 3 | BooleanField, 4 | EmailField, 5 | EnumField, 6 | FloatField, 7 | IntField, 8 | Model, 9 | StringField 10 | } from '../../../src'; 11 | 12 | // Note: this must be exported and in the same file where it's attached with @EnumField 13 | // Also - must use string enums 14 | export enum StringEnum { 15 | FOO = 'FOO', 16 | BAR = 'BAR' 17 | } 18 | 19 | @Model() 20 | export class User extends BaseModel { 21 | @StringField() 22 | firstName?: string; 23 | 24 | @StringField({ nullable: true }) 25 | lastName?: string; 26 | 27 | @EmailField() 28 | email?: string; 29 | 30 | @IntField() 31 | age?: number; 32 | 33 | @BooleanField() 34 | isRequired?: boolean; 35 | 36 | @EnumField('StringEnum', StringEnum) 37 | stringEnumField: StringEnum; 38 | 39 | @FloatField() 40 | rating?: number; 41 | } 42 | -------------------------------------------------------------------------------- /examples/01-simple-model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/01-simple-model/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/02-complex-example/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-example-2 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | WARTHOG_RESOLVERS_PATH=./src/**/*.resolver.ts 10 | -------------------------------------------------------------------------------- /examples/02-complex-example/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 - Complex Example 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `DEBUG=* yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | - Seed the database with test data 14 | 15 | ## Running the App 16 | 17 | To run the project, run `yarn start`. This will: 18 | 19 | - Run the API server 20 | - Open GraphQL Playground 21 | 22 | ## Features in this example 23 | 24 | - Custom logger 25 | - Enums 26 | - Authorization 27 | - Validations 28 | -------------------------------------------------------------------------------- /examples/02-complex-example/db/migrations/1562027331912-UserModel.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class UserModel1562027331912 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE TABLE "users" ("id" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "created_by_id" character varying NOT NULL, "updated_at" TIMESTAMP DEFAULT now(), "updated_by_id" character varying, "deleted_at" TIMESTAMP, "deleted_by_id" character varying, "version" integer NOT NULL, "first_name" character varying(30) NOT NULL, "last_name" character varying(50) NOT NULL, "string_enum_field" character varying NOT NULL, "email" character varying NOT NULL, "nick_name" character varying(30), "private_field" character varying, "json_field" jsonb, CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "UQ_8b0d982b1bfea7d0518840535b2" UNIQUE ("first_name", "string_enum_field"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))` 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`DROP TABLE "users"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/02-complex-example/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | users(limit: 20, where: { typedJsonField_json: { params: { type_eq: "Bar" } } }) { 3 | id 4 | createdAt 5 | typedJsonField { 6 | params { 7 | type 8 | name 9 | value 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/02-complex-example/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/02-complex-example/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/02-complex-example/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["dist/*", "generated/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/02-complex-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example2", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen && yarn db:seed:dev", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "db:seed:dev": "ts-node tools/seed.ts", 11 | "playground": "yarn warthog playground", 12 | "start": "yarn start:ts", 13 | "start:debug": "yarn start:ts --inspect", 14 | "start:ts": "ts-node --type-check src/index.ts", 15 | "test": "jest --detectOpenHandles --verbose --coverage", 16 | "test:watch": "jest --watch", 17 | "//": "Allows us to use the local warthog CLI in commands above", 18 | "warthog": "../../bin/warthog" 19 | }, 20 | "dependencies": { 21 | "debug": "^4.1.1", 22 | "handlebars": "^4.5.2", 23 | "lodash": "^4.17.15", 24 | "reflect-metadata": "^0.1.13", 25 | "typescript": "^3.7.2" 26 | }, 27 | "devDependencies": { 28 | "@types/faker": "^4.1.7", 29 | "@types/isomorphic-fetch": "^0.0.34", 30 | "@types/jest": "^23.3.14", 31 | "@types/node": "^10.17.5", 32 | "faker": "^4.1.0", 33 | "jest": "^23.6.0", 34 | "ts-jest": "^23.10.5", 35 | "ts-node": "^8.10.2" 36 | }, 37 | "jest": { 38 | "transform": { 39 | ".ts": "ts-jest" 40 | }, 41 | "testRegex": "\\.test\\.ts$", 42 | "moduleFileExtensions": [ 43 | "ts", 44 | "js" 45 | ], 46 | "coveragePathIgnorePatterns": [ 47 | "/node_modules/", 48 | "\\.test\\.ts$" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/02-complex-example/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/02-complex-example/src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as Debug from 'debug'; 2 | 3 | // TODO: better logger 4 | export const customLogger = { 5 | error: Debug('custom:error'), 6 | info: Debug('custom:info'), 7 | log: Debug('custom:log'), 8 | warn: Debug('custom:warn') 9 | }; 10 | 11 | type logFunc = (...args: any[]) => void; 12 | -------------------------------------------------------------------------------- /examples/02-complex-example/src/modules/post/post.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, ManyToOne, Model, StringField } from '../../../../../src'; 2 | import { User } from '../user/user.model'; 3 | 4 | @Model() 5 | export class Post extends BaseModel { 6 | @StringField() 7 | title!: string; 8 | 9 | @ManyToOne( 10 | () => User, 11 | (user: User) => user.posts, 12 | { nullable: false } 13 | ) 14 | user?: User; 15 | } 16 | -------------------------------------------------------------------------------- /examples/02-complex-example/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { authChecker, BaseContext, Server } from '../../../src/'; 4 | 5 | import { customLogger } from './logger'; 6 | 7 | interface Context extends BaseContext { 8 | user: { 9 | email: string; 10 | id: string; 11 | permissions: string; 12 | }; 13 | } 14 | 15 | function sleep(ms: number) { 16 | return new Promise(resolve => setTimeout(resolve, ms)); 17 | } 18 | 19 | export function getServer(AppOptions = {}, dbOptions = {}) { 20 | return new Server( 21 | { 22 | authChecker, 23 | // Inject a fake user. In a real app you'd parse a JWT to add the user 24 | context: async () => { 25 | // allows asynchronous resolution of user (or other items you want to put in context) 26 | await sleep(500); 27 | return Promise.resolve({ 28 | user: { 29 | email: 'admin@test.com', 30 | id: 'abc12345', 31 | permissions: ['user:read', 'user:update', 'user:create', 'user:delete', 'photo:delete'] 32 | } 33 | }); 34 | }, 35 | logger: customLogger, 36 | ...AppOptions 37 | }, 38 | dbOptions 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /examples/02-complex-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/02-complex-example/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-example-3 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/README.md: -------------------------------------------------------------------------------- 1 | # Example 3 - Relationships 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | 14 | ## Running the App 15 | 16 | To run the project, run `yarn start`. This will: 17 | 18 | - Run the API server 19 | - Open GraphQL Playground 20 | 21 | ## Example Queries/Mutations 22 | 23 | You can find some examples in [examples.gql](./examples.gql) 24 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | users(orderBy: createdAt_DESC) { 3 | id 4 | firstName 5 | posts { 6 | id 7 | title 8 | createdAt 9 | } 10 | createdAt 11 | } 12 | 13 | posts(where: { userId_in: [""] }, orderBy: createdAt_DESC) { 14 | id 15 | title 16 | userId 17 | createdAt 18 | } 19 | } 20 | 21 | mutation { 22 | createUser(data: { firstName: "TestUser" }) { 23 | id 24 | firstName 25 | createdAt 26 | } 27 | 28 | createPost(data: { title: "Hello World", userId: "" }) { 29 | id 30 | title 31 | createdAt 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["dist/*", "generated/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example3", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "playground": "yarn warthog playground", 11 | "start": "yarn start:ts", 12 | "start:ts": "ts-node --type-check src/index.ts", 13 | "//": "Allows us to use the local warthog CLI in commands above", 14 | "warthog": "../../bin/warthog" 15 | }, 16 | "dependencies": { 17 | "handlebars": "^4.5.2", 18 | "lodash": "^4.17.15", 19 | "reflect-metadata": "^0.1.13", 20 | "typescript": "^3.7.2" 21 | }, 22 | "devDependencies": { 23 | "@types/faker": "^4.1.7", 24 | "@types/isomorphic-fetch": "^0.0.34", 25 | "@types/jest": "^23.3.14", 26 | "@types/node": "^10.17.5", 27 | "faker": "^4.1.0", 28 | "jest": "^23.6.0", 29 | "ts-jest": "^23.10.5", 30 | "ts-node": "^8.10.2" 31 | }, 32 | "jest": { 33 | "transform": { 34 | ".ts": "ts-jest" 35 | }, 36 | "testRegex": "\\.test\\.ts$", 37 | "moduleFileExtensions": [ 38 | "ts", 39 | "js" 40 | ], 41 | "coveragePathIgnorePatterns": [ 42 | "/node_modules/", 43 | "\\.test\\.ts$" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/src/post.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, ManyToOne, Model, StringField } from '../../../src'; 2 | 3 | import { User } from './user.model'; 4 | 5 | @Model() 6 | export class Post extends BaseModel { 7 | @StringField() 8 | title?: string; 9 | 10 | @ManyToOne( 11 | () => User, 12 | (user: User) => user.posts, 13 | { nullable: false } 14 | ) 15 | user?: User; 16 | } 17 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { BaseContext, Server } from '../../../src'; 4 | 5 | interface Context extends BaseContext { 6 | user: { 7 | email: string; 8 | id: string; 9 | permissions: string; 10 | }; 11 | } 12 | 13 | export function getServer(AppOptions = {}, dbOptions = {}) { 14 | return new Server( 15 | { 16 | // Inject a fake user. In a real app you'd parse a JWT to add the user 17 | context: () => { 18 | return { 19 | user: { 20 | id: 'abc123' 21 | } 22 | }; 23 | }, 24 | ...AppOptions 25 | }, 26 | dbOptions 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/src/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../src'; 2 | 3 | import { Post } from './post.model'; 4 | 5 | @Model() 6 | export class User extends BaseModel { 7 | @StringField() 8 | firstName?: string; 9 | 10 | @OneToMany( 11 | () => Post, 12 | (post: Post) => post.user 13 | ) 14 | posts?: Post[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": false, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/03-one-to-many-relationship/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-example-4 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/README.md: -------------------------------------------------------------------------------- 1 | # Example 3 - Relationships 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | 14 | ## Running the App 15 | 16 | To run the project, run `yarn start`. This will: 17 | 18 | - Run the API server 19 | - Open GraphQL Playground 20 | 21 | ## Example Queries/Mutations 22 | 23 | You can find some examples in [examples.gql](./examples.gql) 24 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | users(orderBy: createdAt_DESC) { 3 | id 4 | firstName 5 | createdAt 6 | userRoles { 7 | createdAt 8 | userId 9 | roleId 10 | role { 11 | id 12 | name 13 | createdAt 14 | } 15 | } 16 | } 17 | } 18 | 19 | mutation { 20 | createManyUserRoles( 21 | data: [{ userId: "XJfBX9ybj", roleId: "I7C9xK7vH" }, { userId: "XJfBX9ybj", roleId: "I7C9xK7vH" }] 22 | ) { 23 | id 24 | userId 25 | roleId 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["dist/*", "generated/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example4", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "playground": "yarn warthog playground", 11 | "start": "yarn start:ts", 12 | "start:ts": "ts-node --type-check src/index.ts", 13 | "//": "Allows us to use the local warthog CLI in commands above", 14 | "warthog": "../../bin/warthog" 15 | }, 16 | "dependencies": { 17 | "handlebars": "^4.5.2", 18 | "lodash": "^4.17.15", 19 | "reflect-metadata": "^0.1.13", 20 | "typescript": "^3.7.2" 21 | }, 22 | "devDependencies": { 23 | "@types/faker": "^4.1.7", 24 | "@types/isomorphic-fetch": "^0.0.34", 25 | "@types/jest": "^23.3.14", 26 | "@types/node": "^10.17.5", 27 | "faker": "^4.1.0", 28 | "jest": "^23.6.0", 29 | "ts-jest": "^23.10.5", 30 | "ts-node": "^8.10.2" 31 | }, 32 | "jest": { 33 | "transform": { 34 | ".ts": "ts-jest" 35 | }, 36 | "testRegex": "\\.test\\.ts$", 37 | "moduleFileExtensions": [ 38 | "ts", 39 | "js" 40 | ], 41 | "coveragePathIgnorePatterns": [ 42 | "/node_modules/", 43 | "\\.test\\.ts$" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/join-with-metadata/role.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../../src'; 2 | 3 | import { UserRole } from './user-role.model'; 4 | 5 | @Model() 6 | export class Role extends BaseModel { 7 | @StringField() 8 | name?: string; 9 | 10 | @OneToMany( 11 | () => UserRole, 12 | (userRole: UserRole) => userRole.role 13 | ) 14 | userRoles?: UserRole[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/join-with-metadata/role.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Args, Ctx, Mutation, Query, Resolver } from 'type-graphql'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseContext, BaseResolver } from '../../../../src'; 6 | import { RoleCreateInput, RoleWhereArgs, RoleWhereInput } from '../../generated'; 7 | import { Role } from './role.model'; 8 | 9 | @Resolver(Role) 10 | export class RoleResolver extends BaseResolver { 11 | constructor(@InjectRepository(Role) public readonly roleRepository: Repository) { 12 | super(Role, roleRepository); 13 | } 14 | 15 | @Query(() => [Role]) 16 | async roles(@Args() { where, orderBy, limit, offset }: RoleWhereArgs): Promise { 17 | return this.find(where, orderBy, limit, offset); 18 | } 19 | 20 | @Mutation(() => Role) 21 | async createRole(@Arg('data') data: RoleCreateInput, @Ctx() ctx: BaseContext): Promise { 22 | return this.create(data, ctx.user.id); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/join-with-metadata/user-role.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, ManyToOne, Model, StringField } from '../../../../src'; 2 | 3 | import { Role } from './role.model'; 4 | import { User } from './user.model'; 5 | 6 | // This is a modified many-to-many table that also allows 7 | // for additional metadata as a typical many-to-many is just 8 | // a lightweight join table with the foreign keys 9 | @Model() 10 | export class UserRole extends BaseModel { 11 | @ManyToOne( 12 | () => User, 13 | (user: User) => user.userRoles 14 | ) 15 | user?: User; 16 | 17 | @ManyToOne( 18 | () => Role, 19 | (role: Role) => role.userRoles 20 | ) 21 | role?: Role; 22 | 23 | @StringField({ nullable: true }) 24 | otherMetadata?: string; 25 | } 26 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/join-with-metadata/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../../src'; 2 | 3 | import { UserRole } from './user-role.model'; 4 | 5 | @Model() 6 | export class User extends BaseModel { 7 | @StringField() 8 | firstName?: string; 9 | 10 | @OneToMany( 11 | () => UserRole, 12 | (userRole: UserRole) => userRole.user 13 | ) 14 | userRoles?: UserRole[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/join-with-metadata/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Args, Ctx, FieldResolver, Mutation, Query, Resolver, Root } from 'type-graphql'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseContext, BaseResolver } from '../../../../src'; 6 | import { UserCreateInput, UserWhereArgs, UserWhereInput } from '../../generated'; 7 | import { UserRole } from './user-role.model'; 8 | import { User } from './user.model'; 9 | 10 | @Resolver(User) 11 | export class UserResolver extends BaseResolver { 12 | constructor(@InjectRepository(User) public readonly userRepository: Repository) { 13 | super(User, userRepository); 14 | } 15 | 16 | @FieldResolver(() => [UserRole]) 17 | userRoles(@Root() user: User, @Ctx() ctx: BaseContext): Promise { 18 | return ctx.dataLoader.loaders.User.userRoles.load(user); 19 | } 20 | 21 | @Query(() => [User]) 22 | async users(@Args() { where, orderBy, limit, offset }: UserWhereArgs): Promise { 23 | return this.find(where, orderBy, limit, offset); 24 | } 25 | 26 | @Mutation(() => User) 27 | async createUser(@Arg('data') data: UserCreateInput, @Ctx() ctx: BaseContext): Promise { 28 | return this.create(data, ctx.user.id); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Server } from '../../../src'; 4 | 5 | export function getServer(AppOptions = {}, dbOptions = {}) { 6 | return new Server( 7 | { 8 | context: () => { 9 | return { 10 | user: { 11 | id: 'abc123' 12 | } 13 | }; 14 | }, 15 | introspection: true, 16 | ...AppOptions 17 | }, 18 | dbOptions 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/simple-join-table/author.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../../src'; 2 | 3 | import { Post } from './post.model'; 4 | 5 | @Model() 6 | export class Author extends BaseModel { 7 | @StringField() 8 | firstName?: string; 9 | 10 | @OneToMany( 11 | () => Post, 12 | (post: Post) => post.authors 13 | ) 14 | posts?: Post[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/simple-join-table/author.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Args, Ctx, FieldResolver, Mutation, Query, Resolver, Root } from 'type-graphql'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseContext, BaseResolver } from '../../../../src'; 6 | import { AuthorCreateInput, AuthorWhereArgs, AuthorWhereInput } from '../../generated'; 7 | import { Author } from './author.model'; 8 | import { Post } from './post.model'; 9 | 10 | @Resolver(Author) 11 | export class AuthorResolver extends BaseResolver { 12 | constructor(@InjectRepository(Author) public readonly authorRepository: Repository) { 13 | super(Author, authorRepository); 14 | } 15 | 16 | @FieldResolver(() => [Post]) 17 | posts(@Root() author: Author, @Ctx() ctx: BaseContext): Promise { 18 | return ctx.dataLoader.loaders.Author.posts.load(author); 19 | } 20 | 21 | @Query(() => [Author]) 22 | async authors(@Args() { where, orderBy, limit, offset }: AuthorWhereArgs): Promise { 23 | return this.find(where, orderBy, limit, offset); 24 | } 25 | 26 | @Mutation(() => Author) 27 | async createAuthor( 28 | @Arg('data') data: AuthorCreateInput, 29 | @Ctx() ctx: BaseContext 30 | ): Promise { 31 | return this.create(data, ctx.user.id); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/simple-join-table/post.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../../src'; 2 | 3 | import { Author } from './author.model'; 4 | 5 | @Model() 6 | export class Post extends BaseModel { 7 | @StringField() 8 | name?: string; 9 | 10 | @OneToMany( 11 | () => Author, 12 | (author: Author) => author.posts 13 | ) 14 | authors?: Author[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/src/simple-join-table/post.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Args, Ctx, FieldResolver, Mutation, Query, Resolver, Root } from 'type-graphql'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseContext, BaseResolver } from '../../../../src'; 6 | import { PostCreateInput, PostWhereArgs, PostWhereInput } from '../../generated'; 7 | 8 | import { Author } from './author.model'; 9 | import { Post } from './post.model'; 10 | 11 | @Resolver(Post) 12 | export class PostResolver extends BaseResolver { 13 | constructor(@InjectRepository(Post) public readonly postRepository: Repository) { 14 | super(Post, postRepository); 15 | } 16 | 17 | @FieldResolver(() => [Post]) 18 | posts(@Root() author: Author, @Ctx() ctx: BaseContext): Promise { 19 | return ctx.dataLoader.loaders.Author.posts.load(author); 20 | } 21 | 22 | @Query(() => [Post]) 23 | async roles(@Args() { where, orderBy, limit, offset }: PostWhereArgs): Promise { 24 | return this.find(where, orderBy, limit, offset); 25 | } 26 | 27 | @Mutation(() => Post) 28 | async createPost(@Arg('data') data: PostCreateInput, @Ctx() ctx: BaseContext): Promise { 29 | return this.create(data, ctx.user.id); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": false, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/04-many-to-many-relationship/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/05-migrations/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-example-5 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | -------------------------------------------------------------------------------- /examples/05-migrations/README.md: -------------------------------------------------------------------------------- 1 | # Example 1 - User Model 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | - Generate code in `generated` folder 14 | 15 | ## Generate DB schema migration 16 | 17 | To automatically generate a schema migration file, run `yarn db:migration:generate`. The migration will be put in the `db/migrations` folder 18 | 19 | ## Run the DB migration 20 | 21 | To run the DB migration, run `yarn db:migration:run` 22 | -------------------------------------------------------------------------------- /examples/05-migrations/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | users(orderBy: createdAt_DESC) { 3 | id 4 | lastName 5 | createdAt 6 | } 7 | } 8 | 9 | mutation { 10 | createUser(data: { firstName: "Test", email: "test@fakeemail.com" }) { 11 | id 12 | firstName 13 | lastName 14 | createdAt 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/05-migrations/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/05-migrations/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/05-migrations/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["dist/*", "generated/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/05-migrations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example5", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "db:migrate:generate": "warthog db:migrate:generate --name", 11 | "db:migrate": "warthog db:migrate", 12 | "db:query": "yarn typeorm:cli query 'select * from user;'", 13 | "playground": "yarn warthog playground", 14 | "start": "yarn start:ts", 15 | "start:ts": "DEBUG=warthog* ts-node-dev --type-check src/index.ts", 16 | "start:prod": "ts-node src/index.ts", 17 | "typeorm:cli": "ts-node ./node_modules/.bin/typeorm -f ./generated/ormconfig.ts", 18 | "//": "Allows us to use the local warthog CLI in commands above", 19 | "warthog": "../../bin/warthog" 20 | }, 21 | "dependencies": { 22 | "handlebars": "^4.5.2", 23 | "lodash": "^4.17.15", 24 | "reflect-metadata": "^0.1.13", 25 | "typescript": "^3.7.2" 26 | }, 27 | "devDependencies": { 28 | "@types/faker": "^4.1.7", 29 | "@types/isomorphic-fetch": "^0.0.34", 30 | "@types/jest": "^23.3.14", 31 | "@types/node": "^10.17.5", 32 | "faker": "^4.1.0", 33 | "jest": "^23.6.0", 34 | "ts-jest": "^23.10.5", 35 | "ts-node": "^8.10.2", 36 | "ts-node-dev": "^1.0.0-pre.44" 37 | }, 38 | "jest": { 39 | "transform": { 40 | ".ts": "ts-jest" 41 | }, 42 | "testRegex": "\\.test\\.ts$", 43 | "moduleFileExtensions": [ 44 | "ts", 45 | "js" 46 | ], 47 | "coveragePathIgnorePatterns": [ 48 | "/node_modules/", 49 | "\\.test\\.ts$" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/05-migrations/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/05-migrations/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Server } from '../../../src'; 4 | 5 | export function getServer(AppOptions = {}) { 6 | return new Server( 7 | { 8 | context: () => { 9 | return { 10 | user: { 11 | id: 'abc123' 12 | } 13 | }; 14 | }, 15 | introspection: true, 16 | ...AppOptions 17 | }, 18 | // Make sure TypeORM does not auto-update the DB schema so that we know our CLI commands 19 | // are making the changes 20 | { synchronize: false } 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/05-migrations/src/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, EmailField, Model, StringField } from '../../../src'; 2 | 3 | @Model() 4 | export class User extends BaseModel { 5 | @StringField() 6 | firstName?: string; 7 | 8 | @StringField({ nullable: true }) 9 | lastName?: string; 10 | 11 | @EmailField() 12 | email?: string; 13 | } 14 | -------------------------------------------------------------------------------- /examples/05-migrations/src/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from 'type-graphql'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseResolver } from '../../../src'; 6 | import { UserWhereArgs, UserWhereInput } from '../generated'; 7 | 8 | import { User } from './user.model'; 9 | 10 | // Note: we have to specify `User` here instead of (() => User) because for some reason this 11 | // changes the object reference when it's trying to add the FieldResolver and things break 12 | @Resolver(User) 13 | export class UserResolver extends BaseResolver { 14 | constructor(@InjectRepository(User) public readonly userRepository: Repository) { 15 | super(User, userRepository); 16 | } 17 | 18 | @Query(() => [User]) 19 | async users(@Args() { where, orderBy, limit, offset }: UserWhereArgs): Promise { 20 | return this.find(where, orderBy, limit, offset); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/05-migrations/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/05-migrations/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/06-base-service/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-example-6 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | -------------------------------------------------------------------------------- /examples/06-base-service/README.md: -------------------------------------------------------------------------------- 1 | # Example 6 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `DEBUG=* yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | - Seed the database with test data 14 | 15 | ## Running the App 16 | 17 | To run the project, run `yarn start`. This will: 18 | 19 | - Run the API server 20 | - Open GraphQL Playground 21 | -------------------------------------------------------------------------------- /examples/06-base-service/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/06-base-service/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/06-base-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example6", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen && yarn db:seed:dev", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "db:seed:dev": "ts-node tools/seed.ts", 11 | "playground": "yarn warthog playground", 12 | "start": "yarn start:ts", 13 | "start:debug": "yarn start:ts --inspect", 14 | "start:ts": "ts-node --type-check src/index.ts", 15 | "test": "jest --detectOpenHandles --verbose --coverage", 16 | "test:watch": "jest --watch", 17 | "//": "Allows us to use the local warthog CLI in commands above", 18 | "warthog": "../../bin/warthog" 19 | }, 20 | "dependencies": { 21 | "debug": "^4.1.1", 22 | "handlebars": "^4.5.2", 23 | "lodash": "^4.17.15", 24 | "reflect-metadata": "^0.1.13", 25 | "typescript": "^3.7.2" 26 | }, 27 | "devDependencies": { 28 | "@types/faker": "^4.1.7", 29 | "@types/isomorphic-fetch": "^0.0.34", 30 | "@types/jest": "^23.3.14", 31 | "@types/node": "^10.17.5", 32 | "faker": "^4.1.0", 33 | "jest": "^23.6.0", 34 | "ts-jest": "^23.10.5", 35 | "ts-node": "^8.10.2" 36 | }, 37 | "jest": { 38 | "transform": { 39 | ".ts": "ts-jest" 40 | }, 41 | "testRegex": "\\.test\\.ts$", 42 | "moduleFileExtensions": [ 43 | "ts", 44 | "js" 45 | ], 46 | "coveragePathIgnorePatterns": [ 47 | "/node_modules/", 48 | "\\.test\\.ts$" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/06-base-service/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/06-base-service/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { BaseContext, Server } from '../../../src'; 4 | 5 | interface Context extends BaseContext { 6 | user: { 7 | email: string; 8 | id: string; 9 | permissions: string; 10 | }; 11 | } 12 | 13 | export function getServer(AppOptions = {}, dbOptions = {}) { 14 | return new Server( 15 | { 16 | // Inject a fake user. In a real app you'd parse a JWT to add the user 17 | context: () => { 18 | return { 19 | user: { 20 | email: 'admin@test.com', 21 | id: 'abc12345', 22 | permissions: ['user:read', 'user:update', 'user:create', 'user:delete', 'photo:delete'] 23 | } 24 | }; 25 | }, 26 | ...AppOptions 27 | }, 28 | dbOptions 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/06-base-service/src/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, StringField } from '../../../src'; 2 | 3 | @Model() 4 | export class User extends BaseModel { 5 | @StringField({ maxLength: 30 }) 6 | firstName?: string; 7 | 8 | @StringField({ maxLength: 50, minLength: 2 }) 9 | lastName?: string; 10 | } 11 | -------------------------------------------------------------------------------- /examples/06-base-service/src/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { DeepPartial, Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../src'; 6 | 7 | import { User } from './user.model'; 8 | 9 | @Service('UserService') 10 | export class UserService extends BaseService { 11 | constructor(@InjectRepository(User) protected readonly repository: Repository) { 12 | super(User, repository); 13 | } 14 | 15 | async create(data: DeepPartial, userId: string): Promise { 16 | const newUser = await super.create(data, userId); 17 | 18 | // Perform some side effects 19 | 20 | return newUser; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/06-base-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/06-base-service/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/07-feature-flags/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-example-feature-flag 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | -------------------------------------------------------------------------------- /examples/07-feature-flags/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "ts-node-7", 6 | "type": "node", 7 | "request": "launch", 8 | "args": ["${relativeFile}"], 9 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 10 | "sourceMaps": true, 11 | "cwd": "${workspaceRoot}", 12 | "protocol": "inspector", 13 | "env": { 14 | "TS_NODE_IGNORE": "false", 15 | "DEBUG": "*" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /examples/07-feature-flags/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "search.exclude": { 4 | "dist/": true, 5 | "node_modules/": true 6 | }, 7 | "[javascript]": { 8 | "editor.formatOnSave": true 9 | }, 10 | "[json]": { 11 | "editor.formatOnSave": true 12 | }, 13 | "[typescript]": { 14 | "editor.formatOnSave": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/07-feature-flags/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "command": "source tools/environment_setup.sh", 6 | "type": "shell", 7 | "label": "setupEnvironment" 8 | }, 9 | { 10 | "label": "build", 11 | "type": "typescript", 12 | "tsconfig": "tsconfig.json", 13 | "problemMatcher": ["$tsc"], 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /examples/07-feature-flags/README.md: -------------------------------------------------------------------------------- 1 | # Example 7 - Feature Flags 2 | 3 | ## TODO 4 | 5 | - Need to add `tags` as an array type that is searchable 6 | - FeatureFlagSegment 7 | - UserSegment 8 | - featureFlagsForUser (CUSTOM QUERY) 9 | - Question: how long should `key`s be? 10 | 11 | [LaunchDarkly](https://apidocs.launchdarkly.com/reference) 12 | 13 | ## Setup 14 | 15 | Run `yarn bootstrap && yarn start` 16 | 17 | ## Bootstrapping the App 18 | 19 | Running `DEBUG=* yarn bootstrap` will do the following: 20 | 21 | - Install packages 22 | - Create the example DB 23 | - Seed the database with test data 24 | 25 | ## Running the App 26 | 27 | To run the project, run `yarn start`. This will: 28 | 29 | - Run the API server 30 | - Open GraphQL Playground 31 | -------------------------------------------------------------------------------- /examples/07-feature-flags/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | segments(orderBy: createdAt_DESC) { 3 | id 4 | key 5 | name 6 | createdAt 7 | envKey 8 | environmentId 9 | environment { 10 | id 11 | key 12 | createdAt 13 | } 14 | projKey 15 | projectId 16 | project { 17 | id 18 | key 19 | createdAt 20 | } 21 | } 22 | 23 | projects(orderBy: createdAt_DESC) { 24 | id 25 | key 26 | name 27 | createdAt 28 | environments { 29 | id 30 | key 31 | projKey 32 | projectId 33 | name 34 | createdAt 35 | } 36 | } 37 | 38 | environments(orderBy: createdAt_DESC, limit: 10) { 39 | id 40 | key 41 | projKey 42 | projectId 43 | name 44 | createdAt 45 | project { 46 | id 47 | name 48 | key 49 | segments { 50 | id 51 | key 52 | name 53 | envKey 54 | environmentId 55 | projKey 56 | projectId 57 | createdAt 58 | } 59 | } 60 | segments { 61 | id 62 | key 63 | name 64 | envKey 65 | environmentId 66 | projKey 67 | projectId 68 | createdAt 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/07-feature-flags/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/07-feature-flags/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/07-feature-flags/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example7", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen && yarn db:seed:dev", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "db:seed:dev": "ts-node tools/seed.ts", 11 | "playground": "yarn warthog playground", 12 | "start": "yarn start:ts", 13 | "start:debug": "yarn start:ts --inspect", 14 | "start:ts": "ts-node --type-check src/index.ts", 15 | "test": "jest --detectOpenHandles --verbose --coverage", 16 | "test:watch": "jest --watch", 17 | "//": "Allows us to use the local warthog CLI in commands above", 18 | "warthog": "../../bin/warthog" 19 | }, 20 | "dependencies": {}, 21 | "devDependencies": { 22 | "@types/faker": "^4.1.7", 23 | "@types/isomorphic-fetch": "^0.0.34", 24 | "@types/jest": "^23.3.14", 25 | "@types/node": "^10.17.5", 26 | "jest": "^23.6.0", 27 | "ts-node": "^8.10.2" 28 | }, 29 | "jest": { 30 | "transform": { 31 | ".ts": "ts-jest" 32 | }, 33 | "testRegex": "\\.test\\.ts$", 34 | "moduleFileExtensions": [ 35 | "ts", 36 | "js" 37 | ], 38 | "coveragePathIgnorePatterns": [ 39 | "/node_modules/", 40 | "\\.test\\.ts$" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/environment/environment.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Service } from 'typedi'; 2 | import { DeepPartial, Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../../src'; 6 | 7 | import { ProjectService } from '../project/project.service'; 8 | import { Environment } from './environment.model'; 9 | 10 | @Service('EnvironmentService') 11 | export class EnvironmentService extends BaseService { 12 | constructor( 13 | @InjectRepository(Environment) 14 | protected readonly repository: Repository, 15 | @Inject('ProjectService') readonly projectService: ProjectService 16 | ) { 17 | super(Environment, repository); 18 | } 19 | 20 | // Linking of Environment to Project happens via `projKey` 21 | async create(data: DeepPartial, userId: string): Promise { 22 | const project = await this.projectService.findOne({ key: data.projKey }); 23 | const payload = { ...data, projKey: project.key, projectId: project.id }; 24 | 25 | return super.create(payload, userId); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/feature-flag-user/feature-flag-user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, ManyToOne, Model, StringField } from '../../../../src'; 2 | 3 | import { Environment } from '../environment/environment.model'; 4 | import { FeatureFlag } from '../feature-flag/feature-flag.model'; 5 | import { Project } from '../project/project.model'; 6 | import { User } from '../user/user.model'; 7 | 8 | @Model() 9 | export class FeatureFlagUser extends BaseModel { 10 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 11 | featureKey: string; 12 | 13 | @ManyToOne( 14 | () => FeatureFlag, 15 | (featureFlag: FeatureFlag) => featureFlag.featureFlagUsers, 16 | { 17 | nullable: true, 18 | skipGraphQLField: true 19 | } 20 | ) 21 | featureFlag?: FeatureFlag; 22 | 23 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 24 | userKey: string; 25 | 26 | @ManyToOne( 27 | () => User, 28 | (user: User) => user.featureFlagUsers, 29 | { 30 | skipGraphQLField: true, 31 | nullable: true 32 | } 33 | ) 34 | user?: User; 35 | 36 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 37 | projKey: string; 38 | 39 | @ManyToOne( 40 | () => Project, 41 | (project: Project) => project.featureFlagUsers, 42 | { 43 | skipGraphQLField: true, 44 | nullable: true 45 | } 46 | ) 47 | project?: Project; 48 | 49 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 50 | envKey: string; 51 | 52 | @ManyToOne( 53 | () => Environment, 54 | (environment: Environment) => environment.featureFlagUsers, 55 | { 56 | nullable: true, 57 | skipGraphQLField: true 58 | } 59 | ) 60 | environment?: Environment; 61 | } 62 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/feature-flag/feature-flag.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, ManyToOne, Model, OneToMany, StringField } from '../../../../src'; 2 | 3 | import { Project } from '../project/project.model'; 4 | 5 | import { FeatureFlagSegment } from '../feature-flag-segment/feature-flag-segment.model'; 6 | import { FeatureFlagUser } from '../feature-flag-user/feature-flag-user.model'; 7 | 8 | @Model() 9 | export class FeatureFlag extends BaseModel { 10 | @StringField({ maxLength: 50, minLength: 2, nullable: false }) 11 | name: string; 12 | 13 | @StringField({ maxLength: 50, minLength: 2, nullable: false }) 14 | key: string; 15 | 16 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 17 | projKey: string; 18 | 19 | // TODO: this should not be nullable 20 | // TODO: this should not be exposed through the GraphQL either 21 | // TODO: should create "ManyToOneByKey" to join tables by a non-ID key 22 | @ManyToOne( 23 | () => Project, 24 | (project: Project) => project.featureFlags, 25 | { 26 | skipGraphQLField: true, 27 | nullable: true 28 | } 29 | ) 30 | project?: Project; 31 | 32 | @OneToMany( 33 | () => FeatureFlagUser, 34 | (featureFlagUser: FeatureFlagUser) => featureFlagUser.featureFlag 35 | ) 36 | featureFlagUsers?: FeatureFlagUser[]; 37 | 38 | @OneToMany( 39 | () => FeatureFlagSegment, 40 | (featureFlagSegment: FeatureFlagSegment) => featureFlagSegment.featureFlag 41 | ) 42 | featureFlagSegments?: FeatureFlagSegment[]; 43 | } 44 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import * as util from 'util'; 3 | 4 | import { getBindingError } from '../../../src'; 5 | 6 | export class Logger { 7 | static info(...args: any[]) { 8 | console.log(util.inspect(args, { showHidden: false, depth: null })); 9 | } 10 | 11 | static error(...args: any[]) { 12 | console.error(util.inspect(args, { showHidden: false, depth: null })); 13 | } 14 | 15 | // This takes a raw GraphQL error and pulls out the relevant info 16 | static logGraphQLError(error) { 17 | console.error(util.inspect(getBindingError(error), { showHidden: false, depth: null })); 18 | } 19 | } 20 | /* eslint-enable no-console */ 21 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/models.ts: -------------------------------------------------------------------------------- 1 | export { Environment } from './environment/environment.model'; 2 | export { FeatureFlag } from './feature-flag/feature-flag.model'; 3 | export { FeatureFlagSegment } from './feature-flag-segment/feature-flag-segment.model'; 4 | export { FeatureFlagUser } from './feature-flag-user/feature-flag-user.model'; 5 | export { Project } from './project/project.model'; 6 | export { Segment } from './segment/segment.model'; 7 | export { User } from './user/user.model'; 8 | export { UserSegment } from './user-segment/user-segment.model'; 9 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/project/project.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../../src'; 2 | 3 | import { Environment } from '../environment/environment.model'; 4 | import { FeatureFlagSegment } from '../feature-flag-segment/feature-flag-segment.model'; 5 | import { FeatureFlagUser } from '../feature-flag-user/feature-flag-user.model'; 6 | import { FeatureFlag } from '../feature-flag/feature-flag.model'; 7 | import { Segment } from '../segment/segment.model'; 8 | import { UserSegment } from '../user-segment/user-segment.model'; 9 | 10 | @Model() 11 | export class Project extends BaseModel { 12 | @StringField({ maxLength: 50, minLength: 3, nullable: false }) 13 | name: string; 14 | 15 | @StringField({ maxLength: 20, minLength: 3, nullable: false, unique: true }) 16 | key: string; 17 | 18 | @OneToMany( 19 | () => Environment, 20 | (environment: Environment) => environment.project 21 | ) 22 | environments?: Environment[]; 23 | 24 | @OneToMany( 25 | () => Segment, 26 | (segment: Segment) => segment.project 27 | ) 28 | segments?: Segment[]; 29 | 30 | @OneToMany( 31 | () => FeatureFlag, 32 | (featureFlag: FeatureFlag) => featureFlag.project 33 | ) 34 | featureFlags?: FeatureFlag[]; 35 | 36 | @OneToMany( 37 | () => FeatureFlagUser, 38 | (featureFlagUser: FeatureFlagUser) => featureFlagUser.project 39 | ) 40 | featureFlagUsers?: FeatureFlagUser[]; 41 | 42 | @OneToMany( 43 | () => FeatureFlagSegment, 44 | (featureFlagSegment: FeatureFlagSegment) => featureFlagSegment.project 45 | ) 46 | featureFlagSegments?: FeatureFlagSegment[]; 47 | 48 | @OneToMany( 49 | () => UserSegment, 50 | (userSegment: UserSegment) => userSegment.project 51 | ) 52 | userSegments?: UserSegment[]; 53 | } 54 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/project/project.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../../src'; 6 | 7 | import { Project } from './project.model'; 8 | 9 | @Service('ProjectService') 10 | export class ProjectService extends BaseService { 11 | constructor(@InjectRepository(Project) protected readonly repository: Repository) { 12 | super(Project, repository); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/segment/segment.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Service } from 'typedi'; 2 | import { DeepPartial, Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../../src'; 6 | 7 | import { EnvironmentService } from '../environment/environment.service'; 8 | import { ProjectService } from '../project/project.service'; 9 | 10 | import { Segment } from './segment.model'; 11 | 12 | @Service('SegmentService') 13 | export class SegmentService extends BaseService { 14 | constructor( 15 | @InjectRepository(Segment) protected readonly repository: Repository, 16 | @Inject('EnvironmentService') readonly environmentService: EnvironmentService, 17 | @Inject('ProjectService') readonly projectService: ProjectService 18 | ) { 19 | super(Segment, repository); 20 | } 21 | 22 | // Linking of Environment to Project happens via `projKey` 23 | async create(data: DeepPartial, userId: string): Promise { 24 | const environment = await this.environmentService.findOne({ 25 | key: data.envKey, 26 | projKey: data.projKey 27 | }); 28 | const project = await this.projectService.findOne({ key: data.projKey }); 29 | const payload = { 30 | ...data, 31 | envKey: environment.key, 32 | environmentId: environment.id, 33 | projKey: project.key, 34 | projectId: project.id 35 | }; 36 | 37 | return super.create(payload, userId); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { BaseContext, Server, ServerOptions } from '../../../src'; 4 | 5 | interface Context extends BaseContext { 6 | user: { 7 | email: string; 8 | id: string; 9 | permissions: string; 10 | }; 11 | } 12 | 13 | export function getServer(AppOptions: ServerOptions = {}, dbOptions = {}) { 14 | return new Server( 15 | { 16 | // Inject a fake user. In a real app you'd parse a JWT to add the user 17 | context: () => { 18 | return { 19 | user: { 20 | email: 'admin@test.com', 21 | id: 'abc12345', 22 | permissions: ['user:read', 'user:update', 'user:create', 'user:delete', 'photo:delete'] 23 | } 24 | }; 25 | }, 26 | ...AppOptions 27 | }, 28 | dbOptions 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/user-segment/user-segment.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, ManyToOne, Model, StringField } from '../../../../src'; 2 | 3 | // import {Environment, Project, Segment, User} from '../models' 4 | import { Environment } from '../environment/environment.model'; 5 | import { Project } from '../project/project.model'; 6 | import { Segment } from '../segment/segment.model'; 7 | import { User } from '../user/user.model'; 8 | 9 | @Model() 10 | export class UserSegment extends BaseModel { 11 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 12 | projKey: string; 13 | 14 | @ManyToOne( 15 | () => Project, 16 | (project: Project) => project.userSegments, 17 | { 18 | nullable: true, 19 | skipGraphQLField: true 20 | } 21 | ) 22 | project?: Project; 23 | 24 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 25 | envKey: string; 26 | 27 | @ManyToOne( 28 | () => Environment, 29 | (environment: Environment) => environment.userSegments, 30 | { 31 | nullable: true, 32 | skipGraphQLField: true 33 | } 34 | ) 35 | environment?: Environment; 36 | 37 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 38 | userKey: string; 39 | 40 | @ManyToOne( 41 | () => User, 42 | (user: User) => user.userSegments, 43 | { 44 | nullable: true, 45 | skipGraphQLField: true 46 | } 47 | ) 48 | user?: User; 49 | 50 | @StringField({ maxLength: 20, minLength: 3, nullable: false }) 51 | segmentKey: string; 52 | 53 | @ManyToOne( 54 | () => Segment, 55 | (segment: Segment) => segment.userSegments, 56 | { 57 | nullable: true, 58 | skipGraphQLField: true 59 | } 60 | ) 61 | segment?: Segment; 62 | } 63 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../../src'; 2 | 3 | // import { FeatureFlagUser, UserSegment } from '../models'; 4 | import { FeatureFlagUser } from '../feature-flag-user/feature-flag-user.model'; 5 | import { UserSegment } from '../user-segment/user-segment.model'; 6 | 7 | @Model() 8 | export class User extends BaseModel { 9 | @StringField({ maxLength: 20, minLength: 3, nullable: false, unique: true }) 10 | key: string; 11 | 12 | @OneToMany( 13 | () => FeatureFlagUser, 14 | (featureFlagUser: FeatureFlagUser) => featureFlagUser.user 15 | ) 16 | featureFlagUsers?: FeatureFlagUser[]; 17 | 18 | @OneToMany( 19 | () => UserSegment, 20 | (userSegments: UserSegment) => userSegments.user 21 | ) 22 | userSegments?: UserSegment[]; 23 | } 24 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/user/user.resolver.ts: -------------------------------------------------------------------------------- 1 | // import { GraphQLResolveInfo } from 'graphql'; 2 | import { Arg, Ctx, FieldResolver, Query, Resolver, Root } from 'type-graphql'; 3 | import { Inject } from 'typedi'; 4 | 5 | import { BaseContext } from '../../../../src'; 6 | 7 | import { UserWhereUniqueInput } from '../../generated'; 8 | 9 | import { UserSegment } from '../user-segment/user-segment.model'; 10 | 11 | import { User } from './user.model'; 12 | import { UserService } from './user.service'; 13 | 14 | @Resolver(User) 15 | export class UserResolver { 16 | constructor(@Inject('UserService') readonly service: UserService) {} 17 | 18 | @FieldResolver(() => [UserSegment]) 19 | userSegments(@Root() user: User, @Ctx() ctx: BaseContext): Promise { 20 | return ctx.dataLoader.loaders.User.userSegments.load(user); 21 | } 22 | 23 | @Query(() => User) 24 | async user(@Arg('where') where: UserWhereUniqueInput): Promise { 25 | return this.service.findOne(where); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/07-feature-flags/src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { DeepPartial, Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../../src'; 6 | 7 | import { User } from './user.model'; 8 | 9 | @Service('UserService') 10 | export class UserService extends BaseService { 11 | constructor(@InjectRepository(User) protected readonly repository: Repository) { 12 | super(User, repository); 13 | } 14 | 15 | async findOrCreate(data: DeepPartial, userId: string): Promise { 16 | const users = await this.find(data); 17 | if (users && users.length > 0) { 18 | return users[0]; 19 | } 20 | 21 | return this.create(data, userId); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/07-feature-flags/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": false, // not sure why this is blowing up 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/07-feature-flags/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cliGeneratePath: './src/${kebabName}', 3 | moduleImportPath: '../../../src' 4 | }; 5 | -------------------------------------------------------------------------------- /examples/08-performance/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-example-8 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | -------------------------------------------------------------------------------- /examples/08-performance/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "ts-node-7", 6 | "type": "node", 7 | "request": "launch", 8 | "args": ["${relativeFile}"], 9 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 10 | "sourceMaps": true, 11 | "cwd": "${workspaceRoot}", 12 | "protocol": "inspector", 13 | "env": { 14 | "TS_NODE_IGNORE": "false", 15 | "DEBUG": "*" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /examples/08-performance/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "search.exclude": { 4 | "dist/": true, 5 | "node_modules/": true 6 | }, 7 | "[javascript]": { 8 | "editor.formatOnSave": true 9 | }, 10 | "[json]": { 11 | "editor.formatOnSave": true 12 | }, 13 | "[typescript]": { 14 | "editor.formatOnSave": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/08-performance/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "command": "source tools/environment_setup.sh", 6 | "type": "shell", 7 | "label": "setupEnvironment" 8 | }, 9 | { 10 | "label": "build", 11 | "type": "typescript", 12 | "tsconfig": "tsconfig.json", 13 | "problemMatcher": ["$tsc"], 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /examples/08-performance/README.md: -------------------------------------------------------------------------------- 1 | # Example 3 - Relationships 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | 14 | ## Running the App 15 | 16 | To run the project, run `yarn start`. This will: 17 | 18 | - Run the API server 19 | - Open GraphQL Playground 20 | 21 | ## Example Queries/Mutations 22 | 23 | You can find some examples in [examples.gql](./examples.gql) 24 | -------------------------------------------------------------------------------- /examples/08-performance/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | users(orderBy: createdAt_DESC) { 3 | id 4 | firstName 5 | posts { 6 | id 7 | title 8 | createdAt 9 | } 10 | createdAt 11 | } 12 | 13 | posts(where: { userId_in: [""] }, orderBy: createdAt_DESC) { 14 | id 15 | title 16 | userId 17 | createdAt 18 | } 19 | } 20 | 21 | mutation { 22 | createUser(data: { firstName: "TestUser" }) { 23 | id 24 | firstName 25 | createdAt 26 | } 27 | 28 | createPost(data: { title: "Hello World", userId: "" }) { 29 | id 30 | title 31 | createdAt 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/08-performance/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/08-performance/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/08-performance/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["dist/*", "generated/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/08-performance/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example8", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen && yarn db:seed:dev", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "db:seed:dev": "ts-node tools/seed.ts", 11 | "playground": "yarn warthog playground", 12 | "start": "yarn start:ts", 13 | "start:ts": "ts-node --type-check src/index.ts", 14 | "//": "Allows us to use the local warthog CLI in commands above", 15 | "warthog": "../../bin/warthog" 16 | }, 17 | "dependencies": { 18 | "handlebars": "^4.5.2", 19 | "lodash": "^4.17.15", 20 | "reflect-metadata": "^0.1.13", 21 | "typescript": "^3.7.2" 22 | }, 23 | "devDependencies": { 24 | "@types/faker": "^4.1.7", 25 | "@types/isomorphic-fetch": "^0.0.34", 26 | "@types/jest": "^23.3.14", 27 | "@types/node": "^10.17.5", 28 | "faker": "^4.1.0", 29 | "jest": "^23.6.0", 30 | "ts-jest": "^23.10.5", 31 | "ts-node": "^8.10.2" 32 | }, 33 | "jest": { 34 | "transform": { 35 | ".ts": "ts-jest" 36 | }, 37 | "testRegex": "\\.test\\.ts$", 38 | "moduleFileExtensions": [ 39 | "ts", 40 | "js" 41 | ], 42 | "coveragePathIgnorePatterns": [ 43 | "/node_modules/", 44 | "\\.test\\.ts$" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/08-performance/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/08-performance/src/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import * as util from 'util'; 3 | 4 | import { getBindingError } from '../../../src'; 5 | 6 | export class Logger { 7 | static info(...args: any[]) { 8 | console.log(util.inspect(args, { showHidden: false, depth: null })); 9 | } 10 | 11 | static error(...args: any[]) { 12 | console.error(util.inspect(args, { showHidden: false, depth: null })); 13 | } 14 | 15 | // This takes a raw GraphQL error and pulls out the relevant info 16 | static logGraphQLError(error) { 17 | console.error(util.inspect(getBindingError(error), { showHidden: false, depth: null })); 18 | } 19 | } 20 | /* eslint-enable no-console */ 21 | -------------------------------------------------------------------------------- /examples/08-performance/src/models.ts: -------------------------------------------------------------------------------- 1 | export { Post } from './post.model'; 2 | export { User } from './user.model'; 3 | -------------------------------------------------------------------------------- /examples/08-performance/src/modules/post/post.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, ManyToOne, Model, StringField } from '../../../../../src'; 2 | 3 | import { User } from '../user/user.model'; 4 | 5 | @Model() 6 | export class Post extends BaseModel { 7 | @StringField({ maxLength: 256 }) 8 | title?: string; 9 | 10 | @ManyToOne( 11 | () => User, 12 | (user: User) => user.posts, 13 | { nullable: false } 14 | ) 15 | user?: User; 16 | } 17 | -------------------------------------------------------------------------------- /examples/08-performance/src/modules/post/post.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../../../src'; 6 | import { Post } from './post.model'; 7 | 8 | @Service('PostService') 9 | export class PostService extends BaseService { 10 | constructor(@InjectRepository(Post) protected readonly repository: Repository) { 11 | super(Post, repository); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/08-performance/src/modules/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../../../src'; 2 | 3 | import { Post } from '../post/post.model'; 4 | 5 | @Model() 6 | export class User extends BaseModel { 7 | @StringField() 8 | firstName?: string; 9 | 10 | @OneToMany( 11 | () => Post, 12 | (post: Post) => post.user 13 | ) 14 | posts?: Post[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/08-performance/src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../../../src'; 6 | 7 | import { User } from './user.model'; 8 | 9 | @Service('UserService') 10 | export class UserService extends BaseService { 11 | constructor(@InjectRepository(User) protected readonly repository: Repository) { 12 | super(User, repository); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/08-performance/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { BaseContext, Server } from '../../../src'; 4 | 5 | interface Context extends BaseContext { 6 | user: { 7 | email: string; 8 | id: string; 9 | permissions: string; 10 | }; 11 | } 12 | 13 | export function getServer(AppOptions = {}, dbOptions = {}) { 14 | return new Server( 15 | { 16 | // Inject a fake user. In a real app you'd parse a JWT to add the user 17 | context: () => { 18 | return { 19 | user: { 20 | id: 'abc12345' 21 | } 22 | }; 23 | }, 24 | ...AppOptions 25 | }, 26 | dbOptions 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/08-performance/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": false, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/08-performance/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | allowOptionalIdOnCreate: 'true', 3 | cliGeneratePath: './src/modules/${kebabName}', 4 | moduleImportPath: '../../../src' 5 | }; 6 | -------------------------------------------------------------------------------- /examples/09-production/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_APP_PROTOCOL=http 6 | WARTHOG_DB_DATABASE=warthog-example-9 7 | WARTHOG_DB_HOST=localhost 8 | WARTHOG_DB_LOGGING=none 9 | WARTHOG_DB_PASSWORD= 10 | WARTHOG_DB_SYNCHRONIZE=true 11 | WARTHOG_DB_USERNAME=postgres 12 | -------------------------------------------------------------------------------- /examples/09-production/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "ts-node-7", 6 | "type": "node", 7 | "request": "launch", 8 | "args": ["${relativeFile}"], 9 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 10 | "sourceMaps": true, 11 | "cwd": "${workspaceRoot}", 12 | "protocol": "inspector", 13 | "env": { 14 | "TS_NODE_IGNORE": "false", 15 | "DEBUG": "*" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /examples/09-production/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "search.exclude": { 4 | "dist/": true, 5 | "node_modules/": true 6 | }, 7 | "[javascript]": { 8 | "editor.formatOnSave": true 9 | }, 10 | "[json]": { 11 | "editor.formatOnSave": true 12 | }, 13 | "[typescript]": { 14 | "editor.formatOnSave": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/09-production/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "command": "source tools/environment_setup.sh", 6 | "type": "shell", 7 | "label": "setupEnvironment" 8 | }, 9 | { 10 | "label": "build", 11 | "type": "typescript", 12 | "tsconfig": "tsconfig.json", 13 | "problemMatcher": ["$tsc"], 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /examples/09-production/README.md: -------------------------------------------------------------------------------- 1 | # Example 3 - Relationships 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | 14 | ## Running the App 15 | 16 | To run the project, run `yarn start`. This will: 17 | 18 | - Run the API server 19 | - Open GraphQL Playground 20 | 21 | ## Example Queries/Mutations 22 | 23 | You can find some examples in [examples.gql](./examples.gql) 24 | -------------------------------------------------------------------------------- /examples/09-production/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | users(orderBy: createdAt_DESC) { 3 | id 4 | firstName 5 | posts { 6 | id 7 | title 8 | createdAt 9 | } 10 | createdAt 11 | } 12 | 13 | posts(where: { userId_in: [""] }, orderBy: createdAt_DESC) { 14 | id 15 | title 16 | userId 17 | createdAt 18 | } 19 | } 20 | 21 | mutation { 22 | createUser(data: { firstName: "TestUser" }) { 23 | id 24 | firstName 25 | createdAt 26 | } 27 | 28 | createPost(data: { title: "Hello World", userId: "" }) { 29 | id 30 | title 31 | createdAt 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/09-production/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/09-production/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/09-production/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["dist/*", "generated/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/09-production/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example8", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen && yarn db:seed:dev", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "WARTHOG_DB_OVERRIDE=true yarn warthog db:create", 9 | "db:drop": "WARTHOG_DB_OVERRIDE=true yarn warthog db:drop", 10 | "db:seed:dev": "WARTHOG_DB_OVERRIDE=true ts-node tools/seed.ts", 11 | "playground": "yarn warthog playground", 12 | "start": "yarn start:ts", 13 | "start:ts": "ts-node --type-check src/index.ts", 14 | "//": "Allows us to use the local warthog CLI in commands above", 15 | "warthog": "../../bin/warthog" 16 | }, 17 | "dependencies": { 18 | "handlebars": "^4.5.2", 19 | "lodash": "^4.17.15", 20 | "reflect-metadata": "^0.1.13", 21 | "typescript": "^3.7.2" 22 | }, 23 | "devDependencies": { 24 | "@types/faker": "^4.1.7", 25 | "@types/isomorphic-fetch": "^0.0.34", 26 | "@types/jest": "^23.3.14", 27 | "@types/node": "^10.17.5", 28 | "faker": "^4.1.0", 29 | "jest": "^23.6.0", 30 | "ts-jest": "^23.10.5", 31 | "ts-node": "^8.10.2" 32 | }, 33 | "jest": { 34 | "transform": { 35 | ".ts": "ts-jest" 36 | }, 37 | "testRegex": "\\.test\\.ts$", 38 | "moduleFileExtensions": [ 39 | "ts", 40 | "js" 41 | ], 42 | "coveragePathIgnorePatterns": [ 43 | "/node_modules/", 44 | "\\.test\\.ts$" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/09-production/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer(); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/09-production/src/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import * as util from 'util'; 3 | 4 | import { getBindingError } from '../../../src'; 5 | 6 | export class Logger { 7 | static info(...args: any[]) { 8 | args = args.length === 1 ? args[0] : args; 9 | console.log(util.inspect(args, { showHidden: false, depth: null })); 10 | } 11 | 12 | static error(...args: any[]) { 13 | args = args.length === 1 ? args[0] : args; 14 | console.error(util.inspect(args, { showHidden: false, depth: null })); 15 | } 16 | 17 | static debug(...args: any[]) { 18 | console.debug(args); 19 | } 20 | 21 | static log(...args: any[]) { 22 | console.log(args); 23 | } 24 | 25 | static warn(...args: any[]) { 26 | console.warn(args); 27 | } 28 | 29 | // This takes a raw GraphQL error and pulls out the relevant info 30 | static logGraphQLError(error: Error) { 31 | console.error(util.inspect(getBindingError(error), { showHidden: false, depth: null })); 32 | } 33 | } 34 | /* eslint-enable no-console */ 35 | -------------------------------------------------------------------------------- /examples/09-production/src/models.ts: -------------------------------------------------------------------------------- 1 | export { Post } from './modules/post/post.model'; 2 | export { User } from './modules/user/user.model'; 3 | -------------------------------------------------------------------------------- /examples/09-production/src/modules/post/post.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, ManyToOne, Model, StringField } from '../../../../../src'; 2 | 3 | import { User } from '../user/user.model'; 4 | 5 | @Model() 6 | export class Post extends BaseModel { 7 | @StringField({ maxLength: 256 }) 8 | title?: string; 9 | 10 | @ManyToOne( 11 | () => User, 12 | (user: User) => user.posts, 13 | { nullable: false } 14 | ) 15 | user?: User; 16 | } 17 | -------------------------------------------------------------------------------- /examples/09-production/src/modules/post/post.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../../../src'; 6 | import { Post } from './post.model'; 7 | 8 | @Service('PostService') 9 | export class PostService extends BaseService { 10 | constructor(@InjectRepository(Post) protected readonly repository: Repository) { 11 | super(Post, repository); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/09-production/src/modules/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, OneToMany, StringField } from '../../../../../src'; 2 | 3 | import { Post } from '../post/post.model'; 4 | 5 | @Model() 6 | export class User extends BaseModel { 7 | @StringField() 8 | firstName?: string; 9 | 10 | @OneToMany( 11 | () => Post, 12 | (post: Post) => post.user 13 | ) 14 | posts?: Post[]; 15 | } 16 | -------------------------------------------------------------------------------- /examples/09-production/src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../../../src'; 6 | 7 | import { User } from './user.model'; 8 | 9 | @Service('UserService') 10 | export class UserService extends BaseService { 11 | constructor(@InjectRepository(User) protected readonly repository: Repository) { 12 | super(User, repository); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/09-production/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { BaseContext, Server } from '../../../src'; 4 | 5 | interface Context extends BaseContext { 6 | user: { 7 | email: string; 8 | id: string; 9 | permissions: string; 10 | }; 11 | } 12 | 13 | export function getServer(AppOptions = {}, dbOptions = {}) { 14 | return new Server( 15 | { 16 | // Inject a fake user. In a real app you'd parse a JWT to add the user 17 | context: () => { 18 | return { 19 | user: { 20 | id: 'abc12345' 21 | } 22 | }; 23 | }, 24 | ...AppOptions 25 | }, 26 | dbOptions 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/09-production/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "./src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": false, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/09-production/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cliGeneratePath: './src/modules/${kebabName}', 3 | moduleImportPath: '../../../src' 4 | }; 5 | -------------------------------------------------------------------------------- /examples/10-subscriptions/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-10-subscriptions 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | WARTHOG_SUBSCRIPTIONS=true 10 | -------------------------------------------------------------------------------- /examples/10-subscriptions/README.md: -------------------------------------------------------------------------------- 1 | ## Example 10 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `DEBUG=* yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | - Seed the database with test data 14 | 15 | ## Running the App 16 | 17 | To run the project, run `yarn start`. This will: 18 | 19 | - Run the API server 20 | - Open GraphQL Playground 21 | -------------------------------------------------------------------------------- /examples/10-subscriptions/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/10-subscriptions/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/10-subscriptions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-10", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "bootstrap": "cd ../.. && yarn && cd - && yarn && yarn db:create && yarn codegen && yarn db:seed:dev", 7 | "codegen": "yarn warthog codegen", 8 | "db:create": "yarn warthog db:create", 9 | "db:drop": "yarn warthog db:drop", 10 | "db:seed:dev": "ts-node tools/seed.ts", 11 | "playground": "yarn warthog playground", 12 | "start": "yarn start:ts", 13 | "start:debug": "yarn start:ts --inspect", 14 | "start:ts": "DEBUG=warthog* ts-node-dev --type-check src/index.ts", 15 | "test": "jest --detectOpenHandles --verbose --coverage", 16 | "test:watch": "jest --watch", 17 | "//": "Allows us to use the local warthog CLI in commands above", 18 | "warthog": "../../bin/warthog" 19 | }, 20 | "dependencies": { 21 | "debug": "^4.1.1", 22 | "graphql-subscriptions": "^1.1.0", 23 | "handlebars": "^4.5.2", 24 | "lodash": "^4.17.15", 25 | "reflect-metadata": "^0.1.13", 26 | "typescript": "^3.7.2" 27 | }, 28 | "devDependencies": { 29 | "@types/faker": "^4.1.7", 30 | "@types/isomorphic-fetch": "^0.0.34", 31 | "@types/jest": "^23.3.14", 32 | "@types/node": "^10.17.5", 33 | "faker": "^4.1.0", 34 | "jest": "^23.6.0", 35 | "ts-jest": "^23.10.5", 36 | "ts-node": "^8.10.2", 37 | "ts-node-dev": "^1.0.0-pre.44" 38 | }, 39 | "jest": { 40 | "transform": { 41 | ".ts": "ts-jest" 42 | }, 43 | "testRegex": "\\.test\\.ts$", 44 | "moduleFileExtensions": [ 45 | "ts", 46 | "js" 47 | ], 48 | "coveragePathIgnorePatterns": [ 49 | "/node_modules/", 50 | "\\.test\\.ts$" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/10-subscriptions/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer({ subscriptions: true }); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/10-subscriptions/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Server } from '../../../src'; 4 | 5 | export function getServer(appOptions = {}, dbOptions = {}) { 6 | return new Server( 7 | { 8 | ...appOptions, 9 | context: () => { 10 | return { 11 | user: { 12 | id: 'abc123' 13 | } 14 | }; 15 | } 16 | }, 17 | dbOptions 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/10-subscriptions/src/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, StringField } from '../../../src'; 2 | 3 | @Model() 4 | export class User extends BaseModel { 5 | @StringField({ maxLength: 30 }) 6 | firstName?: string; 7 | 8 | @StringField({ maxLength: 50, minLength: 2 }) 9 | lastName?: string; 10 | } 11 | -------------------------------------------------------------------------------- /examples/10-subscriptions/src/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { DeepPartial, Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../src'; 6 | 7 | import { User } from './user.model'; 8 | 9 | @Service('UserService') 10 | export class UserService extends BaseService { 11 | constructor(@InjectRepository(User) protected readonly repository: Repository) { 12 | super(User, repository); 13 | } 14 | 15 | async create(data: DeepPartial, userId: string): Promise { 16 | const newUser = await super.create(data, userId); 17 | 18 | // Perform some side effects 19 | 20 | return newUser; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/10-subscriptions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*", "tools/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/10-subscriptions/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/11-transactions/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-11-transactions 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | WARTHOG_SUBSCRIPTIONS=true 10 | -------------------------------------------------------------------------------- /examples/11-transactions/README.md: -------------------------------------------------------------------------------- 1 | ## Example 11 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `DEBUG=* yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | - Seed the database with test data 14 | 15 | ## Running the App 16 | 17 | To run the project, run `yarn start`. This will: 18 | 19 | - Run the API server 20 | - Open GraphQL Playground 21 | -------------------------------------------------------------------------------- /examples/11-transactions/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/11-transactions/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/11-transactions/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer({ subscriptions: true }); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/11-transactions/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Server } from '../../../src'; 4 | 5 | export function getServer(appOptions = {}, dbOptions = {}) { 6 | return new Server( 7 | { 8 | ...appOptions, 9 | context: () => { 10 | return { 11 | user: { 12 | id: 'abc123' 13 | } 14 | }; 15 | } 16 | }, 17 | dbOptions 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/11-transactions/src/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, StringField } from '../../../src'; 2 | 3 | @Model() 4 | export class User extends BaseModel { 5 | @StringField({ maxLength: 30 }) 6 | firstName?: string; 7 | 8 | @StringField({ maxLength: 50, minLength: 2 }) 9 | lastName?: string; 10 | } 11 | -------------------------------------------------------------------------------- /examples/11-transactions/src/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Args, Ctx, Mutation, Query, Resolver } from 'type-graphql'; 2 | import { Inject } from 'typedi'; 3 | 4 | import { BaseContext } from '../../../src'; 5 | 6 | import { UserCreateInput, UserWhereArgs, UserWhereInput } from '../generated'; 7 | 8 | import { User } from './user.model'; 9 | import { UserService } from './user.service'; 10 | 11 | @Resolver(User) 12 | export class UserResolver { 13 | constructor(@Inject('UserService') readonly service: UserService) {} 14 | 15 | @Query(() => [User]) 16 | async users(@Args() { where, orderBy, limit, offset }: UserWhereArgs): Promise { 17 | return this.service.find(where, orderBy, limit, offset); 18 | } 19 | 20 | // If this was User instead of [User] you get "Cannot return null for non-nullable field User.id" 21 | @Mutation(() => [User]) 22 | async successfulTransaction( 23 | @Arg('data') data: UserCreateInput, 24 | @Ctx() ctx: BaseContext 25 | ): Promise { 26 | return this.service.successfulTransaction(data, ctx.user.id); 27 | } 28 | 29 | @Mutation(() => [User]) 30 | async failedTransaction( 31 | @Arg('data') data: UserCreateInput, 32 | @Ctx() ctx: BaseContext 33 | ): Promise { 34 | return this.service.failedTransaction(data, ctx.user.id); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/11-transactions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*", "tools/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/11-transactions/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/12-relay-connection/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PGUSER=postgres 3 | WARTHOG_APP_HOST=localhost 4 | WARTHOG_APP_PORT=4100 5 | WARTHOG_DB_DATABASE=warthog-12-relay-example 6 | WARTHOG_DB_USERNAME=postgres 7 | WARTHOG_DB_PASSWORD= 8 | WARTHOG_DB_SYNCHRONIZE=true 9 | WARTHOG_SUBSCRIPTIONS=true 10 | -------------------------------------------------------------------------------- /examples/12-relay-connection/README.md: -------------------------------------------------------------------------------- 1 | ## Example 12 2 | 3 | ## Setup 4 | 5 | Run `yarn bootstrap && yarn start` 6 | 7 | ## Bootstrapping the App 8 | 9 | Running `DEBUG=* yarn bootstrap` will do the following: 10 | 11 | - Install packages 12 | - Create the example DB 13 | - Seed the database with test data 14 | 15 | ## Running the App 16 | 17 | To run the project, run `yarn start`. This will: 18 | 19 | - Run the API server 20 | - Open GraphQL Playground 21 | -------------------------------------------------------------------------------- /examples/12-relay-connection/examples.gql: -------------------------------------------------------------------------------- 1 | query { 2 | UserConnection(where: { firstName_contains: "a" }, orderBy: createdAt_DESC) { 3 | nodes { 4 | id 5 | firstName 6 | lastName 7 | } 8 | pageInfo { 9 | limit 10 | offset 11 | totalCount 12 | hasNextPage 13 | hasPreviousPage 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/12-relay-connection/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /examples/12-relay-connection/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../src'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /examples/12-relay-connection/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { logger } from '../../../src'; 4 | 5 | import { getServer } from './server'; 6 | 7 | async function bootstrap() { 8 | const server = getServer({ subscriptions: true }); 9 | await server.start(); 10 | } 11 | 12 | bootstrap().catch((error: Error) => { 13 | logger.error(error); 14 | if (error.stack) { 15 | logger.error(error.stack.split('\n')); 16 | } 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/12-relay-connection/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Server } from '../../../src'; 4 | 5 | export function getServer(appOptions = {}, dbOptions = {}) { 6 | return new Server( 7 | { 8 | ...appOptions, 9 | context: () => { 10 | return { 11 | user: { 12 | id: 'abc123' 13 | } 14 | }; 15 | } 16 | }, 17 | dbOptions 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/12-relay-connection/src/user.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, StringField } from '../../../src'; 2 | 3 | @Model() 4 | export class User extends BaseModel { 5 | @StringField({ maxLength: 30 }) 6 | firstName?: string; 7 | 8 | @StringField({ maxLength: 50, minLength: 2 }) 9 | lastName?: string; 10 | } 11 | -------------------------------------------------------------------------------- /examples/12-relay-connection/src/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Field, ObjectType, Query, Resolver } from 'type-graphql'; 2 | import { Inject } from 'typedi'; 3 | 4 | import { PageInfo, ConnectionResult } from '../../../src'; 5 | 6 | import { UserWhereArgs, UserWhereInput } from '../generated'; 7 | 8 | import { User } from './user.model'; 9 | import { UserService } from './user.service'; 10 | 11 | @ObjectType() 12 | export class UserConnection implements ConnectionResult { 13 | @Field(() => [User], { nullable: false }) 14 | nodes!: User[]; 15 | 16 | @Field(() => PageInfo, { nullable: false }) 17 | pageInfo!: PageInfo; 18 | } 19 | 20 | @Resolver(User) 21 | export class UserResolver { 22 | constructor(@Inject('UserService') readonly service: UserService) {} 23 | 24 | @Query(() => UserConnection) 25 | async UserConnection( 26 | @Args() { where, orderBy, limit, offset }: UserWhereArgs 27 | ): Promise { 28 | return this.service.findConnection(where, orderBy, limit, offset); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/12-relay-connection/src/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../src'; 6 | 7 | import { User } from './user.model'; 8 | 9 | @Service('UserService') 10 | export class UserService extends BaseService { 11 | constructor(@InjectRepository(User) protected readonly repository: Repository) { 12 | super(User, repository); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/12-relay-connection/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "lib": ["dom", "es5", "es6", "es7", "esnext", "esnext.asynciterable"], 6 | "target": "es5", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "keyofStringsOnly": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": false, 20 | "types": ["jest", "isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*", "tools/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/12-relay-connection/warthog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleImportPath: '../../../src' 3 | }; 4 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | All examples can be bootstapped and run with `yarn bootstrap && yarn start` 4 | 5 | Since Warthog uses TypeORM and TypeGraphQL out of the box, you can also 6 | Eject from Warthog for custom things and use their examples: 7 | 8 | - [TypeORM](https://github.com/typeorm/typeorm/tree/master/sample) 9 | - [TypeGraphQL](https://github.com/19majkel94/type-graphql/tree/master/examples) 10 | -------------------------------------------------------------------------------- /img/warthog-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldcaddy77/warthog/72fad57f9b5df20576286853416d60c24882e328/img/warthog-logo.png -------------------------------------------------------------------------------- /src/cli/README.md: -------------------------------------------------------------------------------- 1 | # Run the following commands 2 | 3 | ```bash 4 | warthog g --name FeatureFlag 5 | 6 | rm -rf generated 7 | ``` 8 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'gluegun'; 2 | 3 | /** 4 | * Create the cli and kick it off 5 | */ 6 | export async function run(argv: string[]) { 7 | // create a CLI runtime 8 | const cli = build() 9 | .brand('warthog') 10 | .src(__dirname) 11 | // .plugins('./node_modules', { matching: 'warthog-*', hidden: true }) 12 | .help() // provides default for help, h, --help, -h 13 | .version() // provides default for version, v, --version, -v 14 | .create(); 15 | 16 | // and run it 17 | const toolbox = await cli.run(argv); 18 | 19 | // send it back (for testing, mostly) 20 | return toolbox; 21 | } 22 | -------------------------------------------------------------------------------- /src/cli/commands/codegen.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../../core'; 2 | import { CodeGenerator } from '../../core/code-generator'; 3 | 4 | import { WarthogGluegunToolbox } from '../types'; 5 | 6 | // BLOG: needed to switch from module.exports because it didn't compile correctly 7 | export default { 8 | // module.exports = { 9 | name: 'codegen', 10 | run: async (toolbox: WarthogGluegunToolbox) => { 11 | const { 12 | config: { load } 13 | } = toolbox; 14 | 15 | const config = load(); 16 | 17 | try { 18 | await new CodeGenerator(config.get('GENERATED_FOLDER'), config.get('DB_ENTITIES'), { 19 | resolversPath: config.get('RESOLVERS_PATH'), 20 | validateResolvers: config.get('VALIDATE_RESOLVERS') === 'true', 21 | warthogImportPath: config.get('MODULE_IMPORT_PATH') 22 | }).generate(); 23 | } catch (error) { 24 | logger.error(error); 25 | if (error.name.indexOf('Cannot determine GraphQL input type') > -1) { 26 | logger.error('This often means you have multiple versions of TypeGraphQL installed.'); 27 | } 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/cli/commands/dbCreate.ts: -------------------------------------------------------------------------------- 1 | import { WarthogGluegunToolbox } from '../types'; 2 | 3 | export default { 4 | name: 'db:create', 5 | run: async (toolbox: WarthogGluegunToolbox) => { 6 | const { 7 | db, 8 | config: { load } 9 | } = toolbox; 10 | 11 | const config = load(); 12 | 13 | await db.create(config.get('DB_DATABASE')); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/cli/commands/dbDrop.ts: -------------------------------------------------------------------------------- 1 | import { WarthogGluegunToolbox } from '../types'; 2 | 3 | export default { 4 | name: 'db:drop', 5 | run: async (toolbox: WarthogGluegunToolbox) => { 6 | const { db } = toolbox; 7 | await db.drop(); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/cli/commands/migrate.ts: -------------------------------------------------------------------------------- 1 | // "db:migration:generate": "yarn typeorm:cli migration:generate -n migration1", 2 | // ts-node ./node_modules/.bin/typeorm -f ./generated/ormconfig.ts 3 | 4 | import { WarthogGluegunToolbox } from '../types'; 5 | 6 | export default { 7 | name: 'db:migrate', 8 | run: async (toolbox: WarthogGluegunToolbox) => { 9 | const { 10 | db, 11 | print: { error } 12 | } = toolbox; 13 | 14 | try { 15 | await db.migrate(); 16 | } catch (e) { 17 | error(e); 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/cli/commands/migrationGenerate.ts: -------------------------------------------------------------------------------- 1 | // "db:migration:generate": "yarn typeorm:cli migration:generate -n migration1", 2 | // ts-node ./node_modules/.bin/typeorm -f ./generated/ormconfig.ts 3 | 4 | import { WarthogGluegunToolbox } from '../types'; 5 | 6 | export default { 7 | name: 'db:migrate:generate', 8 | run: async (toolbox: WarthogGluegunToolbox) => { 9 | const { 10 | db, 11 | parameters: { options }, 12 | print: { error } 13 | } = toolbox; 14 | 15 | if (!options.name) { 16 | return error('"name" option is required'); 17 | } 18 | 19 | try { 20 | await db.generateMigration(options.name); 21 | } catch (e) { 22 | error(e); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/cli/commands/playground.ts: -------------------------------------------------------------------------------- 1 | const open = require('open'); // eslint-disable-line @typescript-eslint/no-var-requires 2 | 3 | import { WarthogGluegunToolbox } from '../types'; 4 | 5 | export default { 6 | name: 'playground', 7 | run: async (toolbox: WarthogGluegunToolbox) => { 8 | const config: any = toolbox.config.load(); 9 | 10 | const host = config.get('APP_HOST'); 11 | const port = config.get('APP_PORT'); 12 | const url = `http://${host}:${port}/playground`; 13 | 14 | return open(url, { wait: false }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/cli/commands/warthog.ts: -------------------------------------------------------------------------------- 1 | import { GluegunToolbox } from 'gluegun'; 2 | 3 | export default { 4 | name: 'warthog', 5 | run: async (toolbox: GluegunToolbox) => { 6 | const { print } = toolbox; 7 | 8 | print.info('Warthog: GraphQL API Framework'); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/cli/docs/commands.md: -------------------------------------------------------------------------------- 1 | # Command Reference for warthog 2 | 3 | TODO: Add your command reference here 4 | -------------------------------------------------------------------------------- /src/cli/docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugin guide for warthog 2 | 3 | Plugins allow you to add features to warthog, such as commands and 4 | extensions to the `toolbox` object that provides the majority of the functionality 5 | used by warthog. 6 | 7 | Creating a warthog plugin is easy. Just create a repo with two folders: 8 | 9 | ``` 10 | commands/ 11 | extensions/ 12 | ``` 13 | 14 | A command is a file that looks something like this: 15 | 16 | ```js 17 | // commands/foo.js 18 | 19 | module.exports = { 20 | run: (toolbox) => { 21 | const { print, filesystem } = toolbox 22 | 23 | const desktopDirectories = filesystem.subdirectories(`~/Desktop`) 24 | print.info(desktopDirectories) 25 | } 26 | } 27 | ``` 28 | 29 | An extension lets you add additional features to the `toolbox`. 30 | 31 | ```js 32 | // extensions/bar-extension.js 33 | 34 | module.exports = (toolbox) => { 35 | const { print } = toolbox 36 | 37 | toolbox.bar = () => { print.info('Bar!') } 38 | } 39 | ``` 40 | 41 | This is then accessible in your plugin's commands as `toolbox.bar`. 42 | 43 | # Loading a plugin 44 | 45 | To load a particular plugin (which has to start with `warthog-*`), 46 | install it to your project using `npm install --save-dev warthog-PLUGINNAME`, 47 | and warthog will pick it up automatically. 48 | -------------------------------------------------------------------------------- /src/cli/extensions/config-extension.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../core'; 2 | 3 | import { WarthogGluegunToolbox } from '../types'; 4 | 5 | module.exports = (toolbox: WarthogGluegunToolbox) => { 6 | toolbox.config = { 7 | load: function create() { 8 | return new Config(); 9 | } 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/cli/extensions/string-extension.ts: -------------------------------------------------------------------------------- 1 | import { WarthogGluegunToolbox } from '../types'; 2 | 3 | module.exports = (toolbox: WarthogGluegunToolbox) => { 4 | toolbox.string = { 5 | supplant: function supplant(str: string, obj: Record) { 6 | return str.replace(/\${([^${}]*)}/g, (a, b) => { 7 | const r = obj[b]; 8 | return typeof r === 'string' ? r : a; 9 | }); 10 | } 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/cli/templates/generate/service.ts.ejs: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | import { BaseService } from '<%= props.warthogPathInSourceFiles %>'; 5 | 6 | import { <%= props.className %> } from './<%= props.kebabName %>.model'; 7 | 8 | @Service('<%= props.className %>Service') 9 | export class <%= props.className %>Service extends BaseService<<%= props.className %>> { 10 | constructor( 11 | @InjectRepository(<%= props.className %>) protected readonly repository: Repository<<%= props.className %>> 12 | ) { 13 | super(<%= props.className %>, repository); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cli/templates/new/_env.ejs: -------------------------------------------------------------------------------- 1 | DEBUG=* 2 | PGUSER=postgres 3 | NODE_ENV=development 4 | WARTHOG_AUTO_OPEN_PLAYGROUND=false 5 | WARTHOG_AUTO_GENERATE_FILES=false 6 | WARTHOG_APP_HOST=localhost 7 | WARTHOG_APP_PORT=4100 8 | WARTHOG_DB_DATABASE=<%= props.kebabName %> 9 | WARTHOG_DB_HOST=localhost 10 | WARTHOG_DB_LOGGING=all 11 | WARTHOG_DB_PASSWORD= 12 | WARTHOG_DB_PORT=5432 13 | WARTHOG_DB_SYNCHRONIZE=true 14 | WARTHOG_DB_USERNAME=postgres 15 | -------------------------------------------------------------------------------- /src/cli/templates/new/_gitignore.ejs: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # OS metadata 5 | .DS_Store 6 | Thumbs.db -------------------------------------------------------------------------------- /src/cli/templates/new/src/config.ts.ejs: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as path from 'path'; 3 | 4 | export function loadConfig() { 5 | delete process.env.NODE_ENV; 6 | dotenv.config({ path: path.join(__dirname, '../.env') }); 7 | } 8 | -------------------------------------------------------------------------------- /src/cli/templates/new/src/index.ts.ejs: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { loadConfig } from '../src/config'; 4 | import { Logger } from '../src/logger'; 5 | 6 | import { getServer } from './server'; 7 | 8 | async function bootstrap() { 9 | await loadConfig(); 10 | 11 | const server = getServer(); 12 | await server.start(); 13 | } 14 | 15 | bootstrap().catch((error: Error) => { 16 | Logger.error(error); 17 | if (error.stack) { 18 | Logger.error(error.stack.split('\n')); 19 | } 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /src/cli/templates/new/src/logger.ts.ejs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import * as util from 'util'; 4 | 5 | import { getBindingError } from 'warthog'; 6 | 7 | export class Logger { 8 | static info(...args: any[]) { 9 | args = args.length === 1 ? args[0] : args; 10 | console.log(util.inspect(args, { showHidden: false, depth: null })); 11 | } 12 | 13 | static error(...args: any[]) { 14 | args = args.length === 1 ? args[0] : args; 15 | console.error(util.inspect(args, { showHidden: false, depth: null })); 16 | } 17 | 18 | // static debug(...args: any[]) { 19 | // console.debug(args); 20 | // } 21 | 22 | static log(...args: any[]) { 23 | console.log(args); 24 | } 25 | 26 | static warn(...args: any[]) { 27 | console.warn(args); 28 | } 29 | 30 | // This takes a raw GraphQL error and pulls out the relevant info 31 | static logGraphQLError(error: Error) { 32 | console.error(util.inspect(getBindingError(error), { showHidden: false, depth: null })); 33 | } 34 | } 35 | /* eslint-enable no-console */ 36 | -------------------------------------------------------------------------------- /src/cli/templates/new/src/server.ts.ejs: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { BaseContext, Server } from 'warthog'; 4 | 5 | import { Logger } from './logger'; 6 | 7 | interface Context extends BaseContext { 8 | user: { 9 | email: string; 10 | id: string; 11 | permissions: string; 12 | }; 13 | } 14 | 15 | export function getServer(AppOptions = {}, dbOptions = {}) { 16 | return new Server( 17 | { 18 | // Inject a fake user. In a real app you'd parse a JWT to add the user 19 | context: (request: any) => { 20 | const userId = JSON.stringify(request.headers).length.toString(); 21 | 22 | return { 23 | user: { 24 | id: `user:${userId}` 25 | } 26 | }; 27 | }, 28 | introspection: true, 29 | logger: Logger, 30 | ...AppOptions 31 | }, 32 | dbOptions 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/cli/templates/new/tsconfig.json.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "skipLibCheck": true, 5 | "outDir": "./dist", 6 | "lib": ["dom", "es5", "es6", "es7", "esnext"], 7 | "target": "es5", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": true, 13 | "keyofStringsOnly": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "types": ["isomorphic-fetch", "node"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules/**/*", "generated/**/*", "tools/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /src/cli/templates/new/warthog.config.js.ejs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cliGeneratePath: './src/modules/${kebabName}' 3 | }; 4 | -------------------------------------------------------------------------------- /src/cli/types.ts: -------------------------------------------------------------------------------- 1 | import { GluegunToolbox } from 'gluegun'; 2 | 3 | export interface WarthogGluegunToolbox extends GluegunToolbox { 4 | config: { 5 | load: Function; 6 | }; 7 | db: { 8 | create: Function; 9 | drop: Function; 10 | migrate: Function; 11 | generateMigration: Function; 12 | }; 13 | string: { 14 | supplant: Function; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/core/Context.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { Connection } from 'typeorm'; 3 | 4 | // TODO-MVP: update with actual context we're getting from Auth0 5 | export interface BaseContext { 6 | connection: Connection; 7 | dataLoader: { 8 | initialized: boolean; 9 | loaders: { [key: string]: { [key: string]: any } }; 10 | }; 11 | request: Request; 12 | user?: any; 13 | } 14 | -------------------------------------------------------------------------------- /src/core/GraphQLInfoService.ts: -------------------------------------------------------------------------------- 1 | // import { GraphQLResolveInfo } from 'graphql'; 2 | import * as graphqlFields from 'graphql-fields'; 3 | 4 | import { Service } from 'typedi'; 5 | 6 | export type ConnectionInputFields = { 7 | totalCount?: object; 8 | edges?: { 9 | node?: object; 10 | cursor?: object; 11 | }; 12 | pageInfo?: { 13 | hasNextPage: object; 14 | hasPreviousPage: object; 15 | startCursor?: object; 16 | endCursor?: object; 17 | }; 18 | }; 19 | 20 | export interface Node { 21 | [key: string]: any; 22 | } 23 | 24 | @Service() 25 | export class GraphQLInfoService { 26 | getFields(info: any): ConnectionInputFields { 27 | return graphqlFields(info); 28 | } 29 | 30 | connectionOptions(fields?: ConnectionInputFields) { 31 | if (!fields) { 32 | return { 33 | selectFields: [], 34 | totalCount: false, 35 | endCursor: false, 36 | startCursor: '', 37 | edgeCursors: '' 38 | }; 39 | } 40 | 41 | return { 42 | selectFields: this.baseFields(fields?.edges?.node), 43 | totalCount: isDefined(fields.totalCount), 44 | endCursor: isDefined(fields.pageInfo?.endCursor), 45 | startCursor: isDefined(fields.pageInfo?.startCursor), 46 | edgeCursors: isDefined(fields?.edges?.cursor) 47 | }; 48 | } 49 | 50 | baseFields(node?: Node): string[] { 51 | if (!node) { 52 | return []; 53 | } 54 | 55 | const scalars = Object.keys(node).filter(item => { 56 | return Object.keys(node[item]).length === 0; 57 | }); 58 | 59 | return scalars; 60 | } 61 | } 62 | 63 | function isDefined(obj: unknown): boolean { 64 | return typeof obj !== 'undefined'; 65 | } 66 | -------------------------------------------------------------------------------- /src/core/config.test.ts: -------------------------------------------------------------------------------- 1 | import { clearConfig } from '../test/server-vars'; 2 | 3 | import { Config } from './config'; 4 | 5 | describe('Config', () => { 6 | beforeEach(() => { 7 | clearConfig(); 8 | }); 9 | 10 | describe('Production', () => { 11 | test('throws if required values are not specified', async () => { 12 | process.env.NODE_ENV = 'production'; 13 | 14 | try { 15 | new Config({ configSearchPath: __dirname }); 16 | } catch (error) { 17 | expect(error.message).toContain('WARTHOG_APP_HOST is required'); 18 | } 19 | }); 20 | }); 21 | 22 | describe('Development', () => { 23 | test('uses correct defaults', async () => { 24 | process.env.NODE_ENV = 'development'; 25 | 26 | const config = new Config({ configSearchPath: __dirname }); 27 | 28 | expect(config.get('DB_HOST')).toEqual('localhost'); 29 | }); 30 | }); 31 | 32 | describe('Test', () => { 33 | test('will never open playground', async () => { 34 | process.env = { 35 | NODE_ENV: 'development', 36 | WARTHOG_AUTO_OPEN_PLAYGROUND: 'true', 37 | JEST_WORKER_ID: '12345' 38 | }; 39 | 40 | const config = new Config({ configSearchPath: __dirname }); 41 | 42 | expect(config.get('WARTHOG_AUTO_OPEN_PLAYGROUND')).toEqual('false'); 43 | }); 44 | }); 45 | 46 | describe('All environments', () => { 47 | test('translates TYPEORM env vars into warthog config', async () => { 48 | process.env = { 49 | NODE_ENV: 'development', 50 | TYPEORM_FOO: 'baz456' 51 | }; 52 | 53 | const config = new Config({ configSearchPath: __dirname }); 54 | 55 | expect(config.get('WARTHOG_DB_FOO')).toEqual('baz456'); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/core/encoding.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { debug } from '../decorators'; 3 | 4 | @Service() 5 | export class EncodingService { 6 | JSON_MARKER = '__JSON__:'; 7 | 8 | encode64(str: string): string { 9 | return Buffer.from(str, 'ascii').toString('base64'); 10 | } 11 | 12 | encode(input: object): string { 13 | return this.encode64(JSON.stringify(input)); 14 | } 15 | 16 | decode64(str: string): string { 17 | return Buffer.from(str, 'base64').toString('ascii'); 18 | } 19 | 20 | @debug('encoding:decode') 21 | decode(str: string): T { 22 | return JSON.parse(this.decode64(str)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/core/http.ts: -------------------------------------------------------------------------------- 1 | export interface GetResponse { 2 | statusCode: number; 3 | body: string; 4 | } 5 | 6 | // Custom http `get` without dependencies 7 | export async function get(url: string): Promise { 8 | // return new pending promise 9 | return new Promise((resolve, reject) => { 10 | // select http or https module, depending on reqested url 11 | const lib = url.startsWith('https') ? require('https') : require('http'); 12 | const request = lib.get(url, (response: any) => { 13 | // temporary data holder 14 | const body: string[] = []; 15 | // on every content chunk, push it to the data array 16 | response.on('data', (chunk: string) => body.push(chunk)); 17 | // we are done, resolve promise with those joined chunks 18 | response.on('end', () => { 19 | return resolve({ 20 | statusCode: response.statusCode, 21 | body: body.join('') 22 | }); 23 | }); 24 | }); 25 | // handle connection errors of the request 26 | request.on('error', (err: Error) => reject(err)); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server'; 2 | export * from './BaseModel'; 3 | export * from './BaseService'; 4 | export * from './config'; 5 | export * from './Context'; 6 | export * from './logger'; 7 | export * from './RelayService'; 8 | export * from './types'; 9 | -------------------------------------------------------------------------------- /src/core/logger.ts: -------------------------------------------------------------------------------- 1 | import * as Debug from 'debug'; 2 | import * as util from 'util'; 3 | 4 | // TODO: better logger 5 | export const logger = { 6 | debug: Debug('warthog:debug'), 7 | error: console.error, // eslint-disable-line 8 | info: console.info, // eslint-disable-line 9 | log: console.log, // eslint-disable-line 10 | warn: console.warn, // eslint-disable-line 11 | logObject: (...args: any[]) => console.log(util.inspect(args, { showHidden: false, depth: null })) // eslint-disable-line 12 | }; 13 | 14 | type logFunc = (...args: any[]) => void; 15 | 16 | export interface Logger { 17 | debug?: logFunc; 18 | error: logFunc; 19 | info: logFunc; 20 | log: logFunc; 21 | warn: logFunc; 22 | } 23 | -------------------------------------------------------------------------------- /src/core/tests/dotenv-files/.env: -------------------------------------------------------------------------------- 1 | WARTHOG_ENV=111 2 | WARTHOG_A=ENV 3 | WARTHOG_B=ENV 4 | WARTHOG_C=ENV 5 | WARTHOG_D=ENV 6 | 7 | # required variables 8 | WARTHOG_APP_HOST=localhost 9 | WARTHOG_APP_PORT=4100 10 | WARTHOG_DB_DATABASE=warthog-example-1 11 | WARTHOG_DB_USERNAME=postgres 12 | WARTHOG_DB_PASSWORD= 13 | WARTHOG_DB_SYNCHRONIZE=true 14 | WARTHOG_DB_HOST=localhost 15 | -------------------------------------------------------------------------------- /src/core/tests/dotenv-files/.env.local: -------------------------------------------------------------------------------- 1 | WARTHOG_ENV_LOCAL=222 2 | WARTHOG_B=ENV_LOCAL 3 | WARTHOG_C=ENV_LOCAL 4 | WARTHOG_D=ENV_LOCAL 5 | -------------------------------------------------------------------------------- /src/core/tests/dotenv-files/.env.local.development: -------------------------------------------------------------------------------- 1 | WARTHOG_ENV_LOCAL_DEVELOPMENT=333 2 | WARTHOG_C=ENV_LOCAL_DEVELOPMENT 3 | WARTHOG_D=ENV_LOCAL_DEVELOPMENT 4 | -------------------------------------------------------------------------------- /src/core/tests/dotenv-files/.env.local.production: -------------------------------------------------------------------------------- 1 | WARTHOG_ENV_LOCAL_PRODUCTION=444 2 | 3 | WARTHOG_D=ENV_LOCAL_PRODUCTION 4 | 5 | # Make sure we use the actual environment variable 6 | WARTHOG_PROPER_ENV_VARIABLE=ERROR 7 | -------------------------------------------------------------------------------- /src/core/tests/dotenv-files/dotenv.test.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../config'; 2 | 3 | describe('Dotenv files', () => { 4 | test('Pulls config in correct order', async () => { 5 | process.env = { 6 | NODE_ENV: 'development', 7 | WARTHOG_PROPER_ENV_VARIABLE: '12345' 8 | }; 9 | 10 | new Config({ dotenvPath: __dirname }); 11 | 12 | expect(process.env.WARTHOG_PROPER_ENV_VARIABLE).toEqual('12345'); 13 | expect(process.env.WARTHOG_ENV_LOCAL_DEVELOPMENT).toEqual('333'); 14 | expect(process.env.WARTHOG_C).toEqual('ENV_LOCAL_DEVELOPMENT'); 15 | expect(process.env.WARTHOG_D).toEqual('ENV_LOCAL_DEVELOPMENT'); 16 | expect(process.env.WARTHOG_ENV_LOCAL).toEqual('222'); 17 | expect(process.env.WARTHOG_B).toEqual('ENV_LOCAL'); 18 | expect(process.env.WARTHOG_ENV).toEqual('111'); 19 | expect(process.env.WARTHOG_A).toEqual('ENV'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/core/tests/entity/MyBase.model.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Column, Entity, Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseModel, BaseService } from '../../'; 6 | 7 | @Entity() 8 | export class MyBase extends BaseModel { 9 | @Column({ nullable: true }) 10 | registered?: boolean; 11 | 12 | @Column() 13 | firstName!: string; 14 | 15 | @Column() 16 | lastName!: string; 17 | } 18 | 19 | @Service('MyBaseService') 20 | export class MyBaseService extends BaseService { 21 | constructor(@InjectRepository(MyBase) protected readonly repository: Repository) { 22 | super(MyBase, repository); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/core/tests/invalid-config-file/.warthogrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "badkey1": "foo" 3 | } 4 | -------------------------------------------------------------------------------- /src/core/tests/invalid-config-file/config.test.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../config'; 2 | 3 | describe('Config (invalid file)', () => { 4 | test('does not allow invalid config keys', async done => { 5 | expect.assertions(2); 6 | try { 7 | new Config({ configSearchPath: __dirname }); 8 | } catch (error) { 9 | expect(error.message).toContain('invalid keys'); 10 | expect(error.message).toContain('badkey1'); 11 | } 12 | 13 | done(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/core/tests/valid-config-file/.warthogrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generatedFolder": "./foo", 3 | "resolversPath": "./r/e/s/o/l/v/e/r/s", 4 | "validateResolvers": "true" 5 | } 6 | -------------------------------------------------------------------------------- /src/core/tests/valid-config-file/config.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Config } from '../../config'; 3 | 4 | describe('Config (valid file)', () => { 5 | let config: Config; 6 | 7 | test('loads static config', async () => { 8 | // Set some defaults or the constructor will blow up in CI 9 | process.env.WARTHOG_APP_HOST = 'localhost'; 10 | process.env.WARTHOG_APP_PORT = '80'; 11 | process.env.WARTHOG_DB_HOST = 'localhost'; 12 | config = new Config({ configSearchPath: __dirname }); 13 | 14 | const vals: any = await config.loadStaticConfigSync(); 15 | 16 | expect(vals.WARTHOG_VALIDATE_RESOLVERS).toEqual('true'); 17 | expect(vals.WARTHOG_GENERATED_FOLDER).toEqual(path.join(__dirname, './foo')); 18 | expect(vals.WARTHOG_RESOLVERS_PATH).toEqual('./r/e/s/o/l/v/e/r/s'); 19 | }); 20 | 21 | test('TypeORM ENV vars beat config file', async () => { 22 | process.env = { 23 | NODE_ENV: 'development', 24 | WARTHOG_MODELS_PATH: 'env/models/path' 25 | }; 26 | 27 | const config = new Config({ configSearchPath: __dirname }); 28 | 29 | expect(config.get('WARTHOG_MODELS_PATH')).toEqual('env/models/path'); 30 | }); 31 | 32 | test('Warthog ENV vars beat TypeORM', async () => { 33 | process.env = { 34 | NODE_ENV: 'development', 35 | WARTHOG_DB_HOST: 'warthog/db/host', 36 | TYPEORM_HOST: 'typeorm/host' 37 | }; 38 | 39 | const config = new Config({ configSearchPath: __dirname }); 40 | 41 | expect(config.get('WARTHOG_DB_HOST')).toEqual('warthog/db/host'); 42 | expect(process.env.WARTHOG_DB_HOST).toEqual('warthog/db/host'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | export type Omit = Pick>; 2 | 3 | export interface StringMap { 4 | [key: string]: string; 5 | } 6 | export interface StringMapOptional { 7 | [key: string]: string | undefined; 8 | } 9 | 10 | export type DateOnlyString = string; 11 | export type DateTimeString = string; 12 | export type IDType = string; 13 | 14 | export interface BaseEntity { 15 | id: IDType; 16 | [key: string]: any; 17 | } 18 | 19 | export interface WhereInput { 20 | id_eq?: IDType; 21 | id_in?: IDType[]; 22 | } 23 | 24 | export interface DeleteReponse { 25 | id: IDType; 26 | } 27 | 28 | export type ClassType = new (...args: any[]) => T; 29 | 30 | export type Constructor = Function & { prototype: T }; 31 | 32 | export type Maybe = T | void; 33 | 34 | export type JsonValue = JsonPrimitive | JsonObject | JsonArray; 35 | 36 | export type JsonPrimitive = string | number | boolean | null; 37 | 38 | export type JsonObject = { [member: string]: JsonValue }; 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 41 | export interface JsonArray extends Array {} 42 | -------------------------------------------------------------------------------- /src/decorators/BooleanField.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLBoolean } from 'graphql'; 2 | 3 | import { DecoratorCommonOptions } from '../metadata'; 4 | import { BooleanWhereOperator } from '../torm'; 5 | import { composeMethodDecorators } from '../utils'; 6 | 7 | import { getCombinedDecorator } from './getCombinedDecorator'; 8 | 9 | interface BooleanFieldOptions extends DecoratorCommonOptions { 10 | default?: boolean; 11 | filter?: boolean | BooleanWhereOperator[]; 12 | } 13 | 14 | export function BooleanField(options: BooleanFieldOptions = {}): any { 15 | const factories = getCombinedDecorator({ 16 | fieldType: 'boolean', 17 | warthogColumnMeta: options, 18 | gqlFieldType: GraphQLBoolean, 19 | dbType: 'boolean' 20 | }); 21 | 22 | return composeMethodDecorators(...factories); 23 | } 24 | -------------------------------------------------------------------------------- /src/decorators/DateField.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLISODateTime } from 'type-graphql'; 2 | 3 | import { DecoratorCommonOptions } from '../metadata'; 4 | import { ColumnType, DateWhereOperator } from '../torm'; 5 | import { composeMethodDecorators } from '../utils'; 6 | 7 | import { getCombinedDecorator } from './getCombinedDecorator'; 8 | 9 | interface DateFieldOptions extends DecoratorCommonOptions { 10 | dataType?: ColumnType; // int16, jsonb, etc... 11 | default?: Date; 12 | filter?: boolean | DateWhereOperator[]; 13 | } 14 | 15 | // V3: Deprecate this usage in favor of DateTimeField 16 | export function DateField(options: DateFieldOptions = {}): any { 17 | const nullableOption = options.nullable === true ? { nullable: true } : {}; 18 | const defaultOption = options.default ? { default: options.default } : {}; 19 | 20 | const factories = getCombinedDecorator({ 21 | fieldType: 'date', 22 | warthogColumnMeta: options, 23 | gqlFieldType: GraphQLISODateTime, 24 | dbType: options.dataType || 'timestamp', 25 | dbColumnOptions: { ...nullableOption, ...defaultOption } 26 | }); 27 | 28 | return composeMethodDecorators(...factories); 29 | } 30 | -------------------------------------------------------------------------------- /src/decorators/DateOnlyField.ts: -------------------------------------------------------------------------------- 1 | // https://www.postgresql.org/docs/10/datatype-datetime.html 2 | import { DateResolver } from 'graphql-scalars'; 3 | 4 | import { DecoratorCommonOptions } from '../metadata'; 5 | import { DateOnlyString } from '../core'; 6 | import { DateOnlyWhereOperator } from '../torm'; 7 | import { composeMethodDecorators } from '../utils'; 8 | 9 | import { getCombinedDecorator } from './getCombinedDecorator'; 10 | 11 | interface DateOnlyFieldOptions extends DecoratorCommonOptions { 12 | default?: DateOnlyString; 13 | filter?: boolean | DateOnlyWhereOperator[]; 14 | } 15 | 16 | // V3: Update this to DateField 17 | export function DateOnlyField(options: DateOnlyFieldOptions = {}): any { 18 | const nullableOption = options.nullable === true ? { nullable: true } : {}; 19 | const defaultOption = options.default ? { default: options.default } : {}; 20 | 21 | const factories = getCombinedDecorator({ 22 | fieldType: 'dateonly', 23 | warthogColumnMeta: options, 24 | gqlFieldType: DateResolver, 25 | dbType: 'date', 26 | dbColumnOptions: { ...nullableOption, ...defaultOption } 27 | }); 28 | 29 | return composeMethodDecorators(...factories); 30 | } 31 | -------------------------------------------------------------------------------- /src/decorators/DateTimeField.ts: -------------------------------------------------------------------------------- 1 | // https://www.postgresql.org/docs/10/datatype-datetime.html 2 | import { GraphQLISODateTime } from 'type-graphql'; 3 | 4 | import { DecoratorCommonOptions } from '../metadata'; 5 | import { DateTimeString } from '../core'; 6 | import { DateTimeWhereOperator } from '../torm'; 7 | import { composeMethodDecorators } from '../utils'; 8 | 9 | import { getCombinedDecorator } from './getCombinedDecorator'; 10 | 11 | interface DateTimeFieldOptions extends DecoratorCommonOptions { 12 | default?: DateTimeString; 13 | filter?: boolean | DateTimeWhereOperator[]; 14 | } 15 | 16 | // V3: Deprecate this usage in favor of DateTimeField 17 | export function DateTimeField(options: DateTimeFieldOptions = {}): any { 18 | const nullableOption = options.nullable === true ? { nullable: true } : {}; 19 | const defaultOption = options.default ? { default: options.default } : {}; 20 | 21 | const factories = getCombinedDecorator({ 22 | fieldType: 'datetime', 23 | warthogColumnMeta: options, 24 | gqlFieldType: GraphQLISODateTime, 25 | dbType: 'timestamp', 26 | dbColumnOptions: { ...nullableOption, ...defaultOption } 27 | }); 28 | 29 | return composeMethodDecorators(...factories); 30 | } 31 | -------------------------------------------------------------------------------- /src/decorators/EmailField.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from 'class-validator'; 2 | 3 | import { DecoratorCommonOptions } from '../metadata'; 4 | import { EmailWhereOperator } from '../torm'; 5 | import { composeMethodDecorators } from '../utils'; 6 | 7 | import { getCombinedDecorator } from './getCombinedDecorator'; 8 | 9 | interface EmailFieldOptions extends DecoratorCommonOptions { 10 | unique?: boolean; 11 | filter?: boolean | EmailWhereOperator[]; 12 | } 13 | 14 | export function EmailField(options: EmailFieldOptions = {}): any { 15 | const optionsWithDefaults = { unique: true, ...options }; 16 | 17 | const factories = getCombinedDecorator({ 18 | fieldType: 'email', 19 | warthogColumnMeta: optionsWithDefaults 20 | }); 21 | 22 | // Adds email validation 23 | factories.push(IsEmail()); 24 | 25 | return composeMethodDecorators(...factories); 26 | } 27 | -------------------------------------------------------------------------------- /src/decorators/EnumField.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { IntrospectionEnumType, IntrospectionSchema } from 'graphql'; 4 | import { ObjectType, Query, Resolver } from 'type-graphql'; 5 | 6 | import { getSchemaInfo } from '../schema'; 7 | 8 | import { EnumField } from './EnumField'; 9 | 10 | describe('Enums', () => { 11 | let schemaIntrospection: IntrospectionSchema; 12 | 13 | beforeAll(async () => { 14 | // TODO: should we set this up as part of the test harness? 15 | // Container.set('warthog.generated-folder', process.cwd()); 16 | 17 | enum StringEnum { 18 | Foo = 'FOO', 19 | Bar = 'BAR' 20 | } 21 | 22 | @ObjectType() 23 | class StringEnumInput { 24 | @EnumField('StringEnum', StringEnum, { nullable: true }) 25 | stringEnumField?: StringEnum; 26 | } 27 | 28 | @Resolver(() => StringEnumInput) 29 | class SampleResolver { 30 | @Query(() => StringEnum) 31 | getStringEnumValue(): StringEnum { 32 | return StringEnum.Foo; 33 | } 34 | } 35 | 36 | const schemaInfo = await getSchemaInfo({ 37 | resolvers: [SampleResolver] 38 | }); 39 | schemaIntrospection = schemaInfo.schemaIntrospection; 40 | }); 41 | 42 | describe('EnumField', () => { 43 | test('Puts an enum in the GraphQL schema', async () => { 44 | const myEnum = schemaIntrospection.types.find((type: any) => { 45 | return type.kind === 'ENUM' && type.name === 'StringEnum'; 46 | }) as IntrospectionEnumType; 47 | 48 | expect(myEnum).toBeDefined(); 49 | expect(myEnum.enumValues.length).toEqual(2); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/decorators/EnumField.ts: -------------------------------------------------------------------------------- 1 | const caller = require('caller'); // eslint-disable-line @typescript-eslint/no-var-requires 2 | import * as path from 'path'; 3 | import { Field, registerEnumType } from 'type-graphql'; 4 | import { Column } from 'typeorm'; 5 | 6 | import { getMetadataStorage, DecoratorCommonOptions } from '../metadata'; 7 | import { composeMethodDecorators, generatedFolderPath, MethodDecoratorFactory } from '../utils'; 8 | import { EnumWhereOperator } from '../torm'; 9 | 10 | interface EnumFieldOptions extends DecoratorCommonOptions { 11 | default?: any; 12 | filter?: boolean | EnumWhereOperator[]; 13 | } 14 | 15 | export function EnumField(name: string, enumeration: object, options: EnumFieldOptions = {}): any { 16 | // Register enum with TypeGraphQL so that it lands in generated schema 17 | registerEnumType(enumeration, { name }); 18 | 19 | // In order to use the enums in the generated classes file, we need to 20 | // save their locations and import them in the generated file 21 | const decoratorSourceFile = caller(); 22 | 23 | // Use relative paths in the source files so that they can be used on different machines 24 | const relativeFilePath = path.relative(generatedFolderPath(), decoratorSourceFile); 25 | 26 | const registerWithWarthog = (target: object, propertyKey: string): any => { 27 | getMetadataStorage().addEnum( 28 | target.constructor.name, 29 | propertyKey, 30 | name, 31 | enumeration, 32 | relativeFilePath, 33 | options 34 | ); 35 | }; 36 | 37 | const factories = [ 38 | registerWithWarthog, 39 | Field(() => enumeration, options), 40 | Column({ enum: enumeration, ...options }) as MethodDecoratorFactory 41 | ]; 42 | 43 | return composeMethodDecorators(...factories); 44 | } 45 | -------------------------------------------------------------------------------- /src/decorators/Fields.ts: -------------------------------------------------------------------------------- 1 | import * as graphqlFields from 'graphql-fields'; 2 | import { createParamDecorator } from 'type-graphql'; 3 | 4 | export function Fields(): ParameterDecorator { 5 | return createParamDecorator(({ info }) => { 6 | // This object will be of the form: 7 | // rawFields { 8 | // __objectType 9 | // baseField: {}, 10 | // association: { subField: "foo"} 11 | // } 12 | // We pull out items with subFields 13 | const rawFields = graphqlFields(info); 14 | 15 | const scalars = Object.keys(rawFields).filter(item => { 16 | return !item.startsWith('__'); 17 | }); 18 | 19 | return scalars; 20 | }); 21 | } 22 | 23 | export function RawFields(): ParameterDecorator { 24 | return createParamDecorator(({ info }) => { 25 | return graphqlFields(info); 26 | }); 27 | } 28 | 29 | export function NestedFields(): ParameterDecorator { 30 | return createParamDecorator(({ info }) => { 31 | // This object will be of the form: 32 | // rawFields { 33 | // baseField: {}, 34 | // association: { subField: "foo"} 35 | // } 36 | // We need to pull out items with subFields 37 | const rawFields = graphqlFields(info); 38 | const output: any = { scalars: [] }; 39 | 40 | for (const fieldKey in rawFields) { 41 | if (Object.keys(rawFields[fieldKey]).length === 0) { 42 | output.scalars.push(fieldKey); 43 | } else { 44 | const subFields = rawFields[fieldKey]; 45 | output[fieldKey] = Object.keys(subFields).filter(subKey => { 46 | return Object.keys(subFields[subKey]).length === 0; 47 | }); 48 | } 49 | } 50 | 51 | return output; 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/decorators/FloatField.ts: -------------------------------------------------------------------------------- 1 | import { Float } from 'type-graphql'; 2 | 3 | import { DecoratorCommonOptions } from '../metadata'; 4 | import { composeMethodDecorators } from '../utils'; 5 | import { FloatColumnType, FloatWhereOperator } from '../torm'; 6 | 7 | import { getCombinedDecorator } from './getCombinedDecorator'; 8 | 9 | interface FloatFieldOptions extends DecoratorCommonOptions { 10 | dataType?: FloatColumnType; // int16, jsonb, etc... 11 | default?: number; 12 | filter?: boolean | FloatWhereOperator[]; 13 | } 14 | 15 | export function FloatField(options: FloatFieldOptions = {}): any { 16 | const nullableOption = options.nullable === true ? { nullable: true } : {}; 17 | const defaultOption = options.default ? { default: options.default } : {}; 18 | 19 | const factories = getCombinedDecorator({ 20 | fieldType: 'float', 21 | warthogColumnMeta: options, 22 | gqlFieldType: Float, 23 | dbType: options.dataType ?? 'float8', 24 | dbColumnOptions: { ...nullableOption, ...defaultOption } 25 | }); 26 | 27 | return composeMethodDecorators(...factories); 28 | } 29 | -------------------------------------------------------------------------------- /src/decorators/ForeignKeyField.ts: -------------------------------------------------------------------------------- 1 | import { Field } from 'type-graphql'; 2 | import { Column } from 'typeorm'; 3 | 4 | import { composeMethodDecorators, MethodDecoratorFactory } from '../utils'; 5 | 6 | // Links two tables within the same DB, so they're joined by the ID columns 7 | export function ForeignKeyField(): any { 8 | return composeMethodDecorators( 9 | Field(() => String), 10 | Column() as MethodDecoratorFactory 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/decorators/IdField.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorCommonOptions } from '../metadata'; 2 | import { IdWhereOperator } from '../torm'; 3 | import { composeMethodDecorators } from '../utils'; 4 | 5 | import { getCombinedDecorator } from './getCombinedDecorator'; 6 | 7 | interface IdFieldOptions extends DecoratorCommonOptions { 8 | unique?: boolean; 9 | filter?: boolean | IdWhereOperator[]; 10 | } 11 | 12 | export function IdField(options: IdFieldOptions = {}): any { 13 | const nullableOption = options.nullable === true ? { nullable: true } : {}; 14 | const uniqueOption = options.unique ? { unique: true } : {}; 15 | 16 | const factories = getCombinedDecorator({ 17 | fieldType: 'id', 18 | warthogColumnMeta: options, 19 | dbColumnOptions: { ...nullableOption, ...uniqueOption } 20 | }); 21 | 22 | return composeMethodDecorators(...factories); 23 | } 24 | -------------------------------------------------------------------------------- /src/decorators/IntField.ts: -------------------------------------------------------------------------------- 1 | import { Int } from 'type-graphql'; 2 | 3 | import { DecoratorCommonOptions } from '../metadata'; 4 | import { IntColumnType, IntWhereOperator } from '../torm'; 5 | import { composeMethodDecorators } from '../utils'; 6 | 7 | import { getCombinedDecorator } from './getCombinedDecorator'; 8 | 9 | interface IntFieldOptions extends DecoratorCommonOptions { 10 | dataType?: IntColumnType; 11 | default?: number; 12 | filter?: boolean | IntWhereOperator[]; 13 | array?: boolean; 14 | } 15 | 16 | export function IntField(options: IntFieldOptions = {}): any { 17 | const defaultOption = options.default ? { default: options.default } : {}; 18 | const nullableOption = options.nullable === true ? { nullable: true } : {}; 19 | 20 | const factories = getCombinedDecorator({ 21 | fieldType: 'integer', 22 | warthogColumnMeta: options, 23 | gqlFieldType: Int, 24 | dbType: options.dataType ?? 'int', 25 | dbColumnOptions: { ...nullableOption, ...defaultOption } 26 | }); 27 | 28 | return composeMethodDecorators(...factories); 29 | } 30 | -------------------------------------------------------------------------------- /src/decorators/InterfaceType.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceType as TypeGraphQLInterfaceType } from 'type-graphql'; 2 | 3 | import { Constructor } from '../core'; 4 | import { getMetadataStorage } from '../metadata'; 5 | import { ClassDecoratorFactory, composeClassDecorators } from '../utils/'; 6 | 7 | type InterfaceOptions = object; // TypeGraphQL InterfaceType's InterfaceOptions 8 | 9 | export function InterfaceType(options: InterfaceOptions = {}) { 10 | // Need to set as "any" here as we're dealing with abstract classes that are not newable, 11 | // So we can't define this as "ClassType" 12 | const registerWithWarthog = (target: Constructor): void => { 13 | getMetadataStorage().addInterfaceType(target.name); 14 | }; 15 | 16 | const factories = [ 17 | TypeGraphQLInterfaceType(options) as ClassDecoratorFactory, 18 | registerWithWarthog as ClassDecoratorFactory 19 | ]; 20 | 21 | return composeClassDecorators(...factories); 22 | } 23 | -------------------------------------------------------------------------------- /src/decorators/JSONField.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const { GraphQLJSONObject } = require('graphql-type-json'); 3 | 4 | import { composeMethodDecorators } from '../utils'; 5 | import { ClassType } from '../core/types'; 6 | 7 | import { getCombinedDecorator } from './getCombinedDecorator'; 8 | 9 | interface JSONFieldOptions { 10 | nullable?: boolean; 11 | filter?: boolean; 12 | gqlFieldType?: ClassType; 13 | } 14 | 15 | export function JSONField(options: JSONFieldOptions = {}): any { 16 | const factories = getCombinedDecorator({ 17 | fieldType: 'json', 18 | warthogColumnMeta: options, 19 | gqlFieldType: options.gqlFieldType ?? GraphQLJSONObject, 20 | dbType: 'jsonb' 21 | }); 22 | 23 | return composeMethodDecorators(...factories); 24 | } 25 | -------------------------------------------------------------------------------- /src/decorators/ManyToMany.ts: -------------------------------------------------------------------------------- 1 | import { Field } from 'type-graphql'; 2 | import { ManyToMany as TypeORMManyToMany } from 'typeorm'; 3 | 4 | import { composeMethodDecorators, MethodDecoratorFactory } from '../utils'; 5 | 6 | export function ManyToMany(parentType: any, joinFunc: any, options: any = {}): any { 7 | const factories = [ 8 | Field(() => [parentType()], { nullable: true, ...options }) as MethodDecoratorFactory, 9 | TypeORMManyToMany(parentType, joinFunc, options) as MethodDecoratorFactory 10 | ]; 11 | 12 | return composeMethodDecorators(...factories); 13 | } 14 | -------------------------------------------------------------------------------- /src/decorators/ManyToManyJoin.ts: -------------------------------------------------------------------------------- 1 | import { Field } from 'type-graphql'; 2 | import { JoinTable, ManyToMany as TypeORMManyToMany } from 'typeorm'; 3 | 4 | import { composeMethodDecorators, MethodDecoratorFactory } from '../utils'; 5 | 6 | // Note: for many to many relationships, you need to set one item as the "JoinTable" 7 | // therefore, we have 2 separate decorators. Just make sure to add one to one table and 8 | // One to the other in the relationship 9 | export function ManyToManyJoin(parentType: any, joinFunc: any, options: any = {}): any { 10 | const factories = [ 11 | JoinTable() as MethodDecoratorFactory, 12 | Field(() => [parentType()], { nullable: true, ...options }) as MethodDecoratorFactory, 13 | TypeORMManyToMany(parentType, joinFunc, options) as MethodDecoratorFactory 14 | ]; 15 | 16 | return composeMethodDecorators(...factories); 17 | } 18 | -------------------------------------------------------------------------------- /src/decorators/NumericField.ts: -------------------------------------------------------------------------------- 1 | import { Float } from 'type-graphql'; 2 | import { ColumnNumericOptions } from 'typeorm/decorator/options/ColumnNumericOptions'; 3 | import { ColumnCommonOptions } from 'typeorm/decorator/options/ColumnCommonOptions'; 4 | 5 | import { DecoratorCommonOptions } from '../metadata'; 6 | import { composeMethodDecorators } from '../utils'; 7 | import { NumericColumnType, NumericWhereOperator } from '../torm'; 8 | 9 | import { getCombinedDecorator } from './getCombinedDecorator'; 10 | 11 | interface NumericFieldOptions 12 | extends ColumnCommonOptions, 13 | ColumnNumericOptions, 14 | DecoratorCommonOptions { 15 | dataType?: NumericColumnType; 16 | filter?: boolean | NumericWhereOperator[]; 17 | } 18 | 19 | export function NumericField(options: NumericFieldOptions = {}): any { 20 | const { dataType, filter, sort, ...dbOptions } = options; 21 | const nullableOption = options.nullable === true ? { nullable: true } : {}; 22 | 23 | const factories = getCombinedDecorator({ 24 | fieldType: 'numeric', 25 | warthogColumnMeta: options, 26 | gqlFieldType: Float, 27 | dbType: options.dataType ?? 'numeric', 28 | dbColumnOptions: { ...nullableOption, ...dbOptions } 29 | }); 30 | 31 | return composeMethodDecorators(...factories); 32 | } 33 | -------------------------------------------------------------------------------- /src/decorators/OneToMany.ts: -------------------------------------------------------------------------------- 1 | import { Field } from 'type-graphql'; 2 | import { OneToMany as TypeORMOneToMany } from 'typeorm'; 3 | 4 | import { composeMethodDecorators, MethodDecoratorFactory } from '../utils'; 5 | 6 | export function OneToMany(parentType: any, joinFunc: any, options: any = {}): any { 7 | const factories = [ 8 | Field(parentType, { nullable: true, ...options }) as MethodDecoratorFactory, 9 | TypeORMOneToMany(parentType, joinFunc) as MethodDecoratorFactory 10 | ]; 11 | 12 | return composeMethodDecorators(...factories); 13 | } 14 | -------------------------------------------------------------------------------- /src/decorators/StringField.ts: -------------------------------------------------------------------------------- 1 | import { MaxLength, MinLength } from 'class-validator'; 2 | 3 | import { DecoratorCommonOptions } from '../metadata'; 4 | import { composeMethodDecorators } from '../utils'; 5 | import { StringColumnType, StringWhereOperator } from '../torm'; 6 | 7 | import { getCombinedDecorator } from './getCombinedDecorator'; 8 | 9 | interface StringFieldOptions extends DecoratorCommonOptions { 10 | dataType?: StringColumnType; // int16, jsonb, etc... 11 | maxLength?: number; 12 | minLength?: number; 13 | default?: string; 14 | unique?: boolean; 15 | filter?: boolean | StringWhereOperator[]; 16 | array?: boolean; 17 | } 18 | 19 | export function StringField(options: StringFieldOptions = {}): any { 20 | const maxLenOption = options.maxLength ? { length: options.maxLength } : {}; 21 | const uniqueOption = options.unique ? { unique: true } : {}; 22 | 23 | const factories = getCombinedDecorator({ 24 | fieldType: 'string', 25 | warthogColumnMeta: options, 26 | gqlFieldType: String, 27 | dbType: options.dataType || 'varchar', 28 | dbColumnOptions: { ...maxLenOption, ...uniqueOption } 29 | }); 30 | 31 | if (options.minLength) { 32 | factories.push(MinLength(options.minLength)); 33 | } 34 | if (options.maxLength) { 35 | factories.push(MaxLength(options.maxLength)); 36 | } 37 | 38 | return composeMethodDecorators(...factories); 39 | } 40 | -------------------------------------------------------------------------------- /src/decorators/UpdateDateField.ts.bak: -------------------------------------------------------------------------------- 1 | import { Field, GraphQLISODateTime } from 'type-graphql'; 2 | import { UpdateDateColumn } from 'typeorm'; 3 | 4 | import { getMetadataStorage } from '../metadata'; 5 | import { composeMethodDecorators, MethodDecoratorFactory } from '../utils'; 6 | 7 | interface DateFieldOptions {} 8 | 9 | export function UpdateDateField(args: DateFieldOptions = {}): any { 10 | const registerWithWarthog = (target: object, propertyKey: string): any => { 11 | getMetadataStorage().addField('date', target.constructor.name, propertyKey); 12 | }; 13 | 14 | // These are the 2 required decorators to get type-graphql and typeorm working 15 | const factories = [ 16 | registerWithWarthog, 17 | Field(() => GraphQLISODateTime, { 18 | nullable: true 19 | }), 20 | UpdateDateColumn({ 21 | nullable: true 22 | }) as MethodDecoratorFactory 23 | ]; 24 | 25 | return composeMethodDecorators(...factories); 26 | } 27 | -------------------------------------------------------------------------------- /src/decorators/UserId.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from 'type-graphql'; 2 | 3 | export function UserId(): ParameterDecorator { 4 | return createParamDecorator(({ context }: { context: { user?: { id?: string } } }) => { 5 | if (!context.user) { 6 | throw new Error('`user` attribute not found on context'); 7 | } 8 | if (!context.user.id) { 9 | throw new Error('`user` attribute does not contain an `id`'); 10 | } 11 | return context.user.id; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/decorators/WarthogField.ts: -------------------------------------------------------------------------------- 1 | import { ColumnMetadata, getMetadataStorage, FieldType } from '../metadata'; 2 | 3 | export function WarthogField(fieldType: FieldType, options: Partial = {}): any { 4 | return (target: object, propertyKey: string): any => { 5 | getMetadataStorage().addField(fieldType, target.constructor.name, propertyKey, options); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/decorators/debug.ts: -------------------------------------------------------------------------------- 1 | import * as Debug from 'debug'; 2 | import { performance } from 'perf_hooks'; 3 | import * as util from 'util'; 4 | 5 | type MethodDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => any; 6 | 7 | export function debug(key: string): MethodDecorator { 8 | const logger = Debug(key); 9 | 10 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 11 | const originalMethod = descriptor.value; 12 | 13 | if (util.types.isAsyncFunction(originalMethod)) { 14 | descriptor.value = async function(...args: unknown[]): Promise { 15 | logger(`Entering ${propertyKey} with args: ${JSON.stringify(args)}`); 16 | const start = performance.now(); 17 | const result = await originalMethod.apply(this, args); 18 | const end = performance.now(); 19 | logger(`Exiting ${propertyKey} after ${(end - start).toFixed(2)} milliseconds.`); 20 | return result; 21 | }; 22 | } else { 23 | descriptor.value = function(...args: unknown[]) { 24 | logger(`Entering ${propertyKey} with args: ${JSON.stringify(args)}`); 25 | const start = performance.now(); 26 | const result = originalMethod.apply(this, args); 27 | const end = performance.now(); 28 | logger(`Exiting ${propertyKey} after ${(end - start).toFixed(2)} milliseconds.`); 29 | return result; 30 | }; 31 | } 32 | return descriptor; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BooleanField'; 2 | export * from './CustomField'; 3 | export * from './DateField'; 4 | export * from './DateOnlyField'; 5 | export * from './DateTimeField'; 6 | export * from './debug'; 7 | export * from './EmailField'; 8 | export * from './EnumField'; 9 | export * from './Fields'; 10 | export * from './FloatField'; 11 | export * from './ForeignKeyField'; 12 | export * from './IdField'; 13 | export * from './IntField'; 14 | export * from './InterfaceType'; 15 | export * from './JSONField'; 16 | export * from './ManyToMany'; 17 | export * from './ManyToManyJoin'; 18 | export * from './ManyToOne'; 19 | export * from './Model'; 20 | export * from './NumericField'; 21 | export * from './ObjectType'; 22 | export * from './OneToMany'; 23 | export * from './StringField'; 24 | export * from './UserId'; 25 | export * from './WarthogField'; 26 | -------------------------------------------------------------------------------- /src/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './binding'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './decorators'; 3 | export * from './gql'; 4 | export * from './middleware'; 5 | export * from './schema'; 6 | export * from './tgql'; 7 | export * from './torm'; 8 | export * from './utils'; 9 | -------------------------------------------------------------------------------- /src/metadata/index.ts: -------------------------------------------------------------------------------- 1 | export * from './metadata-storage'; 2 | -------------------------------------------------------------------------------- /src/middleware/ErrorMiddleware.ts.bak: -------------------------------------------------------------------------------- 1 | import { ArgumentValidationError, MiddlewareInterface, NextFn, ResolverData } from 'type-graphql'; 2 | import { Service } from 'typedi'; 3 | 4 | import { BaseContext, logger } from '../core'; 5 | 6 | @Service() 7 | export class ErrorLoggerMiddleware implements MiddlewareInterface { 8 | constructor() {} 9 | 10 | async use({ context }: ResolverData, next: NextFn) { 11 | try { 12 | return await next(); 13 | } catch (err) { 14 | if (!(err instanceof ArgumentValidationError)) { 15 | // hide errors from db like printing sql query 16 | logger.error(context.user); 17 | throw new Error('Unknown error occurred. Try again later!'); 18 | } 19 | throw err; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/middleware/HealthMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | export function healthCheckMiddleware(req: Request, res: Response) { 4 | res.send({ data: 'alive' }); 5 | return Promise.resolve(); 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DataLoaderMiddleware'; 2 | // export * from './ErrorMiddleware'; 3 | export * from './HealthMiddleware'; 4 | -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SchemaGenerator'; 2 | export * from './getSchemaInfo'; 3 | export * from './type-conversion'; 4 | -------------------------------------------------------------------------------- /src/test/codegen-test-files.sh: -------------------------------------------------------------------------------- 1 | WARTHOG_GENERATED_FOLDER=./src/test/generated \ 2 | WARTHOG_RESOLVERS_PATH=./src/test/modules/**/*.ts \ 3 | WARTHOG_DB_ENTITIES=./src/test/modules \ 4 | WARTHOG_MODULE_IMPORT_PATH=../../ \ 5 | ./bin/warthog codegen -------------------------------------------------------------------------------- /src/test/examples/create-id-model.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Mutation, Resolver } from 'type-graphql'; 2 | import { Inject, Service } from 'typedi'; 3 | import { Repository } from 'typeorm'; 4 | import { InjectRepository } from 'typeorm-typedi-extensions'; 5 | 6 | import { BaseModel, BaseService, Model, StringField } from '../../'; 7 | 8 | import { CreateIDTestCreateInput } from './create-id-model/generated'; 9 | 10 | @Model() 11 | export class CreateIDTest extends BaseModel { 12 | @StringField() 13 | stringField?: string; 14 | } 15 | 16 | @Service('CreateIDTestService') 17 | export class CreateIDTestService extends BaseService { 18 | constructor( 19 | @InjectRepository(CreateIDTest) protected readonly repository: Repository 20 | ) { 21 | super(CreateIDTest, repository); 22 | } 23 | } 24 | @Resolver(CreateIDTest) 25 | export class CreateIDTestResolver { 26 | constructor(@Inject('CreateIDTestService') public readonly service: CreateIDTestService) {} 27 | 28 | @Mutation(() => CreateIDTest) 29 | async createKitchenSink(@Arg('data') data: CreateIDTestCreateInput): Promise { 30 | return this.service.create(data, '1'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/examples/create-id-model/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /src/test/examples/create-id-model/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../../../'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /src/test/functional/schema.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | // This test makes sure that the test schema doesn't change over time 5 | // it has a good combination of data types an options that have been inspected. 6 | // It should not change unless we explicitly change something 7 | describe('schema', () => { 8 | test("test schema doesn't change", async () => { 9 | const file = path.join(__dirname, '..', 'generated', 'schema.graphql'); 10 | const schema = fs.readFileSync(file, 'utf-8'); 11 | 12 | expect(schema).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/test/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './classes'; -------------------------------------------------------------------------------- /src/test/generated/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { getBaseConfig } from '../../'; 2 | 3 | module.exports = getBaseConfig(); -------------------------------------------------------------------------------- /src/test/modules/api-only/api-only.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, StringField } from '../../..'; 2 | 3 | @Model({ db: false }) 4 | export class ApiOnly extends BaseModel { 5 | @StringField() 6 | name?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/test/modules/api-only/api-only.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Authorized, Query, Resolver } from 'type-graphql'; 2 | import { Inject } from 'typedi'; 3 | 4 | import { Fields } from '../../..'; 5 | import { ApiOnlyWhereArgs, ApiOnlyWhereInput } from '../../generated'; 6 | 7 | import { ApiOnly } from './api-only.model'; 8 | import { ApiOnlyService } from './api-only.service'; 9 | 10 | @Resolver(ApiOnly) 11 | export class DishResolver { 12 | constructor(@Inject('ApiOnlyService') public readonly service: ApiOnlyService) {} 13 | 14 | @Authorized('dish:read') 15 | @Query(() => [ApiOnly]) 16 | async dishes( 17 | @Args() { where, orderBy, limit, offset }: ApiOnlyWhereArgs, 18 | @Fields() fields: string[] 19 | ): Promise { 20 | return this.service.find(where, orderBy, limit, offset, fields); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/modules/api-only/api-only.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { ApiOnly } from './api-only.model'; 3 | 4 | // The API only model does not connect to the DB, so you'll need to customize the methods used in the resolver 5 | @Service('ApiOnlyService') 6 | export class ApiOnlyService { 7 | constructor() { 8 | // 9 | } 10 | 11 | find(where?: any, orderBy?: string, limit?: number, offset?: number, fields?: string[]) { 12 | console.log(where, orderBy, limit, offset, fields); 13 | return [] as ApiOnly[]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/modules/db-only/db-only.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, Model, StringField } from '../../..'; 2 | 3 | @Model({ dbOnly: true }) 4 | export class DbOnly extends BaseModel { 5 | @StringField() 6 | stringField?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/test/modules/dish/dish.model.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, EnumField, ManyToOne, Model, StringField } from '../../../'; 2 | 3 | import { KitchenSink } from '../kitchen-sink/kitchen-sink.model'; 4 | 5 | import { StringEnum } from '../shared'; 6 | export { StringEnum }; // Warthog requires this 7 | 8 | @Model() 9 | export class Dish extends BaseModel { 10 | @StringField({ maxLength: 40 }) 11 | name?: string; 12 | 13 | // Exercises the case where multiple models import the same enum 14 | @EnumField('StringEnum', StringEnum, { nullable: true }) 15 | stringEnumField?: StringEnum; 16 | 17 | @ManyToOne( 18 | () => KitchenSink, 19 | (kitchenSink: KitchenSink) => kitchenSink.dishes, 20 | { 21 | nullable: false 22 | } 23 | ) 24 | kitchenSink?: KitchenSink; 25 | } 26 | -------------------------------------------------------------------------------- /src/test/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api-only/api-only.model'; 2 | export * from './dish/dish.model'; 3 | export * from './kitchen-sink/kitchen-sink.model'; 4 | -------------------------------------------------------------------------------- /src/test/modules/kitchen-sink/kitchen-sink.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { BaseService } from '../../../'; 6 | 7 | import { KitchenSink } from './kitchen-sink.model'; 8 | 9 | @Service('KitchenSinkService') 10 | export class KitchenSinkService extends BaseService { 11 | constructor( 12 | @InjectRepository(KitchenSink) protected readonly repository: Repository 13 | ) { 14 | super(KitchenSink, repository); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/modules/shared.ts: -------------------------------------------------------------------------------- 1 | // Also - must use string enums 2 | export enum StringEnum { 3 | FOO = 'FOO', 4 | BAR = 'BAR' 5 | } 6 | -------------------------------------------------------------------------------- /src/test/server-vars.ts: -------------------------------------------------------------------------------- 1 | import { StringMap } from '../core'; 2 | 3 | export function setTestServerEnvironmentVariables(overrides?: StringMap) { 4 | clearConfig(); 5 | 6 | const defaultVars = getStandardEnvironmentVariables(); 7 | Object.keys(defaultVars).forEach(key => { 8 | process.env[key] = defaultVars[key]; 9 | }); 10 | 11 | if (!overrides) { 12 | return; 13 | } 14 | 15 | Object.keys(overrides).forEach(key => { 16 | process.env[key] = overrides[key]; 17 | }); 18 | } 19 | 20 | export function getStandardEnvironmentVariables(): StringMap { 21 | return { 22 | WARTHOG_APP_HOST: 'localhost', 23 | WARTHOG_APP_PORT: '4000', 24 | WARTHOG_APP_PROTOCOL: 'http', 25 | WARTHOG_AUTO_GENERATE_FILES: 'false', 26 | WARTHOG_AUTO_OPEN_PLAYGROUND: 'false', 27 | WARTHOG_DB_DATABASE: 'warthog-test', 28 | WARTHOG_DB_ENTITIES: 'src/test/modules/**/*.model.ts', 29 | WARTHOG_DB_HOST: 'localhost', 30 | WARTHOG_DB_LOGGING: 'none', 31 | WARTHOG_DB_MIGRATIONS_DIR: './tmp/test/migrations', 32 | WARTHOG_DB_OVERRIDE: 'true', // Set so that we can do DB stuff outside of NODE_ENV=development 33 | WARTHOG_DB_USERNAME: 'postgres', 34 | WARTHOG_DB_PASSWORD: '', 35 | WARTHOG_DB_SYNCHRONIZE: 'true', 36 | WARTHOG_GENERATED_FOLDER: './src/test/generated', 37 | WARTHOG_RESOLVERS_PATH: './src/test/modules/**/*.resolver.ts', 38 | WARTHOG_MODULE_IMPORT_PATH: '../../', 39 | WARTHOG_VALIDATE_RESOLVERS: 'false' 40 | }; 41 | } 42 | 43 | export function clearConfig() { 44 | const WARTHOG_PREFIX = 'WARTHOG_'; 45 | const TYPEORM_PREFIX = 'TYPEORM_'; 46 | Object.keys(process.env).forEach(key => { 47 | if (key.startsWith(WARTHOG_PREFIX) || key.startsWith(TYPEORM_PREFIX)) { 48 | delete process.env[key]; 49 | } 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/test/setupFiles.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Container } from 'typedi'; 4 | import { useContainer as typeOrmUseContainer } from 'typeorm'; 5 | 6 | import { Config } from '../'; 7 | import { setTestServerEnvironmentVariables } from '../test/server-vars'; 8 | 9 | if (!(global as any).__warthog_config__) { 10 | // Tell TypeORM to use our typedi instance 11 | typeOrmUseContainer(Container); 12 | 13 | setTestServerEnvironmentVariables(); 14 | 15 | const config = new Config({ container: Container }); 16 | 17 | (global as any).__warthog_config__ = config.get(); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/setupFilesAfterEnv.ts: -------------------------------------------------------------------------------- 1 | jest.setTimeout(20000); 2 | -------------------------------------------------------------------------------- /src/test/test-server.ts: -------------------------------------------------------------------------------- 1 | import { authChecker, Server, ServerOptions } from '../'; 2 | 3 | // This spins up a mock Warthog server using the models and resolvers in the test/modules directory 4 | export function getTestServer(options: ServerOptions = {}) { 5 | return new Server({ 6 | authChecker, 7 | context: () => { 8 | return { 9 | user: { 10 | id: 'abc123', 11 | permissions: [ 12 | 'kitchenSink:create', 13 | 'kitchenSink:read', 14 | 'kitchenSink:update', 15 | 'kitchenSink:delete', 16 | 'dish:create', 17 | 'dish:read', 18 | 'dish:update' 19 | ] 20 | } 21 | }; 22 | }, 23 | introspection: true, 24 | openPlayground: false, 25 | ...options 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/tgql/BaseResolver.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Repository } from 'typeorm'; 2 | 3 | import { BaseModel, BaseService, WhereInput } from '../core'; 4 | 5 | import { StandardDeleteResponse } from './DeleteResponse'; 6 | 7 | export class BaseResolver { 8 | service: any; 9 | 10 | // TODO: need to figure out why we couldn't type this as Repository 11 | constructor(protected entityClass: any, protected repository: Repository) { 12 | this.service = new BaseService(entityClass, this.repository); 13 | } 14 | 15 | async find( 16 | where?: any, 17 | orderBy?: any, // Fix this 18 | limit?: number, 19 | offset?: number, 20 | fields?: string[] 21 | ): Promise { 22 | return this.service.find(where, orderBy, limit, offset, fields); 23 | } 24 | 25 | // TODO: fix - W extends Partial 26 | async findOne(where: W): Promise { 27 | return this.service.findOne(where); 28 | } 29 | 30 | async create(data: DeepPartial, userId: string): Promise { 31 | return this.service.create(data, userId); 32 | } 33 | 34 | async createMany(data: DeepPartial[], userId: string): Promise { 35 | return this.service.createMany(data, userId); 36 | } 37 | 38 | async update(data: DeepPartial, where: W, userId: string): Promise { 39 | return this.service.update(data, where, userId); 40 | } 41 | 42 | async delete(where: W, userId: string): Promise { 43 | return this.service.delete(where, userId); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/tgql/BaseWhereInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from 'type-graphql'; 2 | 3 | @InputType() 4 | export class BaseWhereInput { 5 | @Field(() => String, { nullable: true }) 6 | id_eq?: string; 7 | @Field(() => [String], { nullable: true }) 8 | id_in?: string[]; 9 | 10 | @Field({ nullable: true }) 11 | createdAt_eq?: string; 12 | @Field({ nullable: true }) 13 | createdAt_lt?: string; 14 | @Field({ nullable: true }) 15 | createdAt_lte?: string; 16 | @Field({ nullable: true }) 17 | createdAt_gt?: string; 18 | @Field({ nullable: true }) 19 | createdAt_gte?: string; 20 | @Field({ nullable: true }) 21 | createdById_eq?: string; 22 | 23 | @Field({ nullable: true }) 24 | updatedAt_eq?: string; 25 | @Field({ nullable: true }) 26 | updatedAt_lt?: string; 27 | @Field({ nullable: true }) 28 | updatedAt_lte?: string; 29 | @Field({ nullable: true }) 30 | updatedAt_gt?: string; 31 | @Field({ nullable: true }) 32 | updatedAt_gte?: string; 33 | @Field({ nullable: true }) 34 | updatedById_eq?: string; 35 | 36 | @Field({ nullable: true }) 37 | deletedAt_all?: boolean; // This turns off the default soft-deleted logic 38 | @Field({ nullable: true }) 39 | deletedAt_eq?: string; 40 | @Field({ nullable: true }) 41 | deletedAt_lt?: string; 42 | @Field({ nullable: true }) 43 | deletedAt_lte?: string; 44 | @Field({ nullable: true }) 45 | deletedAt_gt?: string; 46 | @Field({ nullable: true }) 47 | deletedAt_gte?: string; 48 | @Field({ nullable: true }) 49 | deletedById_eq?: string; 50 | } 51 | -------------------------------------------------------------------------------- /src/tgql/DeleteResponse.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, InterfaceType, ObjectType } from 'type-graphql'; 2 | 3 | import { IDType } from '../core'; 4 | 5 | @InterfaceType() 6 | export abstract class DeleteResponse { 7 | @Field(() => ID) 8 | id!: IDType; 9 | } 10 | 11 | @ObjectType() 12 | export class StandardDeleteResponse { 13 | @Field(() => ID) 14 | id!: IDType; 15 | } 16 | -------------------------------------------------------------------------------- /src/tgql/PageInfo.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class PageInfo { 5 | @Field({ nullable: false }) 6 | hasNextPage!: boolean; 7 | 8 | @Field({ nullable: false }) 9 | hasPreviousPage!: boolean; 10 | 11 | @Field({ nullable: true }) 12 | startCursor?: string; 13 | 14 | @Field({ nullable: true }) 15 | endCursor?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/tgql/PaginationArgs.ts: -------------------------------------------------------------------------------- 1 | // type-graphql is hooked into class-validator: https://github.com/typestack/class-validator 2 | // so we can have it automatically validate that args coming in are valid 3 | // See https://github.com/typestack/class-validator#validation-decorators for list of decorators 4 | // See https://github.com/typestack/class-validator/tree/master/sample for examples 5 | import { Min } from 'class-validator'; 6 | import { ArgsType, Field, Int } from 'type-graphql'; 7 | 8 | @ArgsType() 9 | export class PaginationArgs { 10 | @Field(() => Int, { nullable: true }) 11 | @Min(0) 12 | offset?: number; 13 | 14 | @Field(() => Int, { nullable: true }) 15 | @Min(1) 16 | limit?: number = 50; 17 | } 18 | -------------------------------------------------------------------------------- /src/tgql/authChecker.ts: -------------------------------------------------------------------------------- 1 | import { AuthChecker } from 'type-graphql'; 2 | 3 | import { BaseContext } from '../core/Context'; 4 | 5 | // This authChecker is used by type-graphql's @Authorized decorator 6 | export const authChecker: AuthChecker = ({ context: { user } }, permissions) => { 7 | if (!user) { 8 | return false; 9 | } 10 | 11 | // Just checking @Authorized() - return true since we know there is a user now 12 | if (permissions.length === 0) { 13 | return user !== undefined; 14 | } 15 | // Check that permissions overlap 16 | return permissions.some((perm: string) => user.permissions.includes(perm)); 17 | }; 18 | -------------------------------------------------------------------------------- /src/tgql/index.ts: -------------------------------------------------------------------------------- 1 | import { StandardDeleteResponse } from './DeleteResponse'; 2 | 3 | export { authChecker } from './authChecker'; 4 | export { BaseModel } from '../core/BaseModel'; 5 | export * from './BaseResolver'; 6 | export * from './BaseWhereInput'; 7 | export * from './DeleteResponse'; 8 | export * from './PageInfo'; 9 | export * from './PaginationArgs'; 10 | export { loadFromGlobArray } from './loadGlobs'; 11 | 12 | export { StandardDeleteResponse }; 13 | -------------------------------------------------------------------------------- /src/tgql/loadGlobs.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob'; 2 | 3 | export function findFileNamesFromGlob(globString: string) { 4 | return glob.sync(globString); 5 | } 6 | 7 | export function loadFromGlobString(globString: string) { 8 | const filePaths = findFileNamesFromGlob(globString); 9 | filePaths.map(fileName => require(fileName)); 10 | } 11 | 12 | export function loadFromGlobArray(globs: string[]) { 13 | if (!globs.length) { 14 | throw new Error('globs is required!'); 15 | } 16 | globs.forEach(globString => { 17 | if (typeof globString === 'string') { 18 | loadFromGlobString(globString); 19 | } 20 | }); 21 | return undefined; 22 | } 23 | -------------------------------------------------------------------------------- /src/torm/EverythingSubscriber.ts.bak: -------------------------------------------------------------------------------- 1 | // This subscriber will log all CUD operations (left off "read" as it would be too noisy) 2 | import { 3 | EntitySubscriberInterface, 4 | EventSubscriber, 5 | InsertEvent, 6 | RemoveEvent, 7 | UpdateEvent 8 | } from 'typeorm'; 9 | 10 | import { logger } from './../core/logger'; 11 | 12 | @EventSubscriber() 13 | export class EverythingSubscriber implements EntitySubscriberInterface { 14 | /** 15 | * Called before entity insertion. 16 | */ 17 | beforeInsert(event: InsertEvent) { 18 | logger.info(`Before Insert: `, event.entity); 19 | } 20 | 21 | /** 22 | * Called before entity update. 23 | */ 24 | beforeUpdate(event: UpdateEvent) { 25 | logger.info(`BEFORE ENTITY UPDATED: `, event.entity); 26 | } 27 | 28 | /** 29 | * Called before entity deletion. 30 | */ 31 | beforeRemove(event: RemoveEvent) { 32 | logger.info(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); 33 | } 34 | 35 | /** 36 | * Called after entity insertion. 37 | */ 38 | afterInsert(event: InsertEvent) { 39 | logger.info(`AFTER ENTITY INSERTED: `, event.entity); 40 | } 41 | 42 | /** 43 | * Called after entity update. 44 | */ 45 | afterUpdate(event: UpdateEvent) { 46 | logger.info(`AFTER ENTITY UPDATED: `, event.entity); 47 | } 48 | 49 | /** 50 | * Called after entity deletion. 51 | */ 52 | afterRemove(event: RemoveEvent) { 53 | logger.info('Deleted', event.entity, 'with ID', event.entityId); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/torm/SnakeNamingStrategy.ts: -------------------------------------------------------------------------------- 1 | import { DefaultNamingStrategy, NamingStrategyInterface } from 'typeorm'; 2 | import { snakeCase } from 'typeorm/util/StringUtils'; 3 | 4 | export class SnakeNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | tableName(className: string, customName?: string): string { 10 | return customName ? customName : `${snakeCase(className)}s`; 11 | } 12 | 13 | columnName(propertyName: string, customName?: string, embeddedPrefixes: string[] = []): string { 14 | return ( 15 | snakeCase(embeddedPrefixes.join('_')) + (customName ? customName : snakeCase(propertyName)) 16 | ); 17 | } 18 | 19 | relationName(propertyName: string): string { 20 | return snakeCase(propertyName); 21 | } 22 | 23 | joinColumnName(relationName: string, referencedColumnName: string): string { 24 | return snakeCase(`${relationName}_${referencedColumnName}`); 25 | } 26 | 27 | joinTableName(firstTableName: string, secondTableName: string): string { 28 | return snakeCase(`${firstTableName}_${secondTableName}`); 29 | } 30 | 31 | joinTableColumnName(tableName: string, propertyName: string, columnName?: string): string { 32 | return snakeCase(`${tableName}_${columnName ? columnName : propertyName}`); 33 | } 34 | 35 | classTableInheritanceParentColumnName( 36 | parentTableName: any, 37 | parentTableIdPropertyName: any 38 | ): string { 39 | return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/torm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createConnection'; 2 | // export * from './EverythingSubscriber'; 3 | export * from './operators'; 4 | export * from './SnakeNamingStrategy'; 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /src/torm/types.ts: -------------------------------------------------------------------------------- 1 | // See https://github.com/typeorm/typeorm/blob/master/test/functional/database-schema/column-types/postgres/entity/Post.ts 2 | 3 | export type ColumnType = 4 | | IntColumnType 5 | | StringColumnType 6 | | FloatColumnType 7 | | NumericColumnType 8 | | JSONColumnType 9 | | BooleanColumnType 10 | | 'money' 11 | | 'citext' 12 | | 'hstore' 13 | | 'bytea' 14 | | 'bit' 15 | | 'varbit' 16 | | 'bit varying' 17 | | 'timetz' 18 | | 'timestamptz' 19 | | 'timestamp' 20 | | 'timestamp without time zone' 21 | | 'timestamp with time zone' 22 | | 'date' 23 | | 'time' 24 | | 'time without time zone' 25 | | 'time with time zone' 26 | | 'interval' 27 | | 'enum' 28 | | 'point' 29 | | 'line' 30 | | 'lseg' 31 | | 'box' 32 | | 'path' 33 | | 'polygon' 34 | | 'circle' 35 | | 'cidr' 36 | | 'inet' 37 | | 'macaddr' 38 | | 'tsvector' 39 | | 'tsquery' 40 | | 'uuid' 41 | | 'xml' 42 | | 'int4range' 43 | | 'int8range' 44 | | 'numrange' 45 | | 'tsrange' 46 | | 'tstzrange' 47 | | 'daterange' 48 | | 'geometry' 49 | | 'geography'; 50 | 51 | export type StringColumnType = 'varchar' | 'character varying' | 'character' | 'char' | 'text'; 52 | export type IntColumnType = 'int' | 'int2' | 'int4' | 'int8' | 'smallint' | 'integer' | 'bigint'; 53 | export type FloatColumnType = 'float' | 'float4' | 'float8' | 'real' | 'double precision'; 54 | export type NumericColumnType = 'numeric' | 'decimal'; 55 | export type MoneyColumnType = 'money'; 56 | export type JSONColumnType = 'json' | 'jsonb'; 57 | export type BooleanColumnType = 'bool' | 'boolean'; 58 | -------------------------------------------------------------------------------- /src/utils/decoratorComposer.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from '../core'; 2 | 3 | export type MethodDecoratorFactory = ( 4 | target: object, 5 | propertyKey: string, 6 | descriptor: PropertyDescriptor 7 | ) => any; 8 | 9 | export function composeMethodDecorators(...factories: MethodDecoratorFactory[]) { 10 | return (target: object, propertyKey: string, descriptor: PropertyDescriptor): any => { 11 | factories.forEach(factory => factory(target, propertyKey, descriptor)); 12 | }; 13 | } 14 | 15 | export type ClassDecoratorFactory = (target: ClassType) => any; 16 | 17 | // any[] -> ClassDecoratorFactory[] 18 | export function composeClassDecorators(...factories: any[]) { 19 | return (target: ClassType): any => { 20 | // Do NOT return anything here or it will take over the class it's decorating 21 | // See: https://www.typescriptlang.org/docs/handbook/decorators.html 22 | factories.forEach(factory => { 23 | return factory(target); 24 | }); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/generatedFolder.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export const generatedFolderPath = (): string => { 4 | return process.env.WARTHOG_GENERATED_FOLDER || path.join(process.cwd(), 'generated'); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decoratorComposer'; 2 | export * from './generatedFolder'; 3 | export * from './object'; 4 | export * from './string'; 5 | -------------------------------------------------------------------------------- /src/utils/object.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable @typescript-eslint/camelcase 2 | import { ObjectUtil } from './object'; 3 | 4 | describe('ObjectUtil', () => { 5 | describe('prefixKeys', () => { 6 | test('prefixes correctly', async () => { 7 | const original = { 8 | One: 1, 9 | Two: 2 10 | }; 11 | 12 | expect(ObjectUtil.prefixKeys(original, 'prefix')).toEqual({ 13 | prefixOne: 1, 14 | prefixTwo: 2 15 | }); 16 | }); 17 | }); 18 | 19 | describe('constantizeKeys', () => { 20 | test('constantizes correctly', async () => { 21 | const original = { 22 | fourFive: 45, 23 | oneTwoThree: 123 24 | }; 25 | 26 | expect(ObjectUtil.constantizeKeys(original)).toEqual({ 27 | FOUR_FIVE: 45, 28 | ONE_TWO_THREE: 123 29 | }); 30 | }); 31 | }); 32 | }); 33 | 34 | // eslint-enable @typescript-eslint/camelcase 35 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | import { StringUtil } from './string'; 2 | 3 | export class ObjectUtil { 4 | // Ex: prefixKeys({one: 1}, 'PRE_') => {PRE_one: 1} 5 | static prefixKeys(obj: { [key: string]: T }, prefix: string) { 6 | return Object.keys(obj).reduce((result: { [key: string]: T }, key: string) => { 7 | result[`${prefix}${key}`] = obj[key]; 8 | return result; 9 | }, {}); 10 | } 11 | 12 | // Ex: constantizeKeys({helloWorld: 1}) => {HELLO_WORLD: 1} 13 | static constantizeKeys(obj: { [key: string]: T }): { [key: string]: T } { 14 | return Object.keys(obj).reduce((result: { [key: string]: T }, key: string) => { 15 | result[StringUtil.constantize(key)] = obj[key]; 16 | return result; 17 | }, {}); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/string.test.ts: -------------------------------------------------------------------------------- 1 | import { StringUtil } from './string'; 2 | 3 | describe('StringUtil', () => { 4 | describe('toConstant', () => { 5 | test('converts string correctly', async () => { 6 | expect(StringUtil.constantize('myCoolString')).toEqual('MY_COOL_STRING'); 7 | }); 8 | 9 | test('handles consecutive caps correctly', async () => { 10 | expect(StringUtil.constantize('USDValue')).toEqual('USD_VALUE'); 11 | expect(StringUtil.constantize('myUSDValue')).toEqual('MY_USD_VALUE'); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export class StringUtil { 2 | // Ex: HelloWorld -> HELLO_WORLD 3 | static constantize(str: string) { 4 | return ( 5 | str 6 | .split(/([A-Z][a-z]+|[a-z]+)/) 7 | // This will return some empty strings that need to be filtered 8 | .filter((item: string) => { 9 | return item.length > 0; 10 | }) 11 | .join('_') 12 | .toUpperCase() 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tools/bootstrap-all.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | for d in examples/*/ 4 | do 5 | cd $d 6 | # pwd 7 | # yarn upgrade && npx syncyarnlock -s -k && yarn 8 | # rm -rf generated 9 | # yarn db:drop 10 | yarn bootstrap 11 | # yarn codegen 12 | cd - 13 | done 14 | -------------------------------------------------------------------------------- /tools/test.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # Make sure dates are imported in UTC so that we don't have off-by-one issues 4 | export TZ=utc 5 | 6 | if [ -z "$SKIP_DB_CREATION" ] 7 | then 8 | NODE_ENV=test PGUSER=postgres ./bin/warthog db:drop 9 | NODE_ENV=test PGUSER=postgres ./bin/warthog db:create 10 | fi 11 | 12 | # Codegen for test files 13 | NODE_ENV=test ./src/test/codegen-test-files.sh 14 | 15 | # Forward command line args to the jest command 16 | NODE_ENV=test jest --verbose --runInBand $@ 17 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | // eslint complains that the example folders aren't in our tsconfig path, 3 | // so we give eslint it's own config file 4 | "extends": "./tsconfig.json", 5 | "include": ["src/**/*", "examples/**/*"], 6 | "exclude": ["tmp", "node_modules/**/*", "**/generated/*"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "src", 5 | "declaration": true, 6 | "declarationDir": "dist/types", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "keyofStringsOnly": true, 11 | "lib": ["es2016", "dom", "es5", "scripthost", "esnext", "esnext.asynciterable"], 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": false, 18 | "outDir": "./dist", 19 | "pretty": true, 20 | "skipLibCheck": true, 21 | "sourceMap": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "target": "es5", 25 | "typeRoots": ["node_modules/@types", "./typings", "./typings/typings.d.ts"], 26 | "types": ["isomorphic-fetch", "node", "jest"] 27 | }, 28 | "exclude": [ 29 | "node_modules", 30 | "tmp", 31 | "**/node_modules/*", 32 | // "src/**/*.test.ts", 33 | "**/generated/*" 34 | // "src/test/**/*" 35 | ], 36 | "include": ["src/**/*", "typings"] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "types": ["jest", "isomorphic-fetch", "node"], 4 | "compilerOptions": { 5 | "strict": false 6 | }, 7 | "include": ["src/**/*", "test/**/*", "typings"], 8 | "exclude": ["node_modules", "tmp", "**/node_modules/*", "examples"] 9 | } 10 | -------------------------------------------------------------------------------- /typings/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pgtools'; 2 | --------------------------------------------------------------------------------