diff --git a/packages/commands/getschema/.gitignore b/packages/commands/getschema/.gitignore new file mode 100644 index 000000000..48bc1ab0c --- /dev/null +++ b/packages/commands/getschema/.gitignore @@ -0,0 +1,6 @@ +node_modules +npm-debug.log +dist +temp +yarn-error.log +tests/coverage/ \ No newline at end of file diff --git a/packages/commands/getschema/README.md b/packages/commands/getschema/README.md new file mode 100644 index 000000000..cae312279 --- /dev/null +++ b/packages/commands/getschema/README.md @@ -0,0 +1,4 @@ +## GraphQL CLI GetSchema command + +This package is part of the GraphQL CLI ecosystem and it is not designed to be consumed separately. +Go to: https://github.com/Urigo/graphql-cli for more information diff --git a/packages/commands/getschema/package.json b/packages/commands/getschema/package.json new file mode 100644 index 000000000..66f687da1 --- /dev/null +++ b/packages/commands/getschema/package.json @@ -0,0 +1,29 @@ +{ + "name": "@graphql-cli/getschema", + "description": "Get schema from url", + "version": "4.0.0", + "license": "MIT", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/Urigo/graphql-cli", + "directory": "packages/commands/getschema" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "graphql-cli", + "graphql-cli-plugin" + ], + "scripts": { + "build": "tsc" + }, + "peerDependencies": { + "graphql": "15.3.0" + }, + "dependencies": { + "@graphql-cli/common": "4.0.0", + "tslib": "2.0.1" + } +} diff --git a/packages/commands/getschema/src/index.ts b/packages/commands/getschema/src/index.ts new file mode 100644 index 000000000..fcff6ef7a --- /dev/null +++ b/packages/commands/getschema/src/index.ts @@ -0,0 +1,201 @@ +import * as fs from 'fs' +import * as os from 'os' +import * as mkdirp from 'mkdirp' +import { relative, dirname } from 'path' +import { printSchema, GraphQLSchema, buildClientSchema, validateSchema } from 'graphql' +import chalk from 'chalk' +import { Arguments } from 'yargs' + +import { defineCommand } from '@graphql-cli/common'; + +export default defineCommand(() => { + return { + command: 'get-schema', + builder(builder: any) { + return builder.options({ + endpoint: { + alias: 'e', + describe: 'Endpoint name or URL', + type: 'string', + }, + json: { + alias: 'j', + describe: 'Output as JSON', + type: 'boolean', + }, + output: { + alias: 'o', + describe: 'Output file name', + type: 'string', + }, + console: { + alias: 'c', + describe: 'Output to console', + default: false, + }, + insecure: { + alias: 'i', + describe: 'Allow insecure (self-signed) certificates', + type: 'boolean', + }, + header: { + describe: + 'Header to use for downloading (with endpoint URL). Format: Header=Value', + type: 'string', + }, + }); + }, + handler, + }; +}); + +const handler = async (args: any) => { + if (args.endpoint) { + args.all = false + } + + if (args.all && !args.project) { + args.project = args.endpoint = '*' + } + + if (args.insecure) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + } + + if (!args.watch) { + await updateWrapper(args) + } +} + +async function updateWrapper(argv: any) { + try { + await update(argv) + } catch (err) { + console.error(err) + } +} + +async function update(argv: Arguments) { + if (argv.endpoint) { + if (argv.output || argv.console) { + await downloadFromEndpointUrl(argv) + return + } else { + console.log('No output file specified!'); + } + } +} + +async function downloadFromEndpointUrl(argv: Arguments) { + const endpointHeaders = {} + if (argv.header) { + const headers = Array.isArray(argv.header) ? argv.header : [argv.header] + Object.assign( + endpointHeaders, + ...headers.map(h => ({ [h.split('=')[0]]: h.split('=')[1] })), + ) + } + + const endpoint = { + url: argv.endpoint, + headers: endpointHeaders, + } + + await updateSingleProjectEndpoint(endpoint, argv) +} + +async function updateSingleProjectEndpoint( + endpoint: { url: string, headers: string[] }, + argv: Arguments, +): Promise { + console.info(`Downloading introspection from ${chalk.blue(endpoint.url)}`) + let newSchemaResult + try { + newSchemaResult = argv.json + // TODO figure out how to resolve it using graphql-tools + ? await endpoint.resolveIntrospection() + : await endpoint.resolveSchema() + + // Do not save an invalid schema + const clientSchema = argv.json + ? buildClientSchema(newSchemaResult) + : newSchemaResult + const errors = validateSchema(clientSchema) + if (errors.length > 0) { + console.error(chalk.red(`${os.EOL}GraphQL endpoint generated invalid schema: ${errors}`)) + setTimeout(() => { + process.exit(1) + }, 500) + return + } + } catch (err) { + console.log('warning', err.message) + return + } + + let oldSchema: string | undefined + if (!argv.console) { + try { + oldSchema = argv.output + ? fs.readFileSync(argv.output as string, 'utf-8') : "" + } catch (e) { + // ignore error if no previous schema file existed + if (e.message === 'Unsupported schema file extention. Only ".graphql" and ".json" are supported') { + console.error(e.message) + setTimeout(() => { + process.exit(1) + }, 500) + } + // TODO: Add other non-blocking errors to this list + if (e.message.toLowerCase().indexOf('syntax error') > -1) { + console.log(`${os.EOL}Ignoring existing schema because it is invalid: ${chalk.red(e.message)}`) + } else if (e.code !== 'ENOENT') { + throw e + } + } + + if (oldSchema) { + const newSchema = argv.json + ? JSON.stringify(newSchemaResult, null, 2) + : printSchema(newSchemaResult as GraphQLSchema) + if (newSchema === oldSchema) { + console.log( + chalk.green( + `No changes in schema`, + ), + ) + return + } + } + } + + let schemaPath: any = argv.output + if (argv.console) { + console.log( + argv.json + ? JSON.stringify(newSchemaResult, null, 2) + : printSchema(newSchemaResult as GraphQLSchema), + ) + } else if (argv.json) { + if (!fs.existsSync(schemaPath)) { + mkdirp.sync(dirname(schemaPath)) + } + fs.writeFileSync(argv.output as any, JSON.stringify(newSchemaResult, null, 2)) + } else { + console.info("No output provided. Using JSON"); + if (!fs.existsSync(schemaPath)) { + mkdirp.sync(dirname(schemaPath)) + } + fs.writeFileSync(argv.output as any, JSON.stringify(newSchemaResult, null, 2)) + } + + if (schemaPath) { + console.log( + chalk.green( + `Schema file was ${oldSchema ? 'updated' : 'created'}: ${chalk.blue( + relative(process.cwd(), schemaPath), + )}`, + ), + ) + } +} \ No newline at end of file diff --git a/packages/commands/getschema/tsconfig.json b/packages/commands/getschema/tsconfig.json new file mode 100644 index 000000000..6f8237777 --- /dev/null +++ b/packages/commands/getschema/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "esModuleInterop": true, + "importHelpers": true, + "experimentalDecorators": true, + "module": "commonjs", + "target": "es2018", + "lib": ["es6", "esnext", "es2015", "dom"], + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "sourceMap": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "noImplicitAny": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": false + }, + "files": ["src/index.ts"], + "exclude": ["node_modules"] +} diff --git a/website/docs/command-getschema.md b/website/docs/command-getschema.md new file mode 100644 index 000000000..d2172bd06 --- /dev/null +++ b/website/docs/command-getschema.md @@ -0,0 +1,150 @@ +--- +id: getschema +title: get-schema +sidebar_label: get-schema +--- + +Serves a full featured [GraphQL CRUD](https://graphqlcrud.org/) API with subscriptions and data synchronization running in just a few seconds without writing a single line of code - all you need is a data model `.graphql` file. + +GraphQL Serve is a CLI tool that leverages the power of Graphback to generate a codeless Node.js GraphQL API complete with schema and CRUD resolvers and an in-memory MongoDB database. + +### Installation + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + + ``` + yarn global add @graphql-cli/serve + ``` + + + + + + ``` + npm i -g @graphql-cli/serve + ``` + + + + +### Usage + +The bare minimum you need is a GraphQL file with your data models. Create a file called `Note.graphql` and add the following: + +```graphql +""" @model """ +type Note { + _id: GraphbackObjectID! + title: String! + description: String + likes: Int +} + +scalar GraphbackObjectID +``` + +The `@model` annotation indicates that `Note` is a data model and Graphback will generate resolvers, a CRUD service and data source for it. You can learn how to build more complex data models in [Data Model](https://graphback.dev/docs/model/datamodel#model). + +#### Running your codeless GraphQL server + +To start your server, run the following command from the same directory as `Note.graphql`: + +```bash +graphql serve Note.graphql +``` + +This will start a GraphQL server on a random port using the `Note.graphql` data models we just added. + +You can customise the directory of the data models: + +```bash +graphql serve ./path/to/models +``` + +You can also specify where to load the data models from with a Glob pattern: + +```bash +graphql serve ./schema/**/*.graphql +``` + +You can specify which port to start the server on: + +```bash +$ graphql serve ./path/to/models --port 8080 + +Starting server... + +Listening at: http://localhost:8080/graphql +``` + +### Enable Data Synchronization + +GraphQL Serve can also operate on data sync models. Under the hood this uses the [Data Sync](https://graphback.dev/docs/datasync/intro) package. +To enable data synchronization, all we need to do is enable datasync capabilities on our models via the `@datasync` annotation. + +For the `Note` model defined above, this would look like: + +```graphql +""" +@model +@datasync +""" +type Note { + _id: GraphbackObjectID! + title: String! + description: String + likes: Int +} + +scalar GraphbackObjectID +``` + +Once we have a model with datasync capabilities, we can run our GraphQL server by enabling data synchronization as shown below: + +```bash +graphql serve Note.graphql --datasync +``` + +Conflict resolution strategies for datasync enabled models can be specified via the --conflict option: + +```bash +graphql serve Note.graphql --datasync --conflict=clientSideWins +``` + +This defaults to ClientSideWins, if unset. + +The TTL for delta tables, can also be set using the --deltaTTL option: + +```bash +graphql serve Note.graphql --datasync --deltaTTL=172800 +``` + +This value defaults to `172800` when unused + + +#### Arguments + +| argument | description | default | +| --- | --- | --- | +| `Model` | Directory to search for data models | `undefined` | + +#### Options + +| option | alias | description | default | +| --- | --- | --- | --- | +| `--port` | `-p` | Port on which to run the HTTP server | `Random port` | +| `--datasync` | `--ds` | Enable datasynchronization features | `false` | +| `--deltaTTL` | N/A | Specify a conflict resolution strategy with --datasync. Choices: `clientSideWins`, `serverSideWins`, `throwOnConflict` | `clientSideWins` | +| `--conflict` | N/A | Specify a TTL for delta tables with --datasync | `172800` | + +