vovk 3.0.0-draft.6 → 3.0.0-draft.60
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/README.md +8 -95
- package/{HttpException.d.ts → dist/HttpException.d.ts} +2 -2
- package/{HttpException.js → dist/HttpException.js} +3 -3
- package/{StreamResponse.d.ts → dist/StreamJSONResponse.d.ts} +3 -3
- package/{StreamResponse.js → dist/StreamJSONResponse.js} +5 -5
- package/{Segment.d.ts → dist/VovkApp.d.ts} +3 -3
- package/{Segment.js → dist/VovkApp.js} +26 -23
- package/dist/client/createRPC.d.ts +4 -0
- package/{client/clientizeController.js → dist/client/createRPC.js} +21 -39
- package/dist/client/defaultFetcher.d.ts +4 -0
- package/{client → dist/client}/defaultFetcher.js +11 -10
- package/{client → dist/client}/defaultHandler.d.ts +1 -1
- package/dist/client/defaultHandler.js +22 -0
- package/dist/client/defaultStreamHandler.d.ts +4 -0
- package/{client → dist/client}/defaultStreamHandler.js +5 -5
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -0
- package/dist/client/types.d.ts +103 -0
- package/dist/createDecorator.d.ts +4 -0
- package/{createDecorator.js → dist/createDecorator.js} +4 -4
- package/{createSegment.d.ts → dist/createVovkApp.d.ts} +2 -3
- package/{createSegment.js → dist/createVovkApp.js} +25 -25
- package/dist/index.d.ts +59 -0
- package/dist/index.js +22 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +65 -0
- package/dist/utils/generateStaticAPI.d.ts +4 -0
- package/{generateStaticAPI.js → dist/utils/generateStaticAPI.js} +3 -3
- package/{utils → dist/utils}/getSchema.d.ts +1 -2
- package/{utils → dist/utils}/getSchema.js +5 -16
- package/dist/utils/parseQuery.d.ts +25 -0
- package/dist/utils/parseQuery.js +156 -0
- package/dist/utils/reqForm.d.ts +2 -0
- package/dist/utils/reqForm.js +13 -0
- package/{utils → dist/utils}/reqMeta.d.ts +1 -2
- package/{utils → dist/utils}/reqQuery.d.ts +1 -2
- package/dist/utils/reqQuery.js +10 -0
- package/dist/utils/serializeQuery.d.ts +13 -0
- package/dist/utils/serializeQuery.js +65 -0
- package/dist/utils/setClientValidatorsForHandler.d.ts +5 -0
- package/{utils → dist/utils}/setClientValidatorsForHandler.js +4 -6
- package/package.json +5 -2
- package/src/HttpException.ts +16 -0
- package/src/StreamJSONResponse.ts +61 -0
- package/src/VovkApp.ts +242 -0
- package/src/client/createRPC.ts +119 -0
- package/src/client/defaultFetcher.ts +59 -0
- package/src/client/defaultHandler.ts +23 -0
- package/src/client/defaultStreamHandler.ts +88 -0
- package/src/client/index.ts +9 -0
- package/src/client/types.ts +120 -0
- package/src/createDecorator.ts +60 -0
- package/src/createVovkApp.ts +167 -0
- package/src/index.ts +69 -0
- package/src/types.ts +198 -0
- package/src/utils/generateStaticAPI.ts +18 -0
- package/src/utils/getSchema.ts +35 -0
- package/src/utils/parseQuery.ts +160 -0
- package/src/utils/reqForm.ts +16 -0
- package/src/utils/reqMeta.ts +16 -0
- package/src/utils/reqQuery.ts +6 -0
- package/src/utils/serializeQuery.ts +69 -0
- package/src/utils/setClientValidatorsForHandler.ts +45 -0
- package/src/utils/shim.ts +17 -0
- package/.npmignore +0 -2
- package/client/clientizeController.d.ts +0 -4
- package/client/defaultFetcher.d.ts +0 -4
- package/client/defaultHandler.js +0 -21
- package/client/defaultStreamHandler.d.ts +0 -4
- package/client/index.d.ts +0 -4
- package/client/index.js +0 -5
- package/client/types.d.ts +0 -102
- package/createDecorator.d.ts +0 -4
- package/generateStaticAPI.d.ts +0 -4
- package/index.d.ts +0 -60
- package/index.js +0 -20
- package/types.d.ts +0 -191
- package/types.js +0 -65
- package/utils/reqQuery.js +0 -25
- package/utils/setClientValidatorsForHandler.d.ts +0 -5
- package/worker/index.d.ts +0 -3
- package/worker/index.js +0 -7
- package/worker/promisifyWorker.d.ts +0 -2
- package/worker/promisifyWorker.js +0 -143
- package/worker/types.d.ts +0 -31
- package/worker/types.js +0 -2
- package/worker/worker.d.ts +0 -1
- package/worker/worker.js +0 -44
- /package/{client → dist/client}/types.js +0 -0
- /package/{utils → dist/utils}/reqMeta.js +0 -0
- /package/{utils → dist/utils}/shim.d.ts +0 -0
- /package/{utils → dist/utils}/shim.js +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type VovkControllerSchema,
|
|
3
|
+
type ControllerStaticMethod,
|
|
4
|
+
type VovkControllerParams,
|
|
5
|
+
type VovkControllerQuery,
|
|
6
|
+
type KnownAny,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import { type VovkClientOptions, type VovkClient, type VovkDefaultFetcherOptions, VovkValidateOnClient } from './types';
|
|
9
|
+
|
|
10
|
+
import defaultFetcher from './defaultFetcher';
|
|
11
|
+
import { defaultHandler } from './defaultHandler';
|
|
12
|
+
import { defaultStreamHandler } from './defaultStreamHandler';
|
|
13
|
+
import serializeQuery from '../utils/serializeQuery';
|
|
14
|
+
|
|
15
|
+
const trimPath = (path: string) => path.trim().replace(/^\/|\/$/g, '');
|
|
16
|
+
|
|
17
|
+
const getHandlerPath = <T extends ControllerStaticMethod>(
|
|
18
|
+
endpoint: string,
|
|
19
|
+
params?: VovkControllerParams<T>,
|
|
20
|
+
query?: VovkControllerQuery<T>
|
|
21
|
+
) => {
|
|
22
|
+
let result = endpoint;
|
|
23
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
24
|
+
result = result.replace(`:${key}`, value as string);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const queryStr = query ? serializeQuery(query) : null;
|
|
28
|
+
|
|
29
|
+
return `${result}${queryStr ? '?' : ''}${queryStr}`;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const createRPC = <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions>(
|
|
33
|
+
controllerSchema: VovkControllerSchema,
|
|
34
|
+
segmentName?: string,
|
|
35
|
+
options?: VovkClientOptions<OPTS>
|
|
36
|
+
): VovkClient<T, OPTS> => {
|
|
37
|
+
const schema = controllerSchema as T & VovkControllerSchema;
|
|
38
|
+
const client = {} as VovkClient<T, OPTS>;
|
|
39
|
+
if (!schema) throw new Error(`Unable to clientize. Controller schema is not provided`);
|
|
40
|
+
if (!schema.handlers)
|
|
41
|
+
throw new Error(`Unable to clientize. No schema for controller ${String(schema?.controllerName)} provided`);
|
|
42
|
+
const controllerPrefix = trimPath(schema.prefix ?? '');
|
|
43
|
+
const { fetcher: settingsFetcher = defaultFetcher } = options ?? {};
|
|
44
|
+
|
|
45
|
+
for (const [staticMethodName, handlerSchema] of Object.entries(schema.handlers)) {
|
|
46
|
+
const { path, httpMethod, validation } = handlerSchema;
|
|
47
|
+
const getEndpoint = ({
|
|
48
|
+
apiRoot,
|
|
49
|
+
params,
|
|
50
|
+
query,
|
|
51
|
+
}: {
|
|
52
|
+
apiRoot: string;
|
|
53
|
+
params: { [key: string]: string };
|
|
54
|
+
query: { [key: string]: string };
|
|
55
|
+
}) => {
|
|
56
|
+
const mainPrefix =
|
|
57
|
+
(apiRoot.startsWith('http://') || apiRoot.startsWith('https://') || apiRoot.startsWith('/') ? '' : '/') +
|
|
58
|
+
(apiRoot.endsWith('/') ? apiRoot : `${apiRoot}/`) +
|
|
59
|
+
(segmentName ? `${segmentName}/` : '');
|
|
60
|
+
return mainPrefix + getHandlerPath([controllerPrefix, path].filter(Boolean).join('/'), params, query);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handler = (
|
|
64
|
+
input: {
|
|
65
|
+
body?: unknown;
|
|
66
|
+
query?: { [key: string]: string };
|
|
67
|
+
params?: { [key: string]: string };
|
|
68
|
+
validateOnClient?: VovkValidateOnClient;
|
|
69
|
+
fetcher?: VovkClientOptions<OPTS>['fetcher'];
|
|
70
|
+
transform?: (response: unknown) => unknown;
|
|
71
|
+
} & OPTS = {} as OPTS
|
|
72
|
+
) => {
|
|
73
|
+
const fetcher = input.fetcher ?? settingsFetcher;
|
|
74
|
+
const validate = async ({ body, query, endpoint }: { body?: unknown; query?: unknown; endpoint: string }) => {
|
|
75
|
+
await (input.validateOnClient ?? options?.validateOnClient)?.({ body, query, endpoint }, validation ?? {});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const internalOptions: Parameters<typeof fetcher>[0] = {
|
|
79
|
+
name: staticMethodName as keyof T,
|
|
80
|
+
httpMethod,
|
|
81
|
+
getEndpoint,
|
|
82
|
+
validate,
|
|
83
|
+
defaultHandler,
|
|
84
|
+
defaultStreamHandler,
|
|
85
|
+
};
|
|
86
|
+
const internalInput = {
|
|
87
|
+
...options?.defaultOptions,
|
|
88
|
+
...input,
|
|
89
|
+
body: input.body ?? null,
|
|
90
|
+
query: input.query ?? {},
|
|
91
|
+
params: input.params ?? {},
|
|
92
|
+
// TS workaround
|
|
93
|
+
fetcher: undefined,
|
|
94
|
+
validateOnClient: undefined,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
delete internalInput.fetcher;
|
|
98
|
+
delete internalInput.validateOnClient;
|
|
99
|
+
|
|
100
|
+
if (!fetcher) throw new Error('Fetcher is not provided');
|
|
101
|
+
|
|
102
|
+
const fetcherPromise = fetcher(internalOptions, internalInput) as Promise<unknown>;
|
|
103
|
+
|
|
104
|
+
if (!(fetcherPromise instanceof Promise)) return Promise.resolve(fetcherPromise);
|
|
105
|
+
|
|
106
|
+
return input.transform ? fetcherPromise.then(input.transform) : fetcherPromise;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
handler.schema = handlerSchema;
|
|
110
|
+
handler.controllerSchema = schema;
|
|
111
|
+
|
|
112
|
+
// @ts-expect-error TODO
|
|
113
|
+
client[staticMethodName] = handler;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return client;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default createRPC;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { VovkDefaultFetcherOptions, VovkClientFetcher } from './types';
|
|
2
|
+
import { HttpStatus } from '../types';
|
|
3
|
+
import { HttpException } from '../HttpException';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_ERROR_MESSAGE = 'Unknown error at the defaultFetcher';
|
|
6
|
+
|
|
7
|
+
// defaultFetcher uses HttpException class to throw errors of fake HTTP status 0 if client-side error occurs
|
|
8
|
+
// For normal HTTP errors, it uses message and status code from the response of VovkErrorResponse type
|
|
9
|
+
const defaultFetcher: VovkClientFetcher<VovkDefaultFetcherOptions> = async (
|
|
10
|
+
{ httpMethod, getEndpoint, validate, defaultHandler, defaultStreamHandler },
|
|
11
|
+
{ params, query, body, apiRoot = '/api', ...options }
|
|
12
|
+
) => {
|
|
13
|
+
const endpoint = getEndpoint({ apiRoot, params, query });
|
|
14
|
+
|
|
15
|
+
if (!options.disableClientValidation) {
|
|
16
|
+
try {
|
|
17
|
+
await validate({ body, query, endpoint });
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// if HttpException is thrown, rethrow it
|
|
20
|
+
if (e instanceof HttpException) throw e;
|
|
21
|
+
// otherwise, throw HttpException with status 0
|
|
22
|
+
throw new HttpException(HttpStatus.NULL, (e as Error).message ?? DEFAULT_ERROR_MESSAGE);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const init: RequestInit = {
|
|
27
|
+
method: httpMethod,
|
|
28
|
+
...options,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (body instanceof FormData) {
|
|
32
|
+
init.body = body as BodyInit;
|
|
33
|
+
} else if (body) {
|
|
34
|
+
init.body = JSON.stringify(body);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let response: Response;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
response = await fetch(endpoint, init);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// handle network errors
|
|
43
|
+
throw new HttpException(HttpStatus.NULL, (e as Error)?.message ?? DEFAULT_ERROR_MESSAGE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const contentType = response.headers.get('content-type');
|
|
47
|
+
|
|
48
|
+
if (contentType?.includes('application/jsonl')) {
|
|
49
|
+
return defaultStreamHandler(response);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (contentType?.includes('application/json')) {
|
|
53
|
+
return defaultHandler(response);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return response;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default defaultFetcher;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type VovkErrorResponse } from '../types';
|
|
2
|
+
import { 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
|
+
const errorResponse = result as VovkErrorResponse;
|
|
19
|
+
throw new HttpException(response.status, errorResponse?.message ?? DEFAULT_ERROR_MESSAGE, errorResponse?.cause);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { HttpStatus, type VovkErrorResponse } from '../types';
|
|
2
|
+
import type { VovkStreamAsyncIterable } from './types';
|
|
3
|
+
import { 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<VovkStreamAsyncIterable<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
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
KnownAny,
|
|
3
|
+
HttpMethod,
|
|
4
|
+
ControllerStaticMethod,
|
|
5
|
+
VovkControllerBody,
|
|
6
|
+
VovkControllerQuery,
|
|
7
|
+
VovkControllerParams,
|
|
8
|
+
VovkHandlerSchema,
|
|
9
|
+
VovkControllerSchema,
|
|
10
|
+
} from '../types';
|
|
11
|
+
import type { StreamJSONResponse } from '../StreamJSONResponse';
|
|
12
|
+
import type { NextResponse } from 'next/server';
|
|
13
|
+
|
|
14
|
+
export type StaticMethodInput<T extends ControllerStaticMethod> = (VovkControllerBody<T> extends undefined | void
|
|
15
|
+
? { body?: undefined }
|
|
16
|
+
: VovkControllerBody<T> extends null
|
|
17
|
+
? { body?: null }
|
|
18
|
+
: { body: VovkControllerBody<T> }) &
|
|
19
|
+
(VovkControllerQuery<T> extends undefined | void ? { query?: undefined } : { query: VovkControllerQuery<T> }) &
|
|
20
|
+
(VovkControllerParams<T> extends undefined | void ? { params?: undefined } : { params: VovkControllerParams<T> });
|
|
21
|
+
|
|
22
|
+
type ToPromise<T> = T extends PromiseLike<unknown> ? T : Promise<T>;
|
|
23
|
+
|
|
24
|
+
export type VovkStreamAsyncIterable<T> = {
|
|
25
|
+
status: number;
|
|
26
|
+
[Symbol.dispose](): Promise<void> | void;
|
|
27
|
+
[Symbol.asyncDispose](): Promise<void> | void;
|
|
28
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
29
|
+
cancel: () => Promise<void> | void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type StaticMethodReturn<T extends ControllerStaticMethod> =
|
|
33
|
+
ReturnType<T> extends NextResponse<infer U> | Promise<NextResponse<infer U>>
|
|
34
|
+
? U
|
|
35
|
+
: ReturnType<T> extends Response | Promise<Response>
|
|
36
|
+
? Awaited<ReturnType<T>>
|
|
37
|
+
: ReturnType<T>;
|
|
38
|
+
|
|
39
|
+
type StaticMethodReturnPromise<T extends ControllerStaticMethod> = ToPromise<StaticMethodReturn<T>>;
|
|
40
|
+
|
|
41
|
+
type ClientMethod<
|
|
42
|
+
T extends (...args: KnownAny[]) => void | object | StreamJSONResponse<STREAM> | Promise<StreamJSONResponse<STREAM>>,
|
|
43
|
+
OPTS extends Record<string, KnownAny>,
|
|
44
|
+
STREAM extends KnownAny = unknown,
|
|
45
|
+
> = (<R>(
|
|
46
|
+
options: (StaticMethodInput<T> extends { body?: undefined | null; query?: undefined; params?: undefined }
|
|
47
|
+
? unknown
|
|
48
|
+
: Parameters<T>[0] extends void
|
|
49
|
+
? StaticMethodInput<T>['params'] extends object
|
|
50
|
+
? { params: StaticMethodInput<T>['params'] }
|
|
51
|
+
: unknown
|
|
52
|
+
: StaticMethodInput<T>) &
|
|
53
|
+
(Partial<
|
|
54
|
+
OPTS & {
|
|
55
|
+
transform: (staticMethodReturn: Awaited<StaticMethodReturn<T>>) => R;
|
|
56
|
+
}
|
|
57
|
+
> | void)
|
|
58
|
+
) => ReturnType<T> extends
|
|
59
|
+
| Promise<StreamJSONResponse<infer U>>
|
|
60
|
+
| StreamJSONResponse<infer U>
|
|
61
|
+
| Iterator<infer U>
|
|
62
|
+
| AsyncIterator<infer U>
|
|
63
|
+
? Promise<VovkStreamAsyncIterable<U>>
|
|
64
|
+
: R extends object
|
|
65
|
+
? Promise<R>
|
|
66
|
+
: StaticMethodReturnPromise<T>) & {
|
|
67
|
+
schema: VovkHandlerSchema;
|
|
68
|
+
controllerSchema: VovkControllerSchema;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type OmitNever<T> = {
|
|
72
|
+
[K in keyof T as T[K] extends never ? never : K]: T[K];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type VovkClientWithNever<T, OPTS extends { [key: string]: KnownAny }> = {
|
|
76
|
+
[K in keyof T]: T[K] extends (...args: KnownAny) => KnownAny ? ClientMethod<T[K], OPTS> : never;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type VovkClient<T, OPTS extends { [key: string]: KnownAny }> = OmitNever<VovkClientWithNever<T, OPTS>>;
|
|
80
|
+
|
|
81
|
+
export type VovkClientFetcher<OPTS extends Record<string, KnownAny> = Record<string, never>, T = KnownAny> = (
|
|
82
|
+
options: {
|
|
83
|
+
name: keyof T;
|
|
84
|
+
httpMethod: HttpMethod;
|
|
85
|
+
getEndpoint: (data: {
|
|
86
|
+
apiRoot: string;
|
|
87
|
+
params: { [key: string]: string };
|
|
88
|
+
query: { [key: string]: string };
|
|
89
|
+
}) => string;
|
|
90
|
+
validate: (input: { body?: unknown; query?: unknown; endpoint: string }) => void | Promise<void>;
|
|
91
|
+
defaultStreamHandler: (response: Response) => Promise<VovkStreamAsyncIterable<unknown>>;
|
|
92
|
+
defaultHandler: (response: Response) => Promise<unknown>;
|
|
93
|
+
},
|
|
94
|
+
input: {
|
|
95
|
+
body: unknown;
|
|
96
|
+
query: { [key: string]: string };
|
|
97
|
+
params: { [key: string]: string };
|
|
98
|
+
} & OPTS
|
|
99
|
+
) => KnownAny;
|
|
100
|
+
|
|
101
|
+
// `RequestInit` is the type of options passed to fetch function
|
|
102
|
+
export interface VovkDefaultFetcherOptions extends Omit<RequestInit, 'body' | 'method'> {
|
|
103
|
+
reactNative?: { textStreaming: boolean };
|
|
104
|
+
apiRoot?: string;
|
|
105
|
+
segmentName?: string;
|
|
106
|
+
disableClientValidation?: boolean;
|
|
107
|
+
validateOnClient?: VovkValidateOnClient;
|
|
108
|
+
fetcher?: VovkClientFetcher;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type VovkValidateOnClient = (
|
|
112
|
+
input: { body?: unknown; query?: unknown; endpoint: string },
|
|
113
|
+
validators: { body?: unknown; query?: unknown }
|
|
114
|
+
) => void | Promise<void>;
|
|
115
|
+
|
|
116
|
+
export type VovkClientOptions<OPTS extends Record<string, KnownAny> = Record<string, never>> = {
|
|
117
|
+
fetcher?: VovkClientFetcher<OPTS>;
|
|
118
|
+
validateOnClient?: VovkValidateOnClient;
|
|
119
|
+
defaultOptions?: Partial<OPTS>;
|
|
120
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { VovkHandlerSchema, KnownAny, VovkController, VovkRequest } from './types';
|
|
2
|
+
|
|
3
|
+
type Next = () => Promise<unknown>;
|
|
4
|
+
|
|
5
|
+
export function createDecorator<ARGS extends unknown[], REQUEST = VovkRequest>(
|
|
6
|
+
handler: null | ((this: VovkController, req: REQUEST, next: Next, ...args: ARGS) => unknown),
|
|
7
|
+
initHandler?: (
|
|
8
|
+
this: VovkController,
|
|
9
|
+
...args: ARGS
|
|
10
|
+
) =>
|
|
11
|
+
| Omit<VovkHandlerSchema, 'path' | 'httpMethod'>
|
|
12
|
+
| ((handlerSchema: VovkHandlerSchema | null) => Omit<Partial<VovkHandlerSchema>, 'path' | 'httpMethod'>)
|
|
13
|
+
| null
|
|
14
|
+
| undefined
|
|
15
|
+
) {
|
|
16
|
+
return function decoratorCreator(...args: ARGS) {
|
|
17
|
+
return function decorator(target: KnownAny, propertyKey: string) {
|
|
18
|
+
const controller = target as VovkController;
|
|
19
|
+
|
|
20
|
+
const originalMethod = controller[propertyKey] as ((...args: KnownAny) => KnownAny) & {
|
|
21
|
+
_sourceMethod?: (...args: KnownAny) => KnownAny;
|
|
22
|
+
};
|
|
23
|
+
if (typeof originalMethod !== 'function') {
|
|
24
|
+
throw new Error(`Unable to decorate: ${propertyKey} is not a function`);
|
|
25
|
+
}
|
|
26
|
+
const sourceMethod = originalMethod._sourceMethod ?? originalMethod;
|
|
27
|
+
|
|
28
|
+
const handlerSchema: VovkHandlerSchema | null = controller._handlers?.[propertyKey] ?? null;
|
|
29
|
+
const initResultReturn = initHandler?.call(controller, ...args);
|
|
30
|
+
const initResult = typeof initResultReturn === 'function' ? initResultReturn(handlerSchema) : initResultReturn;
|
|
31
|
+
|
|
32
|
+
controller._handlers = {
|
|
33
|
+
...controller._handlers,
|
|
34
|
+
[propertyKey]: {
|
|
35
|
+
...handlerSchema,
|
|
36
|
+
// avoid override of path and httpMethod
|
|
37
|
+
...(initResult?.validation ? { validation: initResult.validation } : {}),
|
|
38
|
+
...(initResult?.custom ? { custom: initResult.custom } : {}),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const method = function method(req: REQUEST, params?: unknown) {
|
|
43
|
+
const next: Next = async () => {
|
|
44
|
+
return (await originalMethod.call(controller, req, params)) as unknown;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return handler ? handler.call(controller, req, next, ...args) : next();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
method._controller = controller;
|
|
51
|
+
|
|
52
|
+
// TODO define internal method type
|
|
53
|
+
(originalMethod as unknown as { _controller: VovkController })._controller = controller;
|
|
54
|
+
|
|
55
|
+
controller[propertyKey] = method;
|
|
56
|
+
|
|
57
|
+
method._sourceMethod = sourceMethod;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { VovkApp as VovkApp } from './VovkApp';
|
|
2
|
+
import {
|
|
3
|
+
HttpMethod,
|
|
4
|
+
type KnownAny,
|
|
5
|
+
type RouteHandler,
|
|
6
|
+
type VovkController,
|
|
7
|
+
type DecoratorOptions,
|
|
8
|
+
type VovkRequest,
|
|
9
|
+
type StaticClass,
|
|
10
|
+
VovkHandlerSchema,
|
|
11
|
+
} from './types';
|
|
12
|
+
import getSchema from './utils/getSchema';
|
|
13
|
+
|
|
14
|
+
const trimPath = (path: string) => path.trim().replace(/^\/|\/$/g, '');
|
|
15
|
+
const isClass = (func: unknown) => typeof func === 'function' && /class/.test(func.toString());
|
|
16
|
+
const toKebabCase = (str: string) =>
|
|
17
|
+
str
|
|
18
|
+
.replace(/([A-Z])/g, '-$1')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/^-/, '');
|
|
21
|
+
|
|
22
|
+
export function createVovkApp() {
|
|
23
|
+
const vovkApp = new VovkApp();
|
|
24
|
+
|
|
25
|
+
const createHTTPDecorator = (httpMethod: HttpMethod) => {
|
|
26
|
+
const assignSchema = (
|
|
27
|
+
controller: VovkController,
|
|
28
|
+
propertyKey: string,
|
|
29
|
+
path: string,
|
|
30
|
+
options?: DecoratorOptions
|
|
31
|
+
) => {
|
|
32
|
+
if (typeof window !== 'undefined') {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'Decorators are intended for server-side use only. You have probably imported a controller on the client-side.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (!isClass(controller)) {
|
|
38
|
+
let decoratorName = httpMethod.toLowerCase();
|
|
39
|
+
if (decoratorName === 'delete') decoratorName = 'del';
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Decorator must be used on a static class method. Check the controller method named "${propertyKey}" used with @${decoratorName}.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const methods: Record<string, RouteHandler> = vovkApp.routes[httpMethod].get(controller) ?? {};
|
|
46
|
+
vovkApp.routes[httpMethod].set(controller, methods);
|
|
47
|
+
|
|
48
|
+
controller._handlers = {
|
|
49
|
+
...controller._handlers,
|
|
50
|
+
[propertyKey]: {
|
|
51
|
+
...((controller._handlers ?? {})[propertyKey] as Partial<VovkHandlerSchema>),
|
|
52
|
+
path,
|
|
53
|
+
httpMethod,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const originalMethod = controller[propertyKey] as ((...args: KnownAny) => KnownAny) & {
|
|
58
|
+
_controller: VovkController;
|
|
59
|
+
_sourceMethod?: (...args: KnownAny) => KnownAny;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
originalMethod._controller = controller;
|
|
63
|
+
originalMethod._sourceMethod = originalMethod._sourceMethod ?? originalMethod;
|
|
64
|
+
|
|
65
|
+
methods[path] = controller[propertyKey] as RouteHandler;
|
|
66
|
+
methods[path]._options = options;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function decoratorCreator(givenPath = '', options?: DecoratorOptions) {
|
|
70
|
+
const path = trimPath(givenPath);
|
|
71
|
+
|
|
72
|
+
function decorator(givenTarget: KnownAny, propertyKey: string) {
|
|
73
|
+
const target = givenTarget as VovkController;
|
|
74
|
+
assignSchema(target, propertyKey, path, options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return decorator;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const auto = (options?: DecoratorOptions) => {
|
|
81
|
+
function decorator(givenTarget: KnownAny, propertyKey: string) {
|
|
82
|
+
const controller = givenTarget as VovkController;
|
|
83
|
+
const methods: Record<string, RouteHandler> = vovkApp.routes[httpMethod].get(controller) ?? {};
|
|
84
|
+
vovkApp.routes[httpMethod].set(controller, methods);
|
|
85
|
+
|
|
86
|
+
controller._handlers = {
|
|
87
|
+
...controller._handlers,
|
|
88
|
+
[propertyKey]: {
|
|
89
|
+
...(controller._handlers ?? {})[propertyKey],
|
|
90
|
+
httpMethod,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
assignSchema(controller, propertyKey, toKebabCase(propertyKey), options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return decorator;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const enhancedDecoratorCreator = decoratorCreator as {
|
|
101
|
+
(...args: Parameters<typeof decoratorCreator>): ReturnType<typeof decoratorCreator>;
|
|
102
|
+
auto: typeof auto;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
enhancedDecoratorCreator.auto = auto;
|
|
106
|
+
|
|
107
|
+
return enhancedDecoratorCreator;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const prefix = (givenPath = '') => {
|
|
111
|
+
const path = trimPath(givenPath);
|
|
112
|
+
|
|
113
|
+
return (givenTarget: KnownAny) => {
|
|
114
|
+
const controller = givenTarget as VovkController;
|
|
115
|
+
controller._prefix = path;
|
|
116
|
+
|
|
117
|
+
return givenTarget;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const initVovk = (options: {
|
|
122
|
+
segmentName?: string;
|
|
123
|
+
controllers: Record<string, StaticClass>;
|
|
124
|
+
exposeValidation?: boolean;
|
|
125
|
+
emitSchema?: boolean;
|
|
126
|
+
onError?: (err: Error, req: VovkRequest) => void | Promise<void>;
|
|
127
|
+
}) => {
|
|
128
|
+
for (const [controllerName, controller] of Object.entries(options.controllers) as [string, VovkController][]) {
|
|
129
|
+
controller._controllerName = controllerName;
|
|
130
|
+
controller._activated = true;
|
|
131
|
+
controller._onError = options?.onError;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function GET_DEV(req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) {
|
|
135
|
+
const params = await data.params;
|
|
136
|
+
if (params[Object.keys(params)[0]]?.[0] === '_schema_') {
|
|
137
|
+
// Wait for schema to be set (it can be set after decorators are called with another setTimeout)
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
139
|
+
const schema = getSchema(options);
|
|
140
|
+
return vovkApp.respond(200, { schema });
|
|
141
|
+
}
|
|
142
|
+
return vovkApp.GET(req, data);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
GET: process.env.NODE_ENV === 'development' ? GET_DEV : vovkApp.GET,
|
|
147
|
+
POST: vovkApp.POST,
|
|
148
|
+
PUT: vovkApp.PUT,
|
|
149
|
+
PATCH: vovkApp.PATCH,
|
|
150
|
+
DELETE: vovkApp.DELETE,
|
|
151
|
+
HEAD: vovkApp.HEAD,
|
|
152
|
+
OPTIONS: vovkApp.OPTIONS,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
get: createHTTPDecorator(HttpMethod.GET),
|
|
158
|
+
post: createHTTPDecorator(HttpMethod.POST),
|
|
159
|
+
put: createHTTPDecorator(HttpMethod.PUT),
|
|
160
|
+
patch: createHTTPDecorator(HttpMethod.PATCH),
|
|
161
|
+
del: createHTTPDecorator(HttpMethod.DELETE),
|
|
162
|
+
head: createHTTPDecorator(HttpMethod.HEAD),
|
|
163
|
+
options: createHTTPDecorator(HttpMethod.OPTIONS),
|
|
164
|
+
prefix,
|
|
165
|
+
initVovk,
|
|
166
|
+
};
|
|
167
|
+
}
|