From 51e6874c37ff0c749240cd484a7840c26b160e5f Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Thu, 21 Mar 2024 16:43:38 +0800 Subject: [PATCH] Update types Remove inflection dependency. --- examples/basic/server.ts | 29 ++-- package.json | 7 +- pnpm-lock.yaml | 107 ++++++------- src/backend/common.ts | 5 - src/backend/core.ts | 89 +---------- src/backend/extenders/method.ts | 2 +- src/backend/extenders/url.ts | 2 +- src/backend/handlers.ts | 3 +- src/backend/server.ts | 275 ++++++++++++++++++-------------- src/backend/utils.ts | 42 ++--- 10 files changed, 251 insertions(+), 310 deletions(-) diff --git a/examples/basic/server.ts b/examples/basic/server.ts index 16f8964..cd9faec 100644 --- a/examples/basic/server.ts +++ b/examples/basic/server.ts @@ -57,22 +57,21 @@ const app = application({ .resource(Piano) .resource(User); -app.create({ +const backend = app.createBackend({ dataSource, -}).then((backend) => { - const server = backend.createServer({ - basePath: '/api' - }); - - server.listen(3000); +}); - setTimeout(() => { - // Allow user operations after 5 seconds from startup - User - .canFetchItem() - .canFetchCollection() - .canCreate() - .canPatch(); - }, 5000); +const server = backend.createServer({ + basePath: '/api' }); +server.listen(3000); + +setTimeout(() => { + // Allow user operations after 5 seconds from startup + User + .canFetchItem() + .canFetchCollection() + .canCreate() + .canPatch(); +}, 5000); diff --git a/package.json b/package.json index 217c01e..887d9a3 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ ], "devDependencies": { "@types/negotiator": "^0.6.3", - "@types/node": "^20.11.0", + "@types/node": "^20.11.30", "pridepack": "2.6.0", "tslib": "^2.6.2", - "typescript": "^5.3.3", - "vitest": "^1.2.0" + "typescript": "^5.4.3", + "vitest": "^1.4.0" }, "scripts": { "prepublishOnly": "pridepack clean && pridepack build", @@ -45,7 +45,6 @@ "access": "public" }, "dependencies": { - "inflection": "^3.0.0", "negotiator": "^0.6.3", "tsx": "^4.7.1", "valibot": "^0.30.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4774ecb..460d7a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,6 @@ settings: excludeLinksFromLockfile: false dependencies: - inflection: - specifier: ^3.0.0 - version: 3.0.0 negotiator: specifier: ^0.6.3 version: 0.6.3 @@ -23,20 +20,20 @@ devDependencies: specifier: ^0.6.3 version: 0.6.3 '@types/node': - specifier: ^20.11.0 - version: 20.11.0 + specifier: ^20.11.30 + version: 20.11.30 pridepack: specifier: 2.6.0 - version: 2.6.0(tslib@2.6.2)(typescript@5.3.3) + version: 2.6.0(tslib@2.6.2)(typescript@5.4.3) tslib: specifier: ^2.6.2 version: 2.6.2 typescript: - specifier: ^5.3.3 - version: 5.3.3 + specifier: ^5.4.3 + version: 5.4.3 vitest: - specifier: ^1.2.0 - version: 1.2.0(@types/node@20.11.0) + specifier: ^1.4.0 + version: 1.4.0(@types/node@20.11.30) packages: @@ -356,44 +353,44 @@ packages: resolution: {integrity: sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==} dev: true - /@types/node@20.11.0: - resolution: {integrity: sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==} + /@types/node@20.11.30: + resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} dependencies: undici-types: 5.26.5 dev: true - /@vitest/expect@1.2.0: - resolution: {integrity: sha512-H+2bHzhyvgp32o7Pgj2h9RTHN0pgYaoi26Oo3mE+dCi1PAqV31kIIVfTbqMO3Bvshd5mIrJLc73EwSRrbol9Lw==} + /@vitest/expect@1.4.0: + resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} dependencies: - '@vitest/spy': 1.2.0 - '@vitest/utils': 1.2.0 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 chai: 4.4.1 dev: true - /@vitest/runner@1.2.0: - resolution: {integrity: sha512-vaJkDoQaNUTroT70OhM0NPznP7H3WyRwt4LvGwCVYs/llLaqhoSLnlIhUClZpbF5RgAee29KRcNz0FEhYcgxqA==} + /@vitest/runner@1.4.0: + resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} dependencies: - '@vitest/utils': 1.2.0 + '@vitest/utils': 1.4.0 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.2.0: - resolution: {integrity: sha512-P33EE7TrVgB3HDLllrjK/GG6WSnmUtWohbwcQqmm7TAk9AVHpdgf7M3F3qRHKm6vhr7x3eGIln7VH052Smo6Kw==} + /@vitest/snapshot@1.4.0: + resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} dependencies: magic-string: 0.30.8 pathe: 1.1.2 pretty-format: 29.7.0 dev: true - /@vitest/spy@1.2.0: - resolution: {integrity: sha512-MNxSAfxUaCeowqyyGwC293yZgk7cECZU9wGb8N1pYQ0yOn/SIr8t0l9XnGRdQZvNV/ZHBYu6GO/W3tj5K3VN1Q==} + /@vitest/spy@1.4.0: + resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} dependencies: tinyspy: 2.2.1 dev: true - /@vitest/utils@1.2.0: - resolution: {integrity: sha512-FyD5bpugsXlwVpTcGLDf3wSPYy8g541fQt14qtzo8mJ4LdEpDKZ9mQy2+qdJm2TZRpjY5JLXihXCgIxiRJgi5g==} + /@vitest/utils@1.4.0: + resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -739,11 +736,6 @@ packages: engines: {node: '>=0.8.19'} dev: true - /inflection@3.0.0: - resolution: {integrity: sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw==} - engines: {node: '>=18.0.0'} - dev: false - /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true @@ -785,6 +777,10 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + /jsonc-parser@3.2.1: resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} dev: true @@ -1022,7 +1018,7 @@ packages: react-is: 18.2.0 dev: true - /pridepack@2.6.0(tslib@2.6.2)(typescript@5.3.3): + /pridepack@2.6.0(tslib@2.6.2)(typescript@5.4.3): resolution: {integrity: sha512-K81TouT+M3zwzPvDi70/CFVtzADvGpn071zAMm419ULb29gZni21pJ24njDFm3O+lJn0txBl4x1dsFBLWqS4iQ==} engines: {node: '>=16'} hasBin: true @@ -1041,7 +1037,7 @@ packages: pretty-bytes: 6.1.1 prompts: 2.4.2 tslib: 2.6.2 - typescript: 5.3.3 + typescript: 5.4.3 yargs: 17.7.2 dev: true @@ -1215,10 +1211,10 @@ packages: engines: {node: '>=12'} dev: true - /strip-literal@1.3.0: - resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} dependencies: - acorn: 8.11.3 + js-tokens: 8.0.3 dev: true /tinybench@2.6.0: @@ -1261,8 +1257,8 @@ packages: is-typedarray: 1.0.0 dev: true - /typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + /typescript@5.4.3: + resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -1290,8 +1286,8 @@ packages: resolution: {integrity: sha512-5POBdbSkM+3nvJ6ZlyQHsggisfRtyT4tVTo1EIIShs6qCdXJnyWU5TJ68vr8iTg5zpOLjXLRiBqNx+9zwZz/rA==} dev: false - /vite-node@1.2.0(@types/node@20.11.0): - resolution: {integrity: sha512-ETnQTHeAbbOxl7/pyBck9oAPZZZo+kYnFt1uQDD+hPReOc+wCjXw4r4jHriBRuVDB5isHmPXxrfc1yJnfBERqg==} + /vite-node@1.4.0(@types/node@20.11.30): + resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -1299,7 +1295,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.1.6(@types/node@20.11.0) + vite: 5.1.6(@types/node@20.11.30) transitivePeerDependencies: - '@types/node' - less @@ -1311,7 +1307,7 @@ packages: - terser dev: true - /vite@5.1.6(@types/node@20.11.0): + /vite@5.1.6(@types/node@20.11.30): resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -1339,7 +1335,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.0 + '@types/node': 20.11.30 esbuild: 0.19.12 postcss: 8.4.35 rollup: 4.12.1 @@ -1347,15 +1343,15 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.2.0(@types/node@20.11.0): - resolution: {integrity: sha512-Ixs5m7BjqvLHXcibkzKRQUvD/XLw0E3rvqaCMlrm/0LMsA0309ZqYvTlPzkhh81VlEyVZXFlwWnkhb6/UMtcaQ==} + /vitest@1.4.0(@types/node@20.11.30): + resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': ^1.0.0 - '@vitest/ui': ^1.0.0 + '@vitest/browser': 1.4.0 + '@vitest/ui': 1.4.0 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -1372,14 +1368,13 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.11.0 - '@vitest/expect': 1.2.0 - '@vitest/runner': 1.2.0 - '@vitest/snapshot': 1.2.0 - '@vitest/spy': 1.2.0 - '@vitest/utils': 1.2.0 + '@types/node': 20.11.30 + '@vitest/expect': 1.4.0 + '@vitest/runner': 1.4.0 + '@vitest/snapshot': 1.4.0 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 acorn-walk: 8.3.2 - cac: 6.7.14 chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 @@ -1388,11 +1383,11 @@ packages: pathe: 1.1.2 picocolors: 1.0.0 std-env: 3.7.0 - strip-literal: 1.3.0 + strip-literal: 2.0.0 tinybench: 2.6.0 tinypool: 0.8.2 - vite: 5.1.6(@types/node@20.11.0) - vite-node: 1.2.0(@types/node@20.11.0) + vite: 5.1.6(@types/node@20.11.30) + vite-node: 1.4.0(@types/node@20.11.30) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/src/backend/common.ts b/src/backend/common.ts index 448be36..6e30be4 100644 --- a/src/backend/common.ts +++ b/src/backend/common.ts @@ -9,11 +9,6 @@ export interface BackendState { charset: Charset; mediaType: MediaType; } - errorHeaders: { - language?: string; - charset?: string; - serializer?: string; - } showTotalItemCountOnGetCollection: boolean; throws404OnDeletingNotFound: boolean; checksSerializersOnDelete: boolean; diff --git a/src/backend/core.ts b/src/backend/core.ts index 30bfec0..a56fb10 100644 --- a/src/backend/core.ts +++ b/src/backend/core.ts @@ -1,5 +1,5 @@ import * as v from 'valibot'; -import {ApplicationState, Language, LanguageStatusMessageMap, Resource} from '../common'; +import {ApplicationState, Resource} from '../common'; import http from 'http'; import {createServer, CreateServerParams} from './server'; import https from 'https'; @@ -22,8 +22,6 @@ export interface BackendResource< dataSource: DataSourceType; } -export interface RequestContext extends http.IncomingMessage {} - export interface BackendBuilder { showTotalItemCountOnGetCollection(b?: boolean): this; showTotalItemCountOnCreateItem(b?: boolean): this; @@ -33,84 +31,6 @@ export interface BackendBuilder { dataSource?: (resource: Resource) => T; } -export class MiddlewareError extends Error {} - -interface ResponseParams { - statusCode: Response['statusCode']; - statusMessage?: Response['statusMessage']; - headers?: Response['headers']; -} - - -interface PlainResponseParams extends ResponseParams { - body?: T; -} - -interface StreamResponseParams extends ResponseParams { - stream: NodeJS.ReadableStream; -} - - -interface HttpMiddlewareErrorParams extends Omit, 'statusMessage'> { - cause?: unknown -} - -export interface Response { - statusCode: number; - - statusMessage?: keyof LanguageStatusMessageMap; - - headers?: Record; -} - -export class PlainResponse implements Response { - readonly statusCode: Response['statusCode']; - - readonly statusMessage?: keyof LanguageStatusMessageMap; - - readonly headers: Response['headers']; - - readonly body?: T; - - constructor(args: PlainResponseParams) { - this.statusCode = args.statusCode; - this.statusMessage = args.statusMessage; - this.headers = args.headers; - this.body = args.body; - } -} - -export class StreamResponse implements Response { - readonly statusCode: Response['statusCode']; - - readonly statusMessage?: keyof LanguageStatusMessageMap; - - readonly headers: Response['headers']; - - readonly stream: NodeJS.ReadableStream; - - constructor(args: StreamResponseParams) { - this.statusCode = args.statusCode; - this.statusMessage = args.statusMessage; - this.headers = args.headers; - this.stream = args.stream; - } -} - -export class HttpMiddlewareError extends MiddlewareError { - readonly response: PlainResponse; - - constructor(statusMessage: keyof Language['statusMessages'], params: HttpMiddlewareErrorParams) { - super(statusMessage, { cause: params.cause }); - this.response = new PlainResponse({ - ...params, - statusMessage, - }); - } -} - -export interface ResponseContext extends http.ServerResponse {} - export interface CreateBackendParams { app: ApplicationState; dataSource: (resource: Resource) => BaseDataSource; @@ -125,13 +45,6 @@ export const createBackend = (params: CreateBackendParams) => { charset: utf8, mediaType: applicationJson }, - errorHeaders: { - // undefined follows user accept headers strictly - // - language: undefined, - charset: undefined, - serializer: undefined, - }, showTotalItemCountOnGetCollection: false, showTotalItemCountOnCreateItem: false, throws404OnDeletingNotFound: false, diff --git a/src/backend/extenders/method.ts b/src/backend/extenders/method.ts index 0eb6d61..3a89c14 100644 --- a/src/backend/extenders/method.ts +++ b/src/backend/extenders/method.ts @@ -1,6 +1,6 @@ import {constants} from 'http2'; import http from 'http'; -import {HttpMiddlewareError} from '../index'; +import {HttpMiddlewareError} from '../server'; interface RequestContext extends http.IncomingMessage { method: string; diff --git a/src/backend/extenders/url.ts b/src/backend/extenders/url.ts index d964f91..eca7199 100644 --- a/src/backend/extenders/url.ts +++ b/src/backend/extenders/url.ts @@ -1,6 +1,6 @@ import {constants} from 'http2'; import http from 'http'; -import {HttpMiddlewareError} from '..'; +import {HttpMiddlewareError} from '../server'; interface RequestContext extends http.IncomingMessage { basePath?: string; diff --git a/src/backend/handlers.ts b/src/backend/handlers.ts index f3a745d..3d97d9b 100644 --- a/src/backend/handlers.ts +++ b/src/backend/handlers.ts @@ -1,7 +1,6 @@ import { constants } from 'http2'; import * as v from 'valibot'; -import {HttpMiddlewareError, PlainResponse} from './core'; -import {Middleware} from './server'; +import {HttpMiddlewareError, PlainResponse, Middleware} from './server'; export const handleGetRoot: Middleware = (req) => { const { backend, basePath } = req; diff --git a/src/backend/server.ts b/src/backend/server.ts index ce5562a..b1ff90d 100644 --- a/src/backend/server.ts +++ b/src/backend/server.ts @@ -1,6 +1,6 @@ import http from 'http'; import {BackendState} from './common'; -import {Language, Resource, Charset, MediaType} from '../common'; +import {Language, Resource, Charset, MediaType, LanguageStatusMessageMap} from '../common'; import * as applicationJson from '../common/media-types/application/json'; import * as utf8 from '../common/charsets/utf-8'; import * as en from '../common/languages/en'; @@ -19,17 +19,65 @@ import { handlePatchItem, } from './handlers'; import { - HttpMiddlewareError, - PlainResponse, - ResponseContext, - StreamResponse, - Response, BackendResource, } from './core'; import * as v from 'valibot'; import {getBody} from './utils'; import {DataSource} from './data-source'; +export interface Response { + statusCode: number; + + statusMessage?: keyof LanguageStatusMessageMap; + + headers?: Record; +} + +interface ResponseParams { + statusCode: Response['statusCode']; + statusMessage?: Response['statusMessage']; + headers?: Response['headers']; +} + +export class MiddlewareError extends Error {} + +interface PlainResponseParams extends ResponseParams { + body?: T; +} + +interface HttpMiddlewareErrorParams extends Omit, 'statusMessage'> { + cause?: unknown +} + +export class PlainResponse implements Response { + readonly statusCode: Response['statusCode']; + + readonly statusMessage?: keyof LanguageStatusMessageMap; + + readonly headers: Response['headers']; + + readonly body?: T; + + constructor(args: PlainResponseParams) { + this.statusCode = args.statusCode; + this.statusMessage = args.statusMessage; + this.headers = args.headers; + this.body = args.body; + } +} + +export class HttpMiddlewareError extends MiddlewareError { + readonly response: PlainResponse; + + constructor(statusMessage: keyof Language['statusMessages'], params: HttpMiddlewareErrorParams) { + super(statusMessage, { cause: params.cause }); + this.response = new PlainResponse({ + ...params, + statusMessage, + }); + } +} + export interface CreateServerParams { basePath?: string; host?: string; @@ -74,6 +122,39 @@ export interface Middleware { (req: Req): undefined | Response | Promise; } +class ServerYasumiRequest extends http.IncomingMessage implements RequestContext { + host = 'localhost'; + + scheme = 'http'; + + basePath = ''; + + backend = {} as BackendState; + + resource = undefined as unknown as BackendResource; + + resourceId?: string; + + query = new URLSearchParams(); + + body?: unknown; + + method = ''; + + url = ''; + + rawUrl = ''; + + readonly cn: { + language: Language; + mediaType: MediaType; + charset: Charset; + } = { + language: en, + mediaType: applicationJson, + charset: utf8, + }; +} const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => { const middlewares = [] as [string, Middleware, v.BaseSchema?][]; if (mainResourceId === '') { @@ -127,46 +208,75 @@ const getAllowedMiddlewares = (resource: Resource, ma return middlewares; }; -export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { - const isHttps = 'key' in serverParams && 'cert' in serverParams; - - class ServerYasumiRequest extends http.IncomingMessage implements RequestContext { - readonly host = serverParams.host ?? 'localhost'; - - readonly scheme = isHttps ? 'https' : 'http'; - - readonly basePath = serverParams.basePath ?? ''; - - readonly backend = backendState; - - resource = undefined as unknown as BackendResource; - - resourceId?: string; - - query = new URLSearchParams(); - - body?: unknown; - - method = ''; - - url = ''; +const adjustRequestForContentNegotiation = (req: RequestContext, res: http.ServerResponse) => { + const negotiator = new Negotiator(req); + const availableLanguages = Array.from(req.backend.app.languages); + const availableCharsets = Array.from(req.backend.app.charsets); + const availableMediaTypes = Array.from(req.backend.app.mediaTypes); + + const languageCandidate = negotiator.language(availableLanguages.map((l) => l.name)) ?? req.backend.cn.language.name; + const charsetCandidate = negotiator.charset(availableCharsets.map((l) => l.name)) ?? req.backend.cn.charset.name; + const mediaTypeCandidate = negotiator.mediaType(availableMediaTypes.map((l) => l.name)) ?? req.backend.cn.mediaType.name; + + // TODO refactor + const currentLanguage = availableLanguages.find((l) => l.name === languageCandidate); + if (typeof currentLanguage === 'undefined') { + const data = req.backend?.cn.language.bodies.languageNotAcceptable(); + const responseRaw = req.backend?.cn.mediaType.serialize(data); + const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; + res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { + 'Content-Language': req.backend?.cn.language.name, + 'Content-Type': [ + req.backend?.cn.mediaType.name, + `charset="${req.backend?.cn.charset.name}"` + ].join('; '), + }); + res.statusMessage = req.backend?.cn.language.statusMessages.languageNotAcceptable() ?? ''; + res.end(response); + return; + } - rawUrl = ''; + const currentMediaType = availableMediaTypes.find((l) => l.name === mediaTypeCandidate); + if (typeof currentMediaType === 'undefined') { + const data = req.backend?.cn.language.bodies.mediaTypeNotAcceptable(); + const responseRaw = req.backend?.cn.mediaType.serialize(data); + const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; + res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { + 'Content-Language': req.backend?.cn.language.name, + 'Content-Type': [ + req.backend?.cn.mediaType.name, + `charset="${req.backend?.cn.charset.name}"` + ].join('; '), + }); + res.statusMessage = req.backend?.cn.language.statusMessages.mediaTypeNotAcceptable() ?? ''; + res.end(response); + return; + } - readonly cn: { - language: Language; - mediaType: MediaType; - charset: Charset; - } = { - language: en, - mediaType: applicationJson, - charset: utf8, - }; + const responseBodyCharset = availableCharsets.find((l) => l.name === charsetCandidate); + if (typeof responseBodyCharset === 'undefined') { + const data = req.backend?.cn.language.bodies.encodingNotAcceptable(); + const responseRaw = req.backend?.cn.mediaType.serialize(data); + const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; + res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { + 'Content-Language': req.backend?.cn.language.name, + 'Content-Type': [ + req.backend?.cn.mediaType.name, + `charset="${req.backend?.cn.charset.name}"` + ].join('; '), + }); + res.statusMessage = req.backend?.cn.language.statusMessages.encodingNotAcceptable() ?? ''; + res.end(response); + return; } - class ServerYasumiResponse extends http.ServerResponse { + req.cn.language = currentLanguage; + req.cn.mediaType = currentMediaType; + req.cn.charset = responseBodyCharset; +}; - } +export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { + const isHttps = 'key' in serverParams && 'cert' in serverParams; const server = isHttps ? https.createServer({ @@ -174,83 +284,17 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr cert: serverParams.cert, requestTimeout: serverParams.requestTimeout, IncomingMessage: ServerYasumiRequest, - ServerResponse: ServerYasumiResponse, }) : http.createServer({ requestTimeout: serverParams.requestTimeout, IncomingMessage: ServerYasumiRequest, - ServerResponse: ServerYasumiResponse, }); - const adjustRequestForContentNegotiation = (req: RequestContext, res: ResponseContext) => { - - const negotiator = new Negotiator(req); - const availableLanguages = Array.from(req.backend.app.languages); - const availableCharsets = Array.from(req.backend.app.charsets); - const availableMediaTypes = Array.from(req.backend.app.mediaTypes); - - const languageCandidate = negotiator.language(availableLanguages.map((l) => l.name)) ?? backendState.cn.language.name; - const charsetCandidate = negotiator.charset(availableCharsets.map((l) => l.name)) ?? backendState.cn.charset.name; - const mediaTypeCandidate = negotiator.mediaType(availableMediaTypes.map((l) => l.name)) ?? backendState.cn.mediaType.name; - - // TODO refactor - const currentLanguage = availableLanguages.find((l) => l.name === languageCandidate); - if (typeof currentLanguage === 'undefined') { - const data = req.backend?.cn.language.bodies.languageNotAcceptable(); - const responseRaw = req.backend?.cn.mediaType.serialize(data); - const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; - res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { - 'Content-Language': req.backend?.cn.language.name, - 'Content-Type': [ - req.backend?.cn.mediaType.name, - `charset="${req.backend?.cn.charset.name}"` - ].join('; '), - }); - res.statusMessage = req.backend?.cn.language.statusMessages.languageNotAcceptable() ?? ''; - res.end(response); - return; - } - - const currentMediaType = availableMediaTypes.find((l) => l.name === mediaTypeCandidate); - if (typeof currentMediaType === 'undefined') { - const data = req.backend?.cn.language.bodies.mediaTypeNotAcceptable(); - const responseRaw = req.backend?.cn.mediaType.serialize(data); - const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; - res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { - 'Content-Language': req.backend?.cn.language.name, - 'Content-Type': [ - req.backend?.cn.mediaType.name, - `charset="${req.backend?.cn.charset.name}"` - ].join('; '), - }); - res.statusMessage = req.backend?.cn.language.statusMessages.mediaTypeNotAcceptable() ?? ''; - res.end(response); - return; - } - - const responseBodyCharset = availableCharsets.find((l) => l.name === charsetCandidate); - if (typeof responseBodyCharset === 'undefined') { - const data = req.backend?.cn.language.bodies.encodingNotAcceptable(); - const responseRaw = req.backend?.cn.mediaType.serialize(data); - const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; - res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { - 'Content-Language': req.backend?.cn.language.name, - 'Content-Type': [ - req.backend?.cn.mediaType.name, - `charset="${req.backend?.cn.charset.name}"` - ].join('; '), - }); - res.statusMessage = req.backend?.cn.language.statusMessages.encodingNotAcceptable() ?? ''; - res.end(response); - return; - } - - req.cn.language = currentLanguage; - req.cn.mediaType = currentMediaType; - req.cn.charset = responseBodyCharset; - }; - server.on('request', async (req: RequestContext, res) => { + req.backend = backendState; + req.basePath = serverParams.basePath ?? ''; + req.host = serverParams.host ?? 'localhost'; + req.scheme = isHttps ? 'https' : 'http'; adjustRequestForContentNegotiation(req, res); try { @@ -426,12 +470,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr 'Content-Language': req.cn.language.name }; - if (middlewareState instanceof StreamResponse) { - res.writeHead(constants.HTTP_STATUS_ACCEPTED, headers); - middlewareState.stream.pipe(res); - middlewareState.stream.on('end', () => { - res.end(); - }); + if (middlewareState instanceof http.ServerResponse) { + // TODO streaming responses + middlewareState.writeHead(constants.HTTP_STATUS_ACCEPTED, headers); return; } diff --git a/src/backend/utils.ts b/src/backend/utils.ts index 70b8ab5..c8036a2 100644 --- a/src/backend/utils.ts +++ b/src/backend/utils.ts @@ -3,26 +3,26 @@ import {MediaType, Charset} from '../common'; import {BaseSchema, parseAsync} from 'valibot'; export const getBody = ( - req: IncomingMessage, - schema: BaseSchema, - encodingPair?: Charset, - deserializer?: MediaType, + req: IncomingMessage, + schema: BaseSchema, + encodingPair?: Charset, + deserializer?: MediaType, ) => new Promise((resolve, reject) => { - let body = Buffer.from(''); - req.on('data', (chunk) => { - body = Buffer.concat([body, chunk]); - }); - req.on('end', async () => { - const bodyStr = encodingPair?.decode(body) ?? body.toString(); - try { - const bodyDeserialized = await parseAsync( - schema, - deserializer?.deserialize(bodyStr) ?? body, - {abortEarly: false}, - ); - resolve(bodyDeserialized); - } catch (err) { - reject(err); - } - }); + let body = Buffer.from(''); + req.on('data', (chunk) => { + body = Buffer.concat([body, chunk]); + }); + req.on('end', async () => { + const bodyStr = encodingPair?.decode(body) ?? body.toString(); + try { + const bodyDeserialized = await parseAsync( + schema, + deserializer?.deserialize(bodyStr) ?? body, + {abortEarly: false}, + ); + resolve(bodyDeserialized); + } catch (err) { + reject(err); + } + }); });