Skip to content

Commit

Permalink
Customize endpoint's HTTP Method (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela authored Jul 17, 2019
1 parent 74b46e0 commit 06a9198
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 23 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,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,
method: {
'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.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,
method: {
'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.
56 changes: 49 additions & 7 deletions src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ 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 @@ -149,14 +150,24 @@ function createQueryRoute({
const hasIdArgument = field.args.some(arg => arg.name === 'id');
const path = getPath(fieldName, isSingle && hasIdArgument);

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

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: 'GET',
method: method.toUpperCase() as Method,
};
}

Expand All @@ -171,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 @@ -181,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.method,
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 @@ -284,3 +306,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;
}
1 change: 1 addition & 0 deletions src/open-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function OpenAPI({
url: path,
operation: info.document,
schema,
useRequestBody: ['POST', 'PUT', 'PATCH'].includes(info.method),
});
},
get() {
Expand Down
7 changes: 4 additions & 3 deletions src/open-api/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@ export function buildPathFromOperation({
url,
schema,
operation,
useRequestBody,
}: {
url: string;
schema: GraphQLSchema;
operation: DocumentNode;
useRequestBody: boolean;
}) {
const info = getOperationInfo(operation)!;
const isQuery = info.operation.operation === 'query';

return {
operationId: info.name,
parameters: isQuery
parameters: !useRequestBody
? resolveParameters(url, info.operation.variableDefinitions)
: [],
requestBody: !isQuery
requestBody: useRequestBody
? {
content: {
'application/json': {
Expand Down
2 changes: 1 addition & 1 deletion src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function resolveVariable({
}

if (isInputObjectType(namedType)) {
return value && JSON.parse(value);
return value && typeof value === 'object' ? value : JSON.parse(value);
}

return value;
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';
import { ErrorHandler } from './express';
Expand All @@ -25,17 +32,27 @@ 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;
errorHandler?: ErrorHandler;
/**
* Overwrites the default HTTP method.
* @example {"Query.field": "GET", "Mutation.field": "POST"}
*/
method?: MethodMap;
}

export interface Sofa {
schema: GraphQLSchema;
context: Context;
models: string[];
ignore: Ignore;
method?: MethodMap;
execute: ExecuteFn;
onRoute?: OnRoute;
errorHandler?: ErrorHandler;
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>;
2 changes: 2 additions & 0 deletions tests/open-api/operations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ test('handle query', async () => {
url: '/api/feed',
operation,
schema,
useRequestBody: false,
});

expect(result.operationId).toEqual('feedQuery');
Expand Down Expand Up @@ -70,6 +71,7 @@ test('handle mutation', async () => {
url: '/api/add-post',
operation,
schema,
useRequestBody: true,
});

// id
Expand Down
50 changes: 43 additions & 7 deletions tests/router.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { makeExecutableSchema } from 'graphql-tools';
import * as supertest from 'supertest';
import * as express from 'express';
import * as bodyParser from 'body-parser';
import { schema, models } from './schema';
import { createRouter } from '../src/express';
import { createSofa } from '../src';
Expand Down Expand Up @@ -55,51 +56,86 @@ 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 */ `
input PageInfoInput {
offset: Int
limit: Int!
}
type User {
id: ID
name: String
}
type Query {
usersInfo(pageInfo: PageInfoInput!): [User]
users(pageInfo: PageInfoInput!): [User]
}
type Mutation {
addRandomUser: User
}
`,
resolvers: {
Query: {
usersInfo: () => users,
users: spy,
},
Mutation: {
addRandomUser: spyMutation,
},
},
}),
method: {
'Query.users': 'POST',
'Mutation.addRandomUser': 'GET',
},
});

const router = createRouter(sofa);
const app = express();

app.use(bodyParser.json());
app.use('/api', router);

const params = {
pageInfo: {
limit: 5,
},
};

supertest(app)
.get('/api/users-info?pageInfo={"limit": 5}')
.post('/api/users')
.send(params)
.expect(200, (err, res) => {
if (err) {
done.fail(err);
} else {
expect(res.body).toEqual(users);
done();
expect(spy.mock.calls[0][1]).toEqual(params);

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();
}
});
}
});
});
3 changes: 2 additions & 1 deletion website/sidebars.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"api/context",
"api/ignore",
"api/execute",
"api/error-handler"
"api/error-handler",
"api/method"
]
}
}

0 comments on commit 06a9198

Please sign in to comment.