From c661bf63e8ddb8e68a5d93eb0f80b0448acba704 Mon Sep 17 00:00:00 2001 From: Minghe Date: Sat, 16 Mar 2024 22:09:51 +0800 Subject: [PATCH 1/2] feat(cors): add cors middleware (highly borrow from hono) (#62) --- README.md | 1 + package.json | 7 ++- src/app.ts | 6 --- src/middleware/cors/index.test.ts | 75 +++++++++++++++++++++++++++ src/middleware/cors/index.ts | 86 +++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 src/middleware/cors/index.test.ts create mode 100644 src/middleware/cors/index.ts diff --git a/README.md b/README.md index 419a605..37f3499 100644 --- a/README.md +++ b/README.md @@ -149,4 +149,5 @@ app.use(responseTime); The builtin middlewares are, * [JWT](src/middleware/jwt) +* [CORS](src/middleware/cors) * [wallclock](src/middleware/wallclock) diff --git a/package.json b/package.json index 5fe567a..e567e56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "edgeql", - "version": "0.1.9", + "version": "0.2.0", "author": "Minghe Huang (https://github.com/metrue)", "license": "MIT", "repository": { @@ -74,6 +74,11 @@ "import": "./dist/esm/middleware/wallclock/index.js", "require": "./dist/cjs/middleware/wallclock/index.js" }, + "./cors": { + "types": "./dist/esm/middleware/cors/index.d.ts", + "import": "./dist/esm/middleware/cors/index.js", + "require": "./dist/cjs/middleware/cors/index.js" + }, "./utils/jwt": { "types": "./dist/esm/utils/jwt/index.d.ts", "import": "./dist/esm/utils/jwt/index.js", diff --git a/src/app.ts b/src/app.ts index 95d1abb..1141c21 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,12 +16,6 @@ export class EdgeQL { env?: Environment, exeContext?: ExecutionContext ): Promise => { - if (request.method !== 'GET' && request.method !== 'POST') { - return new Response('GraphQL only supports GET and POST requests.', { - status: 405, - }) - } - let ctx: Context try { ctx = new Context(request, env, exeContext) diff --git a/src/middleware/cors/index.test.ts b/src/middleware/cors/index.test.ts new file mode 100644 index 0000000..99cd434 --- /dev/null +++ b/src/middleware/cors/index.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest' +import { EdgeQL } from '../../index' +import type { Context } from '../../index' +import { cors } from '../../middleware/cors' + +describe('CORS by Middleware', () => { + it('GET default', async () => { + const app = new EdgeQL() + + app.handle( + ` +type Query { + hello: String +} + `, + (ctx: Context) => { + return `hello from ${ctx.runtime.runtime}` + } + ) + app.use(cors()) + const res = await app.fetch(new Request('http://localhost')) + + expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*') + expect(res.headers.get('Vary')).toBeNull() + expect(res.status).toBe(400) + expect(await res.json()).toEqual({ + data: null, + errors: [ + { + extensions: { + status: 400, + }, + message: 'Must provide query string', + }, + ], + }) + }) + + it('Preflight default', async () => { + const app = new EdgeQL() + + app.handle( + ` +type Query { + hello: String +} + `, + (ctx: Context) => { + return `hello from ${ctx.runtime.runtime}` + } + ) + app.use(cors()) + app.use( + cors({ + origin: 'http://example.com', + allowHeaders: ['X-Custom-Header', 'Upgrade-Insecure-Requests'], + allowMethods: ['POST', 'GET', 'OPTIONS'], + exposeHeaders: ['Content-Length', 'X-Kuma-Revision'], + maxAge: 600, + credentials: true, + }) + ) + + const req = new Request('http://example.com', { method: 'OPTIONS' }) + req.headers.append('Access-Control-Request-Headers', 'X-PINGOTHER, Content-Type') + const res = await app.fetch(req) + + expect(res.status).toBe(204) + expect(res.headers.get('Access-Control-Allow-Methods')?.split(',')[0]).toBe('GET') + expect(res.headers.get('Access-Control-Allow-Headers')?.split(',')).toEqual([ + 'X-PINGOTHER', + 'Content-Type', + ]) + }) +}) diff --git a/src/middleware/cors/index.ts b/src/middleware/cors/index.ts new file mode 100644 index 0000000..9886222 --- /dev/null +++ b/src/middleware/cors/index.ts @@ -0,0 +1,86 @@ +import type { Middleware } from '../../types' + +type CORSOptions = { + origin: string | string[] | ((origin: string) => string | undefined | null) + allowMethods?: string[] + allowHeaders?: string[] + maxAge?: number + credentials?: boolean + exposeHeaders?: string[] +} + +export const cors = (options?: CORSOptions): Middleware => { + const defaults: CORSOptions = { + origin: '*', + allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH'], + allowHeaders: [], + exposeHeaders: [], + } + const opts = { + ...defaults, + ...options, + } + + const findAllowOrigin = ((optsOrigin) => { + if (typeof optsOrigin === 'string') { + return () => optsOrigin + } else if (typeof optsOrigin === 'function') { + return optsOrigin + } else { + return (origin: string) => (optsOrigin.includes(origin) ? origin : optsOrigin[0]) + } + })(opts.origin) + + return async (c, next) => { + function set(key: string, value: string) { + c.http.headers.set(key, value) + } + + const allowOrigin = findAllowOrigin(c.http.request.headers.get('origin') || '') + if (allowOrigin) { + set('Access-Control-Allow-Origin', allowOrigin) + } + + // Suppose the server sends a response with an Access-Control-Allow-Origin value with an explicit origin (rather than the "*" wildcard). + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + if (opts.origin !== '*') { + set('Vary', 'Origin') + } + + if (opts.credentials) { + set('Access-Control-Allow-Credentials', 'true') + } + + if (opts.exposeHeaders?.length) { + set('Access-Control-Expose-Headers', opts.exposeHeaders.join(',')) + } + + if (c.http.request.method === 'OPTIONS') { + if (opts.maxAge != null) { + set('Access-Control-Max-Age', opts.maxAge.toString()) + } + + if (opts.allowMethods?.length) { + set('Access-Control-Allow-Methods', opts.allowMethods.join(',')) + } + + let headers = opts.allowHeaders + if (!headers?.length) { + const requestHeaders = c.http.request.headers.get('Access-Control-Request-Headers') + if (requestHeaders) { + headers = requestHeaders.split(/\s*,\s*/) + } + } + if (headers?.length) { + set('Access-Control-Allow-Headers', headers.join(',')) + c.http.headers.append('Vary', 'Access-Control-Request-Headers') + } + + c.http.status = 204 + c.http.headers.delete('Content-Length') + c.http.headers.delete('Content-Type') + } else { + await next() + } + } +} From 2bcc7cdd375f4b47d6064557c7e4852f65896511 Mon Sep 17 00:00:00 2001 From: Minghe Date: Sat, 16 Mar 2024 23:50:33 +0800 Subject: [PATCH 2/2] fix(types): export types (#64) --- examples/node/src/app.ts | 3 +++ package.json | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/node/src/app.ts b/examples/node/src/app.ts index 990a55e..a686840 100644 --- a/examples/node/src/app.ts +++ b/examples/node/src/app.ts @@ -1,4 +1,5 @@ import type { Context } from 'edgeql' +import { cors } from 'edgeql/cors' import { NodeEdgeQL } from 'edgeql/node' const app = new NodeEdgeQL() @@ -11,6 +12,8 @@ type Query { return `hello from EdgeQL on ${ctx.runtime.runtime}` }) +app.use(cors()) + app.listen({port: 4000}, ({address, family, port}) => { console.log(address, family, port) }) diff --git a/package.json b/package.json index e567e56..f594a2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "edgeql", - "version": "0.2.0", + "version": "0.2.1", "author": "Minghe Huang (https://github.com/metrue)", "license": "MIT", "repository": { @@ -101,6 +101,9 @@ "middleware/wallclock": [ "./dist/esm/middleware/wallclock" ], + "middleware/cors": [ + "./dist/esm/middleware/cors" + ], "utils/jwt": [ "./dist/esm/utils/jwt/index.d.ts" ],