Просмотр исходного кода

chore: adds "method", "path" and "pathname" to the RequestContext

e22m4u 1 год назад
Родитель
Сommit
01eb53399a

+ 93 - 19
README.md

@@ -6,7 +6,7 @@ A pure ES-module of the Node.js HTTP router that uses the
 - Uses [path-to-regexp](https://github.com/pillarjs/path-to-regexp) syntax.
 - Supports path parameters.
 - Parses JSON-body automatically.
-- Parses a query string and the Cookie header.
+- Parses a query string and a `cookie` header.
 - Supports `preHandler` and `postHandler` hooks.
 - Asynchronous request handler.
 
@@ -47,26 +47,37 @@ server.listen(3000, 'localhost');
 
 ### RequestContext
 
-The first parameter of the route handler is the `RequestContext` instance.
+The first parameter of a route handler is a `RequestContext` instance.
 
-- `container: ServiceContainer`
-- `req: IncomingMessage`
-- `res: ServerResponse`
-- `query: ParsedQuery`
-- `headers: ParsedHeaders`
-- `cookie: ParsedCookie`
+- `container: ServiceContainer` is an instance of the [ServiceContainer](https://npmjs.com/package/@e22m4u/js-service)
+- `req: IncomingMessage` is a native request from the `http` module
+- `res: ServerResponse` is a native response from the `http` module
+- `params: ParsedParams` is a key-value object of path parameters
+- `query: ParsedQuery` is a key-value object of a parsed query string
+- `headers: ParsedHeaders` is a key-value object of request headers
+- `cookie: ParsedCookie` is a key-value object of a parsed `cookie` header
+- `method: string` is a request method in lower case like `get`, `post` etc.
+- `path: string` is a request pathname with a query string
+- `pathname: string` is a request pathname without a query string
 
-The `RequestContext` can be destructured.
+Here are possible values of RequestContext properties.
 
 ```js
 router.defineRoute({
-  // ...
-  handler({req, res, query, headers, cookie}) {
-    console.log(req);     // IncomingMessage
-    console.log(res);     // ServerResponse
-    console.log(query);   // {id: '10', ...}
-    console.log(headers); // {'cookie': 'foo=bar', ...}
-    console.log(cookie);  // {foo: 'bar', ...}
+  method: 'get',
+  path: '/users/:id',
+  handler(ctx) {
+    // GET /users/10?include=city
+    // Cookie: foo=bar; baz=qux;
+    console.log(ctx.req);      // IncomingMessage
+    console.log(ctx.res);      // ServerResponse
+    console.log(ctx.params);   // {id: 10}
+    console.log(ctx.query);    // {include: 'city'}
+    console.log(ctx.headers);  // {cookie: 'foo=bar; baz=qux;'}
+    console.log(ctx.cookie);   // {foo: 'bar', baz: 'qux'}
+    console.log(ctx.method);   // "get"
+    console.log(ctx.path);     // "/users/10?include=city"
+    console.log(ctx.pathname); // "/users/10"
     // ...
   },
 });
@@ -74,7 +85,7 @@ router.defineRoute({
 
 ### Sending response
 
-Return values of the route handler will be sent as described below.
+Return values of a route handler will be sent as described below.
 
 | value     | content-type             |
 |-----------|--------------------------|
@@ -97,8 +108,8 @@ router.defineRoute({
 });
 ```
 
-If the `ServerResponse` has been sent manually, then the return
-value will be ignored.
+If the `ServerResponse` has been sent manually, then a return
+value of the route handler will be ignored.
 
 ```js
 router.defineRoute({
@@ -111,6 +122,69 @@ router.defineRoute({
 });
 ```
 
+### Route hooks
+
+A route definition allows you to set following hooks:
+
+- `preHandler` is executed before a route handler.
+- `postHandler` is executed after a route handler.
+
+If the `preHandler` hook returns a value other than `undefined`
+or `null`, it will be used as the server response.
+
+```js
+router.defineRoute({
+  // ...
+  preHandler(ctx) {
+    return 'Are you authenticated?';
+  },
+  handler(ctx) {
+    // the request handler will be skipped because
+    // the "preHandler" hook returns a non-empty value
+    return 'Hello world!';
+  },
+});
+```
+
+A return value of the route handler will be passed as the second
+argument to the `preHandler` hook.
+
+```js
+router.defineRoute({
+  // ...
+  handler(ctx) {
+    return 'Hello world!';
+  },
+  preHandler(ctx, data) {
+    // after the route handler
+    return data.toUpperCase(); // HELLO WORLD!
+  },
+});
+```
+
+### Global hooks
+
+A `Router` instance allows you to set following global hooks:
+
+- `preHandler` is executed before each route handler.
+- `postHandler` is executed after each route handler.
+
+The `addHook` method of a `Router` instance accepts a hook name as the first
+parameter and the hook function as the second.
+
+```js
+router.addHook('preHandler', (ctx) => {
+  // executes before each route handler
+});
+
+router.addHook('postHandler', (ctx, data) => {
+  // executes after each route handler
+});
+```
+
+Similar to a route hook, if a global hook returns a value other than
+`undefined` or `null`, that value will be used as the server response.
+
 ## Debug
 
 Set environment variable `DEBUG=jsTrieRouter*` before start.

+ 2 - 2
src/parsers/cookie-parser.js

@@ -1,6 +1,6 @@
 import {Service} from '../service.js';
 import {parseCookie} from '../utils/index.js';
-import {getRequestPath} from '../utils/index.js';
+import {getRequestPathname} from '../utils/index.js';
 
 /**
  * Cookie parser.
@@ -24,7 +24,7 @@ export class CookieParser extends Service {
       this.debug(
         'The request %s %v has no cookie.',
         req.method,
-        getRequestPath(req),
+        getRequestPathname(req),
       );
     }
     return cookie;

+ 2 - 2
src/parsers/query-parser.js

@@ -1,6 +1,6 @@
 import querystring from 'querystring';
 import {Service} from '../service.js';
-import {getRequestPath} from '../utils/index.js';
+import {getRequestPathname} from '../utils/index.js';
 
 /**
  * Query parser.
@@ -24,7 +24,7 @@ export class QueryParser extends Service {
       this.debug(
         'The request %s %v has no query.',
         req.method,
-        getRequestPath(req),
+        getRequestPathname(req),
       );
     }
     return query;

+ 15 - 0
src/request-context.d.ts

@@ -39,6 +39,21 @@ export declare class RequestContext {
    */
   cookie: ParsedCookie;
 
+  /**
+   * Method.
+   */
+  get method(): string;
+
+  /**
+   * Path.
+   */
+  get path(): string;
+
+  /**
+   * Pathname.
+   */
+  get pathname(): string;
+
   /**
    * Constructor.
    *

+ 38 - 0
src/request-context.js

@@ -2,6 +2,7 @@ import {Errorf} from '@e22m4u/js-format';
 import {isReadableStream} from './utils/index.js';
 import {isWritableStream} from './utils/index.js';
 import {ServiceContainer} from '@e22m4u/js-service';
+import {getRequestPathname} from './utils/index.js';
 
 /**
  * Request context.
@@ -63,6 +64,43 @@ export class RequestContext {
    */
   cookie = {};
 
+  /**
+   * Method.
+   *
+   * @returns {string}
+   */
+  get method() {
+    return this.req.method.toLowerCase();
+  }
+
+  /**
+   * Path.
+   *
+   * @returns {string}
+   */
+  get path() {
+    return this.req.url;
+  }
+
+  /**
+   * Pathname.
+   *
+   * @type {string|undefined}
+   * @private
+   */
+  _pathname = undefined;
+
+  /**
+   * Pathname.
+   *
+   * @returns {string}
+   */
+  get pathname() {
+    if (this._pathname != null) return this._pathname;
+    this._pathname = getRequestPathname(this.req);
+    return this._pathname;
+  }
+
   /**
    * Constructor.
    *

+ 44 - 0
src/request-context.spec.js

@@ -86,4 +86,48 @@ describe('RequestContext', function () {
       expect(ctx.res).to.be.eq(res);
     });
   });
+
+  describe('method', function () {
+    it('returns the method name in lower case', function () {
+      const req = createRequestMock({method: 'POST'});
+      const res = createResponseMock();
+      const cnt = new ServiceContainer();
+      const ctx = new RequestContext(cnt, req, res);
+      expect(ctx.method).to.be.eq('post');
+    });
+  });
+
+  describe('path', function () {
+    it('returns the request pathname with the query string', function () {
+      const req = createRequestMock({path: '/pathname?foo=bar'});
+      const res = createResponseMock();
+      const cnt = new ServiceContainer();
+      const ctx = new RequestContext(cnt, req, res);
+      expect(req.url).to.be.eq('/pathname?foo=bar');
+      expect(ctx.path).to.be.eq('/pathname?foo=bar');
+    });
+  });
+
+  describe('pathname', function () {
+    it('returns the request pathname without the query string', function () {
+      const req = createRequestMock({path: '/pathname?foo=bar'});
+      const res = createResponseMock();
+      const cnt = new ServiceContainer();
+      const ctx = new RequestContext(cnt, req, res);
+      expect(req.url).to.be.eq('/pathname?foo=bar');
+      expect(ctx.pathname).to.be.eq('/pathname');
+    });
+
+    it('sets the cache to the "_pathname" property and uses is for next accesses', function () {
+      const req = createRequestMock({path: '/pathname'});
+      const res = createResponseMock();
+      const cnt = new ServiceContainer();
+      const ctx = new RequestContext(cnt, req, res);
+      expect(ctx._pathname).to.be.undefined;
+      expect(ctx.pathname).to.be.eq('/pathname');
+      expect(ctx._pathname).to.be.eq('/pathname');
+      ctx._pathname = '/overridden';
+      expect(ctx.pathname).to.be.eq('/overridden');
+    });
+  });
 });

+ 2 - 2
src/route.js

@@ -2,7 +2,7 @@ import {Errorf} from '@e22m4u/js-format';
 import {HOOK_NAME} from './hooks/index.js';
 import {HookRegistry} from './hooks/index.js';
 import {createDebugger} from './utils/index.js';
-import {getRequestPath} from './utils/index.js';
+import {getRequestPathname} from './utils/index.js';
 
 /**
  * @typedef {import('./request-context.js').RequestContext} RequestContext
@@ -173,7 +173,7 @@ export class Route {
    * @returns {*}
    */
   handle(context) {
-    const requestPath = getRequestPath(context.req);
+    const requestPath = getRequestPathname(context.req);
     debug(
       'Invoking the Route handler for the request %s %v.',
       this.method.toUpperCase(),

+ 3 - 3
src/senders/error-sender.js

@@ -1,7 +1,7 @@
 import {inspect} from 'util';
 import {Service} from '../service.js';
 import getStatusMessage from 'statuses';
-import {getRequestPath} from '../utils/index.js';
+import {getRequestPathname} from '../utils/index.js';
 
 /**
  * Exposed error properties.
@@ -61,7 +61,7 @@ export class ErrorSender extends Service {
       'The %s error is sent for the request %s %v.',
       statusCode,
       req.method,
-      getRequestPath(req),
+      getRequestPathname(req),
     );
   }
 
@@ -79,7 +79,7 @@ export class ErrorSender extends Service {
     this.debug(
       'The 404 error is sent for the request %s %v.',
       req.method,
-      getRequestPath(req),
+      getRequestPathname(req),
     );
   }
 }

+ 1 - 1
src/trie-router.js

@@ -35,7 +35,7 @@ export class TrieRouter extends Service {
    *   path: '/users/:id',             // The path template may have parameters.
    *   preHandler(ctx) { ... },        // The "preHandler" is executed before a route handler.
    *   handler(ctx) { ... },           // Request handler function.
-   *   postHandler(ctx, data) { ... }, // The "postHandler" is executed after a route handler
+   *   postHandler(ctx, data) { ... }, // The "postHandler" is executed after a route handler.
    * });
    * ```
    *

+ 0 - 1
src/utils/create-request-mock.d.ts

@@ -10,7 +10,6 @@ type RequestPatch = {
   secure?: boolean;
   path?: string;
   query?: object;
-  hash?: string;
   cookie?: object;
   headers?: object;
   body?: string;

+ 2 - 18
src/utils/create-request-mock.js

@@ -14,7 +14,6 @@ import {BUFFER_ENCODING_LIST} from './fetch-request-body.js';
  *   secure?: boolean;
  *   path?: string;
  *   query?: object;
- *   hash?: string;
  *   cookie?: object;
  *   headers?: object;
  *   body?: string;
@@ -74,12 +73,6 @@ export function createRequestMock(patch) {
       patch.query,
     );
   }
-  if (patch.hash != null && typeof patch.hash !== 'string')
-    throw new Errorf(
-      'The parameter "hash" of "createRequestMock" ' +
-        'should be a String, but %v given.',
-      patch.hash,
-    );
   if (
     (patch.cookie != null &&
       typeof patch.cookie !== 'string' &&
@@ -143,7 +136,7 @@ export function createRequestMock(patch) {
   const req =
     patch.stream ||
     createRequestStream(patch.secure, patch.body, patch.encoding);
-  req.url = createRequestUrl(patch.path || '/', patch.query, patch.hash);
+  req.url = createRequestUrl(patch.path || '/', patch.query);
   req.headers = createRequestHeaders(
     patch.host,
     patch.secure,
@@ -199,10 +192,9 @@ function createRequestStream(secure, body, encoding) {
  *
  * @param {string} path
  * @param {string|object|null|undefined} query
- * @param {string|null|undefined} hash
  * @returns {string}
  */
-function createRequestUrl(path, query, hash) {
+function createRequestUrl(path, query) {
   if (typeof path !== 'string')
     throw new Errorf(
       'The parameter "path" of "createRequestUrl" ' +
@@ -219,12 +211,6 @@ function createRequestUrl(path, query, hash) {
       query,
     );
   }
-  if (hash != null && typeof hash !== 'string')
-    throw new Errorf(
-      'The parameter "hash" of "createRequestUrl" ' +
-        'should be a String, but %v given.',
-      path,
-    );
   let url = ('/' + path).replace('//', '/');
   if (typeof query === 'object') {
     const qs = queryString.stringify(query);
@@ -232,8 +218,6 @@ function createRequestUrl(path, query, hash) {
   } else if (typeof query === 'string') {
     url += `?${query.replace(/^\?/, '')}`;
   }
-  hash = (hash || '').replace('#', '');
-  if (hash) url += `#${hash}`;
   return url;
 }
 

+ 4 - 36
src/utils/create-request-mock.spec.js

@@ -128,26 +128,6 @@ describe('createRequestMock', function () {
     throwable(null)();
   });
 
-  it('requires the parameter "hash" to be a String', function () {
-    const throwable = v => () => createRequestMock({hash: v});
-    const error = v =>
-      format(
-        'The parameter "hash" of "createRequestMock" ' +
-          'should be a String, but %s given.',
-        v,
-      );
-    expect(throwable(10)).to.throw(error('10'));
-    expect(throwable(0)).to.throw(error('0'));
-    expect(throwable(true)).to.throw(error('true'));
-    expect(throwable(false)).to.throw(error('false'));
-    expect(throwable([])).to.throw(error('Array'));
-    expect(throwable({})).to.throw(error('Object'));
-    throwable('str')();
-    throwable('')();
-    throwable(undefined)();
-    throwable(null)();
-  });
-
   it('requires the parameter "cookie" to be a String or Object', function () {
     const throwable = v => () => createRequestMock({cookie: v});
     const error = v =>
@@ -289,7 +269,7 @@ describe('createRequestMock', function () {
     expect(req.socket).to.be.instanceof(Socket);
   });
 
-  it('uses the default path "/" without query and hash', function () {
+  it('uses the default path "/" without a query string', function () {
     const req = createRequestMock();
     expect(req.url).to.be.eq('/');
   });
@@ -377,29 +357,17 @@ describe('createRequestMock', function () {
     expect(req.url).to.be.eq('/?p1=foo&p2=bar');
   });
 
-  it('sets the hash value to the request url', async function () {
-    const req = createRequestMock({hash: 'myHash'});
-    expect(req.url).to.be.eq('/#myHash');
-  });
-
-  it('sets the hash value to the request url with the prefix "#"', async function () {
-    const req = createRequestMock({hash: '#myHash'});
-    expect(req.url).to.be.eq('/#myHash');
-  });
-
-  it('set parameters "path", "query" and "hash" to the request url', function () {
+  it('set parameters "path" and "query" to the request url', function () {
     const req1 = createRequestMock({
       path: 'test',
       query: 'p1=foo&p2=bar',
-      hash: 'myHash1',
     });
     const req2 = createRequestMock({
       path: '/test',
       query: {p1: 'baz', p2: 'qux'},
-      hash: '#myHash2',
     });
-    expect(req1.url).to.be.eq('/test?p1=foo&p2=bar#myHash1');
-    expect(req2.url).to.be.eq('/test?p1=baz&p2=qux#myHash2');
+    expect(req1.url).to.be.eq('/test?p1=foo&p2=bar');
+    expect(req2.url).to.be.eq('/test?p1=baz&p2=qux');
   });
 
   it('sets the parameter "method" in uppercase', async function () {

+ 0 - 8
src/utils/get-request-path.d.ts

@@ -1,8 +0,0 @@
-import {IncomingMessage} from 'http';
-
-/**
- * Get request path.
- *
- * @param req
- */
-export declare function getRequestPath(req: IncomingMessage): string;

+ 8 - 0
src/utils/get-request-pathname.d.ts

@@ -0,0 +1,8 @@
+import {IncomingMessage} from 'http';
+
+/**
+ * Get request pathname.
+ *
+ * @param req
+ */
+export declare function getRequestPathname(req: IncomingMessage): string;

+ 3 - 3
src/utils/get-request-path.js → src/utils/get-request-pathname.js

@@ -1,12 +1,12 @@
 import {Errorf} from '@e22m4u/js-format';
 
 /**
- * Get request path.
+ * Get request pathname.
  *
  * @param {import('http').IncomingMessage} req
  * @returns {string}
  */
-export function getRequestPath(req) {
+export function getRequestPathname(req) {
   if (
     !req ||
     typeof req !== 'object' ||
@@ -14,7 +14,7 @@ export function getRequestPath(req) {
     typeof req.url !== 'string'
   ) {
     throw new Errorf(
-      'The first argument of "getRequestPath" should be ' +
+      'The first argument of "getRequestPathname" should be ' +
         'an instance of IncomingMessage, but %v given.',
       req,
     );

+ 7 - 7
src/utils/get-request-path.spec.js → src/utils/get-request-pathname.spec.js

@@ -1,13 +1,13 @@
 import {expect} from '../chai.js';
 import {format} from '@e22m4u/js-format';
-import {getRequestPath} from './get-request-path.js';
+import {getRequestPathname} from './get-request-pathname.js';
 
-describe('getRequestPath', function () {
+describe('getRequestPathname', function () {
   it('requires the argument to be an Object with "url" property', function () {
-    const throwable = v => () => getRequestPath(v);
+    const throwable = v => () => getRequestPathname(v);
     const error = v =>
       format(
-        'The first argument of "getRequestPath" should be ' +
+        'The first argument of "getRequestPathname" should be ' +
           'an instance of IncomingMessage, but %s given.',
         v,
       );
@@ -24,8 +24,8 @@ describe('getRequestPath', function () {
     throwable({url: ''})();
   });
 
-  it('returns the request path without query parameters', function () {
-    const res = getRequestPath({url: '/test?foo=bar'});
-    expect(res).to.be.eq('/test');
+  it('returns the request path without the query string', function () {
+    const res = getRequestPathname({url: '/pathname?foo=bar'});
+    expect(res).to.be.eq('/pathname');
   });
 });

+ 1 - 1
src/utils/index.d.ts

@@ -4,8 +4,8 @@ export * from './create-error.js';
 export * from './to-camel-case.js';
 export * from './create-debugger.js';
 export * from './is-response-sent.js';
-export * from './get-request-path.js';
 export * from './is-readable-stream.js';
 export * from './is-writable-stream.js';
 export * from './fetch-request-body.js';
 export * from './create-cookie-string.js';
+export * from './get-request-pathname.js';

+ 1 - 1
src/utils/index.js

@@ -4,8 +4,8 @@ export * from './create-error.js';
 export * from './to-camel-case.js';
 export * from './create-debugger.js';
 export * from './is-response-sent.js';
-export * from './get-request-path.js';
 export * from './is-readable-stream.js';
 export * from './is-writable-stream.js';
 export * from './fetch-request-body.js';
 export * from './create-cookie-string.js';
+export * from './get-request-pathname.js';