@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/dist/req.js ADDED
@@ -0,0 +1,54 @@
1
+ import { safeParse } from 'secure-json-parse';
2
+ import { Accepts } from './accepts.js';
3
+ export class Req {
4
+ raw;
5
+ method;
6
+ url;
7
+ headers;
8
+ params;
9
+ accepts;
10
+ body;
11
+ constructor(request) {
12
+ this.raw = request;
13
+ this.method = request.method;
14
+ this.url = new URL(request.url);
15
+ this.headers = request.headers;
16
+ this.params = Object.create(null);
17
+ this.accepts = new Accepts(request.headers);
18
+ this.body = new ReqBody(request);
19
+ }
20
+ }
21
+ export class ReqBody {
22
+ request;
23
+ _arrayBuffer;
24
+ _formData;
25
+ _json;
26
+ _text;
27
+ constructor(request) {
28
+ this.request = request;
29
+ }
30
+ arrayBuffer() {
31
+ if (!this._arrayBuffer) {
32
+ this._arrayBuffer = this.request.arrayBuffer();
33
+ }
34
+ return this._arrayBuffer;
35
+ }
36
+ formData() {
37
+ if (!this._formData) {
38
+ this._formData = this.request.formData();
39
+ }
40
+ return this._formData;
41
+ }
42
+ json(reviver) {
43
+ if (!this._json) {
44
+ this._json = this.text().then(text => safeParse(text, reviver));
45
+ }
46
+ return this._json;
47
+ }
48
+ text() {
49
+ if (!this._text) {
50
+ this._text = this.request.text();
51
+ }
52
+ return this._text;
53
+ }
54
+ }
@@ -0,0 +1,22 @@
1
+ export declare type ExtendedBodyInit = BodyInit | boolean | Date | number | object | null;
2
+ export declare class ResponseBuilder {
3
+ readonly headers: Headers;
4
+ private _status;
5
+ private _explicitStatus;
6
+ private _implicitType;
7
+ private _body;
8
+ private _stringifyBody;
9
+ get status(): number;
10
+ set status(value: number);
11
+ get statusText(): string;
12
+ get body(): ExtendedBodyInit | null;
13
+ set body(value: ExtendedBodyInit | null);
14
+ redirect(url: string | URL): void;
15
+ get type(): string;
16
+ set type(value: string);
17
+ get lastModified(): string | Date | null;
18
+ set lastModified(value: string | Date | null);
19
+ get etag(): string | null;
20
+ set etag(value: string | null);
21
+ create(): Response;
22
+ }
@@ -0,0 +1,133 @@
1
+ import statuses from 'statuses';
2
+ export class ResponseBuilder {
3
+ headers = new Headers();
4
+ _status = 404;
5
+ _explicitStatus = false;
6
+ _implicitType = false;
7
+ _body = null;
8
+ _stringifyBody = false;
9
+ get status() {
10
+ return this._status;
11
+ }
12
+ set status(value) {
13
+ this._explicitStatus = true;
14
+ this._status = value;
15
+ if (this.body && statuses.empty[value]) {
16
+ this.body = null;
17
+ }
18
+ }
19
+ get statusText() {
20
+ return statuses[this._status];
21
+ }
22
+ get body() {
23
+ return this._body;
24
+ }
25
+ set body(value) {
26
+ this._body = value;
27
+ if (value === null) {
28
+ if (!statuses.empty[this.status]) {
29
+ this._status = 204;
30
+ }
31
+ this.headers.delete('content-type');
32
+ this.headers.delete('content-length');
33
+ this.headers.delete('transfer-encoding');
34
+ return;
35
+ }
36
+ if (!this._explicitStatus) {
37
+ this._status = 200;
38
+ }
39
+ if (value instanceof Blob ||
40
+ value instanceof FormData ||
41
+ value instanceof URLSearchParams ||
42
+ ArrayBuffer.isView(value) ||
43
+ value instanceof ArrayBuffer ||
44
+ value instanceof ReadableStream) {
45
+ this._stringifyBody = false;
46
+ if (this._implicitType) {
47
+ this._implicitType = false;
48
+ this.headers.delete('content-type');
49
+ }
50
+ }
51
+ else if (typeof value === 'string') {
52
+ this._stringifyBody = false;
53
+ if (!this.headers.has('content-type') || this._implicitType) {
54
+ this._implicitType = true;
55
+ if (/^\s*</.test(value)) {
56
+ this.headers.set('content-type', 'text/html;charset=UTF-8');
57
+ }
58
+ else {
59
+ this.headers.set('content-type', 'text/plain;charset=UTF-8');
60
+ }
61
+ }
62
+ }
63
+ else {
64
+ this._stringifyBody = true;
65
+ this._implicitType = true;
66
+ this.headers.set('content-type', 'application/json;charset=UTF-8');
67
+ }
68
+ }
69
+ redirect(url) {
70
+ if (url instanceof URL) {
71
+ url = url.href;
72
+ }
73
+ this.headers.set('location', url);
74
+ if (!statuses.redirect[this.status]) {
75
+ this.status = 302;
76
+ }
77
+ this.type = 'text/plain;charset=UTF-8';
78
+ this.body = `Redirecting to ${url}.`;
79
+ }
80
+ get type() {
81
+ const type = this.headers.get('content-type');
82
+ if (!type) {
83
+ return '';
84
+ }
85
+ return type.split(';', 1)[0];
86
+ }
87
+ set type(value) {
88
+ this._implicitType = false;
89
+ if (value) {
90
+ this.headers.set('content-type', value);
91
+ }
92
+ else {
93
+ this.headers.delete('content-type');
94
+ }
95
+ }
96
+ get lastModified() {
97
+ const date = this.headers.get('last-modified');
98
+ return date ? new Date(date) : null;
99
+ }
100
+ set lastModified(value) {
101
+ if (value === null) {
102
+ this.headers.delete('last-modified');
103
+ return;
104
+ }
105
+ if (typeof value === 'string') {
106
+ value = new Date(value);
107
+ }
108
+ this.headers.set('last-modified', value.toUTCString());
109
+ }
110
+ get etag() {
111
+ return this.headers.get('etag');
112
+ }
113
+ set etag(value) {
114
+ if (value) {
115
+ if (!/^(W\/)?"/.test(value)) {
116
+ value = `"${value}"`;
117
+ }
118
+ this.headers.set('etag', value);
119
+ }
120
+ else {
121
+ this.headers.delete('etag');
122
+ }
123
+ }
124
+ create() {
125
+ const { body: rawBody, status, statusText, headers } = this;
126
+ const body = this._stringifyBody
127
+ ? JSON.stringify(rawBody)
128
+ : rawBody;
129
+ return new Response(body, { status, statusText, headers });
130
+ }
131
+ }
132
+ const Blob = self.Blob || class {
133
+ };
@@ -0,0 +1,40 @@
1
+ import { ParseOptions, TokensToRegexpOptions } from 'path-to-regexp';
2
+ import { Context } from './context.js';
3
+ import { Middleware } from './middleware.js';
4
+ export declare type PathToRegExpOptions = TokensToRegexpOptions & ParseOptions;
5
+ export declare const Method: (method: string) => ({ req }: Context) => boolean;
6
+ export declare const Get: ({ req }: Context) => boolean;
7
+ export declare const Post: ({ req }: Context) => boolean;
8
+ export declare const Put: ({ req }: Context) => boolean;
9
+ export declare const Patch: ({ req }: Context) => boolean;
10
+ export declare const Delete: ({ req }: Context) => boolean;
11
+ export declare const Head: ({ req }: Context) => boolean;
12
+ export declare const Options: ({ req }: Context) => boolean;
13
+ export declare const Header: (header: string, value: string) => ({ req }: Context) => boolean;
14
+ export declare const Host: (host: string) => ({ req }: Context) => boolean;
15
+ export declare const Referer: (host: string) => ({ req }: Context) => boolean;
16
+ export declare const Path: (pattern: string, options?: PathToRegExpOptions | undefined) => ({ req: { url, params } }: Context) => boolean;
17
+ export declare type RouteCondition = (context: Context) => boolean;
18
+ export interface Route {
19
+ conditions: RouteCondition[];
20
+ middleware: Middleware;
21
+ }
22
+ export interface RouterOptions {
23
+ pathToRegExpOptions?: PathToRegExpOptions;
24
+ }
25
+ export declare const defaultRouterOptions: RouterOptions;
26
+ export declare class Router {
27
+ private readonly options;
28
+ private readonly routes;
29
+ constructor(options?: RouterOptions);
30
+ get(pathname: string, ...middleware: Middleware[]): this;
31
+ post(pathname: string, ...middleware: Middleware[]): this;
32
+ put(pathname: string, ...middleware: Middleware[]): this;
33
+ patch(pathname: string, ...middleware: Middleware[]): this;
34
+ delete(pathname: string, ...middleware: Middleware[]): this;
35
+ head(pathname: string, ...middleware: Middleware[]): this;
36
+ all(pathname: string, ...middleware: Middleware[]): this;
37
+ middleware: Middleware;
38
+ compose(conditions: RouteCondition[], ...middleware: Middleware[]): this;
39
+ private resolve;
40
+ }
package/dist/router.js ADDED
@@ -0,0 +1,99 @@
1
+ import { pathToRegexp } from 'path-to-regexp';
2
+ import { composeMiddleware } from './middleware.js';
3
+ export const Method = (method) => {
4
+ method = method.toUpperCase();
5
+ return ({ req }) => req.method === method;
6
+ };
7
+ export const Get = Method('get');
8
+ export const Post = Method('post');
9
+ export const Put = Method('put');
10
+ export const Patch = Method('patch');
11
+ export const Delete = Method('delete');
12
+ export const Head = Method('head');
13
+ export const Options = Method('options');
14
+ export const Header = (header, value) => {
15
+ value = value.toLowerCase();
16
+ return ({ req }) => req.headers.get(header) === value;
17
+ };
18
+ export const Host = (host) => Header('host', host);
19
+ export const Referer = (host) => Header('referer', host);
20
+ export const Path = (pattern, options) => {
21
+ const keys = [];
22
+ const regExp = pathToRegexp(pattern, keys, options);
23
+ return ({ req: { url, params } }) => {
24
+ const match = url.pathname.match(regExp);
25
+ if (!match) {
26
+ return false;
27
+ }
28
+ collectParameters(keys, match, params);
29
+ return true;
30
+ };
31
+ };
32
+ export const defaultRouterOptions = {
33
+ pathToRegExpOptions: { strict: true }
34
+ };
35
+ export class Router {
36
+ options;
37
+ routes;
38
+ constructor(options = defaultRouterOptions) {
39
+ this.options = options;
40
+ this.routes = [];
41
+ }
42
+ get(pathname, ...middleware) {
43
+ const opts = this.options.pathToRegExpOptions;
44
+ return this.compose([Get, Path(pathname, opts)], ...middleware);
45
+ }
46
+ post(pathname, ...middleware) {
47
+ const opts = this.options.pathToRegExpOptions;
48
+ return this.compose([Post, Path(pathname, opts)], ...middleware);
49
+ }
50
+ put(pathname, ...middleware) {
51
+ const opts = this.options.pathToRegExpOptions;
52
+ return this.compose([Put, Path(pathname, opts)], ...middleware);
53
+ }
54
+ patch(pathname, ...middleware) {
55
+ const opts = this.options.pathToRegExpOptions;
56
+ return this.compose([Patch, Path(pathname, opts)], ...middleware);
57
+ }
58
+ delete(pathname, ...middleware) {
59
+ const opts = this.options.pathToRegExpOptions;
60
+ return this.compose([Delete, Path(pathname, opts)], ...middleware);
61
+ }
62
+ head(pathname, ...middleware) {
63
+ const opts = this.options.pathToRegExpOptions;
64
+ return this.compose([Head, Path(pathname, opts)], ...middleware);
65
+ }
66
+ all(pathname, ...middleware) {
67
+ const opts = this.options.pathToRegExpOptions;
68
+ return this.compose([Path(pathname, opts)], ...middleware);
69
+ }
70
+ middleware = async (ctx, next) => {
71
+ const resolved = this.resolve(ctx);
72
+ if (resolved) {
73
+ await resolved.middleware(ctx, next);
74
+ }
75
+ else {
76
+ await next();
77
+ }
78
+ };
79
+ compose(conditions, ...middleware) {
80
+ this.routes.push({
81
+ conditions,
82
+ middleware: composeMiddleware(middleware)
83
+ });
84
+ return this;
85
+ }
86
+ resolve(ctx) {
87
+ return this.routes.find(({ conditions }) => conditions.length === 0 || conditions.every(c => c(ctx)));
88
+ }
89
+ }
90
+ function collectParameters(keys, match, params) {
91
+ for (let i = 1; i < match.length; i++) {
92
+ const name = keys[i - 1].name;
93
+ const value = match[i];
94
+ if (!value) {
95
+ continue;
96
+ }
97
+ params[name] = decodeURIComponent(value);
98
+ }
99
+ }
@@ -0,0 +1,3 @@
1
+ export declare function toObject<TKey extends string | number | symbol, TValue>(iterable: {
2
+ [Symbol.iterator](): IterableIterator<[TKey, TValue]>;
3
+ }): Record<TKey, TValue>;
@@ -0,0 +1,7 @@
1
+ export function toObject(iterable) {
2
+ const obj = Object.create(null);
3
+ for (const [key, value] of iterable) {
4
+ obj[key] = value;
5
+ }
6
+ return obj;
7
+ }
@@ -0,0 +1,21 @@
1
+ import { OutputUnit, Schema } from '@cfworker/json-schema';
2
+ import { Middleware } from './middleware.js';
3
+ export interface RequestSchemas {
4
+ body?: Schema;
5
+ headers?: Schema;
6
+ params?: Schema;
7
+ search?: Schema;
8
+ }
9
+ export declare type RequestPart = keyof RequestSchemas;
10
+ export declare type RequestParser = (data: URLSearchParams | FormData) => any;
11
+ declare function middlewareFactory(schemas: RequestSchemas, parser?: RequestParser, lookup?: any): Middleware;
12
+ export declare const messages: Record<RequestPart, string>;
13
+ export interface RequestValidationErrorBody {
14
+ status: 400;
15
+ part: RequestPart;
16
+ message: string;
17
+ errors: OutputUnit[];
18
+ schema: Schema;
19
+ }
20
+ export declare const validate: typeof middlewareFactory;
21
+ export {};
@@ -0,0 +1,74 @@
1
+ import { dereference, validate as schemaValidate } from '@cfworker/json-schema';
2
+ import { HttpError } from './http-error.js';
3
+ import { toObject } from './to-object.js';
4
+ const draft = '2019-09';
5
+ const hasBody = {
6
+ POST: true,
7
+ PUT: true,
8
+ DELETE: true,
9
+ PATCH: true
10
+ };
11
+ function middlewareFactory(schemas, parser = toObject, lookup = Object.create(null)) {
12
+ const { headers: $headers, search: $search, params: $params, body: $body } = schemas;
13
+ if ($headers) {
14
+ dereference($headers, { ...lookup });
15
+ }
16
+ if ($search) {
17
+ dereference($search, { ...lookup });
18
+ }
19
+ if ($params) {
20
+ dereference($params, { ...lookup });
21
+ }
22
+ if ($body) {
23
+ dereference($body, { ...lookup });
24
+ }
25
+ return async ({ req }, next) => {
26
+ if ($params) {
27
+ validateRequestPart('params', req.params, $params, lookup);
28
+ }
29
+ if ($headers) {
30
+ const headers = toObject(req.headers);
31
+ validateRequestPart('headers', headers, $headers, lookup);
32
+ }
33
+ if ($search) {
34
+ const search = parser(req.url.searchParams);
35
+ validateRequestPart('search', search, $search, lookup);
36
+ }
37
+ if ($body && hasBody[req.method]) {
38
+ let body;
39
+ const contentType = req.headers.get('content-type') || '';
40
+ if (contentType === 'application/x-www-form-urlencoded' ||
41
+ contentType.startsWith('application/x-www-form-urlencoded;') ||
42
+ contentType.startsWith('multipart/form-data;')) {
43
+ const form = await req.body.formData();
44
+ body = parser(form);
45
+ }
46
+ else {
47
+ body = await req.body.json();
48
+ }
49
+ validateRequestPart('body', body, $body, lookup);
50
+ }
51
+ await next();
52
+ };
53
+ }
54
+ export const messages = {
55
+ params: 'Parameters in request path are invalid.',
56
+ headers: 'Headers are invalid.',
57
+ search: 'Query string parameters are invalid.',
58
+ body: 'Body is invalid.'
59
+ };
60
+ function validateRequestPart(part, instance, schema, lookup) {
61
+ const result = schemaValidate(instance, schema, draft, lookup);
62
+ if (result.valid) {
63
+ return;
64
+ }
65
+ const errorBody = {
66
+ status: 400,
67
+ part,
68
+ message: messages[part],
69
+ errors: result.errors,
70
+ schema
71
+ };
72
+ throw new HttpError(400, errorBody);
73
+ }
74
+ export const validate = middlewareFactory;