@orpc/contract 0.0.0-next.68378b4 → 0.0.0-next.683f2ee
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 +148 -28
- package/dist/index.d.mts +263 -168
- package/dist/index.d.ts +263 -168
- package/dist/index.mjs +184 -54
- package/dist/plugins/index.d.mts +43 -0
- package/dist/plugins/index.d.ts +43 -0
- package/dist/plugins/index.mjs +81 -0
- package/dist/shared/contract.D_dZrO__.mjs +53 -0
- package/dist/shared/contract.TuRtB1Ca.d.mts +254 -0
- package/dist/shared/contract.TuRtB1Ca.d.ts +254 -0
- package/package.json +16 -11
package/dist/index.mjs
CHANGED
|
@@ -1,41 +1,15 @@
|
|
|
1
|
+
import { i as isContractProcedure, C as ContractProcedure, m as mergeErrorMap, V as ValidationError } from './shared/contract.D_dZrO__.mjs';
|
|
2
|
+
export { v as validateORPCError } from './shared/contract.D_dZrO__.mjs';
|
|
3
|
+
import { toHttpPath } from '@orpc/client/standard';
|
|
4
|
+
import { toArray, isAsyncIteratorObject, get, isTypescriptObject, isPropertyKey } from '@orpc/shared';
|
|
5
|
+
export { AsyncIteratorClass } from '@orpc/shared';
|
|
1
6
|
import { mapEventIterator, ORPCError } from '@orpc/client';
|
|
2
7
|
export { ORPCError } from '@orpc/client';
|
|
3
|
-
import { isAsyncIteratorObject } from '@orpc/shared';
|
|
4
|
-
|
|
5
|
-
class ValidationError extends Error {
|
|
6
|
-
issues;
|
|
7
|
-
constructor(options) {
|
|
8
|
-
super(options.message, options);
|
|
9
|
-
this.issues = options.issues;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
function mergeErrorMap(errorMap1, errorMap2) {
|
|
13
|
-
return { ...errorMap1, ...errorMap2 };
|
|
14
|
-
}
|
|
15
8
|
|
|
16
9
|
function mergeMeta(meta1, meta2) {
|
|
17
10
|
return { ...meta1, ...meta2 };
|
|
18
11
|
}
|
|
19
12
|
|
|
20
|
-
class ContractProcedure {
|
|
21
|
-
"~orpc";
|
|
22
|
-
constructor(def) {
|
|
23
|
-
if (def.route?.successStatus && (def.route.successStatus < 200 || def.route?.successStatus > 299)) {
|
|
24
|
-
throw new Error("[ContractProcedure] The successStatus must be between 200 and 299");
|
|
25
|
-
}
|
|
26
|
-
if (Object.values(def.errorMap).some((val) => val && val.status && (val.status < 400 || val.status > 599))) {
|
|
27
|
-
throw new Error("[ContractProcedure] The error status code must be in the 400-599 range.");
|
|
28
|
-
}
|
|
29
|
-
this["~orpc"] = def;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function isContractProcedure(item) {
|
|
33
|
-
if (item instanceof ContractProcedure) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
return (typeof item === "object" || typeof item === "function") && item !== null && "~orpc" in item && typeof item["~orpc"] === "object" && item["~orpc"] !== null && "inputSchema" in item["~orpc"] && "outputSchema" in item["~orpc"] && "errorMap" in item["~orpc"] && "route" in item["~orpc"] && "meta" in item["~orpc"];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
13
|
function mergeRoute(a, b) {
|
|
40
14
|
return { ...a, ...b };
|
|
41
15
|
}
|
|
@@ -60,31 +34,94 @@ function mergePrefix(a, b) {
|
|
|
60
34
|
function mergeTags(a, b) {
|
|
61
35
|
return a ? [...a, ...b] : b;
|
|
62
36
|
}
|
|
63
|
-
function
|
|
37
|
+
function enhanceRoute(route, options) {
|
|
64
38
|
let router = route;
|
|
65
39
|
if (options.prefix) {
|
|
66
40
|
router = prefixRoute(router, options.prefix);
|
|
67
41
|
}
|
|
68
|
-
if (options.tags) {
|
|
42
|
+
if (options.tags?.length) {
|
|
69
43
|
router = unshiftTagRoute(router, options.tags);
|
|
70
44
|
}
|
|
71
45
|
return router;
|
|
72
46
|
}
|
|
73
47
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
function getContractRouter(router, path) {
|
|
49
|
+
let current = router;
|
|
50
|
+
for (let i = 0; i < path.length; i++) {
|
|
51
|
+
const segment = path[i];
|
|
52
|
+
if (!current) {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
if (isContractProcedure(current)) {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
if (typeof current !== "object") {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
current = current[segment];
|
|
62
|
+
}
|
|
63
|
+
return current;
|
|
64
|
+
}
|
|
65
|
+
function enhanceContractRouter(router, options) {
|
|
66
|
+
if (isContractProcedure(router)) {
|
|
67
|
+
const enhanced2 = new ContractProcedure({
|
|
68
|
+
...router["~orpc"],
|
|
69
|
+
errorMap: mergeErrorMap(options.errorMap, router["~orpc"].errorMap),
|
|
70
|
+
route: enhanceRoute(router["~orpc"].route, options)
|
|
80
71
|
});
|
|
81
|
-
return
|
|
72
|
+
return enhanced2;
|
|
73
|
+
}
|
|
74
|
+
if (typeof router !== "object" || router === null) {
|
|
75
|
+
return router;
|
|
76
|
+
}
|
|
77
|
+
const enhanced = {};
|
|
78
|
+
for (const key in router) {
|
|
79
|
+
enhanced[key] = enhanceContractRouter(router[key], options);
|
|
80
|
+
}
|
|
81
|
+
return enhanced;
|
|
82
|
+
}
|
|
83
|
+
function minifyContractRouter(router) {
|
|
84
|
+
if (isContractProcedure(router)) {
|
|
85
|
+
const procedure = {
|
|
86
|
+
"~orpc": {
|
|
87
|
+
errorMap: {},
|
|
88
|
+
meta: router["~orpc"].meta,
|
|
89
|
+
route: router["~orpc"].route
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return procedure;
|
|
82
93
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
adapted[key] = adaptContractRouter(contract[key], options);
|
|
94
|
+
if (typeof router !== "object" || router === null) {
|
|
95
|
+
return router;
|
|
86
96
|
}
|
|
87
|
-
|
|
97
|
+
const json = {};
|
|
98
|
+
for (const key in router) {
|
|
99
|
+
json[key] = minifyContractRouter(router[key]);
|
|
100
|
+
}
|
|
101
|
+
return json;
|
|
102
|
+
}
|
|
103
|
+
function populateContractRouterPaths(router, options = {}) {
|
|
104
|
+
const path = toArray(options.path);
|
|
105
|
+
if (isContractProcedure(router)) {
|
|
106
|
+
if (router["~orpc"].route.path === void 0) {
|
|
107
|
+
return new ContractProcedure({
|
|
108
|
+
...router["~orpc"],
|
|
109
|
+
route: {
|
|
110
|
+
...router["~orpc"].route,
|
|
111
|
+
path: toHttpPath(path)
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return router;
|
|
116
|
+
}
|
|
117
|
+
if (typeof router !== "object" || router === null) {
|
|
118
|
+
return router;
|
|
119
|
+
}
|
|
120
|
+
const populated = {};
|
|
121
|
+
for (const key in router) {
|
|
122
|
+
populated[key] = populateContractRouterPaths(router[key], { ...options, path: [...path, key] });
|
|
123
|
+
}
|
|
124
|
+
return populated;
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
class ContractBuilder extends ContractProcedure {
|
|
@@ -94,7 +131,9 @@ class ContractBuilder extends ContractProcedure {
|
|
|
94
131
|
this["~orpc"].tags = def.tags;
|
|
95
132
|
}
|
|
96
133
|
/**
|
|
97
|
-
*
|
|
134
|
+
* Sets or overrides the initial meta.
|
|
135
|
+
*
|
|
136
|
+
* @see {@link https://orpc.dev/docs/metadata Metadata Docs}
|
|
98
137
|
*/
|
|
99
138
|
$meta(initialMeta) {
|
|
100
139
|
return new ContractBuilder({
|
|
@@ -103,7 +142,11 @@ class ContractBuilder extends ContractProcedure {
|
|
|
103
142
|
});
|
|
104
143
|
}
|
|
105
144
|
/**
|
|
106
|
-
*
|
|
145
|
+
* Sets or overrides the initial route.
|
|
146
|
+
* This option is typically relevant when integrating with OpenAPI.
|
|
147
|
+
*
|
|
148
|
+
* @see {@link https://orpc.dev/docs/openapi/routing OpenAPI Routing Docs}
|
|
149
|
+
* @see {@link https://orpc.dev/docs/openapi/input-output-structure OpenAPI Input/Output Structure Docs}
|
|
107
150
|
*/
|
|
108
151
|
$route(initialRoute) {
|
|
109
152
|
return new ContractBuilder({
|
|
@@ -111,56 +154,114 @@ class ContractBuilder extends ContractProcedure {
|
|
|
111
154
|
route: initialRoute
|
|
112
155
|
});
|
|
113
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Sets or overrides the initial input schema.
|
|
159
|
+
*
|
|
160
|
+
* @see {@link https://orpc.dev/docs/procedure#initial-configuration Initial Procedure Configuration Docs}
|
|
161
|
+
*/
|
|
162
|
+
$input(initialInputSchema) {
|
|
163
|
+
return new ContractBuilder({
|
|
164
|
+
...this["~orpc"],
|
|
165
|
+
inputSchema: initialInputSchema
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Adds type-safe custom errors to the contract.
|
|
170
|
+
* The provided errors are spared-merged with any existing errors in the contract.
|
|
171
|
+
*
|
|
172
|
+
* @see {@link https://orpc.dev/docs/error-handling#type%E2%80%90safe-error-handling Type-Safe Error Handling Docs}
|
|
173
|
+
*/
|
|
114
174
|
errors(errors) {
|
|
115
175
|
return new ContractBuilder({
|
|
116
176
|
...this["~orpc"],
|
|
117
177
|
errorMap: mergeErrorMap(this["~orpc"].errorMap, errors)
|
|
118
178
|
});
|
|
119
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Sets or updates the metadata for the contract.
|
|
182
|
+
* The provided metadata is spared-merged with any existing metadata in the contract.
|
|
183
|
+
*
|
|
184
|
+
* @see {@link https://orpc.dev/docs/metadata Metadata Docs}
|
|
185
|
+
*/
|
|
120
186
|
meta(meta) {
|
|
121
187
|
return new ContractBuilder({
|
|
122
188
|
...this["~orpc"],
|
|
123
189
|
meta: mergeMeta(this["~orpc"].meta, meta)
|
|
124
190
|
});
|
|
125
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Sets or updates the route definition for the contract.
|
|
194
|
+
* The provided route is spared-merged with any existing route in the contract.
|
|
195
|
+
* This option is typically relevant when integrating with OpenAPI.
|
|
196
|
+
*
|
|
197
|
+
* @see {@link https://orpc.dev/docs/openapi/routing OpenAPI Routing Docs}
|
|
198
|
+
* @see {@link https://orpc.dev/docs/openapi/input-output-structure OpenAPI Input/Output Structure Docs}
|
|
199
|
+
*/
|
|
126
200
|
route(route) {
|
|
127
201
|
return new ContractBuilder({
|
|
128
202
|
...this["~orpc"],
|
|
129
203
|
route: mergeRoute(this["~orpc"].route, route)
|
|
130
204
|
});
|
|
131
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Defines the input validation schema for the contract.
|
|
208
|
+
*
|
|
209
|
+
* @see {@link https://orpc.dev/docs/procedure#input-output-validation Input Validation Docs}
|
|
210
|
+
*/
|
|
132
211
|
input(schema) {
|
|
133
212
|
return new ContractBuilder({
|
|
134
213
|
...this["~orpc"],
|
|
135
214
|
inputSchema: schema
|
|
136
215
|
});
|
|
137
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Defines the output validation schema for the contract.
|
|
219
|
+
*
|
|
220
|
+
* @see {@link https://orpc.dev/docs/procedure#input-output-validation Output Validation Docs}
|
|
221
|
+
*/
|
|
138
222
|
output(schema) {
|
|
139
223
|
return new ContractBuilder({
|
|
140
224
|
...this["~orpc"],
|
|
141
225
|
outputSchema: schema
|
|
142
226
|
});
|
|
143
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Prefixes all procedures in the contract router.
|
|
230
|
+
* The provided prefix is post-appended to any existing router prefix.
|
|
231
|
+
*
|
|
232
|
+
* @note This option does not affect procedures that do not define a path in their route definition.
|
|
233
|
+
*
|
|
234
|
+
* @see {@link https://orpc.dev/docs/openapi/routing#route-prefixes OpenAPI Route Prefixes Docs}
|
|
235
|
+
*/
|
|
144
236
|
prefix(prefix) {
|
|
145
237
|
return new ContractBuilder({
|
|
146
238
|
...this["~orpc"],
|
|
147
239
|
prefix: mergePrefix(this["~orpc"].prefix, prefix)
|
|
148
240
|
});
|
|
149
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Adds tags to all procedures in the contract router.
|
|
244
|
+
* This helpful when you want to group procedures together in the OpenAPI specification.
|
|
245
|
+
*
|
|
246
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification#operation-metadata OpenAPI Operation Metadata Docs}
|
|
247
|
+
*/
|
|
150
248
|
tag(...tags) {
|
|
151
249
|
return new ContractBuilder({
|
|
152
250
|
...this["~orpc"],
|
|
153
251
|
tags: mergeTags(this["~orpc"].tags, tags)
|
|
154
252
|
});
|
|
155
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Applies all of the previously defined options to the specified contract router.
|
|
256
|
+
*
|
|
257
|
+
* @see {@link https://orpc.dev/docs/router#extending-router Extending Router Docs}
|
|
258
|
+
*/
|
|
156
259
|
router(router) {
|
|
157
|
-
return
|
|
260
|
+
return enhanceContractRouter(router, this["~orpc"]);
|
|
158
261
|
}
|
|
159
262
|
}
|
|
160
263
|
const oc = new ContractBuilder({
|
|
161
264
|
errorMap: {},
|
|
162
|
-
inputSchema: void 0,
|
|
163
|
-
outputSchema: void 0,
|
|
164
265
|
route: {},
|
|
165
266
|
meta: {}
|
|
166
267
|
});
|
|
@@ -179,11 +280,11 @@ function fallbackContractConfig(key, value) {
|
|
|
179
280
|
return value;
|
|
180
281
|
}
|
|
181
282
|
|
|
182
|
-
const
|
|
283
|
+
const EVENT_ITERATOR_DETAILS_SYMBOL = Symbol("ORPC_EVENT_ITERATOR_DETAILS");
|
|
183
284
|
function eventIterator(yields, returns) {
|
|
184
285
|
return {
|
|
185
286
|
"~standard": {
|
|
186
|
-
[
|
|
287
|
+
[EVENT_ITERATOR_DETAILS_SYMBOL]: { yields, returns },
|
|
187
288
|
vendor: "orpc",
|
|
188
289
|
version: 1,
|
|
189
290
|
validate(iterator) {
|
|
@@ -202,7 +303,8 @@ function eventIterator(yields, returns) {
|
|
|
202
303
|
message: "Event iterator validation failed",
|
|
203
304
|
cause: new ValidationError({
|
|
204
305
|
issues: result.issues,
|
|
205
|
-
message: "Event iterator validation failed"
|
|
306
|
+
message: "Event iterator validation failed",
|
|
307
|
+
data: value
|
|
206
308
|
})
|
|
207
309
|
});
|
|
208
310
|
}
|
|
@@ -219,7 +321,20 @@ function getEventIteratorSchemaDetails(schema) {
|
|
|
219
321
|
if (schema === void 0) {
|
|
220
322
|
return void 0;
|
|
221
323
|
}
|
|
222
|
-
return schema["~standard"][
|
|
324
|
+
return schema["~standard"][EVENT_ITERATOR_DETAILS_SYMBOL];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function inferRPCMethodFromContractRouter(contract) {
|
|
328
|
+
return (_, path) => {
|
|
329
|
+
const procedure = get(contract, path);
|
|
330
|
+
if (!isContractProcedure(procedure)) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`[inferRPCMethodFromContractRouter] No valid procedure found at path "${path.join(".")}". This may happen when the contract router is not properly configured.`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const method = fallbackContractConfig("defaultMethod", procedure["~orpc"].route.method);
|
|
336
|
+
return method === "HEAD" ? "GET" : method;
|
|
337
|
+
};
|
|
223
338
|
}
|
|
224
339
|
|
|
225
340
|
function type(...[map]) {
|
|
@@ -237,4 +352,19 @@ function type(...[map]) {
|
|
|
237
352
|
};
|
|
238
353
|
}
|
|
239
354
|
|
|
240
|
-
|
|
355
|
+
function isSchemaIssue(issue) {
|
|
356
|
+
if (!isTypescriptObject(issue) || typeof issue.message !== "string") {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
if (issue.path !== void 0) {
|
|
360
|
+
if (!Array.isArray(issue.path)) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
if (!issue.path.every((segment) => isPropertyKey(segment) || isTypescriptObject(segment) && isPropertyKey(segment.key))) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export { ContractBuilder, ContractProcedure, ValidationError, enhanceContractRouter, enhanceRoute, eventIterator, fallbackContractConfig, getContractRouter, getEventIteratorSchemaDetails, inferRPCMethodFromContractRouter, isContractProcedure, isSchemaIssue, mergeErrorMap, mergeMeta, mergePrefix, mergeRoute, mergeTags, minifyContractRouter, oc, populateContractRouterPaths, prefixRoute, type, unshiftTagRoute };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ClientContext } from '@orpc/client';
|
|
2
|
+
import { StandardLinkPlugin, StandardLinkOptions } from '@orpc/client/standard';
|
|
3
|
+
import { A as AnyContractRouter } from '../shared/contract.TuRtB1Ca.mjs';
|
|
4
|
+
import '@orpc/shared';
|
|
5
|
+
import '@standard-schema/spec';
|
|
6
|
+
import 'openapi-types';
|
|
7
|
+
|
|
8
|
+
declare class RequestValidationPluginError extends Error {
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A link plugin that validates client requests against your contract schema,
|
|
12
|
+
* ensuring that data sent to your server matches the expected types defined in your contract.
|
|
13
|
+
*
|
|
14
|
+
* @throws {ORPCError} with code `BAD_REQUEST` (same as server side) if input doesn't match the expected schema
|
|
15
|
+
* @see {@link https://orpc.dev/docs/plugins/request-validation Request Validation Plugin Docs}
|
|
16
|
+
*/
|
|
17
|
+
declare class RequestValidationPlugin<T extends ClientContext> implements StandardLinkPlugin<T> {
|
|
18
|
+
private readonly contract;
|
|
19
|
+
constructor(contract: AnyContractRouter);
|
|
20
|
+
init(options: StandardLinkOptions<T>): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A link plugin that validates server responses against your contract schema,
|
|
25
|
+
* ensuring that data returned from your server matches the expected types defined in your contract.
|
|
26
|
+
*
|
|
27
|
+
* - Throws `ValidationError` if output doesn't match the expected schema
|
|
28
|
+
* - Converts mismatched defined errors to normal `ORPCError` instances
|
|
29
|
+
*
|
|
30
|
+
* @see {@link https://orpc.dev/docs/plugins/response-validation Response Validation Plugin Docs}
|
|
31
|
+
*/
|
|
32
|
+
declare class ResponseValidationPlugin<T extends ClientContext> implements StandardLinkPlugin<T> {
|
|
33
|
+
private readonly contract;
|
|
34
|
+
constructor(contract: AnyContractRouter);
|
|
35
|
+
/**
|
|
36
|
+
* run before (validate after) retry plugin, because validation failed can't be retried
|
|
37
|
+
* run before (validate after) durable iterator plugin, because we expect durable iterator to validation (if user use it)
|
|
38
|
+
*/
|
|
39
|
+
order: number;
|
|
40
|
+
init(options: StandardLinkOptions<T>): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { RequestValidationPlugin, RequestValidationPluginError, ResponseValidationPlugin };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ClientContext } from '@orpc/client';
|
|
2
|
+
import { StandardLinkPlugin, StandardLinkOptions } from '@orpc/client/standard';
|
|
3
|
+
import { A as AnyContractRouter } from '../shared/contract.TuRtB1Ca.js';
|
|
4
|
+
import '@orpc/shared';
|
|
5
|
+
import '@standard-schema/spec';
|
|
6
|
+
import 'openapi-types';
|
|
7
|
+
|
|
8
|
+
declare class RequestValidationPluginError extends Error {
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A link plugin that validates client requests against your contract schema,
|
|
12
|
+
* ensuring that data sent to your server matches the expected types defined in your contract.
|
|
13
|
+
*
|
|
14
|
+
* @throws {ORPCError} with code `BAD_REQUEST` (same as server side) if input doesn't match the expected schema
|
|
15
|
+
* @see {@link https://orpc.dev/docs/plugins/request-validation Request Validation Plugin Docs}
|
|
16
|
+
*/
|
|
17
|
+
declare class RequestValidationPlugin<T extends ClientContext> implements StandardLinkPlugin<T> {
|
|
18
|
+
private readonly contract;
|
|
19
|
+
constructor(contract: AnyContractRouter);
|
|
20
|
+
init(options: StandardLinkOptions<T>): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A link plugin that validates server responses against your contract schema,
|
|
25
|
+
* ensuring that data returned from your server matches the expected types defined in your contract.
|
|
26
|
+
*
|
|
27
|
+
* - Throws `ValidationError` if output doesn't match the expected schema
|
|
28
|
+
* - Converts mismatched defined errors to normal `ORPCError` instances
|
|
29
|
+
*
|
|
30
|
+
* @see {@link https://orpc.dev/docs/plugins/response-validation Response Validation Plugin Docs}
|
|
31
|
+
*/
|
|
32
|
+
declare class ResponseValidationPlugin<T extends ClientContext> implements StandardLinkPlugin<T> {
|
|
33
|
+
private readonly contract;
|
|
34
|
+
constructor(contract: AnyContractRouter);
|
|
35
|
+
/**
|
|
36
|
+
* run before (validate after) retry plugin, because validation failed can't be retried
|
|
37
|
+
* run before (validate after) durable iterator plugin, because we expect durable iterator to validation (if user use it)
|
|
38
|
+
*/
|
|
39
|
+
order: number;
|
|
40
|
+
init(options: StandardLinkOptions<T>): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { RequestValidationPlugin, RequestValidationPluginError, ResponseValidationPlugin };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ORPCError } from '@orpc/client';
|
|
2
|
+
import { get } from '@orpc/shared';
|
|
3
|
+
import { i as isContractProcedure, V as ValidationError, v as validateORPCError } from '../shared/contract.D_dZrO__.mjs';
|
|
4
|
+
|
|
5
|
+
class RequestValidationPluginError extends Error {
|
|
6
|
+
}
|
|
7
|
+
class RequestValidationPlugin {
|
|
8
|
+
constructor(contract) {
|
|
9
|
+
this.contract = contract;
|
|
10
|
+
}
|
|
11
|
+
init(options) {
|
|
12
|
+
options.interceptors ??= [];
|
|
13
|
+
options.interceptors.push(async ({ next, path, input }) => {
|
|
14
|
+
const procedure = get(this.contract, path);
|
|
15
|
+
if (!isContractProcedure(procedure)) {
|
|
16
|
+
throw new RequestValidationPluginError(`No valid procedure found at path "${path.join(".")}", this may happen when the contract router is not properly configured.`);
|
|
17
|
+
}
|
|
18
|
+
const inputSchema = procedure["~orpc"].inputSchema;
|
|
19
|
+
if (inputSchema) {
|
|
20
|
+
const result = await inputSchema["~standard"].validate(input);
|
|
21
|
+
if (result.issues) {
|
|
22
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
23
|
+
message: "Input validation failed",
|
|
24
|
+
data: {
|
|
25
|
+
issues: result.issues
|
|
26
|
+
},
|
|
27
|
+
cause: new ValidationError({
|
|
28
|
+
message: "Input validation failed",
|
|
29
|
+
issues: result.issues,
|
|
30
|
+
data: input
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return await next();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class ResponseValidationPlugin {
|
|
41
|
+
constructor(contract) {
|
|
42
|
+
this.contract = contract;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* run before (validate after) retry plugin, because validation failed can't be retried
|
|
46
|
+
* run before (validate after) durable iterator plugin, because we expect durable iterator to validation (if user use it)
|
|
47
|
+
*/
|
|
48
|
+
order = 12e5;
|
|
49
|
+
init(options) {
|
|
50
|
+
options.interceptors ??= [];
|
|
51
|
+
options.interceptors.push(async ({ next, path }) => {
|
|
52
|
+
const procedure = get(this.contract, path);
|
|
53
|
+
if (!isContractProcedure(procedure)) {
|
|
54
|
+
throw new Error(`[ResponseValidationPlugin] no valid procedure found at path "${path.join(".")}", this may happen when the contract router is not properly configured.`);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const output = await next();
|
|
58
|
+
const outputSchema = procedure["~orpc"].outputSchema;
|
|
59
|
+
if (!outputSchema) {
|
|
60
|
+
return output;
|
|
61
|
+
}
|
|
62
|
+
const result = await outputSchema["~standard"].validate(output);
|
|
63
|
+
if (result.issues) {
|
|
64
|
+
throw new ValidationError({
|
|
65
|
+
message: "Server response output does not match expected schema",
|
|
66
|
+
issues: result.issues,
|
|
67
|
+
data: output
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return result.value;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (e instanceof ORPCError) {
|
|
73
|
+
throw await validateORPCError(procedure["~orpc"].errorMap, e);
|
|
74
|
+
}
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { RequestValidationPlugin, RequestValidationPluginError, ResponseValidationPlugin };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { fallbackORPCErrorStatus, ORPCError, isORPCErrorStatus } from '@orpc/client';
|
|
2
|
+
|
|
3
|
+
class ValidationError extends Error {
|
|
4
|
+
issues;
|
|
5
|
+
data;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super(options.message, options);
|
|
8
|
+
this.issues = options.issues;
|
|
9
|
+
this.data = options.data;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function mergeErrorMap(errorMap1, errorMap2) {
|
|
13
|
+
return { ...errorMap1, ...errorMap2 };
|
|
14
|
+
}
|
|
15
|
+
async function validateORPCError(map, error) {
|
|
16
|
+
const { code, status, message, data, cause, defined } = error;
|
|
17
|
+
const config = map?.[error.code];
|
|
18
|
+
if (!config || fallbackORPCErrorStatus(error.code, config.status) !== error.status) {
|
|
19
|
+
return defined ? new ORPCError(code, { defined: false, status, message, data, cause }) : error;
|
|
20
|
+
}
|
|
21
|
+
if (!config.data) {
|
|
22
|
+
return defined ? error : new ORPCError(code, { defined: true, status, message, data, cause });
|
|
23
|
+
}
|
|
24
|
+
const validated = await config.data["~standard"].validate(error.data);
|
|
25
|
+
if (validated.issues) {
|
|
26
|
+
return defined ? new ORPCError(code, { defined: false, status, message, data, cause }) : error;
|
|
27
|
+
}
|
|
28
|
+
return new ORPCError(code, { defined: true, status, message, data: validated.value, cause });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class ContractProcedure {
|
|
32
|
+
/**
|
|
33
|
+
* This property holds the defined options for the contract procedure.
|
|
34
|
+
*/
|
|
35
|
+
"~orpc";
|
|
36
|
+
constructor(def) {
|
|
37
|
+
if (def.route?.successStatus && isORPCErrorStatus(def.route.successStatus)) {
|
|
38
|
+
throw new Error("[ContractProcedure] Invalid successStatus.");
|
|
39
|
+
}
|
|
40
|
+
if (Object.values(def.errorMap).some((val) => val && val.status && !isORPCErrorStatus(val.status))) {
|
|
41
|
+
throw new Error("[ContractProcedure] Invalid error status code.");
|
|
42
|
+
}
|
|
43
|
+
this["~orpc"] = def;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isContractProcedure(item) {
|
|
47
|
+
if (item instanceof ContractProcedure) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return (typeof item === "object" || typeof item === "function") && item !== null && "~orpc" in item && typeof item["~orpc"] === "object" && item["~orpc"] !== null && "errorMap" in item["~orpc"] && "route" in item["~orpc"] && "meta" in item["~orpc"];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { ContractProcedure as C, ValidationError as V, isContractProcedure as i, mergeErrorMap as m, validateORPCError as v };
|