├── mod.ts ├── .gitignore ├── deps.ts ├── examples └── server.ts ├── LICENSE ├── types.ts ├── README.md ├── render-graphiql.ts ├── apply_middleware.ts └── tests └── apply_middleware.test.ts /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./apply_middleware.ts"; 2 | export * from "./types.ts"; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | node_modules 4 | .idea 5 | .vscode 6 | *.iml 7 | *.code-workspace 8 | .DS_Store 9 | sakila.db -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { Accepts } from "https://deno.land/x/accepts/mod.ts"; 2 | 3 | export { 4 | Application, 5 | Context, 6 | HttpException, 7 | } from "https://deno.land/x/abc@v1.0.0-rc8/mod.ts"; 8 | 9 | export * from "https://cdn.pika.dev/graphql@^15.0.0"; 10 | -------------------------------------------------------------------------------- /examples/server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Application, 3 | } from "https://deno.land/x/abc@v1.0.0-rc8/mod.ts"; 4 | import { 5 | GraphQLObjectType, 6 | GraphQLSchema, 7 | GraphQLString, 8 | } from "https://cdn.pika.dev/graphql@^15.0.0"; 9 | 10 | import { applyMiddleware } from "../mod.ts"; 11 | 12 | const schema = new GraphQLSchema({ 13 | query: new GraphQLObjectType({ 14 | name: "Query", 15 | fields: () => ({ 16 | hello: { 17 | type: GraphQLString, 18 | resolve: () => "Hello World!", 19 | }, 20 | }), 21 | }), 22 | }); 23 | 24 | const app = new Application(); 25 | 26 | applyMiddleware({ app, schema }); 27 | 28 | app.start({ port: 4000 }); 29 | 30 | console.log("Started server"); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Daniel Rearden 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Application, 3 | ASTVisitor, 4 | Context, 5 | DocumentNode, 6 | ExecutionArgs, 7 | ExecutionResult, 8 | GraphQLError, 9 | GraphQLFieldResolver, 10 | GraphQLSchema, 11 | GraphQLTypeResolver, 12 | Source, 13 | ValidationContext, 14 | ValidationRule, 15 | } from "./deps.ts"; 16 | 17 | export interface GraphQLMiddlewareOptions { 18 | /** 19 | * ABC Application to which apply the middleware 20 | */ 21 | app: Application; 22 | 23 | /** 24 | * The route path for the GraphQL endpoint. Defaults to "/graphql". 25 | */ 26 | path?: string; 27 | 28 | /** 29 | * A boolean to optionally enable GraphiQL mode. 30 | * Alternatively, instead of `true` you can pass in an options object. 31 | */ 32 | graphiql?: boolean | GraphiQLOptions; 33 | 34 | /** 35 | * The schema against which queries will be executed. 36 | */ 37 | schema: GraphQLSchema; 38 | 39 | /** 40 | * An object to pass as the root value. 41 | */ 42 | rootValue?: any; 43 | 44 | /** 45 | * The value to be used as the context provided to the schema's resolvers. 46 | * If a function is provided, it will be passed the ABC Context as a parameter. 47 | * The function may return a Promise. If no value is provided, the ABC Context will be used. 48 | */ 49 | context?: GraphQLContext; 50 | 51 | /** 52 | * A resolver function to use when one is not provided by the schema. 53 | * If not provided, the default field resolver is used (which looks for a 54 | * value or method on the source value with the field's name). 55 | */ 56 | fieldResolver?: GraphQLFieldResolver; 57 | 58 | /** 59 | * A type resolver function to use when none is provided by the schema. 60 | * If not provided, the default type resolver is used (which looks for a 61 | * `__typename` field or alternatively calls the `isTypeOf` method). 62 | */ 63 | typeResolver?: GraphQLTypeResolver; 64 | 65 | /** 66 | * An optional array of validation rules that will be applied on the document 67 | * in additional to those defined by the GraphQL spec. 68 | */ 69 | validationRules?: ReadonlyArray<(ctx: ValidationContext) => ASTVisitor>; 70 | 71 | /** 72 | * An optional function which will be used to validate instead of default `validate` 73 | * from `graphql-js`. 74 | */ 75 | validateFn?: ( 76 | schema: GraphQLSchema, 77 | documentAST: DocumentNode, 78 | rules: ReadonlyArray, 79 | ) => ReadonlyArray; 80 | 81 | /** 82 | * An optional function which will be used to execute instead of default `execute` 83 | * from `graphql-js`. 84 | */ 85 | executeFn?: (args: ExecutionArgs) => Promise; 86 | 87 | /** 88 | * An optional function which will be used to format any errors produced by 89 | * fulfilling a GraphQL operation. If no function is provided, GraphQL's 90 | * default spec-compliant `formatError` function will be used. 91 | */ 92 | formatErrorFn?: (error: GraphQLError) => any; 93 | 94 | /** 95 | * An optional function which will be used to create a document instead of 96 | * the default `parse` from `graphql-js`. 97 | */ 98 | parseFn?: (source: Source) => DocumentNode; 99 | } 100 | 101 | export interface GraphiQLOptions { 102 | /** 103 | * An optional GraphQL string to use when no query is provided and no stored 104 | * query exists from a previous session. If undefined is provided, GraphiQL 105 | * will use its own default query. 106 | */ 107 | defaultQuery?: string; 108 | } 109 | 110 | export type GraphQLContext = 111 | | Record 112 | | ((context: Context) => Record | Promise>); 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL ABC 2 | 3 | This is a deno module for creating a GraphQL endpoint. This is largely a port of [express-graphql](https://github.com/graphql/express-graphql). It provides middleware to an existing [abc](https://github.com/zhmushan/abc) application instance. 4 | 5 | ### Usage 6 | 7 | Create a new [abc](https://github.com/zhmushan/abc) Application, then just call `applyMiddleware` passing in the `Application` instance and a `GraphQLSchema` instance. 8 | 9 | ```ts 10 | import { 11 | GraphQLObjectType, 12 | GraphQLSchema, 13 | GraphQLString, 14 | } from "https://cdn.pika.dev/graphql@^15.0.0"; 15 | import { Application } from "https://deno.land/x/abc@v1.0.0-rc8/mod.ts"; 16 | import { applyMiddleware } from "https://raw.githubusercontent.com/danielrearden/graphql-middleware/master/mod.ts"; 17 | 18 | const schema = new GraphQLSchema({ 19 | query: new GraphQLObjectType({ 20 | name: "Query", 21 | fields: () => ({ 22 | hello: { 23 | type: GraphQLString, 24 | resolve: () => "Hello World!", 25 | }, 26 | }), 27 | }), 28 | }); 29 | 30 | const app = new Application(); 31 | 32 | applyMiddleware({ app, schema }); 33 | 34 | app.start({ port: 4000 }); 35 | ``` 36 | 37 | ### Configuration 38 | 39 | ```ts 40 | export interface GraphQLMiddlewareOptions { 41 | /** 42 | * ABC Application to which apply the middleware 43 | */ 44 | app: Application; 45 | 46 | /** 47 | * The route path for the GraphQL endpoint. Defaults to "/graphql". 48 | */ 49 | path?: string; 50 | 51 | /** 52 | * A boolean to optionally enable GraphiQL mode. 53 | * Alternatively, instead of `true` you can pass in an options object. 54 | */ 55 | graphiql?: boolean | GraphiQLOptions; 56 | 57 | /** 58 | * The schema against which queries will be executed. 59 | */ 60 | schema: GraphQLSchema; 61 | 62 | /** 63 | * An object to pass as the root value. 64 | */ 65 | rootValue?: any; 66 | 67 | /** 68 | * The value to be used as the context provided to the schema's resolvers. 69 | * If a function is provided, it will be passed the ABC Context as a parameter. 70 | * The function may return a Promise. If no value is provided, the ABC Context will be used. 71 | */ 72 | context?: GraphQLContext; 73 | 74 | /** 75 | * A resolver function to use when one is not provided by the schema. 76 | * If not provided, the default field resolver is used (which looks for a 77 | * value or method on the source value with the field's name). 78 | */ 79 | fieldResolver?: GraphQLFieldResolver; 80 | 81 | /** 82 | * A type resolver function to use when none is provided by the schema. 83 | * If not provided, the default type resolver is used (which looks for a 84 | * `__typename` field or alternatively calls the `isTypeOf` method). 85 | */ 86 | typeResolver?: GraphQLTypeResolver; 87 | 88 | /** 89 | * An optional array of validation rules that will be applied on the document 90 | * in additional to those defined by the GraphQL spec. 91 | */ 92 | validationRules?: ReadonlyArray<(ctx: ValidationContext) => ASTVisitor>; 93 | 94 | /** 95 | * An optional function which will be used to validate instead of default `validate` 96 | * from `graphql-js`. 97 | */ 98 | validateFn?: ( 99 | schema: GraphQLSchema, 100 | documentAST: DocumentNode, 101 | rules: ReadonlyArray, 102 | ) => ReadonlyArray; 103 | 104 | /** 105 | * An optional function which will be used to execute instead of default `execute` 106 | * from `graphql-js`. 107 | */ 108 | executeFn?: (args: ExecutionArgs) => Promise; 109 | 110 | /** 111 | * An optional function which will be used to format any errors produced by 112 | * fulfilling a GraphQL operation. If no function is provided, GraphQL's 113 | * default spec-compliant `formatError` function will be used. 114 | */ 115 | formatErrorFn?: (error: GraphQLError) => any; 116 | 117 | /** 118 | * An optional function which will be used to create a document instead of 119 | * the default `parse` from `graphql-js`. 120 | */ 121 | parseFn?: (source: Source) => DocumentNode; 122 | } 123 | 124 | export interface GraphiQLOptions { 125 | /** 126 | * An optional GraphQL string to use when no query is provided and no stored 127 | * query exists from a previous session. If undefined is provided, GraphiQL 128 | * will use its own default query. 129 | */ 130 | defaultQuery?: string; 131 | } 132 | ``` 133 | -------------------------------------------------------------------------------- /render-graphiql.ts: -------------------------------------------------------------------------------- 1 | export function renderGraphiQL( 2 | queryString?: string, 3 | defaultQuery?: string, 4 | variables?: Record, 5 | operationName?: string, 6 | result: any = null, 7 | ) { 8 | const variablesString = variables ? JSON.stringify(variables, null, 2) : null; 9 | const resultString = result ? JSON.stringify(result, null, 2) : null; 10 | 11 | return ` 12 | 13 | 14 | 15 | 16 | GraphiQL 17 | 18 | 19 | 20 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Loading...
41 | 120 | 121 | 122 | `; 123 | } 124 | 125 | function safeSerialize(data: any) { 126 | return data != null 127 | ? JSON.stringify(data).replace(/\//g, "\\/") 128 | : "undefined"; 129 | } 130 | -------------------------------------------------------------------------------- /apply_middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatError, 3 | execute, 4 | parse, 5 | specifiedRules, 6 | validate, 7 | validateSchema, 8 | Accepts, 9 | Context, 10 | DocumentNode, 11 | GraphQLError, 12 | HttpException, 13 | Source, 14 | } from "./deps.ts"; 15 | import { GraphQLMiddlewareOptions } from "./types.ts"; 16 | import { renderGraphiQL } from "./render-graphiql.ts"; 17 | 18 | export function applyMiddleware(options: GraphQLMiddlewareOptions) { 19 | const { 20 | app, 21 | path = "/graphql", 22 | graphiql = true, 23 | schema, 24 | rootValue, 25 | fieldResolver, 26 | typeResolver, 27 | validationRules = [], 28 | validateFn = validate, 29 | executeFn = execute, 30 | formatErrorFn = formatError, 31 | parseFn = parse, 32 | } = options; 33 | const defaultQuery = typeof graphiql === "object" 34 | ? graphiql.defaultQuery 35 | : undefined; 36 | 37 | const processRequest = async ( 38 | context: Context, 39 | query: string, 40 | variableValues?: Record, 41 | operationName?: string, 42 | ) => { 43 | if (!query) { 44 | throw new HttpException("Must provide query string.", 400); 45 | } 46 | 47 | const result = await executeQuery( 48 | context, 49 | query, 50 | variableValues, 51 | operationName, 52 | ); 53 | 54 | if ("validationErrors" in result) { 55 | throw new HttpException({ errors: result.validationErrors }, 400); 56 | } else if ("serverErrors" in result) { 57 | throw new HttpException( 58 | { errors: result.serverErrors.map(({ message }) => ({ message })) }, 59 | 500, 60 | ); 61 | } else { 62 | return { 63 | data: result.data, 64 | errors: result.errors ? result.errors.map(formatErrorFn) : undefined, 65 | }; 66 | } 67 | }; 68 | 69 | const executeQuery = async ( 70 | context: Context, 71 | query: string, 72 | variableValues?: Record, 73 | operationName?: string, 74 | ): Promise< 75 | | { serverErrors: readonly Error[] } 76 | | { validationErrors: readonly GraphQLError[] } 77 | | { 78 | data: 79 | | { 80 | [key: string]: any; 81 | } 82 | | null 83 | | undefined; 84 | errors: readonly GraphQLError[] | undefined; 85 | } 86 | > => { 87 | // validateSchema caches the result so it will not be validated on every request 88 | const schemaValidationErrors = validateSchema(schema); 89 | if (schemaValidationErrors.length) { 90 | return { validationErrors: schemaValidationErrors }; 91 | } 92 | 93 | const source = new Source(query); 94 | let document: DocumentNode; 95 | 96 | try { 97 | document = parseFn(source); 98 | } catch (syntaxError) { 99 | return { validationErrors: [syntaxError] }; 100 | } 101 | 102 | const validationErrors = validateFn( 103 | schema, 104 | document, 105 | [...specifiedRules, ...validationRules], 106 | ); 107 | 108 | if (validationErrors.length > 0) { 109 | return { validationErrors }; 110 | } 111 | 112 | let contextValue: any; 113 | 114 | try { 115 | contextValue = typeof options.context === "function" 116 | ? await options.context(context) 117 | : options.context || context; 118 | } catch (error) { 119 | return { serverErrors: [error] }; 120 | } 121 | 122 | let { data, errors } = await executeFn({ 123 | schema, 124 | document, 125 | rootValue, 126 | contextValue, 127 | variableValues, 128 | operationName, 129 | fieldResolver, 130 | typeResolver, 131 | }); 132 | 133 | if (errors) { 134 | errors = errors.map(formatErrorFn); 135 | } 136 | 137 | return { data, errors }; 138 | }; 139 | 140 | const graphqlMiddleware = async (context: Context) => { 141 | const accept = new Accepts(context.request.headers); 142 | const acceptedTypes = accept.types(["json", "html"]); 143 | 144 | if (context.method === "POST") { 145 | const contentType = context.request.headers.get("content-type"); 146 | 147 | if (contentType !== "application/json") { 148 | throw new HttpException( 149 | `Invalid Content-Type header. Only application/json is supported.`, 150 | 400, 151 | ); 152 | } 153 | 154 | const { query, variables, operationName } = await context.body(); 155 | return processRequest( 156 | context, 157 | query, 158 | variables, 159 | operationName, 160 | ); 161 | } else if (context.method === "GET") { 162 | const { query, variables, operationName } = context.queryParams; 163 | const variableValues = variables ? JSON.parse(variables) : undefined; 164 | const showGraphiQL = graphiql && !context.queryParams.raw && 165 | acceptedTypes[0] === "html"; 166 | 167 | if (showGraphiQL) { 168 | let result: any = null; 169 | if (query || defaultQuery) { 170 | const executionResult = await executeQuery( 171 | context, 172 | query || defaultQuery || "", 173 | variableValues, 174 | operationName, 175 | ); 176 | if ("validationErrors" in executionResult) { 177 | result = { errors: executionResult.validationErrors }; 178 | } else if ("serverErrors" in executionResult) { 179 | result = { 180 | errors: executionResult.serverErrors.map(({ message }) => ({ 181 | message, 182 | })), 183 | }; 184 | } else { 185 | result = executionResult; 186 | } 187 | } 188 | 189 | return renderGraphiQL( 190 | query, 191 | defaultQuery, 192 | variableValues, 193 | operationName, 194 | result, 195 | ); 196 | } else { 197 | return processRequest( 198 | context, 199 | query, 200 | variableValues, 201 | operationName, 202 | ); 203 | } 204 | } else { 205 | throw new HttpException(`Unsupported method: ${context.method}`, 400); 206 | } 207 | }; 208 | 209 | app.post(path, graphqlMiddleware); 210 | app.get(path, graphqlMiddleware); 211 | } 212 | -------------------------------------------------------------------------------- /tests/apply_middleware.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | assertEquals, 4 | } from "https://deno.land/std/testing/asserts.ts"; 5 | import { 6 | Application, 7 | GraphQLObjectType, 8 | GraphQLSchema, 9 | GraphQLString, 10 | } from "../deps.ts"; 11 | import { 12 | applyMiddleware, 13 | GraphQLMiddlewareOptions, 14 | } from "../mod.ts"; 15 | 16 | const schema = new GraphQLSchema({ 17 | query: new GraphQLObjectType({ 18 | name: "Query", 19 | fields: () => ({ 20 | hello: { 21 | type: GraphQLString, 22 | resolve: () => "Hello World!", 23 | }, 24 | }), 25 | }), 26 | }); 27 | 28 | const withApplication = ( 29 | cb: () => Promise, 30 | options: Partial, 31 | ) => { 32 | return async function () { 33 | const app = new Application(); 34 | let error: Error | undefined = undefined; 35 | 36 | applyMiddleware({ app, schema, ...options }); 37 | 38 | app.start({ port: 4000 }); 39 | 40 | try { 41 | await cb(); 42 | } catch (e) { 43 | error = e; 44 | } 45 | 46 | await app.close(); 47 | 48 | if (error) { 49 | throw error; 50 | } 51 | }; 52 | }; 53 | 54 | Deno.test( 55 | "POST with default options", 56 | withApplication(async () => { 57 | const res = await fetch( 58 | "http://localhost:4000/graphql", 59 | { 60 | method: "POST", 61 | headers: { ["content-type"]: "application/json" }, 62 | body: JSON.stringify({ query: "{hello}" }), 63 | }, 64 | ); 65 | const { data, errors } = await res.json(); 66 | assertEquals(data, { hello: "Hello World!" }); 67 | assertEquals(errors, undefined); 68 | }, {}), 69 | ); 70 | 71 | Deno.test( 72 | "POST with wrong content type", 73 | withApplication(async () => { 74 | const res = await fetch( 75 | "http://localhost:4000/graphql", 76 | { 77 | method: "POST", 78 | headers: { ["content-type"]: "application/xml" }, 79 | body: JSON.stringify({ query: "{hello}" }), 80 | }, 81 | ); 82 | const { message } = await res.json(); 83 | assertEquals(res.status, 400); 84 | assertEquals( 85 | message, 86 | "Invalid Content-Type header. Only application/json is supported.", 87 | ); 88 | }, {}), 89 | ); 90 | 91 | Deno.test( 92 | "GET with default options", 93 | withApplication(async () => { 94 | const res = await fetch( 95 | "http://localhost:4000/graphql?query={hello}", 96 | { 97 | method: "GET", 98 | }, 99 | ); 100 | const { data, errors } = await res.json(); 101 | assertEquals(data, { hello: "Hello World!" }); 102 | assertEquals(errors, undefined); 103 | }, {}), 104 | ); 105 | 106 | Deno.test( 107 | "GET from browser", 108 | withApplication(async () => { 109 | const res = await fetch( 110 | "http://localhost:4000/graphql?query={hello}", 111 | { 112 | method: "GET", 113 | headers: { ["accept"]: "text/html" }, 114 | }, 115 | ); 116 | const text = await res.text(); 117 | assert(text.includes("React.createElement(GraphiQL")); 118 | }, {}), 119 | ); 120 | 121 | Deno.test( 122 | "GET with raw parameter", 123 | withApplication(async () => { 124 | const res = await fetch( 125 | "http://localhost:4000/graphql?query={hello}&raw=true", 126 | { 127 | method: "GET", 128 | headers: { ["accept"]: "text/html" }, 129 | }, 130 | ); 131 | const { data, errors } = await res.json(); 132 | assertEquals(data, { hello: "Hello World!" }); 133 | assertEquals(errors, undefined); 134 | }, {}), 135 | ); 136 | 137 | Deno.test( 138 | "GET with no GraphiQL", 139 | withApplication(async () => { 140 | const res = await fetch( 141 | "http://localhost:4000/graphql?query={hello}", 142 | { 143 | method: "GET", 144 | headers: { ["accept"]: "text/html" }, 145 | }, 146 | ); 147 | const { data, errors } = await res.json(); 148 | assertEquals(data, { hello: "Hello World!" }); 149 | assertEquals(errors, undefined); 150 | }, { graphiql: false }), 151 | ); 152 | 153 | Deno.test( 154 | "variables and multiple operations", 155 | withApplication(async () => { 156 | const res = await fetch( 157 | "http://localhost:4000/graphql", 158 | { 159 | method: "POST", 160 | headers: { ["content-type"]: "application/json" }, 161 | body: JSON.stringify({ 162 | query: ` 163 | query Query1($skip: Boolean!) { 164 | hello @skip(if: $skip) 165 | } 166 | 167 | query Query2($skip: Boolean!) { 168 | hello @skip(if: $skip) 169 | } 170 | `, 171 | variables: { skip: true }, 172 | operationName: "Query2", 173 | }), 174 | }, 175 | ); 176 | const { data, errors } = await res.json(); 177 | assertEquals(data, {}); 178 | assertEquals(errors, undefined); 179 | }, {}), 180 | ); 181 | 182 | Deno.test( 183 | "context", 184 | withApplication( 185 | async () => { 186 | const res = await fetch( 187 | "http://localhost:4000/graphql?name=Daenerys", 188 | { 189 | method: "POST", 190 | headers: { ["content-type"]: "application/json" }, 191 | body: JSON.stringify({ query: "{hello}" }), 192 | }, 193 | ); 194 | const { data, errors } = await res.json(); 195 | assertEquals(data, { hello: "Hello, Daenerys!" }); 196 | assertEquals(errors, undefined); 197 | }, 198 | { 199 | schema: new GraphQLSchema( 200 | { 201 | query: new GraphQLObjectType({ 202 | name: "Query", 203 | fields: { 204 | hello: { 205 | type: GraphQLString, 206 | resolve: (_root, _args, ctx) => `Hello, ${ctx.name}!`, 207 | }, 208 | }, 209 | }), 210 | }, 211 | ), 212 | context: ({ queryParams }) => ({ name: queryParams.name }), 213 | }, 214 | ), 215 | ); 216 | 217 | Deno.test( 218 | "invalid document", 219 | withApplication(async () => { 220 | const res = await fetch( 221 | "http://localhost:4000/graphql", 222 | { 223 | method: "POST", 224 | headers: { ["content-type"]: "application/json" }, 225 | body: JSON.stringify({ query: "{hellooo}" }), 226 | }, 227 | ); 228 | const { data, errors } = await res.json(); 229 | assertEquals(data, undefined); 230 | assertEquals( 231 | errors[0].message, 232 | `Cannot query field "hellooo" on type "Query". Did you mean "hello"?`, 233 | ); 234 | }, {}), 235 | ); 236 | 237 | Deno.test( 238 | "syntax error", 239 | withApplication(async () => { 240 | const res = await fetch( 241 | "http://localhost:4000/graphql", 242 | { 243 | method: "POST", 244 | headers: { ["content-type"]: "application/json" }, 245 | body: JSON.stringify({ query: "{{hello}" }), 246 | }, 247 | ); 248 | const { data, errors } = await res.json(); 249 | assertEquals(data, undefined); 250 | assertEquals( 251 | errors[0].message, 252 | `Syntax Error: Expected Name, found "{".`, 253 | ); 254 | }, {}), 255 | ); 256 | 257 | Deno.test( 258 | "context function error", 259 | withApplication(async () => { 260 | const res = await fetch( 261 | "http://localhost:4000/graphql", 262 | { 263 | method: "POST", 264 | headers: { ["content-type"]: "application/json" }, 265 | body: JSON.stringify({ query: "{hello}" }), 266 | }, 267 | ); 268 | const { data, errors } = await res.json(); 269 | console.log("***", errors); 270 | assertEquals(data, undefined); 271 | assertEquals( 272 | errors[0].message, 273 | "Ooops!", 274 | ); 275 | }, { 276 | context: () => { 277 | throw new Error("Ooops!"); 278 | }, 279 | }), 280 | ); 281 | 282 | Deno.test( 283 | "invalid schema", 284 | withApplication( 285 | async () => { 286 | const res = await fetch( 287 | "http://localhost:4000/graphql", 288 | { 289 | method: "POST", 290 | headers: { ["content-type"]: "application/json" }, 291 | body: JSON.stringify({ query: "{hello}" }), 292 | }, 293 | ); 294 | const { data, errors } = await res.json(); 295 | assertEquals(data, undefined); 296 | assertEquals( 297 | errors[0].message, 298 | "Type Query must define one or more fields.", 299 | ); 300 | }, 301 | { 302 | schema: new GraphQLSchema( 303 | { query: new GraphQLObjectType({ name: "Query", fields: {} }) }, 304 | ), 305 | }, 306 | ), 307 | ); 308 | --------------------------------------------------------------------------------