@cfworker/web 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +91 -0
- package/dist/accepts.d.ts +12 -0
- package/dist/accepts.js +77 -0
- package/dist/application.d.ts +8 -0
- package/dist/application.js +39 -0
- package/dist/context.d.ts +14 -0
- package/dist/context.js +26 -0
- package/dist/cookies.d.ts +8 -0
- package/dist/cookies.js +17 -0
- package/dist/http-error.d.ts +7 -0
- package/dist/http-error.js +31 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/middleware.d.ts +4 -0
- package/dist/middleware.js +2 -0
- package/dist/normalize-pathname.d.ts +3 -0
- package/dist/normalize-pathname.js +17 -0
- package/dist/req.d.ts +24 -0
- package/dist/req.js +54 -0
- package/dist/response-builder.d.ts +22 -0
- package/dist/response-builder.js +133 -0
- package/dist/router.d.ts +40 -0
- package/dist/router.js +99 -0
- package/dist/to-object.d.ts +3 -0
- package/dist/to-object.js +7 -0
- package/dist/validate.d.ts +21 -0
- package/dist/validate.js +74 -0
- package/package.json +64 -0
- package/src/accepts.ts +87 -0
- package/src/application.ts +48 -0
- package/src/context.ts +28 -0
- package/src/cookies.ts +30 -0
- package/src/http-error.ts +36 -0
- package/src/index.ts +13 -0
- package/src/middleware.ts +6 -0
- package/src/normalize-pathname.ts +22 -0
- package/src/req.ts +59 -0
- package/src/response-builder.ts +161 -0
- package/src/router.ts +141 -0
- package/src/to-object.ts +15 -0
- package/src/validate.ts +122 -0
- package/tsconfig.json +12 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Key,
|
|
3
|
+
ParseOptions,
|
|
4
|
+
pathToRegexp,
|
|
5
|
+
TokensToRegexpOptions
|
|
6
|
+
} from 'path-to-regexp';
|
|
7
|
+
import { Context } from './context.js';
|
|
8
|
+
import { composeMiddleware, Middleware } from './middleware.js';
|
|
9
|
+
|
|
10
|
+
export type PathToRegExpOptions = TokensToRegexpOptions & ParseOptions;
|
|
11
|
+
|
|
12
|
+
export const Method = (method: string) => {
|
|
13
|
+
method = method.toUpperCase();
|
|
14
|
+
return ({ req }: Context) => req.method === method;
|
|
15
|
+
};
|
|
16
|
+
export const Get = Method('get');
|
|
17
|
+
export const Post = Method('post');
|
|
18
|
+
export const Put = Method('put');
|
|
19
|
+
export const Patch = Method('patch');
|
|
20
|
+
export const Delete = Method('delete');
|
|
21
|
+
export const Head = Method('head');
|
|
22
|
+
export const Options = Method('options');
|
|
23
|
+
|
|
24
|
+
export const Header = (header: string, value: string) => {
|
|
25
|
+
value = value.toLowerCase();
|
|
26
|
+
return ({ req }: Context) => req.headers.get(header) === value;
|
|
27
|
+
};
|
|
28
|
+
export const Host = (host: string) => Header('host', host);
|
|
29
|
+
export const Referer = (host: string) => Header('referer', host);
|
|
30
|
+
|
|
31
|
+
export const Path = (pattern: string, options?: PathToRegExpOptions) => {
|
|
32
|
+
const keys: Key[] = [];
|
|
33
|
+
const regExp = pathToRegexp(pattern, keys, options);
|
|
34
|
+
|
|
35
|
+
return ({ req: { url, params } }: Context) => {
|
|
36
|
+
const match = url.pathname.match(regExp);
|
|
37
|
+
if (!match) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
collectParameters(keys, match, params);
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type RouteCondition = (context: Context) => boolean;
|
|
46
|
+
|
|
47
|
+
export interface Route {
|
|
48
|
+
conditions: RouteCondition[];
|
|
49
|
+
middleware: Middleware;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RouterOptions {
|
|
53
|
+
pathToRegExpOptions?: PathToRegExpOptions;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const defaultRouterOptions: RouterOptions = {
|
|
57
|
+
pathToRegExpOptions: { strict: true }
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export class Router {
|
|
61
|
+
private readonly routes: Route[];
|
|
62
|
+
|
|
63
|
+
constructor(private readonly options: RouterOptions = defaultRouterOptions) {
|
|
64
|
+
this.routes = [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public get(pathname: string, ...middleware: Middleware[]) {
|
|
68
|
+
const opts = this.options.pathToRegExpOptions;
|
|
69
|
+
return this.compose([Get, Path(pathname, opts)], ...middleware);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public post(pathname: string, ...middleware: Middleware[]) {
|
|
73
|
+
const opts = this.options.pathToRegExpOptions;
|
|
74
|
+
return this.compose([Post, Path(pathname, opts)], ...middleware);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public put(pathname: string, ...middleware: Middleware[]) {
|
|
78
|
+
const opts = this.options.pathToRegExpOptions;
|
|
79
|
+
return this.compose([Put, Path(pathname, opts)], ...middleware);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public patch(pathname: string, ...middleware: Middleware[]) {
|
|
83
|
+
const opts = this.options.pathToRegExpOptions;
|
|
84
|
+
return this.compose([Patch, Path(pathname, opts)], ...middleware);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public delete(pathname: string, ...middleware: Middleware[]) {
|
|
88
|
+
const opts = this.options.pathToRegExpOptions;
|
|
89
|
+
return this.compose([Delete, Path(pathname, opts)], ...middleware);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public head(pathname: string, ...middleware: Middleware[]) {
|
|
93
|
+
const opts = this.options.pathToRegExpOptions;
|
|
94
|
+
return this.compose([Head, Path(pathname, opts)], ...middleware);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public all(pathname: string, ...middleware: Middleware[]) {
|
|
98
|
+
const opts = this.options.pathToRegExpOptions;
|
|
99
|
+
return this.compose([Path(pathname, opts)], ...middleware);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public middleware: Middleware = async (ctx, next) => {
|
|
103
|
+
const resolved = this.resolve(ctx);
|
|
104
|
+
|
|
105
|
+
if (resolved) {
|
|
106
|
+
await resolved.middleware(ctx, next);
|
|
107
|
+
} else {
|
|
108
|
+
await next();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
public compose(conditions: RouteCondition[], ...middleware: Middleware[]) {
|
|
113
|
+
this.routes.push({
|
|
114
|
+
conditions,
|
|
115
|
+
middleware: composeMiddleware(middleware)
|
|
116
|
+
});
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private resolve(ctx: Context) {
|
|
121
|
+
return this.routes.find(
|
|
122
|
+
({ conditions }) =>
|
|
123
|
+
conditions.length === 0 || conditions.every(c => c(ctx))
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectParameters(
|
|
129
|
+
keys: Key[],
|
|
130
|
+
match: RegExpMatchArray,
|
|
131
|
+
params: Record<string, string>
|
|
132
|
+
) {
|
|
133
|
+
for (let i = 1; i < match.length; i++) {
|
|
134
|
+
const name = keys[i - 1].name;
|
|
135
|
+
const value = match[i];
|
|
136
|
+
if (!value) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
params[name] = decodeURIComponent(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/to-object.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert an iterable to an object.
|
|
3
|
+
*/
|
|
4
|
+
export function toObject<
|
|
5
|
+
TKey extends string | number | symbol,
|
|
6
|
+
TValue
|
|
7
|
+
>(iterable: {
|
|
8
|
+
[Symbol.iterator](): IterableIterator<[TKey, TValue]>;
|
|
9
|
+
}): Record<TKey, TValue> {
|
|
10
|
+
const obj = Object.create(null) as Record<TKey, TValue>;
|
|
11
|
+
for (const [key, value] of iterable) {
|
|
12
|
+
obj[key] = value;
|
|
13
|
+
}
|
|
14
|
+
return obj;
|
|
15
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
dereference,
|
|
3
|
+
OutputUnit,
|
|
4
|
+
Schema,
|
|
5
|
+
validate as schemaValidate
|
|
6
|
+
} from '@cfworker/json-schema';
|
|
7
|
+
import { HttpError } from './http-error.js';
|
|
8
|
+
import { Middleware } from './middleware.js';
|
|
9
|
+
import { toObject } from './to-object.js';
|
|
10
|
+
|
|
11
|
+
export interface RequestSchemas {
|
|
12
|
+
body?: Schema;
|
|
13
|
+
headers?: Schema;
|
|
14
|
+
params?: Schema;
|
|
15
|
+
search?: Schema;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type RequestPart = keyof RequestSchemas;
|
|
19
|
+
|
|
20
|
+
export type RequestParser = (data: URLSearchParams | FormData) => any;
|
|
21
|
+
|
|
22
|
+
const draft = '2019-09';
|
|
23
|
+
|
|
24
|
+
const hasBody: Record<string, true> = {
|
|
25
|
+
POST: true,
|
|
26
|
+
PUT: true,
|
|
27
|
+
DELETE: true,
|
|
28
|
+
PATCH: true
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function middlewareFactory(
|
|
32
|
+
schemas: RequestSchemas,
|
|
33
|
+
parser: RequestParser = toObject,
|
|
34
|
+
lookup = Object.create(null)
|
|
35
|
+
): Middleware {
|
|
36
|
+
const {
|
|
37
|
+
headers: $headers,
|
|
38
|
+
search: $search,
|
|
39
|
+
params: $params,
|
|
40
|
+
body: $body
|
|
41
|
+
} = schemas;
|
|
42
|
+
|
|
43
|
+
if ($headers) {
|
|
44
|
+
dereference($headers, { ...lookup });
|
|
45
|
+
}
|
|
46
|
+
if ($search) {
|
|
47
|
+
dereference($search, { ...lookup });
|
|
48
|
+
}
|
|
49
|
+
if ($params) {
|
|
50
|
+
dereference($params, { ...lookup });
|
|
51
|
+
}
|
|
52
|
+
if ($body) {
|
|
53
|
+
dereference($body, { ...lookup });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return async ({ req }, next) => {
|
|
57
|
+
if ($params) {
|
|
58
|
+
validateRequestPart('params', req.params, $params, lookup);
|
|
59
|
+
}
|
|
60
|
+
if ($headers) {
|
|
61
|
+
const headers = toObject(req.headers);
|
|
62
|
+
validateRequestPart('headers', headers, $headers, lookup);
|
|
63
|
+
}
|
|
64
|
+
if ($search) {
|
|
65
|
+
const search = parser(req.url.searchParams);
|
|
66
|
+
validateRequestPart('search', search, $search, lookup);
|
|
67
|
+
}
|
|
68
|
+
if ($body && hasBody[req.method]) {
|
|
69
|
+
let body: any;
|
|
70
|
+
const contentType = req.headers.get('content-type') || '';
|
|
71
|
+
if (
|
|
72
|
+
contentType === 'application/x-www-form-urlencoded' ||
|
|
73
|
+
contentType.startsWith('application/x-www-form-urlencoded;') ||
|
|
74
|
+
contentType.startsWith('multipart/form-data;')
|
|
75
|
+
) {
|
|
76
|
+
const form = await req.body.formData();
|
|
77
|
+
body = parser(form);
|
|
78
|
+
} else {
|
|
79
|
+
body = await req.body.json();
|
|
80
|
+
}
|
|
81
|
+
validateRequestPart('body', body, $body, lookup);
|
|
82
|
+
}
|
|
83
|
+
await next();
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const messages: Record<RequestPart, string> = {
|
|
88
|
+
params: 'Parameters in request path are invalid.',
|
|
89
|
+
headers: 'Headers are invalid.',
|
|
90
|
+
search: 'Query string parameters are invalid.',
|
|
91
|
+
body: 'Body is invalid.'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export interface RequestValidationErrorBody {
|
|
95
|
+
status: 400;
|
|
96
|
+
part: RequestPart;
|
|
97
|
+
message: string;
|
|
98
|
+
errors: OutputUnit[];
|
|
99
|
+
schema: Schema;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateRequestPart(
|
|
103
|
+
part: RequestPart,
|
|
104
|
+
instance: any,
|
|
105
|
+
schema: Schema,
|
|
106
|
+
lookup: Record<string, boolean | Schema>
|
|
107
|
+
) {
|
|
108
|
+
const result = schemaValidate(instance, schema, draft, lookup);
|
|
109
|
+
if (result.valid) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const errorBody: RequestValidationErrorBody = {
|
|
113
|
+
status: 400,
|
|
114
|
+
part,
|
|
115
|
+
message: messages[part],
|
|
116
|
+
errors: result.errors,
|
|
117
|
+
schema
|
|
118
|
+
};
|
|
119
|
+
throw new HttpError(400, errorBody);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const validate = middlewareFactory;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig-base",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "esnext",
|
|
5
|
+
"lib": ["esnext", "webworker"],
|
|
6
|
+
"types": ["@cfworker/worker-types", "@cloudflare/workers-types"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "./src"
|
|
9
|
+
},
|
|
10
|
+
"include": ["src"],
|
|
11
|
+
"references": [{ "path": "../json-schema" }]
|
|
12
|
+
}
|