@emeryld/rrroutes-contract 2.4.13 → 2.4.15
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 +23 -21
- package/dist/core/routesV3.builder.d.ts +29 -21
- package/dist/core/routesV3.core.d.ts +67 -14
- package/dist/index.cjs +108 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +108 -112
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,8 +37,8 @@ import { z } from 'zod'
|
|
|
37
37
|
|
|
38
38
|
// 1) Describe your API
|
|
39
39
|
const leaves = resource('/v1')
|
|
40
|
-
.sub(
|
|
41
|
-
users
|
|
40
|
+
.sub(
|
|
41
|
+
resource('users')
|
|
42
42
|
.get({
|
|
43
43
|
querySchema: z.object({
|
|
44
44
|
search: z.string().optional(),
|
|
@@ -49,11 +49,13 @@ const leaves = resource('/v1')
|
|
|
49
49
|
),
|
|
50
50
|
description: 'Find users',
|
|
51
51
|
})
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
.sub(
|
|
53
|
+
resource(':userId', undefined, z.string().uuid())
|
|
54
|
+
.patch({
|
|
55
|
+
bodySchema: z.object({ name: z.string().min(1) }),
|
|
56
|
+
outputSchema: z.object({ ok: z.literal(true) }),
|
|
57
|
+
})
|
|
58
|
+
.done(),
|
|
57
59
|
)
|
|
58
60
|
.done(),
|
|
59
61
|
)
|
|
@@ -76,7 +78,7 @@ const key = buildCacheKey({
|
|
|
76
78
|
leaf,
|
|
77
79
|
params: { userId: 'f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2' },
|
|
78
80
|
})
|
|
79
|
-
// key => ['patch', 'v1', 'users', 'f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2', {
|
|
81
|
+
// key => ['patch', 'v1', 'users', ['f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2'], {}]
|
|
80
82
|
```
|
|
81
83
|
|
|
82
84
|
## Detailed usage
|
|
@@ -87,10 +89,9 @@ const key = buildCacheKey({
|
|
|
87
89
|
import { resource } from '@emeryld/rrroutes-contract'
|
|
88
90
|
import { z } from 'zod'
|
|
89
91
|
|
|
90
|
-
const leaves = resource('/api')
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
projects
|
|
92
|
+
const leaves = resource('/api')
|
|
93
|
+
.sub(
|
|
94
|
+
resource('projects')
|
|
94
95
|
.get({
|
|
95
96
|
feed: true, // infinite/feed for clients
|
|
96
97
|
querySchema: z.object({
|
|
@@ -107,15 +108,15 @@ const leaves = resource('/api') // base path, optional inherited cfg
|
|
|
107
108
|
outputSchema: z.object({ id: z.string(), name: z.string() }),
|
|
108
109
|
description: 'Create a project',
|
|
109
110
|
})
|
|
110
|
-
.
|
|
111
|
-
|
|
111
|
+
.sub(
|
|
112
|
+
resource(':projectId', undefined, z.string().uuid())
|
|
112
113
|
.get({ outputSchema: z.object({ id: z.string(), name: z.string() }) })
|
|
113
114
|
.patch({
|
|
114
115
|
bodySchema: z.object({ name: z.string().min(1) }),
|
|
115
116
|
outputSchema: z.object({ id: z.string(), name: z.string() }),
|
|
116
117
|
})
|
|
117
|
-
.sub(
|
|
118
|
-
avatar
|
|
118
|
+
.sub(
|
|
119
|
+
resource('avatar')
|
|
119
120
|
.put({
|
|
120
121
|
bodyFiles: [{ name: 'avatar', maxCount: 1 }], // signals multipart upload
|
|
121
122
|
bodySchema: z.object({ avatar: z.instanceof(Blob) }),
|
|
@@ -130,8 +131,8 @@ const leaves = resource('/api') // base path, optional inherited cfg
|
|
|
130
131
|
.done()
|
|
131
132
|
```
|
|
132
133
|
|
|
133
|
-
- `
|
|
134
|
-
- `
|
|
134
|
+
- `resource(segment, nodeCfg?, idSchema?)` scopes a branch. Pass a segment name (e.g. `'projects'`, `':projectId'`) plus optional per-node config. Supplying an `idSchema` along with a `:param` segment wires up the params schema for all descendants.
|
|
135
|
+
- `sub(childA, childB, ...)` mounts one or more child resources built elsewhere via `resource(...).get(...).done()`. Call it once per branch; pass multiple children at once when needed.
|
|
135
136
|
- Methods (`get/post/put/patch/delete`) merge the active param schema unless you override via `paramsSchema`.
|
|
136
137
|
- `done()` closes a branch and returns the collected readonly tuple of leaves.
|
|
137
138
|
|
|
@@ -157,6 +158,7 @@ type Output = InferOutput<typeof leaf> // { id: string; name: string }
|
|
|
157
158
|
// Runtime helpers
|
|
158
159
|
const url = compilePath(leaf.path, { projectId: '123' }) // "/api/projects/123"
|
|
159
160
|
const cacheKey = buildCacheKey({ leaf, params: { projectId: '123' } })
|
|
161
|
+
// cacheKey => ['patch', 'api', 'projects', ['123'], {}]
|
|
160
162
|
|
|
161
163
|
// Typed subsets for routers/microfrontends
|
|
162
164
|
type ProjectRoutes = SubsetRoutes<typeof registry.all, '/api/projects'>
|
|
@@ -201,8 +203,8 @@ const leaves = r
|
|
|
201
203
|
},
|
|
202
204
|
({ collection }) =>
|
|
203
205
|
collection
|
|
204
|
-
.sub(
|
|
205
|
-
stats
|
|
206
|
+
.sub(
|
|
207
|
+
resource('stats')
|
|
206
208
|
.get({
|
|
207
209
|
outputSchema: z.object({ total: z.number() }),
|
|
208
210
|
description: 'Extra endpoint alongside CRUD',
|
|
@@ -280,7 +282,7 @@ function onChatMessage(raw: unknown) {
|
|
|
280
282
|
|
|
281
283
|
### Edge cases and notes
|
|
282
284
|
|
|
283
|
-
- `paramsSchema` on a method overrides the merged schema from
|
|
285
|
+
- `paramsSchema` on a method overrides the merged schema from parent segments—useful when you need stricter validation per verb.
|
|
284
286
|
- `bodyFiles` marks a route as multipart; servers can attach upload middleware, and clients should send `FormData`.
|
|
285
287
|
- CRUD helper only emits create/update routes when the matching `bodySchema` is provided; delete can be disabled via `enable.remove: false`.
|
|
286
288
|
- Feed-only behavior (`feed: true`) is intended for GET endpoints; clients treat them as infinite queries.
|
|
@@ -4,7 +4,6 @@ type ZodTypeAny = z.ZodTypeAny;
|
|
|
4
4
|
type ParamZod<Name extends string, S extends ZodTypeAny> = z.ZodObject<{
|
|
5
5
|
[K in Name]: S;
|
|
6
6
|
}>;
|
|
7
|
-
type MergedParamsResult<PS, Name extends string, P extends ZodTypeAny> = PS extends ZodTypeAny ? z.ZodIntersection<PS, ParamZod<Name, P>> : ParamZod<Name, P>;
|
|
8
7
|
type BaseMethodCfg<C extends MethodCfg> = Merge<Omit<MethodCfg, 'querySchema' | 'outputSchema' | 'feed'>, Omit<C, 'querySchema' | 'outputSchema' | 'feed'>>;
|
|
9
8
|
type FeedField<C extends MethodCfg> = C['feed'] extends true ? {
|
|
10
9
|
feed: true;
|
|
@@ -22,9 +21,29 @@ type ParamsField<C extends MethodCfg, PS> = C['paramsSchema'] extends ZodTypeAny
|
|
|
22
21
|
paramsSchema: PS;
|
|
23
22
|
};
|
|
24
23
|
type EffectiveFeedFields<C extends MethodCfg, PS> = C['feed'] extends true ? FeedField<C> & FeedQueryField<C> & OutputField<C> & ParamsField<C, PS> : FeedField<C> & NonFeedQueryField<C> & OutputField<C> & ParamsField<C, PS>;
|
|
25
|
-
type
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
type WithNodeDefaults<C extends MethodCfg, I extends NodeCfg> = Merge<C, {
|
|
25
|
+
queryExtensionSchema: C['queryExtensionSchema'] extends ZodTypeAny ? C['queryExtensionSchema'] : NodeQueryExtension<I>;
|
|
26
|
+
outputMetaSchema: C['outputMetaSchema'] extends ZodTypeAny ? C['outputMetaSchema'] : NodeOutputMeta<I>;
|
|
27
|
+
}>;
|
|
28
|
+
type EffectiveCfg<C extends MethodCfg, PS, I extends NodeCfg> = Prettify<Merge<MethodCfg, BaseMethodCfg<WithNodeDefaults<C, I>> & EffectiveFeedFields<WithNodeDefaults<C, I>, PS>>>;
|
|
29
|
+
type NodeWithoutSchemas<I extends NodeCfg> = Omit<I, 'queryExtensionSchema' | 'outputMetaSchema'>;
|
|
30
|
+
type BuiltLeaf<M extends HttpMethod, Base extends string, I extends NodeCfg, C extends MethodCfg, PS> = Leaf<M, Base, Merge<NodeWithoutSchemas<I>, LowProfileCfg<EffectiveCfg<C, PS, I>>>>;
|
|
31
|
+
type StripParamName<Name extends string> = Name extends `:${infer P}` ? P : Name;
|
|
32
|
+
type NormalizeBaseLiteral<Base extends string, HasParam extends boolean> = HasParam extends true ? Base extends `:${string}` ? Base : Base extends '' ? '' : `:${Base}` : Base;
|
|
33
|
+
type ParamSchemaForBase<Base extends string, Param extends ZodTypeAny | undefined> = Param extends ZodTypeAny ? ParamZod<StripParamName<Base>, Param> : undefined;
|
|
34
|
+
type ResourceBase<Base extends string, Param extends ZodTypeAny | undefined> = NormalizeBaseLiteral<Base, Param extends ZodTypeAny ? true : false>;
|
|
35
|
+
type NodeQueryExtension<I extends NodeCfg> = I extends {
|
|
36
|
+
queryExtensionSchema?: infer QE;
|
|
37
|
+
} ? QE extends ZodTypeAny ? QE : undefined : undefined;
|
|
38
|
+
type NodeOutputMeta<I extends NodeCfg> = I extends {
|
|
39
|
+
outputMetaSchema?: infer OE;
|
|
40
|
+
} ? OE extends ZodTypeAny ? OE : undefined : undefined;
|
|
41
|
+
type SubResourceCollections = readonly [
|
|
42
|
+
ReadonlyArray<AnyLeafLowProfile>,
|
|
43
|
+
...ReadonlyArray<AnyLeafLowProfile>[]
|
|
44
|
+
];
|
|
45
|
+
type MergeAugmentedCollections<Base extends string, Param extends ZodTypeAny | undefined, Collections extends ReadonlyArray<ReadonlyArray<AnyLeafLowProfile>>, Acc extends readonly AnyLeafLowProfile[] = []> = Collections extends readonly [infer First, ...infer Rest] ? First extends ReadonlyArray<AnyLeafLowProfile> ? Rest extends ReadonlyArray<ReadonlyArray<AnyLeafLowProfile>> ? MergeAugmentedCollections<Base, Param, Rest, MergeArray<Acc, AugmentLeaves<Base, Param, First>>> : MergeArray<Acc, AugmentLeaves<Base, Param, First>> : MergeAugmentedCollections<Base, Param, Rest extends ReadonlyArray<ReadonlyArray<AnyLeafLowProfile>> ? Rest : [], Acc> : Acc;
|
|
46
|
+
type MethodFns<Base extends string, Acc extends readonly AnyLeafLowProfile[], I extends NodeCfg, PS extends ZodTypeAny | undefined, Used extends HttpMethod | 'add' | 'sub'> = {
|
|
28
47
|
/**
|
|
29
48
|
* Register a GET leaf at the current path.
|
|
30
49
|
*/
|
|
@@ -55,24 +74,13 @@ type MethodFns<Base extends string, Acc extends readonly AnyLeafLowProfile[], I
|
|
|
55
74
|
}>, PS>>>, I, PS, Used | 'delete'>;
|
|
56
75
|
};
|
|
57
76
|
/** Builder surface used by `resource(...)` to accumulate leaves. */
|
|
58
|
-
export interface Branch<Base extends string, Acc extends readonly AnyLeafLowProfile[], I extends NodeCfg, PS extends ZodTypeAny | undefined, Used extends HttpMethod | 'add' = never> extends MethodFns<Base, Acc, I, PS, Used> {
|
|
59
|
-
/**
|
|
60
|
-
* Mount a static subtree under `name`.
|
|
61
|
-
* The `leaves` are built externally via `resource(...)` and will be
|
|
62
|
-
* rebased so that their paths become `${Base}/${name}${leaf.path}` and their
|
|
63
|
-
* paramsSchemas are merged with the parameters already accumulated on this branch.
|
|
64
|
-
*/
|
|
65
|
-
sub<Name extends string, const R extends readonly AnyLeafLowProfile[]>(name: Name, leaves: R): Branch<Base, MergeArray<Acc, AugmentLeaves<`${Base}/${Name}`, PS, R>>, I, PS, Used>;
|
|
66
|
-
/**
|
|
67
|
-
* Mount a static subtree under `name` and merge extra node-level config.
|
|
68
|
-
*/
|
|
69
|
-
sub<Name extends string, J extends NodeCfg, const R extends readonly AnyLeafLowProfile[]>(name: Name, cfg: J, leaves: R): Branch<Base, MergeArray<Acc, AugmentLeaves<`${Base}/${Name}`, PS, R>>, Merge<I, J>, PS, Used>;
|
|
77
|
+
export interface Branch<Base extends string, Acc extends readonly AnyLeafLowProfile[], I extends NodeCfg, PS extends ZodTypeAny | undefined, Used extends HttpMethod | 'add' | 'sub' = never> extends MethodFns<Base, Acc, I, PS, Used> {
|
|
70
78
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
79
|
+
* Mount one or more sub-resources that were built via `resource(...)`.
|
|
80
|
+
* Each resource carries its own base path (e.g. `users`, `:userId`, `posts`)
|
|
81
|
+
* so this branch simply prefixes its current `Base` and merges param schemas.
|
|
74
82
|
*/
|
|
75
|
-
|
|
83
|
+
sub: 'sub' extends Used ? never : <const Collections extends SubResourceCollections, NextAcc extends readonly AnyLeafLowProfile[] = MergeArray<Acc, MergeAugmentedCollections<Base, PS, Collections>>>(...collections: Collections) => Branch<Base, NextAcc, I, PS, Used | 'sub'>;
|
|
76
84
|
/**
|
|
77
85
|
* Finish the branch and return the collected leaves.
|
|
78
86
|
* @returns Readonly tuple of accumulated leaves.
|
|
@@ -85,7 +93,7 @@ export interface Branch<Base extends string, Acc extends readonly AnyLeafLowProf
|
|
|
85
93
|
* @param inherited Optional node configuration applied to all descendants.
|
|
86
94
|
* @returns Root `Branch` instance used to compose the route tree.
|
|
87
95
|
*/
|
|
88
|
-
export declare function resource<Base extends string = '', I extends NodeCfg = {}>(base?: Base, inherited?: I): Branch<Base, readonly [], I,
|
|
96
|
+
export declare function resource<Base extends string = '', I extends NodeCfg = {}, Param extends ZodTypeAny | undefined = undefined>(base?: Base, inherited?: I, idSchema?: Param): Branch<ResourceBase<Base, Param>, readonly [], I, ParamSchemaForBase<Base, Param>>;
|
|
89
97
|
/**
|
|
90
98
|
* Merge two readonly tuples (preserves literal tuple information).
|
|
91
99
|
* @param arr1 First tuple.
|
|
@@ -9,10 +9,16 @@ export type FileField = {
|
|
|
9
9
|
maxCount: number;
|
|
10
10
|
};
|
|
11
11
|
/** Configuration that applies to an entire branch of the route tree. */
|
|
12
|
-
export
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
export interface NodeCfg {
|
|
13
|
+
/**
|
|
14
|
+
* Feed-specific query schema applied to all descendants unless they override it.
|
|
15
|
+
*/
|
|
16
|
+
queryExtensionSchema?: ZodType;
|
|
17
|
+
/**
|
|
18
|
+
* Feed meta schema applied to all descendants unless they override it.
|
|
19
|
+
*/
|
|
20
|
+
outputMetaSchema?: ZodType;
|
|
21
|
+
}
|
|
16
22
|
export type RouteSchema<Output = unknown, Input = Output> = {
|
|
17
23
|
__out: Output;
|
|
18
24
|
__in?: Input;
|
|
@@ -52,7 +58,7 @@ export declare function mergeSchemas(a: ZodType | undefined, b: ZodType | undefi
|
|
|
52
58
|
export declare function getZodShape(schema: ZodObject): any;
|
|
53
59
|
export declare function collectNestedFieldSuggestions(shape: Record<string, ZodType> | undefined, prefix?: string[]): string[];
|
|
54
60
|
export type ToRouteSchema<S> = S extends ZodType<infer Out, infer In> ? RouteSchema<Out, In> : S;
|
|
55
|
-
export type LowProfileCfg<Cfg extends MethodCfg> = Prettify<Omit<Cfg, 'bodySchema' | 'querySchema' | 'paramsSchema' | 'outputSchema' | 'outputMetaSchema'> & {
|
|
61
|
+
export type LowProfileCfg<Cfg extends MethodCfg = MethodCfg> = Prettify<Omit<Cfg, 'bodySchema' | 'querySchema' | 'paramsSchema' | 'outputSchema' | 'outputMetaSchema'> & {
|
|
56
62
|
bodySchema: ToRouteSchema<Cfg['bodySchema']>;
|
|
57
63
|
querySchema: ToRouteSchema<Cfg['querySchema']>;
|
|
58
64
|
paramsSchema: ToRouteSchema<Cfg['paramsSchema']>;
|
|
@@ -66,7 +72,7 @@ export type MethodCfg = {
|
|
|
66
72
|
bodySchema?: ZodType;
|
|
67
73
|
/** Zod schema describing the query string. */
|
|
68
74
|
querySchema?: ZodType;
|
|
69
|
-
/** Zod schema describing path params (
|
|
75
|
+
/** Zod schema describing path params (internal only, set through resource segments). */
|
|
70
76
|
paramsSchema?: ZodType;
|
|
71
77
|
/** Zod schema describing the response payload. */
|
|
72
78
|
outputSchema?: ZodType;
|
|
@@ -131,12 +137,25 @@ export type Merge<A, B> = Prettify<Omit<A, keyof B> & B>;
|
|
|
131
137
|
export type Append<T extends readonly unknown[], X> = [...T, X];
|
|
132
138
|
/** Concatenate two readonly tuple types. */
|
|
133
139
|
export type MergeArray<A extends readonly unknown[], B extends readonly unknown[]> = [...A, ...B];
|
|
140
|
+
type TrimTrailingSlash<S extends string> = S extends `${infer R}/` ? TrimTrailingSlash<R> : S;
|
|
141
|
+
type TrimLeadingSlash<S extends string> = S extends `/${infer R}` ? TrimLeadingSlash<R> : S;
|
|
142
|
+
type NormalizedBase<Base extends string> = TrimTrailingSlash<Base>;
|
|
143
|
+
type NormalizedChild<Child extends string> = TrimLeadingSlash<Child>;
|
|
144
|
+
export type JoinPath<Base extends string, Child extends string> = NormalizedBase<Base> extends '' ? Child : NormalizedChild<Child> extends '' ? NormalizedBase<Base> : `${NormalizedBase<Base>}/${NormalizedChild<Child>}`;
|
|
134
145
|
export type IntersectZod<A extends ZodType | undefined, B extends ZodType | undefined> = B extends ZodType ? A extends ZodType ? z.ZodIntersection<A, B> : B : A extends ZodType ? A : undefined;
|
|
135
146
|
type MergeRouteSchemas<Existing extends RouteSchema | undefined, Parent extends ZodType | undefined> = Existing extends RouteSchema<infer ExistingOut> ? Parent extends ZodType<infer ParentOut> ? RouteSchema<ExistingOut & ParentOut> : Existing : Parent extends ZodType<infer ParentOut> ? RouteSchema<ParentOut> : undefined;
|
|
136
147
|
type AugmentedCfg<Cfg extends MethodCfgLowProfile, Param extends ZodType | undefined> = Prettify<Omit<Cfg, 'paramsSchema'> & {
|
|
137
148
|
paramsSchema: MergeRouteSchemas<Cfg['paramsSchema'], Param>;
|
|
138
149
|
}>;
|
|
139
|
-
export type AugmentLeaves<P extends string, Param extends ZodType | undefined, R extends readonly LeafLowProfile[], Acc extends readonly LeafLowProfile[] = []> = R extends readonly [infer First, ...infer Rest] ? First extends LeafLowProfile ? AugmentLeaves<P, Param, Rest extends readonly LeafLowProfile[] ? Rest : [], Append<Acc, LeafLowProfile<First['method'],
|
|
150
|
+
export type AugmentLeaves<P extends string, Param extends ZodType | undefined, R extends readonly LeafLowProfile[], Acc extends readonly LeafLowProfile[] = []> = R extends readonly [infer First, ...infer Rest] ? First extends LeafLowProfile ? AugmentLeaves<P, Param, Rest extends readonly LeafLowProfile[] ? Rest : [], Append<Acc, LeafLowProfile<First['method'], JoinPath<P, First['path']>, AugmentedCfg<First['cfg'], Param>>>> : never : Acc;
|
|
151
|
+
type NodeQueryExtension<Node> = Node extends {
|
|
152
|
+
queryExtensionSchema?: infer QE;
|
|
153
|
+
} ? QE extends ZodType ? QE : undefined : undefined;
|
|
154
|
+
type NodeOutputMeta<Node> = Node extends {
|
|
155
|
+
outputMetaSchema?: infer OM;
|
|
156
|
+
} ? OM extends ZodType ? OM : undefined : undefined;
|
|
157
|
+
type EffectiveQueryExtensionSchema<Method extends ZodType | undefined, Node extends NodeCfg | undefined> = Method extends ZodType ? Method : NodeQueryExtension<Node>;
|
|
158
|
+
type EffectiveOutputMetaSchema<Method extends ZodType | undefined, Node extends NodeCfg | undefined> = Method extends ZodType ? Method : NodeOutputMeta<Node>;
|
|
140
159
|
type SegmentParams<S extends string> = S extends `:${infer P}` ? P : never;
|
|
141
160
|
type Split<S extends string> = S extends '' ? [] : S extends `${infer A}/${infer B}` ? [A, ...Split<B>] : [S];
|
|
142
161
|
type ExtractParamNames<Path extends string> = SegmentParams<Split<Path>[number]>;
|
|
@@ -149,11 +168,35 @@ export type ExtractParamsFromPath<Path extends string> = ExtractParamNames<Path>
|
|
|
149
168
|
* @returns Path string with parameters substituted.
|
|
150
169
|
*/
|
|
151
170
|
export declare function compilePath<Path extends string>(path: Path, params: ExtractParamsFromPath<Path>): string;
|
|
171
|
+
/**
|
|
172
|
+
* Build a deterministic cache key for the given leaf.
|
|
173
|
+
* The key matches the shape consumed by React Query helpers.
|
|
174
|
+
* @param args.leaf Leaf describing the endpoint.
|
|
175
|
+
* @param args.params Optional params used to build the path.
|
|
176
|
+
* @param args.query Optional query payload.
|
|
177
|
+
* @returns Tuple suitable for React Query cache keys.
|
|
178
|
+
*/
|
|
179
|
+
type SplitPath<P extends string> = P extends '' ? [] : P extends `${infer A}/${infer B}` ? [A, ...SplitPath<B>] : [P];
|
|
180
|
+
type ParamValue<Params, Key extends string> = Params extends Record<Key, infer V> ? V : undefined;
|
|
181
|
+
type MapKeySegments<Segments extends readonly string[], Params> = Segments extends [infer A, ...infer Rest] ? A extends string ? A extends `:${infer Key}` ? [
|
|
182
|
+
[
|
|
183
|
+
ParamValue<Params, Key>
|
|
184
|
+
],
|
|
185
|
+
...MapKeySegments<Rest extends readonly string[] ? Rest : [], Params>
|
|
186
|
+
] : [
|
|
187
|
+
A,
|
|
188
|
+
...MapKeySegments<Rest extends readonly string[] ? Rest : [], Params>
|
|
189
|
+
] : [] : [];
|
|
190
|
+
type CacheKeyForLeaf<L extends AnyLeafLowProfile> = readonly [
|
|
191
|
+
L['method'],
|
|
192
|
+
...MapKeySegments<SplitPath<L['path']>, ExtractParamsFromPath<L['path']>>,
|
|
193
|
+
InferQuery<L> extends never ? {} : InferQuery<L>
|
|
194
|
+
];
|
|
152
195
|
export declare function buildCacheKey<L extends AnyLeafLowProfile>(args: {
|
|
153
196
|
leaf: L;
|
|
154
197
|
params?: ExtractParamsFromPath<L['path']>;
|
|
155
198
|
query?: InferQuery<L>;
|
|
156
|
-
}):
|
|
199
|
+
}): CacheKeyForLeaf<L>;
|
|
157
200
|
/** Definition-time method config (for clarity). */
|
|
158
201
|
export type MethodCfgDef = MethodCfg;
|
|
159
202
|
/** Low-profile method config where schemas carry a phantom __out like SocketSchema. */
|
|
@@ -166,9 +209,10 @@ export type MethodCfgLowProfile = Omit<MethodCfg, 'bodySchema' | 'querySchema' |
|
|
|
166
209
|
queryExtensionSchema?: RouteSchema;
|
|
167
210
|
};
|
|
168
211
|
export type AnyLeafLowProfile = LeafLowProfile<HttpMethod, string, MethodCfgLowProfile>;
|
|
169
|
-
export declare function buildLowProfileLeaf<const M extends HttpMethod, const Path extends string, const O extends ZodType | undefined = undefined, const P extends ZodType | undefined = undefined, const Q extends ZodType | undefined = undefined, const B extends ZodType | undefined = undefined, const FO extends ZodType | undefined = undefined, const FQ extends ZodType | undefined = undefined, const Feed extends boolean = false>(leaf: {
|
|
212
|
+
export declare function buildLowProfileLeaf<const M extends HttpMethod, const Path extends string, const Node extends NodeCfg | undefined = undefined, const O extends ZodType | undefined = undefined, const P extends ZodType | undefined = undefined, const Q extends ZodType | undefined = undefined, const B extends ZodType | undefined = undefined, const FO extends ZodType | undefined = undefined, const FQ extends ZodType | undefined = undefined, const Feed extends boolean = false>(leaf: {
|
|
170
213
|
method: M;
|
|
171
214
|
path: Path;
|
|
215
|
+
node?: Node;
|
|
172
216
|
cfg: Omit<MethodCfg, 'bodySchema' | 'querySchema' | 'paramsSchema' | 'outputSchema' | 'feed' | 'outputMetaSchema' | 'queryExtensionSchema'> & {
|
|
173
217
|
feed?: Feed;
|
|
174
218
|
bodySchema?: B;
|
|
@@ -183,15 +227,15 @@ export declare function buildLowProfileLeaf<const M extends HttpMethod, const Pa
|
|
|
183
227
|
bodySchema: B extends ZodType<infer BOut, infer BIn> ? RouteSchema<BOut, BIn> : undefined;
|
|
184
228
|
querySchema: Feed extends true ? FeedQueryField<{
|
|
185
229
|
querySchema: Q;
|
|
186
|
-
queryExtensionSchema: FQ
|
|
230
|
+
queryExtensionSchema: EffectiveQueryExtensionSchema<FQ, Node>;
|
|
187
231
|
}>['querySchema'] extends ZodType<infer QOut, infer QIn> ? RouteSchema<QOut, QIn> : never : Q extends ZodType<infer QOut, infer QIn> ? RouteSchema<QOut, QIn> : undefined;
|
|
188
232
|
paramsSchema: P extends ZodType<infer POut, infer PIn> ? RouteSchema<POut, PIn> : undefined;
|
|
189
233
|
outputSchema: OutputField<{
|
|
190
234
|
outputSchema: O;
|
|
191
|
-
outputMetaSchema: FO
|
|
235
|
+
outputMetaSchema: EffectiveOutputMetaSchema<FO, Node>;
|
|
192
236
|
}>['outputSchema'] extends ZodType<infer OOut, infer OIn> ? RouteSchema<OOut, OIn> : never;
|
|
193
|
-
outputMetaSchema: FO extends ZodType<infer OOut, infer OIn> ? RouteSchema<OOut, OIn> : undefined;
|
|
194
|
-
queryExtensionSchema: FQ extends ZodType<infer QOut, infer QIn> ? RouteSchema<QOut, QIn> : undefined;
|
|
237
|
+
outputMetaSchema: EffectiveOutputMetaSchema<FO, Node> extends ZodType<infer OOut, infer OIn> ? RouteSchema<OOut, OIn> : undefined;
|
|
238
|
+
queryExtensionSchema: EffectiveQueryExtensionSchema<FQ, Node> extends ZodType<infer QOut, infer QIn> ? RouteSchema<QOut, QIn> : undefined;
|
|
195
239
|
}>>;
|
|
196
240
|
export type LeafLowProfile<M extends HttpMethod = HttpMethod, P extends string = string, C extends MethodCfgLowProfile = MethodCfgLowProfile> = {
|
|
197
241
|
/** Lowercase HTTP method (get/post/...). */
|
|
@@ -208,7 +252,16 @@ export type InferQuery<L extends AnyLeafLowProfile, Fallback = never> = L['cfg']
|
|
|
208
252
|
/** Infer request body shape from a Zod schema when present. */
|
|
209
253
|
export type InferBody<L extends AnyLeafLowProfile, Fallback = never> = L['cfg']['bodySchema'] extends RouteSchema<any, infer BIn> ? BIn : L['cfg']['bodySchema'] extends ZodType<any, infer BIn> ? BIn : Fallback;
|
|
210
254
|
/** Infer handler output shape from a Zod schema. Defaults to unknown. */
|
|
211
|
-
|
|
255
|
+
type InferMetaFromCfg<L extends AnyLeafLowProfile> = L['cfg']['outputMetaSchema'] extends RouteSchema<infer O> ? O : L['cfg']['outputMetaSchema'] extends ZodType<infer O> ? O : string | undefined;
|
|
256
|
+
type ResolvedMeta<L extends AnyLeafLowProfile, M> = [M] extends [never] | [unknown] ? InferMetaFromCfg<L> : M;
|
|
257
|
+
type ApplyMetaFallback<L extends AnyLeafLowProfile, O> = O extends {
|
|
258
|
+
meta: infer M;
|
|
259
|
+
} ? (undefined extends ResolvedMeta<L, M> ? {
|
|
260
|
+
meta?: ResolvedMeta<L, M>;
|
|
261
|
+
} : {
|
|
262
|
+
meta: ResolvedMeta<L, M>;
|
|
263
|
+
}) & Omit<O, 'meta'> : O;
|
|
264
|
+
export type InferOutput<L extends AnyLeafLowProfile, Fallback = never> = L['cfg']['outputSchema'] extends RouteSchema<infer O> ? ApplyMetaFallback<L, O> : L['cfg']['outputSchema'] extends ZodType<infer O> ? ApplyMetaFallback<L, O> : Fallback;
|
|
212
265
|
export type InferFeedOutputMeta<L extends AnyLeafLowProfile, Fallback = never> = L['cfg']['outputMetaSchema'] extends RouteSchema<infer O> ? O : L['cfg']['outputMetaSchema'] extends ZodType<infer O> ? O : Fallback;
|
|
213
266
|
export type InferFeedQuery<L extends AnyLeafLowProfile, Fallback = never> = L['cfg']['queryExtensionSchema'] extends RouteSchema<any, infer QIn> ? QIn : L['cfg']['queryExtensionSchema'] extends ZodType<any, infer QIn> ? QIn : Fallback;
|
|
214
267
|
/** Render a type as if it were a simple object literal. */
|
package/dist/index.cjs
CHANGED
|
@@ -87,25 +87,31 @@ function compilePath(path, params) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
function buildCacheKey(args) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const segments = args.leaf.path.split("/").filter(Boolean);
|
|
91
|
+
const keyParts = [args.leaf.method];
|
|
92
|
+
for (const segment of segments) {
|
|
93
|
+
if (segment.startsWith(":")) {
|
|
94
|
+
const paramName = segment.slice(1);
|
|
95
|
+
const value = args.params && paramName in args.params ? args.params[paramName] : void 0;
|
|
96
|
+
keyParts.push([value]);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
keyParts.push(segment);
|
|
93
100
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
...p.split("/").filter(Boolean),
|
|
97
|
-
args.query ?? {}
|
|
98
|
-
];
|
|
101
|
+
keyParts.push(args.query ?? {});
|
|
102
|
+
return keyParts;
|
|
99
103
|
}
|
|
100
104
|
function buildLowProfileLeaf(leaf) {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
const nodeCfg = leaf.node ?? {};
|
|
106
|
+
const mergedCfg = { ...nodeCfg, ...leaf.cfg };
|
|
107
|
+
const effectiveQuerySchema = mergedCfg.feed === true ? mergeSchemas(mergedCfg.querySchema, mergedCfg.queryExtensionSchema) : mergedCfg.querySchema;
|
|
108
|
+
const effectiveOutputSchema = mergedCfg.outputSchema ? import_zod.z.object({
|
|
109
|
+
out: mergedCfg.outputSchema,
|
|
110
|
+
meta: mergedCfg.outputMetaSchema ?? import_zod.z.string().optional()
|
|
105
111
|
}) : import_zod.z.object({
|
|
106
|
-
meta:
|
|
112
|
+
meta: mergedCfg.outputMetaSchema ?? import_zod.z.string().optional()
|
|
107
113
|
});
|
|
108
|
-
if (
|
|
114
|
+
if (mergedCfg.feed === true && effectiveQuerySchema instanceof import_zod.ZodObject) {
|
|
109
115
|
const shape = getZodShape(effectiveQuerySchema);
|
|
110
116
|
const nestedFieldSuggestions = collectNestedFieldSuggestions(shape);
|
|
111
117
|
if (nestedFieldSuggestions.length > 0) {
|
|
@@ -117,41 +123,41 @@ function buildLowProfileLeaf(leaf) {
|
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
return {
|
|
120
|
-
|
|
126
|
+
method: leaf.method,
|
|
127
|
+
path: leaf.path,
|
|
121
128
|
cfg: {
|
|
122
|
-
...
|
|
123
|
-
bodySchema:
|
|
129
|
+
...mergedCfg,
|
|
130
|
+
bodySchema: mergedCfg.bodySchema,
|
|
124
131
|
querySchema: effectiveQuerySchema,
|
|
125
|
-
paramsSchema:
|
|
132
|
+
paramsSchema: mergedCfg.paramsSchema,
|
|
126
133
|
outputSchema: effectiveOutputSchema,
|
|
127
|
-
outputMetaSchema:
|
|
128
|
-
queryExtensionSchema:
|
|
134
|
+
outputMetaSchema: mergedCfg.outputMetaSchema,
|
|
135
|
+
queryExtensionSchema: mergedCfg.queryExtensionSchema
|
|
129
136
|
}
|
|
130
137
|
};
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
// src/core/routesV3.builder.ts
|
|
134
|
-
function resource(base, inherited) {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
141
|
+
function resource(base, inherited, idSchema) {
|
|
142
|
+
const rawBase = base ?? "";
|
|
143
|
+
const normalizedBase = normalizeBaseSegment(
|
|
144
|
+
rawBase,
|
|
145
|
+
Boolean(idSchema)
|
|
146
|
+
);
|
|
147
|
+
const rootInherited = inherited === void 0 ? {} : { ...inherited };
|
|
148
|
+
const rootParams = createParamSchema(rawBase, idSchema);
|
|
137
149
|
function makeBranch(base2, inherited2, mergedParamsSchema) {
|
|
138
150
|
const stack = [];
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
const currentBase = base2;
|
|
152
|
+
const inheritedCfg = inherited2;
|
|
141
153
|
let currentParamsSchema = mergedParamsSchema;
|
|
142
154
|
function add(method, cfg) {
|
|
143
155
|
const effectiveParamsSchema = cfg.paramsSchema ?? currentParamsSchema;
|
|
144
|
-
const fullCfg = effectiveParamsSchema ? {
|
|
145
|
-
...inheritedCfg,
|
|
146
|
-
...cfg,
|
|
147
|
-
paramsSchema: effectiveParamsSchema
|
|
148
|
-
} : {
|
|
149
|
-
...inheritedCfg,
|
|
150
|
-
...cfg
|
|
151
|
-
};
|
|
156
|
+
const fullCfg = effectiveParamsSchema ? { ...cfg, paramsSchema: effectiveParamsSchema } : { ...cfg };
|
|
152
157
|
const leaf = buildLowProfileLeaf({
|
|
153
158
|
method,
|
|
154
159
|
path: currentBase,
|
|
160
|
+
node: inheritedCfg,
|
|
155
161
|
cfg: fullCfg
|
|
156
162
|
});
|
|
157
163
|
stack.push(leaf);
|
|
@@ -159,88 +165,42 @@ function resource(base, inherited) {
|
|
|
159
165
|
}
|
|
160
166
|
const api = {
|
|
161
167
|
/**
|
|
162
|
-
* Mount
|
|
163
|
-
*
|
|
168
|
+
* Mount one or more subtrees built elsewhere.
|
|
164
169
|
* Usage:
|
|
165
|
-
*
|
|
166
|
-
*
|
|
170
|
+
* resource('/api').sub(
|
|
171
|
+
* resource('users').get(...).done(),
|
|
172
|
+
* resource('projects').get(...).done()
|
|
173
|
+
* )
|
|
167
174
|
*/
|
|
168
|
-
sub(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (Array.isArray(cfgOrLeaves)) {
|
|
172
|
-
leaves = cfgOrLeaves;
|
|
173
|
-
} else {
|
|
174
|
-
cfg = cfgOrLeaves;
|
|
175
|
-
leaves = maybeLeaves;
|
|
176
|
-
}
|
|
177
|
-
if (!leaves) {
|
|
178
|
-
throw new Error("sub() expects a leaves array as the last argument");
|
|
179
|
-
}
|
|
180
|
-
const childInherited = {
|
|
181
|
-
...inheritedCfg,
|
|
182
|
-
...cfg ?? {}
|
|
183
|
-
};
|
|
184
|
-
const baseForChildren = `${currentBase}/${name}`;
|
|
185
|
-
for (const leafLow of leaves) {
|
|
186
|
-
const leaf = leafLow;
|
|
187
|
-
const leafCfg = leaf.cfg;
|
|
188
|
-
const leafParams = leafCfg.paramsSchema;
|
|
189
|
-
const effectiveParams = mergeSchemas(
|
|
190
|
-
currentParamsSchema,
|
|
191
|
-
leafParams
|
|
192
|
-
);
|
|
193
|
-
const newCfg = {
|
|
194
|
-
...childInherited,
|
|
195
|
-
...leafCfg
|
|
196
|
-
};
|
|
197
|
-
if (effectiveParams) {
|
|
198
|
-
newCfg.paramsSchema = effectiveParams;
|
|
199
|
-
} else if ("paramsSchema" in newCfg) {
|
|
200
|
-
delete newCfg.paramsSchema;
|
|
201
|
-
}
|
|
202
|
-
const newLeaf = {
|
|
203
|
-
method: leaf.method,
|
|
204
|
-
path: `${baseForChildren}${leaf.path}`,
|
|
205
|
-
cfg: newCfg
|
|
206
|
-
};
|
|
207
|
-
stack.push(newLeaf);
|
|
175
|
+
sub(...collections) {
|
|
176
|
+
if (collections.length === 0) {
|
|
177
|
+
throw new Error("sub() expects at least one resource");
|
|
208
178
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (effectiveParams) {
|
|
234
|
-
newCfg.paramsSchema = effectiveParams;
|
|
235
|
-
} else if ("paramsSchema" in newCfg) {
|
|
236
|
-
delete newCfg.paramsSchema;
|
|
179
|
+
for (const leaves of collections) {
|
|
180
|
+
for (const leafLow of leaves) {
|
|
181
|
+
const leaf = leafLow;
|
|
182
|
+
const leafCfg = leaf.cfg;
|
|
183
|
+
const leafParams = leafCfg.paramsSchema;
|
|
184
|
+
const effectiveParams = mergeSchemas(
|
|
185
|
+
currentParamsSchema,
|
|
186
|
+
leafParams
|
|
187
|
+
);
|
|
188
|
+
const newCfg = {
|
|
189
|
+
...inheritedCfg,
|
|
190
|
+
...leafCfg
|
|
191
|
+
};
|
|
192
|
+
if (effectiveParams) {
|
|
193
|
+
newCfg.paramsSchema = effectiveParams;
|
|
194
|
+
} else if ("paramsSchema" in newCfg) {
|
|
195
|
+
delete newCfg.paramsSchema;
|
|
196
|
+
}
|
|
197
|
+
const newLeaf = {
|
|
198
|
+
method: leaf.method,
|
|
199
|
+
path: joinPaths(currentBase, leaf.path),
|
|
200
|
+
cfg: newCfg
|
|
201
|
+
};
|
|
202
|
+
stack.push(newLeaf);
|
|
237
203
|
}
|
|
238
|
-
const newLeaf = {
|
|
239
|
-
method: leaf.method,
|
|
240
|
-
path: `${baseForChildren}${leaf.path}`,
|
|
241
|
-
cfg: newCfg
|
|
242
|
-
};
|
|
243
|
-
stack.push(newLeaf);
|
|
244
204
|
}
|
|
245
205
|
return api;
|
|
246
206
|
},
|
|
@@ -266,11 +226,47 @@ function resource(base, inherited) {
|
|
|
266
226
|
};
|
|
267
227
|
return api;
|
|
268
228
|
}
|
|
269
|
-
return makeBranch(
|
|
229
|
+
return makeBranch(normalizedBase, rootInherited, rootParams);
|
|
270
230
|
}
|
|
271
231
|
var mergeArrays = (arr1, arr2) => {
|
|
272
232
|
return [...arr1, ...arr2];
|
|
273
233
|
};
|
|
234
|
+
function normalizeBaseSegment(base, hasParam) {
|
|
235
|
+
const value = base ?? "";
|
|
236
|
+
if (!hasParam) return value;
|
|
237
|
+
if (!value) {
|
|
238
|
+
throw new Error("resource() requires a segment name when defining an id schema");
|
|
239
|
+
}
|
|
240
|
+
if (value.includes("/")) {
|
|
241
|
+
const segments = value.split("/").filter(Boolean);
|
|
242
|
+
if (segments.length === 0) {
|
|
243
|
+
throw new Error("resource() received an invalid segment for param injection");
|
|
244
|
+
}
|
|
245
|
+
const lastIndex = segments.length - 1;
|
|
246
|
+
const last = segments[lastIndex];
|
|
247
|
+
segments[lastIndex] = last.startsWith(":") ? last : `:${last}`;
|
|
248
|
+
return `${value.startsWith("/") ? "/" : ""}${segments.join("/")}`;
|
|
249
|
+
}
|
|
250
|
+
return value.startsWith(":") ? value : `:${value}`;
|
|
251
|
+
}
|
|
252
|
+
function createParamSchema(nameSegment, schema) {
|
|
253
|
+
if (!schema) return void 0;
|
|
254
|
+
const segments = nameSegment.split("/").filter(Boolean);
|
|
255
|
+
const rawName = segments.length > 0 ? segments[segments.length - 1] : nameSegment;
|
|
256
|
+
const normalized = rawName.startsWith(":") ? rawName.slice(1) : rawName;
|
|
257
|
+
if (!normalized) {
|
|
258
|
+
throw new Error("resource() requires a non-empty name for id schema");
|
|
259
|
+
}
|
|
260
|
+
return import_zod2.z.object({ [normalized]: schema });
|
|
261
|
+
}
|
|
262
|
+
function joinPaths(parent, child) {
|
|
263
|
+
if (!parent) return child;
|
|
264
|
+
if (!child) return parent;
|
|
265
|
+
const trimmedParent = parent.endsWith("/") ? parent.replace(/\/+$/, "") : parent;
|
|
266
|
+
const trimmedChild = child.startsWith("/") ? child.replace(/^\/+/, "") : child;
|
|
267
|
+
if (!trimmedChild) return trimmedParent;
|
|
268
|
+
return `${trimmedParent}/${trimmedChild}`;
|
|
269
|
+
}
|
|
274
270
|
|
|
275
271
|
// src/core/routesV3.finalize.ts
|
|
276
272
|
function finalize(leaves) {
|