@spoosh/core 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,7 +36,10 @@ type ApiSchema = {
36
36
  $post: Endpoint<{ data: { url: string }; formData: { file: File } }>;
37
37
  };
38
38
  payments: {
39
- $post: Endpoint<{ data: { id: string }; urlEncoded: { amount: number; currency: string } }>;
39
+ $post: Endpoint<{
40
+ data: { id: string };
41
+ urlEncoded: { amount: number; currency: string };
42
+ }>;
40
43
  };
41
44
  };
42
45
  ```
@@ -173,16 +176,16 @@ const updatedContext = await applyMiddlewares(context, middlewares, "before");
173
176
 
174
177
  ## Schema Types
175
178
 
176
- | Type | Description | Example |
177
- | --------------------------------------- | ----------------------------- | --------------------------------------------------------------------- |
178
- | `Endpoint<{ data }>` | Endpoint with data only | `$get: Endpoint<{ data: User[] }>` |
179
- | `Endpoint<{ data; body }>` | Endpoint with JSON body | `$post: Endpoint<{ data: User; body: CreateUserBody }>` |
180
- | `Endpoint<{ data; query }>` | Endpoint with query params | `$get: Endpoint<{ data: User[]; query: { page: number } }>` |
181
- | `Endpoint<{ data; formData }>` | Endpoint with multipart form | `$post: Endpoint<{ data: Result; formData: { file: File } }>` |
182
- | `Endpoint<{ data; urlEncoded }>` | Endpoint with URL-encoded | `$post: Endpoint<{ data: Result; urlEncoded: { amount: number } }>` |
183
- | `Endpoint<{ data; error }>` | Endpoint with typed error | `$get: Endpoint<{ data: User; error: ApiError }>` |
184
- | `void` | No response body | `$delete: void` |
185
- | `_` | Dynamic path segment | `users: { _: { $get: Endpoint<{ data: User }> } }` |
179
+ | Type | Description | Example |
180
+ | -------------------------------- | ---------------------------- | ------------------------------------------------------------------- |
181
+ | `Endpoint<{ data }>` | Endpoint with data only | `$get: Endpoint<{ data: User[] }>` |
182
+ | `Endpoint<{ data; body }>` | Endpoint with JSON body | `$post: Endpoint<{ data: User; body: CreateUserBody }>` |
183
+ | `Endpoint<{ data; query }>` | Endpoint with query params | `$get: Endpoint<{ data: User[]; query: { page: number } }>` |
184
+ | `Endpoint<{ data; formData }>` | Endpoint with multipart form | `$post: Endpoint<{ data: Result; formData: { file: File } }>` |
185
+ | `Endpoint<{ data; urlEncoded }>` | Endpoint with URL-encoded | `$post: Endpoint<{ data: Result; urlEncoded: { amount: number } }>` |
186
+ | `Endpoint<{ data; error }>` | Endpoint with typed error | `$get: Endpoint<{ data: User; error: ApiError }>` |
187
+ | `void` | No response body | `$delete: void` |
188
+ | `_` | Dynamic path segment | `users: { _: { $get: Endpoint<{ data: User }> } }` |
186
189
 
187
190
  ## API Reference
188
191
 
@@ -206,11 +209,8 @@ import { cachePlugin } from "@spoosh/plugin-cache";
206
209
  import { retryPlugin } from "@spoosh/plugin-retry";
207
210
 
208
211
  const client = new Spoosh<ApiSchema, Error>("/api", {
209
- headers: { Authorization: "Bearer token" }
210
- }).use([
211
- cachePlugin({ staleTime: 5000 }),
212
- retryPlugin({ retries: 3 })
213
- ]);
212
+ headers: { Authorization: "Bearer token" },
213
+ }).use([cachePlugin({ staleTime: 5000 }), retryPlugin({ retries: 3 })]);
214
214
 
215
215
  const { api } = client;
216
216
  const { data } = await api.users.$get();
@@ -218,26 +218,25 @@ const { data } = await api.users.$get();
218
218
 
219
219
  **Constructor Parameters:**
220
220
 
221
- | Parameter | Type | Description |
222
- | ---------------- | --------------- | --------------------------------------- |
223
- | `baseUrl` | `string` | Base URL for all API requests |
224
- | `defaultOptions` | `RequestInit` | (Optional) Default fetch options |
221
+ | Parameter | Type | Description |
222
+ | ---------------- | ------------- | -------------------------------- |
223
+ | `baseUrl` | `string` | Base URL for all API requests |
224
+ | `defaultOptions` | `RequestInit` | (Optional) Default fetch options |
225
225
 
226
226
  **Methods:**
227
227
 
228
- | Method | Description |
229
- | ------ | ----------- |
228
+ | Method | Description |
229
+ | --------------- | --------------------------------------------------------------------- |
230
230
  | `.use(plugins)` | Add plugins to the client. Returns a new instance with updated types. |
231
231
 
232
232
  **Properties:**
233
233
 
234
- | Property | Description |
235
- | -------- | ----------- |
236
- | `.api` | Type-safe API client for making requests |
237
- | `.stateManager` | Cache and state management |
238
- | `.eventEmitter` | Event system for refetch/invalidation |
239
- | `.pluginExecutor` | Plugin lifecycle management |
240
-
234
+ | Property | Description |
235
+ | ----------------- | ---------------------------------------- |
236
+ | `.api` | Type-safe API client for making requests |
237
+ | `.stateManager` | Cache and state management |
238
+ | `.eventEmitter` | Event system for refetch/invalidation |
239
+ | `.pluginExecutor` | Plugin lifecycle management |
241
240
 
242
241
  ## Creating Plugins
243
242
 
package/dist/index.d.mts CHANGED
@@ -96,6 +96,8 @@ type AnyRequestOptions = BaseRequestOptions & {
96
96
  urlEncoded?: Record<string, unknown>;
97
97
  params?: Record<string, string | number>;
98
98
  signal?: AbortSignal;
99
+ /** @internal Path transformer function. Set by plugins like path-case. */
100
+ _pathTransformer?: (path: string[]) => string[];
99
101
  } & Partial<RetryConfig>;
100
102
  type DynamicParamsOption = {
101
103
  params?: Record<string, string | number>;
@@ -801,7 +803,6 @@ type HttpMethods<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamN
801
803
  [K in SchemaMethod as K extends keyof TSchema ? K : never]: MethodFn<TSchema, K, TDefaultError, TOptionsMap, TParamNames>;
802
804
  };
803
805
  type DynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = ExtractDynamicSchema<TSchema> extends never ? object : {
804
- [key: number]: SpooshClient<ExtractDynamicSchema<TSchema>, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
805
806
  /**
806
807
  * Dynamic path segment with typed param name.
807
808
  * Use `:paramName` format to get typed params in the response.
@@ -844,7 +845,6 @@ type ExtractParamName<S> = S extends `:${infer P}` ? P : never;
844
845
  type QueryDynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = TSchema extends {
845
846
  _: infer D;
846
847
  } ? HasQueryMethods<D> extends true ? {
847
- [key: number]: QueryOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
848
848
  <TKey extends string | number>(key: TKey): QueryOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | ExtractParamName<TKey>, TRootSchema>;
849
849
  } : object : object;
850
850
  type QueryOnlyClient<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = QueryHttpMethods<TSchema, TDefaultError, TOptionsMap, TParamNames> & QueryDynamicAccess<TSchema, TDefaultError, TOptionsMap, TParamNames, TRootSchema> & {
@@ -856,7 +856,6 @@ type MutationHttpMethods<TSchema, TDefaultError = unknown, TOptionsMap = object,
856
856
  type MutationDynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = TSchema extends {
857
857
  _: infer D;
858
858
  } ? HasMutationMethods<D> extends true ? {
859
- [key: number]: MutationOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string>;
860
859
  <TKey extends string | number>(key: TKey): MutationOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | ExtractParamName<TKey>>;
861
860
  } : object : object;
862
861
  type MutationOnlyClient<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = MutationHttpMethods<TSchema, TDefaultError, TOptionsMap, TParamNames> & MutationDynamicAccess<TSchema, TDefaultError, TOptionsMap, TParamNames> & {
@@ -906,7 +905,7 @@ type EndpointToMethod<T> = (options?: ExtractEndpointRequestOptions<T>) => Promi
906
905
  * trigger({
907
906
  * myCallback: (api) => [
908
907
  * api.posts.$get, // ✓ Valid
909
- * api.users[1].$get, // ✓ Dynamic segment
908
+ * api.users(1).$get, // ✓ Dynamic segment
910
909
  * api.nonexistent.$get, // ✗ Type error
911
910
  * ],
912
911
  * });
@@ -919,7 +918,7 @@ type QuerySchemaHelper<TSchema> = {
919
918
  } & (TSchema extends {
920
919
  _: infer D;
921
920
  } ? HasQueryMethods<D> extends true ? {
922
- [key: number]: QuerySchemaHelper<D>;
921
+ <TKey extends string | number>(key: TKey): QuerySchemaHelper<D>;
923
922
  } : object : object);
924
923
 
925
924
  type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
@@ -1066,7 +1065,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1066
1065
  * const { data } = await api.posts.$post({ body: { title: 'Hello' } });
1067
1066
  *
1068
1067
  * // Dynamic path parameters
1069
- * const { data } = await api.posts[postId].$get();
1068
+ * const { data } = await api.posts(postId).$get();
1070
1069
  * ```
1071
1070
  */
1072
1071
  get api(): SpooshClient<TSchema, TError, CoreRequestOptionsBase>;
@@ -1187,7 +1186,7 @@ type SpooshClientConfig = {
1187
1186
  *
1188
1187
  * // Type-safe API calls
1189
1188
  * const { data } = await api.posts.$get();
1190
- * const { data: post } = await api.posts[123].$get();
1189
+ * const { data: post } = await api.posts(123).$get();
1191
1190
  * ```
1192
1191
  */
1193
1192
  declare function createClient<TSchema, TDefaultError = unknown>(config: SpooshClientConfig): SpooshClient<TSchema, TDefaultError>;
@@ -1260,9 +1259,9 @@ type ProxyHandlerConfig<TOptions = SpooshOptions> = {
1260
1259
  * await api.posts.$get();
1261
1260
  *
1262
1261
  * // Dynamic segments via function call:
1263
- * // api.posts[123].$get() or api.posts('123').$get()
1262
+ * // api.posts(123).$get() or api.posts('123').$get()
1264
1263
  * // Executes: GET /api/posts/123
1265
- * await api.posts[123].$get();
1264
+ * await api.posts(123).$get();
1266
1265
  * ```
1267
1266
  */
1268
1267
  declare function createProxyHandler<TSchema extends object, TOptions = SpooshOptions>(config: ProxyHandlerConfig<TOptions>): TSchema;
package/dist/index.d.ts CHANGED
@@ -96,6 +96,8 @@ type AnyRequestOptions = BaseRequestOptions & {
96
96
  urlEncoded?: Record<string, unknown>;
97
97
  params?: Record<string, string | number>;
98
98
  signal?: AbortSignal;
99
+ /** @internal Path transformer function. Set by plugins like path-case. */
100
+ _pathTransformer?: (path: string[]) => string[];
99
101
  } & Partial<RetryConfig>;
100
102
  type DynamicParamsOption = {
101
103
  params?: Record<string, string | number>;
@@ -801,7 +803,6 @@ type HttpMethods<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamN
801
803
  [K in SchemaMethod as K extends keyof TSchema ? K : never]: MethodFn<TSchema, K, TDefaultError, TOptionsMap, TParamNames>;
802
804
  };
803
805
  type DynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = ExtractDynamicSchema<TSchema> extends never ? object : {
804
- [key: number]: SpooshClient<ExtractDynamicSchema<TSchema>, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
805
806
  /**
806
807
  * Dynamic path segment with typed param name.
807
808
  * Use `:paramName` format to get typed params in the response.
@@ -844,7 +845,6 @@ type ExtractParamName<S> = S extends `:${infer P}` ? P : never;
844
845
  type QueryDynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = TSchema extends {
845
846
  _: infer D;
846
847
  } ? HasQueryMethods<D> extends true ? {
847
- [key: number]: QueryOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string, TRootSchema>;
848
848
  <TKey extends string | number>(key: TKey): QueryOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | ExtractParamName<TKey>, TRootSchema>;
849
849
  } : object : object;
850
850
  type QueryOnlyClient<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never, TRootSchema = TSchema> = QueryHttpMethods<TSchema, TDefaultError, TOptionsMap, TParamNames> & QueryDynamicAccess<TSchema, TDefaultError, TOptionsMap, TParamNames, TRootSchema> & {
@@ -856,7 +856,6 @@ type MutationHttpMethods<TSchema, TDefaultError = unknown, TOptionsMap = object,
856
856
  type MutationDynamicAccess<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = TSchema extends {
857
857
  _: infer D;
858
858
  } ? HasMutationMethods<D> extends true ? {
859
- [key: number]: MutationOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | string>;
860
859
  <TKey extends string | number>(key: TKey): MutationOnlyClient<D, TDefaultError, TOptionsMap, TParamNames | ExtractParamName<TKey>>;
861
860
  } : object : object;
862
861
  type MutationOnlyClient<TSchema, TDefaultError = unknown, TOptionsMap = object, TParamNames extends string = never> = MutationHttpMethods<TSchema, TDefaultError, TOptionsMap, TParamNames> & MutationDynamicAccess<TSchema, TDefaultError, TOptionsMap, TParamNames> & {
@@ -906,7 +905,7 @@ type EndpointToMethod<T> = (options?: ExtractEndpointRequestOptions<T>) => Promi
906
905
  * trigger({
907
906
  * myCallback: (api) => [
908
907
  * api.posts.$get, // ✓ Valid
909
- * api.users[1].$get, // ✓ Dynamic segment
908
+ * api.users(1).$get, // ✓ Dynamic segment
910
909
  * api.nonexistent.$get, // ✗ Type error
911
910
  * ],
912
911
  * });
@@ -919,7 +918,7 @@ type QuerySchemaHelper<TSchema> = {
919
918
  } & (TSchema extends {
920
919
  _: infer D;
921
920
  } ? HasQueryMethods<D> extends true ? {
922
- [key: number]: QuerySchemaHelper<D>;
921
+ <TKey extends string | number>(key: TKey): QuerySchemaHelper<D>;
923
922
  } : object : object);
924
923
 
925
924
  type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
@@ -1066,7 +1065,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1066
1065
  * const { data } = await api.posts.$post({ body: { title: 'Hello' } });
1067
1066
  *
1068
1067
  * // Dynamic path parameters
1069
- * const { data } = await api.posts[postId].$get();
1068
+ * const { data } = await api.posts(postId).$get();
1070
1069
  * ```
1071
1070
  */
1072
1071
  get api(): SpooshClient<TSchema, TError, CoreRequestOptionsBase>;
@@ -1187,7 +1186,7 @@ type SpooshClientConfig = {
1187
1186
  *
1188
1187
  * // Type-safe API calls
1189
1188
  * const { data } = await api.posts.$get();
1190
- * const { data: post } = await api.posts[123].$get();
1189
+ * const { data: post } = await api.posts(123).$get();
1191
1190
  * ```
1192
1191
  */
1193
1192
  declare function createClient<TSchema, TDefaultError = unknown>(config: SpooshClientConfig): SpooshClient<TSchema, TDefaultError>;
@@ -1260,9 +1259,9 @@ type ProxyHandlerConfig<TOptions = SpooshOptions> = {
1260
1259
  * await api.posts.$get();
1261
1260
  *
1262
1261
  * // Dynamic segments via function call:
1263
- * // api.posts[123].$get() or api.posts('123').$get()
1262
+ * // api.posts(123).$get() or api.posts('123').$get()
1264
1263
  * // Executes: GET /api/posts/123
1265
- * await api.posts[123].$get();
1264
+ * await api.posts(123).$get();
1266
1265
  * ```
1267
1266
  */
1268
1267
  declare function createProxyHandler<TSchema extends object, TOptions = SpooshOptions>(config: ProxyHandlerConfig<TOptions>): TSchema;
package/dist/index.js CHANGED
@@ -336,7 +336,8 @@ async function executeCoreFetch(config) {
336
336
  const maxRetries = requestOptions?.retries ?? 3;
337
337
  const baseDelay = requestOptions?.retryDelay ?? 1e3;
338
338
  const retryCount = maxRetries === false ? 0 : maxRetries;
339
- const url = buildUrl(baseUrl, path, requestOptions?.query);
339
+ const finalPath = requestOptions?._pathTransformer?.(path) ?? path;
340
+ const url = buildUrl(baseUrl, finalPath, requestOptions?.query);
340
341
  let headers = await mergeHeaders(defaultHeaders, requestOptions?.headers);
341
342
  const fetchInit = {
342
343
  ...fetchDefaults,
@@ -476,11 +477,9 @@ function createProxyHandler(config) {
476
477
  nextTags
477
478
  });
478
479
  },
479
- // Handles function call syntax for dynamic segments: api.posts("123"), api.users(userId)
480
- // Q. Why allow this syntax?
481
- // A. To support dynamic type inference in frameworks where property access with variables is not possible.
482
- // Eg. api.posts[":id"].$get() <-- TypeScript sees this as bracket notation with a string literal, can't infer param types
483
- // But api.posts(":id").$get() <-- TypeScript can capture ":id" as a template literal type, enabling params: { id: string } inference
480
+ // Handles function call syntax for dynamic segments: api.posts(123), api.posts(":id"), api.users(userId)
481
+ // This is the only way to access dynamic segments in Spoosh.
482
+ // The function call syntax allows TypeScript to capture the literal type, enabling params: { id: string } inference.
484
483
  apply(_target, _thisArg, args) {
485
484
  const [segment] = args;
486
485
  return createProxyHandler({
@@ -531,7 +530,7 @@ function createSelectorProxy(onCapture) {
531
530
  // Handles function call syntax for dynamic segments: api.posts("123"), api.users(userId)
532
531
  apply(_, __, args) {
533
532
  const [segment] = args;
534
- return createProxy([...path, segment]);
533
+ return createProxy([...path, String(segment)]);
535
534
  }
536
535
  });
537
536
  };
@@ -989,7 +988,7 @@ var Spoosh = class _Spoosh {
989
988
  * const { data } = await api.posts.$post({ body: { title: 'Hello' } });
990
989
  *
991
990
  * // Dynamic path parameters
992
- * const { data } = await api.posts[postId].$get();
991
+ * const { data } = await api.posts(postId).$get();
993
992
  * ```
994
993
  */
995
994
  get api() {
package/dist/index.mjs CHANGED
@@ -281,7 +281,8 @@ async function executeCoreFetch(config) {
281
281
  const maxRetries = requestOptions?.retries ?? 3;
282
282
  const baseDelay = requestOptions?.retryDelay ?? 1e3;
283
283
  const retryCount = maxRetries === false ? 0 : maxRetries;
284
- const url = buildUrl(baseUrl, path, requestOptions?.query);
284
+ const finalPath = requestOptions?._pathTransformer?.(path) ?? path;
285
+ const url = buildUrl(baseUrl, finalPath, requestOptions?.query);
285
286
  let headers = await mergeHeaders(defaultHeaders, requestOptions?.headers);
286
287
  const fetchInit = {
287
288
  ...fetchDefaults,
@@ -421,11 +422,9 @@ function createProxyHandler(config) {
421
422
  nextTags
422
423
  });
423
424
  },
424
- // Handles function call syntax for dynamic segments: api.posts("123"), api.users(userId)
425
- // Q. Why allow this syntax?
426
- // A. To support dynamic type inference in frameworks where property access with variables is not possible.
427
- // Eg. api.posts[":id"].$get() <-- TypeScript sees this as bracket notation with a string literal, can't infer param types
428
- // But api.posts(":id").$get() <-- TypeScript can capture ":id" as a template literal type, enabling params: { id: string } inference
425
+ // Handles function call syntax for dynamic segments: api.posts(123), api.posts(":id"), api.users(userId)
426
+ // This is the only way to access dynamic segments in Spoosh.
427
+ // The function call syntax allows TypeScript to capture the literal type, enabling params: { id: string } inference.
429
428
  apply(_target, _thisArg, args) {
430
429
  const [segment] = args;
431
430
  return createProxyHandler({
@@ -476,7 +475,7 @@ function createSelectorProxy(onCapture) {
476
475
  // Handles function call syntax for dynamic segments: api.posts("123"), api.users(userId)
477
476
  apply(_, __, args) {
478
477
  const [segment] = args;
479
- return createProxy([...path, segment]);
478
+ return createProxy([...path, String(segment)]);
480
479
  }
481
480
  });
482
481
  };
@@ -934,7 +933,7 @@ var Spoosh = class _Spoosh {
934
933
  * const { data } = await api.posts.$post({ body: { title: 'Hello' } });
935
934
  *
936
935
  * // Dynamic path parameters
937
- * const { data } = await api.posts[postId].$get();
936
+ * const { data } = await api.posts(postId).$get();
938
937
  * ```
939
938
  */
940
939
  get api() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "description": "Type-safe API client with plugin middleware system",
6
6
  "keywords": [