├── .prettierrc ├── tsconfig.build.json ├── .gitignore ├── examples ├── with-session │ ├── readme.md │ ├── src │ │ ├── scripts │ │ │ └── serve.ts │ │ └── index.ts │ ├── package.json │ └── tsconfig.json └── basic-server │ ├── src │ ├── scripts │ │ └── serve.ts │ ├── specs │ │ └── index.spec.ts │ └── index.ts │ ├── package.json │ └── tsconfig.json ├── .travis.yml ├── src ├── index.ts ├── selectors │ ├── index.ts │ ├── context.ts │ ├── headers.ts │ ├── method.ts │ ├── url.ts │ ├── query.ts │ ├── textBody.ts │ ├── bufferBody.ts │ ├── urlEncodedBody.ts │ ├── body.ts │ └── jsonBody.ts ├── error.ts ├── middleware.ts ├── send.ts ├── prismy.ts ├── bodyReaders.ts ├── types.ts ├── router.ts └── utils.ts ├── scripts └── generate-docs.sh ├── specs ├── selectors │ ├── method.spec.ts │ ├── headers.spec.ts │ ├── textBody.spec.ts │ ├── bufferBody.spec.ts │ ├── context.spec.ts │ ├── urlEncodedBody.spec.ts │ ├── url.spec.ts │ ├── query.spec.ts │ ├── body.spec.ts │ └── jsonBody.spec.ts ├── helpers.ts ├── types │ ├── prismy.ts │ └── middleware.ts ├── middleware.spec.ts ├── utils.spec.ts ├── prismy.spec.ts ├── router.spec.ts ├── bodyReaders.spec.ts └── send.spec.ts ├── tsconfig.json ├── package.json ├── resources └── logo.svg ├── readme.md └── temp └── prismy.api.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .env 5 | coverage 6 | temp 7 | package-lock.json 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /examples/with-session/readme.md: -------------------------------------------------------------------------------- 1 | # With session 2 | 3 | Install and run: 4 | 5 | ``` 6 | npm i 7 | npm build 8 | npm start 9 | ``` 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | - 'lts/*' 5 | install: 6 | - npm i 7 | script: 8 | - npm run test 9 | after_success: 10 | - npm run codecov 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './utils' 3 | export * from './prismy' 4 | export * from './middleware' 5 | export * from './selectors' 6 | export * from './error' 7 | export * from './router' 8 | -------------------------------------------------------------------------------- /examples/basic-server/src/scripts/serve.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import handler from '../index' 3 | 4 | const port = process.env.PORT || 3000 5 | 6 | const server = new http.Server(handler) 7 | 8 | server.listen(port) 9 | console.log(`Hosting... http://localhost:${port}`) 10 | -------------------------------------------------------------------------------- /examples/with-session/src/scripts/serve.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import handler from '../index' 3 | 4 | const port = process.env.PORT || 3000 5 | 6 | const server = new http.Server(handler) 7 | 8 | server.listen(port) 9 | console.log(`Hosting... http://localhost:${port}`) 10 | -------------------------------------------------------------------------------- /examples/basic-server/src/specs/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { rootHandler } from '..' 2 | 3 | describe('index', () => { 4 | it('returns root page html', async () => { 5 | // When 6 | const result = await rootHandler.handler() 7 | 8 | // Then 9 | expect(result.body).toContain('Root Page') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/selectors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './body' 2 | export * from './bufferBody' 3 | export * from './context' 4 | export * from './headers' 5 | export * from './jsonBody' 6 | export * from './method' 7 | export * from './query' 8 | export * from './url' 9 | export * from './urlEncodedBody' 10 | export * from './textBody' 11 | -------------------------------------------------------------------------------- /scripts/generate-docs.sh: -------------------------------------------------------------------------------- 1 | DIR="out" 2 | 3 | if ! [ -d $DIR ]; then 4 | echo "\n[Error!] Docs directory does not exist\n\n" 5 | exit 2 6 | fi 7 | 8 | cd $DIR 9 | git init 10 | touch .nojekyll 11 | git checkout -b gh-pages 12 | git add -A 13 | git commit -a -m "Deployed at $(date)" 14 | git remote add origin https://github.com/prismyland/prismy.git 15 | git push -f origin gh-pages 16 | rm -Rf .git 17 | -------------------------------------------------------------------------------- /src/selectors/context.ts: -------------------------------------------------------------------------------- 1 | import { Context, SyncSelector } from '../types' 2 | 3 | /** 4 | * Selector to extract the request context 5 | * 6 | * @example 7 | * Simple example 8 | * ```ts 9 | * 10 | * const prismyHandler = prismy( 11 | * [contextSelector], 12 | * context => { 13 | * ... 14 | * } 15 | * ) 16 | * ``` 17 | * 18 | * @param context - The request context 19 | * @returns The request context 20 | * 21 | * @public 22 | */ 23 | export const contextSelector: SyncSelector = context => context 24 | -------------------------------------------------------------------------------- /specs/selectors/method.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { methodSelector, prismy, res } from '../../src' 4 | 5 | describe('methodSelector', () => { 6 | it('selects method', async () => { 7 | const handler = prismy([methodSelector], method => { 8 | return res(method) 9 | }) 10 | 11 | await testHandler(handler, async url => { 12 | const response = await got(url) 13 | 14 | expect(response).toMatchObject({ 15 | statusCode: 200, 16 | body: 'GET' 17 | }) 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/selectors/headers.ts: -------------------------------------------------------------------------------- 1 | import { IncomingHttpHeaders } from 'http' 2 | import { SyncSelector } from '../types' 3 | 4 | /** 5 | * A selector to extract the headers of a request 6 | * 7 | * @example 8 | * Simple example 9 | * ```ts 10 | * 11 | * const prismyHandler = prismy( 12 | * [headerSelector], 13 | * headers => { 14 | * ... 15 | * } 16 | * ) 17 | * ``` 18 | * 19 | * @param context - The request context 20 | * @returns The request headers 21 | * 22 | * @public 23 | */ 24 | export const headersSelector: SyncSelector = context => 25 | context.req.headers 26 | -------------------------------------------------------------------------------- /examples/basic-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-server", 3 | "private": true, 4 | "scripts": { 5 | "build": "tsc", 6 | "start": "node dist/scripts/serve.js", 7 | "test": "jest" 8 | }, 9 | "devDependencies": { 10 | "@types/jest": "^26.0.15", 11 | "jest": "^26.6.1", 12 | "ts-jest": "^26.4.2", 13 | "typescript": "^4.0.3" 14 | }, 15 | "dependencies": { 16 | "prismy": "^3.0.0-7" 17 | }, 18 | "jest": { 19 | "preset": "ts-jest", 20 | "testEnvironment": "node", 21 | "testPathIgnorePatterns": [ 22 | "/node_modules/", 23 | "/dist/" 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/selectors/method.ts: -------------------------------------------------------------------------------- 1 | import { SyncSelector } from '../types' 2 | 3 | /** 4 | * Selector to extract the HTTP method from the request 5 | * 6 | * @example 7 | * Simple example 8 | * ```ts 9 | * 10 | * const prismyHandler = prismy( 11 | * [methodSelector], 12 | * method => { 13 | * if (method !== 'GET') { 14 | * throw createError(405) 15 | * } 16 | * } 17 | * ) 18 | * ``` 19 | * 20 | * @param context - The request context 21 | * @returns the http request method 22 | * 23 | * @public 24 | */ 25 | export const methodSelector: SyncSelector = ({ req }) => { 26 | return req.method 27 | } 28 | -------------------------------------------------------------------------------- /specs/helpers.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import listen from 'test-listen' 3 | import { RequestListener } from 'http' 4 | 5 | export type TestCallback = (url: string) => void 6 | 7 | /* istanbul ignore next */ 8 | export async function testHandler( 9 | handler: RequestListener, 10 | testCallback: TestCallback 11 | ): Promise { 12 | const server = new http.Server(handler) 13 | 14 | const url = await listen(server) 15 | try { 16 | await testCallback(url) 17 | } catch (error) { 18 | throw error 19 | } finally { 20 | server.close() 21 | } 22 | } 23 | 24 | /* istanbul ignore next */ 25 | export function expectType(value: T): void {} 26 | -------------------------------------------------------------------------------- /specs/selectors/headers.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { headersSelector, prismy, res } from '../../src' 4 | 5 | describe('headersSelector', () => { 6 | it('select headers', async () => { 7 | const handler = prismy([headersSelector], headers => { 8 | return res(headers['x-test']) 9 | }) 10 | 11 | await testHandler(handler, async url => { 12 | const response = await got(url, { 13 | headers: { 14 | 'x-test': 'Hello, World!' 15 | } 16 | }) 17 | 18 | expect(response).toMatchObject({ 19 | statusCode: 200, 20 | body: 'Hello, World!' 21 | }) 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /examples/with-session/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-session", 3 | "private": true, 4 | "scripts": { 5 | "build": "rimraf dist && tsc", 6 | "start": "node dist/scripts/serve.js" 7 | }, 8 | "devDependencies": { 9 | "rimraf": "^2.6.3", 10 | "typescript": "^3.4.5" 11 | }, 12 | "dependencies": { 13 | "prismy": "^1.3.0", 14 | "prismy-cookie": "^1.0.1", 15 | "prismy-method-router": "^1.0.0", 16 | "prismy-session": "^1.0.1", 17 | "prismy-session-strategy-jwt-cookie": "^1.0.0" 18 | }, 19 | "jest": { 20 | "preset": "ts-jest", 21 | "testEnvironment": "node", 22 | "testPathIgnorePatterns": [ 23 | "/node_modules/", 24 | "/dist/" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/basic-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { prismy, res, router } from 'prismy' 2 | 3 | export const rootHandler = prismy([], () => { 4 | return res( 5 | [ 6 | '', 7 | '', 8 | '

Root Page

', 9 | 'Go to /test', 10 | '', 11 | ].join('') 12 | ) 13 | }) 14 | 15 | const testHandler = prismy([], () => { 16 | return res( 17 | [ 18 | '', 19 | '', 20 | '

Test Page

', 21 | 'Go to Root', 22 | '', 23 | ].join('') 24 | ) 25 | }) 26 | 27 | const myRouter = router([ 28 | ['/', rootHandler], 29 | [['/test', 'get'], testHandler], 30 | ]) 31 | 32 | export default myRouter 33 | -------------------------------------------------------------------------------- /specs/selectors/textBody.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { createTextBodySelector, prismy, res } from '../../src' 4 | 5 | describe('createTextBodySelector', () => { 6 | it('creates buffer body selector', async () => { 7 | const textBodySelector = createTextBodySelector() 8 | const handler = prismy([textBodySelector], body => { 9 | return res(`${body.constructor.name}: ${body}`) 10 | }) 11 | 12 | await testHandler(handler, async url => { 13 | const response = await got(url, { method: 'POST', body: 'Hello, World!' }) 14 | 15 | expect(response).toMatchObject({ 16 | statusCode: 200, 17 | body: 'String: Hello, World!' 18 | }) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /specs/selectors/bufferBody.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { createBufferBodySelector, prismy, res } from '../../src' 4 | 5 | describe('createBufferBodySelector', () => { 6 | it('creates buffer body selector', async () => { 7 | const bufferBodySelector = createBufferBodySelector() 8 | const handler = prismy([bufferBodySelector], body => { 9 | return res(`${body.constructor.name}: ${body}`) 10 | }) 11 | 12 | await testHandler(handler, async url => { 13 | const response = await got(url, { method: 'POST', body: 'Hello, World!' }) 14 | 15 | expect(response).toMatchObject({ 16 | statusCode: 200, 17 | body: 'Buffer: Hello, World!' 18 | }) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /specs/selectors/context.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { contextSelector, prismy, res, headersSelector } from '../../src' 4 | 5 | describe('contextSelector', () => { 6 | it('select context', async () => { 7 | const handler = prismy([contextSelector], async context => { 8 | const headers = await headersSelector(context) 9 | return res(headers['x-test']) 10 | }) 11 | 12 | await testHandler(handler, async url => { 13 | const response = await got(url, { 14 | headers: { 15 | 'x-test': 'Hello, World!' 16 | } 17 | }) 18 | 19 | expect(response).toMatchObject({ 20 | statusCode: 200, 21 | body: 'Hello, World!' 22 | }) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /examples/basic-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "compilerOptions": { 4 | "target": "es2018", 5 | "lib": ["es2018"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "importHelpers": true, 9 | "esModuleInterop": true, 10 | "experimentalDecorators": true, 11 | "jsx": "react", 12 | "declaration": true, 13 | "sourceMap": true, 14 | "outDir": "dist", 15 | "noEmitOnError": true, 16 | 17 | "allowJs": false, 18 | "checkJs": false, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "noUnusedParameters": false, 23 | "noImplicitReturns": true, 24 | "noUnusedLocals": true, 25 | "noFallthroughCasesInSwitch": true, 26 | 27 | "suppressImplicitAnyIndexErrors": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/with-session/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "compilerOptions": { 4 | "target": "es2018", 5 | "lib": ["es2018"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "importHelpers": true, 9 | "esModuleInterop": true, 10 | "experimentalDecorators": true, 11 | "jsx": "react", 12 | "declaration": true, 13 | "sourceMap": true, 14 | "outDir": "dist", 15 | "noEmitOnError": true, 16 | 17 | "allowJs": false, 18 | "checkJs": false, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "noUnusedParameters": false, 23 | "noImplicitReturns": true, 24 | "noUnusedLocals": true, 25 | "noFallthroughCasesInSwitch": true, 26 | 27 | "suppressImplicitAnyIndexErrors": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts", "specs/**/*.ts"], 3 | "compilerOptions": { 4 | "target": "es2018", 5 | "lib": ["es2018"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "importHelpers": true, 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "declaration": true, 12 | "sourceMap": true, 13 | "outDir": "dist", 14 | "noEmitOnError": true, 15 | 16 | "allowJs": false, 17 | "checkJs": false, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "noUnusedParameters": false, 22 | "noImplicitReturns": true, 23 | "noUnusedLocals": true, 24 | "noFallthroughCasesInSwitch": true, 25 | 26 | "suppressImplicitAnyIndexErrors": true 27 | }, 28 | "typedocOptions": { 29 | "out": "out", 30 | "mode": "file", 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /specs/types/prismy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | prismy, 3 | urlSelector, 4 | methodSelector, 5 | res, 6 | AsyncSelector, 7 | ResponseObject 8 | } from '../../src' 9 | import { UrlWithStringQuery } from 'url' 10 | import { expectType } from '../helpers' 11 | 12 | const asyncUrlSelector: AsyncSelector = async context => 13 | urlSelector(context) 14 | 15 | const handler1 = prismy( 16 | [urlSelector, methodSelector, asyncUrlSelector], 17 | (url, method, url2) => { 18 | expectType(url) 19 | expectType(method) 20 | expectType(url2) 21 | return res('') 22 | } 23 | ) 24 | 25 | expectType< 26 | ( 27 | url: UrlWithStringQuery, 28 | method: string | undefined, 29 | url2: UrlWithStringQuery 30 | ) => ResponseObject | Promise> 31 | >(handler1.handler) 32 | -------------------------------------------------------------------------------- /src/selectors/url.ts: -------------------------------------------------------------------------------- 1 | import { UrlWithStringQuery, parse } from 'url' 2 | import { SyncSelector } from '../types' 3 | 4 | const urlSymbol = Symbol('prismy-url') 5 | 6 | /** 7 | * Selector for extracting the requested URL 8 | * 9 | * @example 10 | * Simple example 11 | * ```ts 12 | * 13 | * const prismyHandler = prismy( 14 | * [urlSelector], 15 | * url => { 16 | * return res(url.path) 17 | * } 18 | * ) 19 | * ``` 20 | * 21 | * @param context - Request context 22 | * @returns The url of the request 23 | * 24 | * @public 25 | */ 26 | export const urlSelector: SyncSelector = context => { 27 | let url: UrlWithStringQuery | undefined = context[urlSymbol] 28 | if (url == null) { 29 | const { req } = context 30 | /* istanbul ignore next */ 31 | url = context[urlSymbol] = parse(req.url == null ? '' : req.url) 32 | } 33 | return url 34 | } 35 | -------------------------------------------------------------------------------- /specs/selectors/urlEncodedBody.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { createUrlEncodedBodySelector, prismy, res } from '../../src' 4 | 5 | describe('URLEncodedBody', () => { 6 | it('injects parsed url encoded body', async () => { 7 | const urlEncodedBodySelector = createUrlEncodedBodySelector() 8 | 9 | const handler = prismy([urlEncodedBodySelector], body => { 10 | return res(body) 11 | }) 12 | 13 | await testHandler(handler, async url => { 14 | const response = await got(url, { 15 | method: 'POST', 16 | responseType: 'json', 17 | form: { 18 | message: 'Hello, World!' 19 | } 20 | }) 21 | 22 | expect(response).toMatchObject({ 23 | statusCode: 200, 24 | body: { 25 | message: 'Hello, World!' 26 | } 27 | }) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/selectors/query.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery, parse } from 'querystring' 2 | import { SyncSelector } from '../types' 3 | import { urlSelector } from './url' 4 | 5 | const querySymbol = Symbol('prismy-query') 6 | 7 | /** 8 | * Selector to extract the parsed query from the request URL 9 | * 10 | * @example 11 | * Simple example 12 | * ```ts 13 | * 14 | * const prismyHandler = prismy( 15 | * [querySelector], 16 | * query => { 17 | * doSomethingWithQuery(query) 18 | * } 19 | * ) 20 | * ``` 21 | * 22 | * @param context - Request context 23 | * @returns a selector for the url query 24 | * 25 | * @public 26 | */ 27 | export const querySelector: SyncSelector = context => { 28 | let query: ParsedUrlQuery | undefined = context[querySymbol] 29 | if (query == null) { 30 | const url = urlSelector(context) 31 | /* istanbul ignore next */ 32 | context[querySymbol] = query = url.query != null ? parse(url.query) : {} 33 | } 34 | return query 35 | } 36 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import { res } from './utils' 2 | 3 | /** 4 | * Creates a response object from an error 5 | * 6 | * @remarks 7 | * Convert an error into a simpe response object. 8 | * 9 | * @param error - Options including whether to output json and if in dev mode 10 | * @returns An error response object 11 | * 12 | * @public 13 | */ 14 | export function createErrorResObject(error: any) { 15 | const statusCode = error.statusCode || error.status || 500 16 | /* istanbul ignore next */ 17 | const message = 18 | process.env.NODE_ENV === 'production' ? error.message : error.stack 19 | 20 | return res(message, statusCode) 21 | } 22 | 23 | class PrismyError extends Error { 24 | statusCode?: number 25 | originalError?: unknown 26 | } 27 | 28 | export function createError( 29 | statusCode: number, 30 | message: string, 31 | originalError?: any 32 | ): PrismyError { 33 | const error = new PrismyError(message) 34 | 35 | error.statusCode = statusCode 36 | error.originalError = originalError 37 | return error 38 | } 39 | -------------------------------------------------------------------------------- /src/selectors/textBody.ts: -------------------------------------------------------------------------------- 1 | import { readTextBody } from '../bodyReaders' 2 | import { AsyncSelector } from '../types' 3 | 4 | /** 5 | * Options for {@link createTextBodySelector} 6 | * 7 | * @public 8 | */ 9 | export interface TextBodySelectorOptions { 10 | limit?: string | number 11 | encoding?: string 12 | } 13 | 14 | /** 15 | * Factory function to create a selector to extract the text body from a request 16 | * 17 | * @example 18 | * Simple example 19 | * ```ts 20 | * 21 | * const textBodySelector = createTextBodySelector({ 22 | * limit: "1mb" 23 | * }) 24 | * 25 | * const prismyHandler = prismy( 26 | * [textBodySelector], 27 | * body => { 28 | * ... 29 | * } 30 | * ) 31 | * 32 | * ``` 33 | * 34 | * @param options - Options such as limit and encoding 35 | * @returns a selector for text request bodies 36 | * 37 | * @public 38 | */ 39 | export function createTextBodySelector( 40 | options?: TextBodySelectorOptions 41 | ): AsyncSelector { 42 | return ({ req }) => { 43 | return readTextBody(req, options) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /specs/selectors/url.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { urlSelector, prismy, res } from '../../src' 4 | 5 | describe('urlSelector', () => { 6 | it('selects url', async () => { 7 | const handler = prismy([urlSelector], url => { 8 | return res(url) 9 | }) 10 | 11 | await testHandler(handler, async url => { 12 | const response = await got(url, { 13 | responseType: 'json' 14 | }) 15 | 16 | expect(response).toMatchObject({ 17 | statusCode: 200, 18 | body: expect.objectContaining({ 19 | path: '/' 20 | }) 21 | }) 22 | }) 23 | }) 24 | 25 | it('reuses parsed url', async () => { 26 | const handler = prismy([urlSelector, urlSelector], (url, url2) => { 27 | return res(JSON.stringify(url === url2)) 28 | }) 29 | 30 | await testHandler(handler, async url => { 31 | const response = await got(url) 32 | 33 | expect(response).toMatchObject({ 34 | statusCode: 200, 35 | body: 'true' 36 | }) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /specs/selectors/query.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { querySelector, prismy, res } from '../../src' 4 | 5 | describe('querySelector', () => { 6 | it('selects query', async () => { 7 | const handler = prismy([querySelector], query => { 8 | return res(query) 9 | }) 10 | 11 | await testHandler(handler, async url => { 12 | const response = await got(url, { 13 | searchParams: { message: 'Hello, World!' }, 14 | responseType: 'json' 15 | }) 16 | 17 | expect(response).toMatchObject({ 18 | statusCode: 200, 19 | body: { message: 'Hello, World!' } 20 | }) 21 | }) 22 | }) 23 | 24 | it('reuses parsed query', async () => { 25 | const handler = prismy([querySelector, querySelector], (query, query2) => { 26 | return res(JSON.stringify(query === query2)) 27 | }) 28 | 29 | await testHandler(handler, async url => { 30 | const response = await got(url) 31 | 32 | expect(response).toMatchObject({ 33 | statusCode: 200, 34 | body: 'true' 35 | }) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/selectors/bufferBody.ts: -------------------------------------------------------------------------------- 1 | import { AsyncSelector } from '../types' 2 | import { readBufferBody } from '../bodyReaders' 3 | 4 | /** 5 | * Options for {@link createBufferBodySelector} 6 | * 7 | * @public 8 | */ 9 | export interface BufferBodySelectorOptions { 10 | limit?: string | number 11 | encoding?: string 12 | } 13 | 14 | /** 15 | * Factory function that creates a selector to extract the request body in a buffer 16 | * 17 | * @example 18 | * Simple example 19 | * ```ts 20 | * 21 | * const bufferBodySelector = createBufferBodySelector({ 22 | * limit: "1mb" 23 | * }) 24 | * 25 | * const prismyHandler = prismy( 26 | * [bufferBodySelector], 27 | * bufferBody => { 28 | * ... 29 | * } 30 | * ) 31 | * 32 | * ``` 33 | * 34 | * @param options - {@link BufferBodySelectorOptions | Options} for the buffer 35 | * @returns A selector for extracting request body in a buffer 36 | * 37 | * @public 38 | */ 39 | export function createBufferBodySelector( 40 | options?: BufferBodySelectorOptions 41 | ): AsyncSelector { 42 | return ({ req }) => { 43 | return readBufferBody(req, options) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /specs/types/middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | middleware, 3 | urlSelector, 4 | methodSelector, 5 | AsyncSelector, 6 | ResponseObject, 7 | prismy, 8 | res 9 | } from '../../src' 10 | import { UrlWithStringQuery } from 'url' 11 | import { expectType } from '../helpers' 12 | 13 | const asyncUrlSelector: AsyncSelector = async context => 14 | urlSelector(context) 15 | 16 | const middleware1 = middleware( 17 | [urlSelector, methodSelector, asyncUrlSelector], 18 | next => async (url, method, url2) => { 19 | expectType(url) 20 | expectType(method) 21 | expectType(url2) 22 | return next() 23 | } 24 | ) 25 | 26 | expectType< 27 | ( 28 | next: () => Promise> 29 | ) => ( 30 | url: UrlWithStringQuery, 31 | method: string | undefined, 32 | url2: UrlWithStringQuery 33 | ) => ResponseObject | Promise> 34 | >(middleware1.mhandler) 35 | 36 | expectType< 37 | ( 38 | next: () => Promise> 39 | ) => ( 40 | url: UrlWithStringQuery, 41 | method: string | undefined, 42 | url2: UrlWithStringQuery 43 | ) => ResponseObject | Promise> 44 | >(middleware1.mhandler) 45 | 46 | prismy([], () => res(''), [middleware1]) 47 | -------------------------------------------------------------------------------- /examples/with-session/src/index.ts: -------------------------------------------------------------------------------- 1 | import { prismy, createUrlEncodedBodySelector, redirect, res } from 'prismy' 2 | import { methodRouter } from 'prismy-method-router' 3 | import createSession from 'prismy-session' 4 | import JWTCookieStrategy from 'prismy-session-strategy-jwt-cookie' 5 | 6 | interface SessionData { 7 | message?: string 8 | } 9 | 10 | const { sessionSelector, sessionMiddleware } = createSession( 11 | new JWTCookieStrategy({ 12 | secret: 'RANDOM_HASH' 13 | }) 14 | ) 15 | 16 | const urlEncodedBodySelector = createUrlEncodedBodySelector() 17 | 18 | export default methodRouter( 19 | { 20 | get: prismy([sessionSelector], session => { 21 | const { data } = session 22 | return res( 23 | [ 24 | '', 25 | '', 26 | `

Message: ${data != null ? data.message : 'NULL'}

`, 27 | '
', 28 | '', 29 | '', 30 | '
', 31 | '' 32 | ].join('') 33 | ) 34 | }), 35 | post: prismy([sessionSelector, urlEncodedBodySelector], (session, body) => { 36 | session.data = 37 | typeof body.message === 'string' ? { message: body.message } : null 38 | return redirect('/') 39 | }) 40 | }, 41 | [sessionMiddleware] 42 | ) 43 | -------------------------------------------------------------------------------- /src/selectors/urlEncodedBody.ts: -------------------------------------------------------------------------------- 1 | import { ParsedUrlQuery, parse } from 'querystring' 2 | import { readTextBody } from '../bodyReaders' 3 | import { createError } from '../error' 4 | import { AsyncSelector } from '../types' 5 | 6 | /** 7 | * Options for {@link createUrlEncodedBodySelector} 8 | * 9 | * @public 10 | */ 11 | export interface UrlEncodedBodySelectorOptions { 12 | limit?: string | number 13 | encoding?: string 14 | } 15 | /** 16 | * Factory function to create a selector to extract the url encoded body from a request 17 | * 18 | * @example 19 | * Simple example 20 | * ```ts 21 | * 22 | * const urlEncodedBodySelector = createUrlEncodedBodySelector({ 23 | * limit: "1mb" 24 | * }) 25 | * 26 | * const prismyHandler = prismy( 27 | * [urlEncodedBodySelector], 28 | * body => { 29 | * ... 30 | * } 31 | * ) 32 | * 33 | * ``` 34 | * 35 | * @param options - {@link UrlEncodedBodySelectorOptions | Options} for the body parser 36 | * @returns selector for url encoded request bodies 37 | * 38 | * @throws 39 | * Throws an Error with 400 code if parsing fails 40 | * 41 | * @public 42 | */ 43 | export function createUrlEncodedBodySelector( 44 | options?: UrlEncodedBodySelectorOptions 45 | ): AsyncSelector { 46 | return async ({ req }) => { 47 | const textBody = await readTextBody(req, options) 48 | try { 49 | return parse(textBody) 50 | } catch (error) { 51 | /* istanbul ignore next */ 52 | throw createError(400, 'Invalid url-encoded body', error) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/selectors/body.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'querystring' 2 | import { readJsonBody, readTextBody } from '../bodyReaders' 3 | import { createError } from '../error' 4 | import { AsyncSelector } from '../types' 5 | 6 | /** 7 | * Options for {@link createBodySelector} 8 | * 9 | * @public 10 | */ 11 | export interface BodySelectorOptions { 12 | limit?: string | number 13 | encoding?: string 14 | } 15 | 16 | /** 17 | * Factory function to create a general selector that detects type of body and then extracts it respectively from a request 18 | * 19 | * @example 20 | * Simple example 21 | * ```ts 22 | * 23 | * const bodySelector = createBodySelector({ 24 | * limit: "1mb" 25 | * }) 26 | * 27 | * const prismyHandler = prismy( 28 | * [bodySelector], 29 | * body => { 30 | * ... 31 | * } 32 | * ) 33 | * 34 | * ``` 35 | * 36 | * @param options - Options such as limit and encoding 37 | * @returns a selector for request bodies 38 | * 39 | * @public 40 | */ 41 | export function createBodySelector( 42 | options?: BodySelectorOptions 43 | ): AsyncSelector { 44 | return async ({ req }) => { 45 | const type = req.headers['content-type'] 46 | 47 | if (type === 'application/json' || type === 'application/ld+json') { 48 | return readJsonBody(req, options) 49 | } else if (type === 'application/x-www-form-urlencoded') { 50 | const textBody = await readTextBody(req, options) 51 | try { 52 | return parse(textBody) 53 | } catch (error) { 54 | /* istanbul ignore next */ 55 | throw createError(400, 'Invalid url-encoded body', error) 56 | } 57 | } else { 58 | return readTextBody(req, options) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResponseObject, 3 | Selector, 4 | SelectorReturnTypeTuple, 5 | PrismyMiddleware, 6 | Context 7 | } from './types' 8 | import { compileHandler } from './utils' 9 | 10 | /** 11 | * Factory function to create a prismy compatible middleware. Accepts selectors to help with 12 | * testing, DI etc. 13 | * 14 | * @example 15 | * Simple Example 16 | * ```ts 17 | * 18 | * const withCors = middleware([], next => async () => { 19 | * const resObject = await next() 20 | * 21 | * return updateHeaders(resObject, { 22 | * 'access-control-allow-origin': '*' 23 | * }) 24 | * }) 25 | * 26 | * ``` 27 | * 28 | * @remarks 29 | * Selectors must be a tuple (`[Selector, Selector]`) not an 30 | * array (`Selector|Selector[] `). Be careful when declaring the 31 | * array outside of the function call. 32 | * 33 | * Be carefuly to remember the mhandler is a function which returns an _async_ function. 34 | * Not returning an async function can lead to strange type error messages. 35 | * 36 | * Another reason for long type error messages is not having `{"strict": true}` setting in 37 | * tsconfig.json or not compiling with --strict. 38 | * 39 | * @param selectors - Tuple of selectors 40 | * @param mhandler - Middleware handler 41 | * @returns A prismy compatible middleware 42 | * 43 | * @public 44 | */ 45 | export function middleware[]>( 46 | selectors: [...SS], 47 | mhandler: ( 48 | next: () => Promise> 49 | ) => (...args: SelectorReturnTypeTuple) => Promise> 50 | ): PrismyMiddleware> { 51 | const middleware = (context: Context) => async ( 52 | next: () => Promise> 53 | ) => compileHandler(selectors, mhandler(next))(context) 54 | middleware.mhandler = mhandler 55 | 56 | return middleware 57 | } 58 | -------------------------------------------------------------------------------- /src/selectors/jsonBody.ts: -------------------------------------------------------------------------------- 1 | import { readJsonBody } from '../bodyReaders' 2 | import { createError } from '../error' 3 | import { AsyncSelector } from '../types' 4 | import { headersSelector } from './headers' 5 | 6 | /** 7 | * Options for {@link createJsonBodySelector} 8 | * 9 | * @public 10 | */ 11 | export interface JsonBodySelectorOptions { 12 | skipContentTypeCheck?: boolean 13 | limit?: string | number 14 | encoding?: string 15 | } 16 | 17 | /** 18 | * Factory function to create a selector to extract the JSON encoded body of a request 19 | * 20 | * @example 21 | * Simple example 22 | * ```ts 23 | * 24 | * const jsonBodySelector = createJsonBodySelector({ 25 | * limit: "1mb" 26 | * }) 27 | * 28 | * const prismyHandler = prismy( 29 | * [jsonBodySelector], 30 | * body => { 31 | * ... 32 | * } 33 | * ) 34 | * 35 | * ``` 36 | * 37 | * @param options - {@link JsonBodySelectorOptions | Options} for the body parsing 38 | * @returns A selector for JSON body requests 39 | * 40 | * @throws 41 | * Throws an Error with 400 if content type is not application/json 42 | * 43 | * @public 44 | */ 45 | export function createJsonBodySelector( 46 | options?: JsonBodySelectorOptions 47 | ): AsyncSelector { 48 | return context => { 49 | const { skipContentTypeCheck = false } = options || {} 50 | if (!skipContentTypeCheck) { 51 | const contentType = headersSelector(context)['content-type'] 52 | if (!isContentTypeIsApplicationJSON(contentType)) { 53 | throw createError( 54 | 400, 55 | `Content type must be application/json. (Current: ${contentType})` 56 | ) 57 | } 58 | } 59 | return readJsonBody(context.req, options) 60 | } 61 | } 62 | 63 | function isContentTypeIsApplicationJSON(contentType: string | undefined) { 64 | if (typeof contentType !== 'string') return false 65 | if (!contentType.startsWith('application/json')) return false 66 | return true 67 | } 68 | -------------------------------------------------------------------------------- /specs/middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from './helpers' 3 | import { 4 | prismy, 5 | res, 6 | Selector, 7 | PrismyPureMiddleware, 8 | middleware, 9 | AsyncSelector, 10 | } from '../src' 11 | 12 | describe('middleware', () => { 13 | it('creates Middleware via selectors and middleware handler', async () => { 14 | const rawUrlSelector: Selector = (context) => context.req.url! 15 | const errorMiddleware: PrismyPureMiddleware = middleware( 16 | [rawUrlSelector], 17 | (next) => async (url) => { 18 | try { 19 | return await next() 20 | } catch (error) { 21 | return res(`${url} : ${(error as any).message}`, 500) 22 | } 23 | } 24 | ) 25 | const handler = prismy( 26 | [], 27 | () => { 28 | throw new Error('Hey!') 29 | }, 30 | [errorMiddleware] 31 | ) 32 | 33 | await testHandler(handler, async (url) => { 34 | const response = await got(url, { 35 | throwHttpErrors: false, 36 | }) 37 | expect(response).toMatchObject({ 38 | statusCode: 500, 39 | body: '/ : Hey!', 40 | }) 41 | }) 42 | }) 43 | 44 | it('accepts async selectors', async () => { 45 | const asyncRawUrlSelector: AsyncSelector = async (context) => 46 | context.req.url! 47 | const errorMiddleware = middleware( 48 | [asyncRawUrlSelector], 49 | (next) => async (url) => { 50 | try { 51 | return await next() 52 | } catch (error) { 53 | return res(`${url} : ${(error as any).message}`, 500) 54 | } 55 | } 56 | ) 57 | const handler = prismy( 58 | [], 59 | () => { 60 | throw new Error('Hey!') 61 | }, 62 | [errorMiddleware] 63 | ) 64 | 65 | await testHandler(handler, async (url) => { 66 | const response = await got(url, { 67 | throwHttpErrors: false, 68 | }) 69 | expect(response).toMatchObject({ 70 | statusCode: 500, 71 | body: '/ : Hey!', 72 | }) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /specs/selectors/body.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { createBodySelector } from '../../src/selectors' 4 | import { prismy, res } from '../../src' 5 | 6 | describe('createBodySelector', () => { 7 | it('returns text body', async () => { 8 | expect.hasAssertions() 9 | const bodySelector = createBodySelector() 10 | const handler = prismy([bodySelector], body => { 11 | return res(`${body.constructor.name}: ${body}`) 12 | }) 13 | 14 | await testHandler(handler, async url => { 15 | const response = await got(url, { 16 | method: 'POST', 17 | body: 'Hello, World!' 18 | }) 19 | 20 | expect(response).toMatchObject({ 21 | statusCode: 200, 22 | body: `String: Hello, World!` 23 | }) 24 | }) 25 | }) 26 | 27 | it('returns parsed url encoded body', async () => { 28 | expect.hasAssertions() 29 | const bodySelector = createBodySelector() 30 | const handler = prismy([bodySelector], body => { 31 | return res(body) 32 | }) 33 | 34 | await testHandler(handler, async url => { 35 | const response = await got(url, { 36 | method: 'POST', 37 | responseType: 'json', 38 | form: { 39 | message: 'Hello, World!' 40 | } 41 | }) 42 | 43 | expect(response).toMatchObject({ 44 | statusCode: 200, 45 | body: { 46 | message: 'Hello, World!' 47 | } 48 | }) 49 | }) 50 | }) 51 | 52 | it('returns JSON object body', async () => { 53 | expect.hasAssertions() 54 | const bodySelector = createBodySelector() 55 | const handler = prismy([bodySelector], body => { 56 | return res(body) 57 | }) 58 | 59 | await testHandler(handler, async url => { 60 | const target = { 61 | foo: 'bar' 62 | } 63 | const response = await got(url, { 64 | method: 'POST', 65 | responseType: 'json', 66 | json: target 67 | }) 68 | 69 | expect(response.statusCode).toBe(200) 70 | expect(response.body).toMatchObject(target) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /src/send.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from 'http' 2 | import { readable } from 'is-stream' 3 | import { Stream } from 'stream' 4 | import { ResponseObject } from './types' 5 | 6 | /** 7 | * Function to send data to the client 8 | * 9 | * @param request {@link IncomingMessage} 10 | * @param response {@link ServerResponse} 11 | * @param ResponseObject 12 | * 13 | * @public 14 | */ 15 | export const send = ( 16 | request: IncomingMessage, 17 | response: ServerResponse, 18 | resObject: 19 | | ResponseObject 20 | | ((request: IncomingMessage, response: ServerResponse) => void) 21 | ) => { 22 | if (typeof resObject === 'function') { 23 | resObject(request, response) 24 | return 25 | } 26 | const { statusCode = 200, body, headers = [] } = resObject 27 | Object.entries(headers).forEach(([key, value]) => { 28 | /* istanbul ignore if */ 29 | if (value == null) { 30 | return 31 | } 32 | response.setHeader(key, value) 33 | }) 34 | response.statusCode = statusCode 35 | 36 | if (body == null) { 37 | response.end() 38 | return 39 | } 40 | 41 | if (Buffer.isBuffer(body)) { 42 | if (!response.getHeader('Content-Type')) { 43 | response.setHeader('Content-Type', 'application/octet-stream') 44 | } 45 | 46 | response.setHeader('Content-Length', body.length) 47 | response.end(body) 48 | return 49 | } 50 | 51 | if (body instanceof Stream || readable(body)) { 52 | if (!response.getHeader('Content-Type')) { 53 | response.setHeader('Content-Type', 'application/octet-stream') 54 | } 55 | 56 | body.pipe(response) 57 | return 58 | } 59 | 60 | const bodyIsNotString = typeof body === 'object' || typeof body === 'number' 61 | if (bodyIsNotString) { 62 | if (!response.getHeader('Content-Type')) { 63 | response.setHeader('Content-Type', 'application/json; charset=utf-8') 64 | } 65 | } 66 | 67 | const stringifiedBody = bodyIsNotString 68 | ? JSON.stringify(body) 69 | : body.toString() 70 | 71 | response.setHeader('Content-Length', Buffer.byteLength(stringifiedBody)) 72 | response.end(stringifiedBody) 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prismy", 3 | "version": "3.0.0", 4 | "description": ":rainbow: Simple and fast type safe server library based on micro for now.sh v2.", 5 | "keywords": [ 6 | "micro", 7 | "service", 8 | "microservice", 9 | "serverless", 10 | "API", 11 | "now" 12 | ], 13 | "author": "Junyoung Choi ", 14 | "homepage": "https://github.com/prismyland/prismy", 15 | "license": "MIT", 16 | "main": "dist/index.js", 17 | "types": "dist/index.d.ts", 18 | "files": [ 19 | "dist/index.d.ts", 20 | "dist/**/*" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/prismyland/prismy.git" 25 | }, 26 | "scripts": { 27 | "build": "rimraf dist && tsc -P tsconfig.build.json", 28 | "lint": "prettier --check src/**/*.ts specs/**/*.ts examples/*/src/**/*.ts", 29 | "format": "prettier --write src/**/*.ts specs/**/*.ts examples/*/src/**/*.ts", 30 | "test": "npm run lint && npm run test-type && npm run test-coverage", 31 | "test-api": "jest", 32 | "test-type": "tsc --noEmit", 33 | "test-coverage": "jest --coverage", 34 | "codecov": "codecov -f coverage/*.json", 35 | "prepublishOnly": "npm test && npm run build", 36 | "generate-docs": "typedoc src && sh scripts/generate-docs.sh" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/prismyland/prismy/issues" 40 | }, 41 | "devDependencies": { 42 | "@types/content-type": "^1.1.3", 43 | "@types/jest": "^24.0.13", 44 | "@types/node": "^12.19.1", 45 | "@types/test-listen": "^1.1.0", 46 | "codecov": "^3.8.0", 47 | "jest": "^26.6.1", 48 | "prettier": "^1.17.1", 49 | "rimraf": "^3.0.0", 50 | "test-listen": "^1.1.0", 51 | "ts-jest": "^26.4.2", 52 | "typedoc": "^0.15.0", 53 | "got": "^11.8.0", 54 | "typescript": "^4.0.3" 55 | }, 56 | "jest": { 57 | "preset": "ts-jest", 58 | "testEnvironment": "node", 59 | "testPathIgnorePatterns": [ 60 | "/node_modules/", 61 | "/dist/", 62 | "/examples/" 63 | ] 64 | }, 65 | "dependencies": { 66 | "content-type": "^1.0.4", 67 | "is-stream": "^2.0.0", 68 | "path-to-regexp": "^6.2.0", 69 | "raw-body": "^2.4.1", 70 | "tslib": "^2.0.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/prismy.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from 'http' 2 | import { createErrorResObject } from './error' 3 | import { send } from './send' 4 | import { 5 | ResponseObject, 6 | Selector, 7 | PrismyPureMiddleware, 8 | Promisable, 9 | Context, 10 | ContextHandler, 11 | PrismyHandler, 12 | SelectorReturnTypeTuple, 13 | } from './types' 14 | import { compileHandler } from './utils' 15 | 16 | /** 17 | * Generates a handler to be used by http.Server 18 | * 19 | * @example 20 | * ```ts 21 | * const worldSelector: Selector = () => "world"! 22 | * 23 | * export default prismy([ worldSelector ], async world => { 24 | * return res(`Hello ${world}!`) // Hello world! 25 | * }) 26 | * ``` 27 | * 28 | * @remarks 29 | * Selectors must be a tuple (`[Selector, Selector]`) not an 30 | * array (`Selector|Selector[] `). Be careful when declaring the 31 | * array outside of the function call. 32 | * 33 | * @param selectors - Tuple of Selectors to generate arguments for handler 34 | * @param handler - Business logic handling the request 35 | * @param middlewareList - Middleware to pass request and response through 36 | * 37 | * @public 38 | * 39 | */ 40 | export function prismy[]>( 41 | selectors: [...S], 42 | handler: ( 43 | ...args: SelectorReturnTypeTuple 44 | ) => Promisable>, 45 | middlewareList: PrismyPureMiddleware[] = [] 46 | ): PrismyHandler> { 47 | const contextHandler: ContextHandler = async (context: Context) => { 48 | const next = async () => compileHandler(selectors, handler)(context) 49 | 50 | const pipe = middlewareList.reduce((next, middleware) => { 51 | return () => middleware(context)(next) 52 | }, next) 53 | 54 | let resObject 55 | try { 56 | resObject = await pipe() 57 | } catch (error) { 58 | /* istanbul ignore next */ 59 | if (process.env.NODE_ENV !== 'test') { 60 | console.error(error) 61 | } 62 | resObject = createErrorResObject(error) 63 | } 64 | 65 | return resObject 66 | } 67 | 68 | async function requestListener( 69 | request: IncomingMessage, 70 | response: ServerResponse 71 | ) { 72 | const context = { 73 | req: request, 74 | } 75 | 76 | const resObject = await contextHandler(context) 77 | 78 | await send(request, response, resObject) 79 | } 80 | 81 | requestListener.handler = handler 82 | requestListener.contextHandler = contextHandler 83 | 84 | return requestListener 85 | } 86 | -------------------------------------------------------------------------------- /src/bodyReaders.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseContentType } from 'content-type' 2 | import getRawBody from 'raw-body' 3 | import { IncomingMessage } from 'http' 4 | import { BufferBodyOptions } from './types' 5 | import { createError } from './error' 6 | 7 | const rawBodyMap = new WeakMap() 8 | 9 | /** 10 | * An async function to buffer the incoming request body 11 | * 12 | * @remarks 13 | * Can be called multiple times, as it caches the raw request body the first time 14 | * 15 | * @param req {@link IncomingMessage} 16 | * @param options Options such as limit and encoding 17 | * @returns Buffer body 18 | * 19 | * @public 20 | */ 21 | export const readBufferBody = async ( 22 | req: IncomingMessage, 23 | options?: BufferBodyOptions 24 | ): Promise => { 25 | const { limit, encoding } = resolveBufferBodyOptions(req, options) 26 | const length = req.headers['content-length'] 27 | 28 | if (!req.readable) { 29 | const body = rawBodyMap.get(req) 30 | if (body) { 31 | return body 32 | } 33 | throw createError(500, `The request has already been drained`) 34 | } 35 | 36 | try { 37 | const buffer = await getRawBody(req, { limit, length, encoding }) 38 | rawBodyMap.set(req, buffer) 39 | return buffer 40 | } catch (error) { 41 | if ((error as any).type === 'entity.too.large') { 42 | throw createError(413, `Body exceeded ${limit} limit`, error) 43 | } else { 44 | throw createError(400, `Invalid body`, error) 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * An async function to parse the incoming request body into text 51 | * 52 | * @param req {@link IncomingMessage} 53 | * @param options Options such as limit and encoding 54 | * @returns Text body 55 | * 56 | * @public 57 | */ 58 | export const readTextBody = async ( 59 | req: IncomingMessage, 60 | options?: BufferBodyOptions 61 | ): Promise => { 62 | const { encoding } = resolveBufferBodyOptions(req, options) 63 | const body = await readBufferBody(req, options) 64 | return body.toString(encoding) 65 | } 66 | 67 | /** 68 | * An async function to parse the incoming request body into JSON object 69 | * 70 | * @param req {@link IncomingMessage} 71 | * @param options Options such as limit and encoding 72 | * @returns JSON object 73 | * 74 | * @public 75 | */ 76 | export const readJsonBody = async ( 77 | req: IncomingMessage, 78 | options?: BufferBodyOptions 79 | ): Promise => { 80 | const body = await readTextBody(req, options) 81 | try { 82 | return JSON.parse(body) 83 | } catch (error) { 84 | throw createError(400, `Invalid JSON`, error) 85 | } 86 | } 87 | 88 | function resolveBufferBodyOptions( 89 | req: IncomingMessage, 90 | options?: BufferBodyOptions 91 | ): BufferBodyOptions { 92 | const type = req.headers['content-type'] || 'text/plain' 93 | let { limit = '1mb', encoding } = options || {} 94 | 95 | if (encoding === undefined) { 96 | encoding = parseContentType(type).parameters.charset 97 | } 98 | 99 | return { 100 | limit, 101 | encoding, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from 'http' 2 | 3 | /** 4 | * Request context used in selectors 5 | * 6 | * @public 7 | */ 8 | export interface Context { 9 | req: IncomingMessage 10 | } 11 | 12 | /** 13 | * A Synchronous argument selector 14 | * 15 | * @public 16 | */ 17 | export type SyncSelector = (context: Context) => T 18 | /** 19 | * An asynchronous argument selector 20 | * 21 | * @public 22 | */ 23 | export type AsyncSelector = (context: Context) => Promise 24 | /** 25 | * An argument selector to extract arguments for the handler 26 | * 27 | * @public 28 | */ 29 | export type Selector = SyncSelector | AsyncSelector 30 | 31 | /** 32 | * Get the return type array of Selectors 33 | * 34 | * @public 35 | */ 36 | export type SelectorTuple = [ 37 | ...{ 38 | [I in keyof SS]: Selector 39 | } 40 | ] 41 | 42 | /** 43 | * Get the return type of a Selector 44 | * 45 | * @public 46 | */ 47 | export type SelectorReturnType = S extends Selector ? T : never 48 | 49 | /** 50 | * Get the return type array of Selectors 51 | * 52 | * @public 53 | */ 54 | export type SelectorReturnTypeTuple[]> = [ 55 | ...{ 56 | [I in keyof SS]: SelectorReturnType 57 | } 58 | ] 59 | 60 | /** 61 | * @public 62 | */ 63 | export type PromiseResolve = T extends Promise ? U : T 64 | 65 | /** 66 | * @public 67 | */ 68 | export type Promisable = T | Promise 69 | 70 | /** 71 | * prismy's representation of a response 72 | * 73 | * @public 74 | */ 75 | export interface ResponseObject { 76 | body?: B 77 | statusCode?: number 78 | headers?: OutgoingHttpHeaders 79 | } 80 | 81 | /** 82 | * shorter type alias for ResponseObject 83 | * 84 | * @public 85 | */ 86 | export type Res = ResponseObject 87 | 88 | /** 89 | * alias for Promise> for user with async handlers 90 | * 91 | * @public 92 | */ 93 | export type AsyncRes = Promise> 94 | 95 | /** 96 | * prismy compaticble middleware 97 | * 98 | * @public 99 | */ 100 | export interface PrismyPureMiddleware { 101 | (context: Context): ( 102 | next: () => Promise> 103 | ) => Promise> 104 | } 105 | /** 106 | * prismy compatible middleware 107 | * 108 | * @public 109 | */ 110 | export interface PrismyMiddleware 111 | extends PrismyPureMiddleware { 112 | mhandler( 113 | next: () => Promise> 114 | ): (...args: A) => Promise> 115 | } 116 | 117 | /** 118 | * @public 119 | */ 120 | export type ContextHandler = (context: Context) => Promise> 121 | 122 | /** 123 | * @public 124 | */ 125 | export interface PrismyHandler { 126 | (req: IncomingMessage, res: ServerResponse): void 127 | handler(...args: A): Promisable> 128 | contextHandler: ContextHandler 129 | } 130 | 131 | /** 132 | * @public 133 | */ 134 | export interface BufferBodyOptions { 135 | limit?: string | number 136 | encoding?: string 137 | } 138 | -------------------------------------------------------------------------------- /specs/selectors/jsonBody.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from '../helpers' 3 | import { createJsonBodySelector, prismy, res } from '../../src' 4 | 5 | describe('createJsonBodySelector', () => { 6 | it('creates json body selector', async () => { 7 | const jsonBodySelector = createJsonBodySelector() 8 | const handler = prismy([jsonBodySelector], body => { 9 | return res(body) 10 | }) 11 | 12 | await testHandler(handler, async url => { 13 | const response = await got(url, { 14 | method: 'POST', 15 | responseType: 'json', 16 | json: { 17 | message: 'Hello, World!' 18 | } 19 | }) 20 | 21 | expect(response).toMatchObject({ 22 | statusCode: 200, 23 | body: { 24 | message: 'Hello, World!' 25 | } 26 | }) 27 | }) 28 | }) 29 | 30 | it('throws if content type of a request is not application/json #1 (Anti CSRF)', async () => { 31 | const jsonBodySelector = createJsonBodySelector() 32 | const handler = prismy([jsonBodySelector], body => { 33 | return res(body) 34 | }) 35 | 36 | await testHandler(handler, async url => { 37 | const response = await got(url, { 38 | method: 'POST', 39 | body: JSON.stringify({ 40 | message: 'Hello, World!' 41 | }), 42 | throwHttpErrors: false 43 | }) 44 | 45 | expect(response).toMatchObject({ 46 | statusCode: 400, 47 | body: expect.stringContaining( 48 | 'Error: Content type must be application/json. (Current: undefined)' 49 | ) 50 | }) 51 | }) 52 | }) 53 | 54 | it('throws if content type of a request is not application/json #2 (Anti CSRF)', async () => { 55 | const jsonBodySelector = createJsonBodySelector() 56 | const handler = prismy([jsonBodySelector], body => { 57 | return res(body) 58 | }) 59 | 60 | await testHandler(handler, async url => { 61 | const response = await got(url, { 62 | method: 'POST', 63 | json: { 64 | message: 'Hello, World!' 65 | }, 66 | headers: { 67 | 'content-type': 'text/plain' 68 | }, 69 | throwHttpErrors: false 70 | }) 71 | 72 | expect(response).toMatchObject({ 73 | statusCode: 400, 74 | body: expect.stringContaining( 75 | 'Error: Content type must be application/json. (Current: text/plain)' 76 | ) 77 | }) 78 | }) 79 | }) 80 | 81 | it('skips content-type checking if the option is given', async () => { 82 | const jsonBodySelector = createJsonBodySelector({ 83 | skipContentTypeCheck: true 84 | }) 85 | const handler = prismy([jsonBodySelector], body => { 86 | return res(body) 87 | }) 88 | 89 | await testHandler(handler, async url => { 90 | const response = await got(url, { 91 | method: 'POST', 92 | json: { 93 | message: 'Hello, World!' 94 | }, 95 | headers: { 96 | 'content-type': 'text/plain' 97 | } 98 | }) 99 | 100 | expect(response).toMatchObject({ 101 | statusCode: 200, 102 | body: JSON.stringify({ 103 | message: 'Hello, World!' 104 | }) 105 | }) 106 | }) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import { Context, Selector, SyncSelector, PrismyHandler } from './types' 2 | import { contextSelector, methodSelector, urlSelector } from './selectors' 3 | import { match as createMatchFunction } from 'path-to-regexp' 4 | import { prismy } from './prismy' 5 | import { createError } from './error' 6 | 7 | export type RouteMethod = 8 | | 'get' 9 | | 'put' 10 | | 'patch' 11 | | 'post' 12 | | 'delete' 13 | | 'options' 14 | | '*' 15 | export type RouteIndicator = [string, RouteMethod] 16 | export type RouteParams = [ 17 | string | RouteIndicator, 18 | PrismyHandler 19 | ] 20 | 21 | type Route = { 22 | indicator: RouteIndicator 23 | listener: PrismyHandler 24 | } 25 | 26 | export function router( 27 | routes: RouteParams[], 28 | options: PrismyRouterOptions = {} 29 | ) { 30 | const { notFoundHandler } = options 31 | const compiledRoutes = routes.map((routeParams) => { 32 | const { indicator, listener } = createRoute(routeParams) 33 | const [targetPath, method] = indicator 34 | const compiledTargetPath = removeTralingSlash(targetPath) 35 | const match = createMatchFunction(compiledTargetPath, { strict: false }) 36 | return { 37 | method, 38 | match, 39 | listener, 40 | targetPath: compiledTargetPath, 41 | } 42 | }) 43 | return prismy( 44 | [methodSelector, urlSelector, contextSelector], 45 | (method, url, context) => { 46 | /* istanbul ignore next */ 47 | const normalizedMethod = method?.toLowerCase() 48 | /* istanbul ignore next */ 49 | const normalizedPath = removeTralingSlash(url.pathname || '/') 50 | 51 | for (const route of compiledRoutes) { 52 | const { method: targetMethod, match } = route 53 | if (targetMethod !== '*' && targetMethod !== normalizedMethod) { 54 | continue 55 | } 56 | 57 | const result = match(normalizedPath) 58 | if (!result) { 59 | continue 60 | } 61 | 62 | setRouteParamsToPrismyContext(context, result.params) 63 | 64 | return route.listener.contextHandler(context) 65 | } 66 | 67 | if (notFoundHandler == null) { 68 | throw createError(404, 'Not Found') 69 | } else { 70 | return notFoundHandler.contextHandler(context) 71 | } 72 | } 73 | ) 74 | } 75 | 76 | function createRoute( 77 | routeParams: RouteParams[]> 78 | ): Route[]> { 79 | const [indicator, listener] = routeParams 80 | if (typeof indicator === 'string') { 81 | return { 82 | indicator: [indicator, 'get'], 83 | listener, 84 | } 85 | } 86 | return { 87 | indicator, 88 | listener, 89 | } 90 | } 91 | const routeParamsSymbol = Symbol('route params') 92 | 93 | function setRouteParamsToPrismyContext(context: Context, params: object) { 94 | ;(context as any)[routeParamsSymbol] = params 95 | } 96 | 97 | function getRouteParamsFromPrismyContext(context: Context) { 98 | return (context as any)[routeParamsSymbol] 99 | } 100 | 101 | export function createRouteParamSelector( 102 | paramName: string 103 | ): SyncSelector { 104 | return (context) => { 105 | const param = getRouteParamsFromPrismyContext(context)[paramName] 106 | return param != null ? param : null 107 | } 108 | } 109 | 110 | interface PrismyRouterOptions { 111 | notFoundHandler?: PrismyHandler 112 | } 113 | 114 | function removeTralingSlash(value: string) { 115 | if (value === '/') { 116 | return value 117 | } 118 | return value.replace(/\/$/, '') 119 | } 120 | -------------------------------------------------------------------------------- /specs/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from './helpers' 3 | import { 4 | prismy, 5 | res, 6 | redirect, 7 | setBody, 8 | setStatusCode, 9 | updateHeaders, 10 | setHeaders 11 | } from '../src' 12 | 13 | describe('redirect', () => { 14 | it('redirects', async () => { 15 | const handler = prismy([], () => { 16 | return redirect('https://github.com') 17 | }) 18 | 19 | await testHandler(handler, async url => { 20 | const response = await got(url, { 21 | followRedirect: false 22 | }) 23 | expect(response).toMatchObject({ 24 | statusCode: 302, 25 | headers: { 26 | location: 'https://github.com' 27 | } 28 | }) 29 | }) 30 | }) 31 | 32 | it('redirects with specific statusCode', async () => { 33 | const handler = prismy([], () => { 34 | return redirect('https://github.com', 301) 35 | }) 36 | 37 | await testHandler(handler, async url => { 38 | const response = await got(url, { 39 | followRedirect: false 40 | }) 41 | expect(response).toMatchObject({ 42 | statusCode: 301, 43 | headers: { 44 | location: 'https://github.com' 45 | } 46 | }) 47 | }) 48 | }) 49 | 50 | it('redirects with specific headers', async () => { 51 | const handler = prismy([], () => { 52 | return redirect('https://github.com', undefined, { 53 | 'x-test': 'Hello, World!' 54 | }) 55 | }) 56 | 57 | await testHandler(handler, async url => { 58 | const response = await got(url, { 59 | followRedirect: false 60 | }) 61 | expect(response).toMatchObject({ 62 | statusCode: 302, 63 | headers: { 64 | location: 'https://github.com', 65 | 'x-test': 'Hello, World!' 66 | } 67 | }) 68 | }) 69 | }) 70 | }) 71 | 72 | describe('setBody', () => { 73 | it('replaces body', () => { 74 | const resObj = res('Hello, World!') 75 | 76 | const newResObj = setBody(resObj, 'Good Bye!') 77 | 78 | expect(newResObj).toEqual({ 79 | body: 'Good Bye!', 80 | statusCode: 200, 81 | headers: {} 82 | }) 83 | }) 84 | }) 85 | 86 | describe('setStatusCode', () => { 87 | it('replaces statusCode', () => { 88 | const resObj = res('Hello, World!') 89 | 90 | const newResObj = setStatusCode(resObj, 201) 91 | 92 | expect(newResObj).toEqual({ 93 | body: 'Hello, World!', 94 | statusCode: 201, 95 | headers: {} 96 | }) 97 | }) 98 | }) 99 | 100 | describe('updateHeaders', () => { 101 | it('update headers', () => { 102 | const resObj = res(null, 200, { 103 | 'x-test-message': 'Hello, World!' 104 | }) 105 | 106 | const newResObj = updateHeaders(resObj, { 107 | 'x-test-message': 'Good Bye!', 108 | 'x-extra-message': 'Adios!' 109 | }) 110 | 111 | expect(newResObj).toEqual({ 112 | body: null, 113 | statusCode: 200, 114 | headers: { 115 | 'x-test-message': 'Good Bye!', 116 | 'x-extra-message': 'Adios!' 117 | } 118 | }) 119 | }) 120 | }) 121 | 122 | describe('setHeaders', () => { 123 | it('replace headers', () => { 124 | const resObj = res(null, 200, { 125 | 'x-test-message': 'Hello, World!' 126 | }) 127 | 128 | const newResObj = setHeaders(resObj, { 129 | 'x-extra-message': 'Hola!' 130 | }) 131 | 132 | expect(newResObj).toEqual({ 133 | body: null, 134 | statusCode: 200, 135 | headers: { 136 | 'x-extra-message': 'Hola!' 137 | } 138 | }) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /resources/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /specs/prismy.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from './helpers' 3 | import { prismy, res, Selector, PrismyPureMiddleware, err } from '../src' 4 | 5 | describe('prismy', () => { 6 | it('returns node.js request handler', async () => { 7 | const handler = prismy([], () => res('Hello, World!')) 8 | 9 | await testHandler(handler, async url => { 10 | const response = await got(url) 11 | expect(response).toMatchObject({ 12 | statusCode: 200, 13 | body: 'Hello, World!' 14 | }) 15 | }) 16 | }) 17 | 18 | it('selects value from context via selector', async () => { 19 | const rawUrlSelector: Selector = context => context.req.url! 20 | const handler = prismy([rawUrlSelector], url => res(url)) 21 | 22 | await testHandler(handler, async url => { 23 | const response = await got(url) 24 | expect(response).toMatchObject({ 25 | statusCode: 200, 26 | body: '/' 27 | }) 28 | }) 29 | }) 30 | 31 | it('selects value from context via selector', async () => { 32 | const asyncRawUrlSelector: Selector = async context => 33 | context.req.url! 34 | const handler = prismy([asyncRawUrlSelector], url => res(url)) 35 | 36 | await testHandler(handler, async url => { 37 | const response = await got(url) 38 | expect(response).toMatchObject({ 39 | statusCode: 200, 40 | body: '/' 41 | }) 42 | }) 43 | }) 44 | 45 | it('expose raw prismy handler for unit tests', () => { 46 | const rawUrlSelector: Selector = context => context.req.url! 47 | const handler = prismy([rawUrlSelector], url => res(url)) 48 | 49 | const result = handler.handler('Hello, World!') 50 | 51 | expect(result).toEqual({ 52 | body: 'Hello, World!', 53 | headers: {}, 54 | statusCode: 200 55 | }) 56 | }) 57 | 58 | it('applys middleware', async () => { 59 | const errorMiddleware: PrismyPureMiddleware = context => async next => { 60 | try { 61 | return await next() 62 | } catch (error) { 63 | return err(500, (error as any).message) 64 | } 65 | } 66 | const rawUrlSelector: Selector = context => context.req.url! 67 | const handler = prismy( 68 | [rawUrlSelector], 69 | url => { 70 | throw new Error('Hey!') 71 | }, 72 | [errorMiddleware] 73 | ) 74 | 75 | await testHandler(handler, async url => { 76 | const response = await got(url, { 77 | throwHttpErrors: false 78 | }) 79 | expect(response).toMatchObject({ 80 | statusCode: 500, 81 | body: 'Hey!' 82 | }) 83 | }) 84 | }) 85 | 86 | it('applys middleware orderly', async () => { 87 | const problematicMiddleware: PrismyPureMiddleware = context => async next => { 88 | throw new Error('Hey!') 89 | } 90 | const errorMiddleware: PrismyPureMiddleware = context => async next => { 91 | try { 92 | return await next() 93 | } catch (error) { 94 | return res((error as any).message, 500) 95 | } 96 | } 97 | const rawUrlSelector: Selector = context => context.req.url! 98 | const handler = prismy( 99 | [rawUrlSelector], 100 | url => { 101 | return res(url) 102 | }, 103 | [problematicMiddleware, errorMiddleware] 104 | ) 105 | 106 | await testHandler(handler, async url => { 107 | const response = await got(url, { 108 | throwHttpErrors: false 109 | }) 110 | expect(response).toMatchObject({ 111 | statusCode: 500, 112 | body: 'Hey!' 113 | }) 114 | }) 115 | }) 116 | 117 | it('handles unhandled errors from handlers', async () => { 118 | const handler = prismy( 119 | [], 120 | () => { 121 | throw new Error('Hey!') 122 | }, 123 | [] 124 | ) 125 | await testHandler(handler, async url => { 126 | const response = await got(url, { 127 | throwHttpErrors: false 128 | }) 129 | expect(response).toMatchObject({ 130 | statusCode: 500, 131 | body: expect.stringContaining('Error: Hey!') 132 | }) 133 | }) 134 | }) 135 | 136 | it('handles unhandled errors from selectors', async () => { 137 | const rawUrlSelector: Selector = context => { 138 | throw new Error('Hey!') 139 | } 140 | const handler = prismy( 141 | [rawUrlSelector], 142 | url => { 143 | return res(url) 144 | }, 145 | [] 146 | ) 147 | await testHandler(handler, async url => { 148 | const response = await got(url, { 149 | throwHttpErrors: false 150 | }) 151 | expect(response).toMatchObject({ 152 | statusCode: 500, 153 | body: expect.stringContaining('Error: Hey!') 154 | }) 155 | }) 156 | }) 157 | }) 158 | -------------------------------------------------------------------------------- /specs/router.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { testHandler } from './helpers' 3 | import { createRouteParamSelector, prismy, res, router } from '../src' 4 | import { join } from 'path' 5 | 6 | describe('router', () => { 7 | it('routes with pathname', async () => { 8 | expect.hasAssertions() 9 | const handlerA = prismy([], () => { 10 | return res('a') 11 | }) 12 | const handlerB = prismy([], () => { 13 | return res('b') 14 | }) 15 | 16 | const routerHandler = router([ 17 | ['/a', handlerA], 18 | ['/b', handlerB], 19 | ]) 20 | 21 | await testHandler(routerHandler, async (url) => { 22 | const response = await got(join(url, 'b'), { 23 | method: 'GET', 24 | }) 25 | 26 | expect(response).toMatchObject({ 27 | statusCode: 200, 28 | body: 'b', 29 | }) 30 | }) 31 | }) 32 | 33 | it('routes with method', async () => { 34 | expect.assertions(2) 35 | const handlerA = prismy([], () => { 36 | return res('a') 37 | }) 38 | const handlerB = prismy([], () => { 39 | return res('b') 40 | }) 41 | 42 | const routerHandler = router([ 43 | [['/', 'get'], handlerA], 44 | [['/', 'post'], handlerB], 45 | ]) 46 | 47 | await testHandler(routerHandler, async (url) => { 48 | const response = await got(url, { 49 | method: 'GET', 50 | }) 51 | 52 | expect(response).toMatchObject({ 53 | statusCode: 200, 54 | body: 'a', 55 | }) 56 | }) 57 | 58 | await testHandler(routerHandler, async (url) => { 59 | const response = await got(url, { 60 | method: 'POST', 61 | }) 62 | 63 | expect(response).toMatchObject({ 64 | statusCode: 200, 65 | body: 'b', 66 | }) 67 | }) 68 | }) 69 | 70 | it('resolve params', async () => { 71 | expect.hasAssertions() 72 | const handlerA = prismy([], () => { 73 | return res('a') 74 | }) 75 | const handlerB = prismy([createRouteParamSelector('id')], (id) => { 76 | return res(id) 77 | }) 78 | 79 | const routerHandler = router([ 80 | ['/a', handlerA], 81 | ['/b/:id', handlerB], 82 | ]) 83 | 84 | await testHandler(routerHandler, async (url) => { 85 | const response = await got(join(url, 'b/test-param'), { 86 | method: 'GET', 87 | }) 88 | 89 | expect(response).toMatchObject({ 90 | statusCode: 200, 91 | body: 'test-param', 92 | }) 93 | }) 94 | }) 95 | 96 | it('resolves null if param is missing', async () => { 97 | expect.hasAssertions() 98 | const handlerA = prismy([], () => { 99 | return res('a') 100 | }) 101 | const handlerB = prismy([createRouteParamSelector('not-id')], (notId) => { 102 | return res(notId) 103 | }) 104 | 105 | const routerHandler = router([ 106 | ['/a', handlerA], 107 | ['/b/:id', handlerB], 108 | ]) 109 | 110 | await testHandler(routerHandler, async (url) => { 111 | const response = await got(join(url, 'b/test-param'), { 112 | method: 'GET', 113 | }) 114 | 115 | expect(response).toMatchObject({ 116 | statusCode: 200, 117 | body: '', 118 | }) 119 | }) 120 | }) 121 | 122 | it('throws 404 error when no route found', async () => { 123 | expect.assertions(1) 124 | const handlerA = prismy([], () => { 125 | return res('a') 126 | }) 127 | const handlerB = prismy([], () => { 128 | return res('b') 129 | }) 130 | 131 | const routerHandler = router([ 132 | [['/', 'get'], handlerA], 133 | [['/', 'post'], handlerB], 134 | ]) 135 | 136 | await testHandler(routerHandler, async (url) => { 137 | const response = await got(url, { 138 | method: 'PUT', 139 | throwHttpErrors: false, 140 | }) 141 | 142 | expect(response).toMatchObject({ 143 | statusCode: 404, 144 | body: expect.stringContaining('Error: Not Found'), 145 | }) 146 | }) 147 | }) 148 | 149 | it('uses custom 404 error handler', async () => { 150 | expect.assertions(1) 151 | const handlerA = prismy([], () => { 152 | return res('a') 153 | }) 154 | const handlerB = prismy([], () => { 155 | return res('b') 156 | }) 157 | 158 | const routerHandler = router( 159 | [ 160 | [['/', 'get'], handlerA], 161 | [['/', 'post'], handlerB], 162 | ], 163 | { 164 | notFoundHandler: prismy([], () => { 165 | return res('Not Found(Customized)', 404) 166 | }), 167 | } 168 | ) 169 | 170 | await testHandler(routerHandler, async (url) => { 171 | const response = await got(url, { 172 | method: 'PUT', 173 | throwHttpErrors: false, 174 | }) 175 | 176 | expect(response).toMatchObject({ 177 | statusCode: 404, 178 | body: 'Not Found(Customized)', 179 | }) 180 | }) 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { OutgoingHttpHeaders } from 'http' 2 | import { 3 | ResponseObject, 4 | Selector, 5 | SelectorReturnTypeTuple, 6 | Context 7 | } from './types' 8 | 9 | /** 10 | * Factory function for creating http responses 11 | * 12 | * @param body - Body of the response 13 | * @param statusCode - HTTP status code of the response 14 | * @param headers - HTTP headers for the response 15 | * @returns A {@link ResponseObject | response object} containing necessary information 16 | * 17 | * @public 18 | */ 19 | export function res( 20 | body: B, 21 | statusCode: number = 200, 22 | headers: OutgoingHttpHeaders = {} 23 | ): ResponseObject { 24 | return { 25 | body, 26 | statusCode, 27 | headers 28 | } 29 | } 30 | 31 | export function err( 32 | statusCode: number, 33 | body: B, 34 | headers?: OutgoingHttpHeaders 35 | ): ResponseObject { 36 | return res(body, statusCode, headers) 37 | } 38 | 39 | /** 40 | * Factory function for easily generating a redirect response 41 | * 42 | * @param location - URL to redirect to 43 | * @param statusCode - Status code for response. Defaults to 302 44 | * @param extraHeaders - Additional headers of the response 45 | * @returns A redirect {@link ResponseObject | response} to location 46 | * 47 | * @public 48 | */ 49 | export function redirect( 50 | location: string, 51 | statusCode: number = 302, 52 | extraHeaders: OutgoingHttpHeaders = {} 53 | ): ResponseObject { 54 | return res(null, statusCode, { 55 | location, 56 | ...extraHeaders 57 | }) 58 | } 59 | 60 | /** 61 | * Creates a new response with a new body 62 | * 63 | * @param resObject - The response to set the body on 64 | * @param body - Body to be set 65 | * @returns New {@link ResponseObject | response} with the new body 66 | * 67 | * @public 68 | */ 69 | export function setBody( 70 | resObject: ResponseObject, 71 | body: B2 72 | ): ResponseObject { 73 | return { 74 | ...resObject, 75 | body 76 | } 77 | } 78 | 79 | /** 80 | * Creates a new response with a new status code 81 | * 82 | * @param resObject - The response to set the code to 83 | * @param statusCode - HTTP status code 84 | * @returns New {@link ResponseObject | response} with the new statusCode 85 | * 86 | * @public 87 | */ 88 | export function setStatusCode( 89 | resObject: ResponseObject, 90 | statusCode: number 91 | ): ResponseObject { 92 | return { 93 | ...resObject, 94 | statusCode 95 | } 96 | } 97 | 98 | /** 99 | * Creates a new response with the extra headers. 100 | * 101 | * @param resObject - The response to add the new headers to 102 | * @param extraHeaders - HTTP response headers 103 | * @returns New {@link ResponseObject | response} with the extra headers 104 | * 105 | * @public 106 | */ 107 | export function updateHeaders( 108 | resObject: ResponseObject, 109 | extraHeaders: OutgoingHttpHeaders 110 | ): ResponseObject { 111 | return { 112 | ...resObject, 113 | headers: { 114 | ...resObject.headers, 115 | ...extraHeaders 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Creates a new response overriting all headers with new ones. 122 | * 123 | * @param resObject - response to set new headers on 124 | * @param headers - HTTP response headers to set 125 | * @returns New {@link ResponseObject | response} with new headers set 126 | * 127 | * @public 128 | */ 129 | export function setHeaders( 130 | resObject: ResponseObject, 131 | headers: OutgoingHttpHeaders 132 | ): ResponseObject { 133 | return { 134 | ...resObject, 135 | headers 136 | } 137 | } 138 | 139 | /** 140 | * Compile a handler into a runnable function by resolving selectors 141 | * and injecting the arguments into the handler. 142 | * 143 | * @param selectors - Selectors to gather handler arguments from 144 | * @param handler - Handler to be compiled 145 | * @returns compiled handler ready to be used 146 | * 147 | * @internal 148 | */ 149 | export function compileHandler[], R>( 150 | selectors: [...S], 151 | handler: (...args: SelectorReturnTypeTuple) => R 152 | ): (context: Context) => Promise { 153 | return async (context: Context) => { 154 | return handler(...(await resolveSelectors(context, selectors))) 155 | } 156 | } 157 | 158 | /** 159 | * Executes the selectors and produces an array of args to be passed to 160 | * a handler 161 | * 162 | * @param context - Context object to be passed to the selectors 163 | * @param selectors - array of selectos 164 | * @returns arguments for a handler 165 | * 166 | * @internal 167 | */ 168 | export async function resolveSelectors[]>( 169 | context: Context, 170 | selectors: [...S] 171 | ): Promise> { 172 | const resolvedValues = [] 173 | for (const selector of selectors) { 174 | const resolvedValue = await selector(context) 175 | resolvedValues.push(resolvedValue) 176 | } 177 | 178 | return resolvedValues as SelectorReturnTypeTuple 179 | } 180 | -------------------------------------------------------------------------------- /specs/bodyReaders.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import getRawBody from 'raw-body' 3 | import { Context, middleware, prismy, res } from '../src' 4 | import { readBufferBody, readJsonBody, readTextBody } from '../src/bodyReaders' 5 | import { testHandler } from './helpers' 6 | 7 | describe('readBufferBody', () => { 8 | it('reads buffer body from a request', async () => { 9 | expect.hasAssertions() 10 | 11 | const bufferBodySelector = async ({ req }: Context) => { 12 | const body = await readBufferBody(req) 13 | return body 14 | } 15 | const handler = prismy([bufferBodySelector], (body) => { 16 | return res(body) 17 | }) 18 | 19 | await testHandler(handler, async (url) => { 20 | const targetBuffer = Buffer.from('Hello, world!') 21 | const responsePromise = got(url, { 22 | method: 'POST', 23 | body: targetBuffer, 24 | }) 25 | const bufferPromise = responsePromise.buffer() 26 | const [response, buffer] = await Promise.all([ 27 | responsePromise, 28 | bufferPromise, 29 | ]) 30 | 31 | expect(buffer.equals(targetBuffer)).toBe(true) 32 | expect(response.headers['content-length']).toBe( 33 | targetBuffer.length.toString() 34 | ) 35 | }) 36 | }) 37 | 38 | it('reads buffer body regardless delaying', async () => { 39 | expect.hasAssertions() 40 | 41 | const bufferBodySelector = async ({ req }: Context) => { 42 | const body = await readBufferBody(req) 43 | return body 44 | } 45 | const handler = prismy( 46 | [ 47 | () => { 48 | return new Promise((resolve) => { 49 | setImmediate(resolve) 50 | }) 51 | }, 52 | bufferBodySelector, 53 | ], 54 | (_, body) => { 55 | return res(body) 56 | }, 57 | [ 58 | middleware([], (next) => async () => { 59 | try { 60 | return await next() 61 | } catch (error) { 62 | console.error(error) 63 | throw error 64 | } 65 | }), 66 | ] 67 | ) 68 | 69 | await testHandler(handler, async (url) => { 70 | const targetBuffer = Buffer.from('Hello, world!') 71 | const responsePromise = got(url, { 72 | method: 'POST', 73 | body: targetBuffer, 74 | }) 75 | const bufferPromise = responsePromise.buffer() 76 | const [response, buffer] = await Promise.all([ 77 | responsePromise, 78 | bufferPromise, 79 | ]) 80 | 81 | expect(buffer.equals(targetBuffer)).toBe(true) 82 | expect(response.headers['content-length']).toBe( 83 | targetBuffer.length.toString() 84 | ) 85 | }) 86 | }) 87 | 88 | it('returns cached buffer if it is read already', async () => { 89 | expect.hasAssertions() 90 | 91 | const bufferBodySelector = async ({ req }: Context) => { 92 | await readBufferBody(req) 93 | const body = await readBufferBody(req) 94 | return body 95 | } 96 | const handler = prismy([bufferBodySelector], (body) => { 97 | return res(body) 98 | }) 99 | 100 | await testHandler(handler, async (url) => { 101 | const targetBuffer = Buffer.from('Hello, world!') 102 | const responsePromise = got(url, { 103 | method: 'POST', 104 | body: targetBuffer, 105 | }) 106 | const bufferPromise = responsePromise.buffer() 107 | const [response, buffer] = await Promise.all([ 108 | responsePromise, 109 | bufferPromise, 110 | ]) 111 | 112 | expect(buffer.equals(targetBuffer)).toBe(true) 113 | expect(response.headers['content-length']).toBe( 114 | targetBuffer.length.toString() 115 | ) 116 | }) 117 | }) 118 | 119 | it('throws 413 error if the request body is bigger than limits', async () => { 120 | expect.hasAssertions() 121 | 122 | const bufferBodySelector = async ({ req }: Context) => { 123 | const body = await readBufferBody(req, { limit: '1 byte' }) 124 | return body 125 | } 126 | const handler = prismy([bufferBodySelector], (body) => { 127 | return res(body) 128 | }) 129 | 130 | await testHandler(handler, async (url) => { 131 | const targetBuffer = Buffer.from( 132 | 'Peter Piper picked a peck of pickled peppers' 133 | ) 134 | const response = await got(url, { 135 | throwHttpErrors: false, 136 | method: 'POST', 137 | responseType: 'json', 138 | body: targetBuffer, 139 | }) 140 | 141 | expect(response.statusCode).toBe(413) 142 | expect(response.body).toMatch('Body exceeded 1 byte limit') 143 | }) 144 | }) 145 | 146 | it('throws 400 error if encoding of request body is invalid', async () => { 147 | expect.hasAssertions() 148 | 149 | const bufferBodySelector = async ({ req }: Context) => { 150 | const body = await readBufferBody(req, { encoding: 'lol' }) 151 | return body 152 | } 153 | const handler = prismy([bufferBodySelector], (body) => { 154 | return res(body) 155 | }) 156 | 157 | await testHandler(handler, async (url) => { 158 | const targetBuffer = Buffer.from('Hello, world!') 159 | const response = await got(url, { 160 | throwHttpErrors: false, 161 | method: 'POST', 162 | responseType: 'json', 163 | body: targetBuffer, 164 | }) 165 | 166 | expect(response.statusCode).toBe(400) 167 | expect(response.body).toMatch('Invalid body') 168 | }) 169 | }) 170 | 171 | it('throws 500 error if the request is drained already', async () => { 172 | expect.hasAssertions() 173 | 174 | const bufferBodySelector = async ({ req }: Context) => { 175 | const length = req.headers['content-length'] 176 | await getRawBody(req, { limit: '1mb', length }) 177 | const body = await readBufferBody(req) 178 | return body 179 | } 180 | const handler = prismy([bufferBodySelector], (body) => { 181 | return res(body) 182 | }) 183 | 184 | await testHandler(handler, async (url) => { 185 | const targetBuffer = Buffer.from('Oops!') 186 | const response = await got(url, { 187 | throwHttpErrors: false, 188 | method: 'POST', 189 | responseType: 'json', 190 | body: targetBuffer, 191 | }) 192 | 193 | expect(response.statusCode).toBe(500) 194 | expect(response.body).toMatch('The request has already been drained') 195 | }) 196 | }) 197 | }) 198 | 199 | describe('readTextBody', () => { 200 | it('reads text from request body', async () => { 201 | expect.hasAssertions() 202 | 203 | const textBodySelector = async ({ req }: Context) => { 204 | const body = await readTextBody(req) 205 | return body 206 | } 207 | const handler = prismy([textBodySelector], (body) => { 208 | return res(body) 209 | }) 210 | 211 | await testHandler(handler, async (url) => { 212 | const targetBuffer = Buffer.from('Hello, world!') 213 | const response = await got(url, { 214 | method: 'POST', 215 | body: targetBuffer, 216 | }) 217 | expect(response.body).toBe('Hello, world!') 218 | expect(response.headers['content-length']).toBe( 219 | targetBuffer.length.toString() 220 | ) 221 | }) 222 | }) 223 | }) 224 | 225 | describe('readJsonBody', () => { 226 | it('reads and parse JSON from a request body', async () => { 227 | expect.hasAssertions() 228 | 229 | const jsonBodySelector = async ({ req }: Context) => { 230 | const body = await readJsonBody(req) 231 | return body 232 | } 233 | const handler = prismy([jsonBodySelector], (body) => { 234 | return res(body) 235 | }) 236 | 237 | await testHandler(handler, async (url) => { 238 | const target = { 239 | foo: 'bar', 240 | } 241 | const response = await got(url, { 242 | method: 'POST', 243 | responseType: 'json', 244 | json: target, 245 | }) 246 | expect(response.body).toMatchObject(target) 247 | expect(response.headers['content-length']).toBe( 248 | JSON.stringify(target).length.toString() 249 | ) 250 | }) 251 | }) 252 | 253 | it('throws 400 error if the JSON body is invalid', async () => { 254 | expect.hasAssertions() 255 | 256 | const jsonBodySelector = async ({ req }: Context) => { 257 | const body = await readJsonBody(req) 258 | return body 259 | } 260 | const handler = prismy([jsonBodySelector], (body) => { 261 | return res(body) 262 | }) 263 | 264 | await testHandler(handler, async (url) => { 265 | const target = 'Oopsie' 266 | const response = await got(url, { 267 | throwHttpErrors: false, 268 | method: 'POST', 269 | responseType: 'json', 270 | body: target, 271 | }) 272 | expect(response.statusCode).toBe(400) 273 | expect(response.body).toMatch('Error: Invalid JSON') 274 | }) 275 | }) 276 | }) 277 | -------------------------------------------------------------------------------- /specs/send.spec.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import { IncomingMessage, RequestListener, ServerResponse } from 'http' 3 | import { Readable } from 'stream' 4 | import { send } from '../src/send' 5 | import { testHandler } from './helpers' 6 | 7 | describe('send', () => { 8 | it('sends empty body when body is null', async () => { 9 | expect.hasAssertions() 10 | 11 | const handler: RequestListener = (req, res) => { 12 | send(req, res, {}) 13 | } 14 | 15 | await testHandler(handler, async (url) => { 16 | const response = await got(url) 17 | expect(response.body).toBeFalsy() 18 | }) 19 | }) 20 | 21 | it('sends string body', async () => { 22 | expect.hasAssertions() 23 | 24 | const handler: RequestListener = (req, res) => { 25 | send(req, res, { body: 'test' }) 26 | } 27 | 28 | await testHandler(handler, async (url) => { 29 | const response = await got(url) 30 | expect(response.body).toEqual('test') 31 | }) 32 | }) 33 | 34 | it('sends buffer body', async () => { 35 | expect.hasAssertions() 36 | 37 | const targetBuffer = Buffer.from('Hello, world!') 38 | const handler: RequestListener = (req, res) => { 39 | res.setHeader('Content-Type', 'application/octet-stream') 40 | const statusCode = res.statusCode 41 | send(req, res, { statusCode, body: targetBuffer }) 42 | } 43 | 44 | await testHandler(handler, async (url) => { 45 | const responsePromise = got(url) 46 | const bufferPromise = responsePromise.buffer() 47 | const [response, buffer] = await Promise.all([ 48 | responsePromise, 49 | bufferPromise, 50 | ]) 51 | 52 | expect(targetBuffer.equals(buffer)).toBe(true) 53 | expect(response.headers['content-length']).toBe( 54 | targetBuffer.length.toString() 55 | ) 56 | }) 57 | }) 58 | 59 | it('sets header when Content-Type header is not given (buffer)', async () => { 60 | expect.hasAssertions() 61 | 62 | const targetBuffer = Buffer.from('Hello, world!') 63 | const handler: RequestListener = (req, res) => { 64 | const statusCode = res.statusCode 65 | send(req, res, { 66 | statusCode, 67 | body: targetBuffer, 68 | }) 69 | } 70 | 71 | await testHandler(handler, async (url) => { 72 | const responsePromise = got(url) 73 | const bufferPromise = responsePromise.buffer() 74 | const [response, buffer] = await Promise.all([ 75 | responsePromise, 76 | bufferPromise, 77 | ]) 78 | 79 | expect(targetBuffer.equals(buffer)).toBe(true) 80 | expect(response.headers['content-length']).toBe( 81 | targetBuffer.length.toString() 82 | ) 83 | expect(response.headers['content-type']).toBe('application/octet-stream') 84 | }) 85 | }) 86 | 87 | it('sends buffer body when body is stream', async () => { 88 | expect.hasAssertions() 89 | 90 | const targetBuffer = Buffer.from('Hello, world!') 91 | const stream = Readable.from(targetBuffer.toString()) 92 | const handler: RequestListener = (req, res) => { 93 | res.setHeader('Content-Type', 'application/octet-stream') 94 | const statusCode = res.statusCode 95 | send(req, res, { 96 | statusCode, 97 | body: stream, 98 | }) 99 | } 100 | 101 | await testHandler(handler, async (url) => { 102 | const response = await got(url, { 103 | responseType: 'buffer', 104 | }) 105 | expect(targetBuffer.equals(response.body)).toBe(true) 106 | }) 107 | }) 108 | 109 | it('uses handler when body is function', async () => { 110 | expect.hasAssertions() 111 | const sendHandler = ( 112 | _request: IncomingMessage, 113 | response: ServerResponse 114 | ) => { 115 | response.end('test') 116 | } 117 | const handler: RequestListener = (req, res) => { 118 | send(req, res, sendHandler) 119 | } 120 | 121 | await testHandler(handler, async (url) => { 122 | const response = await got(url) 123 | expect(response.body).toEqual('test') 124 | }) 125 | }) 126 | 127 | it('sets header when Content-Type header is not given (stream)', async () => { 128 | expect.hasAssertions() 129 | 130 | const targetBuffer = Buffer.from('Hello, world!') 131 | const stream = Readable.from(targetBuffer.toString()) 132 | const handler: RequestListener = (req, res) => { 133 | const statusCode = res.statusCode 134 | send(req, res, { statusCode, body: stream }) 135 | } 136 | 137 | await testHandler(handler, async (url) => { 138 | const responsePromise = got(url) 139 | const bufferPromise = responsePromise.buffer() 140 | const [response, buffer] = await Promise.all([ 141 | responsePromise, 142 | bufferPromise, 143 | ]) 144 | expect(targetBuffer.equals(buffer)).toBe(true) 145 | expect(response.headers['content-type']).toBe('application/octet-stream') 146 | }) 147 | }) 148 | 149 | it('sends stringified JSON object when body is object', async () => { 150 | expect.hasAssertions() 151 | 152 | const target = { 153 | foo: 'bar', 154 | } 155 | const handler: RequestListener = (req, res) => { 156 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 157 | const statusCode = res.statusCode 158 | send(req, res, { statusCode, body: target }) 159 | } 160 | 161 | await testHandler(handler, async (url) => { 162 | const response = await got(url, { 163 | responseType: 'json', 164 | }) 165 | expect(response.body).toMatchObject(target) 166 | expect(response.headers['content-length']).toBe( 167 | JSON.stringify(target).length.toString() 168 | ) 169 | }) 170 | }) 171 | 172 | it('sets header when Content-Type header is not given (object)', async () => { 173 | expect.hasAssertions() 174 | 175 | const target = { 176 | foo: 'bar', 177 | } 178 | const handler: RequestListener = (req, res) => { 179 | const statusCode = res.statusCode 180 | send(req, res, { statusCode, body: target }) 181 | } 182 | 183 | await testHandler(handler, async (url) => { 184 | const response = await got(url, { 185 | responseType: 'json', 186 | }) 187 | expect(response.body).toMatchObject(target) 188 | expect(response.headers['content-length']).toBe( 189 | JSON.stringify(target).length.toString() 190 | ) 191 | expect(response.headers['content-type']).toBe( 192 | 'application/json; charset=utf-8' 193 | ) 194 | }) 195 | }) 196 | 197 | it('sends stringified JSON object when body is number', async () => { 198 | expect.hasAssertions() 199 | 200 | const target = 1004 201 | const handler: RequestListener = (req, res) => { 202 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 203 | const statusCode = res.statusCode 204 | send(req, res, { 205 | statusCode, 206 | body: target, 207 | }) 208 | } 209 | 210 | await testHandler(handler, async (url) => { 211 | const response = await got(url) 212 | const stringifiedTarget = JSON.stringify(target) 213 | expect(response.body).toBe(stringifiedTarget) 214 | expect(response.headers['content-length']).toBe( 215 | stringifiedTarget.length.toString() 216 | ) 217 | }) 218 | }) 219 | 220 | it('sets header when Content-Type header is not given (number)', async () => { 221 | expect.hasAssertions() 222 | 223 | const target = 1004 224 | const handler: RequestListener = (req, res) => { 225 | const statusCode = res.statusCode 226 | send(req, res, { 227 | statusCode, 228 | body: target, 229 | }) 230 | } 231 | 232 | await testHandler(handler, async (url) => { 233 | const response = await got(url) 234 | const stringifiedTarget = JSON.stringify(target) 235 | expect(response.body).toBe(stringifiedTarget) 236 | expect(response.headers['content-length']).toBe( 237 | stringifiedTarget.length.toString() 238 | ) 239 | expect(response.headers['content-type']).toBe( 240 | 'application/json; charset=utf-8' 241 | ) 242 | }) 243 | }) 244 | 245 | it('sends with header', async () => { 246 | expect.hasAssertions() 247 | 248 | const handler: RequestListener = (req, res) => { 249 | const statusCode = res.statusCode 250 | send(req, res, { 251 | statusCode, 252 | headers: { 253 | test: 'test value', 254 | }, 255 | }) 256 | } 257 | 258 | await testHandler(handler, async (url) => { 259 | const response = await got(url) 260 | expect(response.body).toBeFalsy() 261 | expect(response.headers['test']).toEqual('test value') 262 | }) 263 | }) 264 | it('sends with header', async () => { 265 | expect.hasAssertions() 266 | 267 | const handler: RequestListener = (req, res) => { 268 | send(req, res, { 269 | headers: { 270 | test: 'test value', 271 | }, 272 | }) 273 | } 274 | 275 | await testHandler(handler, async (url) => { 276 | const response = await got(url) 277 | expect(response.body).toBeFalsy() 278 | expect(response.headers['test']).toEqual('test value') 279 | }) 280 | }) 281 | }) 282 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | prismy 2 | 3 | # `prismy` 4 | 5 | :rainbow: Simple and fast type safe server library based on micro for now.sh v2. 6 | 7 | [![Build Status](https://travis-ci.com/prismyland/prismy.svg?branch=master)](https://travis-ci.com/prismyland/prismy) 8 | [![codecov](https://codecov.io/gh/prismyland/prismy/branch/master/graph/badge.svg)](https://codecov.io/gh/prismyland/prismy) 9 | [![NPM download](https://img.shields.io/npm/dm/prismy.svg)](https://www.npmjs.com/package/prismy) 10 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/prismyland/prismy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/prismyland/prismy/context:javascript) 11 | 12 | [Full API Documentation](https://prismyland.github.io/prismy/globals.html) 13 | 14 | ## Index 15 | 16 | - [Getting Started](#getting-started) 17 | - [Requisition](#requisition) 18 | - [Installation](#installation) 19 | - [Hello World](#hello-world) 20 | - [Guide](#guide) 21 | - [Context](#context) 22 | - [Selectors](#selectors) 23 | - [Middleware](#middleware) 24 | - [Session](#session) 25 | - [Cookies](#cookies) 26 | - [Routing](#routing) 27 | - [Example](#simple-example) 28 | - [Testing](#writing-tests) 29 | - [Gotchas](#gotchas-and-troubleshooting) 30 | 31 | ## Concepts 32 | 33 | 1. _Asynchronously_ pick required values of a handler from context(which having HTTP Request object: IncomingMessage). 34 | 2. _Asynchronously_ execute the handler with the picked values. 35 | 3. **PROFIT!!** 36 | 37 | ## Features 38 | 39 | - Very small (No Expressjs, the only deps are micro and tslib) 40 | - Takes advantage of the asynchronous nature of Javascript with full support for async / await 41 | - Simple and easy argument injection for handlers (Inspired by ReselectJS) 42 | - Completely **TYPE-SAFE** 43 | - No more complicated classes / decorators, only simple functions 44 | - Highly testable (Request handlers can be tested without mocking request or sending actual http requests) 45 | - Single pass (lambda) style composable middleware (Similar to Redux) 46 | 47 | ## Getting Started 48 | 49 | ### Requisition 50 | 51 | - TypeScript v4.x 52 | - Node v12.x and above 53 | 54 | ### Installation 55 | 56 | Create a package.json file. 57 | 58 | ```sh 59 | npm init 60 | ``` 61 | 62 | Install prismy. 63 | 64 | ```sh 65 | npm install prismy --save 66 | ``` 67 | 68 | Make sure typescript strict setting is on if using typescript. 69 | 70 | `tsconfig.json` 71 | 72 | ```json 73 | { 74 | "strict": true 75 | } 76 | ``` 77 | 78 | ### Hello World 79 | 80 | `handler.ts` 81 | 82 | ```ts 83 | import { prismy, res, Selector } from 'prismy' 84 | 85 | const worldSelector: Selector = () => 'world'! 86 | 87 | export default prismy([worldSelector], async world => { 88 | return res(`Hello ${world}`) // Hello world! 89 | }) 90 | ``` 91 | 92 | If you are using now.sh or next.js you can just put handlers in the `pages` directory and your done! 93 | Simple, easy, no hassle. 94 | 95 | Otherwise, serve your application using node.js http server. 96 | 97 | `serve.ts` 98 | 99 | ```ts 100 | import handler from './handler' 101 | import * as http from 'http' 102 | 103 | const server = new http.Server(handler) 104 | 105 | server.listen(process.env.PORT) 106 | ``` 107 | 108 | For more in-depth application see the more in-depth [Example.](#simple-example) 109 | 110 | ## Guide 111 | 112 | ### Context 113 | 114 | `context` is a simple plain object containing native node.js's request instance, `IncomingMessage`. 115 | 116 | ```ts 117 | interface Context { 118 | req: IncomingMessage 119 | } 120 | ``` 121 | 122 | Context is passed into all selectors and middlewares. It can be used to assist memoization and communicate between linked selectors and middlewares. 123 | 124 | :exclamation: **It is highly recommended to use `Symbol('property-name')` in order to prevent duplicating property names and end up overwriting something important.** 125 | Read more about Symbols [here.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 126 | 127 | This way of communicating via symbols on the context object is used in `prismy-session`. 128 | 129 | :exclamation: Due to how prismy resolves selectors, context should **NOT** be used to communicate between selectors. Due to its async nature resolution order cannot be guaranteed. 130 | 131 | ### Selectors 132 | 133 | Many other server libraries support argument injection through the use of 134 | decorators e.g InversifyJS, NestJS and TachiJS. 135 | Decorators can seem nice and clean but have several pitfalls. 136 | 137 | - Controllers must be declared as class. (But not class expressions) 138 | - Argument injection via decorators is not type-safe. 139 | 140 | An example controller in NestJS: 141 | 142 | ```ts 143 | function createController() { 144 | class GeneratedController { 145 | /** 146 | * Using decorators in class expression is not allowed yet. 147 | * So compiler will throw an error. 148 | * https://github.com/microsoft/TypeScript/issues/7342 149 | * */ 150 | run( 151 | // Argument types must be declared carefully because Typescript cannot infer it. 152 | @Query() query: QueryParams 153 | ): string { 154 | return 'Done!' 155 | } 156 | } 157 | return GeneratedController 158 | } 159 | ``` 160 | 161 | Prismy however uses _Selectors_, a pattern inspired by ReselectJS. 162 | Selectors are simple functions used to generate the arguments for the handler. A Selector accepts a 163 | single `context` argument or type `Context`. 164 | 165 | ```ts 166 | import { prismy, res, Selector } from 'prismy' 167 | 168 | // This selector picks the current url off the request object 169 | const urlSelector: Selector = context => { 170 | const url = context.req.url 171 | // So this selector always returns string. 172 | return url != null ? url : '' 173 | } 174 | 175 | export default prismy( 176 | [urlSelector], 177 | // Typescript can infer `url` argument type via the given selector tuple 178 | // making it type safe without having to worry about verbose typings. 179 | url => { 180 | await doSomethingWithUrl(url) 181 | return res('Done!') 182 | } 183 | ) 184 | ``` 185 | 186 | Async selectors are also fully supported out of the box! 187 | It will resolve all selectors right before executing handler. 188 | 189 | ```ts 190 | import { prismy, res, Selector } from 'prismy' 191 | 192 | const asyncSelector: Selector = async context => { 193 | const value = await readValueFromFileSystem() 194 | return value 195 | } 196 | 197 | export default prismy([asyncSelector], async value => { 198 | await doSomething(value) 199 | return res('Done!') 200 | }) 201 | ``` 202 | 203 | #### Included Selectors 204 | 205 | Prismy includes some helper selectors for common actions. 206 | Some examples are: 207 | 208 | - `methodSelector` 209 | - `querySelector` 210 | 211 | Others require configuration and so factory functions are exposed. 212 | 213 | - `createJsonBodySelector` 214 | - `createUrlEncodedBodySelector` 215 | 216 | ```ts 217 | import { createJsonBodySelector } from 'prismy' 218 | 219 | // createJsonBodySelector returns an AsyncSelector 220 | const jsonBodySelector = createJsonBodySelector({ 221 | limit: '1mb' 222 | }) 223 | 224 | export default prismy([jsonBodySelector], async jsonBody => { 225 | await doSomething(jsonBody) 226 | return res('Done!') 227 | }) 228 | ``` 229 | 230 | These helper selectors can be composed to provide more solid typing and error handling. 231 | 232 | ```ts 233 | import { createJsonBodySelector, Selector } from 'prismy' 234 | 235 | interface RequestBody { 236 | data: string 237 | id?: number 238 | } 239 | 240 | const jsonBodySelector = createJsonBodySelector() 241 | 242 | const requestBodySelector: Selector = context => { 243 | const jsonBody = jsonBodySelector(context) 244 | if (!jsonBody.hasOwnProperty('data')) { 245 | throw new Error('Query is required!') 246 | } 247 | return jsonBody 248 | } 249 | 250 | export default prismy([requestBodySelector], requestBody => { 251 | return res(`You're query was ${requestBody.json}!`) 252 | }) 253 | ``` 254 | 255 | For other helper selectors, please refer to the [API Documentation.](#api) 256 | 257 | ### Middleware 258 | 259 | Middleware in Prismy works as a single pass pipeline of composed functions. The next middleware is 260 | accepted as an argument to the previous middleware allowing the request to be progressed or returned as desired. 261 | The middleware stack is composed and so the response travels right to left across the array. 262 | 263 | This pattern, much like Redux middleware, allows you to: 264 | 265 | - Do something before executing handler (e.g Session) 266 | - Do something after executing handler (e.g CORS, Session) 267 | - Do something other than executing handler (e.g Routing, Error handling) 268 | 269 | ```ts 270 | import { middleware, prismy, res, Selector, updateHeaders } from 'prismy' 271 | 272 | const withCors = middleware([], next => async () => { 273 | const resObject = await next() 274 | 275 | return updateHeaders(resObject, { 276 | 'access-control-allow-origin': '*' 277 | }) 278 | }) 279 | 280 | // Middleware also accepts selectors which can be used for DI and unit testing 281 | const urlSelector: Selector = context => context.req.url! 282 | const withErrorHandler = middleware([urlSelector], next => async url => { 283 | try { 284 | return await next() 285 | } catch (error) { 286 | return res(`Error from ${url}: ${error.message}`) 287 | } 288 | }) 289 | 290 | export default prismy( 291 | [], 292 | () => { 293 | throw new Error('Bang!') 294 | }, 295 | /** 296 | * The request will progress through the middleware stack like so: 297 | * withErrorHandler => withCors => handler => withCors => withErrorHandler 298 | * */ 299 | [withCors, withErrorHandler] 300 | ) 301 | ``` 302 | 303 | ### Session 304 | 305 | Although you can implement your own sessions using selectors and middleware, Prismy offers a 306 | simple module to make it easy with `prismy-session`. 307 | 308 | Install it using: 309 | 310 | ```sh 311 | npm install prismy-session --save 312 | ``` 313 | 314 | `prismy-session` exposes `createSession` which accepts a `SessionStrategy` instance and returns a 315 | selector and middleware to give to prismy. 316 | Official strategies include `prismy-session-strategy-jwt-cookie` and `prismy-session-strategy-signed-cookie`. Both available on npm. 317 | 318 | ```ts 319 | import { prismy, res } from 'prismy' 320 | import createSession from 'prismy-session' 321 | import JWTSessionStrategy from 'prismy-session-strategy' 322 | 323 | const { sessionSelector, sessionMiddleware } = createSession( 324 | new JWTSessionStrategy({ 325 | secret: 'RANDOM_HASH' 326 | }) 327 | ) 328 | 329 | default export prismy( 330 | [sessionSelector], 331 | async session => { 332 | const { data } = session 333 | await doSomething(data) 334 | return res('Done') 335 | }, 336 | [sessionMiddleware] 337 | ) 338 | 339 | ``` 340 | 341 | ### Cookies 342 | 343 | Prismy also offers a selector for cookies in the `prismy-cookie` package. 344 | 345 | ```ts 346 | import { prismy, res } from 'prismy' 347 | import { appendCookie, createCookiesSelector } from 'prismy-cookie' 348 | 349 | const cookiesSelector = createCookiesSelector() 350 | 351 | export default prismy([cookiesSelector], async cookies => { 352 | /** appendCookie is a helper function that takes a response object and 353 | * a string key, value tuple returning a new response object with the 354 | * cookie appended. 355 | */ 356 | return appendCookie(res('Cookie added!'), ['key', 'value']) 357 | }) 358 | ``` 359 | 360 | ### Routing 361 | 362 | From v3, `prismy` provides `router` method to create a routing handler. 363 | 364 | ```ts 365 | import { prismy, res } from 'prismy' 366 | import { router } from 'prismy-method-router' 367 | import http from 'http' 368 | 369 | const myRouter = router([ 370 | [ 371 | ['/posts', 'get'], prismy([], () => { 372 | const posts = fetchPostList() 373 | 374 | return res({ posts }) 375 | }) 376 | ], 377 | [ 378 | ['/posts', 'post'], prismy([bodySelector], (body) => { 379 | const post = createPost(body) 380 | 381 | return redirect(`/posts/${post.id}`) 382 | }) 383 | ], 384 | [ 385 | // GET method can be omitted 386 | // You can select route param with `createRouteParamSelector` 387 | '/posts/:postId', prismy([createRouteParamSelector('postId')], (postId) => { 388 | const post = fetchOnePost(postId) 389 | 390 | return res({ post }) 391 | }) 392 | ] 393 | ]) 394 | 395 | // Router is a prismy handler. You can directly pass to the server 396 | const server = new http.Server(handler) 397 | 398 | server.listen(process.env.PORT) 399 | ``` 400 | 401 | > Routing handler is using path-to-regexp internally. Please check their document to learn more routing behavior. 402 | https://github.com/pillarjs/path-to-regexp 403 | 404 | ## Simple Example 405 | 406 | ```ts 407 | import { 408 | createJsonBodySelector, 409 | middleware, 410 | prismy, 411 | querySelector, 412 | redirect, 413 | res, 414 | Selector 415 | } from 'prismy' 416 | import { methodRouter } from 'prismy-method-router' 417 | import createSession from 'prismy-session' 418 | import JWTSessionStrategy from 'prismy-session-strategy-jwt-cookie' 419 | 420 | const jsonBodySelector = createJsonBodySelector({ 421 | limit: '1mb' 422 | }) 423 | 424 | const { sessionSelector, sessionMiddleware } = createSession( 425 | new JWTSessionStrategy({ 426 | secret: 'RANDOM_HASH' 427 | }) 428 | ) 429 | 430 | const authSelector: Selector = async context => { 431 | const { data } = await sessionSelector(context) 432 | const user = await getUser(data.user_id) 433 | return user 434 | } 435 | 436 | const authMiddleware = middleware([authSelector], next => async user => { 437 | if (!isAuthorized(user)) { 438 | return redirect('/login') 439 | } 440 | return next() 441 | }) 442 | 443 | const todoIdSelector: Selector = async context => { 444 | const query = await querySelector(context) 445 | const { id } = query 446 | if (id == null) { 447 | throw new Error('Id is required!') 448 | } 449 | return Array.isArray(id) ? id[0] : id 450 | } 451 | 452 | const contentSelector: Selector = async context => { 453 | const jsonBody = await jsonBodySelector(context) 454 | const { content } = jsonBody 455 | if (content == null) { 456 | throw new Error('content is required!') 457 | } 458 | return jsonBody.content 459 | } 460 | 461 | export default methodRouter( 462 | { 463 | get: prismy([], async () => { 464 | const todos = await getTodos() 465 | return res({ todos }) 466 | }), 467 | post: prismy([contentSelector], async content => { 468 | const todo = await createTodo(content) 469 | return res({ todo }) 470 | }), 471 | delete: prismy([todoIdSelector], async id => { 472 | await deleteTodo(id) 473 | return res('Deleted') 474 | }) 475 | }, 476 | [authMiddleware, sessionMiddleware] 477 | ) 478 | ``` 479 | 480 | ## Writing Tests 481 | 482 | Prismy is designed to be easily testable. To furthur ease testing `prismy-test` exposes the `testHandler` function to create quick and easy end to end tests. 483 | 484 | ### E2E Tests 485 | 486 | End to end tests are very simple. 487 | 488 | ```ts 489 | import got from 'got' 490 | import { testHandler } from "prismy-test" 491 | import handler from './handler' 492 | 493 | describe('handler', () => { 494 | it('e2e test', async () => { 495 | await testHandler(handler, async url => { 496 | const response = await got(url, { 497 | method: 'POST', 498 | responseType: 'json', 499 | json: { 500 | ... // JSON data 501 | } 502 | }) 503 | expect(response).toMatchObject({ 504 | statusCode: 200, 505 | body: '/' 506 | }) 507 | }) 508 | }) 509 | }) 510 | ``` 511 | 512 | ### Unit Tests 513 | 514 | Thanks to Prismy's simple, function-based architecture unit testing in Prismy is extremely simple. 515 | Prismy handler exposes its original handler function so you can directly unit test the handler function even if it is an anonymous function argument to `prismy` without needing to mock http requests. 516 | 517 | ```ts 518 | import handler from './handler' 519 | 520 | decribe('handler', () => { 521 | it('unit test', () => { 522 | /** 523 | * Access the original handler function 524 | * */ 525 | const result = handler.handler({ 526 | ... // whatever arguments you want to test with 527 | }) 528 | 529 | expect(result).toEqual({ 530 | body: 'Done!', 531 | headers: {}, 532 | statusCode: 200 533 | }) 534 | }) 535 | }) 536 | ``` 537 | 538 | ## Gotchas and Troubleshooting 539 | 540 | ### Long `type is not assignable to [Selector ...` error when creating Prismy handler 541 | 542 | - Selectors must be written directly into the array argument in the function call. This is due to a limitation of Typescript type inference. Prismy relies on knowning the tuple type of the array, e.g `[string, number]`. Dynamicly creating the array will infer as `string|number[]` which means Prismy cannot infer the positional types for the handler arguments. 543 | 544 | ```ts 545 | const selectors = [selector1, selector2] 546 | prismy(selectors, handler) // will give type error 547 | 548 | prismy([selector1, selector2], handler) // Ok! 549 | ``` 550 | 551 | - This weird type error may also occur if the handler does not return a `ResponseObject`. Use `res(..)` to generate a `ResponseObject` easily. 552 | 553 | ```ts 554 | // Will show crazy error. 555 | prismy([selector1, selector2], (one, two) => { 556 | return 'Not a ResponseObject' 557 | }) 558 | 559 | // Ok! 560 | prismy([selector1, selector2], (one, two) => { 561 | return res('Is a ResponseObject') 562 | }) 563 | ``` 564 | 565 | ### Long `type is not assignable to [Selector ...` error when creating middleware 566 | 567 | - mhandler argument must be of `type next => async () => T`. Remember the async. 568 | - If using Typescript, `'strict'` compiler option MUST be `true`. This can be set in tsconfig.json. 569 | 570 | 571 | 572 | ## License 573 | 574 | MIT 575 | -------------------------------------------------------------------------------- /temp/prismy.api.md: -------------------------------------------------------------------------------- 1 | ## API Report File for "prismy" 2 | 3 | > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). 4 | 5 | ```ts 6 | 7 | import { createError } from 'micro'; 8 | import { IncomingHttpHeaders } from 'http'; 9 | import { IncomingMessage } from 'http'; 10 | import { OutgoingHttpHeaders } from 'http'; 11 | import { ParsedUrlQuery } from 'querystring'; 12 | import { ServerResponse } from 'http'; 13 | import { UrlWithStringQuery } from 'url'; 14 | 15 | // @public 16 | export type AsyncSelector = (context: Context) => Promise; 17 | 18 | // @public 19 | export interface BufferBodySelectorOptions { 20 | // (undocumented) 21 | encoding?: string; 22 | // (undocumented) 23 | limit?: string | number; 24 | } 25 | 26 | // Warning: (ae-internal-missing-underscore) The name "compileHandler" should be prefixed with an underscore because the declaration is marked as @internal 27 | // 28 | // @internal 29 | export function compileHandler(selectors: Selectors, handler: (...args: A) => R): (context: Context) => Promise; 30 | 31 | // @public 32 | export interface Context { 33 | // (undocumented) 34 | req: IncomingMessage; 35 | } 36 | 37 | // Warning: (ae-forgotten-export) The symbol "SyncSelector_2" needs to be exported by the entry point index.d.ts 38 | // Warning: (ae-forgotten-export) The symbol "Context_2" needs to be exported by the entry point index.d.ts 39 | // 40 | // @public 41 | export const contextSelector: SyncSelector_2; 42 | 43 | // Warning: (ae-forgotten-export) The symbol "AsyncSelector_2" needs to be exported by the entry point index.d.ts 44 | // 45 | // @public 46 | export function createBufferBodySelector(options?: BufferBodySelectorOptions): AsyncSelector_2; 47 | 48 | export { createError } 49 | 50 | // @public 51 | export function createJsonBodySelector(options?: JsonBodySelectorOptions): AsyncSelector_2; 52 | 53 | // @public 54 | export function createTextBodySelector(options?: TextBodySelectorOptions): AsyncSelector_2; 55 | 56 | // @public 57 | export function createUrlEncodedBodySelector(options?: UrlEncodedBodySelectorOptions): AsyncSelector_2; 58 | 59 | // Warning: (ae-forgotten-export) The symbol "WithErrorHandlerOptions" needs to be exported by the entry point index.d.ts 60 | // Warning: (ae-forgotten-export) The symbol "PrismyPureMiddleware_2" needs to be exported by the entry point index.d.ts 61 | // 62 | // @public 63 | export function createWithErrorHandler({ dev, json }?: WithErrorHandlerOptions): PrismyPureMiddleware_2; 64 | 65 | // @public 66 | export const headersSelector: SyncSelector_2; 67 | 68 | // @public 69 | export interface JsonBodySelectorOptions { 70 | // (undocumented) 71 | encoding?: string; 72 | // (undocumented) 73 | limit?: string | number; 74 | // (undocumented) 75 | skipContentTypeCheck?: boolean; 76 | } 77 | 78 | // @public 79 | export const methodSelector: SyncSelector_2; 80 | 81 | // Warning: (ae-forgotten-export) The symbol "ResponseObject_2" needs to be exported by the entry point index.d.ts 82 | // Warning: (ae-forgotten-export) The symbol "PrismyMiddleware_2" needs to be exported by the entry point index.d.ts 83 | // 84 | // @public (undocumented) 85 | export function middleware(selectors: never[], mhandler: (next: () => Promise>) => () => Promise>): PrismyMiddleware_2<[]>; 86 | 87 | // Warning: (ae-forgotten-export) The symbol "Selector_2" needs to be exported by the entry point index.d.ts 88 | // Warning: (ae-forgotten-export) The symbol "Unpromise_2" needs to be exported by the entry point index.d.ts 89 | // 90 | // @public (undocumented) 91 | export function middleware(selectors: [Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2]>; 92 | 93 | // @public (undocumented) 94 | export function middleware(selectors: [Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2]>; 95 | 96 | // @public (undocumented) 97 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2]>; 98 | 99 | // @public (undocumented) 100 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 101 | 102 | // @public (undocumented) 103 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 104 | 105 | // @public (undocumented) 106 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 107 | 108 | // @public (undocumented) 109 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 110 | 111 | // @public (undocumented) 112 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 113 | 114 | // @public (undocumented) 115 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2, arg9: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 116 | 117 | // @public (undocumented) 118 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2, arg9: Unpromise_2, arg10: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 119 | 120 | // @public (undocumented) 121 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2, arg9: Unpromise_2, arg10: Unpromise_2, arg11: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 122 | 123 | // @public 124 | export function middleware(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], mhandler: (next: () => Promise>) => (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2, arg9: Unpromise_2, arg10: Unpromise_2, arg11: Unpromise_2, arg12: Unpromise_2) => Promise>): PrismyMiddleware_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 125 | 126 | // Warning: (ae-forgotten-export) The symbol "Selectors_2" needs to be exported by the entry point index.d.ts 127 | // Warning: (ae-internal-missing-underscore) The name "middlewarex" should be prefixed with an underscore because the declaration is marked as @internal 128 | // 129 | // @internal 130 | export function middlewarex(selectors: Selectors_2, mhandler: (next: () => Promise>) => (...args: A) => Promise>): PrismyMiddleware_2; 131 | 132 | // Warning: (ae-forgotten-export) The symbol "PrismyRequestListener_2" needs to be exported by the entry point index.d.ts 133 | // 134 | // @public (undocumented) 135 | export function prismy(selectors: [], handler: () => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[]>; 136 | 137 | // @public (undocumented) 138 | export function prismy(selectors: [Selector_2], handler: (arg1: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2]>; 139 | 140 | // @public (undocumented) 141 | export function prismy(selectors: [Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2]>; 142 | 143 | // @public (undocumented) 144 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2]>; 145 | 146 | // @public (undocumented) 147 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 148 | 149 | // @public (undocumented) 150 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 151 | 152 | // @public (undocumented) 153 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 154 | 155 | // @public (undocumented) 156 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 157 | 158 | // @public (undocumented) 159 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 160 | 161 | // @public (undocumented) 162 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2, arg9: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 163 | 164 | // @public (undocumented) 165 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2, arg9: Unpromise_2, arg10: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 166 | 167 | // @public (undocumented) 168 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2, arg9: Unpromise_2, arg10: Unpromise_2, arg11: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 169 | 170 | // @public 171 | export function prismy(selectors: [Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2, Selector_2], handler: (arg1: Unpromise_2, arg2: Unpromise_2, arg3: Unpromise_2, arg4: Unpromise_2, arg5: Unpromise_2, arg6: Unpromise_2, arg7: Unpromise_2, arg8: Unpromise_2, arg9: Unpromise_2, arg10: Unpromise_2, arg11: Unpromise_2, arg12: Unpromise_2) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2<[Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2, Unpromise_2]>; 172 | 173 | // Warning: (ae-internal-missing-underscore) The name "PrismyMiddleware" should be prefixed with an underscore because the declaration is marked as @internal 174 | // 175 | // @internal 176 | export interface PrismyMiddleware extends PrismyPureMiddleware { 177 | // (undocumented) 178 | mhandler(next: () => Promise>): (...args: A) => Promise>; 179 | } 180 | 181 | // Warning: (ae-internal-missing-underscore) The name "PrismyPureMiddleware" should be prefixed with an underscore because the declaration is marked as @internal 182 | // 183 | // @internal 184 | export interface PrismyPureMiddleware { 185 | // (undocumented) 186 | (context: Context): (next: () => Promise>) => Promise>; 187 | } 188 | 189 | // Warning: (ae-internal-missing-underscore) The name "PrismyRequestListener" should be prefixed with an underscore because the declaration is marked as @internal 190 | // 191 | // @internal (undocumented) 192 | export interface PrismyRequestListener { 193 | // (undocumented) 194 | (req: IncomingMessage, res: ServerResponse): void; 195 | // (undocumented) 196 | handler(...args: A): ResponseObject | Promise>; 197 | } 198 | 199 | // @public 200 | export function prismyx(selectors: Selectors_2, handler: (...args: A) => ResponseObject_2 | Promise>, middlewareList?: PrismyPureMiddleware_2[]): PrismyRequestListener_2; 201 | 202 | // @public 203 | export const querySelector: SyncSelector_2; 204 | 205 | // @public 206 | export function redirect(location: string, statusCode?: number, extraHeaders?: OutgoingHttpHeaders): ResponseObject; 207 | 208 | // @public 209 | export function res(body: B, statusCode?: number, headers?: OutgoingHttpHeaders): ResponseObject; 210 | 211 | // Warning: (ae-internal-missing-underscore) The name "resolveSelectors" should be prefixed with an underscore because the declaration is marked as @internal 212 | // 213 | // @internal 214 | export function resolveSelectors(context: Context, selectors: Selectors): Promise; 215 | 216 | // @public 217 | export interface ResponseObject { 218 | // (undocumented) 219 | body?: B; 220 | // (undocumented) 221 | headers: OutgoingHttpHeaders; 222 | // (undocumented) 223 | statusCode: number; 224 | } 225 | 226 | // @public 227 | export type Selector = SyncSelector | AsyncSelector; 228 | 229 | // Warning: (ae-internal-missing-underscore) The name "Selectors" should be prefixed with an underscore because the declaration is marked as @internal 230 | // 231 | // @internal (undocumented) 232 | export type Selectors = { 233 | [P in keyof T]: Selector; 234 | }; 235 | 236 | // @public 237 | export function setBody(resObject: ResponseObject, body: B2): ResponseObject; 238 | 239 | // @public 240 | export function setHeaders(resObject: ResponseObject, headers: OutgoingHttpHeaders): ResponseObject; 241 | 242 | // @public 243 | export function setStatusCode(resObject: ResponseObject, statusCode: number): ResponseObject; 244 | 245 | // @public 246 | export type SyncSelector = (context: Context) => T; 247 | 248 | // @public 249 | export interface TextBodySelectorOptions { 250 | // (undocumented) 251 | encoding?: string; 252 | // (undocumented) 253 | limit?: string | number; 254 | } 255 | 256 | // Warning: (ae-internal-missing-underscore) The name "Unpromise" should be prefixed with an underscore because the declaration is marked as @internal 257 | // 258 | // @internal (undocumented) 259 | export type Unpromise = T extends Promise ? U : T; 260 | 261 | // @public 262 | export function updateHeaders(resObject: ResponseObject, extraHeaders: OutgoingHttpHeaders): ResponseObject; 263 | 264 | // @public 265 | export interface UrlEncodedBodySelectorOptions { 266 | // (undocumented) 267 | encoding?: string; 268 | // (undocumented) 269 | limit?: string | number; 270 | } 271 | 272 | // @public 273 | export const urlSelector: SyncSelector_2; 274 | 275 | 276 | // (No @packageDocumentation comment for this package) 277 | 278 | ``` 279 | --------------------------------------------------------------------------------