@@ -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); |
@@ -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" | |||
@@ -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 | |||
@@ -9,11 +9,6 @@ export interface BackendState { | |||
charset: Charset; | |||
mediaType: MediaType; | |||
} | |||
errorHeaders: { | |||
language?: string; | |||
charset?: string; | |||
serializer?: string; | |||
} | |||
showTotalItemCountOnGetCollection: boolean; | |||
throws404OnDeletingNotFound: boolean; | |||
checksSerializersOnDelete: boolean; | |||
@@ -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<T extends BaseDataSource = BaseDataSource> { | |||
showTotalItemCountOnGetCollection(b?: boolean): this; | |||
showTotalItemCountOnCreateItem(b?: boolean): this; | |||
@@ -33,84 +31,6 @@ export interface BackendBuilder<T extends BaseDataSource = BaseDataSource> { | |||
dataSource?: (resource: Resource) => T; | |||
} | |||
export class MiddlewareError extends Error {} | |||
interface ResponseParams { | |||
statusCode: Response['statusCode']; | |||
statusMessage?: Response['statusMessage']; | |||
headers?: Response['headers']; | |||
} | |||
interface PlainResponseParams<T = unknown> extends ResponseParams { | |||
body?: T; | |||
} | |||
interface StreamResponseParams extends ResponseParams { | |||
stream: NodeJS.ReadableStream; | |||
} | |||
interface HttpMiddlewareErrorParams<T = unknown> extends Omit<PlainResponseParams<T>, 'statusMessage'> { | |||
cause?: unknown | |||
} | |||
export interface Response { | |||
statusCode: number; | |||
statusMessage?: keyof LanguageStatusMessageMap; | |||
headers?: Record<string, string>; | |||
} | |||
export class PlainResponse<T = unknown> implements Response { | |||
readonly statusCode: Response['statusCode']; | |||
readonly statusMessage?: keyof LanguageStatusMessageMap; | |||
readonly headers: Response['headers']; | |||
readonly body?: T; | |||
constructor(args: PlainResponseParams<T>) { | |||
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<T extends http.IncomingMessage> extends http.ServerResponse<T> {} | |||
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, | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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<string, string>; | |||
} | |||
interface ResponseParams { | |||
statusCode: Response['statusCode']; | |||
statusMessage?: Response['statusMessage']; | |||
headers?: Response['headers']; | |||
} | |||
export class MiddlewareError extends Error {} | |||
interface PlainResponseParams<T = unknown> extends ResponseParams { | |||
body?: T; | |||
} | |||
interface HttpMiddlewareErrorParams<T = unknown> extends Omit<PlainResponseParams<T>, 'statusMessage'> { | |||
cause?: unknown | |||
} | |||
export class PlainResponse<T = unknown> implements Response { | |||
readonly statusCode: Response['statusCode']; | |||
readonly statusMessage?: keyof LanguageStatusMessageMap; | |||
readonly headers: Response['headers']; | |||
readonly body?: T; | |||
constructor(args: PlainResponseParams<T>) { | |||
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 extends RequestContext = RequestContext> { | |||
(req: Req): undefined | Response | Promise<undefined | Response>; | |||
} | |||
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 = <T extends v.BaseSchema>(resource: Resource<T>, mainResourceId: string) => { | |||
const middlewares = [] as [string, Middleware, v.BaseSchema?][]; | |||
if (mainResourceId === '') { | |||
@@ -127,46 +208,75 @@ const getAllowedMiddlewares = <T extends v.BaseSchema>(resource: Resource<T>, 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<RequestContext>) => { | |||
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<T extends http.IncomingMessage> extends http.ServerResponse<T> { | |||
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<RequestContext>) => { | |||
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; | |||
} | |||
@@ -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); | |||
} | |||
}); | |||
}); |