@spfn/core 0.1.0-alpha.8 → 0.1.0-alpha.82

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.
Files changed (61) hide show
  1. package/README.md +169 -195
  2. package/dist/auto-loader-JFaZ9gON.d.ts +80 -0
  3. package/dist/cache/index.d.ts +211 -0
  4. package/dist/cache/index.js +1013 -0
  5. package/dist/cache/index.js.map +1 -0
  6. package/dist/client/index.d.ts +131 -92
  7. package/dist/client/index.js +93 -85
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/codegen/generators/index.d.ts +19 -0
  10. package/dist/codegen/generators/index.js +1521 -0
  11. package/dist/codegen/generators/index.js.map +1 -0
  12. package/dist/codegen/index.d.ts +76 -60
  13. package/dist/codegen/index.js +1506 -735
  14. package/dist/codegen/index.js.map +1 -1
  15. package/dist/database-errors-BNNmLTJE.d.ts +86 -0
  16. package/dist/db/index.d.ts +844 -44
  17. package/dist/db/index.js +1281 -1307
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/env/index.d.ts +508 -0
  20. package/dist/env/index.js +1127 -0
  21. package/dist/env/index.js.map +1 -0
  22. package/dist/errors/index.d.ts +136 -0
  23. package/dist/errors/index.js +172 -0
  24. package/dist/errors/index.js.map +1 -0
  25. package/dist/index-DHiAqhKv.d.ts +101 -0
  26. package/dist/index.d.ts +3 -374
  27. package/dist/index.js +2424 -2178
  28. package/dist/index.js.map +1 -1
  29. package/dist/logger/index.d.ts +94 -0
  30. package/dist/logger/index.js +795 -0
  31. package/dist/logger/index.js.map +1 -0
  32. package/dist/middleware/index.d.ts +60 -0
  33. package/dist/middleware/index.js +918 -0
  34. package/dist/middleware/index.js.map +1 -0
  35. package/dist/route/index.d.ts +21 -53
  36. package/dist/route/index.js +1259 -219
  37. package/dist/route/index.js.map +1 -1
  38. package/dist/server/index.d.ts +18 -0
  39. package/dist/server/index.js +2419 -2059
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/types/index.d.ts +121 -0
  42. package/dist/types/index.js +38 -0
  43. package/dist/types/index.js.map +1 -0
  44. package/dist/types-BXibIEyj.d.ts +60 -0
  45. package/package.json +67 -17
  46. package/dist/auto-loader-C44TcLmM.d.ts +0 -125
  47. package/dist/bind-pssq1NRT.d.ts +0 -34
  48. package/dist/postgres-errors-CY_Es8EJ.d.ts +0 -1703
  49. package/dist/scripts/index.d.ts +0 -24
  50. package/dist/scripts/index.js +0 -1201
  51. package/dist/scripts/index.js.map +0 -1
  52. package/dist/scripts/templates/api-index.template.txt +0 -10
  53. package/dist/scripts/templates/api-tag.template.txt +0 -11
  54. package/dist/scripts/templates/contract.template.txt +0 -87
  55. package/dist/scripts/templates/entity-type.template.txt +0 -31
  56. package/dist/scripts/templates/entity.template.txt +0 -19
  57. package/dist/scripts/templates/index.template.txt +0 -10
  58. package/dist/scripts/templates/repository.template.txt +0 -37
  59. package/dist/scripts/templates/routes-id.template.txt +0 -59
  60. package/dist/scripts/templates/routes-index.template.txt +0 -44
  61. package/dist/types-SlzTr8ZO.d.ts +0 -143
@@ -1,72 +1,27 @@
1
1
  // src/client/contract-client.ts
2
2
  var ApiClientError = class extends Error {
3
- constructor(message, status, url, response) {
3
+ constructor(message, status, url, response, errorType) {
4
4
  super(message);
5
5
  this.status = status;
6
6
  this.url = url;
7
7
  this.response = response;
8
+ this.errorType = errorType;
8
9
  this.name = "ApiClientError";
9
10
  }
10
11
  };
11
- function buildUrl(path, params) {
12
- if (!params) return path;
13
- let url = path;
14
- for (const [key, value] of Object.entries(params)) {
15
- url = url.replace(`:${key}`, String(value));
16
- }
17
- return url;
18
- }
19
- function buildQuery(query) {
20
- if (!query || Object.keys(query).length === 0) return "";
21
- const params = new URLSearchParams();
22
- for (const [key, value] of Object.entries(query)) {
23
- if (Array.isArray(value)) {
24
- value.forEach((v) => params.append(key, String(v)));
25
- } else if (value !== void 0 && value !== null) {
26
- params.append(key, String(value));
27
- }
28
- }
29
- const queryString = params.toString();
30
- return queryString ? `?${queryString}` : "";
31
- }
32
- function getHttpMethod(contract, options) {
33
- if ("method" in contract && typeof contract.method === "string") {
34
- return contract.method.toUpperCase();
35
- }
36
- if (options?.body !== void 0) {
37
- return "POST";
38
- }
39
- return "GET";
40
- }
41
12
  var ContractClient = class _ContractClient {
42
13
  config;
43
14
  interceptors = [];
44
15
  constructor(config = {}) {
45
16
  this.config = {
46
- baseUrl: config.baseUrl || process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000",
17
+ baseUrl: config.baseUrl || process.env.SERVER_API_URL || process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000",
47
18
  headers: config.headers || {},
48
19
  timeout: config.timeout || 3e4,
49
- fetch: config.fetch || globalThis.fetch
20
+ fetch: config.fetch || globalThis.fetch.bind(globalThis)
50
21
  };
51
22
  }
52
23
  /**
53
24
  * Add request interceptor
54
- *
55
- * Interceptors are executed in the order they are added
56
- *
57
- * @example
58
- * ```ts
59
- * client.use(async (url, init) => {
60
- * // Add auth header
61
- * return {
62
- * ...init,
63
- * headers: {
64
- * ...init.headers,
65
- * Authorization: `Bearer ${token}`
66
- * }
67
- * };
68
- * });
69
- * ```
70
25
  */
71
26
  use(interceptor) {
72
27
  this.interceptors.push(interceptor);
@@ -74,30 +29,26 @@ var ContractClient = class _ContractClient {
74
29
  /**
75
30
  * Make a type-safe API call using a contract
76
31
  *
77
- * @example
78
- * ```ts
79
- * const getUserContract = {
80
- * params: Type.Object({ id: Type.String() }),
81
- * response: Type.Object({ id: Type.Number(), name: Type.String() })
82
- * } as const satisfies RouteContract;
83
- *
84
- * const user = await client.call('/users/:id', getUserContract, {
85
- * params: { id: '123' }
86
- * });
87
- * // ✅ user.name is typed as string
88
- * ```
32
+ * @param contract - Route contract with absolute path
33
+ * @param options - Call options (params, query, body, headers)
89
34
  */
90
- async call(path, contract, options) {
35
+ async call(contract, options) {
91
36
  const baseUrl = options?.baseUrl || this.config.baseUrl;
92
- const urlPath = buildUrl(path, options?.params);
93
- const queryString = buildQuery(options?.query);
37
+ const urlPath = _ContractClient.buildUrl(
38
+ contract.path,
39
+ options?.params
40
+ );
41
+ const queryString = _ContractClient.buildQuery(
42
+ options?.query
43
+ );
94
44
  const url = `${baseUrl}${urlPath}${queryString}`;
95
- const method = getHttpMethod(contract, options);
45
+ const method = _ContractClient.getHttpMethod(contract, options);
96
46
  const headers = {
97
47
  ...this.config.headers,
98
48
  ...options?.headers
99
49
  };
100
- if (options?.body !== void 0 && !headers["Content-Type"]) {
50
+ const isFormData = _ContractClient.isFormData(options?.body);
51
+ if (options?.body !== void 0 && !isFormData && !headers["Content-Type"]) {
101
52
  headers["Content-Type"] = "application/json";
102
53
  }
103
54
  let init = {
@@ -105,7 +56,7 @@ var ContractClient = class _ContractClient {
105
56
  headers
106
57
  };
107
58
  if (options?.body !== void 0) {
108
- init.body = JSON.stringify(options.body);
59
+ init.body = isFormData ? options.body : JSON.stringify(options.body);
109
60
  }
110
61
  const controller = new AbortController();
111
62
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -117,18 +68,20 @@ var ContractClient = class _ContractClient {
117
68
  clearTimeout(timeoutId);
118
69
  if (error instanceof Error && error.name === "AbortError") {
119
70
  throw new ApiClientError(
120
- `${method} ${urlPath} timed out after ${this.config.timeout}ms`,
71
+ `Request timed out after ${this.config.timeout}ms`,
121
72
  0,
122
- "Timeout",
123
- url
73
+ url,
74
+ void 0,
75
+ "timeout"
124
76
  );
125
77
  }
126
78
  if (error instanceof Error) {
127
79
  throw new ApiClientError(
128
- `${method} ${urlPath} network error: ${error.message}`,
80
+ `Network error: ${error.message}`,
129
81
  0,
130
- "Network Error",
131
- url
82
+ url,
83
+ void 0,
84
+ "network"
132
85
  );
133
86
  }
134
87
  throw error;
@@ -140,7 +93,8 @@ var ContractClient = class _ContractClient {
140
93
  `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,
141
94
  response.status,
142
95
  url,
143
- errorBody
96
+ errorBody,
97
+ "http"
144
98
  );
145
99
  }
146
100
  const data = await response.json();
@@ -148,15 +102,6 @@ var ContractClient = class _ContractClient {
148
102
  }
149
103
  /**
150
104
  * Create a new client with merged configuration
151
- *
152
- * Useful for creating clients with specific auth tokens or custom headers
153
- *
154
- * @example
155
- * ```ts
156
- * const authClient = client.withConfig({
157
- * headers: { Authorization: `Bearer ${token}` }
158
- * });
159
- * ```
160
105
  */
161
106
  withConfig(config) {
162
107
  return new _ContractClient({
@@ -166,12 +111,75 @@ var ContractClient = class _ContractClient {
166
111
  fetch: config.fetch || this.config.fetch
167
112
  });
168
113
  }
114
+ static buildUrl(path, params) {
115
+ if (!params) return path;
116
+ let url = path;
117
+ for (const [key, value] of Object.entries(params)) {
118
+ url = url.replace(`:${key}`, String(value));
119
+ }
120
+ return url;
121
+ }
122
+ static buildQuery(query) {
123
+ if (!query || Object.keys(query).length === 0) return "";
124
+ const params = new URLSearchParams();
125
+ for (const [key, value] of Object.entries(query)) {
126
+ if (Array.isArray(value)) {
127
+ value.forEach((v) => params.append(key, String(v)));
128
+ } else if (value !== void 0 && value !== null) {
129
+ params.append(key, String(value));
130
+ }
131
+ }
132
+ const queryString = params.toString();
133
+ return queryString ? `?${queryString}` : "";
134
+ }
135
+ static getHttpMethod(contract, options) {
136
+ if ("method" in contract && typeof contract.method === "string") {
137
+ return contract.method.toUpperCase();
138
+ }
139
+ if (options?.body !== void 0) {
140
+ return "POST";
141
+ }
142
+ return "GET";
143
+ }
144
+ static isFormData(body) {
145
+ return body instanceof FormData;
146
+ }
169
147
  };
170
148
  function createClient(config) {
171
149
  return new ContractClient(config);
172
150
  }
173
- var client = createClient();
151
+ var _clientInstance = new ContractClient();
152
+ function configureClient(config) {
153
+ _clientInstance = new ContractClient(config);
154
+ }
155
+ var client = new Proxy({}, {
156
+ get(_target, prop) {
157
+ return _clientInstance[prop];
158
+ }
159
+ });
160
+ function isTimeoutError(error) {
161
+ return error instanceof ApiClientError && error.errorType === "timeout";
162
+ }
163
+ function isNetworkError(error) {
164
+ return error instanceof ApiClientError && error.errorType === "network";
165
+ }
166
+ function isHttpError(error) {
167
+ return error instanceof ApiClientError && error.errorType === "http";
168
+ }
169
+ function isServerError(error, errorType) {
170
+ if (!isHttpError(error)) return false;
171
+ const response = error.response;
172
+ return response?.error?.type === errorType;
173
+ }
174
+ function getServerErrorType(error) {
175
+ const response = error.response;
176
+ return response?.error?.type;
177
+ }
178
+ function getServerErrorDetails(error) {
179
+ const response = error.response;
180
+ return response?.error?.details;
181
+ }
174
182
 
175
- export { ApiClientError, ContractClient, client, createClient };
183
+ export { ApiClientError, ContractClient, client, configureClient, createClient, getServerErrorDetails, getServerErrorType, isHttpError, isNetworkError, isServerError, isTimeoutError };
176
184
  //# sourceMappingURL=index.js.map
177
185
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/contract-client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Contract-Based API Client\n *\n * Type-safe HTTP client that works with RouteContract for full end-to-end type safety\n *\n * @example\n * ```ts\n * import { createClient } from '@spfn/core/client';\n * import { getUserContract } from './contracts';\n *\n * const client = createClient({ baseUrl: 'http://localhost:4000' });\n * const user = await client.call(getUserContract, { params: { id: '123' } });\n * // ✅ user is fully typed based on contract.response\n * ```\n */\n\nimport type { RouteContract, InferContract } from '../route';\n\n/**\n * Request interceptor function\n *\n * Allows modifying request before it's sent\n */\nexport type RequestInterceptor = (\n url: string,\n init: RequestInit\n) => Promise<RequestInit> | RequestInit;\n\n/**\n * Client configuration\n */\nexport interface ClientConfig\n{\n /**\n * API base URL (e.g., http://localhost:4000)\n * Can be overridden per request\n */\n baseUrl?: string;\n\n /**\n * Default headers to include in all requests\n */\n headers?: Record<string, string>;\n\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Custom fetch implementation (for testing or custom behavior)\n */\n fetch?: typeof fetch;\n}\n\n/**\n * Request options for API calls\n */\nexport interface CallOptions<TContract extends RouteContract>\n{\n /**\n * Path parameters (for dynamic routes like /users/:id)\n */\n params?: InferContract<TContract>['params'];\n\n /**\n * Query parameters (for URL query strings)\n */\n query?: InferContract<TContract>['query'];\n\n /**\n * Request body (for POST, PUT, PATCH)\n */\n body?: InferContract<TContract>['body'];\n\n /**\n * Additional headers for this specific request\n */\n headers?: Record<string, string>;\n\n /**\n * Override base URL for this request\n */\n baseUrl?: string;\n}\n\n/**\n * API Client Error\n */\nexport class ApiClientError extends Error\n{\n constructor(\n message: string,\n public readonly status: number,\n public readonly url: string,\n public readonly response?: unknown\n )\n {\n super(message);\n this.name = 'ApiClientError';\n }\n}\n\n/**\n * Build URL with path parameters replaced\n *\n * @example\n * buildUrl('/users/:id', { id: '123' }) → '/users/123'\n * buildUrl('/posts/:postId/comments/:id', { postId: '1', id: '2' }) → '/posts/1/comments/2'\n */\nfunction buildUrl(path: string, params?: Record<string, string | number>): string\n{\n if (!params) return path;\n\n let url = path;\n for (const [key, value] of Object.entries(params))\n {\n url = url.replace(`:${key}`, String(value));\n }\n\n return url;\n}\n\n/**\n * Build query string from object\n *\n * @example\n * buildQuery({ page: '1', limit: '10' }) → '?page=1&limit=10'\n */\nfunction buildQuery(query?: Record<string, string | string[] | number | boolean>): string\n{\n if (!query || Object.keys(query).length === 0) return '';\n\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query))\n {\n if (Array.isArray(value))\n {\n value.forEach((v) => params.append(key, String(v)));\n }\n else if (value !== undefined && value !== null)\n {\n params.append(key, String(value));\n }\n }\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : '';\n}\n\n/**\n * Extract HTTP method from contract or infer from request type\n */\nfunction getHttpMethod<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n): string\n{\n // If contract has explicit method, use it\n if ('method' in contract && typeof contract.method === 'string')\n {\n return contract.method.toUpperCase();\n }\n\n // Infer from presence of body\n if (options?.body !== undefined)\n {\n return 'POST';\n }\n\n // Default to GET\n return 'GET';\n}\n\n/**\n * Contract-based API Client\n */\nexport class ContractClient\n{\n private readonly config: Required<ClientConfig>;\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(config: ClientConfig = {})\n {\n this.config = {\n baseUrl: config.baseUrl || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n headers: config.headers || {},\n timeout: config.timeout || 30000,\n fetch: config.fetch || globalThis.fetch,\n };\n }\n\n /**\n * Add request interceptor\n *\n * Interceptors are executed in the order they are added\n *\n * @example\n * ```ts\n * client.use(async (url, init) => {\n * // Add auth header\n * return {\n * ...init,\n * headers: {\n * ...init.headers,\n * Authorization: `Bearer ${token}`\n * }\n * };\n * });\n * ```\n */\n use(interceptor: RequestInterceptor): void\n {\n this.interceptors.push(interceptor);\n }\n\n /**\n * Make a type-safe API call using a contract\n *\n * @example\n * ```ts\n * const getUserContract = {\n * params: Type.Object({ id: Type.String() }),\n * response: Type.Object({ id: Type.Number(), name: Type.String() })\n * } as const satisfies RouteContract;\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * // ✅ user.name is typed as string\n * ```\n */\n async call<TContract extends RouteContract>(\n path: string,\n contract: TContract,\n options?: CallOptions<TContract>\n ): Promise<InferContract<TContract>['response']>\n {\n // Build URL\n const baseUrl = options?.baseUrl || this.config.baseUrl;\n const urlPath = buildUrl(path, options?.params as Record<string, string | number>);\n const queryString = buildQuery(options?.query as Record<string, string | string[] | number | boolean>);\n const url = `${baseUrl}${urlPath}${queryString}`;\n\n // Determine HTTP method\n const method = getHttpMethod(contract, options);\n\n // Build headers\n const headers: Record<string, string> = {\n ...this.config.headers,\n ...options?.headers,\n };\n\n // Add Content-Type for requests with body\n if (options?.body !== undefined && !headers['Content-Type'])\n {\n headers['Content-Type'] = 'application/json';\n }\n\n // Build request init\n let init: RequestInit = {\n method,\n headers,\n };\n\n // Add body for POST/PUT/PATCH\n if (options?.body !== undefined)\n {\n init.body = JSON.stringify(options.body);\n }\n\n // Create abort controller for timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n init.signal = controller.signal;\n\n // Execute interceptors\n for (const interceptor of this.interceptors)\n {\n init = await interceptor(url, init);\n }\n\n // Make request\n const response = await this.config.fetch(url, init).catch((error) =>\n {\n clearTimeout(timeoutId);\n\n // Handle abort (timeout)\n if (error instanceof Error && error.name === 'AbortError')\n {\n throw new ApiClientError(\n `${method} ${urlPath} timed out after ${this.config.timeout}ms`,\n 0,\n 'Timeout',\n url\n );\n }\n\n // Handle network errors\n if (error instanceof Error)\n {\n throw new ApiClientError(\n `${method} ${urlPath} network error: ${error.message}`,\n 0,\n 'Network Error',\n url\n );\n }\n\n // Unknown error\n throw error;\n });\n\n // Clear timeout\n clearTimeout(timeoutId);\n\n // Handle non-OK responses\n if (!response.ok)\n {\n const errorBody = await response.json().catch(() => null);\n throw new ApiClientError(\n `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,\n response.status,\n url,\n errorBody\n );\n }\n\n // Parse and return response\n const data = await response.json();\n return data as InferContract<TContract>['response'];\n }\n\n /**\n * Create a new client with merged configuration\n *\n * Useful for creating clients with specific auth tokens or custom headers\n *\n * @example\n * ```ts\n * const authClient = client.withConfig({\n * headers: { Authorization: `Bearer ${token}` }\n * });\n * ```\n */\n withConfig(config: Partial<ClientConfig>): ContractClient\n {\n return new ContractClient({\n baseUrl: config.baseUrl || this.config.baseUrl,\n headers: { ...this.config.headers, ...config.headers },\n timeout: config.timeout || this.config.timeout,\n fetch: config.fetch || this.config.fetch,\n });\n }\n}\n\n/**\n * Create a new contract-based API client\n *\n * @example\n * ```ts\n * const client = createClient({\n * baseUrl: 'http://localhost:4000',\n * headers: { 'X-Custom': 'header' }\n * });\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * ```\n */\nexport function createClient(config?: ClientConfig): ContractClient\n{\n return new ContractClient(config);\n}\n\n/**\n * Default client instance\n *\n * @example\n * ```ts\n * import { client } from '@spfn/core/client';\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * ```\n */\nexport const client = createClient();"]}
1
+ {"version":3,"sources":["../../src/client/contract-client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Contract-Based API Client\n *\n * Type-safe HTTP client that works with RouteContract for full end-to-end type safety\n */\nimport type { RouteContract, InferContract } from '../route';\n\nexport type RequestInterceptor = (\n url: string,\n init: RequestInit\n) => Promise<RequestInit> | RequestInit;\n\nexport interface ClientConfig\n{\n /**\n * API base URL (e.g., http://localhost:4000)\n */\n baseUrl?: string;\n\n /**\n * Default headers to include in all requests\n */\n headers?: Record<string, string>;\n\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Custom fetch implementation\n */\n fetch?: typeof fetch;\n}\n\nexport interface CallOptions<TContract extends RouteContract>\n{\n params?: InferContract<TContract>['params'];\n query?: InferContract<TContract>['query'];\n body?: InferContract<TContract>['body'];\n headers?: Record<string, string>;\n baseUrl?: string;\n}\n\n/**\n * API Client Error\n */\nexport class ApiClientError extends Error\n{\n constructor(\n message: string,\n public readonly status: number,\n public readonly url: string,\n public readonly response?: unknown,\n public readonly errorType?: 'timeout' | 'network' | 'http'\n )\n {\n super(message);\n this.name = 'ApiClientError';\n }\n}\n\n/**\n * Contract-based API Client\n */\nexport class ContractClient\n{\n private readonly config: Required<ClientConfig>;\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(config: ClientConfig = {})\n {\n this.config = {\n baseUrl: config.baseUrl || process.env.SERVER_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n headers: config.headers || {},\n timeout: config.timeout || 30000,\n fetch: config.fetch || globalThis.fetch.bind(globalThis),\n };\n }\n\n /**\n * Add request interceptor\n */\n use(interceptor: RequestInterceptor): void\n {\n this.interceptors.push(interceptor);\n }\n\n /**\n * Make a type-safe API call using a contract\n *\n * @param contract - Route contract with absolute path\n * @param options - Call options (params, query, body, headers)\n */\n async call<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): Promise<InferContract<TContract>['response']>\n {\n const baseUrl = options?.baseUrl || this.config.baseUrl;\n\n // Use contract.path directly (contracts use absolute paths)\n const urlPath = ContractClient.buildUrl(\n contract.path,\n options?.params as Record<string, string | number> | undefined\n );\n\n const queryString = ContractClient.buildQuery(\n options?.query as Record<string, string | string[] | number | boolean> | undefined\n );\n\n const url = `${baseUrl}${urlPath}${queryString}`;\n\n const method = ContractClient.getHttpMethod(contract, options);\n\n const headers: Record<string, string> = {\n ...this.config.headers,\n ...options?.headers,\n };\n\n const isFormData = ContractClient.isFormData(options?.body);\n\n if (options?.body !== undefined && !isFormData && !headers['Content-Type'])\n {\n headers['Content-Type'] = 'application/json';\n }\n\n let init: RequestInit = {\n method,\n headers,\n };\n\n if (options?.body !== undefined)\n {\n init.body = isFormData ? (options.body as FormData) : JSON.stringify(options.body);\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n init.signal = controller.signal;\n\n for (const interceptor of this.interceptors)\n {\n init = await interceptor(url, init);\n }\n\n const response = await this.config.fetch(url, init).catch((error) =>\n {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name === 'AbortError')\n {\n throw new ApiClientError(\n `Request timed out after ${this.config.timeout}ms`,\n 0,\n url,\n undefined,\n 'timeout'\n );\n }\n\n if (error instanceof Error)\n {\n throw new ApiClientError(\n `Network error: ${error.message}`,\n 0,\n url,\n undefined,\n 'network'\n );\n }\n\n throw error;\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok)\n {\n const errorBody = await response.json().catch(() => null);\n throw new ApiClientError(\n `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,\n response.status,\n url,\n errorBody,\n 'http'\n );\n }\n\n const data = await response.json();\n return data as InferContract<TContract>['response'];\n }\n\n /**\n * Create a new client with merged configuration\n */\n withConfig(config: Partial<ClientConfig>): ContractClient\n {\n return new ContractClient({\n baseUrl: config.baseUrl || this.config.baseUrl,\n headers: { ...this.config.headers, ...config.headers },\n timeout: config.timeout || this.config.timeout,\n fetch: config.fetch || this.config.fetch,\n });\n }\n\n private static buildUrl(path: string, params?: Record<string, string | number>): string\n {\n if (!params) return path;\n\n let url = path;\n for (const [key, value] of Object.entries(params))\n {\n url = url.replace(`:${key}`, String(value));\n }\n\n return url;\n }\n\n private static buildQuery(query?: Record<string, string | string[] | number | boolean>): string\n {\n if (!query || Object.keys(query).length === 0) return '';\n\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query))\n {\n if (Array.isArray(value))\n {\n value.forEach((v) => params.append(key, String(v)));\n }\n else if (value !== undefined && value !== null)\n {\n params.append(key, String(value));\n }\n }\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : '';\n }\n\n private static getHttpMethod<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n ): string\n {\n if ('method' in contract && typeof contract.method === 'string')\n {\n return contract.method.toUpperCase();\n }\n\n if (options?.body !== undefined)\n {\n return 'POST';\n }\n\n return 'GET';\n }\n\n private static isFormData(body: unknown): body is FormData\n {\n return body instanceof FormData;\n }\n}\n\n/**\n * Create a new contract-based API client\n */\nexport function createClient(config?: ClientConfig): ContractClient\n{\n return new ContractClient(config);\n}\n\n/**\n * Global client singleton instance\n */\nlet _clientInstance: ContractClient = new ContractClient();\n\n/**\n * Configure the global client instance\n *\n * Call this in your app initialization to set default configuration\n * for all auto-generated API calls.\n *\n * @example\n * ```ts\n * // In app initialization (layout.tsx, _app.tsx, etc)\n * import { configureClient } from '@spfn/core/client';\n *\n * configureClient({\n * baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n * timeout: 60000,\n * headers: {\n * 'X-App-Version': '1.0.0'\n * }\n * });\n *\n * // Add interceptors\n * import { client } from '@spfn/core/client';\n * client.use(async (url, init) => {\n * // Add auth header\n * return {\n * ...init,\n * headers: {\n * ...init.headers,\n * Authorization: `Bearer ${getToken()}`\n * }\n * };\n * });\n * ```\n */\nexport function configureClient(config: ClientConfig): void\n{\n _clientInstance = new ContractClient(config);\n}\n\n/**\n * Global client singleton with Proxy\n *\n * This client can be configured using configureClient() before use.\n * Used by auto-generated API client code.\n */\nexport const client = new Proxy({} as ContractClient, {\n get(_target, prop)\n {\n return _clientInstance[prop as keyof ContractClient];\n }\n});\n\n/**\n * Type guard for timeout errors\n *\n * @example\n * ```ts\n * try {\n * await api.users.getById({ params: { id: '123' } });\n * } catch (error) {\n * if (isTimeoutError(error)) {\n * console.error('Request timed out, retrying...');\n * // Implement retry logic\n * }\n * }\n * ```\n */\nexport function isTimeoutError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'timeout';\n}\n\n/**\n * Type guard for network errors\n *\n * @example\n * ```ts\n * try {\n * await api.users.list();\n * } catch (error) {\n * if (isNetworkError(error)) {\n * showOfflineMessage();\n * }\n * }\n * ```\n */\nexport function isNetworkError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'network';\n}\n\n/**\n * Type guard for HTTP errors (4xx, 5xx)\n *\n * @example\n * ```ts\n * try {\n * await api.users.create({ body: userData });\n * } catch (error) {\n * if (isHttpError(error)) {\n * if (error.status === 401) {\n * redirectToLogin();\n * } else if (error.status === 404) {\n * showNotFoundMessage();\n * }\n * }\n * }\n * ```\n */\nexport function isHttpError(error: unknown): error is ApiClientError\n{\n return error instanceof ApiClientError && error.errorType === 'http';\n}\n\n/**\n * Check if error is a specific server error type\n *\n * @example\n * ```ts\n * try {\n * await api.workflows.getById({ params: { uuid: 'xxx' } });\n * } catch (error) {\n * if (isServerError(error, 'NotFoundError')) {\n * showNotFoundMessage();\n * } else if (isServerError(error, 'ValidationError')) {\n * showValidationErrors(getServerErrorDetails(error));\n * }\n * }\n * ```\n */\nexport function isServerError(error: unknown, errorType: string): error is ApiClientError\n{\n if (!isHttpError(error)) return false;\n const response = error.response as any;\n return response?.error?.type === errorType;\n}\n\n/**\n * Get server error type from ApiClientError\n *\n * @example\n * ```ts\n * const errorType = getServerErrorType(error);\n * // 'NotFoundError', 'ValidationError', 'PaymentFailedError', etc.\n * ```\n */\nexport function getServerErrorType(error: ApiClientError): string | undefined\n{\n const response = error.response as any;\n return response?.error?.type;\n}\n\n/**\n * Get server error details from ApiClientError\n *\n * @example\n * ```ts\n * if (isServerError(error, 'PaymentFailedError')) {\n * const details = getServerErrorDetails(error);\n * console.log('Payment ID:', details.paymentId);\n * }\n * ```\n */\nexport function getServerErrorDetails<T = any>(error: ApiClientError): T | undefined\n{\n const response = error.response as any;\n return response?.error?.details;\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import { c as createContractGenerator } from '../../index-DHiAqhKv.js';
2
+ export { C as ContractGeneratorConfig } from '../../index-DHiAqhKv.js';
3
+
4
+ /**
5
+ * Built-in Generators Export
6
+ *
7
+ * Provides a registry of all built-in generators
8
+ */
9
+
10
+ /**
11
+ * Registry of available generators
12
+ *
13
+ * Used by package-based generator loading (e.g., "@spfn/core:contract")
14
+ */
15
+ declare const generators: {
16
+ contract: typeof createContractGenerator;
17
+ };
18
+
19
+ export { createContractGenerator, generators };