vovk 3.0.0-draft.52 → 3.0.0-draft.53
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-tsc.log +1 -1
- package/README.md +24 -1
- package/dist/client/createRPC.d.ts +0 -1
- package/dist/client/createRPC.js +4 -24
- package/dist/types.d.ts +1 -1
- package/dist/utils/parseQuery.d.ts +25 -0
- package/dist/utils/parseQuery.js +156 -0
- package/dist/utils/reqQuery.js +5 -20
- package/dist/utils/serializeQuery.d.ts +13 -0
- package/dist/utils/serializeQuery.js +65 -0
- package/dist/worker/createWPC.d.ts +1 -1
- package/dist/worker/createWPC.js +2 -2
- package/package.json +1 -1
- package/src/client/createRPC.ts +3 -23
- package/src/types.ts +1 -1
- package/src/utils/parseQuery.ts +160 -0
- package/src/utils/reqQuery.ts +2 -22
- package/src/utils/serializeQuery.ts +69 -0
- package/src/worker/createWPC.ts +2 -5
package/.turbo/turbo-build.log
CHANGED
package/.turbo/turbo-tsc.log
CHANGED
package/README.md
CHANGED
|
@@ -1 +1,24 @@
|
|
|
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>REST + RPC = ♥️</strong>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
Back-end meta-framework for <a href="https://nextjs.org/docs/app">Next.js</a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## vovk [](https://www.npmjs.com/package/vovk)
|
|
17
|
+
|
|
18
|
+
The main library with [zero dependencies](https://bundlephobia.com/result?p=vovk) that's going to be used in production. It provides a wrapper for Next.js API routes, internal RPC API, utilities and types.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install vovk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For more information, please visit the [getting started guide](https://vovk.dev/getting-started) or check out the [Vovk.ts examples](https://vovk-examples.vercel.app/).
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import { type VovkControllerSchema, type KnownAny } from '../types';
|
|
2
2
|
import { type VovkClientOptions, type VovkClient, type VovkDefaultFetcherOptions } from './types';
|
|
3
|
-
export declare const ARRAY_QUERY_KEY = "_vovkarr";
|
|
4
3
|
export declare const createRPC: <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions>(controllerSchema: VovkControllerSchema, segmentName?: string, options?: VovkClientOptions<OPTS>) => VovkClient<T, OPTS>;
|
package/dist/client/createRPC.js
CHANGED
|
@@ -3,39 +3,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.createRPC =
|
|
6
|
+
exports.createRPC = void 0;
|
|
7
7
|
const defaultFetcher_1 = __importDefault(require("./defaultFetcher"));
|
|
8
8
|
const defaultHandler_1 = require("./defaultHandler");
|
|
9
9
|
const defaultStreamHandler_1 = require("./defaultStreamHandler");
|
|
10
|
-
|
|
11
|
-
exports.ARRAY_QUERY_KEY = '_vovkarr';
|
|
10
|
+
const serializeQuery_1 = __importDefault(require("../utils/serializeQuery"));
|
|
12
11
|
const trimPath = (path) => path.trim().replace(/^\/|\/$/g, '');
|
|
13
12
|
const getHandlerPath = (endpoint, params, query) => {
|
|
14
13
|
let result = endpoint;
|
|
15
14
|
for (const [key, value] of Object.entries(params ?? {})) {
|
|
16
15
|
result = result.replace(`:${key}`, value);
|
|
17
16
|
}
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const arrayKeys = [];
|
|
21
|
-
for (const [key, value] of Object.entries(query ?? {})) {
|
|
22
|
-
if (typeof value === 'undefined')
|
|
23
|
-
continue;
|
|
24
|
-
if (value instanceof Array) {
|
|
25
|
-
arrayKeys.push(key);
|
|
26
|
-
for (const val of value) {
|
|
27
|
-
searchParams.append(key, val);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
searchParams.set(key, value);
|
|
32
|
-
}
|
|
33
|
-
hasQuery = true;
|
|
34
|
-
}
|
|
35
|
-
if (arrayKeys.length) {
|
|
36
|
-
searchParams.set(exports.ARRAY_QUERY_KEY, arrayKeys.join(','));
|
|
37
|
-
}
|
|
38
|
-
return `${result}${hasQuery ? '?' : ''}${searchParams.toString()}`;
|
|
17
|
+
const queryStr = query ? (0, serializeQuery_1.default)(query) : null;
|
|
18
|
+
return `${result}${queryStr ? '?' : ''}${queryStr}`;
|
|
39
19
|
};
|
|
40
20
|
const createRPC = (controllerSchema, segmentName, options) => {
|
|
41
21
|
const schema = controllerSchema;
|
package/dist/types.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface VovkRequest<BODY = undefined, QUERY extends object | undefined
|
|
|
77
77
|
form: <T = KnownAny>() => Promise<T>;
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
export type ControllerStaticMethod<REQ extends VovkRequest<KnownAny, KnownAny> = VovkRequest<undefined, Record<string,
|
|
80
|
+
export type ControllerStaticMethod<REQ extends VovkRequest<KnownAny, KnownAny> = VovkRequest<undefined, Record<string, KnownAny>>, PARAMS extends {
|
|
81
81
|
[key: string]: string;
|
|
82
82
|
} = KnownAny> = ((req: REQ, params: PARAMS) => unknown) & {
|
|
83
83
|
_controller?: VovkController;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KnownAny } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Deserialize a bracket-based query string into an object.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Key/value pairs with nested brackets (e.g. "a[b][0]=value")
|
|
7
|
+
* - Arrays with empty bracket (e.g. "arr[]=1&arr[]=2")
|
|
8
|
+
* - Mixed arrays of objects, etc.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* parseQuery("x=xx&y[0]=yy&y[1]=uu&z[f]=x&z[u][0]=uu&z[u][1]=xx&z[d][x]=ee")
|
|
12
|
+
* => {
|
|
13
|
+
* x: "xx",
|
|
14
|
+
* y: ["yy", "uu"],
|
|
15
|
+
* z: {
|
|
16
|
+
* f: "x",
|
|
17
|
+
* u: ["uu", "xx"],
|
|
18
|
+
* d: { x: "ee" }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* @param queryString - The raw query string (e.g. location.search.slice(1))
|
|
23
|
+
* @returns - A nested object representing the query params
|
|
24
|
+
*/
|
|
25
|
+
export default function parseQuery(queryString: string): Record<string, KnownAny>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = parseQuery;
|
|
4
|
+
/**
|
|
5
|
+
* Parse a bracket-based key (e.g. "z[d][0][x]" or "arr[]")
|
|
6
|
+
* into an array of path segments (strings or special push-markers).
|
|
7
|
+
*
|
|
8
|
+
* Example: "z[d][0][x]" => ["z", "d", "0", "x"]
|
|
9
|
+
* Example: "arr[]" => ["arr", "" ] // "" indicates "push" onto array
|
|
10
|
+
*/
|
|
11
|
+
function parseKey(key) {
|
|
12
|
+
// The first segment is everything up to the first '[' (or the entire key if no '[')
|
|
13
|
+
const segments = [];
|
|
14
|
+
const topKeyMatch = key.match(/^([^[\]]+)/);
|
|
15
|
+
if (topKeyMatch) {
|
|
16
|
+
segments.push(topKeyMatch[1]);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// If it starts with brackets, treat it as empty? (edge case)
|
|
20
|
+
segments.push('');
|
|
21
|
+
}
|
|
22
|
+
// Now capture all bracket parts: [something], [0], []
|
|
23
|
+
const bracketRegex = /\[([^[\]]*)\]/g;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = bracketRegex.exec(key)) !== null) {
|
|
26
|
+
// match[1] is the content inside the brackets
|
|
27
|
+
segments.push(match[1]);
|
|
28
|
+
}
|
|
29
|
+
return segments;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Recursively set a value in a nested object/array, given a path of segments.
|
|
33
|
+
* - If segment is numeric => treat as array index
|
|
34
|
+
* - If segment is empty "" => push to array
|
|
35
|
+
* - Else => object property
|
|
36
|
+
*/
|
|
37
|
+
function setValue(obj, path, value) {
|
|
38
|
+
let current = obj;
|
|
39
|
+
for (let i = 0; i < path.length; i++) {
|
|
40
|
+
const segment = path[i];
|
|
41
|
+
// If we're at the last segment, set the value
|
|
42
|
+
if (i === path.length - 1) {
|
|
43
|
+
if (segment === '') {
|
|
44
|
+
// Empty bracket => push
|
|
45
|
+
if (!Array.isArray(current)) {
|
|
46
|
+
current = [];
|
|
47
|
+
}
|
|
48
|
+
current.push(value);
|
|
49
|
+
}
|
|
50
|
+
else if (!isNaN(Number(segment))) {
|
|
51
|
+
// Numeric segment => array index
|
|
52
|
+
const idx = Number(segment);
|
|
53
|
+
if (!Array.isArray(current)) {
|
|
54
|
+
current = [];
|
|
55
|
+
}
|
|
56
|
+
current[idx] = value;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Object property
|
|
60
|
+
current[segment] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Not the last segment: descend into existing structure or create it
|
|
65
|
+
const nextSegment = path[i + 1];
|
|
66
|
+
if (segment === '') {
|
|
67
|
+
// Empty bracket => push
|
|
68
|
+
if (!Array.isArray(current)) {
|
|
69
|
+
// Convert the current node into an array, if not one
|
|
70
|
+
current = [];
|
|
71
|
+
}
|
|
72
|
+
// If we are not at the last path, we need a placeholder object or array
|
|
73
|
+
// for the next segment. We'll push something and move current to that.
|
|
74
|
+
if (current.length === 0) {
|
|
75
|
+
// nothing in array yet
|
|
76
|
+
current.push(typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {});
|
|
77
|
+
}
|
|
78
|
+
else if (typeof nextSegment === 'string' && !isNaN(Number(nextSegment))) {
|
|
79
|
+
// next is numeric => we want an array
|
|
80
|
+
if (!Array.isArray(current[current.length - 1])) {
|
|
81
|
+
current[current.length - 1] = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// next is not numeric => we want an object
|
|
86
|
+
if (typeof current[current.length - 1] !== 'object') {
|
|
87
|
+
current[current.length - 1] = {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
current = current[current.length - 1];
|
|
91
|
+
}
|
|
92
|
+
else if (!isNaN(Number(segment))) {
|
|
93
|
+
// segment is numeric => array index
|
|
94
|
+
const idx = Number(segment);
|
|
95
|
+
if (!Array.isArray(current)) {
|
|
96
|
+
current = [];
|
|
97
|
+
}
|
|
98
|
+
if (current[idx] === undefined) {
|
|
99
|
+
// Create placeholder for next segment
|
|
100
|
+
current[idx] = typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {};
|
|
101
|
+
}
|
|
102
|
+
current = current[idx];
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// segment is an object key
|
|
106
|
+
if (current[segment] === undefined) {
|
|
107
|
+
// Create placeholder
|
|
108
|
+
current[segment] = typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {};
|
|
109
|
+
}
|
|
110
|
+
current = current[segment];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Deserialize a bracket-based query string into an object.
|
|
117
|
+
*
|
|
118
|
+
* Supports:
|
|
119
|
+
* - Key/value pairs with nested brackets (e.g. "a[b][0]=value")
|
|
120
|
+
* - Arrays with empty bracket (e.g. "arr[]=1&arr[]=2")
|
|
121
|
+
* - Mixed arrays of objects, etc.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* parseQuery("x=xx&y[0]=yy&y[1]=uu&z[f]=x&z[u][0]=uu&z[u][1]=xx&z[d][x]=ee")
|
|
125
|
+
* => {
|
|
126
|
+
* x: "xx",
|
|
127
|
+
* y: ["yy", "uu"],
|
|
128
|
+
* z: {
|
|
129
|
+
* f: "x",
|
|
130
|
+
* u: ["uu", "xx"],
|
|
131
|
+
* d: { x: "ee" }
|
|
132
|
+
* }
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* @param queryString - The raw query string (e.g. location.search.slice(1))
|
|
136
|
+
* @returns - A nested object representing the query params
|
|
137
|
+
*/
|
|
138
|
+
function parseQuery(queryString) {
|
|
139
|
+
const result = {};
|
|
140
|
+
if (!queryString)
|
|
141
|
+
return result;
|
|
142
|
+
// Split into key=value pairs
|
|
143
|
+
const pairs = queryString
|
|
144
|
+
.replace(/^\?/, '') // Remove leading "?" if present
|
|
145
|
+
.split('&');
|
|
146
|
+
for (const pair of pairs) {
|
|
147
|
+
const [rawKey, rawVal = ''] = pair.split('=');
|
|
148
|
+
const decodedKey = decodeURIComponent(rawKey || '');
|
|
149
|
+
const decodedVal = decodeURIComponent(rawVal);
|
|
150
|
+
// Parse bracket notation
|
|
151
|
+
const pathSegments = parseKey(decodedKey);
|
|
152
|
+
// Insert into the result object
|
|
153
|
+
setValue(result, pathSegments, decodedVal);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
package/dist/utils/reqQuery.js
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.default = reqQuery;
|
|
4
|
-
const
|
|
7
|
+
const parseQuery_1 = __importDefault(require("./parseQuery"));
|
|
5
8
|
function reqQuery(req) {
|
|
6
|
-
|
|
7
|
-
const entries = [...req.nextUrl.searchParams.entries()];
|
|
8
|
-
const query = entries.reduce((acc, [key, value]) => {
|
|
9
|
-
if (key === createRPC_1.ARRAY_QUERY_KEY)
|
|
10
|
-
return acc;
|
|
11
|
-
if (queryArr?.includes(key)) {
|
|
12
|
-
if (!(key in acc)) {
|
|
13
|
-
acc[key] = [value];
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
acc[key].push(value);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
acc[key] = value;
|
|
21
|
-
}
|
|
22
|
-
return acc;
|
|
23
|
-
}, {});
|
|
24
|
-
return query;
|
|
9
|
+
return (0, parseQuery_1.default)(req.nextUrl.search);
|
|
25
10
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { KnownAny } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Serialize a nested object (including arrays, arrays of objects, etc.)
|
|
4
|
+
* into a bracket-based query string.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* serializeQuery({ x: 'xx', y: [1, 2], z: { f: 'x' } })
|
|
8
|
+
* => "x=xx&y[0]=1&y[1]=2&z[f]=x"
|
|
9
|
+
*
|
|
10
|
+
* @param obj - The input object to be serialized
|
|
11
|
+
* @returns - A bracket-based query string (without leading "?")
|
|
12
|
+
*/
|
|
13
|
+
export default function serializeQuery(obj: Record<string, KnownAny>): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = serializeQuery;
|
|
4
|
+
/**
|
|
5
|
+
* Recursively build query parameters from an object.
|
|
6
|
+
*
|
|
7
|
+
* @param key - The query key so far (e.g. 'user', 'user[0]', 'user[0][name]')
|
|
8
|
+
* @param value - The current value to serialize
|
|
9
|
+
* @returns - An array of `key=value` strings
|
|
10
|
+
*/
|
|
11
|
+
function buildParams(key, value) {
|
|
12
|
+
if (value === null || value === undefined) {
|
|
13
|
+
return []; // skip null/undefined values entirely
|
|
14
|
+
}
|
|
15
|
+
// If value is an object or array, we need to recurse
|
|
16
|
+
if (typeof value === 'object') {
|
|
17
|
+
// Array case
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
/**
|
|
20
|
+
* We use index-based bracket notation here:
|
|
21
|
+
* e.g. for value = ['aa', 'bb'] and key = 'foo'
|
|
22
|
+
* => "foo[0]=aa&foo[1]=bb"
|
|
23
|
+
*
|
|
24
|
+
* If you prefer "foo[]=aa&foo[]=bb" style, replace:
|
|
25
|
+
* `${key}[${i}]`
|
|
26
|
+
* with:
|
|
27
|
+
* `${key}[]`
|
|
28
|
+
*/
|
|
29
|
+
return value.flatMap((v, i) => {
|
|
30
|
+
const newKey = `${key}[${i}]`;
|
|
31
|
+
return buildParams(newKey, v);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Plain object case
|
|
35
|
+
return Object.keys(value).flatMap((k) => {
|
|
36
|
+
const newKey = `${key}[${k}]`;
|
|
37
|
+
return buildParams(newKey, value[k]);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return [`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Serialize a nested object (including arrays, arrays of objects, etc.)
|
|
44
|
+
* into a bracket-based query string.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* serializeQuery({ x: 'xx', y: [1, 2], z: { f: 'x' } })
|
|
48
|
+
* => "x=xx&y[0]=1&y[1]=2&z[f]=x"
|
|
49
|
+
*
|
|
50
|
+
* @param obj - The input object to be serialized
|
|
51
|
+
* @returns - A bracket-based query string (without leading "?")
|
|
52
|
+
*/
|
|
53
|
+
function serializeQuery(obj) {
|
|
54
|
+
if (!obj || typeof obj !== 'object')
|
|
55
|
+
return '';
|
|
56
|
+
// Collect query segments
|
|
57
|
+
const segments = [];
|
|
58
|
+
for (const key in obj) {
|
|
59
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
60
|
+
const value = obj[key];
|
|
61
|
+
segments.push(...buildParams(key, value));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return segments.join('&');
|
|
65
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { WorkerPromiseInstance } from './types';
|
|
2
|
-
export declare function createWPC<T extends object>(
|
|
2
|
+
export declare function createWPC<T extends object>(workerSchema: object, currentWorker?: Worker): WorkerPromiseInstance<T>;
|
package/dist/worker/createWPC.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createWPC = createWPC;
|
|
4
|
-
function createWPC(
|
|
4
|
+
function createWPC(workerSchema, currentWorker) {
|
|
5
5
|
if (!workerSchema)
|
|
6
6
|
throw new Error('Worker schema is not provided');
|
|
7
7
|
const schema = workerSchema;
|
|
@@ -23,7 +23,7 @@ function createWPC(currentWorker, workerSchema) {
|
|
|
23
23
|
instance.worker = worker;
|
|
24
24
|
return instance;
|
|
25
25
|
};
|
|
26
|
-
instance.fork = (worker) => createWPC(
|
|
26
|
+
instance.fork = (worker) => createWPC(schema, worker);
|
|
27
27
|
for (const methodName of Object.keys(schema.handlers)) {
|
|
28
28
|
const { isGenerator } = schema.handlers[methodName];
|
|
29
29
|
if (isGenerator) {
|
package/package.json
CHANGED
package/src/client/createRPC.ts
CHANGED
|
@@ -10,9 +10,7 @@ import { type VovkClientOptions, type VovkClient, type VovkDefaultFetcherOptions
|
|
|
10
10
|
import defaultFetcher from './defaultFetcher';
|
|
11
11
|
import { defaultHandler } from './defaultHandler';
|
|
12
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';
|
|
13
|
+
import serializeQuery from '../utils/serializeQuery';
|
|
16
14
|
|
|
17
15
|
const trimPath = (path: string) => path.trim().replace(/^\/|\/$/g, '');
|
|
18
16
|
|
|
@@ -26,27 +24,9 @@ const getHandlerPath = <T extends ControllerStaticMethod>(
|
|
|
26
24
|
result = result.replace(`:${key}`, value as string);
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
const
|
|
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
|
-
}
|
|
27
|
+
const queryStr = query ? serializeQuery(query) : null;
|
|
48
28
|
|
|
49
|
-
return `${result}${
|
|
29
|
+
return `${result}${queryStr ? '?' : ''}${queryStr}`;
|
|
50
30
|
};
|
|
51
31
|
|
|
52
32
|
export const createRPC = <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions>(
|
package/src/types.ts
CHANGED
|
@@ -107,7 +107,7 @@ export interface VovkRequest<BODY = undefined, QUERY extends object | undefined
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export type ControllerStaticMethod<
|
|
110
|
-
REQ extends VovkRequest<KnownAny, KnownAny> = VovkRequest<undefined, Record<string,
|
|
110
|
+
REQ extends VovkRequest<KnownAny, KnownAny> = VovkRequest<undefined, Record<string, KnownAny>>,
|
|
111
111
|
PARAMS extends { [key: string]: string } = KnownAny,
|
|
112
112
|
> = ((req: REQ, params: PARAMS) => unknown) & {
|
|
113
113
|
_controller?: VovkController;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { KnownAny } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a bracket-based key (e.g. "z[d][0][x]" or "arr[]")
|
|
5
|
+
* into an array of path segments (strings or special push-markers).
|
|
6
|
+
*
|
|
7
|
+
* Example: "z[d][0][x]" => ["z", "d", "0", "x"]
|
|
8
|
+
* Example: "arr[]" => ["arr", "" ] // "" indicates "push" onto array
|
|
9
|
+
*/
|
|
10
|
+
function parseKey(key: string): string[] {
|
|
11
|
+
// The first segment is everything up to the first '[' (or the entire key if no '[')
|
|
12
|
+
const segments: string[] = [];
|
|
13
|
+
const topKeyMatch = key.match(/^([^[\]]+)/);
|
|
14
|
+
if (topKeyMatch) {
|
|
15
|
+
segments.push(topKeyMatch[1]);
|
|
16
|
+
} else {
|
|
17
|
+
// If it starts with brackets, treat it as empty? (edge case)
|
|
18
|
+
segments.push('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Now capture all bracket parts: [something], [0], []
|
|
22
|
+
const bracketRegex = /\[([^[\]]*)\]/g;
|
|
23
|
+
let match: RegExpExecArray | null;
|
|
24
|
+
while ((match = bracketRegex.exec(key)) !== null) {
|
|
25
|
+
// match[1] is the content inside the brackets
|
|
26
|
+
segments.push(match[1]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return segments;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Recursively set a value in a nested object/array, given a path of segments.
|
|
34
|
+
* - If segment is numeric => treat as array index
|
|
35
|
+
* - If segment is empty "" => push to array
|
|
36
|
+
* - Else => object property
|
|
37
|
+
*/
|
|
38
|
+
function setValue(obj: Record<string, KnownAny>, path: string[], value: KnownAny): void {
|
|
39
|
+
let current: KnownAny = obj;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < path.length; i++) {
|
|
42
|
+
const segment = path[i];
|
|
43
|
+
|
|
44
|
+
// If we're at the last segment, set the value
|
|
45
|
+
if (i === path.length - 1) {
|
|
46
|
+
if (segment === '') {
|
|
47
|
+
// Empty bracket => push
|
|
48
|
+
if (!Array.isArray(current)) {
|
|
49
|
+
current = [];
|
|
50
|
+
}
|
|
51
|
+
current.push(value);
|
|
52
|
+
} else if (!isNaN(Number(segment))) {
|
|
53
|
+
// Numeric segment => array index
|
|
54
|
+
const idx = Number(segment);
|
|
55
|
+
if (!Array.isArray(current)) {
|
|
56
|
+
current = [];
|
|
57
|
+
}
|
|
58
|
+
current[idx] = value;
|
|
59
|
+
} else {
|
|
60
|
+
// Object property
|
|
61
|
+
current[segment] = value;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// Not the last segment: descend into existing structure or create it
|
|
65
|
+
const nextSegment = path[i + 1];
|
|
66
|
+
|
|
67
|
+
if (segment === '') {
|
|
68
|
+
// Empty bracket => push
|
|
69
|
+
if (!Array.isArray(current)) {
|
|
70
|
+
// Convert the current node into an array, if not one
|
|
71
|
+
current = [];
|
|
72
|
+
}
|
|
73
|
+
// If we are not at the last path, we need a placeholder object or array
|
|
74
|
+
// for the next segment. We'll push something and move current to that.
|
|
75
|
+
if (current.length === 0) {
|
|
76
|
+
// nothing in array yet
|
|
77
|
+
current.push(typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {});
|
|
78
|
+
} else if (typeof nextSegment === 'string' && !isNaN(Number(nextSegment))) {
|
|
79
|
+
// next is numeric => we want an array
|
|
80
|
+
if (!Array.isArray(current[current.length - 1])) {
|
|
81
|
+
current[current.length - 1] = [];
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
// next is not numeric => we want an object
|
|
85
|
+
if (typeof current[current.length - 1] !== 'object') {
|
|
86
|
+
current[current.length - 1] = {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
current = current[current.length - 1];
|
|
90
|
+
} else if (!isNaN(Number(segment))) {
|
|
91
|
+
// segment is numeric => array index
|
|
92
|
+
const idx = Number(segment);
|
|
93
|
+
if (!Array.isArray(current)) {
|
|
94
|
+
current = [];
|
|
95
|
+
}
|
|
96
|
+
if (current[idx] === undefined) {
|
|
97
|
+
// Create placeholder for next segment
|
|
98
|
+
current[idx] = typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {};
|
|
99
|
+
}
|
|
100
|
+
current = current[idx];
|
|
101
|
+
} else {
|
|
102
|
+
// segment is an object key
|
|
103
|
+
if (current[segment] === undefined) {
|
|
104
|
+
// Create placeholder
|
|
105
|
+
current[segment] = typeof nextSegment === 'string' && !isNaN(Number(nextSegment)) ? [] : {};
|
|
106
|
+
}
|
|
107
|
+
current = current[segment];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Deserialize a bracket-based query string into an object.
|
|
115
|
+
*
|
|
116
|
+
* Supports:
|
|
117
|
+
* - Key/value pairs with nested brackets (e.g. "a[b][0]=value")
|
|
118
|
+
* - Arrays with empty bracket (e.g. "arr[]=1&arr[]=2")
|
|
119
|
+
* - Mixed arrays of objects, etc.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* parseQuery("x=xx&y[0]=yy&y[1]=uu&z[f]=x&z[u][0]=uu&z[u][1]=xx&z[d][x]=ee")
|
|
123
|
+
* => {
|
|
124
|
+
* x: "xx",
|
|
125
|
+
* y: ["yy", "uu"],
|
|
126
|
+
* z: {
|
|
127
|
+
* f: "x",
|
|
128
|
+
* u: ["uu", "xx"],
|
|
129
|
+
* d: { x: "ee" }
|
|
130
|
+
* }
|
|
131
|
+
* }
|
|
132
|
+
*
|
|
133
|
+
* @param queryString - The raw query string (e.g. location.search.slice(1))
|
|
134
|
+
* @returns - A nested object representing the query params
|
|
135
|
+
*/
|
|
136
|
+
export default function parseQuery(queryString: string): Record<string, KnownAny> {
|
|
137
|
+
const result: Record<string, KnownAny> = {};
|
|
138
|
+
|
|
139
|
+
if (!queryString) return result;
|
|
140
|
+
|
|
141
|
+
// Split into key=value pairs
|
|
142
|
+
const pairs = queryString
|
|
143
|
+
.replace(/^\?/, '') // Remove leading "?" if present
|
|
144
|
+
.split('&');
|
|
145
|
+
|
|
146
|
+
for (const pair of pairs) {
|
|
147
|
+
const [rawKey, rawVal = ''] = pair.split('=');
|
|
148
|
+
|
|
149
|
+
const decodedKey = decodeURIComponent(rawKey || '');
|
|
150
|
+
const decodedVal = decodeURIComponent(rawVal);
|
|
151
|
+
|
|
152
|
+
// Parse bracket notation
|
|
153
|
+
const pathSegments = parseKey(decodedKey);
|
|
154
|
+
|
|
155
|
+
// Insert into the result object
|
|
156
|
+
setValue(result, pathSegments, decodedVal);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
package/src/utils/reqQuery.ts
CHANGED
|
@@ -1,26 +1,6 @@
|
|
|
1
|
-
import { ARRAY_QUERY_KEY } from '../client/createRPC';
|
|
2
1
|
import type { KnownAny, VovkRequest } from '../types';
|
|
2
|
+
import parseQuery from './parseQuery';
|
|
3
3
|
|
|
4
4
|
export default function reqQuery<T extends object | undefined>(req: VovkRequest<KnownAny, T>): T {
|
|
5
|
-
|
|
6
|
-
const queryArr = (req.nextUrl.searchParams.get(ARRAY_QUERY_KEY as keyof T) as string | undefined)?.split(',') ?? null;
|
|
7
|
-
const entries = [...req.nextUrl.searchParams.entries()] as [string, string][];
|
|
8
|
-
const query = entries.reduce(
|
|
9
|
-
(acc, [key, value]) => {
|
|
10
|
-
if (key === ARRAY_QUERY_KEY) return acc;
|
|
11
|
-
if (queryArr?.includes(key)) {
|
|
12
|
-
if (!(key in acc)) {
|
|
13
|
-
acc[key] = [value];
|
|
14
|
-
} else {
|
|
15
|
-
(acc[key] as string[]).push(value);
|
|
16
|
-
}
|
|
17
|
-
} else {
|
|
18
|
-
acc[key] = value;
|
|
19
|
-
}
|
|
20
|
-
return acc;
|
|
21
|
-
},
|
|
22
|
-
{} as Record<string, string | string[]>
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
return query as Query;
|
|
5
|
+
return parseQuery(req.nextUrl.search) as NonNullable<T>;
|
|
26
6
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { KnownAny } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursively build query parameters from an object.
|
|
5
|
+
*
|
|
6
|
+
* @param key - The query key so far (e.g. 'user', 'user[0]', 'user[0][name]')
|
|
7
|
+
* @param value - The current value to serialize
|
|
8
|
+
* @returns - An array of `key=value` strings
|
|
9
|
+
*/
|
|
10
|
+
function buildParams(key: string, value: KnownAny): string[] {
|
|
11
|
+
if (value === null || value === undefined) {
|
|
12
|
+
return []; // skip null/undefined values entirely
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// If value is an object or array, we need to recurse
|
|
16
|
+
if (typeof value === 'object') {
|
|
17
|
+
// Array case
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
/**
|
|
20
|
+
* We use index-based bracket notation here:
|
|
21
|
+
* e.g. for value = ['aa', 'bb'] and key = 'foo'
|
|
22
|
+
* => "foo[0]=aa&foo[1]=bb"
|
|
23
|
+
*
|
|
24
|
+
* If you prefer "foo[]=aa&foo[]=bb" style, replace:
|
|
25
|
+
* `${key}[${i}]`
|
|
26
|
+
* with:
|
|
27
|
+
* `${key}[]`
|
|
28
|
+
*/
|
|
29
|
+
return value.flatMap((v, i) => {
|
|
30
|
+
const newKey = `${key}[${i}]`;
|
|
31
|
+
return buildParams(newKey, v);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Plain object case
|
|
36
|
+
return Object.keys(value).flatMap((k) => {
|
|
37
|
+
const newKey = `${key}[${k}]`;
|
|
38
|
+
return buildParams(newKey, value[k]);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return [`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Serialize a nested object (including arrays, arrays of objects, etc.)
|
|
47
|
+
* into a bracket-based query string.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* serializeQuery({ x: 'xx', y: [1, 2], z: { f: 'x' } })
|
|
51
|
+
* => "x=xx&y[0]=1&y[1]=2&z[f]=x"
|
|
52
|
+
*
|
|
53
|
+
* @param obj - The input object to be serialized
|
|
54
|
+
* @returns - A bracket-based query string (without leading "?")
|
|
55
|
+
*/
|
|
56
|
+
export default function serializeQuery(obj: Record<string, KnownAny>): string {
|
|
57
|
+
if (!obj || typeof obj !== 'object') return '';
|
|
58
|
+
|
|
59
|
+
// Collect query segments
|
|
60
|
+
const segments: string[] = [];
|
|
61
|
+
for (const key in obj) {
|
|
62
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
63
|
+
const value = obj[key];
|
|
64
|
+
segments.push(...buildParams(key, value));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return segments.join('&');
|
|
69
|
+
}
|
package/src/worker/createWPC.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { VovkWorkerSchema } from '../types';
|
|
2
2
|
import type { WorkerInput, WorkerOutput, WorkerPromiseInstance } from './types';
|
|
3
3
|
|
|
4
|
-
export function createWPC<T extends object>(
|
|
5
|
-
currentWorker: Worker | null,
|
|
6
|
-
workerSchema: object
|
|
7
|
-
): WorkerPromiseInstance<T> {
|
|
4
|
+
export function createWPC<T extends object>(workerSchema: object, currentWorker?: Worker): WorkerPromiseInstance<T> {
|
|
8
5
|
if (!workerSchema) throw new Error('Worker schema is not provided');
|
|
9
6
|
const schema = workerSchema as T & VovkWorkerSchema;
|
|
10
7
|
const instance = {
|
|
@@ -26,7 +23,7 @@ export function createWPC<T extends object>(
|
|
|
26
23
|
return instance;
|
|
27
24
|
};
|
|
28
25
|
|
|
29
|
-
instance.fork = (worker: Worker) => createWPC<T>(
|
|
26
|
+
instance.fork = (worker: Worker) => createWPC<T>(schema, worker);
|
|
30
27
|
|
|
31
28
|
for (const methodName of Object.keys(schema.handlers) as (keyof T & string)[]) {
|
|
32
29
|
const { isGenerator } = schema.handlers[methodName];
|