@tuyau/core 1.0.0-beta.2 → 1.0.0-beta.4

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.
@@ -31,6 +31,46 @@ function generateRouteParams(route) {
31
31
  const paramsTuple = dynamicParams.map(() => "string").join(", ");
32
32
  return { paramsType, paramsTuple };
33
33
  }
34
+ function toCamelCase(str) {
35
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
36
+ }
37
+ function buildTreeStructure(routes) {
38
+ const tree = /* @__PURE__ */ new Map();
39
+ for (const route of routes) {
40
+ const segments = route.name.split(".");
41
+ let current = tree;
42
+ for (let i = 0; i < segments.length; i++) {
43
+ const segment = toCamelCase(segments[i]);
44
+ const isLast = i === segments.length - 1;
45
+ if (isLast) {
46
+ current.set(segment, { routeName: route.name, route });
47
+ } else {
48
+ if (!current.has(segment)) {
49
+ current.set(segment, /* @__PURE__ */ new Map());
50
+ }
51
+ current = current.get(segment);
52
+ }
53
+ }
54
+ }
55
+ return tree;
56
+ }
57
+ function generateTreeInterface(tree, indent = 2) {
58
+ const spaces = " ".repeat(indent);
59
+ const lines = [];
60
+ for (const [key, value] of tree) {
61
+ if (value instanceof Map) {
62
+ lines.push(`${spaces}${key}: {`);
63
+ lines.push(generateTreeInterface(value, indent + 2));
64
+ lines.push(`${spaces}}`);
65
+ } else {
66
+ lines.push(`${spaces}${key}: Registry['${value.routeName}']`);
67
+ }
68
+ }
69
+ return lines.join("\n");
70
+ }
71
+ function normalizeImportPaths(typeString) {
72
+ return typeString.replace(/import\('app\//g, "import('#app/").replace(/\.ts'\)/g, "')");
73
+ }
34
74
  function generateRuntimeRegistryEntry(route) {
35
75
  const routeName = route.name;
36
76
  return ` '${routeName}': {
@@ -40,51 +80,97 @@ function generateRuntimeRegistryEntry(route) {
40
80
  types: placeholder as Registry['${routeName}']['types'],
41
81
  }`;
42
82
  }
83
+ function wrapResponseType(responseType) {
84
+ if (responseType === "unknown" || responseType === "{}") return responseType;
85
+ if (responseType.startsWith("ReturnType<")) {
86
+ return `Awaited<${responseType}>`;
87
+ }
88
+ return responseType;
89
+ }
90
+ function determineBodyAndQueryTypes(options) {
91
+ const { methods, requestType } = options;
92
+ const primaryMethod = methods[0];
93
+ const isGetLike = primaryMethod === "GET" || primaryMethod === "HEAD";
94
+ const hasValidator = requestType !== "{}";
95
+ if (!hasValidator) {
96
+ return { bodyType: "{}", queryType: "{}" };
97
+ }
98
+ if (isGetLike) {
99
+ return { bodyType: "{}", queryType: requestType };
100
+ }
101
+ return {
102
+ bodyType: `ExtractBody<${requestType}>`,
103
+ queryType: `ExtractQuery<${requestType}>`
104
+ };
105
+ }
43
106
  function generateTypesRegistryEntry(route) {
44
- const requestType = route.request?.type || "{}";
45
- const responseType = route.response?.type || "unknown";
107
+ const requestType = normalizeImportPaths(route.request?.type || "{}");
108
+ const responseType = wrapResponseType(route.response?.type || "unknown");
46
109
  const { paramsType, paramsTuple } = generateRouteParams(route);
47
110
  const routeName = route.name;
111
+ const { bodyType, queryType } = determineBodyAndQueryTypes({
112
+ methods: route.methods,
113
+ requestType
114
+ });
48
115
  return ` '${routeName}': {
49
116
  methods: ${JSON.stringify(route.methods)}
50
117
  pattern: '${route.pattern}'
51
118
  types: {
52
- body: ${requestType}
119
+ body: ${bodyType}
53
120
  paramsTuple: [${paramsTuple}]
54
121
  params: ${paramsType ? `{ ${paramsType} }` : "{}"}
55
- query: {}
122
+ query: ${queryType}
56
123
  response: ${responseType}
57
124
  }
58
125
  }`;
59
126
  }
60
127
  function generateRegistryEntry(route) {
61
- const requestType = route.request?.type || "{}";
62
- const responseType = route.response?.type || "unknown";
128
+ const requestType = normalizeImportPaths(route.request?.type || "{}");
129
+ const responseType = wrapResponseType(route.response?.type || "unknown");
63
130
  const { paramsType, paramsTuple } = generateRouteParams(route);
64
131
  const routeName = route.name;
132
+ const { bodyType, queryType } = determineBodyAndQueryTypes({
133
+ methods: route.methods,
134
+ requestType
135
+ });
65
136
  return ` '${routeName}': {
66
137
  methods: ${JSON.stringify(route.methods)},
67
138
  pattern: '${route.pattern}',
68
139
  tokens: ${JSON.stringify(route.tokens)},
69
140
  types: placeholder as {
70
- body: ${requestType}
141
+ body: ${bodyType}
71
142
  paramsTuple: [${paramsTuple}]
72
143
  params: ${paramsType ? `{ ${paramsType} }` : "{}"}
73
- query: {}
144
+ query: ${queryType}
74
145
  response: ${responseType}
75
146
  },
76
147
  }`;
77
148
  }
78
149
  function generateRuntimeContent(routes) {
79
150
  const registryEntries = routes.map(generateRuntimeRegistryEntry).join(",\n");
151
+ const tree = buildTreeStructure(routes);
152
+ const treeInterface = generateTreeInterface(tree);
80
153
  return `/* eslint-disable prettier/prettier */
81
- import { type AdonisEndpoint } from '@tuyau/core/types'
82
- import type { Registry } from './registry.schema'
154
+ import type { AdonisEndpoint } from '@tuyau/core/types'
155
+ import type { Registry } from './registry.schema.d.ts'
83
156
  const placeholder: any = {}
84
157
 
85
- export const registry = {
86
- ${registryEntries}
158
+ export interface ApiDefinition {
159
+ ${treeInterface}
160
+ }
161
+
162
+ const routes = {
163
+ ${registryEntries},
87
164
  } as const satisfies Record<string, AdonisEndpoint>
165
+
166
+ export const registry = {
167
+ routes,
168
+ $tree: {} as ApiDefinition,
169
+ }
170
+
171
+ declare module '@tuyau/core/types' {
172
+ export interface UserApiDefinition extends ApiDefinition {}
173
+ }
88
174
  `;
89
175
  }
90
176
  function generateTypesContent(routes) {
@@ -92,7 +178,7 @@ function generateTypesContent(routes) {
92
178
  return `/* eslint-disable prettier/prettier */
93
179
  /// <reference path="../../adonisrc.ts" />
94
180
 
95
- import type { AdonisEndpoint } from '@tuyau/core/types'
181
+ import type { ExtractBody, ExtractQuery } from '@tuyau/core/types'
96
182
  import type { Infer } from '@vinejs/vine/types'
97
183
 
98
184
  export interface Registry {
@@ -106,17 +192,29 @@ declare module '@tuyau/core/types' {
106
192
  }
107
193
  function generateRegistryContent(routes) {
108
194
  const registryEntries = routes.map(generateRegistryEntry).join(",\n");
195
+ const tree = buildTreeStructure(routes);
196
+ const treeInterface = generateTreeInterface(tree);
109
197
  return `/* eslint-disable prettier/prettier */
110
- import type { AdonisEndpoint } from '@tuyau/core/types'
198
+ import type { AdonisEndpoint, ExtractBody, ExtractQuery } from '@tuyau/core/types'
111
199
  import type { Infer } from '@vinejs/vine/types'
112
200
 
113
201
  const placeholder: any = {}
114
- export const registry = {
202
+
203
+ interface ApiDefinition {
204
+ ${treeInterface}
205
+ }
206
+
207
+ const routes = {
115
208
  ${registryEntries}
116
209
  } as const satisfies Record<string, AdonisEndpoint>
117
210
 
211
+ export const registry = {
212
+ routes,
213
+ $tree: {} as ApiDefinition,
214
+ }
215
+
118
216
  declare module '@tuyau/core/types' {
119
- type Registry = typeof registry
217
+ type Registry = typeof routes
120
218
  export interface UserRegistry extends Registry {}
121
219
  }
122
220
  `;
@@ -1,64 +1,62 @@
1
1
  import { UrlFor } from '@adonisjs/http-server/client/url_builder';
2
- import { AdonisEndpoint, TuyauConfiguration, BuildNamed, RegistryGroupedByMethod, PatternsByMethod, RequestArgs, EndpointByMethodPattern, StrKeys, Method } from './types/index.js';
2
+ import { TuyauRegistry, TuyauConfiguration, AdonisEndpoint, InferRoutes, TransformApiDefinition, InferTree, RegistryGroupedByMethod, PatternsByMethod, RequestArgs, EndpointByMethodPattern, StrKeys, Method } from './types/index.js';
3
3
  import { KyResponse, KyRequest, HTTPError } from 'ky';
4
4
 
5
5
  /**
6
6
  * Main client class for making HTTP requests to AdonisJS endpoints
7
7
  * Provides both fluent API and direct method calling capabilities
8
+ *
9
+ * @typeParam Reg - The full registry containing routes and $tree
10
+ * @typeParam Routes - The routes record extracted from the registry
8
11
  */
9
- declare class Tuyau<R extends Record<string, AdonisEndpoint>> {
12
+ declare class Tuyau<Reg extends TuyauRegistry, Routes extends Record<string, AdonisEndpoint> = InferRoutes<Reg>> {
10
13
  #private;
11
- private config;
12
- readonly api: BuildNamed<R>;
13
- readonly urlFor: UrlFor<RegistryGroupedByMethod<R>>;
14
+ readonly api: TransformApiDefinition<InferTree<Reg>>;
15
+ readonly urlFor: UrlFor<RegistryGroupedByMethod<Routes>>;
14
16
  /**
15
17
  * Initializes the Tuyau client with provided configuration
16
18
  */
17
- constructor(config: TuyauConfiguration<R>);
19
+ constructor(config: TuyauConfiguration<Reg>);
18
20
  /**
19
21
  * Makes a GET request to the specified pattern
20
22
  */
21
- get<P extends PatternsByMethod<R, 'GET'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'GET', P>>): Promise<EndpointByMethodPattern<R, 'GET', P>['types']['response']>;
23
+ get<P extends PatternsByMethod<Routes, 'GET'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<Routes, 'GET', P>>): Promise<EndpointByMethodPattern<Routes, 'GET', P>['types']['response']>;
22
24
  /**
23
25
  * Makes a POST request to the specified pattern
24
26
  */
25
- post<P extends PatternsByMethod<R, 'POST'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'POST', P>>): Promise<EndpointByMethodPattern<R, 'POST', P>['types']['response']>;
27
+ post<P extends PatternsByMethod<Routes, 'POST'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<Routes, 'POST', P>>): Promise<EndpointByMethodPattern<Routes, 'POST', P>['types']['response']>;
26
28
  /**
27
29
  * Makes a PUT request to the specified pattern
28
30
  */
29
- put<P extends PatternsByMethod<R, 'PUT'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'PUT', P>>): Promise<EndpointByMethodPattern<R, 'PUT', P>['types']['response']>;
31
+ put<P extends PatternsByMethod<Routes, 'PUT'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<Routes, 'PUT', P>>): Promise<EndpointByMethodPattern<Routes, 'PUT', P>['types']['response']>;
30
32
  /**
31
33
  * Makes a PATCH request to the specified pattern
32
34
  */
33
- patch<P extends PatternsByMethod<R, 'PATCH'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'PATCH', P>>): Promise<EndpointByMethodPattern<R, 'PATCH', P>['types']['response']>;
35
+ patch<P extends PatternsByMethod<Routes, 'PATCH'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<Routes, 'PATCH', P>>): Promise<EndpointByMethodPattern<Routes, 'PATCH', P>['types']['response']>;
34
36
  /**
35
37
  * Makes a DELETE request to the specified pattern
36
38
  */
37
- delete<P extends PatternsByMethod<R, 'DELETE'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'DELETE', P>>): Promise<EndpointByMethodPattern<R, 'DELETE', P>['types']['response']>;
39
+ delete<P extends PatternsByMethod<Routes, 'DELETE'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<Routes, 'DELETE', P>>): Promise<EndpointByMethodPattern<Routes, 'DELETE', P>['types']['response']>;
38
40
  /**
39
41
  * Makes a HEAD request to the specified pattern
40
42
  */
41
- head<P extends PatternsByMethod<R, 'HEAD'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<R, 'HEAD', P>>): Promise<EndpointByMethodPattern<R, 'HEAD', P>['types']['response']>;
43
+ head<P extends PatternsByMethod<Routes, 'HEAD'>>(pattern: P, args: RequestArgs<EndpointByMethodPattern<Routes, 'HEAD', P>>): Promise<EndpointByMethodPattern<Routes, 'HEAD', P>['types']['response']>;
42
44
  /**
43
45
  * Makes a request to a named endpoint
44
46
  */
45
- request<Name extends StrKeys<R>>(name: Name, args: RequestArgs<R[Name]>): Promise<R[Name]['types']['response']>;
47
+ request<Name extends StrKeys<Routes>>(name: Name, args: RequestArgs<Routes[Name]>): Promise<Routes[Name]['types']['response']>;
46
48
  /**
47
49
  * Gets route information by name including URL and HTTP method
48
50
  */
49
- getRoute<Name extends StrKeys<R>>(name: Name, args: RequestArgs<R[Name]>): {
51
+ getRoute<Name extends StrKeys<Routes>>(name: Name, args: RequestArgs<Routes[Name]>): {
50
52
  url: string;
51
53
  methods: Method[];
52
54
  };
53
- /**
54
- * Creates a proxy-based fluent API for accessing endpoints by name
55
- */
56
- private makeNamed;
57
55
  }
58
56
  /**
59
57
  * Factory function to create a new Tuyau client instance
60
58
  */
61
- declare function createTuyau<R extends Record<string, AdonisEndpoint>>(config: TuyauConfiguration<R>): Tuyau<R>;
59
+ declare function createTuyau<Reg extends TuyauRegistry>(config: TuyauConfiguration<Reg>): Tuyau<Reg, InferRoutes<Reg>>;
62
60
 
63
61
  declare function parseResponse(response?: KyResponse): Promise<unknown>;
64
62
  declare class TuyauHTTPError extends Error {
@@ -1,6 +1,6 @@
1
1
  // src/client/tuyau.ts
2
- import ky, { HTTPError } from "ky";
3
2
  import { serialize } from "object-to-formdata";
3
+ import ky, { HTTPError } from "ky";
4
4
  import { createUrlBuilder } from "@adonisjs/http-server/client/url_builder";
5
5
 
6
6
  // src/client/errors.ts
@@ -33,6 +33,7 @@ var TuyauHTTPError = class extends Error {
33
33
  }
34
34
  };
35
35
  var TuyauNetworkError = class extends Error {
36
+ cause;
36
37
  constructor(cause, request) {
37
38
  const message = request ? `Network error: ${request.method.toUpperCase()} ${request.url}` : "Network error occurred";
38
39
  super(message, { cause });
@@ -76,32 +77,22 @@ function segmentsToRouteName(segments) {
76
77
 
77
78
  // src/client/tuyau.ts
78
79
  var Tuyau = class {
79
- /**
80
- * Initializes the Tuyau client with provided configuration
81
- */
82
- constructor(config) {
83
- this.config = config;
84
- this.api = this.makeNamed([]);
85
- this.#entries = Object.entries(this.config.registry);
86
- this.urlFor = this.#createUrlBuilder();
87
- this.#applyPlugins();
88
- this.#client = ky.create(this.#mergeKyConfiguration());
89
- }
90
80
  api;
91
81
  urlFor;
92
82
  #entries;
93
83
  #client;
84
+ #config;
94
85
  /**
95
86
  * Merges the default Ky configuration with user-provided config
96
87
  */
97
88
  #mergeKyConfiguration() {
98
89
  return {
99
- prefixUrl: this.config.baseUrl,
100
- ...this.config,
90
+ prefixUrl: this.#config.baseUrl,
91
+ ...this.#config,
101
92
  hooks: {
102
- ...this.config.hooks,
93
+ ...this.#config.hooks,
103
94
  beforeRequest: [
104
- ...this.config.hooks?.beforeRequest || [],
95
+ ...this.#config.hooks?.beforeRequest || [],
105
96
  this.#appendCsrfToken.bind(this)
106
97
  ]
107
98
  }
@@ -111,7 +102,7 @@ var Tuyau = class {
111
102
  * Applies registered plugins to the client configuration
112
103
  */
113
104
  #applyPlugins() {
114
- this.config.plugins?.forEach((plugin) => plugin({ options: this.config }));
105
+ this.#config.plugins?.forEach((plugin) => plugin({ options: this.#config }));
115
106
  }
116
107
  /**
117
108
  * Creates a URL builder instance for generating URLs based on the route registry
@@ -120,6 +111,17 @@ var Tuyau = class {
120
111
  const rootEntries = this.#entries.map(([name, entry]) => ({ name, domain: "root", ...entry }));
121
112
  return createUrlBuilder({ root: rootEntries }, buildSearchParams);
122
113
  }
114
+ /**
115
+ * Initializes the Tuyau client with provided configuration
116
+ */
117
+ constructor(config) {
118
+ this.#config = config;
119
+ this.api = this.#makeNamed([]);
120
+ this.#entries = Object.entries(this.#config.registry.routes);
121
+ this.urlFor = this.#createUrlBuilder();
122
+ this.#applyPlugins();
123
+ this.#client = ky.create(this.#mergeKyConfiguration());
124
+ }
123
125
  /**
124
126
  * Automatically appends CSRF token from cookies to requests
125
127
  */
@@ -254,14 +256,14 @@ var Tuyau = class {
254
256
  * Makes a request to a named endpoint
255
257
  */
256
258
  request(name, args) {
257
- const def = this.config.registry[name];
259
+ const def = this.#config.registry.routes[name];
258
260
  return this.#doFetch(name, def.methods[0], args);
259
261
  }
260
262
  /**
261
263
  * Gets route information by name including URL and HTTP method
262
264
  */
263
265
  getRoute(name, args) {
264
- const def = this.config.registry[name];
266
+ const def = this.#config.registry.routes[name];
265
267
  if (!def) throw new Error(`Route ${String(name)} not found`);
266
268
  const url = this.#buildUrl(name, def.methods[0], args);
267
269
  return { url, methods: def.methods };
@@ -269,17 +271,17 @@ var Tuyau = class {
269
271
  /**
270
272
  * Creates a proxy-based fluent API for accessing endpoints by name
271
273
  */
272
- makeNamed(segments) {
274
+ #makeNamed(segments) {
273
275
  const routeName = segmentsToRouteName(segments);
274
- const def = this.config.registry[routeName];
276
+ const def = this.#config.registry.routes[routeName];
275
277
  if (def) {
276
278
  const fn = (args) => this.#doFetch(routeName, def.methods[0], args);
277
279
  return new Proxy(fn, {
278
- get: (_t, prop) => this.makeNamed([...segments, String(prop)]),
280
+ get: (_t, prop) => this.#makeNamed([...segments, String(prop)]),
279
281
  apply: (_t, _this, argArray) => fn(...argArray)
280
282
  });
281
283
  }
282
- return new Proxy({}, { get: (_t, prop) => this.makeNamed([...segments, String(prop)]) });
284
+ return new Proxy({}, { get: (_t, prop) => this.#makeNamed([...segments, String(prop)]) });
283
285
  }
284
286
  };
285
287
  function createTuyau(config) {
@@ -6,97 +6,133 @@ import { ClientRouteMatchItTokens } from '@adonisjs/http-server/client/url_build
6
6
  */
7
7
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
8
8
  /**
9
- * Definition of an AdonisJS endpoint with types and metadata
9
+ * Base endpoint types structure
10
+ */
11
+ interface EndpointTypes {
12
+ paramsTuple: [...any[]];
13
+ params: Record<string, string | number | boolean>;
14
+ query: Record<string, any>;
15
+ body: unknown;
16
+ response: unknown;
17
+ }
18
+ /**
19
+ * Schema endpoint, used in generated registry.schema.d.ts
20
+ * Does not include tokens (only present in runtime routes)
10
21
  */
11
- interface AdonisEndpoint {
22
+ interface SchemaEndpoint {
12
23
  methods: Method[];
13
24
  pattern: string;
25
+ types: EndpointTypes;
26
+ }
27
+ /**
28
+ * Definition of an AdonisJS endpoint with types and metadata
29
+ * Includes tokens for runtime route building
30
+ */
31
+ interface AdonisEndpoint extends SchemaEndpoint {
14
32
  tokens: ClientRouteMatchItTokens[];
15
- types: {
16
- paramsTuple: [...any[]];
17
- params: Record<string, string | number | boolean>;
18
- query: Record<string, any>;
19
- body: unknown;
20
- response: unknown;
21
- };
22
33
  }
34
+ /**
35
+ * Extract query params from a validator type if it has a 'query' property.
36
+ * Used in generated registry to separate query params from body for POST/PUT/PATCH/DELETE.
37
+ * For GET/HEAD, the validator type is used directly as query.
38
+ */
39
+ type ExtractQuery<T> = T extends {
40
+ query: infer Q;
41
+ } ? (Q extends undefined ? {} : Q) : {};
42
+ /**
43
+ * Extract body from a validator type, excluding 'query' and 'params' properties.
44
+ * Used in generated registry to separate body from query/params for POST/PUT/PATCH/DELETE.
45
+ * Returns {} if the validator only contains query/params, otherwise returns the body fields.
46
+ */
47
+ type ExtractBody<T> = T extends {
48
+ query: any;
49
+ } | {
50
+ params: any;
51
+ } ? Omit<T, 'query' | 'params'> extends infer B ? keyof B extends never ? {} : B : {} : T;
23
52
  /**
24
53
  * Registry mapping endpoint names to their definitions
25
54
  */
26
55
  interface AdonisRegistry extends Record<string, AdonisEndpoint> {
27
56
  }
57
+ /**
58
+ * Should be augmented by the user to provide their API definition tree
59
+ * This is the pre-computed tree structure generated at build time
60
+ */
61
+ interface UserApiDefinition {
62
+ }
28
63
  type ValueOf<T> = T[keyof T];
29
- type IsEmptyObj<T> = keyof T extends never ? true : false;
30
- type IsEmptyTuple<T> = T extends [] ? true : false;
31
- type Endpoints = ValueOf<AdonisRegistry>;
32
- type EndpointByName<Name extends keyof AdonisRegistry & string> = AdonisRegistry[Name];
33
64
  /**
34
- * Checks if a type has any keys
65
+ * Split a string by a delimiter
35
66
  */
36
- type HasKeys<T> = keyof T extends never ? false : true;
67
+ type Split<S extends string, D extends string = '.'> = S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
37
68
  /**
38
- * Checks if a type has required keys (non-optional properties)
69
+ * Convert a union type to an intersection type
39
70
  */
40
- type HasRequiredKeys<T> = T extends Record<string, any> ? keyof T extends never ? false : {} extends Pick<T, keyof T> ? false : true : false;
71
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
41
72
  /**
42
- * Constructs the request arguments type for an endpoint
73
+ * Structure of a Tuyau registry containing routes and optional tree
43
74
  */
44
- type RequestArgs<E extends AdonisEndpoint> = (HasKeys<E['types']['params']> extends true ? HasRequiredKeys<E['types']['params']> extends true ? {
45
- params: E['types']['params'];
46
- } : {
47
- params?: E['types']['params'];
48
- } : {}) & (E['types']['query'] extends object ? HasRequiredKeys<E['types']['query']> extends true ? {
49
- query: E['types']['query'];
50
- } : {
51
- query?: E['types']['query'];
52
- } : {}) & (E['types']['body'] extends never | undefined ? {} : HasRequiredKeys<E['types']['body']> extends true ? {
53
- body: E['types']['body'];
54
- } : {
55
- body?: E['types']['body'];
56
- }) & Omit<Options, 'body' | 'params' | 'searchParams' | 'method' | 'json' | 'prefixUrl'>;
75
+ interface TuyauRegistry<Routes extends Record<string, AdonisEndpoint> = Record<string, AdonisEndpoint>, Tree = unknown> {
76
+ routes: Routes;
77
+ $tree?: Tree;
78
+ }
57
79
  /**
58
- * Extracts response type from an endpoint
80
+ * Extracts the $tree type from a registry
81
+ * Uses direct access instead of conditional type for performance
59
82
  */
60
- type ResponseOf<E extends AdonisEndpoint> = E['types']['response'];
83
+ type InferTree<R extends TuyauRegistry> = R['$tree'];
61
84
  /**
62
- * Splits a dot-separated string into an array of strings
85
+ * Extracts the routes from a registry
63
86
  */
64
- type Split<S extends string> = S extends `${infer H}.${infer T}` ? [H, ...Split<T>] : [S];
87
+ type InferRoutes<R extends TuyauRegistry> = R['routes'];
88
+ type Endpoints = ValueOf<AdonisRegistry>;
89
+ type EndpointByName<Name extends keyof AdonisRegistry & string> = AdonisRegistry[Name];
65
90
  /**
66
- * Converts a string to camelCase
91
+ * Pre-computed base Ky options to avoid recomputing Omit on every request
67
92
  */
68
- type CamelCase<S extends string> = S extends `${infer H}${infer T}` ? `${Lowercase<H>}${CamelCaseRest<T>}` : S;
69
- type CamelCaseRest<S extends string> = S extends `_${infer H}${infer T}` ? `${Uppercase<H>}${CamelCaseRest<T>}` : S extends `${infer H}${infer T}` ? `${H}${CamelCaseRest<T>}` : S;
93
+ type BaseRequestOptions = Omit<Options, 'body' | 'params' | 'searchParams' | 'method' | 'json' | 'prefixUrl'>;
70
94
  /**
71
- * Applies camelCase to each segment in a split array
95
+ * Helper types for optional/required fields - using literal types instead of mapped types
72
96
  */
73
- type CamelCaseSplit<T extends string[]> = T extends [
74
- infer H extends string,
75
- ...infer Rest extends string[]
76
- ] ? [CamelCase<H>, ...CamelCaseSplit<Rest>] : [];
97
+ type ParamsArg<T> = keyof T extends never ? {} : {} extends T ? {
98
+ params?: T;
99
+ } : {
100
+ params: T;
101
+ };
102
+ type QueryArg<T> = keyof T extends never ? {} : {} extends T ? {
103
+ query?: T;
104
+ } : {
105
+ query: T;
106
+ };
107
+ type BodyArg<T> = keyof T extends never ? {} : {} extends T ? {
108
+ body?: T;
109
+ } : {
110
+ body: T;
111
+ };
77
112
  /**
78
- * Converts a union type to an intersection type
113
+ * Request args without ky options
79
114
  */
80
- type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
115
+ type RawRequestArgs<E extends SchemaEndpoint> = ParamsArg<E['types']['params']> & QueryArg<E['types']['query']> & BodyArg<E['types']['body']>;
81
116
  /**
82
- * Builds a nested object type for the fluent API based on endpoint names
117
+ * Constructs the request arguments type for an endpoint
83
118
  */
84
- type BuildNamed<Reg extends Record<string, AdonisEndpoint>> = UnionToIntersection<{
85
- [K in keyof Reg & string]: SetAtPath<CamelCaseSplit<Split<K>>, EndpointFn<Reg[K]>>;
86
- }[keyof Reg & string]>;
119
+ type RequestArgs<E extends SchemaEndpoint> = RawRequestArgs<E> & BaseRequestOptions;
87
120
  /**
88
- * Sets a value at a specific path in a nested object type
121
+ * Extracts response type from an endpoint
89
122
  */
90
- type SetAtPath<Path extends string[], V> = Path extends [
91
- infer H extends string,
92
- ...infer T extends string[]
93
- ] ? {
94
- [K in H]: T['length'] extends 0 ? V : SetAtPath<T, V>;
95
- } : {};
123
+ type ResponseOf<E extends SchemaEndpoint> = E['types']['response'];
96
124
  /**
97
125
  * Function type for calling an endpoint
98
126
  */
99
- type EndpointFn<E extends AdonisEndpoint> = (args: RequestArgs<E>) => Promise<E['types']['response']>;
127
+ type EndpointFn<E extends SchemaEndpoint> = (args: RequestArgs<E>) => Promise<E['types']['response']>;
128
+ /**
129
+ * Transforms a pre-computed ApiDefinition tree into callable endpoint functions
130
+ * This recursively converts each endpoint in the tree to a callable function
131
+ * Fully inlined for maximum performance
132
+ */
133
+ type TransformApiDefinition<T> = {
134
+ [K in keyof T]: T[K] extends AdonisEndpoint ? (args: ParamsArg<T[K]['types']['params']> & QueryArg<T[K]['types']['query']> & BodyArg<T[K]['types']['body']> & BaseRequestOptions) => Promise<T[K]['types']['response']> : TransformApiDefinition<T[K]>;
135
+ };
100
136
  /**
101
137
  * Filters endpoints by HTTP method
102
138
  */
@@ -130,7 +166,7 @@ interface QueryParameters extends Record<string, MaybeArray<string | number | bo
130
166
  /**
131
167
  * Configuration options for creating a Tuyau client
132
168
  */
133
- interface TuyauConfiguration<T extends Record<string, AdonisEndpoint>> extends Omit<Options, 'prefixUrl' | 'body' | 'json' | 'method' | 'searchParams'> {
169
+ interface TuyauConfiguration<T extends TuyauRegistry> extends Omit<Options, 'prefixUrl' | 'body' | 'json' | 'method' | 'searchParams'> {
134
170
  registry: T;
135
171
  baseUrl: string;
136
172
  plugins?: TuyauPlugin[];
@@ -143,7 +179,7 @@ interface UserRegistry {
143
179
  type UserAdonisRegistry = UserRegistry extends Record<string, AdonisEndpoint> ? UserRegistry : Record<string, AdonisEndpoint>;
144
180
  type UserEndpointByName<Name extends keyof UserAdonisRegistry> = UserAdonisRegistry[Name];
145
181
  type FilterByMethodPathForRegistry<Reg extends Record<string, AdonisEndpoint>, M extends Method, P extends ValueOf<Reg>['pattern'] & string> = {
146
- [K in keyof Reg]: [Reg[K]['pattern'], M] extends [P, Reg[K]['methods'][number]] ? Reg[K] : never;
182
+ [K in keyof Reg]: Reg[K]['pattern'] extends P ? M extends Reg[K]['methods'][number] ? Reg[K] : never : never;
147
183
  }[keyof Reg];
148
184
  type EndpointByNameForRegistry<Reg extends Record<string, AdonisEndpoint>, Name extends keyof Reg> = Reg[Name];
149
185
  /**
@@ -195,11 +231,11 @@ type ParamsShape<E> = E extends {
195
231
  paramsTuple: infer PT;
196
232
  params: infer P;
197
233
  };
198
- } ? (IsEmptyTuple<PT> extends true ? {
234
+ } ? (PT extends [] ? {
199
235
  paramsTuple?: PT;
200
236
  } : {
201
237
  paramsTuple: PT;
202
- }) & (IsEmptyObj<P> extends true ? {} : {
238
+ }) & (keyof P extends never ? {} : {
203
239
  params: P;
204
240
  }) : never;
205
241
  type RegistryGroupedByMethod<R extends Record<string, AdonisEndpoint>, M extends Method = Method> = {
@@ -208,4 +244,4 @@ type RegistryGroupedByMethod<R extends Record<string, AdonisEndpoint>, M extends
208
244
  };
209
245
  };
210
246
 
211
- export { type AdonisEndpoint, type AdonisRegistry, type BuildNamed, type CamelCase, type CamelCaseSplit, type EndpointByMethodPattern, type EndpointByName, type Endpoints, type EndpointsByMethod, type HasKeys, type HasRequiredKeys, type MaybeArray, type Method, Path, PathWithRegistry, type PatternsByMethod, type QueryParameters, type RegValues, type RegistryGroupedByMethod, type RequestArgs, type ResponseOf, Route, RouteWithRegistry, type SetAtPath, type Split, type StrKeys, type TuyauConfiguration, type TuyauPlugin, type UnionToIntersection, type UserRegistry, type ValueOf };
247
+ export { type AdonisEndpoint, type AdonisRegistry, type EndpointByMethodPattern, type EndpointByName, type EndpointFn, type EndpointTypes, type Endpoints, type EndpointsByMethod, type ExtractBody, type ExtractQuery, type InferRoutes, type InferTree, type MaybeArray, type Method, Path, PathWithRegistry, type PatternsByMethod, type QueryParameters, type RawRequestArgs, type RegValues, type RegistryGroupedByMethod, type RequestArgs, type ResponseOf, Route, RouteWithRegistry, type SchemaEndpoint, type Split, type StrKeys, type TransformApiDefinition, type TuyauConfiguration, type TuyauPlugin, type TuyauRegistry, type UnionToIntersection, type UserApiDefinition, type UserRegistry, type ValueOf };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuyau/core",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.2",
4
+ "version": "1.0.0-beta.4",
5
5
  "description": "e2e client for AdonisJS",
6
6
  "author": "Julien Ripouteau <julien@ripouteau.com>",
7
7
  "license": "MIT",
@@ -34,7 +34,6 @@
34
34
  "@adonisjs/assembler": "8.0.0-next.19",
35
35
  "@adonisjs/core": "^7.0.0-next.10",
36
36
  "@adonisjs/http-server": "8.0.0-next.12",
37
- "@arktype/attest": "^0.53.0",
38
37
  "@faker-js/faker": "^10.1.0",
39
38
  "@poppinss/ts-exec": "^1.4.1",
40
39
  "@types/node": "^24.10.0",