@real-router/search-schema-plugin 0.1.1 → 0.1.3
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 +14 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/esm/index.d.mts.map +1 -0
- package/package.json +5 -5
- package/src/constants.ts +3 -0
- package/src/factory.ts +25 -0
- package/src/helpers.ts +40 -0
- package/src/index.ts +16 -0
- package/src/plugin.ts +208 -0
- package/src/types.ts +91 -0
- package/src/validation.ts +25 -0
package/README.md
CHANGED
|
@@ -54,6 +54,20 @@ router.usePlugin(searchSchemaPlugin({ mode: "development" }));
|
|
|
54
54
|
|
|
55
55
|
> **Schema libraries:** Any library implementing [Standard Schema V1](https://github.com/standard-schema/standard-schema) works — Zod 3.24+, Valibot 1.0+, ArkType. Install and configure your chosen library separately; the plugin has no schema-library dependency.
|
|
56
56
|
|
|
57
|
+
## TypeScript Support
|
|
58
|
+
|
|
59
|
+
Import `@real-router/search-schema-plugin` to enable TypeScript support for `searchSchema` on route definitions:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import "@real-router/search-schema-plugin"; // enables Route.searchSchema type
|
|
63
|
+
|
|
64
|
+
const routes = [
|
|
65
|
+
{ name: "users", path: "/users", searchSchema: z.object({ page: z.number() }) },
|
|
66
|
+
];
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This works via [module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) — the package extends the `Route` interface from `@real-router/core`.
|
|
70
|
+
|
|
57
71
|
## Configuration
|
|
58
72
|
|
|
59
73
|
| Option | Type | Default | Description |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;UAUiB,qBAAA;EAAA,SACN,OAAA;EAAA,SACA,IAAA,aACK,WAAA;IAAA,SAAyB,GAAA,EAAK,WAAA;EAAA;AAAA;;KAKlC,sBAAA;EAAA,SACG,KAAA,EAAO,MAAA;AAAA;EAAA,SACP,MAAA,WAAiB,qBAAA;AAAA;AAFhC;;;;;;AAAA,UAUiB,gBAAA,2BAA2C,KAAA;EAAA,SACjD,WAAA;IAAA,SACE,OAAA;IAAA,SACA,MAAA;IAAA,SACA,QAAA,GACP,KAAA,cAEE,sBAAA,CAAuB,MAAA,IACvB,OAAA,CAAQ,sBAAA,CAAuB,MAAA;IAAA,SAC1B,KAAA;MAAA,SAEM,KAAA,EAAO,KAAA;MAAA,SACP,MAAA,EAAQ,MAAA;IAAA;EAAA;AAAA;AAAA,UAUV,yBAAA;EAdD;;;;;;;;;EAAA,SAwBL,IAAA;EA9BE;;;;;;;EAAA,SAuCF,MAAA;EAjC4B;;;;;;;;AAcvC;;;;;;EAduC,SAiD5B,OAAA,IACP,SAAA,UACA,MAAA,EAAQ,MAAA,EACR,MAAA,WAAiB,qBAAA,OACd,MAAA;AAAA;;;iBCjFS,kBAAA,CACd,OAAA,GAAS,yBAAA,GACR,aAAA;;;;YCLS,KAAA;IACR,YAAA,GAAe,gBAAA;EAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;UAUiB,qBAAA;EAAA,SACN,OAAA;EAAA,SACA,IAAA,aACK,WAAA;IAAA,SAAyB,GAAA,EAAK,WAAA;EAAA;AAAA;;KAKlC,sBAAA;EAAA,SACG,KAAA,EAAO,MAAA;AAAA;EAAA,SACP,MAAA,WAAiB,qBAAA;AAAA;AAFhC;;;;;;AAAA,UAUiB,gBAAA,2BAA2C,KAAA;EAAA,SACjD,WAAA;IAAA,SACE,OAAA;IAAA,SACA,MAAA;IAAA,SACA,QAAA,GACP,KAAA,cAEE,sBAAA,CAAuB,MAAA,IACvB,OAAA,CAAQ,sBAAA,CAAuB,MAAA;IAAA,SAC1B,KAAA;MAAA,SAEM,KAAA,EAAO,KAAA;MAAA,SACP,MAAA,EAAQ,MAAA;IAAA;EAAA;AAAA;AAAA,UAUV,yBAAA;EAdD;;;;;;;;;EAAA,SAwBL,IAAA;EA9BE;;;;;;;EAAA,SAuCF,MAAA;EAjC4B;;;;;;;;AAcvC;;;;;;EAduC,SAiD5B,OAAA,IACP,SAAA,UACA,MAAA,EAAQ,MAAA,EACR,MAAA,WAAiB,qBAAA,OACd,MAAA;AAAA;;;iBCjFS,kBAAA,CACd,OAAA,GAAS,yBAAA,GACR,aAAA;;;;YCLS,KAAA;IACR,YAAA,GAAe,gBAAA;EAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/search-schema-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Runtime search parameter validation via Standard Schema for Real-Router",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"types": "./dist/esm/index.d.mts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"development": "./src/index.ts",
|
|
12
11
|
"types": {
|
|
13
12
|
"import": "./dist/esm/index.d.mts",
|
|
14
13
|
"require": "./dist/cjs/index.d.ts"
|
|
@@ -18,7 +17,8 @@
|
|
|
18
17
|
}
|
|
19
18
|
},
|
|
20
19
|
"files": [
|
|
21
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
22
22
|
],
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"homepage": "https://github.com/greydragon888/real-router",
|
|
46
46
|
"sideEffects": false,
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@real-router/core": "^0.
|
|
48
|
+
"@real-router/core": "^0.46.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"test": "vitest",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"build": "tsdown --config-loader unrun",
|
|
54
54
|
"type-check": "tsc --noEmit",
|
|
55
55
|
"lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
|
|
56
|
-
"lint:package": "
|
|
56
|
+
"lint:package": "publint",
|
|
57
57
|
"lint:types": "attw --pack .",
|
|
58
58
|
"build:dist-only": "tsdown --config-loader unrun"
|
|
59
59
|
}
|
package/src/constants.ts
ADDED
package/src/factory.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getPluginApi, getRoutesApi } from "@real-router/core/api";
|
|
2
|
+
|
|
3
|
+
import { SearchSchemaPlugin } from "./plugin";
|
|
4
|
+
import { validateOptions } from "./validation";
|
|
5
|
+
|
|
6
|
+
import type { SearchSchemaPluginOptions } from "./types";
|
|
7
|
+
import type { PluginFactory, Plugin } from "@real-router/core";
|
|
8
|
+
|
|
9
|
+
export function searchSchemaPlugin(
|
|
10
|
+
options: SearchSchemaPluginOptions = {},
|
|
11
|
+
): PluginFactory {
|
|
12
|
+
validateOptions(options);
|
|
13
|
+
|
|
14
|
+
const frozenOptions: SearchSchemaPluginOptions = Object.freeze({
|
|
15
|
+
...options,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return (router): Plugin => {
|
|
19
|
+
const pluginApi = getPluginApi(router);
|
|
20
|
+
const routesApi = getRoutesApi(router);
|
|
21
|
+
const plugin = new SearchSchemaPlugin(pluginApi, routesApi, frozenOptions);
|
|
22
|
+
|
|
23
|
+
return plugin.getPlugin();
|
|
24
|
+
};
|
|
25
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// packages/search-schema-plugin/src/helpers.ts
|
|
2
|
+
|
|
3
|
+
import type { StandardSchemaV1Issue } from "./types";
|
|
4
|
+
import type { Params } from "@real-router/core";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract top-level keys from validation issues.
|
|
8
|
+
* Only processes issues with a non-empty path — issues without path
|
|
9
|
+
* affect the whole object and can't be stripped by key.
|
|
10
|
+
*/
|
|
11
|
+
export function getInvalidKeys(
|
|
12
|
+
issues: readonly StandardSchemaV1Issue[],
|
|
13
|
+
): Set<string> {
|
|
14
|
+
const keys = new Set<string>();
|
|
15
|
+
|
|
16
|
+
for (const issue of issues) {
|
|
17
|
+
if (issue.path && issue.path.length > 0) {
|
|
18
|
+
const segment = issue.path[0];
|
|
19
|
+
const key =
|
|
20
|
+
typeof segment === "object" && "key" in segment ? segment.key : segment;
|
|
21
|
+
|
|
22
|
+
keys.add(String(key));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return keys;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Create a shallow copy of params without the specified keys. */
|
|
30
|
+
export function omitKeys(params: Params, keys: Set<string>): Params {
|
|
31
|
+
const result: Params = {};
|
|
32
|
+
|
|
33
|
+
for (const key of Object.keys(params)) {
|
|
34
|
+
if (!keys.has(key)) {
|
|
35
|
+
result[key] = params[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "./types";
|
|
2
|
+
|
|
3
|
+
export { searchSchemaPlugin } from "./factory";
|
|
4
|
+
|
|
5
|
+
declare module "@real-router/core" {
|
|
6
|
+
interface Route {
|
|
7
|
+
searchSchema?: StandardSchemaV1;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
SearchSchemaPluginOptions,
|
|
13
|
+
StandardSchemaV1,
|
|
14
|
+
StandardSchemaV1Issue,
|
|
15
|
+
StandardSchemaV1Result,
|
|
16
|
+
} from "./types";
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { ERROR_PREFIX } from "./constants";
|
|
2
|
+
import { getInvalidKeys, omitKeys } from "./helpers";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
SearchSchemaPluginOptions,
|
|
6
|
+
StandardSchemaV1,
|
|
7
|
+
StandardSchemaV1Issue,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import type { Params, Plugin, Route } from "@real-router/core";
|
|
10
|
+
import type { PluginApi, RoutesApi } from "@real-router/core/api";
|
|
11
|
+
|
|
12
|
+
export class SearchSchemaPlugin {
|
|
13
|
+
readonly #pluginApi: PluginApi;
|
|
14
|
+
readonly #routesApi: RoutesApi;
|
|
15
|
+
readonly #mode: "development" | "production";
|
|
16
|
+
readonly #strict: boolean;
|
|
17
|
+
readonly #onError:
|
|
18
|
+
| ((
|
|
19
|
+
routeName: string,
|
|
20
|
+
params: Params,
|
|
21
|
+
issues: readonly StandardSchemaV1Issue[],
|
|
22
|
+
) => Params)
|
|
23
|
+
| undefined;
|
|
24
|
+
readonly #removeForwardStateInterceptor: () => void;
|
|
25
|
+
readonly #removeAddInterceptor: () => void;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
pluginApi: PluginApi,
|
|
29
|
+
routesApi: RoutesApi,
|
|
30
|
+
options: SearchSchemaPluginOptions,
|
|
31
|
+
) {
|
|
32
|
+
this.#pluginApi = pluginApi;
|
|
33
|
+
this.#routesApi = routesApi;
|
|
34
|
+
this.#mode = options.mode ?? "development";
|
|
35
|
+
this.#strict = options.strict ?? false;
|
|
36
|
+
this.#onError = options.onError;
|
|
37
|
+
|
|
38
|
+
this.#validateExistingDefaultParams();
|
|
39
|
+
|
|
40
|
+
this.#removeForwardStateInterceptor = this.#pluginApi.addInterceptor(
|
|
41
|
+
"forwardState",
|
|
42
|
+
(next, routeName, routeParams) => {
|
|
43
|
+
const result = next(routeName, routeParams);
|
|
44
|
+
|
|
45
|
+
return this.#validateState(result);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
this.#removeAddInterceptor = this.#pluginApi.addInterceptor(
|
|
50
|
+
"add",
|
|
51
|
+
(next, routes, addOptions) => {
|
|
52
|
+
next(routes, addOptions);
|
|
53
|
+
this.#validateRoutesDefaultParams(routes, addOptions?.parent);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getPlugin(): Plugin {
|
|
59
|
+
return {
|
|
60
|
+
teardown: () => {
|
|
61
|
+
this.#removeForwardStateInterceptor();
|
|
62
|
+
this.#removeAddInterceptor();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#getSchema(routeName: string): StandardSchemaV1 | undefined {
|
|
68
|
+
return this.#pluginApi.getRouteConfig(routeName)?.searchSchema as
|
|
69
|
+
| StandardSchemaV1
|
|
70
|
+
| undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#validateState(result: { name: string; params: Params }): {
|
|
74
|
+
name: string;
|
|
75
|
+
params: Params;
|
|
76
|
+
} {
|
|
77
|
+
const schema = this.#getSchema(result.name);
|
|
78
|
+
|
|
79
|
+
if (!schema) {
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const validation = schema["~standard"].validate(result.params);
|
|
84
|
+
|
|
85
|
+
if (validation instanceof Promise) {
|
|
86
|
+
throw new TypeError(
|
|
87
|
+
`${ERROR_PREFIX} Async schema validation is not supported. Route "${result.name}" returned a Promise from ~standard.validate().`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ("value" in validation) {
|
|
92
|
+
const params = this.#strict
|
|
93
|
+
? (validation.value as Params)
|
|
94
|
+
: { ...result.params, ...(validation.value as Params) };
|
|
95
|
+
|
|
96
|
+
return { ...result, params };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (this.#onError) {
|
|
100
|
+
return {
|
|
101
|
+
...result,
|
|
102
|
+
params: this.#onError(result.name, result.params, validation.issues),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this.#mode === "development") {
|
|
107
|
+
console.error(
|
|
108
|
+
`${ERROR_PREFIX} Route "${result.name}": invalid search params`,
|
|
109
|
+
validation.issues,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const invalidKeys = getInvalidKeys(validation.issues);
|
|
114
|
+
const stripped = omitKeys(result.params, invalidKeys);
|
|
115
|
+
const route = this.#routesApi.get(result.name);
|
|
116
|
+
const defaults = route?.defaultParams;
|
|
117
|
+
const restored = defaults ? { ...defaults, ...stripped } : stripped;
|
|
118
|
+
|
|
119
|
+
return { ...result, params: restored };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#validateExistingDefaultParams(): void {
|
|
123
|
+
if (this.#mode !== "development") {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const tree = this.#pluginApi.getTree() as unknown as
|
|
128
|
+
| { fullName?: string; children?: ReadonlyMap<string, unknown> }
|
|
129
|
+
| undefined;
|
|
130
|
+
|
|
131
|
+
/* v8 ignore next -- @preserve: getTree() always returns a RouteTree, defensive check */
|
|
132
|
+
if (!tree) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.#walkTree(tree);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#walkTree(node: {
|
|
140
|
+
fullName?: string;
|
|
141
|
+
children?: ReadonlyMap<string, unknown>;
|
|
142
|
+
}): void {
|
|
143
|
+
if (node.fullName) {
|
|
144
|
+
this.#validateSingleRouteDefaultParams(node.fullName);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* v8 ignore next 3 -- @preserve: children is always a Map in RouteTree */
|
|
148
|
+
if (node.children instanceof Map) {
|
|
149
|
+
for (const child of node.children.values()) {
|
|
150
|
+
if (child && typeof child === "object") {
|
|
151
|
+
this.#walkTree(
|
|
152
|
+
child as {
|
|
153
|
+
fullName?: string;
|
|
154
|
+
children?: ReadonlyMap<string, unknown>;
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#validateSingleRouteDefaultParams(routeName: string): void {
|
|
163
|
+
const schema = this.#getSchema(routeName);
|
|
164
|
+
|
|
165
|
+
if (!schema) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const route = this.#routesApi.get(routeName);
|
|
170
|
+
const defaultParams = route?.defaultParams;
|
|
171
|
+
|
|
172
|
+
if (!defaultParams) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const validation = schema["~standard"].validate(defaultParams);
|
|
177
|
+
|
|
178
|
+
if (validation instanceof Promise) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if ("issues" in validation) {
|
|
183
|
+
console.warn(
|
|
184
|
+
`${ERROR_PREFIX} Route "${routeName}": defaultParams do not pass searchSchema`,
|
|
185
|
+
validation.issues,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#validateRoutesDefaultParams(routes: Route[], prefix = ""): void {
|
|
191
|
+
if (this.#mode !== "development") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const route of routes) {
|
|
196
|
+
/* v8 ignore next -- @preserve: Route.name is always a non-empty string */
|
|
197
|
+
if (route.name) {
|
|
198
|
+
const fullName = prefix ? `${prefix}.${route.name}` : route.name;
|
|
199
|
+
|
|
200
|
+
this.#validateSingleRouteDefaultParams(fullName);
|
|
201
|
+
|
|
202
|
+
if (route.children) {
|
|
203
|
+
this.#validateRoutesDefaultParams(route.children, fullName);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// packages/search-schema-plugin/src/types.ts
|
|
2
|
+
|
|
3
|
+
import type { Params } from "@real-router/core";
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Standard Schema V1 (inline — zero external deps)
|
|
7
|
+
// https://github.com/standard-schema/standard-schema
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
/** A single validation issue from Standard Schema V1. */
|
|
11
|
+
export interface StandardSchemaV1Issue {
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly path?:
|
|
14
|
+
| readonly (PropertyKey | { readonly key: PropertyKey })[]
|
|
15
|
+
| undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Validation result — either success or failure. */
|
|
19
|
+
export type StandardSchemaV1Result<Output = unknown> =
|
|
20
|
+
| { readonly value: Output }
|
|
21
|
+
| { readonly issues: readonly StandardSchemaV1Issue[] };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Standard Schema V1 interface.
|
|
25
|
+
*
|
|
26
|
+
* Supported by Zod 3.24+, Valibot 1.0+, ArkType.
|
|
27
|
+
* The plugin doesn't depend on any specific schema library.
|
|
28
|
+
*/
|
|
29
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
30
|
+
readonly "~standard": {
|
|
31
|
+
readonly version: 1;
|
|
32
|
+
readonly vendor: string;
|
|
33
|
+
readonly validate: (
|
|
34
|
+
value: unknown,
|
|
35
|
+
) =>
|
|
36
|
+
| StandardSchemaV1Result<Output>
|
|
37
|
+
| Promise<StandardSchemaV1Result<Output>>;
|
|
38
|
+
readonly types?:
|
|
39
|
+
| {
|
|
40
|
+
readonly input: Input;
|
|
41
|
+
readonly output: Output;
|
|
42
|
+
}
|
|
43
|
+
| undefined;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Plugin Options
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
export interface SearchSchemaPluginOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Error handling mode.
|
|
54
|
+
* - "development" (default): strip invalid + console.error
|
|
55
|
+
* - "production": silent strip
|
|
56
|
+
*
|
|
57
|
+
* For recovery of invalid params use defaultParams (strip + merge + diagnostics).
|
|
58
|
+
* For filling absent params use .default() in schema (no diagnostics).
|
|
59
|
+
* .catch() is not recommended — suppresses errors, mode: "development" won't see the issue.
|
|
60
|
+
*/
|
|
61
|
+
readonly mode?: "development" | "production";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Strip search params not described in schema.
|
|
65
|
+
* - false (default): unknown params pass through
|
|
66
|
+
* - true: unknown params are removed
|
|
67
|
+
*
|
|
68
|
+
* Per-route override: .strict() / .passthrough() in Zod schema.
|
|
69
|
+
*/
|
|
70
|
+
readonly strict?: boolean;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Custom error handler (overrides mode completely).
|
|
74
|
+
* Must return cleaned params.
|
|
75
|
+
*
|
|
76
|
+
* Contract:
|
|
77
|
+
* - Returned params are used as-is, without re-validation.
|
|
78
|
+
* Responsibility for correctness is on the callback author.
|
|
79
|
+
* (Re-validation would risk infinite loops.)
|
|
80
|
+
* - Exceptions from onError propagate up without suppression.
|
|
81
|
+
* Consistent with interceptor behavior in core.
|
|
82
|
+
* - When onError is set, neither console.error (mode: "development"),
|
|
83
|
+
* nor silent strip (mode: "production") are executed.
|
|
84
|
+
* All responsibility for diagnostics and recovery is on the callback.
|
|
85
|
+
*/
|
|
86
|
+
readonly onError?: (
|
|
87
|
+
routeName: string,
|
|
88
|
+
params: Params,
|
|
89
|
+
issues: readonly StandardSchemaV1Issue[],
|
|
90
|
+
) => Params;
|
|
91
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ERROR_PREFIX } from "./constants";
|
|
2
|
+
|
|
3
|
+
import type { SearchSchemaPluginOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
const VALID_MODES = new Set(["development", "production"]);
|
|
6
|
+
|
|
7
|
+
export function validateOptions(options: SearchSchemaPluginOptions): void {
|
|
8
|
+
if (options.mode !== undefined && !VALID_MODES.has(options.mode)) {
|
|
9
|
+
throw new TypeError(
|
|
10
|
+
`${ERROR_PREFIX} Invalid mode: "${options.mode}". Must be "development" or "production".`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (options.strict !== undefined && typeof options.strict !== "boolean") {
|
|
15
|
+
throw new TypeError(
|
|
16
|
+
`${ERROR_PREFIX} Invalid strict option: expected boolean, got ${typeof options.strict}.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (options.onError !== undefined && typeof options.onError !== "function") {
|
|
21
|
+
throw new TypeError(
|
|
22
|
+
`${ERROR_PREFIX} Invalid onError: expected function, got ${typeof options.onError}.`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|