@orpc/openapi 0.17.0 → 0.18.0

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.
@@ -0,0 +1,665 @@
1
+ // src/fetch/bracket-notation.ts
2
+ import { isPlainObject } from "@orpc/shared";
3
+ function serialize(payload, parentKey = "") {
4
+ if (!Array.isArray(payload) && !isPlainObject(payload))
5
+ return [["", payload]];
6
+ const result = [];
7
+ function helper(value, path) {
8
+ if (Array.isArray(value)) {
9
+ value.forEach((item, index) => {
10
+ helper(item, [...path, String(index)]);
11
+ });
12
+ } else if (isPlainObject(value)) {
13
+ for (const [key, val] of Object.entries(value)) {
14
+ helper(val, [...path, key]);
15
+ }
16
+ } else {
17
+ result.push([stringifyPath(path), value]);
18
+ }
19
+ }
20
+ helper(payload, parentKey ? [parentKey] : []);
21
+ return result;
22
+ }
23
+ function deserialize(entities) {
24
+ if (entities.length === 0) {
25
+ return void 0;
26
+ }
27
+ const isRootArray = entities.every(([path]) => path === "");
28
+ const result = isRootArray ? [] : {};
29
+ const arrayPushPaths = /* @__PURE__ */ new Set();
30
+ for (const [path, _] of entities) {
31
+ const segments = parsePath(path);
32
+ const base = segments.slice(0, -1).join(".");
33
+ const last = segments[segments.length - 1];
34
+ if (last === "") {
35
+ arrayPushPaths.add(base);
36
+ } else {
37
+ arrayPushPaths.delete(base);
38
+ }
39
+ }
40
+ function setValue(obj, segments, value, fullPath) {
41
+ const [first, ...rest_] = segments;
42
+ if (Array.isArray(obj) && first === "") {
43
+ ;
44
+ obj.push(value);
45
+ return;
46
+ }
47
+ const objAsRecord = obj;
48
+ if (rest_.length === 0) {
49
+ objAsRecord[first] = value;
50
+ return;
51
+ }
52
+ const rest = rest_;
53
+ if (rest[0] === "") {
54
+ const pathToCheck = segments.slice(0, -1).join(".");
55
+ if (rest.length === 1 && arrayPushPaths.has(pathToCheck)) {
56
+ if (!(first in objAsRecord)) {
57
+ objAsRecord[first] = [];
58
+ }
59
+ if (Array.isArray(objAsRecord[first])) {
60
+ ;
61
+ objAsRecord[first].push(value);
62
+ return;
63
+ }
64
+ }
65
+ if (!(first in objAsRecord)) {
66
+ objAsRecord[first] = {};
67
+ }
68
+ const target = objAsRecord[first];
69
+ target[""] = value;
70
+ return;
71
+ }
72
+ if (!(first in objAsRecord)) {
73
+ objAsRecord[first] = {};
74
+ }
75
+ setValue(
76
+ objAsRecord[first],
77
+ rest,
78
+ value,
79
+ fullPath
80
+ );
81
+ }
82
+ for (const [path, value] of entities) {
83
+ const segments = parsePath(path);
84
+ setValue(result, segments, value, path);
85
+ }
86
+ return result;
87
+ }
88
+ function escapeSegment(segment) {
89
+ return segment.replace(/[\\[\]]/g, (match) => {
90
+ switch (match) {
91
+ case "\\":
92
+ return "\\\\";
93
+ case "[":
94
+ return "\\[";
95
+ case "]":
96
+ return "\\]";
97
+ default:
98
+ return match;
99
+ }
100
+ });
101
+ }
102
+ function stringifyPath(path) {
103
+ const [first, ...rest] = path;
104
+ const firstSegment = escapeSegment(first);
105
+ const base = first === "" ? "" : firstSegment;
106
+ return rest.reduce(
107
+ (result, segment) => `${result}[${escapeSegment(segment)}]`,
108
+ base
109
+ );
110
+ }
111
+ function parsePath(path) {
112
+ if (path === "")
113
+ return [""];
114
+ const result = [];
115
+ let currentSegment = "";
116
+ let inBracket = false;
117
+ let bracketContent = "";
118
+ let backslashCount = 0;
119
+ for (let i = 0; i < path.length; i++) {
120
+ const char = path[i];
121
+ if (char === "\\") {
122
+ backslashCount++;
123
+ continue;
124
+ }
125
+ if (backslashCount > 0) {
126
+ const literalBackslashes = "\\".repeat(Math.floor(backslashCount / 2));
127
+ if (char === "[" || char === "]") {
128
+ if (backslashCount % 2 === 1) {
129
+ if (inBracket) {
130
+ bracketContent += literalBackslashes + char;
131
+ } else {
132
+ currentSegment += literalBackslashes + char;
133
+ }
134
+ } else {
135
+ if (inBracket) {
136
+ bracketContent += literalBackslashes;
137
+ } else {
138
+ currentSegment += literalBackslashes;
139
+ }
140
+ if (char === "[" && !inBracket) {
141
+ if (currentSegment !== "" || result.length === 0) {
142
+ result.push(currentSegment);
143
+ }
144
+ inBracket = true;
145
+ bracketContent = "";
146
+ currentSegment = "";
147
+ } else if (char === "]" && inBracket) {
148
+ result.push(bracketContent);
149
+ inBracket = false;
150
+ bracketContent = "";
151
+ } else {
152
+ if (inBracket) {
153
+ bracketContent += char;
154
+ } else {
155
+ currentSegment += char;
156
+ }
157
+ }
158
+ }
159
+ } else {
160
+ const allBackslashes = "\\".repeat(backslashCount);
161
+ if (inBracket) {
162
+ bracketContent += allBackslashes + char;
163
+ } else {
164
+ currentSegment += allBackslashes + char;
165
+ }
166
+ }
167
+ backslashCount = 0;
168
+ continue;
169
+ }
170
+ if (char === "[" && !inBracket) {
171
+ if (currentSegment !== "" || result.length === 0) {
172
+ result.push(currentSegment);
173
+ }
174
+ inBracket = true;
175
+ bracketContent = "";
176
+ currentSegment = "";
177
+ continue;
178
+ }
179
+ if (char === "]" && inBracket) {
180
+ result.push(bracketContent);
181
+ inBracket = false;
182
+ bracketContent = "";
183
+ continue;
184
+ }
185
+ if (inBracket) {
186
+ bracketContent += char;
187
+ } else {
188
+ currentSegment += char;
189
+ }
190
+ }
191
+ if (backslashCount > 0) {
192
+ const remainingBackslashes = "\\".repeat(backslashCount);
193
+ if (inBracket) {
194
+ bracketContent += remainingBackslashes;
195
+ } else {
196
+ currentSegment += remainingBackslashes;
197
+ }
198
+ }
199
+ if (inBracket) {
200
+ if (currentSegment !== "" || result.length === 0) {
201
+ result.push(currentSegment);
202
+ }
203
+ result.push(`[${bracketContent}`);
204
+ } else if (currentSegment !== "" || result.length === 0) {
205
+ result.push(currentSegment);
206
+ }
207
+ return result;
208
+ }
209
+
210
+ // src/fetch/input-builder-full.ts
211
+ var InputBuilderFull = class {
212
+ build(params, query, headers, body) {
213
+ return {
214
+ params,
215
+ query,
216
+ headers,
217
+ body
218
+ };
219
+ }
220
+ };
221
+
222
+ // src/fetch/input-builder-simple.ts
223
+ import { isPlainObject as isPlainObject2 } from "@orpc/shared";
224
+ var InputBuilderSimple = class {
225
+ build(params, payload) {
226
+ if (Object.keys(params).length === 0) {
227
+ return payload;
228
+ }
229
+ if (!isPlainObject2(payload)) {
230
+ return params;
231
+ }
232
+ return {
233
+ ...params,
234
+ ...payload
235
+ };
236
+ }
237
+ };
238
+
239
+ // src/fetch/openapi-handler.ts
240
+ import { createProcedureClient, ORPCError as ORPCError2 } from "@orpc/server";
241
+ import { executeWithHooks, ORPC_HANDLER_HEADER, trim } from "@orpc/shared";
242
+
243
+ // src/fetch/openapi-payload-codec.ts
244
+ import { ORPCError } from "@orpc/server";
245
+ import { findDeepMatches, isPlainObject as isPlainObject3 } from "@orpc/shared";
246
+ import cd from "content-disposition";
247
+ import { safeParse } from "fast-content-type-parse";
248
+ import wcmatch from "wildcard-match";
249
+ var OpenAPIPayloadCodec = class {
250
+ encode(payload, accept) {
251
+ const typeMatchers = (accept?.split(",").map(safeParse) ?? [{ type: "*/*" }]).map(({ type }) => wcmatch(type));
252
+ if (payload instanceof Blob) {
253
+ const contentType = payload.type || "application/octet-stream";
254
+ if (typeMatchers.some((isMatch) => isMatch(contentType))) {
255
+ const headers = new Headers({
256
+ "Content-Type": contentType
257
+ });
258
+ if (payload instanceof File && payload.name) {
259
+ headers.append("Content-Disposition", cd(payload.name));
260
+ }
261
+ return {
262
+ body: payload,
263
+ headers
264
+ };
265
+ }
266
+ }
267
+ const handledPayload = this.serialize(payload);
268
+ const hasBlobs = findDeepMatches((v) => v instanceof Blob, handledPayload).values.length > 0;
269
+ const isExpectedMultipartFormData = typeMatchers.some(
270
+ (isMatch) => isMatch("multipart/form-data")
271
+ );
272
+ if (hasBlobs && isExpectedMultipartFormData) {
273
+ return this.encodeAsFormData(handledPayload);
274
+ }
275
+ if (typeMatchers.some((isMatch) => isMatch("application/json"))) {
276
+ return this.encodeAsJSON(handledPayload);
277
+ }
278
+ if (typeMatchers.some(
279
+ (isMatch) => isMatch("application/x-www-form-urlencoded")
280
+ )) {
281
+ return this.encodeAsURLSearchParams(handledPayload);
282
+ }
283
+ if (isExpectedMultipartFormData) {
284
+ return this.encodeAsFormData(handledPayload);
285
+ }
286
+ throw new ORPCError({
287
+ code: "NOT_ACCEPTABLE",
288
+ message: `Unsupported content-type: ${accept}`
289
+ });
290
+ }
291
+ encodeAsJSON(payload) {
292
+ if (payload === void 0) {
293
+ return {
294
+ body: void 0,
295
+ headers: new Headers({
296
+ "content-type": "application/json"
297
+ })
298
+ };
299
+ }
300
+ return {
301
+ body: JSON.stringify(payload),
302
+ headers: new Headers({
303
+ "content-type": "application/json"
304
+ })
305
+ };
306
+ }
307
+ encodeAsFormData(payload) {
308
+ const form = new FormData();
309
+ for (const [path, value] of serialize(payload)) {
310
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
311
+ form.append(path, value.toString());
312
+ } else if (value === null) {
313
+ form.append(path, "null");
314
+ } else if (value instanceof Date) {
315
+ form.append(
316
+ path,
317
+ Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString()
318
+ );
319
+ } else if (value instanceof Blob) {
320
+ form.append(path, value);
321
+ }
322
+ }
323
+ return {
324
+ body: form
325
+ };
326
+ }
327
+ encodeAsURLSearchParams(payload) {
328
+ const params = new URLSearchParams();
329
+ for (const [path, value] of serialize(payload)) {
330
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
331
+ params.append(path, value.toString());
332
+ } else if (value === null) {
333
+ params.append(path, "null");
334
+ } else if (value instanceof Date) {
335
+ params.append(
336
+ path,
337
+ Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString()
338
+ );
339
+ }
340
+ }
341
+ return {
342
+ body: params.toString(),
343
+ headers: new Headers({
344
+ "content-type": "application/x-www-form-urlencoded"
345
+ })
346
+ };
347
+ }
348
+ serialize(payload) {
349
+ if (payload instanceof Set)
350
+ return this.serialize([...payload]);
351
+ if (payload instanceof Map)
352
+ return this.serialize([...payload.entries()]);
353
+ if (Array.isArray(payload)) {
354
+ return payload.map((v) => v === void 0 ? "undefined" : this.serialize(v));
355
+ }
356
+ if (Number.isNaN(payload))
357
+ return "NaN";
358
+ if (typeof payload === "bigint")
359
+ return payload.toString();
360
+ if (payload instanceof Date && Number.isNaN(payload.getTime())) {
361
+ return "Invalid Date";
362
+ }
363
+ if (payload instanceof RegExp)
364
+ return payload.toString();
365
+ if (payload instanceof URL)
366
+ return payload.toString();
367
+ if (!isPlainObject3(payload))
368
+ return payload;
369
+ return Object.keys(payload).reduce(
370
+ (carry, key) => {
371
+ const val = payload[key];
372
+ carry[key] = this.serialize(val);
373
+ return carry;
374
+ },
375
+ {}
376
+ );
377
+ }
378
+ async decode(re) {
379
+ if (re instanceof Headers || re instanceof URLSearchParams || re instanceof FormData) {
380
+ return deserialize([...re.entries()]);
381
+ }
382
+ const contentType = re.headers.get("content-type");
383
+ const contentDisposition = re.headers.get("content-disposition");
384
+ const fileName = contentDisposition ? cd.parse(contentDisposition).parameters.filename : void 0;
385
+ if (fileName) {
386
+ const blob2 = await re.blob();
387
+ const file = new File([blob2], fileName, {
388
+ type: blob2.type
389
+ });
390
+ return file;
391
+ }
392
+ if (!contentType || contentType.startsWith("application/json")) {
393
+ if (!re.body) {
394
+ return void 0;
395
+ }
396
+ return await re.json();
397
+ }
398
+ if (contentType.startsWith("application/x-www-form-urlencoded")) {
399
+ const params = new URLSearchParams(await re.text());
400
+ return this.decode(params);
401
+ }
402
+ if (contentType.startsWith("text/")) {
403
+ const text = await re.text();
404
+ return text;
405
+ }
406
+ if (contentType.startsWith("multipart/form-data")) {
407
+ const form = await re.formData();
408
+ return this.decode(form);
409
+ }
410
+ const blob = await re.blob();
411
+ return new File([blob], "blob", {
412
+ type: blob.type
413
+ });
414
+ }
415
+ };
416
+
417
+ // src/fetch/openapi-procedure-matcher.ts
418
+ import { getLazyRouterPrefix, getRouterChild, isProcedure as isProcedure2, unlazy } from "@orpc/server";
419
+ import { mapValues } from "@orpc/shared";
420
+
421
+ // src/utils.ts
422
+ import { isContractProcedure } from "@orpc/contract";
423
+ import { getRouterContract, isLazy, isProcedure } from "@orpc/server";
424
+ function forEachContractProcedure(options, callback, result = [], isCurrentRouterContract = false) {
425
+ const hiddenContract = getRouterContract(options.router);
426
+ if (!isCurrentRouterContract && hiddenContract) {
427
+ return forEachContractProcedure(
428
+ {
429
+ path: options.path,
430
+ router: hiddenContract
431
+ },
432
+ callback,
433
+ result,
434
+ true
435
+ );
436
+ }
437
+ if (isLazy(options.router)) {
438
+ result.push({
439
+ router: options.router,
440
+ path: options.path
441
+ });
442
+ } else if (isProcedure(options.router)) {
443
+ callback({
444
+ contract: options.router["~orpc"].contract,
445
+ path: options.path
446
+ });
447
+ } else if (isContractProcedure(options.router)) {
448
+ callback({
449
+ contract: options.router,
450
+ path: options.path
451
+ });
452
+ } else {
453
+ for (const key in options.router) {
454
+ forEachContractProcedure(
455
+ {
456
+ router: options.router[key],
457
+ path: [...options.path, key]
458
+ },
459
+ callback,
460
+ result
461
+ );
462
+ }
463
+ }
464
+ return result;
465
+ }
466
+ function standardizeHTTPPath(path) {
467
+ return `/${path.replace(/\/{2,}/g, "/").replace(/^\/|\/$/g, "")}`;
468
+ }
469
+
470
+ // src/fetch/openapi-procedure-matcher.ts
471
+ var OpenAPIProcedureMatcher = class {
472
+ constructor(hono, router) {
473
+ this.hono = hono;
474
+ this.router = router;
475
+ this.pendingRouters = [{ path: [], router }];
476
+ }
477
+ pendingRouters;
478
+ async match(method, pathname) {
479
+ await this.handlePendingRouters(pathname);
480
+ const [matches, paramStash] = this.hono.match(method, pathname);
481
+ const [match] = matches.sort((a, b) => {
482
+ const slashCountA = a[0][0].split("/").length;
483
+ const slashCountB = b[0][0].split("/").length;
484
+ if (slashCountA !== slashCountB) {
485
+ return slashCountB - slashCountA;
486
+ }
487
+ const paramsCountA = Object.keys(a[1]).length;
488
+ const paramsCountB = Object.keys(b[1]).length;
489
+ return paramsCountA - paramsCountB;
490
+ });
491
+ if (!match) {
492
+ return void 0;
493
+ }
494
+ const path = match[0][1];
495
+ const params = paramStash ? mapValues(
496
+ match[1],
497
+ // if paramStash is defined, then match[1] is ParamIndexMap
498
+ (v) => paramStash[v]
499
+ ) : match[1];
500
+ const { default: maybeProcedure } = await unlazy(getRouterChild(this.router, ...path));
501
+ if (!isProcedure2(maybeProcedure)) {
502
+ return void 0;
503
+ }
504
+ return {
505
+ path,
506
+ procedure: maybeProcedure,
507
+ params: { ...params }
508
+ // normalize params from hono
509
+ };
510
+ }
511
+ add(path, router) {
512
+ const lazies = forEachContractProcedure({ path, router }, ({ path: path2, contract }) => {
513
+ const method = contract["~orpc"].route?.method ?? "POST";
514
+ const httpPath = contract["~orpc"].route?.path ? this.convertOpenAPIPathToRouterPath(contract["~orpc"].route?.path) : `/${path2.map(encodeURIComponent).join("/")}`;
515
+ this.hono.add(method, httpPath, [httpPath, path2]);
516
+ });
517
+ this.pendingRouters.push(...lazies);
518
+ }
519
+ async handlePendingRouters(pathname) {
520
+ const newPendingLazyRouters = [];
521
+ for (const item of this.pendingRouters) {
522
+ const lazyPrefix = getLazyRouterPrefix(item.router);
523
+ if (lazyPrefix && !pathname.startsWith(lazyPrefix) && !pathname.startsWith(`/${item.path.map(encodeURIComponent).join("/")}`)) {
524
+ newPendingLazyRouters.push(item);
525
+ continue;
526
+ }
527
+ const { default: router } = await unlazy(item.router);
528
+ this.add(item.path, router);
529
+ }
530
+ this.pendingRouters = newPendingLazyRouters;
531
+ }
532
+ convertOpenAPIPathToRouterPath(path) {
533
+ return standardizeHTTPPath(path).replace(/\{([^}]+)\}/g, ":$1");
534
+ }
535
+ };
536
+
537
+ // src/fetch/schema-coercer.ts
538
+ var CompositeSchemaCoercer = class {
539
+ constructor(coercers) {
540
+ this.coercers = coercers;
541
+ }
542
+ coerce(schema, value) {
543
+ let current = value;
544
+ for (const coercer of this.coercers) {
545
+ current = coercer.coerce(schema, current);
546
+ }
547
+ return current;
548
+ }
549
+ };
550
+
551
+ // src/fetch/openapi-handler.ts
552
+ var OpenAPIHandler = class {
553
+ constructor(hono, router, options) {
554
+ this.options = options;
555
+ this.procedureMatcher = options?.procedureMatcher ?? new OpenAPIProcedureMatcher(hono, router);
556
+ this.payloadCodec = options?.payloadCodec ?? new OpenAPIPayloadCodec();
557
+ this.inputBuilderSimple = options?.inputBuilderSimple ?? new InputBuilderSimple();
558
+ this.inputBuilderFull = options?.inputBuilderFull ?? new InputBuilderFull();
559
+ this.compositeSchemaCoercer = new CompositeSchemaCoercer(options?.schemaCoercers ?? []);
560
+ }
561
+ procedureMatcher;
562
+ payloadCodec;
563
+ inputBuilderSimple;
564
+ inputBuilderFull;
565
+ compositeSchemaCoercer;
566
+ condition(request) {
567
+ return request.headers.get(ORPC_HANDLER_HEADER) === null;
568
+ }
569
+ async fetch(request, ...[options]) {
570
+ const context = options?.context;
571
+ const headers = request.headers;
572
+ const accept = headers.get("Accept") || void 0;
573
+ const execute = async () => {
574
+ const url = new URL(request.url);
575
+ const pathname = `/${trim(url.pathname.replace(options?.prefix ?? "", ""), "/")}`;
576
+ const query = url.searchParams;
577
+ const customMethod = request.method === "POST" ? query.get("method")?.toUpperCase() : void 0;
578
+ const method = customMethod || request.method;
579
+ const match = await this.procedureMatcher.match(method, pathname);
580
+ if (!match) {
581
+ throw new ORPCError2({ code: "NOT_FOUND", message: "Not found" });
582
+ }
583
+ const decodedPayload = request.method === "GET" ? await this.payloadCodec.decode(query) : await this.payloadCodec.decode(request);
584
+ const input = this.inputBuilderSimple.build(match.params, decodedPayload);
585
+ const coercedInput = this.compositeSchemaCoercer.coerce(match.procedure["~orpc"].contract["~orpc"].InputSchema, input);
586
+ const client = createProcedureClient({
587
+ context,
588
+ procedure: match.procedure,
589
+ path: match.path
590
+ });
591
+ const output = await client(coercedInput, { signal: options?.signal });
592
+ const { body, headers: headers2 } = this.payloadCodec.encode(output);
593
+ return new Response(body, { headers: headers2 });
594
+ };
595
+ try {
596
+ return await executeWithHooks({
597
+ context,
598
+ execute,
599
+ input: request,
600
+ hooks: this.options,
601
+ meta: {
602
+ signal: options?.signal
603
+ }
604
+ });
605
+ } catch (e) {
606
+ const error = this.convertToORPCError(e);
607
+ try {
608
+ const { body, headers: headers2 } = this.payloadCodec.encode(error.toJSON(), accept);
609
+ return new Response(body, {
610
+ status: error.status,
611
+ headers: headers2
612
+ });
613
+ } catch (e2) {
614
+ const error2 = this.convertToORPCError(e2);
615
+ const { body, headers: headers2 } = this.payloadCodec.encode(error2.toJSON());
616
+ return new Response(body, {
617
+ status: error2.status,
618
+ headers: headers2
619
+ });
620
+ }
621
+ }
622
+ }
623
+ convertToORPCError(e) {
624
+ return e instanceof ORPCError2 ? e : new ORPCError2({
625
+ code: "INTERNAL_SERVER_ERROR",
626
+ message: "Internal server error",
627
+ cause: e
628
+ });
629
+ }
630
+ };
631
+
632
+ // src/fetch/openapi-handler-server.ts
633
+ import { TrieRouter } from "hono/router/trie-router";
634
+ var OpenAPIServerHandler = class extends OpenAPIHandler {
635
+ constructor(router, options) {
636
+ super(new TrieRouter(), router, options);
637
+ }
638
+ };
639
+
640
+ // src/fetch/openapi-handler-serverless.ts
641
+ import { LinearRouter } from "hono/router/linear-router";
642
+ var OpenAPIServerlessHandler = class extends OpenAPIHandler {
643
+ constructor(router, options) {
644
+ super(new LinearRouter(), router, options);
645
+ }
646
+ };
647
+
648
+ export {
649
+ serialize,
650
+ deserialize,
651
+ escapeSegment,
652
+ stringifyPath,
653
+ parsePath,
654
+ InputBuilderFull,
655
+ InputBuilderSimple,
656
+ OpenAPIPayloadCodec,
657
+ forEachContractProcedure,
658
+ standardizeHTTPPath,
659
+ OpenAPIProcedureMatcher,
660
+ CompositeSchemaCoercer,
661
+ OpenAPIHandler,
662
+ OpenAPIServerHandler,
663
+ OpenAPIServerlessHandler
664
+ };
665
+ //# sourceMappingURL=chunk-UPDKQRQG.js.map