vovk 3.0.0-draft.5 → 3.0.0-draft.51

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.
Files changed (97) hide show
  1. package/.DS_Store +0 -0
  2. package/.npmignore +2 -1
  3. package/.turbo/turbo-build.log +6 -0
  4. package/.turbo/turbo-ncu.log +9 -0
  5. package/.turbo/turbo-tsc.log +6 -0
  6. package/README.md +1 -111
  7. package/{HttpException.d.ts → dist/HttpException.d.ts} +2 -2
  8. package/{HttpException.js → dist/HttpException.js} +3 -3
  9. package/{StreamResponse.d.ts → dist/StreamJSONResponse.d.ts} +4 -3
  10. package/{StreamResponse.js → dist/StreamJSONResponse.js} +6 -5
  11. package/{Segment.d.ts → dist/VovkApp.d.ts} +3 -3
  12. package/{Segment.js → dist/VovkApp.js} +26 -23
  13. package/dist/client/createRPC.d.ts +4 -0
  14. package/{client/clientizeController.js → dist/client/createRPC.js} +17 -17
  15. package/dist/client/defaultFetcher.d.ts +4 -0
  16. package/{client → dist/client}/defaultFetcher.js +7 -7
  17. package/{client → dist/client}/defaultHandler.d.ts +1 -1
  18. package/dist/client/defaultHandler.js +22 -0
  19. package/dist/client/defaultStreamHandler.d.ts +4 -0
  20. package/{client → dist/client}/defaultStreamHandler.js +5 -5
  21. package/dist/client/index.d.ts +4 -0
  22. package/dist/client/index.js +5 -0
  23. package/dist/client/types.d.ts +100 -0
  24. package/dist/createDecorator.d.ts +4 -0
  25. package/{createDecorator.js → dist/createDecorator.js} +4 -4
  26. package/{createSegment.d.ts → dist/createVovkApp.d.ts} +2 -2
  27. package/{createSegment.js → dist/createVovkApp.js} +25 -25
  28. package/dist/index.d.ts +61 -0
  29. package/dist/index.js +25 -0
  30. package/dist/types.d.ts +157 -0
  31. package/dist/types.js +65 -0
  32. package/dist/utils/generateStaticAPI.d.ts +4 -0
  33. package/{generateStaticAPI.js → dist/utils/generateStaticAPI.js} +3 -3
  34. package/{utils → dist/utils}/getSchema.d.ts +1 -1
  35. package/{utils → dist/utils}/getSchema.js +9 -9
  36. package/dist/utils/reqForm.d.ts +2 -0
  37. package/dist/utils/reqForm.js +13 -0
  38. package/{utils → dist/utils}/reqMeta.d.ts +1 -2
  39. package/{utils → dist/utils}/reqQuery.d.ts +1 -2
  40. package/{utils → dist/utils}/reqQuery.js +3 -3
  41. package/dist/utils/setClientValidatorsForHandler.d.ts +5 -0
  42. package/{utils → dist/utils}/setClientValidatorsForHandler.js +4 -6
  43. package/dist/worker/createWPC.d.ts +2 -0
  44. package/{worker/promisifyWorker.js → dist/worker/createWPC.js} +7 -9
  45. package/dist/worker/index.d.ts +3 -0
  46. package/dist/worker/index.js +7 -0
  47. package/{worker → dist/worker}/types.d.ts +7 -7
  48. package/dist/worker/worker.d.ts +1 -0
  49. package/{worker → dist/worker}/worker.js +2 -3
  50. package/package.json +5 -2
  51. package/src/HttpException.ts +16 -0
  52. package/src/StreamJSONResponse.ts +62 -0
  53. package/src/VovkApp.ts +242 -0
  54. package/src/client/createRPC.ts +133 -0
  55. package/src/client/defaultFetcher.ts +57 -0
  56. package/src/client/defaultHandler.ts +23 -0
  57. package/src/client/defaultStreamHandler.ts +88 -0
  58. package/src/client/index.ts +5 -0
  59. package/src/client/types.ts +115 -0
  60. package/src/createDecorator.ts +60 -0
  61. package/src/createVovkApp.ts +167 -0
  62. package/src/index.ts +66 -0
  63. package/src/types.ts +215 -0
  64. package/src/utils/generateStaticAPI.ts +18 -0
  65. package/src/utils/getSchema.ts +48 -0
  66. package/src/utils/reqForm.ts +16 -0
  67. package/src/utils/reqMeta.ts +16 -0
  68. package/src/utils/reqQuery.ts +26 -0
  69. package/src/utils/setClientValidatorsForHandler.ts +45 -0
  70. package/src/utils/shim.ts +17 -0
  71. package/src/worker/createWPC.ts +156 -0
  72. package/src/worker/index.ts +4 -0
  73. package/src/worker/types.ts +45 -0
  74. package/src/worker/worker.ts +53 -0
  75. package/client/clientizeController.d.ts +0 -4
  76. package/client/defaultFetcher.d.ts +0 -4
  77. package/client/defaultHandler.js +0 -21
  78. package/client/defaultStreamHandler.d.ts +0 -4
  79. package/client/index.d.ts +0 -4
  80. package/client/index.js +0 -5
  81. package/client/types.d.ts +0 -102
  82. package/createDecorator.d.ts +0 -4
  83. package/generateStaticAPI.d.ts +0 -4
  84. package/index.d.ts +0 -60
  85. package/index.js +0 -20
  86. package/types.d.ts +0 -191
  87. package/types.js +0 -65
  88. package/utils/setClientValidatorsForHandler.d.ts +0 -5
  89. package/worker/index.d.ts +0 -3
  90. package/worker/index.js +0 -7
  91. package/worker/promisifyWorker.d.ts +0 -2
  92. package/worker/worker.d.ts +0 -1
  93. /package/{client → dist/client}/types.js +0 -0
  94. /package/{utils → dist/utils}/reqMeta.js +0 -0
  95. /package/{utils → dist/utils}/shim.d.ts +0 -0
  96. /package/{utils → dist/utils}/shim.js +0 -0
  97. /package/{worker → dist/worker}/types.js +0 -0
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = reqQuery;
4
- const clientizeController_1 = require("../client/clientizeController");
4
+ const createRPC_1 = require("../client/createRPC");
5
5
  function reqQuery(req) {
6
- const queryArr = req.nextUrl.searchParams.get(clientizeController_1.ARRAY_QUERY_KEY)?.split(',') ?? null;
6
+ const queryArr = req.nextUrl.searchParams.get(createRPC_1.ARRAY_QUERY_KEY)?.split(',') ?? null;
7
7
  const entries = [...req.nextUrl.searchParams.entries()];
8
8
  const query = entries.reduce((acc, [key, value]) => {
9
- if (key === clientizeController_1.ARRAY_QUERY_KEY)
9
+ if (key === createRPC_1.ARRAY_QUERY_KEY)
10
10
  return acc;
11
11
  if (queryArr?.includes(key)) {
12
12
  if (!(key in acc)) {
@@ -0,0 +1,5 @@
1
+ import type { KnownAny } from '../types';
2
+ export declare function setClientValidatorsForHandler(h: (...args: KnownAny[]) => KnownAny, validation: {
3
+ body: unknown;
4
+ query: unknown;
5
+ }): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = setClientValidatorsForHandler;
4
- function setClientValidatorsForHandler(h, validators) {
3
+ exports.setClientValidatorsForHandler = setClientValidatorsForHandler;
4
+ function setClientValidatorsForHandler(h, validation) {
5
5
  return new Promise((resolve) => {
6
+ // the setTimeout is necessary to ensure that the _controller is already defined
6
7
  setTimeout(() => {
7
8
  const controller = h._controller;
8
9
  if (!controller) {
@@ -16,10 +17,7 @@ function setClientValidatorsForHandler(h, validators) {
16
17
  ...controller._handlers,
17
18
  [handlerName]: {
18
19
  ...controller._handlers[handlerName],
19
- clientValidators: {
20
- body: validators.body,
21
- query: validators.query,
22
- },
20
+ validation,
23
21
  },
24
22
  };
25
23
  resolve();
@@ -0,0 +1,2 @@
1
+ import type { WorkerPromiseInstance } from './types';
2
+ export declare function createWPC<T extends object>(currentWorker: Worker | null, workerSchema: object): WorkerPromiseInstance<T>;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._promisifyWorker = _promisifyWorker;
4
- function _promisifyWorker(currentWorker, givenWorkerService) {
5
- if (!givenWorkerService)
3
+ exports.createWPC = createWPC;
4
+ function createWPC(currentWorker, workerSchema) {
5
+ if (!workerSchema)
6
6
  throw new Error('Worker schema is not provided');
7
- const workerService = givenWorkerService;
7
+ const schema = workerSchema;
8
8
  const instance = {
9
9
  worker: currentWorker,
10
10
  };
@@ -23,11 +23,9 @@ function _promisifyWorker(currentWorker, givenWorkerService) {
23
23
  instance.worker = worker;
24
24
  return instance;
25
25
  };
26
- instance.fork = (worker) => {
27
- return _promisifyWorker(worker, givenWorkerService);
28
- };
29
- for (const methodName of Object.keys(workerService._handlers)) {
30
- const { isGenerator } = workerService._handlers[methodName];
26
+ instance.fork = (worker) => createWPC(worker, schema);
27
+ for (const methodName of Object.keys(schema.handlers)) {
28
+ const { isGenerator } = schema.handlers[methodName];
31
29
  if (isGenerator) {
32
30
  const method = (...args) => {
33
31
  const key = callsKey;
@@ -0,0 +1,3 @@
1
+ import { worker } from './worker';
2
+ import { createWPC } from './createWPC';
3
+ export { worker, createWPC };
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWPC = exports.worker = void 0;
4
+ const worker_1 = require("./worker");
5
+ Object.defineProperty(exports, "worker", { enumerable: true, get: function () { return worker_1.worker; } });
6
+ const createWPC_1 = require("./createWPC");
7
+ Object.defineProperty(exports, "createWPC", { enumerable: true, get: function () { return createWPC_1.createWPC; } });
@@ -1,27 +1,27 @@
1
- import type { _KnownAny as KnownAny } from '../types';
1
+ import type { KnownAny } from '../types';
2
2
  type ToPromise<T> = T extends PromiseLike<unknown> ? T : Promise<T>;
3
3
  type ToAsyncGenerator<T> = T extends AsyncGenerator<unknown, unknown, unknown> ? T : T extends Generator<infer U, unknown, unknown> ? AsyncGenerator<U, unknown, unknown> : AsyncGenerator<T, unknown, unknown>;
4
4
  type ToProperReturnType<T> = T extends Generator<unknown, unknown, unknown> | AsyncGenerator<unknown, unknown, unknown> ? ToAsyncGenerator<T> : ToPromise<T>;
5
5
  type OmitNever<T> = {
6
6
  [K in keyof T as T[K] extends never ? never : K]: T[K];
7
7
  };
8
- export type _WorkerPromiseInstanceWithNever<T> = {
8
+ export type WorkerPromiseInstanceWithNever<T> = {
9
9
  [K in keyof T]: T[K] extends (...args: KnownAny[]) => KnownAny ? (...args: Parameters<T[K]>) => ToProperReturnType<ReturnType<T[K]>> : never;
10
10
  };
11
- export type _WorkerPromiseInstance<T> = OmitNever<_WorkerPromiseInstanceWithNever<T>> & {
11
+ export type WorkerPromiseInstance<T> = OmitNever<WorkerPromiseInstanceWithNever<T>> & {
12
12
  terminate: () => void;
13
- employ: (w: Worker) => _WorkerPromiseInstance<T>;
14
- fork: (w: Worker) => _WorkerPromiseInstance<T>;
13
+ employ: (w: Worker) => WorkerPromiseInstance<T>;
14
+ fork: (w: Worker) => WorkerPromiseInstance<T>;
15
15
  worker: Worker | null;
16
16
  _isTerminated?: true;
17
17
  [Symbol.dispose]: () => void;
18
18
  };
19
- export interface _WorkerInput {
19
+ export interface WorkerInput {
20
20
  methodName: string;
21
21
  args: unknown[];
22
22
  key: number;
23
23
  }
24
- export interface _WorkerOutput {
24
+ export interface WorkerOutput {
25
25
  methodName: string;
26
26
  result?: unknown;
27
27
  error?: unknown;
@@ -0,0 +1 @@
1
+ export declare function worker(): (t: object) => void;
@@ -1,11 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._worker = _worker;
4
- function _worker() {
3
+ exports.worker = worker;
4
+ function worker() {
5
5
  return (t) => {
6
6
  const target = t;
7
7
  target._handlers = {};
8
- // TODO: Experimental: You can pass Worker Service instead of schema to prommisify worker
9
8
  for (const key of Object.getOwnPropertyNames(target)) {
10
9
  const member = target[key];
11
10
  if (typeof member === 'function') {
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "vovk",
3
- "version": "3.0.0-draft.5",
3
+ "version": "3.0.0-draft.51",
4
+ "main": "dist/index.js",
4
5
  "description": "RESTful RPC for Next.js - Transforms Next.js into a powerful REST API platform with RPC capabilities.",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "git+https://github.com/finom/vovk.git"
8
9
  },
9
10
  "scripts": {
10
- "build": "rm -rf dist && tsc && cp {package.json,LICENSE,.npmignore} dist && cp ../../README.md dist",
11
+ "build": "tsc",
12
+ "rm-dist": "shx rm -rf dist",
11
13
  "lint": "eslint . --fix",
14
+ "tsc": "tsc --noEmit",
12
15
  "npm-publish": "if [ -z \"$NPM_TAG\" ]; then echo 'Error: NPM_TAG is not set'; exit 1; fi; cd ./dist && npm publish --tag=$NPM_TAG && cd ..",
13
16
  "ncu": "npm-check-updates -u"
14
17
  },
@@ -0,0 +1,16 @@
1
+ import type { 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
+ }
@@ -0,0 +1,62 @@
1
+ import type { KnownAny, StreamAbortMessage } from './types';
2
+ import './utils/shim';
3
+
4
+ export class StreamJSONResponse<T> extends Response {
5
+ public static defaultHeaders = {
6
+ 'content-type': 'text/plain; charset=utf-8',
7
+ 'x-vovk-stream': 'true',
8
+ };
9
+
10
+ public isClosed = false;
11
+
12
+ public controller?: ReadableStreamDefaultController;
13
+
14
+ public readonly encoder: TextEncoder;
15
+
16
+ public readonly readableStream: ReadableStream;
17
+
18
+ constructor(init?: ResponseInit) {
19
+ const encoder = new TextEncoder();
20
+ let readableController: ReadableStreamDefaultController;
21
+
22
+ const readableStream = new ReadableStream({
23
+ cancel: () => {
24
+ this.isClosed = true;
25
+ },
26
+ start: (controller) => {
27
+ readableController = controller;
28
+ },
29
+ });
30
+
31
+ super(readableStream, {
32
+ ...init,
33
+ headers: init?.headers ?? StreamJSONResponse.defaultHeaders,
34
+ });
35
+
36
+ this.readableStream = readableStream;
37
+ this.encoder = encoder;
38
+ this.controller = readableController!;
39
+ }
40
+
41
+ public send(data: T | StreamAbortMessage) {
42
+ const { controller, encoder } = this;
43
+ if (this.isClosed) return;
44
+ return controller?.enqueue(encoder.encode(JSON.stringify(data) + '\n'));
45
+ }
46
+
47
+ public close() {
48
+ const { controller } = this;
49
+ if (this.isClosed) return;
50
+ this.isClosed = true;
51
+ controller?.close();
52
+ }
53
+
54
+ public throw(e: KnownAny) {
55
+ this.send({ isError: true, reason: e instanceof Error ? e.message : (e as unknown) });
56
+ return this.close();
57
+ }
58
+
59
+ public [Symbol.dispose]() {
60
+ this.close();
61
+ }
62
+ }
package/src/VovkApp.ts ADDED
@@ -0,0 +1,242 @@
1
+ import {
2
+ HttpMethod,
3
+ HttpStatus,
4
+ type RouteHandler,
5
+ type VovkErrorResponse,
6
+ type VovkController,
7
+ type DecoratorOptions,
8
+ type VovkRequest,
9
+ type KnownAny,
10
+ } from './types';
11
+ import { HttpException as HttpException } from './HttpException';
12
+ import { StreamJSONResponse } from './StreamJSONResponse';
13
+ import reqQuery from './utils/reqQuery';
14
+ import reqMeta from './utils/reqMeta';
15
+ import reqForm from './utils/reqForm';
16
+
17
+ export class VovkApp {
18
+ private static getHeadersFromOptions(options?: DecoratorOptions) {
19
+ if (!options) return {};
20
+
21
+ const corsHeaders = {
22
+ 'Access-Control-Allow-Origin': '*',
23
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
24
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
25
+ };
26
+
27
+ const headers = {
28
+ ...(options.cors ? corsHeaders : {}),
29
+ ...(options.headers ?? {}),
30
+ };
31
+
32
+ return headers;
33
+ }
34
+
35
+ routes: Record<HttpMethod, Map<VovkController, Record<string, RouteHandler>>> = {
36
+ GET: new Map(),
37
+ POST: new Map(),
38
+ PUT: new Map(),
39
+ PATCH: new Map(),
40
+ DELETE: new Map(),
41
+ HEAD: new Map(),
42
+ OPTIONS: new Map(),
43
+ };
44
+
45
+ GET = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
46
+ this.#callMethod(HttpMethod.GET, req, await data.params);
47
+
48
+ POST = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
49
+ this.#callMethod(HttpMethod.POST, req, await data.params);
50
+
51
+ PUT = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
52
+ this.#callMethod(HttpMethod.PUT, req, await data.params);
53
+
54
+ PATCH = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
55
+ this.#callMethod(HttpMethod.PATCH, req, await data.params);
56
+
57
+ DELETE = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
58
+ this.#callMethod(HttpMethod.DELETE, req, await data.params);
59
+
60
+ HEAD = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
61
+ this.#callMethod(HttpMethod.HEAD, req, await data.params);
62
+
63
+ OPTIONS = async (req: VovkRequest, data: { params: Promise<Record<string, string[]>> }) =>
64
+ this.#callMethod(HttpMethod.OPTIONS, req, await data.params);
65
+
66
+ respond = (status: HttpStatus, body: unknown, options?: DecoratorOptions) => {
67
+ return new Response(JSON.stringify(body), {
68
+ status,
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ ...VovkApp.getHeadersFromOptions(options),
72
+ },
73
+ });
74
+ };
75
+
76
+ #respondWithError = (statusCode: HttpStatus, message: string, options?: DecoratorOptions, cause?: unknown) => {
77
+ return this.respond(
78
+ statusCode,
79
+ {
80
+ cause,
81
+ statusCode,
82
+ message,
83
+ isError: true,
84
+ } satisfies VovkErrorResponse,
85
+ options
86
+ );
87
+ };
88
+
89
+ #callMethod = async (
90
+ httpMethod: HttpMethod,
91
+ req: VovkRequest<KnownAny, KnownAny>,
92
+ params: Record<string, string[]>
93
+ ) => {
94
+ const controllers = this.routes[httpMethod];
95
+ const methodParams: Record<string, string> = {};
96
+ const path = params[Object.keys(params)[0]];
97
+
98
+ const handlers: Record<string, { staticMethod: RouteHandler; controller: VovkController }> = {};
99
+ controllers.forEach((staticMethods, controller) => {
100
+ const prefix = controller._prefix ?? '';
101
+
102
+ if (!controller._activated) {
103
+ throw new HttpException(
104
+ HttpStatus.INTERNAL_SERVER_ERROR,
105
+ `Controller "${controller.name}" found but not activated`
106
+ );
107
+ }
108
+
109
+ Object.entries(staticMethods).forEach(([path, staticMethod]) => {
110
+ const fullPath = [prefix, path].filter(Boolean).join('/');
111
+ handlers[fullPath] = { staticMethod, controller };
112
+ });
113
+ });
114
+
115
+ const getHandler = () => {
116
+ if (Object.keys(params).length === 0) {
117
+ return handlers[''];
118
+ }
119
+
120
+ const allMethodKeys = Object.keys(handlers);
121
+
122
+ let methodKeys: string[] = [];
123
+
124
+ methodKeys = allMethodKeys
125
+ // First, try to match literal routes exactly.
126
+ .filter((p) => {
127
+ if (p.includes(':')) return false; // Skip parameterized paths
128
+ return p === path.join('/');
129
+ });
130
+
131
+ if (!methodKeys.length) {
132
+ methodKeys = allMethodKeys.filter((p) => {
133
+ const routeSegments = p.split('/');
134
+ if (routeSegments.length !== path.length) return false;
135
+
136
+ for (let i = 0; i < routeSegments.length; i++) {
137
+ const routeSegment = routeSegments[i];
138
+ const pathSegment = path[i];
139
+
140
+ if (routeSegment.startsWith(':')) {
141
+ const parameter = routeSegment.slice(1);
142
+
143
+ if (parameter in methodParams) {
144
+ throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Duplicate parameter "${parameter}"`);
145
+ }
146
+
147
+ // If it's a parameterized segment, capture the parameter value.
148
+ methodParams[parameter] = pathSegment;
149
+ } else if (routeSegment !== pathSegment) {
150
+ // If it's a literal segment and it does not match the corresponding path segment, return false.
151
+ return false;
152
+ }
153
+ }
154
+ return true;
155
+ });
156
+ }
157
+
158
+ if (methodKeys.length > 1) {
159
+ throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Conflicting routes found: ${methodKeys.join(', ')}`);
160
+ }
161
+
162
+ const [methodKey] = methodKeys;
163
+
164
+ if (methodKey) {
165
+ return handlers[methodKey];
166
+ }
167
+
168
+ return null;
169
+ };
170
+
171
+ const handler = getHandler();
172
+
173
+ if (!handler) {
174
+ return this.#respondWithError(HttpStatus.NOT_FOUND, `Route ${path.join('/')} is not found`);
175
+ }
176
+
177
+ const { staticMethod, controller } = handler;
178
+
179
+ req.vovk = {
180
+ body: () => req.json(),
181
+ query: () => reqQuery(req),
182
+ meta: <T = KnownAny>(meta?: T | null) => reqMeta<T>(req, meta),
183
+ form: <T = KnownAny>() => reqForm<T>(req),
184
+ };
185
+
186
+ try {
187
+ const result = await staticMethod.call(controller, req, methodParams);
188
+
189
+ const isIterator =
190
+ typeof result === 'object' &&
191
+ !!result &&
192
+ ((Reflect.has(result, Symbol.iterator) &&
193
+ typeof (result as Iterable<unknown>)[Symbol.iterator] === 'function') ||
194
+ (Reflect.has(result, Symbol.asyncIterator) &&
195
+ typeof (result as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'));
196
+
197
+ if (isIterator && !(result instanceof Array)) {
198
+ const streamResponse = new StreamJSONResponse({
199
+ headers: {
200
+ ...StreamJSONResponse.defaultHeaders,
201
+ ...VovkApp.getHeadersFromOptions(staticMethod._options),
202
+ },
203
+ });
204
+
205
+ void (async () => {
206
+ try {
207
+ for await (const chunk of result as AsyncGenerator<unknown>) {
208
+ streamResponse.send(chunk);
209
+ }
210
+ } catch (e) {
211
+ return streamResponse.throw(e);
212
+ }
213
+
214
+ return streamResponse.close();
215
+ })();
216
+
217
+ return streamResponse;
218
+ }
219
+
220
+ if (result instanceof Response) {
221
+ return result;
222
+ }
223
+
224
+ return this.respond(200, result ?? null, staticMethod._options);
225
+ } catch (e) {
226
+ const err = e as HttpException;
227
+ try {
228
+ await controller._onError?.(err, req);
229
+ } catch (onErrorError) {
230
+ // eslint-disable-next-line no-console
231
+ console.error(onErrorError);
232
+ }
233
+
234
+ if (err.message !== 'NEXT_REDIRECT' && err.message !== 'NEXT_NOT_FOUND') {
235
+ const statusCode = err.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR;
236
+ return this.#respondWithError(statusCode, err.message, staticMethod._options, err.cause);
237
+ }
238
+
239
+ throw e; // if NEXT_REDIRECT or NEXT_NOT_FOUND, rethrow it
240
+ }
241
+ };
242
+ }
@@ -0,0 +1,133 @@
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
+
14
+ // TODO Ugly workaround. Need your ideas how to distinguish between array and non-array query params
15
+ export const ARRAY_QUERY_KEY = '_vovkarr';
16
+
17
+ const trimPath = (path: string) => path.trim().replace(/^\/|\/$/g, '');
18
+
19
+ const getHandlerPath = <T extends ControllerStaticMethod>(
20
+ endpoint: string,
21
+ params?: VovkControllerParams<T>,
22
+ query?: VovkControllerQuery<T>
23
+ ) => {
24
+ let result = endpoint;
25
+ for (const [key, value] of Object.entries(params ?? {})) {
26
+ result = result.replace(`:${key}`, value as string);
27
+ }
28
+
29
+ const searchParams = new URLSearchParams();
30
+ let hasQuery = false;
31
+ const arrayKeys: string[] = [];
32
+ for (const [key, value] of Object.entries(query ?? {})) {
33
+ if (typeof value === 'undefined') continue;
34
+ if (value instanceof Array) {
35
+ arrayKeys.push(key);
36
+ for (const val of value) {
37
+ searchParams.append(key, val);
38
+ }
39
+ } else {
40
+ searchParams.set(key, value);
41
+ }
42
+ hasQuery = true;
43
+ }
44
+
45
+ if (arrayKeys.length) {
46
+ searchParams.set(ARRAY_QUERY_KEY, arrayKeys.join(','));
47
+ }
48
+
49
+ return `${result}${hasQuery ? '?' : ''}${searchParams.toString()}`;
50
+ };
51
+
52
+ export const createRPC = <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions>(
53
+ controllerSchema: VovkControllerSchema,
54
+ segmentName?: string,
55
+ options?: VovkClientOptions<OPTS>
56
+ ): VovkClient<T, OPTS> => {
57
+ const schema = controllerSchema as T & VovkControllerSchema;
58
+ const client = {} as VovkClient<T, OPTS>;
59
+ if (!schema) throw new Error(`Unable to clientize. Controller schema is not provided`);
60
+ if (!schema.handlers)
61
+ throw new Error(`Unable to clientize. No schema for controller ${String(schema?.controllerName)} provided`);
62
+ const controllerPrefix = trimPath(schema.prefix ?? '');
63
+ const { fetcher: settingsFetcher = defaultFetcher } = options ?? {};
64
+
65
+ for (const [staticMethodName, { path, httpMethod, validation }] of Object.entries(schema.handlers)) {
66
+ const getEndpoint = ({
67
+ apiRoot,
68
+ params,
69
+ query,
70
+ }: {
71
+ apiRoot: string;
72
+ params: { [key: string]: string };
73
+ query: { [key: string]: string };
74
+ }) => {
75
+ const mainPrefix =
76
+ (apiRoot.startsWith('http://') || apiRoot.startsWith('https://') || apiRoot.startsWith('/') ? '' : '/') +
77
+ (apiRoot.endsWith('/') ? apiRoot : `${apiRoot}/`) +
78
+ (segmentName ? `${segmentName}/` : '');
79
+ return mainPrefix + getHandlerPath([controllerPrefix, path].filter(Boolean).join('/'), params, query);
80
+ };
81
+
82
+ const handler = (
83
+ input: {
84
+ body?: unknown;
85
+ query?: { [key: string]: string };
86
+ params?: { [key: string]: string };
87
+ validateOnClient?: VovkValidateOnClient;
88
+ fetcher?: VovkClientOptions<OPTS>['fetcher'];
89
+ transform?: (response: unknown) => unknown;
90
+ } & OPTS = {} as OPTS
91
+ ) => {
92
+ const fetcher = input.fetcher ?? settingsFetcher;
93
+ const validate = async ({ body, query, endpoint }: { body?: unknown; query?: unknown; endpoint: string }) => {
94
+ await (input.validateOnClient ?? options?.validateOnClient)?.({ body, query, endpoint }, validation ?? {});
95
+ };
96
+
97
+ const internalOptions: Parameters<typeof fetcher>[0] = {
98
+ name: staticMethodName as keyof T,
99
+ httpMethod,
100
+ getEndpoint,
101
+ validate,
102
+ defaultHandler,
103
+ defaultStreamHandler,
104
+ };
105
+ const internalInput = {
106
+ ...options?.defaultOptions,
107
+ ...input,
108
+ body: input.body ?? null,
109
+ query: input.query ?? {},
110
+ params: input.params ?? {},
111
+ // TS workaround
112
+ fetcher: undefined,
113
+ validateOnClient: undefined,
114
+ };
115
+
116
+ delete internalInput.fetcher;
117
+ delete internalInput.validateOnClient;
118
+
119
+ if (!fetcher) throw new Error('Fetcher is not provided');
120
+
121
+ const fetcherPromise = fetcher(internalOptions, internalInput) as Promise<unknown>;
122
+
123
+ if (!(fetcherPromise instanceof Promise)) return Promise.resolve(fetcherPromise);
124
+
125
+ return input.transform ? fetcherPromise.then(input.transform) : fetcherPromise;
126
+ };
127
+
128
+ // @ts-expect-error TODO
129
+ client[staticMethodName] = handler;
130
+ }
131
+
132
+ return client;
133
+ };
@@ -0,0 +1,57 @@
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
+ if (response.headers.get('content-type')?.includes('application/json')) {
47
+ return defaultHandler(response);
48
+ }
49
+
50
+ if (response.headers.get('x-vovk-stream') === 'true') {
51
+ return defaultStreamHandler(response);
52
+ }
53
+
54
+ return response;
55
+ };
56
+
57
+ export default defaultFetcher;