openapi-ff 0.1.2 → 0.2.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  openapi-ff is a type-safe tiny wrapper around [effector](https://effector.dev/) and [farfetched](https://ff.effector.dev/) to work with OpenAPI schema.
4
4
 
5
- It works by using [openapi-fetch](../openapi-fetch) and [openapi-typescript](../openapi-typescript).
5
+ It works by using [openapi-fetch](https://openapi-ts.dev/openapi-fetch/) and [openapi-typescript](https://openapi-ts.dev/).
6
6
 
7
7
  ## Setup
8
8
 
@@ -21,17 +21,17 @@ npx openapi-typescript ./path/to/api/v1.yaml -o ./src/shared/api/schema.d.ts
21
21
 
22
22
  ```ts
23
23
  import createFetchClient from "openapi-fetch";
24
- import { createClient } from "openapi-ff";
24
+ import { createEffectorClient } from "openapi-ff";
25
25
  import { createQuery } from "@farfetched/core";
26
26
  import type { paths } from "./schema"; // generated by openapi-typescript
27
27
 
28
28
  export const client = createFetchClient<paths>({
29
29
  baseUrl: "https://myapi.dev/v1/",
30
30
  });
31
- export const { createApiEffect } = createClient(client);
31
+ export const { createApiEffect } = createEffectorClient(client);
32
32
 
33
33
  const blogpostQuery = createQuery({
34
- effect: createApiEffect("get", "/blogposts/{post_id}"),
34
+ ...createApiEffect("get", "/blogposts/{post_id}"),
35
35
  });
36
36
  ```
37
37
 
@@ -60,11 +60,10 @@ import { chainRoute } from "atomic-router";
60
60
  import { startChain } from "@farfetched/atomic-router";
61
61
  import { isApiError } from "openapi-ff";
62
62
 
63
- const getBlogpostFx = createApiEffect("get", "/blogposts/{post_id}", {
64
- mapParams: (args: { postId: string }) => ({ params: { path: { post_id: args.postId } } }),
65
- });
66
63
  const blogpostQuery = createQuery({
67
- effect: getBlogpostFx,
64
+ ...createApiEffect("get", "/blogposts/{post_id}", {
65
+ mapParams: (args: { postId: string }) => ({ params: { path: { post_id: args.postId } } }),
66
+ }),
68
67
  mapData: ({ result, params }) => ({ ...result, ...params }),
69
68
  initialData: { body: "-", title: "-", postId: "0" },
70
69
  });
@@ -80,16 +79,31 @@ const apiError = sample({
80
79
  });
81
80
  ```
82
81
 
82
+ Map with source:
83
+
84
+ ```ts
85
+ import { mergeInitHeaders } from 'openapi-ff';
86
+
87
+ const blogpostQuery = createQuery({
88
+ ...createApiEffect("get", "/blogposts/{post_id}", {
89
+ mapParams: {
90
+ source: $token,
91
+ fn: (token, init) => mergeInitHeaders(init, { Authorization: token }),
92
+ },
93
+ }),
94
+ });
95
+ ```
96
+
83
97
  ## Runtime Validation
84
98
 
85
99
  `openapi-ff` does not handle runtime validation, as `openapi-typescript` [does not support it](https://github.com/openapi-ts/openapi-typescript/issues/1420#issuecomment-1792909086).
86
100
 
87
101
  > openapi-typescript by its design generates runtime-free static types, and only static types.
88
102
 
89
- However, `openapi-ff` allows adding a contract factory when creating a client and provides a corresponding method, `createApiEffectWithContract`:
103
+ However, `openapi-ff` allows adding a contract factory when creating a client:
90
104
 
91
105
  ```ts
92
- const { createApiEffectWithContract } = createClient(fetchClient, {
106
+ const { createApiEffect } = createEffectorClient(fetchClient, {
93
107
  createContract(method, path) {
94
108
  // ... create your own contract
95
109
  return contract; // Contract<unknown, unknown>
@@ -97,7 +111,7 @@ const { createApiEffectWithContract } = createClient(fetchClient, {
97
111
  });
98
112
 
99
113
  const query = createQuery({
100
- ...createApiEffectWithContract("get", "/blogposts"),
114
+ ...createApiEffect("get", "/blogposts"),
101
115
  });
102
116
  ```
103
117
 
@@ -112,10 +126,9 @@ pnpm install zod @farfetched/zod
112
126
  import { EndpointByMethod } from "./zod";
113
127
  import { zodContract } from "@farfetched/zod";
114
128
 
115
- const { createApiEffectWithContract } = createClient(fetchClient, {
129
+ const { createApiEffect } = createEffectorClient(fetchClient, {
116
130
  createContract(method, path) {
117
- const endpoints = EndpointByMethod[method] as any;
118
- const response = endpoints[path]?.response;
131
+ const response = (EndpointByMethod as any)[method][path]?.response;
119
132
  if (!response) {
120
133
  throw new Error(`Response schema for route "${method} ${path}" doesn't exist`);
121
134
  }
@@ -124,39 +137,6 @@ const { createApiEffectWithContract } = createClient(fetchClient, {
124
137
  });
125
138
 
126
139
  const query = createQuery({
127
- ...createApiEffectWithContract("get", "/blogposts"),
128
- });
129
- ```
130
-
131
- ### [orval](https://orval.dev/) example
132
-
133
- Alternatively, you can simply add any contract to a query:
134
-
135
- ```bash
136
- pnpm install zod @farfetched/zod
137
- npx orval --input path/to/api.yaml --output src/zod.ts --client zod --mode single
138
- ```
139
-
140
- ```ts
141
- import { zodContract } from "@farfetched/zod";
142
- import { getBlogpostsResponseItem } from "./zod";
143
-
144
- const blogpostQuery = createQuery({
145
- effect: createApiEffect("get", "/blogposts/{post_id}"),
146
- contract: zodContract(getBlogpostsResponseItem),
147
- });
148
- ```
149
-
150
- ## TODO
151
-
152
- Add `createApiQuery`:
153
-
154
- ```ts
155
- createApiQuery({
156
- method: "get",
157
- path: "/blogposts/{post_id}",
158
- mapParams: (args: { postId: string }) => ({ params: { path: { post_id: args.postId } } }),
159
- mapData: ({ result, params }) => ({ ...result, ...params }),
160
- initialData: { body: "-", title: "-", postId: "0" },
140
+ ...createApiEffect("get", "/blogposts"),
161
141
  });
162
142
  ```
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p=require("effector"),l=require("@farfetched/core"),h="API";function C(t){return{...t,errorType:h,explanation:"Request was finished with unsuccessful HTTP code"}}function A(t){var e;return((e=t.error)==null?void 0:e.errorType)===h}function g(t,e={}){const u=(s,a,r)=>{const w=s.toUpperCase(),E=t[w];return p.createEffect(async i=>{var f;const{data:T,error:o,response:n}=await E(a,((f=r==null?void 0:r.mapParams)==null?void 0:f.call(r,i))??i).catch(c=>{throw l.networkError({reason:(c==null?void 0:c.message)??null,cause:c})});if(o!=null&&o!=="")throw C({status:n.status,statusText:n.statusText,response:o});if(!n.ok)throw l.httpError({status:n.status,statusText:n.statusText,response:null});return T})};return{createApiEffect:u,createApiEffectWithContract:(s,a,r)=>{if(e.createContract==null)throw new Error("'createContract' is missing in config");return{effect:u(s,a,r),contract:e.createContract(s,a.toString())}}}}exports.createClient=g;exports.isApiError=A;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("@farfetched/core"),P=require("effector"),I=require("openapi-fetch"),g="API";function p(t){var r;return((r=t.error)==null?void 0:r.errorType)===g}function y(t,r){return{createApiEffect:(f,o,e)=>{var h,E;const w=f.toUpperCase(),x=t[w],A=((h=e==null?void 0:e.mapParams)==null?void 0:h.source)??P.createStore(null),i=!!(e!=null&&e.mapParams),l=!!(e!=null&&e.mapParams.fn),m=i?l?e.mapParams.fn:e.mapParams:null;return{effect:P.attach({source:A,effect:async(T,c)=>{let s=c;i&&(l?s=m(T,c):s=m(c));const{data:q,error:u,response:a}=await x(o,s).catch(n=>{throw d.networkError({reason:(n==null?void 0:n.message)??null,cause:n})});if(u!=null&&u!=="")throw C({status:a.status,statusText:a.statusText,response:u});if(!a.ok)throw d.httpError({status:a.status,statusText:a.statusText,response:null});return q}}),contract:((E=r==null?void 0:r.createContract)==null?void 0:E.call(r,f,o))??F()}}}}function C(t){return{...t,errorType:g,explanation:"Request was finished with unsuccessful API response"}}function S(t,r){return{...t,headers:I.mergeHeaders(t.headers,r)}}function F(){return{isData:t=>!0,getErrorMessages:()=>[]}}exports.createEffectorClient=y;exports.isApiError=p;exports.mergeInitHeaders=S;
@@ -1,50 +1,114 @@
1
+ import { BodySerializer } from 'openapi-fetch';
1
2
  import { Client } from 'openapi-fetch';
3
+ import { ClientOptions } from 'openapi-fetch';
2
4
  import { Contract } from '@farfetched/core';
3
5
  import { Effect } from 'effector';
4
6
  import { FarfetchedError } from '@farfetched/core';
7
+ import { FetchOptions } from 'openapi-fetch';
5
8
  import { FetchResponse } from 'openapi-fetch';
9
+ import { HeadersOptions } from 'openapi-fetch';
6
10
  import { HttpError } from '@farfetched/core';
7
- import { HttpMethod } from 'openapi-typescript-helpers';
8
11
  import { MaybeOptionalInit } from 'openapi-fetch';
9
- import { MediaType } from 'openapi-typescript-helpers';
10
12
  import { NetworkError } from '@farfetched/core';
11
- import { PathsWithMethod } from 'openapi-typescript-helpers';
13
+ import { ParamsOption } from 'openapi-fetch';
14
+ import { ParseAs } from 'openapi-fetch';
15
+ import { QuerySerializer } from 'openapi-fetch';
16
+ import { QuerySerializerOptions } from 'openapi-fetch';
17
+ import { RequestBodyOption } from 'openapi-fetch';
18
+ import { Store } from 'effector';
12
19
 
13
20
  declare const API = "API";
14
21
 
15
- export declare interface ApiError<Status extends number = number, ApiResponse = unknown> extends FarfetchedError<typeof API> {
16
- status: Status;
22
+ declare type ApiError<S extends ErrorStatus, T> = {
23
+ status: S;
17
24
  statusText: string;
18
- response: ApiResponse;
19
- }
25
+ response: T;
26
+ } & FarfetchedError<typeof API>;
20
27
 
21
- declare type CreateApiEffect<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, Options extends CreateApiEffectOptions<Init> = {}>(method: Method, path: Path, options?: Options) => Effect<Init extends undefined ? void : InitOrPrependInit<Init, Parameters<NonNullable<Options["mapParams"]>>[0], Options>, Response["data"], ApiError<number, Response["error"]> | HttpError | NetworkError>;
28
+ declare type ApiErrors<Responses extends Record<string | number, any>> = {
29
+ [K in keyof Responses]: K extends ErrorStatus ? Responses[K] extends {
30
+ content: Record<string, any>;
31
+ } ? ApiError<K, FilterKeys<Responses[K]['content'], `${string}/json`>> : never : never;
32
+ }[keyof Responses & ErrorStatus];
22
33
 
23
- declare type CreateApiEffectOptions<Init> = {
24
- mapParams?: (init: any) => Init;
34
+ declare type CreateApiEffect<Paths extends Record<string, Record<HttpMethod, {}>>> = {
35
+ <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Operation extends Paths[Path][Method], Responses extends ResponseObjectMap<Operation>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, `${string}/json`>>>(method: Method, path: Path): {
36
+ effect: Effect<OptionalParams<Init>, Response['data'], ApiErrors<Responses> | HttpError | NetworkError>;
37
+ contract: Contract<unknown, Response['data']>;
38
+ };
39
+ <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Operation extends Paths[Path][Method], Responses extends ResponseObjectMap<Operation>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, `${string}/json`>>, Source, NewInit = Init>(method: Method, path: Path, options: {
40
+ mapParams: {
41
+ source: Store<Source>;
42
+ fn: (source: Source, params: NewInit) => Init;
43
+ };
44
+ }): {
45
+ effect: Effect<OptionalParams<NewInit>, Response['data'], ApiErrors<Responses> | HttpError | NetworkError>;
46
+ contract: Contract<unknown, Response['data']>;
47
+ };
48
+ <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Operation extends Paths[Path][Method], Responses extends ResponseObjectMap<Operation>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, `${string}/json`>>, NewInit>(method: Method, path: Path, options: {
49
+ mapParams: (params: NewInit) => Init;
50
+ }): {
51
+ effect: Effect<OptionalParams<NewInit>, Response['data'], ApiErrors<Responses> | HttpError | NetworkError>;
52
+ contract: Contract<unknown, Response['data']>;
53
+ };
25
54
  };
26
55
 
27
- declare type CreateApiEffectWithContract<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, Options extends CreateApiEffectOptions<Init> = {}>(method: Method, path: Path, options?: Options) => {
28
- effect: Effect<Init extends undefined ? void : InitOrPrependInit<Init, Parameters<NonNullable<Options["mapParams"]>>[0], Options>, Response["data"], ApiError<number, Response["error"]> | HttpError | NetworkError>;
29
- contract: Contract<unknown, Response["data"]>;
30
- };
56
+ export declare function createEffectorClient<Paths extends {}>(client: Client<Paths>, options?: {
57
+ createContract?: <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>>(method: Method, path: Path) => Contract<unknown, unknown>;
58
+ }): OpenapiEffectorClient<Paths>;
59
+
60
+ /** 2XX statuses */
61
+ declare type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | '5XX' | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 444 | 450 | 451 | 497 | 498 | 499 | '4XX' | "default";
62
+
63
+ /** Find first match of multiple keys */
64
+ declare type FilterKeys<Obj, Matchers> = Obj[keyof Obj & Matchers];
65
+
66
+ declare type HasRequiredKeys<T> = {} extends T ? true : false;
31
67
 
32
- export declare function createClient<Paths extends {}, Media extends MediaType = MediaType>(client: Client<Paths, Media>, config?: EffectorClientOptions): OpenapiEffectorClient<Paths, Media>;
68
+ declare type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace';
69
+
70
+ export declare function isApiError<S extends ErrorStatus, R>(args: WithError): args is WithError<ApiError<S, R>>;
71
+
72
+ export declare function mergeInitHeaders<T>(init: FetchOptions<T>, headers: Record<string, any>): ParamsOption<T> & RequestBodyOption<T> & {
73
+ baseUrl?: string;
74
+ querySerializer?: QuerySerializerOptions | QuerySerializer<T> | undefined;
75
+ bodySerializer?: BodySerializer<T> | undefined;
76
+ parseAs?: ParseAs;
77
+ fetch?: ClientOptions["fetch"];
78
+ headers?: HeadersOptions;
79
+ } & {
80
+ headers: Headers;
81
+ cache?: RequestCache | undefined;
82
+ credentials?: RequestCredentials | undefined;
83
+ integrity?: string | undefined;
84
+ keepalive?: boolean | undefined;
85
+ method?: string | undefined;
86
+ mode?: RequestMode | undefined;
87
+ priority?: RequestPriority | undefined;
88
+ redirect?: RequestRedirect | undefined;
89
+ referrer?: string | undefined;
90
+ referrerPolicy?: ReferrerPolicy | undefined;
91
+ signal?: (AbortSignal | null) | undefined;
92
+ window?: null | undefined;
93
+ };
33
94
 
34
- declare type EffectorClientOptions = {
35
- createContract?: (method: HttpMethod, path: string) => Contract<unknown, unknown>;
95
+ declare type OpenapiEffectorClient<Paths extends {}> = {
96
+ createApiEffect: CreateApiEffect<Paths>;
36
97
  };
37
98
 
38
- declare type InitOrPrependInit<Init, PrependInit, Options extends CreateApiEffectOptions<Init>> = Options extends {
39
- mapParams: (init: any) => any;
40
- } ? PrependInit : Init;
99
+ declare type OptionalParams<T> = HasRequiredKeys<T> extends true ? T | void : T;
41
100
 
42
- export declare function isApiError(args: WithError): args is WithError<ApiError>;
101
+ /** Given an OpenAPI **Paths Object**, find all paths that have the given method */
102
+ declare type PathsWithMethod<Paths extends {}, PathnameMethod extends HttpMethod> = {
103
+ [Pathname in keyof Paths]: Paths[Pathname] extends {
104
+ [K in PathnameMethod]: any;
105
+ } ? Pathname : never;
106
+ }[keyof Paths];
43
107
 
44
- declare interface OpenapiEffectorClient<Paths extends {}, Media extends MediaType = MediaType> {
45
- createApiEffect: CreateApiEffect<Paths, Media>;
46
- createApiEffectWithContract: CreateApiEffectWithContract<Paths, Media>;
47
- }
108
+ /** Return `responses` for an Operation Object */
109
+ declare type ResponseObjectMap<T> = T extends {
110
+ responses: any;
111
+ } ? T['responses'] : unknown;
48
112
 
49
113
  declare type WithError<T = any, P = Record<string, unknown>> = P & {
50
114
  error: T;
@@ -1,50 +1,114 @@
1
+ import { BodySerializer } from 'openapi-fetch';
1
2
  import { Client } from 'openapi-fetch';
3
+ import { ClientOptions } from 'openapi-fetch';
2
4
  import { Contract } from '@farfetched/core';
3
5
  import { Effect } from 'effector';
4
6
  import { FarfetchedError } from '@farfetched/core';
7
+ import { FetchOptions } from 'openapi-fetch';
5
8
  import { FetchResponse } from 'openapi-fetch';
9
+ import { HeadersOptions } from 'openapi-fetch';
6
10
  import { HttpError } from '@farfetched/core';
7
- import { HttpMethod } from 'openapi-typescript-helpers';
8
11
  import { MaybeOptionalInit } from 'openapi-fetch';
9
- import { MediaType } from 'openapi-typescript-helpers';
10
12
  import { NetworkError } from '@farfetched/core';
11
- import { PathsWithMethod } from 'openapi-typescript-helpers';
13
+ import { ParamsOption } from 'openapi-fetch';
14
+ import { ParseAs } from 'openapi-fetch';
15
+ import { QuerySerializer } from 'openapi-fetch';
16
+ import { QuerySerializerOptions } from 'openapi-fetch';
17
+ import { RequestBodyOption } from 'openapi-fetch';
18
+ import { Store } from 'effector';
12
19
 
13
20
  declare const API = "API";
14
21
 
15
- export declare interface ApiError<Status extends number = number, ApiResponse = unknown> extends FarfetchedError<typeof API> {
16
- status: Status;
22
+ declare type ApiError<S extends ErrorStatus, T> = {
23
+ status: S;
17
24
  statusText: string;
18
- response: ApiResponse;
19
- }
25
+ response: T;
26
+ } & FarfetchedError<typeof API>;
20
27
 
21
- declare type CreateApiEffect<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, Options extends CreateApiEffectOptions<Init> = {}>(method: Method, path: Path, options?: Options) => Effect<Init extends undefined ? void : InitOrPrependInit<Init, Parameters<NonNullable<Options["mapParams"]>>[0], Options>, Response["data"], ApiError<number, Response["error"]> | HttpError | NetworkError>;
28
+ declare type ApiErrors<Responses extends Record<string | number, any>> = {
29
+ [K in keyof Responses]: K extends ErrorStatus ? Responses[K] extends {
30
+ content: Record<string, any>;
31
+ } ? ApiError<K, FilterKeys<Responses[K]['content'], `${string}/json`>> : never : never;
32
+ }[keyof Responses & ErrorStatus];
22
33
 
23
- declare type CreateApiEffectOptions<Init> = {
24
- mapParams?: (init: any) => Init;
34
+ declare type CreateApiEffect<Paths extends Record<string, Record<HttpMethod, {}>>> = {
35
+ <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Operation extends Paths[Path][Method], Responses extends ResponseObjectMap<Operation>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, `${string}/json`>>>(method: Method, path: Path): {
36
+ effect: Effect<OptionalParams<Init>, Response['data'], ApiErrors<Responses> | HttpError | NetworkError>;
37
+ contract: Contract<unknown, Response['data']>;
38
+ };
39
+ <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Operation extends Paths[Path][Method], Responses extends ResponseObjectMap<Operation>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, `${string}/json`>>, Source, NewInit = Init>(method: Method, path: Path, options: {
40
+ mapParams: {
41
+ source: Store<Source>;
42
+ fn: (source: Source, params: NewInit) => Init;
43
+ };
44
+ }): {
45
+ effect: Effect<OptionalParams<NewInit>, Response['data'], ApiErrors<Responses> | HttpError | NetworkError>;
46
+ contract: Contract<unknown, Response['data']>;
47
+ };
48
+ <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Operation extends Paths[Path][Method], Responses extends ResponseObjectMap<Operation>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, `${string}/json`>>, NewInit>(method: Method, path: Path, options: {
49
+ mapParams: (params: NewInit) => Init;
50
+ }): {
51
+ effect: Effect<OptionalParams<NewInit>, Response['data'], ApiErrors<Responses> | HttpError | NetworkError>;
52
+ contract: Contract<unknown, Response['data']>;
53
+ };
25
54
  };
26
55
 
27
- declare type CreateApiEffectWithContract<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>, Options extends CreateApiEffectOptions<Init> = {}>(method: Method, path: Path, options?: Options) => {
28
- effect: Effect<Init extends undefined ? void : InitOrPrependInit<Init, Parameters<NonNullable<Options["mapParams"]>>[0], Options>, Response["data"], ApiError<number, Response["error"]> | HttpError | NetworkError>;
29
- contract: Contract<unknown, Response["data"]>;
30
- };
56
+ export declare function createEffectorClient<Paths extends {}>(client: Client<Paths>, options?: {
57
+ createContract?: <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>>(method: Method, path: Path) => Contract<unknown, unknown>;
58
+ }): OpenapiEffectorClient<Paths>;
59
+
60
+ /** 2XX statuses */
61
+ declare type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | '5XX' | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 444 | 450 | 451 | 497 | 498 | 499 | '4XX' | "default";
62
+
63
+ /** Find first match of multiple keys */
64
+ declare type FilterKeys<Obj, Matchers> = Obj[keyof Obj & Matchers];
65
+
66
+ declare type HasRequiredKeys<T> = {} extends T ? true : false;
31
67
 
32
- export declare function createClient<Paths extends {}, Media extends MediaType = MediaType>(client: Client<Paths, Media>, config?: EffectorClientOptions): OpenapiEffectorClient<Paths, Media>;
68
+ declare type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace';
69
+
70
+ export declare function isApiError<S extends ErrorStatus, R>(args: WithError): args is WithError<ApiError<S, R>>;
71
+
72
+ export declare function mergeInitHeaders<T>(init: FetchOptions<T>, headers: Record<string, any>): ParamsOption<T> & RequestBodyOption<T> & {
73
+ baseUrl?: string;
74
+ querySerializer?: QuerySerializerOptions | QuerySerializer<T> | undefined;
75
+ bodySerializer?: BodySerializer<T> | undefined;
76
+ parseAs?: ParseAs;
77
+ fetch?: ClientOptions["fetch"];
78
+ headers?: HeadersOptions;
79
+ } & {
80
+ headers: Headers;
81
+ cache?: RequestCache | undefined;
82
+ credentials?: RequestCredentials | undefined;
83
+ integrity?: string | undefined;
84
+ keepalive?: boolean | undefined;
85
+ method?: string | undefined;
86
+ mode?: RequestMode | undefined;
87
+ priority?: RequestPriority | undefined;
88
+ redirect?: RequestRedirect | undefined;
89
+ referrer?: string | undefined;
90
+ referrerPolicy?: ReferrerPolicy | undefined;
91
+ signal?: (AbortSignal | null) | undefined;
92
+ window?: null | undefined;
93
+ };
33
94
 
34
- declare type EffectorClientOptions = {
35
- createContract?: (method: HttpMethod, path: string) => Contract<unknown, unknown>;
95
+ declare type OpenapiEffectorClient<Paths extends {}> = {
96
+ createApiEffect: CreateApiEffect<Paths>;
36
97
  };
37
98
 
38
- declare type InitOrPrependInit<Init, PrependInit, Options extends CreateApiEffectOptions<Init>> = Options extends {
39
- mapParams: (init: any) => any;
40
- } ? PrependInit : Init;
99
+ declare type OptionalParams<T> = HasRequiredKeys<T> extends true ? T | void : T;
41
100
 
42
- export declare function isApiError(args: WithError): args is WithError<ApiError>;
101
+ /** Given an OpenAPI **Paths Object**, find all paths that have the given method */
102
+ declare type PathsWithMethod<Paths extends {}, PathnameMethod extends HttpMethod> = {
103
+ [Pathname in keyof Paths]: Paths[Pathname] extends {
104
+ [K in PathnameMethod]: any;
105
+ } ? Pathname : never;
106
+ }[keyof Paths];
43
107
 
44
- declare interface OpenapiEffectorClient<Paths extends {}, Media extends MediaType = MediaType> {
45
- createApiEffect: CreateApiEffect<Paths, Media>;
46
- createApiEffectWithContract: CreateApiEffectWithContract<Paths, Media>;
47
- }
108
+ /** Return `responses` for an Operation Object */
109
+ declare type ResponseObjectMap<T> = T extends {
110
+ responses: any;
111
+ } ? T['responses'] : unknown;
48
112
 
49
113
  declare type WithError<T = any, P = Record<string, unknown>> = P & {
50
114
  error: T;
@@ -1,59 +1,67 @@
1
- import { createEffect as m } from "effector";
2
- import { networkError as E, httpError as T } from "@farfetched/core";
3
- const l = "API";
4
- function x(t) {
1
+ import { networkError as g, httpError as p } from "@farfetched/core";
2
+ import { createStore as I, attach as C } from "effector";
3
+ import { mergeHeaders as q } from "openapi-fetch";
4
+ const x = "API";
5
+ function R(t) {
6
+ var e;
7
+ return ((e = t.error) == null ? void 0 : e.errorType) === x;
8
+ }
9
+ function U(t, e) {
10
+ return { createApiEffect: (f, m, r) => {
11
+ var h, w;
12
+ const E = f.toUpperCase(), P = t[E], d = ((h = r == null ? void 0 : r.mapParams) == null ? void 0 : h.source) ?? I(null), o = !!(r != null && r.mapParams), l = !!(r != null && r.mapParams.fn), i = o ? l ? r.mapParams.fn : r.mapParams : null;
13
+ return {
14
+ effect: C({
15
+ source: d,
16
+ effect: async (A, s) => {
17
+ let c = s;
18
+ o && (l ? c = i(A, s) : c = i(s));
19
+ const { data: T, error: u, response: a } = await P(m, c).catch((n) => {
20
+ throw g({
21
+ reason: (n == null ? void 0 : n.message) ?? null,
22
+ cause: n
23
+ });
24
+ });
25
+ if (u != null && u !== "")
26
+ throw y({
27
+ status: a.status,
28
+ statusText: a.statusText,
29
+ response: u
30
+ });
31
+ if (!a.ok)
32
+ throw p({
33
+ status: a.status,
34
+ statusText: a.statusText,
35
+ response: null
36
+ });
37
+ return T;
38
+ }
39
+ }),
40
+ contract: ((w = e == null ? void 0 : e.createContract) == null ? void 0 : w.call(e, f, m)) ?? k()
41
+ };
42
+ } };
43
+ }
44
+ function y(t) {
5
45
  return {
6
46
  ...t,
7
- errorType: l,
8
- explanation: "Request was finished with unsuccessful HTTP code"
47
+ errorType: x,
48
+ explanation: "Request was finished with unsuccessful API response"
9
49
  };
10
50
  }
11
- function P(t) {
12
- var e;
13
- return ((e = t.error) == null ? void 0 : e.errorType) === l;
14
- }
15
- function g(t, e = {}) {
16
- const f = (a, s, r) => {
17
- const h = a.toUpperCase(), p = t[h];
18
- return m(async (u) => {
19
- var i;
20
- const { data: w, error: o, response: n } = await p(
21
- s,
22
- ((i = r == null ? void 0 : r.mapParams) == null ? void 0 : i.call(r, u)) ?? u
23
- ).catch((c) => {
24
- throw E({
25
- reason: (c == null ? void 0 : c.message) ?? null,
26
- cause: c
27
- });
28
- });
29
- if (o != null && o !== "")
30
- throw x({
31
- status: n.status,
32
- statusText: n.statusText,
33
- response: o
34
- });
35
- if (!n.ok)
36
- throw T({
37
- status: n.status,
38
- statusText: n.statusText,
39
- response: null
40
- });
41
- return w;
42
- });
51
+ function _(t, e) {
52
+ return {
53
+ ...t,
54
+ headers: q(t.headers, e)
43
55
  };
56
+ }
57
+ function k() {
44
58
  return {
45
- createApiEffect: f,
46
- createApiEffectWithContract: (a, s, r) => {
47
- if (e.createContract == null)
48
- throw new Error("'createContract' is missing in config");
49
- return {
50
- effect: f(a, s, r),
51
- contract: e.createContract(a, s.toString())
52
- };
53
- }
59
+ isData: (t) => !0,
60
+ getErrorMessages: () => []
54
61
  };
55
62
  }
56
63
  export {
57
- g as createClient,
58
- P as isApiError
64
+ U as createEffectorClient,
65
+ R as isApiError,
66
+ _ as mergeInitHeaders
59
67
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openapi-ff",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "test": "vitest run --typecheck",
@@ -31,7 +31,7 @@
31
31
  "@biomejs/biome": "^1.9.4",
32
32
  "@farfetched/zod": "^0.12.8",
33
33
  "@types/node": "^20.12.7",
34
- "typescript": "5.1.6",
34
+ "typescript": "5.8.3",
35
35
  "undici": "^6.21.0",
36
36
  "vite": "5",
37
37
  "vite-plugin-dts": "^3.8.3",
@@ -44,7 +44,6 @@
44
44
  "effector": "^23.2.3"
45
45
  },
46
46
  "dependencies": {
47
- "openapi-fetch": "^0.13.0",
48
- "openapi-typescript-helpers": "^0.0.15"
47
+ "openapi-fetch": "^0.14.0"
49
48
  }
50
49
  }