├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── ci.yml │ └── publish-package.yml ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── codecov.yml ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── prisma ├── generated │ └── schema.graphql ├── rules.ts └── schema.prisma ├── src ├── __mocks__ │ └── transpile.js ├── converters │ ├── __mocks__ │ │ └── convertScalar.js │ ├── addTypeModifiers.test.ts │ ├── addTypeModifiers.ts │ ├── convertScalar.test.ts │ ├── convertScalar.ts │ ├── convertType.test.ts │ ├── convertType.ts │ ├── rules │ │ ├── modifier.ts │ │ └── scalar.ts │ └── types.ts ├── diff │ ├── diff.test.ts │ └── previous.cache ├── extractors │ ├── extractId.test.ts │ ├── extractId.ts │ ├── extractScalars.test.ts │ ├── extractScalars.ts │ ├── extractUniques.test.ts │ └── extractUniques.ts ├── formatters │ ├── formatDefinition.test.ts │ ├── formatDefinition.ts │ ├── formatField.test.ts │ ├── formatField.ts │ ├── formatScalar.test.ts │ └── formatScalar.ts ├── generateGraphqlSchema.test.ts ├── generateGraphqlSchema.ts ├── index.ts ├── parse.test.ts ├── parse.ts ├── transpile.test.ts ├── transpile.ts └── utils.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | __mocks__ -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@dooboo/eslint-config', 4 | rules: { 5 | 'eslint-comments/no-unlimited-disable': 0, 6 | 'eslint-comments/no-unused-disable': 0, 7 | }, 8 | globals: { 9 | context: 'readonly', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | platform: [ubuntu-latest] 14 | node: ['16'] 15 | name: Node ${{ matrix.node }} (${{ matrix.platform }}) 16 | runs-on: ${{ matrix.platform }} 17 | 18 | services: 19 | # Label used to access the service container 20 | postgres: 21 | # Docker Hub image 22 | image: postgres 23 | # Provide the password for postgres 24 | env: 25 | POSTGRES_DB: test 26 | POSTGRES_USER: test 27 | POSTGRES_PASSWORD: test! 28 | # Set health checks to wait until postgres has started 29 | options: >- 30 | --health-cmd pg_isready 31 | --health-interval 10s 32 | --health-timeout 5s 33 | --health-retries 5 34 | ports: 35 | # Maps tcp port 5432 on service container to the host 36 | - 5432:5432 37 | 38 | steps: 39 | - name: Get current time 40 | uses: 1466587594/get-current-time@v2 41 | id: current-time 42 | with: 43 | format: YYYYMMDD-HH-mm-ss 44 | utcOffset: "+09:00" 45 | 46 | - uses: actions/checkout@v2 47 | - uses: actions/setup-node@v1 48 | with: 49 | node-version: ${{ matrix.node }} 50 | - uses: pnpm/action-setup@v2.0.1 51 | with: 52 | version: 6.22.2 53 | 54 | - name: Install modules 55 | run: pnpm install 56 | 57 | - name: Check linting 58 | run: pnpm lint 59 | 60 | - name: Test 61 | run: pnpm test -- --coverage --detectOpenHandles --forceExit 62 | 63 | - name: Generate schema.graphql with test data 64 | run: pnpm dev 65 | 66 | - name: Upload generated schema.graphql 67 | uses: actions/upload-artifact@v2 68 | with: 69 | name: ${{ steps.current-time.outputs.formattedTime }}-KST-schema.graphql 70 | path: prisma/generated/schema.graphql 71 | if-no-files-found: error 72 | 73 | - name: Upload coverage to Codecov 74 | uses: codecov/codecov-action@v1.0.10 75 | with: 76 | token: ${{ secrets.CODECOV_TOKEN }} 77 | directory: ./coverage/ 78 | flags: unittests 79 | name: codecov-umbrella 80 | fail_ci_if_error: false 81 | path_to_write_report: ./coverage/codecov_report.gz 82 | -------------------------------------------------------------------------------- /.github/workflows/publish-package.yml: -------------------------------------------------------------------------------- 1 | name: publish-package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: ${{ matrix.node }} 15 | - uses: pnpm/action-setup@v2.0.1 16 | with: 17 | version: 6.22.2 18 | 19 | - uses: actions/setup-node@v2 20 | with: 21 | node-version: '14.x' 22 | registry-url: 'https://registry.npmjs.org' 23 | - run: pnpm install 24 | - run: pnpm build 25 | - run: npm publish 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .DS_Store 4 | .env 5 | 6 | lib 7 | coverage 8 | 9 | dist-* 10 | tsconfig.esm.tsbuildinfo 11 | tsconfig.cjs.tsbuildinfo 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist-cjs 3 | !dist-esm 4 | !README.md 5 | !package.json 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // prettier.config.js or .prettierrc.js 2 | module.exports = { 3 | trailingComma: "all", 4 | arrowParens: "always", 5 | singleQuote: true, 6 | jsxSingleQuote: false, 7 | bracketSpacing: false, 8 | }; 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/dist-cjs": true, 4 | "**/dist-esm": true, 5 | "**/__snapshots__": true, 6 | "pnpm-lock.yaml": true 7 | }, 8 | "typescript.tsdk": "node_modules/typescript/lib", 9 | "typescript.enablePromptUseWorkspaceTsdk": true 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelogs 2 | 3 | ## 0.1.0 4 | 5 | Release first working version. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GraphQL-Schema-Generator 2 | 3 | Thanks for taking the time to contribute! 4 | 5 | The following is a set of guidelines for contributing to GraphQL-Schema-Generator. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## Architecture 8 | 9 | GraphQL-Schema-Generator relies on [Prisma Generator](https://prismaio.notion.site/Prisma-Generators-a2cdf262207a4e9dbcd0e362dfac8dc0). 10 | 11 | ![Architecture](https://user-images.githubusercontent.com/61503739/146618416-1b4334a1-955a-47e3-8ca0-3806a1d8f6c8.png) 12 | `1. Parse Schema, 2. Provide DMMF` is done by `@prisma/sdk` and `3. parse.ts, 4. transpile.ts` is from this codebase. 13 | 14 | ## development 15 | 16 | Currently, there're three ways to see how changes you made affects the output. 17 | 18 | 1. Write a test about it (Ideally, it should happen before changes in code). 19 | 2. Run `pnpm dev` to see output of example in `schema.prisma`. (You can try other file) 20 | 3. Remove `skip` in `src/diff/diff.test.ts` and see `diff` of previous and current output. Using with jest watch mode, you can get instant feedback about changes you made. 21 | 22 | ## Testing 23 | 24 | Testing is the most important part of your contribution. Every pull request, except for refactoring or simple cases such as typo correction, and documentation, should contain tests that describe what you did in the PR. 25 | 26 | To run test, use the following command. 27 | 28 | ```shell 29 | pnpm test 30 | ``` 31 | 32 | All test file should be adjacent to the target file. (No folders like `/test`) 33 | 34 | It is most ideal that the test code is written well as presented in the guide, **but if you do not, write an explanation with sufficient examples. We will help you fill out the test.** 35 | 36 | ## PR / Issue 37 | 38 | PR and issue should be minimal as possible. 39 | 40 | ## Feature 41 | 42 | Keep in mind that each test should act like documentation. 43 | 44 | ## Bug Report / Fix 45 | 46 | 1. Write a failing test that describe bug. (If you stop here, it's a bug report.) 47 | 2. Write a code that passes the test. (You solve it!) 48 | 49 | ## Refactoring 50 | 51 | If target code and test code are both modified, it is not refactoring. 52 | 53 | ## Etc 54 | 55 | Other contributions such as documentation is always welcomed. We will make guild about this in the future. 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 dooboolab 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL-Schema-Generator for Prisma 2 | 3 | 4 | [![CI](https://github.com/prisma-korea/graphql-schema-generator/actions/workflows/ci.yml/badge.svg)](https://github.com/prisma-korea/graphql-schema-generator/actions/workflows/ci.yml) 5 | [![codecov](https://codecov.io/gh/prisma-korea/graphql-schema-generator/branch/master/graph/badge.svg?token=H4VN0S3ES9)](https://codecov.io/gh/prisma-korea/graphql-schema-generator) 6 | 7 | 8 | Generate **GraphQL schema (SDL)** from **Prisma schema** using a custom Prisma generator. 9 | 10 | ## Getting Started 11 | 12 | 1. Install this package in your project using: 13 | 14 | ```sh 15 | // pnpm, npm, or yarn 16 | pnpm install @prisma-korea/graphql-schema-generator 17 | ``` 18 | 19 | 2. Add the generator to the `schema.prisma`: 20 | 21 | ```prisma 22 | generator graphql { 23 | provider = "graphql-schema-generator" 24 | createCRUD = "true" 25 | # output = "./generated" This is default path. 26 | } 27 | ``` 28 | 29 | 3. Run `npx prisma generate` to run the generator 30 | 31 | 4. Check `schema.graphql` in `./prisma/generated` 🎉 32 | 33 | ## Custom Rules 34 | You can apply custom rules to manipulate behavior. [(Some use cases)](https://github.com/prisma-korea/graphql-schema-generator/issues/34). See [this](https://github.com/prisma-korea/graphql-schema-generator/tree/master/prisma) for example. 35 | 36 | > Example dir structure 37 | 38 | ![image](https://user-images.githubusercontent.com/27461460/149453371-3991e868-ba43-4cf4-9d2a-c03e66c6eb75.png) 39 | 40 | > Example usage 41 | 42 | ``` 43 | generator graphql { 44 | provider = "graphql-schema-generator" 45 | output = "../src/schemas" 46 | createCRUD = "true" 47 | customRules = "../prisma/rules.ts" 48 | } 49 | ``` 50 | 51 | > Example [rules code](https://github.com/prisma-korea/graphql-schema-generator/issues/15#issuecomment-1012775364) 52 | 53 | ## Contributing 54 | 55 | Any contributions are welcome. If you are interested, check out our [guidelines](https://github.com/prisma-korea/graphql-schema-generator/blob/master/CONTRIBUTING.md). 56 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-typescript', 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | project: yes 12 | patch: yes 13 | changes: no 14 | 15 | parsers: 16 | gcov: 17 | branch_detection: 18 | conditional: yes 19 | loop: yes 20 | method: no 21 | macro: no 22 | 23 | comment: 24 | layout: "header, diff" 25 | behavior: default 26 | require_changes: no 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: [ 3 | "jest-plugin-context/setup" 4 | ], 5 | transform: { 6 | '^.+\\.js$': 'babel-jest', 7 | '^.+\\.ts$': 'ts-jest', 8 | }, 9 | modulePaths: [''], 10 | moduleDirectories: ['node_modules', 'src'], 11 | moduleFileExtensions: ['js', 'ts'], 12 | modulePathIgnorePatterns: ['dist', 'build'], 13 | testEnvironment: 'jsdom', 14 | globals: { 15 | window: {}, 16 | 'ts-jest': { 17 | babelConfig: true, 18 | tsconfig: 'tsconfig.json', 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prisma-korea/graphql-schema-generator", 3 | "version": "0.3.0", 4 | "main": "dist-cjs/index.js", 5 | "types": "dist-cjs/index.d.ts", 6 | "engines": { 7 | "node": ">=12.6" 8 | }, 9 | "scripts": { 10 | "dev": "pnpm build && rm -rf prisma/generated && npx prisma generate", 11 | "clean": "rm -rf dist-cjs dist-esm node_modules/.cache", 12 | "build": "pnpm clean && tsc --build tsconfig.cjs.json tsconfig.esm.json", 13 | "test": "jest", 14 | "lint": "eslint src --ext .ts,.js" 15 | }, 16 | "author": "dooboolab ", 17 | "maintainers": [ 18 | "Jayden ", 19 | "Hyo " 20 | ], 21 | "sideEffects": false, 22 | "dependencies": { 23 | "@prisma/generator-helper": "^3.5.0", 24 | "@prisma/sdk": "^3.5.0", 25 | "graphql": "^16.2.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.15.8", 29 | "@babel/preset-env": "^7.15.8", 30 | "@babel/preset-typescript": "^7.15.0", 31 | "@dooboo/eslint-config": "^1.2.1", 32 | "@prisma/client": "3.4.0", 33 | "@types/jest": "^27.0.2", 34 | "@types/jest-plugin-context": "^2.9.4", 35 | "@typescript-eslint/eslint-plugin": "^5.7.0", 36 | "@typescript-eslint/parser": "^5.7.0", 37 | "babel-jest": "^27.3.1", 38 | "eslint": "^8.4.1", 39 | "eslint-config-airbnb-base": "^15.0.0", 40 | "eslint-import-resolver-typescript": "^2.5.0", 41 | "eslint-plugin-import": "^2.25.3", 42 | "jest": "^27.3.1", 43 | "jest-diff": "^27.4.2", 44 | "jest-plugin-context": "^2.9.0", 45 | "jest-transform-stub": "^2.0.0", 46 | "prettier": "^2.5.1", 47 | "prisma": "3.4.0", 48 | "ts-jest": "^27.0.7", 49 | "tslib": "^2.3.1", 50 | "typescript": "^4.4.4", 51 | "typescript-eslint": "^0.0.1-alpha.0" 52 | }, 53 | "peerDependencies": { 54 | "prisma": "*" 55 | }, 56 | "peerDependenciesMeta": { 57 | "prisma": { 58 | "optional": true 59 | } 60 | }, 61 | "description": "Generate Graphql schema(SDL) from Prisma schema using Prisma generator.", 62 | "repository": { 63 | "type": "git", 64 | "url": "git+https://github.com/prisma-korea/graphql-schema-generator.git" 65 | }, 66 | "keywords": [ 67 | "prisma", 68 | "graphql", 69 | "schema" 70 | ], 71 | "license": "MIT", 72 | "bugs": { 73 | "url": "https://github.com/prisma-korea/graphql-schema-generator/issues" 74 | }, 75 | "homepage": "https://github.com/prisma-korea/graphql-schema-generator#readme", 76 | "files": [ 77 | "dist-cjs", 78 | "dist-esm" 79 | ], 80 | "exports": { 81 | ".": { 82 | "require": "./dist-cjs/index.js", 83 | "import": "./dist-esm/index.js" 84 | }, 85 | "./*": { 86 | "default": "./*.js" 87 | } 88 | }, 89 | "typesVersions": { 90 | "*": { 91 | "index.d.ts": [ 92 | "./dist-cjs/entrypoints/main.d.ts" 93 | ], 94 | "*": [ 95 | "./*" 96 | ] 97 | } 98 | }, 99 | "bin": { 100 | "graphql-schema-generator": "./dist-cjs/index.js" 101 | } 102 | } -------------------------------------------------------------------------------- /prisma/generated/schema.graphql: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | This file was generated by graphql-schema-generator which is 4 | maintained by prisma-korea. 5 | 6 | Do not make changes to this file directly. 7 | Read more about in https://github.com/prisma-korea/graphql-schema-generator. 8 | """ 9 | type Query { 10 | user(id: ID!): User 11 | users: [User!]! 12 | notification(id: ID!): Notification 13 | notifications: [Notification!]! 14 | profile(id: ID!): Profile 15 | profiles: [Profile!]! 16 | friend(id: ID!): Friend 17 | friends: [Friend!]! 18 | blockeduser(id: ID!): BlockedUser 19 | blockedusers: [BlockedUser!]! 20 | report(id: ID!): Report 21 | reports: [Report!]! 22 | membership(id: ID!): Membership 23 | memberships: [Membership!]! 24 | channel(id: ID!): Channel 25 | channels: [Channel!]! 26 | message(id: ID!): Message 27 | messages: [Message!]! 28 | reply(id: ID!): Reply 29 | replys: [Reply!]! 30 | reaction(id: ID!): Reaction 31 | reactions: [Reaction!]! 32 | } 33 | 34 | input UserCreateInput { 35 | email: String 36 | password: String 37 | name: String 38 | nickname: String 39 | thumbURL: String 40 | photoURL: String 41 | birthday: DateTime 42 | gender: Gender 43 | phone: String 44 | statusMessage: String 45 | verified: Boolean 46 | lastSignedIn: DateTime 47 | isOnline: Boolean 48 | createdAt: DateTime 49 | updatedAt: DateTime 50 | isDeleted: Boolean 51 | profile: Profile 52 | notifications: [Notification] 53 | friends: [Friend] 54 | memberships: [Membership] 55 | blockedUsers: [BlockedUser] 56 | reports: [Report] 57 | messages: [Message] 58 | replys: [Reply] 59 | reactions: [Reaction] 60 | Friend: [Friend] 61 | Report: [Report] 62 | BlockedUser: [BlockedUser] 63 | } 64 | 65 | input UserUpdateInput { 66 | email: String 67 | password: String 68 | name: String 69 | nickname: String 70 | thumbURL: String 71 | photoURL: String 72 | birthday: DateTime 73 | gender: Gender 74 | phone: String 75 | statusMessage: String 76 | verified: Boolean 77 | lastSignedIn: DateTime 78 | isOnline: Boolean 79 | createdAt: DateTime 80 | updatedAt: DateTime 81 | isDeleted: Boolean 82 | profile: Profile 83 | notifications: [Notification] 84 | friends: [Friend] 85 | memberships: [Membership] 86 | blockedUsers: [BlockedUser] 87 | reports: [Report] 88 | messages: [Message] 89 | replys: [Reply] 90 | reactions: [Reaction] 91 | Friend: [Friend] 92 | Report: [Report] 93 | BlockedUser: [BlockedUser] 94 | } 95 | 96 | input NotificationCreateInput { 97 | token: String! 98 | device: String 99 | os: String 100 | user: User! 101 | createdAt: DateTime 102 | } 103 | 104 | input NotificationUpdateInput { 105 | token: String 106 | device: String 107 | os: String 108 | user: User 109 | createdAt: DateTime 110 | } 111 | 112 | input ProfileCreateInput { 113 | socialId: String 114 | authType: AuthType 115 | User: User! 116 | } 117 | 118 | input ProfileUpdateInput { 119 | socialId: String 120 | authType: AuthType 121 | User: User 122 | } 123 | 124 | input FriendCreateInput { 125 | createdAt: DateTime 126 | updatedAt: DateTime 127 | isDeleted: Boolean 128 | user: User! 129 | friend: User! 130 | } 131 | 132 | input FriendUpdateInput { 133 | createdAt: DateTime 134 | updatedAt: DateTime 135 | isDeleted: Boolean 136 | user: User 137 | friend: User 138 | } 139 | 140 | input BlockedUserCreateInput { 141 | createdAt: DateTime 142 | updatedAt: DateTime 143 | isDeleted: Boolean 144 | user: User! 145 | blockedUser: User! 146 | } 147 | 148 | input BlockedUserUpdateInput { 149 | createdAt: DateTime 150 | updatedAt: DateTime 151 | isDeleted: Boolean 152 | user: User 153 | blockedUser: User 154 | } 155 | 156 | input ReportCreateInput { 157 | report: String! 158 | createdAt: DateTime 159 | updatedAt: DateTime 160 | isDeleted: Boolean 161 | user: User! 162 | reportedUser: User! 163 | } 164 | 165 | input ReportUpdateInput { 166 | report: String 167 | createdAt: DateTime 168 | updatedAt: DateTime 169 | isDeleted: Boolean 170 | user: User 171 | reportedUser: User 172 | } 173 | 174 | input MembershipCreateInput { 175 | alertMode: AlertMode 176 | membershipType: MembershipType! 177 | isVisible: Boolean! 178 | createdAt: DateTime 179 | updatedAt: DateTime 180 | user: User! 181 | channel: Channel! 182 | } 183 | 184 | input MembershipUpdateInput { 185 | alertMode: AlertMode 186 | membershipType: MembershipType 187 | isVisible: Boolean 188 | createdAt: DateTime 189 | updatedAt: DateTime 190 | user: User 191 | channel: Channel 192 | } 193 | 194 | input ChannelCreateInput { 195 | name: String 196 | messages: [Message] 197 | membership: [Membership] 198 | lastMessageId: String 199 | createdAt: DateTime 200 | updatedAt: DateTime 201 | isDeleted: Boolean 202 | } 203 | 204 | input ChannelUpdateInput { 205 | name: String 206 | messages: [Message] 207 | membership: [Membership] 208 | lastMessageId: String 209 | createdAt: DateTime 210 | updatedAt: DateTime 211 | isDeleted: Boolean 212 | } 213 | 214 | input MessageCreateInput { 215 | messageType: MessageType! 216 | text: String 217 | imageUrls: [String] 218 | fileUrls: [String] 219 | reactions: [Reaction] 220 | replies: [Reply] 221 | createdAt: DateTime 222 | updatedAt: DateTime 223 | isDeleted: Boolean 224 | channel: Channel! 225 | sender: User! 226 | } 227 | 228 | input MessageUpdateInput { 229 | messageType: MessageType 230 | text: String 231 | imageUrls: [String] 232 | fileUrls: [String] 233 | reactions: [Reaction] 234 | replies: [Reply] 235 | createdAt: DateTime 236 | updatedAt: DateTime 237 | isDeleted: Boolean 238 | channel: Channel 239 | sender: User 240 | } 241 | 242 | input ReplyCreateInput { 243 | messageType: MessageType! 244 | text: String 245 | imageUrls: [String] 246 | fileUrls: [String] 247 | reactions: [Reaction] 248 | createdAt: DateTime 249 | updatedAt: DateTime 250 | isDeleted: Boolean 251 | sender: User! 252 | message: Message! 253 | } 254 | 255 | input ReplyUpdateInput { 256 | messageType: MessageType 257 | text: String 258 | imageUrls: [String] 259 | fileUrls: [String] 260 | reactions: [Reaction] 261 | createdAt: DateTime 262 | updatedAt: DateTime 263 | isDeleted: Boolean 264 | sender: User 265 | message: Message 266 | } 267 | 268 | input ReactionCreateInput { 269 | value: String! 270 | user: User! 271 | message: Message! 272 | reply: Reply! 273 | } 274 | 275 | input ReactionUpdateInput { 276 | value: String 277 | user: User 278 | message: Message 279 | reply: Reply 280 | } 281 | 282 | type Mutation { 283 | createUser(user: UserCreateInput!): User 284 | updateUser(user: UserUpdateInput!): User 285 | deleteUser(id: ID!): User 286 | createNotification(notification: NotificationCreateInput!): Notification 287 | updateNotification(notification: NotificationUpdateInput!): Notification 288 | deleteNotification(id: ID!): Notification 289 | createProfile(profile: ProfileCreateInput!): Profile 290 | updateProfile(profile: ProfileUpdateInput!): Profile 291 | deleteProfile(id: ID!): Profile 292 | createFriend(friend: FriendCreateInput!): Friend 293 | updateFriend(friend: FriendUpdateInput!): Friend 294 | deleteFriend(id: ID!): Friend 295 | createBlockedUser(blockeduser: BlockedUserCreateInput!): BlockedUser 296 | updateBlockedUser(blockeduser: BlockedUserUpdateInput!): BlockedUser 297 | deleteBlockedUser(id: ID!): BlockedUser 298 | createReport(report: ReportCreateInput!): Report 299 | updateReport(report: ReportUpdateInput!): Report 300 | deleteReport(id: ID!): Report 301 | createMembership(membership: MembershipCreateInput!): Membership 302 | updateMembership(membership: MembershipUpdateInput!): Membership 303 | deleteMembership(id: ID!): Membership 304 | createChannel(channel: ChannelCreateInput!): Channel 305 | updateChannel(channel: ChannelUpdateInput!): Channel 306 | deleteChannel(id: ID!): Channel 307 | createMessage(message: MessageCreateInput!): Message 308 | updateMessage(message: MessageUpdateInput!): Message 309 | deleteMessage(id: ID!): Message 310 | createReply(reply: ReplyCreateInput!): Reply 311 | updateReply(reply: ReplyUpdateInput!): Reply 312 | deleteReply(id: ID!): Reply 313 | createReaction(reaction: ReactionCreateInput!): Reaction 314 | updateReaction(reaction: ReactionUpdateInput!): Reaction 315 | deleteReaction(id: ID!): Reaction 316 | } 317 | 318 | scalar DateTime 319 | 320 | enum Gender { 321 | male 322 | female 323 | } 324 | 325 | enum AuthType { 326 | email 327 | facebook 328 | google 329 | apple 330 | } 331 | 332 | enum MembershipType { 333 | owner 334 | admin 335 | member 336 | } 337 | 338 | enum AlertMode { 339 | sound 340 | vibrate 341 | silent 342 | } 343 | 344 | enum ChannelType { 345 | private 346 | public 347 | self 348 | } 349 | 350 | enum MessageType { 351 | text 352 | photo 353 | file 354 | } 355 | 356 | type User { 357 | id: ID! 358 | email: String 359 | password: String 360 | name: String 361 | nickname: String 362 | thumbURL: String 363 | photoURL: String 364 | birthday: DateTime 365 | gender: Gender 366 | phone: String 367 | statusMessage: String 368 | verified: Boolean 369 | lastSignedIn: DateTime 370 | isOnline: Boolean 371 | createdAt: DateTime 372 | updatedAt: DateTime 373 | isDeleted: Boolean 374 | profile: Profile 375 | notifications: [Notification] 376 | friends: [Friend] 377 | memberships: [Membership] 378 | blockedUsers: [BlockedUser] 379 | reports: [Report] 380 | messages: [Message] 381 | replys: [Reply] 382 | reactions: [Reaction] 383 | Friend: [Friend] 384 | Report: [Report] 385 | BlockedUser: [BlockedUser] 386 | } 387 | 388 | type Notification { 389 | id: ID! 390 | token: String! 391 | device: String 392 | os: String 393 | user: User! 394 | createdAt: DateTime 395 | } 396 | 397 | type Profile { 398 | id: ID! 399 | socialId: String 400 | authType: AuthType 401 | User: User! 402 | } 403 | 404 | type Friend { 405 | createdAt: DateTime 406 | updatedAt: DateTime 407 | isDeleted: Boolean 408 | user: User! 409 | friend: User! 410 | } 411 | 412 | type BlockedUser { 413 | createdAt: DateTime 414 | updatedAt: DateTime 415 | isDeleted: Boolean 416 | user: User! 417 | blockedUser: User! 418 | } 419 | 420 | type Report { 421 | id: ID! 422 | report: String! 423 | createdAt: DateTime 424 | updatedAt: DateTime 425 | isDeleted: Boolean 426 | user: User! 427 | reportedUser: User! 428 | } 429 | 430 | type Membership { 431 | alertMode: AlertMode 432 | membershipType: MembershipType! 433 | isVisible: Boolean! 434 | createdAt: DateTime 435 | updatedAt: DateTime 436 | user: User! 437 | channel: Channel! 438 | } 439 | 440 | type Channel { 441 | id: ID! 442 | name: String 443 | messages: [Message] 444 | membership: [Membership] 445 | lastMessageId: String 446 | createdAt: DateTime 447 | updatedAt: DateTime 448 | isDeleted: Boolean 449 | } 450 | 451 | type Message { 452 | id: ID! 453 | messageType: MessageType! 454 | text: String 455 | imageUrls: [String] 456 | fileUrls: [String] 457 | reactions: [Reaction] 458 | replies: [Reply] 459 | createdAt: DateTime 460 | updatedAt: DateTime 461 | isDeleted: Boolean 462 | channel: Channel! 463 | sender: User! 464 | } 465 | 466 | type Reply { 467 | id: ID! 468 | messageType: MessageType! 469 | text: String 470 | imageUrls: [String] 471 | fileUrls: [String] 472 | reactions: [Reaction] 473 | createdAt: DateTime 474 | updatedAt: DateTime 475 | isDeleted: Boolean 476 | sender: User! 477 | message: Message! 478 | } 479 | 480 | type Reaction { 481 | id: ID! 482 | value: String! 483 | user: User! 484 | message: Message! 485 | reply: Reply! 486 | } -------------------------------------------------------------------------------- /prisma/rules.ts: -------------------------------------------------------------------------------- 1 | const {SDL} = require('../dist-cjs'); 2 | 3 | const rules = { 4 | beforeAddingTypeModifiers: [ 5 | { 6 | matcher: (field) => { 7 | const {name} = field; 8 | 9 | if (name === 'deletedAt') { 10 | return true; 11 | } 12 | 13 | return false; 14 | }, 15 | transformer: (field) => { 16 | return { 17 | ...field, 18 | name: 'isDeleted', 19 | type: SDL.Boolean, 20 | }; 21 | }, 22 | }, 23 | { 24 | matcher: (field) => { 25 | const {type} = field; 26 | 27 | if (type === 'ChannelType') { 28 | return true; 29 | } 30 | 31 | return false; 32 | }, 33 | transformer: () => { 34 | throw null; 35 | }, 36 | }, 37 | ], 38 | afterAddingTypeModifiers: [ 39 | { 40 | matcher: (field) => { 41 | const {type} = field; 42 | 43 | if (/\[(\w+)!]!/gm.exec(type)) { 44 | return true; 45 | } 46 | 47 | return false; 48 | }, 49 | transformer: (field) => { 50 | const {type} = field; 51 | 52 | const match = /\[(\w+)!]!/gm.exec(type); 53 | 54 | if (!match) { 55 | return field; 56 | } 57 | 58 | const [_, typeWithoutModifiers] = match; 59 | 60 | return {...field, type: `[${typeWithoutModifiers}]`}; 61 | }, 62 | }, 63 | ], 64 | }; 65 | 66 | module.exports = {rules}; 67 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator graphql { 7 | provider = "node ./dist-cjs/index.js" 8 | output = "./generated" 9 | createCRUD = "true" 10 | customRules = "./rules.ts" 11 | } 12 | 13 | // Try yours! Schema below is from here: 14 | // https://github.com/dooboolab/hackatalk/blob/master/server/prisma/schema.prisma 15 | 16 | enum Gender { 17 | male 18 | female 19 | } 20 | 21 | enum AuthType { 22 | email 23 | facebook 24 | google 25 | apple 26 | } 27 | 28 | model User { 29 | id String @id @default(cuid()) 30 | email String? @unique 31 | password String? 32 | name String? 33 | nickname String? 34 | thumbURL String? 35 | photoURL String? 36 | birthday DateTime? 37 | gender Gender? 38 | phone String? 39 | statusMessage String? 40 | verified Boolean? @default(false) 41 | lastSignedIn DateTime? 42 | isOnline Boolean? @default(false) 43 | createdAt DateTime? @default(now()) 44 | updatedAt DateTime? @default(now()) @updatedAt 45 | deletedAt DateTime? 46 | profile Profile? 47 | notifications Notification[] 48 | friends Friend[] 49 | memberships Membership[] 50 | blockedUsers BlockedUser[] 51 | reports Report[] @relation("UserReport") 52 | messages Message[] 53 | replys Reply[] 54 | reactions Reaction[] 55 | Friend Friend[] @relation("FriendFrom") 56 | Report Report[] 57 | BlockedUser BlockedUser[] @relation("UserBlock") 58 | } 59 | 60 | model Notification { 61 | id Int @id @default(autoincrement()) 62 | token String @unique 63 | device String? 64 | os String? 65 | userId String 66 | user User @relation(fields: [userId], references: [id]) 67 | createdAt DateTime? @default(now()) 68 | } 69 | 70 | model Profile { 71 | id Int @id @default(autoincrement()) 72 | socialId String? 73 | authType AuthType? 74 | userId String @unique 75 | User User @relation(fields: [userId], references: [id]) 76 | } 77 | 78 | model Friend { 79 | createdAt DateTime? @default(now()) 80 | updatedAt DateTime? @default(now()) @updatedAt 81 | deletedAt DateTime? 82 | userId String 83 | user User @relation("FriendFrom", fields: [userId], references: [id]) 84 | friendId String 85 | friend User @relation(fields: [friendId], references: [id]) 86 | @@id([userId, friendId]) 87 | } 88 | 89 | model BlockedUser { 90 | createdAt DateTime? @default(now()) 91 | updatedAt DateTime? @default(now()) @updatedAt 92 | deletedAt DateTime? 93 | userId String 94 | user User @relation("UserBlock", fields: [userId], references: [id]) 95 | blockedUserId String 96 | blockedUser User @relation(fields: [blockedUserId], references: [id]) 97 | @@id([userId, blockedUserId]) 98 | } 99 | 100 | model Report { 101 | id String @id @default(cuid()) 102 | report String 103 | createdAt DateTime? @default(now()) 104 | updatedAt DateTime? @default(now()) @updatedAt 105 | deletedAt DateTime? 106 | userId String 107 | user User @relation("UserReport", fields: [userId], references: [id]) 108 | reportedUserId String 109 | reportedUser User @relation(fields: [reportedUserId], references: [id]) 110 | } 111 | 112 | enum MembershipType { 113 | owner 114 | admin 115 | member 116 | } 117 | 118 | enum AlertMode { 119 | sound 120 | vibrate 121 | silent 122 | } 123 | 124 | model Membership { 125 | alertMode AlertMode? @default(sound) 126 | membershipType MembershipType @default(member) 127 | isVisible Boolean @default(true) 128 | createdAt DateTime? @default(now()) 129 | updatedAt DateTime? @default(now()) @updatedAt 130 | userId String 131 | user User @relation(fields: [userId], references: [id]) 132 | channelId String 133 | channel Channel @relation(fields: [channelId], references: [id]) 134 | @@id([userId, channelId]) 135 | @@index([updatedAt]) 136 | } 137 | 138 | enum ChannelType { 139 | private 140 | public 141 | self 142 | } 143 | 144 | model Channel { 145 | id String @id @default(cuid()) 146 | channelType ChannelType 147 | name String? 148 | messages Message[] @relation("Message") 149 | membership Membership[] 150 | lastMessageId String? 151 | createdAt DateTime? @default(now()) 152 | updatedAt DateTime? @default(now()) @updatedAt 153 | deletedAt DateTime? 154 | } 155 | 156 | enum MessageType { 157 | text 158 | photo 159 | file 160 | } 161 | 162 | model Message { 163 | id String @id @default(cuid()) 164 | messageType MessageType @default(text) 165 | text String? 166 | imageUrls String[] 167 | fileUrls String[] 168 | reactions Reaction[] 169 | replies Reply[] 170 | createdAt DateTime? @default(now()) 171 | updatedAt DateTime? @default(now()) @updatedAt 172 | deletedAt DateTime? 173 | channelId String 174 | channel Channel @relation("Message", fields: [channelId], references: [id]) 175 | senderId String 176 | sender User @relation(fields: [senderId], references: [id]) 177 | } 178 | 179 | model Reply { 180 | id Int @id @default(autoincrement()) 181 | messageType MessageType @default(text) 182 | text String? 183 | imageUrls String[] 184 | fileUrls String[] 185 | reactions Reaction[] 186 | createdAt DateTime? @default(now()) 187 | updatedAt DateTime? @default(now()) @updatedAt 188 | deletedAt DateTime? 189 | senderId String 190 | sender User @relation(fields: [senderId], references: [id]) 191 | messageId String 192 | message Message @relation(fields: [messageId], references: [id]) 193 | } 194 | 195 | model Reaction { 196 | id Int @id @default(autoincrement()) 197 | value String 198 | userId String 199 | user User @relation(fields: [userId], references: [id]) 200 | messageId String 201 | message Message @relation(fields: [messageId], references: [id]) 202 | replyId Int 203 | reply Reply @relation(fields: [replyId], references: [id]) 204 | } 205 | -------------------------------------------------------------------------------- /src/__mocks__/transpile.js: -------------------------------------------------------------------------------- 1 | const transpile = jest.fn(); 2 | 3 | export default transpile; 4 | -------------------------------------------------------------------------------- /src/converters/__mocks__/convertScalar.js: -------------------------------------------------------------------------------- 1 | const convertScaler = jest.fn(); 2 | 3 | export default convertScaler; 4 | -------------------------------------------------------------------------------- /src/converters/addTypeModifiers.test.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | import {CustomRules, PSL} from './types'; 4 | import addTypeModifier from './addTypeModifiers'; 5 | 6 | describe('addTypeModifier', () => { 7 | context('when custom rule is provided', () => { 8 | it('ignores existing rule, and apply custom rules', () => { 9 | const customRules: CustomRules = { 10 | afterAddingTypeModifiers: [ 11 | { 12 | matcher: (field) => { 13 | const {type} = field; 14 | 15 | return type === 'String'; 16 | }, 17 | transformer: (field) => { 18 | const {type} = field; 19 | 20 | return {...field, type: `${type}?`}; 21 | }, 22 | }, 23 | ], 24 | }; 25 | 26 | expect( 27 | addTypeModifier( 28 | {type: PSL.String, isRequired: true} as DMMF.Field, 29 | {} as DMMF.Model, 30 | ), 31 | ).toEqual({type: 'String!', isRequired: true}); 32 | 33 | expect( 34 | addTypeModifier( 35 | {type: PSL.String, isRequired: true} as DMMF.Field, 36 | {} as DMMF.Model, 37 | {customRules}, 38 | ), 39 | ).toEqual({type: 'String?', isRequired: true}); 40 | }); 41 | }); 42 | 43 | context('when custom rule is not provided', () => { 44 | it('adds ! for non-nullable type', () => { 45 | expect( 46 | addTypeModifier( 47 | {type: PSL.String, isRequired: false} as DMMF.Field, 48 | {} as DMMF.Model, 49 | ), 50 | ).toEqual({type: PSL.String, isRequired: false}); 51 | 52 | expect( 53 | addTypeModifier( 54 | {type: PSL.String, isRequired: true} as DMMF.Field, 55 | {} as DMMF.Model, 56 | ), 57 | ).toEqual({type: 'String!', isRequired: true}); 58 | }); 59 | 60 | it('add [!]! for list', () => { 61 | const field = {type: PSL.String, isList: true, isRequired: true}; 62 | 63 | expect(addTypeModifier(field as DMMF.Field, {} as DMMF.Model)).toEqual({ 64 | type: '[String!]!', 65 | isList: true, 66 | isRequired: true, 67 | }); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/converters/addTypeModifiers.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | import {Rule} from './types'; 4 | import existingRules from './rules/modifier'; 5 | 6 | import type {Config} from '../generateGraphqlSchema'; 7 | 8 | const addTypeModifiers = ( 9 | initialField: DMMF.Field, 10 | model: DMMF.Model, 11 | config?: Config, 12 | ): DMMF.Field => { 13 | const rules = config?.customRules?.afterAddingTypeModifiers 14 | ? config.customRules.afterAddingTypeModifiers 15 | : existingRules; 16 | 17 | const newField = rules.reduce( 18 | (field, {matcher, transformer}: Rule): DMMF.Field => { 19 | if (matcher(field, model)) { 20 | return transformer(field); 21 | } 22 | 23 | return field; 24 | }, 25 | initialField, 26 | ); 27 | 28 | return newField; 29 | }; 30 | 31 | export default addTypeModifiers; 32 | -------------------------------------------------------------------------------- /src/converters/convertScalar.test.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | import {PSL, SDL, Scalar, CustomRules} from './types'; 4 | import convertScalar from './convertScalar'; 5 | 6 | describe('convertScalar', () => { 7 | context('when custom rule is provided', () => { 8 | it('ignores existing rule, and apply custom rules', () => { 9 | const customRules: CustomRules = { 10 | beforeAddingTypeModifiers: [ 11 | { 12 | matcher: () => true, 13 | transformer: (field) => ({...field, type: SDL.Boolean}), 14 | }, 15 | ], 16 | }; 17 | 18 | const field = { 19 | type: PSL.BigInt, 20 | }; 21 | 22 | expect( 23 | convertScalar( 24 | field as DMMF.Field, 25 | {fields: []} as unknown as DMMF.Model, 26 | {customRules}, 27 | ), 28 | ).toEqual({...field, type: SDL.Boolean}); 29 | }); 30 | 31 | it('converts Json -> String', () => { 32 | const field = { 33 | type: PSL.Json, 34 | }; 35 | 36 | expect( 37 | convertScalar( 38 | field as DMMF.Field, 39 | {fields: []} as unknown as DMMF.Model, 40 | ), 41 | ).toEqual({...field, type: SDL.String}); 42 | }); 43 | }); 44 | 45 | context('when custom rule is not provided', () => { 46 | it.each(['String', 'Boolean', 'Int', 'Float'])( 47 | 'does nothing for %s', 48 | (type) => { 49 | type T = 'String' | 'Boolean' | 'Int' | 'Float'; 50 | 51 | const field = { 52 | type: PSL[type as T], 53 | }; 54 | 55 | expect( 56 | convertScalar( 57 | field as DMMF.Field, 58 | {fields: []} as unknown as DMMF.Model, 59 | ), 60 | ).toEqual(field); 61 | }, 62 | ); 63 | 64 | it('converts BigInt to Int', () => { 65 | const field = { 66 | type: PSL.BigInt, 67 | }; 68 | 69 | expect( 70 | convertScalar( 71 | field as DMMF.Field, 72 | {fields: []} as unknown as DMMF.Model, 73 | ), 74 | ).toEqual({...field, type: SDL.Int}); 75 | }); 76 | 77 | it('converts Decimal to Float', () => { 78 | const field = { 79 | type: PSL.Decimal, 80 | }; 81 | 82 | expect( 83 | convertScalar( 84 | field as DMMF.Field, 85 | {fields: []} as unknown as DMMF.Model, 86 | ), 87 | ).toEqual({...field, type: SDL.Float}); 88 | }); 89 | 90 | it('converts Bytes to ByteArray', () => { 91 | const field = { 92 | type: PSL.Bytes, 93 | }; 94 | 95 | expect( 96 | convertScalar( 97 | field as DMMF.Field, 98 | {fields: []} as unknown as DMMF.Model, 99 | ), 100 | ).toEqual({...field, type: Scalar.ByteArray}); 101 | }); 102 | 103 | it('converts every type declared as @id to ID', () => { 104 | expect( 105 | convertScalar( 106 | {type: PSL.String, isId: false} as DMMF.Field, 107 | {fields: []} as unknown as DMMF.Model, 108 | ), 109 | ).toEqual({type: SDL.String, isId: false}); 110 | 111 | expect( 112 | convertScalar( 113 | {type: PSL.Json, isId: false} as DMMF.Field, 114 | {fields: []} as unknown as DMMF.Model, 115 | ), 116 | ).toEqual({type: SDL.String, isId: false}); 117 | 118 | expect( 119 | convertScalar( 120 | {type: PSL.String, isId: true} as DMMF.Field, 121 | {fields: []} as unknown as DMMF.Model, 122 | ), 123 | ).toEqual({type: SDL.ID, isId: true}); 124 | 125 | expect( 126 | convertScalar( 127 | {type: PSL.Json, isId: true} as DMMF.Field, 128 | {fields: []} as unknown as DMMF.Model, 129 | ), 130 | ).toEqual({type: SDL.ID, isId: true}); 131 | }); 132 | 133 | it('converts solo @unique to ID if no @id exists', () => { 134 | expect( 135 | convertScalar( 136 | {type: PSL.String, isUnique: true, isId: false} as DMMF.Field, 137 | { 138 | name: 'User', 139 | fields: [ 140 | {type: PSL.String, isUnique: true, isId: false}, 141 | {type: PSL.Int, isUnique: false, isId: false}, 142 | ], 143 | } as DMMF.Model, 144 | ), 145 | ).toEqual({type: SDL.ID, isUnique: true, isId: false}); 146 | }); 147 | 148 | it('does nothing for multiple @unique even if no @id exists', () => { 149 | expect( 150 | convertScalar( 151 | {type: PSL.String, isUnique: true, isId: false} as DMMF.Field, 152 | { 153 | name: 'User', 154 | fields: [ 155 | {type: PSL.String, isUnique: true, isId: false} as DMMF.Field, 156 | {type: PSL.Int, isUnique: true, isId: false} as DMMF.Field, 157 | ], 158 | } as DMMF.Model, 159 | ), 160 | ).toEqual({type: PSL.String, isUnique: true, isId: false}); 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /src/converters/convertScalar.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | import type {Rule} from './types'; 4 | import existingRules from './rules/scalar'; 5 | 6 | import type {Config} from '../generateGraphqlSchema'; 7 | 8 | const convertScalar = ( 9 | initialField: DMMF.Field, 10 | model: DMMF.Model, 11 | config?: Config, 12 | ): DMMF.Field => { 13 | const rules = config?.customRules?.beforeAddingTypeModifiers 14 | ? config.customRules.beforeAddingTypeModifiers 15 | : existingRules; 16 | 17 | const newField = rules.reduce( 18 | (field, {matcher, transformer}: Rule): DMMF.Field => { 19 | if (matcher(field, model)) { 20 | return transformer(field); 21 | } 22 | 23 | return field; 24 | }, 25 | initialField, 26 | ); 27 | 28 | return newField; 29 | }; 30 | 31 | export default convertScalar; 32 | -------------------------------------------------------------------------------- /src/converters/convertType.test.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | import convertScalar from './convertScalar'; 3 | import convertType from './convertType'; 4 | 5 | import type {Config} from '../generateGraphqlSchema'; 6 | 7 | jest.mock('./convertScalar'); 8 | 9 | describe('typeConverter', () => { 10 | it('calls other converters by field kind', () => { 11 | const field = {kind: 'scalar'} as DMMF.Field; 12 | const model = {} as DMMF.Model; 13 | const config = {} as Config; 14 | 15 | convertType(field, model, config); 16 | 17 | expect(convertScalar).toBeCalledWith(field, model, config); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/converters/convertType.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | import type {Config} from '../generateGraphqlSchema'; 4 | 5 | import convertScalar from './convertScalar'; 6 | 7 | const convertType = ( 8 | field: DMMF.Field, 9 | model: DMMF.Model, 10 | config?: Config, 11 | ): DMMF.Field => { 12 | const {kind} = field; 13 | 14 | if (kind === 'scalar' || config?.customRules) { 15 | return convertScalar(field, model, config); 16 | } 17 | 18 | // TODO 19 | return field; 20 | }; 21 | 22 | export default convertType; 23 | -------------------------------------------------------------------------------- /src/converters/rules/modifier.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | import {Rule} from '../types'; 3 | 4 | const addBrasket = (field: DMMF.Field): DMMF.Field => { 5 | const {type} = field; 6 | 7 | return {...field, type: `[${type}]`}; 8 | }; 9 | 10 | const addExclamation = (field: DMMF.Field): DMMF.Field => { 11 | const {type} = field; 12 | 13 | return {...field, type: `${type}!`}; 14 | }; 15 | 16 | const rules: Rule[] = [ 17 | { 18 | matcher: (field) => { 19 | const {isList, isRequired} = field; 20 | 21 | if (isList) { 22 | console.assert(isRequired); 23 | } 24 | 25 | return isList; 26 | }, 27 | transformer: (field) => { 28 | return [addExclamation, addBrasket, addExclamation].reduce( 29 | (acc, cur) => cur(acc), 30 | field, 31 | ); 32 | }, 33 | }, 34 | { 35 | matcher: (field) => { 36 | const {isList, isRequired} = field; 37 | 38 | return !isList && isRequired; 39 | }, 40 | transformer: (field) => addExclamation(field), 41 | }, 42 | ]; 43 | 44 | export default rules; 45 | -------------------------------------------------------------------------------- /src/converters/rules/scalar.ts: -------------------------------------------------------------------------------- 1 | import {PSL, Rule, SDL, Scalar} from '../types'; 2 | import extractId from '../../extractors/extractId'; 3 | import extractUniques from '../../extractors/extractUniques'; 4 | 5 | const rules: Rule[] = [ 6 | { 7 | matcher: (field) => { 8 | const {type} = field; 9 | 10 | if (type === PSL.Json) { 11 | return true; 12 | } 13 | 14 | return false; 15 | }, 16 | transformer: (field) => ({...field, type: SDL.String}), 17 | }, 18 | { 19 | matcher: (field) => { 20 | const {type} = field; 21 | 22 | if (type === PSL.BigInt) { 23 | return true; 24 | } 25 | 26 | return false; 27 | }, 28 | transformer: (field) => ({...field, type: SDL.Int}), 29 | }, 30 | { 31 | matcher: (field) => { 32 | const {type} = field; 33 | 34 | if (type === PSL.Decimal) { 35 | return true; 36 | } 37 | 38 | return false; 39 | }, 40 | transformer: (field) => ({...field, type: SDL.Float}), 41 | }, 42 | { 43 | matcher: (field) => { 44 | const {type} = field; 45 | 46 | if (type === PSL.Bytes) { 47 | return true; 48 | } 49 | 50 | return false; 51 | }, 52 | transformer: (field) => ({...field, type: Scalar.ByteArray}), 53 | }, 54 | { 55 | matcher: (field) => { 56 | const {isId} = field; 57 | 58 | return isId; 59 | }, 60 | transformer: (field) => ({...field, type: SDL.ID}), 61 | }, 62 | { 63 | matcher: (field, model) => { 64 | const {isUnique} = field; 65 | 66 | const idField = extractId(model); 67 | const uniqueFields = extractUniques(model); 68 | 69 | return !idField && uniqueFields.length === 1 && isUnique; 70 | }, 71 | transformer: (field) => ({...field, type: SDL.ID}), 72 | }, 73 | ]; 74 | 75 | export default rules; 76 | -------------------------------------------------------------------------------- /src/converters/types.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | enum SDL { 4 | ID = 'ID', 5 | Int = 'Int', 6 | Float = 'Float', 7 | String = 'String', 8 | Boolean = 'Boolean', 9 | } 10 | 11 | enum PSL { 12 | Int = 'Int', 13 | Float = 'Float', 14 | String = 'String', 15 | BigInt = 'BigInt', 16 | Boolean = 'Boolean', 17 | Decimal = 'Decimal', 18 | DateTime = 'DateTime', 19 | Json = 'Json', 20 | Bytes = 'Bytes', 21 | Unsupported = 'Unsupported', 22 | } 23 | 24 | enum Scalar { 25 | ByteArray = 'ByteArray', 26 | DateTime = 'DateTime', 27 | } 28 | 29 | enum ReservedName { 30 | Query = 'Query', 31 | Mutation = 'Mutation', 32 | } 33 | 34 | enum Definition { 35 | type = 'type', 36 | enum = 'enum', 37 | input = 'input', 38 | } 39 | 40 | type Rule = { 41 | matcher: (field: DMMF.Field, model: DMMF.Model) => boolean; 42 | transformer: (field: DMMF.Field) => DMMF.Field; 43 | }; 44 | 45 | type CustomRules = { 46 | beforeAddingTypeModifiers?: Rule[]; 47 | afterAddingTypeModifiers?: Rule[]; 48 | }; 49 | 50 | export type {Rule, CustomRules}; 51 | 52 | export {SDL, PSL, Scalar, ReservedName, Definition}; 53 | -------------------------------------------------------------------------------- /src/diff/diff.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | import {diff} from 'jest-diff'; 4 | import generateGraphqlSchema from '../generateGraphqlSchema'; 5 | 6 | // eslint-disable-next-line jest/no-disabled-tests 7 | it.skip('diff', async () => { 8 | const sourcePath = './prisma/schema.prisma'; 9 | const cachePath = './src/diff/previous.cache'; 10 | 11 | const buffer = fs.readFileSync(sourcePath); 12 | 13 | const current = await generateGraphqlSchema(buffer.toString(), { 14 | createCRUD: 'true', 15 | }); 16 | 17 | const previous = fs.readFileSync(cachePath).toString(); 18 | 19 | // eslint-disable-next-line no-console 20 | console.log(diff(previous, current)); 21 | 22 | fs.writeFileSync(cachePath, current); 23 | }); 24 | -------------------------------------------------------------------------------- /src/diff/previous.cache: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | This file was generated by graphql-schema-generator which is 4 | maintained by prisma-korea. 5 | 6 | Do not make changes to this file directly. 7 | Read more about in https://github.com/prisma-korea/graphql-schema-generator. 8 | """ 9 | type Query { 10 | user(id: ID!): User 11 | users: [User!]! 12 | notification(id: ID!): Notification 13 | notifications: [Notification!]! 14 | profile(id: ID!): Profile 15 | profiles: [Profile!]! 16 | friend(id: ID!): Friend 17 | friends: [Friend!]! 18 | blockeduser(id: ID!): BlockedUser 19 | blockedusers: [BlockedUser!]! 20 | report(id: ID!): Report 21 | reports: [Report!]! 22 | membership(id: ID!): Membership 23 | memberships: [Membership!]! 24 | channel(id: ID!): Channel 25 | channels: [Channel!]! 26 | message(id: ID!): Message 27 | messages: [Message!]! 28 | reply(id: ID!): Reply 29 | replys: [Reply!]! 30 | reaction(id: ID!): Reaction 31 | reactions: [Reaction!]! 32 | } 33 | 34 | input UserCreateInput { 35 | email: String 36 | password: String 37 | name: String 38 | nickname: String 39 | thumbURL: String 40 | photoURL: String 41 | birthday: DateTime 42 | gender: Gender 43 | phone: String 44 | statusMessage: String 45 | verified: Boolean 46 | lastSignedIn: DateTime 47 | isOnline: Boolean 48 | createdAt: DateTime 49 | updatedAt: DateTime 50 | deletedAt: DateTime 51 | profile: Profile 52 | notifications: [Notification!]! 53 | friends: [Friend!]! 54 | memberships: [Membership!]! 55 | blockedUsers: [BlockedUser!]! 56 | reports: [Report!]! 57 | messages: [Message!]! 58 | replys: [Reply!]! 59 | reactions: [Reaction!]! 60 | Friend: [Friend!]! 61 | Report: [Report!]! 62 | BlockedUser: [BlockedUser!]! 63 | } 64 | 65 | input UserUpdateInput { 66 | email: String 67 | password: String 68 | name: String 69 | nickname: String 70 | thumbURL: String 71 | photoURL: String 72 | birthday: DateTime 73 | gender: Gender 74 | phone: String 75 | statusMessage: String 76 | verified: Boolean 77 | lastSignedIn: DateTime 78 | isOnline: Boolean 79 | createdAt: DateTime 80 | updatedAt: DateTime 81 | deletedAt: DateTime 82 | profile: Profile 83 | notifications: [Notification!] 84 | friends: [Friend!] 85 | memberships: [Membership!] 86 | blockedUsers: [BlockedUser!] 87 | reports: [Report!] 88 | messages: [Message!] 89 | replys: [Reply!] 90 | reactions: [Reaction!] 91 | Friend: [Friend!] 92 | Report: [Report!] 93 | BlockedUser: [BlockedUser!] 94 | } 95 | 96 | input NotificationCreateInput { 97 | token: String! 98 | device: String 99 | os: String 100 | user: User! 101 | createdAt: DateTime 102 | } 103 | 104 | input NotificationUpdateInput { 105 | token: String 106 | device: String 107 | os: String 108 | user: User 109 | createdAt: DateTime 110 | } 111 | 112 | input ProfileCreateInput { 113 | socialId: String 114 | authType: AuthType 115 | User: User! 116 | } 117 | 118 | input ProfileUpdateInput { 119 | socialId: String 120 | authType: AuthType 121 | User: User 122 | } 123 | 124 | input FriendCreateInput { 125 | createdAt: DateTime 126 | updatedAt: DateTime 127 | deletedAt: DateTime 128 | user: User! 129 | friend: User! 130 | } 131 | 132 | input FriendUpdateInput { 133 | createdAt: DateTime 134 | updatedAt: DateTime 135 | deletedAt: DateTime 136 | user: User 137 | friend: User 138 | } 139 | 140 | input BlockedUserCreateInput { 141 | createdAt: DateTime 142 | updatedAt: DateTime 143 | deletedAt: DateTime 144 | user: User! 145 | blockedUser: User! 146 | } 147 | 148 | input BlockedUserUpdateInput { 149 | createdAt: DateTime 150 | updatedAt: DateTime 151 | deletedAt: DateTime 152 | user: User 153 | blockedUser: User 154 | } 155 | 156 | input ReportCreateInput { 157 | report: String! 158 | createdAt: DateTime 159 | updatedAt: DateTime 160 | deletedAt: DateTime 161 | user: User! 162 | reportedUser: User! 163 | } 164 | 165 | input ReportUpdateInput { 166 | report: String 167 | createdAt: DateTime 168 | updatedAt: DateTime 169 | deletedAt: DateTime 170 | user: User 171 | reportedUser: User 172 | } 173 | 174 | input MembershipCreateInput { 175 | alertMode: AlertMode 176 | membershipType: MembershipType! 177 | isVisible: Boolean! 178 | createdAt: DateTime 179 | updatedAt: DateTime 180 | user: User! 181 | channel: Channel! 182 | } 183 | 184 | input MembershipUpdateInput { 185 | alertMode: AlertMode 186 | membershipType: MembershipType 187 | isVisible: Boolean 188 | createdAt: DateTime 189 | updatedAt: DateTime 190 | user: User 191 | channel: Channel 192 | } 193 | 194 | input ChannelCreateInput { 195 | channelType: ChannelType! 196 | name: String 197 | messages: [Message!]! 198 | membership: [Membership!]! 199 | lastMessageId: String 200 | createdAt: DateTime 201 | updatedAt: DateTime 202 | deletedAt: DateTime 203 | } 204 | 205 | input ChannelUpdateInput { 206 | channelType: ChannelType 207 | name: String 208 | messages: [Message!] 209 | membership: [Membership!] 210 | lastMessageId: String 211 | createdAt: DateTime 212 | updatedAt: DateTime 213 | deletedAt: DateTime 214 | } 215 | 216 | input MessageCreateInput { 217 | messageType: MessageType! 218 | text: String 219 | imageUrls: [String!]! 220 | fileUrls: [String!]! 221 | reactions: [Reaction!]! 222 | replies: [Reply!]! 223 | createdAt: DateTime 224 | updatedAt: DateTime 225 | deletedAt: DateTime 226 | channel: Channel! 227 | sender: User! 228 | } 229 | 230 | input MessageUpdateInput { 231 | messageType: MessageType 232 | text: String 233 | imageUrls: [String!] 234 | fileUrls: [String!] 235 | reactions: [Reaction!] 236 | replies: [Reply!] 237 | createdAt: DateTime 238 | updatedAt: DateTime 239 | deletedAt: DateTime 240 | channel: Channel 241 | sender: User 242 | } 243 | 244 | input ReplyCreateInput { 245 | messageType: MessageType! 246 | text: String 247 | imageUrls: [String!]! 248 | fileUrls: [String!]! 249 | reactions: [Reaction!]! 250 | createdAt: DateTime 251 | updatedAt: DateTime 252 | deletedAt: DateTime 253 | sender: User! 254 | message: Message! 255 | } 256 | 257 | input ReplyUpdateInput { 258 | messageType: MessageType 259 | text: String 260 | imageUrls: [String!] 261 | fileUrls: [String!] 262 | reactions: [Reaction!] 263 | createdAt: DateTime 264 | updatedAt: DateTime 265 | deletedAt: DateTime 266 | sender: User 267 | message: Message 268 | } 269 | 270 | input ReactionCreateInput { 271 | value: String! 272 | user: User! 273 | message: Message! 274 | reply: Reply! 275 | } 276 | 277 | input ReactionUpdateInput { 278 | value: String 279 | user: User 280 | message: Message 281 | reply: Reply 282 | } 283 | 284 | type Mutation { 285 | createUser(user: UserCreateInput!): User 286 | updateUser(user: UserUpdateInput!): User 287 | deleteUser(id: ID!): User 288 | createNotification(notification: NotificationCreateInput!): Notification 289 | updateNotification(notification: NotificationUpdateInput!): Notification 290 | deleteNotification(id: ID!): Notification 291 | createProfile(profile: ProfileCreateInput!): Profile 292 | updateProfile(profile: ProfileUpdateInput!): Profile 293 | deleteProfile(id: ID!): Profile 294 | createFriend(friend: FriendCreateInput!): Friend 295 | updateFriend(friend: FriendUpdateInput!): Friend 296 | deleteFriend(id: ID!): Friend 297 | createBlockedUser(blockeduser: BlockedUserCreateInput!): BlockedUser 298 | updateBlockedUser(blockeduser: BlockedUserUpdateInput!): BlockedUser 299 | deleteBlockedUser(id: ID!): BlockedUser 300 | createReport(report: ReportCreateInput!): Report 301 | updateReport(report: ReportUpdateInput!): Report 302 | deleteReport(id: ID!): Report 303 | createMembership(membership: MembershipCreateInput!): Membership 304 | updateMembership(membership: MembershipUpdateInput!): Membership 305 | deleteMembership(id: ID!): Membership 306 | createChannel(channel: ChannelCreateInput!): Channel 307 | updateChannel(channel: ChannelUpdateInput!): Channel 308 | deleteChannel(id: ID!): Channel 309 | createMessage(message: MessageCreateInput!): Message 310 | updateMessage(message: MessageUpdateInput!): Message 311 | deleteMessage(id: ID!): Message 312 | createReply(reply: ReplyCreateInput!): Reply 313 | updateReply(reply: ReplyUpdateInput!): Reply 314 | deleteReply(id: ID!): Reply 315 | createReaction(reaction: ReactionCreateInput!): Reaction 316 | updateReaction(reaction: ReactionUpdateInput!): Reaction 317 | deleteReaction(id: ID!): Reaction 318 | } 319 | 320 | scalar DateTime 321 | 322 | enum Gender { 323 | male 324 | female 325 | } 326 | 327 | enum AuthType { 328 | email 329 | facebook 330 | google 331 | apple 332 | } 333 | 334 | enum MembershipType { 335 | owner 336 | admin 337 | member 338 | } 339 | 340 | enum AlertMode { 341 | sound 342 | vibrate 343 | silent 344 | } 345 | 346 | enum ChannelType { 347 | private 348 | public 349 | self 350 | } 351 | 352 | enum MessageType { 353 | text 354 | photo 355 | file 356 | } 357 | 358 | type User { 359 | id: ID! 360 | email: String 361 | password: String 362 | name: String 363 | nickname: String 364 | thumbURL: String 365 | photoURL: String 366 | birthday: DateTime 367 | gender: Gender 368 | phone: String 369 | statusMessage: String 370 | verified: Boolean 371 | lastSignedIn: DateTime 372 | isOnline: Boolean 373 | createdAt: DateTime 374 | updatedAt: DateTime 375 | deletedAt: DateTime 376 | profile: Profile 377 | notifications: [Notification!]! 378 | friends: [Friend!]! 379 | memberships: [Membership!]! 380 | blockedUsers: [BlockedUser!]! 381 | reports: [Report!]! 382 | messages: [Message!]! 383 | replys: [Reply!]! 384 | reactions: [Reaction!]! 385 | Friend: [Friend!]! 386 | Report: [Report!]! 387 | BlockedUser: [BlockedUser!]! 388 | } 389 | 390 | type Notification { 391 | id: ID! 392 | token: String! 393 | device: String 394 | os: String 395 | user: User! 396 | createdAt: DateTime 397 | } 398 | 399 | type Profile { 400 | id: ID! 401 | socialId: String 402 | authType: AuthType 403 | User: User! 404 | } 405 | 406 | type Friend { 407 | createdAt: DateTime 408 | updatedAt: DateTime 409 | deletedAt: DateTime 410 | user: User! 411 | friend: User! 412 | } 413 | 414 | type BlockedUser { 415 | createdAt: DateTime 416 | updatedAt: DateTime 417 | deletedAt: DateTime 418 | user: User! 419 | blockedUser: User! 420 | } 421 | 422 | type Report { 423 | id: ID! 424 | report: String! 425 | createdAt: DateTime 426 | updatedAt: DateTime 427 | deletedAt: DateTime 428 | user: User! 429 | reportedUser: User! 430 | } 431 | 432 | type Membership { 433 | alertMode: AlertMode 434 | membershipType: MembershipType! 435 | isVisible: Boolean! 436 | createdAt: DateTime 437 | updatedAt: DateTime 438 | user: User! 439 | channel: Channel! 440 | } 441 | 442 | type Channel { 443 | id: ID! 444 | channelType: ChannelType! 445 | name: String 446 | messages: [Message!]! 447 | membership: [Membership!]! 448 | lastMessageId: String 449 | createdAt: DateTime 450 | updatedAt: DateTime 451 | deletedAt: DateTime 452 | } 453 | 454 | type Message { 455 | id: ID! 456 | messageType: MessageType! 457 | text: String 458 | imageUrls: [String!]! 459 | fileUrls: [String!]! 460 | reactions: [Reaction!]! 461 | replies: [Reply!]! 462 | createdAt: DateTime 463 | updatedAt: DateTime 464 | deletedAt: DateTime 465 | channel: Channel! 466 | sender: User! 467 | } 468 | 469 | type Reply { 470 | id: ID! 471 | messageType: MessageType! 472 | text: String 473 | imageUrls: [String!]! 474 | fileUrls: [String!]! 475 | reactions: [Reaction!]! 476 | createdAt: DateTime 477 | updatedAt: DateTime 478 | deletedAt: DateTime 479 | sender: User! 480 | message: Message! 481 | } 482 | 483 | type Reaction { 484 | id: ID! 485 | value: String! 486 | user: User! 487 | message: Message! 488 | reply: Reply! 489 | } -------------------------------------------------------------------------------- /src/extractors/extractId.test.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | import extractId from './extractId'; 4 | 5 | describe('extractId', () => { 6 | it('extracts field which is @id', () => { 7 | const model = { 8 | fields: [ 9 | { 10 | name: 'name2', 11 | isId: false, 12 | }, 13 | { 14 | name: 'name1', 15 | isId: true, 16 | }, 17 | ], 18 | }; 19 | 20 | expect(extractId(model as DMMF.Model)).toEqual({ 21 | name: 'name1', 22 | isId: true, 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/extractors/extractId.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | const extractId = (model: DMMF.Model): DMMF.Field | undefined => { 4 | const {fields} = model; 5 | 6 | return fields.find(({isId}) => isId); 7 | }; 8 | 9 | export default extractId; 10 | -------------------------------------------------------------------------------- /src/extractors/extractScalars.test.ts: -------------------------------------------------------------------------------- 1 | import {PSL, Scalar} from '../converters/types'; 2 | import extractScalars from './extractScalars'; 3 | import {DataModel} from '../parse'; 4 | 5 | describe('extractScalars', () => { 6 | it('extracts DateTime, and ByteArray from DateTime, Bytes', () => { 7 | const dataModel = { 8 | names: ['name1', 'name2'], 9 | models: { 10 | name1: { 11 | fields: [ 12 | { 13 | type: PSL.DateTime, 14 | }, 15 | ], 16 | }, 17 | 18 | name2: { 19 | fields: [ 20 | { 21 | type: PSL.Bytes, 22 | }, 23 | ], 24 | }, 25 | }, 26 | }; 27 | 28 | expect(extractScalars(dataModel as unknown as DataModel)).toEqual([ 29 | Scalar.DateTime, 30 | Scalar.ByteArray, 31 | ]); 32 | }); 33 | 34 | it('igores duplicates', () => { 35 | const dataModel = { 36 | names: ['name1'], 37 | models: { 38 | name1: { 39 | fields: [ 40 | { 41 | type: PSL.DateTime, 42 | }, 43 | { 44 | type: PSL.DateTime, 45 | }, 46 | ], 47 | }, 48 | }, 49 | }; 50 | 51 | expect(extractScalars(dataModel as unknown as DataModel)).toEqual([ 52 | Scalar.DateTime, 53 | ]); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/extractors/extractScalars.ts: -------------------------------------------------------------------------------- 1 | import {DataModel} from '../parse'; 2 | import {Scalar, PSL} from '../converters/types'; 3 | 4 | const extractScalars = (dataModel: DataModel): string[] => { 5 | const {models, names} = dataModel; 6 | 7 | const scalars = new Set(); 8 | 9 | names.forEach((name) => { 10 | const model = models[name]; 11 | 12 | model.fields.forEach((field) => { 13 | const {type} = field; 14 | 15 | if (type === PSL.DateTime) { 16 | scalars.add(Scalar.DateTime); 17 | } 18 | 19 | if (type === PSL.Bytes) { 20 | scalars.add(Scalar.ByteArray); 21 | } 22 | }); 23 | }); 24 | 25 | return Array.from(scalars.values()); 26 | }; 27 | 28 | export default extractScalars; 29 | -------------------------------------------------------------------------------- /src/extractors/extractUniques.test.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | import extractUniques from './extractUniques'; 4 | 5 | describe('extractUniques', () => { 6 | it('extracts all fields that is @unique', () => { 7 | const model = { 8 | fields: [ 9 | {name: '1', isUnique: true} as DMMF.Field, 10 | {name: '2', isUnique: false} as DMMF.Field, 11 | {name: '3', isUnique: true} as DMMF.Field, 12 | ], 13 | } as DMMF.Model; 14 | 15 | expect(extractUniques(model)).toEqual([ 16 | {name: '1', isUnique: true} as DMMF.Field, 17 | {name: '3', isUnique: true} as DMMF.Field, 18 | ]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/extractors/extractUniques.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | const extractUniques = (model: DMMF.Model): DMMF.Field[] => { 4 | return model.fields.filter(({isUnique}) => isUnique); 5 | }; 6 | 7 | export default extractUniques; 8 | -------------------------------------------------------------------------------- /src/formatters/formatDefinition.test.ts: -------------------------------------------------------------------------------- 1 | import {Definition} from '../converters/types'; 2 | import formatDefinition from './formatDefinition'; 3 | 4 | describe('formatDefinition', () => { 5 | it('formats the whole definition', () => { 6 | const type = Definition.enum; 7 | const name = 'Model1'; 8 | const fields = ['name1: type1', 'name2: type2']; 9 | 10 | expect(formatDefinition({type, name, fields})).toBe( 11 | 'enum Model1 {\nname1: type1\nname2: type2\n}\n', 12 | ); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/formatters/formatDefinition.ts: -------------------------------------------------------------------------------- 1 | import {Definition} from '../converters/types'; 2 | 3 | const formatModel = ({ 4 | type, 5 | name, 6 | fields, 7 | }: { 8 | type: Definition; 9 | name: string; 10 | fields: string[]; 11 | }): string => `${type} ${name} {\n${fields.join('\n')}\n}\n`; 12 | 13 | export default formatModel; 14 | -------------------------------------------------------------------------------- /src/formatters/formatField.test.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | import fieldFormatter from './formatField'; 3 | 4 | describe('fieldFormatter', () => { 5 | it('formats the whole field', () => { 6 | expect(fieldFormatter({name: 'name', type: 'type'} as DMMF.Field)).toBe( 7 | '\tname: type', 8 | ); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/formatters/formatField.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | 3 | const formatField = (field: DMMF.Field): string => { 4 | const {name, type} = field; 5 | 6 | return `\t${name}: ${type}`; 7 | }; 8 | 9 | export default formatField; 10 | -------------------------------------------------------------------------------- /src/formatters/formatScalar.test.ts: -------------------------------------------------------------------------------- 1 | import formatScalar from './formatScalar'; 2 | 3 | describe('formatScalar', () => { 4 | it('formats the whole scalar', () => { 5 | expect(formatScalar('User')).toBe('scalar User\n'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/formatters/formatScalar.ts: -------------------------------------------------------------------------------- 1 | const formatScalar = (name: string): string => `scalar ${name}\n`; 2 | 3 | export default formatScalar; 4 | -------------------------------------------------------------------------------- /src/generateGraphqlSchema.test.ts: -------------------------------------------------------------------------------- 1 | import parse from './parse'; 2 | import transpile from './transpile'; 3 | import generateGraphqlSchema, {description} from './generateGraphqlSchema'; 4 | 5 | jest.mock('./transpile'); 6 | 7 | describe('generateGraphqlSchema', () => { 8 | it.each([ 9 | [ 10 | {createCRUD: 'true', someThing: 'else'}, 11 | {createQuery: 'true', createMutation: 'true', someThing: 'else'}, 12 | ], 13 | [{createCRUD: 'not true'}, {createQuery: 'false', createMutation: 'false'}], 14 | ])( 15 | 'calls transpiler with model and converted config', 16 | async (originalConfig, convertedConfig) => { 17 | const prismaSchema = /* Prisma */ ` 18 | model Post { 19 | id Int @id 20 | content1 Bytes 21 | } 22 | `; 23 | 24 | await generateGraphqlSchema(prismaSchema, originalConfig); 25 | 26 | const model = await parse(prismaSchema); 27 | expect(transpile).toBeCalledWith(model, convertedConfig); 28 | }, 29 | ); 30 | 31 | it('adds description', async () => { 32 | const prismaSchema = /* Prisma */ ` 33 | model Post { 34 | id Int @id 35 | content1 Bytes 36 | } 37 | `; 38 | 39 | const result = await generateGraphqlSchema(prismaSchema, {}); 40 | 41 | expect(result).toEqual(expect.stringContaining(description)); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/generateGraphqlSchema.ts: -------------------------------------------------------------------------------- 1 | import parse from './parse'; 2 | import transpile from './transpile'; 3 | 4 | import type {CustomRules} from './converters/types'; 5 | 6 | export const description = ` 7 | """ 8 | This file was generated by graphql-schema-generator which is 9 | maintained by prisma-korea. 10 | 11 | Do not make changes to this file directly. 12 | Read more about in https://github.com/prisma-korea/graphql-schema-generator. 13 | """ 14 | `; 15 | export type Config = { 16 | customRules?: CustomRules; 17 | [key: string]: any; 18 | }; 19 | 20 | const generateGraphqlSchema = async ( 21 | source: string, 22 | config: Config, 23 | ): Promise => { 24 | const model = await parse(source); 25 | 26 | const {createCRUD, ...rest} = config; 27 | 28 | const graphqlSchema = 29 | createCRUD === 'true' 30 | ? transpile(model, { 31 | ...rest, 32 | createQuery: 'true', 33 | createMutation: 'true', 34 | }) 35 | : transpile(model, { 36 | ...rest, 37 | createQuery: 'false', 38 | createMutation: 'false', 39 | }); 40 | 41 | return description + graphqlSchema; 42 | }; 43 | 44 | export default generateGraphqlSchema; 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import {generatorHandler} from '@prisma/generator-helper'; 6 | 7 | import generateGraphqlSchema from './generateGraphqlSchema'; 8 | 9 | export * from './converters/types'; 10 | 11 | generatorHandler({ 12 | onManifest() { 13 | return { 14 | defaultOutput: './generated', 15 | prettyName: 'GraphQL-Schema-Generator', 16 | }; 17 | }, 18 | async onGenerate(options) { 19 | const output = options.generator.output?.value; 20 | const {config} = options.generator; 21 | 22 | if (output) { 23 | if (config?.customRules) { 24 | const module = await import( 25 | path.join(output, '..', config?.customRules) 26 | ); 27 | 28 | config.customRules = module.default.rules; 29 | } 30 | 31 | const result = await generateGraphqlSchema(options.datamodel, config); 32 | 33 | try { 34 | await fs.promises.mkdir(output, { 35 | recursive: true, 36 | }); 37 | 38 | await fs.promises.writeFile( 39 | path.join(output, 'schema.graphql'), 40 | result, 41 | ); 42 | } catch (e) { 43 | console.error( 44 | 'Error: unable to write files for GraphQL-Schema-Generator', 45 | ); 46 | throw e; 47 | } 48 | } else { 49 | throw new Error('No output was specified for GraphQL-Schema-Generator'); 50 | } 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /src/parse.test.ts: -------------------------------------------------------------------------------- 1 | import parse from './parse'; 2 | 3 | const prismaSchema = /* Prisma */ ` 4 | enum Role { 5 | USER 6 | ADMIN 7 | } 8 | 9 | model Post { 10 | authorId Int? 11 | content String? 12 | id Int @default(autoincrement()) @id 13 | published Boolean @default(false) 14 | author User? @relation(fields: [authorId], references: [id]) 15 | } 16 | 17 | model User { 18 | email String @unique 19 | id Int @default(autoincrement()) @id 20 | name String? 21 | posts Post[] 22 | } 23 | `; 24 | 25 | describe('DataModel', () => { 26 | it('returns name of models', async () => { 27 | const dataModel = await parse(prismaSchema); 28 | 29 | expect(dataModel.names).toEqual(['Post', 'User']); 30 | }); 31 | 32 | it('returns detail model data that can be accessed with name', async () => { 33 | const dataModel = await parse(prismaSchema); 34 | 35 | const {Post, User} = dataModel.models; 36 | 37 | expect(Object.keys(Post)).toEqual([ 38 | 'name', 39 | 'dbName', 40 | 'fields', 41 | 'isGenerated', 42 | 'primaryKey', 43 | 'uniqueFields', 44 | 'uniqueIndexes', 45 | ]); 46 | 47 | expect(Object.keys(User)).toEqual([ 48 | 'name', 49 | 'dbName', 50 | 'fields', 51 | 'isGenerated', 52 | 'primaryKey', 53 | 'uniqueFields', 54 | 'uniqueIndexes', 55 | ]); 56 | }); 57 | 58 | it('returns enums', async () => { 59 | const dataModel = await parse(prismaSchema); 60 | 61 | expect(dataModel.enums).toEqual({ 62 | Role: [ 63 | {name: 'USER', dbName: null}, 64 | {name: 'ADMIN', dbName: null}, 65 | ], 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import {DMMF} from '@prisma/generator-helper'; 2 | import {getDMMF} from '@prisma/sdk'; 3 | 4 | export type Models = {[name: string]: DMMF.Model}; 5 | export type Enums = {[name: string]: DMMF.DatamodelEnum['values']}; 6 | 7 | export class DataModel { 8 | dataModel: DMMF.Datamodel; 9 | 10 | constructor(dataModel: DMMF.Datamodel) { 11 | this.dataModel = dataModel; 12 | } 13 | 14 | get names(): string[] { 15 | const {models} = this.dataModel; 16 | 17 | return models.map((model) => model.name); 18 | } 19 | 20 | get models(): Models { 21 | const {models} = this.dataModel; 22 | 23 | return models.reduce((acc, model) => { 24 | const {name} = model; 25 | 26 | return {...acc, [name]: model}; 27 | }, {}); 28 | } 29 | 30 | get enums(): Enums { 31 | const {enums} = this.dataModel; 32 | 33 | return enums.reduce((acc, cur) => { 34 | const {name, values} = cur; 35 | 36 | return {...acc, [name]: values}; 37 | }, {}); 38 | } 39 | } 40 | 41 | const parse = async (prismaSchema: string): Promise => { 42 | const dmmf = await getDMMF({datamodel: prismaSchema}); 43 | 44 | return new DataModel(dmmf.datamodel); 45 | }; 46 | 47 | export default parse; 48 | -------------------------------------------------------------------------------- /src/transpile.test.ts: -------------------------------------------------------------------------------- 1 | import transpile from './transpile'; 2 | import parse from './parse'; 3 | import {CustomRules, SDL} from './converters/types'; 4 | 5 | import {sdl} from './utils'; 6 | 7 | describe('transpile', () => { 8 | it('can be controlled with custom rules', async () => { 9 | const prismaSchema = /* Prisma */ ` 10 | model User { 11 | id Int @id 12 | content Json 13 | } 14 | 15 | model Post { 16 | email String @unique 17 | password String 18 | } 19 | `; 20 | 21 | const graphqlSchema = sdl(` 22 | type User { 23 | id: ID! 24 | content: String! 25 | } 26 | 27 | type Post { 28 | email: String! 29 | } 30 | `); 31 | 32 | const customRules: CustomRules = { 33 | beforeAddingTypeModifiers: [ 34 | { 35 | matcher: (field) => { 36 | const {name} = field; 37 | 38 | if (name === 'password') { 39 | return true; 40 | } 41 | 42 | return false; 43 | }, 44 | transformer: () => { 45 | throw null; 46 | }, 47 | }, 48 | { 49 | matcher: (field) => { 50 | const {type} = field; 51 | 52 | if (type === SDL.ID) { 53 | return true; 54 | } 55 | 56 | return false; 57 | }, 58 | transformer: (field) => ({...field, type: SDL.String}), 59 | }, 60 | ], 61 | afterAddingTypeModifiers: [ 62 | { 63 | matcher: (field) => { 64 | const {name} = field; 65 | 66 | if (name === 'id') { 67 | return true; 68 | } 69 | 70 | return false; 71 | }, 72 | transformer: (field) => { 73 | return {...field, type: `${SDL.ID}!`}; 74 | }, 75 | }, 76 | ], 77 | }; 78 | 79 | const model = await parse(prismaSchema); 80 | expect(transpile(model, {customRules})).toBe(graphqlSchema); 81 | }); 82 | 83 | it('adds queries', async () => { 84 | const prismaSchema = /* Prisma */ ` 85 | model User { 86 | id Int @id 87 | content String 88 | } 89 | 90 | model Post { 91 | email String @unique 92 | content String 93 | } 94 | `; 95 | 96 | const graphqlSchema = sdl(` 97 | type Query { 98 | user(id: ID!): User 99 | users: [User!]! 100 | post(email: ID!): Post 101 | posts: [Post!]! 102 | } 103 | 104 | type User { 105 | id: ID! 106 | content: String! 107 | } 108 | 109 | type Post { 110 | email: ID! 111 | content: String! 112 | } 113 | `); 114 | 115 | const model = await parse(prismaSchema); 116 | 117 | expect(transpile(model, {createQuery: 'true'})).toBe(graphqlSchema); 118 | }); 119 | 120 | it('adds mutations', async () => { 121 | const prismaSchema = /* Prisma */ ` 122 | model User { 123 | id Int @id 124 | content String 125 | name String? 126 | } 127 | 128 | model Post { 129 | email String @unique 130 | content String 131 | } 132 | `; 133 | 134 | const graphqlSchema = sdl(` 135 | input UserCreateInput { 136 | content: String! 137 | name: String 138 | } 139 | 140 | input UserUpdateInput { 141 | content: String 142 | name: String 143 | } 144 | 145 | input PostCreateInput { 146 | content: String! 147 | } 148 | 149 | input PostUpdateInput { 150 | content: String 151 | } 152 | 153 | type Mutation { 154 | createUser(user: UserCreateInput!): User 155 | updateUser(user: UserUpdateInput!): User 156 | deleteUser(id: ID!): User 157 | createPost(post: PostCreateInput!): Post 158 | updatePost(post: PostUpdateInput!): Post 159 | deletePost(email: ID!): Post 160 | } 161 | 162 | type User { 163 | id: ID! 164 | content: String! 165 | name: String 166 | } 167 | 168 | type Post { 169 | email: ID! 170 | content: String! 171 | } 172 | `); 173 | 174 | const model = await parse(prismaSchema); 175 | 176 | expect(transpile(model, {createMutation: 'true'})).toBe(graphqlSchema); 177 | }); 178 | 179 | it('adds scalars', async () => { 180 | const prismaSchema = /* Prisma */ ` 181 | model Post { 182 | id Int @id 183 | date DateTime 184 | content1 Bytes? 185 | content2 Bytes 186 | } 187 | `; 188 | 189 | const graphqlSchema = sdl(` 190 | scalar DateTime 191 | scalar ByteArray 192 | 193 | type Post { 194 | id: ID! 195 | date: DateTime! 196 | content1: ByteArray 197 | content2: ByteArray! 198 | } 199 | `); 200 | 201 | const model = await parse(prismaSchema); 202 | 203 | expect(transpile(model)).toBe(graphqlSchema); 204 | }); 205 | 206 | it('adds enums', async () => { 207 | const prismaSchema = /* Prisma */ ` 208 | enum Role { 209 | USER 210 | ADMIN 211 | } 212 | enum Gender { 213 | male 214 | female 215 | } 216 | `; 217 | 218 | const graphqlSchema = sdl(` 219 | enum Role { 220 | USER 221 | ADMIN 222 | } 223 | enum Gender { 224 | male 225 | female 226 | } 227 | `); 228 | 229 | const model = await parse(prismaSchema); 230 | 231 | expect(transpile(model)).toBe(graphqlSchema); 232 | }); 233 | 234 | it('adds models', async () => { 235 | const prismaSchema = /* Prisma */ ` 236 | model Post { 237 | authorId Int? 238 | content String? 239 | id Int @default(autoincrement()) @id 240 | published Boolean @default(false) 241 | author User? @relation(fields: [authorId], references: [id]) 242 | } 243 | 244 | model User { 245 | email String @unique 246 | id Int @default(autoincrement()) @id 247 | name String? 248 | detail String? 249 | posts Post[] 250 | } 251 | `; 252 | 253 | const graphqlSchema = sdl(` 254 | type Post { 255 | content: String 256 | id: ID! 257 | published: Boolean! 258 | author: User 259 | } 260 | 261 | type User { 262 | email: String! 263 | id: ID! 264 | name: String 265 | detail: String 266 | posts: [Post!]! 267 | } 268 | `); 269 | 270 | const model = await parse(prismaSchema); 271 | 272 | expect(transpile(model)).toBe(graphqlSchema); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /src/transpile.ts: -------------------------------------------------------------------------------- 1 | import type {DMMF} from '@prisma/generator-helper'; 2 | 3 | import {DataModel} from './parse'; 4 | import {Definition, ReservedName, SDL} from './converters/types'; 5 | 6 | import addTypeModifiers from './converters/addTypeModifiers'; 7 | import convertType from './converters/convertType'; 8 | import extractScalars from './extractors/extractScalars'; 9 | import formatDefinition from './formatters/formatDefinition'; 10 | import formatField from './formatters/formatField'; 11 | import formatScalar from './formatters/formatScalar'; 12 | 13 | import {removeExclamation, sdl} from './utils'; 14 | 15 | import type {Config} from './generateGraphqlSchema'; 16 | 17 | const getTypeConvertedFields = ( 18 | model: DMMF.Model, 19 | config?: Config, 20 | ): DMMF.Field[] => { 21 | if (!model) { 22 | return []; 23 | } 24 | 25 | const shouldIgnore = model.fields.reduce( 26 | (acc: {[key: string]: boolean}, cur: DMMF.Field) => { 27 | const {relationFromFields} = cur; 28 | 29 | if (relationFromFields) { 30 | relationFromFields.forEach((field: string) => { 31 | acc[field] = true; 32 | }); 33 | } 34 | 35 | return acc; 36 | }, 37 | {}, 38 | ); 39 | 40 | const typeConvertedFields = model.fields.reduce( 41 | (collected: DMMF.Field[], field: DMMF.Field): DMMF.Field[] => { 42 | const {name} = field; 43 | 44 | if (shouldIgnore[name]) { 45 | return collected; 46 | } 47 | 48 | const applyCustomRulesBeforeTypeModifiersAddition = ( 49 | f: DMMF.Field, 50 | m: DMMF.Model, 51 | ): DMMF.Field => { 52 | return convertType(f, m, config); 53 | }; 54 | 55 | const applyCustomRulesAfterTypeModifiersAddition = ( 56 | f: DMMF.Field, 57 | m: DMMF.Model, 58 | ): DMMF.Field => { 59 | return addTypeModifiers(f, m, config); 60 | }; 61 | 62 | const transformers = [ 63 | convertType, 64 | config?.customRules?.beforeAddingTypeModifiers && 65 | applyCustomRulesBeforeTypeModifiersAddition, 66 | addTypeModifiers, 67 | config?.customRules?.afterAddingTypeModifiers && 68 | applyCustomRulesAfterTypeModifiersAddition, 69 | ].filter(Boolean); 70 | 71 | try { 72 | const typeConvertedField = transformers.reduce((acc, transformer) => { 73 | return transformer!(acc, model); 74 | }, field); 75 | 76 | return [...collected, typeConvertedField]; 77 | } catch { 78 | return collected; 79 | } 80 | }, 81 | [], 82 | ); 83 | 84 | return typeConvertedFields; 85 | }; 86 | 87 | const transpile = (dataModel: DataModel, config?: Config): string => { 88 | const {models, enums, names} = dataModel; 89 | 90 | const queryFields = dataModel.names.reduce((acc: string[], name) => { 91 | const modelFields = getTypeConvertedFields(models[name], config); 92 | 93 | const {name: idName} = modelFields.find(({type}) => { 94 | if (typeof type !== 'string') { 95 | return false; 96 | } 97 | 98 | return type.match(SDL.ID); 99 | }) ?? {name: 'id'}; 100 | 101 | return [ 102 | ...acc, 103 | `${name.toLowerCase()}(${idName}: ID!): ${name}`, 104 | `${name.toLowerCase()}s: [${name}!]!`, 105 | ]; 106 | }, []); 107 | 108 | const queriesOfSchema = formatDefinition({ 109 | type: Definition.type, 110 | name: ReservedName.Query, 111 | fields: queryFields, 112 | }); 113 | 114 | const mutationFields = dataModel.names.reduce((acc: string[], name) => { 115 | const modelFields = getTypeConvertedFields(models[name], config); 116 | 117 | const {name: idName} = modelFields.find(({type}) => { 118 | if (typeof type !== 'string') { 119 | return false; 120 | } 121 | 122 | return type.match(SDL.ID); 123 | }) ?? {name: 'id'}; 124 | 125 | return [ 126 | ...acc, 127 | `create${name}(${name.toLowerCase()}: ${name}CreateInput!): ${name}`, 128 | `update${name}(${name.toLowerCase()}: ${name}UpdateInput!): ${name}`, 129 | `delete${name}(${idName}: ID!): ${name}`, 130 | ]; 131 | }, []); 132 | 133 | const mutationInputs = dataModel.names.reduce( 134 | (inputs: string[], modelName) => { 135 | const modelFields = getTypeConvertedFields(models[modelName], config); 136 | 137 | const fieldsWithoutID = modelFields.reduce( 138 | (fields: DMMF.Field[], cur) => { 139 | const {type} = cur; 140 | 141 | if (typeof type === 'string' && type.match(SDL.ID)) { 142 | return fields; 143 | } 144 | 145 | return [...fields, cur]; 146 | }, 147 | [], 148 | ); 149 | 150 | const createInputFields = fieldsWithoutID.map( 151 | ({name, type}) => `${name}: ${type}`, 152 | ); 153 | 154 | const updateInputFields = fieldsWithoutID.map( 155 | ({name, type}) => `${name}: ${removeExclamation(type as string)}`, 156 | ); 157 | 158 | return [ 159 | ...inputs, 160 | formatDefinition({ 161 | type: Definition.input, 162 | name: `${modelName}CreateInput`, 163 | fields: createInputFields, 164 | }) + 165 | formatDefinition({ 166 | type: Definition.input, 167 | name: `${modelName}UpdateInput`, 168 | fields: updateInputFields, 169 | }), 170 | ]; 171 | }, 172 | [], 173 | ); 174 | 175 | const mutation = formatDefinition({ 176 | type: Definition.type, 177 | name: ReservedName.Mutation, 178 | fields: mutationFields, 179 | }); 180 | 181 | const mutationsOfSchema = mutationInputs + mutation; 182 | 183 | const scalars = extractScalars(dataModel); 184 | 185 | const scalarsOfSchema = scalars 186 | .map((scalar) => formatScalar(scalar)) 187 | .join(''); 188 | 189 | const enumsOfSchema = Object.entries(enums) 190 | .map(([name, anEnum]) => { 191 | const fields = anEnum.map(({name: field}) => `\t${field}`); 192 | 193 | return formatDefinition({ 194 | type: Definition.enum, 195 | name, 196 | fields, 197 | }); 198 | }) 199 | .join(''); 200 | 201 | const modelsOfSchema = names 202 | .map((name) => { 203 | const fields = getTypeConvertedFields(models[name], config).map((field) => 204 | formatField(field), 205 | ); 206 | 207 | return formatDefinition({ 208 | type: Definition.type, 209 | name, 210 | fields, 211 | }); 212 | }) 213 | .join(''); 214 | 215 | const schema = 216 | (config?.createQuery === 'true' ? queriesOfSchema : '') + 217 | (config?.createMutation === 'true' ? mutationsOfSchema : '') + 218 | scalarsOfSchema + 219 | enumsOfSchema + 220 | modelsOfSchema; 221 | 222 | return sdl(schema); 223 | }; 224 | 225 | export default transpile; 226 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import {printSchema, buildSchema} from 'graphql'; 2 | 3 | export const sdl = (s: string): string => printSchema(buildSchema(s)); 4 | 5 | export const removeExclamation = (s: string): string => { 6 | if (s.match(/!$/)) { 7 | return s.slice(0, -1); 8 | } 9 | 10 | return s; 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist-cjs", 5 | "module": "commonjs", 6 | "rootDir": "src", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationMap": false 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist-esm", 5 | "module": "ES2015", 6 | "rootDir": "src", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationMap": true 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "baseUrl": "src", 6 | "outDir": "lib", 7 | "incremental": true, 8 | "lib": ["esnext"], 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "declaration": true, 12 | 13 | "strict": true, 14 | "noImplicitAny": false, 15 | "noUncheckedIndexedAccess": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "useUnknownInCatchVariables": false, 19 | "skipDefaultLibCheck": true, 20 | 21 | "moduleResolution": "Node", 22 | "importHelpers": true, 23 | "tsBuildInfoFile": "node_modules/.cache/.tsbuildinfo" 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "src/**/*.test**", 28 | "lib/**/*" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------