From 1a110b34cf6ae8ea79804f582cc392d469bdbec8 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 4 Jan 2024 09:53:56 +0900 Subject: [PATCH 001/109] Pass context via async storage --- package.json | 6 +- specs/bodyReaders.spec.ts | 130 +++++++++++-------------- specs/helpers.ts | 7 +- specs/middleware.spec.ts | 56 ++++------- specs/prismy.spec.ts | 24 ++--- specs/router.spec.ts | 86 +++++----------- specs/selectors/body.spec.ts | 33 +++---- specs/selectors/bufferBody.spec.ts | 9 +- specs/selectors/context.spec.ts | 25 ----- specs/selectors/headers.spec.ts | 10 +- specs/selectors/jsonBody.spec.ts | 70 ++++++------- specs/selectors/method.spec.ts | 6 +- specs/selectors/query.spec.ts | 12 +-- specs/selectors/textBody.spec.ts | 9 +- specs/selectors/url.spec.ts | 22 +++-- specs/selectors/urlEncodedBody.spec.ts | 16 ++- specs/types/middleware.ts | 27 +++-- specs/types/prismy.ts | 17 ++-- src/middleware.ts | 13 +-- src/prismy.ts | 43 ++++---- src/router.ts | 99 +++++++------------ src/selectors/body.ts | 10 +- src/selectors/bufferBody.ts | 8 +- src/selectors/context.ts | 23 ----- src/selectors/headers.ts | 8 +- src/selectors/index.ts | 1 - src/selectors/jsonBody.ts | 16 +-- src/selectors/method.ts | 4 +- src/selectors/query.ts | 9 +- src/selectors/textBody.ts | 10 +- src/selectors/url.ts | 14 ++- src/selectors/urlEncodedBody.ts | 8 +- src/send.ts | 14 +-- src/types.ts | 33 +++---- src/utils.ts | 42 ++------ 35 files changed, 385 insertions(+), 535 deletions(-) delete mode 100644 specs/selectors/context.spec.ts delete mode 100644 src/selectors/context.ts diff --git a/package.json b/package.json index b14a14f..f182164 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "prismy", "version": "3.0.0", - "description": ":rainbow: Simple and fast type safe server library based on micro for now.sh v2.", + "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "micro", "service", @@ -44,13 +44,13 @@ "@types/node": "^12.19.1", "@types/test-listen": "^1.1.0", "codecov": "^3.8.0", + "got": "^11.8.0", "jest": "^26.6.1", - "prettier": "^1.17.1", + "prettier": "^3.1.1", "rimraf": "^3.0.0", "test-listen": "^1.1.0", "ts-jest": "^26.4.2", "typedoc": "^0.15.0", - "got": "^11.8.0", "typescript": "^4.0.3" }, "jest": { diff --git a/specs/bodyReaders.spec.ts b/specs/bodyReaders.spec.ts index 37657f7..13e9c85 100644 --- a/specs/bodyReaders.spec.ts +++ b/specs/bodyReaders.spec.ts @@ -1,6 +1,6 @@ import got from 'got' import getRawBody from 'raw-body' -import { Context, middleware, prismy, res } from '../src' +import { middleware, prismy, res, getPrismyContext } from '../src' import { readBufferBody, readJsonBody, readTextBody } from '../src/bodyReaders' import { testHandler } from './helpers' @@ -8,134 +8,121 @@ describe('readBufferBody', () => { it('reads buffer body from a request', async () => { expect.hasAssertions() - const bufferBodySelector = async ({ req }: Context) => { + const bufferBodySelector = async () => { + const { req } = getPrismyContext() const body = await readBufferBody(req) return body } - const handler = prismy([bufferBodySelector], (body) => { + const handler = prismy([bufferBodySelector], body => { return res(body) }) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const targetBuffer = Buffer.from('Hello, world!') const responsePromise = got(url, { method: 'POST', - body: targetBuffer, + body: targetBuffer }) const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([ - responsePromise, - bufferPromise, - ]) + const [response, buffer] = await Promise.all([responsePromise, bufferPromise]) expect(buffer.equals(targetBuffer)).toBe(true) - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString() - ) + expect(response.headers['content-length']).toBe(targetBuffer.length.toString()) }) }) it('reads buffer body regardless delaying', async () => { expect.hasAssertions() - const bufferBodySelector = async ({ req }: Context) => { + const bufferBodySelector = async () => { + const { req } = getPrismyContext() const body = await readBufferBody(req) return body } const handler = prismy( [ () => { - return new Promise((resolve) => { + return new Promise(resolve => { setImmediate(resolve) }) }, - bufferBodySelector, + bufferBodySelector ], (_, body) => { return res(body) }, [ - middleware([], (next) => async () => { + middleware([], next => async () => { try { return await next() } catch (error) { console.error(error) throw error } - }), + }) ] ) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const targetBuffer = Buffer.from('Hello, world!') const responsePromise = got(url, { method: 'POST', - body: targetBuffer, + body: targetBuffer }) const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([ - responsePromise, - bufferPromise, - ]) + const [response, buffer] = await Promise.all([responsePromise, bufferPromise]) expect(buffer.equals(targetBuffer)).toBe(true) - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString() - ) + expect(response.headers['content-length']).toBe(targetBuffer.length.toString()) }) }) it('returns cached buffer if it is read already', async () => { expect.hasAssertions() - const bufferBodySelector = async ({ req }: Context) => { + const bufferBodySelector = async () => { + const { req } = getPrismyContext() await readBufferBody(req) const body = await readBufferBody(req) return body } - const handler = prismy([bufferBodySelector], (body) => { + const handler = prismy([bufferBodySelector], body => { return res(body) }) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const targetBuffer = Buffer.from('Hello, world!') const responsePromise = got(url, { method: 'POST', - body: targetBuffer, + body: targetBuffer }) const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([ - responsePromise, - bufferPromise, - ]) + const [response, buffer] = await Promise.all([responsePromise, bufferPromise]) expect(buffer.equals(targetBuffer)).toBe(true) - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString() - ) + expect(response.headers['content-length']).toBe(targetBuffer.length.toString()) }) }) it('throws 413 error if the request body is bigger than limits', async () => { expect.hasAssertions() - const bufferBodySelector = async ({ req }: Context) => { + const bufferBodySelector = async () => { + const { req } = getPrismyContext() const body = await readBufferBody(req, { limit: '1 byte' }) return body } - const handler = prismy([bufferBodySelector], (body) => { + const handler = prismy([bufferBodySelector], body => { return res(body) }) - await testHandler(handler, async (url) => { - const targetBuffer = Buffer.from( - 'Peter Piper picked a peck of pickled peppers' - ) + await testHandler(handler, async url => { + const targetBuffer = Buffer.from('Peter Piper picked a peck of pickled peppers') const response = await got(url, { throwHttpErrors: false, method: 'POST', responseType: 'json', - body: targetBuffer, + body: targetBuffer }) expect(response.statusCode).toBe(413) @@ -146,21 +133,22 @@ describe('readBufferBody', () => { it('throws 400 error if encoding of request body is invalid', async () => { expect.hasAssertions() - const bufferBodySelector = async ({ req }: Context) => { + const bufferBodySelector = async () => { + const { req } = getPrismyContext() const body = await readBufferBody(req, { encoding: 'lol' }) return body } - const handler = prismy([bufferBodySelector], (body) => { + const handler = prismy([bufferBodySelector], body => { return res(body) }) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const targetBuffer = Buffer.from('Hello, world!') const response = await got(url, { throwHttpErrors: false, method: 'POST', responseType: 'json', - body: targetBuffer, + body: targetBuffer }) expect(response.statusCode).toBe(400) @@ -171,23 +159,24 @@ describe('readBufferBody', () => { it('throws 500 error if the request is drained already', async () => { expect.hasAssertions() - const bufferBodySelector = async ({ req }: Context) => { + const bufferBodySelector = async () => { + const { req } = getPrismyContext() const length = req.headers['content-length'] await getRawBody(req, { limit: '1mb', length }) const body = await readBufferBody(req) return body } - const handler = prismy([bufferBodySelector], (body) => { + const handler = prismy([bufferBodySelector], body => { return res(body) }) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const targetBuffer = Buffer.from('Oops!') const response = await got(url, { throwHttpErrors: false, method: 'POST', responseType: 'json', - body: targetBuffer, + body: targetBuffer }) expect(response.statusCode).toBe(500) @@ -200,24 +189,23 @@ describe('readTextBody', () => { it('reads text from request body', async () => { expect.hasAssertions() - const textBodySelector = async ({ req }: Context) => { + const textBodySelector = async () => { + const { req } = getPrismyContext() const body = await readTextBody(req) return body } - const handler = prismy([textBodySelector], (body) => { + const handler = prismy([textBodySelector], body => { return res(body) }) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const targetBuffer = Buffer.from('Hello, world!') const response = await got(url, { method: 'POST', - body: targetBuffer, + body: targetBuffer }) expect(response.body).toBe('Hello, world!') - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString() - ) + expect(response.headers['content-length']).toBe(targetBuffer.length.toString()) }) }) }) @@ -226,48 +214,48 @@ describe('readJsonBody', () => { it('reads and parse JSON from a request body', async () => { expect.hasAssertions() - const jsonBodySelector = async ({ req }: Context) => { + const jsonBodySelector = async () => { + const { req } = getPrismyContext() const body = await readJsonBody(req) return body } - const handler = prismy([jsonBodySelector], (body) => { + const handler = prismy([jsonBodySelector], body => { return res(body) }) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const target = { - foo: 'bar', + foo: 'bar' } const response = await got(url, { method: 'POST', responseType: 'json', - json: target, + json: target }) expect(response.body).toMatchObject(target) - expect(response.headers['content-length']).toBe( - JSON.stringify(target).length.toString() - ) + expect(response.headers['content-length']).toBe(JSON.stringify(target).length.toString()) }) }) it('throws 400 error if the JSON body is invalid', async () => { expect.hasAssertions() - const jsonBodySelector = async ({ req }: Context) => { + const jsonBodySelector = async () => { + const { req } = getPrismyContext() const body = await readJsonBody(req) return body } - const handler = prismy([jsonBodySelector], (body) => { + const handler = prismy([jsonBodySelector], body => { return res(body) }) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const target = 'Oopsie' const response = await got(url, { throwHttpErrors: false, method: 'POST', responseType: 'json', - body: target, + body: target }) expect(response.statusCode).toBe(400) expect(response.body).toMatch('Error: Invalid JSON') diff --git a/specs/helpers.ts b/specs/helpers.ts index 0f5bc1e..56fa32e 100644 --- a/specs/helpers.ts +++ b/specs/helpers.ts @@ -2,13 +2,10 @@ import http from 'http' import listen from 'test-listen' import { RequestListener } from 'http' -export type TestCallback = (url: string) => void +export type TestCallback = (url: string) => Promise | void /* istanbul ignore next */ -export async function testHandler( - handler: RequestListener, - testCallback: TestCallback -): Promise { +export async function testHandler(handler: RequestListener, testCallback: TestCallback): Promise { const server = new http.Server(handler) const url = await listen(server) diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 21e176a..29d928e 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,27 +1,17 @@ import got from 'got' import { testHandler } from './helpers' -import { - prismy, - res, - Selector, - PrismyPureMiddleware, - middleware, - AsyncSelector, -} from '../src' +import { prismy, res, Selector, PrismyPureMiddleware, middleware, AsyncSelector, getPrismyContext } from '../src' describe('middleware', () => { it('creates Middleware via selectors and middleware handler', async () => { - const rawUrlSelector: Selector = (context) => context.req.url! - const errorMiddleware: PrismyPureMiddleware = middleware( - [rawUrlSelector], - (next) => async (url) => { - try { - return await next() - } catch (error) { - return res(`${url} : ${(error as any).message}`, 500) - } + const rawUrlSelector: Selector = () => getPrismyContext().req.url! + const errorMiddleware: PrismyPureMiddleware = middleware([rawUrlSelector], next => async url => { + try { + return await next() + } catch (error) { + return res(`${url} : ${(error as any).message}`, 500) } - ) + }) const handler = prismy( [], () => { @@ -30,30 +20,26 @@ describe('middleware', () => { [errorMiddleware] ) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const response = await got(url, { - throwHttpErrors: false, + throwHttpErrors: false }) expect(response).toMatchObject({ statusCode: 500, - body: '/ : Hey!', + body: '/ : Hey!' }) }) }) it('accepts async selectors', async () => { - const asyncRawUrlSelector: AsyncSelector = async (context) => - context.req.url! - const errorMiddleware = middleware( - [asyncRawUrlSelector], - (next) => async (url) => { - try { - return await next() - } catch (error) { - return res(`${url} : ${(error as any).message}`, 500) - } + const asyncRawUrlSelector: AsyncSelector = async () => getPrismyContext().req.url! + const errorMiddleware = middleware([asyncRawUrlSelector], next => async url => { + try { + return await next() + } catch (error) { + return res(`${url} : ${(error as any).message}`, 500) } - ) + }) const handler = prismy( [], () => { @@ -62,13 +48,13 @@ describe('middleware', () => { [errorMiddleware] ) - await testHandler(handler, async (url) => { + await testHandler(handler, async url => { const response = await got(url, { - throwHttpErrors: false, + throwHttpErrors: false }) expect(response).toMatchObject({ statusCode: 500, - body: '/ : Hey!', + body: '/ : Hey!' }) }) }) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 0341db7..7e2acc4 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -1,6 +1,6 @@ import got from 'got' import { testHandler } from './helpers' -import { prismy, res, Selector, PrismyPureMiddleware, err } from '../src' +import { prismy, res, Selector, PrismyPureMiddleware, err, getPrismyContext } from '../src' describe('prismy', () => { it('returns node.js request handler', async () => { @@ -16,7 +16,10 @@ describe('prismy', () => { }) it('selects value from context via selector', async () => { - const rawUrlSelector: Selector = context => context.req.url! + const rawUrlSelector: Selector = () => { + const { req } = getPrismyContext() + return req.url! + } const handler = prismy([rawUrlSelector], url => res(url)) await testHandler(handler, async url => { @@ -29,8 +32,7 @@ describe('prismy', () => { }) it('selects value from context via selector', async () => { - const asyncRawUrlSelector: Selector = async context => - context.req.url! + const asyncRawUrlSelector: Selector = async () => getPrismyContext().req.url! const handler = prismy([asyncRawUrlSelector], url => res(url)) await testHandler(handler, async url => { @@ -43,7 +45,7 @@ describe('prismy', () => { }) it('expose raw prismy handler for unit tests', () => { - const rawUrlSelector: Selector = context => context.req.url! + const rawUrlSelector: Selector = () => getPrismyContext().req.url! const handler = prismy([rawUrlSelector], url => res(url)) const result = handler.handler('Hello, World!') @@ -56,14 +58,14 @@ describe('prismy', () => { }) it('applys middleware', async () => { - const errorMiddleware: PrismyPureMiddleware = context => async next => { + const errorMiddleware: PrismyPureMiddleware = () => async next => { try { return await next() } catch (error) { return err(500, (error as any).message) } } - const rawUrlSelector: Selector = context => context.req.url! + const rawUrlSelector: Selector = () => getPrismyContext().req.url! const handler = prismy( [rawUrlSelector], url => { @@ -84,17 +86,17 @@ describe('prismy', () => { }) it('applys middleware orderly', async () => { - const problematicMiddleware: PrismyPureMiddleware = context => async next => { + const problematicMiddleware: PrismyPureMiddleware = () => async next => { throw new Error('Hey!') } - const errorMiddleware: PrismyPureMiddleware = context => async next => { + const errorMiddleware: PrismyPureMiddleware = () => async next => { try { return await next() } catch (error) { return res((error as any).message, 500) } } - const rawUrlSelector: Selector = context => context.req.url! + const rawUrlSelector: Selector = () => getPrismyContext().req.url! const handler = prismy( [rawUrlSelector], url => { @@ -134,7 +136,7 @@ describe('prismy', () => { }) it('handles unhandled errors from selectors', async () => { - const rawUrlSelector: Selector = context => { + const rawUrlSelector: Selector = () => { throw new Error('Hey!') } const handler = prismy( diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 538f931..92d7df1 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -1,6 +1,6 @@ import got from 'got' import { testHandler } from './helpers' -import { createRouteParamSelector, prismy, res, router } from '../src' +import { routeParamSelector, prismy, res, router } from '../src' import { join } from 'path' describe('router', () => { @@ -15,17 +15,17 @@ describe('router', () => { const routerHandler = router([ ['/a', handlerA], - ['/b', handlerB], + ['/b', handlerB] ]) - await testHandler(routerHandler, async (url) => { + await testHandler(routerHandler, async url => { const response = await got(join(url, 'b'), { - method: 'GET', + method: 'GET' }) expect(response).toMatchObject({ statusCode: 200, - body: 'b', + body: 'b' }) }) }) @@ -41,28 +41,28 @@ describe('router', () => { const routerHandler = router([ [['/', 'get'], handlerA], - [['/', 'post'], handlerB], + [['/', 'post'], handlerB] ]) - await testHandler(routerHandler, async (url) => { + await testHandler(routerHandler, async url => { const response = await got(url, { - method: 'GET', + method: 'GET' }) expect(response).toMatchObject({ statusCode: 200, - body: 'a', + body: 'a' }) }) - await testHandler(routerHandler, async (url) => { + await testHandler(routerHandler, async url => { const response = await got(url, { - method: 'POST', + method: 'POST' }) expect(response).toMatchObject({ statusCode: 200, - body: 'b', + body: 'b' }) }) }) @@ -72,23 +72,23 @@ describe('router', () => { const handlerA = prismy([], () => { return res('a') }) - const handlerB = prismy([createRouteParamSelector('id')], (id) => { + const handlerB = prismy([routeParamSelector('id')], id => { return res(id) }) const routerHandler = router([ ['/a', handlerA], - ['/b/:id', handlerB], + ['/b/:id', handlerB] ]) - await testHandler(routerHandler, async (url) => { + await testHandler(routerHandler, async url => { const response = await got(join(url, 'b/test-param'), { - method: 'GET', + method: 'GET' }) expect(response).toMatchObject({ statusCode: 200, - body: 'test-param', + body: 'test-param' }) }) }) @@ -98,23 +98,23 @@ describe('router', () => { const handlerA = prismy([], () => { return res('a') }) - const handlerB = prismy([createRouteParamSelector('not-id')], (notId) => { + const handlerB = prismy([routeParamSelector('not-id')], notId => { return res(notId) }) const routerHandler = router([ ['/a', handlerA], - ['/b/:id', handlerB], + ['/b/:id', handlerB] ]) - await testHandler(routerHandler, async (url) => { + await testHandler(routerHandler, async url => { const response = await got(join(url, 'b/test-param'), { - method: 'GET', + method: 'GET' }) expect(response).toMatchObject({ statusCode: 200, - body: '', + body: '' }) }) }) @@ -130,52 +130,18 @@ describe('router', () => { const routerHandler = router([ [['/', 'get'], handlerA], - [['/', 'post'], handlerB], + [['/', 'post'], handlerB] ]) - await testHandler(routerHandler, async (url) => { + await testHandler(routerHandler, async url => { const response = await got(url, { method: 'PUT', - throwHttpErrors: false, + throwHttpErrors: false }) expect(response).toMatchObject({ statusCode: 404, - body: expect.stringContaining('Error: Not Found'), - }) - }) - }) - - it('uses custom 404 error handler', async () => { - expect.assertions(1) - const handlerA = prismy([], () => { - return res('a') - }) - const handlerB = prismy([], () => { - return res('b') - }) - - const routerHandler = router( - [ - [['/', 'get'], handlerA], - [['/', 'post'], handlerB], - ], - { - notFoundHandler: prismy([], () => { - return res('Not Found(Customized)', 404) - }), - } - ) - - await testHandler(routerHandler, async (url) => { - const response = await got(url, { - method: 'PUT', - throwHttpErrors: false, - }) - - expect(response).toMatchObject({ - statusCode: 404, - body: 'Not Found(Customized)', + body: expect.stringContaining('Error: Not Found') }) }) }) diff --git a/specs/selectors/body.spec.ts b/specs/selectors/body.spec.ts index 4d7e766..2cc2d0c 100644 --- a/specs/selectors/body.spec.ts +++ b/specs/selectors/body.spec.ts @@ -1,69 +1,66 @@ import got from 'got' import { testHandler } from '../helpers' -import { createBodySelector } from '../../src/selectors' +import { BodySelector } from '../../src/selectors' import { prismy, res } from '../../src' describe('createBodySelector', () => { it('returns text body', async () => { expect.hasAssertions() - const bodySelector = createBodySelector() - const handler = prismy([bodySelector], body => { + const handler = prismy([BodySelector()], (body) => { return res(`${body.constructor.name}: ${body}`) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', - body: 'Hello, World!' + body: 'Hello, World!', }) expect(response).toMatchObject({ statusCode: 200, - body: `String: Hello, World!` + body: `String: Hello, World!`, }) }) }) it('returns parsed url encoded body', async () => { expect.hasAssertions() - const bodySelector = createBodySelector() - const handler = prismy([bodySelector], body => { + const handler = prismy([BodySelector()], (body) => { return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', responseType: 'json', form: { - message: 'Hello, World!' - } + message: 'Hello, World!', + }, }) expect(response).toMatchObject({ statusCode: 200, body: { - message: 'Hello, World!' - } + message: 'Hello, World!', + }, }) }) }) it('returns JSON object body', async () => { expect.hasAssertions() - const bodySelector = createBodySelector() - const handler = prismy([bodySelector], body => { + const handler = prismy([BodySelector()], (body) => { return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const target = { - foo: 'bar' + foo: 'bar', } const response = await got(url, { method: 'POST', responseType: 'json', - json: target + json: target, }) expect(response.statusCode).toBe(200) diff --git a/specs/selectors/bufferBody.spec.ts b/specs/selectors/bufferBody.spec.ts index 92942f9..3463684 100644 --- a/specs/selectors/bufferBody.spec.ts +++ b/specs/selectors/bufferBody.spec.ts @@ -1,20 +1,19 @@ import got from 'got' import { testHandler } from '../helpers' -import { createBufferBodySelector, prismy, res } from '../../src' +import { BufferBodySelector, prismy, res } from '../../src' describe('createBufferBodySelector', () => { it('creates buffer body selector', async () => { - const bufferBodySelector = createBufferBodySelector() - const handler = prismy([bufferBodySelector], body => { + const handler = prismy([BufferBodySelector()], (body) => { return res(`${body.constructor.name}: ${body}`) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', body: 'Hello, World!' }) expect(response).toMatchObject({ statusCode: 200, - body: 'Buffer: Hello, World!' + body: 'Buffer: Hello, World!', }) }) }) diff --git a/specs/selectors/context.spec.ts b/specs/selectors/context.spec.ts deleted file mode 100644 index 0580e2e..0000000 --- a/specs/selectors/context.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { contextSelector, prismy, res, headersSelector } from '../../src' - -describe('contextSelector', () => { - it('select context', async () => { - const handler = prismy([contextSelector], async context => { - const headers = await headersSelector(context) - return res(headers['x-test']) - }) - - await testHandler(handler, async url => { - const response = await got(url, { - headers: { - 'x-test': 'Hello, World!' - } - }) - - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!' - }) - }) - }) -}) diff --git a/specs/selectors/headers.spec.ts b/specs/selectors/headers.spec.ts index a3745c5..ccfaca9 100644 --- a/specs/selectors/headers.spec.ts +++ b/specs/selectors/headers.spec.ts @@ -4,20 +4,20 @@ import { headersSelector, prismy, res } from '../../src' describe('headersSelector', () => { it('select headers', async () => { - const handler = prismy([headersSelector], headers => { + const handler = prismy([headersSelector], (headers) => { return res(headers['x-test']) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { headers: { - 'x-test': 'Hello, World!' - } + 'x-test': 'Hello, World!', + }, }) expect(response).toMatchObject({ statusCode: 200, - body: 'Hello, World!' + body: 'Hello, World!', }) }) }) diff --git a/specs/selectors/jsonBody.spec.ts b/specs/selectors/jsonBody.spec.ts index abbbbc8..b680c61 100644 --- a/specs/selectors/jsonBody.spec.ts +++ b/specs/selectors/jsonBody.spec.ts @@ -1,107 +1,107 @@ import got from 'got' import { testHandler } from '../helpers' -import { createJsonBodySelector, prismy, res } from '../../src' +import { JsonBodySelector, prismy, res } from '../../src' -describe('createJsonBodySelector', () => { +describe('JsonBodySelector', () => { it('creates json body selector', async () => { - const jsonBodySelector = createJsonBodySelector() - const handler = prismy([jsonBodySelector], body => { + const jsonBodySelector = JsonBodySelector() + const handler = prismy([jsonBodySelector], (body) => { return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', responseType: 'json', json: { - message: 'Hello, World!' - } + message: 'Hello, World!', + }, }) expect(response).toMatchObject({ statusCode: 200, body: { - message: 'Hello, World!' - } + message: 'Hello, World!', + }, }) }) }) - it('throws if content type of a request is not application/json #1 (Anti CSRF)', async () => { - const jsonBodySelector = createJsonBodySelector() - const handler = prismy([jsonBodySelector], body => { + it('throw if content typeof a request is not set', async () => { + const jsonBodySelector = JsonBodySelector() + const handler = prismy([jsonBodySelector], (body) => { return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', body: JSON.stringify({ - message: 'Hello, World!' + message: 'Hello, World!', }), - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 400, body: expect.stringContaining( - 'Error: Content type must be application/json. (Current: undefined)' - ) + 'Error: Content type must be application/json. (Current: undefined)', + ), }) }) }) - it('throws if content type of a request is not application/json #2 (Anti CSRF)', async () => { - const jsonBodySelector = createJsonBodySelector() - const handler = prismy([jsonBodySelector], body => { + it('throws if content type of a request is not application/json', async () => { + const jsonBodySelector = JsonBodySelector() + const handler = prismy([jsonBodySelector], (body) => { return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', json: { - message: 'Hello, World!' + message: 'Hello, World!', }, headers: { - 'content-type': 'text/plain' + 'content-type': 'text/plain', }, - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 400, body: expect.stringContaining( - 'Error: Content type must be application/json. (Current: text/plain)' - ) + 'Error: Content type must be application/json. (Current: text/plain)', + ), }) }) }) it('skips content-type checking if the option is given', async () => { - const jsonBodySelector = createJsonBodySelector({ - skipContentTypeCheck: true + const jsonBodySelector = JsonBodySelector({ + skipContentTypeCheck: true, }) - const handler = prismy([jsonBodySelector], body => { + const handler = prismy([jsonBodySelector], (body) => { return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', json: { - message: 'Hello, World!' + message: 'Hello, World!', }, headers: { - 'content-type': 'text/plain' - } + 'content-type': 'text/plain', + }, }) expect(response).toMatchObject({ statusCode: 200, body: JSON.stringify({ - message: 'Hello, World!' - }) + message: 'Hello, World!', + }), }) }) }) diff --git a/specs/selectors/method.spec.ts b/specs/selectors/method.spec.ts index 18c15a0..d21ace4 100644 --- a/specs/selectors/method.spec.ts +++ b/specs/selectors/method.spec.ts @@ -4,16 +4,16 @@ import { methodSelector, prismy, res } from '../../src' describe('methodSelector', () => { it('selects method', async () => { - const handler = prismy([methodSelector], method => { + const handler = prismy([methodSelector], (method) => { return res(method) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url) expect(response).toMatchObject({ statusCode: 200, - body: 'GET' + body: 'GET', }) }) }) diff --git a/specs/selectors/query.spec.ts b/specs/selectors/query.spec.ts index b1a1e0b..6678e22 100644 --- a/specs/selectors/query.spec.ts +++ b/specs/selectors/query.spec.ts @@ -4,19 +4,19 @@ import { querySelector, prismy, res } from '../../src' describe('querySelector', () => { it('selects query', async () => { - const handler = prismy([querySelector], query => { + const handler = prismy([querySelector], (query) => { return res(query) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { searchParams: { message: 'Hello, World!' }, - responseType: 'json' + responseType: 'json', }) expect(response).toMatchObject({ statusCode: 200, - body: { message: 'Hello, World!' } + body: { message: 'Hello, World!' }, }) }) }) @@ -26,12 +26,12 @@ describe('querySelector', () => { return res(JSON.stringify(query === query2)) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url) expect(response).toMatchObject({ statusCode: 200, - body: 'true' + body: 'true', }) }) }) diff --git a/specs/selectors/textBody.spec.ts b/specs/selectors/textBody.spec.ts index e2cb8fd..4a7e5a0 100644 --- a/specs/selectors/textBody.spec.ts +++ b/specs/selectors/textBody.spec.ts @@ -1,20 +1,19 @@ import got from 'got' import { testHandler } from '../helpers' -import { createTextBodySelector, prismy, res } from '../../src' +import { prismy, res, TextBodySelector } from '../../src' describe('createTextBodySelector', () => { it('creates buffer body selector', async () => { - const textBodySelector = createTextBodySelector() - const handler = prismy([textBodySelector], body => { + const handler = prismy([TextBodySelector()], (body) => { return res(`${body.constructor.name}: ${body}`) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', body: 'Hello, World!' }) expect(response).toMatchObject({ statusCode: 200, - body: 'String: Hello, World!' + body: 'String: Hello, World!', }) }) }) diff --git a/specs/selectors/url.spec.ts b/specs/selectors/url.spec.ts index fba16a3..baf7940 100644 --- a/specs/selectors/url.spec.ts +++ b/specs/selectors/url.spec.ts @@ -4,20 +4,24 @@ import { urlSelector, prismy, res } from '../../src' describe('urlSelector', () => { it('selects url', async () => { - const handler = prismy([urlSelector], url => { - return res(url) + const handler = prismy([urlSelector], (url) => { + return res({ + pathname: url.pathname, + search: url.search, + }) }) - await testHandler(handler, async url => { - const response = await got(url, { - responseType: 'json' + await testHandler(handler, async (url) => { + const response = await got(url + '/test?query=true#hash', { + responseType: 'json', }) expect(response).toMatchObject({ statusCode: 200, body: expect.objectContaining({ - path: '/' - }) + pathname: '/test', + search: '?query=true', + }), }) }) }) @@ -27,12 +31,12 @@ describe('urlSelector', () => { return res(JSON.stringify(url === url2)) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url) expect(response).toMatchObject({ statusCode: 200, - body: 'true' + body: 'true', }) }) }) diff --git a/specs/selectors/urlEncodedBody.spec.ts b/specs/selectors/urlEncodedBody.spec.ts index ef2932c..42135e3 100644 --- a/specs/selectors/urlEncodedBody.spec.ts +++ b/specs/selectors/urlEncodedBody.spec.ts @@ -1,29 +1,27 @@ import got from 'got' import { testHandler } from '../helpers' -import { createUrlEncodedBodySelector, prismy, res } from '../../src' +import { prismy, res, UrlEncodedBodySelector } from '../../src' describe('URLEncodedBody', () => { it('injects parsed url encoded body', async () => { - const urlEncodedBodySelector = createUrlEncodedBodySelector() - - const handler = prismy([urlEncodedBodySelector], body => { + const handler = prismy([UrlEncodedBodySelector()], (body) => { return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { method: 'POST', responseType: 'json', form: { - message: 'Hello, World!' - } + message: 'Hello, World!', + }, }) expect(response).toMatchObject({ statusCode: 200, body: { - message: 'Hello, World!' - } + message: 'Hello, World!', + }, }) }) }) diff --git a/specs/types/middleware.ts b/specs/types/middleware.ts index 95a7186..af4e53b 100644 --- a/specs/types/middleware.ts +++ b/specs/types/middleware.ts @@ -5,41 +5,40 @@ import { AsyncSelector, ResponseObject, prismy, - res + res, } from '../../src' -import { UrlWithStringQuery } from 'url' +import { URL } from 'url' import { expectType } from '../helpers' -const asyncUrlSelector: AsyncSelector = async context => - urlSelector(context) +const asyncUrlSelector: AsyncSelector = async () => urlSelector() const middleware1 = middleware( [urlSelector, methodSelector, asyncUrlSelector], - next => async (url, method, url2) => { - expectType(url) + (next) => async (url, method, url2) => { + expectType(url) expectType(method) - expectType(url2) + expectType(url2) return next() - } + }, ) expectType< ( - next: () => Promise> + next: () => Promise>, ) => ( - url: UrlWithStringQuery, + url: URL, method: string | undefined, - url2: UrlWithStringQuery + url2: URL, ) => ResponseObject | Promise> >(middleware1.mhandler) expectType< ( - next: () => Promise> + next: () => Promise>, ) => ( - url: UrlWithStringQuery, + url: URL, method: string | undefined, - url2: UrlWithStringQuery + url2: URL, ) => ResponseObject | Promise> >(middleware1.mhandler) diff --git a/specs/types/prismy.ts b/specs/types/prismy.ts index 76d935c..3c6298f 100644 --- a/specs/types/prismy.ts +++ b/specs/types/prismy.ts @@ -4,28 +4,27 @@ import { methodSelector, res, AsyncSelector, - ResponseObject + ResponseObject, } from '../../src' -import { UrlWithStringQuery } from 'url' +import { URL } from 'url' import { expectType } from '../helpers' -const asyncUrlSelector: AsyncSelector = async context => - urlSelector(context) +const asyncUrlSelector: AsyncSelector = async () => urlSelector() const handler1 = prismy( [urlSelector, methodSelector, asyncUrlSelector], (url, method, url2) => { - expectType(url) + expectType(url) expectType(method) - expectType(url2) + expectType(url2) return res('') - } + }, ) expectType< ( - url: UrlWithStringQuery, + url: URL, method: string | undefined, - url2: UrlWithStringQuery + url2: URL, ) => ResponseObject | Promise> >(handler1.handler) diff --git a/src/middleware.ts b/src/middleware.ts index 3b0cf6e..476d627 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,10 +1,4 @@ -import { - ResponseObject, - Selector, - SelectorReturnTypeTuple, - PrismyMiddleware, - Context -} from './types' +import { ResponseObject, Selector, SelectorReturnTypeTuple, PrismyMiddleware } from './types' import { compileHandler } from './utils' /** @@ -48,9 +42,8 @@ export function middleware[]>( next: () => Promise> ) => (...args: SelectorReturnTypeTuple) => Promise> ): PrismyMiddleware> { - const middleware = (context: Context) => async ( - next: () => Promise> - ) => compileHandler(selectors, mhandler(next))(context) + const middleware = () => async (next: () => Promise>) => + compileHandler(selectors, mhandler(next))() middleware.mhandler = mhandler return middleware diff --git a/src/prismy.ts b/src/prismy.ts index ccda6ff..5ad7c3d 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -1,3 +1,4 @@ +import { AsyncLocalStorage } from 'async_hooks' import { IncomingMessage, ServerResponse } from 'http' import { createErrorResObject } from './error' import { send } from './send' @@ -6,13 +7,21 @@ import { Selector, PrismyPureMiddleware, Promisable, - Context, - ContextHandler, + PrismyContext, PrismyHandler, - SelectorReturnTypeTuple, + SelectorReturnTypeTuple } from './types' import { compileHandler } from './utils' +export const prismyContextStorage = new AsyncLocalStorage() +export function getPrismyContext(): PrismyContext { + const context = prismyContextStorage.getStore() + if (context == null) { + throw new Error('Prismy context is not loaded.') + } + return context +} + /** * Generates a handler to be used by http.Server * @@ -39,16 +48,14 @@ import { compileHandler } from './utils' */ export function prismy[]>( selectors: [...S], - handler: ( - ...args: SelectorReturnTypeTuple - ) => Promisable>, + handler: (...args: SelectorReturnTypeTuple) => Promisable>, middlewareList: PrismyPureMiddleware[] = [] ): PrismyHandler> { - const contextHandler: ContextHandler = async (context: Context) => { - const next = async () => compileHandler(selectors, handler)(context) + const resResolver = async () => { + const next = async () => compileHandler(selectors, handler)() const pipe = middlewareList.reduce((next, middleware) => { - return () => middleware(context)(next) + return () => middleware()(next) }, next) let resObject @@ -65,21 +72,19 @@ export function prismy[]>( return resObject } - async function requestListener( - request: IncomingMessage, - response: ServerResponse - ) { - const context = { - req: request, + async function requestListener(request: IncomingMessage, response: ServerResponse) { + const context: PrismyContext = { + req: request } + prismyContextStorage.run(context, async () => { + const resObject = await resResolver() - const resObject = await contextHandler(context) - - await send(request, response, resObject) + send(request, response, resObject) + }) } requestListener.handler = handler - requestListener.contextHandler = contextHandler + requestListener.contextHandler = resResolver return requestListener } diff --git a/src/router.ts b/src/router.ts index 1b8e89f..ae1fc14 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,34 +1,20 @@ -import { Context, Selector, SyncSelector, PrismyHandler } from './types' -import { contextSelector, methodSelector, urlSelector } from './selectors' +import { PrismyContext, Selector, SyncSelector, PrismyHandler } from './types' +import { methodSelector, urlSelector } from './selectors' import { match as createMatchFunction } from 'path-to-regexp' -import { prismy } from './prismy' +import { getPrismyContext, prismy } from './prismy' import { createError } from './error' -export type RouteMethod = - | 'get' - | 'put' - | 'patch' - | 'post' - | 'delete' - | 'options' - | '*' +export type RouteMethod = 'get' | 'put' | 'patch' | 'post' | 'delete' | 'options' | '*' export type RouteIndicator = [string, RouteMethod] -export type RouteParams = [ - string | RouteIndicator, - PrismyHandler -] +export type RouteParams = [string | RouteIndicator, PrismyHandler] type Route = { indicator: RouteIndicator listener: PrismyHandler } -export function router( - routes: RouteParams[], - options: PrismyRouterOptions = {} -) { - const { notFoundHandler } = options - const compiledRoutes = routes.map((routeParams) => { +export function router(routes: RouteParams[], options: PrismyRouterOptions = {}) { + const compiledRoutes = routes.map(routeParams => { const { indicator, listener } = createRoute(routeParams) const [targetPath, method] = indicator const compiledTargetPath = removeTralingSlash(targetPath) @@ -37,79 +23,68 @@ export function router( method, match, listener, - targetPath: compiledTargetPath, + targetPath: compiledTargetPath } }) - return prismy( - [methodSelector, urlSelector, contextSelector], - (method, url, context) => { - /* istanbul ignore next */ - const normalizedMethod = method?.toLowerCase() - /* istanbul ignore next */ - const normalizedPath = removeTralingSlash(url.pathname || '/') + return prismy([methodSelector, urlSelector], (method, url) => { + const prismyContext = getPrismyContext() + /* istanbul ignore next */ + const normalizedMethod = method != null ? method.toLowerCase() : null + /* istanbul ignore next */ + const normalizedPath = removeTralingSlash(url.pathname || '/') - for (const route of compiledRoutes) { - const { method: targetMethod, match } = route - if (targetMethod !== '*' && targetMethod !== normalizedMethod) { - continue - } - - const result = match(normalizedPath) - if (!result) { - continue - } - - setRouteParamsToPrismyContext(context, result.params) - - return route.listener.contextHandler(context) + for (const route of compiledRoutes) { + const { method: targetMethod, match } = route + if (targetMethod !== '*' && targetMethod !== normalizedMethod) { + continue } - if (notFoundHandler == null) { - throw createError(404, 'Not Found') - } else { - return notFoundHandler.contextHandler(context) + const result = match(normalizedPath) + if (!result) { + continue } + + setRouteParamsToPrismyContext(prismyContext, result.params) + + return route.listener.contextHandler() } - ) + + throw createError(404, 'Not Found') + }) } -function createRoute( - routeParams: RouteParams[]> -): Route[]> { +function createRoute(routeParams: RouteParams[]>): Route[]> { const [indicator, listener] = routeParams if (typeof indicator === 'string') { return { indicator: [indicator, 'get'], - listener, + listener } } return { indicator, - listener, + listener } } const routeParamsSymbol = Symbol('route params') -function setRouteParamsToPrismyContext(context: Context, params: object) { +function setRouteParamsToPrismyContext(context: PrismyContext, params: object) { ;(context as any)[routeParamsSymbol] = params } -function getRouteParamsFromPrismyContext(context: Context) { +function getRouteParamsFromPrismyContext(context: PrismyContext) { return (context as any)[routeParamsSymbol] } -export function createRouteParamSelector( - paramName: string -): SyncSelector { - return (context) => { +export function routeParamSelector(paramName: string): SyncSelector { + return () => { + const context = getPrismyContext() const param = getRouteParamsFromPrismyContext(context)[paramName] return param != null ? param : null } } -interface PrismyRouterOptions { - notFoundHandler?: PrismyHandler -} +interface PrismyRouterOptions {} function removeTralingSlash(value: string) { if (value === '/') { diff --git a/src/selectors/body.ts b/src/selectors/body.ts index 971126f..76b8b6c 100644 --- a/src/selectors/body.ts +++ b/src/selectors/body.ts @@ -1,10 +1,11 @@ import { parse } from 'querystring' +import { getPrismyContext } from '../prismy' import { readJsonBody, readTextBody } from '../bodyReaders' import { createError } from '../error' import { AsyncSelector } from '../types' /** - * Options for {@link createBodySelector} + * Options for {@link bodySelector} * * @public */ @@ -38,10 +39,11 @@ export interface BodySelectorOptions { * * @public */ -export function createBodySelector( - options?: BodySelectorOptions +export function BodySelector( + options?: BodySelectorOptions, ): AsyncSelector { - return async ({ req }) => { + return async () => { + const { req } = getPrismyContext() const type = req.headers['content-type'] if (type === 'application/json' || type === 'application/ld+json') { diff --git a/src/selectors/bufferBody.ts b/src/selectors/bufferBody.ts index d58ccd9..99c873b 100644 --- a/src/selectors/bufferBody.ts +++ b/src/selectors/bufferBody.ts @@ -1,5 +1,6 @@ import { AsyncSelector } from '../types' import { readBufferBody } from '../bodyReaders' +import { getPrismyContext } from '../prismy' /** * Options for {@link createBufferBodySelector} @@ -36,10 +37,11 @@ export interface BufferBodySelectorOptions { * * @public */ -export function createBufferBodySelector( - options?: BufferBodySelectorOptions +export function BufferBodySelector( + options?: BufferBodySelectorOptions, ): AsyncSelector { - return ({ req }) => { + return () => { + const { req } = getPrismyContext() return readBufferBody(req, options) } } diff --git a/src/selectors/context.ts b/src/selectors/context.ts deleted file mode 100644 index 91933e9..0000000 --- a/src/selectors/context.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Context, SyncSelector } from '../types' - -/** - * Selector to extract the request context - * - * @example - * Simple example - * ```ts - * - * const prismyHandler = prismy( - * [contextSelector], - * context => { - * ... - * } - * ) - * ``` - * - * @param context - The request context - * @returns The request context - * - * @public - */ -export const contextSelector: SyncSelector = context => context diff --git a/src/selectors/headers.ts b/src/selectors/headers.ts index 723c839..69dbdc2 100644 --- a/src/selectors/headers.ts +++ b/src/selectors/headers.ts @@ -1,4 +1,5 @@ import { IncomingHttpHeaders } from 'http' +import { getPrismyContext } from '../prismy' import { SyncSelector } from '../types' /** @@ -16,10 +17,11 @@ import { SyncSelector } from '../types' * ) * ``` * - * @param context - The request context * @returns The request headers * * @public */ -export const headersSelector: SyncSelector = context => - context.req.headers +export const headersSelector: SyncSelector = () => { + const { req } = getPrismyContext() + return req.headers +} diff --git a/src/selectors/index.ts b/src/selectors/index.ts index 7f3fcaa..780432f 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -1,6 +1,5 @@ export * from './body' export * from './bufferBody' -export * from './context' export * from './headers' export * from './jsonBody' export * from './method' diff --git a/src/selectors/jsonBody.ts b/src/selectors/jsonBody.ts index b965762..b4b8eb7 100644 --- a/src/selectors/jsonBody.ts +++ b/src/selectors/jsonBody.ts @@ -1,7 +1,7 @@ import { readJsonBody } from '../bodyReaders' import { createError } from '../error' import { AsyncSelector } from '../types' -import { headersSelector } from './headers' +import { getPrismyContext } from '../prismy' /** * Options for {@link createJsonBodySelector} @@ -42,21 +42,23 @@ export interface JsonBodySelectorOptions { * * @public */ -export function createJsonBodySelector( - options?: JsonBodySelectorOptions +export function JsonBodySelector( + options?: JsonBodySelectorOptions, ): AsyncSelector { - return context => { + return () => { + const { req } = getPrismyContext() const { skipContentTypeCheck = false } = options || {} if (!skipContentTypeCheck) { - const contentType = headersSelector(context)['content-type'] + const contentType = req.headers['content-type'] if (!isContentTypeIsApplicationJSON(contentType)) { throw createError( 400, - `Content type must be application/json. (Current: ${contentType})` + `Content type must be application/json. (Current: ${contentType})`, ) } } - return readJsonBody(context.req, options) + + return readJsonBody(req, options) } } diff --git a/src/selectors/method.ts b/src/selectors/method.ts index 02a70f2..8d186db 100644 --- a/src/selectors/method.ts +++ b/src/selectors/method.ts @@ -1,3 +1,4 @@ +import { getPrismyContext } from '../prismy' import { SyncSelector } from '../types' /** @@ -22,6 +23,7 @@ import { SyncSelector } from '../types' * * @public */ -export const methodSelector: SyncSelector = ({ req }) => { +export const methodSelector: SyncSelector = () => { + const { req } = getPrismyContext() return req.method } diff --git a/src/selectors/query.ts b/src/selectors/query.ts index 2111bdd..a753eb4 100644 --- a/src/selectors/query.ts +++ b/src/selectors/query.ts @@ -1,4 +1,5 @@ import { ParsedUrlQuery, parse } from 'querystring' +import { prismyContextStorage } from '../prismy' import { SyncSelector } from '../types' import { urlSelector } from './url' @@ -19,17 +20,17 @@ const querySymbol = Symbol('prismy-query') * ) * ``` * - * @param context - Request context * @returns a selector for the url query * * @public */ -export const querySelector: SyncSelector = context => { +export const querySelector: SyncSelector = () => { + const context = prismyContextStorage.getStore()! let query: ParsedUrlQuery | undefined = context[querySymbol] if (query == null) { - const url = urlSelector(context) + const url = urlSelector() /* istanbul ignore next */ - context[querySymbol] = query = url.query != null ? parse(url.query) : {} + context[querySymbol] = query = url.search != null ? parse(url.search.slice(1)) : {} } return query } diff --git a/src/selectors/textBody.ts b/src/selectors/textBody.ts index c5c98ee..d4632ff 100644 --- a/src/selectors/textBody.ts +++ b/src/selectors/textBody.ts @@ -1,8 +1,9 @@ +import { getPrismyContext } from '../prismy' import { readTextBody } from '../bodyReaders' import { AsyncSelector } from '../types' /** - * Options for {@link createTextBodySelector} + * Options for {@link textBodySelector} * * @public */ @@ -36,10 +37,11 @@ export interface TextBodySelectorOptions { * * @public */ -export function createTextBodySelector( - options?: TextBodySelectorOptions +export function TextBodySelector( + options?: TextBodySelectorOptions, ): AsyncSelector { - return ({ req }) => { + return () => { + const { req } = getPrismyContext() return readTextBody(req, options) } } diff --git a/src/selectors/url.ts b/src/selectors/url.ts index d74712e..4505c8f 100644 --- a/src/selectors/url.ts +++ b/src/selectors/url.ts @@ -1,4 +1,5 @@ -import { UrlWithStringQuery, parse } from 'url' +import { URL } from 'url' +import { getPrismyContext } from '../prismy' import { SyncSelector } from '../types' const urlSymbol = Symbol('prismy-url') @@ -18,17 +19,20 @@ const urlSymbol = Symbol('prismy-url') * ) * ``` * - * @param context - Request context * @returns The url of the request * * @public */ -export const urlSelector: SyncSelector = context => { - let url: UrlWithStringQuery | undefined = context[urlSymbol] +export const urlSelector: SyncSelector = () => { + const context = getPrismyContext() + let url: URL | undefined = context[urlSymbol] if (url == null) { const { req } = context /* istanbul ignore next */ - url = context[urlSymbol] = parse(req.url == null ? '' : req.url) + url = context[urlSymbol] = new URL( + req.url == null ? '' : req.url, + `http://${req.headers.host}`, + ) } return url } diff --git a/src/selectors/urlEncodedBody.ts b/src/selectors/urlEncodedBody.ts index 7f0d08c..f946435 100644 --- a/src/selectors/urlEncodedBody.ts +++ b/src/selectors/urlEncodedBody.ts @@ -1,4 +1,5 @@ import { ParsedUrlQuery, parse } from 'querystring' +import { getPrismyContext } from '../prismy' import { readTextBody } from '../bodyReaders' import { createError } from '../error' import { AsyncSelector } from '../types' @@ -40,10 +41,11 @@ export interface UrlEncodedBodySelectorOptions { * * @public */ -export function createUrlEncodedBodySelector( - options?: UrlEncodedBodySelectorOptions +export function UrlEncodedBodySelector( + options?: UrlEncodedBodySelectorOptions, ): AsyncSelector { - return async ({ req }) => { + return async () => { + const { req } = getPrismyContext() const textBody = await readTextBody(req, options) try { return parse(textBody) diff --git a/src/send.ts b/src/send.ts index 665cc32..aee4e9d 100644 --- a/src/send.ts +++ b/src/send.ts @@ -15,15 +15,13 @@ import { ResponseObject } from './types' export const send = ( request: IncomingMessage, response: ServerResponse, - resObject: - | ResponseObject - | ((request: IncomingMessage, response: ServerResponse) => void) + sendable: ((request: IncomingMessage, response: ServerResponse) => void) | ResponseObject ) => { - if (typeof resObject === 'function') { - resObject(request, response) + if (typeof sendable === 'function') { + sendable(request, response) return } - const { statusCode = 200, body, headers = [] } = resObject + const { statusCode = 200, body, headers = [] } = sendable Object.entries(headers).forEach(([key, value]) => { /* istanbul ignore if */ if (value == null) { @@ -64,9 +62,7 @@ export const send = ( } } - const stringifiedBody = bodyIsNotString - ? JSON.stringify(body) - : body.toString() + const stringifiedBody = bodyIsNotString ? JSON.stringify(body) : body.toString() response.setHeader('Content-Length', Buffer.byteLength(stringifiedBody)) response.end(stringifiedBody) diff --git a/src/types.ts b/src/types.ts index 3f16e93..cd979b8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,7 @@ import { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from 'http' * * @public */ -export interface Context { +export interface PrismyContext { req: IncomingMessage } @@ -14,13 +14,13 @@ export interface Context { * * @public */ -export type SyncSelector = (context: Context) => T +export type SyncSelector = () => T /** * An asynchronous argument selector * * @public */ -export type AsyncSelector = (context: Context) => Promise +export type AsyncSelector = () => Promise /** * An argument selector to extract arguments for the handler * @@ -98,34 +98,33 @@ export type AsyncRes = Promise> * @public */ export interface PrismyPureMiddleware { - (context: Context): ( - next: () => Promise> - ) => Promise> + (): (next: () => Promise>) => Promise> } /** * prismy compatible middleware * * @public */ -export interface PrismyMiddleware - extends PrismyPureMiddleware { - mhandler( - next: () => Promise> - ): (...args: A) => Promise> +export interface PrismyMiddleware extends PrismyPureMiddleware { + mhandler(next: () => Promise>): (...args: A) => Promise> } -/** - * @public - */ -export type ContextHandler = (context: Context) => Promise> - /** * @public */ export interface PrismyHandler { (req: IncomingMessage, res: ServerResponse): void + /** + * PrismyHandler exposes `handler` for unit testing the handler. + * @param args selected arguments + */ handler(...args: A): Promisable> - contextHandler: ContextHandler + /** + * PrismyHandler exposes compiled funciton which must be ran in a prismy context. + * This is useful when using a prismy handler in other prismy handler or a middleware like Routing. + * `router()` is also using this prop to run a handler after matching URL. + */ + contextHandler: () => Promise> } /** diff --git a/src/utils.ts b/src/utils.ts index a16707a..bbeda9e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,5 @@ import { OutgoingHttpHeaders } from 'http' -import { - ResponseObject, - Selector, - SelectorReturnTypeTuple, - Context -} from './types' +import { ResponseObject, Selector, SelectorReturnTypeTuple } from './types' /** * Factory function for creating http responses @@ -28,11 +23,7 @@ export function res( } } -export function err( - statusCode: number, - body: B, - headers?: OutgoingHttpHeaders -): ResponseObject { +export function err(statusCode: number, body: B, headers?: OutgoingHttpHeaders): ResponseObject { return res(body, statusCode, headers) } @@ -66,10 +57,7 @@ export function redirect( * * @public */ -export function setBody( - resObject: ResponseObject, - body: B2 -): ResponseObject { +export function setBody(resObject: ResponseObject, body: B2): ResponseObject { return { ...resObject, body @@ -85,10 +73,7 @@ export function setBody( * * @public */ -export function setStatusCode( - resObject: ResponseObject, - statusCode: number -): ResponseObject { +export function setStatusCode(resObject: ResponseObject, statusCode: number): ResponseObject { return { ...resObject, statusCode @@ -104,10 +89,7 @@ export function setStatusCode( * * @public */ -export function updateHeaders( - resObject: ResponseObject, - extraHeaders: OutgoingHttpHeaders -): ResponseObject { +export function updateHeaders(resObject: ResponseObject, extraHeaders: OutgoingHttpHeaders): ResponseObject { return { ...resObject, headers: { @@ -126,10 +108,7 @@ export function updateHeaders( * * @public */ -export function setHeaders( - resObject: ResponseObject, - headers: OutgoingHttpHeaders -): ResponseObject { +export function setHeaders(resObject: ResponseObject, headers: OutgoingHttpHeaders): ResponseObject { return { ...resObject, headers @@ -149,9 +128,9 @@ export function setHeaders( export function compileHandler[], R>( selectors: [...S], handler: (...args: SelectorReturnTypeTuple) => R -): (context: Context) => Promise { - return async (context: Context) => { - return handler(...(await resolveSelectors(context, selectors))) +): () => Promise { + return async () => { + return handler(...(await resolveSelectors(selectors))) } } @@ -166,12 +145,11 @@ export function compileHandler[], R>( * @internal */ export async function resolveSelectors[]>( - context: Context, selectors: [...S] ): Promise> { const resolvedValues = [] for (const selector of selectors) { - const resolvedValue = await selector(context) + const resolvedValue = await selector() resolvedValues.push(resolvedValue) } From 41d2cd22df098c451fc3fdd732df68fae6c30d7d Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 4 Jan 2024 15:56:34 +0900 Subject: [PATCH 002/109] Add alias for body selector factories --- src/selectors/body.ts | 5 +++++ src/selectors/bufferBody.ts | 5 +++++ src/selectors/jsonBody.ts | 5 +++++ src/selectors/query.ts | 3 ++- src/selectors/textBody.ts | 5 +++++ src/selectors/urlEncodedBody.ts | 5 +++++ 6 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/selectors/body.ts b/src/selectors/body.ts index 76b8b6c..073aca5 100644 --- a/src/selectors/body.ts +++ b/src/selectors/body.ts @@ -61,3 +61,8 @@ export function BodySelector( } } } + +/** + * @deprecated Use `BodySelector` + */ +export const createBodySelector = BodySelector diff --git a/src/selectors/bufferBody.ts b/src/selectors/bufferBody.ts index 99c873b..56dfe30 100644 --- a/src/selectors/bufferBody.ts +++ b/src/selectors/bufferBody.ts @@ -45,3 +45,8 @@ export function BufferBodySelector( return readBufferBody(req, options) } } + +/** + * @deprecated Use `BufferBodySelector` + */ +export const createBufferBodySelector = BufferBodySelector diff --git a/src/selectors/jsonBody.ts b/src/selectors/jsonBody.ts index b4b8eb7..50a8f7c 100644 --- a/src/selectors/jsonBody.ts +++ b/src/selectors/jsonBody.ts @@ -67,3 +67,8 @@ function isContentTypeIsApplicationJSON(contentType: string | undefined) { if (!contentType.startsWith('application/json')) return false return true } + +/** + * @deprecated Use `JsonBodySelector` + */ +export const createJsonBodySelector = JsonBodySelector diff --git a/src/selectors/query.ts b/src/selectors/query.ts index a753eb4..c8cbcd5 100644 --- a/src/selectors/query.ts +++ b/src/selectors/query.ts @@ -30,7 +30,8 @@ export const querySelector: SyncSelector = () => { if (query == null) { const url = urlSelector() /* istanbul ignore next */ - context[querySymbol] = query = url.search != null ? parse(url.search.slice(1)) : {} + context[querySymbol] = query = + url.search != null ? parse(url.search.slice(1)) : {} } return query } diff --git a/src/selectors/textBody.ts b/src/selectors/textBody.ts index d4632ff..a34708f 100644 --- a/src/selectors/textBody.ts +++ b/src/selectors/textBody.ts @@ -45,3 +45,8 @@ export function TextBodySelector( return readTextBody(req, options) } } + +/** + * @deprecated Use `TextBodySelector` + */ +export const createTextBodySelector = TextBodySelector diff --git a/src/selectors/urlEncodedBody.ts b/src/selectors/urlEncodedBody.ts index f946435..6f29ef3 100644 --- a/src/selectors/urlEncodedBody.ts +++ b/src/selectors/urlEncodedBody.ts @@ -55,3 +55,8 @@ export function UrlEncodedBodySelector( } } } + +/** + * @deprecated Use `UrlEncodedBodySelector` + */ +export const createUrlEncodedBodySelector = UrlEncodedBodySelector From 9ce33c4c2dede55d2a79f53e57fae2ec4ca1fce6 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 4 Jan 2024 15:56:40 +0900 Subject: [PATCH 003/109] Add Temp docs --- v4-todo.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 v4-todo.md diff --git a/v4-todo.md b/v4-todo.md new file mode 100644 index 0000000..0d01d5e --- /dev/null +++ b/v4-todo.md @@ -0,0 +1,88 @@ +# V4 changes(TEMP) + +- redesigned router interface + - introduced route method + - Removed notFoundHandler option +- Redesign selectors interfaces + ```ts + // SelectorFactory, must be `PascalCase` + const BodySelector: () => Selector + + // Selector, must be `camelCase` + const bodySelector: Selector + + prismy([ + // Below two are functionally identical. Use whichever style you like. + BodySelector(), + bodySelector + ], handler) + ``` +- `urlSelector` is now retruning WHATWG URL, not legacy `urlObject` to improve security. +- Removed `skipContentTypeCheck` option from `JsonBodySelectorOptions` to improve security. +- One param for one query selector like URL +- Added Symbol to selector to avoid misconfig +- [ ] Adopted async local storage to communicate between selectors, middleware and handlers + - Added `getPrismyContext` method to get context. (must be used in the scope of selectors, middleware and handlers) + - Removed `contextSelector`, use `getPrismyContext` +- Simplified middleware interface + - Before + + ```ts + (context: Context) => (next: () => Promise) => Promise + ``` + + Now + + ```ts + (next: () => Promise) => Promise + ``` +- Return without res + +# Fix router + + +# Goal + +```ts +const serverHandler = router([ + route(routeInfo, [selector], handler), + route(routeInfo, prismyHandler), + route(['/deprecated', '*'], ()=> redirect('/')), + notFoundRoute(() => '*') +], { + prefix: '...' + middleware: [...], +}) + +router([ + route(), + notFoundRoute() +], { + prefix: '...' + middleware: [...], + +}) + + +combineRouters( +...routers +) + +interface NodeServerHandler { + (req: Req, res: Res): void +} + +interface PrismyHandler extends NodeServerHandler{ + selectors: [] + handler: () => void + middleware: Middleware[] +} + +type Retunnable = + +``` + +subdomain routing +Dont use context anymore. Async storage is enough. + +Inject symbol to selectors to prevent mistake From d9be9c664056d22edace68e59e4a218c455b28ba Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Fri, 5 Jan 2024 08:58:53 +0900 Subject: [PATCH 004/109] Remove skipCOntentTypeCheck --- specs/selectors/jsonBody.spec.ts | 28 ---------------------------- src/selectors/jsonBody.ts | 16 ++++++---------- 2 files changed, 6 insertions(+), 38 deletions(-) diff --git a/specs/selectors/jsonBody.spec.ts b/specs/selectors/jsonBody.spec.ts index b680c61..01cac27 100644 --- a/specs/selectors/jsonBody.spec.ts +++ b/specs/selectors/jsonBody.spec.ts @@ -77,32 +77,4 @@ describe('JsonBodySelector', () => { }) }) }) - - it('skips content-type checking if the option is given', async () => { - const jsonBodySelector = JsonBodySelector({ - skipContentTypeCheck: true, - }) - const handler = prismy([jsonBodySelector], (body) => { - return res(body) - }) - - await testHandler(handler, async (url) => { - const response = await got(url, { - method: 'POST', - json: { - message: 'Hello, World!', - }, - headers: { - 'content-type': 'text/plain', - }, - }) - - expect(response).toMatchObject({ - statusCode: 200, - body: JSON.stringify({ - message: 'Hello, World!', - }), - }) - }) - }) }) diff --git a/src/selectors/jsonBody.ts b/src/selectors/jsonBody.ts index 50a8f7c..afd78f8 100644 --- a/src/selectors/jsonBody.ts +++ b/src/selectors/jsonBody.ts @@ -9,7 +9,6 @@ import { getPrismyContext } from '../prismy' * @public */ export interface JsonBodySelectorOptions { - skipContentTypeCheck?: boolean limit?: string | number encoding?: string } @@ -47,15 +46,12 @@ export function JsonBodySelector( ): AsyncSelector { return () => { const { req } = getPrismyContext() - const { skipContentTypeCheck = false } = options || {} - if (!skipContentTypeCheck) { - const contentType = req.headers['content-type'] - if (!isContentTypeIsApplicationJSON(contentType)) { - throw createError( - 400, - `Content type must be application/json. (Current: ${contentType})`, - ) - } + const contentType = req.headers['content-type'] + if (!isContentTypeIsApplicationJSON(contentType)) { + throw createError( + 400, + `Content type must be application/json. (Current: ${contentType})`, + ) } return readJsonBody(req, options) From 9da724dffb2d4bcc54cc8ac5bfa66a542beba38f Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Fri, 5 Jan 2024 20:36:38 +0900 Subject: [PATCH 005/109] Added search param selectors --- specs/selectors/searchParam.spec.ts | 86 +++++++++++++++++++++++++++++ src/selectors/index.ts | 1 + src/selectors/query.ts | 6 +- src/selectors/searchParam.ts | 59 ++++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 specs/selectors/searchParam.spec.ts create mode 100644 src/selectors/searchParam.ts diff --git a/specs/selectors/searchParam.spec.ts b/specs/selectors/searchParam.spec.ts new file mode 100644 index 0000000..f2bd475 --- /dev/null +++ b/specs/selectors/searchParam.spec.ts @@ -0,0 +1,86 @@ +import got from 'got' +import { testHandler } from '../helpers' +import { + SearchParamSelector, + SearchParamListSelector, + prismy, + res, +} from '../../src' +import { URLSearchParams } from 'url' + +describe('SearchParamSelector', () => { + it('selects a search param', async () => { + const handler = prismy([SearchParamSelector('message')], (message) => { + return res({ message }) + }) + + await testHandler(handler, async (url) => { + const response = await got(url, { + searchParams: { message: 'Hello, World!' }, + responseType: 'json', + }) + + expect(response).toMatchObject({ + statusCode: 200, + body: { message: 'Hello, World!' }, + }) + }) + }) + + it('selects null if there is no param with the name', async () => { + const handler = prismy([SearchParamSelector('message')], (message) => { + return res({ message }) + }) + + await testHandler(handler, async (url) => { + const response = await got(url, { + responseType: 'json', + }) + + expect(response).toMatchObject({ + statusCode: 200, + body: { message: null }, + }) + }) + }) +}) + +describe('SearchParamListSelector', () => { + it('selects a search param list', async () => { + const handler = prismy([SearchParamListSelector('message')], (messages) => { + return res({ messages }) + }) + + await testHandler(handler, async (url) => { + const response = await got(url, { + searchParams: new URLSearchParams([ + ['message', 'Hello, World!'], + ['message', 'Have a nice day!'], + ]), + responseType: 'json', + }) + + expect(response).toMatchObject({ + statusCode: 200, + body: { messages: ['Hello, World!', 'Have a nice day!'] }, + }) + }) + }) + + it('selects null if there is no param with the name', async () => { + const handler = prismy([SearchParamListSelector('message')], (messages) => { + return res({ messages }) + }) + + await testHandler(handler, async (url) => { + const response = await got(url, { + responseType: 'json', + }) + + expect(response).toMatchObject({ + statusCode: 200, + body: { messages: [] }, + }) + }) + }) +}) diff --git a/src/selectors/index.ts b/src/selectors/index.ts index 780432f..704e8d9 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -4,6 +4,7 @@ export * from './headers' export * from './jsonBody' export * from './method' export * from './query' +export * from './searchParam' export * from './url' export * from './urlEncodedBody' export * from './textBody' diff --git a/src/selectors/query.ts b/src/selectors/query.ts index c8cbcd5..88ccdc6 100644 --- a/src/selectors/query.ts +++ b/src/selectors/query.ts @@ -6,7 +6,11 @@ import { urlSelector } from './url' const querySymbol = Symbol('prismy-query') /** - * Selector to extract the parsed query from the request URL + * @deprecated Use SearchParamSelector or SearchParamListSelector. + * To get all search params, use urlSelector, which resolves WHATWG URL object, and access `url.searchParams`. + * + * Selector to extract the parsed query from the request URL. + * Using `querystring.parse` internally. * * @example * Simple example diff --git a/src/selectors/searchParam.ts b/src/selectors/searchParam.ts new file mode 100644 index 0000000..dda6a51 --- /dev/null +++ b/src/selectors/searchParam.ts @@ -0,0 +1,59 @@ +import { SyncSelector } from '../types' +import { urlSelector } from './url' + +/** + * Create a selector which resolves the first value of the search param. + * Using `url.searchParams.get(name)` internally. + * + * @example + * Simple example + * ```ts + * // Expecting a request like `/?user_id=123` + * const prismyHandler = prismy( + * [SearchParamSelector('user_id')], + * userId => { + * doSomethingWithUserId(userId) + * } + * ) + * ``` + * + * @param name + * @returns a selector for the search param + */ +export const SearchParamSelector: ( + name: string, +) => SyncSelector = (name) => () => { + const url = urlSelector() + + return url.searchParams.get(name) +} + +/** + * Create a selector which resolves a value list of the search param. + * Useful when you expect to have multiple values for the same name. + * Using `url.searchParams.getAll(name)` internally. + * + * @example + * Simple example + * ```ts + * // Expecting a request like `/?user_id=123&user_id=456` + * const prismyHandler = prismy( + * [SearchParamSelector('user_id')], + * userIdList => { + * if (userIdList.length > 0) { + * doSomethingWithUserIdList(userIdList) + * } + * } + * ) + * ``` + * + * @param name + * @returns a selector for the search param list + */ +export const SearchParamListSelector: ( + name: string, +) => SyncSelector = (name) => () => { + const url = urlSelector() + + return url.searchParams.getAll(name) +} From eed87712ea1392f9dfe8de048c5094f31b39abec Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 09:24:07 +0900 Subject: [PATCH 006/109] Restructure utils --- specs/{utils.spec.ts => res.spec.ts} | 0 src/error.ts | 4 +- src/index.ts | 2 +- src/res.ts | 132 +++++++++++++++++++++++++++ src/utils.ts | 123 +------------------------ 5 files changed, 139 insertions(+), 122 deletions(-) rename specs/{utils.spec.ts => res.spec.ts} (100%) create mode 100644 src/res.ts diff --git a/specs/utils.spec.ts b/specs/res.spec.ts similarity index 100% rename from specs/utils.spec.ts rename to specs/res.spec.ts diff --git a/src/error.ts b/src/error.ts index 2227254..9777d5f 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,4 @@ -import { res } from './utils' +import { res } from './res' /** * Creates a response object from an error @@ -28,7 +28,7 @@ class PrismyError extends Error { export function createError( statusCode: number, message: string, - originalError?: any + originalError?: any, ): PrismyError { const error = new PrismyError(message) diff --git a/src/index.ts b/src/index.ts index e95575b..cacd723 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ export * from './types' -export * from './utils' export * from './prismy' export * from './middleware' export * from './selectors' export * from './error' export * from './router' +export * from './res' diff --git a/src/res.ts b/src/res.ts new file mode 100644 index 0000000..8962dec --- /dev/null +++ b/src/res.ts @@ -0,0 +1,132 @@ +import { OutgoingHttpHeaders } from 'http' +import { ResponseObject } from './types' + +/** + * Factory function for creating http responses + * + * @param body - Body of the response + * @param statusCode - HTTP status code of the response + * @param headers - HTTP headers for the response + * @returns A {@link ResponseObject | response object} containing necessary information + * + * @public + */ +export function res( + body: B, + statusCode: number = 200, + headers: OutgoingHttpHeaders = {}, +): ResponseObject { + return { + body, + statusCode, + headers, + } +} + +export function err( + statusCode: number, + body: B, + headers?: OutgoingHttpHeaders, +): ResponseObject { + return res(body, statusCode, headers) +} + +/** + * Factory function for easily generating a redirect response + * + * @param location - URL to redirect to + * @param statusCode - Status code for response. Defaults to 302 + * @param extraHeaders - Additional headers of the response + * @returns A redirect {@link ResponseObject | response} to location + * + * @public + */ +export function redirect( + location: string, + statusCode: number = 302, + extraHeaders: OutgoingHttpHeaders = {}, +): ResponseObject { + return res(null, statusCode, { + location, + ...extraHeaders, + }) +} + +/** + * Creates a new response with a new body + * + * @param resObject - The response to set the body on + * @param body - Body to be set + * @returns New {@link ResponseObject | response} with the new body + * + * @public + */ +export function setBody( + resObject: ResponseObject, + body: B2, +): ResponseObject { + return { + ...resObject, + body, + } +} + +/** + * Creates a new response with a new status code + * + * @param resObject - The response to set the code to + * @param statusCode - HTTP status code + * @returns New {@link ResponseObject | response} with the new statusCode + * + * @public + */ +export function setStatusCode( + resObject: ResponseObject, + statusCode: number, +): ResponseObject { + return { + ...resObject, + statusCode, + } +} + +/** + * Creates a new response with the extra headers. + * + * @param resObject - The response to add the new headers to + * @param extraHeaders - HTTP response headers + * @returns New {@link ResponseObject | response} with the extra headers + * + * @public + */ +export function updateHeaders( + resObject: ResponseObject, + extraHeaders: OutgoingHttpHeaders, +): ResponseObject { + return { + ...resObject, + headers: { + ...resObject.headers, + ...extraHeaders, + }, + } +} + +/** + * Creates a new response overriting all headers with new ones. + * + * @param resObject - response to set new headers on + * @param headers - HTTP response headers to set + * @returns New {@link ResponseObject | response} with new headers set + * + * @public + */ +export function setHeaders( + resObject: ResponseObject, + headers: OutgoingHttpHeaders, +): ResponseObject { + return { + ...resObject, + headers, + } +} diff --git a/src/utils.ts b/src/utils.ts index bbeda9e..cf7573f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,119 +1,4 @@ -import { OutgoingHttpHeaders } from 'http' -import { ResponseObject, Selector, SelectorReturnTypeTuple } from './types' - -/** - * Factory function for creating http responses - * - * @param body - Body of the response - * @param statusCode - HTTP status code of the response - * @param headers - HTTP headers for the response - * @returns A {@link ResponseObject | response object} containing necessary information - * - * @public - */ -export function res( - body: B, - statusCode: number = 200, - headers: OutgoingHttpHeaders = {} -): ResponseObject { - return { - body, - statusCode, - headers - } -} - -export function err(statusCode: number, body: B, headers?: OutgoingHttpHeaders): ResponseObject { - return res(body, statusCode, headers) -} - -/** - * Factory function for easily generating a redirect response - * - * @param location - URL to redirect to - * @param statusCode - Status code for response. Defaults to 302 - * @param extraHeaders - Additional headers of the response - * @returns A redirect {@link ResponseObject | response} to location - * - * @public - */ -export function redirect( - location: string, - statusCode: number = 302, - extraHeaders: OutgoingHttpHeaders = {} -): ResponseObject { - return res(null, statusCode, { - location, - ...extraHeaders - }) -} - -/** - * Creates a new response with a new body - * - * @param resObject - The response to set the body on - * @param body - Body to be set - * @returns New {@link ResponseObject | response} with the new body - * - * @public - */ -export function setBody(resObject: ResponseObject, body: B2): ResponseObject { - return { - ...resObject, - body - } -} - -/** - * Creates a new response with a new status code - * - * @param resObject - The response to set the code to - * @param statusCode - HTTP status code - * @returns New {@link ResponseObject | response} with the new statusCode - * - * @public - */ -export function setStatusCode(resObject: ResponseObject, statusCode: number): ResponseObject { - return { - ...resObject, - statusCode - } -} - -/** - * Creates a new response with the extra headers. - * - * @param resObject - The response to add the new headers to - * @param extraHeaders - HTTP response headers - * @returns New {@link ResponseObject | response} with the extra headers - * - * @public - */ -export function updateHeaders(resObject: ResponseObject, extraHeaders: OutgoingHttpHeaders): ResponseObject { - return { - ...resObject, - headers: { - ...resObject.headers, - ...extraHeaders - } - } -} - -/** - * Creates a new response overriting all headers with new ones. - * - * @param resObject - response to set new headers on - * @param headers - HTTP response headers to set - * @returns New {@link ResponseObject | response} with new headers set - * - * @public - */ -export function setHeaders(resObject: ResponseObject, headers: OutgoingHttpHeaders): ResponseObject { - return { - ...resObject, - headers - } -} +import { Selector, SelectorReturnTypeTuple } from './types' /** * Compile a handler into a runnable function by resolving selectors @@ -127,7 +12,7 @@ export function setHeaders(resObject: ResponseObject, headers: OutgoingHtt */ export function compileHandler[], R>( selectors: [...S], - handler: (...args: SelectorReturnTypeTuple) => R + handler: (...args: SelectorReturnTypeTuple) => R, ): () => Promise { return async () => { return handler(...(await resolveSelectors(selectors))) @@ -144,8 +29,8 @@ export function compileHandler[], R>( * * @internal */ -export async function resolveSelectors[]>( - selectors: [...S] +async function resolveSelectors[]>( + selectors: [...S], ): Promise> { const resolvedValues = [] for (const selector of selectors) { From 57c89ff767325c5622f10a7b3871a450c3c85179 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 11:59:47 +0900 Subject: [PATCH 007/109] Renew selector interface --- specs/bodyReaders.spec.ts | 158 ++++++++++++++++---------------- specs/middleware.spec.ts | 67 +++++++++----- specs/prismy.spec.ts | 97 +++++++++++--------- specs/types/middleware.ts | 4 +- specs/types/prismy.ts | 4 +- src/middleware.ts | 13 ++- src/prismy.ts | 19 ++-- src/router.ts | 45 ++++++--- src/selectors/body.ts | 8 +- src/selectors/bufferBody.ts | 8 +- src/selectors/createSelector.ts | 15 +++ src/selectors/headers.ts | 11 ++- src/selectors/jsonBody.ts | 8 +- src/selectors/method.ts | 11 ++- src/selectors/query.ts | 25 ++--- src/selectors/searchParam.ts | 20 ++-- src/selectors/textBody.ts | 8 +- src/selectors/url.ts | 6 +- src/selectors/urlEncodedBody.ts | 8 +- src/send.ts | 8 +- src/types.ts | 39 +++----- src/utils.ts | 9 +- 22 files changed, 331 insertions(+), 260 deletions(-) create mode 100644 src/selectors/createSelector.ts diff --git a/specs/bodyReaders.spec.ts b/specs/bodyReaders.spec.ts index 13e9c85..f33311c 100644 --- a/specs/bodyReaders.spec.ts +++ b/specs/bodyReaders.spec.ts @@ -2,127 +2,129 @@ import got from 'got' import getRawBody from 'raw-body' import { middleware, prismy, res, getPrismyContext } from '../src' import { readBufferBody, readJsonBody, readTextBody } from '../src/bodyReaders' +import { createPrismySelector } from '../src/selectors/createSelector' import { testHandler } from './helpers' describe('readBufferBody', () => { it('reads buffer body from a request', async () => { expect.hasAssertions() - const bufferBodySelector = async () => { + const handler = prismy([], async () => { const { req } = getPrismyContext() const body = await readBufferBody(req) - return body - } - const handler = prismy([bufferBodySelector], body => { + return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const targetBuffer = Buffer.from('Hello, world!') const responsePromise = got(url, { method: 'POST', - body: targetBuffer + body: targetBuffer, }) const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([responsePromise, bufferPromise]) + const [response, buffer] = await Promise.all([ + responsePromise, + bufferPromise, + ]) expect(buffer.equals(targetBuffer)).toBe(true) - expect(response.headers['content-length']).toBe(targetBuffer.length.toString()) + expect(response.headers['content-length']).toBe( + targetBuffer.length.toString(), + ) }) }) it('reads buffer body regardless delaying', async () => { expect.hasAssertions() - const bufferBodySelector = async () => { - const { req } = getPrismyContext() - const body = await readBufferBody(req) - return body - } const handler = prismy( [ - () => { - return new Promise(resolve => { - setImmediate(resolve) + createPrismySelector(() => { + return new Promise((resolve) => { + setTimeout(resolve, 1000) }) - }, - bufferBodySelector + }), ], - (_, body) => { + async (_) => { + const { req } = getPrismyContext() + const body = await readBufferBody(req) return res(body) }, [ - middleware([], next => async () => { + middleware([], (next) => async () => { try { return await next() } catch (error) { console.error(error) throw error } - }) - ] + }), + ], ) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const targetBuffer = Buffer.from('Hello, world!') const responsePromise = got(url, { method: 'POST', - body: targetBuffer + body: targetBuffer, }) const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([responsePromise, bufferPromise]) + const [response, buffer] = await Promise.all([ + responsePromise, + bufferPromise, + ]) expect(buffer.equals(targetBuffer)).toBe(true) - expect(response.headers['content-length']).toBe(targetBuffer.length.toString()) + expect(response.headers['content-length']).toBe( + targetBuffer.length.toString(), + ) }) }) it('returns cached buffer if it is read already', async () => { expect.hasAssertions() - const bufferBodySelector = async () => { + const handler = prismy([], async () => { const { req } = getPrismyContext() - await readBufferBody(req) - const body = await readBufferBody(req) - return body - } - const handler = prismy([bufferBodySelector], body => { - return res(body) + const body1 = await readBufferBody(req) + const body2 = await readBufferBody(req) + + return res({ + isCached: body1 === body2, + }) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const targetBuffer = Buffer.from('Hello, world!') - const responsePromise = got(url, { + const result = await got(url, { method: 'POST', - body: targetBuffer - }) - const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([responsePromise, bufferPromise]) + body: targetBuffer, + }).json() - expect(buffer.equals(targetBuffer)).toBe(true) - expect(response.headers['content-length']).toBe(targetBuffer.length.toString()) + expect((result as any).isCached).toBe(true) }) }) it('throws 413 error if the request body is bigger than limits', async () => { expect.hasAssertions() - const bufferBodySelector = async () => { + const handler = prismy([], async () => { const { req } = getPrismyContext() const body = await readBufferBody(req, { limit: '1 byte' }) - return body - } - const handler = prismy([bufferBodySelector], body => { + return res(body) }) - await testHandler(handler, async url => { - const targetBuffer = Buffer.from('Peter Piper picked a peck of pickled peppers') + await testHandler(handler, async (url) => { + const targetBuffer = Buffer.from( + 'Peter Piper picked a peck of pickled peppers', + ) const response = await got(url, { throwHttpErrors: false, method: 'POST', responseType: 'json', - body: targetBuffer + body: targetBuffer, }) expect(response.statusCode).toBe(413) @@ -133,22 +135,20 @@ describe('readBufferBody', () => { it('throws 400 error if encoding of request body is invalid', async () => { expect.hasAssertions() - const bufferBodySelector = async () => { + const handler = prismy([], async () => { const { req } = getPrismyContext() const body = await readBufferBody(req, { encoding: 'lol' }) - return body - } - const handler = prismy([bufferBodySelector], body => { + return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const targetBuffer = Buffer.from('Hello, world!') const response = await got(url, { throwHttpErrors: false, method: 'POST', responseType: 'json', - body: targetBuffer + body: targetBuffer, }) expect(response.statusCode).toBe(400) @@ -159,24 +159,22 @@ describe('readBufferBody', () => { it('throws 500 error if the request is drained already', async () => { expect.hasAssertions() - const bufferBodySelector = async () => { + const handler = prismy([], async () => { const { req } = getPrismyContext() const length = req.headers['content-length'] await getRawBody(req, { limit: '1mb', length }) const body = await readBufferBody(req) - return body - } - const handler = prismy([bufferBodySelector], body => { + return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const targetBuffer = Buffer.from('Oops!') const response = await got(url, { throwHttpErrors: false, method: 'POST', responseType: 'json', - body: targetBuffer + body: targetBuffer, }) expect(response.statusCode).toBe(500) @@ -189,23 +187,23 @@ describe('readTextBody', () => { it('reads text from request body', async () => { expect.hasAssertions() - const textBodySelector = async () => { + const handler = prismy([], async () => { const { req } = getPrismyContext() const body = await readTextBody(req) - return body - } - const handler = prismy([textBodySelector], body => { + return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const targetBuffer = Buffer.from('Hello, world!') const response = await got(url, { method: 'POST', - body: targetBuffer + body: targetBuffer, }) expect(response.body).toBe('Hello, world!') - expect(response.headers['content-length']).toBe(targetBuffer.length.toString()) + expect(response.headers['content-length']).toBe( + targetBuffer.length.toString(), + ) }) }) }) @@ -214,48 +212,46 @@ describe('readJsonBody', () => { it('reads and parse JSON from a request body', async () => { expect.hasAssertions() - const jsonBodySelector = async () => { + const handler = prismy([], async () => { const { req } = getPrismyContext() const body = await readJsonBody(req) - return body - } - const handler = prismy([jsonBodySelector], body => { + return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const target = { - foo: 'bar' + foo: 'bar', } const response = await got(url, { method: 'POST', responseType: 'json', - json: target + json: target, }) expect(response.body).toMatchObject(target) - expect(response.headers['content-length']).toBe(JSON.stringify(target).length.toString()) + expect(response.headers['content-length']).toBe( + JSON.stringify(target).length.toString(), + ) }) }) it('throws 400 error if the JSON body is invalid', async () => { expect.hasAssertions() - const jsonBodySelector = async () => { + const handler = prismy([], async () => { const { req } = getPrismyContext() const body = await readJsonBody(req) - return body - } - const handler = prismy([jsonBodySelector], body => { + return res(body) }) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const target = 'Oopsie' const response = await got(url, { throwHttpErrors: false, method: 'POST', responseType: 'json', - body: target + body: target, }) expect(response.statusCode).toBe(400) expect(response.body).toMatch('Error: Invalid JSON') diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 29d928e..2651876 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,60 +1,77 @@ import got from 'got' import { testHandler } from './helpers' -import { prismy, res, Selector, PrismyPureMiddleware, middleware, AsyncSelector, getPrismyContext } from '../src' +import { + prismy, + res, + PrismyPureMiddleware, + middleware, + getPrismyContext, +} from '../src' +import { createPrismySelector } from '../src/selectors/createSelector' describe('middleware', () => { it('creates Middleware via selectors and middleware handler', async () => { - const rawUrlSelector: Selector = () => getPrismyContext().req.url! - const errorMiddleware: PrismyPureMiddleware = middleware([rawUrlSelector], next => async url => { - try { - return await next() - } catch (error) { - return res(`${url} : ${(error as any).message}`, 500) - } - }) + const rawUrlSelector = createPrismySelector( + () => getPrismyContext().req.url!, + ) + const errorMiddleware: PrismyPureMiddleware = middleware( + [rawUrlSelector], + (next) => async (url) => { + try { + return await next() + } catch (error) { + return res(`${url} : ${(error as any).message}`, 500) + } + }, + ) const handler = prismy( [], () => { throw new Error('Hey!') }, - [errorMiddleware] + [errorMiddleware], ) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 500, - body: '/ : Hey!' + body: '/ : Hey!', }) }) }) it('accepts async selectors', async () => { - const asyncRawUrlSelector: AsyncSelector = async () => getPrismyContext().req.url! - const errorMiddleware = middleware([asyncRawUrlSelector], next => async url => { - try { - return await next() - } catch (error) { - return res(`${url} : ${(error as any).message}`, 500) - } - }) + const asyncRawUrlSelector = createPrismySelector( + async () => getPrismyContext().req.url!, + ) + const errorMiddleware = middleware( + [asyncRawUrlSelector], + (next) => async (url) => { + try { + return await next() + } catch (error) { + return res(`${url} : ${(error as any).message}`, 500) + } + }, + ) const handler = prismy( [], () => { throw new Error('Hey!') }, - [errorMiddleware] + [errorMiddleware], ) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 500, - body: '/ : Hey!' + body: '/ : Hey!', }) }) }) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 7e2acc4..31114a2 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -1,117 +1,132 @@ import got from 'got' import { testHandler } from './helpers' -import { prismy, res, Selector, PrismyPureMiddleware, err, getPrismyContext } from '../src' +import { + prismy, + res, + PrismyPureMiddleware, + err, + getPrismyContext, +} from '../src' +import { createPrismySelector } from '../src/selectors/createSelector' describe('prismy', () => { it('returns node.js request handler', async () => { const handler = prismy([], () => res('Hello, World!')) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url) expect(response).toMatchObject({ statusCode: 200, - body: 'Hello, World!' + body: 'Hello, World!', }) }) }) it('selects value from context via selector', async () => { - const rawUrlSelector: Selector = () => { + const rawUrlSelector = createPrismySelector(() => { const { req } = getPrismyContext() return req.url! - } - const handler = prismy([rawUrlSelector], url => res(url)) + }) + const handler = prismy([rawUrlSelector], (url) => res(url)) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url) expect(response).toMatchObject({ statusCode: 200, - body: '/' + body: '/', }) }) }) it('selects value from context via selector', async () => { - const asyncRawUrlSelector: Selector = async () => getPrismyContext().req.url! - const handler = prismy([asyncRawUrlSelector], url => res(url)) + const asyncRawUrlSelector = createPrismySelector( + async () => getPrismyContext().req.url!, + ) + const handler = prismy([asyncRawUrlSelector], (url) => res(url)) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url) expect(response).toMatchObject({ statusCode: 200, - body: '/' + body: '/', }) }) }) it('expose raw prismy handler for unit tests', () => { - const rawUrlSelector: Selector = () => getPrismyContext().req.url! - const handler = prismy([rawUrlSelector], url => res(url)) + const rawUrlSelector = createPrismySelector( + () => getPrismyContext().req.url!, + ) + const handler = prismy([rawUrlSelector], (url) => res(url)) const result = handler.handler('Hello, World!') expect(result).toEqual({ body: 'Hello, World!', headers: {}, - statusCode: 200 + statusCode: 200, }) }) it('applys middleware', async () => { - const errorMiddleware: PrismyPureMiddleware = () => async next => { + const errorMiddleware: PrismyPureMiddleware = () => async (next) => { try { return await next() } catch (error) { return err(500, (error as any).message) } } - const rawUrlSelector: Selector = () => getPrismyContext().req.url! + const rawUrlSelector = createPrismySelector( + () => getPrismyContext().req.url!, + ) const handler = prismy( [rawUrlSelector], - url => { + (url) => { throw new Error('Hey!') }, - [errorMiddleware] + [errorMiddleware], ) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 500, - body: 'Hey!' + body: 'Hey!', }) }) }) it('applys middleware orderly', async () => { - const problematicMiddleware: PrismyPureMiddleware = () => async next => { + const problematicMiddleware: PrismyPureMiddleware = () => async (next) => { throw new Error('Hey!') } - const errorMiddleware: PrismyPureMiddleware = () => async next => { + const errorMiddleware: PrismyPureMiddleware = () => async (next) => { try { return await next() } catch (error) { return res((error as any).message, 500) } } - const rawUrlSelector: Selector = () => getPrismyContext().req.url! + const rawUrlSelector = createPrismySelector( + () => getPrismyContext().req.url!, + ) const handler = prismy( [rawUrlSelector], - url => { + (url) => { return res(url) }, - [problematicMiddleware, errorMiddleware] + [problematicMiddleware, errorMiddleware], ) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 500, - body: 'Hey!' + body: 'Hey!', }) }) }) @@ -122,37 +137,37 @@ describe('prismy', () => { () => { throw new Error('Hey!') }, - [] + [], ) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 500, - body: expect.stringContaining('Error: Hey!') + body: expect.stringContaining('Error: Hey!'), }) }) }) it('handles unhandled errors from selectors', async () => { - const rawUrlSelector: Selector = () => { + const rawUrlSelector = createPrismySelector(() => { throw new Error('Hey!') - } + }) const handler = prismy( [rawUrlSelector], - url => { + (url) => { return res(url) }, - [] + [], ) - await testHandler(handler, async url => { + await testHandler(handler, async (url) => { const response = await got(url, { - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 500, - body: expect.stringContaining('Error: Hey!') + body: expect.stringContaining('Error: Hey!'), }) }) }) diff --git a/specs/types/middleware.ts b/specs/types/middleware.ts index af4e53b..c84fdc7 100644 --- a/specs/types/middleware.ts +++ b/specs/types/middleware.ts @@ -2,15 +2,15 @@ import { middleware, urlSelector, methodSelector, - AsyncSelector, ResponseObject, prismy, res, } from '../../src' import { URL } from 'url' import { expectType } from '../helpers' +import { PrismySelector } from '../../src/selectors/createSelector' -const asyncUrlSelector: AsyncSelector = async () => urlSelector() +const asyncUrlSelector: PrismySelector = urlSelector const middleware1 = middleware( [urlSelector, methodSelector, asyncUrlSelector], diff --git a/specs/types/prismy.ts b/specs/types/prismy.ts index 3c6298f..d649ca4 100644 --- a/specs/types/prismy.ts +++ b/specs/types/prismy.ts @@ -3,13 +3,13 @@ import { urlSelector, methodSelector, res, - AsyncSelector, ResponseObject, } from '../../src' import { URL } from 'url' import { expectType } from '../helpers' +import { PrismySelector } from '../../src/selectors/createSelector' -const asyncUrlSelector: AsyncSelector = async () => urlSelector() +const asyncUrlSelector: PrismySelector = urlSelector const handler1 = prismy( [urlSelector, methodSelector, asyncUrlSelector], diff --git a/src/middleware.ts b/src/middleware.ts index 476d627..da156e5 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,4 +1,9 @@ -import { ResponseObject, Selector, SelectorReturnTypeTuple, PrismyMiddleware } from './types' +import { PrismySelector } from './selectors/createSelector' +import { + ResponseObject, + SelectorReturnTypeTuple, + PrismyMiddleware, +} from './types' import { compileHandler } from './utils' /** @@ -36,11 +41,11 @@ import { compileHandler } from './utils' * * @public */ -export function middleware[]>( +export function middleware[]>( selectors: [...SS], mhandler: ( - next: () => Promise> - ) => (...args: SelectorReturnTypeTuple) => Promise> + next: () => Promise>, + ) => (...args: SelectorReturnTypeTuple) => Promise>, ): PrismyMiddleware> { const middleware = () => async (next: () => Promise>) => compileHandler(selectors, mhandler(next))() diff --git a/src/prismy.ts b/src/prismy.ts index 5ad7c3d..3aa0c40 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -1,15 +1,15 @@ import { AsyncLocalStorage } from 'async_hooks' import { IncomingMessage, ServerResponse } from 'http' import { createErrorResObject } from './error' +import { PrismySelector } from './selectors/createSelector' import { send } from './send' import { ResponseObject, - Selector, PrismyPureMiddleware, Promisable, PrismyContext, PrismyHandler, - SelectorReturnTypeTuple + SelectorReturnTypeTuple, } from './types' import { compileHandler } from './utils' @@ -46,10 +46,12 @@ export function getPrismyContext(): PrismyContext { * @public * */ -export function prismy[]>( +export function prismy[]>( selectors: [...S], - handler: (...args: SelectorReturnTypeTuple) => Promisable>, - middlewareList: PrismyPureMiddleware[] = [] + handler: ( + ...args: SelectorReturnTypeTuple + ) => Promisable>, + middlewareList: PrismyPureMiddleware[] = [], ): PrismyHandler> { const resResolver = async () => { const next = async () => compileHandler(selectors, handler)() @@ -72,9 +74,12 @@ export function prismy[]>( return resObject } - async function requestListener(request: IncomingMessage, response: ServerResponse) { + async function requestListener( + request: IncomingMessage, + response: ServerResponse, + ) { const context: PrismyContext = { - req: request + req: request, } prismyContextStorage.run(context, async () => { const resObject = await resResolver() diff --git a/src/router.ts b/src/router.ts index ae1fc14..1500b4e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,20 +1,37 @@ -import { PrismyContext, Selector, SyncSelector, PrismyHandler } from './types' +import { PrismyContext, PrismyHandler } from './types' import { methodSelector, urlSelector } from './selectors' import { match as createMatchFunction } from 'path-to-regexp' import { getPrismyContext, prismy } from './prismy' import { createError } from './error' +import { + createPrismySelector, + PrismySelector, +} from './selectors/createSelector' -export type RouteMethod = 'get' | 'put' | 'patch' | 'post' | 'delete' | 'options' | '*' +export type RouteMethod = + | 'get' + | 'put' + | 'patch' + | 'post' + | 'delete' + | 'options' + | '*' export type RouteIndicator = [string, RouteMethod] -export type RouteParams = [string | RouteIndicator, PrismyHandler] +export type RouteParams = [ + string | RouteIndicator, + PrismyHandler, +] type Route = { indicator: RouteIndicator listener: PrismyHandler } -export function router(routes: RouteParams[], options: PrismyRouterOptions = {}) { - const compiledRoutes = routes.map(routeParams => { +export function router( + routes: RouteParams[], + options: PrismyRouterOptions = {}, +) { + const compiledRoutes = routes.map((routeParams) => { const { indicator, listener } = createRoute(routeParams) const [targetPath, method] = indicator const compiledTargetPath = removeTralingSlash(targetPath) @@ -23,7 +40,7 @@ export function router(routes: RouteParams[], options: PrismyRouterOpti method, match, listener, - targetPath: compiledTargetPath + targetPath: compiledTargetPath, } }) return prismy([methodSelector, urlSelector], (method, url) => { @@ -53,17 +70,19 @@ export function router(routes: RouteParams[], options: PrismyRouterOpti }) } -function createRoute(routeParams: RouteParams[]>): Route[]> { +function createRoute( + routeParams: RouteParams[]>, +): Route[]> { const [indicator, listener] = routeParams if (typeof indicator === 'string') { return { indicator: [indicator, 'get'], - listener + listener, } } return { indicator, - listener + listener, } } const routeParamsSymbol = Symbol('route params') @@ -76,12 +95,14 @@ function getRouteParamsFromPrismyContext(context: PrismyContext) { return (context as any)[routeParamsSymbol] } -export function routeParamSelector(paramName: string): SyncSelector { - return () => { +export function routeParamSelector( + paramName: string, +): PrismySelector { + return createPrismySelector(() => { const context = getPrismyContext() const param = getRouteParamsFromPrismyContext(context)[paramName] return param != null ? param : null - } + }) } interface PrismyRouterOptions {} diff --git a/src/selectors/body.ts b/src/selectors/body.ts index 073aca5..c1e124b 100644 --- a/src/selectors/body.ts +++ b/src/selectors/body.ts @@ -2,7 +2,7 @@ import { parse } from 'querystring' import { getPrismyContext } from '../prismy' import { readJsonBody, readTextBody } from '../bodyReaders' import { createError } from '../error' -import { AsyncSelector } from '../types' +import { createPrismySelector, PrismySelector } from './createSelector' /** * Options for {@link bodySelector} @@ -41,8 +41,8 @@ export interface BodySelectorOptions { */ export function BodySelector( options?: BodySelectorOptions, -): AsyncSelector { - return async () => { +): PrismySelector { + return createPrismySelector(async () => { const { req } = getPrismyContext() const type = req.headers['content-type'] @@ -59,7 +59,7 @@ export function BodySelector( } else { return readTextBody(req, options) } - } + }) } /** diff --git a/src/selectors/bufferBody.ts b/src/selectors/bufferBody.ts index 56dfe30..b4bc2af 100644 --- a/src/selectors/bufferBody.ts +++ b/src/selectors/bufferBody.ts @@ -1,6 +1,6 @@ -import { AsyncSelector } from '../types' import { readBufferBody } from '../bodyReaders' import { getPrismyContext } from '../prismy' +import { createPrismySelector, PrismySelector } from './createSelector' /** * Options for {@link createBufferBodySelector} @@ -39,11 +39,11 @@ export interface BufferBodySelectorOptions { */ export function BufferBodySelector( options?: BufferBodySelectorOptions, -): AsyncSelector { - return () => { +): PrismySelector { + return createPrismySelector(() => { const { req } = getPrismyContext() return readBufferBody(req, options) - } + }) } /** diff --git a/src/selectors/createSelector.ts b/src/selectors/createSelector.ts new file mode 100644 index 0000000..deae0ed --- /dev/null +++ b/src/selectors/createSelector.ts @@ -0,0 +1,15 @@ +export class PrismySelector { + selectorFunction: () => Promise | T + constructor(selectorFunction: () => Promise | T) { + this.selectorFunction = selectorFunction + } + resolve(): T | Promise { + return this.selectorFunction() + } +} + +export function createPrismySelector( + selectorFunction: () => T | Promise, +) { + return new PrismySelector(selectorFunction) +} diff --git a/src/selectors/headers.ts b/src/selectors/headers.ts index 69dbdc2..f417418 100644 --- a/src/selectors/headers.ts +++ b/src/selectors/headers.ts @@ -1,6 +1,6 @@ import { IncomingHttpHeaders } from 'http' import { getPrismyContext } from '../prismy' -import { SyncSelector } from '../types' +import { createPrismySelector, PrismySelector } from './createSelector' /** * A selector to extract the headers of a request @@ -21,7 +21,8 @@ import { SyncSelector } from '../types' * * @public */ -export const headersSelector: SyncSelector = () => { - const { req } = getPrismyContext() - return req.headers -} +export const headersSelector: PrismySelector = + createPrismySelector(() => { + const { req } = getPrismyContext() + return req.headers + }) diff --git a/src/selectors/jsonBody.ts b/src/selectors/jsonBody.ts index afd78f8..137b0d4 100644 --- a/src/selectors/jsonBody.ts +++ b/src/selectors/jsonBody.ts @@ -1,7 +1,7 @@ import { readJsonBody } from '../bodyReaders' import { createError } from '../error' -import { AsyncSelector } from '../types' import { getPrismyContext } from '../prismy' +import { createPrismySelector, PrismySelector } from './createSelector' /** * Options for {@link createJsonBodySelector} @@ -43,8 +43,8 @@ export interface JsonBodySelectorOptions { */ export function JsonBodySelector( options?: JsonBodySelectorOptions, -): AsyncSelector { - return () => { +): PrismySelector { + return createPrismySelector(() => { const { req } = getPrismyContext() const contentType = req.headers['content-type'] if (!isContentTypeIsApplicationJSON(contentType)) { @@ -55,7 +55,7 @@ export function JsonBodySelector( } return readJsonBody(req, options) - } + }) } function isContentTypeIsApplicationJSON(contentType: string | undefined) { diff --git a/src/selectors/method.ts b/src/selectors/method.ts index 8d186db..80b886b 100644 --- a/src/selectors/method.ts +++ b/src/selectors/method.ts @@ -1,5 +1,5 @@ import { getPrismyContext } from '../prismy' -import { SyncSelector } from '../types' +import { createPrismySelector, PrismySelector } from './createSelector' /** * Selector to extract the HTTP method from the request @@ -23,7 +23,8 @@ import { SyncSelector } from '../types' * * @public */ -export const methodSelector: SyncSelector = () => { - const { req } = getPrismyContext() - return req.method -} +export const methodSelector: PrismySelector = + createPrismySelector(() => { + const { req } = getPrismyContext() + return req.method + }) diff --git a/src/selectors/query.ts b/src/selectors/query.ts index 88ccdc6..b5e81af 100644 --- a/src/selectors/query.ts +++ b/src/selectors/query.ts @@ -1,6 +1,6 @@ import { ParsedUrlQuery, parse } from 'querystring' import { prismyContextStorage } from '../prismy' -import { SyncSelector } from '../types' +import { createPrismySelector, PrismySelector } from './createSelector' import { urlSelector } from './url' const querySymbol = Symbol('prismy-query') @@ -28,14 +28,15 @@ const querySymbol = Symbol('prismy-query') * * @public */ -export const querySelector: SyncSelector = () => { - const context = prismyContextStorage.getStore()! - let query: ParsedUrlQuery | undefined = context[querySymbol] - if (query == null) { - const url = urlSelector() - /* istanbul ignore next */ - context[querySymbol] = query = - url.search != null ? parse(url.search.slice(1)) : {} - } - return query -} +export const querySelector: PrismySelector = + createPrismySelector(async () => { + const context = prismyContextStorage.getStore()! + let query: ParsedUrlQuery | undefined = context[querySymbol] + if (query == null) { + const url = await urlSelector.resolve() + /* istanbul ignore next */ + context[querySymbol] = query = + url.search != null ? parse(url.search.slice(1)) : {} + } + return query + }) diff --git a/src/selectors/searchParam.ts b/src/selectors/searchParam.ts index dda6a51..2fab603 100644 --- a/src/selectors/searchParam.ts +++ b/src/selectors/searchParam.ts @@ -1,4 +1,4 @@ -import { SyncSelector } from '../types' +import { createPrismySelector, PrismySelector } from './createSelector' import { urlSelector } from './url' /** @@ -22,11 +22,12 @@ import { urlSelector } from './url' */ export const SearchParamSelector: ( name: string, -) => SyncSelector = (name) => () => { - const url = urlSelector() +) => PrismySelector = (name) => + createPrismySelector(async () => { + const url = await urlSelector.resolve() - return url.searchParams.get(name) -} + return url.searchParams.get(name) + }) /** * Create a selector which resolves a value list of the search param. @@ -52,8 +53,9 @@ export const SearchParamSelector: ( */ export const SearchParamListSelector: ( name: string, -) => SyncSelector = (name) => () => { - const url = urlSelector() +) => PrismySelector = (name) => + createPrismySelector(async () => { + const url = await urlSelector.resolve() - return url.searchParams.getAll(name) -} + return url.searchParams.getAll(name) + }) diff --git a/src/selectors/textBody.ts b/src/selectors/textBody.ts index a34708f..c8363a3 100644 --- a/src/selectors/textBody.ts +++ b/src/selectors/textBody.ts @@ -1,6 +1,6 @@ import { getPrismyContext } from '../prismy' import { readTextBody } from '../bodyReaders' -import { AsyncSelector } from '../types' +import { createPrismySelector, PrismySelector } from './createSelector' /** * Options for {@link textBodySelector} @@ -39,11 +39,11 @@ export interface TextBodySelectorOptions { */ export function TextBodySelector( options?: TextBodySelectorOptions, -): AsyncSelector { - return () => { +): PrismySelector { + return createPrismySelector(() => { const { req } = getPrismyContext() return readTextBody(req, options) - } + }) } /** diff --git a/src/selectors/url.ts b/src/selectors/url.ts index 4505c8f..50f511b 100644 --- a/src/selectors/url.ts +++ b/src/selectors/url.ts @@ -1,6 +1,6 @@ import { URL } from 'url' import { getPrismyContext } from '../prismy' -import { SyncSelector } from '../types' +import { createPrismySelector } from './createSelector' const urlSymbol = Symbol('prismy-url') @@ -23,7 +23,7 @@ const urlSymbol = Symbol('prismy-url') * * @public */ -export const urlSelector: SyncSelector = () => { +export const urlSelector = createPrismySelector((): URL => { const context = getPrismyContext() let url: URL | undefined = context[urlSymbol] if (url == null) { @@ -35,4 +35,4 @@ export const urlSelector: SyncSelector = () => { ) } return url -} +}) diff --git a/src/selectors/urlEncodedBody.ts b/src/selectors/urlEncodedBody.ts index 6f29ef3..526160e 100644 --- a/src/selectors/urlEncodedBody.ts +++ b/src/selectors/urlEncodedBody.ts @@ -2,7 +2,7 @@ import { ParsedUrlQuery, parse } from 'querystring' import { getPrismyContext } from '../prismy' import { readTextBody } from '../bodyReaders' import { createError } from '../error' -import { AsyncSelector } from '../types' +import { createPrismySelector, PrismySelector } from './createSelector' /** * Options for {@link createUrlEncodedBodySelector} @@ -43,8 +43,8 @@ export interface UrlEncodedBodySelectorOptions { */ export function UrlEncodedBodySelector( options?: UrlEncodedBodySelectorOptions, -): AsyncSelector { - return async () => { +): PrismySelector { + return createPrismySelector(async () => { const { req } = getPrismyContext() const textBody = await readTextBody(req, options) try { @@ -53,7 +53,7 @@ export function UrlEncodedBodySelector( /* istanbul ignore next */ throw createError(400, 'Invalid url-encoded body', error) } - } + }) } /** diff --git a/src/send.ts b/src/send.ts index aee4e9d..5117bcd 100644 --- a/src/send.ts +++ b/src/send.ts @@ -15,7 +15,9 @@ import { ResponseObject } from './types' export const send = ( request: IncomingMessage, response: ServerResponse, - sendable: ((request: IncomingMessage, response: ServerResponse) => void) | ResponseObject + sendable: + | ((request: IncomingMessage, response: ServerResponse) => void) + | ResponseObject, ) => { if (typeof sendable === 'function') { sendable(request, response) @@ -62,7 +64,9 @@ export const send = ( } } - const stringifiedBody = bodyIsNotString ? JSON.stringify(body) : body.toString() + const stringifiedBody = bodyIsNotString + ? JSON.stringify(body) + : body.toString() response.setHeader('Content-Length', Buffer.byteLength(stringifiedBody)) response.end(stringifiedBody) diff --git a/src/types.ts b/src/types.ts index cd979b8..cd83bc2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from 'http' +import { PrismySelector } from './selectors/createSelector' /** * Request context used in selectors @@ -9,25 +10,6 @@ export interface PrismyContext { req: IncomingMessage } -/** - * A Synchronous argument selector - * - * @public - */ -export type SyncSelector = () => T -/** - * An asynchronous argument selector - * - * @public - */ -export type AsyncSelector = () => Promise -/** - * An argument selector to extract arguments for the handler - * - * @public - */ -export type Selector = SyncSelector | AsyncSelector - /** * Get the return type array of Selectors * @@ -35,8 +17,8 @@ export type Selector = SyncSelector | AsyncSelector */ export type SelectorTuple = [ ...{ - [I in keyof SS]: Selector - } + [I in keyof SS]: PrismySelector + }, ] /** @@ -44,17 +26,19 @@ export type SelectorTuple = [ * * @public */ -export type SelectorReturnType = S extends Selector ? T : never +export type SelectorReturnType = S extends PrismySelector + ? T + : never /** * Get the return type array of Selectors * * @public */ -export type SelectorReturnTypeTuple[]> = [ +export type SelectorReturnTypeTuple[]> = [ ...{ [I in keyof SS]: SelectorReturnType - } + }, ] /** @@ -105,8 +89,11 @@ export interface PrismyPureMiddleware { * * @public */ -export interface PrismyMiddleware extends PrismyPureMiddleware { - mhandler(next: () => Promise>): (...args: A) => Promise> +export interface PrismyMiddleware + extends PrismyPureMiddleware { + mhandler( + next: () => Promise>, + ): (...args: A) => Promise> } /** diff --git a/src/utils.ts b/src/utils.ts index cf7573f..bef76c2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ -import { Selector, SelectorReturnTypeTuple } from './types' +import { PrismySelector } from './selectors/createSelector' +import { SelectorReturnTypeTuple } from './types' /** * Compile a handler into a runnable function by resolving selectors @@ -10,7 +11,7 @@ import { Selector, SelectorReturnTypeTuple } from './types' * * @internal */ -export function compileHandler[], R>( +export function compileHandler[], R>( selectors: [...S], handler: (...args: SelectorReturnTypeTuple) => R, ): () => Promise { @@ -29,12 +30,12 @@ export function compileHandler[], R>( * * @internal */ -async function resolveSelectors[]>( +async function resolveSelectors[]>( selectors: [...S], ): Promise> { const resolvedValues = [] for (const selector of selectors) { - const resolvedValue = await selector() + const resolvedValue = await selector.resolve() resolvedValues.push(resolvedValue) } From c7696f99e45d64b45eb4bb060391825dd3479b00 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 12:01:16 +0900 Subject: [PATCH 008/109] Simplify type tests --- specs/helpers.ts | 5 ++++- specs/types/middleware.ts | 8 ++------ specs/types/prismy.ts | 17 +++++------------ 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/specs/helpers.ts b/specs/helpers.ts index 56fa32e..60263c0 100644 --- a/specs/helpers.ts +++ b/specs/helpers.ts @@ -5,7 +5,10 @@ import { RequestListener } from 'http' export type TestCallback = (url: string) => Promise | void /* istanbul ignore next */ -export async function testHandler(handler: RequestListener, testCallback: TestCallback): Promise { +export async function testHandler( + handler: RequestListener, + testCallback: TestCallback, +): Promise { const server = new http.Server(handler) const url = await listen(server) diff --git a/specs/types/middleware.ts b/specs/types/middleware.ts index c84fdc7..a91e2c6 100644 --- a/specs/types/middleware.ts +++ b/specs/types/middleware.ts @@ -8,16 +8,12 @@ import { } from '../../src' import { URL } from 'url' import { expectType } from '../helpers' -import { PrismySelector } from '../../src/selectors/createSelector' - -const asyncUrlSelector: PrismySelector = urlSelector const middleware1 = middleware( - [urlSelector, methodSelector, asyncUrlSelector], - (next) => async (url, method, url2) => { + [urlSelector, methodSelector], + (next) => async (url, method) => { expectType(url) expectType(method) - expectType(url2) return next() }, ) diff --git a/specs/types/prismy.ts b/specs/types/prismy.ts index d649ca4..d1197dc 100644 --- a/specs/types/prismy.ts +++ b/specs/types/prismy.ts @@ -7,19 +7,12 @@ import { } from '../../src' import { URL } from 'url' import { expectType } from '../helpers' -import { PrismySelector } from '../../src/selectors/createSelector' -const asyncUrlSelector: PrismySelector = urlSelector - -const handler1 = prismy( - [urlSelector, methodSelector, asyncUrlSelector], - (url, method, url2) => { - expectType(url) - expectType(method) - expectType(url2) - return res('') - }, -) +const handler1 = prismy([urlSelector, methodSelector], (url, method) => { + expectType(url) + expectType(method) + return res('') +}) expectType< ( From 9de0b50bf71675b7efb3badf9c5c422d9e11d1c1 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 17:38:11 +0900 Subject: [PATCH 009/109] Simplify middleware interface --- specs/prismy.spec.ts | 30 ++++++++++++++++++------------ src/middleware.ts | 8 +++++--- src/prismy.ts | 5 +++-- src/types.ts | 15 +++++---------- src/utils.ts | 3 ++- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 31114a2..ab465da 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -69,11 +69,13 @@ describe('prismy', () => { }) it('applys middleware', async () => { - const errorMiddleware: PrismyPureMiddleware = () => async (next) => { - try { - return await next() - } catch (error) { - return err(500, (error as any).message) + const errorMiddleware: PrismyPureMiddleware = (next) => { + return async () => { + try { + return await next() + } catch (error) { + return err(500, (error as any).message) + } } } const rawUrlSelector = createPrismySelector( @@ -99,14 +101,18 @@ describe('prismy', () => { }) it('applys middleware orderly', async () => { - const problematicMiddleware: PrismyPureMiddleware = () => async (next) => { - throw new Error('Hey!') + const problematicMiddleware: PrismyPureMiddleware = (next) => { + return () => { + throw new Error('Hey!') + } } - const errorMiddleware: PrismyPureMiddleware = () => async (next) => { - try { - return await next() - } catch (error) { - return res((error as any).message, 500) + const errorMiddleware: PrismyPureMiddleware = (next) => { + return async () => { + try { + return await next() + } catch (error) { + return res((error as any).message, 500) + } } } const rawUrlSelector = createPrismySelector( diff --git a/src/middleware.ts b/src/middleware.ts index da156e5..20ced9d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,3 +1,4 @@ +import { PrismyNextFunction } from '.' import { PrismySelector } from './selectors/createSelector' import { ResponseObject, @@ -44,11 +45,12 @@ import { compileHandler } from './utils' export function middleware[]>( selectors: [...SS], mhandler: ( - next: () => Promise>, + next: PrismyNextFunction, ) => (...args: SelectorReturnTypeTuple) => Promise>, ): PrismyMiddleware> { - const middleware = () => async (next: () => Promise>) => - compileHandler(selectors, mhandler(next))() + const middleware = (next: PrismyNextFunction) => { + return compileHandler(selectors, mhandler(next)) + } middleware.mhandler = mhandler return middleware diff --git a/src/prismy.ts b/src/prismy.ts index 3aa0c40..9537cd8 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -1,5 +1,6 @@ import { AsyncLocalStorage } from 'async_hooks' import { IncomingMessage, ServerResponse } from 'http' +import { PrismyNextFunction } from '.' import { createErrorResObject } from './error' import { PrismySelector } from './selectors/createSelector' import { send } from './send' @@ -54,10 +55,10 @@ export function prismy[]>( middlewareList: PrismyPureMiddleware[] = [], ): PrismyHandler> { const resResolver = async () => { - const next = async () => compileHandler(selectors, handler)() + const next: PrismyNextFunction = compileHandler(selectors, handler) const pipe = middlewareList.reduce((next, middleware) => { - return () => middleware()(next) + return middleware(next) }, next) let resObject diff --git a/src/types.ts b/src/types.ts index cd83bc2..b423a82 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,12 +69,7 @@ export interface ResponseObject { */ export type Res = ResponseObject -/** - * alias for Promise> for user with async handlers - * - * @public - */ -export type AsyncRes = Promise> +export type PrismyNextFunction = () => Promise> /** * prismy compaticble middleware @@ -82,7 +77,7 @@ export type AsyncRes = Promise> * @public */ export interface PrismyPureMiddleware { - (): (next: () => Promise>) => Promise> + (next: PrismyNextFunction): PrismyNextFunction } /** * prismy compatible middleware @@ -91,9 +86,9 @@ export interface PrismyPureMiddleware { */ export interface PrismyMiddleware extends PrismyPureMiddleware { - mhandler( - next: () => Promise>, - ): (...args: A) => Promise> + mhandler: ( + next: PrismyNextFunction, + ) => (...args: A) => Promise> } /** diff --git a/src/utils.ts b/src/utils.ts index bef76c2..2630077 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,4 @@ +import { Promisable } from '.' import { PrismySelector } from './selectors/createSelector' import { SelectorReturnTypeTuple } from './types' @@ -13,7 +14,7 @@ import { SelectorReturnTypeTuple } from './types' */ export function compileHandler[], R>( selectors: [...S], - handler: (...args: SelectorReturnTypeTuple) => R, + handler: (...args: SelectorReturnTypeTuple) => Promisable, ): () => Promise { return async () => { return handler(...(await resolveSelectors(selectors))) From 03bb1128d5584a923b1b91c5c480be40dc6f8424 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 17:53:00 +0900 Subject: [PATCH 010/109] Remove PrismyPureMiddleware type --- specs/middleware.spec.ts | 10 ++-------- specs/prismy.spec.ts | 40 +++++++++++++++------------------------- src/prismy.ts | 5 ++--- src/types.ts | 12 ++---------- 4 files changed, 21 insertions(+), 46 deletions(-) diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 2651876..0893e97 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,12 +1,6 @@ import got from 'got' import { testHandler } from './helpers' -import { - prismy, - res, - PrismyPureMiddleware, - middleware, - getPrismyContext, -} from '../src' +import { prismy, res, middleware, getPrismyContext } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' describe('middleware', () => { @@ -14,7 +8,7 @@ describe('middleware', () => { const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) - const errorMiddleware: PrismyPureMiddleware = middleware( + const errorMiddleware = middleware( [rawUrlSelector], (next) => async (url) => { try { diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index ab465da..7688adc 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -1,12 +1,6 @@ import got from 'got' import { testHandler } from './helpers' -import { - prismy, - res, - PrismyPureMiddleware, - err, - getPrismyContext, -} from '../src' +import { prismy, res, err, getPrismyContext, middleware } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' describe('prismy', () => { @@ -53,7 +47,7 @@ describe('prismy', () => { }) }) - it('expose raw prismy handler for unit tests', () => { + it('exposes raw prismy handler for unit tests', () => { const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) @@ -68,8 +62,8 @@ describe('prismy', () => { }) }) - it('applys middleware', async () => { - const errorMiddleware: PrismyPureMiddleware = (next) => { + it('applies middleware', async () => { + const errorMiddleware = middleware([], (next) => { return async () => { try { return await next() @@ -77,7 +71,7 @@ describe('prismy', () => { return err(500, (error as any).message) } } - } + }) const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) @@ -100,21 +94,17 @@ describe('prismy', () => { }) }) - it('applys middleware orderly', async () => { - const problematicMiddleware: PrismyPureMiddleware = (next) => { - return () => { - throw new Error('Hey!') - } - } - const errorMiddleware: PrismyPureMiddleware = (next) => { - return async () => { - try { - return await next() - } catch (error) { - return res((error as any).message, 500) - } + it('applies middleware orderly', async () => { + const problematicMiddleware = middleware([], (next) => () => { + throw new Error('Hey!') + }) + const errorMiddleware = middleware([], (next) => async () => { + try { + return await next() + } catch (error) { + return res((error as any).message, 500) } - } + }) const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) diff --git a/src/prismy.ts b/src/prismy.ts index 9537cd8..cbd185f 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -1,12 +1,11 @@ import { AsyncLocalStorage } from 'async_hooks' import { IncomingMessage, ServerResponse } from 'http' -import { PrismyNextFunction } from '.' +import { PrismyMiddleware, PrismyNextFunction } from '.' import { createErrorResObject } from './error' import { PrismySelector } from './selectors/createSelector' import { send } from './send' import { ResponseObject, - PrismyPureMiddleware, Promisable, PrismyContext, PrismyHandler, @@ -52,7 +51,7 @@ export function prismy[]>( handler: ( ...args: SelectorReturnTypeTuple ) => Promisable>, - middlewareList: PrismyPureMiddleware[] = [], + middlewareList: PrismyMiddleware[] = [], ): PrismyHandler> { const resResolver = async () => { const next: PrismyNextFunction = compileHandler(selectors, handler) diff --git a/src/types.ts b/src/types.ts index b423a82..c5fa5ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,21 +71,13 @@ export type Res = ResponseObject export type PrismyNextFunction = () => Promise> -/** - * prismy compaticble middleware - * - * @public - */ -export interface PrismyPureMiddleware { - (next: PrismyNextFunction): PrismyNextFunction -} /** * prismy compatible middleware * * @public */ -export interface PrismyMiddleware - extends PrismyPureMiddleware { +export interface PrismyMiddleware { + (next: PrismyNextFunction): PrismyNextFunction mhandler: ( next: PrismyNextFunction, ) => (...args: A) => Promise> From efc66460a18a69dfddcfbf17b2e251f1bc307819 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 18:22:38 +0900 Subject: [PATCH 011/109] Use weakmap instead of symbol --- src/router.ts | 7 ++++--- src/selectors/query.ts | 8 ++++---- src/selectors/url.ts | 10 ++++------ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/router.ts b/src/router.ts index 1500b4e..040b625 100644 --- a/src/router.ts +++ b/src/router.ts @@ -85,14 +85,15 @@ function createRoute( listener, } } -const routeParamsSymbol = Symbol('route params') + +const routeParamsMap = new WeakMap() function setRouteParamsToPrismyContext(context: PrismyContext, params: object) { - ;(context as any)[routeParamsSymbol] = params + routeParamsMap.set(context, params) } function getRouteParamsFromPrismyContext(context: PrismyContext) { - return (context as any)[routeParamsSymbol] + return routeParamsMap.get(context) } export function routeParamSelector( diff --git a/src/selectors/query.ts b/src/selectors/query.ts index b5e81af..c89b0de 100644 --- a/src/selectors/query.ts +++ b/src/selectors/query.ts @@ -3,7 +3,7 @@ import { prismyContextStorage } from '../prismy' import { createPrismySelector, PrismySelector } from './createSelector' import { urlSelector } from './url' -const querySymbol = Symbol('prismy-query') +const queryMap = new WeakMap() /** * @deprecated Use SearchParamSelector or SearchParamListSelector. @@ -31,12 +31,12 @@ const querySymbol = Symbol('prismy-query') export const querySelector: PrismySelector = createPrismySelector(async () => { const context = prismyContextStorage.getStore()! - let query: ParsedUrlQuery | undefined = context[querySymbol] + let query: ParsedUrlQuery | undefined = queryMap.get(context) if (query == null) { const url = await urlSelector.resolve() /* istanbul ignore next */ - context[querySymbol] = query = - url.search != null ? parse(url.search.slice(1)) : {} + query = url.search != null ? parse(url.search.slice(1)) : {} + queryMap.set(context, query) } return query }) diff --git a/src/selectors/url.ts b/src/selectors/url.ts index 50f511b..c2bd4f8 100644 --- a/src/selectors/url.ts +++ b/src/selectors/url.ts @@ -2,7 +2,7 @@ import { URL } from 'url' import { getPrismyContext } from '../prismy' import { createPrismySelector } from './createSelector' -const urlSymbol = Symbol('prismy-url') +const urlMap = new WeakMap() /** * Selector for extracting the requested URL @@ -25,14 +25,12 @@ const urlSymbol = Symbol('prismy-url') */ export const urlSelector = createPrismySelector((): URL => { const context = getPrismyContext() - let url: URL | undefined = context[urlSymbol] + let url: URL | undefined = urlMap.get(context) if (url == null) { const { req } = context /* istanbul ignore next */ - url = context[urlSymbol] = new URL( - req.url == null ? '' : req.url, - `http://${req.headers.host}`, - ) + url = new URL(req.url == null ? '' : req.url, `http://${req.headers.host}`) + urlMap.set(context, url) } return url }) From 0c5a79d4cb7eefee304ef951708d0c914f4c6569 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 18:35:50 +0900 Subject: [PATCH 012/109] Update v4 todo --- v4-todo.md | 56 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/v4-todo.md b/v4-todo.md index 0d01d5e..d4164ed 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -3,13 +3,12 @@ - redesigned router interface - introduced route method - Removed notFoundHandler option -- Redesign selectors interfaces +- [x] Redesigned selector interface + - [x] Renamed factory method (ex: createBodySelector(Deprecated) => BodySelector) ```ts - // SelectorFactory, must be `PascalCase` - const BodySelector: () => Selector - - // Selector, must be `camelCase` - const bodySelector: Selector + import { BodySelector } from 'prismy' + // Use `camelCase` when naming a created selector. + const bodySelector = BodySelector() prismy([ // Below two are functionally identical. Use whichever style you like. @@ -17,14 +16,41 @@ bodySelector ], handler) ``` -- `urlSelector` is now retruning WHATWG URL, not legacy `urlObject` to improve security. -- Removed `skipContentTypeCheck` option from `JsonBodySelectorOptions` to improve security. -- One param for one query selector like URL -- Added Symbol to selector to avoid misconfig -- [ ] Adopted async local storage to communicate between selectors, middleware and handlers - - Added `getPrismyContext` method to get context. (must be used in the scope of selectors, middleware and handlers) - - Removed `contextSelector`, use `getPrismyContext` -- Simplified middleware interface + - [x] Changed selector interface and its name + - [x] Removed `Selector`, `SyncSelector` and `AsyncSelector` interface. Use `PrismySelector` + - [x] Selector is not a function anymore. It must be extends from `PrismySelector` Class. + - So typescript can throws an error if any of selectors in `prismy([...selectors], handler)` is not valid. + ```ts + // If the selector interface were a function, the statement below won't throw any type error although it is incorrect. + prismy([ + BodySelector(), // `() => Body` this function is a selector. + BodySelector // `() => PrismySelector`, this function creates a selector when called but definitely not a selector by itself. + ]) + ``` + - To create a custom selector, you must use `createPrismySelector` + ```ts + function selectSomethingFromContext(context: PrismyContext): T + + // Previous + const legacySelector: Selector = (context): T | Promise => { + const selected = selectSomethingFromContext(context) + return selected + } + + // Now + const newSelector = createPrismySelector(() => { + const context = getPrismyContext() + const selected = selectSomethingFromContext(context) + return selected + }) + ``` +- [x] `urlSelector` is now retruning WHATWG URL, not legacy `urlObject` to improve security. +- [x] Removed `skipContentTypeCheck` option from `JsonBodySelectorOptions` to improve security. +- [x] One param for one query selector like URL +- [x] Adopted async local storage to communicate between selectors, middleware and handlers + - [x] Added `getPrismyContext` method to get context. (must be used in the scope of selectors, middleware and handlers) + - [x] Removed `contextSelector`, use `getPrismyContext` +- [x] Simplified middleware interface - Before ```ts @@ -37,6 +63,8 @@ (next: () => Promise) => Promise ``` - Return without res +- Include prismy-cookie +- Added DI Selector # Fix router From 4212e782b93060f2683a49a4ce7947bac10ade29 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 20:15:06 +0900 Subject: [PATCH 013/109] Refactor router --- specs/router.spec.ts | 63 ++++++++++++++---------------- src/router.ts | 93 ++++++++++++++++++++++++-------------------- 2 files changed, 81 insertions(+), 75 deletions(-) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 92d7df1..c27424d 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -1,6 +1,6 @@ import got from 'got' import { testHandler } from './helpers' -import { routeParamSelector, prismy, res, router } from '../src' +import { routeParamSelector, prismy, res, router, Route } from '../src' import { join } from 'path' describe('router', () => { @@ -13,19 +13,16 @@ describe('router', () => { return res('b') }) - const routerHandler = router([ - ['/a', handlerA], - ['/b', handlerB] - ]) + const routerHandler = router([Route('/a', handlerA), Route('/b', handlerB)]) - await testHandler(routerHandler, async url => { + await testHandler(routerHandler, async (url) => { const response = await got(join(url, 'b'), { - method: 'GET' + method: 'GET', }) expect(response).toMatchObject({ statusCode: 200, - body: 'b' + body: 'b', }) }) }) @@ -40,29 +37,29 @@ describe('router', () => { }) const routerHandler = router([ - [['/', 'get'], handlerA], - [['/', 'post'], handlerB] + Route(['/', 'get'], handlerA), + Route(['/', 'post'], handlerB), ]) - await testHandler(routerHandler, async url => { + await testHandler(routerHandler, async (url) => { const response = await got(url, { - method: 'GET' + method: 'GET', }) expect(response).toMatchObject({ statusCode: 200, - body: 'a' + body: 'a', }) }) - await testHandler(routerHandler, async url => { + await testHandler(routerHandler, async (url) => { const response = await got(url, { - method: 'POST' + method: 'POST', }) expect(response).toMatchObject({ statusCode: 200, - body: 'b' + body: 'b', }) }) }) @@ -72,23 +69,23 @@ describe('router', () => { const handlerA = prismy([], () => { return res('a') }) - const handlerB = prismy([routeParamSelector('id')], id => { + const handlerB = prismy([routeParamSelector('id')], (id) => { return res(id) }) const routerHandler = router([ - ['/a', handlerA], - ['/b/:id', handlerB] + Route('/a', handlerA), + Route('/b/:id', handlerB), ]) - await testHandler(routerHandler, async url => { + await testHandler(routerHandler, async (url) => { const response = await got(join(url, 'b/test-param'), { - method: 'GET' + method: 'GET', }) expect(response).toMatchObject({ statusCode: 200, - body: 'test-param' + body: 'test-param', }) }) }) @@ -98,23 +95,23 @@ describe('router', () => { const handlerA = prismy([], () => { return res('a') }) - const handlerB = prismy([routeParamSelector('not-id')], notId => { + const handlerB = prismy([routeParamSelector('not-id')], (notId) => { return res(notId) }) const routerHandler = router([ - ['/a', handlerA], - ['/b/:id', handlerB] + Route('/a', handlerA), + Route('/b/:id', handlerB), ]) - await testHandler(routerHandler, async url => { + await testHandler(routerHandler, async (url) => { const response = await got(join(url, 'b/test-param'), { - method: 'GET' + method: 'GET', }) expect(response).toMatchObject({ statusCode: 200, - body: '' + body: '', }) }) }) @@ -129,19 +126,19 @@ describe('router', () => { }) const routerHandler = router([ - [['/', 'get'], handlerA], - [['/', 'post'], handlerB] + Route(['/', 'get'], handlerA), + Route(['/', 'post'], handlerB), ]) - await testHandler(routerHandler, async url => { + await testHandler(routerHandler, async (url) => { const response = await got(url, { method: 'PUT', - throwHttpErrors: false + throwHttpErrors: false, }) expect(response).toMatchObject({ statusCode: 404, - body: expect.stringContaining('Error: Not Found') + body: expect.stringContaining('Error: Not Found'), }) }) }) diff --git a/src/router.ts b/src/router.ts index 040b625..8ec5878 100644 --- a/src/router.ts +++ b/src/router.ts @@ -7,6 +7,7 @@ import { createPrismySelector, PrismySelector, } from './selectors/createSelector' +import { PrismyMiddleware } from '.' export type RouteMethod = | 'get' @@ -17,22 +18,28 @@ export type RouteMethod = | 'options' | '*' export type RouteIndicator = [string, RouteMethod] -export type RouteParams = [ - string | RouteIndicator, - PrismyHandler, -] type Route = { indicator: RouteIndicator listener: PrismyHandler } +export class PrismyRoute { + indicator: RouteIndicator + listener: PrismyHandler + + constructor(indicator: RouteIndicator, listener: PrismyHandler) { + this.indicator = indicator + this.listener = listener + } +} + export function router( - routes: RouteParams[], - options: PrismyRouterOptions = {}, + routes: PrismyRoute[], + { prefix, middleware = [] }: PrismyRouterOptions = {}, ) { - const compiledRoutes = routes.map((routeParams) => { - const { indicator, listener } = createRoute(routeParams) + const compiledRoutes = routes.map((route) => { + const { indicator, listener } = route const [targetPath, method] = indicator const compiledTargetPath = removeTralingSlash(targetPath) const match = createMatchFunction(compiledTargetPath, { strict: false }) @@ -43,47 +50,46 @@ export function router( targetPath: compiledTargetPath, } }) - return prismy([methodSelector, urlSelector], (method, url) => { - const prismyContext = getPrismyContext() - /* istanbul ignore next */ - const normalizedMethod = method != null ? method.toLowerCase() : null - /* istanbul ignore next */ - const normalizedPath = removeTralingSlash(url.pathname || '/') - - for (const route of compiledRoutes) { - const { method: targetMethod, match } = route - if (targetMethod !== '*' && targetMethod !== normalizedMethod) { - continue - } - const result = match(normalizedPath) - if (!result) { - continue - } + return prismy( + [methodSelector, urlSelector], + (method, url) => { + const prismyContext = getPrismyContext() + /* istanbul ignore next */ + const normalizedMethod = method != null ? method.toLowerCase() : null + /* istanbul ignore next */ + const normalizedPath = removeTralingSlash(url.pathname || '/') - setRouteParamsToPrismyContext(prismyContext, result.params) + for (const route of compiledRoutes) { + const { method: targetMethod, match } = route + if (targetMethod !== '*' && targetMethod !== normalizedMethod) { + continue + } - return route.listener.contextHandler() - } + const result = match(normalizedPath) + if (!result) { + continue + } - throw createError(404, 'Not Found') - }) + setRouteParamsToPrismyContext(prismyContext, result.params) + + return route.listener.contextHandler() + } + + throw createError(404, 'Not Found') + }, + middleware, + ) } -function createRoute( - routeParams: RouteParams[]>, -): Route[]> { - const [indicator, listener] = routeParams +export function Route( + indicator: RouteIndicator | string, + listener: PrismyHandler, +): PrismyRoute { if (typeof indicator === 'string') { - return { - indicator: [indicator, 'get'], - listener, - } - } - return { - indicator, - listener, + return new PrismyRoute([indicator, 'get'], listener) } + return new PrismyRoute(indicator, listener) } const routeParamsMap = new WeakMap() @@ -106,7 +112,10 @@ export function routeParamSelector( }) } -interface PrismyRouterOptions {} +interface PrismyRouterOptions { + prefix?: string + middleware?: PrismyMiddleware[] +} function removeTralingSlash(value: string) { if (value === '/') { From cb279c1391f232bc5923c097ca580ef4b7be6c5b Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Jan 2024 23:44:02 +0900 Subject: [PATCH 014/109] Introduce PrismyHandler class --- specs/prismy.spec.ts | 4 +- specs/router.spec.ts | 33 +++++------ specs/types/{prismy.ts => handler.ts} | 11 +--- src/handler.ts | 81 +++++++++++++++++++++++++++ src/prismy.ts | 77 +++++++++++-------------- src/router.ts | 28 +++++---- src/types.ts | 18 ------ 7 files changed, 153 insertions(+), 99 deletions(-) rename specs/types/{prismy.ts => handler.ts} (61%) create mode 100644 src/handler.ts diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 7688adc..f6f28af 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -2,6 +2,7 @@ import got from 'got' import { testHandler } from './helpers' import { prismy, res, err, getPrismyContext, middleware } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' +import { Handler } from '../src/handler' describe('prismy', () => { it('returns node.js request handler', async () => { @@ -47,11 +48,12 @@ describe('prismy', () => { }) }) + // TODO: move to handler.spec.ts it('exposes raw prismy handler for unit tests', () => { const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) - const handler = prismy([rawUrlSelector], (url) => res(url)) + const handler = Handler([rawUrlSelector], (url) => res(url)) const result = handler.handler('Hello, World!') diff --git a/specs/router.spec.ts b/specs/router.spec.ts index c27424d..ede2fa1 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -2,20 +2,21 @@ import got from 'got' import { testHandler } from './helpers' import { routeParamSelector, prismy, res, router, Route } from '../src' import { join } from 'path' +import { Handler } from '../src/handler' describe('router', () => { it('routes with pathname', async () => { expect.hasAssertions() - const handlerA = prismy([], () => { + const handlerA = Handler([], () => { return res('a') }) - const handlerB = prismy([], () => { + const handlerB = Handler([], () => { return res('b') }) const routerHandler = router([Route('/a', handlerA), Route('/b', handlerB)]) - await testHandler(routerHandler, async (url) => { + await testHandler(prismy(routerHandler), async (url) => { const response = await got(join(url, 'b'), { method: 'GET', }) @@ -29,10 +30,10 @@ describe('router', () => { it('routes with method', async () => { expect.assertions(2) - const handlerA = prismy([], () => { + const handlerA = Handler([], () => { return res('a') }) - const handlerB = prismy([], () => { + const handlerB = Handler([], () => { return res('b') }) @@ -41,7 +42,7 @@ describe('router', () => { Route(['/', 'post'], handlerB), ]) - await testHandler(routerHandler, async (url) => { + await testHandler(prismy(routerHandler), async (url) => { const response = await got(url, { method: 'GET', }) @@ -52,7 +53,7 @@ describe('router', () => { }) }) - await testHandler(routerHandler, async (url) => { + await testHandler(prismy(routerHandler), async (url) => { const response = await got(url, { method: 'POST', }) @@ -66,10 +67,10 @@ describe('router', () => { it('resolve params', async () => { expect.hasAssertions() - const handlerA = prismy([], () => { + const handlerA = Handler([], () => { return res('a') }) - const handlerB = prismy([routeParamSelector('id')], (id) => { + const handlerB = Handler([routeParamSelector('id')], (id) => { return res(id) }) @@ -78,7 +79,7 @@ describe('router', () => { Route('/b/:id', handlerB), ]) - await testHandler(routerHandler, async (url) => { + await testHandler(prismy(routerHandler), async (url) => { const response = await got(join(url, 'b/test-param'), { method: 'GET', }) @@ -92,10 +93,10 @@ describe('router', () => { it('resolves null if param is missing', async () => { expect.hasAssertions() - const handlerA = prismy([], () => { + const handlerA = Handler([], () => { return res('a') }) - const handlerB = prismy([routeParamSelector('not-id')], (notId) => { + const handlerB = Handler([routeParamSelector('not-id')], (notId) => { return res(notId) }) @@ -104,7 +105,7 @@ describe('router', () => { Route('/b/:id', handlerB), ]) - await testHandler(routerHandler, async (url) => { + await testHandler(prismy(routerHandler), async (url) => { const response = await got(join(url, 'b/test-param'), { method: 'GET', }) @@ -118,10 +119,10 @@ describe('router', () => { it('throws 404 error when no route found', async () => { expect.assertions(1) - const handlerA = prismy([], () => { + const handlerA = Handler([], () => { return res('a') }) - const handlerB = prismy([], () => { + const handlerB = Handler([], () => { return res('b') }) @@ -130,7 +131,7 @@ describe('router', () => { Route(['/', 'post'], handlerB), ]) - await testHandler(routerHandler, async (url) => { + await testHandler(prismy(routerHandler), async (url) => { const response = await got(url, { method: 'PUT', throwHttpErrors: false, diff --git a/specs/types/prismy.ts b/specs/types/handler.ts similarity index 61% rename from specs/types/prismy.ts rename to specs/types/handler.ts index d1197dc..5833601 100644 --- a/specs/types/prismy.ts +++ b/specs/types/handler.ts @@ -1,14 +1,9 @@ -import { - prismy, - urlSelector, - methodSelector, - res, - ResponseObject, -} from '../../src' +import { urlSelector, methodSelector, res, ResponseObject } from '../../src' import { URL } from 'url' import { expectType } from '../helpers' +import { Handler } from '../../src/handler' -const handler1 = prismy([urlSelector, methodSelector], (url, method) => { +const handler1 = Handler([urlSelector, methodSelector], (url, method) => { expectType(url) expectType(method) return res('') diff --git a/src/handler.ts b/src/handler.ts new file mode 100644 index 0000000..8334c66 --- /dev/null +++ b/src/handler.ts @@ -0,0 +1,81 @@ +import { + createErrorResObject, + PrismyMiddleware, + PrismyNextFunction, + Promisable, + ResponseObject, + SelectorReturnTypeTuple, +} from '.' +import { PrismySelector } from './selectors/createSelector' +import { compileHandler } from './utils' + +export class PrismyHandler[]> { + constructor( + public selectors: [...S], + /** + * PrismyHandler exposes `handler` for unit testing the handler. + * @param args selected arguments + */ + public handler: ( + ...args: SelectorReturnTypeTuple + ) => Promisable>, + public middlewareList: PrismyMiddleware[], + ) {} + + async handle(): Promise> { + const next: PrismyNextFunction = compileHandler( + this.selectors, + this.handler, + ) + + const pipe = this.middlewareList.reduce((next, middleware) => { + return middleware(next) + }, next) + + let resObject + try { + resObject = await pipe() + } catch (error) { + /* istanbul ignore next */ + if (process.env.NODE_ENV !== 'test') { + console.error(error) + } + resObject = createErrorResObject(error) + } + + return resObject + } +} + +/** + * Generates a handler to be used by http.Server + * + * @example + * ```ts + * const worldSelector: Selector = () => "world"! + * + * const handler = Handler([ worldSelector ], async world => { + * return res(`Hello ${world}!`) // Hello world! + * }) + * ``` + * + * @remarks + * Selectors must be a tuple (`[PrismySelector, PrismySelector]`) not an + * array (`Selector|Selector[] `). Be careful when declaring the + * array outside of the function call. + * + * @param selectors - Tuple of Selectors to generate arguments for handler + * @param handler - Business logic handling the request + * @param middlewareList - Middleware to pass request and response through + * + * @public * + */ +export function Handler[]>( + selectors: [...S], + handler: ( + ...args: SelectorReturnTypeTuple + ) => Promisable>, + middlewareList: PrismyMiddleware[] = [], +) { + return new PrismyHandler(selectors, handler, middlewareList) +} diff --git a/src/prismy.ts b/src/prismy.ts index cbd185f..93c23ee 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -1,17 +1,15 @@ import { AsyncLocalStorage } from 'async_hooks' -import { IncomingMessage, ServerResponse } from 'http' -import { PrismyMiddleware, PrismyNextFunction } from '.' -import { createErrorResObject } from './error' +import { IncomingMessage, RequestListener, ServerResponse } from 'http' +import { PrismyMiddleware } from '.' +import { Handler, PrismyHandler } from './handler' import { PrismySelector } from './selectors/createSelector' import { send } from './send' import { ResponseObject, Promisable, PrismyContext, - PrismyHandler, SelectorReturnTypeTuple, } from './types' -import { compileHandler } from './utils' export const prismyContextStorage = new AsyncLocalStorage() export function getPrismyContext(): PrismyContext { @@ -27,52 +25,46 @@ export function getPrismyContext(): PrismyContext { * * @example * ```ts - * const worldSelector: Selector = () => "world"! + * const handler = Handler([], () => { ... }) * - * export default prismy([ worldSelector ], async world => { - * return res(`Hello ${world}!`) // Hello world! - * }) + * http.createServer(prismy(handler)) + * // Or directly + * http.createServer([], () => { ... }) * ``` * - * @remarks - * Selectors must be a tuple (`[Selector, Selector]`) not an - * array (`Selector|Selector[] `). Be careful when declaring the - * array outside of the function call. - * - * @param selectors - Tuple of Selectors to generate arguments for handler - * @param handler - Business logic handling the request - * @param middlewareList - Middleware to pass request and response through + * @param prismyHandler + */ +export function prismy[]>( + prismyHandler: PrismyHandler, +): RequestListener + +/** + * Generates a handler to be used by http.Server * - * @public + * prismy(Handler(...)) * + * @param selectors + * @param handler + * @param middlewareList */ export function prismy[]>( selectors: [...S], handler: ( ...args: SelectorReturnTypeTuple ) => Promisable>, - middlewareList: PrismyMiddleware[] = [], -): PrismyHandler> { - const resResolver = async () => { - const next: PrismyNextFunction = compileHandler(selectors, handler) - - const pipe = middlewareList.reduce((next, middleware) => { - return middleware(next) - }, next) - - let resObject - try { - resObject = await pipe() - } catch (error) { - /* istanbul ignore next */ - if (process.env.NODE_ENV !== 'test') { - console.error(error) - } - resObject = createErrorResObject(error) - } - - return resObject - } + middlewareList?: PrismyMiddleware[], +): RequestListener +export function prismy[]>( + selectorsOrPrismyHandler: [...S] | PrismyHandler, + handler?: ( + ...args: SelectorReturnTypeTuple + ) => Promisable>, + middlewareList?: PrismyMiddleware[], +): RequestListener { + const injectedHandler = + selectorsOrPrismyHandler instanceof PrismyHandler + ? selectorsOrPrismyHandler + : Handler(selectorsOrPrismyHandler, handler!, middlewareList) async function requestListener( request: IncomingMessage, @@ -82,14 +74,11 @@ export function prismy[]>( req: request, } prismyContextStorage.run(context, async () => { - const resObject = await resResolver() + const resObject = await injectedHandler.handle() send(request, response, resObject) }) } - requestListener.handler = handler - requestListener.contextHandler = resResolver - return requestListener } diff --git a/src/router.ts b/src/router.ts index 8ec5878..5f74328 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,13 +1,14 @@ -import { PrismyContext, PrismyHandler } from './types' +import { PrismyContext } from './types' import { methodSelector, urlSelector } from './selectors' import { match as createMatchFunction } from 'path-to-regexp' -import { getPrismyContext, prismy } from './prismy' +import { getPrismyContext } from './prismy' import { createError } from './error' import { createPrismySelector, PrismySelector, } from './selectors/createSelector' import { PrismyMiddleware } from '.' +import { Handler, PrismyHandler } from './handler' export type RouteMethod = | 'get' @@ -19,23 +20,26 @@ export type RouteMethod = | '*' export type RouteIndicator = [string, RouteMethod] -type Route = { +type Route = { indicator: RouteIndicator - listener: PrismyHandler + listener: PrismyHandler[]> } -export class PrismyRoute { +export class PrismyRoute { indicator: RouteIndicator - listener: PrismyHandler + listener: PrismyHandler[]> - constructor(indicator: RouteIndicator, listener: PrismyHandler) { + constructor( + indicator: RouteIndicator, + listener: PrismyHandler[]>, + ) { this.indicator = indicator this.listener = listener } } export function router( - routes: PrismyRoute[], + routes: PrismyRoute[], { prefix, middleware = [] }: PrismyRouterOptions = {}, ) { const compiledRoutes = routes.map((route) => { @@ -51,7 +55,7 @@ export function router( } }) - return prismy( + return Handler( [methodSelector, urlSelector], (method, url) => { const prismyContext = getPrismyContext() @@ -73,7 +77,7 @@ export function router( setRouteParamsToPrismyContext(prismyContext, result.params) - return route.listener.contextHandler() + return route.listener.handle() } throw createError(404, 'Not Found') @@ -82,9 +86,9 @@ export function router( ) } -export function Route( +export function Route( indicator: RouteIndicator | string, - listener: PrismyHandler, + listener: PrismyHandler[]>, ): PrismyRoute { if (typeof indicator === 'string') { return new PrismyRoute([indicator, 'get'], listener) diff --git a/src/types.ts b/src/types.ts index c5fa5ad..77f8c04 100644 --- a/src/types.ts +++ b/src/types.ts @@ -83,24 +83,6 @@ export interface PrismyMiddleware { ) => (...args: A) => Promise> } -/** - * @public - */ -export interface PrismyHandler { - (req: IncomingMessage, res: ServerResponse): void - /** - * PrismyHandler exposes `handler` for unit testing the handler. - * @param args selected arguments - */ - handler(...args: A): Promisable> - /** - * PrismyHandler exposes compiled funciton which must be ran in a prismy context. - * This is useful when using a prismy handler in other prismy handler or a middleware like Routing. - * `router()` is also using this prop to run a handler after matching URL. - */ - contextHandler: () => Promise> -} - /** * @public */ From 2281fd790700f23129b6bb7245cbec828a66fdfa Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 08:50:22 +0900 Subject: [PATCH 015/109] Rename Promisable to MaybePromise --- src/handler.ts | 6 +++--- src/prismy.ts | 6 +++--- src/types.ts | 4 ++-- src/utils.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/handler.ts b/src/handler.ts index 8334c66..128a83c 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -2,7 +2,7 @@ import { createErrorResObject, PrismyMiddleware, PrismyNextFunction, - Promisable, + MaybePromise, ResponseObject, SelectorReturnTypeTuple, } from '.' @@ -18,7 +18,7 @@ export class PrismyHandler[]> { */ public handler: ( ...args: SelectorReturnTypeTuple - ) => Promisable>, + ) => MaybePromise>, public middlewareList: PrismyMiddleware[], ) {} @@ -74,7 +74,7 @@ export function Handler[]>( selectors: [...S], handler: ( ...args: SelectorReturnTypeTuple - ) => Promisable>, + ) => MaybePromise>, middlewareList: PrismyMiddleware[] = [], ) { return new PrismyHandler(selectors, handler, middlewareList) diff --git a/src/prismy.ts b/src/prismy.ts index 93c23ee..8c927b1 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -6,7 +6,7 @@ import { PrismySelector } from './selectors/createSelector' import { send } from './send' import { ResponseObject, - Promisable, + MaybePromise, PrismyContext, SelectorReturnTypeTuple, } from './types' @@ -51,14 +51,14 @@ export function prismy[]>( selectors: [...S], handler: ( ...args: SelectorReturnTypeTuple - ) => Promisable>, + ) => MaybePromise>, middlewareList?: PrismyMiddleware[], ): RequestListener export function prismy[]>( selectorsOrPrismyHandler: [...S] | PrismyHandler, handler?: ( ...args: SelectorReturnTypeTuple - ) => Promisable>, + ) => MaybePromise>, middlewareList?: PrismyMiddleware[], ): RequestListener { const injectedHandler = diff --git a/src/types.ts b/src/types.ts index 77f8c04..f36549a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from 'http' +import { IncomingMessage, OutgoingHttpHeaders } from 'http' import { PrismySelector } from './selectors/createSelector' /** @@ -49,7 +49,7 @@ export type PromiseResolve = T extends Promise ? U : T /** * @public */ -export type Promisable = T | Promise +export type MaybePromise = T | Promise /** * prismy's representation of a response diff --git a/src/utils.ts b/src/utils.ts index 2630077..4152ff2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Promisable } from '.' +import { MaybePromise } from '.' import { PrismySelector } from './selectors/createSelector' import { SelectorReturnTypeTuple } from './types' @@ -14,7 +14,7 @@ import { SelectorReturnTypeTuple } from './types' */ export function compileHandler[], R>( selectors: [...S], - handler: (...args: SelectorReturnTypeTuple) => Promisable, + handler: (...args: SelectorReturnTypeTuple) => MaybePromise, ): () => Promise { return async () => { return handler(...(await resolveSelectors(selectors))) From 612a83d2eef260d23d9c0fe94d9ebd8239854fc4 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 09:10:09 +0900 Subject: [PATCH 016/109] Implement prefix and notFoundHandler --- specs/router.spec.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++ src/handler.ts | 4 +- src/router.ts | 11 +++++- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index ede2fa1..9cc400b 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -143,4 +143,94 @@ describe('router', () => { }) }) }) + + it('uses custom not found handler if set', async () => { + expect.assertions(1) + const handlerA = Handler([], () => { + return res('a') + }) + const handlerB = Handler([], () => { + return res('b') + }) + const customNotFoundHandler = Handler([], () => { + return res('Error: Customized Not Found Response', 404) + }) + + const routerHandler = router( + [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], + { + notFoundHandler: customNotFoundHandler, + }, + ) + + await testHandler(prismy(routerHandler), async (url) => { + const response = await got(url, { + method: 'PUT', + throwHttpErrors: false, + }) + + expect(response).toMatchObject({ + statusCode: 404, + body: expect.stringContaining('Error: Customized Not Found Response'), + }) + }) + }) + + it('prepends prefix to route path', async () => { + expect.assertions(1) + const handlerA = Handler([], () => { + return res('a') + }) + const handlerB = Handler([], () => { + return res('b') + }) + + const routerHandler = router( + [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], + { + prefix: '/admin', + }, + ) + + await testHandler(prismy(routerHandler), async (url) => { + const response = await got(join(url, 'admin'), { + method: 'GET', + throwHttpErrors: false, + }) + + expect(response).toMatchObject({ + statusCode: 200, + body: expect.stringContaining('a'), + }) + }) + }) + + it('prepends prefix to route path (without root `/`)', async () => { + expect.assertions(1) + const handlerA = Handler([], () => { + return res('a') + }) + const handlerB = Handler([], () => { + return res('b') + }) + + const routerHandler = router( + [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], + { + prefix: '/admin', + }, + ) + + await testHandler(prismy(routerHandler), async (url) => { + const response = await got(join(url, 'admin'), { + method: 'GET', + throwHttpErrors: false, + }) + + expect(response).toMatchObject({ + statusCode: 200, + body: expect.stringContaining('a'), + }) + }) + }) }) diff --git a/src/handler.ts b/src/handler.ts index 128a83c..736476a 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -9,7 +9,9 @@ import { import { PrismySelector } from './selectors/createSelector' import { compileHandler } from './utils' -export class PrismyHandler[]> { +export class PrismyHandler< + S extends PrismySelector[] = PrismySelector[], +> { constructor( public selectors: [...S], /** diff --git a/src/router.ts b/src/router.ts index 5f74328..7a72da8 100644 --- a/src/router.ts +++ b/src/router.ts @@ -9,6 +9,7 @@ import { } from './selectors/createSelector' import { PrismyMiddleware } from '.' import { Handler, PrismyHandler } from './handler' +import { join as joinPath } from 'path' export type RouteMethod = | 'get' @@ -40,12 +41,14 @@ export class PrismyRoute { export function router( routes: PrismyRoute[], - { prefix, middleware = [] }: PrismyRouterOptions = {}, + { prefix = '/', middleware = [], notFoundHandler }: PrismyRouterOptions = {}, ) { const compiledRoutes = routes.map((route) => { const { indicator, listener } = route const [targetPath, method] = indicator - const compiledTargetPath = removeTralingSlash(targetPath) + const compiledTargetPath = removeTralingSlash( + joinPath('/', prefix, targetPath), + ) const match = createMatchFunction(compiledTargetPath, { strict: false }) return { method, @@ -80,6 +83,9 @@ export function router( return route.listener.handle() } + if (notFoundHandler != null) { + return notFoundHandler.handle() + } throw createError(404, 'Not Found') }, middleware, @@ -119,6 +125,7 @@ export function routeParamSelector( interface PrismyRouterOptions { prefix?: string middleware?: PrismyMiddleware[] + notFoundHandler?: PrismyHandler } function removeTralingSlash(value: string) { From 44bfa1d2ce2f65cec138223e9f53f7bc4a16ed05 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 15:34:13 +0900 Subject: [PATCH 017/109] Use PascalCase for factory method --- specs/types/middleware.ts | 4 ++-- src/middleware.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/types/middleware.ts b/specs/types/middleware.ts index a91e2c6..5646d7e 100644 --- a/specs/types/middleware.ts +++ b/specs/types/middleware.ts @@ -1,5 +1,5 @@ import { - middleware, + Middleware, urlSelector, methodSelector, ResponseObject, @@ -9,7 +9,7 @@ import { import { URL } from 'url' import { expectType } from '../helpers' -const middleware1 = middleware( +const middleware1 = Middleware( [urlSelector, methodSelector], (next) => async (url, method) => { expectType(url) diff --git a/src/middleware.ts b/src/middleware.ts index 20ced9d..4178d8c 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -42,7 +42,7 @@ import { compileHandler } from './utils' * * @public */ -export function middleware[]>( +export function Middleware[]>( selectors: [...SS], mhandler: ( next: PrismyNextFunction, From 6a00bb880e84508c00772b26d0508f120c144392 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 15:34:50 +0900 Subject: [PATCH 018/109] Use node-fetch instead of got --- package.json | 5 ++- specs/middleware.spec.ts | 28 +++++--------- specs/prismy.spec.ts | 80 ++++++++++++++++------------------------ 3 files changed, 46 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index f182164..3ada239 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,14 @@ "devDependencies": { "@types/content-type": "^1.1.3", "@types/jest": "^24.0.13", - "@types/node": "^12.19.1", + "@types/node": "^20.10.6", + "@types/node-fetch": "^2.6.10", "@types/test-listen": "^1.1.0", + "async-listen": "^3.0.1", "codecov": "^3.8.0", "got": "^11.8.0", "jest": "^26.6.1", + "node-fetch": "^2.7.0", "prettier": "^3.1.1", "rimraf": "^3.0.0", "test-listen": "^1.1.0", diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 0893e97..0d5d285 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,6 +1,6 @@ -import got from 'got' +import fetch from 'node-fetch' import { testHandler } from './helpers' -import { prismy, res, middleware, getPrismyContext } from '../src' +import { prismy, res, Middleware, getPrismyContext } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' describe('middleware', () => { @@ -8,7 +8,7 @@ describe('middleware', () => { const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) - const errorMiddleware = middleware( + const errorMiddleware = Middleware( [rawUrlSelector], (next) => async (url) => { try { @@ -27,13 +27,9 @@ describe('middleware', () => { ) await testHandler(handler, async (url) => { - const response = await got(url, { - throwHttpErrors: false, - }) - expect(response).toMatchObject({ - statusCode: 500, - body: '/ : Hey!', - }) + const response = await fetch(url) + expect(response.status).toBe(500) + expect(await response.text()).toBe('/ : Hey!') }) }) @@ -41,7 +37,7 @@ describe('middleware', () => { const asyncRawUrlSelector = createPrismySelector( async () => getPrismyContext().req.url!, ) - const errorMiddleware = middleware( + const errorMiddleware = Middleware( [asyncRawUrlSelector], (next) => async (url) => { try { @@ -60,13 +56,9 @@ describe('middleware', () => { ) await testHandler(handler, async (url) => { - const response = await got(url, { - throwHttpErrors: false, - }) - expect(response).toMatchObject({ - statusCode: 500, - body: '/ : Hey!', - }) + const response = await fetch(url) + expect(response.status).toBe(500) + expect(await response.text()).toBe('/ : Hey!') }) }) }) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index f6f28af..bf4b13b 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -1,6 +1,6 @@ -import got from 'got' +import fetch from 'node-fetch' import { testHandler } from './helpers' -import { prismy, res, err, getPrismyContext, middleware } from '../src' +import { prismy, res, err, getPrismyContext, Middleware } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' import { Handler } from '../src/handler' @@ -9,11 +9,9 @@ describe('prismy', () => { const handler = prismy([], () => res('Hello, World!')) await testHandler(handler, async (url) => { - const response = await got(url) - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) + const response = await fetch(url) + expect(response.status).toBe(200) + expect(await response.text()).toBe('Hello, World!') }) }) @@ -25,11 +23,9 @@ describe('prismy', () => { const handler = prismy([rawUrlSelector], (url) => res(url)) await testHandler(handler, async (url) => { - const response = await got(url) - expect(response).toMatchObject({ - statusCode: 200, - body: '/', - }) + const response = await fetch(url) + expect(response.status).toBe(200) + expect(await response.text()).toBe('/') }) }) @@ -40,11 +36,9 @@ describe('prismy', () => { const handler = prismy([asyncRawUrlSelector], (url) => res(url)) await testHandler(handler, async (url) => { - const response = await got(url) - expect(response).toMatchObject({ - statusCode: 200, - body: '/', - }) + const response = await fetch(url) + expect(response.status).toBe(200) + expect(await response.text()).toBe('/') }) }) @@ -65,7 +59,7 @@ describe('prismy', () => { }) it('applies middleware', async () => { - const errorMiddleware = middleware([], (next) => { + const errorMiddleware = Middleware([], (next) => { return async () => { try { return await next() @@ -86,21 +80,17 @@ describe('prismy', () => { ) await testHandler(handler, async (url) => { - const response = await got(url, { - throwHttpErrors: false, - }) - expect(response).toMatchObject({ - statusCode: 500, - body: 'Hey!', - }) + const response = await fetch(url) + expect(response.status).toBe(500) + expect(await response.text()).toBe('Hey!') }) }) it('applies middleware orderly', async () => { - const problematicMiddleware = middleware([], (next) => () => { + const problematicMiddleware = Middleware([], (next) => () => { throw new Error('Hey!') }) - const errorMiddleware = middleware([], (next) => async () => { + const errorMiddleware = Middleware([], (next) => async () => { try { return await next() } catch (error) { @@ -119,13 +109,9 @@ describe('prismy', () => { ) await testHandler(handler, async (url) => { - const response = await got(url, { - throwHttpErrors: false, - }) - expect(response).toMatchObject({ - statusCode: 500, - body: 'Hey!', - }) + const response = await fetch(url, {}) + expect(response.status).toBe(500) + expect(await response.text()).toBe('Hey!') }) }) @@ -138,13 +124,12 @@ describe('prismy', () => { [], ) await testHandler(handler, async (url) => { - const response = await got(url, { - throwHttpErrors: false, - }) - expect(response).toMatchObject({ - statusCode: 500, - body: expect.stringContaining('Error: Hey!'), - }) + const response = await fetch(url, {}) + + expect(response.status).toBe(500) + expect(await response.text()).toEqual( + expect.stringContaining('Error: Hey!'), + ) }) }) @@ -160,13 +145,12 @@ describe('prismy', () => { [], ) await testHandler(handler, async (url) => { - const response = await got(url, { - throwHttpErrors: false, - }) - expect(response).toMatchObject({ - statusCode: 500, - body: expect.stringContaining('Error: Hey!'), - }) + const response = await fetch(url, {}) + + expect(response.status).toBe(500) + expect(await response.text()).toEqual( + expect.stringContaining('Error: Hey!'), + ) }) }) }) From d09a259ebc3c949aaf7310250fa01f339e74d669 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 16:48:05 +0900 Subject: [PATCH 019/109] Add testFetch heleper --- specs/bodyReaders.spec.ts | 4 +- specs/helpers.ts | 22 +++++++-- specs/middleware.spec.ts | 15 +++---- specs/prismy.spec.ts | 67 ++++++++++++++++------------ specs/router.spec.ts | 93 +++++++++++++++++++++++++++------------ src/handler.ts | 1 - 6 files changed, 133 insertions(+), 69 deletions(-) diff --git a/specs/bodyReaders.spec.ts b/specs/bodyReaders.spec.ts index f33311c..db014e3 100644 --- a/specs/bodyReaders.spec.ts +++ b/specs/bodyReaders.spec.ts @@ -1,6 +1,6 @@ import got from 'got' import getRawBody from 'raw-body' -import { middleware, prismy, res, getPrismyContext } from '../src' +import { Middleware, prismy, res, getPrismyContext } from '../src' import { readBufferBody, readJsonBody, readTextBody } from '../src/bodyReaders' import { createPrismySelector } from '../src/selectors/createSelector' import { testHandler } from './helpers' @@ -52,7 +52,7 @@ describe('readBufferBody', () => { return res(body) }, [ - middleware([], (next) => async () => { + Middleware([], (next) => async () => { try { return await next() } catch (error) { diff --git a/specs/helpers.ts b/specs/helpers.ts index 60263c0..5877dba 100644 --- a/specs/helpers.ts +++ b/specs/helpers.ts @@ -1,6 +1,8 @@ import http from 'http' -import listen from 'test-listen' +import listen from 'async-listen' import { RequestListener } from 'http' +import { URL } from 'url' +import fetch, { RequestInit } from 'node-fetch' export type TestCallback = (url: string) => Promise | void @@ -11,9 +13,9 @@ export async function testHandler( ): Promise { const server = new http.Server(handler) - const url = await listen(server) + const url: URL = await listen(server) try { - await testCallback(url) + await testCallback(url.origin) } catch (error) { throw error } finally { @@ -23,3 +25,17 @@ export async function testHandler( /* istanbul ignore next */ export function expectType(value: T): void {} + +export interface TestFetchOptions { + method: string +} + +export async function testFetch(url: string, options?: RequestInit) { + const response = await fetch(url, options) + const testResult = await response.text() + + return { + statusCode: response.status, + body: testResult, + } +} diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 0d5d285..269ebb6 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,5 +1,4 @@ -import fetch from 'node-fetch' -import { testHandler } from './helpers' +import { testFetch, testHandler } from './helpers' import { prismy, res, Middleware, getPrismyContext } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' @@ -27,9 +26,9 @@ describe('middleware', () => { ) await testHandler(handler, async (url) => { - const response = await fetch(url) - expect(response.status).toBe(500) - expect(await response.text()).toBe('/ : Hey!') + const response = await testFetch(url) + + expect(response).toMatchObject({ statusCode: 500, body: '/ : Hey!' }) }) }) @@ -56,9 +55,9 @@ describe('middleware', () => { ) await testHandler(handler, async (url) => { - const response = await fetch(url) - expect(response.status).toBe(500) - expect(await response.text()).toBe('/ : Hey!') + const response = await testFetch(url) + + expect(response).toMatchObject({ statusCode: 500, body: '/ : Hey!' }) }) }) }) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index bf4b13b..afabfaf 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -1,5 +1,4 @@ -import fetch from 'node-fetch' -import { testHandler } from './helpers' +import { testFetch, testHandler } from './helpers' import { prismy, res, err, getPrismyContext, Middleware } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' import { Handler } from '../src/handler' @@ -9,9 +8,12 @@ describe('prismy', () => { const handler = prismy([], () => res('Hello, World!')) await testHandler(handler, async (url) => { - const response = await fetch(url) - expect(response.status).toBe(200) - expect(await response.text()).toBe('Hello, World!') + const response = await testFetch(url) + + expect(response).toMatchObject({ + statusCode: 200, + body: 'Hello, World!', + }) }) }) @@ -23,9 +25,12 @@ describe('prismy', () => { const handler = prismy([rawUrlSelector], (url) => res(url)) await testHandler(handler, async (url) => { - const response = await fetch(url) - expect(response.status).toBe(200) - expect(await response.text()).toBe('/') + const response = await testFetch(url) + + expect(response).toMatchObject({ + statusCode: 200, + body: '/', + }) }) }) @@ -36,9 +41,12 @@ describe('prismy', () => { const handler = prismy([asyncRawUrlSelector], (url) => res(url)) await testHandler(handler, async (url) => { - const response = await fetch(url) - expect(response.status).toBe(200) - expect(await response.text()).toBe('/') + const response = await testFetch(url) + + expect(response).toMatchObject({ + statusCode: 200, + body: '/', + }) }) }) @@ -80,9 +88,11 @@ describe('prismy', () => { ) await testHandler(handler, async (url) => { - const response = await fetch(url) - expect(response.status).toBe(500) - expect(await response.text()).toBe('Hey!') + const response = await testFetch(url) + expect(response).toMatchObject({ + statusCode: 500, + body: 'Hey!', + }) }) }) @@ -109,9 +119,12 @@ describe('prismy', () => { ) await testHandler(handler, async (url) => { - const response = await fetch(url, {}) - expect(response.status).toBe(500) - expect(await response.text()).toBe('Hey!') + const response = await testFetch(url, {}) + + expect(response).toMatchObject({ + statusCode: 500, + body: 'Hey!', + }) }) }) @@ -124,12 +137,12 @@ describe('prismy', () => { [], ) await testHandler(handler, async (url) => { - const response = await fetch(url, {}) + const response = await testFetch(url, {}) - expect(response.status).toBe(500) - expect(await response.text()).toEqual( - expect.stringContaining('Error: Hey!'), - ) + expect(response).toMatchObject({ + statusCode: 500, + body: expect.stringContaining('Error: Hey!'), + }) }) }) @@ -145,12 +158,12 @@ describe('prismy', () => { [], ) await testHandler(handler, async (url) => { - const response = await fetch(url, {}) + const response = await testFetch(url, {}) - expect(response.status).toBe(500) - expect(await response.text()).toEqual( - expect.stringContaining('Error: Hey!'), - ) + expect(response).toMatchObject({ + statusCode: 500, + body: expect.stringContaining('Error: Hey!'), + }) }) }) }) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 9cc400b..4b0f8ec 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -1,8 +1,16 @@ -import got from 'got' -import { testHandler } from './helpers' -import { routeParamSelector, prismy, res, router, Route } from '../src' +import { testFetch, testHandler } from './helpers' +import { + routeParamSelector, + prismy, + res, + router, + Route, + Middleware, + getPrismyContext, +} from '../src' import { join } from 'path' import { Handler } from '../src/handler' +import fetch from 'node-fetch' describe('router', () => { it('routes with pathname', async () => { @@ -17,9 +25,7 @@ describe('router', () => { const routerHandler = router([Route('/a', handlerA), Route('/b', handlerB)]) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(join(url, 'b'), { - method: 'GET', - }) + const response = await testFetch(join(url, 'b')) expect(response).toMatchObject({ statusCode: 200, @@ -29,7 +35,8 @@ describe('router', () => { }) it('routes with method', async () => { - expect.assertions(2) + expect.hasAssertions() + const handlerA = Handler([], () => { return res('a') }) @@ -43,9 +50,7 @@ describe('router', () => { ]) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(url, { - method: 'GET', - }) + const response = await testFetch(url) expect(response).toMatchObject({ statusCode: 200, @@ -54,8 +59,8 @@ describe('router', () => { }) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(url, { - method: 'POST', + const response = await testFetch(url, { + method: 'post', }) expect(response).toMatchObject({ @@ -80,9 +85,7 @@ describe('router', () => { ]) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(join(url, 'b/test-param'), { - method: 'GET', - }) + const response = await testFetch(join(url, 'b/test-param')) expect(response).toMatchObject({ statusCode: 200, @@ -106,7 +109,7 @@ describe('router', () => { ]) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(join(url, 'b/test-param'), { + const response = await testFetch(join(url, 'b/test-param'), { method: 'GET', }) @@ -118,7 +121,7 @@ describe('router', () => { }) it('throws 404 error when no route found', async () => { - expect.assertions(1) + expect.hasAssertions() const handlerA = Handler([], () => { return res('a') }) @@ -132,9 +135,8 @@ describe('router', () => { ]) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(url, { + const response = await testFetch(url, { method: 'PUT', - throwHttpErrors: false, }) expect(response).toMatchObject({ @@ -145,7 +147,7 @@ describe('router', () => { }) it('uses custom not found handler if set', async () => { - expect.assertions(1) + expect.hasAssertions() const handlerA = Handler([], () => { return res('a') }) @@ -164,9 +166,8 @@ describe('router', () => { ) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(url, { + const response = await testFetch(url, { method: 'PUT', - throwHttpErrors: false, }) expect(response).toMatchObject({ @@ -177,7 +178,7 @@ describe('router', () => { }) it('prepends prefix to route path', async () => { - expect.assertions(1) + expect.hasAssertions() const handlerA = Handler([], () => { return res('a') }) @@ -193,9 +194,8 @@ describe('router', () => { ) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(join(url, 'admin'), { + const response = await testFetch(join(url, 'admin'), { method: 'GET', - throwHttpErrors: false, }) expect(response).toMatchObject({ @@ -206,7 +206,7 @@ describe('router', () => { }) it('prepends prefix to route path (without root `/`)', async () => { - expect.assertions(1) + expect.hasAssertions() const handlerA = Handler([], () => { return res('a') }) @@ -222,9 +222,8 @@ describe('router', () => { ) await testHandler(prismy(routerHandler), async (url) => { - const response = await got(join(url, 'admin'), { + const response = await testFetch(join(url, 'admin'), { method: 'GET', - throwHttpErrors: false, }) expect(response).toMatchObject({ @@ -233,4 +232,42 @@ describe('router', () => { }) }) }) + + it('applies middleware', async () => { + expect.hasAssertions() + + const weakMap = new WeakMap() + const handlerA = Handler([], () => { + const context = getPrismyContext() + + return res(weakMap.get(context)) + }) + const handlerB = Handler([], () => { + return res('b') + }) + + const routerHandler = router( + [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], + { + middleware: [ + Middleware([], (next) => () => { + const context = getPrismyContext() + weakMap.set(context, (weakMap.get(context) || '') + 'a') + return next() + }), + Middleware([], (next) => () => { + const context = getPrismyContext() + weakMap.set(context, (weakMap.get(context) || '') + 'b') + return next() + }), + ], + }, + ) + + await testHandler(prismy(routerHandler), async (url) => { + const response = await fetch(url) + expect(response.status).toBe(200) + expect(await response.text()).toBe('ba') + }) + }) }) diff --git a/src/handler.ts b/src/handler.ts index 736476a..945b982 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -33,7 +33,6 @@ export class PrismyHandler< const pipe = this.middlewareList.reduce((next, middleware) => { return middleware(next) }, next) - let resObject try { resObject = await pipe() From c526829e3864b0326dce212cb1d9c976aa072c1a Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 16:48:13 +0900 Subject: [PATCH 020/109] Remove test-listen --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 3ada239..aec5abb 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "node-fetch": "^2.7.0", "prettier": "^3.1.1", "rimraf": "^3.0.0", - "test-listen": "^1.1.0", "ts-jest": "^26.4.2", "typedoc": "^0.15.0", "typescript": "^4.0.3" From 4f1904d9a4df2420471aa8b94be495dfd72f87e4 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 16:52:01 +0900 Subject: [PATCH 021/109] Fix type error in bodyReader --- src/bodyReaders.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bodyReaders.ts b/src/bodyReaders.ts index 4105836..9649e87 100644 --- a/src/bodyReaders.ts +++ b/src/bodyReaders.ts @@ -20,7 +20,7 @@ const rawBodyMap = new WeakMap() */ export const readBufferBody = async ( req: IncomingMessage, - options?: BufferBodyOptions + options?: BufferBodyOptions, ): Promise => { const { limit, encoding } = resolveBufferBodyOptions(req, options) const length = req.headers['content-length'] @@ -57,11 +57,11 @@ export const readBufferBody = async ( */ export const readTextBody = async ( req: IncomingMessage, - options?: BufferBodyOptions + options?: BufferBodyOptions, ): Promise => { const { encoding } = resolveBufferBodyOptions(req, options) const body = await readBufferBody(req, options) - return body.toString(encoding) + return body.toString(encoding as any) } /** @@ -75,7 +75,7 @@ export const readTextBody = async ( */ export const readJsonBody = async ( req: IncomingMessage, - options?: BufferBodyOptions + options?: BufferBodyOptions, ): Promise => { const body = await readTextBody(req, options) try { @@ -87,7 +87,7 @@ export const readJsonBody = async ( function resolveBufferBodyOptions( req: IncomingMessage, - options?: BufferBodyOptions + options?: BufferBodyOptions, ): BufferBodyOptions { const type = req.headers['content-type'] || 'text/plain' let { limit = '1mb', encoding } = options || {} From eae88b2b5c499e3360e6f8f7256473f8f4814c27 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 17:14:59 +0900 Subject: [PATCH 022/109] Upgrade ts --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aec5abb..827ff8c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "rimraf": "^3.0.0", "ts-jest": "^26.4.2", "typedoc": "^0.15.0", - "typescript": "^4.0.3" + "typescript": "^4.9.5" }, "jest": { "preset": "ts-jest", From 45c764bb097de97990b07077c3523e370c5cbfcb Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 17:19:40 +0900 Subject: [PATCH 023/109] Update tsconfig for node 18 --- tsconfig.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index e2aafd3..23f512d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "include": ["src/**/*.ts", "specs/**/*.ts"], "compilerOptions": { - "target": "es2018", - "lib": ["es2018"], - "module": "commonjs", + "lib": ["ES2022"], + "module": "node16", + "target": "ES2022", "moduleResolution": "node", "importHelpers": true, "esModuleInterop": true, @@ -27,6 +27,6 @@ }, "typedocOptions": { "out": "out", - "mode": "file", + "mode": "file" } } From a2b7a5980d1c5a9d605c4a684f3cdd8589e9e3a1 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 17:20:36 +0900 Subject: [PATCH 024/109] Convert middleware into class PrismyMiddleware --- specs/types/middleware.ts | 6 ++---- src/handler.ts | 2 +- src/middleware.ts | 44 ++++++++++++++++++++++++--------------- src/prismy.ts | 8 +++---- src/types.ts | 12 ----------- 5 files changed, 34 insertions(+), 38 deletions(-) diff --git a/specs/types/middleware.ts b/specs/types/middleware.ts index 5646d7e..5c36a91 100644 --- a/specs/types/middleware.ts +++ b/specs/types/middleware.ts @@ -24,9 +24,8 @@ expectType< ) => ( url: URL, method: string | undefined, - url2: URL, ) => ResponseObject | Promise> ->(middleware1.mhandler) +>(middleware1.handler) expectType< ( @@ -34,8 +33,7 @@ expectType< ) => ( url: URL, method: string | undefined, - url2: URL, ) => ResponseObject | Promise> ->(middleware1.mhandler) +>(middleware1.handler) prismy([], () => res(''), [middleware1]) diff --git a/src/handler.ts b/src/handler.ts index 945b982..58636ca 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -31,7 +31,7 @@ export class PrismyHandler< ) const pipe = this.middlewareList.reduce((next, middleware) => { - return middleware(next) + return middleware.pipe(next) }, next) let resObject try { diff --git a/src/middleware.ts b/src/middleware.ts index 4178d8c..df43b12 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,14 +1,29 @@ import { PrismyNextFunction } from '.' import { PrismySelector } from './selectors/createSelector' -import { - ResponseObject, - SelectorReturnTypeTuple, - PrismyMiddleware, -} from './types' +import { ResponseObject, SelectorReturnTypeTuple } from './types' import { compileHandler } from './utils' +export class PrismyMiddleware< + S extends PrismySelector[] = PrismySelector[], +> { + constructor( + public selectors: [...S], + /** + * PrismyHandler exposes `handler` for unit testing the handler. + * @param args selected arguments + */ + public handler: ( + next: PrismyNextFunction, + ) => (...args: SelectorReturnTypeTuple) => Promise>, + ) {} + + pipe(next: PrismyNextFunction) { + return compileHandler(this.selectors, this.handler(next)) + } +} + /** - * Factory function to create a prismy compatible middleware. Accepts selectors to help with + * Factory function to create a prismy middleware. Accepts selectors to help with * testing, DI etc. * * @example @@ -30,28 +45,23 @@ import { compileHandler } from './utils' * array (`Selector|Selector[] `). Be careful when declaring the * array outside of the function call. * - * Be carefuly to remember the mhandler is a function which returns an _async_ function. + * Be carefuly to remember the handler is a function which returns an _async_ function. * Not returning an async function can lead to strange type error messages. * * Another reason for long type error messages is not having `{"strict": true}` setting in * tsconfig.json or not compiling with --strict. * * @param selectors - Tuple of selectors - * @param mhandler - Middleware handler + * @param handler - Middleware handler * @returns A prismy compatible middleware * * @public */ -export function Middleware[]>( +export function Middleware[]>( selectors: [...SS], - mhandler: ( + handler: ( next: PrismyNextFunction, ) => (...args: SelectorReturnTypeTuple) => Promise>, -): PrismyMiddleware> { - const middleware = (next: PrismyNextFunction) => { - return compileHandler(selectors, mhandler(next)) - } - middleware.mhandler = mhandler - - return middleware +): PrismyMiddleware { + return new PrismyMiddleware(selectors, handler) } diff --git a/src/prismy.ts b/src/prismy.ts index 8c927b1..21e2919 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -1,6 +1,6 @@ import { AsyncLocalStorage } from 'async_hooks' import { IncomingMessage, RequestListener, ServerResponse } from 'http' -import { PrismyMiddleware } from '.' +import { PrismyMiddleware } from './middleware' import { Handler, PrismyHandler } from './handler' import { PrismySelector } from './selectors/createSelector' import { send } from './send' @@ -47,19 +47,19 @@ export function prismy[]>( * @param handler * @param middlewareList */ -export function prismy[]>( +export function prismy[]>( selectors: [...S], handler: ( ...args: SelectorReturnTypeTuple ) => MaybePromise>, - middlewareList?: PrismyMiddleware[], + middlewareList?: PrismyMiddleware[]>[], ): RequestListener export function prismy[]>( selectorsOrPrismyHandler: [...S] | PrismyHandler, handler?: ( ...args: SelectorReturnTypeTuple ) => MaybePromise>, - middlewareList?: PrismyMiddleware[], + middlewareList?: PrismyMiddleware[]>[], ): RequestListener { const injectedHandler = selectorsOrPrismyHandler instanceof PrismyHandler diff --git a/src/types.ts b/src/types.ts index f36549a..3ad29f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,18 +71,6 @@ export type Res = ResponseObject export type PrismyNextFunction = () => Promise> -/** - * prismy compatible middleware - * - * @public - */ -export interface PrismyMiddleware { - (next: PrismyNextFunction): PrismyNextFunction - mhandler: ( - next: PrismyNextFunction, - ) => (...args: A) => Promise> -} - /** * @public */ From 6566523b661a8ca6bb5afd5bfa030c5417a44a4f Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Jan 2024 18:08:50 +0900 Subject: [PATCH 025/109] Update v4 todo --- v4-todo.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/v4-todo.md b/v4-todo.md index d4164ed..6a82076 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -1,8 +1,30 @@ # V4 changes(TEMP) +- Legacy support: Provide snippets which can provide compatibility +- [x] Improve server test(Replace legacy got with node-fetch@2) + - server listner replacer? +- Introduce tsd to test types properly +- [x] Seperate Handler and Prismy + ```ts + import http from 'http' + + // Take handler + const requestListener: http.RequestListener = prismy(Handler([...], () => {})) + // Can omit Handler + // const serverHandler = Prismy([...], () => {}) -- redesigned router interface - - introduced route method - - Removed notFoundHandler option + http.createServer(requestListener).listen() + + ``` +- [x] redesigned router interface + - [x] introduced route method + - [ ] Add combine router to put routers together + - [ ] NotFoundHandler must be ignored when routers are combined. + - Should roll back NotFoundHandler + - combineRouters should take NotFoundHandler + - [ ] Add tests + - [ ] Wildcard parm handling + - [ ] Router middleware test +- [ ] Replace res with `PrismyResult` and `Result()` - [x] Redesigned selector interface - [x] Renamed factory method (ex: createBodySelector(Deprecated) => BodySelector) ```ts @@ -50,6 +72,7 @@ - [x] Adopted async local storage to communicate between selectors, middleware and handlers - [x] Added `getPrismyContext` method to get context. (must be used in the scope of selectors, middleware and handlers) - [x] Removed `contextSelector`, use `getPrismyContext` +- [ ] Add `createConcurrentSelector(...selectors: PrismySelector[])` - [x] Simplified middleware interface - Before @@ -62,21 +85,22 @@ ```ts (next: () => Promise) => Promise ``` -- Return without res -- Include prismy-cookie -- Added DI Selector +- [ ] Make middleware into a class +- [ ] Return without res +- [ ] Include prismy-cookie +- [ ] Added DI Selector # Fix router - # Goal ```ts -const serverHandler = router([ - route(routeInfo, [selector], handler), - route(routeInfo, prismyHandler), - route(['/deprecated', '*'], ()=> redirect('/')), - notFoundRoute(() => '*') +import {Router, Route} from 'prismy' +const serverHandler = Router([ + Route(routeInfo, [selector], handler), + Route(routeInfo, prismyHandler), + Route(['/deprecated', '*'], ()=> redirect('/')), + NotFoundRoute(() => res('Not Found', 404)) ], { prefix: '...' middleware: [...], @@ -114,3 +138,54 @@ subdomain routing Dont use context anymore. Async storage is enough. Inject symbol to selectors to prevent mistake + + +## Callstack + +```ts +const handler = Handler([s1, s2, s3], (v1, v2, v3) => { + return res +}, [m1, m2, m3]) +``` + +`m3 { m2 { m1 { s1 -> s2 -> s3 -> handler } } }` + +Middleware can: +- Do something before running its inner middleware and handler (Validating token, Retrieving session) +- Do something after running its inner middleware and handler (Rotating cookie, Handling an error) +- Skip running its inner middleware and handler (When token validation fails, should throw an error) + +But it cannot control its outer middleware + +```ts +const middleware = Middleware([], next => async () => { + // Do something before the handler is triggered + const result = doSomethingBefore() + if (isBad(result)) { + // Skip running `next` handler + return res() + } + const result = await next() + + // Do something after the handler is triggered + doSomethingAfter() + + return result +}) +``` + +### Middleware order + +Later is the outer. +Outer has more responsibility. + +```ts +[sessionMiddleware, ...., loggingMiddleware, errorMiddleware] +``` + +### Selector order + +First one goes first. +If the first one fails, latter ones never triggered. + +If selectors don't cause side effect, you can even run them concurrently. From 0cda40a2d6239e904f3d9bc11c9f61682751a0d2 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 09:00:39 +0900 Subject: [PATCH 026/109] Reconfigure jest for fast testing --- jest.config.ts | 17 +++++++++++++++++ package.json | 16 ++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 jest.config.ts diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..9619fde --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,17 @@ +import type { JestConfigWithTsJest } from 'ts-jest' + +const jestConfig: JestConfigWithTsJest = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/node_modules/', '/dist/', '/examples/'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + isolatedModules: true, + }, + ], + }, +} + +export default jestConfig diff --git a/package.json b/package.json index 827ff8c..ee15cf3 100644 --- a/package.json +++ b/package.json @@ -40,30 +40,22 @@ }, "devDependencies": { "@types/content-type": "^1.1.3", - "@types/jest": "^24.0.13", + "@types/jest": "^29.5.11", "@types/node": "^20.10.6", "@types/node-fetch": "^2.6.10", "@types/test-listen": "^1.1.0", "async-listen": "^3.0.1", "codecov": "^3.8.0", "got": "^11.8.0", - "jest": "^26.6.1", + "jest": "^29.7.0", "node-fetch": "^2.7.0", "prettier": "^3.1.1", "rimraf": "^3.0.0", - "ts-jest": "^26.4.2", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", "typedoc": "^0.15.0", "typescript": "^4.9.5" }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "testPathIgnorePatterns": [ - "/node_modules/", - "/dist/", - "/examples/" - ] - }, "dependencies": { "content-type": "^1.0.4", "is-stream": "^2.0.0", From 21113e1e4873d1ae337f1c981f061b657b407617 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 11:10:53 +0900 Subject: [PATCH 027/109] Remove res --- examples/basic-server/src/index.ts | 10 +- examples/with-session/src/index.ts | 18 +- readme.md | 46 ++--- specs/bodyReaders.spec.ts | 20 +-- specs/helpers.ts | 1 + specs/middleware.spec.ts | 6 +- specs/prismy.spec.ts | 26 +-- specs/res.spec.ts | 140 --------------- specs/result.spec.ts | 137 ++++++++++++++ specs/router.spec.ts | 40 ++--- specs/selectors/body.spec.ts | 8 +- specs/selectors/bufferBody.spec.ts | 4 +- specs/selectors/headers.spec.ts | 4 +- specs/selectors/jsonBody.spec.ts | 8 +- specs/selectors/method.spec.ts | 4 +- specs/selectors/query.spec.ts | 6 +- specs/selectors/searchParam.spec.ts | 10 +- specs/selectors/textBody.spec.ts | 4 +- specs/selectors/url.spec.ts | 6 +- specs/selectors/urlEncodedBody.spec.ts | 4 +- specs/send.spec.ts | 18 +- specs/types/handler.ts | 4 +- specs/types/middleware.ts | 4 +- src/error.ts | 4 +- src/handler.ts | 5 +- src/prismy.ts | 5 + src/res.ts | 235 +++++++++++++++---------- src/selectors/url.ts | 2 +- 28 files changed, 418 insertions(+), 361 deletions(-) delete mode 100644 specs/res.spec.ts create mode 100644 specs/result.spec.ts diff --git a/examples/basic-server/src/index.ts b/examples/basic-server/src/index.ts index ac8a1f1..590005f 100644 --- a/examples/basic-server/src/index.ts +++ b/examples/basic-server/src/index.ts @@ -1,26 +1,26 @@ -import { prismy, res, router } from 'prismy' +import { prismy, Result, router } from 'prismy' export const rootHandler = prismy([], () => { - return res( + return Result( [ '', '', '

Root Page

', '
Go to /test', '', - ].join('') + ].join(''), ) }) const testHandler = prismy([], () => { - return res( + return Result( [ '', '', '

Test Page

', 'Go to Root', '', - ].join('') + ].join(''), ) }) diff --git a/examples/with-session/src/index.ts b/examples/with-session/src/index.ts index 3829142..ab33ec4 100644 --- a/examples/with-session/src/index.ts +++ b/examples/with-session/src/index.ts @@ -1,4 +1,4 @@ -import { prismy, createUrlEncodedBodySelector, redirect, res } from 'prismy' +import { prismy, createUrlEncodedBodySelector, redirect, Result } from 'prismy' import { methodRouter } from 'prismy-method-router' import createSession from 'prismy-session' import JWTCookieStrategy from 'prismy-session-strategy-jwt-cookie' @@ -9,17 +9,17 @@ interface SessionData { const { sessionSelector, sessionMiddleware } = createSession( new JWTCookieStrategy({ - secret: 'RANDOM_HASH' - }) + secret: 'RANDOM_HASH', + }), ) const urlEncodedBodySelector = createUrlEncodedBodySelector() export default methodRouter( { - get: prismy([sessionSelector], session => { + get: prismy([sessionSelector], (session) => { const { data } = session - return res( + return Result( [ '', '', @@ -28,15 +28,15 @@ export default methodRouter( '', '', '', - '' - ].join('') + '', + ].join(''), ) }), post: prismy([sessionSelector, urlEncodedBodySelector], (session, body) => { session.data = typeof body.message === 'string' ? { message: body.message } : null return redirect('/') - }) + }), }, - [sessionMiddleware] + [sessionMiddleware], ) diff --git a/readme.md b/readme.md index a1fcd8a..d833daa 100644 --- a/readme.md +++ b/readme.md @@ -80,12 +80,12 @@ Make sure typescript strict setting is on if using typescript. `handler.ts` ```ts -import { prismy, res, Selector } from 'prismy' +import { prismy, Result, Selector } from 'prismy' const worldSelector: Selector = () => 'world'! export default prismy([worldSelector], async world => { - return res(`Hello ${world}`) // Hello world! + return Result(`Hello ${world}`) // Hello world! }) ``` @@ -163,7 +163,7 @@ Selectors are simple functions used to generate the arguments for the handler. A single `context` argument or type `Context`. ```ts -import { prismy, res, Selector } from 'prismy' +import { prismy, Result, Selector } from 'prismy' // This selector picks the current url off the request object const urlSelector: Selector = context => { @@ -178,7 +178,7 @@ export default prismy( // making it type safe without having to worry about verbose typings. url => { await doSomethingWithUrl(url) - return res('Done!') + return Result('Done!') } ) ``` @@ -187,7 +187,7 @@ Async selectors are also fully supported out of the box! It will resolve all selectors right before executing handler. ```ts -import { prismy, res, Selector } from 'prismy' +import { prismy, Result, Selector } from 'prismy' const asyncSelector: Selector = async context => { const value = await readValueFromFileSystem() @@ -196,7 +196,7 @@ const asyncSelector: Selector = async context => { export default prismy([asyncSelector], async value => { await doSomething(value) - return res('Done!') + return Result('Done!') }) ``` @@ -223,7 +223,7 @@ const jsonBodySelector = createJsonBodySelector({ export default prismy([jsonBodySelector], async jsonBody => { await doSomething(jsonBody) - return res('Done!') + return Result('Done!') }) ``` @@ -248,7 +248,7 @@ const requestBodySelector: Selector = context => { } export default prismy([requestBodySelector], requestBody => { - return res(`You're query was ${requestBody.json}!`) + return Result(`You're query was ${requestBody.json}!`) }) ``` @@ -267,7 +267,7 @@ This pattern, much like Redux middleware, allows you to: - Do something other than executing handler (e.g Routing, Error handling) ```ts -import { middleware, prismy, res, Selector, updateHeaders } from 'prismy' +import { middleware, prismy, Result, Selector, updateHeaders } from 'prismy' const withCors = middleware([], next => async () => { const resObject = await next() @@ -283,7 +283,7 @@ const withErrorHandler = middleware([urlSelector], next => async url => { try { return await next() } catch (error) { - return res(`Error from ${url}: ${error.message}`) + return Result(`Error from ${url}: ${error.message}`) } }) @@ -316,7 +316,7 @@ selector and middleware to give to prismy. Official strategies include `prismy-session-strategy-jwt-cookie` and `prismy-session-strategy-signed-cookie`. Both available on npm. ```ts -import { prismy, res } from 'prismy' +import { prismy, Result } from 'prismy' import createSession from 'prismy-session' import JWTSessionStrategy from 'prismy-session-strategy' @@ -331,7 +331,7 @@ default export prismy( async session => { const { data } = session await doSomething(data) - return res('Done') + return Result('Done') }, [sessionMiddleware] ) @@ -343,7 +343,7 @@ default export prismy( Prismy also offers a selector for cookies in the `prismy-cookie` package. ```ts -import { prismy, res } from 'prismy' +import { prismy, Result } from 'prismy' import { appendCookie, createCookiesSelector } from 'prismy-cookie' const cookiesSelector = createCookiesSelector() @@ -353,7 +353,7 @@ export default prismy([cookiesSelector], async cookies => { * a string key, value tuple returning a new response object with the * cookie appended. */ - return appendCookie(res('Cookie added!'), ['key', 'value']) + return appendCookie(Result('Cookie added!'), ['key', 'value']) }) ``` @@ -362,7 +362,7 @@ export default prismy([cookiesSelector], async cookies => { From v3, `prismy` provides `router` method to create a routing handler. ```ts -import { prismy, res } from 'prismy' +import { prismy, Result } from 'prismy' import { router } from 'prismy-method-router' import http from 'http' @@ -371,7 +371,7 @@ const myRouter = router([ ['/posts', 'get'], prismy([], () => { const posts = fetchPostList() - return res({ posts }) + return Result({ posts }) }) ], [ @@ -387,7 +387,7 @@ const myRouter = router([ '/posts/:postId', prismy([createRouteParamSelector('postId')], (postId) => { const post = fetchOnePost(postId) - return res({ post }) + return Result({ post }) }) ] ]) @@ -410,7 +410,7 @@ import { prismy, querySelector, redirect, - res, + Result, Selector } from 'prismy' import { methodRouter } from 'prismy-method-router' @@ -462,15 +462,15 @@ export default methodRouter( { get: prismy([], async () => { const todos = await getTodos() - return res({ todos }) + return Result({ todos }) }), post: prismy([contentSelector], async content => { const todo = await createTodo(content) - return res({ todo }) + return Result({ todo }) }), delete: prismy([todoIdSelector], async id => { await deleteTodo(id) - return res('Deleted') + return Result('Deleted') }) }, [authMiddleware, sessionMiddleware] @@ -548,7 +548,7 @@ prismy(selectors, handler) // will give type error prismy([selector1, selector2], handler) // Ok! ``` -- This weird type error may also occur if the handler does not return a `ResponseObject`. Use `res(..)` to generate a `ResponseObject` easily. +- This weird type error may also occur if the handler does not return a `ResponseObject`. Use `Result(..)` to generate a `ResponseObject` easily. ```ts // Will show crazy error. @@ -558,7 +558,7 @@ prismy([selector1, selector2], (one, two) => { // Ok! prismy([selector1, selector2], (one, two) => { - return res('Is a ResponseObject') + return Result('Is a ResponseObject') }) ``` diff --git a/specs/bodyReaders.spec.ts b/specs/bodyReaders.spec.ts index db014e3..329ca58 100644 --- a/specs/bodyReaders.spec.ts +++ b/specs/bodyReaders.spec.ts @@ -1,6 +1,6 @@ import got from 'got' import getRawBody from 'raw-body' -import { Middleware, prismy, res, getPrismyContext } from '../src' +import { Middleware, prismy, Result, getPrismyContext } from '../src' import { readBufferBody, readJsonBody, readTextBody } from '../src/bodyReaders' import { createPrismySelector } from '../src/selectors/createSelector' import { testHandler } from './helpers' @@ -13,7 +13,7 @@ describe('readBufferBody', () => { const { req } = getPrismyContext() const body = await readBufferBody(req) - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -49,7 +49,7 @@ describe('readBufferBody', () => { async (_) => { const { req } = getPrismyContext() const body = await readBufferBody(req) - return res(body) + return Result(body) }, [ Middleware([], (next) => async () => { @@ -90,7 +90,7 @@ describe('readBufferBody', () => { const body1 = await readBufferBody(req) const body2 = await readBufferBody(req) - return res({ + return Result({ isCached: body1 === body2, }) }) @@ -113,7 +113,7 @@ describe('readBufferBody', () => { const { req } = getPrismyContext() const body = await readBufferBody(req, { limit: '1 byte' }) - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -139,7 +139,7 @@ describe('readBufferBody', () => { const { req } = getPrismyContext() const body = await readBufferBody(req, { encoding: 'lol' }) - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -165,7 +165,7 @@ describe('readBufferBody', () => { await getRawBody(req, { limit: '1mb', length }) const body = await readBufferBody(req) - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -191,7 +191,7 @@ describe('readTextBody', () => { const { req } = getPrismyContext() const body = await readTextBody(req) - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -216,7 +216,7 @@ describe('readJsonBody', () => { const { req } = getPrismyContext() const body = await readJsonBody(req) - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -242,7 +242,7 @@ describe('readJsonBody', () => { const { req } = getPrismyContext() const body = await readJsonBody(req) - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { diff --git a/specs/helpers.ts b/specs/helpers.ts index 5877dba..108ac23 100644 --- a/specs/helpers.ts +++ b/specs/helpers.ts @@ -37,5 +37,6 @@ export async function testFetch(url: string, options?: RequestInit) { return { statusCode: response.status, body: testResult, + headers: response.headers, } } diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 269ebb6..a135346 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,5 +1,5 @@ import { testFetch, testHandler } from './helpers' -import { prismy, res, Middleware, getPrismyContext } from '../src' +import { prismy, Result, Middleware, getPrismyContext } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' describe('middleware', () => { @@ -13,7 +13,7 @@ describe('middleware', () => { try { return await next() } catch (error) { - return res(`${url} : ${(error as any).message}`, 500) + return Result(`${url} : ${(error as any).message}`, 500) } }, ) @@ -42,7 +42,7 @@ describe('middleware', () => { try { return await next() } catch (error) { - return res(`${url} : ${(error as any).message}`, 500) + return Result(`${url} : ${(error as any).message}`, 500) } }, ) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index afabfaf..2a68806 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -1,11 +1,17 @@ import { testFetch, testHandler } from './helpers' -import { prismy, res, err, getPrismyContext, Middleware } from '../src' +import { + prismy, + getPrismyContext, + Middleware, + Result, + ErrorResult, +} from '../src' import { createPrismySelector } from '../src/selectors/createSelector' import { Handler } from '../src/handler' describe('prismy', () => { it('returns node.js request handler', async () => { - const handler = prismy([], () => res('Hello, World!')) + const handler = prismy([], () => Result('Hello, World!')) await testHandler(handler, async (url) => { const response = await testFetch(url) @@ -22,7 +28,7 @@ describe('prismy', () => { const { req } = getPrismyContext() return req.url! }) - const handler = prismy([rawUrlSelector], (url) => res(url)) + const handler = prismy([rawUrlSelector], (url) => Result(url)) await testHandler(handler, async (url) => { const response = await testFetch(url) @@ -38,7 +44,7 @@ describe('prismy', () => { const asyncRawUrlSelector = createPrismySelector( async () => getPrismyContext().req.url!, ) - const handler = prismy([asyncRawUrlSelector], (url) => res(url)) + const handler = prismy([asyncRawUrlSelector], (url) => Result(url)) await testHandler(handler, async (url) => { const response = await testFetch(url) @@ -55,11 +61,11 @@ describe('prismy', () => { const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) - const handler = Handler([rawUrlSelector], (url) => res(url)) + const handler = Handler([rawUrlSelector], (url) => Result(url)) const result = handler.handler('Hello, World!') - expect(result).toEqual({ + expect(result).toMatchObject({ body: 'Hello, World!', headers: {}, statusCode: 200, @@ -72,7 +78,7 @@ describe('prismy', () => { try { return await next() } catch (error) { - return err(500, (error as any).message) + return ErrorResult(500, (error as any).message) } } }) @@ -104,7 +110,7 @@ describe('prismy', () => { try { return await next() } catch (error) { - return res((error as any).message, 500) + return ErrorResult(500, (error as any).message) } }) const rawUrlSelector = createPrismySelector( @@ -113,7 +119,7 @@ describe('prismy', () => { const handler = prismy( [rawUrlSelector], (url) => { - return res(url) + return Result(url) }, [problematicMiddleware, errorMiddleware], ) @@ -153,7 +159,7 @@ describe('prismy', () => { const handler = prismy( [rawUrlSelector], (url) => { - return res(url) + return Result(url) }, [], ) diff --git a/specs/res.spec.ts b/specs/res.spec.ts deleted file mode 100644 index 2bd41e5..0000000 --- a/specs/res.spec.ts +++ /dev/null @@ -1,140 +0,0 @@ -import got from 'got' -import { testHandler } from './helpers' -import { - prismy, - res, - redirect, - setBody, - setStatusCode, - updateHeaders, - setHeaders -} from '../src' - -describe('redirect', () => { - it('redirects', async () => { - const handler = prismy([], () => { - return redirect('https://github.com') - }) - - await testHandler(handler, async url => { - const response = await got(url, { - followRedirect: false - }) - expect(response).toMatchObject({ - statusCode: 302, - headers: { - location: 'https://github.com' - } - }) - }) - }) - - it('redirects with specific statusCode', async () => { - const handler = prismy([], () => { - return redirect('https://github.com', 301) - }) - - await testHandler(handler, async url => { - const response = await got(url, { - followRedirect: false - }) - expect(response).toMatchObject({ - statusCode: 301, - headers: { - location: 'https://github.com' - } - }) - }) - }) - - it('redirects with specific headers', async () => { - const handler = prismy([], () => { - return redirect('https://github.com', undefined, { - 'x-test': 'Hello, World!' - }) - }) - - await testHandler(handler, async url => { - const response = await got(url, { - followRedirect: false - }) - expect(response).toMatchObject({ - statusCode: 302, - headers: { - location: 'https://github.com', - 'x-test': 'Hello, World!' - } - }) - }) - }) -}) - -describe('setBody', () => { - it('replaces body', () => { - const resObj = res('Hello, World!') - - const newResObj = setBody(resObj, 'Good Bye!') - - expect(newResObj).toEqual({ - body: 'Good Bye!', - statusCode: 200, - headers: {} - }) - }) -}) - -describe('setStatusCode', () => { - it('replaces statusCode', () => { - const resObj = res('Hello, World!') - - const newResObj = setStatusCode(resObj, 201) - - expect(newResObj).toEqual({ - body: 'Hello, World!', - statusCode: 201, - headers: {} - }) - }) -}) - -describe('updateHeaders', () => { - it('update headers', () => { - const resObj = res(null, 200, { - 'x-test-message': 'Hello, World!' - }) - - const newResObj = updateHeaders(resObj, { - 'x-test-message': 'Good Bye!', - 'x-extra-message': 'Adios!' - }) - - expect(newResObj).toEqual({ - body: null, - statusCode: 200, - headers: { - 'x-test-message': 'Good Bye!', - 'x-extra-message': 'Adios!' - } - }) - }) -}) - -describe('setHeaders', () => { - it('replace headers', () => { - const resObj = res(null, 200, { - 'x-test-message': 'Hello, World!' - }) - - const newResObj = setHeaders(resObj, { - 'x-extra-message': 'Hola!' - }) - - expect(newResObj).toEqual({ - body: null, - statusCode: 200, - headers: { - 'x-extra-message': 'Hola!' - } - }) - }) -}) diff --git a/specs/result.spec.ts b/specs/result.spec.ts new file mode 100644 index 0000000..0900e1a --- /dev/null +++ b/specs/result.spec.ts @@ -0,0 +1,137 @@ +import { prismy, Redirect, Result } from '../src' +import { testFetch, testHandler } from './helpers' + +// TODO: Implement tests +describe('PrismyResult', () => {}) + +describe('ErrorResult', () => {}) + +describe('PrismySendResult', () => { + describe('#setStatusCode', () => { + it('sets status code', async () => { + const handler = prismy([], () => + Result('Hello, World!').setStatusCode(201), + ) + + await testHandler(handler, async (url) => { + const response = await testFetch(url) + + expect(response).toMatchObject({ + statusCode: 201, + body: 'Hello, World!', + }) + }) + }) + }) + + describe('#updateHeaders', () => { + it('adds headers', async () => { + const handler = prismy([], () => + Result('Hello, World!', 200, { + 'existing-header': 'Hello', + }).updateHeaders({ + 'new-header': 'Hola', + }), + ) + + await testHandler(handler, async (url) => { + const response = await testFetch(url) + + expect(response).toMatchObject({ + statusCode: 200, + body: 'Hello, World!', + }) + expect(response.headers.get('existing-header')).toBe('Hello') + expect(response.headers.get('new-header')).toBe('Hola') + }) + }) + + it('replaces existing headers if duplicated, but other headers are still intact', async () => { + const handler = prismy([], () => + Result('Hello, World!', 200, { + 'existing-header': 'Hello', + 'other-existing-header': 'World', + }).updateHeaders({ + 'existing-header': 'Hola', + }), + ) + + await testHandler(handler, async (url) => { + const response = await testFetch(url) + + expect(response).toMatchObject({ + statusCode: 200, + body: 'Hello, World!', + }) + expect(response.headers.get('existing-header')).toBe('Hola') + expect(response.headers.get('other-existing-header')).toBe('World') + }) + }) + }) + + describe('#setHeaders', () => { + it('replaces headers', async () => { + const handler = prismy([], () => + Result('Hello, World!', 200, { + 'existing-header': 'Hello', + }).setHeaders({ + 'new-header': 'Hola', + }), + ) + + await testHandler(handler, async (url) => { + const response = await testFetch(url) + + expect(response).toMatchObject({ + statusCode: 200, + body: 'Hello, World!', + }) + expect(response.headers.get('existing-header')).toBe(null) + expect(response.headers.get('new-header')).toBe('Hola') + }) + }) + }) +}) + +describe('Redirect', () => { + it('redirects', async () => { + const handler = prismy([], () => Redirect('https://github.com/')) + + await testHandler(handler, async (url) => { + const response = await testFetch(url, { redirect: 'manual' }) + expect(response).toMatchObject({ + statusCode: 302, + }) + expect(response.headers.get('location')).toBe('https://github.com/') + }) + }) + + it('sets statusCode', async () => { + const handler = prismy([], () => Redirect('https://github.com/', 301)) + + await testHandler(handler, async (url) => { + const response = await testFetch(url, { redirect: 'manual' }) + expect(response).toMatchObject({ + statusCode: 301, + }) + expect(response.headers.get('location')).toBe('https://github.com/') + }) + }) + + it('sets headers', async () => { + const handler = prismy([], () => + Redirect('https://github.com/', 302, { + 'custom-header': 'Hello!', + }), + ) + + await testHandler(handler, async (url) => { + const response = await testFetch(url, { redirect: 'manual' }) + expect(response).toMatchObject({ + statusCode: 302, + }) + expect(response.headers.get('location')).toBe('https://github.com/') + expect(response.headers.get('custom-header')).toBe('Hello!') + }) + }) +}) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 4b0f8ec..a7e7c81 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -2,7 +2,7 @@ import { testFetch, testHandler } from './helpers' import { routeParamSelector, prismy, - res, + Result, router, Route, Middleware, @@ -16,10 +16,10 @@ describe('router', () => { it('routes with pathname', async () => { expect.hasAssertions() const handlerA = Handler([], () => { - return res('a') + return Result('a') }) const handlerB = Handler([], () => { - return res('b') + return Result('b') }) const routerHandler = router([Route('/a', handlerA), Route('/b', handlerB)]) @@ -38,10 +38,10 @@ describe('router', () => { expect.hasAssertions() const handlerA = Handler([], () => { - return res('a') + return Result('a') }) const handlerB = Handler([], () => { - return res('b') + return Result('b') }) const routerHandler = router([ @@ -73,10 +73,10 @@ describe('router', () => { it('resolve params', async () => { expect.hasAssertions() const handlerA = Handler([], () => { - return res('a') + return Result('a') }) const handlerB = Handler([routeParamSelector('id')], (id) => { - return res(id) + return Result(id) }) const routerHandler = router([ @@ -97,10 +97,10 @@ describe('router', () => { it('resolves null if param is missing', async () => { expect.hasAssertions() const handlerA = Handler([], () => { - return res('a') + return Result('a') }) const handlerB = Handler([routeParamSelector('not-id')], (notId) => { - return res(notId) + return Result(notId) }) const routerHandler = router([ @@ -123,10 +123,10 @@ describe('router', () => { it('throws 404 error when no route found', async () => { expect.hasAssertions() const handlerA = Handler([], () => { - return res('a') + return Result('a') }) const handlerB = Handler([], () => { - return res('b') + return Result('b') }) const routerHandler = router([ @@ -149,13 +149,13 @@ describe('router', () => { it('uses custom not found handler if set', async () => { expect.hasAssertions() const handlerA = Handler([], () => { - return res('a') + return Result('a') }) const handlerB = Handler([], () => { - return res('b') + return Result('b') }) const customNotFoundHandler = Handler([], () => { - return res('Error: Customized Not Found Response', 404) + return Result('Error: Customized Not Found Response', 404) }) const routerHandler = router( @@ -180,10 +180,10 @@ describe('router', () => { it('prepends prefix to route path', async () => { expect.hasAssertions() const handlerA = Handler([], () => { - return res('a') + return Result('a') }) const handlerB = Handler([], () => { - return res('b') + return Result('b') }) const routerHandler = router( @@ -208,10 +208,10 @@ describe('router', () => { it('prepends prefix to route path (without root `/`)', async () => { expect.hasAssertions() const handlerA = Handler([], () => { - return res('a') + return Result('a') }) const handlerB = Handler([], () => { - return res('b') + return Result('b') }) const routerHandler = router( @@ -240,10 +240,10 @@ describe('router', () => { const handlerA = Handler([], () => { const context = getPrismyContext() - return res(weakMap.get(context)) + return Result(weakMap.get(context)) }) const handlerB = Handler([], () => { - return res('b') + return Result('b') }) const routerHandler = router( diff --git a/specs/selectors/body.spec.ts b/specs/selectors/body.spec.ts index 2cc2d0c..57dedcd 100644 --- a/specs/selectors/body.spec.ts +++ b/specs/selectors/body.spec.ts @@ -1,13 +1,13 @@ import got from 'got' import { testHandler } from '../helpers' import { BodySelector } from '../../src/selectors' -import { prismy, res } from '../../src' +import { prismy, Result } from '../../src' describe('createBodySelector', () => { it('returns text body', async () => { expect.hasAssertions() const handler = prismy([BodySelector()], (body) => { - return res(`${body.constructor.name}: ${body}`) + return Result(`${body.constructor.name}: ${body}`) }) await testHandler(handler, async (url) => { @@ -26,7 +26,7 @@ describe('createBodySelector', () => { it('returns parsed url encoded body', async () => { expect.hasAssertions() const handler = prismy([BodySelector()], (body) => { - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -50,7 +50,7 @@ describe('createBodySelector', () => { it('returns JSON object body', async () => { expect.hasAssertions() const handler = prismy([BodySelector()], (body) => { - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/bufferBody.spec.ts b/specs/selectors/bufferBody.spec.ts index 3463684..38d0b87 100644 --- a/specs/selectors/bufferBody.spec.ts +++ b/specs/selectors/bufferBody.spec.ts @@ -1,11 +1,11 @@ import got from 'got' import { testHandler } from '../helpers' -import { BufferBodySelector, prismy, res } from '../../src' +import { BufferBodySelector, prismy, Result } from '../../src' describe('createBufferBodySelector', () => { it('creates buffer body selector', async () => { const handler = prismy([BufferBodySelector()], (body) => { - return res(`${body.constructor.name}: ${body}`) + return Result(`${body.constructor.name}: ${body}`) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/headers.spec.ts b/specs/selectors/headers.spec.ts index ccfaca9..92d0151 100644 --- a/specs/selectors/headers.spec.ts +++ b/specs/selectors/headers.spec.ts @@ -1,11 +1,11 @@ import got from 'got' import { testHandler } from '../helpers' -import { headersSelector, prismy, res } from '../../src' +import { headersSelector, prismy, Result } from '../../src' describe('headersSelector', () => { it('select headers', async () => { const handler = prismy([headersSelector], (headers) => { - return res(headers['x-test']) + return Result(headers['x-test']) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/jsonBody.spec.ts b/specs/selectors/jsonBody.spec.ts index 01cac27..820cdba 100644 --- a/specs/selectors/jsonBody.spec.ts +++ b/specs/selectors/jsonBody.spec.ts @@ -1,12 +1,12 @@ import got from 'got' import { testHandler } from '../helpers' -import { JsonBodySelector, prismy, res } from '../../src' +import { JsonBodySelector, prismy, Result } from '../../src' describe('JsonBodySelector', () => { it('creates json body selector', async () => { const jsonBodySelector = JsonBodySelector() const handler = prismy([jsonBodySelector], (body) => { - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -30,7 +30,7 @@ describe('JsonBodySelector', () => { it('throw if content typeof a request is not set', async () => { const jsonBodySelector = JsonBodySelector() const handler = prismy([jsonBodySelector], (body) => { - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { @@ -54,7 +54,7 @@ describe('JsonBodySelector', () => { it('throws if content type of a request is not application/json', async () => { const jsonBodySelector = JsonBodySelector() const handler = prismy([jsonBodySelector], (body) => { - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/method.spec.ts b/specs/selectors/method.spec.ts index d21ace4..7b57e99 100644 --- a/specs/selectors/method.spec.ts +++ b/specs/selectors/method.spec.ts @@ -1,11 +1,11 @@ import got from 'got' import { testHandler } from '../helpers' -import { methodSelector, prismy, res } from '../../src' +import { methodSelector, prismy, Result } from '../../src' describe('methodSelector', () => { it('selects method', async () => { const handler = prismy([methodSelector], (method) => { - return res(method) + return Result(method) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/query.spec.ts b/specs/selectors/query.spec.ts index 6678e22..2804e03 100644 --- a/specs/selectors/query.spec.ts +++ b/specs/selectors/query.spec.ts @@ -1,11 +1,11 @@ import got from 'got' import { testHandler } from '../helpers' -import { querySelector, prismy, res } from '../../src' +import { querySelector, prismy, Result } from '../../src' describe('querySelector', () => { it('selects query', async () => { const handler = prismy([querySelector], (query) => { - return res(query) + return Result(query) }) await testHandler(handler, async (url) => { @@ -23,7 +23,7 @@ describe('querySelector', () => { it('reuses parsed query', async () => { const handler = prismy([querySelector, querySelector], (query, query2) => { - return res(JSON.stringify(query === query2)) + return Result(JSON.stringify(query === query2)) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/searchParam.spec.ts b/specs/selectors/searchParam.spec.ts index f2bd475..ab86101 100644 --- a/specs/selectors/searchParam.spec.ts +++ b/specs/selectors/searchParam.spec.ts @@ -4,14 +4,14 @@ import { SearchParamSelector, SearchParamListSelector, prismy, - res, + Result, } from '../../src' import { URLSearchParams } from 'url' describe('SearchParamSelector', () => { it('selects a search param', async () => { const handler = prismy([SearchParamSelector('message')], (message) => { - return res({ message }) + return Result({ message }) }) await testHandler(handler, async (url) => { @@ -29,7 +29,7 @@ describe('SearchParamSelector', () => { it('selects null if there is no param with the name', async () => { const handler = prismy([SearchParamSelector('message')], (message) => { - return res({ message }) + return Result({ message }) }) await testHandler(handler, async (url) => { @@ -48,7 +48,7 @@ describe('SearchParamSelector', () => { describe('SearchParamListSelector', () => { it('selects a search param list', async () => { const handler = prismy([SearchParamListSelector('message')], (messages) => { - return res({ messages }) + return Result({ messages }) }) await testHandler(handler, async (url) => { @@ -69,7 +69,7 @@ describe('SearchParamListSelector', () => { it('selects null if there is no param with the name', async () => { const handler = prismy([SearchParamListSelector('message')], (messages) => { - return res({ messages }) + return Result({ messages }) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/textBody.spec.ts b/specs/selectors/textBody.spec.ts index 4a7e5a0..c969e3b 100644 --- a/specs/selectors/textBody.spec.ts +++ b/specs/selectors/textBody.spec.ts @@ -1,11 +1,11 @@ import got from 'got' import { testHandler } from '../helpers' -import { prismy, res, TextBodySelector } from '../../src' +import { prismy, Result, TextBodySelector } from '../../src' describe('createTextBodySelector', () => { it('creates buffer body selector', async () => { const handler = prismy([TextBodySelector()], (body) => { - return res(`${body.constructor.name}: ${body}`) + return Result(`${body.constructor.name}: ${body}`) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/url.spec.ts b/specs/selectors/url.spec.ts index baf7940..6885c43 100644 --- a/specs/selectors/url.spec.ts +++ b/specs/selectors/url.spec.ts @@ -1,11 +1,11 @@ import got from 'got' import { testHandler } from '../helpers' -import { urlSelector, prismy, res } from '../../src' +import { urlSelector, prismy, Result } from '../../src' describe('urlSelector', () => { it('selects url', async () => { const handler = prismy([urlSelector], (url) => { - return res({ + return Result({ pathname: url.pathname, search: url.search, }) @@ -28,7 +28,7 @@ describe('urlSelector', () => { it('reuses parsed url', async () => { const handler = prismy([urlSelector, urlSelector], (url, url2) => { - return res(JSON.stringify(url === url2)) + return Result(JSON.stringify(url === url2)) }) await testHandler(handler, async (url) => { diff --git a/specs/selectors/urlEncodedBody.spec.ts b/specs/selectors/urlEncodedBody.spec.ts index 42135e3..c6eaf5d 100644 --- a/specs/selectors/urlEncodedBody.spec.ts +++ b/specs/selectors/urlEncodedBody.spec.ts @@ -1,11 +1,11 @@ import got from 'got' import { testHandler } from '../helpers' -import { prismy, res, UrlEncodedBodySelector } from '../../src' +import { prismy, Result, UrlEncodedBodySelector } from '../../src' describe('URLEncodedBody', () => { it('injects parsed url encoded body', async () => { const handler = prismy([UrlEncodedBodySelector()], (body) => { - return res(body) + return Result(body) }) await testHandler(handler, async (url) => { diff --git a/specs/send.spec.ts b/specs/send.spec.ts index 9471286..e99a17f 100644 --- a/specs/send.spec.ts +++ b/specs/send.spec.ts @@ -51,7 +51,7 @@ describe('send', () => { expect(targetBuffer.equals(buffer)).toBe(true) expect(response.headers['content-length']).toBe( - targetBuffer.length.toString() + targetBuffer.length.toString(), ) }) }) @@ -78,7 +78,7 @@ describe('send', () => { expect(targetBuffer.equals(buffer)).toBe(true) expect(response.headers['content-length']).toBe( - targetBuffer.length.toString() + targetBuffer.length.toString(), ) expect(response.headers['content-type']).toBe('application/octet-stream') }) @@ -110,7 +110,7 @@ describe('send', () => { expect.hasAssertions() const sendHandler = ( _request: IncomingMessage, - response: ServerResponse + response: ServerResponse, ) => { response.end('test') } @@ -164,7 +164,7 @@ describe('send', () => { }) expect(response.body).toMatchObject(target) expect(response.headers['content-length']).toBe( - JSON.stringify(target).length.toString() + JSON.stringify(target).length.toString(), ) }) }) @@ -186,10 +186,10 @@ describe('send', () => { }) expect(response.body).toMatchObject(target) expect(response.headers['content-length']).toBe( - JSON.stringify(target).length.toString() + JSON.stringify(target).length.toString(), ) expect(response.headers['content-type']).toBe( - 'application/json; charset=utf-8' + 'application/json; charset=utf-8', ) }) }) @@ -212,7 +212,7 @@ describe('send', () => { const stringifiedTarget = JSON.stringify(target) expect(response.body).toBe(stringifiedTarget) expect(response.headers['content-length']).toBe( - stringifiedTarget.length.toString() + stringifiedTarget.length.toString(), ) }) }) @@ -234,10 +234,10 @@ describe('send', () => { const stringifiedTarget = JSON.stringify(target) expect(response.body).toBe(stringifiedTarget) expect(response.headers['content-length']).toBe( - stringifiedTarget.length.toString() + stringifiedTarget.length.toString(), ) expect(response.headers['content-type']).toBe( - 'application/json; charset=utf-8' + 'application/json; charset=utf-8', ) }) }) diff --git a/specs/types/handler.ts b/specs/types/handler.ts index 5833601..36badb3 100644 --- a/specs/types/handler.ts +++ b/specs/types/handler.ts @@ -1,4 +1,4 @@ -import { urlSelector, methodSelector, res, ResponseObject } from '../../src' +import { urlSelector, methodSelector, Result, ResponseObject } from '../../src' import { URL } from 'url' import { expectType } from '../helpers' import { Handler } from '../../src/handler' @@ -6,7 +6,7 @@ import { Handler } from '../../src/handler' const handler1 = Handler([urlSelector, methodSelector], (url, method) => { expectType(url) expectType(method) - return res('') + return Result('') }) expectType< diff --git a/specs/types/middleware.ts b/specs/types/middleware.ts index 5c36a91..5e51c5b 100644 --- a/specs/types/middleware.ts +++ b/specs/types/middleware.ts @@ -4,7 +4,7 @@ import { methodSelector, ResponseObject, prismy, - res, + Result, } from '../../src' import { URL } from 'url' import { expectType } from '../helpers' @@ -36,4 +36,4 @@ expectType< ) => ResponseObject | Promise> >(middleware1.handler) -prismy([], () => res(''), [middleware1]) +prismy([], () => Result(''), [middleware1]) diff --git a/src/error.ts b/src/error.ts index 9777d5f..fcdce11 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,4 @@ -import { res } from './res' +import { Result } from './res' /** * Creates a response object from an error @@ -17,7 +17,7 @@ export function createErrorResObject(error: any) { const message = process.env.NODE_ENV === 'production' ? error.message : error.stack - return res(message, statusCode) + return Result(message, statusCode) } class PrismyError extends Error { diff --git a/src/handler.ts b/src/handler.ts index 58636ca..5d08741 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -5,6 +5,7 @@ import { MaybePromise, ResponseObject, SelectorReturnTypeTuple, + PrismySendResult, } from '.' import { PrismySelector } from './selectors/createSelector' import { compileHandler } from './utils' @@ -24,7 +25,7 @@ export class PrismyHandler< public middlewareList: PrismyMiddleware[], ) {} - async handle(): Promise> { + async handle(): Promise | PrismySendResult> { const next: PrismyNextFunction = compileHandler( this.selectors, this.handler, @@ -56,7 +57,7 @@ export class PrismyHandler< * const worldSelector: Selector = () => "world"! * * const handler = Handler([ worldSelector ], async world => { - * return res(`Hello ${world}!`) // Hello world! + * return Result(`Hello ${world}!`) // Hello world! * }) * ``` * diff --git a/src/prismy.ts b/src/prismy.ts index 21e2919..c282e87 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -10,6 +10,7 @@ import { PrismyContext, SelectorReturnTypeTuple, } from './types' +import { PrismyResult } from './res' export const prismyContextStorage = new AsyncLocalStorage() export function getPrismyContext(): PrismyContext { @@ -76,6 +77,10 @@ export function prismy[]>( prismyContextStorage.run(context, async () => { const resObject = await injectedHandler.handle() + if (resObject instanceof PrismyResult) { + resObject.resolve(request, response) + return + } send(request, response, resObject) }) } diff --git a/src/res.ts b/src/res.ts index 8962dec..a63d95c 100644 --- a/src/res.ts +++ b/src/res.ts @@ -1,34 +1,160 @@ -import { OutgoingHttpHeaders } from 'http' +import { + IncomingMessage, + OutgoingHttpHeaders, + RequestListener, + ServerResponse, +} from 'http' +import { send } from './send' import { ResponseObject } from './types' +export class PrismyResult { + constructor(public resolver: RequestListener) {} +} + +/** + * Create a raw result it will directly handle node.js's requests and responses. + * It is useful when controlling raw request stream and raw response stream. + * + * @param resolver + * @returns {@link PrismyResult} + */ +export function RawResult(resolver: RequestListener): PrismyResult { + return new PrismyResult(resolver) +} + +export class PrismySendResult extends PrismyResult { + constructor( + public readonly body: B, + public readonly statusCode: number, + public readonly headers: OutgoingHttpHeaders, + ) { + super((request: IncomingMessage, response: ServerResponse) => { + this.resolve(request, response) + }) + } + + /** + * Resolve function used by http.Server + * @param request + * @param response + */ + resolve(request: IncomingMessage, response: ServerResponse) { + send(request, response, { + body: this.body, + statusCode: this.statusCode, + headers: this.headers, + }) + } + + /** + * Creates a new result with a new status code + * + * @param statusCode - HTTP status code + * @returns New {@link PrismySendResult} + * + * @public + */ + setStatusCode(statusCode: number) { + return new PrismySendResult(this.body, statusCode, this.headers) + } + + /** + * Creates a new result with a new body + * + * @param body - Body to be set + * @returns New {@link PrismySendResult} + * + * @public + */ + setBody(body: BB) { + return new PrismySendResult(body, this.statusCode, this.headers) + } + + /** + * Creates a new result with the extra headers. + * Merging new headers into existing headers. But duplicated headers will be replaced with new ones. + * `{...existingHeaders, ...newHeaders}` + * + * @param newHeaders - HTTP response headers + * @returns New {@link PrismySendResult} + * + * To set multiple headers with same name, use an array. + * @example + * ```ts + * const existingHeaderValue = result.headers['multiple-values'] + * let newHeaderValues = [...] + * + * if (existingHeaderValue != null) { + * if (Array.isArray(existingHeaderValue)) { + * newHeaderValues = [...existingHeaderValue, ...newHeaderValues] + * } else { + * newHeaderValues = [existingHeaderValue, ...newHeaderValues] + * } + * } + * + * result.updateHeaders({ + * 'multiple-values': newHeaderValues + * }) + * ``` + * + * @public + */ + updateHeaders(newHeaders: OutgoingHttpHeaders) { + return new PrismySendResult(this.body, this.statusCode, { + ...this.headers, + ...newHeaders, + }) + } + + /** + * Creates a new result with the new headers. + * This will flush all existing headers and set new ones only. + * + * @param newHeaders - HTTP response headers + * @returns New {@link PrismySendResult} + * + * @public + */ + setHeaders(headers: OutgoingHttpHeaders) { + return new PrismySendResult(this.body, this.statusCode, headers) + } +} + /** * Factory function for creating http responses * * @param body - Body of the response * @param statusCode - HTTP status code of the response * @param headers - HTTP headers for the response - * @returns A {@link ResponseObject | response object} containing necessary information + * @returns A {@link PrismySendResult} containing necessary information * * @public */ -export function res( +export function Result( body: B, statusCode: number = 200, headers: OutgoingHttpHeaders = {}, -): ResponseObject { - return { - body, - statusCode, - headers, - } +): PrismySendResult { + return new PrismySendResult(body, statusCode, headers) } -export function err( +/** + * Factory function for creating error http responses + * Technically, it is functionally identical to `Result` but its arguments order is different from `Result`. + * + * @param body - Body of the response + * @param statusCode - HTTP status code of the response + * @param headers - HTTP headers for the response + * @returns A {@link PrismySendResult} containing necessary information + * + * @public + */ +export function ErrorResult( statusCode: number, body: B, - headers?: OutgoingHttpHeaders, -): ResponseObject { - return res(body, statusCode, headers) + headers: OutgoingHttpHeaders = {}, +): PrismySendResult { + return new PrismySendResult(body, statusCode, headers) } /** @@ -41,92 +167,13 @@ export function err( * * @public */ -export function redirect( +export function Redirect( location: string, statusCode: number = 302, extraHeaders: OutgoingHttpHeaders = {}, ): ResponseObject { - return res(null, statusCode, { + return Result(null, statusCode, { location, ...extraHeaders, }) } - -/** - * Creates a new response with a new body - * - * @param resObject - The response to set the body on - * @param body - Body to be set - * @returns New {@link ResponseObject | response} with the new body - * - * @public - */ -export function setBody( - resObject: ResponseObject, - body: B2, -): ResponseObject { - return { - ...resObject, - body, - } -} - -/** - * Creates a new response with a new status code - * - * @param resObject - The response to set the code to - * @param statusCode - HTTP status code - * @returns New {@link ResponseObject | response} with the new statusCode - * - * @public - */ -export function setStatusCode( - resObject: ResponseObject, - statusCode: number, -): ResponseObject { - return { - ...resObject, - statusCode, - } -} - -/** - * Creates a new response with the extra headers. - * - * @param resObject - The response to add the new headers to - * @param extraHeaders - HTTP response headers - * @returns New {@link ResponseObject | response} with the extra headers - * - * @public - */ -export function updateHeaders( - resObject: ResponseObject, - extraHeaders: OutgoingHttpHeaders, -): ResponseObject { - return { - ...resObject, - headers: { - ...resObject.headers, - ...extraHeaders, - }, - } -} - -/** - * Creates a new response overriting all headers with new ones. - * - * @param resObject - response to set new headers on - * @param headers - HTTP response headers to set - * @returns New {@link ResponseObject | response} with new headers set - * - * @public - */ -export function setHeaders( - resObject: ResponseObject, - headers: OutgoingHttpHeaders, -): ResponseObject { - return { - ...resObject, - headers, - } -} diff --git a/src/selectors/url.ts b/src/selectors/url.ts index c2bd4f8..e83e31d 100644 --- a/src/selectors/url.ts +++ b/src/selectors/url.ts @@ -14,7 +14,7 @@ const urlMap = new WeakMap() * const prismyHandler = prismy( * [urlSelector], * url => { - * return res(url.path) + * return Result(url.path) * } * ) * ``` From dba32e376ebe8f781e1e0f4e7efc0885d7ed718a Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 15:55:48 +0900 Subject: [PATCH 028/109] Fix handler type --- src/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handler.ts b/src/handler.ts index 5d08741..56be7b7 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -77,7 +77,7 @@ export function Handler[]>( handler: ( ...args: SelectorReturnTypeTuple ) => MaybePromise>, - middlewareList: PrismyMiddleware[] = [], + middlewareList: PrismyMiddleware[]>[] = [], ) { return new PrismyHandler(selectors, handler, middlewareList) } From a251ef2636dfbc7fe148de5c1c459cb9dab6f4b0 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 15:56:05 +0900 Subject: [PATCH 029/109] Export handler --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index cacd723..4586f6e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from './selectors' export * from './error' export * from './router' export * from './res' +export * from './handler' From 6ccb33dae14465b87ae43e49a06883a7e0c9dba5 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 15:56:25 +0900 Subject: [PATCH 030/109] Use PascalCase for factory fn --- src/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/router.ts b/src/router.ts index 7a72da8..fcacb6d 100644 --- a/src/router.ts +++ b/src/router.ts @@ -39,7 +39,7 @@ export class PrismyRoute { } } -export function router( +export function Router( routes: PrismyRoute[], { prefix = '/', middleware = [], notFoundHandler }: PrismyRouterOptions = {}, ) { From eb56c90fd3d5ddee3704f0f0dc05bc211d454614 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 15:56:54 +0900 Subject: [PATCH 031/109] Improve test tools --- specs/helpers.ts | 114 ++++++++++++++++++++++++--- specs/middleware.spec.ts | 28 ++++--- specs/prismy.spec.ts | 83 ++++++++++---------- specs/result.spec.ts | 119 ++++++++++++++-------------- specs/router.spec.ts | 166 ++++++++++++++++++--------------------- 5 files changed, 297 insertions(+), 213 deletions(-) diff --git a/specs/helpers.ts b/specs/helpers.ts index 108ac23..4311fa4 100644 --- a/specs/helpers.ts +++ b/specs/helpers.ts @@ -1,8 +1,7 @@ -import http from 'http' +import http, { IncomingMessage, RequestListener, ServerResponse } from 'http' import listen from 'async-listen' -import { RequestListener } from 'http' import { URL } from 'url' -import fetch, { RequestInit } from 'node-fetch' +import { prismy, PrismyHandler } from '../src' export type TestCallback = (url: string) => Promise | void @@ -26,12 +25,7 @@ export async function testHandler( /* istanbul ignore next */ export function expectType(value: T): void {} -export interface TestFetchOptions { - method: string -} - -export async function testFetch(url: string, options?: RequestInit) { - const response = await fetch(url, options) +async function resolveTestResponse(response: Response) { const testResult = await response.text() return { @@ -40,3 +34,105 @@ export async function testFetch(url: string, options?: RequestInit) { headers: response.headers, } } + +let testServer: null | PrismyTestServer = null +export const testServerManager = { + start: () => { + if (testServer == null) { + testServer = new PrismyTestServer() + } + return testServer.start() + }, + close: () => { + if (testServer == null) { + throw new Error('No test server to close') + } + return testServer.close() + }, + load: (handler: PrismyHandler) => testServer!.load(handler), + call: (url?: string, options?: RequestInit) => + testServer!.call(url, options).then(resolveTestResponse), + loadRequestListener: (listener: RequestListener) => + testServer!.loadRequestListener(listener), + loadAndCall: ( + handler: PrismyHandler, + url?: string, + options?: RequestInit, + ) => { + testServer!.load(handler) + return testServer!.call(url, options).then(resolveTestResponse) + }, + loadRequestListenerAndCall: ( + listener: RequestListener, + url?: string, + options?: RequestInit, + ) => { + testServer!.loadRequestListener(listener) + return testServer!.call(url, options).then(resolveTestResponse) + }, +} +export class PrismyTestServer { + server: http.Server | null = null + url: string = '' + listener: RequestListener = () => { + throw new Error('PrismyTestServer: Listener is not set') + } + status: 'idle' | 'starting' | 'closing' = 'idle' + + loadRequestListener(listener: RequestListener) { + this.listener = listener + } + load(handler: PrismyHandler) { + this.listener = prismy(handler) + } + + listen(req: IncomingMessage, res: ServerResponse) { + this.listener(req, res) + } + + call(url: string = '/', options?: RequestInit) { + return fetch(this.url + url, options) + } + + async start() { + if (this.status !== 'idle') { + throw new Error( + `Cannot start test server (Current status: ${this.status})`, + ) + } + + if (this.server == null) { + const server = new http.Server(this.listen.bind(this)) + const url = await listen(server) + + this.server = server + this.url = url.origin + } + } + + async close() { + if (this.status !== 'idle') { + throw new Error( + `Cannot close test server (Current status: ${this.status})`, + ) + } + + if (this.server == null) { + return + } + + const server = this.server + this.server = null + this.url = '' + + await new Promise((resolve, reject) => { + server!.close((error) => { + if (error != null) { + reject(error) + } else { + resolve(null) + } + }) + }) + } +} diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index a135346..06c244c 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,6 +1,14 @@ -import { testFetch, testHandler } from './helpers' -import { prismy, Result, Middleware, getPrismyContext } from '../src' +import { Result, Middleware, getPrismyContext, Handler } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' +import { testServerManager } from './helpers' + +beforeAll(async () => { + await testServerManager.start() +}) + +afterAll(async () => { + await testServerManager.close() +}) describe('middleware', () => { it('creates Middleware via selectors and middleware handler', async () => { @@ -17,7 +25,7 @@ describe('middleware', () => { } }, ) - const handler = prismy( + const handler = Handler( [], () => { throw new Error('Hey!') @@ -25,11 +33,9 @@ describe('middleware', () => { [errorMiddleware], ) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadAndCall(handler) - expect(response).toMatchObject({ statusCode: 500, body: '/ : Hey!' }) - }) + expect(response).toMatchObject({ statusCode: 500, body: '/ : Hey!' }) }) it('accepts async selectors', async () => { @@ -46,7 +52,7 @@ describe('middleware', () => { } }, ) - const handler = prismy( + const handler = Handler( [], () => { throw new Error('Hey!') @@ -54,10 +60,8 @@ describe('middleware', () => { [errorMiddleware], ) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadAndCall(handler) - expect(response).toMatchObject({ statusCode: 500, body: '/ : Hey!' }) - }) + expect(response).toMatchObject({ statusCode: 500, body: '/ : Hey!' }) }) }) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 2a68806..76affa1 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -1,4 +1,3 @@ -import { testFetch, testHandler } from './helpers' import { prismy, getPrismyContext, @@ -8,18 +7,25 @@ import { } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' import { Handler } from '../src/handler' +import { testServerManager } from './helpers' + +beforeAll(async () => { + await testServerManager.start() +}) + +afterAll(async () => { + await testServerManager.close() +}) describe('prismy', () => { it('returns node.js request handler', async () => { const handler = prismy([], () => Result('Hello, World!')) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadRequestListenerAndCall(handler) - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) + expect(response).toMatchObject({ + statusCode: 200, + body: 'Hello, World!', }) }) @@ -30,13 +36,11 @@ describe('prismy', () => { }) const handler = prismy([rawUrlSelector], (url) => Result(url)) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadRequestListenerAndCall(handler) - expect(response).toMatchObject({ - statusCode: 200, - body: '/', - }) + expect(response).toMatchObject({ + statusCode: 200, + body: '/', }) }) @@ -46,13 +50,11 @@ describe('prismy', () => { ) const handler = prismy([asyncRawUrlSelector], (url) => Result(url)) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadRequestListenerAndCall(handler) - expect(response).toMatchObject({ - statusCode: 200, - body: '/', - }) + expect(response).toMatchObject({ + statusCode: 200, + body: '/', }) }) @@ -93,12 +95,11 @@ describe('prismy', () => { [errorMiddleware], ) - await testHandler(handler, async (url) => { - const response = await testFetch(url) - expect(response).toMatchObject({ - statusCode: 500, - body: 'Hey!', - }) + const response = await testServerManager.loadRequestListenerAndCall(handler) + + expect(response).toMatchObject({ + statusCode: 500, + body: 'Hey!', }) }) @@ -124,13 +125,11 @@ describe('prismy', () => { [problematicMiddleware, errorMiddleware], ) - await testHandler(handler, async (url) => { - const response = await testFetch(url, {}) + const response = await testServerManager.loadRequestListenerAndCall(handler) - expect(response).toMatchObject({ - statusCode: 500, - body: 'Hey!', - }) + expect(response).toMatchObject({ + statusCode: 500, + body: 'Hey!', }) }) @@ -142,13 +141,12 @@ describe('prismy', () => { }, [], ) - await testHandler(handler, async (url) => { - const response = await testFetch(url, {}) - expect(response).toMatchObject({ - statusCode: 500, - body: expect.stringContaining('Error: Hey!'), - }) + const response = await testServerManager.loadRequestListenerAndCall(handler) + + expect(response).toMatchObject({ + statusCode: 500, + body: expect.stringContaining('Error: Hey!'), }) }) @@ -163,13 +161,12 @@ describe('prismy', () => { }, [], ) - await testHandler(handler, async (url) => { - const response = await testFetch(url, {}) - expect(response).toMatchObject({ - statusCode: 500, - body: expect.stringContaining('Error: Hey!'), - }) + const response = await testServerManager.loadRequestListenerAndCall(handler) + + expect(response).toMatchObject({ + statusCode: 500, + body: expect.stringContaining('Error: Hey!'), }) }) }) diff --git a/specs/result.spec.ts b/specs/result.spec.ts index 0900e1a..a463a76 100644 --- a/specs/result.spec.ts +++ b/specs/result.spec.ts @@ -1,5 +1,13 @@ -import { prismy, Redirect, Result } from '../src' -import { testFetch, testHandler } from './helpers' +import { Redirect, Result, Handler } from '../src' +import { testServerManager } from './helpers' + +beforeAll(async () => { + await testServerManager.start() +}) + +afterAll(async () => { + await testServerManager.close() +}) // TODO: Implement tests describe('PrismyResult', () => {}) @@ -9,24 +17,22 @@ describe('ErrorResult', () => {}) describe('PrismySendResult', () => { describe('#setStatusCode', () => { it('sets status code', async () => { - const handler = prismy([], () => + const handler = Handler([], () => Result('Hello, World!').setStatusCode(201), ) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadAndCall(handler) - expect(response).toMatchObject({ - statusCode: 201, - body: 'Hello, World!', - }) + expect(response).toMatchObject({ + statusCode: 201, + body: 'Hello, World!', }) }) }) describe('#updateHeaders', () => { it('adds headers', async () => { - const handler = prismy([], () => + const handler = Handler([], () => Result('Hello, World!', 200, { 'existing-header': 'Hello', }).updateHeaders({ @@ -34,20 +40,18 @@ describe('PrismySendResult', () => { }), ) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadAndCall(handler) - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) - expect(response.headers.get('existing-header')).toBe('Hello') - expect(response.headers.get('new-header')).toBe('Hola') + expect(response).toMatchObject({ + statusCode: 200, + body: 'Hello, World!', }) + expect(response.headers.get('existing-header')).toBe('Hello') + expect(response.headers.get('new-header')).toBe('Hola') }) it('replaces existing headers if duplicated, but other headers are still intact', async () => { - const handler = prismy([], () => + const handler = Handler([], () => Result('Hello, World!', 200, { 'existing-header': 'Hello', 'other-existing-header': 'World', @@ -56,22 +60,20 @@ describe('PrismySendResult', () => { }), ) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadAndCall(handler) - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) - expect(response.headers.get('existing-header')).toBe('Hola') - expect(response.headers.get('other-existing-header')).toBe('World') + expect(response).toMatchObject({ + statusCode: 200, + body: 'Hello, World!', }) + expect(response.headers.get('existing-header')).toBe('Hola') + expect(response.headers.get('other-existing-header')).toBe('World') }) }) describe('#setHeaders', () => { it('replaces headers', async () => { - const handler = prismy([], () => + const handler = Handler([], () => Result('Hello, World!', 200, { 'existing-header': 'Hello', }).setHeaders({ @@ -79,59 +81,60 @@ describe('PrismySendResult', () => { }), ) - await testHandler(handler, async (url) => { - const response = await testFetch(url) + const response = await testServerManager.loadAndCall(handler) - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) - expect(response.headers.get('existing-header')).toBe(null) - expect(response.headers.get('new-header')).toBe('Hola') + expect(response).toMatchObject({ + statusCode: 200, + body: 'Hello, World!', }) + expect(response.headers.get('existing-header')).toBe(null) + expect(response.headers.get('new-header')).toBe('Hola') }) }) }) describe('Redirect', () => { it('redirects', async () => { - const handler = prismy([], () => Redirect('https://github.com/')) + const handler = Handler([], () => Redirect('https://github.com/')) - await testHandler(handler, async (url) => { - const response = await testFetch(url, { redirect: 'manual' }) - expect(response).toMatchObject({ - statusCode: 302, - }) - expect(response.headers.get('location')).toBe('https://github.com/') + const response = await testServerManager.loadAndCall(handler, '/', { + redirect: 'manual', }) + + expect(response).toMatchObject({ + statusCode: 302, + }) + expect(response.headers.get('location')).toBe('https://github.com/') }) it('sets statusCode', async () => { - const handler = prismy([], () => Redirect('https://github.com/', 301)) + const handler = Handler([], () => Redirect('https://github.com/', 301)) - await testHandler(handler, async (url) => { - const response = await testFetch(url, { redirect: 'manual' }) - expect(response).toMatchObject({ - statusCode: 301, - }) - expect(response.headers.get('location')).toBe('https://github.com/') + const response = await testServerManager.loadAndCall(handler, '/', { + redirect: 'manual', + }) + + expect(response).toMatchObject({ + statusCode: 301, }) + expect(response.headers.get('location')).toBe('https://github.com/') }) it('sets headers', async () => { - const handler = prismy([], () => + const handler = Handler([], () => Redirect('https://github.com/', 302, { 'custom-header': 'Hello!', }), ) - await testHandler(handler, async (url) => { - const response = await testFetch(url, { redirect: 'manual' }) - expect(response).toMatchObject({ - statusCode: 302, - }) - expect(response.headers.get('location')).toBe('https://github.com/') - expect(response.headers.get('custom-header')).toBe('Hello!') + const response = await testServerManager.loadAndCall(handler, '/', { + redirect: 'manual', + }) + + expect(response).toMatchObject({ + statusCode: 302, }) + expect(response.headers.get('location')).toBe('https://github.com/') + expect(response.headers.get('custom-header')).toBe('Hello!') }) }) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index a7e7c81..018bd0b 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -1,42 +1,41 @@ -import { testFetch, testHandler } from './helpers' import { routeParamSelector, - prismy, Result, - router, + Router, Route, Middleware, getPrismyContext, } from '../src' -import { join } from 'path' import { Handler } from '../src/handler' -import fetch from 'node-fetch' +import { testServerManager } from './helpers' + +beforeAll(async () => { + await testServerManager.start() +}) + +afterAll(async () => { + await testServerManager.close() +}) describe('router', () => { it('routes with pathname', async () => { - expect.hasAssertions() const handlerA = Handler([], () => { return Result('a') }) const handlerB = Handler([], () => { return Result('b') }) + const routerHandler = Router([Route('/a', handlerA), Route('/b', handlerB)]) - const routerHandler = router([Route('/a', handlerA), Route('/b', handlerB)]) + const response = await testServerManager.loadAndCall(routerHandler, '/b') - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(join(url, 'b')) - - expect(response).toMatchObject({ - statusCode: 200, - body: 'b', - }) + expect(response).toMatchObject({ + statusCode: 200, + body: 'b', }) }) it('routes with method', async () => { - expect.hasAssertions() - const handlerA = Handler([], () => { return Result('a') }) @@ -44,34 +43,28 @@ describe('router', () => { return Result('b') }) - const routerHandler = router([ + const routerHandler = Router([ Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB), ]) + testServerManager.load(routerHandler) - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(url) + const response1 = await testServerManager.call('/', { method: 'get' }) - expect(response).toMatchObject({ - statusCode: 200, - body: 'a', - }) + expect(response1).toMatchObject({ + statusCode: 200, + body: 'a', }) - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(url, { - method: 'post', - }) + const response2 = await testServerManager.call('/', { method: 'post' }) - expect(response).toMatchObject({ - statusCode: 200, - body: 'b', - }) + expect(response2).toMatchObject({ + statusCode: 200, + body: 'b', }) }) it('resolve params', async () => { - expect.hasAssertions() const handlerA = Handler([], () => { return Result('a') }) @@ -79,18 +72,19 @@ describe('router', () => { return Result(id) }) - const routerHandler = router([ + const routerHandler = Router([ Route('/a', handlerA), Route('/b/:id', handlerB), ]) - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(join(url, 'b/test-param')) + const response = await testServerManager.loadAndCall( + routerHandler, + '/b/test-param', + ) - expect(response).toMatchObject({ - statusCode: 200, - body: 'test-param', - }) + expect(response).toMatchObject({ + statusCode: 200, + body: 'test-param', }) }) @@ -103,20 +97,19 @@ describe('router', () => { return Result(notId) }) - const routerHandler = router([ + const routerHandler = Router([ Route('/a', handlerA), Route('/b/:id', handlerB), ]) - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(join(url, 'b/test-param'), { - method: 'GET', - }) + const response = await testServerManager.loadAndCall( + routerHandler, + '/b/test-param', + ) - expect(response).toMatchObject({ - statusCode: 200, - body: '', - }) + expect(response).toMatchObject({ + statusCode: 200, + body: '', }) }) @@ -129,20 +122,18 @@ describe('router', () => { return Result('b') }) - const routerHandler = router([ + const routerHandler = Router([ Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB), ]) - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(url, { - method: 'PUT', - }) + const response = await testServerManager.loadAndCall(routerHandler, '/', { + method: 'put', + }) - expect(response).toMatchObject({ - statusCode: 404, - body: expect.stringContaining('Error: Not Found'), - }) + expect(response).toMatchObject({ + statusCode: 404, + body: expect.stringContaining('Error: Not Found'), }) }) @@ -158,22 +149,19 @@ describe('router', () => { return Result('Error: Customized Not Found Response', 404) }) - const routerHandler = router( + const routerHandler = Router( [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], { notFoundHandler: customNotFoundHandler, }, ) - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(url, { - method: 'PUT', - }) - - expect(response).toMatchObject({ - statusCode: 404, - body: expect.stringContaining('Error: Customized Not Found Response'), - }) + const response = await testServerManager.loadAndCall(routerHandler, '/', { + method: 'put', + }) + expect(response).toMatchObject({ + statusCode: 404, + body: expect.stringContaining('Error: Customized Not Found Response'), }) }) @@ -186,22 +174,21 @@ describe('router', () => { return Result('b') }) - const routerHandler = router( + const routerHandler = Router( [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], { prefix: '/admin', }, ) - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(join(url, 'admin'), { - method: 'GET', - }) + const response = await testServerManager.loadAndCall( + routerHandler, + '/admin', + ) - expect(response).toMatchObject({ - statusCode: 200, - body: expect.stringContaining('a'), - }) + expect(response).toMatchObject({ + statusCode: 200, + body: expect.stringContaining('a'), }) }) @@ -214,22 +201,21 @@ describe('router', () => { return Result('b') }) - const routerHandler = router( + const routerHandler = Router( [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], { prefix: '/admin', }, ) - await testHandler(prismy(routerHandler), async (url) => { - const response = await testFetch(join(url, 'admin'), { - method: 'GET', - }) + const response = await testServerManager.loadAndCall( + routerHandler, + '/admin', + ) - expect(response).toMatchObject({ - statusCode: 200, - body: expect.stringContaining('a'), - }) + expect(response).toMatchObject({ + statusCode: 200, + body: expect.stringContaining('a'), }) }) @@ -246,7 +232,7 @@ describe('router', () => { return Result('b') }) - const routerHandler = router( + const routerHandler = Router( [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], { middleware: [ @@ -264,10 +250,8 @@ describe('router', () => { }, ) - await testHandler(prismy(routerHandler), async (url) => { - const response = await fetch(url) - expect(response.status).toBe(200) - expect(await response.text()).toBe('ba') - }) + const response = await testServerManager.loadAndCall(routerHandler) + + expect(response.statusCode).toBe(200) }) }) From 323c07b1f1a2e5dbe09135d8b4838ebe5b9bc520 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 15:57:10 +0900 Subject: [PATCH 032/109] Use node 18 types --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee15cf3..920d1e6 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@types/content-type": "^1.1.3", "@types/jest": "^29.5.11", - "@types/node": "^20.10.6", + "@types/node": "^18.19.5", "@types/node-fetch": "^2.6.10", "@types/test-listen": "^1.1.0", "async-listen": "^3.0.1", From e3e3998e8d066389ce3c8038dc60771a19f06880 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 16:16:04 +0900 Subject: [PATCH 033/109] Upgrade tslib --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 920d1e6..f6b4a3b 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,6 @@ "is-stream": "^2.0.0", "path-to-regexp": "^6.2.0", "raw-body": "^2.4.1", - "tslib": "^2.0.3" + "tslib": "^2.6.2" } } From 705496f82c627c5d5f7c1711047b82667b9d3c41 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 16:18:59 +0900 Subject: [PATCH 034/109] Update keyword --- package.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index f6b4a3b..ecfacc8 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,13 @@ "version": "3.0.0", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ - "micro", - "service", - "microservice", - "serverless", - "API", - "now" + "http", + "server", + "web", + "type", + "type-safe", + "lightweight", + "fast" ], "author": "Junyoung Choi ", "homepage": "https://github.com/prismyland/prismy", From ba7a2590a0a8a93cb1ce9c9871a28f20449f7d69 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 16:44:44 +0900 Subject: [PATCH 035/109] Reimplement type tests --- specs/types/{middleware.ts => basic.ts} | 46 +++++++++++++++---------- specs/types/handler.ts | 18 ---------- specs/types/reaemd.md | 3 ++ v4-todo.md | 17 ++++++--- 4 files changed, 42 insertions(+), 42 deletions(-) rename specs/types/{middleware.ts => basic.ts} (52%) delete mode 100644 specs/types/handler.ts create mode 100644 specs/types/reaemd.md diff --git a/specs/types/middleware.ts b/specs/types/basic.ts similarity index 52% rename from specs/types/middleware.ts rename to specs/types/basic.ts index 5e51c5b..183b19d 100644 --- a/specs/types/middleware.ts +++ b/specs/types/basic.ts @@ -1,14 +1,36 @@ import { - Middleware, - urlSelector, + BodySelector, + Handler, methodSelector, - ResponseObject, + Middleware, prismy, + PrismyHandler, + PrismyNextFunction, + ResponseObject, Result, + urlSelector, } from '../../src' -import { URL } from 'url' import { expectType } from '../helpers' +const handler1 = Handler([urlSelector, methodSelector], (url, method) => { + expectType(url) + expectType(method) + return Result('') +}) + +expectType< + ( + url: URL, + method: string | undefined, + url2: URL, + ) => ResponseObject | Promise> +>(handler1.handler) + +expectType(Handler([BodySelector()], () => Result(null))) + +// @ts-expect-error +expectError(Handler([BodySelector], () => Result(null))) + const middleware1 = Middleware( [urlSelector, methodSelector], (next) => async (url, method) => { @@ -19,21 +41,7 @@ const middleware1 = Middleware( ) expectType< - ( - next: () => Promise>, - ) => ( - url: URL, - method: string | undefined, - ) => ResponseObject | Promise> ->(middleware1.handler) - -expectType< - ( - next: () => Promise>, - ) => ( - url: URL, - method: string | undefined, - ) => ResponseObject | Promise> + (next: PrismyNextFunction) => (url: URL, method: string | undefined) => any >(middleware1.handler) prismy([], () => Result(''), [middleware1]) diff --git a/specs/types/handler.ts b/specs/types/handler.ts deleted file mode 100644 index 36badb3..0000000 --- a/specs/types/handler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { urlSelector, methodSelector, Result, ResponseObject } from '../../src' -import { URL } from 'url' -import { expectType } from '../helpers' -import { Handler } from '../../src/handler' - -const handler1 = Handler([urlSelector, methodSelector], (url, method) => { - expectType(url) - expectType(method) - return Result('') -}) - -expectType< - ( - url: URL, - method: string | undefined, - url2: URL, - ) => ResponseObject | Promise> ->(handler1.handler) diff --git a/specs/types/reaemd.md b/specs/types/reaemd.md new file mode 100644 index 0000000..b0e32d6 --- /dev/null +++ b/specs/types/reaemd.md @@ -0,0 +1,3 @@ +# Don't run script in this directoroy. + +`.ts` files in this folders are just for testing types. diff --git a/v4-todo.md b/v4-todo.md index 6a82076..43bcbc9 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -2,7 +2,7 @@ - Legacy support: Provide snippets which can provide compatibility - [x] Improve server test(Replace legacy got with node-fetch@2) - server listner replacer? -- Introduce tsd to test types properly +- [x] Reimplement type test ~~Introduce tsd to test types properly~~ - [x] Seperate Handler and Prismy ```ts import http from 'http' @@ -23,8 +23,10 @@ - combineRouters should take NotFoundHandler - [ ] Add tests - [ ] Wildcard parm handling - - [ ] Router middleware test + - [x] Router middleware test - [ ] Replace res with `PrismyResult` and `Result()` + - [x] Support PrismyResult + - [ ] Discard res, res obj - [x] Redesigned selector interface - [x] Renamed factory method (ex: createBodySelector(Deprecated) => BodySelector) ```ts @@ -85,11 +87,16 @@ ```ts (next: () => Promise) => Promise ``` -- [ ] Make middleware into a class +- [x] Make middleware into a class - [ ] Return without res - [ ] Include prismy-cookie - [ ] Added DI Selector +# V5 TODO(TBD) + +- ESM support + - Replace jest with node-tap, mocha or ava + # Fix router # Goal @@ -100,7 +107,7 @@ const serverHandler = Router([ Route(routeInfo, [selector], handler), Route(routeInfo, prismyHandler), Route(['/deprecated', '*'], ()=> redirect('/')), - NotFoundRoute(() => res('Not Found', 404)) + NotFoundRoute(() => Result('Not Found', 404)) ], { prefix: '...' middleware: [...], @@ -163,7 +170,7 @@ const middleware = Middleware([], next => async () => { const result = doSomethingBefore() if (isBad(result)) { // Skip running `next` handler - return res() + return Result() } const result = await next() From 6f461dfe3495a626ca6b5c2ef0de0a21b4405355 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 17:01:50 +0900 Subject: [PATCH 036/109] Improve type tests --- specs/types/basic.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 183b19d..5cf9e57 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -11,6 +11,7 @@ import { urlSelector, } from '../../src' import { expectType } from '../helpers' +import http from 'http' const handler1 = Handler([urlSelector, methodSelector], (url, method) => { expectType(url) @@ -28,8 +29,10 @@ expectType< expectType(Handler([BodySelector()], () => Result(null))) +http.createServer(prismy(handler1)) + // @ts-expect-error -expectError(Handler([BodySelector], () => Result(null))) +Handler([BodySelector], () => Result(null)) const middleware1 = Middleware( [urlSelector, methodSelector], @@ -44,4 +47,7 @@ expectType< (next: PrismyNextFunction) => (url: URL, method: string | undefined) => any >(middleware1.handler) -prismy([], () => Result(''), [middleware1]) +// @ts-expect-error +Middleware([BodySelector], () => () => Result(null)) + +http.createServer(prismy([], () => Result(''), [middleware1])) From 2a4b1cdefee30c5c2994a9e11326d5aa2a0a7f91 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 17:02:10 +0900 Subject: [PATCH 037/109] Update v4 todo --- v4-todo.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/v4-todo.md b/v4-todo.md index 43bcbc9..9f37350 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -15,12 +15,10 @@ http.createServer(requestListener).listen() ``` +- [ ] Update docs +- [ ] Shorthand Prismy, Route - [x] redesigned router interface - [x] introduced route method - - [ ] Add combine router to put routers together - - [ ] NotFoundHandler must be ignored when routers are combined. - - Should roll back NotFoundHandler - - combineRouters should take NotFoundHandler - [ ] Add tests - [ ] Wildcard parm handling - [x] Router middleware test @@ -74,7 +72,6 @@ - [x] Adopted async local storage to communicate between selectors, middleware and handlers - [x] Added `getPrismyContext` method to get context. (must be used in the scope of selectors, middleware and handlers) - [x] Removed `contextSelector`, use `getPrismyContext` -- [ ] Add `createConcurrentSelector(...selectors: PrismySelector[])` - [x] Simplified middleware interface - Before @@ -88,7 +85,6 @@ (next: () => Promise) => Promise ``` - [x] Make middleware into a class -- [ ] Return without res - [ ] Include prismy-cookie - [ ] Added DI Selector @@ -96,6 +92,12 @@ - ESM support - Replace jest with node-tap, mocha or ava +- Add `createConcurrentSelector(...selectors: PrismySelector[])` + +# Idea, Might not be worth to do. +- Return without res +- Add combine router to put routers together + - notFoundHandler must be ignored when routers are combined. # Fix router From 22a8294d59d92ac653b0dbd67c07007934280c4d Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 21:35:32 +0900 Subject: [PATCH 038/109] Fix route parm selector to select string only --- specs/router.spec.ts | 301 ++++++++++++++++++++++++++++++++++++------- src/router.ts | 4 +- v4-todo.md | 7 +- 3 files changed, 259 insertions(+), 53 deletions(-) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 018bd0b..c1dfaaf 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -1,5 +1,5 @@ import { - routeParamSelector, + RouteParamSelector, Result, Router, Route, @@ -64,55 +64,6 @@ describe('router', () => { }) }) - it('resolve params', async () => { - const handlerA = Handler([], () => { - return Result('a') - }) - const handlerB = Handler([routeParamSelector('id')], (id) => { - return Result(id) - }) - - const routerHandler = Router([ - Route('/a', handlerA), - Route('/b/:id', handlerB), - ]) - - const response = await testServerManager.loadAndCall( - routerHandler, - '/b/test-param', - ) - - expect(response).toMatchObject({ - statusCode: 200, - body: 'test-param', - }) - }) - - it('resolves null if param is missing', async () => { - expect.hasAssertions() - const handlerA = Handler([], () => { - return Result('a') - }) - const handlerB = Handler([routeParamSelector('not-id')], (notId) => { - return Result(notId) - }) - - const routerHandler = Router([ - Route('/a', handlerA), - Route('/b/:id', handlerB), - ]) - - const response = await testServerManager.loadAndCall( - routerHandler, - '/b/test-param', - ) - - expect(response).toMatchObject({ - statusCode: 200, - body: '', - }) - }) - it('throws 404 error when no route found', async () => { expect.hasAssertions() const handlerA = Handler([], () => { @@ -255,3 +206,253 @@ describe('router', () => { expect(response.statusCode).toBe(200) }) }) + +describe('RouteParamSelector', () => { + it('resolves null if the param is missing', async () => { + expect.hasAssertions() + const handlerA = Handler([], () => { + return Result('a') + }) + const handlerB = Handler([RouteParamSelector('not-id')], (notId) => { + return Result(notId) + }) + + const routerHandler = Router([ + Route('/a', handlerA), + Route('/b/:id', handlerB), + ]) + + const response = await testServerManager.loadAndCall( + routerHandler, + '/b/test-param', + ) + + expect(response).toMatchObject({ + statusCode: 200, + body: '', + }) + }) + + it('resolves a param (named parameter)', async () => { + const handlerA = Handler([], () => { + return Result('a') + }) + const handlerB = Handler([RouteParamSelector('id')], (id) => { + return Result(id) + }) + + const routerHandler = Router([ + Route('/a', handlerA), + Route('/b/:id', handlerB), + ]) + + const response = await testServerManager.loadAndCall( + routerHandler, + '/b/test-param', + ) + + expect(response).toMatchObject({ + statusCode: 200, + body: 'test-param', + }) + }) + + it('resolves params (custom suffix)', async () => { + const handlerA = Handler([], () => { + return Result('a') + }) + const handlerB = Handler( + [ + RouteParamSelector('attr1'), + RouteParamSelector('attr2'), + RouteParamSelector('attr3'), + ], + (attr1, attr2, attr3) => { + return Result({ + attr1, + attr2, + attr3, + }) + }, + ) + + const routerHandler = Router([ + Route('/a', handlerA), + Route('/b/:attr1?{-:attr2}?{-:attr3}?', handlerB), + ]) + + const response1 = await testServerManager.loadAndCall( + routerHandler, + '/b/test1-test2-test3', + ) + + expect(response1).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response1.body)).toMatchObject({ + attr1: 'test1', + attr2: 'test2', + attr3: 'test3', + }) + + const response2 = await testServerManager.loadAndCall( + routerHandler, + '/b/test1-test2', + ) + expect(response2).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response2.body)).toMatchObject({ + attr1: 'test1', + attr2: 'test2', + attr3: null, + }) + }) + + it('resolves a param (unnamed parameter)', async () => { + const handlerA = Handler([], () => { + return Result('a') + }) + const handlerB = Handler( + [RouteParamSelector('id'), RouteParamSelector('0')], + (id, unnamedParam) => { + return Result({ + id, + unnamedParam, + }) + }, + ) + + const routerHandler = Router([ + Route('/a', handlerA), + Route('/b/:id/(.*)', handlerB), + ]) + + const response = await testServerManager.loadAndCall( + routerHandler, + '/b/test1/test2/test3', + ) + + expect(response).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response.body)).toMatchObject({ + id: 'test1', + unnamedParam: 'test2/test3', + }) + }) + + it('resolves a param (optional)', async () => { + const handlerA = Handler([], () => { + return Result('a') + }) + const handlerB = Handler( + [RouteParamSelector('param1'), RouteParamSelector('param2')], + (param1, param2) => { + return Result({ + param1, + param2, + }) + }, + ) + + const routerHandler = Router([ + Route('/a', handlerA), + Route('/b/:param1/:param2?', handlerB), + ]) + + const response1 = await testServerManager.loadAndCall( + routerHandler, + '/b/test1/test2', + ) + + expect(response1).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response1.body)).toMatchObject({ + param1: 'test1', + param2: 'test2', + }) + + const response2 = await testServerManager.call('/b/test1') + + expect(response2).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response2.body)).toMatchObject({ + param1: 'test1', + param2: null, + }) + }) + + it('resolves the first param only (zero or more)', async () => { + const handlerA = Handler([], () => { + return Result('a') + }) + const handlerB = Handler([RouteParamSelector('param')], (param) => { + return Result({ + param, + }) + }) + + const routerHandler = Router([ + Route('/a', handlerA), + Route('/b/:param*', handlerB), + ]) + + const response1 = await testServerManager.loadAndCall( + routerHandler, + '/b/test1/test2', + ) + + expect(response1).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response1.body)).toMatchObject({ + param: 'test1', + }) + + const response2 = await testServerManager.loadAndCall(routerHandler, '/b') + + expect(response2).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response2.body)).toMatchObject({ + param: null, + }) + }) + + it('resolves the first param only (one or more)', async () => { + const handlerA = Handler([], () => { + return Result('a') + }) + const handlerB = Handler([RouteParamSelector('param')], (param) => { + return Result({ + param, + }) + }) + + const routerHandler = Router([ + Route('/a', handlerA), + Route('/b/:param+', handlerB), + ]) + + const response1 = await testServerManager.loadAndCall( + routerHandler, + '/b/test1/test2', + ) + + expect(response1).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response1.body)).toMatchObject({ + param: 'test1', + }) + + const response2 = await testServerManager.loadAndCall(routerHandler, '/b') + + expect(response2).toMatchObject({ + statusCode: 404, + }) + }) +}) diff --git a/src/router.ts b/src/router.ts index fcacb6d..b746c24 100644 --- a/src/router.ts +++ b/src/router.ts @@ -112,13 +112,13 @@ function getRouteParamsFromPrismyContext(context: PrismyContext) { return routeParamsMap.get(context) } -export function routeParamSelector( +export function RouteParamSelector( paramName: string, ): PrismySelector { return createPrismySelector(() => { const context = getPrismyContext() const param = getRouteParamsFromPrismyContext(context)[paramName] - return param != null ? param : null + return param != null ? (Array.isArray(param) ? param[0] : param) : null }) } diff --git a/v4-todo.md b/v4-todo.md index 9f37350..96e7fb0 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -16,6 +16,7 @@ ``` - [ ] Update docs +- [ ] Update examples - [ ] Shorthand Prismy, Route - [x] redesigned router interface - [x] introduced route method @@ -87,6 +88,9 @@ - [x] Make middleware into a class - [ ] Include prismy-cookie - [ ] Added DI Selector +- [ ] File uploading +- [ ] Rewrite prismy-session + - [ ] Make it compatible with SessionStore of express-session # V5 TODO(TBD) @@ -94,7 +98,8 @@ - Replace jest with node-tap, mocha or ava - Add `createConcurrentSelector(...selectors: PrismySelector[])` -# Idea, Might not be worth to do. +# ight not be worth to do. + - Return without res - Add combine router to put routers together - notFoundHandler must be ignored when routers are combined. From 11e4aa9dfeb3c82441bd69d18ba76eb6ec434751 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 22:10:02 +0900 Subject: [PATCH 039/109] Remove ResObject --- specs/send.spec.ts | 70 ++++++++++++-------------------------------- specs/types/basic.ts | 4 +-- src/handler.ts | 11 +++---- src/middleware.ts | 9 +++--- src/prismy.ts | 22 +++----------- src/send.ts | 12 ++++---- src/types.ts | 23 ++------------- 7 files changed, 41 insertions(+), 110 deletions(-) diff --git a/specs/send.spec.ts b/specs/send.spec.ts index e99a17f..42b5297 100644 --- a/specs/send.spec.ts +++ b/specs/send.spec.ts @@ -1,7 +1,8 @@ import got from 'got' import { IncomingMessage, RequestListener, ServerResponse } from 'http' import { Readable } from 'stream' -import { send } from '../src/send' +import { Result } from '../src' +import { sendPrismyResult } from '../src/send' import { testHandler } from './helpers' describe('send', () => { @@ -9,7 +10,7 @@ describe('send', () => { expect.hasAssertions() const handler: RequestListener = (req, res) => { - send(req, res, {}) + sendPrismyResult(req, res, Result(null)) } await testHandler(handler, async (url) => { @@ -22,7 +23,7 @@ describe('send', () => { expect.hasAssertions() const handler: RequestListener = (req, res) => { - send(req, res, { body: 'test' }) + sendPrismyResult(req, res, Result('test')) } await testHandler(handler, async (url) => { @@ -38,7 +39,7 @@ describe('send', () => { const handler: RequestListener = (req, res) => { res.setHeader('Content-Type', 'application/octet-stream') const statusCode = res.statusCode - send(req, res, { statusCode, body: targetBuffer }) + sendPrismyResult(req, res, Result(targetBuffer, statusCode)) } await testHandler(handler, async (url) => { @@ -62,10 +63,7 @@ describe('send', () => { const targetBuffer = Buffer.from('Hello, world!') const handler: RequestListener = (req, res) => { const statusCode = res.statusCode - send(req, res, { - statusCode, - body: targetBuffer, - }) + sendPrismyResult(req, res, Result(targetBuffer, statusCode)) } await testHandler(handler, async (url) => { @@ -92,10 +90,7 @@ describe('send', () => { const handler: RequestListener = (req, res) => { res.setHeader('Content-Type', 'application/octet-stream') const statusCode = res.statusCode - send(req, res, { - statusCode, - body: stream, - }) + sendPrismyResult(req, res, Result(stream, statusCode)) } await testHandler(handler, async (url) => { @@ -115,7 +110,7 @@ describe('send', () => { response.end('test') } const handler: RequestListener = (req, res) => { - send(req, res, sendHandler) + sendPrismyResult(req, res, Result(sendHandler)) } await testHandler(handler, async (url) => { @@ -131,7 +126,7 @@ describe('send', () => { const stream = Readable.from(targetBuffer.toString()) const handler: RequestListener = (req, res) => { const statusCode = res.statusCode - send(req, res, { statusCode, body: stream }) + sendPrismyResult(req, res, Result(stream, statusCode)) } await testHandler(handler, async (url) => { @@ -154,8 +149,7 @@ describe('send', () => { } const handler: RequestListener = (req, res) => { res.setHeader('Content-Type', 'application/json; charset=utf-8') - const statusCode = res.statusCode - send(req, res, { statusCode, body: target }) + sendPrismyResult(req, res, Result(target)) } await testHandler(handler, async (url) => { @@ -176,8 +170,7 @@ describe('send', () => { foo: 'bar', } const handler: RequestListener = (req, res) => { - const statusCode = res.statusCode - send(req, res, { statusCode, body: target }) + sendPrismyResult(req, res, Result(target)) } await testHandler(handler, async (url) => { @@ -200,11 +193,7 @@ describe('send', () => { const target = 1004 const handler: RequestListener = (req, res) => { res.setHeader('Content-Type', 'application/json; charset=utf-8') - const statusCode = res.statusCode - send(req, res, { - statusCode, - body: target, - }) + sendPrismyResult(req, res, Result(target)) } await testHandler(handler, async (url) => { @@ -222,11 +211,7 @@ describe('send', () => { const target = 1004 const handler: RequestListener = (req, res) => { - const statusCode = res.statusCode - send(req, res, { - statusCode, - body: target, - }) + sendPrismyResult(req, res, Result(target)) } await testHandler(handler, async (url) => { @@ -246,30 +231,13 @@ describe('send', () => { expect.hasAssertions() const handler: RequestListener = (req, res) => { - const statusCode = res.statusCode - send(req, res, { - statusCode, - headers: { + sendPrismyResult( + req, + res, + Result(null, 200, { test: 'test value', - }, - }) - } - - await testHandler(handler, async (url) => { - const response = await got(url) - expect(response.body).toBeFalsy() - expect(response.headers['test']).toEqual('test value') - }) - }) - it('sends with header', async () => { - expect.hasAssertions() - - const handler: RequestListener = (req, res) => { - send(req, res, { - headers: { - test: 'test value', - }, - }) + }), + ) } await testHandler(handler, async (url) => { diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 5cf9e57..ab7c084 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -6,7 +6,7 @@ import { prismy, PrismyHandler, PrismyNextFunction, - ResponseObject, + PrismyResult, Result, urlSelector, } from '../../src' @@ -24,7 +24,7 @@ expectType< url: URL, method: string | undefined, url2: URL, - ) => ResponseObject | Promise> + ) => PrismyResult | Promise >(handler1.handler) expectType(Handler([BodySelector()], () => Result(null))) diff --git a/src/handler.ts b/src/handler.ts index 56be7b7..b82a7ca 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -3,9 +3,8 @@ import { PrismyMiddleware, PrismyNextFunction, MaybePromise, - ResponseObject, SelectorReturnTypeTuple, - PrismySendResult, + PrismyResult, } from '.' import { PrismySelector } from './selectors/createSelector' import { compileHandler } from './utils' @@ -21,11 +20,11 @@ export class PrismyHandler< */ public handler: ( ...args: SelectorReturnTypeTuple - ) => MaybePromise>, + ) => MaybePromise, public middlewareList: PrismyMiddleware[], ) {} - async handle(): Promise | PrismySendResult> { + async handle(): Promise { const next: PrismyNextFunction = compileHandler( this.selectors, this.handler, @@ -74,9 +73,7 @@ export class PrismyHandler< */ export function Handler[]>( selectors: [...S], - handler: ( - ...args: SelectorReturnTypeTuple - ) => MaybePromise>, + handler: (...args: SelectorReturnTypeTuple) => MaybePromise, middlewareList: PrismyMiddleware[]>[] = [], ) { return new PrismyHandler(selectors, handler, middlewareList) diff --git a/src/middleware.ts b/src/middleware.ts index df43b12..ff9f328 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,6 @@ -import { PrismyNextFunction } from '.' +import { PrismyNextFunction, PrismyResult } from '.' import { PrismySelector } from './selectors/createSelector' -import { ResponseObject, SelectorReturnTypeTuple } from './types' +import { SelectorReturnTypeTuple } from './types' import { compileHandler } from './utils' export class PrismyMiddleware< @@ -14,7 +14,7 @@ export class PrismyMiddleware< */ public handler: ( next: PrismyNextFunction, - ) => (...args: SelectorReturnTypeTuple) => Promise>, + ) => (...args: SelectorReturnTypeTuple) => Promise, ) {} pipe(next: PrismyNextFunction) { @@ -37,7 +37,6 @@ export class PrismyMiddleware< * 'access-control-allow-origin': '*' * }) * }) - * * ``` * * @remarks @@ -61,7 +60,7 @@ export function Middleware[]>( selectors: [...SS], handler: ( next: PrismyNextFunction, - ) => (...args: SelectorReturnTypeTuple) => Promise>, + ) => (...args: SelectorReturnTypeTuple) => Promise, ): PrismyMiddleware { return new PrismyMiddleware(selectors, handler) } diff --git a/src/prismy.ts b/src/prismy.ts index c282e87..3c267fe 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -3,13 +3,7 @@ import { IncomingMessage, RequestListener, ServerResponse } from 'http' import { PrismyMiddleware } from './middleware' import { Handler, PrismyHandler } from './handler' import { PrismySelector } from './selectors/createSelector' -import { send } from './send' -import { - ResponseObject, - MaybePromise, - PrismyContext, - SelectorReturnTypeTuple, -} from './types' +import { MaybePromise, PrismyContext, SelectorReturnTypeTuple } from './types' import { PrismyResult } from './res' export const prismyContextStorage = new AsyncLocalStorage() @@ -50,16 +44,12 @@ export function prismy[]>( */ export function prismy[]>( selectors: [...S], - handler: ( - ...args: SelectorReturnTypeTuple - ) => MaybePromise>, + handler: (...args: SelectorReturnTypeTuple) => MaybePromise, middlewareList?: PrismyMiddleware[]>[], ): RequestListener export function prismy[]>( selectorsOrPrismyHandler: [...S] | PrismyHandler, - handler?: ( - ...args: SelectorReturnTypeTuple - ) => MaybePromise>, + handler?: (...args: SelectorReturnTypeTuple) => MaybePromise, middlewareList?: PrismyMiddleware[]>[], ): RequestListener { const injectedHandler = @@ -77,11 +67,7 @@ export function prismy[]>( prismyContextStorage.run(context, async () => { const resObject = await injectedHandler.handle() - if (resObject instanceof PrismyResult) { - resObject.resolve(request, response) - return - } - send(request, response, resObject) + resObject.resolve(request, response) }) } diff --git a/src/send.ts b/src/send.ts index 5117bcd..5f259c2 100644 --- a/src/send.ts +++ b/src/send.ts @@ -1,7 +1,7 @@ import { IncomingMessage, ServerResponse } from 'http' import { readable } from 'is-stream' import { Stream } from 'stream' -import { ResponseObject } from './types' +import { PrismyResult } from './res' /** * Function to send data to the client @@ -12,15 +12,13 @@ import { ResponseObject } from './types' * * @public */ -export const send = ( +export const sendPrismyResult = ( request: IncomingMessage, response: ServerResponse, - sendable: - | ((request: IncomingMessage, response: ServerResponse) => void) - | ResponseObject, + sendable: PrismyResult, ) => { - if (typeof sendable === 'function') { - sendable(request, response) + if (typeof sendable.body === 'function') { + sendable.body(request, response) return } const { statusCode = 200, body, headers = [] } = sendable diff --git a/src/types.ts b/src/types.ts index 3ad29f0..ccb35d0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ -import { IncomingMessage, OutgoingHttpHeaders } from 'http' +import { IncomingMessage } from 'http' +import { PrismyResult } from './res' import { PrismySelector } from './selectors/createSelector' /** @@ -51,25 +52,7 @@ export type PromiseResolve = T extends Promise ? U : T */ export type MaybePromise = T | Promise -/** - * prismy's representation of a response - * - * @public - */ -export interface ResponseObject { - body?: B - statusCode?: number - headers?: OutgoingHttpHeaders -} - -/** - * shorter type alias for ResponseObject - * - * @public - */ -export type Res = ResponseObject - -export type PrismyNextFunction = () => Promise> +export type PrismyNextFunction = () => Promise> /** * @public From 78854f5819a6b05c8fe095be002d8f93e6723768 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 22:11:30 +0900 Subject: [PATCH 040/109] Remove raw result --- specs/result.spec.ts | 6 ++-- src/res.ts | 69 +++++++++++++------------------------------- 2 files changed, 22 insertions(+), 53 deletions(-) diff --git a/specs/result.spec.ts b/specs/result.spec.ts index a463a76..42b2951 100644 --- a/specs/result.spec.ts +++ b/specs/result.spec.ts @@ -9,12 +9,10 @@ afterAll(async () => { await testServerManager.close() }) -// TODO: Implement tests -describe('PrismyResult', () => {}) - +// TODO: Implement Error Result tests describe('ErrorResult', () => {}) -describe('PrismySendResult', () => { +describe('PrismyResult', () => { describe('#setStatusCode', () => { it('sets status code', async () => { const handler = Handler([], () => diff --git a/src/res.ts b/src/res.ts index a63d95c..9fe0349 100644 --- a/src/res.ts +++ b/src/res.ts @@ -1,37 +1,12 @@ -import { - IncomingMessage, - OutgoingHttpHeaders, - RequestListener, - ServerResponse, -} from 'http' -import { send } from './send' -import { ResponseObject } from './types' +import { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'http' +import { sendPrismyResult } from './send' -export class PrismyResult { - constructor(public resolver: RequestListener) {} -} - -/** - * Create a raw result it will directly handle node.js's requests and responses. - * It is useful when controlling raw request stream and raw response stream. - * - * @param resolver - * @returns {@link PrismyResult} - */ -export function RawResult(resolver: RequestListener): PrismyResult { - return new PrismyResult(resolver) -} - -export class PrismySendResult extends PrismyResult { +export class PrismyResult { constructor( public readonly body: B, public readonly statusCode: number, public readonly headers: OutgoingHttpHeaders, - ) { - super((request: IncomingMessage, response: ServerResponse) => { - this.resolve(request, response) - }) - } + ) {} /** * Resolve function used by http.Server @@ -39,35 +14,31 @@ export class PrismySendResult extends PrismyResult { * @param response */ resolve(request: IncomingMessage, response: ServerResponse) { - send(request, response, { - body: this.body, - statusCode: this.statusCode, - headers: this.headers, - }) + sendPrismyResult(request, response, this) } /** * Creates a new result with a new status code * * @param statusCode - HTTP status code - * @returns New {@link PrismySendResult} + * @returns New {@link PrismyResult} * * @public */ setStatusCode(statusCode: number) { - return new PrismySendResult(this.body, statusCode, this.headers) + return new PrismyResult(this.body, statusCode, this.headers) } /** * Creates a new result with a new body * * @param body - Body to be set - * @returns New {@link PrismySendResult} + * @returns New {@link PrismyResult} * * @public */ setBody(body: BB) { - return new PrismySendResult(body, this.statusCode, this.headers) + return new PrismyResult(body, this.statusCode, this.headers) } /** @@ -76,7 +47,7 @@ export class PrismySendResult extends PrismyResult { * `{...existingHeaders, ...newHeaders}` * * @param newHeaders - HTTP response headers - * @returns New {@link PrismySendResult} + * @returns New {@link PrismyResult} * * To set multiple headers with same name, use an array. * @example @@ -100,7 +71,7 @@ export class PrismySendResult extends PrismyResult { * @public */ updateHeaders(newHeaders: OutgoingHttpHeaders) { - return new PrismySendResult(this.body, this.statusCode, { + return new PrismyResult(this.body, this.statusCode, { ...this.headers, ...newHeaders, }) @@ -111,12 +82,12 @@ export class PrismySendResult extends PrismyResult { * This will flush all existing headers and set new ones only. * * @param newHeaders - HTTP response headers - * @returns New {@link PrismySendResult} + * @returns New {@link PrismyResult} * * @public */ setHeaders(headers: OutgoingHttpHeaders) { - return new PrismySendResult(this.body, this.statusCode, headers) + return new PrismyResult(this.body, this.statusCode, headers) } } @@ -126,7 +97,7 @@ export class PrismySendResult extends PrismyResult { * @param body - Body of the response * @param statusCode - HTTP status code of the response * @param headers - HTTP headers for the response - * @returns A {@link PrismySendResult} containing necessary information + * @returns A {@link PrismyResult} containing necessary information * * @public */ @@ -134,8 +105,8 @@ export function Result( body: B, statusCode: number = 200, headers: OutgoingHttpHeaders = {}, -): PrismySendResult { - return new PrismySendResult(body, statusCode, headers) +): PrismyResult { + return new PrismyResult(body, statusCode, headers) } /** @@ -145,7 +116,7 @@ export function Result( * @param body - Body of the response * @param statusCode - HTTP status code of the response * @param headers - HTTP headers for the response - * @returns A {@link PrismySendResult} containing necessary information + * @returns A {@link PrismyResult} containing necessary information * * @public */ @@ -153,8 +124,8 @@ export function ErrorResult( statusCode: number, body: B, headers: OutgoingHttpHeaders = {}, -): PrismySendResult { - return new PrismySendResult(body, statusCode, headers) +): PrismyResult { + return new PrismyResult(body, statusCode, headers) } /** @@ -171,7 +142,7 @@ export function Redirect( location: string, statusCode: number = 302, extraHeaders: OutgoingHttpHeaders = {}, -): ResponseObject { +): PrismyResult { return Result(null, statusCode, { location, ...extraHeaders, From 7a4af6026f6d83042fd5d5bf958c0cc138e168c5 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 8 Jan 2024 23:20:59 +0900 Subject: [PATCH 041/109] Add InjectSelector --- specs/types/basic.ts | 17 +++++++++++++++++ src/selectors/inject.ts | 5 +++++ v4-todo.md | 14 +++++++------- 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/selectors/inject.ts diff --git a/specs/types/basic.ts b/specs/types/basic.ts index ab7c084..d2a4f0e 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -12,6 +12,7 @@ import { } from '../../src' import { expectType } from '../helpers' import http from 'http' +import { InjectSelector } from '../../src/selectors/injector' const handler1 = Handler([urlSelector, methodSelector], (url, method) => { expectType(url) @@ -51,3 +52,19 @@ expectType< Middleware([BodySelector], () => () => Result(null)) http.createServer(prismy([], () => Result(''), [middleware1])) + +abstract class MailService {} +class ProductionMailService extends MailService {} +class TestMailService extends MailService {} + +const mailService: MailService = + process.env.NODE_ENV === 'production' + ? new ProductionMailService() + : new TestMailService() + +const mailServiceSelector = InjectSelector(mailService) + +Handler([mailServiceSelector], (mailService) => { + expectType(mailService) + return Result(null) +}) diff --git a/src/selectors/inject.ts b/src/selectors/inject.ts new file mode 100644 index 0000000..7f9474a --- /dev/null +++ b/src/selectors/inject.ts @@ -0,0 +1,5 @@ +import { createPrismySelector, PrismySelector } from './createSelector' + +export function InjectSelector(value: V): PrismySelector { + return createPrismySelector(() => value) +} diff --git a/v4-todo.md b/v4-todo.md index 96e7fb0..51734ae 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -16,16 +16,16 @@ ``` - [ ] Update docs -- [ ] Update examples +- [ ] Update - [ ] Shorthand Prismy, Route - [x] redesigned router interface - - [x] introduced route method - - [ ] Add tests - - [ ] Wildcard parm handling + - [x] introduced route methodexamples + - [x] Add tests + - [x] Wildcard parm handling - [x] Router middleware test -- [ ] Replace res with `PrismyResult` and `Result()` +- [x] Replace res with `PrismyResult` and `Result()` - [x] Support PrismyResult - - [ ] Discard res, res obj + - [x] Discard res, res obj - [x] Redesigned selector interface - [x] Renamed factory method (ex: createBodySelector(Deprecated) => BodySelector) ```ts @@ -87,7 +87,7 @@ ``` - [x] Make middleware into a class - [ ] Include prismy-cookie -- [ ] Added DI Selector +- [x] Added DI Selector - [ ] File uploading - [ ] Rewrite prismy-session - [ ] Make it compatible with SessionStore of express-session From 996114761d827ac8b296430bfc4d8c962ff63dc0 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 9 Jan 2024 19:41:04 +0900 Subject: [PATCH 042/109] Implement cookie handling --- package.json | 2 ++ specs/result.spec.ts | 23 ++++++++++++++++++++++ specs/selectors/cookie.spec.ts | 35 ++++++++++++++++++++++++++++++++++ src/res.ts | 28 +++++++++++++++++++++++++++ src/selectors/cookie.ts | 23 ++++++++++++++++++++++ src/selectors/index.ts | 1 + v4-todo.md | 2 +- 7 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 specs/selectors/cookie.spec.ts create mode 100644 src/selectors/cookie.ts diff --git a/package.json b/package.json index ecfacc8..189ae93 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@types/content-type": "^1.1.3", + "@types/cookie": "^0.6.0", "@types/jest": "^29.5.11", "@types/node": "^18.19.5", "@types/node-fetch": "^2.6.10", @@ -59,6 +60,7 @@ }, "dependencies": { "content-type": "^1.0.4", + "cookie": "^0.6.0", "is-stream": "^2.0.0", "path-to-regexp": "^6.2.0", "raw-body": "^2.4.1", diff --git a/specs/result.spec.ts b/specs/result.spec.ts index 42b2951..9c5a7ad 100644 --- a/specs/result.spec.ts +++ b/specs/result.spec.ts @@ -135,4 +135,27 @@ describe('Redirect', () => { expect(response.headers.get('location')).toBe('https://github.com/') expect(response.headers.get('custom-header')).toBe('Hello!') }) + + it('sets cookies', async () => { + const handler = Handler([], () => + Result(null) + .setCookie('testCookie', 'testValue', { + secure: true, + domain: 'https://example.com', + }) + .setCookie('testCookie2', 'testValue2', { + httpOnly: true, + }), + ) + + const response = await testServerManager.loadAndCall(handler, '/') + + expect(response).toMatchObject({ + statusCode: 200, + }) + expect(response.headers.getSetCookie()).toEqual([ + 'testCookie=testValue; Domain=https://example.com; Secure', + 'testCookie2=testValue2; HttpOnly', + ]) + }) }) diff --git a/specs/selectors/cookie.spec.ts b/specs/selectors/cookie.spec.ts new file mode 100644 index 0000000..22c2e60 --- /dev/null +++ b/specs/selectors/cookie.spec.ts @@ -0,0 +1,35 @@ +import { testServerManager } from '../helpers' +import { CookieSelector, Handler, Result } from '../../src' + +beforeAll(async () => { + await testServerManager.start() +}) + +afterAll(async () => { + await testServerManager.close() +}) + +describe('CookieSelector', () => { + it('selects a cookie value', async () => { + const handler = Handler([CookieSelector('test')], (cookieValue) => { + return Result({ cookieValue }) + }) + + const response = await testServerManager.loadAndCall( + handler, + '/test?query=true', + { + headers: { + cookie: 'test=Hello!', + }, + }, + ) + + expect(response).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response.body)).toMatchObject({ + cookieValue: 'Hello!', + }) + }) +}) diff --git a/src/res.ts b/src/res.ts index 9fe0349..d30f318 100644 --- a/src/res.ts +++ b/src/res.ts @@ -1,5 +1,6 @@ import { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'http' import { sendPrismyResult } from './send' +import cookie from 'cookie' export class PrismyResult { constructor( @@ -77,6 +78,33 @@ export class PrismyResult { }) } + /** + * Appends `set-cookie` header. This method won't replace existing `set-cookie` header. + * To remove or replace existing headers, please use `updateHeaders` method. + * + * @param key Cookie key + * @param value Cookie value + * @param options Cookie options + * @returns New {@link PrismyResult} + */ + setCookie( + key: string, + value: string, + options?: cookie.CookieSerializeOptions, + ) { + const existingValue = this.headers['set-cookie'] + const newValue = cookie.serialize(key, value, options) + + return this.updateHeaders({ + 'set-cookie': + existingValue == null + ? [newValue] + : Array.isArray(existingValue) + ? [...existingValue, newValue] + : [existingValue, newValue], + }) + } + /** * Creates a new result with the new headers. * This will flush all existing headers and set new ones only. diff --git a/src/selectors/cookie.ts b/src/selectors/cookie.ts new file mode 100644 index 0000000..4e554c0 --- /dev/null +++ b/src/selectors/cookie.ts @@ -0,0 +1,23 @@ +import cookie from 'cookie' +import { getPrismyContext } from '../prismy' +import { createPrismySelector } from './createSelector' + +const cookieMap = new WeakMap() + +export function CookieSelector(key: string) { + return createPrismySelector((): string | null => { + const context = getPrismyContext() + let parsedCookie: { [key: string]: string | undefined } = + cookieMap.get(context) + if (parsedCookie == null) { + const { req } = context + parsedCookie = + req.headers['cookie'] != null ? cookie.parse(req.headers['cookie']) : {} + cookieMap.set(context, parsedCookie) + } + + const cookieValue = parsedCookie[key] + + return cookieValue != null ? cookieValue : null + }) +} diff --git a/src/selectors/index.ts b/src/selectors/index.ts index 704e8d9..072537a 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -8,3 +8,4 @@ export * from './searchParam' export * from './url' export * from './urlEncodedBody' export * from './textBody' +export * from './cookie' diff --git a/v4-todo.md b/v4-todo.md index 51734ae..209896a 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -86,7 +86,7 @@ (next: () => Promise) => Promise ``` - [x] Make middleware into a class -- [ ] Include prismy-cookie +- [x] Include prismy-cookie - [x] Added DI Selector - [ ] File uploading - [ ] Rewrite prismy-session From 582f0025786f87a8487595981d9f63e014764243 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 13 Jan 2024 11:22:17 +0900 Subject: [PATCH 043/109] Fix typo in jsonBody --- src/selectors/jsonBody.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/selectors/jsonBody.ts b/src/selectors/jsonBody.ts index 137b0d4..94bdd1e 100644 --- a/src/selectors/jsonBody.ts +++ b/src/selectors/jsonBody.ts @@ -47,7 +47,7 @@ export function JsonBodySelector( return createPrismySelector(() => { const { req } = getPrismyContext() const contentType = req.headers['content-type'] - if (!isContentTypeIsApplicationJSON(contentType)) { + if (!isContentTypeApplicationJSON(contentType)) { throw createError( 400, `Content type must be application/json. (Current: ${contentType})`, @@ -58,7 +58,7 @@ export function JsonBodySelector( }) } -function isContentTypeIsApplicationJSON(contentType: string | undefined) { +function isContentTypeApplicationJSON(contentType: string | undefined) { if (typeof contentType !== 'string') return false if (!contentType.startsWith('application/json')) return false return true From 8b8dd5b5afe8fb86c482fd0a5233115fe2af01f4 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Jan 2024 05:40:48 +0900 Subject: [PATCH 044/109] Add file upload temp example --- examples/file-upload/specs/file.spec.ts | 226 ++++++++++++++++++++++++ examples/file-upload/specs/file.ts | 93 ++++++++++ examples/file-upload/specs/files.ts | 7 + 3 files changed, 326 insertions(+) create mode 100644 examples/file-upload/specs/file.spec.ts create mode 100644 examples/file-upload/specs/file.ts create mode 100644 examples/file-upload/specs/files.ts diff --git a/examples/file-upload/specs/file.spec.ts b/examples/file-upload/specs/file.spec.ts new file mode 100644 index 0000000..3c5a0ac --- /dev/null +++ b/examples/file-upload/specs/file.spec.ts @@ -0,0 +1,226 @@ +import { testServerManager } from '../../../specs/helpers' +import { + ErrorResult, + getPrismyContext, + Middleware, + prismy, + Result, +} from '../../../src' +import { MultipartBodySelector } from './file' +import path from 'path' +import { File } from 'buffer' +import fs from 'fs' +import { nanoid } from 'nanoid' +import Formidable from 'formidable' + +const testBoundary = `------test-boundary-${nanoid()}` +const testUploadRoot = path.join(process.cwd(), 'file-test-dest') + +beforeAll(async () => { + await testServerManager.start() +}) + +afterAll(async () => { + await testServerManager.close() + fs.rmSync(testUploadRoot, { recursive: true }) + fs.mkdirSync(testUploadRoot) +}) + +describe('MultipartBodySelector', () => { + it('parse a file and a field (FormData)', async () => { + const handler = MultipartTestHandler() + + const formData = new FormData() + const fileBuffer = fs.readFileSync( + path.join(process.cwd(), 'specs/dummyFiles/smallFile.txt'), + ) + const fileBlob = new Blob([fileBuffer]) + const file = new File([fileBlob], 'smallFile.txt') + formData.append('testFile', file) + formData.append('testField', 'testValue') + + const response = await testServerManager.loadRequestListenerAndCall( + handler, + '/', + { + method: 'post', + body: formData, + }, + ) + + expect(response).toMatchObject({ + statusCode: 200, + }) + expect(JSON.parse(response.body)).toMatchObject({ + fields: { + testField: 'testValue', + }, + files: { + testFile: [ + { + filepath: expect.stringMatching( + new RegExp(testUploadRoot + '/[A-z0-9]+'), + ), + mimetype: 'application/octet-stream', + newFilename: expect.stringMatching('[A-z0-9]+'), + originalFilename: 'smallFile.txt', + size: file.size, + }, + ], + }, + }) + }) + + it('parse a file and a field (Raw)', async () => { + const handler = MultipartTestHandler() + + const response = await testServerManager.loadRequestListenerAndCall( + handler, + '/', + { + method: 'post', + headers: { + 'content-type': `multipart/form-data; boundary=${testBoundary}`, + }, + body: [ + '--' + testBoundary, + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '--' + testBoundary, + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '--' + testBoundary + '--', + ].join('\r\n'), + }, + ) + + expect(JSON.parse(response.body)).toMatchObject({ + fields: [ + { + name: 'file_name_0', + value: 'super alpha file', + info: { + encoding: '7bit', + mimeType: 'text/plain', + nameTruncated: false, + valueTruncated: false, + }, + }, + ], + files: [ + { + filePath: expect.stringMatching( + new RegExp( + `${testUploadRoot}/prismy-temp-dir-[A-z0-9_-]+/prismy-upload-[A-z0-9_-]+`, + ), + ), + info: { + encoding: '7bit', + filename: '1k_a.dat', + mimeType: 'application/octet-stream', + }, + name: 'upload_file_0', + }, + ], + }) + expect(response).toMatchObject({ + statusCode: 200, + }) + }) + + it('throws when body is empty', async () => { + const handler = MultipartTestHandler() + + const response = await testServerManager.loadRequestListenerAndCall( + handler, + '/', + { + method: 'post', + headers: { + 'content-type': `multipart/form-data; boundary=${testBoundary}`, + }, + body: '', + }, + ) + + expect(JSON.parse(response.body)).toMatchObject({ + error: expect.stringContaining('Error: Unexpected end of form'), + }) + expect(response).toMatchObject({ + statusCode: 400, + }) + }) + + it('throws if field size hits limit', async () => { + const handler = MultipartTestHandler({ + maxFileSize: 13, + maxFieldsSize: 3, + }) + + const response = await testServerManager.loadRequestListenerAndCall( + handler, + '/', + { + method: 'post', + headers: { + 'content-type': `multipart/form-data; boundary=${testBoundary}`, + }, + body: [ + '--' + testBoundary, + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '--' + testBoundary, + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '--' + testBoundary + '--', + ].join('\r\n'), + }, + ) + + const jsonBody = JSON.parse(response.body) + expect(jsonBody).toMatchObject({ + error: expect.stringContaining('Error: options.maxFieldsSize'), + }) + expect(response).toMatchObject({ + statusCode: 413, + }) + }) +}) + +const errorDataMap = new WeakMap() +function MultipartTestHandler(options: Formidable.Options = {}) { + const multipartBodySelector = MultipartBodySelector({ + uploadDir: testUploadRoot, + ...options, + }) + return prismy( + [multipartBodySelector], + (body) => { + return Result(body) + }, + [ + Middleware([], (next) => async () => { + try { + return await next() + } catch (error: any) { + const context = getPrismyContext() + return ErrorResult( + error.statusCode != null ? error.statusCode : 500, + { + error: error.stack, + data: errorDataMap.get(context), + }, + ) + } + }), + ], + ) +} diff --git a/examples/file-upload/specs/file.ts b/examples/file-upload/specs/file.ts new file mode 100644 index 0000000..5311fe7 --- /dev/null +++ b/examples/file-upload/specs/file.ts @@ -0,0 +1,93 @@ +import { getPrismyContext } from '../prismy' +import { createPrismySelector, PrismySelector } from './createSelector' +import { createError } from '../error' +import Formidable, { errors as formidableErrors, File } from 'formidable' + +function createMultipartBodyReader( + options: Formidable.Options & { multivaluedFields?: MVF[] } = {}, +): () => Promise<{ + fields: Omit<{ [key: string]: undefined | string }, MVF> & { + [key in MVF]: string[] | undefined + } + files: { [key: string]: File[] | undefined } +}> { + return async () => { + const context = getPrismyContext() + const contentType = context.req.headers['content-type'] + if (!isContentTypeMultipart(contentType)) { + throw createError( + 400, + `Content type must be multipart/form-data. (Current: ${contentType})`, + ) + } + const { multivaluedFields = [], ...otherOptions } = options + const form = Formidable(otherOptions) + try { + const [fields, files] = await form.parse(context.req) + return { + fields: Object.entries(fields).reduce( + (obj, [key, value]) => { + if (value == null) { + return obj + } + if (multivaluedFields.indexOf(key as any) > -1) { + obj[key] = value + } else { + obj[key] = value[0] + } + + return obj + }, + {} as Omit<{ [key: string]: undefined | string }, MVF> & { + [key in MVF]: string[] | undefined + }, + ), + files, + } + } catch (error: any) { + switch (error.code) { + case formidableErrors.biggerThanMaxFileSize: + throw createError(413, error.message, error) + } + console.log(error) + throw error + } + } +} + +export function MultipartBodySelector( + options: Formidable.Options & { multivaluedFields?: MVF[] } = {}, +): PrismySelector<{ + fields: Omit<{ [key: string]: undefined | string }, MVF> & { + [key in MVF]: string[] | undefined + } + files: { [key: string]: File[] | undefined } +}> { + return createPrismySelector(async () => { + return createMultipartBodyReader(options)() + }) +} +export function MultipartBodyReaderSelector( + options: Formidable.Options & { multivaluedFields?: MVF[] } = {}, +): PrismySelector< + () => Promise<{ + fields: Omit<{ [key: string]: undefined | string }, MVF> & { + [key in MVF]: string[] | undefined + } + files: { [key: string]: File[] | undefined } + }> +> { + return createPrismySelector(async () => { + return createMultipartBodyReader(options) + }) +} + +function isContentTypeMultipart(contentType: string | undefined) { + if (contentType == null) { + return false + } + if (contentType.startsWith('multipart/form-data')) { + return true + } + return false +} diff --git a/examples/file-upload/specs/files.ts b/examples/file-upload/specs/files.ts new file mode 100644 index 0000000..1443a6f --- /dev/null +++ b/examples/file-upload/specs/files.ts @@ -0,0 +1,7 @@ +import { MultipartBodySelector } from '../../../src/selectors/file' +import { expectType } from '../../../specs/helpers' + +const selector = MultipartBodySelector({ multivaluedFields: ['hello'] }) +const body = await selector.resolve() +expectType(body.fields.hello) +expectType(body.fields.noHello) From e4aa3ea1c0e27fe1b43032244e3ef49029318c43 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Jan 2024 05:41:25 +0900 Subject: [PATCH 045/109] clean up --- package.json | 1 - specs/types/basic.ts | 20 +++++++++++++++++++- src/middleware.ts | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 189ae93..7433897 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "codecov": "^3.8.0", "got": "^11.8.0", "jest": "^29.7.0", - "node-fetch": "^2.7.0", "prettier": "^3.1.1", "rimraf": "^3.0.0", "ts-jest": "^29.1.1", diff --git a/specs/types/basic.ts b/specs/types/basic.ts index d2a4f0e..956d91d 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -12,8 +12,26 @@ import { } from '../../src' import { expectType } from '../helpers' import http from 'http' -import { InjectSelector } from '../../src/selectors/injector' +import { InjectSelector } from '../../src/selectors/inject' +/** + * + | "missingPlugin" + | "pluginFunction" + | "aborted" + | "noParser" + | "uninitializedParser" + | "filenameNotString" + | "maxFieldsSizeExceeded" + | "maxFieldsExceeded" + | "smallerThanMinFileSize" + | "biggerThanMaxFileSize" + | "noEmptyFiles" + | "missingContentType" + | "malformedMultipart" + | "missingMultipartBoundary" + | "unknownTransferEncoding", + */ const handler1 = Handler([urlSelector, methodSelector], (url, method) => { expectType(url) expectType(method) diff --git a/src/middleware.ts b/src/middleware.ts index ff9f328..30ac864 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -30,7 +30,7 @@ export class PrismyMiddleware< * Simple Example * ```ts * - * const withCors = middleware([], next => async () => { + * const withCors = Middleware([], next => async () => { * const resObject = await next() * * return updateHeaders(resObject, { From 048817c0ca749de176ba30e3750687fb9df461f4 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Jan 2024 05:43:46 +0900 Subject: [PATCH 046/109] Update todo --- v4-todo.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/v4-todo.md b/v4-todo.md index 209896a..1617887 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -16,7 +16,7 @@ ``` - [ ] Update docs -- [ ] Update +- [ ] Update examples - [ ] Shorthand Prismy, Route - [x] redesigned router interface - [x] introduced route methodexamples @@ -88,7 +88,8 @@ - [x] Make middleware into a class - [x] Include prismy-cookie - [x] Added DI Selector -- [ ] File uploading +- [x] File uploading + - Interface is a bit confusing and too different and rewriting cost is too high. Should just provide it as an example instead of including this project. - [ ] Rewrite prismy-session - [ ] Make it compatible with SessionStore of express-session From 8c7d9fbad7ad6b6bd66df6b540d7c33db23bfebc Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Jan 2024 07:27:05 +0900 Subject: [PATCH 047/109] Fix basic interfaces --- specs/prismy.spec.ts | 2 +- specs/types/basic.ts | 42 +++++++++++++++++++------------------ src/handler.ts | 12 ++++++----- src/prismy.ts | 26 +++++++++++++---------- src/router.ts | 49 ++++++++++++++++++++++++++++++-------------- 5 files changed, 79 insertions(+), 52 deletions(-) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 76affa1..06d3d6f 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -65,7 +65,7 @@ describe('prismy', () => { ) const handler = Handler([rawUrlSelector], (url) => Result(url)) - const result = handler.handler('Hello, World!') + const result = handler.handlerFunction('Hello, World!') expect(result).toMatchObject({ body: 'Hello, World!', diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 956d91d..0eb6681 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -1,37 +1,23 @@ import { BodySelector, Handler, + MaybePromise, methodSelector, Middleware, prismy, PrismyHandler, PrismyNextFunction, PrismyResult, + PrismyRoute, Result, + Route, urlSelector, } from '../../src' import { expectType } from '../helpers' import http from 'http' import { InjectSelector } from '../../src/selectors/inject' +import { PrismySelector } from '../../src/selectors/createSelector' -/** - * - | "missingPlugin" - | "pluginFunction" - | "aborted" - | "noParser" - | "uninitializedParser" - | "filenameNotString" - | "maxFieldsSizeExceeded" - | "maxFieldsExceeded" - | "smallerThanMinFileSize" - | "biggerThanMaxFileSize" - | "noEmptyFiles" - | "missingContentType" - | "malformedMultipart" - | "missingMultipartBoundary" - | "unknownTransferEncoding", - */ const handler1 = Handler([urlSelector, methodSelector], (url, method) => { expectType(url) expectType(method) @@ -44,7 +30,7 @@ expectType< method: string | undefined, url2: URL, ) => PrismyResult | Promise ->(handler1.handler) +>(handler1.handlerFunction) expectType(Handler([BodySelector()], () => Result(null))) @@ -82,7 +68,23 @@ const mailService: MailService = const mailServiceSelector = InjectSelector(mailService) -Handler([mailServiceSelector], (mailService) => { +const handler = Handler([mailServiceSelector], (mailService) => { expectType(mailService) return Result(null) }) + +const mailHandlerRoute = Route('/', handler) +expectType]>>(mailHandlerRoute) + +const shortRoute = Route('/', [urlSelector, methodSelector], (url, method) => { + expectType(url) + expectType(method) + return Result(null) +}) + +expectType< + PrismyRoute<[PrismySelector, PrismySelector]> +>(shortRoute) +expectType< + (url: URL, method: string | undefined) => MaybePromise> +>(shortRoute.handler.handlerFunction) diff --git a/src/handler.ts b/src/handler.ts index b82a7ca..449b389 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -18,7 +18,7 @@ export class PrismyHandler< * PrismyHandler exposes `handler` for unit testing the handler. * @param args selected arguments */ - public handler: ( + public handlerFunction: ( ...args: SelectorReturnTypeTuple ) => MaybePromise, public middlewareList: PrismyMiddleware[], @@ -27,7 +27,7 @@ export class PrismyHandler< async handle(): Promise { const next: PrismyNextFunction = compileHandler( this.selectors, - this.handler, + this.handlerFunction, ) const pipe = this.middlewareList.reduce((next, middleware) => { @@ -66,15 +66,17 @@ export class PrismyHandler< * array outside of the function call. * * @param selectors - Tuple of Selectors to generate arguments for handler - * @param handler - Business logic handling the request + * @param handlerFunction - Business logic handling the request * @param middlewareList - Middleware to pass request and response through * * @public * */ export function Handler[]>( selectors: [...S], - handler: (...args: SelectorReturnTypeTuple) => MaybePromise, + handlerFunction: ( + ...args: SelectorReturnTypeTuple + ) => MaybePromise, middlewareList: PrismyMiddleware[]>[] = [], ) { - return new PrismyHandler(selectors, handler, middlewareList) + return new PrismyHandler(selectors, handlerFunction, middlewareList) } diff --git a/src/prismy.ts b/src/prismy.ts index 3c267fe..b4a73ea 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -16,15 +16,17 @@ export function getPrismyContext(): PrismyContext { } /** - * Generates a handler to be used by http.Server + * Make a RequestListener from PrismyHandler * * @example * ```ts * const handler = Handler([], () => { ... }) * - * http.createServer(prismy(handler)) - * // Or directly - * http.createServer([], () => { ... }) + * const listener = prismy(handler) + * // Or + * // const listener = prismy([], () => {...}) + * + * http.createServerlistener) * ``` * * @param prismyHandler @@ -34,28 +36,30 @@ export function prismy[]>( ): RequestListener /** - * Generates a handler to be used by http.Server - * - * prismy(Handler(...)) + * Make a RequestListener from PrismyHandler * * @param selectors - * @param handler + * @param handlerFunction * @param middlewareList */ export function prismy[]>( selectors: [...S], - handler: (...args: SelectorReturnTypeTuple) => MaybePromise, + handlerFunction: ( + ...args: SelectorReturnTypeTuple + ) => MaybePromise, middlewareList?: PrismyMiddleware[]>[], ): RequestListener export function prismy[]>( selectorsOrPrismyHandler: [...S] | PrismyHandler, - handler?: (...args: SelectorReturnTypeTuple) => MaybePromise, + handlerFunction?: ( + ...args: SelectorReturnTypeTuple + ) => MaybePromise, middlewareList?: PrismyMiddleware[]>[], ): RequestListener { const injectedHandler = selectorsOrPrismyHandler instanceof PrismyHandler ? selectorsOrPrismyHandler - : Handler(selectorsOrPrismyHandler, handler!, middlewareList) + : Handler(selectorsOrPrismyHandler, handlerFunction!, middlewareList) async function requestListener( request: IncomingMessage, diff --git a/src/router.ts b/src/router.ts index b746c24..d1f454d 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -import { PrismyContext } from './types' +import { MaybePromise, PrismyContext, SelectorReturnTypeTuple } from './types' import { methodSelector, urlSelector } from './selectors' import { match as createMatchFunction } from 'path-to-regexp' import { getPrismyContext } from './prismy' @@ -7,7 +7,7 @@ import { createPrismySelector, PrismySelector, } from './selectors/createSelector' -import { PrismyMiddleware } from '.' +import { PrismyMiddleware, PrismyResult } from '.' import { Handler, PrismyHandler } from './handler' import { join as joinPath } from 'path' @@ -26,16 +26,15 @@ type Route = { listener: PrismyHandler[]> } -export class PrismyRoute { +export class PrismyRoute< + S extends PrismySelector[] = PrismySelector[], +> { indicator: RouteIndicator - listener: PrismyHandler[]> + handler: PrismyHandler - constructor( - indicator: RouteIndicator, - listener: PrismyHandler[]>, - ) { + constructor(indicator: RouteIndicator, handler: PrismyHandler) { this.indicator = indicator - this.listener = listener + this.handler = handler } } @@ -44,7 +43,7 @@ export function Router( { prefix = '/', middleware = [], notFoundHandler }: PrismyRouterOptions = {}, ) { const compiledRoutes = routes.map((route) => { - const { indicator, listener } = route + const { indicator, handler: listener } = route const [targetPath, method] = indicator const compiledTargetPath = removeTralingSlash( joinPath('/', prefix, targetPath), @@ -92,14 +91,34 @@ export function Router( ) } -export function Route( +export function Route[]>( + indicator: RouteIndicator | string, + handler: PrismyHandler, +): PrismyRoute +export function Route[]>( + indicator: RouteIndicator | string, + selectors: [...S], + handlerFunction?: ( + ...args: SelectorReturnTypeTuple + ) => MaybePromise, + middlewareList?: PrismyMiddleware[]>[], +): PrismyRoute +export function Route[]>( indicator: RouteIndicator | string, - listener: PrismyHandler[]>, -): PrismyRoute { + selectorsOrPrismyHandler: [...S] | PrismyHandler, + handlerFunction?: ( + ...args: SelectorReturnTypeTuple + ) => MaybePromise, + middlewareList?: PrismyMiddleware[]>[], +): PrismyRoute { + const handler = + selectorsOrPrismyHandler instanceof PrismyHandler + ? selectorsOrPrismyHandler + : Handler(selectorsOrPrismyHandler, handlerFunction!, middlewareList) if (typeof indicator === 'string') { - return new PrismyRoute([indicator, 'get'], listener) + return new PrismyRoute([indicator, 'get'], handler) } - return new PrismyRoute(indicator, listener) + return new PrismyRoute(indicator, handler) } const routeParamsMap = new WeakMap() From 40a2c228f934b03bf33ac3aa4835faa942ab5f10 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Jan 2024 07:27:27 +0900 Subject: [PATCH 048/109] 4.0.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7433897..0b86821 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "3.0.0", + "version": "4.0.0-0", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 4d5e4af3df1d6a8b906986e36a1cad63d72b65f5 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 17:19:31 +0900 Subject: [PATCH 049/109] Expose test tool --- package.json | 10 +++++++ specs/helpers.ts | 70 ++------------------------------------------ src/test.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ v4-todo.md | 2 +- 4 files changed, 90 insertions(+), 68 deletions(-) create mode 100644 src/test.ts diff --git a/package.json b/package.json index 0b86821..960c013 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,16 @@ "type": "git", "url": "git+https://github.com/prismyland/prismy.git" }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js" + }, + "./test": { + "types": "./dist/test.d.ts", + "require": "./dist/test.js" + } + }, "scripts": { "build": "rimraf dist && tsc -P tsconfig.build.json", "lint": "prettier --check src/**/*.ts specs/**/*.ts examples/*/src/**/*.ts", diff --git a/specs/helpers.ts b/specs/helpers.ts index 4311fa4..59b7501 100644 --- a/specs/helpers.ts +++ b/specs/helpers.ts @@ -1,7 +1,8 @@ -import http, { IncomingMessage, RequestListener, ServerResponse } from 'http' +import http, { RequestListener } from 'http' import listen from 'async-listen' import { URL } from 'url' -import { prismy, PrismyHandler } from '../src' +import { PrismyHandler } from '../src' +import { PrismyTestServer } from '../src/test' export type TestCallback = (url: string) => Promise | void @@ -71,68 +72,3 @@ export const testServerManager = { return testServer!.call(url, options).then(resolveTestResponse) }, } -export class PrismyTestServer { - server: http.Server | null = null - url: string = '' - listener: RequestListener = () => { - throw new Error('PrismyTestServer: Listener is not set') - } - status: 'idle' | 'starting' | 'closing' = 'idle' - - loadRequestListener(listener: RequestListener) { - this.listener = listener - } - load(handler: PrismyHandler) { - this.listener = prismy(handler) - } - - listen(req: IncomingMessage, res: ServerResponse) { - this.listener(req, res) - } - - call(url: string = '/', options?: RequestInit) { - return fetch(this.url + url, options) - } - - async start() { - if (this.status !== 'idle') { - throw new Error( - `Cannot start test server (Current status: ${this.status})`, - ) - } - - if (this.server == null) { - const server = new http.Server(this.listen.bind(this)) - const url = await listen(server) - - this.server = server - this.url = url.origin - } - } - - async close() { - if (this.status !== 'idle') { - throw new Error( - `Cannot close test server (Current status: ${this.status})`, - ) - } - - if (this.server == null) { - return - } - - const server = this.server - this.server = null - this.url = '' - - await new Promise((resolve, reject) => { - server!.close((error) => { - if (error != null) { - reject(error) - } else { - resolve(null) - } - }) - }) - } -} diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..27f3261 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,76 @@ +import http, { IncomingMessage, RequestListener, ServerResponse } from 'http' +import listen from 'async-listen' +import { prismy, PrismyHandler } from './' + +export class PrismyTestServer { + server: http.Server | null = null + url: string = '' + listener: RequestListener = () => { + throw new Error('PrismyTestServer: Listener is not set') + } + status: 'idle' | 'starting' | 'closing' = 'idle' + + loadRequestListener(listener: RequestListener) { + this.listener = listener + return this + } + + load(handler: PrismyHandler) { + this.listener = prismy(handler) + return this + } + + private listen(req: IncomingMessage, res: ServerResponse) { + this.listener(req, res) + } + + call(url: string = '/', options?: RequestInit) { + return fetch(this.url + url, options) + } + + async start() { + if (this.status !== 'idle') { + throw new Error( + `Cannot start test server (Current status: ${this.status})`, + ) + } + + if (this.server == null) { + const server = new http.Server(this.listen.bind(this)) + const url = await listen(server) + + this.server = server + this.url = url.origin + } + } + + async close() { + if (this.status !== 'idle') { + throw new Error( + `Cannot close test server (Current status: ${this.status})`, + ) + } + + if (this.server == null) { + return + } + + const server = this.server + this.server = null + this.url = '' + + await new Promise((resolve, reject) => { + server!.close((error) => { + if (error != null) { + reject(error) + } else { + resolve(null) + } + }) + }) + } +} + +export function TestServer() { + return new PrismyTestServer() +} diff --git a/v4-todo.md b/v4-todo.md index 1617887..9efd1ac 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -17,7 +17,7 @@ ``` - [ ] Update docs - [ ] Update examples -- [ ] Shorthand Prismy, Route +- [x] Shorthand Prismy, Route - [x] redesigned router interface - [x] introduced route methodexamples - [x] Add tests From e59efa80457f4e145e638074382a9a9b0a5024fb Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 17:19:42 +0900 Subject: [PATCH 050/109] 4.0.0-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 960c013..7b3ac0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-0", + "version": "4.0.0-1", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 75bcbd8d9e0223087621da1949735b05697fc082 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 17:21:34 +0900 Subject: [PATCH 051/109] Fix test config --- jest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.ts b/jest.config.ts index 9619fde..6c580ea 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,7 +3,7 @@ import type { JestConfigWithTsJest } from 'ts-jest' const jestConfig: JestConfigWithTsJest = { preset: 'ts-jest', testEnvironment: 'node', - testPathIgnorePatterns: ['/node_modules/', '/dist/', '/examples/'], + testPathIgnorePatterns: ['/node_modules/', '/dist/', '/examples/', '/src'], transform: { '^.+\\.tsx?$': [ 'ts-jest', From ca03cfb531c6aa50741c3b973eb96b46a2d68eeb Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 17:32:20 +0900 Subject: [PATCH 052/109] Remove main and types --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 7b3ac0d..b5c4277 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,6 @@ "author": "Junyoung Choi ", "homepage": "https://github.com/prismyland/prismy", "license": "MIT", - "main": "dist/index.js", - "types": "dist/index.d.ts", "files": [ "dist/index.d.ts", "dist/**/*" From c258b1d08f5a70b4f8caaf71db67d4a9c99c03f6 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 17:32:33 +0900 Subject: [PATCH 053/109] 4.0.0-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5c4277..7758849 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-1", + "version": "4.0.0-2", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From a6fc16303020ef5b2381a40ee098636490f5f833 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 17:40:45 +0900 Subject: [PATCH 054/109] Rollback main and types --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 7758849..67d2c1b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "author": "Junyoung Choi ", "homepage": "https://github.com/prismyland/prismy", "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", "files": [ "dist/index.d.ts", "dist/**/*" From 33251c7ce17554ad7d6abb9c8f600cc43c63f1f6 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 17:40:51 +0900 Subject: [PATCH 055/109] 4.0.0-3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 67d2c1b..3cea856 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-2", + "version": "4.0.0-3", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 7a6df061cb13e4316b667b0883d325d64512e916 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 18:20:15 +0900 Subject: [PATCH 056/109] Export createPrismySelector --- src/selectors/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/selectors/index.ts b/src/selectors/index.ts index 072537a..3e530a2 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -9,3 +9,4 @@ export * from './url' export * from './urlEncodedBody' export * from './textBody' export * from './cookie' +export * from './createSelector' From 76042ee23ac6d432b3f2c9d33f4d8347030c4622 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 18:20:22 +0900 Subject: [PATCH 057/109] 4.0.0-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cea856..ba4a3b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-3", + "version": "4.0.0-4", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 6220670c8abea98ff72f9db70cce35c68579cad9 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 18:55:21 +0900 Subject: [PATCH 058/109] Move async-listen from devDep to dep --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index ba4a3b7..cf2300a 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,7 @@ "@types/cookie": "^0.6.0", "@types/jest": "^29.5.11", "@types/node": "^18.19.5", - "@types/node-fetch": "^2.6.10", "@types/test-listen": "^1.1.0", - "async-listen": "^3.0.1", "codecov": "^3.8.0", "got": "^11.8.0", "jest": "^29.7.0", @@ -68,6 +66,7 @@ "typescript": "^4.9.5" }, "dependencies": { + "async-listen": "^3.0.1", "content-type": "^1.0.4", "cookie": "^0.6.0", "is-stream": "^2.0.0", From d84e891a5b62897903f36cd4fe1f6c3dcce11acc Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 18:55:27 +0900 Subject: [PATCH 059/109] 4.0.0-5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf2300a..74d9b48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-4", + "version": "4.0.0-5", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 41d5d8c28c60e18da0b0009a58c4b156f498e6ca Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 23:26:53 +0900 Subject: [PATCH 060/109] Fix router typing --- specs/router.spec.ts | 36 ++++++++++++++++-------------------- src/router.ts | 2 +- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index c1dfaaf..2f9b9e9 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -7,6 +7,7 @@ import { getPrismyContext, } from '../src' import { Handler } from '../src/handler' +import { InjectSelector } from '../src/selectors/inject' import { testServerManager } from './helpers' beforeAll(async () => { @@ -179,31 +180,26 @@ describe('router', () => { return Result(weakMap.get(context)) }) - const handlerB = Handler([], () => { - return Result('b') - }) - const routerHandler = Router( - [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], - { - middleware: [ - Middleware([], (next) => () => { - const context = getPrismyContext() - weakMap.set(context, (weakMap.get(context) || '') + 'a') - return next() - }), - Middleware([], (next) => () => { - const context = getPrismyContext() - weakMap.set(context, (weakMap.get(context) || '') + 'b') - return next() - }), - ], - }, - ) + const routerHandler = Router([Route(['/', 'get'], handlerA)], { + middleware: [ + Middleware([InjectSelector('a')], (next) => (value) => { + const context = getPrismyContext() + weakMap.set(context, (weakMap.get(context) || '') + value) + return next() + }), + Middleware([InjectSelector('b')], (next) => (value) => { + const context = getPrismyContext() + weakMap.set(context, (weakMap.get(context) || '') + value) + return next() + }), + ], + }) const response = await testServerManager.loadAndCall(routerHandler) expect(response.statusCode).toBe(200) + expect(response.body).toBe('ba') }) }) diff --git a/src/router.ts b/src/router.ts index d1f454d..c2a5609 100644 --- a/src/router.ts +++ b/src/router.ts @@ -143,7 +143,7 @@ export function RouteParamSelector( interface PrismyRouterOptions { prefix?: string - middleware?: PrismyMiddleware[] + middleware?: PrismyMiddleware[]>[] notFoundHandler?: PrismyHandler } From 7773be1c22f3c90070421942bdea96da56672b3b Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 21 Jan 2024 23:26:57 +0900 Subject: [PATCH 061/109] 4.0.0-6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 74d9b48..d6b4171 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-5", + "version": "4.0.0-6", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 5e4523d0b93901bd3c27bf7fd2f19ddbcb979c43 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 22 Jan 2024 16:29:13 +0900 Subject: [PATCH 062/109] test coverage:prismy.ts --- specs/prismy.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 06d3d6f..697398b 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -170,3 +170,12 @@ describe('prismy', () => { }) }) }) + +describe('getPrismyContext', () => { + it('throws if context is not set(resolved outside of prismy)', () => { + expect(() => { + const context = getPrismyContext() + throw new Error(`should fail to get context. ${context}`) + }).toThrow('Prismy context is not loaded.') + }) +}) From 506e3ae650fc1b30f89d2e1f850e695d62647893 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 22 Jan 2024 16:37:25 +0900 Subject: [PATCH 063/109] Rename res to result --- src/error.ts | 2 +- src/index.ts | 2 +- src/prismy.ts | 2 +- src/{res.ts => result.ts} | 0 src/send.ts | 2 +- src/types.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename src/{res.ts => result.ts} (100%) diff --git a/src/error.ts b/src/error.ts index fcdce11..9bca49e 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,4 @@ -import { Result } from './res' +import { Result } from './result' /** * Creates a response object from an error diff --git a/src/index.ts b/src/index.ts index 4586f6e..e2b60e3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,5 @@ export * from './middleware' export * from './selectors' export * from './error' export * from './router' -export * from './res' +export * from './result' export * from './handler' diff --git a/src/prismy.ts b/src/prismy.ts index b4a73ea..3dfed38 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -4,7 +4,7 @@ import { PrismyMiddleware } from './middleware' import { Handler, PrismyHandler } from './handler' import { PrismySelector } from './selectors/createSelector' import { MaybePromise, PrismyContext, SelectorReturnTypeTuple } from './types' -import { PrismyResult } from './res' +import { PrismyResult } from './result' export const prismyContextStorage = new AsyncLocalStorage() export function getPrismyContext(): PrismyContext { diff --git a/src/res.ts b/src/result.ts similarity index 100% rename from src/res.ts rename to src/result.ts diff --git a/src/send.ts b/src/send.ts index 5f259c2..dd24524 100644 --- a/src/send.ts +++ b/src/send.ts @@ -1,7 +1,7 @@ import { IncomingMessage, ServerResponse } from 'http' import { readable } from 'is-stream' import { Stream } from 'stream' -import { PrismyResult } from './res' +import { PrismyResult } from './result' /** * Function to send data to the client diff --git a/src/types.ts b/src/types.ts index ccb35d0..a213182 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import { IncomingMessage } from 'http' -import { PrismyResult } from './res' +import { PrismyResult } from './result' import { PrismySelector } from './selectors/createSelector' /** From c63cebe34a57e888048bd1a7e378fe3491346b11 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 22 Jan 2024 21:36:41 +0900 Subject: [PATCH 064/109] Fix result test coverage --- specs/result.spec.ts | 40 +++++++++++++++++++++++++++++++++++++--- src/result.ts | 16 ++++++++-------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/specs/result.spec.ts b/specs/result.spec.ts index 9c5a7ad..86cf380 100644 --- a/specs/result.spec.ts +++ b/specs/result.spec.ts @@ -1,4 +1,4 @@ -import { Redirect, Result, Handler } from '../src' +import { Redirect, Result, Handler, ErrorResult, PrismyResult } from '../src' import { testServerManager } from './helpers' beforeAll(async () => { @@ -9,11 +9,25 @@ afterAll(async () => { await testServerManager.close() }) -// TODO: Implement Error Result tests -describe('ErrorResult', () => {}) +describe('ErrorResult', () => { + it('creates error PrismyResult', () => { + const errorResult = ErrorResult(400, 'Invalid Format') + + expect(errorResult).toBeInstanceOf(PrismyResult) + expect(errorResult.statusCode).toBe(400) + expect(errorResult.body).toBe('Invalid Format') + }) +}) describe('PrismyResult', () => { describe('#setStatusCode', () => { + it('set body', async () => { + const handler = Handler([], () => Result('Hello!').setBody('Hola!')) + + const response = await testServerManager.loadAndCall(handler) + expect(response.body).toBe('Hola!') + }) + it('sets status code', async () => { const handler = Handler([], () => Result('Hello, World!').setStatusCode(201), @@ -158,4 +172,24 @@ describe('Redirect', () => { 'testCookie2=testValue2; HttpOnly', ]) }) + + it('appends set cookie header', async () => { + const handler = Handler([], () => + Result(null) + .updateHeaders({ + 'set-cookie': 'testCookie=testValue', + }) + .setCookie('testCookie2', 'testValue2'), + ) + + const response = await testServerManager.loadAndCall(handler, '/') + + expect(response).toMatchObject({ + statusCode: 200, + }) + expect(response.headers.getSetCookie()).toEqual([ + 'testCookie=testValue', + 'testCookie2=testValue2', + ]) + }) }) diff --git a/src/result.ts b/src/result.ts index d30f318..f6263ce 100644 --- a/src/result.ts +++ b/src/result.ts @@ -92,16 +92,16 @@ export class PrismyResult { value: string, options?: cookie.CookieSerializeOptions, ) { - const existingValue = this.headers['set-cookie'] - const newValue = cookie.serialize(key, value, options) - + const existingSetCookieHeaders = this.headers['set-cookie'] + const newSetCookieHeader = cookie.serialize(key, value, options) + console.log(existingSetCookieHeaders) return this.updateHeaders({ 'set-cookie': - existingValue == null - ? [newValue] - : Array.isArray(existingValue) - ? [...existingValue, newValue] - : [existingValue, newValue], + existingSetCookieHeaders == null + ? [newSetCookieHeader] + : Array.isArray(existingSetCookieHeaders) + ? [...existingSetCookieHeaders, newSetCookieHeader] + : [existingSetCookieHeaders, newSetCookieHeader], }) } From 8672fea7f79ba4c879a6d034ca5d2030bb3941a2 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 22 Jan 2024 21:39:20 +0900 Subject: [PATCH 065/109] Fix router test coverage --- specs/router.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 2f9b9e9..5e60fbf 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -36,6 +36,20 @@ describe('router', () => { }) }) + it('routes with pathname(shorthand)', async () => { + const routerHandler = Router([ + Route('/a', [InjectSelector('a')], (data) => Result(data)), + Route('/b', [InjectSelector('b')], (data) => Result(data)), + ]) + + const response = await testServerManager.loadAndCall(routerHandler, '/b') + + expect(response).toMatchObject({ + statusCode: 200, + body: 'b', + }) + }) + it('routes with method', async () => { const handlerA = Handler([], () => { return Result('a') From dc60b09ec628b2974ec27f80da6cfd7bb8c7aa12 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 23 Jan 2024 22:31:43 +0900 Subject: [PATCH 066/109] Emit text coverage only --- jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.config.ts b/jest.config.ts index 6c580ea..be69ae0 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -12,6 +12,7 @@ const jestConfig: JestConfigWithTsJest = { }, ], }, + coverageReporters: ['text', 'text-summary'], } export default jestConfig From 9d942aa5f81bb3567aa12704a4e84473a0beb84c Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Wed, 24 Jan 2024 18:12:31 +0900 Subject: [PATCH 067/109] Fix cookie test coverage --- specs/send.spec.ts | 229 ++++++++++++--------------------------------- src/send.ts | 2 +- 2 files changed, 62 insertions(+), 169 deletions(-) diff --git a/specs/send.spec.ts b/specs/send.spec.ts index 42b5297..2acf101 100644 --- a/specs/send.spec.ts +++ b/specs/send.spec.ts @@ -1,108 +1,75 @@ -import got from 'got' import { IncomingMessage, RequestListener, ServerResponse } from 'http' import { Readable } from 'stream' import { Result } from '../src' import { sendPrismyResult } from '../src/send' -import { testHandler } from './helpers' +import { TestServer } from '../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('send', () => { it('sends empty body when body is null', async () => { - expect.hasAssertions() - const handler: RequestListener = (req, res) => { sendPrismyResult(req, res, Result(null)) } - await testHandler(handler, async (url) => { - const response = await got(url) - expect(response.body).toBeFalsy() - }) + const res = await ts.loadRequestListener(handler).call('/') + + expect(await res.text()).toBe('') }) it('sends string body', async () => { - expect.hasAssertions() - const handler: RequestListener = (req, res) => { sendPrismyResult(req, res, Result('test')) } - await testHandler(handler, async (url) => { - const response = await got(url) - expect(response.body).toEqual('test') - }) + const res = await ts.loadRequestListener(handler).call('/') + + expect(await res.text()).toBe('test') }) it('sends buffer body', async () => { - expect.hasAssertions() - const targetBuffer = Buffer.from('Hello, world!') const handler: RequestListener = (req, res) => { - res.setHeader('Content-Type', 'application/octet-stream') - const statusCode = res.statusCode - sendPrismyResult(req, res, Result(targetBuffer, statusCode)) + sendPrismyResult(req, res, Result(targetBuffer)) } - await testHandler(handler, async (url) => { - const responsePromise = got(url) - const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([ - responsePromise, - bufferPromise, - ]) - - expect(targetBuffer.equals(buffer)).toBe(true) - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString(), - ) - }) - }) - - it('sets header when Content-Type header is not given (buffer)', async () => { - expect.hasAssertions() - - const targetBuffer = Buffer.from('Hello, world!') - const handler: RequestListener = (req, res) => { - const statusCode = res.statusCode - sendPrismyResult(req, res, Result(targetBuffer, statusCode)) - } + const res = await ts.loadRequestListener(handler).call('/') - await testHandler(handler, async (url) => { - const responsePromise = got(url) - const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([ - responsePromise, - bufferPromise, - ]) - - expect(targetBuffer.equals(buffer)).toBe(true) - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString(), - ) - expect(response.headers['content-type']).toBe('application/octet-stream') - }) + const resBodyBuffer = Buffer.from(await res.arrayBuffer()) + expect(resBodyBuffer.equals(targetBuffer)).toBeTruthy() + expect(res.headers.get('content-length')).toBe( + targetBuffer.length.toString(), + ) + expect(res.headers.get('content-type')).toBe('application/octet-stream') }) - it('sends buffer body when body is stream', async () => { + it('sends buffer body when body is stream (lenght is not available)', async () => { expect.hasAssertions() const targetBuffer = Buffer.from('Hello, world!') const stream = Readable.from(targetBuffer.toString()) const handler: RequestListener = (req, res) => { - res.setHeader('Content-Type', 'application/octet-stream') const statusCode = res.statusCode sendPrismyResult(req, res, Result(stream, statusCode)) } - await testHandler(handler, async (url) => { - const response = await got(url, { - responseType: 'buffer', - }) - expect(targetBuffer.equals(response.body)).toBe(true) - }) + const res = await ts.loadRequestListener(handler).call('/') + + const resBodyBuffer = Buffer.from(await res.arrayBuffer()) + expect(resBodyBuffer.equals(targetBuffer)).toBeTruthy() + expect(res.headers.get('content-length')).toBeNull() + expect(res.headers.get('content-type')).toBe('application/octet-stream') }) - it('uses handler when body is function', async () => { - expect.hasAssertions() + it('delegates response handling if body is a function', async () => { const sendHandler = ( _request: IncomingMessage, response: ServerResponse, @@ -113,137 +80,63 @@ describe('send', () => { sendPrismyResult(req, res, Result(sendHandler)) } - await testHandler(handler, async (url) => { - const response = await got(url) - expect(response.body).toEqual('test') - }) - }) - - it('sets header when Content-Type header is not given (stream)', async () => { - expect.hasAssertions() - - const targetBuffer = Buffer.from('Hello, world!') - const stream = Readable.from(targetBuffer.toString()) - const handler: RequestListener = (req, res) => { - const statusCode = res.statusCode - sendPrismyResult(req, res, Result(stream, statusCode)) - } + const res = await ts.loadRequestListener(handler).call('/') - await testHandler(handler, async (url) => { - const responsePromise = got(url) - const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([ - responsePromise, - bufferPromise, - ]) - expect(targetBuffer.equals(buffer)).toBe(true) - expect(response.headers['content-type']).toBe('application/octet-stream') - }) + expect(await res.text()).toBe('test') }) - it('sends stringified JSON object when body is object', async () => { - expect.hasAssertions() - + it('sends stringified JSON object when body is an JSON stringifiable object', async () => { const target = { foo: 'bar', } const handler: RequestListener = (req, res) => { - res.setHeader('Content-Type', 'application/json; charset=utf-8') sendPrismyResult(req, res, Result(target)) } - await testHandler(handler, async (url) => { - const response = await got(url, { - responseType: 'json', - }) - expect(response.body).toMatchObject(target) - expect(response.headers['content-length']).toBe( - JSON.stringify(target).length.toString(), - ) - }) - }) + const res = await ts.loadRequestListener(handler).call('/') - it('sets header when Content-Type header is not given (object)', async () => { - expect.hasAssertions() - - const target = { + expect(await res.json()).toEqual({ foo: 'bar', - } - const handler: RequestListener = (req, res) => { - sendPrismyResult(req, res, Result(target)) - } - - await testHandler(handler, async (url) => { - const response = await got(url, { - responseType: 'json', - }) - expect(response.body).toMatchObject(target) - expect(response.headers['content-length']).toBe( - JSON.stringify(target).length.toString(), - ) - expect(response.headers['content-type']).toBe( - 'application/json; charset=utf-8', - ) }) + expect(res.headers.get('content-type')).toBe( + 'application/json; charset=utf-8', + ) + expect(res.headers.get('content-length')).toEqual( + JSON.stringify(target).length.toString(), + ) }) it('sends stringified JSON object when body is number', async () => { - expect.hasAssertions() - - const target = 1004 + const target = 777 const handler: RequestListener = (req, res) => { - res.setHeader('Content-Type', 'application/json; charset=utf-8') sendPrismyResult(req, res, Result(target)) } - await testHandler(handler, async (url) => { - const response = await got(url) - const stringifiedTarget = JSON.stringify(target) - expect(response.body).toBe(stringifiedTarget) - expect(response.headers['content-length']).toBe( - stringifiedTarget.length.toString(), - ) - }) - }) - - it('sets header when Content-Type header is not given (number)', async () => { - expect.hasAssertions() + const res = await ts.loadRequestListener(handler).call('/') - const target = 1004 - const handler: RequestListener = (req, res) => { - sendPrismyResult(req, res, Result(target)) - } - - await testHandler(handler, async (url) => { - const response = await got(url) - const stringifiedTarget = JSON.stringify(target) - expect(response.body).toBe(stringifiedTarget) - expect(response.headers['content-length']).toBe( - stringifiedTarget.length.toString(), - ) - expect(response.headers['content-type']).toBe( - 'application/json; charset=utf-8', - ) - }) + expect(await res.json()).toEqual(777) + expect(res.headers.get('content-type')).toBe( + 'application/json; charset=utf-8', + ) + expect(res.headers.get('content-length')).toEqual( + JSON.stringify(target).length.toString(), + ) }) - it('sends with header', async () => { - expect.hasAssertions() - + it('sets headers', async () => { const handler: RequestListener = (req, res) => { sendPrismyResult( req, res, - Result(null, 200, { - test: 'test value', + Result(null, 201, { + test1: 'test value1', + test2: 'test value2', }), ) } - - await testHandler(handler, async (url) => { - const response = await got(url) - expect(response.body).toBeFalsy() - expect(response.headers['test']).toEqual('test value') - }) + const res = await ts.loadRequestListener(handler).call('/') + expect(res.headers.get('test1')).toBe('test value1') + expect(res.headers.get('test2')).toBe('test value2') + expect(res.status).toBe(201) }) }) diff --git a/src/send.ts b/src/send.ts index dd24524..c6cf958 100644 --- a/src/send.ts +++ b/src/send.ts @@ -21,7 +21,7 @@ export const sendPrismyResult = ( sendable.body(request, response) return } - const { statusCode = 200, body, headers = [] } = sendable + const { statusCode, body, headers } = sendable Object.entries(headers).forEach(([key, value]) => { /* istanbul ignore if */ if (value == null) { From 7e5df26c98748eed124a3203bf3a77c9c9f374b1 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Wed, 24 Jan 2024 18:17:33 +0900 Subject: [PATCH 068/109] Fix cookie test coverage --- specs/selectors/cookie.spec.ts | 39 +++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/specs/selectors/cookie.spec.ts b/specs/selectors/cookie.spec.ts index 22c2e60..cf2dffc 100644 --- a/specs/selectors/cookie.spec.ts +++ b/specs/selectors/cookie.spec.ts @@ -1,12 +1,14 @@ -import { testServerManager } from '../helpers' import { CookieSelector, Handler, Result } from '../../src' +import { TestServer } from '../../src/test' + +const ts = TestServer() beforeAll(async () => { - await testServerManager.start() + await ts.start() }) afterAll(async () => { - await testServerManager.close() + await ts.close() }) describe('CookieSelector', () => { @@ -15,21 +17,28 @@ describe('CookieSelector', () => { return Result({ cookieValue }) }) - const response = await testServerManager.loadAndCall( - handler, - '/test?query=true', - { - headers: { - cookie: 'test=Hello!', - }, + const res = await ts.load(handler).call('/', { + headers: { + cookie: 'test=Hello!', }, - ) - - expect(response).toMatchObject({ - statusCode: 200, }) - expect(JSON.parse(response.body)).toMatchObject({ + + expect(await res.json()).toMatchObject({ cookieValue: 'Hello!', }) + expect(res.status).toBe(200) + }) + + it('selects null if cookie is not set', async () => { + const handler = Handler([CookieSelector('test')], (cookieValue) => { + return Result({ cookieValue }) + }) + + const res = await ts.load(handler).call('/') + + expect(res.status).toBe(200) + expect(await res.json()).toMatchObject({ + cookieValue: null, + }) }) }) From 6d7c304e4d55d1e9e85b776ad8b37e8e7c00fa51 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Wed, 24 Jan 2024 19:02:48 +0900 Subject: [PATCH 069/109] Remove logging --- src/result.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/result.ts b/src/result.ts index f6263ce..2e5736c 100644 --- a/src/result.ts +++ b/src/result.ts @@ -94,7 +94,7 @@ export class PrismyResult { ) { const existingSetCookieHeaders = this.headers['set-cookie'] const newSetCookieHeader = cookie.serialize(key, value, options) - console.log(existingSetCookieHeaders) + return this.updateHeaders({ 'set-cookie': existingSetCookieHeaders == null From 3e85d60db672781c326def83f8fd1baf8f28ef10 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Wed, 24 Jan 2024 19:03:11 +0900 Subject: [PATCH 070/109] Fix test coverage of TestServer --- specs/test.spec.ts | 16 ++++++++++++++++ src/test.ts | 25 ++++++++++--------------- 2 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 specs/test.spec.ts diff --git a/specs/test.spec.ts b/specs/test.spec.ts new file mode 100644 index 0000000..bb1e73a --- /dev/null +++ b/specs/test.spec.ts @@ -0,0 +1,16 @@ +import { TestServer } from '../src/test' + +describe('PrismyTestServer', () => { + it('throws when calling server without starting', async () => { + const error = await TestServer() + .call() + .catch((error) => error) + expect(error.toString()).toMatch('PrismyTestServer: Server is not ready.') + }) + + it('default listener throws', () => { + expect(TestServer().listener).toThrow( + 'PrismyTestServer: Listener is not set.', + ) + }) +}) diff --git a/src/test.ts b/src/test.ts index 27f3261..3d17d8c 100644 --- a/src/test.ts +++ b/src/test.ts @@ -5,10 +5,10 @@ import { prismy, PrismyHandler } from './' export class PrismyTestServer { server: http.Server | null = null url: string = '' + /* istanbul ignore next */ listener: RequestListener = () => { - throw new Error('PrismyTestServer: Listener is not set') + throw new Error('PrismyTestServer: Listener is not set.') } - status: 'idle' | 'starting' | 'closing' = 'idle' loadRequestListener(listener: RequestListener) { this.listener = listener @@ -24,17 +24,16 @@ export class PrismyTestServer { this.listener(req, res) } - call(url: string = '/', options?: RequestInit) { - return fetch(this.url + url, options) - } - - async start() { - if (this.status !== 'idle') { + async call(url: string = '/', options?: RequestInit) { + if (this.server == null) { throw new Error( - `Cannot start test server (Current status: ${this.status})`, + 'PrismyTestServer: Server is not ready. Please call `.start()` and wait till it finish.', ) } + return fetch(this.url + url, options) + } + async start() { if (this.server == null) { const server = new http.Server(this.listen.bind(this)) const url = await listen(server) @@ -45,12 +44,7 @@ export class PrismyTestServer { } async close() { - if (this.status !== 'idle') { - throw new Error( - `Cannot close test server (Current status: ${this.status})`, - ) - } - + /* istanbul ignore next */ if (this.server == null) { return } @@ -61,6 +55,7 @@ export class PrismyTestServer { await new Promise((resolve, reject) => { server!.close((error) => { + /* istanbul ignore next */ if (error != null) { reject(error) } else { From 261a4bea2a3478cc61c86980637ea15d7a592340 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Wed, 24 Jan 2024 23:31:02 +0900 Subject: [PATCH 071/109] Refactor middleware test --- specs/middleware.spec.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 06c244c..1e76e08 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,13 +1,15 @@ import { Result, Middleware, getPrismyContext, Handler } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' -import { testServerManager } from './helpers' +import { TestServer } from '../src/test' + +const ts = TestServer() beforeAll(async () => { - await testServerManager.start() + await ts.start() }) afterAll(async () => { - await testServerManager.close() + await ts.close() }) describe('middleware', () => { @@ -33,9 +35,10 @@ describe('middleware', () => { [errorMiddleware], ) - const response = await testServerManager.loadAndCall(handler) + const res = await ts.load(handler).call() - expect(response).toMatchObject({ statusCode: 500, body: '/ : Hey!' }) + expect(await res.text()).toBe('/ : Hey!') + expect(res.status).toBe(500) }) it('accepts async selectors', async () => { @@ -60,8 +63,9 @@ describe('middleware', () => { [errorMiddleware], ) - const response = await testServerManager.loadAndCall(handler) + const res = await ts.load(handler).call() - expect(response).toMatchObject({ statusCode: 500, body: '/ : Hey!' }) + expect(await res.text()).toBe('/ : Hey!') + expect(res.status).toBe(500) }) }) From e5d4c08f482f369b0d0f1bfc0bc2f45c6b8d0a30 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Wed, 24 Jan 2024 23:31:11 +0900 Subject: [PATCH 072/109] Refactor prismy tests --- specs/handler.spec.ts | 18 +++++++ specs/prismy.spec.ts | 121 +++++++++++++++++------------------------- 2 files changed, 67 insertions(+), 72 deletions(-) create mode 100644 specs/handler.spec.ts diff --git a/specs/handler.spec.ts b/specs/handler.spec.ts new file mode 100644 index 0000000..dc62c2d --- /dev/null +++ b/specs/handler.spec.ts @@ -0,0 +1,18 @@ +import { Handler, Result, createPrismySelector, getPrismyContext } from '../src' + +describe('Handler', () => { + it('exposes raw prismy handler for unit tests', () => { + const rawUrlSelector = createPrismySelector( + () => getPrismyContext().req.url!, + ) + const handler = Handler([rawUrlSelector], (url) => Result(url)) + + const result = handler.handlerFunction('Hello, World!') + + expect(result).toMatchObject({ + body: 'Hello, World!', + headers: {}, + statusCode: 200, + }) + }) +}) diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 697398b..00e2647 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -6,27 +6,26 @@ import { ErrorResult, } from '../src' import { createPrismySelector } from '../src/selectors/createSelector' -import { Handler } from '../src/handler' -import { testServerManager } from './helpers' +import { TestServer } from '../src/test' + +const ts = TestServer() beforeAll(async () => { - await testServerManager.start() + await ts.start() }) afterAll(async () => { - await testServerManager.close() + await ts.close() }) describe('prismy', () => { it('returns node.js request handler', async () => { - const handler = prismy([], () => Result('Hello, World!')) + const listener = prismy([], () => Result('Hello, World!')) - const response = await testServerManager.loadRequestListenerAndCall(handler) + const res = await ts.loadRequestListener(listener).call() - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) + expect(await res.text()).toBe('Hello, World!') + expect(res.status).toBe(200) }) it('selects value from context via selector', async () => { @@ -34,44 +33,12 @@ describe('prismy', () => { const { req } = getPrismyContext() return req.url! }) - const handler = prismy([rawUrlSelector], (url) => Result(url)) + const listener = prismy([rawUrlSelector], (url) => Result(url)) - const response = await testServerManager.loadRequestListenerAndCall(handler) + const res = await ts.loadRequestListener(listener).call() - expect(response).toMatchObject({ - statusCode: 200, - body: '/', - }) - }) - - it('selects value from context via selector', async () => { - const asyncRawUrlSelector = createPrismySelector( - async () => getPrismyContext().req.url!, - ) - const handler = prismy([asyncRawUrlSelector], (url) => Result(url)) - - const response = await testServerManager.loadRequestListenerAndCall(handler) - - expect(response).toMatchObject({ - statusCode: 200, - body: '/', - }) - }) - - // TODO: move to handler.spec.ts - it('exposes raw prismy handler for unit tests', () => { - const rawUrlSelector = createPrismySelector( - () => getPrismyContext().req.url!, - ) - const handler = Handler([rawUrlSelector], (url) => Result(url)) - - const result = handler.handlerFunction('Hello, World!') - - expect(result).toMatchObject({ - body: 'Hello, World!', - headers: {}, - statusCode: 200, - }) + expect(await res.text()).toBe('/') + expect(res.status).toBe(200) }) it('applies middleware', async () => { @@ -87,7 +54,7 @@ describe('prismy', () => { const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) - const handler = prismy( + const listener = prismy( [rawUrlSelector], (url) => { throw new Error('Hey!') @@ -95,15 +62,13 @@ describe('prismy', () => { [errorMiddleware], ) - const response = await testServerManager.loadRequestListenerAndCall(handler) + const res = await ts.loadRequestListener(listener).call() - expect(response).toMatchObject({ - statusCode: 500, - body: 'Hey!', - }) + expect(await res.text()).toBe('Hey!') + expect(res.status).toBe(500) }) - it('applies middleware orderly', async () => { + it('applies middleware in order (later = deeper)', async () => { const problematicMiddleware = Middleware([], (next) => () => { throw new Error('Hey!') }) @@ -111,7 +76,7 @@ describe('prismy', () => { try { return await next() } catch (error) { - return ErrorResult(500, (error as any).message) + return ErrorResult(500, 'Something is wrong!') } }) const rawUrlSelector = createPrismySelector( @@ -125,16 +90,14 @@ describe('prismy', () => { [problematicMiddleware, errorMiddleware], ) - const response = await testServerManager.loadRequestListenerAndCall(handler) + const res = await ts.loadRequestListener(handler).call() - expect(response).toMatchObject({ - statusCode: 500, - body: 'Hey!', - }) + expect(await res.text()).toBe('Something is wrong!') + expect(res.status).toBe(500) }) - it('handles unhandled errors from handlers', async () => { - const handler = prismy( + it('handles errors from handlers by default', async () => { + const listener = prismy( [], () => { throw new Error('Hey!') @@ -142,19 +105,17 @@ describe('prismy', () => { [], ) - const response = await testServerManager.loadRequestListenerAndCall(handler) + const res = await ts.loadRequestListener(listener).call() - expect(response).toMatchObject({ - statusCode: 500, - body: expect.stringContaining('Error: Hey!'), - }) + expect(await res.text()).toContain('Error: Hey!') + expect(res.status).toBe(500) }) - it('handles unhandled errors from selectors', async () => { + it('handles errors from selectors by default', async () => { const rawUrlSelector = createPrismySelector(() => { throw new Error('Hey!') }) - const handler = prismy( + const listener = prismy( [rawUrlSelector], (url) => { return Result(url) @@ -162,12 +123,28 @@ describe('prismy', () => { [], ) - const response = await testServerManager.loadRequestListenerAndCall(handler) + const res = await ts.loadRequestListener(listener).call() + + expect(await res.text()).toContain('Error: Hey!') + expect(res.status).toBe(500) + }) - expect(response).toMatchObject({ - statusCode: 500, - body: expect.stringContaining('Error: Hey!'), + it('handles errors from middleware by default', async () => { + const middleware = Middleware([], (next) => () => { + throw new Error('Hey!') }) + const listener = prismy( + [], + () => { + return Result('Hello!') + }, + [middleware], + ) + + const res = await ts.loadRequestListener(listener).call() + + expect(await res.text()).toContain('Error: Hey!') + expect(res.status).toBe(500) }) }) From 6fcb9045a7142d4c41260fec99d991f7a8199e54 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Wed, 24 Jan 2024 23:31:29 +0900 Subject: [PATCH 073/109] Refactor handler --- src/handler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/handler.ts b/src/handler.ts index 449b389..2b01081 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -33,18 +33,18 @@ export class PrismyHandler< const pipe = this.middlewareList.reduce((next, middleware) => { return middleware.pipe(next) }, next) - let resObject + let result: PrismyResult try { - resObject = await pipe() + result = await pipe() } catch (error) { /* istanbul ignore next */ if (process.env.NODE_ENV !== 'test') { console.error(error) } - resObject = createErrorResObject(error) + result = createErrorResObject(error) } - return resObject + return result } } From f4f948c6d8ff3929b0d66041d90aa3de48c7f98c Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Wed, 24 Jan 2024 23:38:08 +0900 Subject: [PATCH 074/109] Refactor result test --- specs/result.spec.ts | 99 +++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 57 deletions(-) diff --git a/specs/result.spec.ts b/specs/result.spec.ts index 86cf380..56cd71c 100644 --- a/specs/result.spec.ts +++ b/specs/result.spec.ts @@ -1,12 +1,14 @@ import { Redirect, Result, Handler, ErrorResult, PrismyResult } from '../src' -import { testServerManager } from './helpers' +import { TestServer } from '../src/test' + +const ts = TestServer() beforeAll(async () => { - await testServerManager.start() + await ts.start() }) afterAll(async () => { - await testServerManager.close() + await ts.close() }) describe('ErrorResult', () => { @@ -24,8 +26,9 @@ describe('PrismyResult', () => { it('set body', async () => { const handler = Handler([], () => Result('Hello!').setBody('Hola!')) - const response = await testServerManager.loadAndCall(handler) - expect(response.body).toBe('Hola!') + const res = await ts.load(handler).call() + + expect(await res.text()).toBe('Hola!') }) it('sets status code', async () => { @@ -33,12 +36,10 @@ describe('PrismyResult', () => { Result('Hello, World!').setStatusCode(201), ) - const response = await testServerManager.loadAndCall(handler) + const res = await ts.load(handler).call() - expect(response).toMatchObject({ - statusCode: 201, - body: 'Hello, World!', - }) + expect(await res.text()).toBe('Hello, World!') + expect(res.status).toBe(201) }) }) @@ -52,14 +53,12 @@ describe('PrismyResult', () => { }), ) - const response = await testServerManager.loadAndCall(handler) + const res = await ts.load(handler).call() - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) - expect(response.headers.get('existing-header')).toBe('Hello') - expect(response.headers.get('new-header')).toBe('Hola') + expect(await res.text()).toBe('Hello, World!') + expect(res.status).toBe(200) + expect(res.headers.get('existing-header')).toBe('Hello') + expect(res.headers.get('new-header')).toBe('Hola') }) it('replaces existing headers if duplicated, but other headers are still intact', async () => { @@ -72,14 +71,12 @@ describe('PrismyResult', () => { }), ) - const response = await testServerManager.loadAndCall(handler) + const res = await ts.load(handler).call() - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) - expect(response.headers.get('existing-header')).toBe('Hola') - expect(response.headers.get('other-existing-header')).toBe('World') + expect(await res.text()).toBe('Hello, World!') + expect(res.status).toBe(200) + expect(res.headers.get('existing-header')).toBe('Hola') + expect(res.headers.get('other-existing-header')).toBe('World') }) }) @@ -93,14 +90,12 @@ describe('PrismyResult', () => { }), ) - const response = await testServerManager.loadAndCall(handler) + const res = await ts.load(handler).call() - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) - expect(response.headers.get('existing-header')).toBe(null) - expect(response.headers.get('new-header')).toBe('Hola') + expect(await res.text()).toBe('Hello, World!') + expect(res.status).toBe(200) + expect(res.headers.get('existing-header')).toBeNull() + expect(res.headers.get('new-header')).toBe('Hola') }) }) }) @@ -109,27 +104,23 @@ describe('Redirect', () => { it('redirects', async () => { const handler = Handler([], () => Redirect('https://github.com/')) - const response = await testServerManager.loadAndCall(handler, '/', { + const res = await ts.load(handler).call('/', { redirect: 'manual', }) - expect(response).toMatchObject({ - statusCode: 302, - }) - expect(response.headers.get('location')).toBe('https://github.com/') + expect(res.status).toBe(302) + expect(res.headers.get('location')).toBe('https://github.com/') }) it('sets statusCode', async () => { const handler = Handler([], () => Redirect('https://github.com/', 301)) - const response = await testServerManager.loadAndCall(handler, '/', { + const res = await ts.load(handler).call('/', { redirect: 'manual', }) - expect(response).toMatchObject({ - statusCode: 301, - }) - expect(response.headers.get('location')).toBe('https://github.com/') + expect(res.status).toBe(301) + expect(res.headers.get('location')).toBe('https://github.com/') }) it('sets headers', async () => { @@ -139,15 +130,13 @@ describe('Redirect', () => { }), ) - const response = await testServerManager.loadAndCall(handler, '/', { + const res = await ts.load(handler).call('/', { redirect: 'manual', }) - expect(response).toMatchObject({ - statusCode: 302, - }) - expect(response.headers.get('location')).toBe('https://github.com/') - expect(response.headers.get('custom-header')).toBe('Hello!') + expect(res.status).toBe(302) + expect(res.headers.get('location')).toBe('https://github.com/') + expect(res.headers.get('custom-header')).toBe('Hello!') }) it('sets cookies', async () => { @@ -162,12 +151,10 @@ describe('Redirect', () => { }), ) - const response = await testServerManager.loadAndCall(handler, '/') + const res = await ts.load(handler).call('/') - expect(response).toMatchObject({ - statusCode: 200, - }) - expect(response.headers.getSetCookie()).toEqual([ + expect(res.status).toBe(200) + expect(res.headers.getSetCookie()).toEqual([ 'testCookie=testValue; Domain=https://example.com; Secure', 'testCookie2=testValue2; HttpOnly', ]) @@ -182,12 +169,10 @@ describe('Redirect', () => { .setCookie('testCookie2', 'testValue2'), ) - const response = await testServerManager.loadAndCall(handler, '/') + const res = await ts.load(handler).call('/') - expect(response).toMatchObject({ - statusCode: 200, - }) - expect(response.headers.getSetCookie()).toEqual([ + expect(res.status).toBe(200) + expect(res.headers.getSetCookie()).toEqual([ 'testCookie=testValue', 'testCookie2=testValue2', ]) From b589a0a9eac5ab699eadd3bc0d58885b95802aa8 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Fri, 26 Jan 2024 09:57:33 +0900 Subject: [PATCH 075/109] Refactor bodyReaders --- specs/bodyReaders.spec.ts | 262 ++++++++++++++------------------------ src/bodyReaders.ts | 4 +- 2 files changed, 100 insertions(+), 166 deletions(-) diff --git a/specs/bodyReaders.spec.ts b/specs/bodyReaders.spec.ts index 329ca58..d9f26b0 100644 --- a/specs/bodyReaders.spec.ts +++ b/specs/bodyReaders.spec.ts @@ -1,91 +1,43 @@ -import got from 'got' import getRawBody from 'raw-body' -import { Middleware, prismy, Result, getPrismyContext } from '../src' +import { Result, getPrismyContext, Handler } from '../src' import { readBufferBody, readJsonBody, readTextBody } from '../src/bodyReaders' -import { createPrismySelector } from '../src/selectors/createSelector' -import { testHandler } from './helpers' +import { TestServer } from '../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('readBufferBody', () => { it('reads buffer body from a request', async () => { - expect.hasAssertions() - - const handler = prismy([], async () => { + const targetBuffer = Buffer.from('Hello, world!') + const handler = Handler([], async () => { const { req } = getPrismyContext() + const body = await readBufferBody(req) return Result(body) }) - await testHandler(handler, async (url) => { - const targetBuffer = Buffer.from('Hello, world!') - const responsePromise = got(url, { - method: 'POST', - body: targetBuffer, - }) - const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([ - responsePromise, - bufferPromise, - ]) - - expect(buffer.equals(targetBuffer)).toBe(true) - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString(), - ) + const res = await ts.load(handler).call('/', { + method: 'post', + body: targetBuffer, }) - }) - - it('reads buffer body regardless delaying', async () => { - expect.hasAssertions() - - const handler = prismy( - [ - createPrismySelector(() => { - return new Promise((resolve) => { - setTimeout(resolve, 1000) - }) - }), - ], - async (_) => { - const { req } = getPrismyContext() - const body = await readBufferBody(req) - return Result(body) - }, - [ - Middleware([], (next) => async () => { - try { - return await next() - } catch (error) { - console.error(error) - throw error - } - }), - ], + const resBuffer = Buffer.from(await res.arrayBuffer()) + expect(resBuffer.equals(targetBuffer)).toBeTruthy() + expect(res.headers.get('content-length')).toBe( + targetBuffer.length.toString(), ) - - await testHandler(handler, async (url) => { - const targetBuffer = Buffer.from('Hello, world!') - const responsePromise = got(url, { - method: 'POST', - body: targetBuffer, - }) - const bufferPromise = responsePromise.buffer() - const [response, buffer] = await Promise.all([ - responsePromise, - bufferPromise, - ]) - - expect(buffer.equals(targetBuffer)).toBe(true) - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString(), - ) - }) }) - it('returns cached buffer if it is read already', async () => { - expect.hasAssertions() - - const handler = prismy([], async () => { + it('returns cached buffer if it has been read already', async () => { + const targetBuffer = Buffer.from('Hello, world!') + const handler = Handler([], async () => { const { req } = getPrismyContext() const body1 = await readBufferBody(req) const body2 = await readBufferBody(req) @@ -95,71 +47,62 @@ describe('readBufferBody', () => { }) }) - await testHandler(handler, async (url) => { - const targetBuffer = Buffer.from('Hello, world!') - const result = await got(url, { - method: 'POST', - body: targetBuffer, - }).json() + const res = await ts.load(handler).call('/', { + method: 'post', + body: targetBuffer, + }) - expect((result as any).isCached).toBe(true) + expect(await res.json()).toEqual({ + isCached: true, }) }) it('throws 413 error if the request body is bigger than limits', async () => { - expect.hasAssertions() + const targetBuffer = Buffer.from( + 'Peter Piper picked a peck of pickled peppers', + ) - const handler = prismy([], async () => { + const handler = Handler([], async () => { const { req } = getPrismyContext() const body = await readBufferBody(req, { limit: '1 byte' }) return Result(body) }) - await testHandler(handler, async (url) => { - const targetBuffer = Buffer.from( - 'Peter Piper picked a peck of pickled peppers', - ) - const response = await got(url, { - throwHttpErrors: false, - method: 'POST', - responseType: 'json', - body: targetBuffer, - }) - - expect(response.statusCode).toBe(413) - expect(response.body).toMatch('Body exceeded 1 byte limit') + const res = await ts.load(handler).call('/', { + method: 'post', + body: targetBuffer, }) + expect(await res.text()).toContain( + 'Error: Body is too large (limit: 1 byte)', + ) + expect(res.status).toBe(413) }) it('throws 400 error if encoding of request body is invalid', async () => { - expect.hasAssertions() - - const handler = prismy([], async () => { + const targetBuffer = Buffer.from('Hello, world!') + const handler = Handler([], async () => { const { req } = getPrismyContext() const body = await readBufferBody(req, { encoding: 'lol' }) return Result(body) }) - await testHandler(handler, async (url) => { - const targetBuffer = Buffer.from('Hello, world!') - const response = await got(url, { - throwHttpErrors: false, - method: 'POST', - responseType: 'json', - body: targetBuffer, - }) - - expect(response.statusCode).toBe(400) - expect(response.body).toMatch('Invalid body') + const res = await ts.load(handler).call('/', { + method: 'post', + headers: { + 'content-type': 'application/json', + }, + body: targetBuffer, }) + + expect(await res.text()).toContain('Invalid body') + expect(res.status).toBe(400) }) it('throws 500 error if the request is drained already', async () => { - expect.hasAssertions() - - const handler = prismy([], async () => { + const targetBuffer = Buffer.from('Oops!') + const handler = Handler([], async () => { const { req } = getPrismyContext() const length = req.headers['content-length'] await getRawBody(req, { limit: '1mb', length }) @@ -168,93 +111,82 @@ describe('readBufferBody', () => { return Result(body) }) - await testHandler(handler, async (url) => { - const targetBuffer = Buffer.from('Oops!') - const response = await got(url, { - throwHttpErrors: false, - method: 'POST', - responseType: 'json', - body: targetBuffer, - }) - - expect(response.statusCode).toBe(500) - expect(response.body).toMatch('The request has already been drained') + const res = await ts.load(handler).call('/', { + method: 'post', + headers: { + 'content-type': 'application/json', + }, + body: targetBuffer, }) + + expect(await res.text()).toContain('The request has already been drained') }) }) describe('readTextBody', () => { it('reads text from request body', async () => { - expect.hasAssertions() - - const handler = prismy([], async () => { + const targetBuffer = Buffer.from('Hello, World!') + const handler = Handler([], async () => { const { req } = getPrismyContext() const body = await readTextBody(req) return Result(body) }) - await testHandler(handler, async (url) => { - const targetBuffer = Buffer.from('Hello, world!') - const response = await got(url, { - method: 'POST', - body: targetBuffer, - }) - expect(response.body).toBe('Hello, world!') - expect(response.headers['content-length']).toBe( - targetBuffer.length.toString(), - ) + const res = await ts.load(handler).call('/', { + method: 'post', + body: targetBuffer, }) + + expect(await res.text()).toBe('Hello, World!') + expect(res.headers.get('content-length')).toBe( + targetBuffer.length.toString(), + ) }) }) describe('readJsonBody', () => { it('reads and parse JSON from a request body', async () => { - expect.hasAssertions() - - const handler = prismy([], async () => { + const targetObject = { + foo: 'bar', + } + const stringifiedTargetObject = JSON.stringify(targetObject) + const handler = Handler([], async () => { const { req } = getPrismyContext() const body = await readJsonBody(req) return Result(body) }) - await testHandler(handler, async (url) => { - const target = { - foo: 'bar', - } - const response = await got(url, { - method: 'POST', - responseType: 'json', - json: target, - }) - expect(response.body).toMatchObject(target) - expect(response.headers['content-length']).toBe( - JSON.stringify(target).length.toString(), - ) + const res = await ts.load(handler).call('/', { + method: 'post', + body: stringifiedTargetObject, }) + + expect(await res.text()).toEqual(stringifiedTargetObject) + expect(res.headers.get('content-length')).toBe( + stringifiedTargetObject.length.toString(), + ) }) it('throws 400 error if the JSON body is invalid', async () => { - expect.hasAssertions() - - const handler = prismy([], async () => { + const target = 'Oopsie! This is definitely not a JSON body' + const handler = Handler([], async () => { const { req } = getPrismyContext() const body = await readJsonBody(req) return Result(body) }) - await testHandler(handler, async (url) => { - const target = 'Oopsie' - const response = await got(url, { - throwHttpErrors: false, - method: 'POST', - responseType: 'json', - body: target, - }) - expect(response.statusCode).toBe(400) - expect(response.body).toMatch('Error: Invalid JSON') + const res = await ts.load(handler).call('/', { + method: 'post', + headers: { + 'content-type': 'application/json', + }, + body: target, }) + + expect(await res.text()).toContain('Invalid JSON') + expect(res.status).toBe(400) }) }) diff --git a/src/bodyReaders.ts b/src/bodyReaders.ts index 9649e87..a399286 100644 --- a/src/bodyReaders.ts +++ b/src/bodyReaders.ts @@ -9,6 +9,8 @@ const rawBodyMap = new WeakMap() /** * An async function to buffer the incoming request body * + * **This method is not checking content-type of request header. Please check it manually or use JsonBodySelector.** + * * @remarks * Can be called multiple times, as it caches the raw request body the first time * @@ -39,7 +41,7 @@ export const readBufferBody = async ( return buffer } catch (error) { if ((error as any).type === 'entity.too.large') { - throw createError(413, `Body exceeded ${limit} limit`, error) + throw createError(413, `Body is too large (limit: ${limit})`, error) } else { throw createError(400, `Invalid body`, error) } From e5a27074dd67c52c5c10079cf141fbe3aba4548c Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 12 Feb 2024 15:45:54 +0900 Subject: [PATCH 076/109] Remove deprecated methods --- specs/selectors/body.spec.ts | 2 +- specs/selectors/bufferBody.spec.ts | 2 +- specs/selectors/query.spec.ts | 38 ----------------------- specs/selectors/textBody.spec.ts | 2 +- specs/selectors/urlEncodedBody.spec.ts | 2 +- src/selectors/bufferBody.ts | 5 --- src/selectors/index.ts | 1 - src/selectors/jsonBody.ts | 5 --- src/selectors/query.ts | 42 -------------------------- src/selectors/textBody.ts | 5 --- src/selectors/urlEncodedBody.ts | 5 --- 11 files changed, 4 insertions(+), 105 deletions(-) delete mode 100644 specs/selectors/query.spec.ts delete mode 100644 src/selectors/query.ts diff --git a/specs/selectors/body.spec.ts b/specs/selectors/body.spec.ts index 57dedcd..c2a0373 100644 --- a/specs/selectors/body.spec.ts +++ b/specs/selectors/body.spec.ts @@ -3,7 +3,7 @@ import { testHandler } from '../helpers' import { BodySelector } from '../../src/selectors' import { prismy, Result } from '../../src' -describe('createBodySelector', () => { +describe('BodySelector', () => { it('returns text body', async () => { expect.hasAssertions() const handler = prismy([BodySelector()], (body) => { diff --git a/specs/selectors/bufferBody.spec.ts b/specs/selectors/bufferBody.spec.ts index 38d0b87..4ffbf98 100644 --- a/specs/selectors/bufferBody.spec.ts +++ b/specs/selectors/bufferBody.spec.ts @@ -2,7 +2,7 @@ import got from 'got' import { testHandler } from '../helpers' import { BufferBodySelector, prismy, Result } from '../../src' -describe('createBufferBodySelector', () => { +describe('BufferBodySelector', () => { it('creates buffer body selector', async () => { const handler = prismy([BufferBodySelector()], (body) => { return Result(`${body.constructor.name}: ${body}`) diff --git a/specs/selectors/query.spec.ts b/specs/selectors/query.spec.ts deleted file mode 100644 index 2804e03..0000000 --- a/specs/selectors/query.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { querySelector, prismy, Result } from '../../src' - -describe('querySelector', () => { - it('selects query', async () => { - const handler = prismy([querySelector], (query) => { - return Result(query) - }) - - await testHandler(handler, async (url) => { - const response = await got(url, { - searchParams: { message: 'Hello, World!' }, - responseType: 'json', - }) - - expect(response).toMatchObject({ - statusCode: 200, - body: { message: 'Hello, World!' }, - }) - }) - }) - - it('reuses parsed query', async () => { - const handler = prismy([querySelector, querySelector], (query, query2) => { - return Result(JSON.stringify(query === query2)) - }) - - await testHandler(handler, async (url) => { - const response = await got(url) - - expect(response).toMatchObject({ - statusCode: 200, - body: 'true', - }) - }) - }) -}) diff --git a/specs/selectors/textBody.spec.ts b/specs/selectors/textBody.spec.ts index c969e3b..63aa633 100644 --- a/specs/selectors/textBody.spec.ts +++ b/specs/selectors/textBody.spec.ts @@ -2,7 +2,7 @@ import got from 'got' import { testHandler } from '../helpers' import { prismy, Result, TextBodySelector } from '../../src' -describe('createTextBodySelector', () => { +describe('TextBodySelector', () => { it('creates buffer body selector', async () => { const handler = prismy([TextBodySelector()], (body) => { return Result(`${body.constructor.name}: ${body}`) diff --git a/specs/selectors/urlEncodedBody.spec.ts b/specs/selectors/urlEncodedBody.spec.ts index c6eaf5d..60d69b7 100644 --- a/specs/selectors/urlEncodedBody.spec.ts +++ b/specs/selectors/urlEncodedBody.spec.ts @@ -2,7 +2,7 @@ import got from 'got' import { testHandler } from '../helpers' import { prismy, Result, UrlEncodedBodySelector } from '../../src' -describe('URLEncodedBody', () => { +describe('UrlEncodedBody', () => { it('injects parsed url encoded body', async () => { const handler = prismy([UrlEncodedBodySelector()], (body) => { return Result(body) diff --git a/src/selectors/bufferBody.ts b/src/selectors/bufferBody.ts index b4bc2af..bc41e97 100644 --- a/src/selectors/bufferBody.ts +++ b/src/selectors/bufferBody.ts @@ -45,8 +45,3 @@ export function BufferBodySelector( return readBufferBody(req, options) }) } - -/** - * @deprecated Use `BufferBodySelector` - */ -export const createBufferBodySelector = BufferBodySelector diff --git a/src/selectors/index.ts b/src/selectors/index.ts index 3e530a2..d46ab0b 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -3,7 +3,6 @@ export * from './bufferBody' export * from './headers' export * from './jsonBody' export * from './method' -export * from './query' export * from './searchParam' export * from './url' export * from './urlEncodedBody' diff --git a/src/selectors/jsonBody.ts b/src/selectors/jsonBody.ts index 94bdd1e..3cca617 100644 --- a/src/selectors/jsonBody.ts +++ b/src/selectors/jsonBody.ts @@ -63,8 +63,3 @@ function isContentTypeApplicationJSON(contentType: string | undefined) { if (!contentType.startsWith('application/json')) return false return true } - -/** - * @deprecated Use `JsonBodySelector` - */ -export const createJsonBodySelector = JsonBodySelector diff --git a/src/selectors/query.ts b/src/selectors/query.ts deleted file mode 100644 index c89b0de..0000000 --- a/src/selectors/query.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ParsedUrlQuery, parse } from 'querystring' -import { prismyContextStorage } from '../prismy' -import { createPrismySelector, PrismySelector } from './createSelector' -import { urlSelector } from './url' - -const queryMap = new WeakMap() - -/** - * @deprecated Use SearchParamSelector or SearchParamListSelector. - * To get all search params, use urlSelector, which resolves WHATWG URL object, and access `url.searchParams`. - * - * Selector to extract the parsed query from the request URL. - * Using `querystring.parse` internally. - * - * @example - * Simple example - * ```ts - * - * const prismyHandler = prismy( - * [querySelector], - * query => { - * doSomethingWithQuery(query) - * } - * ) - * ``` - * - * @returns a selector for the url query - * - * @public - */ -export const querySelector: PrismySelector = - createPrismySelector(async () => { - const context = prismyContextStorage.getStore()! - let query: ParsedUrlQuery | undefined = queryMap.get(context) - if (query == null) { - const url = await urlSelector.resolve() - /* istanbul ignore next */ - query = url.search != null ? parse(url.search.slice(1)) : {} - queryMap.set(context, query) - } - return query - }) diff --git a/src/selectors/textBody.ts b/src/selectors/textBody.ts index c8363a3..8a6c3cf 100644 --- a/src/selectors/textBody.ts +++ b/src/selectors/textBody.ts @@ -45,8 +45,3 @@ export function TextBodySelector( return readTextBody(req, options) }) } - -/** - * @deprecated Use `TextBodySelector` - */ -export const createTextBodySelector = TextBodySelector diff --git a/src/selectors/urlEncodedBody.ts b/src/selectors/urlEncodedBody.ts index 526160e..1de566a 100644 --- a/src/selectors/urlEncodedBody.ts +++ b/src/selectors/urlEncodedBody.ts @@ -55,8 +55,3 @@ export function UrlEncodedBodySelector( } }) } - -/** - * @deprecated Use `UrlEncodedBodySelector` - */ -export const createUrlEncodedBodySelector = UrlEncodedBodySelector From 71f62ec26504d24072b91a12b6f38be4c51e352a Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 12 Feb 2024 15:46:15 +0900 Subject: [PATCH 077/109] Use camelcase for all selectors --- specs/selectors/headers.spec.ts | 4 ++-- specs/selectors/method.spec.ts | 6 +++--- specs/selectors/url.spec.ts | 8 ++++---- specs/types/basic.ts | 22 +++++++++++++--------- src/router.ts | 4 ++-- src/selectors/headers.ts | 20 ++++++++++++-------- src/selectors/method.ts | 23 ++++++++++++++--------- src/selectors/searchParam.ts | 4 +++- src/selectors/url.ts | 26 +++++++++++++++----------- 9 files changed, 68 insertions(+), 49 deletions(-) diff --git a/specs/selectors/headers.spec.ts b/specs/selectors/headers.spec.ts index 92d0151..15b415f 100644 --- a/specs/selectors/headers.spec.ts +++ b/specs/selectors/headers.spec.ts @@ -1,10 +1,10 @@ import got from 'got' import { testHandler } from '../helpers' -import { headersSelector, prismy, Result } from '../../src' +import { HeadersSelector, prismy, Result } from '../../src' describe('headersSelector', () => { it('select headers', async () => { - const handler = prismy([headersSelector], (headers) => { + const handler = prismy([HeadersSelector()], (headers) => { return Result(headers['x-test']) }) diff --git a/specs/selectors/method.spec.ts b/specs/selectors/method.spec.ts index 7b57e99..81b9693 100644 --- a/specs/selectors/method.spec.ts +++ b/specs/selectors/method.spec.ts @@ -1,10 +1,10 @@ import got from 'got' import { testHandler } from '../helpers' -import { methodSelector, prismy, Result } from '../../src' +import { MethodSelector, prismy, Result } from '../../src' -describe('methodSelector', () => { +describe('MethodSelector', () => { it('selects method', async () => { - const handler = prismy([methodSelector], (method) => { + const handler = prismy([MethodSelector()], (method) => { return Result(method) }) diff --git a/specs/selectors/url.spec.ts b/specs/selectors/url.spec.ts index 6885c43..8775ee8 100644 --- a/specs/selectors/url.spec.ts +++ b/specs/selectors/url.spec.ts @@ -1,10 +1,10 @@ import got from 'got' import { testHandler } from '../helpers' -import { urlSelector, prismy, Result } from '../../src' +import { prismy, Result, UrlSelector } from '../../src' -describe('urlSelector', () => { +describe('UrlSelector', () => { it('selects url', async () => { - const handler = prismy([urlSelector], (url) => { + const handler = prismy([UrlSelector()], (url) => { return Result({ pathname: url.pathname, search: url.search, @@ -27,7 +27,7 @@ describe('urlSelector', () => { }) it('reuses parsed url', async () => { - const handler = prismy([urlSelector, urlSelector], (url, url2) => { + const handler = prismy([UrlSelector(), UrlSelector()], (url, url2) => { return Result(JSON.stringify(url === url2)) }) diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 0eb6681..d0c6da5 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -2,7 +2,7 @@ import { BodySelector, Handler, MaybePromise, - methodSelector, + MethodSelector, Middleware, prismy, PrismyHandler, @@ -11,14 +11,14 @@ import { PrismyRoute, Result, Route, - urlSelector, + UrlSelector, } from '../../src' import { expectType } from '../helpers' import http from 'http' import { InjectSelector } from '../../src/selectors/inject' import { PrismySelector } from '../../src/selectors/createSelector' -const handler1 = Handler([urlSelector, methodSelector], (url, method) => { +const handler1 = Handler([UrlSelector(), MethodSelector()], (url, method) => { expectType(url) expectType(method) return Result('') @@ -40,7 +40,7 @@ http.createServer(prismy(handler1)) Handler([BodySelector], () => Result(null)) const middleware1 = Middleware( - [urlSelector, methodSelector], + [UrlSelector(), MethodSelector()], (next) => async (url, method) => { expectType(url) expectType(method) @@ -76,11 +76,15 @@ const handler = Handler([mailServiceSelector], (mailService) => { const mailHandlerRoute = Route('/', handler) expectType]>>(mailHandlerRoute) -const shortRoute = Route('/', [urlSelector, methodSelector], (url, method) => { - expectType(url) - expectType(method) - return Result(null) -}) +const shortRoute = Route( + '/', + [UrlSelector(), MethodSelector()], + (url, method) => { + expectType(url) + expectType(method) + return Result(null) + }, +) expectType< PrismyRoute<[PrismySelector, PrismySelector]> diff --git a/src/router.ts b/src/router.ts index c2a5609..de4708b 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,5 +1,5 @@ import { MaybePromise, PrismyContext, SelectorReturnTypeTuple } from './types' -import { methodSelector, urlSelector } from './selectors' +import { MethodSelector, UrlSelector } from './selectors' import { match as createMatchFunction } from 'path-to-regexp' import { getPrismyContext } from './prismy' import { createError } from './error' @@ -58,7 +58,7 @@ export function Router( }) return Handler( - [methodSelector, urlSelector], + [MethodSelector(), UrlSelector()], (method, url) => { const prismyContext = getPrismyContext() /* istanbul ignore next */ diff --git a/src/selectors/headers.ts b/src/selectors/headers.ts index f417418..d0e11f6 100644 --- a/src/selectors/headers.ts +++ b/src/selectors/headers.ts @@ -2,27 +2,31 @@ import { IncomingHttpHeaders } from 'http' import { getPrismyContext } from '../prismy' import { createPrismySelector, PrismySelector } from './createSelector' +const headersSelector: PrismySelector = + createPrismySelector(() => { + const { req } = getPrismyContext() + return req.headers + }) + /** - * A selector to extract the headers of a request + * Returns a selector to extract the headers of a request * * @example * Simple example * ```ts * * const prismyHandler = prismy( - * [headerSelector], + * [HeaderSelector()], * headers => { * ... * } * ) * ``` * - * @returns The request headers + * @returns PrismySelector * * @public */ -export const headersSelector: PrismySelector = - createPrismySelector(() => { - const { req } = getPrismyContext() - return req.headers - }) +export function HeadersSelector() { + return headersSelector +} diff --git a/src/selectors/method.ts b/src/selectors/method.ts index 80b886b..bb68c41 100644 --- a/src/selectors/method.ts +++ b/src/selectors/method.ts @@ -1,15 +1,22 @@ import { getPrismyContext } from '../prismy' import { createPrismySelector, PrismySelector } from './createSelector' +const methodSelector: PrismySelector = createPrismySelector( + () => { + const { req } = getPrismyContext() + return req.method + }, +) + /** - * Selector to extract the HTTP method from the request + * Returns a selector to extract the HTTP method from the request * * @example * Simple example * ```ts * * const prismyHandler = prismy( - * [methodSelector], + * [MethodSelector()], * method => { * if (method !== 'GET') { * throw createError(405) @@ -18,13 +25,11 @@ import { createPrismySelector, PrismySelector } from './createSelector' * ) * ``` * - * @param context - The request context - * @returns the http request method + * @returns PrismySelector * * @public */ -export const methodSelector: PrismySelector = - createPrismySelector(() => { - const { req } = getPrismyContext() - return req.method - }) + +export function MethodSelector() { + return methodSelector +} diff --git a/src/selectors/searchParam.ts b/src/selectors/searchParam.ts index 2fab603..de457ec 100644 --- a/src/selectors/searchParam.ts +++ b/src/selectors/searchParam.ts @@ -1,5 +1,7 @@ import { createPrismySelector, PrismySelector } from './createSelector' -import { urlSelector } from './url' +import { UrlSelector } from './url' + +const urlSelector = UrlSelector() /** * Create a selector which resolves the first value of the search param. diff --git a/src/selectors/url.ts b/src/selectors/url.ts index e83e31d..b1e9dfc 100644 --- a/src/selectors/url.ts +++ b/src/selectors/url.ts @@ -4,6 +4,18 @@ import { createPrismySelector } from './createSelector' const urlMap = new WeakMap() +const urlSelector = createPrismySelector((): URL => { + const context = getPrismyContext() + let url: URL | undefined = urlMap.get(context) + if (url == null) { + const { req } = context + /* istanbul ignore next */ + url = new URL(req.url == null ? '' : req.url, `http://${req.headers.host}`) + urlMap.set(context, url) + } + return url +}) + /** * Selector for extracting the requested URL * @@ -23,14 +35,6 @@ const urlMap = new WeakMap() * * @public */ -export const urlSelector = createPrismySelector((): URL => { - const context = getPrismyContext() - let url: URL | undefined = urlMap.get(context) - if (url == null) { - const { req } = context - /* istanbul ignore next */ - url = new URL(req.url == null ? '' : req.url, `http://${req.headers.host}`) - urlMap.set(context, url) - } - return url -}) +export function UrlSelector() { + return urlSelector +} From f2f4a1100cbbd17bbf7879711884eb6bec034b76 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 12 Feb 2024 22:44:16 +0900 Subject: [PATCH 078/109] Fix coverage --- package.json | 2 - readme.md | 2 +- specs/helpers.ts | 74 --------- specs/router.spec.ts | 198 ++++++++----------------- specs/selectors/body.spec.ts | 88 +++++------ specs/selectors/bufferBody.spec.ts | 29 ++-- specs/selectors/headers.spec.ts | 34 +++-- specs/selectors/jsonBody.spec.ts | 93 ++++-------- specs/selectors/method.spec.ts | 32 ++-- specs/selectors/searchParam.spec.ts | 91 +++++------- specs/selectors/textBody.spec.ts | 31 ++-- specs/selectors/url.spec.ts | 43 +++--- specs/selectors/urlEncodedBody.spec.ts | 39 ++--- specs/types/basic.ts | 3 +- src/selectors/body.ts | 10 +- src/selectors/jsonBody.ts | 1 + 16 files changed, 302 insertions(+), 468 deletions(-) delete mode 100644 specs/helpers.ts diff --git a/package.json b/package.json index d6b4171..4eaa0fc 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,7 @@ "@types/cookie": "^0.6.0", "@types/jest": "^29.5.11", "@types/node": "^18.19.5", - "@types/test-listen": "^1.1.0", "codecov": "^3.8.0", - "got": "^11.8.0", "jest": "^29.7.0", "prettier": "^3.1.1", "rimraf": "^3.0.0", diff --git a/readme.md b/readme.md index d833daa..e4ceba7 100644 --- a/readme.md +++ b/readme.md @@ -36,7 +36,7 @@ ## Features -- Very small (No Expressjs, the only deps are micro and tslib) +- Very small (using node.js's http module directly) - Takes advantage of the asynchronous nature of Javascript with full support for async / await - Simple and easy argument injection for handlers (Inspired by ReselectJS) - Completely **TYPE-SAFE** diff --git a/specs/helpers.ts b/specs/helpers.ts deleted file mode 100644 index 59b7501..0000000 --- a/specs/helpers.ts +++ /dev/null @@ -1,74 +0,0 @@ -import http, { RequestListener } from 'http' -import listen from 'async-listen' -import { URL } from 'url' -import { PrismyHandler } from '../src' -import { PrismyTestServer } from '../src/test' - -export type TestCallback = (url: string) => Promise | void - -/* istanbul ignore next */ -export async function testHandler( - handler: RequestListener, - testCallback: TestCallback, -): Promise { - const server = new http.Server(handler) - - const url: URL = await listen(server) - try { - await testCallback(url.origin) - } catch (error) { - throw error - } finally { - server.close() - } -} - -/* istanbul ignore next */ -export function expectType(value: T): void {} - -async function resolveTestResponse(response: Response) { - const testResult = await response.text() - - return { - statusCode: response.status, - body: testResult, - headers: response.headers, - } -} - -let testServer: null | PrismyTestServer = null -export const testServerManager = { - start: () => { - if (testServer == null) { - testServer = new PrismyTestServer() - } - return testServer.start() - }, - close: () => { - if (testServer == null) { - throw new Error('No test server to close') - } - return testServer.close() - }, - load: (handler: PrismyHandler) => testServer!.load(handler), - call: (url?: string, options?: RequestInit) => - testServer!.call(url, options).then(resolveTestResponse), - loadRequestListener: (listener: RequestListener) => - testServer!.loadRequestListener(listener), - loadAndCall: ( - handler: PrismyHandler, - url?: string, - options?: RequestInit, - ) => { - testServer!.load(handler) - return testServer!.call(url, options).then(resolveTestResponse) - }, - loadRequestListenerAndCall: ( - listener: RequestListener, - url?: string, - options?: RequestInit, - ) => { - testServer!.loadRequestListener(listener) - return testServer!.call(url, options).then(resolveTestResponse) - }, -} diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 5e60fbf..dad14fd 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -8,14 +8,16 @@ import { } from '../src' import { Handler } from '../src/handler' import { InjectSelector } from '../src/selectors/inject' -import { testServerManager } from './helpers' +import { TestServer } from '../src/test' + +const ts = TestServer() beforeAll(async () => { - await testServerManager.start() + await ts.start() }) afterAll(async () => { - await testServerManager.close() + await ts.close() }) describe('router', () => { @@ -28,12 +30,9 @@ describe('router', () => { }) const routerHandler = Router([Route('/a', handlerA), Route('/b', handlerB)]) - const response = await testServerManager.loadAndCall(routerHandler, '/b') + const res = await ts.load(routerHandler).call('/b') - expect(response).toMatchObject({ - statusCode: 200, - body: 'b', - }) + expect(await res.text()).toBe('b') }) it('routes with pathname(shorthand)', async () => { @@ -42,12 +41,9 @@ describe('router', () => { Route('/b', [InjectSelector('b')], (data) => Result(data)), ]) - const response = await testServerManager.loadAndCall(routerHandler, '/b') + const res = await ts.load(routerHandler).call('/b') - expect(response).toMatchObject({ - statusCode: 200, - body: 'b', - }) + expect(await res.text()).toBe('b') }) it('routes with method', async () => { @@ -62,25 +58,17 @@ describe('router', () => { Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB), ]) - testServerManager.load(routerHandler) - const response1 = await testServerManager.call('/', { method: 'get' }) + const res1 = await ts.load(routerHandler).call('/') - expect(response1).toMatchObject({ - statusCode: 200, - body: 'a', - }) + expect(await res1.text()).toBe('a') - const response2 = await testServerManager.call('/', { method: 'post' }) + const res2 = await ts.call('/', { method: 'post' }) - expect(response2).toMatchObject({ - statusCode: 200, - body: 'b', - }) + expect(await res2.text()).toBe('b') }) it('throws 404 error when no route found', async () => { - expect.hasAssertions() const handlerA = Handler([], () => { return Result('a') }) @@ -93,18 +81,15 @@ describe('router', () => { Route(['/', 'post'], handlerB), ]) - const response = await testServerManager.loadAndCall(routerHandler, '/', { + const res = await ts.load(routerHandler).call('/', { method: 'put', }) - expect(response).toMatchObject({ - statusCode: 404, - body: expect.stringContaining('Error: Not Found'), - }) + expect(await res.text()).toContain('Error: Not Found') + expect(res.status).toBe(404) }) it('uses custom not found handler if set', async () => { - expect.hasAssertions() const handlerA = Handler([], () => { return Result('a') }) @@ -122,17 +107,15 @@ describe('router', () => { }, ) - const response = await testServerManager.loadAndCall(routerHandler, '/', { + const res = await ts.load(routerHandler).call('/', { method: 'put', }) - expect(response).toMatchObject({ - statusCode: 404, - body: expect.stringContaining('Error: Customized Not Found Response'), - }) + + expect(res.status).toBe(404) + expect(await res.text()).toContain('Error: Customized Not Found Response') }) it('prepends prefix to route path', async () => { - expect.hasAssertions() const handlerA = Handler([], () => { return Result('a') }) @@ -147,19 +130,13 @@ describe('router', () => { }, ) - const response = await testServerManager.loadAndCall( - routerHandler, - '/admin', - ) + const res = await ts.load(routerHandler).call('/admin') - expect(response).toMatchObject({ - statusCode: 200, - body: expect.stringContaining('a'), - }) + expect(res.status).toBe(200) + expect(await res.text()).toContain('a') }) it('prepends prefix to route path (without root `/`)', async () => { - expect.hasAssertions() const handlerA = Handler([], () => { return Result('a') }) @@ -170,19 +147,14 @@ describe('router', () => { const routerHandler = Router( [Route(['/', 'get'], handlerA), Route(['/', 'post'], handlerB)], { - prefix: '/admin', + prefix: 'admin', }, ) - const response = await testServerManager.loadAndCall( - routerHandler, - '/admin', - ) + const res = await ts.load(routerHandler).call('/admin') - expect(response).toMatchObject({ - statusCode: 200, - body: expect.stringContaining('a'), - }) + expect(res.status).toBe(200) + expect(await res.text()).toContain('a') }) it('applies middleware', async () => { @@ -210,10 +182,10 @@ describe('router', () => { ], }) - const response = await testServerManager.loadAndCall(routerHandler) + const res = await ts.load(routerHandler).call() - expect(response.statusCode).toBe(200) - expect(response.body).toBe('ba') + expect(res.status).toBe(200) + expect(await res.text()).toBe('ba') }) }) @@ -232,15 +204,10 @@ describe('RouteParamSelector', () => { Route('/b/:id', handlerB), ]) - const response = await testServerManager.loadAndCall( - routerHandler, - '/b/test-param', - ) + const res = await ts.load(routerHandler).call('/b/test-param') - expect(response).toMatchObject({ - statusCode: 200, - body: '', - }) + expect(res.status).toBe(200) + expect(await res.text()).toBe('') }) it('resolves a param (named parameter)', async () => { @@ -256,15 +223,10 @@ describe('RouteParamSelector', () => { Route('/b/:id', handlerB), ]) - const response = await testServerManager.loadAndCall( - routerHandler, - '/b/test-param', - ) + const res = await ts.load(routerHandler).call('/b/test-param') - expect(response).toMatchObject({ - statusCode: 200, - body: 'test-param', - }) + expect(res.status).toBe(200) + expect(await res.text()).toBe('test-param') }) it('resolves params (custom suffix)', async () => { @@ -291,28 +253,18 @@ describe('RouteParamSelector', () => { Route('/b/:attr1?{-:attr2}?{-:attr3}?', handlerB), ]) - const response1 = await testServerManager.loadAndCall( - routerHandler, - '/b/test1-test2-test3', - ) + const res = await ts.load(routerHandler).call('/b/test1-test2-test3') - expect(response1).toMatchObject({ - statusCode: 200, - }) - expect(JSON.parse(response1.body)).toMatchObject({ + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ attr1: 'test1', attr2: 'test2', attr3: 'test3', }) - const response2 = await testServerManager.loadAndCall( - routerHandler, - '/b/test1-test2', - ) - expect(response2).toMatchObject({ - statusCode: 200, - }) - expect(JSON.parse(response2.body)).toMatchObject({ + const res2 = await ts.call('/b/test1-test2') + expect(res2.status).toBe(200) + expect(await res2.json()).toEqual({ attr1: 'test1', attr2: 'test2', attr3: null, @@ -338,15 +290,10 @@ describe('RouteParamSelector', () => { Route('/b/:id/(.*)', handlerB), ]) - const response = await testServerManager.loadAndCall( - routerHandler, - '/b/test1/test2/test3', - ) + const res = await ts.load(routerHandler).call('/b/test1/test2/test3') - expect(response).toMatchObject({ - statusCode: 200, - }) - expect(JSON.parse(response.body)).toMatchObject({ + expect(res.status).toBe(200) + expect(await res.json()).toMatchObject({ id: 'test1', unnamedParam: 'test2/test3', }) @@ -371,25 +318,18 @@ describe('RouteParamSelector', () => { Route('/b/:param1/:param2?', handlerB), ]) - const response1 = await testServerManager.loadAndCall( - routerHandler, - '/b/test1/test2', - ) + const res1 = await ts.load(routerHandler).call('/b/test1/test2') - expect(response1).toMatchObject({ - statusCode: 200, - }) - expect(JSON.parse(response1.body)).toMatchObject({ + expect(res1.status).toBe(200) + expect(await res1.json()).toEqual({ param1: 'test1', param2: 'test2', }) - const response2 = await testServerManager.call('/b/test1') + const res2 = await ts.load(routerHandler).call('/b/test1') - expect(response2).toMatchObject({ - statusCode: 200, - }) - expect(JSON.parse(response2.body)).toMatchObject({ + expect(res2.status).toBe(200) + expect(await res2.json()).toEqual({ param1: 'test1', param2: null, }) @@ -410,24 +350,17 @@ describe('RouteParamSelector', () => { Route('/b/:param*', handlerB), ]) - const response1 = await testServerManager.loadAndCall( - routerHandler, - '/b/test1/test2', - ) + const res1 = await ts.load(routerHandler).call('/b/test1/test2') - expect(response1).toMatchObject({ - statusCode: 200, - }) - expect(JSON.parse(response1.body)).toMatchObject({ + expect(res1.status).toBe(200) + expect(await res1.json()).toEqual({ param: 'test1', }) - const response2 = await testServerManager.loadAndCall(routerHandler, '/b') + const res2 = await ts.load(routerHandler).call('/b') - expect(response2).toMatchObject({ - statusCode: 200, - }) - expect(JSON.parse(response2.body)).toMatchObject({ + expect(res2.status).toBe(200) + expect(await res2.json()).toEqual({ param: null, }) }) @@ -447,22 +380,15 @@ describe('RouteParamSelector', () => { Route('/b/:param+', handlerB), ]) - const response1 = await testServerManager.loadAndCall( - routerHandler, - '/b/test1/test2', - ) + const res1 = await ts.load(routerHandler).call('/b/test1/test2') - expect(response1).toMatchObject({ - statusCode: 200, - }) - expect(JSON.parse(response1.body)).toMatchObject({ + expect(res1.status).toBe(200) + expect(await res1.json()).toEqual({ param: 'test1', }) - const response2 = await testServerManager.loadAndCall(routerHandler, '/b') + const res2 = await ts.load(routerHandler).call('/b') - expect(response2).toMatchObject({ - statusCode: 404, - }) + expect(res2.status).toBe(404) }) }) diff --git a/specs/selectors/body.spec.ts b/specs/selectors/body.spec.ts index c2a0373..8d845dc 100644 --- a/specs/selectors/body.spec.ts +++ b/specs/selectors/body.spec.ts @@ -1,70 +1,60 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { BodySelector } from '../../src/selectors' -import { prismy, Result } from '../../src' +import { Handler, Result, BodySelector } from '../../src' +import { TestServer } from '../../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('BodySelector', () => { - it('returns text body', async () => { - expect.hasAssertions() - const handler = prismy([BodySelector()], (body) => { - return Result(`${body.constructor.name}: ${body}`) + it('selects text body', async () => { + const handler = Handler([BodySelector()], (body) => { + return Result(body) }) - await testHandler(handler, async (url) => { - const response = await got(url, { - method: 'POST', - body: 'Hello, World!', - }) - - expect(response).toMatchObject({ - statusCode: 200, - body: `String: Hello, World!`, - }) + const res = await ts.load(handler).call('/', { + method: 'post', + body: 'Hello!', }) + + expect(await res.text()).toBe('Hello!') }) - it('returns parsed url encoded body', async () => { - expect.hasAssertions() - const handler = prismy([BodySelector()], (body) => { + it('selects parsed form', async () => { + const handler = Handler([BodySelector()], (body) => { return Result(body) }) - await testHandler(handler, async (url) => { - const response = await got(url, { - method: 'POST', - responseType: 'json', - form: { - message: 'Hello, World!', - }, - }) + const res = await ts.load(handler).call('/', { + method: 'post', + body: new URLSearchParams([['message', 'Hello!']]), + }) - expect(response).toMatchObject({ - statusCode: 200, - body: { - message: 'Hello, World!', - }, - }) + expect(await res.json()).toEqual({ + message: 'Hello!', }) }) - it('returns JSON object body', async () => { - expect.hasAssertions() - const handler = prismy([BodySelector()], (body) => { + it('selects json body', async () => { + const handler = Handler([BodySelector()], (body) => { return Result(body) }) - await testHandler(handler, async (url) => { - const target = { - foo: 'bar', - } - const response = await got(url, { - method: 'POST', - responseType: 'json', - json: target, - }) + const res = await ts.load(handler).call('/', { + method: 'post', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ message: 'Hello!' }), + }) - expect(response.statusCode).toBe(200) - expect(response.body).toMatchObject(target) + expect(await res.json()).toEqual({ + message: 'Hello!', }) }) }) diff --git a/specs/selectors/bufferBody.spec.ts b/specs/selectors/bufferBody.spec.ts index 4ffbf98..1f7e8c1 100644 --- a/specs/selectors/bufferBody.spec.ts +++ b/specs/selectors/bufferBody.spec.ts @@ -1,20 +1,27 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { BufferBodySelector, prismy, Result } from '../../src' +import { BufferBodySelector, Handler, Result } from '../../src' +import { TestServer } from '../../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('BufferBodySelector', () => { it('creates buffer body selector', async () => { - const handler = prismy([BufferBodySelector()], (body) => { + const handler = Handler([BufferBodySelector()], (body) => { return Result(`${body.constructor.name}: ${body}`) }) - await testHandler(handler, async (url) => { - const response = await got(url, { method: 'POST', body: 'Hello, World!' }) - - expect(response).toMatchObject({ - statusCode: 200, - body: 'Buffer: Hello, World!', - }) + const res = await ts.load(handler).call('/', { + method: 'post', + body: Buffer.from('Hello!'), }) + + expect(await res.text()).toBe('Buffer: Hello!') }) }) diff --git a/specs/selectors/headers.spec.ts b/specs/selectors/headers.spec.ts index 15b415f..10c0186 100644 --- a/specs/selectors/headers.spec.ts +++ b/specs/selectors/headers.spec.ts @@ -1,24 +1,28 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { HeadersSelector, prismy, Result } from '../../src' +import { Handler, HeadersSelector, Result } from '../../src' +import { TestServer } from '../../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('headersSelector', () => { it('select headers', async () => { - const handler = prismy([HeadersSelector()], (headers) => { + const handler = Handler([HeadersSelector()], (headers) => { return Result(headers['x-test']) }) - await testHandler(handler, async (url) => { - const response = await got(url, { - headers: { - 'x-test': 'Hello, World!', - }, - }) - - expect(response).toMatchObject({ - statusCode: 200, - body: 'Hello, World!', - }) + const res = await ts.load(handler).call('/', { + headers: { + 'x-test': 'Hello, World!', + }, }) + + expect(await res.text()).toBe('Hello, World!') }) }) diff --git a/specs/selectors/jsonBody.spec.ts b/specs/selectors/jsonBody.spec.ts index 820cdba..c25eb5e 100644 --- a/specs/selectors/jsonBody.spec.ts +++ b/specs/selectors/jsonBody.spec.ts @@ -1,80 +1,47 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { JsonBodySelector, prismy, Result } from '../../src' +import { Handler, JsonBodySelector, Result } from '../../src' +import { TestServer } from '../../src/test' -describe('JsonBodySelector', () => { - it('creates json body selector', async () => { - const jsonBodySelector = JsonBodySelector() - const handler = prismy([jsonBodySelector], (body) => { - return Result(body) - }) +const ts = TestServer() - await testHandler(handler, async (url) => { - const response = await got(url, { - method: 'POST', - responseType: 'json', - json: { - message: 'Hello, World!', - }, - }) +beforeAll(async () => { + await ts.start() +}) - expect(response).toMatchObject({ - statusCode: 200, - body: { - message: 'Hello, World!', - }, - }) - }) - }) +afterAll(async () => { + await ts.close() +}) - it('throw if content typeof a request is not set', async () => { - const jsonBodySelector = JsonBodySelector() - const handler = prismy([jsonBodySelector], (body) => { +describe('JsonBodySelector', () => { + it('creates json body selector', async () => { + const handler = Handler([JsonBodySelector()], (body) => { return Result(body) }) - await testHandler(handler, async (url) => { - const response = await got(url, { - method: 'POST', - body: JSON.stringify({ - message: 'Hello, World!', - }), - throwHttpErrors: false, - }) + const res = await ts.load(handler).call('/', { + method: 'post', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ message: 'Hello!' }), + }) - expect(response).toMatchObject({ - statusCode: 400, - body: expect.stringContaining( - 'Error: Content type must be application/json. (Current: undefined)', - ), - }) + expect(await res.json()).toEqual({ + message: 'Hello!', }) }) - it('throws if content type of a request is not application/json', async () => { - const jsonBodySelector = JsonBodySelector() - const handler = prismy([jsonBodySelector], (body) => { + it('throw if content type of a request is not application/json', async () => { + const handler = Handler([JsonBodySelector()], (body) => { return Result(body) }) - await testHandler(handler, async (url) => { - const response = await got(url, { - method: 'POST', - json: { - message: 'Hello, World!', - }, - headers: { - 'content-type': 'text/plain', - }, - throwHttpErrors: false, - }) - - expect(response).toMatchObject({ - statusCode: 400, - body: expect.stringContaining( - 'Error: Content type must be application/json. (Current: text/plain)', - ), - }) + const res = await ts.load(handler).call('/', { + method: 'post', + body: JSON.stringify({ message: 'Hello!' }), }) + + expect(await res.text()).toContain( + 'Error: Content type must be application/json. (Current: text/plain', + ) }) }) diff --git a/specs/selectors/method.spec.ts b/specs/selectors/method.spec.ts index 81b9693..5cde8f9 100644 --- a/specs/selectors/method.spec.ts +++ b/specs/selectors/method.spec.ts @@ -1,20 +1,32 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { MethodSelector, prismy, Result } from '../../src' +import { Handler, MethodSelector, Result } from '../../src' +import { TestServer } from '../../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('MethodSelector', () => { it('selects method', async () => { - const handler = prismy([MethodSelector()], (method) => { + const handler = Handler([MethodSelector()], (method) => { return Result(method) }) - await testHandler(handler, async (url) => { - const response = await got(url) + const res = await ts.load(handler).call('/', { + method: 'get', + }) + + expect(await res.text()).toBe('GET') - expect(response).toMatchObject({ - statusCode: 200, - body: 'GET', - }) + const res2 = await ts.call('/', { + method: 'put', }) + + expect(await res2.text()).toBe('PUT') }) }) diff --git a/specs/selectors/searchParam.spec.ts b/specs/selectors/searchParam.spec.ts index ab86101..34399a8 100644 --- a/specs/selectors/searchParam.spec.ts +++ b/specs/selectors/searchParam.spec.ts @@ -1,86 +1,77 @@ -import got from 'got' -import { testHandler } from '../helpers' import { SearchParamSelector, SearchParamListSelector, - prismy, Result, + Handler, } from '../../src' -import { URLSearchParams } from 'url' +import { TestServer } from '../../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('SearchParamSelector', () => { it('selects a search param', async () => { - const handler = prismy([SearchParamSelector('message')], (message) => { + const handler = Handler([SearchParamSelector('message')], (message) => { return Result({ message }) }) - await testHandler(handler, async (url) => { - const response = await got(url, { - searchParams: { message: 'Hello, World!' }, - responseType: 'json', - }) + const res = await ts.load(handler).call('/?message=Hello!') - expect(response).toMatchObject({ - statusCode: 200, - body: { message: 'Hello, World!' }, - }) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ + message: 'Hello!', }) }) it('selects null if there is no param with the name', async () => { - const handler = prismy([SearchParamSelector('message')], (message) => { + const handler = Handler([SearchParamSelector('message')], (message) => { return Result({ message }) }) - await testHandler(handler, async (url) => { - const response = await got(url, { - responseType: 'json', - }) + const res = await ts.load(handler).call('/') - expect(response).toMatchObject({ - statusCode: 200, - body: { message: null }, - }) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ + message: null, }) }) }) describe('SearchParamListSelector', () => { it('selects a search param list', async () => { - const handler = prismy([SearchParamListSelector('message')], (messages) => { - return Result({ messages }) - }) + const handler = Handler( + [SearchParamListSelector('message')], + (messages) => { + return Result({ messages }) + }, + ) - await testHandler(handler, async (url) => { - const response = await got(url, { - searchParams: new URLSearchParams([ - ['message', 'Hello, World!'], - ['message', 'Have a nice day!'], - ]), - responseType: 'json', - }) + const res = await ts.load(handler).call('?message=Hello!&message=Hi!') - expect(response).toMatchObject({ - statusCode: 200, - body: { messages: ['Hello, World!', 'Have a nice day!'] }, - }) + expect(await res.json()).toEqual({ + messages: ['Hello!', 'Hi!'], }) }) - it('selects null if there is no param with the name', async () => { - const handler = prismy([SearchParamListSelector('message')], (messages) => { - return Result({ messages }) - }) + it('selects an empty array if there is no param with the name', async () => { + const handler = Handler( + [SearchParamListSelector('message')], + (messages) => { + return Result({ messages }) + }, + ) - await testHandler(handler, async (url) => { - const response = await got(url, { - responseType: 'json', - }) + const res = await ts.load(handler).call('/') - expect(response).toMatchObject({ - statusCode: 200, - body: { messages: [] }, - }) + expect(await res.json()).toEqual({ + messages: [], }) }) }) diff --git a/specs/selectors/textBody.spec.ts b/specs/selectors/textBody.spec.ts index 63aa633..31ba327 100644 --- a/specs/selectors/textBody.spec.ts +++ b/specs/selectors/textBody.spec.ts @@ -1,20 +1,27 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { prismy, Result, TextBodySelector } from '../../src' +import { Handler, Result, TextBodySelector } from '../../src' +import { TestServer } from '../../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('TextBodySelector', () => { it('creates buffer body selector', async () => { - const handler = prismy([TextBodySelector()], (body) => { - return Result(`${body.constructor.name}: ${body}`) + const handler = Handler([TextBodySelector()], (body) => { + return Result(`${typeof body}: ${body}`) }) - await testHandler(handler, async (url) => { - const response = await got(url, { method: 'POST', body: 'Hello, World!' }) - - expect(response).toMatchObject({ - statusCode: 200, - body: 'String: Hello, World!', - }) + const res = await ts.load(handler).call('/', { + method: 'post', + body: 'Hello, World!', }) + + expect(await res.text()).toBe('string: Hello, World!') }) }) diff --git a/specs/selectors/url.spec.ts b/specs/selectors/url.spec.ts index 8775ee8..e8ea1e4 100644 --- a/specs/selectors/url.spec.ts +++ b/specs/selectors/url.spec.ts @@ -1,43 +1,40 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { prismy, Result, UrlSelector } from '../../src' +import { Handler, Result, UrlSelector } from '../../src' +import { TestServer } from '../../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('UrlSelector', () => { it('selects url', async () => { - const handler = prismy([UrlSelector()], (url) => { + const handler = Handler([UrlSelector()], (url) => { return Result({ pathname: url.pathname, search: url.search, }) }) - await testHandler(handler, async (url) => { - const response = await got(url + '/test?query=true#hash', { - responseType: 'json', - }) + const res = await ts.load(handler).call('/test?query=true#hash') - expect(response).toMatchObject({ - statusCode: 200, - body: expect.objectContaining({ - pathname: '/test', - search: '?query=true', - }), - }) + expect(await res.json()).toEqual({ + pathname: '/test', + search: '?query=true', }) }) it('reuses parsed url', async () => { - const handler = prismy([UrlSelector(), UrlSelector()], (url, url2) => { + const handler = Handler([UrlSelector(), UrlSelector()], (url, url2) => { return Result(JSON.stringify(url === url2)) }) - await testHandler(handler, async (url) => { - const response = await got(url) + const res = await ts.load(handler).call('/test?query=true#hash') - expect(response).toMatchObject({ - statusCode: 200, - body: 'true', - }) - }) + expect(await res.json()).toEqual(true) }) }) diff --git a/specs/selectors/urlEncodedBody.spec.ts b/specs/selectors/urlEncodedBody.spec.ts index 60d69b7..26ba119 100644 --- a/specs/selectors/urlEncodedBody.spec.ts +++ b/specs/selectors/urlEncodedBody.spec.ts @@ -1,28 +1,31 @@ -import got from 'got' -import { testHandler } from '../helpers' -import { prismy, Result, UrlEncodedBodySelector } from '../../src' +import { Handler, Result, UrlEncodedBodySelector } from '../../src' +import { TestServer } from '../../src/test' + +const ts = TestServer() + +beforeAll(async () => { + await ts.start() +}) + +afterAll(async () => { + await ts.close() +}) describe('UrlEncodedBody', () => { it('injects parsed url encoded body', async () => { - const handler = prismy([UrlEncodedBodySelector()], (body) => { + const handler = Handler([UrlEncodedBodySelector()], (body) => { return Result(body) }) + const body = new URLSearchParams() + body.append('message', 'Hello, World!') - await testHandler(handler, async (url) => { - const response = await got(url, { - method: 'POST', - responseType: 'json', - form: { - message: 'Hello, World!', - }, - }) + const res = await ts.load(handler).call('/', { + method: 'post', + body: body, + }) - expect(response).toMatchObject({ - statusCode: 200, - body: { - message: 'Hello, World!', - }, - }) + expect(await res.json()).toEqual({ + message: 'Hello, World!', }) }) }) diff --git a/specs/types/basic.ts b/specs/types/basic.ts index d0c6da5..64d784b 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -13,11 +13,12 @@ import { Route, UrlSelector, } from '../../src' -import { expectType } from '../helpers' import http from 'http' import { InjectSelector } from '../../src/selectors/inject' import { PrismySelector } from '../../src/selectors/createSelector' +function expectType(value: T): void {} + const handler1 = Handler([UrlSelector(), MethodSelector()], (url, method) => { expectType(url) expectType(method) diff --git a/src/selectors/body.ts b/src/selectors/body.ts index c1e124b..801176a 100644 --- a/src/selectors/body.ts +++ b/src/selectors/body.ts @@ -44,11 +44,15 @@ export function BodySelector( ): PrismySelector { return createPrismySelector(async () => { const { req } = getPrismyContext() - const type = req.headers['content-type'] + /* istanbul ignore next */ + const type = req.headers['content-type'] || '' - if (type === 'application/json' || type === 'application/ld+json') { + if ( + type.startsWith('application/json') || + type.startsWith('application/ld+json') + ) { return readJsonBody(req, options) - } else if (type === 'application/x-www-form-urlencoded') { + } else if (type.startsWith('application/x-www-form-urlencoded')) { const textBody = await readTextBody(req, options) try { return parse(textBody) diff --git a/src/selectors/jsonBody.ts b/src/selectors/jsonBody.ts index 3cca617..594f963 100644 --- a/src/selectors/jsonBody.ts +++ b/src/selectors/jsonBody.ts @@ -59,6 +59,7 @@ export function JsonBodySelector( } function isContentTypeApplicationJSON(contentType: string | undefined) { + /* istanbul ignore next */ if (typeof contentType !== 'string') return false if (!contentType.startsWith('application/json')) return false return true From 86f8469d0ad95a55231c6099b5bd4b114c689bb6 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 12 Feb 2024 22:45:16 +0900 Subject: [PATCH 079/109] 4.0.0-7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4eaa0fc..4c7aa7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-6", + "version": "4.0.0-7", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 0cadf13091ddb0f2ade322f9d8f8e7d2b71f31ea Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 16 Mar 2024 09:08:21 +0900 Subject: [PATCH 080/109] Rename handler methods for better testing DX --- specs/handler.spec.ts | 2 +- specs/types/basic.ts | 4 ++-- src/handler.ts | 9 +++------ src/prismy.ts | 2 +- src/router.ts | 4 ++-- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/specs/handler.spec.ts b/specs/handler.spec.ts index dc62c2d..e42d4d3 100644 --- a/specs/handler.spec.ts +++ b/specs/handler.spec.ts @@ -7,7 +7,7 @@ describe('Handler', () => { ) const handler = Handler([rawUrlSelector], (url) => Result(url)) - const result = handler.handlerFunction('Hello, World!') + const result = handler.handle('Hello, World!') expect(result).toMatchObject({ body: 'Hello, World!', diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 64d784b..14c8264 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -31,7 +31,7 @@ expectType< method: string | undefined, url2: URL, ) => PrismyResult | Promise ->(handler1.handlerFunction) +>(handler1.handle) expectType(Handler([BodySelector()], () => Result(null))) @@ -92,4 +92,4 @@ expectType< >(shortRoute) expectType< (url: URL, method: string | undefined) => MaybePromise> ->(shortRoute.handler.handlerFunction) +>(shortRoute.handler.handle) diff --git a/src/handler.ts b/src/handler.ts index 2b01081..a1e74a9 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -18,17 +18,14 @@ export class PrismyHandler< * PrismyHandler exposes `handler` for unit testing the handler. * @param args selected arguments */ - public handlerFunction: ( + public handle: ( ...args: SelectorReturnTypeTuple ) => MaybePromise, public middlewareList: PrismyMiddleware[], ) {} - async handle(): Promise { - const next: PrismyNextFunction = compileHandler( - this.selectors, - this.handlerFunction, - ) + async __internal__handler(): Promise { + const next: PrismyNextFunction = compileHandler(this.selectors, this.handle) const pipe = this.middlewareList.reduce((next, middleware) => { return middleware.pipe(next) diff --git a/src/prismy.ts b/src/prismy.ts index 3dfed38..17b4ac5 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -69,7 +69,7 @@ export function prismy[]>( req: request, } prismyContextStorage.run(context, async () => { - const resObject = await injectedHandler.handle() + const resObject = await injectedHandler.__internal__handler() resObject.resolve(request, response) }) diff --git a/src/router.ts b/src/router.ts index de4708b..fe3572c 100644 --- a/src/router.ts +++ b/src/router.ts @@ -79,11 +79,11 @@ export function Router( setRouteParamsToPrismyContext(prismyContext, result.params) - return route.listener.handle() + return route.listener.__internal__handler() } if (notFoundHandler != null) { - return notFoundHandler.handle() + return notFoundHandler.__internal__handler() } throw createError(404, 'Not Found') }, From c11a7a4ee27c2f5be9d55c9a3692fef31953a9a9 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 16 Mar 2024 09:26:06 +0900 Subject: [PATCH 081/109] Add result type generic arg to handler class --- specs/types/basic.ts | 17 ++++++++++++----- src/handler.ts | 16 ++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 14c8264..4ca896c 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -19,11 +19,14 @@ import { PrismySelector } from '../../src/selectors/createSelector' function expectType(value: T): void {} -const handler1 = Handler([UrlSelector(), MethodSelector()], (url, method) => { - expectType(url) - expectType(method) - return Result('') -}) +const handler1 = Handler( + [UrlSelector(), MethodSelector()], + async (url, method) => { + expectType(url) + expectType(method) + return Result('') + }, +) expectType< ( @@ -35,6 +38,10 @@ expectType< expectType(Handler([BodySelector()], () => Result(null))) +expectType>>( + handler1.handle(new URL('...'), 'get'), +) + http.createServer(prismy(handler1)) // @ts-expect-error diff --git a/src/handler.ts b/src/handler.ts index a1e74a9..1454fda 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -11,6 +11,7 @@ import { compileHandler } from './utils' export class PrismyHandler< S extends PrismySelector[] = PrismySelector[], + R extends PrismyResult = PrismyResult, > { constructor( public selectors: [...S], @@ -18,13 +19,11 @@ export class PrismyHandler< * PrismyHandler exposes `handler` for unit testing the handler. * @param args selected arguments */ - public handle: ( - ...args: SelectorReturnTypeTuple - ) => MaybePromise, + public handle: (...args: SelectorReturnTypeTuple) => MaybePromise, public middlewareList: PrismyMiddleware[], ) {} - async __internal__handler(): Promise { + async __internal__handler(): Promise> { const next: PrismyNextFunction = compileHandler(this.selectors, this.handle) const pipe = this.middlewareList.reduce((next, middleware) => { @@ -68,11 +67,12 @@ export class PrismyHandler< * * @public * */ -export function Handler[]>( +export function Handler< + S extends PrismySelector[], + R extends PrismyResult = PrismyResult, +>( selectors: [...S], - handlerFunction: ( - ...args: SelectorReturnTypeTuple - ) => MaybePromise, + handlerFunction: (...args: SelectorReturnTypeTuple) => MaybePromise, middlewareList: PrismyMiddleware[]>[] = [], ) { return new PrismyHandler(selectors, handlerFunction, middlewareList) From 74527c96259001cf8bfc51a90c300edf680df61a Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 16 Mar 2024 09:26:18 +0900 Subject: [PATCH 082/109] 4.0.0-8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4c7aa7b..620b995 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-7", + "version": "4.0.0-8", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 10ffca898931462ff9f4329892058007eaf038b3 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 17 Mar 2024 15:45:30 +0900 Subject: [PATCH 083/109] Build es module --- package.json | 10 +++++++--- tsconfig.es.json | 9 +++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 tsconfig.es.json diff --git a/package.json b/package.json index 620b995..1299931 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "homepage": "https://github.com/prismyland/prismy", "license": "MIT", "main": "dist/index.js", + "module": "dist/es/index.js", "types": "dist/index.d.ts", "files": [ "dist/index.d.ts", @@ -27,15 +28,18 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "require": "./dist/index.js" + "require": "./dist/index.js", + "import": "./dist/es/index.js" }, "./test": { "types": "./dist/test.d.ts", - "require": "./dist/test.js" + "require": "./dist/test.js", + "import": "./dist/es/index.js" } }, "scripts": { - "build": "rimraf dist && tsc -P tsconfig.build.json", + "build": "rimraf dist && tsc -P tsconfig.build.json && npm run build-es", + "build-es": "tsc -P tsconfig.es.json", "lint": "prettier --check src/**/*.ts specs/**/*.ts examples/*/src/**/*.ts", "format": "prettier --write src/**/*.ts specs/**/*.ts examples/*/src/**/*.ts", "test": "npm run lint && npm run test-type && npm run test-coverage", diff --git a/tsconfig.es.json b/tsconfig.es.json new file mode 100644 index 0000000..c29a0ea --- /dev/null +++ b/tsconfig.es.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node16", + "outDir": "dist/es" + }, + "include": ["src/**/*.ts"] +} From 4d663f910d5d5bdd9368f5cf829a6d36e4f24e61 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 17 Mar 2024 15:45:40 +0900 Subject: [PATCH 084/109] 4.0.0-9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1299931..13c5ae0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-8", + "version": "4.0.0-9", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 6aab8c69ffa8c747d720388473f6c1b91b2115d0 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 17 Mar 2024 15:51:59 +0900 Subject: [PATCH 085/109] Don't emit type defs when compiling es modules --- tsconfig.es.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tsconfig.es.json b/tsconfig.es.json index c29a0ea..5b68901 100644 --- a/tsconfig.es.json +++ b/tsconfig.es.json @@ -3,7 +3,9 @@ "compilerOptions": { "module": "ESNext", "moduleResolution": "node16", - "outDir": "dist/es" + "outDir": "dist/es", + "declaration": false }, + "include": ["src/**/*.ts"] } From 0a5974ce51a4547b21bccb4a0b2046391d85eb38 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 4 Apr 2024 04:46:17 +0900 Subject: [PATCH 086/109] Make selector can use selectors --- specs/types/basic.ts | 20 +++++++++++++++++ src/selectors/createSelector.ts | 38 ++++++++++++++++++++++++++------- src/utils.ts | 2 +- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 4ca896c..63f6e43 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -1,5 +1,6 @@ import { BodySelector, + createPrismySelector, Handler, MaybePromise, MethodSelector, @@ -100,3 +101,22 @@ expectType< expectType< (url: URL, method: string | undefined) => MaybePromise> >(shortRoute.handler.handle) + +expectType>( + createPrismySelector(() => { + return 0 + }), +) + +const UrlPortSelector = () => + createPrismySelector([UrlSelector()], (url) => { + return { + pathname: url.pathname, + hash: url.hash, + } + }) + +expectType<{ + pathname: string + hash: string +}>(await UrlPortSelector().select(new URL(''))) diff --git a/src/selectors/createSelector.ts b/src/selectors/createSelector.ts index deae0ed..0b87fe7 100644 --- a/src/selectors/createSelector.ts +++ b/src/selectors/createSelector.ts @@ -1,15 +1,37 @@ -export class PrismySelector { - selectorFunction: () => Promise | T - constructor(selectorFunction: () => Promise | T) { - this.selectorFunction = selectorFunction - } - resolve(): T | Promise { - return this.selectorFunction() +import { SelectorReturnTypeTuple } from '..' +import { compileHandler } from '../utils' + +export class PrismySelector< + T, + S extends PrismySelector[] = PrismySelector[], +> { + constructor( + public selectors: [...S], + public select: (...args: SelectorReturnTypeTuple) => Promise | T, + ) {} + + __internal__selector(): T | Promise { + const compiledSelector = compileHandler(this.selectors, this.select) + return compiledSelector() } } +export function createPrismySelector< + T, + S extends PrismySelector[] = PrismySelector[], +>( + selectors: [...S], + selectorFunction: (...args: SelectorReturnTypeTuple) => T | Promise, +): PrismySelector export function createPrismySelector( selectorFunction: () => T | Promise, +): PrismySelector +export function createPrismySelector( + selectorsOrFn: any, + selectorFunction?: any, ) { - return new PrismySelector(selectorFunction) + if (selectorFunction == null) { + return new PrismySelector([], selectorsOrFn) + } + return new PrismySelector(selectorsOrFn, selectorFunction) } diff --git a/src/utils.ts b/src/utils.ts index 4152ff2..a4e4831 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -36,7 +36,7 @@ async function resolveSelectors[]>( ): Promise> { const resolvedValues = [] for (const selector of selectors) { - const resolvedValue = await selector.resolve() + const resolvedValue = await selector.__internal__selector() resolvedValues.push(resolvedValue) } From 7f9c76db9126cd3391f594d83b178366f8e0ed3f Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 4 Apr 2024 04:49:52 +0900 Subject: [PATCH 087/109] Use selectors arg for searchparamselector --- src/selectors/searchParam.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/selectors/searchParam.ts b/src/selectors/searchParam.ts index de457ec..12359f6 100644 --- a/src/selectors/searchParam.ts +++ b/src/selectors/searchParam.ts @@ -1,8 +1,6 @@ import { createPrismySelector, PrismySelector } from './createSelector' import { UrlSelector } from './url' -const urlSelector = UrlSelector() - /** * Create a selector which resolves the first value of the search param. * Using `url.searchParams.get(name)` internally. @@ -25,9 +23,7 @@ const urlSelector = UrlSelector() export const SearchParamSelector: ( name: string, ) => PrismySelector = (name) => - createPrismySelector(async () => { - const url = await urlSelector.resolve() - + createPrismySelector([UrlSelector()], async (url) => { return url.searchParams.get(name) }) @@ -56,8 +52,6 @@ export const SearchParamSelector: ( export const SearchParamListSelector: ( name: string, ) => PrismySelector = (name) => - createPrismySelector(async () => { - const url = await urlSelector.resolve() - + createPrismySelector([UrlSelector()], async (url) => { return url.searchParams.getAll(name) }) From a0439e781f49f2f1ba3cb6d6174ccd088acbc83f Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 4 Apr 2024 05:44:28 +0900 Subject: [PATCH 088/109] Can omit selectors for Handler and Route --- specs/handler.spec.ts | 12 ++++++++++++ specs/router.spec.ts | 15 +++++++++++++-- specs/types/basic.ts | 4 ++++ src/handler.ts | 27 +++++++++++++++++++++++++-- src/router.ts | 19 ++++++++++++------- 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/specs/handler.spec.ts b/specs/handler.spec.ts index e42d4d3..f5f79e3 100644 --- a/specs/handler.spec.ts +++ b/specs/handler.spec.ts @@ -15,4 +15,16 @@ describe('Handler', () => { statusCode: 200, }) }) + + it('handles without a selector', () => { + const handler = Handler(() => Result('Hello!')) + + const result = handler.handle() + + expect(result).toMatchObject({ + body: 'Hello!', + headers: {}, + statusCode: 200, + }) + }) }) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index dad14fd..5598192 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -22,10 +22,10 @@ afterAll(async () => { describe('router', () => { it('routes with pathname', async () => { - const handlerA = Handler([], () => { + const handlerA = Handler(() => { return Result('a') }) - const handlerB = Handler([], () => { + const handlerB = Handler(() => { return Result('b') }) const routerHandler = Router([Route('/a', handlerA), Route('/b', handlerB)]) @@ -46,6 +46,17 @@ describe('router', () => { expect(await res.text()).toBe('b') }) + it('routes with pathname(shorthand w/o selectors)', async () => { + const routerHandler = Router([ + Route('/a', () => Result('a')), + Route('/b', () => Result('b')), + ]) + + const res = await ts.load(routerHandler).call('/b') + + expect(await res.text()).toBe('b') + }) + it('routes with method', async () => { const handlerA = Handler([], () => { return Result('a') diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 63f6e43..dbb1d89 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -120,3 +120,7 @@ expectType<{ pathname: string hash: string }>(await UrlPortSelector().select(new URL(''))) + +Handler(() => { + return Result('') +}) diff --git a/src/handler.ts b/src/handler.ts index 1454fda..6a9c859 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -73,7 +73,30 @@ export function Handler< >( selectors: [...S], handlerFunction: (...args: SelectorReturnTypeTuple) => MaybePromise, - middlewareList: PrismyMiddleware[]>[] = [], + middlewareList?: PrismyMiddleware[]>[], +): PrismyHandler +export function Handler = PrismyResult>( + handlerFunction: () => MaybePromise, + middlewareList?: PrismyMiddleware[]>[], +): PrismyHandler<[], R> +export function Handler< + S extends PrismySelector[], + R extends PrismyResult = PrismyResult, +>( + selectorsOrHandler: any, + handlerFunctionOrMiddlewareList?: any | any[], + middlewareList?: any[], ) { - return new PrismyHandler(selectors, handlerFunction, middlewareList) + if (Array.isArray(selectorsOrHandler)) { + return new PrismyHandler( + selectorsOrHandler, + handlerFunctionOrMiddlewareList, + middlewareList || [], + ) + } + return new PrismyHandler( + [], + selectorsOrHandler, + handlerFunctionOrMiddlewareList || [], + ) } diff --git a/src/router.ts b/src/router.ts index fe3572c..cc8c026 100644 --- a/src/router.ts +++ b/src/router.ts @@ -97,24 +97,29 @@ export function Route[]>( ): PrismyRoute export function Route[]>( indicator: RouteIndicator | string, - selectors: [...S], - handlerFunction?: ( - ...args: SelectorReturnTypeTuple - ) => MaybePromise, + handler: (...args: SelectorReturnTypeTuple) => MaybePromise, middlewareList?: PrismyMiddleware[]>[], ): PrismyRoute export function Route[]>( indicator: RouteIndicator | string, - selectorsOrPrismyHandler: [...S] | PrismyHandler, + selectors: [...S], handlerFunction?: ( ...args: SelectorReturnTypeTuple ) => MaybePromise, middlewareList?: PrismyMiddleware[]>[], -): PrismyRoute { +): PrismyRoute +export function Route( + indicator: RouteIndicator | string, + selectorsOrPrismyHandler: any, + handlerFunction?: any, + middlewareList?: any, +): PrismyRoute { const handler = selectorsOrPrismyHandler instanceof PrismyHandler ? selectorsOrPrismyHandler - : Handler(selectorsOrPrismyHandler, handlerFunction!, middlewareList) + : Array.isArray(selectorsOrPrismyHandler) + ? Handler(selectorsOrPrismyHandler, handlerFunction!, middlewareList) + : Handler([], selectorsOrPrismyHandler, handlerFunction) if (typeof indicator === 'string') { return new PrismyRoute([indicator, 'get'], handler) } From cd317358e249d38ae523b4a7cbba171b5307abc1 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Thu, 4 Apr 2024 05:44:41 +0900 Subject: [PATCH 089/109] 4.0.0-10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 13c5ae0..a88a9bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-9", + "version": "4.0.0-10", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From d61dd44b782887a5ba092687200e5928e8589814 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Apr 2024 10:33:50 +0900 Subject: [PATCH 090/109] Add PrismyErrorResult --- specs/types/basic.ts | 22 ++++++++++++++++++++++ src/result.ts | 8 ++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/specs/types/basic.ts b/specs/types/basic.ts index dbb1d89..7f49638 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -1,11 +1,13 @@ import { BodySelector, createPrismySelector, + ErrorResult, Handler, MaybePromise, MethodSelector, Middleware, prismy, + PrismyErrorResult, PrismyHandler, PrismyNextFunction, PrismyResult, @@ -124,3 +126,23 @@ expectType<{ Handler(() => { return Result('') }) + +Handler((): PrismyResult<{ data: string }> | PrismyErrorResult => { + if ('' === '') { + return ErrorResult(400, null) + } else if ('' === '') { + // @ts-expect-error + return Result({ test: 'test' }) + } + return Result({ data: '123' }) +}) + +Handler((): PrismyResult<{ data: string }> | PrismyErrorResult => { + if ('' === '') { + return ErrorResult(400, '') + } else if ('' === '') { + // @ts-expect-error + return ErrorResult(400, null) + } + return Result({ data: '123' }) +}) diff --git a/src/result.ts b/src/result.ts index 2e5736c..c222f18 100644 --- a/src/result.ts +++ b/src/result.ts @@ -152,8 +152,8 @@ export function ErrorResult( statusCode: number, body: B, headers: OutgoingHttpHeaders = {}, -): PrismyResult { - return new PrismyResult(body, statusCode, headers) +): PrismyErrorResult { + return new PrismyErrorResult(body, statusCode, headers) } /** @@ -176,3 +176,7 @@ export function Redirect( ...extraHeaders, }) } + +export class PrismyErrorResult extends PrismyResult { + readonly __isError = true +} From 5f8938d34961e2e33ef836420ab37b351b2c79e1 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 6 Apr 2024 10:34:18 +0900 Subject: [PATCH 091/109] 4.0.0-11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a88a9bc..5568caf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-10", + "version": "4.0.0-11", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From a0e7f488345adfc88b133610ee3e0125f9057949 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Apr 2024 03:12:49 +0900 Subject: [PATCH 092/109] Make RedirectResult distinguishable --- src/result.ts | 68 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/src/result.ts b/src/result.ts index c222f18..363079a 100644 --- a/src/result.ts +++ b/src/result.ts @@ -156,6 +156,37 @@ export function ErrorResult( return new PrismyErrorResult(body, statusCode, headers) } +export class PrismyErrorResult extends PrismyResult { + readonly __isError = true +} + +export function isErrorResult( + result: PrismyResult, +): result is PrismyErrorResult { + if ((result as PrismyErrorResult).__isError) { + return true + } + return false +} + +export function assertErrorResult( + result: PrismyResult, +): asserts result is PrismyErrorResult { + if (isErrorResult(result)) { + return + } + throw new Error('The given PrismyResult is NOT an error result.') +} + +export function assertNoErrorResult

( + result: P, +): asserts result is P extends PrismyErrorResult ? never : P { + if (!isErrorResult(result)) { + return + } + throw new Error('The given PrismyResult is an error result.') +} + /** * Factory function for easily generating a redirect response * @@ -166,17 +197,44 @@ export function ErrorResult( * * @public */ -export function Redirect( +export function RedirectResult( location: string, statusCode: number = 302, extraHeaders: OutgoingHttpHeaders = {}, -): PrismyResult { - return Result(null, statusCode, { +): PrismyRedirectResult { + return new PrismyRedirectResult(null, statusCode, { location, ...extraHeaders, }) } -export class PrismyErrorResult extends PrismyResult { - readonly __isError = true +export class PrismyRedirectResult extends PrismyResult { + readonly __isRedirect = true +} + +export function isRedirectResult( + result: PrismyResult, +): result is PrismyRedirectResult { + if ((result as PrismyRedirectResult).__isRedirect) { + return true + } + return false +} + +export function assertRedirectResult( + result: PrismyResult, +): asserts result is PrismyRedirectResult { + if (isRedirectResult(result)) { + return + } + throw new Error('The given PrismyResult is NOT a redirect result.') +} + +export function assertNoRedirectResult

( + result: P, +): asserts result is P extends PrismyRedirectResult ? never : P { + if (!isRedirectResult(result)) { + return + } + throw new Error('The given PrismyResult is a redirect result.') } From 37736b0699fb5c6635360f5ea3f1dad887824346 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Apr 2024 03:13:09 +0900 Subject: [PATCH 093/109] Add tests for ErrorResult utils --- specs/result.spec.ts | 85 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/specs/result.spec.ts b/specs/result.spec.ts index 56cd71c..623a5d8 100644 --- a/specs/result.spec.ts +++ b/specs/result.spec.ts @@ -1,4 +1,14 @@ -import { Redirect, Result, Handler, ErrorResult, PrismyResult } from '../src' +import { + RedirectResult, + Result, + Handler, + ErrorResult, + PrismyResult, + PrismyErrorResult, + isErrorResult, + assertErrorResult, + assertNoErrorResult, +} from '../src' import { TestServer } from '../src/test' const ts = TestServer() @@ -16,6 +26,7 @@ describe('ErrorResult', () => { const errorResult = ErrorResult(400, 'Invalid Format') expect(errorResult).toBeInstanceOf(PrismyResult) + expect(errorResult).toBeInstanceOf(PrismyErrorResult) expect(errorResult.statusCode).toBe(400) expect(errorResult.body).toBe('Invalid Format') }) @@ -100,9 +111,71 @@ describe('PrismyResult', () => { }) }) -describe('Redirect', () => { +describe('isErrorResult', () => { + it('returns false if result is NOT an error result', () => { + const result = Result(null) + + const value = isErrorResult(result) + + expect(value).toBe(false) + }) + + it('returns true if result is an error result', () => { + const result = ErrorResult(400, null) + + const value = isErrorResult(result) + + expect(value).toBe(true) + }) +}) + +describe('assertErrorResult', () => { + it('throws error if result is NOT an error result', () => { + const result = Result(null) + try { + assertErrorResult(result) + } catch (error) { + expect(error) + return + } + throw new Error('must throw') + }) + + it('does not throw if result is an error result', () => { + const result = ErrorResult(400, null) + try { + assertErrorResult(result) + } catch (error) { + throw new Error('must NOT throw') + } + }) +}) + +describe('assertErrorResult', () => { + it('throws error if result is an error result', () => { + const result = ErrorResult(400, null) + try { + assertNoErrorResult(result) + } catch (error) { + expect(error) + return + } + throw new Error('must throw') + }) + + it('does not throw if result is NOT an error result', () => { + const result = Result(null) + try { + assertNoErrorResult(result) + } catch (error) { + throw new Error('must NOT throw') + } + }) +}) + +describe('RedirectResult', () => { it('redirects', async () => { - const handler = Handler([], () => Redirect('https://github.com/')) + const handler = Handler([], () => RedirectResult('https://github.com/')) const res = await ts.load(handler).call('/', { redirect: 'manual', @@ -113,7 +186,9 @@ describe('Redirect', () => { }) it('sets statusCode', async () => { - const handler = Handler([], () => Redirect('https://github.com/', 301)) + const handler = Handler([], () => + RedirectResult('https://github.com/', 301), + ) const res = await ts.load(handler).call('/', { redirect: 'manual', @@ -125,7 +200,7 @@ describe('Redirect', () => { it('sets headers', async () => { const handler = Handler([], () => - Redirect('https://github.com/', 302, { + RedirectResult('https://github.com/', 302, { 'custom-header': 'Hello!', }), ) From 5f9063e81b12599673817417efa60a6412d1f826 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Apr 2024 04:11:35 +0900 Subject: [PATCH 094/109] Add tests for RedirectResult utils --- specs/result.spec.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/specs/result.spec.ts b/specs/result.spec.ts index 623a5d8..e41f20b 100644 --- a/specs/result.spec.ts +++ b/specs/result.spec.ts @@ -8,6 +8,9 @@ import { isErrorResult, assertErrorResult, assertNoErrorResult, + isRedirectResult, + assertRedirectResult, + assertNoRedirectResult, } from '../src' import { TestServer } from '../src/test' @@ -253,3 +256,65 @@ describe('RedirectResult', () => { ]) }) }) + +describe('isRedirectResult', () => { + it('returns false if result is NOT an error result', () => { + const result = Result(null) + + const value = isRedirectResult(result) + + expect(value).toBe(false) + }) + + it('returns true if result is an error result', () => { + const result = RedirectResult('/') + + const value = isRedirectResult(result) + + expect(value).toBe(true) + }) +}) + +describe('assertRedirectResult', () => { + it('throws error if result is NOT an error result', () => { + const result = Result(null) + try { + assertRedirectResult(result) + } catch (error) { + expect(error) + return + } + throw new Error('must throw') + }) + + it('does not throw if result is an error result', () => { + const result = RedirectResult('/') + try { + assertRedirectResult(result) + } catch (error) { + throw new Error('must NOT throw') + } + }) +}) + +describe('assertRedirectResult', () => { + it('throws error if result is an error result', () => { + const result = RedirectResult('/') + try { + assertNoRedirectResult(result) + } catch (error) { + expect(error) + return + } + throw new Error('must throw') + }) + + it('does not throw if result is NOT an error result', () => { + const result = Result(null) + try { + assertNoRedirectResult(result) + } catch (error) { + throw new Error('must NOT throw') + } + }) +}) From 62841903238c8cb09dc77640c87cb056336ff0ca Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 7 Apr 2024 04:11:39 +0900 Subject: [PATCH 095/109] 4.0.0-12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5568caf..2baad1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-11", + "version": "4.0.0-12", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From e71cd1934b4838bb7771dfbcd0191b7a60b92df6 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sun, 14 Apr 2024 13:28:33 +0900 Subject: [PATCH 096/109] Move default error handlers from PrismyHandler to prismy fn --- specs/router.spec.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++ src/error.ts | 2 +- src/handler.ts | 13 +--------- src/prismy.ts | 13 ++++++++-- 4 files changed, 72 insertions(+), 15 deletions(-) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 5598192..09144c3 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -5,6 +5,8 @@ import { Route, Middleware, getPrismyContext, + ErrorResult, + createPrismySelector, } from '../src' import { Handler } from '../src/handler' import { InjectSelector } from '../src/selectors/inject' @@ -198,6 +200,63 @@ describe('router', () => { expect(res.status).toBe(200) expect(await res.text()).toBe('ba') }) + + it('applies error handler middleware(Error from handler', async () => { + expect.hasAssertions() + + const handler = Handler([], () => { + throw new Error('Hello, Error!') + }) + + const routerHandler = Router([Route(['/', 'get'], handler)], { + middleware: [ + Middleware([], (next) => async () => { + try { + return await next() + } catch (error) { + return ErrorResult(500, 'hijacked') + } + }), + ], + }) + + const res = await ts.load(routerHandler).call() + + expect(res.status).toBe(500) + expect(await res.text()).toBe('hijacked') + }) + + it('applies error handler middleware(Error from handler selectors)', async () => { + expect.hasAssertions() + + const handler = Handler( + [ + createPrismySelector(() => { + throw new Error('Hello from a selector') + }), + ], + () => { + return Result(null) + }, + ) + + const routerHandler = Router([Route(['/', 'get'], handler)], { + middleware: [ + Middleware([], (next) => async () => { + try { + return await next() + } catch (error) { + return ErrorResult(500, 'hijacked') + } + }), + ], + }) + + const res = await ts.load(routerHandler).call() + + expect(res.status).toBe(500) + expect(await res.text()).toBe('hijacked') + }) }) describe('RouteParamSelector', () => { diff --git a/src/error.ts b/src/error.ts index 9bca49e..3e123d2 100644 --- a/src/error.ts +++ b/src/error.ts @@ -11,7 +11,7 @@ import { Result } from './result' * * @public */ -export function createErrorResObject(error: any) { +export function createErrorResultFromError(error: any) { const statusCode = error.statusCode || error.status || 500 /* istanbul ignore next */ const message = diff --git a/src/handler.ts b/src/handler.ts index 6a9c859..ed12d02 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -1,5 +1,4 @@ import { - createErrorResObject, PrismyMiddleware, PrismyNextFunction, MaybePromise, @@ -29,18 +28,8 @@ export class PrismyHandler< const pipe = this.middlewareList.reduce((next, middleware) => { return middleware.pipe(next) }, next) - let result: PrismyResult - try { - result = await pipe() - } catch (error) { - /* istanbul ignore next */ - if (process.env.NODE_ENV !== 'test') { - console.error(error) - } - result = createErrorResObject(error) - } - return result + return await pipe() } } diff --git a/src/prismy.ts b/src/prismy.ts index 17b4ac5..1de83aa 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -5,6 +5,7 @@ import { Handler, PrismyHandler } from './handler' import { PrismySelector } from './selectors/createSelector' import { MaybePromise, PrismyContext, SelectorReturnTypeTuple } from './types' import { PrismyResult } from './result' +import { createErrorResultFromError } from './error' export const prismyContextStorage = new AsyncLocalStorage() export function getPrismyContext(): PrismyContext { @@ -69,9 +70,17 @@ export function prismy[]>( req: request, } prismyContextStorage.run(context, async () => { - const resObject = await injectedHandler.__internal__handler() + try { + const result = await injectedHandler.__internal__handler() - resObject.resolve(request, response) + result.resolve(request, response) + } catch (error) { + /* istanbul ignore next */ + if (process.env.NODE_ENV !== 'test') { + console.error(error) + } + createErrorResultFromError(error).resolve(request, response) + } }) } From a89bc24fdc845716d9b68a1b548572436777d315 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Apr 2024 06:12:37 +0900 Subject: [PATCH 097/109] Simplify middleware api --- specs/middleware.spec.ts | 19 ++++++++----------- specs/prismy.spec.ts | 18 ++++++++---------- specs/router.spec.ts | 8 ++++---- src/middleware.ts | 14 +++++++++----- src/utils.ts | 2 +- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 1e76e08..dd9a6a2 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -17,16 +17,13 @@ describe('middleware', () => { const rawUrlSelector = createPrismySelector( () => getPrismyContext().req.url!, ) - const errorMiddleware = Middleware( - [rawUrlSelector], - (next) => async (url) => { - try { - return await next() - } catch (error) { - return Result(`${url} : ${(error as any).message}`, 500) - } - }, - ) + const errorMiddleware = Middleware([rawUrlSelector], async (next, url) => { + try { + return await next() + } catch (error) { + return Result(`${url} : ${(error as any).message}`, 500) + } + }) const handler = Handler( [], () => { @@ -47,7 +44,7 @@ describe('middleware', () => { ) const errorMiddleware = Middleware( [asyncRawUrlSelector], - (next) => async (url) => { + async (next, url) => { try { return await next() } catch (error) { diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index 00e2647..e4dd249 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -42,13 +42,11 @@ describe('prismy', () => { }) it('applies middleware', async () => { - const errorMiddleware = Middleware([], (next) => { - return async () => { - try { - return await next() - } catch (error) { - return ErrorResult(500, (error as any).message) - } + const errorMiddleware = Middleware([], async (next) => { + try { + return await next() + } catch (error) { + return ErrorResult(500, (error as any).message) } }) const rawUrlSelector = createPrismySelector( @@ -69,10 +67,10 @@ describe('prismy', () => { }) it('applies middleware in order (later = deeper)', async () => { - const problematicMiddleware = Middleware([], (next) => () => { + const problematicMiddleware = Middleware([], () => { throw new Error('Hey!') }) - const errorMiddleware = Middleware([], (next) => async () => { + const errorMiddleware = Middleware([], async (next) => { try { return await next() } catch (error) { @@ -130,7 +128,7 @@ describe('prismy', () => { }) it('handles errors from middleware by default', async () => { - const middleware = Middleware([], (next) => () => { + const middleware = Middleware([], () => { throw new Error('Hey!') }) const listener = prismy( diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 09144c3..4bd819a 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -182,12 +182,12 @@ describe('router', () => { const routerHandler = Router([Route(['/', 'get'], handlerA)], { middleware: [ - Middleware([InjectSelector('a')], (next) => (value) => { + Middleware([InjectSelector('a')], (next, value) => { const context = getPrismyContext() weakMap.set(context, (weakMap.get(context) || '') + value) return next() }), - Middleware([InjectSelector('b')], (next) => (value) => { + Middleware([InjectSelector('b')], (next, value) => { const context = getPrismyContext() weakMap.set(context, (weakMap.get(context) || '') + value) return next() @@ -210,7 +210,7 @@ describe('router', () => { const routerHandler = Router([Route(['/', 'get'], handler)], { middleware: [ - Middleware([], (next) => async () => { + Middleware([], async (next) => { try { return await next() } catch (error) { @@ -242,7 +242,7 @@ describe('router', () => { const routerHandler = Router([Route(['/', 'get'], handler)], { middleware: [ - Middleware([], (next) => async () => { + Middleware([], async (next) => { try { return await next() } catch (error) { diff --git a/src/middleware.ts b/src/middleware.ts index 30ac864..fc45de3 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,7 +1,7 @@ import { PrismyNextFunction, PrismyResult } from '.' import { PrismySelector } from './selectors/createSelector' import { SelectorReturnTypeTuple } from './types' -import { compileHandler } from './utils' +import { resolveSelectors } from './utils' export class PrismyMiddleware< S extends PrismySelector[] = PrismySelector[], @@ -14,11 +14,14 @@ export class PrismyMiddleware< */ public handler: ( next: PrismyNextFunction, - ) => (...args: SelectorReturnTypeTuple) => Promise, + ...args: SelectorReturnTypeTuple + ) => Promise, ) {} - pipe(next: PrismyNextFunction) { - return compileHandler(this.selectors, this.handler(next)) + pipe(next: PrismyNextFunction): PrismyNextFunction { + return async () => { + return this.handler(next, ...(await resolveSelectors(this.selectors))) + } } } @@ -60,7 +63,8 @@ export function Middleware[]>( selectors: [...SS], handler: ( next: PrismyNextFunction, - ) => (...args: SelectorReturnTypeTuple) => Promise, + ...args: SelectorReturnTypeTuple + ) => Promise, ): PrismyMiddleware { return new PrismyMiddleware(selectors, handler) } diff --git a/src/utils.ts b/src/utils.ts index a4e4831..683ed5f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,7 +31,7 @@ export function compileHandler[], R>( * * @internal */ -async function resolveSelectors[]>( +export async function resolveSelectors[]>( selectors: [...S], ): Promise> { const resolvedValues = [] From 3365fefb32ef5762af351469c18acd2c634b8eb9 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Apr 2024 06:53:03 +0900 Subject: [PATCH 098/109] Remove utils and restruct selector resolving module --- specs/middleware.spec.ts | 2 +- specs/prismy.spec.ts | 2 +- specs/types/basic.ts | 6 +-- src/handler.ts | 8 ++-- src/index.ts | 1 + src/middleware.ts | 5 +-- src/prismy.ts | 2 +- src/router.ts | 5 +-- .../createSelector.ts => selector.ts} | 30 ++++++++++--- src/selectors/body.ts | 2 +- src/selectors/bufferBody.ts | 2 +- src/selectors/cookie.ts | 2 +- src/selectors/headers.ts | 2 +- src/selectors/index.ts | 1 - src/selectors/inject.ts | 2 +- src/selectors/jsonBody.ts | 2 +- src/selectors/method.ts | 2 +- src/selectors/searchParam.ts | 2 +- src/selectors/textBody.ts | 2 +- src/selectors/url.ts | 2 +- src/selectors/urlEncodedBody.ts | 2 +- src/types.ts | 2 +- src/utils.ts | 44 ------------------- 23 files changed, 52 insertions(+), 78 deletions(-) rename src/{selectors/createSelector.ts => selector.ts} (51%) delete mode 100644 src/utils.ts diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index dd9a6a2..3869a39 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -1,5 +1,5 @@ import { Result, Middleware, getPrismyContext, Handler } from '../src' -import { createPrismySelector } from '../src/selectors/createSelector' +import { createPrismySelector } from '../src/selector' import { TestServer } from '../src/test' const ts = TestServer() diff --git a/specs/prismy.spec.ts b/specs/prismy.spec.ts index e4dd249..d38b5d4 100644 --- a/specs/prismy.spec.ts +++ b/specs/prismy.spec.ts @@ -4,8 +4,8 @@ import { Middleware, Result, ErrorResult, + createPrismySelector, } from '../src' -import { createPrismySelector } from '../src/selectors/createSelector' import { TestServer } from '../src/test' const ts = TestServer() diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 7f49638..416a9c8 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -18,7 +18,7 @@ import { } from '../../src' import http from 'http' import { InjectSelector } from '../../src/selectors/inject' -import { PrismySelector } from '../../src/selectors/createSelector' +import { PrismySelector } from '../../src/selector' function expectType(value: T): void {} @@ -52,7 +52,7 @@ Handler([BodySelector], () => Result(null)) const middleware1 = Middleware( [UrlSelector(), MethodSelector()], - (next) => async (url, method) => { + async (next, url, method) => { expectType(url) expectType(method) return next() @@ -60,7 +60,7 @@ const middleware1 = Middleware( ) expectType< - (next: PrismyNextFunction) => (url: URL, method: string | undefined) => any + (next: PrismyNextFunction, url: URL, method: string | undefined) => any >(middleware1.handler) // @ts-expect-error diff --git a/src/handler.ts b/src/handler.ts index ed12d02..ce007b5 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -4,9 +4,9 @@ import { MaybePromise, SelectorReturnTypeTuple, PrismyResult, + resolveSelectors, } from '.' -import { PrismySelector } from './selectors/createSelector' -import { compileHandler } from './utils' +import { PrismySelector } from './selector' export class PrismyHandler< S extends PrismySelector[] = PrismySelector[], @@ -23,7 +23,9 @@ export class PrismyHandler< ) {} async __internal__handler(): Promise> { - const next: PrismyNextFunction = compileHandler(this.selectors, this.handle) + const next: PrismyNextFunction = async () => { + return this.handle(...(await resolveSelectors(this.selectors))) + } const pipe = this.middlewareList.reduce((next, middleware) => { return middleware.pipe(next) diff --git a/src/index.ts b/src/index.ts index e2b60e3..7468862 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './types' export * from './prismy' export * from './middleware' +export * from './selector' export * from './selectors' export * from './error' export * from './router' diff --git a/src/middleware.ts b/src/middleware.ts index fc45de3..d0b2f13 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,7 +1,6 @@ -import { PrismyNextFunction, PrismyResult } from '.' -import { PrismySelector } from './selectors/createSelector' +import { PrismyNextFunction, PrismyResult, resolveSelectors } from '.' +import { PrismySelector } from './selector' import { SelectorReturnTypeTuple } from './types' -import { resolveSelectors } from './utils' export class PrismyMiddleware< S extends PrismySelector[] = PrismySelector[], diff --git a/src/prismy.ts b/src/prismy.ts index 1de83aa..9f308d8 100644 --- a/src/prismy.ts +++ b/src/prismy.ts @@ -2,7 +2,7 @@ import { AsyncLocalStorage } from 'async_hooks' import { IncomingMessage, RequestListener, ServerResponse } from 'http' import { PrismyMiddleware } from './middleware' import { Handler, PrismyHandler } from './handler' -import { PrismySelector } from './selectors/createSelector' +import { PrismySelector } from './selector' import { MaybePromise, PrismyContext, SelectorReturnTypeTuple } from './types' import { PrismyResult } from './result' import { createErrorResultFromError } from './error' diff --git a/src/router.ts b/src/router.ts index cc8c026..fab4b6f 100644 --- a/src/router.ts +++ b/src/router.ts @@ -3,10 +3,7 @@ import { MethodSelector, UrlSelector } from './selectors' import { match as createMatchFunction } from 'path-to-regexp' import { getPrismyContext } from './prismy' import { createError } from './error' -import { - createPrismySelector, - PrismySelector, -} from './selectors/createSelector' +import { createPrismySelector, PrismySelector } from './selector' import { PrismyMiddleware, PrismyResult } from '.' import { Handler, PrismyHandler } from './handler' import { join as joinPath } from 'path' diff --git a/src/selectors/createSelector.ts b/src/selector.ts similarity index 51% rename from src/selectors/createSelector.ts rename to src/selector.ts index 0b87fe7..a71ea8a 100644 --- a/src/selectors/createSelector.ts +++ b/src/selector.ts @@ -1,5 +1,4 @@ -import { SelectorReturnTypeTuple } from '..' -import { compileHandler } from '../utils' +import { SelectorReturnTypeTuple } from '.' export class PrismySelector< T, @@ -10,9 +9,8 @@ export class PrismySelector< public select: (...args: SelectorReturnTypeTuple) => Promise | T, ) {} - __internal__selector(): T | Promise { - const compiledSelector = compileHandler(this.selectors, this.select) - return compiledSelector() + async __internal__selector(): Promise { + return this.select(...(await resolveSelectors(this.selectors))) } } @@ -35,3 +33,25 @@ export function createPrismySelector( } return new PrismySelector(selectorsOrFn, selectorFunction) } + +/** + * Executes the selectors and produces an array of args to be passed to + * a handler + * + * @param context - Context object to be passed to the selectors + * @param selectors - array of selectos + * @returns arguments for a handler + * + * @internal + */ +export async function resolveSelectors[]>( + selectors: [...S], +): Promise> { + const resolvedValues = [] + for (const selector of selectors) { + const resolvedValue = await selector.__internal__selector() + resolvedValues.push(resolvedValue) + } + + return resolvedValues as SelectorReturnTypeTuple +} diff --git a/src/selectors/body.ts b/src/selectors/body.ts index 801176a..547128a 100644 --- a/src/selectors/body.ts +++ b/src/selectors/body.ts @@ -2,7 +2,7 @@ import { parse } from 'querystring' import { getPrismyContext } from '../prismy' import { readJsonBody, readTextBody } from '../bodyReaders' import { createError } from '../error' -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' /** * Options for {@link bodySelector} diff --git a/src/selectors/bufferBody.ts b/src/selectors/bufferBody.ts index bc41e97..1f2dcdc 100644 --- a/src/selectors/bufferBody.ts +++ b/src/selectors/bufferBody.ts @@ -1,6 +1,6 @@ import { readBufferBody } from '../bodyReaders' import { getPrismyContext } from '../prismy' -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' /** * Options for {@link createBufferBodySelector} diff --git a/src/selectors/cookie.ts b/src/selectors/cookie.ts index 4e554c0..8eee8fb 100644 --- a/src/selectors/cookie.ts +++ b/src/selectors/cookie.ts @@ -1,6 +1,6 @@ import cookie from 'cookie' import { getPrismyContext } from '../prismy' -import { createPrismySelector } from './createSelector' +import { createPrismySelector } from '../selector' const cookieMap = new WeakMap() diff --git a/src/selectors/headers.ts b/src/selectors/headers.ts index d0e11f6..bb4fa62 100644 --- a/src/selectors/headers.ts +++ b/src/selectors/headers.ts @@ -1,6 +1,6 @@ import { IncomingHttpHeaders } from 'http' import { getPrismyContext } from '../prismy' -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' const headersSelector: PrismySelector = createPrismySelector(() => { diff --git a/src/selectors/index.ts b/src/selectors/index.ts index d46ab0b..ce35009 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -8,4 +8,3 @@ export * from './url' export * from './urlEncodedBody' export * from './textBody' export * from './cookie' -export * from './createSelector' diff --git a/src/selectors/inject.ts b/src/selectors/inject.ts index 7f9474a..7d44e9e 100644 --- a/src/selectors/inject.ts +++ b/src/selectors/inject.ts @@ -1,4 +1,4 @@ -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' export function InjectSelector(value: V): PrismySelector { return createPrismySelector(() => value) diff --git a/src/selectors/jsonBody.ts b/src/selectors/jsonBody.ts index 594f963..2ef4735 100644 --- a/src/selectors/jsonBody.ts +++ b/src/selectors/jsonBody.ts @@ -1,7 +1,7 @@ import { readJsonBody } from '../bodyReaders' import { createError } from '../error' import { getPrismyContext } from '../prismy' -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' /** * Options for {@link createJsonBodySelector} diff --git a/src/selectors/method.ts b/src/selectors/method.ts index bb68c41..02eb751 100644 --- a/src/selectors/method.ts +++ b/src/selectors/method.ts @@ -1,5 +1,5 @@ import { getPrismyContext } from '../prismy' -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' const methodSelector: PrismySelector = createPrismySelector( () => { diff --git a/src/selectors/searchParam.ts b/src/selectors/searchParam.ts index 12359f6..5eff2f9 100644 --- a/src/selectors/searchParam.ts +++ b/src/selectors/searchParam.ts @@ -1,4 +1,4 @@ -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' import { UrlSelector } from './url' /** diff --git a/src/selectors/textBody.ts b/src/selectors/textBody.ts index 8a6c3cf..05b3bb1 100644 --- a/src/selectors/textBody.ts +++ b/src/selectors/textBody.ts @@ -1,6 +1,6 @@ import { getPrismyContext } from '../prismy' import { readTextBody } from '../bodyReaders' -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' /** * Options for {@link textBodySelector} diff --git a/src/selectors/url.ts b/src/selectors/url.ts index b1e9dfc..f90206f 100644 --- a/src/selectors/url.ts +++ b/src/selectors/url.ts @@ -1,6 +1,6 @@ import { URL } from 'url' import { getPrismyContext } from '../prismy' -import { createPrismySelector } from './createSelector' +import { createPrismySelector } from '../selector' const urlMap = new WeakMap() diff --git a/src/selectors/urlEncodedBody.ts b/src/selectors/urlEncodedBody.ts index 1de566a..def15ed 100644 --- a/src/selectors/urlEncodedBody.ts +++ b/src/selectors/urlEncodedBody.ts @@ -2,7 +2,7 @@ import { ParsedUrlQuery, parse } from 'querystring' import { getPrismyContext } from '../prismy' import { readTextBody } from '../bodyReaders' import { createError } from '../error' -import { createPrismySelector, PrismySelector } from './createSelector' +import { createPrismySelector, PrismySelector } from '../selector' /** * Options for {@link createUrlEncodedBodySelector} diff --git a/src/types.ts b/src/types.ts index a213182..ae47eda 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import { IncomingMessage } from 'http' import { PrismyResult } from './result' -import { PrismySelector } from './selectors/createSelector' +import { PrismySelector } from './selector' /** * Request context used in selectors diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 683ed5f..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { MaybePromise } from '.' -import { PrismySelector } from './selectors/createSelector' -import { SelectorReturnTypeTuple } from './types' - -/** - * Compile a handler into a runnable function by resolving selectors - * and injecting the arguments into the handler. - * - * @param selectors - Selectors to gather handler arguments from - * @param handler - Handler to be compiled - * @returns compiled handler ready to be used - * - * @internal - */ -export function compileHandler[], R>( - selectors: [...S], - handler: (...args: SelectorReturnTypeTuple) => MaybePromise, -): () => Promise { - return async () => { - return handler(...(await resolveSelectors(selectors))) - } -} - -/** - * Executes the selectors and produces an array of args to be passed to - * a handler - * - * @param context - Context object to be passed to the selectors - * @param selectors - array of selectos - * @returns arguments for a handler - * - * @internal - */ -export async function resolveSelectors[]>( - selectors: [...S], -): Promise> { - const resolvedValues = [] - for (const selector of selectors) { - const resolvedValue = await selector.__internal__selector() - resolvedValues.push(resolvedValue) - } - - return resolvedValues as SelectorReturnTypeTuple -} From 0c3da97b425a3b25cc061c8e750465762219a378 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Apr 2024 07:04:36 +0900 Subject: [PATCH 099/109] Make middleware selectors optional --- specs/middleware.spec.ts | 22 ++++++++++++++++++++++ specs/types/basic.ts | 10 +++++++++- src/middleware.ts | 16 ++++++++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/specs/middleware.spec.ts b/specs/middleware.spec.ts index 3869a39..7074eab 100644 --- a/specs/middleware.spec.ts +++ b/specs/middleware.spec.ts @@ -65,4 +65,26 @@ describe('middleware', () => { expect(await res.text()).toBe('/ : Hey!') expect(res.status).toBe(500) }) + + it('creates without selectors(shorthand)', async () => { + const errorMiddleware = Middleware(async (next) => { + try { + return await next() + } catch (error) { + return Result(`Customized : ${(error as any).message}`, 500) + } + }) + const handler = Handler( + [], + () => { + throw new Error('Hey!') + }, + [errorMiddleware], + ) + + const res = await ts.load(handler).call() + + expect(await res.text()).toBe('Customized : Hey!') + expect(res.status).toBe(500) + }) }) diff --git a/specs/types/basic.ts b/specs/types/basic.ts index 416a9c8..5dca01e 100644 --- a/specs/types/basic.ts +++ b/specs/types/basic.ts @@ -9,6 +9,7 @@ import { prismy, PrismyErrorResult, PrismyHandler, + PrismyMiddleware, PrismyNextFunction, PrismyResult, PrismyRoute, @@ -63,8 +64,15 @@ expectType< (next: PrismyNextFunction, url: URL, method: string | undefined) => any >(middleware1.handler) +const middleware2 = Middleware((next) => { + return next() +}) + +expectType>(middleware2) +expectType<(next: PrismyNextFunction) => any>(middleware2.handler) + // @ts-expect-error -Middleware([BodySelector], () => () => Result(null)) +Middleware([BodySelector], () => Result(null)) http.createServer(prismy([], () => Result(''), [middleware1])) diff --git a/src/middleware.ts b/src/middleware.ts index d0b2f13..fa790be 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -64,6 +64,18 @@ export function Middleware[]>( next: PrismyNextFunction, ...args: SelectorReturnTypeTuple ) => Promise, -): PrismyMiddleware { - return new PrismyMiddleware(selectors, handler) +): PrismyMiddleware +export function Middleware( + handler: (next: PrismyNextFunction) => Promise, +): PrismyMiddleware<[]> +export function Middleware( + selectorsOrHandler: + | any[] + | ((next: PrismyNextFunction) => Promise), + handler?: (next: PrismyNextFunction, ...args: any[]) => Promise, +): PrismyMiddleware { + if (Array.isArray(selectorsOrHandler)) { + return new PrismyMiddleware(selectorsOrHandler, handler!) + } + return new PrismyMiddleware([], selectorsOrHandler) } From 9fcd0e44848665dbf89561719b40d402c324f58e Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Apr 2024 07:08:14 +0900 Subject: [PATCH 100/109] Update v4 todo --- v4-todo.md | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/v4-todo.md b/v4-todo.md index 9efd1ac..7afd8e9 100644 --- a/v4-todo.md +++ b/v4-todo.md @@ -90,8 +90,11 @@ - [x] Added DI Selector - [x] File uploading - Interface is a bit confusing and too different and rewriting cost is too high. Should just provide it as an example instead of including this project. -- [ ] Rewrite prismy-session - - [ ] Make it compatible with SessionStore of express-session +- [x] Rewrite prismy-session +- [ ] Make middleware selectors omittable +- [x] Fix midelware behavior +- [ ] Combine Routers + # V5 TODO(TBD) @@ -204,3 +207,35 @@ First one goes first. If the first one fails, latter ones never triggered. If selectors don't cause side effect, you can even run them concurrently. + +## Middleare issue +Handler can resolve error by itself. it should not handle the error. error must be handled in prismy + +Handler should throw error but middleware must be applied. +Default error handling behavior must be performed in prismy not in handlers. + +## Combine Routers + +There might be multiple notFoundHandlers +Override it when combining routers +If it is not givenm, use last one should be used + +Not gonna be implemented. Must be handled outside of this lib + +## Simplify middleware + +Simplify structrue + +```js +// from +createMiddleware([...selctors]() => (next) => (...selectedValues) => ResObj) + +// To +Middleware([...selectors], (next, ...selectedValues) => ResObj) +``` + +Make selectors optionable + +```js +Middleware((next) => ResObj) +``` From 5d10fdfc581cf3a3eb8896991194601753cdfa37 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Apr 2024 07:08:50 +0900 Subject: [PATCH 101/109] 4.0.0-13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2baad1d..d691ce6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-12", + "version": "4.0.0-13", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 8613f13068291853527864b172d723992fc4b05b Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Apr 2024 07:51:37 +0900 Subject: [PATCH 102/109] Export PrismyError --- src/error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/error.ts b/src/error.ts index 3e123d2..715bcb9 100644 --- a/src/error.ts +++ b/src/error.ts @@ -20,7 +20,7 @@ export function createErrorResultFromError(error: any) { return Result(message, statusCode) } -class PrismyError extends Error { +export class PrismyError extends Error { statusCode?: number originalError?: unknown } From e5e2e36509ee334d4f4e14aa8c45c1d8ad055925 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 15 Apr 2024 07:51:58 +0900 Subject: [PATCH 103/109] 4.0.0-14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d691ce6..c0d7ee9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-13", + "version": "4.0.0-14", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 12775ad50dde6e5630678c19da5a7af5d9c83b4f Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 23 Apr 2024 18:09:49 +0900 Subject: [PATCH 104/109] Improve error message of assertion methods --- specs/result.spec.ts | 50 +++++++++++++++++++++++++++++------------ src/result.ts | 53 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 18 deletions(-) diff --git a/specs/result.spec.ts b/specs/result.spec.ts index e41f20b..8927267 100644 --- a/specs/result.spec.ts +++ b/specs/result.spec.ts @@ -138,7 +138,14 @@ describe('assertErrorResult', () => { try { assertErrorResult(result) } catch (error) { - expect(error) + expect((error as Error).message).toEqual( + [ + 'The given PrismyResult is NOT an error result.', + '', + 'Result:', + JSON.stringify(result, null, 2), + ].join('\n'), + ) return } throw new Error('must throw') @@ -146,11 +153,8 @@ describe('assertErrorResult', () => { it('does not throw if result is an error result', () => { const result = ErrorResult(400, null) - try { - assertErrorResult(result) - } catch (error) { - throw new Error('must NOT throw') - } + + assertErrorResult(result) }) }) @@ -160,7 +164,14 @@ describe('assertErrorResult', () => { try { assertNoErrorResult(result) } catch (error) { - expect(error) + expect((error as Error).message).toEqual( + [ + 'The given PrismyResult is an error result.', + '', + 'Result:', + JSON.stringify(result, null, 2), + ].join('\n'), + ) return } throw new Error('must throw') @@ -168,11 +179,8 @@ describe('assertErrorResult', () => { it('does not throw if result is NOT an error result', () => { const result = Result(null) - try { - assertNoErrorResult(result) - } catch (error) { - throw new Error('must NOT throw') - } + + assertNoErrorResult(result) }) }) @@ -281,7 +289,14 @@ describe('assertRedirectResult', () => { try { assertRedirectResult(result) } catch (error) { - expect(error) + expect((error as Error).message).toEqual( + [ + 'The given PrismyResult is NOT a redirect result.', + '', + 'Result:', + JSON.stringify(result, null, 2), + ].join('\n'), + ) return } throw new Error('must throw') @@ -303,7 +318,14 @@ describe('assertRedirectResult', () => { try { assertNoRedirectResult(result) } catch (error) { - expect(error) + expect((error as Error).message).toEqual( + [ + 'The given PrismyResult is a redirect result.', + '', + 'Result:', + JSON.stringify(result, null, 2), + ].join('\n'), + ) return } throw new Error('must throw') diff --git a/src/result.ts b/src/result.ts index 363079a..ca6cf77 100644 --- a/src/result.ts +++ b/src/result.ts @@ -175,7 +175,14 @@ export function assertErrorResult( if (isErrorResult(result)) { return } - throw new Error('The given PrismyResult is NOT an error result.') + throw new Error( + [ + 'The given PrismyResult is NOT an error result.', + '', + 'Result:', + jsonStringifyRecursive(result), + ].join('\n'), + ) } export function assertNoErrorResult

( @@ -184,7 +191,14 @@ export function assertNoErrorResult

( if (!isErrorResult(result)) { return } - throw new Error('The given PrismyResult is an error result.') + throw new Error( + [ + 'The given PrismyResult is an error result.', + '', + 'Result:', + jsonStringifyRecursive(result), + ].join('\n'), + ) } /** @@ -227,7 +241,14 @@ export function assertRedirectResult( if (isRedirectResult(result)) { return } - throw new Error('The given PrismyResult is NOT a redirect result.') + throw new Error( + [ + 'The given PrismyResult is NOT a redirect result.', + '', + 'Result:', + jsonStringifyRecursive(result), + ].join('\n'), + ) } export function assertNoRedirectResult

( @@ -236,5 +257,29 @@ export function assertNoRedirectResult

( if (!isRedirectResult(result)) { return } - throw new Error('The given PrismyResult is a redirect result.') + throw new Error( + [ + 'The given PrismyResult is a redirect result.', + '', + 'Result:', + jsonStringifyRecursive(result), + ].join('\n'), + ) +} + +function jsonStringifyRecursive(value: any) { + const cache = new Set() + return JSON.stringify( + value, + (_key, value) => { + if (typeof value === 'object' && value !== null) { + if (cache.has(value)) { + return '(Recursive)' + } + cache.add(value) + } + return value + }, + 2, + ) } From 73bbac9774050b0ebdf466f1b0af06f2a9292044 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 23 Apr 2024 18:58:20 +0900 Subject: [PATCH 105/109] Make PrismyResult throwable --- specs/error.spec.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++ src/error.ts | 6 ++++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 specs/error.spec.ts diff --git a/specs/error.spec.ts b/specs/error.spec.ts new file mode 100644 index 0000000..828b40c --- /dev/null +++ b/specs/error.spec.ts @@ -0,0 +1,54 @@ +import { + ErrorResult, + Result, + createError, + createErrorResultFromError, +} from '../src' + +describe('createErrorResultFromError', () => { + it('returns error as it is if the error is PrismyResult', () => { + // Given + const error = Result('test') + + // When + const result = createErrorResultFromError(error) + + // Then + expect(result).toBe(error) + }) + + it('returns error as it is if the error is PrismyErrorResult', () => { + // Given + const error = ErrorResult(403, 'Forbidden') + + // When + const result = createErrorResultFromError(error) + + // Then + expect(result).toBe(error) + }) + + it('creates 500 result if the error is unknown', () => { + // Given + const error = new Error('Unknown') + + // When + const result = createErrorResultFromError(error) + + // Then + expect(result.statusCode).toBe(500) + expect(result.body).toMatch(/^Error: Unknown/) + }) + + it('uses statusCode if provided', () => { + // Given + const error = createError(403, 'Forbidden') + + // When + const result = createErrorResultFromError(error) + + // Then + expect(result.statusCode).toBe(403) + expect(result.body).toMatch(/^Error: Forbidden/) + }) +}) diff --git a/src/error.ts b/src/error.ts index 715bcb9..96cf015 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,4 @@ -import { Result } from './result' +import { PrismyResult, Result } from './result' /** * Creates a response object from an error @@ -12,6 +12,10 @@ import { Result } from './result' * @public */ export function createErrorResultFromError(error: any) { + if (error instanceof PrismyResult) { + return error + } + const statusCode = error.statusCode || error.status || 500 /* istanbul ignore next */ const message = From df924993a12f7f3aed5c066d104d426e061433fc Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 23 Apr 2024 19:00:34 +0900 Subject: [PATCH 106/109] Fix coverage --- src/result.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/result.ts b/src/result.ts index ca6cf77..eb1ec97 100644 --- a/src/result.ts +++ b/src/result.ts @@ -267,6 +267,7 @@ export function assertNoRedirectResult

( ) } +/* istanbul ignore next */ function jsonStringifyRecursive(value: any) { const cache = new Set() return JSON.stringify( From 0117b459335b8c77fb69c70b5c3f396591b19843 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 23 Apr 2024 19:00:48 +0900 Subject: [PATCH 107/109] 4.0.0-15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c0d7ee9..8c5ff83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-14", + "version": "4.0.0-15", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http", From 355d9f8951b021246e84daaaf6856f1c373f3da5 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 6 May 2024 12:51:43 +0900 Subject: [PATCH 108/109] Add OptionalRouteParamSelector --- specs/router.spec.ts | 19 +++++++++++-------- src/router.ts | 22 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/specs/router.spec.ts b/specs/router.spec.ts index 4bd819a..837244e 100644 --- a/specs/router.spec.ts +++ b/specs/router.spec.ts @@ -7,6 +7,7 @@ import { getPrismyContext, ErrorResult, createPrismySelector, + OptionalRouteParamSelector, } from '../src' import { Handler } from '../src/handler' import { InjectSelector } from '../src/selectors/inject' @@ -260,7 +261,7 @@ describe('router', () => { }) describe('RouteParamSelector', () => { - it('resolves null if the param is missing', async () => { + it('throws an error if the param is missing', async () => { expect.hasAssertions() const handlerA = Handler([], () => { return Result('a') @@ -276,8 +277,10 @@ describe('RouteParamSelector', () => { const res = await ts.load(routerHandler).call('/b/test-param') - expect(res.status).toBe(200) - expect(await res.text()).toBe('') + expect(res.status).toBe(404) + expect(await res.text()).toContain( + 'Error: Route parameter not-id not found', + ) }) it('resolves a param (named parameter)', async () => { @@ -305,9 +308,9 @@ describe('RouteParamSelector', () => { }) const handlerB = Handler( [ - RouteParamSelector('attr1'), - RouteParamSelector('attr2'), - RouteParamSelector('attr3'), + OptionalRouteParamSelector('attr1'), + OptionalRouteParamSelector('attr2'), + OptionalRouteParamSelector('attr3'), ], (attr1, attr2, attr3) => { return Result({ @@ -374,7 +377,7 @@ describe('RouteParamSelector', () => { return Result('a') }) const handlerB = Handler( - [RouteParamSelector('param1'), RouteParamSelector('param2')], + [RouteParamSelector('param1'), OptionalRouteParamSelector('param2')], (param1, param2) => { return Result({ param1, @@ -409,7 +412,7 @@ describe('RouteParamSelector', () => { const handlerA = Handler([], () => { return Result('a') }) - const handlerB = Handler([RouteParamSelector('param')], (param) => { + const handlerB = Handler([OptionalRouteParamSelector('param')], (param) => { return Result({ param, }) diff --git a/src/router.ts b/src/router.ts index fab4b6f..698b323 100644 --- a/src/router.ts +++ b/src/router.ts @@ -133,13 +133,27 @@ function getRouteParamsFromPrismyContext(context: PrismyContext) { return routeParamsMap.get(context) } -export function RouteParamSelector( +function resolveRouteParam(paramName: string) { + const context = getPrismyContext() + const param = getRouteParamsFromPrismyContext(context)[paramName] + return param != null ? (Array.isArray(param) ? param[0] : param) : null +} + +export function RouteParamSelector(paramName: string): PrismySelector { + return createPrismySelector(() => { + const resolvedParam = resolveRouteParam(paramName) + if (resolvedParam == null) { + throw createError(404, `Route parameter ${paramName} not found`) + } + return resolvedParam + }) +} + +export function OptionalRouteParamSelector( paramName: string, ): PrismySelector { return createPrismySelector(() => { - const context = getPrismyContext() - const param = getRouteParamsFromPrismyContext(context)[paramName] - return param != null ? (Array.isArray(param) ? param[0] : param) : null + return resolveRouteParam(paramName) }) } From 932e139b651f3d5454b289b77693ef3bce5d4d78 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 6 May 2024 12:51:57 +0900 Subject: [PATCH 109/109] 4.0.0-16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c5ff83..9a4a06c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prismy", - "version": "4.0.0-15", + "version": "4.0.0-16", "description": ":rainbow: Simple and fast type safe server library.", "keywords": [ "http",