Skip to content

Commit

Permalink
feat(transport-commons): add context.http.response (#2524)
Browse files Browse the repository at this point in the history
  • Loading branch information
vonagam committed Apr 4, 2022
1 parent 3c66997 commit 5bc9d44
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 51 deletions.
28 changes: 20 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions packages/express/src/rest.ts
Expand Up @@ -36,14 +36,13 @@ const serviceMiddleware = (): RequestHandler => {
const contextBase = createContext(service, method, { http: {} });
res.hook = contextBase;

const context = await (service as any)[method](...args, contextBase);
const context = await (service as any)[method](...args, contextBase);
res.hook = context;

const result = http.getData(context);
const statusCode = http.getStatusCode(context, result);

res.data = result;
res.statusCode = statusCode;
const response = http.getResponse(context);
res.statusCode = response.status;
res.set(response.headers);
res.data = response.body;

return next();
});
Expand Down
21 changes: 20 additions & 1 deletion packages/express/test/rest.test.ts
Expand Up @@ -196,7 +196,7 @@ describe('@feathersjs/express/rest provider', () => {

app.service('hook-status').hooks({
after (hook: HookContext) {
hook.http.statusCode = 206;
hook.http.status = 206;
}
});

Expand All @@ -205,6 +205,25 @@ describe('@feathersjs/express/rest provider', () => {
assert.strictEqual(res.status, 206);
});

it('allows to set response headers in a hook', async () => {
app.use('/hook-headers', {
async get () {
return {};
}
});

app.service('hook-headers').hooks({
after (hook: HookContext) {
hook.http.headers = { foo: 'first', bar: ['second', 'third'] };
}
});

const res = await axios.get<any>('http://localhost:4777/hook-headers/dishes');

assert.strictEqual(res.headers.foo, 'first');
assert.strictEqual(res.headers.bar, 'second, third');
});

it('sets the hook object in res.hook on error', async () => {
const params = {
route: {},
Expand Down
17 changes: 12 additions & 5 deletions packages/feathers/src/declarations.ts
Expand Up @@ -256,10 +256,17 @@ export interface Params {

export interface Http {
/**
* A writeable, optional property that allows to override the standard HTTP status
* code that should be returned.
* A writeable, optional property with status code override.
*/
statusCode?: number;
status?: number;
/**
* A writeable, optional property with headers.
*/
headers?: { [key: string]: string | string[] };
/**
* A writeable, optional property with `Location` header's value.
*/
location?: string;
}

export interface HookContext<A = Application, S = any> extends BaseHookContext<ServiceGenericType<S>> {
Expand Down Expand Up @@ -333,11 +340,11 @@ export interface HookContext<A = Application, S = any> extends BaseHookContext<S
* A writeable, optional property that allows to override the standard HTTP status
* code that should be returned.
*
* @deprecated Use `http.statusCode` instead.
* @deprecated Use `http.status` instead.
*/
statusCode?: number;
/**
* A writeable, optional property that contains options specific to HTTP transports.
* A writeable, optional property with options specific to HTTP transports.
*/
http?: Http;
/**
Expand Down
4 changes: 2 additions & 2 deletions packages/feathers/src/hooks/index.ts
Expand Up @@ -82,10 +82,10 @@ export function hookMixin<A> (
event: null,
type: null,
get statusCode () {
return this.http?.statusCode;
return this.http?.status;
},
set statusCode (value: number) {
(this.http ||= {}).statusCode = value;
(this.http ||= {}).status = value;
}
});

Expand Down
11 changes: 5 additions & 6 deletions packages/koa/src/rest.ts
Expand Up @@ -32,14 +32,13 @@ const serviceMiddleware = (): Middleware => {
const contextBase = createContext(service, method, { http: {} });
ctx.hook = contextBase;

const context = await (service as any)[method](...args, contextBase);
const context = await (service as any)[method](...args, contextBase);
ctx.hook = context;

const result = http.getData(context);
const statusCode = http.getStatusCode(context, result);

ctx.body = result;
ctx.status = statusCode;
const response = http.getResponse(context);
ctx.status = response.status;
ctx.set(response.headers);
ctx.body = response.body;

return next();
};
Expand Down
8 changes: 5 additions & 3 deletions packages/transport-commons/package.json
Expand Up @@ -53,12 +53,14 @@
"*.js"
],
"dependencies": {
"@feathersjs/commons": "^5.0.0-pre.17",
"@feathersjs/errors": "^5.0.0-pre.17",
"@feathersjs/feathers": "^5.0.0-pre.17",
"@feathersjs/commons": "^5.0.0-pre.16",
"@feathersjs/errors": "^5.0.0-pre.16",
"@feathersjs/feathers": "^5.0.0-pre.16",
"encodeurl": "^1.0.2",
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/encodeurl": "^1.0.0",
"@types/lodash": "^4.14.181",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.23",
Expand Down
41 changes: 30 additions & 11 deletions packages/transport-commons/src/http.ts
@@ -1,5 +1,6 @@
import { MethodNotAllowed } from '@feathersjs/errors/lib';
import { HookContext, NullableId, Params } from '@feathersjs/feathers';
import encodeUrl from 'encodeurl';

export const METHOD_HEADER = 'x-service-method';

Expand All @@ -13,7 +14,8 @@ export const statusCodes = {
created: 201,
noContent: 204,
methodNotAllowed: 405,
success: 200
success: 200,
seeOther: 303
};

export const knownMethods: { [key: string]: string } = {
Expand All @@ -25,7 +27,7 @@ export const knownMethods: { [key: string]: string } = {

export function getServiceMethod (_httpMethod: string, id: unknown, headerOverride?: string) {
const httpMethod = _httpMethod.toLowerCase();

if (httpMethod === 'post' && headerOverride) {
return headerOverride;
}
Expand Down Expand Up @@ -53,24 +55,41 @@ export const argumentsFor = {
default: ({ data, params }: ServiceParams) => [ data, params ]
}

export function getData (context: HookContext) {
return context.dispatch !== undefined
? context.dispatch
: context.result;
}
export function getStatusCode (context: HookContext, body: any, location: string|string[]) {
const { http = {} } = context;

export function getStatusCode (context: HookContext, data?: any) {
if (context.http?.statusCode) {
return context.http.statusCode;
if (http.status) {
return http.status;
}

if (context.method === 'create') {
return statusCodes.created;
}

if (!data) {
if (location !== undefined) {
return statusCodes.seeOther;
}

if (!body) {
return statusCodes.noContent;
}

return statusCodes.success;
}

export function getResponse (context: HookContext) {
const { http = {} } = context;
const body = context.dispatch !== undefined ? context.dispatch : context.result;

let headers = http.headers || {};
let location = headers.Location;

if (http.location !== undefined) {
location = encodeUrl(http.location);
headers = { ...headers, Location: location };
}

const status = getStatusCode(context, body, location);

return { status, headers, body };
}
37 changes: 28 additions & 9 deletions packages/transport-commons/test/http.test.ts
Expand Up @@ -3,7 +3,7 @@ import { HookContext } from '@feathersjs/feathers';
import { http } from '../src';

describe('@feathersjs/transport-commons HTTP helpers', () => {
it('getData', () => {
it('getResponse body', () => {
const plainData = { message: 'hi' };
const dispatch = { message: 'from dispatch' };
const resultContext = {
Expand All @@ -13,22 +13,41 @@ describe('@feathersjs/transport-commons HTTP helpers', () => {
dispatch
};

assert.deepStrictEqual(http.getData(resultContext as HookContext), plainData);
assert.deepStrictEqual(http.getData(dispatchContext as HookContext), dispatch);
assert.strictEqual(http.getResponse(resultContext as HookContext).body, plainData);
assert.strictEqual(http.getResponse(dispatchContext as HookContext).body, dispatch);
});

it('getStatusCode', async () => {
it('getResponse status', () => {
const statusContext = {
http: { statusCode: 202 }
http: { status: 202 }
};
const createContext = {
method: 'create'
};
const redirectContext = {
http: { location: '/' }
};

assert.strictEqual(http.getResponse(statusContext as HookContext).status, 202);
assert.strictEqual(http.getResponse(createContext as HookContext).status, http.statusCodes.created);
assert.strictEqual(http.getResponse(redirectContext as HookContext).status, http.statusCodes.seeOther);
assert.strictEqual(http.getResponse({} as HookContext).status, http.statusCodes.noContent);
assert.strictEqual(http.getResponse({result: true} as HookContext).status, http.statusCodes.success);
});

it('getResponse headers', () => {
const headers = { key: 'value' } as any;
const headersContext = {
http: { headers }
};
const locationContext = {
http: { location: '/' }
};

assert.strictEqual(http.getStatusCode(statusContext as HookContext, {}), 202);
assert.strictEqual(http.getStatusCode(createContext as HookContext, {}), http.statusCodes.created);
assert.strictEqual(http.getStatusCode({} as HookContext), http.statusCodes.noContent);
assert.strictEqual(http.getStatusCode({} as HookContext, {}), http.statusCodes.success);
assert.deepStrictEqual(http.getResponse({} as HookContext).headers, {});
assert.deepStrictEqual(http.getResponse({http: {}} as HookContext).headers, {});
assert.strictEqual(http.getResponse(headersContext as HookContext).headers, headers);
assert.deepStrictEqual(http.getResponse(locationContext as HookContext).headers, { Location: '/' });
});

it('getServiceMethod', () => {
Expand Down

0 comments on commit 5bc9d44

Please sign in to comment.