vovk 3.0.0-beta.2 → 3.0.0-beta.20
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/.npmignore +2 -0
- package/HttpException.d.ts +7 -0
- package/HttpException.js +15 -0
- package/LICENSE +28 -0
- package/README.md +111 -1
- package/Segment.d.ts +28 -0
- package/Segment.js +184 -0
- package/StreamResponse.d.ts +16 -0
- package/StreamResponse.js +53 -0
- package/client/clientizeController.d.ts +4 -0
- package/client/clientizeController.js +92 -0
- package/client/defaultFetcher.d.ts +4 -0
- package/client/defaultFetcher.js +49 -0
- package/client/defaultHandler.d.ts +2 -0
- package/client/defaultHandler.js +21 -0
- package/client/defaultStreamHandler.d.ts +4 -0
- package/client/defaultStreamHandler.js +82 -0
- package/client/{index.ts → index.d.ts} +1 -6
- package/client/index.js +5 -0
- package/client/types.d.ts +101 -0
- package/client/types.js +2 -0
- package/createDecorator.d.ts +4 -0
- package/createDecorator.js +38 -0
- package/createSegment.d.ts +62 -0
- package/createSegment.js +153 -0
- package/generateStaticAPI.d.ts +3 -0
- package/generateStaticAPI.js +19 -0
- package/index.d.ts +60 -0
- package/index.js +20 -0
- package/package.json +3 -3
- package/types.d.ts +169 -0
- package/types.js +65 -0
- package/utils/reqMeta.d.ts +3 -0
- package/utils/reqMeta.js +13 -0
- package/utils/reqQuery.d.ts +3 -0
- package/utils/reqQuery.js +25 -0
- package/utils/setClientValidatorsForHandler.d.ts +5 -0
- package/utils/setClientValidatorsForHandler.js +28 -0
- package/utils/shim.d.ts +0 -0
- package/utils/shim.js +17 -0
- package/worker/{index.ts → index.d.ts} +0 -1
- package/worker/index.js +7 -0
- package/worker/promisifyWorker.d.ts +2 -0
- package/worker/promisifyWorker.js +142 -0
- package/worker/types.d.ts +31 -0
- package/worker/types.js +2 -0
- package/worker/worker.d.ts +1 -0
- package/worker/worker.js +45 -0
- 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/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/promisifyWorker.ts +0 -159
- package/worker/types.ts +0 -45
- package/worker/worker.ts +0 -55
package/.npmignore
ADDED
package/HttpException.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._HttpException = void 0;
|
|
4
|
+
class _HttpException extends Error {
|
|
5
|
+
statusCode;
|
|
6
|
+
message;
|
|
7
|
+
cause;
|
|
8
|
+
constructor(statusCode, message, cause) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
this.message = message;
|
|
12
|
+
this.cause = cause;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports._HttpException = _HttpException;
|
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vovk",
|
|
3
|
+
"version": "3.0.0-beta.9",
|
|
4
|
+
"description": "RESTful RPC for Next.js - Transforms Next.js into a powerful REST API platform with RPC capabilities.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/finom/vovk.git"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rm -rf dist && tsc && cp {package.json,LICENSE} && cp ../../README.md dist ",
|
|
11
|
+
"lint": "eslint . --fix",
|
|
12
|
+
"npm-publish": "npm publish ./dist --dry-run",
|
|
13
|
+
"ncu": "npm-check-updates -u"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nextjs",
|
|
17
|
+
"router"
|
|
18
|
+
],
|
|
19
|
+
"author": "Andrii Gubanov",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/finom/vovk/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://vovk.dev",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"next": "*"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/README.md
CHANGED
|
@@ -1 +1,111 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source width="300" media="(prefers-color-scheme: dark)" srcset="https://vovk.dev/vovk-logo-white.svg">
|
|
4
|
+
<source width="300" media="(prefers-color-scheme: light)" srcset="https://vovk.dev/vovk-logo.svg">
|
|
5
|
+
<img width="300" alt="vovk" src="https://vovk.dev/vovk-logo.svg">
|
|
6
|
+
</picture><br>
|
|
7
|
+
<strong>RESTful RPC for Next.js</strong>
|
|
8
|
+
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
Transforms <a href="https://nextjs.org/docs/app">Next.js</a> into a powerful REST API platform with RPC capabilities.
|
|
13
|
+
<br><br>
|
|
14
|
+
ℹ️ Improved syntax for Zod and DTO validation is coming soon. Stay tuned!
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="https://vovk.dev/">Documentation</a>
|
|
19
|
+
<a href="https://discord.gg/qdT8WEHUuP">Discord</a>
|
|
20
|
+
<a href="https://github.com/finom/vovk-examples">Code Examples</a>
|
|
21
|
+
<a href="https://github.com/finom/vovk-zod">vovk-zod</a>
|
|
22
|
+
<a href="https://github.com/finom/vovk-hello-world">vovk-hello-world</a>
|
|
23
|
+
<a href="https://github.com/finom/vovk-react-native-example">vovk-react-native-example</a>
|
|
24
|
+
</p>
|
|
25
|
+
<p align="center">
|
|
26
|
+
<a href="https://www.npmjs.com/package/vovk"><img src="https://badge.fury.io/js/vovk.svg" alt="npm version" /></a>
|
|
27
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg" alt="TypeScript" /></a>
|
|
28
|
+
<a href="https://github.com/finom/vovk/actions/workflows/main.yml"><img src="https://github.com/finom/vovk/actions/workflows/main.yml/badge.svg" alt="Build status" /></a>
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<br />
|
|
33
|
+
|
|
34
|
+
Example back-end Controller Class:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// /src/modules/post/PostController.ts
|
|
38
|
+
import { get, prefix, type VovkRequest } from 'vovk';
|
|
39
|
+
import PostService from './PostService';
|
|
40
|
+
|
|
41
|
+
@prefix('posts')
|
|
42
|
+
export default class PostController {
|
|
43
|
+
/**
|
|
44
|
+
* Create a comment on a post
|
|
45
|
+
* POST /api/posts/:postId/comments
|
|
46
|
+
*/
|
|
47
|
+
@post(':postId/comments')
|
|
48
|
+
static async createComment(
|
|
49
|
+
// decorate NextRequest type with body and query types
|
|
50
|
+
req: VovkRequest<
|
|
51
|
+
{ content: string; userId: string },
|
|
52
|
+
{ notificationType: 'push' | 'email' }
|
|
53
|
+
>,
|
|
54
|
+
{ postId }: { postId: string } // params
|
|
55
|
+
) {
|
|
56
|
+
// use standard Next.js API to get body and query
|
|
57
|
+
const { content, userId } = await req.json();
|
|
58
|
+
const notificationType = req.nextUrl.searchParams.get('notificationType');
|
|
59
|
+
|
|
60
|
+
// perform the request to the database in a custom service
|
|
61
|
+
return PostService.createComment({
|
|
62
|
+
postId, content, userId, notificationType,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Example component that uses the auto-generated client library:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
'use client';
|
|
72
|
+
import { useState } from 'react';
|
|
73
|
+
import { PostController } from 'vovk-client';
|
|
74
|
+
import type { VovkReturnType } from 'vovk';
|
|
75
|
+
|
|
76
|
+
export default function Example() {
|
|
77
|
+
const [response, setResponse] = useState<VovkReturnType<typeof PostController.createComment>>();
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<button
|
|
82
|
+
onClick={async () => setResponse(
|
|
83
|
+
await PostController.createComment({
|
|
84
|
+
body: {
|
|
85
|
+
content: 'Hello, World!',
|
|
86
|
+
userId: '1',
|
|
87
|
+
},
|
|
88
|
+
params: { postId: '69' },
|
|
89
|
+
query: { notificationType: 'push' }
|
|
90
|
+
})
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
Post a comment
|
|
94
|
+
</button>
|
|
95
|
+
<div>{JSON.stringify(response)}</div>
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Alternatively, the resource can be fetched wit the regular `fetch` function:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
fetch('/api/posts/69?notificationType=push', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
content: 'Hello, World!',
|
|
108
|
+
userId: '1',
|
|
109
|
+
}),
|
|
110
|
+
})
|
|
111
|
+
```
|
package/Segment.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { _HttpMethod as HttpMethod, _HttpStatus as HttpStatus, type _RouteHandler as RouteHandler, type _VovkController as VovkController, type _DecoratorOptions as DecoratorOptions, type _VovkRequest as VovkRequest } from './types';
|
|
2
|
+
export declare class _Segment {
|
|
3
|
+
#private;
|
|
4
|
+
private static getHeadersFromOptions;
|
|
5
|
+
_routes: Record<HttpMethod, Map<VovkController, Record<string, RouteHandler>>>;
|
|
6
|
+
GET: (req: VovkRequest, data: {
|
|
7
|
+
params: Record<string, string[]>;
|
|
8
|
+
}) => Promise<Response>;
|
|
9
|
+
POST: (req: VovkRequest, data: {
|
|
10
|
+
params: Record<string, string[]>;
|
|
11
|
+
}) => Promise<Response>;
|
|
12
|
+
PUT: (req: VovkRequest, data: {
|
|
13
|
+
params: Record<string, string[]>;
|
|
14
|
+
}) => Promise<Response>;
|
|
15
|
+
PATCH: (req: VovkRequest, data: {
|
|
16
|
+
params: Record<string, string[]>;
|
|
17
|
+
}) => Promise<Response>;
|
|
18
|
+
DELETE: (req: VovkRequest, data: {
|
|
19
|
+
params: Record<string, string[]>;
|
|
20
|
+
}) => Promise<Response>;
|
|
21
|
+
HEAD: (req: VovkRequest, data: {
|
|
22
|
+
params: Record<string, string[]>;
|
|
23
|
+
}) => Promise<Response>;
|
|
24
|
+
OPTIONS: (req: VovkRequest, data: {
|
|
25
|
+
params: Record<string, string[]>;
|
|
26
|
+
}) => Promise<Response>;
|
|
27
|
+
respond: (status: HttpStatus, body: unknown, options?: DecoratorOptions) => Response;
|
|
28
|
+
}
|
package/Segment.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
var _a;
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports._Segment = void 0;
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
const HttpException_1 = require("./HttpException");
|
|
10
|
+
const StreamResponse_1 = require("./StreamResponse");
|
|
11
|
+
const reqQuery_1 = __importDefault(require("./utils/reqQuery"));
|
|
12
|
+
const reqMeta_1 = __importDefault(require("./utils/reqMeta"));
|
|
13
|
+
class _Segment {
|
|
14
|
+
static getHeadersFromOptions(options) {
|
|
15
|
+
if (!options)
|
|
16
|
+
return {};
|
|
17
|
+
const corsHeaders = {
|
|
18
|
+
'Access-Control-Allow-Origin': '*',
|
|
19
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
|
|
20
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
21
|
+
};
|
|
22
|
+
const headers = {
|
|
23
|
+
...(options.cors ? corsHeaders : {}),
|
|
24
|
+
...(options.headers ?? {}),
|
|
25
|
+
};
|
|
26
|
+
return headers;
|
|
27
|
+
}
|
|
28
|
+
_routes = {
|
|
29
|
+
GET: new Map(),
|
|
30
|
+
POST: new Map(),
|
|
31
|
+
PUT: new Map(),
|
|
32
|
+
PATCH: new Map(),
|
|
33
|
+
DELETE: new Map(),
|
|
34
|
+
HEAD: new Map(),
|
|
35
|
+
OPTIONS: new Map(),
|
|
36
|
+
};
|
|
37
|
+
GET = (req, data) => {
|
|
38
|
+
return this.#callMethod(types_1._HttpMethod.GET, req, data.params);
|
|
39
|
+
};
|
|
40
|
+
POST = (req, data) => this.#callMethod(types_1._HttpMethod.POST, req, data.params);
|
|
41
|
+
PUT = (req, data) => this.#callMethod(types_1._HttpMethod.PUT, req, data.params);
|
|
42
|
+
PATCH = (req, data) => this.#callMethod(types_1._HttpMethod.PATCH, req, data.params);
|
|
43
|
+
DELETE = (req, data) => this.#callMethod(types_1._HttpMethod.DELETE, req, data.params);
|
|
44
|
+
HEAD = (req, data) => this.#callMethod(types_1._HttpMethod.HEAD, req, data.params);
|
|
45
|
+
OPTIONS = (req, data) => this.#callMethod(types_1._HttpMethod.OPTIONS, req, data.params);
|
|
46
|
+
respond = (status, body, options) => {
|
|
47
|
+
return new Response(JSON.stringify(body), {
|
|
48
|
+
status,
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
..._a.getHeadersFromOptions(options),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
#respondWithError = (statusCode, message, options) => {
|
|
56
|
+
return this.respond(statusCode, {
|
|
57
|
+
statusCode,
|
|
58
|
+
message,
|
|
59
|
+
isError: true,
|
|
60
|
+
}, options);
|
|
61
|
+
};
|
|
62
|
+
#callMethod = async (httpMethod, req, params) => {
|
|
63
|
+
const controllers = this._routes[httpMethod];
|
|
64
|
+
const methodParams = {};
|
|
65
|
+
const path = params[Object.keys(params)[0]];
|
|
66
|
+
const handlers = {};
|
|
67
|
+
controllers.forEach((staticMethods, controller) => {
|
|
68
|
+
const prefix = controller._prefix ?? '';
|
|
69
|
+
if (!controller._activated) {
|
|
70
|
+
throw new HttpException_1._HttpException(types_1._HttpStatus.INTERNAL_SERVER_ERROR, `Controller "${controller.name}" found but not activated`);
|
|
71
|
+
}
|
|
72
|
+
Object.entries(staticMethods).forEach(([path, staticMethod]) => {
|
|
73
|
+
const fullPath = [prefix, path].filter(Boolean).join('/');
|
|
74
|
+
handlers[fullPath] = { staticMethod, controller };
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
const getHandler = () => {
|
|
78
|
+
if (Object.keys(params).length === 0) {
|
|
79
|
+
return handlers[''];
|
|
80
|
+
}
|
|
81
|
+
const allMethodKeys = Object.keys(handlers);
|
|
82
|
+
let methodKeys = [];
|
|
83
|
+
methodKeys = allMethodKeys
|
|
84
|
+
// First, try to match literal routes exactly.
|
|
85
|
+
.filter((p) => {
|
|
86
|
+
if (p.includes(':'))
|
|
87
|
+
return false; // Skip parameterized paths
|
|
88
|
+
return p === path.join('/');
|
|
89
|
+
});
|
|
90
|
+
if (!methodKeys.length) {
|
|
91
|
+
methodKeys = allMethodKeys.filter((p) => {
|
|
92
|
+
const routeSegments = p.split('/');
|
|
93
|
+
if (routeSegments.length !== path.length)
|
|
94
|
+
return false;
|
|
95
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
96
|
+
const routeSegment = routeSegments[i];
|
|
97
|
+
const pathSegment = path[i];
|
|
98
|
+
if (routeSegment.startsWith(':')) {
|
|
99
|
+
const parameter = routeSegment.slice(1);
|
|
100
|
+
if (parameter in methodParams) {
|
|
101
|
+
throw new HttpException_1._HttpException(types_1._HttpStatus.INTERNAL_SERVER_ERROR, `Duplicate parameter "${parameter}"`);
|
|
102
|
+
}
|
|
103
|
+
// If it's a parameterized segment, capture the parameter value.
|
|
104
|
+
methodParams[parameter] = pathSegment;
|
|
105
|
+
}
|
|
106
|
+
else if (routeSegment !== pathSegment) {
|
|
107
|
+
// If it's a literal segment and it does not match the corresponding path segment, return false.
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (methodKeys.length > 1) {
|
|
115
|
+
throw new HttpException_1._HttpException(types_1._HttpStatus.INTERNAL_SERVER_ERROR, `Conflicting routes found: ${methodKeys.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
const [methodKey] = methodKeys;
|
|
118
|
+
if (methodKey) {
|
|
119
|
+
return handlers[methodKey];
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
const handler = getHandler();
|
|
124
|
+
if (!handler) {
|
|
125
|
+
return this.#respondWithError(types_1._HttpStatus.NOT_FOUND, `Route ${path.join('/')} is not found`);
|
|
126
|
+
}
|
|
127
|
+
const { staticMethod, controller } = handler;
|
|
128
|
+
req.vovk = {
|
|
129
|
+
body: () => req.json(),
|
|
130
|
+
query: () => (0, reqQuery_1.default)(req),
|
|
131
|
+
meta: (metadata) => (0, reqMeta_1.default)(req, metadata),
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
const result = await staticMethod.call(controller, req, methodParams);
|
|
135
|
+
const isIterator = typeof result === 'object' &&
|
|
136
|
+
!!result &&
|
|
137
|
+
((Reflect.has(result, Symbol.iterator) &&
|
|
138
|
+
typeof result[Symbol.iterator] === 'function') ||
|
|
139
|
+
(Reflect.has(result, Symbol.asyncIterator) &&
|
|
140
|
+
typeof result[Symbol.asyncIterator] === 'function'));
|
|
141
|
+
if (isIterator && !(result instanceof Array)) {
|
|
142
|
+
const streamResponse = new StreamResponse_1._StreamResponse({
|
|
143
|
+
headers: {
|
|
144
|
+
...StreamResponse_1._StreamResponse.defaultHeaders,
|
|
145
|
+
..._a.getHeadersFromOptions(staticMethod._options),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
void (async () => {
|
|
149
|
+
try {
|
|
150
|
+
for await (const chunk of result) {
|
|
151
|
+
streamResponse.send(chunk);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
return streamResponse.throw(e);
|
|
156
|
+
}
|
|
157
|
+
return streamResponse.close();
|
|
158
|
+
})();
|
|
159
|
+
return streamResponse;
|
|
160
|
+
}
|
|
161
|
+
if (result instanceof Response) {
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
return this.respond(200, result ?? null, staticMethod._options);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
const err = e;
|
|
168
|
+
try {
|
|
169
|
+
await controller._onError?.(err, req);
|
|
170
|
+
}
|
|
171
|
+
catch (onErrorError) {
|
|
172
|
+
// eslint-disable-next-line no-console
|
|
173
|
+
console.error(onErrorError);
|
|
174
|
+
}
|
|
175
|
+
if (err.message !== 'NEXT_REDIRECT' && err.message !== 'NEXT_NOT_FOUND') {
|
|
176
|
+
const statusCode = err.statusCode ?? types_1._HttpStatus.INTERNAL_SERVER_ERROR;
|
|
177
|
+
return this.#respondWithError(statusCode, err.message, staticMethod._options);
|
|
178
|
+
}
|
|
179
|
+
throw e; // if NEXT_REDIRECT or NEXT_NOT_FOUND, rethrow it
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
exports._Segment = _Segment;
|
|
184
|
+
_a = _Segment;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { _KnownAny as KnownAny, _StreamAbortMessage as StreamAbortMessage } from './types';
|
|
2
|
+
import './utils/shim';
|
|
3
|
+
export declare class _StreamResponse<T> extends Response {
|
|
4
|
+
static defaultHeaders: {
|
|
5
|
+
'Content-Type': string;
|
|
6
|
+
};
|
|
7
|
+
isClosed: boolean;
|
|
8
|
+
controller?: ReadableStreamDefaultController;
|
|
9
|
+
readonly encoder: TextEncoder;
|
|
10
|
+
readonly readableStream: ReadableStream;
|
|
11
|
+
constructor(init?: ResponseInit);
|
|
12
|
+
send(data: T | StreamAbortMessage): void;
|
|
13
|
+
close(): void;
|
|
14
|
+
throw(e: KnownAny): void;
|
|
15
|
+
[Symbol.dispose](): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._StreamResponse = void 0;
|
|
4
|
+
require("./utils/shim");
|
|
5
|
+
class _StreamResponse extends Response {
|
|
6
|
+
static defaultHeaders = {
|
|
7
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
8
|
+
};
|
|
9
|
+
isClosed = false;
|
|
10
|
+
controller;
|
|
11
|
+
encoder;
|
|
12
|
+
readableStream;
|
|
13
|
+
constructor(init) {
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
let readableController;
|
|
16
|
+
const readableStream = new ReadableStream({
|
|
17
|
+
cancel: () => {
|
|
18
|
+
this.isClosed = true;
|
|
19
|
+
},
|
|
20
|
+
start: (controller) => {
|
|
21
|
+
readableController = controller;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
super(readableStream, {
|
|
25
|
+
...init,
|
|
26
|
+
headers: init?.headers ?? _StreamResponse.defaultHeaders,
|
|
27
|
+
});
|
|
28
|
+
this.readableStream = readableStream;
|
|
29
|
+
this.encoder = encoder;
|
|
30
|
+
this.controller = readableController;
|
|
31
|
+
}
|
|
32
|
+
send(data) {
|
|
33
|
+
const { controller, encoder } = this;
|
|
34
|
+
if (this.isClosed)
|
|
35
|
+
return;
|
|
36
|
+
return controller?.enqueue(encoder.encode(JSON.stringify(data) + '\n'));
|
|
37
|
+
}
|
|
38
|
+
close() {
|
|
39
|
+
const { controller } = this;
|
|
40
|
+
if (this.isClosed)
|
|
41
|
+
return;
|
|
42
|
+
this.isClosed = true;
|
|
43
|
+
controller?.close();
|
|
44
|
+
}
|
|
45
|
+
throw(e) {
|
|
46
|
+
this.send({ isError: true, reason: e instanceof Error ? e.message : e });
|
|
47
|
+
return this.close();
|
|
48
|
+
}
|
|
49
|
+
[Symbol.dispose]() {
|
|
50
|
+
this.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports._StreamResponse = _StreamResponse;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type _VovkControllerMetadata as VovkControllerMetadata, type _KnownAny as KnownAny } from '../types';
|
|
2
|
+
import { type _VovkClientOptions as VovkClientOptions, type _VovkClient as VovkClient, type _VovkDefaultFetcherOptions as VovkDefaultFetcherOptions } from './types';
|
|
3
|
+
export declare const ARRAY_QUERY_KEY = "_vovkarr";
|
|
4
|
+
export declare const _clientizeController: <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions>(givenController: VovkControllerMetadata, segmentName: string, options?: VovkClientOptions<OPTS>) => VovkClient<T, OPTS>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports._clientizeController = exports.ARRAY_QUERY_KEY = void 0;
|
|
7
|
+
const defaultFetcher_1 = __importDefault(require("./defaultFetcher"));
|
|
8
|
+
const defaultHandler_1 = require("./defaultHandler");
|
|
9
|
+
const defaultStreamHandler_1 = require("./defaultStreamHandler");
|
|
10
|
+
exports.ARRAY_QUERY_KEY = '_vovkarr';
|
|
11
|
+
const trimPath = (path) => path.trim().replace(/^\/|\/$/g, '');
|
|
12
|
+
const getHandlerPath = (endpoint, params, query) => {
|
|
13
|
+
let result = endpoint;
|
|
14
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
15
|
+
result = result.replace(`:${key}`, value);
|
|
16
|
+
}
|
|
17
|
+
const searchParams = new URLSearchParams();
|
|
18
|
+
let hasQuery = false;
|
|
19
|
+
const arrayKeys = [];
|
|
20
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
21
|
+
if (value instanceof Array) {
|
|
22
|
+
arrayKeys.push(key);
|
|
23
|
+
for (const val of value) {
|
|
24
|
+
searchParams.append(key, val);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
searchParams.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
hasQuery = true;
|
|
31
|
+
}
|
|
32
|
+
if (arrayKeys.length) {
|
|
33
|
+
searchParams.set(exports.ARRAY_QUERY_KEY, arrayKeys.join(','));
|
|
34
|
+
}
|
|
35
|
+
return `${result}${hasQuery ? '?' : ''}${searchParams.toString()}`;
|
|
36
|
+
};
|
|
37
|
+
const _clientizeController = (givenController, segmentName, options) => {
|
|
38
|
+
const controller = givenController;
|
|
39
|
+
const client = {};
|
|
40
|
+
if (!controller)
|
|
41
|
+
throw new Error(`Unable to clientize. Controller metadata is not provided`);
|
|
42
|
+
const metadata = controller._handlers;
|
|
43
|
+
if (!metadata)
|
|
44
|
+
throw new Error(`Unable to clientize. No metadata for controller ${String(controller?._controllerName)}`);
|
|
45
|
+
const controllerPrefix = trimPath(controller._prefix ?? '');
|
|
46
|
+
const { fetcher: settingsFetcher = defaultFetcher_1.default } = options ?? {};
|
|
47
|
+
for (const [staticMethodName, { path, httpMethod, clientValidators }] of Object.entries(metadata)) {
|
|
48
|
+
const getEndpoint = ({ prefix, params, query, }) => {
|
|
49
|
+
const mainPrefix = (prefix.startsWith('http://') || prefix.startsWith('https://') || prefix.startsWith('/') ? '' : '/') +
|
|
50
|
+
(prefix.endsWith('/') ? prefix : `${prefix}/`) +
|
|
51
|
+
(segmentName ? `${segmentName}/` : '');
|
|
52
|
+
return mainPrefix + getHandlerPath([controllerPrefix, path].filter(Boolean).join('/'), params, query);
|
|
53
|
+
};
|
|
54
|
+
const handler = (input = {}) => {
|
|
55
|
+
const fetcher = input.fetcher ?? settingsFetcher;
|
|
56
|
+
const validate = async ({ body, query, endpoint }) => {
|
|
57
|
+
await (input.validateOnClient ?? options?.validateOnClient)?.({ body, query, endpoint }, clientValidators ?? {});
|
|
58
|
+
};
|
|
59
|
+
const internalOptions = {
|
|
60
|
+
name: staticMethodName,
|
|
61
|
+
httpMethod,
|
|
62
|
+
getEndpoint,
|
|
63
|
+
validate,
|
|
64
|
+
defaultHandler: defaultHandler_1._defaultHandler,
|
|
65
|
+
defaultStreamHandler: defaultStreamHandler_1._defaultStreamHandler,
|
|
66
|
+
};
|
|
67
|
+
const internalInput = {
|
|
68
|
+
...options?.defaultOptions,
|
|
69
|
+
...input,
|
|
70
|
+
body: input.body ?? null,
|
|
71
|
+
query: input.query ?? {},
|
|
72
|
+
params: input.params ?? {},
|
|
73
|
+
segmentName,
|
|
74
|
+
// TS workaround
|
|
75
|
+
fetcher: undefined,
|
|
76
|
+
validateOnClient: undefined,
|
|
77
|
+
};
|
|
78
|
+
delete internalInput.fetcher;
|
|
79
|
+
delete internalInput.validateOnClient;
|
|
80
|
+
if (!fetcher)
|
|
81
|
+
throw new Error('Fetcher is not provided');
|
|
82
|
+
const fetcherPromise = fetcher(internalOptions, internalInput);
|
|
83
|
+
if (!(fetcherPromise instanceof Promise))
|
|
84
|
+
return Promise.resolve(fetcherPromise);
|
|
85
|
+
return input.transform ? fetcherPromise.then(input.transform) : fetcherPromise;
|
|
86
|
+
};
|
|
87
|
+
// @ts-expect-error TODO: Fix this
|
|
88
|
+
client[staticMethodName] = handler;
|
|
89
|
+
}
|
|
90
|
+
return client;
|
|
91
|
+
};
|
|
92
|
+
exports._clientizeController = _clientizeController;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { _VovkDefaultFetcherOptions as VovkDefaultFetcherOptions, _VovkClientFetcher as VovkClientFetcher } from './types';
|
|
2
|
+
export declare const DEFAULT_ERROR_MESSAGE = "Unknown error at defaultFetcher";
|
|
3
|
+
declare const defaultFetcher: VovkClientFetcher<VovkDefaultFetcherOptions>;
|
|
4
|
+
export default defaultFetcher;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_ERROR_MESSAGE = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const HttpException_1 = require("../HttpException");
|
|
6
|
+
exports.DEFAULT_ERROR_MESSAGE = 'Unknown error at defaultFetcher';
|
|
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 = async ({ httpMethod, getEndpoint, validate, defaultHandler, defaultStreamHandler }, { params, query, body, prefix = '/api', segmentName = '', ...options }) => {
|
|
10
|
+
const endpoint = getEndpoint({ prefix, segmentName, params, query });
|
|
11
|
+
if (!options.disableClientValidation) {
|
|
12
|
+
try {
|
|
13
|
+
await validate({ body, query, endpoint });
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
// if HttpException is thrown, rethrow it
|
|
17
|
+
if (e instanceof HttpException_1._HttpException)
|
|
18
|
+
throw e;
|
|
19
|
+
// otherwise, throw HttpException with status 0
|
|
20
|
+
throw new HttpException_1._HttpException(types_1._HttpStatus.NULL, e.message ?? exports.DEFAULT_ERROR_MESSAGE);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const init = {
|
|
24
|
+
method: httpMethod,
|
|
25
|
+
...options,
|
|
26
|
+
};
|
|
27
|
+
if (body instanceof FormData) {
|
|
28
|
+
init.body = body;
|
|
29
|
+
}
|
|
30
|
+
else if (body) {
|
|
31
|
+
init.body = JSON.stringify(body);
|
|
32
|
+
}
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
response = await fetch(endpoint, init);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
// handle network errors
|
|
39
|
+
throw new HttpException_1._HttpException(types_1._HttpStatus.NULL, e?.message ?? exports.DEFAULT_ERROR_MESSAGE);
|
|
40
|
+
}
|
|
41
|
+
if (response.headers.get('content-type')?.includes('application/json')) {
|
|
42
|
+
return defaultHandler(response);
|
|
43
|
+
}
|
|
44
|
+
if (response.headers.get('content-type')?.includes('text/plain; charset=utf-8')) {
|
|
45
|
+
return defaultStreamHandler(response);
|
|
46
|
+
}
|
|
47
|
+
return response;
|
|
48
|
+
};
|
|
49
|
+
exports.default = defaultFetcher;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._defaultHandler = exports.DEFAULT_ERROR_MESSAGE = void 0;
|
|
4
|
+
const HttpException_1 = require("../HttpException");
|
|
5
|
+
exports.DEFAULT_ERROR_MESSAGE = 'Unknown error at defaultHandler';
|
|
6
|
+
const _defaultHandler = async (response) => {
|
|
7
|
+
let result;
|
|
8
|
+
try {
|
|
9
|
+
result = await response.json();
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
// handle parsing errors
|
|
13
|
+
throw new HttpException_1._HttpException(response.status, e?.message ?? exports.DEFAULT_ERROR_MESSAGE);
|
|
14
|
+
}
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
// handle server errors
|
|
17
|
+
throw new HttpException_1._HttpException(response.status, result?.message ?? exports.DEFAULT_ERROR_MESSAGE);
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
exports._defaultHandler = _defaultHandler;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { _StreamAsyncIterator as StreamAsyncIterator } from './types';
|
|
2
|
+
import '../utils/shim';
|
|
3
|
+
export declare const DEFAULT_ERROR_MESSAGE = "Unknown error at defaultStreamHandler";
|
|
4
|
+
export declare const _defaultStreamHandler: (response: Response) => Promise<StreamAsyncIterator<unknown>>;
|