vovk 3.0.0-beta.2 → 3.0.0-beta.5
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/package.json +2 -2
- package/HttpException.ts +0 -16
- package/Segment.ts +0 -238
- package/StreamResponse.ts +0 -61
- package/client/clientizeController.ts +0 -141
- package/client/defaultFetcher.ts +0 -60
- package/client/defaultHandler.ts +0 -22
- package/client/defaultStreamHandler.ts +0 -88
- package/client/index.ts +0 -9
- package/client/types.ts +0 -116
- package/createDecorator.ts +0 -65
- package/createSegment.ts +0 -239
- package/generateStaticAPI.ts +0 -19
- package/index.ts +0 -59
- package/tsconfig.json +0 -9
- package/types.ts +0 -237
- package/utils/reqMeta.ts +0 -17
- package/utils/reqQuery.ts +0 -27
- package/utils/setClientValidatorsForHandler.ts +0 -45
- package/utils/shim.ts +0 -17
- package/worker/index.ts +0 -4
- package/worker/promisifyWorker.ts +0 -159
- package/worker/types.ts +0 -45
- package/worker/worker.ts +0 -55
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vovk",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.5",
|
|
4
4
|
"description": "RESTful RPC for Next.js - Transforms Next.js into a powerful REST API platform with RPC capabilities.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/finom/vovk.git"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "rm -rf dist && tsc && cp package.json dist && cp
|
|
10
|
+
"build": "rm -rf dist && tsc && cp package.json dist && cp package-lock.json dist && cp .npmignore dist && cp ../../README.md dist ",
|
|
11
11
|
"lint": "eslint . --fix",
|
|
12
12
|
"npm-publish": "npm publish ./dist",
|
|
13
13
|
"ncu": "npm-check-updates -u"
|
package/HttpException.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { _HttpStatus as HttpStatus } from './types';
|
|
2
|
-
|
|
3
|
-
export class _HttpException extends Error {
|
|
4
|
-
statusCode: HttpStatus;
|
|
5
|
-
|
|
6
|
-
message: string;
|
|
7
|
-
|
|
8
|
-
cause?: unknown;
|
|
9
|
-
|
|
10
|
-
constructor(statusCode: HttpStatus, message: string, cause?: unknown) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.statusCode = statusCode;
|
|
13
|
-
this.message = message;
|
|
14
|
-
this.cause = cause;
|
|
15
|
-
}
|
|
16
|
-
}
|
package/Segment.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
_HttpMethod as HttpMethod,
|
|
3
|
-
_HttpStatus as HttpStatus,
|
|
4
|
-
type _RouteHandler as RouteHandler,
|
|
5
|
-
type _VovkErrorResponse as VovkErrorResponse,
|
|
6
|
-
type _VovkController as VovkController,
|
|
7
|
-
type _DecoratorOptions as DecoratorOptions,
|
|
8
|
-
type _VovkRequest as VovkRequest,
|
|
9
|
-
type _KnownAny as _KnownAny,
|
|
10
|
-
} from './types';
|
|
11
|
-
import { _HttpException as HttpException } from './HttpException';
|
|
12
|
-
import { _StreamResponse as StreamResponse } from './StreamResponse';
|
|
13
|
-
import reqQuery from './utils/reqQuery';
|
|
14
|
-
import reqMeta from './utils/reqMeta';
|
|
15
|
-
|
|
16
|
-
export class _Segment {
|
|
17
|
-
private static getHeadersFromOptions(options?: DecoratorOptions) {
|
|
18
|
-
if (!options) return {};
|
|
19
|
-
|
|
20
|
-
const corsHeaders = {
|
|
21
|
-
'Access-Control-Allow-Origin': '*',
|
|
22
|
-
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
|
|
23
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const headers = {
|
|
27
|
-
...(options.cors ? corsHeaders : {}),
|
|
28
|
-
...(options.headers ?? {}),
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
return headers;
|
|
32
|
-
}
|
|
33
|
-
_routes: Record<HttpMethod, Map<VovkController, Record<string, RouteHandler>>> = {
|
|
34
|
-
GET: new Map(),
|
|
35
|
-
POST: new Map(),
|
|
36
|
-
PUT: new Map(),
|
|
37
|
-
PATCH: new Map(),
|
|
38
|
-
DELETE: new Map(),
|
|
39
|
-
HEAD: new Map(),
|
|
40
|
-
OPTIONS: new Map(),
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
GET = (req: VovkRequest, data: { params: Record<string, string[]> }) => {
|
|
44
|
-
return this.#callMethod(HttpMethod.GET, req, data.params);
|
|
45
|
-
};
|
|
46
|
-
POST = (req: VovkRequest, data: { params: Record<string, string[]> }) =>
|
|
47
|
-
this.#callMethod(HttpMethod.POST, req, data.params);
|
|
48
|
-
|
|
49
|
-
PUT = (req: VovkRequest, data: { params: Record<string, string[]> }) =>
|
|
50
|
-
this.#callMethod(HttpMethod.PUT, req, data.params);
|
|
51
|
-
|
|
52
|
-
PATCH = (req: VovkRequest, data: { params: Record<string, string[]> }) =>
|
|
53
|
-
this.#callMethod(HttpMethod.PATCH, req, data.params);
|
|
54
|
-
|
|
55
|
-
DELETE = (req: VovkRequest, data: { params: Record<string, string[]> }) =>
|
|
56
|
-
this.#callMethod(HttpMethod.DELETE, req, data.params);
|
|
57
|
-
|
|
58
|
-
HEAD = (req: VovkRequest, data: { params: Record<string, string[]> }) =>
|
|
59
|
-
this.#callMethod(HttpMethod.HEAD, req, data.params);
|
|
60
|
-
|
|
61
|
-
OPTIONS = (req: VovkRequest, data: { params: Record<string, string[]> }) =>
|
|
62
|
-
this.#callMethod(HttpMethod.OPTIONS, req, data.params);
|
|
63
|
-
|
|
64
|
-
#respond = (status: HttpStatus, body: unknown, options?: DecoratorOptions) => {
|
|
65
|
-
return new Response(JSON.stringify(body), {
|
|
66
|
-
status,
|
|
67
|
-
headers: {
|
|
68
|
-
'Content-Type': 'application/json',
|
|
69
|
-
..._Segment.getHeadersFromOptions(options),
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
#respondWithError = (statusCode: HttpStatus, message: string, options?: DecoratorOptions) => {
|
|
75
|
-
return this.#respond(
|
|
76
|
-
statusCode,
|
|
77
|
-
{
|
|
78
|
-
statusCode,
|
|
79
|
-
message,
|
|
80
|
-
isError: true,
|
|
81
|
-
} satisfies VovkErrorResponse,
|
|
82
|
-
options
|
|
83
|
-
);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
#callMethod = async (httpMethod: HttpMethod, req: VovkRequest, params: Record<string, string[]>) => {
|
|
87
|
-
const controllers = this._routes[httpMethod];
|
|
88
|
-
const methodParams: Record<string, string> = {};
|
|
89
|
-
const path = params[Object.keys(params)[0]];
|
|
90
|
-
|
|
91
|
-
if (params[Object.keys(params)[0]]?.[0] === '_vovk-ping_') {
|
|
92
|
-
return this.#respond(200, { message: 'pong' });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const handlers: Record<string, { staticMethod: RouteHandler; controller: VovkController }> = {};
|
|
96
|
-
controllers.forEach((staticMethods, controller) => {
|
|
97
|
-
const prefix = controller._prefix ?? '';
|
|
98
|
-
|
|
99
|
-
if (!controller._activated) {
|
|
100
|
-
throw new HttpException(
|
|
101
|
-
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
102
|
-
`Controller "${controller.name}" found but not activated`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
Object.entries(staticMethods).forEach(([path, staticMethod]) => {
|
|
107
|
-
const fullPath = [prefix, path].filter(Boolean).join('/');
|
|
108
|
-
handlers[fullPath] = { staticMethod, controller };
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const getHandler = () => {
|
|
113
|
-
if (Object.keys(params).length === 0) {
|
|
114
|
-
return handlers[''];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const allMethodKeys = Object.keys(handlers);
|
|
118
|
-
|
|
119
|
-
let methodKeys: string[] = [];
|
|
120
|
-
|
|
121
|
-
methodKeys = allMethodKeys
|
|
122
|
-
// First, try to match literal routes exactly.
|
|
123
|
-
.filter((p) => {
|
|
124
|
-
if (p.includes(':')) return false; // Skip parameterized paths
|
|
125
|
-
return p === path.join('/');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
if (!methodKeys.length) {
|
|
129
|
-
methodKeys = allMethodKeys.filter((p) => {
|
|
130
|
-
const routeSegments = p.split('/');
|
|
131
|
-
if (routeSegments.length !== path.length) return false;
|
|
132
|
-
|
|
133
|
-
for (let i = 0; i < routeSegments.length; i++) {
|
|
134
|
-
const routeSegment = routeSegments[i];
|
|
135
|
-
const pathSegment = path[i];
|
|
136
|
-
|
|
137
|
-
if (routeSegment.startsWith(':')) {
|
|
138
|
-
const parameter = routeSegment.slice(1);
|
|
139
|
-
|
|
140
|
-
if (parameter in methodParams) {
|
|
141
|
-
throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Duplicate parameter "${parameter}"`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// If it's a parameterized segment, capture the parameter value.
|
|
145
|
-
methodParams[parameter] = pathSegment;
|
|
146
|
-
} else if (routeSegment !== pathSegment) {
|
|
147
|
-
// If it's a literal segment and it does not match the corresponding path segment, return false.
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return true;
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (methodKeys.length > 1) {
|
|
156
|
-
throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Conflicting routes found: ${methodKeys.join(', ')}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const [methodKey] = methodKeys;
|
|
160
|
-
|
|
161
|
-
if (methodKey) {
|
|
162
|
-
return handlers[methodKey];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return null;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const handler = getHandler();
|
|
169
|
-
|
|
170
|
-
if (!handler) {
|
|
171
|
-
return this.#respondWithError(HttpStatus.NOT_FOUND, `Route ${path.join('/')} is not found`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const { staticMethod, controller } = handler;
|
|
175
|
-
|
|
176
|
-
req.vovk = {
|
|
177
|
-
body: () => req.json(),
|
|
178
|
-
query: () => reqQuery(req),
|
|
179
|
-
meta: <T = _KnownAny>(metadata?: T | null) => reqMeta<T>(req, metadata),
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const result = await staticMethod.call(controller, req, methodParams);
|
|
184
|
-
|
|
185
|
-
const isIterator =
|
|
186
|
-
typeof result === 'object' &&
|
|
187
|
-
!!result &&
|
|
188
|
-
((Reflect.has(result, Symbol.iterator) &&
|
|
189
|
-
typeof (result as Iterable<unknown>)[Symbol.iterator] === 'function') ||
|
|
190
|
-
(Reflect.has(result, Symbol.asyncIterator) &&
|
|
191
|
-
typeof (result as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'));
|
|
192
|
-
|
|
193
|
-
if (isIterator && !(result instanceof Array)) {
|
|
194
|
-
const streamResponse = new StreamResponse({
|
|
195
|
-
headers: {
|
|
196
|
-
...StreamResponse.defaultHeaders,
|
|
197
|
-
..._Segment.getHeadersFromOptions(staticMethod._options),
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
void (async () => {
|
|
202
|
-
try {
|
|
203
|
-
for await (const chunk of result as AsyncGenerator<unknown>) {
|
|
204
|
-
streamResponse.send(chunk);
|
|
205
|
-
}
|
|
206
|
-
} catch (e) {
|
|
207
|
-
return streamResponse.throw(e);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return streamResponse.close();
|
|
211
|
-
})();
|
|
212
|
-
|
|
213
|
-
return streamResponse;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (result instanceof Response) {
|
|
217
|
-
return result;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return this.#respond(200, result ?? null, staticMethod._options);
|
|
221
|
-
} catch (e) {
|
|
222
|
-
const err = e as HttpException;
|
|
223
|
-
try {
|
|
224
|
-
await controller._onError?.(err, req);
|
|
225
|
-
} catch (onErrorError) {
|
|
226
|
-
// eslint-disable-next-line no-console
|
|
227
|
-
console.error(onErrorError);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (err.message !== 'NEXT_REDIRECT' && err.message !== 'NEXT_NOT_FOUND') {
|
|
231
|
-
const statusCode = err.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR;
|
|
232
|
-
return this.#respondWithError(statusCode, err.message, staticMethod._options);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
throw e; // if NEXT_REDIRECT or NEXT_NOT_FOUND, rethrow it
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
}
|
package/StreamResponse.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { _KnownAny as KnownAny, _StreamAbortMessage as StreamAbortMessage } from './types';
|
|
2
|
-
import './utils/shim';
|
|
3
|
-
|
|
4
|
-
export class _StreamResponse<T> extends Response {
|
|
5
|
-
public static defaultHeaders = {
|
|
6
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
public isClosed = false;
|
|
10
|
-
|
|
11
|
-
public controller?: ReadableStreamDefaultController;
|
|
12
|
-
|
|
13
|
-
public readonly encoder: TextEncoder;
|
|
14
|
-
|
|
15
|
-
public readonly readableStream: ReadableStream;
|
|
16
|
-
|
|
17
|
-
constructor(init?: ResponseInit) {
|
|
18
|
-
const encoder = new TextEncoder();
|
|
19
|
-
let readableController: ReadableStreamDefaultController;
|
|
20
|
-
|
|
21
|
-
const readableStream = new ReadableStream({
|
|
22
|
-
cancel: () => {
|
|
23
|
-
this.isClosed = true;
|
|
24
|
-
},
|
|
25
|
-
start: (controller) => {
|
|
26
|
-
readableController = controller;
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
super(readableStream, {
|
|
31
|
-
...init,
|
|
32
|
-
headers: init?.headers ?? _StreamResponse.defaultHeaders,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
this.readableStream = readableStream;
|
|
36
|
-
this.encoder = encoder;
|
|
37
|
-
this.controller = readableController!;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
public send(data: T | StreamAbortMessage) {
|
|
41
|
-
const { controller, encoder } = this;
|
|
42
|
-
if (this.isClosed) return;
|
|
43
|
-
return controller?.enqueue(encoder.encode(JSON.stringify(data) + '\n'));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
public close() {
|
|
47
|
-
const { controller } = this;
|
|
48
|
-
if (this.isClosed) return;
|
|
49
|
-
this.isClosed = true;
|
|
50
|
-
controller?.close();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public throw(e: KnownAny) {
|
|
54
|
-
this.send({ isError: true, reason: e instanceof Error ? e.message : (e as unknown) });
|
|
55
|
-
return this.close();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public [Symbol.dispose]() {
|
|
59
|
-
this.close();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type _VovkControllerMetadata as VovkControllerMetadata,
|
|
3
|
-
type _ControllerStaticMethod as ControllerStaticMethod,
|
|
4
|
-
type _VovkControllerParams as VovkControllerParams,
|
|
5
|
-
type _VovkControllerQuery as VovkControllerQuery,
|
|
6
|
-
type _KnownAny as KnownAny,
|
|
7
|
-
} from '../types';
|
|
8
|
-
import {
|
|
9
|
-
type _VovkClientOptions as VovkClientOptions,
|
|
10
|
-
type _VovkClient as VovkClient,
|
|
11
|
-
type _VovkDefaultFetcherOptions as VovkDefaultFetcherOptions,
|
|
12
|
-
_VovkValidateOnClient,
|
|
13
|
-
} from './types';
|
|
14
|
-
|
|
15
|
-
import defaultFetcher from './defaultFetcher';
|
|
16
|
-
import { _defaultHandler as defaultHandler } from './defaultHandler';
|
|
17
|
-
import { _defaultStreamHandler as defaultStreamHandler } from './defaultStreamHandler';
|
|
18
|
-
|
|
19
|
-
export const ARRAY_QUERY_KEY = '_vovkarr';
|
|
20
|
-
|
|
21
|
-
const trimPath = (path: string) => path.trim().replace(/^\/|\/$/g, '');
|
|
22
|
-
|
|
23
|
-
const getHandlerPath = <T extends ControllerStaticMethod>(
|
|
24
|
-
endpoint: string,
|
|
25
|
-
params?: VovkControllerParams<T>,
|
|
26
|
-
query?: VovkControllerQuery<T>
|
|
27
|
-
) => {
|
|
28
|
-
let result = endpoint;
|
|
29
|
-
for (const [key, value] of Object.entries(params ?? {})) {
|
|
30
|
-
result = result.replace(`:${key}`, value as string);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const searchParams = new URLSearchParams();
|
|
34
|
-
let hasQuery = false;
|
|
35
|
-
const arrayKeys: string[] = [];
|
|
36
|
-
for (const [key, value] of Object.entries(query ?? {})) {
|
|
37
|
-
if (value instanceof Array) {
|
|
38
|
-
arrayKeys.push(key);
|
|
39
|
-
for (const val of value) {
|
|
40
|
-
searchParams.append(key, val);
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
searchParams.set(key, value);
|
|
44
|
-
}
|
|
45
|
-
hasQuery = true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (arrayKeys.length) {
|
|
49
|
-
searchParams.set(ARRAY_QUERY_KEY, arrayKeys.join(','));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return `${result}${hasQuery ? '?' : ''}${searchParams.toString()}`;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const _clientizeController = <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions>(
|
|
56
|
-
givenController: VovkControllerMetadata,
|
|
57
|
-
segmentName: string,
|
|
58
|
-
options?: VovkClientOptions<OPTS>
|
|
59
|
-
): VovkClient<T, OPTS> => {
|
|
60
|
-
const controller = givenController as T & VovkControllerMetadata;
|
|
61
|
-
const client = {} as VovkClient<T, OPTS>;
|
|
62
|
-
if (!controller) throw new Error(`Unable to clientize. Controller metadata is not provided`);
|
|
63
|
-
const metadata = controller._handlers;
|
|
64
|
-
if (!metadata)
|
|
65
|
-
throw new Error(`Unable to clientize. No metadata for controller ${String(controller?._controllerName)}`);
|
|
66
|
-
const controllerPrefix = trimPath(controller._prefix ?? '');
|
|
67
|
-
const { fetcher: settingsFetcher = defaultFetcher } = options ?? {};
|
|
68
|
-
|
|
69
|
-
for (const [staticMethodName, { path, httpMethod, clientValidators }] of Object.entries(metadata)) {
|
|
70
|
-
const getEndpoint = ({
|
|
71
|
-
prefix,
|
|
72
|
-
params,
|
|
73
|
-
query,
|
|
74
|
-
}: {
|
|
75
|
-
prefix: string;
|
|
76
|
-
params: { [key: string]: string };
|
|
77
|
-
query: { [key: string]: string };
|
|
78
|
-
}) => {
|
|
79
|
-
const mainPrefix =
|
|
80
|
-
(prefix.startsWith('http://') || prefix.startsWith('https://') || prefix.startsWith('/') ? '' : '/') +
|
|
81
|
-
(prefix.endsWith('/') ? prefix : `${prefix}/`) +
|
|
82
|
-
(segmentName ? `${segmentName}/` : '');
|
|
83
|
-
return mainPrefix + getHandlerPath([controllerPrefix, path].filter(Boolean).join('/'), params, query);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const handler = (
|
|
87
|
-
input: {
|
|
88
|
-
body?: unknown;
|
|
89
|
-
query?: { [key: string]: string };
|
|
90
|
-
params?: { [key: string]: string };
|
|
91
|
-
validateOnClient?: _VovkValidateOnClient;
|
|
92
|
-
fetcher?: VovkClientOptions<OPTS>['fetcher'];
|
|
93
|
-
transform?: (response: unknown) => unknown;
|
|
94
|
-
} & OPTS = {} as OPTS
|
|
95
|
-
) => {
|
|
96
|
-
const fetcher = input.fetcher ?? settingsFetcher;
|
|
97
|
-
const validate = async ({ body, query, endpoint }: { body?: unknown; query?: unknown; endpoint: string }) => {
|
|
98
|
-
await (input.validateOnClient ?? options?.validateOnClient)?.(
|
|
99
|
-
{ body, query, endpoint },
|
|
100
|
-
clientValidators ?? {}
|
|
101
|
-
);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const internalOptions: Parameters<typeof fetcher>[0] = {
|
|
105
|
-
name: staticMethodName as keyof T,
|
|
106
|
-
httpMethod,
|
|
107
|
-
getEndpoint,
|
|
108
|
-
validate,
|
|
109
|
-
defaultHandler,
|
|
110
|
-
defaultStreamHandler,
|
|
111
|
-
};
|
|
112
|
-
const internalInput = {
|
|
113
|
-
...options?.defaultOptions,
|
|
114
|
-
...input,
|
|
115
|
-
body: input.body ?? null,
|
|
116
|
-
query: input.query ?? {},
|
|
117
|
-
params: input.params ?? {},
|
|
118
|
-
segmentName,
|
|
119
|
-
// TS workaround
|
|
120
|
-
fetcher: undefined,
|
|
121
|
-
validateOnClient: undefined,
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
delete internalInput.fetcher;
|
|
125
|
-
delete internalInput.validateOnClient;
|
|
126
|
-
|
|
127
|
-
if (!fetcher) throw new Error('Fetcher is not provided');
|
|
128
|
-
|
|
129
|
-
const fetcherPromise = fetcher(internalOptions, internalInput) as Promise<unknown>;
|
|
130
|
-
|
|
131
|
-
if (!(fetcherPromise instanceof Promise)) return Promise.resolve(fetcherPromise);
|
|
132
|
-
|
|
133
|
-
return input.transform ? fetcherPromise.then(input.transform) : fetcherPromise;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// @ts-expect-error TODO: Fix this
|
|
137
|
-
client[staticMethodName] = handler;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return client;
|
|
141
|
-
};
|
package/client/defaultFetcher.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
_VovkDefaultFetcherOptions as VovkDefaultFetcherOptions,
|
|
3
|
-
_VovkClientFetcher as VovkClientFetcher,
|
|
4
|
-
} from './types';
|
|
5
|
-
import { _HttpStatus as HttpStatus } from '../types';
|
|
6
|
-
import { _HttpException as HttpException } from '../HttpException';
|
|
7
|
-
|
|
8
|
-
export const DEFAULT_ERROR_MESSAGE = 'Unknown error at defaultFetcher';
|
|
9
|
-
|
|
10
|
-
// defaultFetcher uses HttpException class to throw errors of fake HTTP status 0 if client-side error occurs
|
|
11
|
-
// For normal HTTP errors, it uses message and status code from the response of VovkErrorResponse type
|
|
12
|
-
const defaultFetcher: VovkClientFetcher<VovkDefaultFetcherOptions> = async (
|
|
13
|
-
{ httpMethod, getEndpoint, validate, defaultHandler, defaultStreamHandler },
|
|
14
|
-
{ params, query, body, prefix = '/api', segmentName, ...options }
|
|
15
|
-
) => {
|
|
16
|
-
const endpoint = getEndpoint({ prefix, segmentName, params, query });
|
|
17
|
-
|
|
18
|
-
if (!options.disableClientValidation) {
|
|
19
|
-
try {
|
|
20
|
-
await validate({ body, query, endpoint });
|
|
21
|
-
} catch (e) {
|
|
22
|
-
// if HttpException is thrown, rethrow it
|
|
23
|
-
if (e instanceof HttpException) throw e;
|
|
24
|
-
// otherwise, throw HttpException with status 0
|
|
25
|
-
throw new HttpException(HttpStatus.NULL, (e as Error).message ?? DEFAULT_ERROR_MESSAGE);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const init: RequestInit = {
|
|
30
|
-
method: httpMethod,
|
|
31
|
-
...options,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if (body instanceof FormData) {
|
|
35
|
-
init.body = body as BodyInit;
|
|
36
|
-
} else if (body) {
|
|
37
|
-
init.body = JSON.stringify(body);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let response: Response;
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
response = await fetch(endpoint, init);
|
|
44
|
-
} catch (e) {
|
|
45
|
-
// handle network errors
|
|
46
|
-
throw new HttpException(HttpStatus.NULL, (e as Error)?.message ?? DEFAULT_ERROR_MESSAGE);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (response.headers.get('content-type')?.includes('application/json')) {
|
|
50
|
-
return defaultHandler(response);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (response.headers.get('content-type')?.includes('text/plain; charset=utf-8')) {
|
|
54
|
-
return defaultStreamHandler(response);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return response;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export default defaultFetcher;
|
package/client/defaultHandler.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type _VovkErrorResponse as VovkErrorResponse } from '../types';
|
|
2
|
-
import { _HttpException as HttpException } from '../HttpException';
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_ERROR_MESSAGE = 'Unknown error at defaultHandler';
|
|
5
|
-
|
|
6
|
-
export const _defaultHandler = async (response: Response) => {
|
|
7
|
-
let result: unknown;
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
result = await response.json();
|
|
11
|
-
} catch (e) {
|
|
12
|
-
// handle parsing errors
|
|
13
|
-
throw new HttpException(response.status, (e as Error)?.message ?? DEFAULT_ERROR_MESSAGE);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (!response.ok) {
|
|
17
|
-
// handle server errors
|
|
18
|
-
throw new HttpException(response.status, (result as VovkErrorResponse)?.message ?? DEFAULT_ERROR_MESSAGE);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return result;
|
|
22
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { _HttpStatus as HttpStatus, type _VovkErrorResponse as VovkErrorResponse } from '../types';
|
|
2
|
-
import type { _StreamAsyncIterator as StreamAsyncIterator } from './types';
|
|
3
|
-
import { _HttpException as HttpException } from '../HttpException';
|
|
4
|
-
import '../utils/shim';
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_ERROR_MESSAGE = 'Unknown error at defaultStreamHandler';
|
|
7
|
-
|
|
8
|
-
export const _defaultStreamHandler = async (response: Response): Promise<StreamAsyncIterator<unknown>> => {
|
|
9
|
-
if (!response.ok) {
|
|
10
|
-
let result: unknown;
|
|
11
|
-
try {
|
|
12
|
-
result = await response.json();
|
|
13
|
-
} catch {
|
|
14
|
-
// ignore parsing errors
|
|
15
|
-
}
|
|
16
|
-
// handle server errors
|
|
17
|
-
throw new HttpException(response.status, (result as VovkErrorResponse).message ?? DEFAULT_ERROR_MESSAGE);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (!response.body) throw new HttpException(HttpStatus.NULL, 'Stream body is falsy. Check your controller code.');
|
|
21
|
-
|
|
22
|
-
const reader = response.body.getReader();
|
|
23
|
-
|
|
24
|
-
// if streaming is too rapid, we need to make sure that the loop is stopped
|
|
25
|
-
let canceled = false;
|
|
26
|
-
|
|
27
|
-
async function* asyncIterator() {
|
|
28
|
-
let prepend = '';
|
|
29
|
-
|
|
30
|
-
while (true) {
|
|
31
|
-
let value: Uint8Array | undefined;
|
|
32
|
-
let done = false;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
({ value, done } = await reader.read());
|
|
36
|
-
} catch (error) {
|
|
37
|
-
await reader.cancel();
|
|
38
|
-
const err = new Error('Stream error. ' + String(error));
|
|
39
|
-
err.cause = error;
|
|
40
|
-
throw err;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (done) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// typeof value === 'number' is a workaround for React Native
|
|
48
|
-
const string = typeof value === 'number' ? String.fromCharCode(value) : new TextDecoder().decode(value);
|
|
49
|
-
prepend += string;
|
|
50
|
-
const lines = prepend.split('\n').filter(Boolean);
|
|
51
|
-
for (const line of lines) {
|
|
52
|
-
let data;
|
|
53
|
-
try {
|
|
54
|
-
data = JSON.parse(line) as object;
|
|
55
|
-
prepend = '';
|
|
56
|
-
} catch {
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (data) {
|
|
61
|
-
if ('isError' in data && 'reason' in data) {
|
|
62
|
-
const upcomingError = data.reason;
|
|
63
|
-
await reader.cancel();
|
|
64
|
-
|
|
65
|
-
if (typeof upcomingError === 'string') {
|
|
66
|
-
throw new Error(upcomingError);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
throw upcomingError;
|
|
70
|
-
} else if (!canceled) {
|
|
71
|
-
yield data;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
status: response.status,
|
|
80
|
-
[Symbol.asyncIterator]: asyncIterator,
|
|
81
|
-
[Symbol.dispose]: () => reader.cancel(),
|
|
82
|
-
[Symbol.asyncDispose]: () => reader.cancel(),
|
|
83
|
-
cancel: () => {
|
|
84
|
-
canceled = true;
|
|
85
|
-
return reader.cancel();
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
};
|
package/client/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { _clientizeController as clientizeController } from './clientizeController';
|
|
2
|
-
import type {
|
|
3
|
-
_VovkClientFetcher as VovkClientFetcher,
|
|
4
|
-
_VovkClientOptions as VovkClientOptions,
|
|
5
|
-
_VovkDefaultFetcherOptions as VovkDefaultFetcherOptions,
|
|
6
|
-
} from './types';
|
|
7
|
-
|
|
8
|
-
export { clientizeController };
|
|
9
|
-
export type { VovkClientFetcher, VovkClientOptions, VovkDefaultFetcherOptions };
|