├── .github └── workflows │ └── release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── dprint.json ├── drizzle.test-config.ts ├── package.json ├── pnpm-lock.yaml ├── scripts └── build.ts ├── src ├── index.ts ├── types.ts └── util │ ├── builders │ ├── common.ts │ ├── index.ts │ ├── mysql.ts │ ├── pg.ts │ ├── sqlite.ts │ └── types.ts │ ├── case-ops │ └── index.ts │ ├── data-mappers │ └── index.ts │ └── type-converter │ ├── index.ts │ └── types.ts ├── tests ├── mysql-custom.test.ts ├── mysql.test.ts ├── pg-custom.test.ts ├── pg.test.ts ├── schema │ ├── mysql.ts │ ├── pg.ts │ └── sqlite.ts ├── sqlite-custom.test.ts ├── sqlite.test.ts ├── tsconfig.json └── util │ └── query │ └── index.ts ├── tsconfig.build.json ├── tsconfig.dts.json ├── tsconfig.json └── vitest.config.ts /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | release: 7 | permissions: write-all 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | package: 12 | - drizzle-graphql 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 18 20 | registry-url: 'https://registry.npmjs.org' 21 | 22 | - uses: pnpm/action-setup@v3 23 | name: Install pnpm 24 | id: pnpm-install 25 | with: 26 | version: '9' 27 | run_install: false 28 | 29 | - name: Get pnpm store directory 30 | id: pnpm-cache 31 | shell: bash 32 | run: | 33 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT 34 | 35 | - uses: actions/cache@v4 36 | name: Setup pnpm cache 37 | with: 38 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 39 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pnpm-store- 42 | 43 | - name: Install dependencies 44 | run: pnpm install --frozen-lockfile 45 | 46 | - name: Lint 47 | run: pnpm lint 48 | 49 | - name: Build 50 | run: | 51 | pnpm build 52 | 53 | - name: Pack 54 | shell: bash 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 57 | run: | 58 | npm run pack 59 | 60 | - name: Run @arethetypeswrong/cli 61 | run: | 62 | pnpm attw package.tgz 63 | 64 | - name: Publish 65 | shell: bash 66 | env: 67 | NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 68 | run: | 69 | version="$(jq -r .version package.json)" 70 | 71 | echo "Publishing ${{ matrix.package }}@$version" 72 | npm run publish --access public 73 | 74 | echo "npm: \`+ ${{ matrix.package }}@$version\`" >> $GITHUB_STEP_SUMMARY 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.sqlite 4 | drizzle.config.ts 5 | *.tgz 6 | temp.ts 7 | dist-dts 8 | dist.new 9 | server 10 | runMysql.ts 11 | runMysql-old.ts 12 | runPg.ts 13 | tests/.temp 14 | tests/Migrations 15 | .DS_Store 16 | drizzle.test-heavy.ts 17 | changes.md 18 | tests/isolated.test.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drizzle-GraphQL 2 | 3 | Automatically create GraphQL schema or customizable schema config fields from Drizzle ORM schema 4 | 5 | ## Usage 6 | 7 | - Pass your drizzle database instance and schema into builder to generate `{ schema, entities }` object 8 | - Use `schema` if pre-built schema already satisfies all your neeeds. It's compatible witn any server that consumes `GraphQLSchema` class instance 9 | 10 | Example: hosting schema using [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) 11 | 12 | ```Typescript 13 | import { createServer } from 'node:http' 14 | import { createYoga } from 'graphql-yoga' 15 | import { buildSchema } from 'drizzle-graphql' 16 | 17 | // db - your drizzle instance 18 | import { db } from './database' 19 | 20 | const { schema } = buildSchema(db) 21 | 22 | const yoga = createYoga({ schema }) 23 | 24 | server.listen(4000, () => { 25 | console.info('Server is running on http://localhost:4000/graphql') 26 | }) 27 | ``` 28 | 29 | - If you want to customize your schema, you can use `entities` object to build your own new schema 30 | 31 | ```Typescript 32 | import { createServer } from 'node:http' 33 | import { GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema } from 'graphql' 34 | import { createYoga } from 'graphql-yoga' 35 | import { buildSchema } from 'drizzle-graphql' 36 | 37 | // Schema contains 'Users' and 'Customers' tables 38 | import { db } from './database' 39 | 40 | const { entities } = buildSchema(db) 41 | 42 | // You can customize which parts of queries or mutations you want 43 | const schema = new GraphQLSchema({ 44 | query: new GraphQLObjectType({ 45 | name: 'Query', 46 | fields: { 47 | // Select only wanted queries out of all generated 48 | users: entities.queries.users, 49 | customer: entities.queries.customersSingle, 50 | 51 | // Create a custom one 52 | customUsers: { 53 | // You can reuse and customize types from original schema 54 | type: new GraphQLList(new GraphQLNonNull(entities.types.UsersItem)), 55 | args: { 56 | // You can reuse inputs as well 57 | where: { 58 | type: entities.inputs.UsersFilters 59 | } 60 | }, 61 | resolve: async (source, args, context, info) => { 62 | // Your custom logic goes here... 63 | const result = await db.select(schema.Users).where()... 64 | 65 | return result 66 | } 67 | } 68 | } 69 | }), 70 | // Same rules apply to mutations 71 | mutation: new GraphQLObjectType({ 72 | name: 'Mutation', 73 | fields: entities.mutations 74 | }), 75 | // In case you need types inside your schema 76 | types: [...Object.values(entities.types), ...Object.values(entities.inputs)] 77 | }) 78 | 79 | const yoga = createYoga({ 80 | schema 81 | }) 82 | 83 | server.listen(4000, () => { 84 | console.info('Server is running on http://localhost:4000/graphql') 85 | }) 86 | ``` 87 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": { 3 | "useTabs": true, 4 | "quoteStyle": "preferSingle", 5 | "quoteProps": "asNeeded", 6 | "arrowFunction.useParentheses": "force", 7 | "jsx.quoteStyle": "preferSingle" 8 | }, 9 | "json": { 10 | "useTabs": true 11 | }, 12 | "markdown": {}, 13 | "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json}"], 14 | "excludes": [ 15 | "**/node_modules", 16 | "dist", 17 | "dist-dts", 18 | "dist.new", 19 | "**/drizzle/**/meta", 20 | "**/drizzle2/**/meta", 21 | "**/*snapshot.json", 22 | "**/_journal.json", 23 | "**/tsup.config*.mjs", 24 | "**/.sst" 25 | ], 26 | "plugins": [ 27 | "https://plugins.dprint.dev/typescript-0.83.0.wasm", 28 | "https://plugins.dprint.dev/json-0.19.2.wasm", 29 | "https://plugins.dprint.dev/markdown-0.15.2.wasm" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /drizzle.test-config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit'; 2 | const dbType = process.env['DB_TYPE']; 3 | 4 | let config; 5 | switch (dbType) { 6 | case 'pg': 7 | config = { 8 | dialect: 'postgresql', 9 | schema: './tests/schema/pg.ts', 10 | out: './tests/migrations/pg/', 11 | } satisfies Config; 12 | break; 13 | case 'mysql': 14 | config = { 15 | dialect: 'mysql', 16 | schema: './tests/schema/mysql.ts', 17 | out: './tests/migrations/mysql/', 18 | } satisfies Config; 19 | break; 20 | case 'sqlite': 21 | config = { 22 | dialect: 'sqlite', 23 | schema: './tests/schema/sqlite.ts', 24 | out: './tests/migrations/sqlite/', 25 | } satisfies Config; 26 | break; 27 | } 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drizzle-graphql", 3 | "type": "module", 4 | "author": "Drizzle Team", 5 | "version": "0.8.5", 6 | "description": "Automatically generate GraphQL schema or customizable schema config fields from Drizzle ORM schema", 7 | "scripts": { 8 | "build": "pnpm tsx scripts/build.ts", 9 | "b": "pnpm build", 10 | "pack": "(cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz", 11 | "publish": "npm publish package.tgz", 12 | "test": "vitest run", 13 | "server-test:pg": "DB_TYPE=pg tsx watch server/server.ts", 14 | "server-test:mysql": "DB_TYPE=mysql tsx watch server/server.ts", 15 | "server-test:sqlite": "DB_TYPE=sqlite tsx watch server/server.ts", 16 | "server-generate-migrations": "DB_TYPE=pg drizzle-kit generate && DB_TYPE=mysql drizzle-kit generate && DB_TYPE=sqlite drizzle-kit generate", 17 | "test-generate-migrations": "DB_TYPE=pg drizzle-kit generate --config=./drizzle.test-config.ts && DB_TYPE=mysql drizzle-kit generate --config=./drizzle.test-config.ts && DB_TYPE=sqlite drizzle-kit generate --config=./drizzle.test-config.ts", 18 | "lint": "dprint check --list-different" 19 | }, 20 | "exports": { 21 | ".": { 22 | "import": { 23 | "types": "./index.d.ts", 24 | "default": "./index.js" 25 | }, 26 | "require": { 27 | "types": "./index.d.cjs", 28 | "default": "./index.cjs" 29 | }, 30 | "types": "./index.d.ts", 31 | "default": "./index.js" 32 | } 33 | }, 34 | "license": "Apache-2.0", 35 | "devDependencies": { 36 | "@arethetypeswrong/cli": "^0.15.2", 37 | "@babel/parser": "^7.24.1", 38 | "@libsql/client": "^0.5.6", 39 | "@originjs/vite-plugin-commonjs": "^1.0.3", 40 | "@types/dockerode": "^3.3.26", 41 | "@types/pg": "^8.11.6", 42 | "@types/uuid": "^9.0.8", 43 | "axios": "^1.6.8", 44 | "cpy": "^11.0.1", 45 | "dockerode": "^4.0.2", 46 | "dprint": "^0.45.1", 47 | "drizzle-kit": "^0.24.0", 48 | "drizzle-orm": "0.33.0", 49 | "get-port": "^7.0.0", 50 | "glob": "^10.3.10", 51 | "graphql": "^16.3.0", 52 | "graphql-yoga": "^5.1.1", 53 | "mysql2": "^3.9.2", 54 | "node-pg": "^1.0.1", 55 | "pg": "^8.12.0", 56 | "postgres": "^3.4.3", 57 | "recast": "^0.23.6", 58 | "resolve-tspaths": "^0.8.18", 59 | "rimraf": "^5.0.5", 60 | "tsup": "^8.0.2", 61 | "tsx": "^4.7.1", 62 | "typescript": "^5.4.2", 63 | "uuid": "^9.0.1", 64 | "vite-tsconfig-paths": "^4.3.2", 65 | "vitest": "^1.4.0", 66 | "zod": "^3.22.4", 67 | "zx": "^7.2.3" 68 | }, 69 | "keywords": [ 70 | "drizzle", 71 | "graphql", 72 | "orm", 73 | "pg", 74 | "mysql", 75 | "postgresql", 76 | "postgres", 77 | "sqlite", 78 | "database", 79 | "sql", 80 | "typescript", 81 | "ts" 82 | ], 83 | "main": "./index.cjs", 84 | "module": "./index.js", 85 | "types": "./index.d.ts", 86 | "sideEffects": false, 87 | "publishConfig": { 88 | "provenance": true 89 | }, 90 | "repository": { 91 | "type": "git", 92 | "url": "git+https://github.com/drizzle-team/drizzle-graphql.git" 93 | }, 94 | "homepage": "https://orm.drizzle.team/docs/graphql", 95 | "dependencies": { 96 | "graphql-parse-resolve-info": "^4.13.0" 97 | }, 98 | "peerDependencies": { 99 | "drizzle-orm": ">=0.30.9", 100 | "graphql": ">=16.3.0" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import 'zx/globals'; 2 | 3 | import { build } from 'tsup'; 4 | 5 | fs.removeSync('dist'); 6 | 7 | await build({ 8 | entry: ['src/index.ts'], 9 | splitting: false, 10 | sourcemap: true, 11 | dts: true, 12 | format: ['cjs', 'esm'], 13 | outExtension(ctx) { 14 | if (ctx.format === 'cjs') { 15 | return { 16 | dts: '.d.cts', 17 | js: '.cjs', 18 | }; 19 | } 20 | return { 21 | dts: '.d.ts', 22 | js: '.js', 23 | }; 24 | }, 25 | }); 26 | 27 | fs.copyFileSync('package.json', 'dist/package.json'); 28 | fs.copyFileSync('README.md', 'dist/README.md'); 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { is } from 'drizzle-orm'; 2 | import { MySqlDatabase } from 'drizzle-orm/mysql-core'; 3 | import { PgDatabase } from 'drizzle-orm/pg-core'; 4 | import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'; 5 | import { 6 | GraphQLFieldConfig, 7 | GraphQLInputObjectType, 8 | GraphQLObjectType, 9 | GraphQLSchema, 10 | GraphQLSchemaConfig, 11 | } from 'graphql'; 12 | 13 | import { generateMySQL, generatePG, generateSQLite } from '@/util/builders'; 14 | import { ObjMap } from 'graphql/jsutils/ObjMap'; 15 | import type { AnyDrizzleDB, BuildSchemaConfig, GeneratedData } from './types'; 16 | 17 | export const buildSchema = >( 18 | db: TDbClient, 19 | config?: BuildSchemaConfig, 20 | ): GeneratedData => { 21 | const schema = db._.fullSchema; 22 | if (!schema) { 23 | throw new Error( 24 | "Drizzle-GraphQL Error: Schema not found in drizzle instance. Make sure you're using drizzle-orm v0.30.9 or above and schema is passed to drizzle constructor!", 25 | ); 26 | } 27 | 28 | if (typeof config?.relationsDepthLimit === 'number') { 29 | if (config.relationsDepthLimit < 0) { 30 | throw new Error( 31 | 'Drizzle-GraphQL Error: config.relationsDepthLimit is supposed to be nonnegative integer or undefined!', 32 | ); 33 | } 34 | if (config.relationsDepthLimit !== ~~config.relationsDepthLimit) { 35 | throw new Error( 36 | 'Drizzle-GraphQL Error: config.relationsDepthLimit is supposed to be nonnegative integer or undefined!', 37 | ); 38 | } 39 | } 40 | 41 | let generatorOutput; 42 | if (is(db, MySqlDatabase)) { 43 | generatorOutput = generateMySQL(db, schema, config?.relationsDepthLimit); 44 | } else if (is(db, PgDatabase)) { 45 | generatorOutput = generatePG(db, schema, config?.relationsDepthLimit); 46 | } else if (is(db, BaseSQLiteDatabase)) { 47 | generatorOutput = generateSQLite(db, schema, config?.relationsDepthLimit); 48 | } else throw new Error('Drizzle-GraphQL Error: Unknown database instance type'); 49 | 50 | const { queries, mutations, inputs, types } = generatorOutput; 51 | 52 | const graphQLSchemaConfig: GraphQLSchemaConfig = { 53 | types: [...Object.values(inputs), ...Object.values(types)] as (GraphQLInputObjectType | GraphQLObjectType)[], 54 | query: new GraphQLObjectType({ 55 | name: 'Query', 56 | fields: queries as ObjMap>, 57 | }), 58 | }; 59 | 60 | if (config?.mutations !== false) { 61 | const mutation = new GraphQLObjectType({ 62 | name: 'Mutation', 63 | fields: mutations as ObjMap>, 64 | }); 65 | 66 | graphQLSchemaConfig.mutation = mutation; 67 | } 68 | 69 | const outputSchema = new GraphQLSchema(graphQLSchemaConfig); 70 | 71 | return { schema: outputSchema, entities: generatorOutput }; 72 | }; 73 | 74 | export * from './types'; 75 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Many, One, Relation, Relations, Table, TableRelationalConfig, TablesRelationalConfig } from 'drizzle-orm'; 2 | import type { MySqlDatabase } from 'drizzle-orm/mysql-core'; 3 | import type { RelationalQueryBuilder as MySqlQuery } from 'drizzle-orm/mysql-core/query-builders/query'; 4 | import type { PgDatabase } from 'drizzle-orm/pg-core'; 5 | import type { RelationalQueryBuilder as PgQuery } from 'drizzle-orm/pg-core/query-builders/query'; 6 | import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'; 7 | import type { RelationalQueryBuilder as SQLiteQuery } from 'drizzle-orm/sqlite-core/query-builders/query'; 8 | import type { 9 | GraphQLInputObjectType, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLObjectType, 13 | GraphQLResolveInfo, 14 | GraphQLScalarType, 15 | GraphQLSchema, 16 | } from 'graphql'; 17 | 18 | import type { 19 | Filters, 20 | GetRemappedTableDataType, 21 | GetRemappedTableInsertDataType, 22 | GetRemappedTableUpdateDataType, 23 | OrderByArgs, 24 | } from '@/util/builders'; 25 | 26 | export type AnyDrizzleDB> = 27 | | PgDatabase 28 | | BaseSQLiteDatabase 29 | | MySqlDatabase; 30 | 31 | export type AnyQueryBuiler = 32 | | PgQuery 33 | | MySqlQuery 34 | | SQLiteQuery; 35 | 36 | export type ExtractTables> = { 37 | [K in keyof TSchema as TSchema[K] extends Table ? K : never]: TSchema[K] extends Table ? TSchema[K] : never; 38 | }; 39 | 40 | export type ExtractRelations> = { 41 | [K in keyof TSchema as TSchema[K] extends Relations ? K : never]: TSchema[K] extends Relations ? TSchema[K] : never; 42 | }; 43 | 44 | export type ExtractTableRelations> = { 45 | [ 46 | K in keyof TSchemaRelations as TSchemaRelations[K]['table']['_']['name'] extends TTable['_']['name'] ? K 47 | : never 48 | ]: TSchemaRelations[K]['table']['_']['name'] extends TTable['_']['name'] 49 | ? TSchemaRelations[K] extends Relations ? RelationConfig 50 | : never 51 | : never; 52 | }; 53 | 54 | export type ExtractTableByName, TName extends string> = { 55 | [ 56 | K in keyof TTableSchema as TTableSchema[K]['_']['name'] extends TName ? K 57 | : never 58 | ]: TTableSchema[K]['_']['name'] extends TName ? TTableSchema[K] : never; 59 | }; 60 | 61 | export type MutationReturnlessResult = { 62 | isSuccess: boolean; 63 | }; 64 | 65 | export type QueryArgs = Partial< 66 | (isSingle extends true ? { 67 | offset: number; 68 | } 69 | : { 70 | offset: number; 71 | limit: number; 72 | }) & { 73 | where: Filters; 74 | orderBy: OrderByArgs; 75 | } 76 | >; 77 | 78 | export type InsertArgs = isSingle extends true ? { 79 | values: GetRemappedTableInsertDataType; 80 | } 81 | : { 82 | values: Array>; 83 | }; 84 | 85 | export type UpdateArgs = Partial<{ 86 | set: GetRemappedTableUpdateDataType; 87 | where?: Filters; 88 | }>; 89 | 90 | export type DeleteArgs = { 91 | where?: Filters; 92 | }; 93 | 94 | export type SelectResolver< 95 | TTable extends Table, 96 | TTables extends Record, 97 | TRelations extends Record, 98 | > = ( 99 | source: any, 100 | args: Partial>, 101 | context: any, 102 | info: GraphQLResolveInfo, 103 | ) => Promise< 104 | keyof TRelations extends infer RelKey ? RelKey extends string ? Array< 105 | & GetRemappedTableDataType 106 | & { 107 | [K in RelKey]: TRelations[K] extends One ? 108 | | GetRemappedTableDataType< 109 | ExtractTableByName extends infer T ? T[keyof T] 110 | : never 111 | > 112 | | null 113 | : TRelations[K] extends Many ? Array< 114 | GetRemappedTableDataType< 115 | ExtractTableByName< 116 | TTables, 117 | TRelations[K]['referencedTableName'] 118 | > extends infer T ? T[keyof T] 119 | : never 120 | > 121 | > 122 | : never; 123 | } 124 | > 125 | : Array> 126 | : Array> 127 | >; 128 | 129 | export type SelectSingleResolver< 130 | TTable extends Table, 131 | TTables extends Record, 132 | TRelations extends Record, 133 | > = ( 134 | source: any, 135 | args: Partial>, 136 | context: any, 137 | info: GraphQLResolveInfo, 138 | ) => Promise< 139 | | (keyof TRelations extends infer RelKey ? RelKey extends string ? 140 | & GetRemappedTableDataType 141 | & { 142 | [K in RelKey]: TRelations[K] extends One ? 143 | | GetRemappedTableDataType< 144 | ExtractTableByName extends infer T ? T[keyof T] 145 | : never 146 | > 147 | | null 148 | : TRelations[K] extends Many ? Array< 149 | GetRemappedTableDataType< 150 | ExtractTableByName< 151 | TTables, 152 | TRelations[K]['referencedTableName'] 153 | > extends infer T ? T[keyof T] 154 | : never 155 | > 156 | > 157 | : never; 158 | } 159 | : GetRemappedTableDataType 160 | : GetRemappedTableDataType) 161 | | null 162 | >; 163 | 164 | export type InsertResolver = ( 165 | source: any, 166 | args: Partial>, 167 | context: any, 168 | info: GraphQLResolveInfo, 169 | ) => Promise> : MutationReturnlessResult>; 170 | 171 | export type InsertArrResolver = ( 172 | source: any, 173 | args: Partial>, 174 | context: any, 175 | info: GraphQLResolveInfo, 176 | ) => Promise | undefined : MutationReturnlessResult>; 177 | 178 | export type UpdateResolver = ( 179 | source: any, 180 | args: UpdateArgs, 181 | context: any, 182 | info: GraphQLResolveInfo, 183 | ) => Promise | undefined : MutationReturnlessResult>; 184 | 185 | export type DeleteResolver = ( 186 | source: any, 187 | args: DeleteArgs, 188 | context: any, 189 | info: GraphQLResolveInfo, 190 | ) => Promise | undefined : MutationReturnlessResult>; 191 | 192 | export type QueriesCore< 193 | TSchemaTables extends Record, 194 | TSchemaRelations extends Record, 195 | TInputs extends Record, 196 | TOutputs extends Record, 197 | > = 198 | & { 199 | [TName in keyof TSchemaTables as TName extends string ? `${Uncapitalize}` : never]: TName extends string ? { 200 | type: GraphQLNonNull}SelectItem`]>>>; 201 | args: { 202 | offset: { 203 | type: GraphQLScalarType; 204 | }; 205 | limit: { 206 | type: GraphQLScalarType; 207 | }; 208 | orderBy: { 209 | type: TInputs[`${Capitalize}OrderBy`] extends GraphQLInputObjectType 210 | ? TInputs[`${Capitalize}OrderBy`] 211 | : never; 212 | }; 213 | where: { 214 | type: TInputs[`${Capitalize}Filters`] extends GraphQLInputObjectType 215 | ? TInputs[`${Capitalize}Filters`] 216 | : never; 217 | }; 218 | }; 219 | resolve: SelectResolver< 220 | TSchemaTables[TName], 221 | TSchemaTables, 222 | ExtractTableRelations extends infer R ? R[keyof R] : never 223 | >; 224 | } 225 | : never; 226 | } 227 | & { 228 | [TName in keyof TSchemaTables as TName extends string ? `${Uncapitalize}Single` : never]: TName extends 229 | string ? { 230 | type: TOutputs[`${Capitalize}SelectItem`]; 231 | args: { 232 | offset: { 233 | type: GraphQLScalarType; 234 | }; 235 | orderBy: { 236 | type: TInputs[`${Capitalize}OrderBy`] extends GraphQLInputObjectType 237 | ? TInputs[`${Capitalize}OrderBy`] 238 | : never; 239 | }; 240 | where: { 241 | type: TInputs[`${Capitalize}Filters`] extends GraphQLInputObjectType 242 | ? TInputs[`${Capitalize}Filters`] 243 | : never; 244 | }; 245 | }; 246 | resolve: SelectSingleResolver< 247 | TSchemaTables[TName], 248 | TSchemaTables, 249 | ExtractTableRelations extends infer R ? R[keyof R] : never 250 | >; 251 | } 252 | : never; 253 | }; 254 | 255 | export type MutationsCore< 256 | TSchemaTables extends Record, 257 | TInputs extends Record, 258 | TOutputs extends Record, 259 | IsReturnless extends boolean, 260 | > = 261 | & { 262 | [ 263 | TName in keyof TSchemaTables as TName extends string ? `insertInto${Capitalize}` 264 | : never 265 | ]: TName extends string ? { 266 | type: IsReturnless extends true 267 | ? TOutputs['MutationReturn'] extends GraphQLObjectType ? TOutputs['MutationReturn'] 268 | : never 269 | : GraphQLNonNull}Item`]>>>; 270 | args: { 271 | values: { 272 | type: GraphQLNonNull}InsertInput`]>>>; 273 | }; 274 | }; 275 | resolve: InsertArrResolver; 276 | } 277 | : never; 278 | } 279 | & { 280 | [ 281 | TName in keyof TSchemaTables as TName extends string ? `insertInto${Capitalize}Single` 282 | : never 283 | ]: TName extends string ? { 284 | type: IsReturnless extends true 285 | ? TOutputs['MutationReturn'] extends GraphQLObjectType ? TOutputs['MutationReturn'] 286 | : never 287 | : TOutputs[`${Capitalize}Item`]; 288 | 289 | args: { 290 | values: { 291 | type: GraphQLNonNull}InsertInput`]>; 292 | }; 293 | }; 294 | resolve: InsertResolver; 295 | } 296 | : never; 297 | } 298 | & { 299 | [TName in keyof TSchemaTables as TName extends string ? `update${Capitalize}` : never]: TName extends string 300 | ? { 301 | type: IsReturnless extends true 302 | ? TOutputs['MutationReturn'] extends GraphQLObjectType ? TOutputs['MutationReturn'] 303 | : never 304 | : GraphQLNonNull}Item`]>>>; 305 | args: { 306 | set: { 307 | type: GraphQLNonNull}UpdateInput`]>; 308 | }; 309 | where: { 310 | type: TInputs[`${Capitalize}Filters`] extends GraphQLInputObjectType 311 | ? TInputs[`${Capitalize}Filters`] 312 | : never; 313 | }; 314 | }; 315 | resolve: UpdateResolver; 316 | } 317 | : never; 318 | } 319 | & { 320 | [ 321 | TName in keyof TSchemaTables as TName extends string ? `deleteFrom${Capitalize}` 322 | : never 323 | ]: TName extends string ? { 324 | type: IsReturnless extends true 325 | ? TOutputs['MutationReturn'] extends GraphQLObjectType ? TOutputs['MutationReturn'] 326 | : never 327 | : GraphQLNonNull}Item`]>>>; 328 | args: { 329 | where: { 330 | type: TInputs[`${Capitalize}Filters`] extends GraphQLInputObjectType 331 | ? TInputs[`${Capitalize}Filters`] 332 | : never; 333 | }; 334 | }; 335 | resolve: DeleteResolver; 336 | } 337 | : never; 338 | }; 339 | 340 | export type GeneratedInputs> = 341 | & { 342 | [TName in keyof TSchema as TName extends string ? `${Capitalize}InsertInput` : never]: 343 | GraphQLInputObjectType; 344 | } 345 | & { 346 | [TName in keyof TSchema as TName extends string ? `${Capitalize}UpdateInput` : never]: 347 | GraphQLInputObjectType; 348 | } 349 | & { 350 | [TName in keyof TSchema as TName extends string ? `${Capitalize}OrderBy` : never]: GraphQLInputObjectType; 351 | } 352 | & { 353 | [TName in keyof TSchema as TName extends string ? `${Capitalize}Filters` : never]: GraphQLInputObjectType; 354 | }; 355 | 356 | export type GeneratedOutputs, IsReturnless extends Boolean> = 357 | & { 358 | [TName in keyof TSchema as TName extends string ? `${Capitalize}SelectItem` : never]: GraphQLObjectType; 359 | } 360 | & (IsReturnless extends true ? { 361 | MutationReturn: GraphQLObjectType; 362 | } 363 | : { 364 | [TName in keyof TSchema as TName extends string ? `${Capitalize}Item` : never]: GraphQLObjectType; 365 | }); 366 | 367 | export type GeneratedEntities< 368 | TDatabase extends AnyDrizzleDB, 369 | TSchema extends Record = TDatabase extends AnyDrizzleDB ? ISchema : never, 370 | TSchemaTables extends ExtractTables = ExtractTables, 371 | TSchemaRelations extends ExtractRelations = ExtractRelations, 372 | TInputs extends GeneratedInputs = GeneratedInputs, 373 | TOutputs extends GeneratedOutputs< 374 | TSchemaTables, 375 | TDatabase extends MySqlDatabase ? true : false 376 | > = GeneratedOutputs ? true : false>, 377 | > = { 378 | queries: QueriesCore; 379 | mutations: MutationsCore< 380 | TSchemaTables, 381 | TInputs, 382 | TOutputs, 383 | TDatabase extends MySqlDatabase ? true : false 384 | >; 385 | inputs: TInputs; 386 | types: TOutputs; 387 | }; 388 | 389 | export type GeneratedData< 390 | TDatabase extends AnyDrizzleDB, 391 | > = { 392 | schema: GraphQLSchema; 393 | entities: GeneratedEntities; 394 | }; 395 | 396 | export type BuildSchemaConfig = { 397 | /** 398 | * Determines whether generated mutations will be passed to returned schema. 399 | * 400 | * Set value to `false` to omit mutations from returned schema. 401 | * 402 | * Flag is treated as if set to `true` by default. 403 | */ 404 | mutations?: boolean; 405 | /** 406 | * Limits depth of generated relation fields on queries. 407 | * 408 | * Expects non-negative integer or undefined. 409 | * 410 | * Set value to `undefined` to not limit relation depth. 411 | * 412 | * Set value to `0` to omit relations altogether. 413 | * 414 | * Value is treated as if set to `undefined` by default. 415 | */ 416 | relationsDepthLimit?: number; 417 | }; 418 | -------------------------------------------------------------------------------- /src/util/builders/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | and, 3 | asc, 4 | desc, 5 | eq, 6 | getTableColumns, 7 | gt, 8 | gte, 9 | ilike, 10 | inArray, 11 | is, 12 | isNotNull, 13 | isNull, 14 | like, 15 | lt, 16 | lte, 17 | ne, 18 | notIlike, 19 | notInArray, 20 | notLike, 21 | One, 22 | or, 23 | SQL, 24 | } from 'drizzle-orm'; 25 | import { 26 | GraphQLBoolean, 27 | GraphQLEnumType, 28 | GraphQLError, 29 | GraphQLInputObjectType, 30 | GraphQLInt, 31 | GraphQLList, 32 | GraphQLNonNull, 33 | GraphQLObjectType, 34 | GraphQLString, 35 | } from 'graphql'; 36 | 37 | import { capitalize } from '@/util/case-ops'; 38 | import { remapFromGraphQLCore } from '@/util/data-mappers'; 39 | import { 40 | ConvertedColumn, 41 | ConvertedInputColumn, 42 | ConvertedRelationColumnWithArgs, 43 | drizzleColumnToGraphQLType, 44 | } from '@/util/type-converter'; 45 | 46 | import type { Column, Table } from 'drizzle-orm'; 47 | import type { ResolveTree } from 'graphql-parse-resolve-info'; 48 | import type { 49 | FilterColumnOperators, 50 | FilterColumnOperatorsCore, 51 | Filters, 52 | FiltersCore, 53 | GeneratedTableTypes, 54 | GeneratedTableTypesOutputs, 55 | OrderByArgs, 56 | ProcessedTableSelectArgs, 57 | SelectData, 58 | SelectedColumnsRaw, 59 | SelectedSQLColumns, 60 | TableNamedRelations, 61 | TableSelectArgs, 62 | } from './types'; 63 | 64 | const rqbCrashTypes = [ 65 | 'SQLiteBigInt', 66 | 'SQLiteBlobJson', 67 | 'SQLiteBlobBuffer', 68 | ]; 69 | 70 | export const extractSelectedColumnsFromTree = ( 71 | tree: Record, 72 | table: Table, 73 | ): Record => { 74 | const tableColumns = getTableColumns(table); 75 | 76 | const treeEntries = Object.entries(tree); 77 | const selectedColumns: SelectedColumnsRaw = []; 78 | 79 | for (const [fieldName, fieldData] of treeEntries) { 80 | if (!tableColumns[fieldData.name]) continue; 81 | 82 | selectedColumns.push([fieldData.name, true]); 83 | } 84 | 85 | if (!selectedColumns.length) { 86 | const columnKeys = Object.entries(tableColumns); 87 | const columnName = columnKeys.find((e) => rqbCrashTypes.find((haram) => e[1].columnType !== haram))?.[0] 88 | ?? columnKeys[0]![0]; 89 | 90 | selectedColumns.push([columnName, true]); 91 | } 92 | 93 | return Object.fromEntries(selectedColumns); 94 | }; 95 | 96 | /** 97 | * Can't automatically determine column type on type level 98 | * Since drizzle table types extend eachother 99 | */ 100 | export const extractSelectedColumnsFromTreeSQLFormat = ( 101 | tree: Record, 102 | table: Table, 103 | ): Record => { 104 | const tableColumns = getTableColumns(table); 105 | 106 | const treeEntries = Object.entries(tree); 107 | const selectedColumns: SelectedSQLColumns = []; 108 | 109 | for (const [fieldName, fieldData] of treeEntries) { 110 | if (!tableColumns[fieldData.name]) continue; 111 | 112 | selectedColumns.push([fieldData.name, tableColumns[fieldData.name]!]); 113 | } 114 | 115 | if (!selectedColumns.length) { 116 | const columnKeys = Object.entries(tableColumns); 117 | const columnName = columnKeys.find((e) => rqbCrashTypes.find((haram) => e[1].columnType !== haram))?.[0] 118 | ?? columnKeys[0]![0]; 119 | 120 | selectedColumns.push([columnName, tableColumns[columnName]!]); 121 | } 122 | 123 | return Object.fromEntries(selectedColumns) as Record; 124 | }; 125 | 126 | export const innerOrder = new GraphQLInputObjectType({ 127 | name: 'InnerOrder' as const, 128 | fields: { 129 | direction: { 130 | type: new GraphQLNonNull( 131 | new GraphQLEnumType({ 132 | name: 'OrderDirection', 133 | description: 'Order by direction', 134 | values: { 135 | asc: { 136 | value: 'asc', 137 | description: 'Ascending order', 138 | }, 139 | desc: { 140 | value: 'desc', 141 | description: 'Descending order', 142 | }, 143 | }, 144 | }), 145 | ), 146 | }, 147 | priority: { type: new GraphQLNonNull(GraphQLInt), description: 'Priority of current field' }, 148 | } as const, 149 | }); 150 | 151 | const generateColumnFilterValues = (column: Column, tableName: string, columnName: string): GraphQLInputObjectType => { 152 | const columnGraphQLType = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true); 153 | const columnArr = new GraphQLList(new GraphQLNonNull(columnGraphQLType.type)); 154 | 155 | const baseFields = { 156 | eq: { type: columnGraphQLType.type, description: columnGraphQLType.description }, 157 | ne: { type: columnGraphQLType.type, description: columnGraphQLType.description }, 158 | lt: { type: columnGraphQLType.type, description: columnGraphQLType.description }, 159 | lte: { type: columnGraphQLType.type, description: columnGraphQLType.description }, 160 | gt: { type: columnGraphQLType.type, description: columnGraphQLType.description }, 161 | gte: { type: columnGraphQLType.type, description: columnGraphQLType.description }, 162 | like: { type: GraphQLString }, 163 | notLike: { type: GraphQLString }, 164 | ilike: { type: GraphQLString }, 165 | notIlike: { type: GraphQLString }, 166 | inArray: { type: columnArr, description: `Array<${columnGraphQLType.description}>` }, 167 | notInArray: { type: columnArr, description: `Array<${columnGraphQLType.description}>` }, 168 | isNull: { type: GraphQLBoolean }, 169 | isNotNull: { type: GraphQLBoolean }, 170 | }; 171 | 172 | const type: GraphQLInputObjectType = new GraphQLInputObjectType({ 173 | name: `${capitalize(tableName)}${capitalize(columnName)}Filters`, 174 | fields: { 175 | ...baseFields, 176 | OR: { 177 | type: new GraphQLList( 178 | new GraphQLNonNull( 179 | new GraphQLInputObjectType({ 180 | name: `${capitalize(tableName)}${capitalize(columnName)}filtersOr`, 181 | fields: { 182 | ...baseFields, 183 | }, 184 | }), 185 | ), 186 | ), 187 | }, 188 | }, 189 | }); 190 | 191 | return type; 192 | }; 193 | 194 | const orderMap = new WeakMap>(); 195 | const generateTableOrderCached = (table: Table) => { 196 | if (orderMap.has(table)) return orderMap.get(table)!; 197 | 198 | const columns = getTableColumns(table); 199 | const columnEntries = Object.entries(columns); 200 | 201 | const remapped = Object.fromEntries( 202 | columnEntries.map(([columnName, columnDescription]) => [columnName, { type: innerOrder }]), 203 | ); 204 | 205 | orderMap.set(table, remapped); 206 | 207 | return remapped; 208 | }; 209 | 210 | const filterMap = new WeakMap>(); 211 | const generateTableFilterValuesCached = (table: Table, tableName: string) => { 212 | if (filterMap.has(table)) return filterMap.get(table)!; 213 | 214 | const columns = getTableColumns(table); 215 | const columnEntries = Object.entries(columns); 216 | 217 | const remapped = Object.fromEntries( 218 | columnEntries.map(([columnName, columnDescription]) => [ 219 | columnName, 220 | { 221 | type: generateColumnFilterValues(columnDescription, tableName, columnName), 222 | }, 223 | ]), 224 | ); 225 | 226 | filterMap.set(table, remapped); 227 | 228 | return remapped; 229 | }; 230 | 231 | const fieldMap = new WeakMap>(); 232 | const generateTableSelectTypeFieldsCached = (table: Table, tableName: string): Record => { 233 | if (fieldMap.has(table)) return fieldMap.get(table)!; 234 | 235 | const columns = getTableColumns(table); 236 | const columnEntries = Object.entries(columns); 237 | 238 | const remapped = Object.fromEntries( 239 | columnEntries.map(([columnName, columnDescription]) => [ 240 | columnName, 241 | drizzleColumnToGraphQLType(columnDescription, columnName, tableName), 242 | ]), 243 | ); 244 | 245 | fieldMap.set(table, remapped); 246 | 247 | return remapped; 248 | }; 249 | 250 | const orderTypeMap = new WeakMap(); 251 | const generateTableOrderTypeCached = (table: Table, tableName: string) => { 252 | if (orderTypeMap.has(table)) return orderTypeMap.get(table)!; 253 | 254 | const orderColumns = generateTableOrderCached(table); 255 | const order = new GraphQLInputObjectType({ 256 | name: `${capitalize(tableName)}OrderBy`, 257 | fields: orderColumns, 258 | }); 259 | 260 | orderTypeMap.set(table, order); 261 | 262 | return order; 263 | }; 264 | 265 | const filterTypeMap = new WeakMap(); 266 | const generateTableFilterTypeCached = (table: Table, tableName: string) => { 267 | if (filterTypeMap.has(table)) return filterTypeMap.get(table)!; 268 | 269 | const filterColumns = generateTableFilterValuesCached(table, tableName); 270 | const filters: GraphQLInputObjectType = new GraphQLInputObjectType({ 271 | name: `${capitalize(tableName)}Filters`, 272 | fields: { 273 | ...filterColumns, 274 | OR: { 275 | type: new GraphQLList( 276 | new GraphQLNonNull( 277 | new GraphQLInputObjectType({ 278 | name: `${capitalize(tableName)}FiltersOr`, 279 | fields: filterColumns, 280 | }), 281 | ), 282 | ), 283 | }, 284 | }, 285 | }); 286 | 287 | filterTypeMap.set(table, filters); 288 | 289 | return filters; 290 | }; 291 | 292 | const generateSelectFields = ( 293 | tables: Record, 294 | tableName: string, 295 | relationMap: Record>, 296 | typeName: string, 297 | withOrder: TWithOrder, 298 | relationsDepthLimit: number | undefined, 299 | currentDepth: number = 0, 300 | usedTables: Set = new Set(), 301 | ): SelectData => { 302 | const relations = relationMap[tableName]; 303 | const relationEntries: [string, TableNamedRelations][] = relations ? Object.entries(relations) : []; 304 | 305 | const table = tables[tableName]!; 306 | 307 | const order = withOrder 308 | ? generateTableOrderTypeCached(table, tableName) 309 | : undefined; 310 | 311 | const filters = generateTableFilterTypeCached(table, tableName); 312 | 313 | const tableFields = generateTableSelectTypeFieldsCached(table, tableName); 314 | 315 | if ( 316 | usedTables.has(tableName) || (typeof relationsDepthLimit === 'number' && currentDepth >= relationsDepthLimit) 317 | || !relationEntries.length 318 | ) { 319 | return { 320 | order, 321 | filters, 322 | tableFields, 323 | relationFields: {}, 324 | } as SelectData; 325 | } 326 | 327 | const rawRelationFields: [string, ConvertedRelationColumnWithArgs][] = []; 328 | const updatedUsedTables = new Set(usedTables).add(tableName); 329 | const newDepth = currentDepth + 1; 330 | 331 | for (const [relationName, { targetTableName, relation }] of relationEntries) { 332 | const relTypeName = `${typeName}${capitalize(relationName)}Relation`; 333 | const isOne = is(relation, One); 334 | 335 | const relData = generateSelectFields( 336 | tables, 337 | targetTableName, 338 | relationMap, 339 | relTypeName, 340 | !isOne, 341 | relationsDepthLimit, 342 | newDepth, 343 | updatedUsedTables, 344 | ); 345 | 346 | const relType = new GraphQLObjectType({ 347 | name: relTypeName, 348 | fields: { ...relData.tableFields, ...relData.relationFields }, 349 | }); 350 | 351 | if (isOne) { 352 | rawRelationFields.push([ 353 | relationName, 354 | { 355 | type: relType, 356 | args: { 357 | where: { type: relData.filters }, 358 | }, 359 | }, 360 | ]); 361 | 362 | continue; 363 | } 364 | 365 | rawRelationFields.push([ 366 | relationName, 367 | { 368 | type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(relType))), 369 | args: { 370 | where: { type: relData.filters }, 371 | orderBy: { type: relData.order! }, 372 | offset: { type: GraphQLInt }, 373 | limit: { type: GraphQLInt }, 374 | }, 375 | }, 376 | ]); 377 | } 378 | 379 | const relationFields = Object.fromEntries(rawRelationFields); 380 | 381 | return { order, filters, tableFields, relationFields } as SelectData; 382 | }; 383 | 384 | export const generateTableTypes = < 385 | WithReturning extends boolean, 386 | >( 387 | tableName: string, 388 | tables: Record, 389 | relationMap: Record>, 390 | withReturning: WithReturning, 391 | relationsDepthLimit: number | undefined, 392 | ): GeneratedTableTypes => { 393 | const stylizedName = capitalize(tableName); 394 | const { tableFields, relationFields, filters, order } = generateSelectFields( 395 | tables, 396 | tableName, 397 | relationMap, 398 | stylizedName, 399 | true, 400 | relationsDepthLimit, 401 | ); 402 | 403 | const table = tables[tableName]!; 404 | const columns = getTableColumns(table); 405 | const columnEntries = Object.entries(columns); 406 | 407 | const insertFields = Object.fromEntries( 408 | columnEntries.map(([columnName, columnDescription]) => [ 409 | columnName, 410 | drizzleColumnToGraphQLType(columnDescription, columnName, tableName, false, true, true), 411 | ]), 412 | ); 413 | 414 | const updateFields = Object.fromEntries( 415 | columnEntries.map(([columnName, columnDescription]) => [ 416 | columnName, 417 | drizzleColumnToGraphQLType(columnDescription, columnName, tableName, true, false, true), 418 | ]), 419 | ); 420 | 421 | const insertInput = new GraphQLInputObjectType({ 422 | name: `${stylizedName}InsertInput`, 423 | fields: insertFields, 424 | }); 425 | 426 | const selectSingleOutput = new GraphQLObjectType({ 427 | name: `${stylizedName}SelectItem`, 428 | fields: { ...tableFields, ...relationFields }, 429 | }); 430 | 431 | const selectArrOutput = new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(selectSingleOutput))); 432 | 433 | const singleTableItemOutput = withReturning 434 | ? new GraphQLObjectType({ 435 | name: `${stylizedName}Item`, 436 | fields: tableFields, 437 | }) 438 | : undefined; 439 | 440 | const arrTableItemOutput = withReturning 441 | ? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(singleTableItemOutput!))) 442 | : undefined; 443 | 444 | const updateInput = new GraphQLInputObjectType({ 445 | name: `${stylizedName}UpdateInput`, 446 | fields: updateFields, 447 | }); 448 | 449 | const inputs = { 450 | insertInput, 451 | updateInput, 452 | tableOrder: order, 453 | tableFilters: filters, 454 | }; 455 | 456 | const outputs = ( 457 | withReturning 458 | ? { 459 | selectSingleOutput, 460 | selectArrOutput, 461 | singleTableItemOutput: singleTableItemOutput!, 462 | arrTableItemOutput: arrTableItemOutput!, 463 | } 464 | : { 465 | selectSingleOutput, 466 | selectArrOutput, 467 | } 468 | ) as GeneratedTableTypesOutputs; 469 | 470 | return { 471 | inputs, 472 | outputs, 473 | }; 474 | }; 475 | 476 | export const extractOrderBy = = OrderByArgs>( 477 | table: TTable, 478 | orderArgs: TArgs, 479 | ): SQL[] => { 480 | const res = [] as SQL[]; 481 | 482 | for ( 483 | const [column, config] of Object.entries(orderArgs).sort( 484 | (a, b) => (b[1]?.priority ?? 0) - (a[1]?.priority ?? 0), 485 | ) 486 | ) { 487 | if (!config) continue; 488 | const { direction } = config; 489 | 490 | res.push(direction === 'asc' ? asc(getTableColumns(table)[column]!) : desc(getTableColumns(table)[column]!)); 491 | } 492 | 493 | return res; 494 | }; 495 | 496 | export const extractFiltersColumn = ( 497 | column: TColumn, 498 | columnName: string, 499 | operators: FilterColumnOperators, 500 | ): SQL | undefined => { 501 | if (!operators.OR?.length) delete operators.OR; 502 | 503 | const entries = Object.entries(operators as FilterColumnOperatorsCore); 504 | 505 | if (operators.OR) { 506 | if (entries.length > 1) { 507 | throw new GraphQLError(`WHERE ${columnName}: Cannot specify both fields and 'OR' in column operators!`); 508 | } 509 | 510 | const variants = [] as SQL[]; 511 | 512 | for (const variant of operators.OR) { 513 | const extracted = extractFiltersColumn(column, columnName, variant); 514 | 515 | if (extracted) variants.push(extracted); 516 | } 517 | 518 | return variants.length ? (variants.length > 1 ? or(...variants) : variants[0]) : undefined; 519 | } 520 | 521 | const variants = [] as SQL[]; 522 | for (const [operatorName, operatorValue] of entries) { 523 | if (operatorValue === null || operatorValue === false) continue; 524 | 525 | let operator: ((...args: any[]) => SQL) | undefined; 526 | switch (operatorName as keyof FilterColumnOperatorsCore) { 527 | // @ts-ignore 528 | case 'eq': 529 | operator = operator ?? eq; 530 | // @ts-ignore 531 | case 'ne': 532 | operator = operator ?? ne; 533 | // @ts-ignore 534 | case 'gt': 535 | operator = operator ?? gt; 536 | // @ts-ignore 537 | case 'gte': 538 | operator = operator ?? gte; 539 | // @ts-ignore 540 | case 'lt': 541 | operator = operator ?? lt; 542 | case 'lte': 543 | operator = operator ?? lte; 544 | 545 | const singleValue = remapFromGraphQLCore(operatorValue, column, columnName); 546 | variants.push(operator(column, singleValue)); 547 | 548 | break; 549 | 550 | // @ts-ignore 551 | case 'like': 552 | operator = operator ?? like; 553 | // @ts-ignore 554 | case 'notLike': 555 | operator = operator ?? notLike; 556 | // @ts-ignore 557 | case 'ilike': 558 | operator = operator ?? ilike; 559 | case 'notIlike': 560 | operator = operator ?? notIlike; 561 | 562 | variants.push(operator(column, operatorValue as string)); 563 | 564 | break; 565 | 566 | // @ts-ignore 567 | case 'inArray': 568 | operator = operator ?? inArray; 569 | case 'notInArray': 570 | operator = operator ?? notInArray; 571 | 572 | if (!(operatorValue as any[]).length) { 573 | throw new GraphQLError( 574 | `WHERE ${columnName}: Unable to use operator ${operatorName} with an empty array!`, 575 | ); 576 | } 577 | const arrayValue = (operatorValue as any[]).map((val) => remapFromGraphQLCore(val, column, columnName)); 578 | 579 | variants.push(operator(column, arrayValue)); 580 | break; 581 | 582 | // @ts-ignore 583 | case 'isNull': 584 | operator = operator ?? isNull; 585 | case 'isNotNull': 586 | operator = operator ?? isNotNull; 587 | 588 | variants.push(operator(column)); 589 | } 590 | } 591 | 592 | return variants.length ? (variants.length > 1 ? and(...variants) : variants[0]) : undefined; 593 | }; 594 | 595 | export const extractFilters = ( 596 | table: TTable, 597 | tableName: string, 598 | filters: Filters, 599 | ): SQL | undefined => { 600 | if (!filters.OR?.length) delete filters.OR; 601 | 602 | const entries = Object.entries(filters as FiltersCore); 603 | if (!entries.length) return; 604 | 605 | if (filters.OR) { 606 | if (entries.length > 1) { 607 | throw new GraphQLError(`WHERE ${tableName}: Cannot specify both fields and 'OR' in table filters!`); 608 | } 609 | 610 | const variants = [] as SQL[]; 611 | 612 | for (const variant of filters.OR) { 613 | const extracted = extractFilters(table, tableName, variant); 614 | if (extracted) variants.push(extracted); 615 | } 616 | 617 | return variants.length ? (variants.length > 1 ? or(...variants) : variants[0]) : undefined; 618 | } 619 | 620 | const variants = [] as SQL[]; 621 | for (const [columnName, operators] of entries) { 622 | if (operators === null) continue; 623 | 624 | const column = getTableColumns(table)[columnName]!; 625 | variants.push(extractFiltersColumn(column, columnName, operators)!); 626 | } 627 | 628 | return variants.length ? (variants.length > 1 ? and(...variants) : variants[0]) : undefined; 629 | }; 630 | 631 | const extractRelationsParamsInner = ( 632 | relationMap: Record>, 633 | tables: Record, 634 | tableName: string, 635 | typeName: string, 636 | originField: ResolveTree, 637 | isInitial: boolean = false, 638 | ) => { 639 | const relations = relationMap[tableName]; 640 | if (!relations) return undefined; 641 | 642 | const baseField = Object.entries(originField.fieldsByTypeName).find(([key, value]) => key === typeName)?.[1]; 643 | if (!baseField) return undefined; 644 | 645 | const args: Record> = {}; 646 | 647 | for (const [relName, { targetTableName, relation }] of Object.entries(relations)) { 648 | const relTypeName = `${isInitial ? capitalize(tableName) : typeName}${capitalize(relName)}Relation`; 649 | const relFieldSelection = Object.values(baseField).find((field) => 650 | field.name === relName 651 | )?.fieldsByTypeName[relTypeName]; 652 | if (!relFieldSelection) continue; 653 | 654 | const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName]!); 655 | 656 | const thisRecord: Partial = {}; 657 | thisRecord.columns = columns; 658 | 659 | const relationField = Object.values(baseField).find((e) => e.name === relName); 660 | const relationArgs: Partial | undefined = relationField?.args; 661 | 662 | const orderBy = relationArgs?.orderBy ? extractOrderBy(tables[targetTableName]!, relationArgs.orderBy!) : undefined; 663 | const where = relationArgs?.where 664 | ? extractFilters(tables[targetTableName]!, relName, relationArgs?.where) 665 | : undefined; 666 | const offset = relationArgs?.offset ?? undefined; 667 | const limit = relationArgs?.limit ?? undefined; 668 | 669 | thisRecord.orderBy = orderBy; 670 | thisRecord.where = where; 671 | thisRecord.offset = offset; 672 | thisRecord.limit = limit; 673 | 674 | const relWith = relationField 675 | ? extractRelationsParamsInner(relationMap, tables, targetTableName, relTypeName, relationField) 676 | : undefined; 677 | thisRecord.with = relWith; 678 | 679 | args[relName] = thisRecord; 680 | } 681 | 682 | return args; 683 | }; 684 | 685 | export const extractRelationsParams = ( 686 | relationMap: Record>, 687 | tables: Record, 688 | tableName: string, 689 | info: ResolveTree | undefined, 690 | typeName: string, 691 | ): Record> | undefined => { 692 | if (!info) return undefined; 693 | 694 | return extractRelationsParamsInner(relationMap, tables, tableName, typeName, info, true); 695 | }; 696 | -------------------------------------------------------------------------------- /src/util/builders/index.ts: -------------------------------------------------------------------------------- 1 | export { generateSchemaData as generateMySQL } from './mysql'; 2 | export { generateSchemaData as generatePG } from './pg'; 3 | export { generateSchemaData as generateSQLite } from './sqlite'; 4 | 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /src/util/builders/mysql.ts: -------------------------------------------------------------------------------- 1 | import { createTableRelationsHelpers, is, Relation, Relations, Table } from 'drizzle-orm'; 2 | import { MySqlDatabase, MySqlTable } from 'drizzle-orm/mysql-core'; 3 | import { 4 | GraphQLBoolean, 5 | GraphQLError, 6 | GraphQLInputObjectType, 7 | GraphQLInt, 8 | GraphQLList, 9 | GraphQLNonNull, 10 | GraphQLObjectType, 11 | } from 'graphql'; 12 | 13 | import { 14 | extractFilters, 15 | extractOrderBy, 16 | extractRelationsParams, 17 | extractSelectedColumnsFromTree, 18 | generateTableTypes, 19 | } from '@/util/builders/common'; 20 | import { capitalize, uncapitalize } from '@/util/case-ops'; 21 | import { 22 | remapFromGraphQLArrayInput, 23 | remapFromGraphQLSingleInput, 24 | remapToGraphQLArrayOutput, 25 | remapToGraphQLSingleOutput, 26 | } from '@/util/data-mappers'; 27 | import { parseResolveInfo } from 'graphql-parse-resolve-info'; 28 | 29 | import type { GeneratedEntities } from '@/types'; 30 | import type { RelationalQueryBuilder } from 'drizzle-orm/mysql-core/query-builders/query'; 31 | import type { GraphQLFieldConfig, GraphQLFieldConfigArgumentMap, ThunkObjMap } from 'graphql'; 32 | import type { ResolveTree } from 'graphql-parse-resolve-info'; 33 | import type { CreatedResolver, Filters, TableNamedRelations, TableSelectArgs } from './types'; 34 | 35 | const generateSelectArray = ( 36 | db: MySqlDatabase, 37 | tableName: string, 38 | tables: Record, 39 | relationMap: Record>, 40 | orderArgs: GraphQLInputObjectType, 41 | filterArgs: GraphQLInputObjectType, 42 | ): CreatedResolver => { 43 | const queryName = `${uncapitalize(tableName)}`; 44 | const queryBase = db.query[tableName as keyof typeof db.query] as unknown as 45 | | RelationalQueryBuilder 46 | | undefined; 47 | if (!queryBase) { 48 | throw new Error( 49 | `Drizzle-GraphQL Error: Table ${tableName} not found in drizzle instance. Did you forget to pass schema to drizzle constructor?`, 50 | ); 51 | } 52 | 53 | const queryArgs = { 54 | offset: { 55 | type: GraphQLInt, 56 | }, 57 | limit: { 58 | type: GraphQLInt, 59 | }, 60 | orderBy: { 61 | type: orderArgs, 62 | }, 63 | where: { 64 | type: filterArgs, 65 | }, 66 | } as GraphQLFieldConfigArgumentMap; 67 | 68 | const typeName = `${capitalize(tableName)}SelectItem`; 69 | const table = tables[tableName]!; 70 | 71 | return { 72 | name: queryName, 73 | resolver: async (source, args: Partial, context, info) => { 74 | try { 75 | const { offset, limit, orderBy, where } = args; 76 | 77 | const parsedInfo = parseResolveInfo(info, { 78 | deep: true, 79 | }) as ResolveTree; 80 | 81 | const query = queryBase.findMany({ 82 | columns: extractSelectedColumnsFromTree( 83 | parsedInfo.fieldsByTypeName[typeName]!, 84 | table, 85 | ), 86 | offset, 87 | limit, 88 | orderBy: orderBy ? extractOrderBy(table, orderBy) : undefined, 89 | where: where ? extractFilters(table, tableName, where) : undefined, 90 | with: relationMap[tableName] 91 | ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName) 92 | : undefined, 93 | }); 94 | 95 | const result = await query; 96 | 97 | return remapToGraphQLArrayOutput(result, tableName, table, relationMap); 98 | } catch (e) { 99 | if (typeof e === 'object' && typeof ( e).message === 'string') { 100 | throw new GraphQLError(( e).message); 101 | } 102 | 103 | throw e; 104 | } 105 | }, 106 | args: queryArgs, 107 | }; 108 | }; 109 | 110 | const generateSelectSingle = ( 111 | db: MySqlDatabase, 112 | tableName: string, 113 | tables: Record, 114 | relationMap: Record>, 115 | orderArgs: GraphQLInputObjectType, 116 | filterArgs: GraphQLInputObjectType, 117 | ): CreatedResolver => { 118 | const queryName = `${uncapitalize(tableName)}Single`; 119 | const queryBase = db.query[tableName as keyof typeof db.query] as unknown as 120 | | RelationalQueryBuilder 121 | | undefined; 122 | if (!queryBase) { 123 | throw new Error( 124 | `Drizzle-GraphQL Error: Table ${tableName} not found in drizzle instance. Did you forget to pass schema to drizzle constructor?`, 125 | ); 126 | } 127 | 128 | const queryArgs = { 129 | offset: { 130 | type: GraphQLInt, 131 | }, 132 | orderBy: { 133 | type: orderArgs, 134 | }, 135 | where: { 136 | type: filterArgs, 137 | }, 138 | } as GraphQLFieldConfigArgumentMap; 139 | 140 | const typeName = `${capitalize(tableName)}SelectItem`; 141 | const table = tables[tableName]!; 142 | 143 | return { 144 | name: queryName, 145 | resolver: async (source, args: Partial, context, info) => { 146 | try { 147 | const { offset, orderBy, where } = args; 148 | 149 | const parsedInfo = parseResolveInfo(info, { 150 | deep: true, 151 | }) as ResolveTree; 152 | 153 | const query = queryBase.findFirst({ 154 | columns: extractSelectedColumnsFromTree( 155 | parsedInfo.fieldsByTypeName[typeName]!, 156 | table, 157 | ), 158 | offset, 159 | orderBy: orderBy ? extractOrderBy(table, orderBy) : undefined, 160 | where: where ? extractFilters(table, tableName, where) : undefined, 161 | with: relationMap[tableName] 162 | ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName) 163 | : undefined, 164 | }); 165 | 166 | const result = await query; 167 | if (!result) return undefined; 168 | 169 | return remapToGraphQLSingleOutput(result, tableName, table, relationMap); 170 | } catch (e) { 171 | if (typeof e === 'object' && typeof ( e).message === 'string') { 172 | throw new GraphQLError(( e).message); 173 | } 174 | 175 | throw e; 176 | } 177 | }, 178 | args: queryArgs, 179 | }; 180 | }; 181 | 182 | const generateInsertArray = ( 183 | db: MySqlDatabase, 184 | tableName: string, 185 | table: MySqlTable, 186 | baseType: GraphQLInputObjectType, 187 | ): CreatedResolver => { 188 | const queryName = `insertInto${capitalize(tableName)}`; 189 | 190 | const queryArgs: GraphQLFieldConfigArgumentMap = { 191 | values: { 192 | type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(baseType))), 193 | }, 194 | }; 195 | 196 | return { 197 | name: queryName, 198 | resolver: async (source, args: { values: Record[] }, context, info) => { 199 | try { 200 | const input = remapFromGraphQLArrayInput(args.values, table); 201 | if (!input.length) throw new GraphQLError('No values were provided!'); 202 | 203 | await db.insert(table).values(input); 204 | 205 | return { isSuccess: true }; 206 | } catch (e) { 207 | if (typeof e === 'object' && typeof ( e).message === 'string') { 208 | throw new GraphQLError(( e).message); 209 | } 210 | 211 | throw e; 212 | } 213 | }, 214 | args: queryArgs, 215 | }; 216 | }; 217 | 218 | const generateInsertSingle = ( 219 | db: MySqlDatabase, 220 | tableName: string, 221 | table: MySqlTable, 222 | baseType: GraphQLInputObjectType, 223 | ): CreatedResolver => { 224 | const queryName = `insertInto${capitalize(tableName)}Single`; 225 | 226 | const queryArgs: GraphQLFieldConfigArgumentMap = { 227 | values: { 228 | type: new GraphQLNonNull(baseType), 229 | }, 230 | }; 231 | 232 | return { 233 | name: queryName, 234 | resolver: async (source, args: { values: Record }, context, info) => { 235 | try { 236 | const input = remapFromGraphQLSingleInput(args.values, table); 237 | 238 | await db.insert(table).values(input); 239 | 240 | return { isSuccess: true }; 241 | } catch (e) { 242 | if (typeof e === 'object' && typeof ( e).message === 'string') { 243 | throw new GraphQLError(( e).message); 244 | } 245 | 246 | throw e; 247 | } 248 | }, 249 | args: queryArgs, 250 | }; 251 | }; 252 | 253 | const generateUpdate = ( 254 | db: MySqlDatabase, 255 | tableName: string, 256 | table: MySqlTable, 257 | setArgs: GraphQLInputObjectType, 258 | filterArgs: GraphQLInputObjectType, 259 | ): CreatedResolver => { 260 | const queryName = `update${capitalize(tableName)}`; 261 | 262 | const queryArgs = { 263 | set: { 264 | type: new GraphQLNonNull(setArgs), 265 | }, 266 | where: { 267 | type: filterArgs, 268 | }, 269 | } as const satisfies GraphQLFieldConfigArgumentMap; 270 | 271 | return { 272 | name: queryName, 273 | resolver: async (source, args: { where?: Filters; set: Record }, context, info) => { 274 | try { 275 | const { where, set } = args; 276 | 277 | const input = remapFromGraphQLSingleInput(set, table); 278 | if (!Object.keys(input).length) throw new GraphQLError('Unable to update with no values specified!'); 279 | 280 | let query = db.update(table).set(input); 281 | if (where) { 282 | const filters = extractFilters(table, tableName, where); 283 | query = query.where(filters) as any; 284 | } 285 | 286 | await query; 287 | 288 | return { isSuccess: true }; 289 | } catch (e) { 290 | if (typeof e === 'object' && typeof ( e).message === 'string') { 291 | throw new GraphQLError(( e).message); 292 | } 293 | 294 | throw e; 295 | } 296 | }, 297 | args: queryArgs, 298 | }; 299 | }; 300 | 301 | const generateDelete = ( 302 | db: MySqlDatabase, 303 | tableName: string, 304 | table: MySqlTable, 305 | filterArgs: GraphQLInputObjectType, 306 | ): CreatedResolver => { 307 | const queryName = `deleteFrom${tableName}`; 308 | 309 | const queryArgs = { 310 | where: { 311 | type: filterArgs, 312 | }, 313 | } as const satisfies GraphQLFieldConfigArgumentMap; 314 | 315 | return { 316 | name: queryName, 317 | resolver: async (source, args: { where?: Filters
}, context, info) => { 318 | try { 319 | const { where } = args; 320 | 321 | let query = db.delete(table); 322 | if (where) { 323 | const filters = extractFilters(table, tableName, where); 324 | query = query.where(filters) as any; 325 | } 326 | 327 | await query; 328 | 329 | return { isSuccess: true }; 330 | } catch (e) { 331 | if (typeof e === 'object' && typeof ( e).message === 'string') { 332 | throw new GraphQLError(( e).message); 333 | } 334 | 335 | throw e; 336 | } 337 | }, 338 | args: queryArgs, 339 | }; 340 | }; 341 | 342 | export const generateSchemaData = < 343 | TDrizzleInstance extends MySqlDatabase, 344 | TSchema extends Record, 345 | >( 346 | db: TDrizzleInstance, 347 | schema: TSchema, 348 | relationsDepthLimit: number | undefined, 349 | ): GeneratedEntities => { 350 | const rawSchema = schema; 351 | const schemaEntries = Object.entries(rawSchema); 352 | 353 | const tableEntries = schemaEntries.filter(([key, value]) => is(value, MySqlTable)) as [string, MySqlTable][]; 354 | const tables = Object.fromEntries(tableEntries); 355 | 356 | if (!tableEntries.length) { 357 | throw new Error( 358 | "Drizzle-GraphQL Error: No tables detected in Drizzle-ORM's database instance. Did you forget to pass schema to drizzle constructor?", 359 | ); 360 | } 361 | 362 | const rawRelations = schemaEntries 363 | .filter(([key, value]) => is(value, Relations)) 364 | .map<[string, Relations]>(([key, value]) => [ 365 | tableEntries.find( 366 | ([tableName, tableValue]) => tableValue === (value as Relations).table, 367 | )![0] as string, 368 | value as Relations, 369 | ]).map<[string, Record]>(([tableName, relValue]) => [ 370 | tableName, 371 | relValue.config(createTableRelationsHelpers(tables[tableName]!)), 372 | ]); 373 | 374 | const namedRelations = Object.fromEntries( 375 | rawRelations 376 | .map(([relName, config]) => { 377 | const namedConfig: Record = Object.fromEntries( 378 | Object.entries(config).map(([innerRelName, innerRelValue]) => [innerRelName, { 379 | relation: innerRelValue, 380 | targetTableName: tableEntries.find(([tableName, tableValue]) => 381 | tableValue === innerRelValue.referencedTable 382 | )![0], 383 | }]), 384 | ); 385 | 386 | return [ 387 | relName, 388 | namedConfig, 389 | ]; 390 | }), 391 | ); 392 | 393 | const queries: ThunkObjMap> = {}; 394 | const mutations: ThunkObjMap> = {}; 395 | const gqlSchemaTypes = Object.fromEntries( 396 | Object.entries(tables).map(([tableName, table]) => [ 397 | tableName, 398 | generateTableTypes(tableName, tables, namedRelations, false, relationsDepthLimit), 399 | ]), 400 | ); 401 | 402 | const mutationReturnType = new GraphQLObjectType({ 403 | name: `MutationReturn`, 404 | fields: { 405 | isSuccess: { 406 | type: new GraphQLNonNull(GraphQLBoolean), 407 | }, 408 | }, 409 | }); 410 | 411 | const inputs: Record = {}; 412 | const outputs: Record = { 413 | MutationReturn: mutationReturnType, 414 | }; 415 | 416 | for (const [tableName, tableTypes] of Object.entries(gqlSchemaTypes)) { 417 | const { insertInput, updateInput, tableFilters, tableOrder } = tableTypes.inputs; 418 | const { selectSingleOutput, selectArrOutput } = tableTypes.outputs; 419 | 420 | const selectArrGenerated = generateSelectArray( 421 | db, 422 | tableName, 423 | tables, 424 | namedRelations, 425 | tableOrder, 426 | tableFilters, 427 | ); 428 | const selectSingleGenerated = generateSelectSingle( 429 | db, 430 | tableName, 431 | tables, 432 | namedRelations, 433 | tableOrder, 434 | tableFilters, 435 | ); 436 | const insertArrGenerated = generateInsertArray(db, tableName, schema[tableName] as MySqlTable, insertInput); 437 | const insertSingleGenerated = generateInsertSingle(db, tableName, schema[tableName] as MySqlTable, insertInput); 438 | const updateGenerated = generateUpdate( 439 | db, 440 | tableName, 441 | schema[tableName] as MySqlTable, 442 | updateInput, 443 | tableFilters, 444 | ); 445 | const deleteGenerated = generateDelete(db, tableName, schema[tableName] as MySqlTable, tableFilters); 446 | 447 | queries[selectArrGenerated.name] = { 448 | type: selectArrOutput, 449 | args: selectArrGenerated.args, 450 | resolve: selectArrGenerated.resolver, 451 | }; 452 | queries[selectSingleGenerated.name] = { 453 | type: selectSingleOutput, 454 | args: selectSingleGenerated.args, 455 | resolve: selectSingleGenerated.resolver, 456 | }; 457 | mutations[insertArrGenerated.name] = { 458 | type: mutationReturnType, 459 | args: insertArrGenerated.args, 460 | resolve: insertArrGenerated.resolver, 461 | }; 462 | mutations[insertSingleGenerated.name] = { 463 | type: mutationReturnType, 464 | args: insertSingleGenerated.args, 465 | resolve: insertSingleGenerated.resolver, 466 | }; 467 | mutations[updateGenerated.name] = { 468 | type: mutationReturnType, 469 | args: updateGenerated.args, 470 | resolve: updateGenerated.resolver, 471 | }; 472 | mutations[deleteGenerated.name] = { 473 | type: mutationReturnType, 474 | args: deleteGenerated.args, 475 | resolve: deleteGenerated.resolver, 476 | }; 477 | [insertInput, updateInput, tableFilters, tableOrder].forEach((e) => (inputs[e.name] = e)); 478 | outputs[selectSingleOutput.name] = selectSingleOutput; 479 | } 480 | 481 | return { queries, mutations, inputs, types: outputs } as any; 482 | }; 483 | -------------------------------------------------------------------------------- /src/util/builders/pg.ts: -------------------------------------------------------------------------------- 1 | import { createTableRelationsHelpers, is, Relation, Relations, Table } from 'drizzle-orm'; 2 | import { PgColumn, PgDatabase, PgTable } from 'drizzle-orm/pg-core'; 3 | import { 4 | GraphQLError, 5 | GraphQLInputObjectType, 6 | GraphQLInt, 7 | GraphQLList, 8 | GraphQLNonNull, 9 | GraphQLObjectType, 10 | } from 'graphql'; 11 | 12 | import { 13 | extractFilters, 14 | extractOrderBy, 15 | extractRelationsParams, 16 | extractSelectedColumnsFromTree, 17 | extractSelectedColumnsFromTreeSQLFormat, 18 | generateTableTypes, 19 | } from '@/util/builders/common'; 20 | import { capitalize, uncapitalize } from '@/util/case-ops'; 21 | import { 22 | remapFromGraphQLArrayInput, 23 | remapFromGraphQLSingleInput, 24 | remapToGraphQLArrayOutput, 25 | remapToGraphQLSingleOutput, 26 | } from '@/util/data-mappers'; 27 | import { parseResolveInfo } from 'graphql-parse-resolve-info'; 28 | 29 | import type { GeneratedEntities } from '@/types'; 30 | import type { RelationalQueryBuilder } from 'drizzle-orm/mysql-core/query-builders/query'; 31 | import type { GraphQLFieldConfig, GraphQLFieldConfigArgumentMap, ThunkObjMap } from 'graphql'; 32 | import type { ResolveTree } from 'graphql-parse-resolve-info'; 33 | import type { CreatedResolver, Filters, TableNamedRelations, TableSelectArgs } from './types'; 34 | 35 | const generateSelectArray = ( 36 | db: PgDatabase, 37 | tableName: string, 38 | tables: Record, 39 | relationMap: Record>, 40 | orderArgs: GraphQLInputObjectType, 41 | filterArgs: GraphQLInputObjectType, 42 | ): CreatedResolver => { 43 | const queryName = `${uncapitalize(tableName)}`; 44 | const queryBase = db.query[tableName as keyof typeof db.query] as unknown as 45 | | RelationalQueryBuilder 46 | | undefined; 47 | if (!queryBase) { 48 | throw new Error( 49 | `Drizzle-GraphQL Error: Table ${tableName} not found in drizzle instance. Did you forget to pass schema to drizzle constructor?`, 50 | ); 51 | } 52 | 53 | const queryArgs = { 54 | offset: { 55 | type: GraphQLInt, 56 | }, 57 | limit: { 58 | type: GraphQLInt, 59 | }, 60 | orderBy: { 61 | type: orderArgs, 62 | }, 63 | where: { 64 | type: filterArgs, 65 | }, 66 | } as GraphQLFieldConfigArgumentMap; 67 | 68 | const typeName = `${capitalize(tableName)}SelectItem`; 69 | const table = tables[tableName]!; 70 | 71 | return { 72 | name: queryName, 73 | resolver: async (source, args: Partial, context, info) => { 74 | try { 75 | const { offset, limit, orderBy, where } = args; 76 | 77 | const parsedInfo = parseResolveInfo(info, { 78 | deep: true, 79 | }) as ResolveTree; 80 | 81 | const query = queryBase.findMany({ 82 | columns: extractSelectedColumnsFromTree( 83 | parsedInfo.fieldsByTypeName[typeName]!, 84 | table, 85 | ), /*extractSelectedColumnsFromNode(tableSelection, info.fragments, table) */ 86 | offset, 87 | limit, 88 | orderBy: orderBy ? extractOrderBy(table, orderBy) : undefined, 89 | where: where ? extractFilters(table, tableName, where) : undefined, 90 | with: relationMap[tableName] 91 | ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName) 92 | : undefined, 93 | }); 94 | 95 | const result = await query; 96 | 97 | return remapToGraphQLArrayOutput(result, tableName, table, relationMap); 98 | } catch (e) { 99 | if (typeof e === 'object' && typeof ( e).message === 'string') { 100 | throw new GraphQLError(( e).message); 101 | } 102 | 103 | throw e; 104 | } 105 | }, 106 | args: queryArgs, 107 | }; 108 | }; 109 | 110 | const generateSelectSingle = ( 111 | db: PgDatabase, 112 | tableName: string, 113 | tables: Record, 114 | relationMap: Record>, 115 | orderArgs: GraphQLInputObjectType, 116 | filterArgs: GraphQLInputObjectType, 117 | ): CreatedResolver => { 118 | const queryName = `${uncapitalize(tableName)}Single`; 119 | const queryBase = db.query[tableName as keyof typeof db.query] as unknown as 120 | | RelationalQueryBuilder 121 | | undefined; 122 | if (!queryBase) { 123 | throw new Error( 124 | `Drizzle-GraphQL Error: Table ${tableName} not found in drizzle instance. Did you forget to pass schema to drizzle constructor?`, 125 | ); 126 | } 127 | 128 | const queryArgs = { 129 | offset: { 130 | type: GraphQLInt, 131 | }, 132 | orderBy: { 133 | type: orderArgs, 134 | }, 135 | where: { 136 | type: filterArgs, 137 | }, 138 | } as GraphQLFieldConfigArgumentMap; 139 | 140 | const typeName = `${capitalize(tableName)}SelectItem`; 141 | const table = tables[tableName]!; 142 | 143 | return { 144 | name: queryName, 145 | resolver: async (source, args: Partial, context, info) => { 146 | try { 147 | const { offset, orderBy, where } = args; 148 | 149 | const parsedInfo = parseResolveInfo(info, { 150 | deep: true, 151 | }) as ResolveTree; 152 | 153 | const query = queryBase.findFirst({ 154 | columns: extractSelectedColumnsFromTree( 155 | parsedInfo.fieldsByTypeName[typeName]!, 156 | table, 157 | ), 158 | offset, 159 | orderBy: orderBy ? extractOrderBy(table, orderBy) : undefined, 160 | where: where ? extractFilters(table, tableName, where) : undefined, 161 | with: relationMap[tableName] 162 | ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName) 163 | : undefined, 164 | }); 165 | 166 | const result = await query; 167 | if (!result) return undefined; 168 | 169 | return remapToGraphQLSingleOutput(result, tableName, table, relationMap); 170 | } catch (e) { 171 | if (typeof e === 'object' && typeof ( e).message === 'string') { 172 | throw new GraphQLError(( e).message); 173 | } 174 | 175 | throw e; 176 | } 177 | }, 178 | args: queryArgs, 179 | }; 180 | }; 181 | 182 | const generateInsertArray = ( 183 | db: PgDatabase, 184 | tableName: string, 185 | table: PgTable, 186 | baseType: GraphQLInputObjectType, 187 | ): CreatedResolver => { 188 | const queryName = `insertInto${capitalize(tableName)}`; 189 | const typeName = `${capitalize(tableName)}Item`; 190 | 191 | const queryArgs: GraphQLFieldConfigArgumentMap = { 192 | values: { 193 | type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(baseType))), 194 | }, 195 | }; 196 | 197 | return { 198 | name: queryName, 199 | resolver: async (source, args: { values: Record[] }, context, info) => { 200 | try { 201 | const input = remapFromGraphQLArrayInput(args.values, table); 202 | if (!input.length) throw new GraphQLError('No values were provided!'); 203 | 204 | const parsedInfo = parseResolveInfo(info, { 205 | deep: true, 206 | }) as ResolveTree; 207 | 208 | const columns = extractSelectedColumnsFromTreeSQLFormat( 209 | parsedInfo.fieldsByTypeName[typeName]!, 210 | table, 211 | ); 212 | 213 | const result = await db.insert(table).values(input).returning(columns) 214 | .onConflictDoNothing(); 215 | 216 | return remapToGraphQLArrayOutput(result, tableName, table); 217 | } catch (e) { 218 | if (typeof e === 'object' && typeof ( e).message === 'string') { 219 | throw new GraphQLError(( e).message); 220 | } 221 | 222 | throw e; 223 | } 224 | }, 225 | args: queryArgs, 226 | }; 227 | }; 228 | 229 | const generateInsertSingle = ( 230 | db: PgDatabase, 231 | tableName: string, 232 | table: PgTable, 233 | baseType: GraphQLInputObjectType, 234 | ): CreatedResolver => { 235 | const queryName = `insertInto${capitalize(tableName)}Single`; 236 | const typeName = `${capitalize(tableName)}Item`; 237 | 238 | const queryArgs: GraphQLFieldConfigArgumentMap = { 239 | values: { 240 | type: new GraphQLNonNull(baseType), 241 | }, 242 | }; 243 | 244 | return { 245 | name: queryName, 246 | resolver: async (source, args: { values: Record }, context, info) => { 247 | try { 248 | const input = remapFromGraphQLSingleInput(args.values, table); 249 | 250 | const parsedInfo = parseResolveInfo(info, { 251 | deep: true, 252 | }) as ResolveTree; 253 | 254 | const columns = extractSelectedColumnsFromTreeSQLFormat( 255 | parsedInfo.fieldsByTypeName[typeName]!, 256 | table, 257 | ); 258 | 259 | const result = await db.insert(table).values(input).returning(columns) 260 | .onConflictDoNothing(); 261 | 262 | if (!result[0]) return undefined; 263 | 264 | return remapToGraphQLSingleOutput(result[0], tableName, table); 265 | } catch (e) { 266 | if (typeof e === 'object' && typeof ( e).message === 'string') { 267 | throw new GraphQLError(( e).message); 268 | } 269 | 270 | throw e; 271 | } 272 | }, 273 | args: queryArgs, 274 | }; 275 | }; 276 | 277 | const generateUpdate = ( 278 | db: PgDatabase, 279 | tableName: string, 280 | table: PgTable, 281 | setArgs: GraphQLInputObjectType, 282 | filterArgs: GraphQLInputObjectType, 283 | ): CreatedResolver => { 284 | const queryName = `update${capitalize(tableName)}`; 285 | const typeName = `${capitalize(tableName)}Item`; 286 | 287 | const queryArgs = { 288 | set: { 289 | type: new GraphQLNonNull(setArgs), 290 | }, 291 | where: { 292 | type: filterArgs, 293 | }, 294 | } as const satisfies GraphQLFieldConfigArgumentMap; 295 | 296 | return { 297 | name: queryName, 298 | resolver: async (source, args: { where?: Filters
; set: Record }, context, info) => { 299 | try { 300 | const { where, set } = args; 301 | 302 | const parsedInfo = parseResolveInfo(info, { 303 | deep: true, 304 | }) as ResolveTree; 305 | 306 | const columns = extractSelectedColumnsFromTreeSQLFormat( 307 | parsedInfo.fieldsByTypeName[typeName]!, 308 | table, 309 | ); 310 | 311 | const input = remapFromGraphQLSingleInput(set, table); 312 | if (!Object.keys(input).length) throw new GraphQLError('Unable to update with no values specified!'); 313 | 314 | let query = db.update(table).set(input); 315 | if (where) { 316 | const filters = extractFilters(table, tableName, where); 317 | query = query.where(filters) as any; 318 | } 319 | 320 | query = query.returning(columns) as any; 321 | 322 | const result = await query; 323 | 324 | return remapToGraphQLArrayOutput(result, tableName, table); 325 | } catch (e) { 326 | if (typeof e === 'object' && typeof ( e).message === 'string') { 327 | throw new GraphQLError(( e).message); 328 | } 329 | 330 | throw e; 331 | } 332 | }, 333 | args: queryArgs, 334 | }; 335 | }; 336 | 337 | const generateDelete = ( 338 | db: PgDatabase, 339 | tableName: string, 340 | table: PgTable, 341 | filterArgs: GraphQLInputObjectType, 342 | ): CreatedResolver => { 343 | const queryName = `deleteFrom${capitalize(tableName)}`; 344 | const typeName = `${capitalize(tableName)}Item`; 345 | 346 | const queryArgs = { 347 | where: { 348 | type: filterArgs, 349 | }, 350 | } as const satisfies GraphQLFieldConfigArgumentMap; 351 | 352 | return { 353 | name: queryName, 354 | resolver: async (source, args: { where?: Filters
}, context, info) => { 355 | try { 356 | const { where } = args; 357 | 358 | const parsedInfo = parseResolveInfo(info, { 359 | deep: true, 360 | }) as ResolveTree; 361 | 362 | const columns = extractSelectedColumnsFromTreeSQLFormat( 363 | parsedInfo.fieldsByTypeName[typeName]!, 364 | table, 365 | ); 366 | 367 | let query = db.delete(table); 368 | if (where) { 369 | const filters = extractFilters(table, tableName, where); 370 | query = query.where(filters) as any; 371 | } 372 | 373 | query = query.returning(columns) as any; 374 | 375 | const result = await query; 376 | 377 | return remapToGraphQLArrayOutput(result, tableName, table); 378 | } catch (e) { 379 | if (typeof e === 'object' && typeof ( e).message === 'string') { 380 | throw new GraphQLError(( e).message); 381 | } 382 | 383 | throw e; 384 | } 385 | }, 386 | args: queryArgs, 387 | }; 388 | }; 389 | 390 | export const generateSchemaData = < 391 | TDrizzleInstance extends PgDatabase, 392 | TSchema extends Record, 393 | >( 394 | db: TDrizzleInstance, 395 | schema: TSchema, 396 | relationsDepthLimit: number | undefined, 397 | ): GeneratedEntities => { 398 | const rawSchema = schema; 399 | const schemaEntries = Object.entries(rawSchema); 400 | 401 | const tableEntries = schemaEntries.filter(([key, value]) => is(value, PgTable)) as [string, PgTable][]; 402 | const tables = Object.fromEntries(tableEntries) as Record< 403 | string, 404 | PgTable 405 | >; 406 | 407 | if (!tableEntries.length) { 408 | throw new Error( 409 | "Drizzle-GraphQL Error: No tables detected in Drizzle-ORM's database instance. Did you forget to pass schema to drizzle constructor?", 410 | ); 411 | } 412 | 413 | const rawRelations = schemaEntries 414 | .filter(([key, value]) => is(value, Relations)) 415 | .map<[string, Relations]>(([key, value]) => [ 416 | tableEntries.find( 417 | ([tableName, tableValue]) => tableValue === (value as Relations).table, 418 | )![0] as string, 419 | value as Relations, 420 | ]).map<[string, Record]>(([tableName, relValue]) => [ 421 | tableName, 422 | relValue.config(createTableRelationsHelpers(tables[tableName]!)), 423 | ]); 424 | 425 | const namedRelations = Object.fromEntries( 426 | rawRelations 427 | .map(([relName, config]) => { 428 | const namedConfig: Record = Object.fromEntries( 429 | Object.entries(config).map(([innerRelName, innerRelValue]) => [innerRelName, { 430 | relation: innerRelValue, 431 | targetTableName: tableEntries.find(([tableName, tableValue]) => 432 | tableValue === innerRelValue.referencedTable 433 | )![0], 434 | }]), 435 | ); 436 | 437 | return [ 438 | relName, 439 | namedConfig, 440 | ]; 441 | }), 442 | ); 443 | 444 | const queries: ThunkObjMap> = {}; 445 | const mutations: ThunkObjMap> = {}; 446 | const gqlSchemaTypes = Object.fromEntries( 447 | Object.entries(tables).map(([tableName, table]) => [ 448 | tableName, 449 | generateTableTypes(tableName, tables, namedRelations, true, relationsDepthLimit), 450 | ]), 451 | ); 452 | 453 | const inputs: Record = {}; 454 | const outputs: Record = {}; 455 | 456 | for (const [tableName, tableTypes] of Object.entries(gqlSchemaTypes)) { 457 | const { insertInput, updateInput, tableFilters, tableOrder } = tableTypes.inputs; 458 | const { selectSingleOutput, selectArrOutput, singleTableItemOutput, arrTableItemOutput } = tableTypes.outputs; 459 | 460 | const selectArrGenerated = generateSelectArray( 461 | db, 462 | tableName, 463 | tables, 464 | namedRelations, 465 | tableOrder, 466 | tableFilters, 467 | ); 468 | const selectSingleGenerated = generateSelectSingle( 469 | db, 470 | tableName, 471 | tables, 472 | namedRelations, 473 | tableOrder, 474 | tableFilters, 475 | ); 476 | const insertArrGenerated = generateInsertArray(db, tableName, schema[tableName] as PgTable, insertInput); 477 | const insertSingleGenerated = generateInsertSingle(db, tableName, schema[tableName] as PgTable, insertInput); 478 | const updateGenerated = generateUpdate(db, tableName, schema[tableName] as PgTable, updateInput, tableFilters); 479 | const deleteGenerated = generateDelete(db, tableName, schema[tableName] as PgTable, tableFilters); 480 | 481 | queries[selectArrGenerated.name] = { 482 | type: selectArrOutput, 483 | args: selectArrGenerated.args, 484 | resolve: selectArrGenerated.resolver, 485 | }; 486 | queries[selectSingleGenerated.name] = { 487 | type: selectSingleOutput, 488 | args: selectSingleGenerated.args, 489 | resolve: selectSingleGenerated.resolver, 490 | }; 491 | mutations[insertArrGenerated.name] = { 492 | type: arrTableItemOutput, 493 | args: insertArrGenerated.args, 494 | resolve: insertArrGenerated.resolver, 495 | }; 496 | mutations[insertSingleGenerated.name] = { 497 | type: singleTableItemOutput, 498 | args: insertSingleGenerated.args, 499 | resolve: insertSingleGenerated.resolver, 500 | }; 501 | mutations[updateGenerated.name] = { 502 | type: arrTableItemOutput, 503 | args: updateGenerated.args, 504 | resolve: updateGenerated.resolver, 505 | }; 506 | mutations[deleteGenerated.name] = { 507 | type: arrTableItemOutput, 508 | args: deleteGenerated.args, 509 | resolve: deleteGenerated.resolver, 510 | }; 511 | [insertInput, updateInput, tableFilters, tableOrder].forEach((e) => (inputs[e.name] = e)); 512 | outputs[selectSingleOutput.name] = selectSingleOutput; 513 | outputs[singleTableItemOutput.name] = singleTableItemOutput; 514 | } 515 | 516 | return { queries, mutations, inputs, types: outputs } as any; 517 | }; 518 | -------------------------------------------------------------------------------- /src/util/builders/sqlite.ts: -------------------------------------------------------------------------------- 1 | import { createTableRelationsHelpers, is, Relation, Relations, Table } from 'drizzle-orm'; 2 | import { BaseSQLiteDatabase, SQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'; 3 | import { 4 | GraphQLError, 5 | GraphQLInputObjectType, 6 | GraphQLInt, 7 | GraphQLList, 8 | GraphQLNonNull, 9 | GraphQLObjectType, 10 | } from 'graphql'; 11 | 12 | import { 13 | extractFilters, 14 | extractOrderBy, 15 | extractRelationsParams, 16 | extractSelectedColumnsFromTree, 17 | extractSelectedColumnsFromTreeSQLFormat, 18 | generateTableTypes, 19 | } from '@/util/builders/common'; 20 | import { capitalize, uncapitalize } from '@/util/case-ops'; 21 | import { 22 | remapFromGraphQLArrayInput, 23 | remapFromGraphQLSingleInput, 24 | remapToGraphQLArrayOutput, 25 | remapToGraphQLSingleOutput, 26 | } from '@/util/data-mappers'; 27 | import { parseResolveInfo } from 'graphql-parse-resolve-info'; 28 | 29 | import type { GeneratedEntities } from '@/types'; 30 | import type { RelationalQueryBuilder } from 'drizzle-orm/mysql-core/query-builders/query'; 31 | import type { GraphQLFieldConfig, GraphQLFieldConfigArgumentMap, ThunkObjMap } from 'graphql'; 32 | import type { ResolveTree } from 'graphql-parse-resolve-info'; 33 | import type { CreatedResolver, Filters, TableNamedRelations, TableSelectArgs } from './types'; 34 | 35 | const generateSelectArray = ( 36 | db: BaseSQLiteDatabase, 37 | tableName: string, 38 | tables: Record, 39 | relationMap: Record>, 40 | orderArgs: GraphQLInputObjectType, 41 | filterArgs: GraphQLInputObjectType, 42 | ): CreatedResolver => { 43 | const queryName = `${uncapitalize(tableName)}`; 44 | const queryBase = db.query[tableName as keyof typeof db.query] as unknown as 45 | | RelationalQueryBuilder 46 | | undefined; 47 | if (!queryBase) { 48 | throw new Error( 49 | `Drizzle-GraphQL Error: Table ${tableName} not found in drizzle instance. Did you forget to pass schema to drizzle constructor?`, 50 | ); 51 | } 52 | 53 | const queryArgs = { 54 | offset: { 55 | type: GraphQLInt, 56 | }, 57 | limit: { 58 | type: GraphQLInt, 59 | }, 60 | orderBy: { 61 | type: orderArgs, 62 | }, 63 | where: { 64 | type: filterArgs, 65 | }, 66 | } as GraphQLFieldConfigArgumentMap; 67 | 68 | const typeName = `${capitalize(tableName)}SelectItem`; 69 | const table = tables[tableName]!; 70 | 71 | return { 72 | name: queryName, 73 | resolver: async (source, args: Partial, context, info) => { 74 | try { 75 | const { offset, limit, orderBy, where } = args; 76 | 77 | const parsedInfo = parseResolveInfo(info, { 78 | deep: true, 79 | }) as ResolveTree; 80 | 81 | const query = queryBase.findMany({ 82 | columns: extractSelectedColumnsFromTree( 83 | parsedInfo.fieldsByTypeName[typeName]!, 84 | table, 85 | ), 86 | offset, 87 | limit, 88 | orderBy: orderBy ? extractOrderBy(table, orderBy) : undefined, 89 | where: where ? extractFilters(table, tableName, where) : undefined, 90 | with: relationMap[tableName] 91 | ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName) 92 | : undefined, 93 | }); 94 | 95 | const result = await query; 96 | 97 | return remapToGraphQLArrayOutput(result, tableName, table, relationMap); 98 | } catch (e) { 99 | if (typeof e === 'object' && typeof ( e).message === 'string') { 100 | throw new GraphQLError(( e).message); 101 | } 102 | 103 | throw e; 104 | } 105 | }, 106 | args: queryArgs, 107 | }; 108 | }; 109 | 110 | const generateSelectSingle = ( 111 | db: BaseSQLiteDatabase, 112 | tableName: string, 113 | tables: Record, 114 | relationMap: Record>, 115 | orderArgs: GraphQLInputObjectType, 116 | filterArgs: GraphQLInputObjectType, 117 | ): CreatedResolver => { 118 | const queryName = `${uncapitalize(tableName)}Single`; 119 | const queryBase = db.query[tableName as keyof typeof db.query] as unknown as 120 | | RelationalQueryBuilder 121 | | undefined; 122 | if (!queryBase) { 123 | throw new Error( 124 | `Drizzle-GraphQL Error: Table ${tableName} not found in drizzle instance. Did you forget to pass schema to drizzle constructor?`, 125 | ); 126 | } 127 | 128 | const queryArgs = { 129 | offset: { 130 | type: GraphQLInt, 131 | }, 132 | orderBy: { 133 | type: orderArgs, 134 | }, 135 | where: { 136 | type: filterArgs, 137 | }, 138 | } as GraphQLFieldConfigArgumentMap; 139 | 140 | const typeName = `${capitalize(tableName)}SelectItem`; 141 | const table = tables[tableName]!; 142 | 143 | return { 144 | name: queryName, 145 | resolver: async (source, args: Partial, context, info) => { 146 | try { 147 | const { offset, orderBy, where } = args; 148 | 149 | const parsedInfo = parseResolveInfo(info, { 150 | deep: true, 151 | }) as ResolveTree; 152 | 153 | const query = queryBase.findFirst({ 154 | columns: extractSelectedColumnsFromTree( 155 | parsedInfo.fieldsByTypeName[typeName]!, 156 | table, 157 | ), 158 | offset, 159 | orderBy: orderBy ? extractOrderBy(table, orderBy) : undefined, 160 | where: where ? extractFilters(table, tableName, where) : undefined, 161 | with: relationMap[tableName] 162 | ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName) 163 | : undefined, 164 | }); 165 | 166 | const result = await query; 167 | if (!result) return undefined; 168 | 169 | return remapToGraphQLSingleOutput(result, tableName, table, relationMap); 170 | } catch (e) { 171 | if (typeof e === 'object' && typeof ( e).message === 'string') { 172 | throw new GraphQLError(( e).message); 173 | } 174 | 175 | throw e; 176 | } 177 | }, 178 | args: queryArgs, 179 | }; 180 | }; 181 | 182 | const generateInsertArray = ( 183 | db: BaseSQLiteDatabase, 184 | tableName: string, 185 | table: SQLiteTable, 186 | baseType: GraphQLInputObjectType, 187 | ): CreatedResolver => { 188 | const queryName = `insertInto${capitalize(tableName)}`; 189 | const typeName = `${capitalize(tableName)}Item`; 190 | 191 | const queryArgs: GraphQLFieldConfigArgumentMap = { 192 | values: { 193 | type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(baseType))), 194 | }, 195 | }; 196 | 197 | return { 198 | name: queryName, 199 | resolver: async (source, args: { values: Record[] }, context, info) => { 200 | try { 201 | const input = remapFromGraphQLArrayInput(args.values, table); 202 | if (!input.length) throw new GraphQLError('No values were provided!'); 203 | 204 | const parsedInfo = parseResolveInfo(info, { 205 | deep: true, 206 | }) as ResolveTree; 207 | 208 | const columns = extractSelectedColumnsFromTreeSQLFormat( 209 | parsedInfo.fieldsByTypeName[typeName]!, 210 | table, 211 | ); 212 | 213 | const result = await db 214 | .insert(table) 215 | .values(input) 216 | .returning(columns) 217 | .onConflictDoNothing(); 218 | 219 | return remapToGraphQLArrayOutput(result, tableName, table); 220 | } catch (e) { 221 | if (typeof e === 'object' && typeof ( e).message === 'string') { 222 | throw new GraphQLError(( e).message); 223 | } 224 | 225 | throw e; 226 | } 227 | }, 228 | args: queryArgs, 229 | }; 230 | }; 231 | 232 | const generateInsertSingle = ( 233 | db: BaseSQLiteDatabase, 234 | tableName: string, 235 | table: SQLiteTable, 236 | baseType: GraphQLInputObjectType, 237 | ): CreatedResolver => { 238 | const queryName = `insertInto${capitalize(tableName)}Single`; 239 | const typeName = `${capitalize(tableName)}Item`; 240 | 241 | const queryArgs: GraphQLFieldConfigArgumentMap = { 242 | values: { 243 | type: new GraphQLNonNull(baseType), 244 | }, 245 | }; 246 | 247 | return { 248 | name: queryName, 249 | resolver: async (source, args: { values: Record }, context, info) => { 250 | try { 251 | const input = remapFromGraphQLSingleInput(args.values, table); 252 | 253 | const parsedInfo = parseResolveInfo(info, { 254 | deep: true, 255 | }) as ResolveTree; 256 | 257 | const columns = extractSelectedColumnsFromTreeSQLFormat( 258 | parsedInfo.fieldsByTypeName[typeName]!, 259 | table, 260 | ); 261 | const result = await db.insert(table).values(input).returning(columns).onConflictDoNothing(); 262 | 263 | if (!result[0]) return undefined; 264 | 265 | return remapToGraphQLSingleOutput(result[0], tableName, table); 266 | } catch (e) { 267 | if (typeof e === 'object' && typeof ( e).message === 'string') { 268 | throw new GraphQLError(( e).message); 269 | } 270 | 271 | throw e; 272 | } 273 | }, 274 | args: queryArgs, 275 | }; 276 | }; 277 | 278 | const generateUpdate = ( 279 | db: BaseSQLiteDatabase, 280 | tableName: string, 281 | table: SQLiteTable, 282 | setArgs: GraphQLInputObjectType, 283 | filterArgs: GraphQLInputObjectType, 284 | ): CreatedResolver => { 285 | const queryName = `update${capitalize(tableName)}`; 286 | const typeName = `${capitalize(tableName)}Item`; 287 | 288 | const queryArgs = { 289 | set: { 290 | type: new GraphQLNonNull(setArgs), 291 | }, 292 | where: { 293 | type: filterArgs, 294 | }, 295 | } as const satisfies GraphQLFieldConfigArgumentMap; 296 | 297 | return { 298 | name: queryName, 299 | resolver: async (source, args: { where?: Filters
; set: Record }, context, info) => { 300 | try { 301 | const { where, set } = args; 302 | 303 | const parsedInfo = parseResolveInfo(info, { 304 | deep: true, 305 | }) as ResolveTree; 306 | 307 | const columns = extractSelectedColumnsFromTreeSQLFormat( 308 | parsedInfo.fieldsByTypeName[typeName]!, 309 | table, 310 | ); 311 | 312 | const input = remapFromGraphQLSingleInput(set, table); 313 | if (!Object.keys(input).length) throw new GraphQLError('Unable to update with no values specified!'); 314 | 315 | let query = db.update(table).set(input); 316 | if (where) { 317 | const filters = extractFilters(table, tableName, where); 318 | query = query.where(filters) as any; 319 | } 320 | 321 | query = query.returning(columns) as any; 322 | 323 | const result = await query; 324 | 325 | return remapToGraphQLArrayOutput(result, tableName, table); 326 | } catch (e) { 327 | if (typeof e === 'object' && typeof ( e).message === 'string') { 328 | throw new GraphQLError(( e).message); 329 | } 330 | 331 | throw e; 332 | } 333 | }, 334 | args: queryArgs, 335 | }; 336 | }; 337 | 338 | const generateDelete = ( 339 | db: BaseSQLiteDatabase, 340 | tableName: string, 341 | table: SQLiteTable, 342 | filterArgs: GraphQLInputObjectType, 343 | ): CreatedResolver => { 344 | const queryName = `deleteFrom${capitalize(tableName)}`; 345 | const typeName = `${capitalize(tableName)}Item`; 346 | 347 | const queryArgs = { 348 | where: { 349 | type: filterArgs, 350 | }, 351 | } as const satisfies GraphQLFieldConfigArgumentMap; 352 | 353 | return { 354 | name: queryName, 355 | resolver: async (source, args: { where?: Filters
}, context, info) => { 356 | try { 357 | const { where } = args; 358 | 359 | const parsedInfo = parseResolveInfo(info, { 360 | deep: true, 361 | }) as ResolveTree; 362 | 363 | const columns = extractSelectedColumnsFromTreeSQLFormat( 364 | parsedInfo.fieldsByTypeName[typeName]!, 365 | table, 366 | ); 367 | 368 | let query = db.delete(table); 369 | if (where) { 370 | const filters = extractFilters(table, tableName, where); 371 | query = query.where(filters) as any; 372 | } 373 | 374 | query = query.returning(columns) as any; 375 | 376 | const result = await query; 377 | 378 | return remapToGraphQLArrayOutput(result, tableName, table); 379 | } catch (e) { 380 | if (typeof e === 'object' && typeof ( e).message === 'string') { 381 | throw new GraphQLError(( e).message); 382 | } 383 | 384 | throw e; 385 | } 386 | }, 387 | args: queryArgs, 388 | }; 389 | }; 390 | 391 | export const generateSchemaData = < 392 | TDrizzleInstance extends BaseSQLiteDatabase, 393 | TSchema extends Record, 394 | >( 395 | db: TDrizzleInstance, 396 | schema: TSchema, 397 | relationsDepthLimit: number | undefined, 398 | ): GeneratedEntities => { 399 | const rawSchema = schema; 400 | const schemaEntries = Object.entries(rawSchema); 401 | 402 | const tableEntries = schemaEntries.filter(([key, value]) => is(value, SQLiteTable)) as [string, SQLiteTable][]; 403 | const tables = Object.fromEntries(tableEntries) as Record< 404 | string, 405 | SQLiteTable 406 | >; 407 | 408 | if (!tableEntries.length) { 409 | throw new Error( 410 | "Drizzle-GraphQL Error: No tables detected in Drizzle-ORM's database instance. Did you forget to pass schema to drizzle constructor?", 411 | ); 412 | } 413 | 414 | const rawRelations = schemaEntries 415 | .filter(([key, value]) => is(value, Relations)) 416 | .map<[string, Relations]>(([key, value]) => [ 417 | tableEntries.find( 418 | ([tableName, tableValue]) => tableValue === (value as Relations).table, 419 | )![0] as string, 420 | value as Relations, 421 | ]).map<[string, Record]>(([tableName, relValue]) => [ 422 | tableName, 423 | relValue.config(createTableRelationsHelpers(tables[tableName]!)), 424 | ]); 425 | 426 | const namedRelations = Object.fromEntries( 427 | rawRelations 428 | .map(([relName, config]) => { 429 | const namedConfig: Record = Object.fromEntries( 430 | Object.entries(config).map(([innerRelName, innerRelValue]) => [innerRelName, { 431 | relation: innerRelValue, 432 | targetTableName: tableEntries.find(([tableName, tableValue]) => 433 | tableValue === innerRelValue.referencedTable 434 | )![0], 435 | }]), 436 | ); 437 | 438 | return [ 439 | relName, 440 | namedConfig, 441 | ]; 442 | }), 443 | ); 444 | 445 | const queries: ThunkObjMap> = {}; 446 | const mutations: ThunkObjMap> = {}; 447 | const gqlSchemaTypes = Object.fromEntries( 448 | Object.entries(tables).map(([tableName, table]) => [ 449 | tableName, 450 | generateTableTypes(tableName, tables, namedRelations, true, relationsDepthLimit), 451 | ]), 452 | ); 453 | 454 | const inputs: Record = {}; 455 | const outputs: Record = {}; 456 | 457 | for (const [tableName, tableTypes] of Object.entries(gqlSchemaTypes)) { 458 | const { insertInput, updateInput, tableFilters, tableOrder } = tableTypes.inputs; 459 | const { selectSingleOutput, selectArrOutput, singleTableItemOutput, arrTableItemOutput } = tableTypes.outputs; 460 | 461 | const selectArrGenerated = generateSelectArray( 462 | db, 463 | tableName, 464 | tables, 465 | namedRelations, 466 | tableOrder, 467 | tableFilters, 468 | ); 469 | const selectSingleGenerated = generateSelectSingle( 470 | db, 471 | tableName, 472 | tables, 473 | namedRelations, 474 | tableOrder, 475 | tableFilters, 476 | ); 477 | const insertArrGenerated = generateInsertArray(db, tableName, schema[tableName] as SQLiteTable, insertInput); 478 | const insertSingleGenerated = generateInsertSingle(db, tableName, schema[tableName] as SQLiteTable, insertInput); 479 | const updateGenerated = generateUpdate( 480 | db, 481 | tableName, 482 | schema[tableName] as SQLiteTable, 483 | updateInput, 484 | tableFilters, 485 | ); 486 | const deleteGenerated = generateDelete(db, tableName, schema[tableName] as SQLiteTable, tableFilters); 487 | 488 | queries[selectArrGenerated.name] = { 489 | type: selectArrOutput, 490 | args: selectArrGenerated.args, 491 | resolve: selectArrGenerated.resolver, 492 | }; 493 | queries[selectSingleGenerated.name] = { 494 | type: selectSingleOutput, 495 | args: selectSingleGenerated.args, 496 | resolve: selectSingleGenerated.resolver, 497 | }; 498 | mutations[insertArrGenerated.name] = { 499 | type: arrTableItemOutput, 500 | args: insertArrGenerated.args, 501 | resolve: insertArrGenerated.resolver, 502 | }; 503 | mutations[insertSingleGenerated.name] = { 504 | type: singleTableItemOutput, 505 | args: insertSingleGenerated.args, 506 | resolve: insertSingleGenerated.resolver, 507 | }; 508 | mutations[updateGenerated.name] = { 509 | type: arrTableItemOutput, 510 | args: updateGenerated.args, 511 | resolve: updateGenerated.resolver, 512 | }; 513 | mutations[deleteGenerated.name] = { 514 | type: arrTableItemOutput, 515 | args: deleteGenerated.args, 516 | resolve: deleteGenerated.resolver, 517 | }; 518 | [insertInput, updateInput, tableFilters, tableOrder].forEach((e) => (inputs[e.name] = e)); 519 | outputs[selectSingleOutput.name] = selectSingleOutput; 520 | outputs[singleTableItemOutput.name] = singleTableItemOutput; 521 | } 522 | 523 | return { queries, mutations, inputs, types: outputs } as any; 524 | }; 525 | -------------------------------------------------------------------------------- /src/util/builders/types.ts: -------------------------------------------------------------------------------- 1 | import type { Column, Relation, SQL, Table } from 'drizzle-orm'; 2 | import type { PgArray } from 'drizzle-orm/pg-core'; 3 | import type { 4 | GraphQLFieldConfigArgumentMap, 5 | GraphQLFieldResolver, 6 | GraphQLInputObjectType, 7 | GraphQLList, 8 | GraphQLNonNull, 9 | GraphQLObjectType, 10 | GraphQLScalarType, 11 | } from 'graphql'; 12 | import type { ConvertedColumn, ConvertedRelationColumnWithArgs } from '../type-converter'; 13 | 14 | export type TableNamedRelations = { 15 | relation: Relation; 16 | targetTableName: string; 17 | }; 18 | 19 | export type TableSelectArgs = { 20 | offset: number; 21 | limit: number; 22 | where: Filters
; 23 | orderBy: OrderByArgs
; 24 | }; 25 | 26 | export type ProcessedTableSelectArgs = { 27 | columns: Record; 28 | offset: number; 29 | limit: number; 30 | where: SQL; 31 | orderBy: SQL[]; 32 | with?: Record>; 33 | }; 34 | 35 | export type SelectedColumnsRaw = [string, true][]; 36 | 37 | export type SelectedSQLColumns = [string, Column][]; 38 | 39 | export type SelectedColumns = { 40 | [columnName in keyof Table['_']['columns']]: true; 41 | }; 42 | 43 | export type CreatedResolver = { 44 | name: string; 45 | resolver: GraphQLFieldResolver; 46 | args: GraphQLFieldConfigArgumentMap; 47 | }; 48 | 49 | export type ArgMapToArgsType = { 50 | [Key in keyof TArgMap]?: TArgMap[Key] extends { type: GraphQLScalarType } ? R : never; 51 | }; 52 | 53 | export type ColTypeIsNull = TColumn['_']['notNull'] extends true ? TColType 54 | : TColType | null; 55 | 56 | export type ColTypeIsNullOrUndefined = TColumn['_']['notNull'] extends true ? TColType 57 | : TColType | null | undefined; 58 | 59 | export type ColTypeIsNullOrUndefinedWithDefault = TColumn['_']['notNull'] extends true 60 | ? TColumn['_']['hasDefault'] extends true ? TColType | null | undefined 61 | : TColumn['defaultFn'] extends undefined ? TColType 62 | : TColType | null | undefined 63 | : TColType | null | undefined; 64 | 65 | export type GetColumnGqlDataType = TColumn['dataType'] extends 'boolean' 66 | ? ColTypeIsNull 67 | : TColumn['dataType'] extends 'json' 68 | ? TColumn['_']['columnType'] extends 'PgGeometryObject' ? ColTypeIsNull 72 | : ColTypeIsNull 73 | : TColumn['dataType'] extends 'date' | 'string' | 'bigint' 74 | ? TColumn['enumValues'] extends [string, ...string[]] ? ColTypeIsNull 75 | : ColTypeIsNull 76 | : TColumn['dataType'] extends 'number' ? ColTypeIsNull 77 | : TColumn['dataType'] extends 'buffer' ? ColTypeIsNull 78 | : TColumn['dataType'] extends 'array' ? TColumn['columnType'] extends 'PgVector' ? ColTypeIsNull 79 | : TColumn['columnType'] extends 'PgGeometry' ? ColTypeIsNullOrUndefinedWithDefault 80 | : ColTypeIsNull< 81 | TColumn, 82 | Array< 83 | GetColumnGqlDataType extends 84 | infer InnerColType ? InnerColType extends null | undefined ? never 85 | : InnerColType 86 | : never 87 | > 88 | > 89 | : never; 90 | 91 | export type GetColumnGqlInsertDataType = TColumn['dataType'] extends 'boolean' 92 | ? ColTypeIsNullOrUndefinedWithDefault 93 | : TColumn['dataType'] extends 'json' 94 | ? TColumn['_']['columnType'] extends 'PgGeometryObject' ? ColTypeIsNullOrUndefinedWithDefault 98 | : ColTypeIsNullOrUndefinedWithDefault 99 | : TColumn['dataType'] extends 'date' | 'string' | 'bigint' 100 | ? TColumn['enumValues'] extends [string, ...string[]] 101 | ? ColTypeIsNullOrUndefinedWithDefault 102 | : ColTypeIsNullOrUndefinedWithDefault 103 | : TColumn['dataType'] extends 'number' ? ColTypeIsNullOrUndefinedWithDefault 104 | : TColumn['dataType'] extends 'buffer' ? ColTypeIsNullOrUndefinedWithDefault 105 | : TColumn['dataType'] extends 'array' 106 | ? TColumn['columnType'] extends 'PgVector' ? ColTypeIsNullOrUndefinedWithDefault 107 | : TColumn['columnType'] extends 'PgGeometry' ? ColTypeIsNullOrUndefinedWithDefault 108 | : ColTypeIsNullOrUndefinedWithDefault< 109 | TColumn, 110 | Array< 111 | GetColumnGqlDataType extends 112 | infer InnerColType ? InnerColType extends null | undefined ? never 113 | : InnerColType 114 | : never 115 | > 116 | > 117 | : never; 118 | 119 | export type GetColumnGqlUpdateDataType = TColumn['dataType'] extends 'boolean' 120 | ? boolean | null | undefined 121 | : TColumn['dataType'] extends 'json' ? TColumn['_']['columnType'] extends 'PgGeometryObject' ? 122 | | { 123 | x: number; 124 | y: number; 125 | } 126 | | null 127 | | undefined 128 | : string | null | undefined 129 | : TColumn['dataType'] extends 'date' | 'string' | 'bigint' 130 | ? TColumn['enumValues'] extends [string, ...string[]] ? TColumn['enumValues'][number] | null | undefined 131 | : string | null | undefined 132 | : TColumn['dataType'] extends 'number' ? number | null | undefined 133 | : TColumn['dataType'] extends 'buffer' ? number[] | null | undefined 134 | : TColumn['dataType'] extends 'array' ? TColumn['columnType'] extends 'PgVector' ? number[] | null | undefined 135 | : TColumn['columnType'] extends 'PgGeometry' ? [number, number] | null | undefined 136 | : 137 | | Array< 138 | GetColumnGqlDataType extends 139 | infer InnerColType ? InnerColType extends null | undefined ? never 140 | : InnerColType 141 | : never 142 | > 143 | | null 144 | | undefined 145 | : never; 146 | 147 | export type GetRemappedTableDataType< 148 | TTable extends Table, 149 | TColumns extends TTable['_']['columns'] = TTable['_']['columns'], 150 | > = { 151 | [K in keyof TColumns]: GetColumnGqlDataType; 152 | }; 153 | 154 | export type GetRemappedTableInsertDataType = { 155 | [K in keyof TTable['_']['columns']]: GetColumnGqlInsertDataType; 156 | }; 157 | 158 | export type GetRemappedTableUpdateDataType = { 159 | [K in keyof TTable['_']['columns']]: GetColumnGqlUpdateDataType; 160 | }; 161 | 162 | export type FilterColumnOperatorsCore> = Partial<{ 163 | eq: TColType; 164 | ne: TColType; 165 | lt: TColType; 166 | lte: TColType; 167 | gt: TColType; 168 | gte: TColType; 169 | like: string; 170 | notLike: string; 171 | ilike: string; 172 | notIlike: string; 173 | inArray: Array; 174 | notInArray: Array; 175 | isNull: boolean; 176 | isNotNull: boolean; 177 | }>; 178 | 179 | export type FilterColumnOperators< 180 | TColumn extends Column, 181 | TOperators extends FilterColumnOperatorsCore = FilterColumnOperatorsCore, 182 | > = TOperators & { 183 | OR?: TOperators[]; 184 | }; 185 | 186 | export type FiltersCore = Partial< 187 | { 188 | [Column in keyof TTable['_']['columns']]: FilterColumnOperatorsCore; 189 | } 190 | >; 191 | 192 | export type Filters> = TFilterType & { 193 | OR?: TFilterType[]; 194 | }; 195 | 196 | export type OrderByArgs = { 197 | [Key in keyof TTable['_']['columns']]?: { 198 | direction: 'asc' | 'desc'; 199 | priority: number; 200 | }; 201 | }; 202 | 203 | export type GeneratedTableTypesInputs = { 204 | insertInput: GraphQLInputObjectType; 205 | updateInput: GraphQLInputObjectType; 206 | tableOrder: GraphQLInputObjectType; 207 | tableFilters: GraphQLInputObjectType; 208 | }; 209 | 210 | export type GeneratedTableTypesOutputs = WithReturning extends true ? { 211 | selectSingleOutput: GraphQLObjectType; 212 | selectArrOutput: GraphQLNonNull>>; 213 | singleTableItemOutput: GraphQLObjectType; 214 | arrTableItemOutput: GraphQLNonNull>>; 215 | } 216 | : { 217 | selectSingleOutput: GraphQLObjectType; 218 | selectArrOutput: GraphQLNonNull>>; 219 | }; 220 | 221 | export type GeneratedTableTypes = { 222 | inputs: GeneratedTableTypesInputs; 223 | outputs: GeneratedTableTypesOutputs; 224 | }; 225 | 226 | export type SelectData = { 227 | filters: GraphQLInputObjectType; 228 | tableFields: Record; 229 | relationFields: Record; 230 | order: TWithOrder extends true ? GraphQLInputObjectType : undefined; 231 | }; 232 | -------------------------------------------------------------------------------- /src/util/case-ops/index.ts: -------------------------------------------------------------------------------- 1 | export const uncapitalize = (input: T) => 2 | (input.length 3 | ? `${input[0]!.toLocaleLowerCase()}${input.length > 1 ? input.slice(1, input.length) : ''}` 4 | : input) as Uncapitalize; 5 | 6 | export const capitalize = (input: T) => 7 | (input.length 8 | ? `${input[0]!.toLocaleUpperCase()}${input.length > 1 ? input.slice(1, input.length) : ''}` 9 | : input) as Capitalize; 10 | -------------------------------------------------------------------------------- /src/util/data-mappers/index.ts: -------------------------------------------------------------------------------- 1 | import { type Column, getTableColumns, type Table } from 'drizzle-orm'; 2 | import { GraphQLError } from 'graphql'; 3 | import { TableNamedRelations } from '../builders'; 4 | 5 | export const remapToGraphQLCore = ( 6 | key: string, 7 | value: any, 8 | tableName: string, 9 | column: Column, 10 | relationMap?: Record>, 11 | ): any => { 12 | if (value instanceof Date) return value.toISOString(); 13 | 14 | if (value instanceof Buffer) return Array.from(value); 15 | 16 | if (typeof value === 'bigint') return value.toString(); 17 | 18 | if (Array.isArray(value)) { 19 | const relations = relationMap?.[tableName]; 20 | if (relations?.[key]) { 21 | return remapToGraphQLArrayOutput( 22 | value, 23 | relations[key]!.targetTableName, 24 | relations[key]!.relation.referencedTable, 25 | relationMap, 26 | ); 27 | } 28 | if (column.columnType === 'PgGeometry' || column.columnType === 'PgVector') return value; 29 | 30 | return value.map((arrVal) => remapToGraphQLCore(key, arrVal, tableName, column, relationMap)); 31 | } 32 | 33 | if (typeof value === 'object') { 34 | const relations = relationMap?.[tableName]; 35 | if (relations?.[key]) { 36 | return remapToGraphQLSingleOutput( 37 | value, 38 | relations[key]!.targetTableName, 39 | relations[key]!.relation.referencedTable, 40 | relationMap, 41 | ); 42 | } 43 | if (column.columnType === 'PgGeometryObject') return value; 44 | 45 | return JSON.stringify(value); 46 | } 47 | 48 | return value; 49 | }; 50 | 51 | export const remapToGraphQLSingleOutput = ( 52 | queryOutput: Record, 53 | tableName: string, 54 | table: Table, 55 | relationMap?: Record>, 56 | ) => { 57 | for (const [key, value] of Object.entries(queryOutput)) { 58 | if (value === undefined || value === null) { 59 | delete queryOutput[key]; 60 | } else { 61 | queryOutput[key] = remapToGraphQLCore(key, value, tableName, table[key as keyof Table]! as Column, relationMap); 62 | } 63 | } 64 | 65 | return queryOutput; 66 | }; 67 | 68 | export const remapToGraphQLArrayOutput = ( 69 | queryOutput: Record[], 70 | tableName: string, 71 | table: Table, 72 | relationMap?: Record>, 73 | ) => { 74 | for (const entry of queryOutput) { 75 | remapToGraphQLSingleOutput(entry, tableName, table, relationMap); 76 | } 77 | 78 | return queryOutput; 79 | }; 80 | 81 | export const remapFromGraphQLCore = (value: any, column: Column, columnName: string) => { 82 | switch (column.dataType) { 83 | case 'date': { 84 | const formatted = new Date(value); 85 | if (Number.isNaN(formatted.getTime())) throw new GraphQLError(`Field '${columnName}' is not a valid date!`); 86 | 87 | return formatted; 88 | } 89 | 90 | case 'buffer': { 91 | if (!Array.isArray(value)) { 92 | throw new GraphQLError(`Field '${columnName}' is not an array!`); 93 | } 94 | 95 | return Buffer.from(value); 96 | } 97 | 98 | case 'json': { 99 | if (column.columnType === 'PgGeometryObject') return value; 100 | 101 | try { 102 | return JSON.parse(value); 103 | } catch (e) { 104 | throw new GraphQLError( 105 | `Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : 'Unknown error'}`, 106 | ); 107 | } 108 | } 109 | 110 | case 'array': { 111 | if (!Array.isArray(value)) { 112 | throw new GraphQLError(`Field '${columnName}' is not an array!`); 113 | } 114 | 115 | if (column.columnType === 'PgGeometry' && value.length !== 2) { 116 | throw new GraphQLError( 117 | `Invalid float tuple in field '${columnName}': expected array with length of 2, received ${value.length}`, 118 | ); 119 | } 120 | 121 | return value; 122 | } 123 | 124 | case 'bigint': { 125 | try { 126 | return BigInt(value); 127 | } catch (error) { 128 | throw new GraphQLError(`Field '${columnName}' is not a BigInt!`); 129 | } 130 | } 131 | 132 | default: { 133 | return value; 134 | } 135 | } 136 | }; 137 | 138 | export const remapFromGraphQLSingleInput = (queryInput: Record, table: Table) => { 139 | for (const [key, value] of Object.entries(queryInput)) { 140 | if (value === undefined) { 141 | delete queryInput[key]; 142 | } else { 143 | const column = getTableColumns(table)[key]; 144 | if (!column) throw new GraphQLError(`Unknown column: ${key}`); 145 | 146 | if (value === null && column.notNull) { 147 | delete queryInput[key]; 148 | continue; 149 | } 150 | 151 | queryInput[key] = remapFromGraphQLCore(value, column, key); 152 | } 153 | } 154 | 155 | return queryInput; 156 | }; 157 | 158 | export const remapFromGraphQLArrayInput = (queryInput: Record[], table: Table) => { 159 | for (const entry of queryInput) remapFromGraphQLSingleInput(entry, table); 160 | 161 | return queryInput; 162 | }; 163 | -------------------------------------------------------------------------------- /src/util/type-converter/index.ts: -------------------------------------------------------------------------------- 1 | import { is } from 'drizzle-orm'; 2 | import { MySqlInt, MySqlSerial } from 'drizzle-orm/mysql-core'; 3 | import { PgInteger, PgSerial } from 'drizzle-orm/pg-core'; 4 | import { SQLiteInteger } from 'drizzle-orm/sqlite-core'; 5 | import { 6 | GraphQLBoolean, 7 | GraphQLEnumType, 8 | GraphQLFloat, 9 | GraphQLInputObjectType, 10 | GraphQLInt, 11 | GraphQLList, 12 | GraphQLNonNull, 13 | GraphQLObjectType, 14 | GraphQLScalarType, 15 | GraphQLString, 16 | } from 'graphql'; 17 | 18 | import type { Column } from 'drizzle-orm'; 19 | import type { PgArray } from 'drizzle-orm/pg-core'; 20 | import { capitalize } from '../case-ops'; 21 | import type { ConvertedColumn } from './types'; 22 | 23 | const allowedNameChars = /^[a-zA-Z0-9_]+$/; 24 | 25 | const enumMap = new WeakMap(); 26 | const generateEnumCached = (column: Column, columnName: string, tableName: string): GraphQLEnumType => { 27 | if (enumMap.has(column)) return enumMap.get(column)!; 28 | 29 | const gqlEnum = new GraphQLEnumType({ 30 | name: `${capitalize(tableName)}${capitalize(columnName)}Enum`, 31 | values: Object.fromEntries(column.enumValues!.map((e, index) => [allowedNameChars.test(e) ? e : `Option${index}`, { 32 | value: e, 33 | description: `Value: ${e}`, 34 | }])), 35 | }); 36 | 37 | enumMap.set(column, gqlEnum); 38 | 39 | return gqlEnum; 40 | }; 41 | 42 | const geoXyType = new GraphQLObjectType({ 43 | name: 'PgGeometryObject', 44 | fields: { 45 | x: { type: GraphQLFloat }, 46 | y: { type: GraphQLFloat }, 47 | }, 48 | }); 49 | 50 | const geoXyInputType = new GraphQLInputObjectType({ 51 | name: 'PgGeometryObjectInput', 52 | fields: { 53 | x: { type: GraphQLFloat }, 54 | y: { type: GraphQLFloat }, 55 | }, 56 | }); 57 | 58 | const columnToGraphQLCore = ( 59 | column: Column, 60 | columnName: string, 61 | tableName: string, 62 | isInput: boolean, 63 | ): ConvertedColumn => { 64 | switch (column.dataType) { 65 | case 'boolean': 66 | return { type: GraphQLBoolean, description: 'Boolean' }; 67 | case 'json': 68 | return column.columnType === 'PgGeometryObject' 69 | ? { 70 | type: isInput ? geoXyInputType : geoXyType, 71 | description: 'Geometry points XY', 72 | } 73 | : { type: GraphQLString, description: 'JSON' }; 74 | case 'date': 75 | return { type: GraphQLString, description: 'Date' }; 76 | case 'string': 77 | if (column.enumValues?.length) return { type: generateEnumCached(column, columnName, tableName) }; 78 | 79 | return { type: GraphQLString, description: 'String' }; 80 | case 'bigint': 81 | return { type: GraphQLString, description: 'BigInt' }; 82 | case 'number': 83 | return is(column, PgInteger) 84 | || is(column, PgSerial) 85 | || is(column, MySqlInt) 86 | || is(column, MySqlSerial) 87 | || is(column, SQLiteInteger) 88 | ? { type: GraphQLInt, description: 'Integer' } 89 | : { type: GraphQLFloat, description: 'Float' }; 90 | case 'buffer': 91 | return { type: new GraphQLList(new GraphQLNonNull(GraphQLInt)), description: 'Buffer' }; 92 | case 'array': { 93 | if (column.columnType === 'PgVector') { 94 | return { 95 | type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)), 96 | description: 'Array', 97 | }; 98 | } 99 | 100 | if (column.columnType === 'PgGeometry') { 101 | return { 102 | type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)), 103 | description: 'Tuple<[Float, Float]>', 104 | }; 105 | } 106 | 107 | const innerType = columnToGraphQLCore( 108 | (column as Column as PgArray).baseColumn, 109 | columnName, 110 | tableName, 111 | isInput, 112 | ); 113 | 114 | return { 115 | type: new GraphQLList(new GraphQLNonNull(innerType.type as GraphQLScalarType)), 116 | description: `Array<${innerType.description}>`, 117 | }; 118 | } 119 | case 'custom': 120 | default: 121 | throw new Error(`Drizzle-GraphQL Error: Type ${column.dataType} is not implemented!`); 122 | } 123 | }; 124 | 125 | export const drizzleColumnToGraphQLType = ( 126 | column: TColumn, 127 | columnName: string, 128 | tableName: string, 129 | forceNullable = false, 130 | defaultIsNullable = false, 131 | isInput: TIsInput = false as TIsInput, 132 | ): ConvertedColumn => { 133 | const typeDesc = columnToGraphQLCore(column, columnName, tableName, isInput); 134 | const noDesc = ['string', 'boolean', 'number']; 135 | if (noDesc.find((e) => e === column.dataType)) delete typeDesc.description; 136 | 137 | if (forceNullable) return typeDesc as ConvertedColumn; 138 | if (column.notNull && !(defaultIsNullable && (column.hasDefault || column.defaultFn))) { 139 | return { 140 | type: new GraphQLNonNull(typeDesc.type), 141 | description: typeDesc.description, 142 | } as ConvertedColumn; 143 | } 144 | 145 | return typeDesc as ConvertedColumn; 146 | }; 147 | 148 | export * from './types'; 149 | -------------------------------------------------------------------------------- /src/util/type-converter/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GraphQLEnumType, 3 | GraphQLFieldConfig, 4 | GraphQLInputObjectType, 5 | GraphQLList, 6 | GraphQLNonNull, 7 | GraphQLObjectType, 8 | GraphQLScalarType, 9 | } from 'graphql'; 10 | 11 | export type ConvertedColumn = { 12 | type: 13 | | GraphQLScalarType 14 | | GraphQLEnumType 15 | | GraphQLNonNull 16 | | GraphQLNonNull 17 | | GraphQLList 18 | | GraphQLList> 19 | | GraphQLNonNull> 20 | | GraphQLNonNull>> 21 | | (TIsInput extends true ? 22 | | GraphQLInputObjectType 23 | | GraphQLNonNull 24 | | GraphQLList 25 | | GraphQLNonNull> 26 | | GraphQLNonNull>> 27 | : 28 | | GraphQLObjectType 29 | | GraphQLNonNull 30 | | GraphQLList 31 | | GraphQLNonNull> 32 | | GraphQLNonNull>>); 33 | description?: string; 34 | }; 35 | 36 | export type ConvertedColumnWithArgs = ConvertedColumn & { 37 | args?: GraphQLFieldConfig['args']; 38 | }; 39 | 40 | export type ConvertedInputColumn = { 41 | type: GraphQLInputObjectType; 42 | description?: string; 43 | }; 44 | 45 | export type ConvertedRelationColumn = { 46 | type: 47 | | GraphQLObjectType 48 | | GraphQLNonNull 49 | | GraphQLNonNull>>; 50 | }; 51 | 52 | export type ConvertedRelationColumnWithArgs = ConvertedRelationColumn & { 53 | args?: GraphQLFieldConfig['args']; 54 | }; 55 | -------------------------------------------------------------------------------- /tests/schema/mysql.ts: -------------------------------------------------------------------------------- 1 | import { relations } from 'drizzle-orm'; 2 | import { 3 | bigint, 4 | boolean, 5 | char, 6 | date, 7 | int, 8 | mysqlEnum, 9 | mysqlTable, 10 | text, 11 | timestamp, 12 | varchar, 13 | } from 'drizzle-orm/mysql-core'; 14 | 15 | export const Users = mysqlTable('users', { 16 | id: int('id').autoincrement().primaryKey(), 17 | name: text('name').notNull(), 18 | email: text('email'), 19 | bigint: bigint('big_int', { mode: 'bigint', unsigned: true }), 20 | birthdayString: date('birthday_string', { mode: 'string' }), 21 | birthdayDate: date('birthday_date', { mode: 'date' }), 22 | createdAt: timestamp('created_at').notNull().defaultNow(), 23 | role: mysqlEnum('role', ['admin', 'user']), 24 | roleText: text('role1', { enum: ['admin', 'user'] }), 25 | roleText2: text('role2', { enum: ['admin', 'user'] }).default('user'), 26 | profession: varchar('profession', { length: 20 }), 27 | initials: char('initials', { length: 2 }), 28 | isConfirmed: boolean('is_confirmed'), 29 | }); 30 | 31 | export const Customers = mysqlTable('customers', { 32 | id: int('id').autoincrement().primaryKey(), 33 | address: text('address').notNull(), 34 | isConfirmed: boolean('is_confirmed'), 35 | registrationDate: timestamp('registration_date').notNull().defaultNow(), 36 | userId: int('user_id') 37 | .references(() => Users.id) 38 | .notNull(), 39 | }); 40 | 41 | export const Posts = mysqlTable('posts', { 42 | id: int('id').autoincrement().primaryKey(), 43 | content: text('content'), 44 | authorId: int('author_id'), 45 | }); 46 | 47 | export const usersRelations = relations(Users, ({ one, many }) => ({ 48 | posts: many(Posts), 49 | customer: one(Customers, { 50 | fields: [Users.id], 51 | references: [Customers.userId], 52 | }), 53 | })); 54 | 55 | export const customersRelations = relations(Customers, ({ one, many }) => ({ 56 | user: one(Users, { 57 | fields: [Customers.userId], 58 | references: [Users.id], 59 | }), 60 | posts: many(Posts), 61 | })); 62 | 63 | export const postsRelations = relations(Posts, ({ one }) => ({ 64 | author: one(Users, { 65 | fields: [Posts.authorId], 66 | references: [Users.id], 67 | }), 68 | customer: one(Customers, { 69 | fields: [Posts.authorId], 70 | references: [Customers.userId], 71 | }), 72 | })); 73 | -------------------------------------------------------------------------------- /tests/schema/pg.ts: -------------------------------------------------------------------------------- 1 | import { relations } from 'drizzle-orm'; 2 | import { 3 | boolean, 4 | char, 5 | date, 6 | geometry, 7 | integer, 8 | pgEnum, 9 | pgTable, 10 | serial, 11 | text, 12 | timestamp, 13 | varchar, 14 | vector, 15 | } from 'drizzle-orm/pg-core'; 16 | 17 | export const roleEnum = pgEnum('role', ['admin', 'user']); 18 | 19 | export const Users = pgTable('users', { 20 | a: integer('a').array(), 21 | id: serial('id').primaryKey(), 22 | name: text('name').notNull(), 23 | email: text('email'), 24 | birthdayString: date('birthday_string', { mode: 'string' }), 25 | birthdayDate: date('birthday_date', { mode: 'date' }), 26 | createdAt: timestamp('created_at').notNull().defaultNow(), 27 | role: roleEnum('role'), 28 | roleText: text('role1', { enum: ['admin', 'user'] }), 29 | roleText2: text('role2', { enum: ['admin', 'user'] }).default('user'), 30 | profession: varchar('profession', { length: 20 }), 31 | initials: char('initials', { length: 2 }), 32 | isConfirmed: boolean('is_confirmed'), 33 | vector: vector('vector_column', { dimensions: 5 }), 34 | geoXy: geometry('geometry_xy', { 35 | mode: 'xy', 36 | }), 37 | geoTuple: geometry('geometry_tuple', { 38 | mode: 'tuple', 39 | }), 40 | }); 41 | 42 | export const Customers = pgTable('customers', { 43 | id: serial('id').primaryKey(), 44 | address: text('address').notNull(), 45 | isConfirmed: boolean('is_confirmed'), 46 | registrationDate: timestamp('registration_date').notNull().defaultNow(), 47 | userId: integer('user_id') 48 | .references(() => Users.id) 49 | .notNull(), 50 | }); 51 | 52 | export const Posts = pgTable('posts', { 53 | id: serial('id').primaryKey(), 54 | content: text('content'), 55 | authorId: integer('author_id'), 56 | }); 57 | 58 | export const usersRelations = relations(Users, ({ one, many }) => ({ 59 | posts: many(Posts), 60 | customer: one(Customers, { 61 | fields: [Users.id], 62 | references: [Customers.userId], 63 | }), 64 | })); 65 | 66 | export const customersRelations = relations(Customers, ({ one, many }) => ({ 67 | user: one(Users, { 68 | fields: [Customers.userId], 69 | references: [Users.id], 70 | }), 71 | posts: many(Posts), 72 | })); 73 | 74 | export const postsRelations = relations(Posts, ({ one }) => ({ 75 | author: one(Users, { 76 | fields: [Posts.authorId], 77 | references: [Users.id], 78 | }), 79 | customer: one(Customers, { 80 | fields: [Posts.authorId], 81 | references: [Customers.userId], 82 | }), 83 | })); 84 | -------------------------------------------------------------------------------- /tests/schema/sqlite.ts: -------------------------------------------------------------------------------- 1 | import { relations } from 'drizzle-orm'; 2 | import { blob, integer, numeric, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 3 | 4 | export const Users = sqliteTable('users', { 5 | id: integer('id').primaryKey().notNull(), 6 | name: text('name').notNull(), 7 | email: text('email'), 8 | textJson: text('text_json', { mode: 'json' }), 9 | blobBigInt: blob('blob_bigint', { mode: 'bigint' }), 10 | numeric: numeric('numeric'), 11 | createdAt: integer('created_at', { mode: 'timestamp' }), 12 | createdAtMs: integer('created_at_ms', { mode: 'timestamp_ms' }), 13 | real: real('real'), 14 | text: text('text', { length: 255 }), 15 | role: text('role', { enum: ['admin', 'user'] }).default('user'), 16 | isConfirmed: integer('is_confirmed', { 17 | mode: 'boolean', 18 | }), 19 | }); 20 | 21 | export const Customers = sqliteTable('customers', { 22 | id: integer('id').primaryKey(), 23 | address: text('address').notNull(), 24 | isConfirmed: integer('is_confirmed', { mode: 'boolean' }), 25 | registrationDate: integer('registration_date', { mode: 'timestamp_ms' }) 26 | .notNull() 27 | .$defaultFn(() => new Date()), 28 | userId: integer('user_id') 29 | .references(() => Users.id) 30 | .notNull(), 31 | }); 32 | 33 | export const Posts = sqliteTable('posts', { 34 | id: integer('id').primaryKey(), 35 | content: text('content'), 36 | authorId: integer('author_id'), 37 | }); 38 | 39 | export const usersRelations = relations(Users, ({ one, many }) => ({ 40 | posts: many(Posts), 41 | customer: one(Customers, { 42 | fields: [Users.id], 43 | references: [Customers.userId], 44 | }), 45 | })); 46 | 47 | export const customersRelations = relations(Customers, ({ one, many }) => ({ 48 | user: one(Users, { 49 | fields: [Customers.userId], 50 | references: [Users.id], 51 | }), 52 | posts: many(Posts), 53 | })); 54 | 55 | export const postsRelations = relations(Posts, ({ one }) => ({ 56 | author: one(Users, { 57 | fields: [Posts.authorId], 58 | references: [Users.id], 59 | }), 60 | customer: one(Customers, { 61 | fields: [Posts.authorId], 62 | references: [Customers.userId], 63 | }), 64 | })); 65 | -------------------------------------------------------------------------------- /tests/sqlite-custom.test.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema, type GeneratedEntities } from '@/index'; 2 | import { type Client, createClient } from '@libsql/client'; 3 | import { sql } from 'drizzle-orm'; 4 | import { drizzle } from 'drizzle-orm/libsql'; 5 | import { type BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'; 6 | import { GraphQLObjectType, GraphQLSchema } from 'graphql'; 7 | import { createYoga } from 'graphql-yoga'; 8 | import { createServer, type Server } from 'node:http'; 9 | import path from 'path'; 10 | import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; 11 | import * as schema from './schema/sqlite'; 12 | import { GraphQLClient } from './util/query'; 13 | 14 | interface Context { 15 | db: BaseSQLiteDatabase<'async', any, typeof schema>; 16 | client: Client; 17 | schema: GraphQLSchema; 18 | entities: GeneratedEntities>; 19 | server: Server; 20 | gql: GraphQLClient; 21 | } 22 | 23 | const ctx: Context = {} as any; 24 | 25 | beforeAll(async () => { 26 | const sleep = 250; 27 | let timeLeft = 5000; 28 | let connected = false; 29 | let lastError: unknown | undefined; 30 | 31 | do { 32 | try { 33 | ctx.client = createClient({ 34 | url: `file://${path.join(__dirname, '/.temp/db-custom.sqlite')}`, 35 | }); 36 | connected = true; 37 | break; 38 | } catch (e) { 39 | lastError = e; 40 | await new Promise((resolve) => setTimeout(resolve, sleep)); 41 | timeLeft -= sleep; 42 | } 43 | } while (timeLeft > 0); 44 | 45 | if (!connected) { 46 | console.error('Cannot connect to libsql'); 47 | throw lastError; 48 | } 49 | 50 | ctx.db = drizzle(ctx.client, { 51 | schema, 52 | logger: process.env['LOG_SQL'] ? true : false, 53 | }); 54 | 55 | const { entities } = buildSchema(ctx.db); 56 | 57 | const customSchema = new GraphQLSchema({ 58 | query: new GraphQLObjectType({ 59 | name: 'Query', 60 | fields: { 61 | customUsersSingle: entities.queries.usersSingle, 62 | customUsers: entities.queries.users, 63 | customCustomersSingle: entities.queries.customersSingle, 64 | customCustomers: entities.queries.customers, 65 | customPostsSingle: entities.queries.postsSingle, 66 | customPosts: entities.queries.posts, 67 | }, 68 | }), 69 | mutation: new GraphQLObjectType({ 70 | name: 'Mutation', 71 | fields: { 72 | deleteFromCustomUsers: entities.mutations.deleteFromUsers, 73 | deleteFromCustomCustomers: entities.mutations.deleteFromCustomers, 74 | deleteFromCustomPosts: entities.mutations.deleteFromPosts, 75 | updateCustomUsers: entities.mutations.updateUsers, 76 | updateCustomCustomers: entities.mutations.updateCustomers, 77 | updateCustomPosts: entities.mutations.updatePosts, 78 | insertIntoCustomUsers: entities.mutations.insertIntoUsers, 79 | insertIntoCustomUsersSingle: entities.mutations.insertIntoUsersSingle, 80 | insertIntoCustomCustomers: entities.mutations.insertIntoCustomers, 81 | insertIntoCustomCustomersSingle: entities.mutations.insertIntoCustomersSingle, 82 | insertIntoCustomPosts: entities.mutations.insertIntoPosts, 83 | insertIntoCustomPostsSingle: entities.mutations.insertIntoPostsSingle, 84 | }, 85 | }), 86 | types: [...Object.values(entities.types), ...Object.values(entities.inputs)], 87 | }); 88 | 89 | const yoga = createYoga({ 90 | schema: customSchema, 91 | }); 92 | const server = createServer(yoga); 93 | 94 | const port = 5001; 95 | server.listen(port); 96 | const gql = new GraphQLClient(`http://localhost:${port}/graphql`); 97 | 98 | ctx.schema = customSchema; 99 | ctx.entities = entities; 100 | ctx.server = server; 101 | ctx.gql = gql; 102 | }); 103 | 104 | afterAll(async (t) => { 105 | ctx.client.close(); 106 | }); 107 | 108 | beforeEach(async (t) => { 109 | await ctx.db.run(sql`CREATE TABLE IF NOT EXISTS \`customers\` ( 110 | \`id\` integer PRIMARY KEY NOT NULL, 111 | \`address\` text NOT NULL, 112 | \`is_confirmed\` integer, 113 | \`registration_date\` integer NOT NULL, 114 | \`user_id\` integer NOT NULL, 115 | FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE no action 116 | );`); 117 | 118 | await ctx.db.run(sql`CREATE TABLE IF NOT EXISTS \`posts\` ( 119 | \`id\` integer PRIMARY KEY NOT NULL, 120 | \`content\` text, 121 | \`author_id\` integer 122 | );`); 123 | 124 | await ctx.db.run(sql`CREATE TABLE IF NOT EXISTS \`users\` ( 125 | \`id\` integer PRIMARY KEY NOT NULL, 126 | \`name\` text NOT NULL, 127 | \`email\` text, 128 | \`text_json\` text, 129 | \`blob_bigint\` blob, 130 | \`numeric\` numeric, 131 | \`created_at\` integer, 132 | \`created_at_ms\` integer, 133 | \`real\` real, 134 | \`text\` text(255), 135 | \`role\` text DEFAULT 'user', 136 | \`is_confirmed\` integer 137 | );`); 138 | 139 | await ctx.db.insert(schema.Users).values([ 140 | { 141 | id: 1, 142 | name: 'FirstUser', 143 | email: 'userOne@notmail.com', 144 | textJson: { field: 'value' }, 145 | blobBigInt: BigInt(10), 146 | numeric: '250.2', 147 | createdAt: new Date('2024-04-02T06:44:41.785Z'), 148 | createdAtMs: new Date('2024-04-02T06:44:41.785Z'), 149 | real: 13.5, 150 | text: 'sometext', 151 | role: 'admin', 152 | isConfirmed: true, 153 | }, 154 | { 155 | id: 2, 156 | name: 'SecondUser', 157 | createdAt: new Date('2024-04-02T06:44:41.785Z'), 158 | }, 159 | { 160 | id: 5, 161 | name: 'FifthUser', 162 | createdAt: new Date('2024-04-02T06:44:41.785Z'), 163 | }, 164 | ]); 165 | 166 | await ctx.db.insert(schema.Posts).values([ 167 | { 168 | id: 1, 169 | authorId: 1, 170 | content: '1MESSAGE', 171 | }, 172 | { 173 | id: 2, 174 | authorId: 1, 175 | content: '2MESSAGE', 176 | }, 177 | { 178 | id: 3, 179 | authorId: 1, 180 | content: '3MESSAGE', 181 | }, 182 | { 183 | id: 4, 184 | authorId: 5, 185 | content: '1MESSAGE', 186 | }, 187 | { 188 | id: 5, 189 | authorId: 5, 190 | content: '2MESSAGE', 191 | }, 192 | { 193 | id: 6, 194 | authorId: 1, 195 | content: '4MESSAGE', 196 | }, 197 | ]); 198 | 199 | await ctx.db.insert(schema.Customers).values([ 200 | { 201 | id: 1, 202 | address: 'AdOne', 203 | isConfirmed: false, 204 | registrationDate: new Date('2024-03-27T03:54:45.235Z'), 205 | userId: 1, 206 | }, 207 | { 208 | id: 2, 209 | address: 'AdTwo', 210 | isConfirmed: false, 211 | registrationDate: new Date('2024-03-27T03:55:42.358Z'), 212 | userId: 2, 213 | }, 214 | ]); 215 | }); 216 | 217 | afterEach(async (t) => { 218 | await ctx.db.run(sql`PRAGMA foreign_keys = OFF;`); 219 | await ctx.db.run(sql`DROP TABLE IF EXISTS \`customers\`;`); 220 | await ctx.db.run(sql`DROP TABLE IF EXISTS \`posts\`;`); 221 | await ctx.db.run(sql`DROP TABLE IF EXISTS \`users\`;`); 222 | await ctx.db.run(sql`PRAGMA foreign_keys = ON;`); 223 | }); 224 | 225 | describe.sequential('Query tests', async () => { 226 | it(`Select single`, async () => { 227 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 228 | { 229 | customUsersSingle { 230 | id 231 | name 232 | email 233 | textJson 234 | blobBigInt 235 | numeric 236 | createdAt 237 | createdAtMs 238 | real 239 | text 240 | role 241 | isConfirmed 242 | } 243 | 244 | customPostsSingle { 245 | id 246 | authorId 247 | content 248 | } 249 | } 250 | `); 251 | 252 | expect(res).toStrictEqual({ 253 | data: { 254 | customUsersSingle: { 255 | id: 1, 256 | name: 'FirstUser', 257 | email: 'userOne@notmail.com', 258 | textJson: '{"field":"value"}', 259 | blobBigInt: '10', 260 | numeric: '250.2', 261 | createdAt: '2024-04-02T06:44:41.000Z', 262 | createdAtMs: '2024-04-02T06:44:41.785Z', 263 | real: 13.5, 264 | text: 'sometext', 265 | role: 'admin', 266 | isConfirmed: true, 267 | }, 268 | customPostsSingle: { 269 | id: 1, 270 | authorId: 1, 271 | content: '1MESSAGE', 272 | }, 273 | }, 274 | }); 275 | }); 276 | 277 | it(`Select array`, async () => { 278 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 279 | { 280 | customUsers { 281 | id 282 | name 283 | email 284 | textJson 285 | blobBigInt 286 | numeric 287 | createdAt 288 | createdAtMs 289 | real 290 | text 291 | role 292 | isConfirmed 293 | } 294 | 295 | customPosts { 296 | id 297 | authorId 298 | content 299 | } 300 | } 301 | `); 302 | 303 | expect(res).toStrictEqual({ 304 | data: { 305 | customUsers: [ 306 | { 307 | id: 1, 308 | name: 'FirstUser', 309 | email: 'userOne@notmail.com', 310 | textJson: '{"field":"value"}', 311 | blobBigInt: '10', 312 | numeric: '250.2', 313 | createdAt: '2024-04-02T06:44:41.000Z', 314 | createdAtMs: '2024-04-02T06:44:41.785Z', 315 | real: 13.5, 316 | text: 'sometext', 317 | role: 'admin', 318 | isConfirmed: true, 319 | }, 320 | { 321 | id: 2, 322 | name: 'SecondUser', 323 | email: null, 324 | blobBigInt: null, 325 | textJson: null, 326 | createdAt: '2024-04-02T06:44:41.000Z', 327 | createdAtMs: null, 328 | numeric: null, 329 | real: null, 330 | text: null, 331 | role: 'user', 332 | isConfirmed: null, 333 | }, 334 | { 335 | id: 5, 336 | name: 'FifthUser', 337 | email: null, 338 | createdAt: '2024-04-02T06:44:41.000Z', 339 | role: 'user', 340 | blobBigInt: null, 341 | textJson: null, 342 | createdAtMs: null, 343 | numeric: null, 344 | real: null, 345 | text: null, 346 | isConfirmed: null, 347 | }, 348 | ], 349 | customPosts: [ 350 | { 351 | id: 1, 352 | authorId: 1, 353 | content: '1MESSAGE', 354 | }, 355 | { 356 | id: 2, 357 | authorId: 1, 358 | content: '2MESSAGE', 359 | }, 360 | { 361 | id: 3, 362 | authorId: 1, 363 | content: '3MESSAGE', 364 | }, 365 | { 366 | id: 4, 367 | authorId: 5, 368 | content: '1MESSAGE', 369 | }, 370 | { 371 | id: 5, 372 | authorId: 5, 373 | content: '2MESSAGE', 374 | }, 375 | { 376 | id: 6, 377 | authorId: 1, 378 | content: '4MESSAGE', 379 | }, 380 | ], 381 | }, 382 | }); 383 | }); 384 | 385 | it(`Select single with relations`, async () => { 386 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 387 | { 388 | customUsersSingle { 389 | id 390 | name 391 | email 392 | textJson 393 | blobBigInt 394 | numeric 395 | createdAt 396 | createdAtMs 397 | real 398 | text 399 | role 400 | isConfirmed 401 | posts { 402 | id 403 | authorId 404 | content 405 | } 406 | } 407 | 408 | customPostsSingle { 409 | id 410 | authorId 411 | content 412 | author { 413 | id 414 | name 415 | email 416 | textJson 417 | numeric 418 | createdAt 419 | createdAtMs 420 | real 421 | text 422 | role 423 | isConfirmed 424 | } 425 | } 426 | } 427 | `); 428 | 429 | expect(res).toStrictEqual({ 430 | data: { 431 | customUsersSingle: { 432 | id: 1, 433 | name: 'FirstUser', 434 | email: 'userOne@notmail.com', 435 | textJson: '{"field":"value"}', 436 | blobBigInt: '10', 437 | numeric: '250.2', 438 | createdAt: '2024-04-02T06:44:41.000Z', 439 | createdAtMs: '2024-04-02T06:44:41.785Z', 440 | real: 13.5, 441 | text: 'sometext', 442 | role: 'admin', 443 | isConfirmed: true, 444 | posts: [ 445 | { 446 | id: 1, 447 | authorId: 1, 448 | content: '1MESSAGE', 449 | }, 450 | { 451 | id: 2, 452 | authorId: 1, 453 | content: '2MESSAGE', 454 | }, 455 | { 456 | id: 3, 457 | authorId: 1, 458 | content: '3MESSAGE', 459 | }, 460 | 461 | { 462 | id: 6, 463 | authorId: 1, 464 | content: '4MESSAGE', 465 | }, 466 | ], 467 | }, 468 | customPostsSingle: { 469 | id: 1, 470 | authorId: 1, 471 | content: '1MESSAGE', 472 | author: { 473 | id: 1, 474 | name: 'FirstUser', 475 | email: 'userOne@notmail.com', 476 | textJson: '{"field":"value"}', 477 | // RQB can't handle blobs in JSON, for now 478 | // blobBigInt: '10', 479 | numeric: '250.2', 480 | createdAt: '2024-04-02T06:44:41.000Z', 481 | createdAtMs: '2024-04-02T06:44:41.785Z', 482 | real: 13.5, 483 | text: 'sometext', 484 | role: 'admin', 485 | isConfirmed: true, 486 | }, 487 | }, 488 | }, 489 | }); 490 | }); 491 | 492 | it(`Select array with relations`, async () => { 493 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 494 | { 495 | customUsers { 496 | id 497 | name 498 | email 499 | textJson 500 | blobBigInt 501 | numeric 502 | createdAt 503 | createdAtMs 504 | real 505 | text 506 | role 507 | isConfirmed 508 | posts { 509 | id 510 | authorId 511 | content 512 | } 513 | } 514 | 515 | customPosts { 516 | id 517 | authorId 518 | content 519 | author { 520 | id 521 | name 522 | email 523 | textJson 524 | numeric 525 | createdAt 526 | createdAtMs 527 | real 528 | text 529 | role 530 | isConfirmed 531 | } 532 | } 533 | } 534 | `); 535 | 536 | expect(res).toStrictEqual({ 537 | data: { 538 | customUsers: [ 539 | { 540 | id: 1, 541 | name: 'FirstUser', 542 | email: 'userOne@notmail.com', 543 | textJson: '{"field":"value"}', 544 | blobBigInt: '10', 545 | numeric: '250.2', 546 | createdAt: '2024-04-02T06:44:41.000Z', 547 | createdAtMs: '2024-04-02T06:44:41.785Z', 548 | real: 13.5, 549 | text: 'sometext', 550 | role: 'admin', 551 | isConfirmed: true, 552 | posts: [ 553 | { 554 | id: 1, 555 | authorId: 1, 556 | content: '1MESSAGE', 557 | }, 558 | { 559 | id: 2, 560 | authorId: 1, 561 | content: '2MESSAGE', 562 | }, 563 | { 564 | id: 3, 565 | authorId: 1, 566 | content: '3MESSAGE', 567 | }, 568 | { 569 | id: 6, 570 | authorId: 1, 571 | content: '4MESSAGE', 572 | }, 573 | ], 574 | }, 575 | { 576 | id: 2, 577 | name: 'SecondUser', 578 | email: null, 579 | textJson: null, 580 | blobBigInt: null, 581 | numeric: null, 582 | createdAt: '2024-04-02T06:44:41.000Z', 583 | createdAtMs: null, 584 | real: null, 585 | text: null, 586 | role: 'user', 587 | isConfirmed: null, 588 | posts: [], 589 | }, 590 | { 591 | id: 5, 592 | name: 'FifthUser', 593 | email: null, 594 | textJson: null, 595 | blobBigInt: null, 596 | numeric: null, 597 | createdAt: '2024-04-02T06:44:41.000Z', 598 | createdAtMs: null, 599 | real: null, 600 | text: null, 601 | role: 'user', 602 | isConfirmed: null, 603 | posts: [ 604 | { 605 | id: 4, 606 | authorId: 5, 607 | content: '1MESSAGE', 608 | }, 609 | { 610 | id: 5, 611 | authorId: 5, 612 | content: '2MESSAGE', 613 | }, 614 | ], 615 | }, 616 | ], 617 | customPosts: [ 618 | { 619 | id: 1, 620 | authorId: 1, 621 | content: '1MESSAGE', 622 | author: { 623 | id: 1, 624 | name: 'FirstUser', 625 | email: 'userOne@notmail.com', 626 | textJson: '{"field":"value"}', 627 | // RQB can't handle blobs in JSON, for now 628 | // blobBigInt: '10', 629 | numeric: '250.2', 630 | createdAt: '2024-04-02T06:44:41.000Z', 631 | createdAtMs: '2024-04-02T06:44:41.785Z', 632 | real: 13.5, 633 | text: 'sometext', 634 | role: 'admin', 635 | isConfirmed: true, 636 | }, 637 | }, 638 | { 639 | id: 2, 640 | authorId: 1, 641 | content: '2MESSAGE', 642 | author: { 643 | id: 1, 644 | name: 'FirstUser', 645 | email: 'userOne@notmail.com', 646 | textJson: '{"field":"value"}', 647 | // RQB can't handle blobs in JSON, for now 648 | // blobBigInt: '10', 649 | numeric: '250.2', 650 | createdAt: '2024-04-02T06:44:41.000Z', 651 | createdAtMs: '2024-04-02T06:44:41.785Z', 652 | real: 13.5, 653 | text: 'sometext', 654 | role: 'admin', 655 | isConfirmed: true, 656 | }, 657 | }, 658 | { 659 | id: 3, 660 | authorId: 1, 661 | content: '3MESSAGE', 662 | author: { 663 | id: 1, 664 | name: 'FirstUser', 665 | email: 'userOne@notmail.com', 666 | textJson: '{"field":"value"}', 667 | // RQB can't handle blobs in JSON, for now 668 | // blobBigInt: '10', 669 | numeric: '250.2', 670 | createdAt: '2024-04-02T06:44:41.000Z', 671 | createdAtMs: '2024-04-02T06:44:41.785Z', 672 | real: 13.5, 673 | text: 'sometext', 674 | role: 'admin', 675 | isConfirmed: true, 676 | }, 677 | }, 678 | { 679 | id: 4, 680 | authorId: 5, 681 | content: '1MESSAGE', 682 | author: { 683 | id: 5, 684 | name: 'FifthUser', 685 | email: null, 686 | textJson: null, 687 | // RQB can't handle blobs in JSON, for now 688 | // blobBigInt: null, 689 | numeric: null, 690 | createdAt: '2024-04-02T06:44:41.000Z', 691 | createdAtMs: null, 692 | real: null, 693 | text: null, 694 | role: 'user', 695 | isConfirmed: null, 696 | }, 697 | }, 698 | { 699 | id: 5, 700 | authorId: 5, 701 | content: '2MESSAGE', 702 | author: { 703 | id: 5, 704 | name: 'FifthUser', 705 | email: null, 706 | textJson: null, 707 | // RQB can't handle blobs in JSON, for now 708 | // blobBigInt: null, 709 | numeric: null, 710 | createdAt: '2024-04-02T06:44:41.000Z', 711 | createdAtMs: null, 712 | real: null, 713 | text: null, 714 | role: 'user', 715 | isConfirmed: null, 716 | }, 717 | }, 718 | { 719 | id: 6, 720 | authorId: 1, 721 | content: '4MESSAGE', 722 | author: { 723 | id: 1, 724 | name: 'FirstUser', 725 | email: 'userOne@notmail.com', 726 | textJson: '{"field":"value"}', 727 | // RQB can't handle blobs in JSON, for now 728 | // blobBigInt: '10', 729 | numeric: '250.2', 730 | createdAt: '2024-04-02T06:44:41.000Z', 731 | createdAtMs: '2024-04-02T06:44:41.785Z', 732 | real: 13.5, 733 | text: 'sometext', 734 | role: 'admin', 735 | isConfirmed: true, 736 | }, 737 | }, 738 | ], 739 | }, 740 | }); 741 | }); 742 | 743 | it(`Select single by fragment`, async () => { 744 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 745 | query testQuery { 746 | customUsersSingle { 747 | ...UsersFrag 748 | } 749 | 750 | customPostsSingle { 751 | ...PostsFrag 752 | } 753 | } 754 | 755 | fragment UsersFrag on UsersSelectItem { 756 | id 757 | name 758 | email 759 | textJson 760 | blobBigInt 761 | numeric 762 | createdAt 763 | createdAtMs 764 | real 765 | text 766 | role 767 | isConfirmed 768 | } 769 | 770 | fragment PostsFrag on PostsSelectItem { 771 | id 772 | authorId 773 | content 774 | } 775 | `); 776 | 777 | expect(res).toStrictEqual({ 778 | data: { 779 | customUsersSingle: { 780 | id: 1, 781 | name: 'FirstUser', 782 | email: 'userOne@notmail.com', 783 | textJson: '{"field":"value"}', 784 | blobBigInt: '10', 785 | numeric: '250.2', 786 | createdAt: '2024-04-02T06:44:41.000Z', 787 | createdAtMs: '2024-04-02T06:44:41.785Z', 788 | real: 13.5, 789 | text: 'sometext', 790 | role: 'admin', 791 | isConfirmed: true, 792 | }, 793 | customPostsSingle: { 794 | id: 1, 795 | authorId: 1, 796 | content: '1MESSAGE', 797 | }, 798 | }, 799 | }); 800 | }); 801 | 802 | it(`Select array by fragment`, async () => { 803 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 804 | query testQuery { 805 | customUsers { 806 | ...UsersFrag 807 | } 808 | 809 | customPosts { 810 | ...PostsFrag 811 | } 812 | } 813 | 814 | fragment UsersFrag on UsersSelectItem { 815 | id 816 | name 817 | email 818 | textJson 819 | blobBigInt 820 | numeric 821 | createdAt 822 | createdAtMs 823 | real 824 | text 825 | role 826 | isConfirmed 827 | } 828 | 829 | fragment PostsFrag on PostsSelectItem { 830 | id 831 | authorId 832 | content 833 | } 834 | `); 835 | 836 | expect(res).toStrictEqual({ 837 | data: { 838 | customUsers: [ 839 | { 840 | id: 1, 841 | name: 'FirstUser', 842 | email: 'userOne@notmail.com', 843 | textJson: '{"field":"value"}', 844 | blobBigInt: '10', 845 | numeric: '250.2', 846 | createdAt: '2024-04-02T06:44:41.000Z', 847 | createdAtMs: '2024-04-02T06:44:41.785Z', 848 | real: 13.5, 849 | text: 'sometext', 850 | role: 'admin', 851 | isConfirmed: true, 852 | }, 853 | { 854 | id: 2, 855 | name: 'SecondUser', 856 | email: null, 857 | blobBigInt: null, 858 | textJson: null, 859 | createdAt: '2024-04-02T06:44:41.000Z', 860 | createdAtMs: null, 861 | numeric: null, 862 | real: null, 863 | text: null, 864 | role: 'user', 865 | isConfirmed: null, 866 | }, 867 | { 868 | id: 5, 869 | name: 'FifthUser', 870 | email: null, 871 | createdAt: '2024-04-02T06:44:41.000Z', 872 | role: 'user', 873 | blobBigInt: null, 874 | textJson: null, 875 | createdAtMs: null, 876 | numeric: null, 877 | real: null, 878 | text: null, 879 | isConfirmed: null, 880 | }, 881 | ], 882 | customPosts: [ 883 | { 884 | id: 1, 885 | authorId: 1, 886 | content: '1MESSAGE', 887 | }, 888 | { 889 | id: 2, 890 | authorId: 1, 891 | content: '2MESSAGE', 892 | }, 893 | { 894 | id: 3, 895 | authorId: 1, 896 | content: '3MESSAGE', 897 | }, 898 | { 899 | id: 4, 900 | authorId: 5, 901 | content: '1MESSAGE', 902 | }, 903 | { 904 | id: 5, 905 | authorId: 5, 906 | content: '2MESSAGE', 907 | }, 908 | { 909 | id: 6, 910 | authorId: 1, 911 | content: '4MESSAGE', 912 | }, 913 | ], 914 | }, 915 | }); 916 | }); 917 | 918 | it(`Select single with relations by fragment`, async () => { 919 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 920 | query testQuery { 921 | customUsersSingle { 922 | ...UsersFrag 923 | } 924 | 925 | customPostsSingle { 926 | ...PostsFrag 927 | } 928 | } 929 | 930 | fragment UsersFrag on UsersSelectItem { 931 | id 932 | name 933 | email 934 | textJson 935 | blobBigInt 936 | numeric 937 | createdAt 938 | createdAtMs 939 | real 940 | text 941 | role 942 | isConfirmed 943 | posts { 944 | id 945 | authorId 946 | content 947 | } 948 | } 949 | 950 | fragment PostsFrag on PostsSelectItem { 951 | id 952 | authorId 953 | content 954 | author { 955 | id 956 | name 957 | email 958 | textJson 959 | numeric 960 | createdAt 961 | createdAtMs 962 | real 963 | text 964 | role 965 | isConfirmed 966 | } 967 | } 968 | `); 969 | 970 | expect(res).toStrictEqual({ 971 | data: { 972 | customUsersSingle: { 973 | id: 1, 974 | name: 'FirstUser', 975 | email: 'userOne@notmail.com', 976 | textJson: '{"field":"value"}', 977 | blobBigInt: '10', 978 | numeric: '250.2', 979 | createdAt: '2024-04-02T06:44:41.000Z', 980 | createdAtMs: '2024-04-02T06:44:41.785Z', 981 | real: 13.5, 982 | text: 'sometext', 983 | role: 'admin', 984 | isConfirmed: true, 985 | posts: [ 986 | { 987 | id: 1, 988 | authorId: 1, 989 | content: '1MESSAGE', 990 | }, 991 | { 992 | id: 2, 993 | authorId: 1, 994 | content: '2MESSAGE', 995 | }, 996 | { 997 | id: 3, 998 | authorId: 1, 999 | content: '3MESSAGE', 1000 | }, 1001 | 1002 | { 1003 | id: 6, 1004 | authorId: 1, 1005 | content: '4MESSAGE', 1006 | }, 1007 | ], 1008 | }, 1009 | customPostsSingle: { 1010 | id: 1, 1011 | authorId: 1, 1012 | content: '1MESSAGE', 1013 | author: { 1014 | id: 1, 1015 | name: 'FirstUser', 1016 | email: 'userOne@notmail.com', 1017 | textJson: '{"field":"value"}', 1018 | // RQB can't handle blobs in JSON, for now 1019 | // blobBigInt: '10', 1020 | numeric: '250.2', 1021 | createdAt: '2024-04-02T06:44:41.000Z', 1022 | createdAtMs: '2024-04-02T06:44:41.785Z', 1023 | real: 13.5, 1024 | text: 'sometext', 1025 | role: 'admin', 1026 | isConfirmed: true, 1027 | }, 1028 | }, 1029 | }, 1030 | }); 1031 | }); 1032 | 1033 | it(`Select array with relations by fragment`, async () => { 1034 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1035 | query testQuery { 1036 | customUsers { 1037 | ...UsersFrag 1038 | } 1039 | 1040 | customPosts { 1041 | ...PostsFrag 1042 | } 1043 | } 1044 | 1045 | fragment UsersFrag on UsersSelectItem { 1046 | id 1047 | name 1048 | email 1049 | textJson 1050 | blobBigInt 1051 | numeric 1052 | createdAt 1053 | createdAtMs 1054 | real 1055 | text 1056 | role 1057 | isConfirmed 1058 | posts { 1059 | id 1060 | authorId 1061 | content 1062 | } 1063 | } 1064 | 1065 | fragment PostsFrag on PostsSelectItem { 1066 | id 1067 | authorId 1068 | content 1069 | author { 1070 | id 1071 | name 1072 | email 1073 | textJson 1074 | numeric 1075 | createdAt 1076 | createdAtMs 1077 | real 1078 | text 1079 | role 1080 | isConfirmed 1081 | } 1082 | } 1083 | `); 1084 | 1085 | expect(res).toStrictEqual({ 1086 | data: { 1087 | customUsers: [ 1088 | { 1089 | id: 1, 1090 | name: 'FirstUser', 1091 | email: 'userOne@notmail.com', 1092 | textJson: '{"field":"value"}', 1093 | blobBigInt: '10', 1094 | numeric: '250.2', 1095 | createdAt: '2024-04-02T06:44:41.000Z', 1096 | createdAtMs: '2024-04-02T06:44:41.785Z', 1097 | real: 13.5, 1098 | text: 'sometext', 1099 | role: 'admin', 1100 | isConfirmed: true, 1101 | posts: [ 1102 | { 1103 | id: 1, 1104 | authorId: 1, 1105 | content: '1MESSAGE', 1106 | }, 1107 | { 1108 | id: 2, 1109 | authorId: 1, 1110 | content: '2MESSAGE', 1111 | }, 1112 | { 1113 | id: 3, 1114 | authorId: 1, 1115 | content: '3MESSAGE', 1116 | }, 1117 | { 1118 | id: 6, 1119 | authorId: 1, 1120 | content: '4MESSAGE', 1121 | }, 1122 | ], 1123 | }, 1124 | { 1125 | id: 2, 1126 | name: 'SecondUser', 1127 | email: null, 1128 | textJson: null, 1129 | blobBigInt: null, 1130 | numeric: null, 1131 | createdAt: '2024-04-02T06:44:41.000Z', 1132 | createdAtMs: null, 1133 | real: null, 1134 | text: null, 1135 | role: 'user', 1136 | isConfirmed: null, 1137 | posts: [], 1138 | }, 1139 | { 1140 | id: 5, 1141 | name: 'FifthUser', 1142 | email: null, 1143 | textJson: null, 1144 | blobBigInt: null, 1145 | numeric: null, 1146 | createdAt: '2024-04-02T06:44:41.000Z', 1147 | createdAtMs: null, 1148 | real: null, 1149 | text: null, 1150 | role: 'user', 1151 | isConfirmed: null, 1152 | posts: [ 1153 | { 1154 | id: 4, 1155 | authorId: 5, 1156 | content: '1MESSAGE', 1157 | }, 1158 | { 1159 | id: 5, 1160 | authorId: 5, 1161 | content: '2MESSAGE', 1162 | }, 1163 | ], 1164 | }, 1165 | ], 1166 | customPosts: [ 1167 | { 1168 | id: 1, 1169 | authorId: 1, 1170 | content: '1MESSAGE', 1171 | author: { 1172 | id: 1, 1173 | name: 'FirstUser', 1174 | email: 'userOne@notmail.com', 1175 | textJson: '{"field":"value"}', 1176 | // RQB can't handle blobs in JSON, for now 1177 | // blobBigInt: '10', 1178 | numeric: '250.2', 1179 | createdAt: '2024-04-02T06:44:41.000Z', 1180 | createdAtMs: '2024-04-02T06:44:41.785Z', 1181 | real: 13.5, 1182 | text: 'sometext', 1183 | role: 'admin', 1184 | isConfirmed: true, 1185 | }, 1186 | }, 1187 | { 1188 | id: 2, 1189 | authorId: 1, 1190 | content: '2MESSAGE', 1191 | author: { 1192 | id: 1, 1193 | name: 'FirstUser', 1194 | email: 'userOne@notmail.com', 1195 | textJson: '{"field":"value"}', 1196 | // RQB can't handle blobs in JSON, for now 1197 | // blobBigInt: '10', 1198 | numeric: '250.2', 1199 | createdAt: '2024-04-02T06:44:41.000Z', 1200 | createdAtMs: '2024-04-02T06:44:41.785Z', 1201 | real: 13.5, 1202 | text: 'sometext', 1203 | role: 'admin', 1204 | isConfirmed: true, 1205 | }, 1206 | }, 1207 | { 1208 | id: 3, 1209 | authorId: 1, 1210 | content: '3MESSAGE', 1211 | author: { 1212 | id: 1, 1213 | name: 'FirstUser', 1214 | email: 'userOne@notmail.com', 1215 | textJson: '{"field":"value"}', 1216 | // RQB can't handle blobs in JSON, for now 1217 | // blobBigInt: '10', 1218 | numeric: '250.2', 1219 | createdAt: '2024-04-02T06:44:41.000Z', 1220 | createdAtMs: '2024-04-02T06:44:41.785Z', 1221 | real: 13.5, 1222 | text: 'sometext', 1223 | role: 'admin', 1224 | isConfirmed: true, 1225 | }, 1226 | }, 1227 | { 1228 | id: 4, 1229 | authorId: 5, 1230 | content: '1MESSAGE', 1231 | author: { 1232 | id: 5, 1233 | name: 'FifthUser', 1234 | email: null, 1235 | textJson: null, 1236 | // RQB can't handle blobs in JSON, for now 1237 | // blobBigInt: null, 1238 | numeric: null, 1239 | createdAt: '2024-04-02T06:44:41.000Z', 1240 | createdAtMs: null, 1241 | real: null, 1242 | text: null, 1243 | role: 'user', 1244 | isConfirmed: null, 1245 | }, 1246 | }, 1247 | { 1248 | id: 5, 1249 | authorId: 5, 1250 | content: '2MESSAGE', 1251 | author: { 1252 | id: 5, 1253 | name: 'FifthUser', 1254 | email: null, 1255 | textJson: null, 1256 | // RQB can't handle blobs in JSON, for now 1257 | // blobBigInt: null, 1258 | numeric: null, 1259 | createdAt: '2024-04-02T06:44:41.000Z', 1260 | createdAtMs: null, 1261 | real: null, 1262 | text: null, 1263 | role: 'user', 1264 | isConfirmed: null, 1265 | }, 1266 | }, 1267 | { 1268 | id: 6, 1269 | authorId: 1, 1270 | content: '4MESSAGE', 1271 | author: { 1272 | id: 1, 1273 | name: 'FirstUser', 1274 | email: 'userOne@notmail.com', 1275 | textJson: '{"field":"value"}', 1276 | // RQB can't handle blobs in JSON, for now 1277 | // blobBigInt: '10', 1278 | numeric: '250.2', 1279 | createdAt: '2024-04-02T06:44:41.000Z', 1280 | createdAtMs: '2024-04-02T06:44:41.785Z', 1281 | real: 13.5, 1282 | text: 'sometext', 1283 | role: 'admin', 1284 | isConfirmed: true, 1285 | }, 1286 | }, 1287 | ], 1288 | }, 1289 | }); 1290 | }); 1291 | 1292 | it(`Insert single`, async () => { 1293 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1294 | mutation { 1295 | insertIntoCustomUsersSingle( 1296 | values: { 1297 | id: 3 1298 | name: "ThirdUser" 1299 | email: "userThree@notmail.com" 1300 | textJson: "{ \\"field\\": \\"value\\" }" 1301 | blobBigInt: "10" 1302 | numeric: "250.2" 1303 | createdAt: "2024-04-02T06:44:41.785Z" 1304 | createdAtMs: "2024-04-02T06:44:41.785Z" 1305 | real: 13.5 1306 | text: "sometext" 1307 | role: admin 1308 | isConfirmed: true 1309 | } 1310 | ) { 1311 | id 1312 | name 1313 | email 1314 | textJson 1315 | blobBigInt 1316 | numeric 1317 | createdAt 1318 | createdAtMs 1319 | real 1320 | text 1321 | role 1322 | isConfirmed 1323 | } 1324 | } 1325 | `); 1326 | 1327 | expect(res).toStrictEqual({ 1328 | data: { 1329 | insertIntoCustomUsersSingle: { 1330 | id: 3, 1331 | name: 'ThirdUser', 1332 | email: 'userThree@notmail.com', 1333 | textJson: '{"field":"value"}', 1334 | blobBigInt: '10', 1335 | numeric: '250.2', 1336 | createdAt: '2024-04-02T06:44:41.000Z', 1337 | createdAtMs: '2024-04-02T06:44:41.785Z', 1338 | real: 13.5, 1339 | text: 'sometext', 1340 | role: 'admin', 1341 | isConfirmed: true, 1342 | }, 1343 | }, 1344 | }); 1345 | }); 1346 | 1347 | it(`Insert array`, async () => { 1348 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1349 | mutation { 1350 | insertIntoCustomUsers( 1351 | values: [ 1352 | { 1353 | id: 3 1354 | name: "ThirdUser" 1355 | email: "userThree@notmail.com" 1356 | textJson: "{ \\"field\\": \\"value\\" }" 1357 | blobBigInt: "10" 1358 | numeric: "250.2" 1359 | createdAt: "2024-04-02T06:44:41.785Z" 1360 | createdAtMs: "2024-04-02T06:44:41.785Z" 1361 | real: 13.5 1362 | text: "sometext" 1363 | role: admin 1364 | isConfirmed: true 1365 | } 1366 | { 1367 | id: 4 1368 | name: "FourthUser" 1369 | email: "userFour@notmail.com" 1370 | textJson: "{ \\"field\\": \\"value\\" }" 1371 | blobBigInt: "10" 1372 | numeric: "250.2" 1373 | createdAt: "2024-04-02T06:44:41.785Z" 1374 | createdAtMs: "2024-04-02T06:44:41.785Z" 1375 | real: 13.5 1376 | text: "sometext" 1377 | role: user 1378 | isConfirmed: false 1379 | } 1380 | ] 1381 | ) { 1382 | id 1383 | name 1384 | email 1385 | textJson 1386 | blobBigInt 1387 | numeric 1388 | createdAt 1389 | createdAtMs 1390 | real 1391 | text 1392 | role 1393 | isConfirmed 1394 | } 1395 | } 1396 | `); 1397 | 1398 | expect(res).toStrictEqual({ 1399 | data: { 1400 | insertIntoCustomUsers: [ 1401 | { 1402 | id: 3, 1403 | name: 'ThirdUser', 1404 | email: 'userThree@notmail.com', 1405 | textJson: '{"field":"value"}', 1406 | blobBigInt: '10', 1407 | numeric: '250.2', 1408 | createdAt: '2024-04-02T06:44:41.000Z', 1409 | createdAtMs: '2024-04-02T06:44:41.785Z', 1410 | real: 13.5, 1411 | text: 'sometext', 1412 | role: 'admin', 1413 | isConfirmed: true, 1414 | }, 1415 | { 1416 | id: 4, 1417 | name: 'FourthUser', 1418 | email: 'userFour@notmail.com', 1419 | textJson: '{"field":"value"}', 1420 | blobBigInt: '10', 1421 | numeric: '250.2', 1422 | createdAt: '2024-04-02T06:44:41.000Z', 1423 | createdAtMs: '2024-04-02T06:44:41.785Z', 1424 | real: 13.5, 1425 | text: 'sometext', 1426 | role: 'user', 1427 | isConfirmed: false, 1428 | }, 1429 | ], 1430 | }, 1431 | }); 1432 | }); 1433 | 1434 | it(`Update`, async () => { 1435 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1436 | mutation { 1437 | updateCustomCustomers(set: { isConfirmed: true, address: "Edited" }) { 1438 | id 1439 | address 1440 | isConfirmed 1441 | registrationDate 1442 | userId 1443 | } 1444 | } 1445 | `); 1446 | 1447 | expect(res).toStrictEqual({ 1448 | data: { 1449 | updateCustomCustomers: [ 1450 | { 1451 | id: 1, 1452 | address: 'Edited', 1453 | isConfirmed: true, 1454 | registrationDate: '2024-03-27T03:54:45.235Z', 1455 | userId: 1, 1456 | }, 1457 | { 1458 | id: 2, 1459 | address: 'Edited', 1460 | isConfirmed: true, 1461 | registrationDate: '2024-03-27T03:55:42.358Z', 1462 | userId: 2, 1463 | }, 1464 | ], 1465 | }, 1466 | }); 1467 | }); 1468 | 1469 | it(`Delete`, async () => { 1470 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1471 | mutation { 1472 | deleteFromCustomCustomers { 1473 | id 1474 | address 1475 | isConfirmed 1476 | registrationDate 1477 | userId 1478 | } 1479 | } 1480 | `); 1481 | 1482 | expect(res).toStrictEqual({ 1483 | data: { 1484 | deleteFromCustomCustomers: [ 1485 | { 1486 | id: 1, 1487 | address: 'AdOne', 1488 | isConfirmed: false, 1489 | registrationDate: '2024-03-27T03:54:45.235Z', 1490 | userId: 1, 1491 | }, 1492 | { 1493 | id: 2, 1494 | address: 'AdTwo', 1495 | isConfirmed: false, 1496 | registrationDate: '2024-03-27T03:55:42.358Z', 1497 | userId: 2, 1498 | }, 1499 | ], 1500 | }, 1501 | }); 1502 | }); 1503 | }); 1504 | 1505 | describe.sequential('Arguments tests', async () => { 1506 | it('Order by', async () => { 1507 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1508 | { 1509 | customPosts( 1510 | orderBy: { authorId: { priority: 1, direction: desc }, content: { priority: 0, direction: asc } } 1511 | ) { 1512 | id 1513 | authorId 1514 | content 1515 | } 1516 | } 1517 | `); 1518 | 1519 | expect(res).toStrictEqual({ 1520 | data: { 1521 | customPosts: [ 1522 | { 1523 | id: 4, 1524 | authorId: 5, 1525 | content: '1MESSAGE', 1526 | }, 1527 | { 1528 | id: 5, 1529 | authorId: 5, 1530 | content: '2MESSAGE', 1531 | }, 1532 | { 1533 | id: 1, 1534 | authorId: 1, 1535 | content: '1MESSAGE', 1536 | }, 1537 | { 1538 | id: 2, 1539 | authorId: 1, 1540 | content: '2MESSAGE', 1541 | }, 1542 | { 1543 | id: 3, 1544 | authorId: 1, 1545 | content: '3MESSAGE', 1546 | }, 1547 | 1548 | { 1549 | id: 6, 1550 | authorId: 1, 1551 | content: '4MESSAGE', 1552 | }, 1553 | ], 1554 | }, 1555 | }); 1556 | }); 1557 | 1558 | it('Order by on single', async () => { 1559 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1560 | { 1561 | customPostsSingle( 1562 | orderBy: { authorId: { priority: 1, direction: desc }, content: { priority: 0, direction: asc } } 1563 | ) { 1564 | id 1565 | authorId 1566 | content 1567 | } 1568 | } 1569 | `); 1570 | 1571 | expect(res).toStrictEqual({ 1572 | data: { 1573 | customPostsSingle: { 1574 | id: 4, 1575 | authorId: 5, 1576 | content: '1MESSAGE', 1577 | }, 1578 | }, 1579 | }); 1580 | }); 1581 | 1582 | it('Offset & limit', async () => { 1583 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1584 | { 1585 | customPosts(offset: 1, limit: 2) { 1586 | id 1587 | authorId 1588 | content 1589 | } 1590 | } 1591 | `); 1592 | 1593 | expect(res).toStrictEqual({ 1594 | data: { 1595 | customPosts: [ 1596 | { 1597 | id: 2, 1598 | authorId: 1, 1599 | content: '2MESSAGE', 1600 | }, 1601 | { 1602 | id: 3, 1603 | authorId: 1, 1604 | content: '3MESSAGE', 1605 | }, 1606 | ], 1607 | }, 1608 | }); 1609 | }); 1610 | 1611 | it('Offset on single', async () => { 1612 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1613 | { 1614 | customPostsSingle(offset: 1) { 1615 | id 1616 | authorId 1617 | content 1618 | } 1619 | } 1620 | `); 1621 | 1622 | expect(res).toStrictEqual({ 1623 | data: { 1624 | customPostsSingle: { 1625 | id: 2, 1626 | authorId: 1, 1627 | content: '2MESSAGE', 1628 | }, 1629 | }, 1630 | }); 1631 | }); 1632 | 1633 | it('Filters - top level AND', async () => { 1634 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1635 | { 1636 | customPosts(where: { id: { inArray: [2, 3, 4, 5, 6] }, authorId: { ne: 5 }, content: { ne: "3MESSAGE" } }) { 1637 | id 1638 | authorId 1639 | content 1640 | } 1641 | } 1642 | `); 1643 | 1644 | expect(res).toStrictEqual({ 1645 | data: { 1646 | customPosts: [ 1647 | { 1648 | id: 2, 1649 | authorId: 1, 1650 | content: '2MESSAGE', 1651 | }, 1652 | { 1653 | id: 6, 1654 | authorId: 1, 1655 | content: '4MESSAGE', 1656 | }, 1657 | ], 1658 | }, 1659 | }); 1660 | }); 1661 | 1662 | it('Filters - top level OR', async () => { 1663 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1664 | { 1665 | customPosts(where: { OR: [{ id: { lte: 3 } }, { authorId: { eq: 5 } }] }) { 1666 | id 1667 | authorId 1668 | content 1669 | } 1670 | } 1671 | `); 1672 | 1673 | expect(res).toStrictEqual({ 1674 | data: { 1675 | customPosts: [ 1676 | { 1677 | id: 1, 1678 | authorId: 1, 1679 | content: '1MESSAGE', 1680 | }, 1681 | { 1682 | id: 2, 1683 | authorId: 1, 1684 | content: '2MESSAGE', 1685 | }, 1686 | { 1687 | id: 3, 1688 | authorId: 1, 1689 | content: '3MESSAGE', 1690 | }, 1691 | { 1692 | id: 4, 1693 | authorId: 5, 1694 | content: '1MESSAGE', 1695 | }, 1696 | { 1697 | id: 5, 1698 | authorId: 5, 1699 | content: '2MESSAGE', 1700 | }, 1701 | ], 1702 | }, 1703 | }); 1704 | }); 1705 | 1706 | it('Update filters', async () => { 1707 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1708 | mutation { 1709 | updateCustomPosts(where: { OR: [{ id: { lte: 3 } }, { authorId: { eq: 5 } }] }, set: { content: "UPDATED" }) { 1710 | id 1711 | authorId 1712 | content 1713 | } 1714 | } 1715 | `); 1716 | 1717 | expect(res).toStrictEqual({ 1718 | data: { 1719 | updateCustomPosts: [ 1720 | { 1721 | id: 1, 1722 | authorId: 1, 1723 | content: 'UPDATED', 1724 | }, 1725 | { 1726 | id: 2, 1727 | authorId: 1, 1728 | content: 'UPDATED', 1729 | }, 1730 | { 1731 | id: 3, 1732 | authorId: 1, 1733 | content: 'UPDATED', 1734 | }, 1735 | { 1736 | id: 4, 1737 | authorId: 5, 1738 | content: 'UPDATED', 1739 | }, 1740 | { 1741 | id: 5, 1742 | authorId: 5, 1743 | content: 'UPDATED', 1744 | }, 1745 | ], 1746 | }, 1747 | }); 1748 | }); 1749 | 1750 | it('Delete filters', async () => { 1751 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1752 | mutation { 1753 | deleteFromCustomPosts(where: { OR: [{ id: { lte: 3 } }, { authorId: { eq: 5 } }] }) { 1754 | id 1755 | authorId 1756 | content 1757 | } 1758 | } 1759 | `); 1760 | 1761 | expect(res).toStrictEqual({ 1762 | data: { 1763 | deleteFromCustomPosts: [ 1764 | { 1765 | id: 1, 1766 | authorId: 1, 1767 | content: '1MESSAGE', 1768 | }, 1769 | { 1770 | id: 2, 1771 | authorId: 1, 1772 | content: '2MESSAGE', 1773 | }, 1774 | { 1775 | id: 3, 1776 | authorId: 1, 1777 | content: '3MESSAGE', 1778 | }, 1779 | { 1780 | id: 4, 1781 | authorId: 5, 1782 | content: '1MESSAGE', 1783 | }, 1784 | { 1785 | id: 5, 1786 | authorId: 5, 1787 | content: '2MESSAGE', 1788 | }, 1789 | ], 1790 | }, 1791 | }); 1792 | }); 1793 | 1794 | it('Relations orderBy', async () => { 1795 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1796 | { 1797 | customUsers { 1798 | id 1799 | posts(orderBy: { id: { priority: 1, direction: desc } }) { 1800 | id 1801 | authorId 1802 | content 1803 | } 1804 | } 1805 | } 1806 | `); 1807 | 1808 | expect(res).toStrictEqual({ 1809 | data: { 1810 | customUsers: [ 1811 | { 1812 | id: 1, 1813 | posts: [ 1814 | { 1815 | id: 6, 1816 | authorId: 1, 1817 | content: '4MESSAGE', 1818 | }, 1819 | { 1820 | id: 3, 1821 | authorId: 1, 1822 | content: '3MESSAGE', 1823 | }, 1824 | { 1825 | id: 2, 1826 | authorId: 1, 1827 | content: '2MESSAGE', 1828 | }, 1829 | { 1830 | id: 1, 1831 | authorId: 1, 1832 | content: '1MESSAGE', 1833 | }, 1834 | ], 1835 | }, 1836 | { 1837 | id: 2, 1838 | posts: [], 1839 | }, 1840 | { 1841 | id: 5, 1842 | posts: [ 1843 | { 1844 | id: 5, 1845 | authorId: 5, 1846 | content: '2MESSAGE', 1847 | }, 1848 | { 1849 | id: 4, 1850 | authorId: 5, 1851 | content: '1MESSAGE', 1852 | }, 1853 | ], 1854 | }, 1855 | ], 1856 | }, 1857 | }); 1858 | }); 1859 | 1860 | it('Relations offset & limit', async () => { 1861 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1862 | { 1863 | customUsers { 1864 | id 1865 | posts(offset: 1, limit: 2) { 1866 | id 1867 | authorId 1868 | content 1869 | } 1870 | } 1871 | } 1872 | `); 1873 | 1874 | expect(res).toStrictEqual({ 1875 | data: { 1876 | customUsers: [ 1877 | { 1878 | id: 1, 1879 | posts: [ 1880 | { 1881 | id: 2, 1882 | authorId: 1, 1883 | content: '2MESSAGE', 1884 | }, 1885 | { 1886 | id: 3, 1887 | authorId: 1, 1888 | content: '3MESSAGE', 1889 | }, 1890 | ], 1891 | }, 1892 | { 1893 | id: 2, 1894 | posts: [], 1895 | }, 1896 | { 1897 | id: 5, 1898 | posts: [ 1899 | { 1900 | id: 5, 1901 | authorId: 5, 1902 | content: '2MESSAGE', 1903 | }, 1904 | ], 1905 | }, 1906 | ], 1907 | }, 1908 | }); 1909 | }); 1910 | 1911 | it('Relations filters', async () => { 1912 | const res = await ctx.gql.queryGql(/* GraphQL */ ` 1913 | { 1914 | customUsers { 1915 | id 1916 | posts(where: { content: { like: "2%" } }) { 1917 | id 1918 | authorId 1919 | content 1920 | } 1921 | } 1922 | } 1923 | `); 1924 | 1925 | expect(res).toStrictEqual({ 1926 | data: { 1927 | customUsers: [ 1928 | { 1929 | id: 1, 1930 | posts: [ 1931 | { 1932 | id: 2, 1933 | authorId: 1, 1934 | content: '2MESSAGE', 1935 | }, 1936 | ], 1937 | }, 1938 | { 1939 | id: 2, 1940 | posts: [], 1941 | }, 1942 | { 1943 | id: 5, 1944 | posts: [ 1945 | { 1946 | id: 5, 1947 | authorId: 5, 1948 | content: '2MESSAGE', 1949 | }, 1950 | ], 1951 | }, 1952 | ], 1953 | }, 1954 | }); 1955 | }); 1956 | }); 1957 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "esnext", 6 | "noEmit": true, 7 | "rootDir": "..", 8 | "outDir": "./.cache" 9 | }, 10 | "include": [".", "../src"] 11 | } 12 | -------------------------------------------------------------------------------- /tests/util/query/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from 'axios'; 2 | 3 | export class GraphQLClient { 4 | constructor(private url: string) {} 5 | 6 | public queryGql = async (query: string) => { 7 | try { 8 | const res = await axios.post( 9 | this.url, 10 | JSON.stringify({ 11 | query: query, 12 | variables: {}, 13 | }), 14 | { 15 | headers: { 16 | accept: 'application/graphql-response+json, application/json', 17 | 'content-type': 'application/json', 18 | }, 19 | }, 20 | ); 21 | 22 | return res.data; 23 | } catch (e) { 24 | const err = e as AxiosError; 25 | 26 | console.warn(err.status, err.response?.data.errors); 27 | return err.response?.data; 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.dts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "composite": false, 5 | "rootDir": "src", 6 | "outDir": "dist-dts", 7 | "declaration": true, 8 | "noEmit": false, 9 | "emitDeclarationOnly": true, 10 | "incremental": false 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "isolatedModules": true, 5 | "composite": false, 6 | "target": "esnext", 7 | "module": "esnext", 8 | "moduleResolution": "bundler", 9 | "lib": ["es2020", "es2018", "es2017", "es7", "es6", "es5"], 10 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 11 | "declarationMap": false, 12 | "sourceMap": false, 13 | "allowJs": true, 14 | "incremental": false, 15 | "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 16 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 17 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 18 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 19 | "strict": true, /* Enable all strict type-checking options. */ 20 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 21 | "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 22 | "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 23 | "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 24 | "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 25 | "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 26 | "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 27 | "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 28 | "exactOptionalPropertyTypes": false, /* Interpret optional property types as written, rather than adding 'undefined'. */ 29 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 30 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 31 | "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 32 | "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 33 | "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 34 | "allowUnusedLabels": false, /* Disable error reporting for unused labels. */ 35 | "allowUnreachableCode": false, /* Disable error reporting for unreachable code. */ 36 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 37 | "noErrorTruncation": true, /* Disable truncating types in error messages. */ 38 | "checkJs": true, 39 | "allowImportingTsExtensions": true, 40 | "baseUrl": ".", 41 | "paths": { 42 | "@/*": ["src/*"] 43 | }, 44 | "outDir": "dist" 45 | }, 46 | "exclude": ["/**/node_modules/**/*", "**/dist"], 47 | "include": ["src/**/*", "drizzle.config.ts", "Tests/**/*", "Server/**/*"] 48 | } 49 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | test: { 7 | include: ['tests/**/*.test.ts'], 8 | isolate: true, 9 | typecheck: { 10 | tsconfig: 'tsconfig.json', 11 | }, 12 | testTimeout: 100000, 13 | hookTimeout: 100000, 14 | }, 15 | plugins: [viteCommonjs(), tsconfigPaths()], 16 | resolve: { alias: { graphql: 'graphql/index.js' } }, 17 | }); 18 | --------------------------------------------------------------------------------