Skip to content

Commit

Permalink
feat: Customize endpoint's HTTP Method
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Jul 17, 2019
1 parent d02deb6 commit cf0cb63
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 32 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ Whenever Sofa tries to resolve an author of a message, instead of exposing an ID

> Pattern is easy: `Type:field` or `Type`
### Customize endpoint's HTTP Method

Sofa allows you to cutomize the http method. For example, in case you need `POST` instead of `GET` method in one of your query, you do the following:

```typescript
api.use(
'/api',
sofa({
schema,
methodMap: {
'Query.feed': 'POST',
},
})
);
```

When Sofa tries to define a route for `feed` of `Query`, instead of exposing it under `GET` (default for Query type) it will use `POST` method.

> Pattern is easy: `Type.field` where `Type` is your query or mutation type.
### Custom depth limit

Sofa prevents circular references by default, but only one level deep. In order to change it, set the `depthLimit` option to any number:
Expand Down
2 changes: 1 addition & 1 deletion docs/api/ignore.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ api.use(

Whenever Sofa tries to resolve an author of a message, instead of exposing an ID it will pass whole data.

> Pattern is easy: `Type:field` or `Type`
> Pattern is easy: `Type.field` or `Type`
21 changes: 21 additions & 0 deletions docs/api/method-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: Customize endpoint's HTTP Method
---

Sofa allows you to cutomize the http method. For example, in case you need `POST` instead of `GET` method in one of your query, you do the following:

```typescript
api.use(
'/api',
sofa({
schema,
methodMap: {
'Query.feed': 'POST',
},
})
);
```

When Sofa tries to define a route for `feed` of `Query`, instead of exposing it under `GET` (default for Query type) it will use `POST` method.

> Pattern is easy: `Type.field` where `Type` is your query or mutation type.
74 changes: 49 additions & 25 deletions src/express.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import * as express from 'express';
import {
DocumentNode,
print,
isObjectType,
isNonNullType,
GraphQLInputObjectType,
GraphQLNonNull,
} from 'graphql';
import { DocumentNode, print, isObjectType, isNonNullType } from 'graphql';

import { buildOperation } from './operation';
import { getOperationInfo, OperationInfo } from './ast';
import { Sofa, isContextFn } from './sofa';
import { RouteInfo } from './types';
import { RouteInfo, Method, MethodMap } from './types';
import { convertName } from './common';
import { parseVariable } from './parse';
import { StartSubscriptionEvent, SubscriptionManager } from './subscriptions';
import { logger } from './logger';

export type ErrorHandler = (res: express.Response, error: any) => void;
export type ExpressMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';

export function createRouter(sofa: Sofa): express.Router {
logger.debug('[Sofa] Creating router');
Expand Down Expand Up @@ -156,16 +150,24 @@ function createQueryRoute({
const hasIdArgument = field.args.some(arg => arg.name === 'id');
const path = getPath(fieldName, isSingle && hasIdArgument);

const method = field.args.find(arg => isInputType(arg.type)) ? 'post' : 'get';
const method = produceMethod({
typeName: queryType.name,
fieldName,
methodMap: sofa.methodMap,
defaultValue: 'GET',
});

router[method](path, useHandler({ info, fieldName, sofa, operation }));
router[method.toLocaleLowerCase() as ExpressMethod](
path,
useHandler({ info, fieldName, sofa, operation })
);

logger.debug(`[Router] ${fieldName} query available at ${path}`);
logger.debug(`[Router] ${fieldName} query available at ${method} ${path}`);

return {
document: operation,
path,
method: method.toUpperCase() as 'POST' | 'GET',
method: method.toUpperCase() as Method,
};
}

Expand All @@ -180,6 +182,7 @@ function createMutationRoute({
}): RouteInfo {
logger.debug(`[Router] Creating ${fieldName} mutation`);

const mutationType = sofa.schema.getMutationType()!;
const operation = buildOperation({
kind: 'mutation',
schema: sofa.schema,
Expand All @@ -190,14 +193,24 @@ function createMutationRoute({
const info = getOperationInfo(operation)!;
const path = getPath(fieldName);

router.post(path, useHandler({ info, fieldName, sofa, operation }));
const method = produceMethod({
typeName: mutationType.name,
fieldName,
methodMap: sofa.methodMap,
defaultValue: 'POST',
});

router[method.toLowerCase() as ExpressMethod](
path,
useHandler({ info, fieldName, sofa, operation })
);

logger.debug(`[Router] ${fieldName} mutation available at ${path}`);
logger.debug(`[Router] ${fieldName} mutation available at ${method} ${path}`);

return {
document: operation,
path,
method: 'POST',
method: method.toUpperCase() as Method,
};
}

Expand Down Expand Up @@ -268,15 +281,6 @@ function pickParam(req: express.Request, name: string) {
}
}

// Graphql provided isInputType accepts GraphQLScalarType, GraphQLEnumType.
function isInputType(type: any): boolean {
if (type instanceof GraphQLNonNull) {
return isInputType(type.ofType);
}

return type instanceof GraphQLInputObjectType;
}

function useAsync<T = any>(
handler: (
req: express.Request,
Expand All @@ -295,3 +299,23 @@ function useAsync<T = any>(
});
};
}

function produceMethod({
typeName,
fieldName,
methodMap,
defaultValue,
}: {
typeName: string;
fieldName: string;
methodMap?: MethodMap;
defaultValue: Method;
}): Method {
const path = `${typeName}.${fieldName}`;

if (methodMap && methodMap[path]) {
return methodMap[path];
}

return defaultValue;
}
2 changes: 1 addition & 1 deletion src/open-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function OpenAPI({
url: path,
operation: info.document,
schema,
useRequestBody: info.method === 'POST',
useRequestBody: ['POST', 'PUT', 'PATCH'].includes(info.method),
});
},
get() {
Expand Down
21 changes: 19 additions & 2 deletions src/sofa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import {
GraphQLOutputType,
} from 'graphql';

import { Ignore, Context, ContextFn, ExecuteFn, OnRoute } from './types';
import {
Ignore,
Context,
ContextFn,
ExecuteFn,
OnRoute,
MethodMap,
} from './types';
import { convertName } from './common';
import { logger } from './logger';

Expand All @@ -24,16 +31,26 @@ export interface SofaConfig {
schema: GraphQLSchema;
context?: Context;
execute?: ExecuteFn;
ignore?: Ignore; // treat an Object with an ID as not a model - accepts ['User', 'Message.author']
/**
* Treats an Object with an ID as not a model.
* @example ["User", "Message.author"]
*/
ignore?: Ignore;
onRoute?: OnRoute;
depthLimit?: number;
/**
* Overwrites the default HTTP method.
* @example {"Query.field": "GET", "Mutation.field": "POST"}
*/
methodMap?: MethodMap;
}

export interface Sofa {
schema: GraphQLSchema;
context: Context;
models: string[];
ignore: Ignore;
methodMap?: MethodMap;
execute: ExecuteFn;
onRoute?: OnRoute;
}
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ export type Ignore = string[];

export type ExecuteFn = (args: GraphQLArgs) => Promise<ExecutionResult<any>>;

export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

export interface RouteInfo {
document: DocumentNode;
path: string;
method: 'GET' | 'POST';
method: Method;
}
export type OnRoute = (info: RouteInfo) => void;

export type MethodMap = Record<string, Method>;
27 changes: 25 additions & 2 deletions tests/router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ test('should work with Mutation', async () => {
expect(route.methods.post).toEqual(true);
});

test('should parse InputTypeObject', done => {
test('should overwrite a default http method on demand', done => {
const users = [
{
id: 'user:foo',
name: 'Foo',
},
];
const spy = jest.fn(() => users);
const spyMutation = jest.fn(() => users[0]);
const sofa = createSofa({
schema: makeExecutableSchema({
typeDefs: /* GraphQL */ `
Expand All @@ -80,13 +81,24 @@ test('should parse InputTypeObject', done => {
type Query {
users(pageInfo: PageInfoInput!): [User]
}
type Mutation {
addRandomUser: User
}
`,
resolvers: {
Query: {
users: spy,
},
Mutation: {
addRandomUser: spyMutation,
},
},
}),
methodMap: {
'Query.users': 'POST',
'Mutation.addRandomUser': 'GET',
},
});

const router = createRouter(sofa);
Expand All @@ -110,7 +122,18 @@ test('should parse InputTypeObject', done => {
} else {
expect(res.body).toEqual(users);
expect(spy.mock.calls[0][1]).toEqual(params);
done();

supertest(app)
.get('/api/add-random-user')
.send()
.expect(200, (err, res) => {
if (err) {
done.fail(err);
} else {
expect(res.body).toEqual(users[0]);
done();
}
});
}
});
});

0 comments on commit cf0cb63

Please sign in to comment.