Browse Source

Update types

Remove inflection dependency.
master
TheoryOfNekomata 7 months ago
parent
commit
51e6874c37
10 changed files with 251 additions and 310 deletions
  1. +14
    -15
      examples/basic/server.ts
  2. +3
    -4
      package.json
  3. +51
    -56
      pnpm-lock.yaml
  4. +0
    -5
      src/backend/common.ts
  5. +1
    -88
      src/backend/core.ts
  6. +1
    -1
      src/backend/extenders/method.ts
  7. +1
    -1
      src/backend/extenders/url.ts
  8. +1
    -2
      src/backend/handlers.ts
  9. +158
    -117
      src/backend/server.ts
  10. +21
    -21
      src/backend/utils.ts

+ 14
- 15
examples/basic/server.ts View File

@@ -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);

+ 3
- 4
package.json View File

@@ -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"


+ 51
- 56
pnpm-lock.yaml View File

@@ -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


+ 0
- 5
src/backend/common.ts View File

@@ -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
- 88
src/backend/core.ts View File

@@ -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
- 1
src/backend/extenders/method.ts View File

@@ -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
- 1
src/backend/extenders/url.ts View File

@@ -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
- 2
src/backend/handlers.ts View File

@@ -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;


+ 158
- 117
src/backend/server.ts View File

@@ -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;
}



+ 21
- 21
src/backend/utils.ts View File

@@ -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);
}
});
});

Loading…
Cancel
Save