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