@real-router/validation-plugin 0.0.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 +148 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/metafile-cjs.json +1 -0
- package/dist/esm/index.d.mts +6 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/metafile-esm.json +1 -0
- package/package.json +65 -0
- package/src/helpers.ts +11 -0
- package/src/index.ts +5 -0
- package/src/validationPlugin.ts +330 -0
- package/src/validators/dependencies.ts +159 -0
- package/src/validators/eventBus.ts +48 -0
- package/src/validators/forwardTo.ts +301 -0
- package/src/validators/lifecycle.ts +94 -0
- package/src/validators/navigation.ts +57 -0
- package/src/validators/options.ts +305 -0
- package/src/validators/plugins.ts +109 -0
- package/src/validators/retrospective.ts +540 -0
- package/src/validators/routes.ts +578 -0
- package/src/validators/state.ts +34 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
// packages/validation-plugin/src/validators/retrospective.ts
|
|
2
|
+
|
|
3
|
+
import { resolveForwardChain as coreResolveForwardChain } from "@real-router/core";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrospective validators — run AFTER the route tree is already built.
|
|
7
|
+
* Called by the validation plugin at usePlugin() time, in a try/catch with rollback.
|
|
8
|
+
*
|
|
9
|
+
* The plugin is registered AFTER the constructor, so all routes are already in the store.
|
|
10
|
+
* These functions receive store objects as parameters and cast internally using
|
|
11
|
+
* local structural interfaces to avoid tight coupling to core internal types.
|
|
12
|
+
*
|
|
13
|
+
* All parameters are typed as `unknown` — cast internally as needed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Local structural interfaces (cast-only, not imported from core internals)
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
interface LocalSegmentParamMeta {
|
|
21
|
+
urlParams: readonly string[];
|
|
22
|
+
spatParams: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LocalRouteSegment {
|
|
26
|
+
paramMeta: LocalSegmentParamMeta;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface LocalRouteTree {
|
|
30
|
+
children: Map<string, LocalRouteTree>;
|
|
31
|
+
paramMeta: LocalSegmentParamMeta;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface LocalRouteMatcher {
|
|
35
|
+
getSegmentsByName: (
|
|
36
|
+
name: string,
|
|
37
|
+
) => readonly LocalRouteSegment[] | null | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface LocalRouteConfig {
|
|
41
|
+
forwardMap: Record<string, string>;
|
|
42
|
+
forwardFnMap: Record<string, unknown>;
|
|
43
|
+
defaultParams: Record<string, unknown>;
|
|
44
|
+
decoders: Record<string, unknown>;
|
|
45
|
+
encoders: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface LocalRouteDefinition {
|
|
49
|
+
name: string;
|
|
50
|
+
path: string;
|
|
51
|
+
children?: LocalRouteDefinition[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface LocalRoutesStore {
|
|
55
|
+
definitions: LocalRouteDefinition[];
|
|
56
|
+
config: LocalRouteConfig;
|
|
57
|
+
tree: LocalRouteTree;
|
|
58
|
+
matcher: LocalRouteMatcher;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface LocalDependencyLimits {
|
|
62
|
+
maxDependencies: number;
|
|
63
|
+
maxPlugins: number;
|
|
64
|
+
maxListeners: number;
|
|
65
|
+
warnListeners: number;
|
|
66
|
+
maxEventDepth: number;
|
|
67
|
+
maxLifecycleHandlers: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Private helpers
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
function assertRoutesStore(store: unknown, fnName: string): LocalRoutesStore {
|
|
75
|
+
if (!store || typeof store !== "object") {
|
|
76
|
+
throw new TypeError(
|
|
77
|
+
`[validation-plugin] ${fnName}: store must be an object`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const storeRecord = store as Record<string, unknown>;
|
|
82
|
+
|
|
83
|
+
if (!Array.isArray(storeRecord.definitions)) {
|
|
84
|
+
throw new TypeError(
|
|
85
|
+
`[validation-plugin] ${fnName}: store.definitions must be an array`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!storeRecord.config || typeof storeRecord.config !== "object") {
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
`[validation-plugin] ${fnName}: store.config must be an object`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!storeRecord.tree || typeof storeRecord.tree !== "object") {
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
`[validation-plugin] ${fnName}: store.tree must be an object`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return storeRecord as unknown as LocalRoutesStore;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function walkDefinitions(
|
|
105
|
+
definitions: LocalRouteDefinition[],
|
|
106
|
+
callback: (def: LocalRouteDefinition, fullName: string) => void,
|
|
107
|
+
parentName = "",
|
|
108
|
+
): void {
|
|
109
|
+
for (const def of definitions) {
|
|
110
|
+
const fullName = parentName ? `${parentName}.${def.name}` : def.name;
|
|
111
|
+
|
|
112
|
+
callback(def, fullName);
|
|
113
|
+
|
|
114
|
+
if (def.children) {
|
|
115
|
+
walkDefinitions(def.children, callback, fullName);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function routeExistsInTree(tree: LocalRouteTree, routeName: string): boolean {
|
|
121
|
+
const segments = routeName.split(".");
|
|
122
|
+
let current: LocalRouteTree | undefined = tree;
|
|
123
|
+
|
|
124
|
+
for (const segment of segments) {
|
|
125
|
+
current = current.children.get(segment);
|
|
126
|
+
|
|
127
|
+
if (!current) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Wraps core's resolveForwardChain with [validation-plugin] prefix on errors.
|
|
137
|
+
* Core's version throws plain Error messages; retrospective validation
|
|
138
|
+
* needs the [validation-plugin] prefix for consistency.
|
|
139
|
+
*/
|
|
140
|
+
function resolveForwardChainWithPrefix(
|
|
141
|
+
startRoute: string,
|
|
142
|
+
forwardMap: Record<string, string>,
|
|
143
|
+
): string {
|
|
144
|
+
try {
|
|
145
|
+
return coreResolveForwardChain(startRoute, forwardMap);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new Error(`[validation-plugin] ${(error as Error).message}`, {
|
|
148
|
+
cause: error,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function collectUrlParams(segments: readonly LocalRouteSegment[]): Set<string> {
|
|
154
|
+
const params = new Set<string>();
|
|
155
|
+
|
|
156
|
+
for (const segment of segments) {
|
|
157
|
+
for (const param of segment.paramMeta.urlParams) {
|
|
158
|
+
params.add(param);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const param of segment.paramMeta.spatParams) {
|
|
162
|
+
params.add(param);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return params;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Asserts that a function value is not async (native or transpiled).
|
|
171
|
+
* Adapted from: assertNotAsync() in RoutesNamespace/validators.ts
|
|
172
|
+
*/
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- needs constructor.name access
|
|
174
|
+
function assertNotAsync(fn: Function, label: string, routeName: string): void {
|
|
175
|
+
const function_ = fn as {
|
|
176
|
+
constructor: { name: string };
|
|
177
|
+
toString: () => string;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
function_.constructor.name === "AsyncFunction" ||
|
|
182
|
+
function_.toString().includes("__awaiter")
|
|
183
|
+
) {
|
|
184
|
+
throw new TypeError(
|
|
185
|
+
`[validation-plugin] Route "${routeName}" ${label} cannot be async`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// 1. validateExistingRoutes
|
|
192
|
+
// =============================================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validates the existing route tree/definitions for structural integrity.
|
|
196
|
+
* Walks all route definitions, checks for duplicate names and invalid structure.
|
|
197
|
+
* Adapted from: validateRoutes() in RoutesNamespace/validators.ts
|
|
198
|
+
*
|
|
199
|
+
* @param store - RoutesStore instance (typed as unknown to avoid core coupling)
|
|
200
|
+
* @throws {TypeError} If store shape is invalid or definitions have structural issues
|
|
201
|
+
* @throws {Error} If duplicate route names are detected
|
|
202
|
+
*/
|
|
203
|
+
export function validateExistingRoutes(store: unknown): void {
|
|
204
|
+
const routesStore = assertRoutesStore(store, "validateExistingRoutes");
|
|
205
|
+
const seenNames = new Set<string>();
|
|
206
|
+
|
|
207
|
+
walkDefinitions(routesStore.definitions, (def, fullName) => {
|
|
208
|
+
if (typeof def.name !== "string" || !def.name) {
|
|
209
|
+
throw new TypeError(
|
|
210
|
+
`[validation-plugin] validateExistingRoutes: route has invalid name: ${def.name}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (typeof def.path !== "string") {
|
|
215
|
+
throw new TypeError(
|
|
216
|
+
`[validation-plugin] validateExistingRoutes: route "${fullName}" has non-string path (${typeof def.path})`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (seenNames.has(fullName)) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`[validation-plugin] validateExistingRoutes: duplicate route name detected: "${fullName}"`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
seenNames.add(fullName);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// =============================================================================
|
|
231
|
+
// 2. validateForwardToConsistency
|
|
232
|
+
// =============================================================================
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Validates forwardTo consistency across all chains in the store.
|
|
236
|
+
* Checks target existence, param compatibility, and circular chain detection.
|
|
237
|
+
* Adapted from: validateForwardToTargets() in forwardToValidation.ts
|
|
238
|
+
*
|
|
239
|
+
* @param store - RoutesStore instance (typed as unknown to avoid core coupling)
|
|
240
|
+
* @throws {Error} If any forwardTo target does not exist in the tree
|
|
241
|
+
* @throws {Error} If param incompatibility is detected across a forwardTo pair
|
|
242
|
+
* @throws {Error} If a circular forwardTo chain is detected
|
|
243
|
+
*/
|
|
244
|
+
export function validateForwardToConsistency(store: unknown): void {
|
|
245
|
+
const routesStore = assertRoutesStore(store, "validateForwardToConsistency");
|
|
246
|
+
const { config, tree, matcher } = routesStore;
|
|
247
|
+
|
|
248
|
+
// Check target existence and param compatibility for each static mapping
|
|
249
|
+
for (const [fromRoute, targetRoute] of Object.entries(config.forwardMap)) {
|
|
250
|
+
if (!routeExistsInTree(tree, targetRoute)) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`[validation-plugin] validateForwardToConsistency: forwardTo target "${targetRoute}" ` +
|
|
253
|
+
`does not exist in tree (source route: "${fromRoute}")`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Validate param compatibility: target must not require params absent in source
|
|
258
|
+
const sourceSegments = matcher.getSegmentsByName(fromRoute);
|
|
259
|
+
const targetSegments = matcher.getSegmentsByName(targetRoute);
|
|
260
|
+
|
|
261
|
+
if (sourceSegments && targetSegments) {
|
|
262
|
+
const sourceParams = collectUrlParams(sourceSegments);
|
|
263
|
+
const targetParams = collectUrlParams(targetSegments);
|
|
264
|
+
const missingParams = [...targetParams].filter(
|
|
265
|
+
(param) => !sourceParams.has(param),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (missingParams.length > 0) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`[validation-plugin] validateForwardToConsistency: forwardTo target "${targetRoute}" ` +
|
|
271
|
+
`requires params [${missingParams.join(", ")}] not available in source route "${fromRoute}"`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Detect cycles in the full forwardMap (catches multi-hop cycles)
|
|
278
|
+
for (const fromRoute of Object.keys(config.forwardMap)) {
|
|
279
|
+
resolveForwardChainWithPrefix(fromRoute, config.forwardMap);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// =============================================================================
|
|
284
|
+
// 3. validateRouteProperties
|
|
285
|
+
// =============================================================================
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Validates route properties for all registered routes in the store.
|
|
289
|
+
* Checks decoder/encoder types, defaultParams structure, and async forwardTo callbacks.
|
|
290
|
+
* Adapted from: validateRouteProperties() in forwardToValidation.ts
|
|
291
|
+
*
|
|
292
|
+
* @param store - RoutesStore instance (typed as unknown to avoid core coupling)
|
|
293
|
+
* @throws {TypeError} If any registered decoder/encoder is not a valid sync function
|
|
294
|
+
* @throws {TypeError} If any defaultParams is not a plain object
|
|
295
|
+
* @throws {TypeError} If any forwardTo callback is async
|
|
296
|
+
*/
|
|
297
|
+
export function validateRoutePropertiesStore(store: unknown): void {
|
|
298
|
+
const routesStore = assertRoutesStore(store, "validateRoutePropertiesStore");
|
|
299
|
+
const { config } = routesStore;
|
|
300
|
+
|
|
301
|
+
// Validate decoders — must be non-async functions (sync required for matchPath/buildPath)
|
|
302
|
+
for (const [routeName, decoder] of Object.entries(config.decoders)) {
|
|
303
|
+
if (typeof decoder !== "function") {
|
|
304
|
+
throw new TypeError(
|
|
305
|
+
`[validation-plugin] validateRoutePropertiesStore: route "${routeName}" decoder must be a function, got ${typeof decoder}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
assertNotAsync(decoder, "decoder", routeName);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Validate encoders — must be non-async functions (sync required for matchPath/buildPath)
|
|
313
|
+
for (const [routeName, encoder] of Object.entries(config.encoders)) {
|
|
314
|
+
if (typeof encoder !== "function") {
|
|
315
|
+
throw new TypeError(
|
|
316
|
+
`[validation-plugin] validateRoutePropertiesStore: route "${routeName}" encoder must be a function, got ${typeof encoder}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
assertNotAsync(encoder, "encoder", routeName);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Validate defaultParams — must be plain objects (not null, array, or other types)
|
|
324
|
+
for (const [routeName, params] of Object.entries(config.defaultParams)) {
|
|
325
|
+
if (
|
|
326
|
+
params === null ||
|
|
327
|
+
typeof params !== "object" ||
|
|
328
|
+
Array.isArray(params)
|
|
329
|
+
) {
|
|
330
|
+
throw new TypeError(
|
|
331
|
+
`[validation-plugin] validateRoutePropertiesStore: route "${routeName}" defaultParams must be a plain object, got ${Array.isArray(params) ? "array" : typeof params}`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Validate forwardTo function callbacks — must be non-async functions
|
|
337
|
+
for (const [routeName, callback] of Object.entries(config.forwardFnMap)) {
|
|
338
|
+
if (typeof callback !== "function") {
|
|
339
|
+
throw new TypeError(
|
|
340
|
+
`[validation-plugin] validateRoutePropertiesStore: route "${routeName}" forwardTo callback must be a function, got ${typeof callback}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
assertNotAsync(callback, "forwardTo callback", routeName);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// =============================================================================
|
|
349
|
+
// 4. validateForwardToTargets
|
|
350
|
+
// =============================================================================
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Validates that all static forwardTo targets exist in the route tree.
|
|
354
|
+
* This is a focused existence-only check (param compat is in validateForwardToConsistency).
|
|
355
|
+
* Adapted from: validateForwardToTargets() in forwardToValidation.ts
|
|
356
|
+
*
|
|
357
|
+
* @param store - RoutesStore instance (typed as unknown to avoid core coupling)
|
|
358
|
+
* @throws {Error} If any forwardTo target route does not exist in the tree
|
|
359
|
+
*/
|
|
360
|
+
export function validateForwardToTargetsStore(store: unknown): void {
|
|
361
|
+
const routesStore = assertRoutesStore(store, "validateForwardToTargetsStore");
|
|
362
|
+
const { config, tree } = routesStore;
|
|
363
|
+
|
|
364
|
+
for (const [fromRoute, targetRoute] of Object.entries(config.forwardMap)) {
|
|
365
|
+
if (!routeExistsInTree(tree, targetRoute)) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`[validation-plugin] validateForwardToTargetsStore: forwardTo target "${targetRoute}" ` +
|
|
368
|
+
`does not exist for route "${fromRoute}"`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// =============================================================================
|
|
375
|
+
// 5. validateDependenciesStructure
|
|
376
|
+
// =============================================================================
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Validates the full structure of the dependencies store.
|
|
380
|
+
* Checks that the dependencies object is valid, has no getters, and limits are well-formed.
|
|
381
|
+
* Adapted from: validateDependenciesObject() in DependenciesNamespace/validators.ts
|
|
382
|
+
*
|
|
383
|
+
* @param deps - DependenciesStore instance (typed as unknown to avoid core coupling)
|
|
384
|
+
* @throws {TypeError} If deps is not an object
|
|
385
|
+
* @throws {TypeError} If deps.dependencies is not a valid plain object (or has getters)
|
|
386
|
+
* @throws {TypeError} If deps.limits is missing or has non-numeric limit values
|
|
387
|
+
*/
|
|
388
|
+
export function validateDependenciesStructure(deps: unknown): void {
|
|
389
|
+
if (!deps || typeof deps !== "object") {
|
|
390
|
+
throw new TypeError(
|
|
391
|
+
"[validation-plugin] validateDependenciesStructure: deps must be an object",
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const depsRecord = deps as Record<string, unknown>;
|
|
396
|
+
|
|
397
|
+
// Validate dependencies field exists and is an object
|
|
398
|
+
if (!depsRecord.dependencies || typeof depsRecord.dependencies !== "object") {
|
|
399
|
+
throw new TypeError(
|
|
400
|
+
"[validation-plugin] validateDependenciesStructure: deps.dependencies must be an object",
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const dependencies = depsRecord.dependencies as Record<string, unknown>;
|
|
405
|
+
|
|
406
|
+
// Getters can throw, return different values, or have side effects — reject them
|
|
407
|
+
for (const key of Object.keys(dependencies)) {
|
|
408
|
+
if (Object.getOwnPropertyDescriptor(dependencies, key)?.get) {
|
|
409
|
+
throw new TypeError(
|
|
410
|
+
`[validation-plugin] validateDependenciesStructure: dependency "${key}" must not use a getter`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Validate limits field exists and is an object
|
|
416
|
+
if (!depsRecord.limits || typeof depsRecord.limits !== "object") {
|
|
417
|
+
throw new TypeError(
|
|
418
|
+
"[validation-plugin] validateDependenciesStructure: deps.limits must be an object",
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const limits = depsRecord.limits as Record<string, unknown>;
|
|
423
|
+
const expectedLimitKeys: (keyof LocalDependencyLimits)[] = [
|
|
424
|
+
"maxDependencies",
|
|
425
|
+
"maxPlugins",
|
|
426
|
+
"maxListeners",
|
|
427
|
+
"warnListeners",
|
|
428
|
+
"maxEventDepth",
|
|
429
|
+
"maxLifecycleHandlers",
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
for (const key of expectedLimitKeys) {
|
|
433
|
+
if (typeof limits[key] !== "number") {
|
|
434
|
+
throw new TypeError(
|
|
435
|
+
`[validation-plugin] validateDependenciesStructure: deps.limits.${key} must be a number, got ${typeof limits[key]}`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// =============================================================================
|
|
442
|
+
// 6. validateLimitsConsistency
|
|
443
|
+
// =============================================================================
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Validates that actual resource counts don't exceed configured limits.
|
|
447
|
+
* Compares dependency count vs maxDependencies limit from the deps store.
|
|
448
|
+
* Any route-level limit configured in options is also checked against definitions.
|
|
449
|
+
* Adapted from: validateLimits() in OptionsNamespace/validators.ts
|
|
450
|
+
*
|
|
451
|
+
* @param options - Router options (typed as unknown to avoid core coupling)
|
|
452
|
+
* @throws {RangeError} If dependency count reaches or exceeds maxDependencies limit
|
|
453
|
+
* @throws {RangeError} If route count exceeds a configured maxRoutes limit in options
|
|
454
|
+
*/
|
|
455
|
+
function extractConfiguredLimits(options: unknown): Record<string, unknown> {
|
|
456
|
+
const opts =
|
|
457
|
+
options && typeof options === "object"
|
|
458
|
+
? (options as Record<string, unknown>)
|
|
459
|
+
: {};
|
|
460
|
+
|
|
461
|
+
return opts.limits && typeof opts.limits === "object"
|
|
462
|
+
? (opts.limits as Record<string, unknown>)
|
|
463
|
+
: {};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function checkRouteCountLimit(
|
|
467
|
+
store: unknown,
|
|
468
|
+
configuredLimits: Record<string, unknown>,
|
|
469
|
+
): void {
|
|
470
|
+
if (!store || typeof store !== "object") {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const storeRecord = store as Record<string, unknown>;
|
|
475
|
+
|
|
476
|
+
if (!Array.isArray(storeRecord.definitions)) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const routeCount = (storeRecord.definitions as unknown[]).length;
|
|
481
|
+
const maxRoutes = configuredLimits.maxRoutes;
|
|
482
|
+
|
|
483
|
+
if (
|
|
484
|
+
typeof maxRoutes === "number" &&
|
|
485
|
+
maxRoutes > 0 &&
|
|
486
|
+
routeCount > maxRoutes
|
|
487
|
+
) {
|
|
488
|
+
throw new RangeError(
|
|
489
|
+
`[validation-plugin] validateLimitsConsistency: route count (${routeCount}) exceeds configured limit (${maxRoutes})`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function checkDepCountLimit(
|
|
495
|
+
deps: unknown,
|
|
496
|
+
configuredLimits: Record<string, unknown>,
|
|
497
|
+
): void {
|
|
498
|
+
if (!deps || typeof deps !== "object") {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const depsRecord = deps as Record<string, unknown>;
|
|
503
|
+
const dependencies = depsRecord.dependencies;
|
|
504
|
+
const depsLimits = depsRecord.limits;
|
|
505
|
+
|
|
506
|
+
if (
|
|
507
|
+
!dependencies ||
|
|
508
|
+
typeof dependencies !== "object" ||
|
|
509
|
+
!depsLimits ||
|
|
510
|
+
typeof depsLimits !== "object"
|
|
511
|
+
) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const depCount = Object.keys(dependencies).length;
|
|
516
|
+
const limitsRecord = depsLimits as Record<string, unknown>;
|
|
517
|
+
const maxDepsFromOptions = configuredLimits.maxDependencies;
|
|
518
|
+
const maxDepsFromStore = limitsRecord.maxDependencies;
|
|
519
|
+
const maxDeps =
|
|
520
|
+
typeof maxDepsFromOptions === "number"
|
|
521
|
+
? maxDepsFromOptions
|
|
522
|
+
: maxDepsFromStore;
|
|
523
|
+
|
|
524
|
+
if (typeof maxDeps === "number" && maxDeps > 0 && depCount >= maxDeps) {
|
|
525
|
+
throw new RangeError(
|
|
526
|
+
`[validation-plugin] validateLimitsConsistency: dependency count (${depCount}) reaches or exceeds maxDependencies limit (${maxDeps})`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function validateLimitsConsistency(
|
|
532
|
+
options: unknown,
|
|
533
|
+
store: unknown,
|
|
534
|
+
deps: unknown,
|
|
535
|
+
): void {
|
|
536
|
+
const configuredLimits = extractConfiguredLimits(options);
|
|
537
|
+
|
|
538
|
+
checkRouteCountLimit(store, configuredLimits);
|
|
539
|
+
checkDepCountLimit(deps, configuredLimits);
|
|
540
|
+
}
|