@orpc/openapi 1.14.6 → 2.0.0-beta.1
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 +74 -109
- package/dist/adapters/fetch/index.d.mts +20 -16
- package/dist/adapters/fetch/index.d.ts +20 -16
- package/dist/adapters/fetch/index.mjs +24 -8
- package/dist/adapters/node/index.d.mts +8 -13
- package/dist/adapters/node/index.d.ts +8 -13
- package/dist/adapters/node/index.mjs +10 -7
- package/dist/adapters/standard/index.d.mts +46 -16
- package/dist/adapters/standard/index.d.ts +46 -16
- package/dist/adapters/standard/index.mjs +9 -6
- package/dist/extensions/route.d.mts +43 -0
- package/dist/extensions/route.d.ts +43 -0
- package/dist/extensions/route.mjs +14 -0
- package/dist/helpers/index.d.mts +51 -0
- package/dist/helpers/index.d.ts +51 -0
- package/dist/helpers/index.mjs +39 -0
- package/dist/index.d.mts +92 -111
- package/dist/index.d.ts +92 -111
- package/dist/index.mjs +894 -34
- package/dist/plugins/index.d.mts +55 -51
- package/dist/plugins/index.d.ts +55 -51
- package/dist/plugins/index.mjs +147 -142
- package/dist/shared/openapi.B2SK0ZAr.mjs +359 -0
- package/dist/shared/openapi.B9PQzqBn.mjs +49 -0
- package/dist/shared/openapi.BQzzr4-4.d.ts +299 -0
- package/dist/shared/openapi.BcEtAxQj.d.mts +299 -0
- package/dist/shared/openapi.Bt87OzTt.mjs +131 -0
- package/dist/shared/openapi.C7m7NAmH.d.mts +142 -0
- package/dist/shared/openapi.C7m7NAmH.d.ts +142 -0
- package/dist/shared/openapi.C9Olbxd5.d.ts +75 -0
- package/dist/shared/openapi.CYgMBSUF.d.mts +18 -0
- package/dist/shared/openapi.CYgMBSUF.d.ts +18 -0
- package/dist/shared/openapi.CcyUEuTs.mjs +313 -0
- package/dist/shared/openapi.DmAa7YPO.mjs +275 -0
- package/dist/shared/openapi.Drd1OtzG.d.mts +75 -0
- package/package.json +29 -23
- package/dist/adapters/aws-lambda/index.d.mts +0 -20
- package/dist/adapters/aws-lambda/index.d.ts +0 -20
- package/dist/adapters/aws-lambda/index.mjs +0 -18
- package/dist/adapters/fastify/index.d.mts +0 -23
- package/dist/adapters/fastify/index.d.ts +0 -23
- package/dist/adapters/fastify/index.mjs +0 -18
- package/dist/shared/openapi.BB-W-NKv.mjs +0 -204
- package/dist/shared/openapi.BGy4N6eR.d.mts +0 -120
- package/dist/shared/openapi.BGy4N6eR.d.ts +0 -120
- package/dist/shared/openapi.BwdtJjDu.mjs +0 -878
- package/dist/shared/openapi.DwaweYRb.d.mts +0 -54
- package/dist/shared/openapi.DwaweYRb.d.ts +0 -54
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { COMMON_ERROR_STATUS_MAP } from '@orpc/client';
|
|
2
|
+
import { walkProcedureContractsSync, Procedure, unlazy, getRouter, createContractProcedure, DEFAULT_SUCCESS_STATUS, DEFAULT_ERROR_STATUS } from '@orpc/server';
|
|
3
|
+
import { value, pathToHttpPath, mergeHttpPath, normalizeHttpPath, tryDecodeURIComponent, isPlainObject, stringifyJSON, parseEmptyableJSON, isTypescriptObject, NullProtoObj } from '@orpc/shared';
|
|
4
|
+
import { parseStandardUrl, isStandardHeaders } from '@standardserver/core';
|
|
5
|
+
import { D as DEFAULT_OPENAPI_METHOD, g as getDynamicPathParams, O as OpenAPISerializer, a as DEFAULT_OPENAPI_INPUT_STRUCTURE, b as DEFAULT_OPENAPI_OUTPUT_STRUCTURE } from './openapi.DmAa7YPO.mjs';
|
|
6
|
+
import { g as getOpenAPIMeta } from './openapi.B9PQzqBn.mjs';
|
|
7
|
+
import { createRouter, addRoute, findRoute, routeToRegExp } from 'rou3';
|
|
8
|
+
|
|
9
|
+
class OpenAPIMatcher {
|
|
10
|
+
filter;
|
|
11
|
+
rootRouter;
|
|
12
|
+
tree = createRouter();
|
|
13
|
+
pendingLazyRouters = [];
|
|
14
|
+
constructor(router, options = {}) {
|
|
15
|
+
this.filter = options.filter ?? true;
|
|
16
|
+
this.rootRouter = router;
|
|
17
|
+
this.index(router);
|
|
18
|
+
}
|
|
19
|
+
index(router, path = []) {
|
|
20
|
+
const lazyResults = walkProcedureContractsSync(router, (contract, path2) => {
|
|
21
|
+
if (!value(this.filter, contract, path2)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const meta = getOpenAPIMeta(contract);
|
|
25
|
+
const method = meta?.method ?? DEFAULT_OPENAPI_METHOD;
|
|
26
|
+
const postHttpPath = meta?.path ?? pathToHttpPath(path2);
|
|
27
|
+
const openapiPath = meta?.prefix ? mergeHttpPath(meta.prefix, postHttpPath) : postHttpPath;
|
|
28
|
+
const rou3Path = toRou3Pattern(openapiPath);
|
|
29
|
+
addRoute(this.tree, method, rou3Path, {
|
|
30
|
+
path: path2,
|
|
31
|
+
contract,
|
|
32
|
+
procedure: contract instanceof Procedure ? contract : void 0
|
|
33
|
+
});
|
|
34
|
+
}, path);
|
|
35
|
+
this.pendingLazyRouters.push(...lazyResults.map((result) => {
|
|
36
|
+
const prefix = getOpenAPIMeta(result.router)?.prefix;
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
matcher: prefix ? toRou3PrefixMatcher(prefix) : void 0
|
|
40
|
+
};
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
async match(method, pathname, prefix) {
|
|
44
|
+
if (prefix) {
|
|
45
|
+
if (!pathname.startsWith(prefix)) {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
const charAfterPrefix = pathname[prefix.length];
|
|
49
|
+
if (charAfterPrefix === "/") {
|
|
50
|
+
pathname = pathname.slice(prefix.length);
|
|
51
|
+
} else if (charAfterPrefix === void 0) {
|
|
52
|
+
pathname = "/";
|
|
53
|
+
} else if (prefix[prefix.length - 1] === "/") {
|
|
54
|
+
pathname = pathname.slice(prefix.length - 1);
|
|
55
|
+
} else {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const result = await this.matchPathname(method, pathname);
|
|
60
|
+
if (!result && pathname.includes("%")) {
|
|
61
|
+
return this.matchPathname(method, normalizeHttpPath(pathname));
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
async matchPathname(method, pathname) {
|
|
66
|
+
await this.resolvePendingLazyRouters(pathname);
|
|
67
|
+
const match = findRoute(this.tree, method, pathname);
|
|
68
|
+
if (!match) {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
const procedure = await this.resolveProcedure(match.data);
|
|
72
|
+
return {
|
|
73
|
+
path: match.data.path,
|
|
74
|
+
procedure,
|
|
75
|
+
params: match.params ? decodeParams(match.params) : void 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async resolvePendingLazyRouters(pathname) {
|
|
79
|
+
if (!this.pendingLazyRouters.length) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const stillPending = [];
|
|
83
|
+
for (const pending of this.pendingLazyRouters) {
|
|
84
|
+
if (!pending.matcher || pending.matcher.test(pathname)) {
|
|
85
|
+
const { default: router } = await unlazy(pending.router);
|
|
86
|
+
this.index(router, pending.path);
|
|
87
|
+
} else {
|
|
88
|
+
stillPending.push(pending);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.pendingLazyRouters = stillPending;
|
|
92
|
+
}
|
|
93
|
+
async resolveProcedure(entry) {
|
|
94
|
+
if (entry.procedure) {
|
|
95
|
+
return entry.procedure;
|
|
96
|
+
}
|
|
97
|
+
const { default: maybeProcedure } = await unlazy(getRouter(this.rootRouter, entry.path));
|
|
98
|
+
if (!(maybeProcedure instanceof Procedure)) {
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
`[Contract-First] Missing or invalid implementation for procedure at path: "${entry.path.join(".")}". Ensure the procedure is correctly implemented and matches its contract.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
entry.procedure = createContractProcedure(maybeProcedure, entry.contract);
|
|
104
|
+
return entry.procedure;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function toRou3Pattern(path) {
|
|
108
|
+
const params = getDynamicPathParams(path);
|
|
109
|
+
if (!params?.length) {
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
112
|
+
for (let i = params.length - 1; i >= 0; i--) {
|
|
113
|
+
const param = params[i];
|
|
114
|
+
const pattern = param.allowsSlash ? `**:${param.parameterName}` : `:${param.parameterName}`;
|
|
115
|
+
path = path.slice(0, param.startIndex) + pattern + path.slice(param.startIndex + param.segment.length);
|
|
116
|
+
}
|
|
117
|
+
return path;
|
|
118
|
+
}
|
|
119
|
+
function toRou3PrefixMatcher(path) {
|
|
120
|
+
const pattern = toRou3Pattern(path);
|
|
121
|
+
return routeToRegExp(pattern === "/" ? "/**" : `${pattern}/**`);
|
|
122
|
+
}
|
|
123
|
+
function decodeParams(params) {
|
|
124
|
+
return Object.fromEntries(Object.entries(params).map(([key, val]) => [key, tryDecodeURIComponent(val)]));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class OpenAPIHandlerCodecError extends TypeError {
|
|
128
|
+
}
|
|
129
|
+
class OpenAPIHandlerCodec {
|
|
130
|
+
matcher;
|
|
131
|
+
serializer;
|
|
132
|
+
errorStatusMap;
|
|
133
|
+
customErrorResponseBodySerializer;
|
|
134
|
+
constructor(router, options = {}) {
|
|
135
|
+
this.matcher = new OpenAPIMatcher(router, options);
|
|
136
|
+
this.serializer = options.serializer ?? new OpenAPISerializer();
|
|
137
|
+
this.errorStatusMap = options.errorStatusMap ?? COMMON_ERROR_STATUS_MAP;
|
|
138
|
+
this.customErrorResponseBodySerializer = options.customErrorResponseBodyEncoder;
|
|
139
|
+
}
|
|
140
|
+
async resolveProcedure(request, options) {
|
|
141
|
+
const [pathname, search] = parseStandardUrl(request.url);
|
|
142
|
+
const matched = await this.matcher.match(request.method, pathname, options.prefix);
|
|
143
|
+
if (!matched) {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
procedure: matched.procedure,
|
|
148
|
+
path: matched.path,
|
|
149
|
+
decodeInput: async () => {
|
|
150
|
+
const meta = getOpenAPIMeta(matched.procedure);
|
|
151
|
+
const inputStructure = meta?.inputStructure ?? DEFAULT_OPENAPI_INPUT_STRUCTURE;
|
|
152
|
+
const params = this.deserializeParams(matched.params, meta?.paramsStyles);
|
|
153
|
+
const query = this.deserializeQuery(search, meta?.queryStyles);
|
|
154
|
+
if (inputStructure === "compact") {
|
|
155
|
+
const data = request.method === "GET" ? query : this.serializer.deserialize(await request.resolveBody(meta?.requestBodyHint));
|
|
156
|
+
if (data === void 0) {
|
|
157
|
+
return params;
|
|
158
|
+
}
|
|
159
|
+
if (!params || Object.keys(params).length < 1) {
|
|
160
|
+
return data;
|
|
161
|
+
}
|
|
162
|
+
if (isPlainObject(data)) {
|
|
163
|
+
return {
|
|
164
|
+
...params,
|
|
165
|
+
...data
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return data;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
params,
|
|
172
|
+
query,
|
|
173
|
+
headers: request.headers,
|
|
174
|
+
body: this.serializer.deserialize(await request.resolveBody(meta?.requestBodyHint))
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* @throws {Error} If `outputStructure` is "detailed" and the output doesn't match the expected structure.
|
|
181
|
+
*/
|
|
182
|
+
encodeOutput(output, procedure, path, _options) {
|
|
183
|
+
const meta = getOpenAPIMeta(procedure);
|
|
184
|
+
const successStatus = meta?.successStatus ?? DEFAULT_SUCCESS_STATUS;
|
|
185
|
+
const outputStructure = meta?.outputStructure ?? DEFAULT_OPENAPI_OUTPUT_STRUCTURE;
|
|
186
|
+
if (outputStructure === "compact") {
|
|
187
|
+
return {
|
|
188
|
+
status: successStatus,
|
|
189
|
+
headers: {},
|
|
190
|
+
body: this.serializer.serialize(output)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (!isValidDetailedOutput(output)) {
|
|
194
|
+
throw new OpenAPIHandlerCodecError(`
|
|
195
|
+
Invalid "detailed" output structure returned by procedure (${path.join(".")}):
|
|
196
|
+
\u2022 Expected an object with optional properties:
|
|
197
|
+
- status (number 200-399)
|
|
198
|
+
- headers (Record<string, string | string[] | undefined>)
|
|
199
|
+
- body (any)
|
|
200
|
+
\u2022 No extra keys allowed.
|
|
201
|
+
|
|
202
|
+
Actual value:
|
|
203
|
+
${stringifyJSON(output)}
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
status: output.status ?? successStatus,
|
|
208
|
+
headers: output.headers ?? {},
|
|
209
|
+
body: this.serializer.serialize(output.body)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
encodeError(error, _procedure, _path, _options) {
|
|
213
|
+
const status = this.errorStatusMap[error.code] ?? DEFAULT_ERROR_STATUS;
|
|
214
|
+
return {
|
|
215
|
+
status,
|
|
216
|
+
headers: {},
|
|
217
|
+
body: this.serializer.serialize(this.customErrorResponseBodySerializer?.(error) ?? error.toJSON())
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
deserializeQuery(search, styles) {
|
|
221
|
+
const searchParams = new URLSearchParams(search);
|
|
222
|
+
const parsed = this.serializer.deserialize(searchParams);
|
|
223
|
+
if (!styles || !isPlainObject(parsed)) {
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
Object.entries(styles).forEach(([key, hint]) => {
|
|
227
|
+
if (hint === void 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const values = searchParams.getAll(key);
|
|
231
|
+
let parsedValue;
|
|
232
|
+
if (hint === "primitive") {
|
|
233
|
+
parsedValue = values.at(-1);
|
|
234
|
+
} else if (hint === "array") {
|
|
235
|
+
parsedValue = values;
|
|
236
|
+
} else if (hint === "comma-delimited-array") {
|
|
237
|
+
parsedValue = decodeDelimitedArray(values.at(-1), ",");
|
|
238
|
+
} else if (hint === "comma-delimited-object") {
|
|
239
|
+
parsedValue = decodeDelimitedObject(values.at(-1), ",");
|
|
240
|
+
} else if (hint === "space-delimited-array") {
|
|
241
|
+
parsedValue = decodeDelimitedArray(values.at(-1), " ");
|
|
242
|
+
} else if (hint === "space-delimited-object") {
|
|
243
|
+
parsedValue = decodeDelimitedObject(values.at(-1), " ");
|
|
244
|
+
} else if (hint === "pipe-delimited-array") {
|
|
245
|
+
parsedValue = decodeDelimitedArray(values.at(-1), "|");
|
|
246
|
+
} else if (hint === "pipe-delimited-object") {
|
|
247
|
+
parsedValue = decodeDelimitedObject(values.at(-1), "|");
|
|
248
|
+
} else {
|
|
249
|
+
const last = values.at(-1);
|
|
250
|
+
try {
|
|
251
|
+
parsedValue = parseEmptyableJSON(last);
|
|
252
|
+
} catch {
|
|
253
|
+
parsedValue = last;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
parsed[key] = this.serializer.deserialize(parsedValue);
|
|
257
|
+
});
|
|
258
|
+
return parsed;
|
|
259
|
+
}
|
|
260
|
+
deserializeParams(params, styles) {
|
|
261
|
+
if (!params || !styles) {
|
|
262
|
+
return params;
|
|
263
|
+
}
|
|
264
|
+
const parsed = { ...params };
|
|
265
|
+
Object.entries(styles).forEach(([key, hint]) => {
|
|
266
|
+
if (hint === void 0 || hint === "primitive") {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const value = params[key];
|
|
270
|
+
if (hint === "comma-delimited-array") {
|
|
271
|
+
parsed[key] = decodeDelimitedArray(value, ",");
|
|
272
|
+
} else {
|
|
273
|
+
parsed[key] = decodeDelimitedObject(value, ",");
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
return parsed;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function isValidDetailedOutput(output) {
|
|
280
|
+
if (!isTypescriptObject(output)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
if (Object.keys(output).some((key) => key !== "status" && key !== "headers" && key !== "body")) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (output.status !== void 0 && (typeof output.status !== "number" || !Number.isInteger(output.status) || output.status < 200 || output.status > 399)) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (output.headers !== void 0 && !isStandardHeaders(output.headers)) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
function decodeDelimitedArray(value, delimiter) {
|
|
295
|
+
if (value === void 0) {
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
return value.split(delimiter);
|
|
299
|
+
}
|
|
300
|
+
function decodeDelimitedObject(value, delimiter) {
|
|
301
|
+
if (value === void 0) {
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
const obj = new NullProtoObj();
|
|
305
|
+
const parts = value.split(delimiter);
|
|
306
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
307
|
+
const key = parts[i];
|
|
308
|
+
obj[key] = parts[i + 1];
|
|
309
|
+
}
|
|
310
|
+
return obj;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export { OpenAPIHandlerCodec as O, OpenAPIHandlerCodecError as a, OpenAPIMatcher as b };
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { wrapEventIteratorPreservingMeta, toORPCError, isORPCErrorJson, createORPCErrorFromJson } from '@orpc/client';
|
|
2
|
+
import { isPlainObject, isAsyncIteratorObject } from '@orpc/shared';
|
|
3
|
+
import { ErrorEvent } from '@standardserver/core';
|
|
4
|
+
import { B as BracketNotationSerializer } from './openapi.Bt87OzTt.mjs';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_OPENAPI_METHOD = "POST";
|
|
7
|
+
const DEFAULT_OPENAPI_SUCCESS_DESCRIPTION = "OK";
|
|
8
|
+
const DEFAULT_OPENAPI_INPUT_STRUCTURE = "compact";
|
|
9
|
+
const DEFAULT_OPENAPI_OUTPUT_STRUCTURE = "compact";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_OPEN_API_JSON_SERIALIZER_HANDLERS = {
|
|
12
|
+
undefined: {
|
|
13
|
+
condition(data) {
|
|
14
|
+
return data === void 0;
|
|
15
|
+
},
|
|
16
|
+
serialize() {
|
|
17
|
+
return null;
|
|
18
|
+
},
|
|
19
|
+
isTerminal: true
|
|
20
|
+
},
|
|
21
|
+
bigint: {
|
|
22
|
+
condition(data) {
|
|
23
|
+
return typeof data === "bigint";
|
|
24
|
+
},
|
|
25
|
+
serialize(data) {
|
|
26
|
+
return data.toString();
|
|
27
|
+
},
|
|
28
|
+
isTerminal: true
|
|
29
|
+
},
|
|
30
|
+
date: {
|
|
31
|
+
condition(data) {
|
|
32
|
+
return data instanceof Date;
|
|
33
|
+
},
|
|
34
|
+
serialize(data) {
|
|
35
|
+
if (Number.isNaN(data.getTime())) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return data.toISOString();
|
|
39
|
+
},
|
|
40
|
+
isTerminal: true
|
|
41
|
+
},
|
|
42
|
+
nan: {
|
|
43
|
+
condition(data) {
|
|
44
|
+
return typeof data === "number" && Number.isNaN(data);
|
|
45
|
+
},
|
|
46
|
+
serialize() {
|
|
47
|
+
return null;
|
|
48
|
+
},
|
|
49
|
+
isTerminal: true
|
|
50
|
+
},
|
|
51
|
+
url: {
|
|
52
|
+
condition(data) {
|
|
53
|
+
return data instanceof URL;
|
|
54
|
+
},
|
|
55
|
+
serialize(data) {
|
|
56
|
+
return data.toString();
|
|
57
|
+
},
|
|
58
|
+
isTerminal: true
|
|
59
|
+
},
|
|
60
|
+
regexp: {
|
|
61
|
+
condition(data) {
|
|
62
|
+
return data instanceof RegExp;
|
|
63
|
+
},
|
|
64
|
+
serialize(data) {
|
|
65
|
+
return data.toString();
|
|
66
|
+
},
|
|
67
|
+
isTerminal: true
|
|
68
|
+
},
|
|
69
|
+
set: {
|
|
70
|
+
condition(data) {
|
|
71
|
+
return data instanceof Set;
|
|
72
|
+
},
|
|
73
|
+
serialize(data) {
|
|
74
|
+
return Array.from(data);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
map: {
|
|
78
|
+
condition(data) {
|
|
79
|
+
return data instanceof Map;
|
|
80
|
+
},
|
|
81
|
+
serialize(data) {
|
|
82
|
+
return Array.from(data.entries());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
class OpenAPIJsonSerializer {
|
|
87
|
+
handlers;
|
|
88
|
+
omitUndefinedProperties;
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
this.handlers = {
|
|
91
|
+
...DEFAULT_OPEN_API_JSON_SERIALIZER_HANDLERS,
|
|
92
|
+
...options.handlers
|
|
93
|
+
};
|
|
94
|
+
this.omitUndefinedProperties = options.omitUndefinedProperties !== false;
|
|
95
|
+
}
|
|
96
|
+
serialize(data) {
|
|
97
|
+
const [json, maps, blobs] = this.serializeValue(data, [], [], []);
|
|
98
|
+
return { json, maps, blobs };
|
|
99
|
+
}
|
|
100
|
+
serializeValue(data, segments, maps, blobs) {
|
|
101
|
+
for (const key in this.handlers) {
|
|
102
|
+
const handler = this.handlers[key];
|
|
103
|
+
if (handler && handler.condition(data)) {
|
|
104
|
+
const serialized = handler.serialize(data);
|
|
105
|
+
if (handler.isTerminal) {
|
|
106
|
+
return [serialized, maps, blobs];
|
|
107
|
+
}
|
|
108
|
+
const result = this.serializeValue(serialized, segments, maps, blobs);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (data instanceof Blob) {
|
|
113
|
+
maps.push(segments);
|
|
114
|
+
blobs.push(data);
|
|
115
|
+
return [data, maps, blobs];
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(data)) {
|
|
118
|
+
const json = data.map((v, i) => {
|
|
119
|
+
return this.serializeValue(v, [...segments, i], maps, blobs)[0];
|
|
120
|
+
});
|
|
121
|
+
return [json, maps, blobs];
|
|
122
|
+
}
|
|
123
|
+
if (isPlainObject(data)) {
|
|
124
|
+
const json = {};
|
|
125
|
+
for (const k in data) {
|
|
126
|
+
const v = data[k];
|
|
127
|
+
if (k === "toJSON" && typeof v === "function") {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (v === void 0 && this.omitUndefinedProperties) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
json[k] = this.serializeValue(v, [...segments, k], maps, blobs)[0];
|
|
134
|
+
}
|
|
135
|
+
return [json, maps, blobs];
|
|
136
|
+
}
|
|
137
|
+
return [data, maps, blobs];
|
|
138
|
+
}
|
|
139
|
+
deserialize(serialized) {
|
|
140
|
+
const ref = { data: serialized.json };
|
|
141
|
+
if (serialized.blobs?.length) {
|
|
142
|
+
serialized.maps.forEach((segments, i) => {
|
|
143
|
+
let currentRef = ref;
|
|
144
|
+
let preSegment = "data";
|
|
145
|
+
segments.forEach((segment) => {
|
|
146
|
+
currentRef = currentRef[preSegment];
|
|
147
|
+
preSegment = segment;
|
|
148
|
+
if (!Object.hasOwn(currentRef, preSegment)) {
|
|
149
|
+
throw new Error(`Security error: Invalid serialized data. Segment "${preSegment}" does not exist.`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
currentRef[preSegment] = serialized.blobs[i];
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return ref.data;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
class OpenAPISerializer {
|
|
160
|
+
jsonSerializer;
|
|
161
|
+
bracketNotation;
|
|
162
|
+
defaultSerializeOptions;
|
|
163
|
+
constructor({ bracketNotation, serialize, ...options } = {}) {
|
|
164
|
+
this.jsonSerializer = new OpenAPIJsonSerializer(options);
|
|
165
|
+
this.bracketNotation = new BracketNotationSerializer(bracketNotation);
|
|
166
|
+
this.defaultSerializeOptions = serialize;
|
|
167
|
+
}
|
|
168
|
+
serialize(data, options = {}) {
|
|
169
|
+
const useFormDataForBlobFields = options.useFormDataForBlobFields ?? this.defaultSerializeOptions?.useFormDataForBlobFields ?? true;
|
|
170
|
+
const asFormData = options.asFormData ?? this.defaultSerializeOptions?.asFormData ?? false;
|
|
171
|
+
if (!options.asFormData) {
|
|
172
|
+
if (data === void 0 || data instanceof ReadableStream || data instanceof Blob) {
|
|
173
|
+
return data;
|
|
174
|
+
}
|
|
175
|
+
if (isAsyncIteratorObject(data)) {
|
|
176
|
+
return wrapEventIteratorPreservingMeta(data, {
|
|
177
|
+
mapResult: (result) => {
|
|
178
|
+
if (result.value === void 0) {
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
return { done: result.done, value: this.serializeValue(result.value, { asFormData: false, useFormDataForBlobFields: false }) };
|
|
182
|
+
},
|
|
183
|
+
mapError: (e) => {
|
|
184
|
+
return new ErrorEvent({
|
|
185
|
+
data: this.serializeValue(toORPCError(e).toJSON(), { asFormData: false, useFormDataForBlobFields: false }),
|
|
186
|
+
cause: e
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return this.serializeValue(data, { useFormDataForBlobFields, asFormData });
|
|
193
|
+
}
|
|
194
|
+
serializeValue(value, options) {
|
|
195
|
+
const { json, blobs } = this.jsonSerializer.serialize(value);
|
|
196
|
+
if (!options.asFormData && (json instanceof Blob || json === void 0 || !blobs?.length || !options.useFormDataForBlobFields)) {
|
|
197
|
+
return json;
|
|
198
|
+
}
|
|
199
|
+
const form = new FormData();
|
|
200
|
+
for (const [path, value2] of this.bracketNotation.serialize(json)) {
|
|
201
|
+
if (value2 instanceof Blob) {
|
|
202
|
+
form.append(path, value2);
|
|
203
|
+
} else if (value2 !== void 0 && value2 !== null) {
|
|
204
|
+
form.append(path, String(value2));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return form;
|
|
208
|
+
}
|
|
209
|
+
deserialize(data) {
|
|
210
|
+
if (data === void 0 || data instanceof ReadableStream || data instanceof Blob) {
|
|
211
|
+
return data;
|
|
212
|
+
}
|
|
213
|
+
if (isAsyncIteratorObject(data)) {
|
|
214
|
+
return wrapEventIteratorPreservingMeta(data, {
|
|
215
|
+
mapResult: (result) => {
|
|
216
|
+
if (result.value === void 0) {
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
return { done: result.done, value: this.jsonSerializer.deserialize({ json: result.value }) };
|
|
220
|
+
},
|
|
221
|
+
mapError: (e) => {
|
|
222
|
+
if (e instanceof ErrorEvent) {
|
|
223
|
+
const deserialized = this.jsonSerializer.deserialize({ json: e.data });
|
|
224
|
+
if (isORPCErrorJson(deserialized)) {
|
|
225
|
+
return createORPCErrorFromJson(deserialized, { cause: e });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return e;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (data instanceof URLSearchParams || data instanceof FormData) {
|
|
233
|
+
data = this.bracketNotation.deserialize(Array.from(data.entries()));
|
|
234
|
+
}
|
|
235
|
+
return this.jsonSerializer.deserialize({ json: data });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const PARAMETER_NAME_REGEX = /^[\w-]+$/;
|
|
240
|
+
function getDynamicPathParams(path) {
|
|
241
|
+
if (!path.includes("{")) {
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
const len = path.length;
|
|
245
|
+
let params;
|
|
246
|
+
let index = 1;
|
|
247
|
+
while (index < len) {
|
|
248
|
+
const segmentStart = index;
|
|
249
|
+
const slashPos = path.indexOf("/", index);
|
|
250
|
+
const segmentEnd = slashPos === -1 ? len : slashPos;
|
|
251
|
+
if (segmentEnd > segmentStart && path.charCodeAt(segmentStart) === 123 && path.charCodeAt(segmentEnd - 1) === 125) {
|
|
252
|
+
let parameterStart = segmentStart + 1;
|
|
253
|
+
let allowsSlash = false;
|
|
254
|
+
const operator = path.charCodeAt(parameterStart);
|
|
255
|
+
if (operator === 43) {
|
|
256
|
+
allowsSlash = true;
|
|
257
|
+
parameterStart++;
|
|
258
|
+
}
|
|
259
|
+
const parameterName = path.slice(parameterStart, segmentEnd - 1);
|
|
260
|
+
if (PARAMETER_NAME_REGEX.test(parameterName)) {
|
|
261
|
+
params ??= [];
|
|
262
|
+
params.push({
|
|
263
|
+
segment: path.slice(segmentStart, segmentEnd),
|
|
264
|
+
startIndex: segmentStart,
|
|
265
|
+
parameterName,
|
|
266
|
+
allowsSlash
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
index = segmentEnd + 1;
|
|
271
|
+
}
|
|
272
|
+
return params;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export { DEFAULT_OPENAPI_METHOD as D, OpenAPISerializer as O, DEFAULT_OPENAPI_INPUT_STRUCTURE as a, DEFAULT_OPENAPI_OUTPUT_STRUCTURE as b, DEFAULT_OPENAPI_SUCCESS_DESCRIPTION as c, OpenAPIJsonSerializer as d, getDynamicPathParams as g };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AnyORPCError } from '@orpc/client';
|
|
2
|
+
import { AnyProcedure, AnyRouter, Context } from '@orpc/server';
|
|
3
|
+
import { StandardHandlerCodec, StandardHandlerHandleOptions, StandardHandlerCodecResolvedProcedure } from '@orpc/server/standard';
|
|
4
|
+
import { Value, Promisable } from '@orpc/shared';
|
|
5
|
+
import { StandardLazyRequest, StandardResponse } from '@standardserver/core';
|
|
6
|
+
import { AnyProcedureContract } from '@orpc/contract';
|
|
7
|
+
import { O as OpenAPISerializer } from './openapi.C7m7NAmH.mjs';
|
|
8
|
+
|
|
9
|
+
interface OpenAPIMatcherOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Filter which procedures are exposed for matching. Return `false` to exclude.
|
|
12
|
+
*
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
filter?: Value<boolean, [contract: AnyProcedureContract | AnyProcedure, path: string[]]>;
|
|
16
|
+
}
|
|
17
|
+
declare class OpenAPIMatcher {
|
|
18
|
+
private readonly filter;
|
|
19
|
+
private readonly rootRouter;
|
|
20
|
+
private readonly tree;
|
|
21
|
+
private pendingLazyRouters;
|
|
22
|
+
constructor(router: AnyRouter, options?: OpenAPIMatcherOptions);
|
|
23
|
+
private index;
|
|
24
|
+
match(method: string, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
|
|
25
|
+
path: string[];
|
|
26
|
+
procedure: AnyProcedure;
|
|
27
|
+
params?: Record<string, string> | undefined;
|
|
28
|
+
} | undefined>;
|
|
29
|
+
private matchPathname;
|
|
30
|
+
private resolvePendingLazyRouters;
|
|
31
|
+
private resolveProcedure;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare class OpenAPIHandlerCodecError extends TypeError {
|
|
35
|
+
}
|
|
36
|
+
interface OpenAPIHandlerCodecOptions<_T extends Context> extends OpenAPIMatcherOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Override the default OpenAPI serializer.
|
|
39
|
+
*/
|
|
40
|
+
serializer?: Pick<OpenAPISerializer, keyof OpenAPISerializer>;
|
|
41
|
+
/**
|
|
42
|
+
* Mapping ORPCError Code -> HTTP Status Code
|
|
43
|
+
*
|
|
44
|
+
* @default COMMON_ERROR_STATUS_MAP, DEFAULT_ERROR_STATUS
|
|
45
|
+
*/
|
|
46
|
+
errorStatusMap?: Record<string, number> | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Customize how an ORPC error is serialized into a response body.
|
|
49
|
+
* Use this if your API needs a different error output structure.
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* - Return `null | undefined` to fallback to default behavior
|
|
53
|
+
*
|
|
54
|
+
* @default ((e) => e.toJSON())
|
|
55
|
+
*/
|
|
56
|
+
customErrorResponseBodyEncoder?: (error: AnyORPCError) => unknown;
|
|
57
|
+
}
|
|
58
|
+
declare class OpenAPIHandlerCodec<T extends Context> implements StandardHandlerCodec<T> {
|
|
59
|
+
private readonly matcher;
|
|
60
|
+
private readonly serializer;
|
|
61
|
+
private readonly errorStatusMap;
|
|
62
|
+
private readonly customErrorResponseBodySerializer;
|
|
63
|
+
constructor(router: AnyRouter, options?: OpenAPIHandlerCodecOptions<T>);
|
|
64
|
+
resolveProcedure(request: StandardLazyRequest, options: StandardHandlerHandleOptions<T>): Promise<StandardHandlerCodecResolvedProcedure | undefined>;
|
|
65
|
+
/**
|
|
66
|
+
* @throws {Error} If `outputStructure` is "detailed" and the output doesn't match the expected structure.
|
|
67
|
+
*/
|
|
68
|
+
encodeOutput(output: unknown, procedure: AnyProcedure, path: string[], _options: StandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
|
|
69
|
+
encodeError(error: AnyORPCError, _procedure: AnyProcedure, _path: string[], _options: StandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
|
|
70
|
+
private deserializeQuery;
|
|
71
|
+
private deserializeParams;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { OpenAPIHandlerCodec as a, OpenAPIHandlerCodecError as b, OpenAPIMatcher as c };
|
|
75
|
+
export type { OpenAPIHandlerCodecOptions as O, OpenAPIMatcherOptions as d };
|