From 65719b560bc62e9821e2d28882587b1608c1b975 Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Mon, 13 Mar 2023 18:52:50 +0530 Subject: [PATCH] feat: introduce gql schema sdl generation to the backend (#35) * feat: introduce gql schema sdl generation to the backend * chore: update gql-codegen consumers to get schema from generated sdl * chore: hoppscotch-backend generates gql sdl on postinstall * fix: add back missed part of generate-gql-sdl script * chore: updated generate sdl script to hardcode whitelisted domains * chore: add prisma generate on postinstall script --------- Co-authored-by: ankitsridhar16 --- .gitignore | 3 + package.json | 2 + packages/hoppscotch-backend/.gitignore | 3 + packages/hoppscotch-backend/package.json | 3 + packages/hoppscotch-backend/src/gql-schema.ts | 95 +++++++++++++++++++ packages/hoppscotch-backend/src/main.ts | 8 +- packages/hoppscotch-common/gql-codegen.yml | 3 +- packages/hoppscotch-sh-admin/gql-codegen.yml | 2 +- pnpm-lock.yaml | 6 +- 9 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 packages/hoppscotch-backend/src/gql-schema.ts diff --git a/.gitignore b/.gitignore index 2bed306f..c4607f89 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,6 @@ tests/*/videos # PNPM .pnpm-store + +# GQL SDL generated for the frontends +gql-gen/ diff --git a/package.json b/package.json index 92110d46..97841f1f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preinstall": "npx only-allow pnpm", "prepare": "husky install", "dev": "pnpm -r do-dev", + "gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl", "generate": "pnpm -r do-build-prod", "start": "http-server packages/hoppscotch-web/dist -p 3000", "lint": "pnpm -r do-lint", @@ -29,6 +30,7 @@ "@commitlint/cli": "^16.2.3", "@commitlint/config-conventional": "^16.2.1", "@types/node": "^17.0.24", + "cross-env": "^7.0.3", "http-server": "^14.1.1" } } diff --git a/packages/hoppscotch-backend/.gitignore b/packages/hoppscotch-backend/.gitignore index 3c0649e8..e7bdbdae 100644 --- a/packages/hoppscotch-backend/.gitignore +++ b/packages/hoppscotch-backend/.gitignore @@ -38,3 +38,6 @@ lerna-debug.log* !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json + +# Generated artifacts (GQL Schema SDL generation etc.) +gen/ diff --git a/packages/hoppscotch-backend/package.json b/packages/hoppscotch-backend/package.json index ccf897ae..ef2e23a4 100644 --- a/packages/hoppscotch-backend/package.json +++ b/packages/hoppscotch-backend/package.json @@ -8,6 +8,7 @@ "scripts": { "prebuild": "rimraf dist", "build": "nest build", + "generate-gql-sdl": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' GENERATE_GQL_SCHEMA=true WHITELISTED_ORIGINS='' nest start", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", @@ -15,6 +16,7 @@ "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", + "postinstall": "prisma generate && pnpm run generate-gql-sdl", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", @@ -76,6 +78,7 @@ "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", + "cross-env": "^7.0.3", "eslint": "^8.29.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/packages/hoppscotch-backend/src/gql-schema.ts b/packages/hoppscotch-backend/src/gql-schema.ts new file mode 100644 index 00000000..dc0121f5 --- /dev/null +++ b/packages/hoppscotch-backend/src/gql-schema.ts @@ -0,0 +1,95 @@ +import { NestFactory } from '@nestjs/core'; +import { + GraphQLSchemaBuilderModule, + GraphQLSchemaFactory, +} from '@nestjs/graphql'; +import { printSchema } from 'graphql/utilities'; +import * as path from 'path'; +import * as fs from 'fs'; +import { ShortcodeResolver } from './shortcode/shortcode.resolver'; +import { TeamCollectionResolver } from './team-collection/team-collection.resolver'; +import { TeamEnvironmentsResolver } from './team-environments/team-environments.resolver'; +import { TeamInvitationResolver } from './team-invitation/team-invitation.resolver'; +import { TeamRequestResolver } from './team-request/team-request.resolver'; +import { TeamMemberResolver } from './team/team-member.resolver'; +import { TeamResolver } from './team/team.resolver'; +import { UserCollectionResolver } from './user-collection/user-collection.resolver'; +import { UserEnvironmentsResolver } from './user-environment/user-environments.resolver'; +import { UserHistoryResolver } from './user-history/user-history.resolver'; +import { UserRequestResolver } from './user-request/resolvers/user-request.resolver'; +import { UserSettingsResolver } from './user-settings/user-settings.resolver'; +import { UserResolver } from './user/user.resolver'; +import { Logger } from '@nestjs/common'; + +/** + * All the resolvers present in the application. + * + * NOTE: This needs to be KEPT UP-TO-DATE to keep the schema accurate + */ +const RESOLVERS = [ + ShortcodeResolver, + TeamResolver, + TeamMemberResolver, + TeamCollectionResolver, + TeamEnvironmentsResolver, + TeamInvitationResolver, + TeamRequestResolver, + UserResolver, + UserCollectionResolver, + UserEnvironmentsResolver, + UserHistoryResolver, + UserCollectionResolver, + UserRequestResolver, + UserSettingsResolver, +]; + +/** + * All the custom scalars present in the application. + * + * NOTE: This needs to be KEPT UP-TO-DATE to keep the schema accurate + */ +const SCALARS = []; + +/** + * Generates the GraphQL Schema SDL definition and writes it into the location + * specified by the `GQL_SCHEMA_EMIT_LOCATION` environment variable. + */ +export async function emitGQLSchemaFile() { + const logger = new Logger('emitGQLSchemaFile'); + + try { + const destination = path.resolve( + __dirname, + process.env.GQL_SCHEMA_EMIT_LOCATION ?? '../gen/schema.gql', + ); + + logger.log(`GQL_SCHEMA_EMIT_LOCATION: ${destination}`); + + const app = await NestFactory.create(GraphQLSchemaBuilderModule); + await app.init(); + + const gqlSchemaFactory = app.get(GraphQLSchemaFactory); + + logger.log( + `Generating Schema against ${RESOLVERS.length} resolvers and ${SCALARS.length} custom scalars`, + ); + + const schema = await gqlSchemaFactory.create(RESOLVERS, SCALARS); + + const schemaString = printSchema(schema, { + commentDescriptions: true, + }); + + logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`); + + // Generating folders if required to emit to the given output + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.writeFileSync(destination, schemaString); + + logger.log(`Wrote schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`); + } catch (e) { + logger.error( + `Failed writing schema to GQL_SCHEMA_EMIT_LOCATION. Reason: ${e}`, + ); + } +} diff --git a/packages/hoppscotch-backend/src/main.ts b/packages/hoppscotch-backend/src/main.ts index 31f8e737..4e1d06c5 100644 --- a/packages/hoppscotch-backend/src/main.ts +++ b/packages/hoppscotch-backend/src/main.ts @@ -3,6 +3,7 @@ import { json } from 'express'; import { AppModule } from './app.module'; import * as cookieParser from 'cookie-parser'; import { VersioningType } from '@nestjs/common'; +import { emitGQLSchemaFile } from './gql-schema'; async function bootstrap() { console.log(`Running in production: ${process.env.PRODUCTION}`); @@ -38,4 +39,9 @@ async function bootstrap() { app.use(cookieParser()); await app.listen(process.env.PORT || 3170); } -bootstrap(); + +if (!process.env.GENERATE_GQL_SCHEMA) { + bootstrap(); +} else { + emitGQLSchemaFile(); +} diff --git a/packages/hoppscotch-common/gql-codegen.yml b/packages/hoppscotch-common/gql-codegen.yml index 95aae725..05927a7c 100644 --- a/packages/hoppscotch-common/gql-codegen.yml +++ b/packages/hoppscotch-common/gql-codegen.yml @@ -1,6 +1,5 @@ overwrite: true -schema: - - ${VITE_BACKEND_GQL_URL} +schema: "../../gql-gen/*.gql" generates: src/helpers/backend/graphql.ts: documents: "src/**/*.graphql" diff --git a/packages/hoppscotch-sh-admin/gql-codegen.yml b/packages/hoppscotch-sh-admin/gql-codegen.yml index 5b4e1855..76892aa9 100644 --- a/packages/hoppscotch-sh-admin/gql-codegen.yml +++ b/packages/hoppscotch-sh-admin/gql-codegen.yml @@ -1,5 +1,5 @@ overwrite: true -schema: ${VITE_BACKEND_GQL_URL} +schema: "../../gql-gen/*.gql" generates: src/helpers/backend/graphql.ts: documents: 'src/**/*.graphql' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index daa4f49b..b56665a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ importers: '@commitlint/cli': ^16.2.3 '@commitlint/config-conventional': ^16.2.1 '@types/node': ^17.0.24 + cross-env: ^7.0.3 http-server: ^14.1.1 husky: ^7.0.4 lint-staged: ^12.3.8 @@ -17,6 +18,7 @@ importers: '@commitlint/cli': 16.3.0 '@commitlint/config-conventional': 16.2.4 '@types/node': 17.0.45 + cross-env: 7.0.3 http-server: 14.1.1 packages/codemirror-lang-graphql: @@ -77,6 +79,7 @@ importers: bcrypt: ^5.1.0 cookie: ^0.5.0 cookie-parser: ^1.4.6 + cross-env: ^7.0.3 eslint: ^8.29.0 eslint-config-prettier: ^8.5.0 eslint-plugin-prettier: ^4.2.1 @@ -166,6 +169,7 @@ importers: '@types/supertest': 2.0.12 '@typescript-eslint/eslint-plugin': 5.45.0_yjegg5cyoezm3fzsmuszzhetym '@typescript-eslint/parser': 5.45.0_s5ps7njkmjlaqajutnox5ntcla + cross-env: 7.0.3 eslint: 8.29.0 eslint-config-prettier: 8.5.0_eslint@8.29.0 eslint-plugin-prettier: 4.2.1_eabs6augosioka4cd2cz5tdama @@ -9957,7 +9961,7 @@ packages: dev: false /concat-map/0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} /concat-stream/1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}