This repository has been archived by the owner on Apr 11, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 387
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #84 from Shopify/add-graphql-proxy
Add GraphQL proxy functionality
- Loading branch information
Showing
6 changed files
with
1,419 additions
and
826 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import http from 'http'; | ||
|
||
import {GraphqlClient} from '../clients/graphql'; | ||
import * as ShopifyErrors from '../error'; | ||
|
||
import loadCurrentSession from './load-current-session'; | ||
|
||
export default async function graphqlProxy(userReq: http.IncomingMessage, userRes: http.ServerResponse): Promise<void> { | ||
const session = await loadCurrentSession(userReq, userRes); | ||
if (!session) { | ||
throw new ShopifyErrors.SessionNotFound( | ||
'Cannot proxy query. No session found.', | ||
); | ||
} else if (!session.accessToken) { | ||
throw new ShopifyErrors.InvalidSession( | ||
'Cannot proxy query. Session not authenticated.', | ||
); | ||
} | ||
|
||
const shopName: string = session.shop; | ||
const token: string = session.accessToken; | ||
let query = ''; | ||
|
||
const promise: Promise<void> = new Promise((resolve, _reject) => { // eslint-disable-line promise/param-names | ||
userReq.on('data', (chunk) => { | ||
query += chunk; | ||
}); | ||
|
||
userReq.on('end', async () => { | ||
let body: unknown = ''; | ||
try { | ||
const options = { | ||
data: query, | ||
}; | ||
const client = new GraphqlClient(shopName, token); | ||
const response = await client.query(options); | ||
body = response.body; | ||
} catch (err) { | ||
let status; | ||
switch (err.constructor.name) { | ||
case 'MissingRequiredArgument': | ||
status = 400; | ||
break; | ||
case 'HttpResponseError': | ||
status = err.code; | ||
break; | ||
case 'HttpThrottlingError': | ||
status = 429; | ||
break; | ||
default: | ||
status = 500; | ||
} | ||
userRes.statusCode = status; | ||
body = err.message; | ||
} finally { | ||
userRes.end(JSON.stringify(body)); | ||
} | ||
return resolve(); | ||
}); | ||
}); | ||
return promise; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import http from 'http'; | ||
|
||
import jwt from 'jsonwebtoken'; | ||
import express from 'express'; | ||
import request from 'supertest'; | ||
|
||
import '../../test/test_helper'; | ||
import {Session} from '../../auth/session'; | ||
import {InvalidSession, SessionNotFound} from '../../error'; | ||
import graphqlProxy from '../graphql_proxy'; | ||
import {Context} from '../../context'; | ||
import {JwtPayload} from '../decode-session-token'; | ||
|
||
const successResponse = { | ||
data: { | ||
shop: { | ||
name: 'Shop', | ||
}, | ||
}, | ||
}; | ||
const shopQuery = `{ | ||
shop { | ||
name | ||
} | ||
}`; | ||
const shop = 'shop.myshopify.com'; | ||
const accessToken = 'dangit'; | ||
let token = ''; | ||
|
||
describe('GraphQL proxy with session', () => { | ||
beforeEach(async () => { | ||
Context.IS_EMBEDDED_APP = true; | ||
Context.initialize(Context); | ||
const jwtPayload: JwtPayload = { | ||
iss: 'https://shop.myshopify.com/admin', | ||
dest: 'https://shop.myshopify.com', | ||
aud: Context.API_KEY, | ||
sub: '1', | ||
exp: Date.now() / 1000 + 3600, | ||
nbf: 1234, | ||
iat: 1234, | ||
jti: '4321', | ||
sid: 'abc123', | ||
}; | ||
|
||
const session = new Session(`shop.myshopify.com_${jwtPayload.sub}`); | ||
session.shop = shop; | ||
session.accessToken = accessToken; | ||
await Context.SESSION_STORAGE.storeSession(session); | ||
token = jwt.sign(jwtPayload, Context.API_SECRET_KEY, {algorithm: 'HS256'}); | ||
}); | ||
|
||
it('can forward query and return response', async () => { | ||
const app = express(); | ||
app.post('/proxy', graphqlProxy); | ||
|
||
fetchMock.mockResponseOnce(JSON.stringify(successResponse)); | ||
const response = await request(app) | ||
.post('/proxy') | ||
.set('authorization', `Bearer ${token}`) | ||
.send(shopQuery) | ||
.expect(200); | ||
|
||
expect(JSON.parse(response.text)).toEqual(successResponse); | ||
}); | ||
|
||
it('rejects if no query', async () => { | ||
const app = express(); | ||
app.post('/proxy', graphqlProxy); | ||
|
||
const response = await request(app) | ||
.post('/proxy') | ||
.set('authorization', `Bearer ${token}`) | ||
.expect(400); | ||
|
||
expect(JSON.parse(response.text)).toEqual('Query missing.'); | ||
}); | ||
}); | ||
|
||
describe('GraphQL proxy', () => { | ||
it('throws an error if no token', async () => { | ||
Context.IS_EMBEDDED_APP = true; | ||
Context.initialize(Context); | ||
const jwtPayload: JwtPayload = { | ||
iss: 'https://test-shop.myshopify.io/admin', | ||
dest: 'https://test-shop.myshopify.io', | ||
aud: Context.API_KEY, | ||
sub: '1', | ||
exp: Date.now() / 1000 + 3600, | ||
nbf: 1234, | ||
iat: 1234, | ||
jti: '4321', | ||
sid: 'abc123', | ||
}; | ||
|
||
const token = jwt.sign(jwtPayload, Context.API_SECRET_KEY, {algorithm: 'HS256'}); | ||
const req = { | ||
headers: { | ||
authorization: `Bearer ${token}`, | ||
}, | ||
} as http.IncomingMessage; | ||
const res = {} as http.ServerResponse; | ||
const session = new Session(`test-shop.myshopify.io_${jwtPayload.sub}`); | ||
Context.SESSION_STORAGE.storeSession(session); | ||
|
||
await expect(graphqlProxy(req, res)).rejects.toThrow(InvalidSession); | ||
}); | ||
|
||
it('throws an error if no session', async () => { | ||
const req = {headers: {}} as http.IncomingMessage; | ||
const res = {} as http.ServerResponse; | ||
await expect(graphqlProxy(req, res)).rejects.toThrow(SessionNotFound); | ||
}); | ||
}); |
Oops, something went wrong.