api-sdk-gen 1.0.2 → 1.0.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.
package/dist/index.js CHANGED
@@ -19,7 +19,9 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
19
19
  if ("$ref" in schema) {
20
20
  writer.write((refPrefix ?? "") + resolveSchema(schema.$ref));
21
21
  } else if (schema.enum) {
22
- writer.write(schema.enum.map((value) => JSON.stringify(value)).join(" | "));
22
+ writer.write(
23
+ schema.enum.map((value) => JSON.stringify(value)).join(" | ")
24
+ );
23
25
  } else if (schema.anyOf || schema.oneOf) {
24
26
  (schema.anyOf || schema.oneOf)?.forEach((subSchema, index) => {
25
27
  if (index > 0) {
@@ -32,7 +34,9 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
32
34
  case "object": {
33
35
  const requiredProps = new Set(schema.required || []);
34
36
  writer.write("{").newLine();
35
- for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
37
+ for (const [propName, propSchema] of Object.entries(
38
+ schema.properties || {}
39
+ )) {
36
40
  writer.indent(() => {
37
41
  writer.write(propName);
38
42
  if (!requiredProps.has(propName)) {
@@ -66,12 +70,16 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
66
70
  break;
67
71
  case "string": {
68
72
  const s = schema;
73
+ if (s.format === "date-time" || s.format === "date") {
74
+ writer.write("Date | string");
75
+ break;
76
+ }
69
77
  const isFile = s.format === "binary" || s.format === "base64" || s.contentMediaType;
70
78
  if (isFile) {
71
79
  writer.write("File | Blob");
72
- } else {
73
- writer.write(s.type);
80
+ break;
74
81
  }
82
+ writer.write(s.type);
75
83
  break;
76
84
  }
77
85
  case "number":
package/dist/index.mjs CHANGED
@@ -25,7 +25,9 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
25
25
  if ("$ref" in schema) {
26
26
  writer.write((refPrefix ?? "") + resolveSchema(schema.$ref));
27
27
  } else if (schema.enum) {
28
- writer.write(schema.enum.map((value) => JSON.stringify(value)).join(" | "));
28
+ writer.write(
29
+ schema.enum.map((value) => JSON.stringify(value)).join(" | ")
30
+ );
29
31
  } else if (schema.anyOf || schema.oneOf) {
30
32
  (schema.anyOf || schema.oneOf)?.forEach((subSchema, index) => {
31
33
  if (index > 0) {
@@ -38,7 +40,9 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
38
40
  case "object": {
39
41
  const requiredProps = new Set(schema.required || []);
40
42
  writer.write("{").newLine();
41
- for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
43
+ for (const [propName, propSchema] of Object.entries(
44
+ schema.properties || {}
45
+ )) {
42
46
  writer.indent(() => {
43
47
  writer.write(propName);
44
48
  if (!requiredProps.has(propName)) {
@@ -72,12 +76,16 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
72
76
  break;
73
77
  case "string": {
74
78
  const s = schema;
79
+ if (s.format === "date-time" || s.format === "date") {
80
+ writer.write("Date | string");
81
+ break;
82
+ }
75
83
  const isFile = s.format === "binary" || s.format === "base64" || s.contentMediaType;
76
84
  if (isFile) {
77
85
  writer.write("File | Blob");
78
- } else {
79
- writer.write(s.type);
86
+ break;
80
87
  }
88
+ writer.write(s.type);
81
89
  break;
82
90
  }
83
91
  case "number":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-sdk-gen",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -0,0 +1,214 @@
1
+ type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
2
+
3
+ interface BaseOperation {
4
+ parameters?: {
5
+ query?: Record<string, unknown>;
6
+ path?: Record<string, string>;
7
+ header?: Record<string, string>;
8
+ }
9
+ requestBody?: Record<string, any>;
10
+ responses?: Record<string | number, {
11
+ headers?: Record<string, Record<string, any>>;
12
+ content?: Record<string, any>
13
+ }>;
14
+ }
15
+
16
+ type ApiDoc = Record<string, Partial<BaseOperation & Record<HttpMethod, BaseOperation>>>;
17
+
18
+ type BaseRequest = {
19
+ path?: Record<string, string>;
20
+ query?: Record<string, string>;
21
+ header?: Record<string, string>;
22
+ contentType?: string;
23
+ body?: any;
24
+ }
25
+
26
+ type Pretty<T> = {
27
+ [K in keyof T]: T[K];
28
+ };
29
+
30
+ type DeepPretty<T> = T extends object ? Pretty<{
31
+ [K in keyof T]: DeepPretty<T[K]>;
32
+ }> : T;
33
+
34
+ type GetOp<T extends ApiDoc, P extends keyof T, M extends keyof T[P] & HttpMethod> =
35
+ T[P][M] extends infer Op ? (Op extends BaseOperation ? Op : never) : never;
36
+
37
+ type RequiredKeys<T> = { [K in keyof T]-?: (Record<string, never>) extends Pick<T, K> ? never : K }[keyof T];
38
+
39
+ type OptionalIfEmpty<K extends string, T> =
40
+ [T] extends [never] ? unknown :
41
+ [T] extends [undefined] ? unknown :
42
+ RequiredKeys<T> extends never
43
+ ? (keyof T extends never ? unknown : { [P in K]?: T })
44
+ : { [P in K]: T };
45
+
46
+ type OpBody<Op extends BaseOperation> =
47
+ Op['requestBody'] extends infer RB
48
+ ? RB extends Record<string, unknown>
49
+ ? { [K in keyof RB]: {
50
+ contentType: K,
51
+ body: K extends 'multipart/form-data' ? (RB[K] | FormData) : RB[K]
52
+ } }[keyof RB]
53
+ : unknown
54
+ : unknown;
55
+
56
+ type ToNumber<S> = S extends number ? S : S extends `${infer N extends number}` ? N : S;
57
+
58
+ type ApiFetchResponse<Op extends BaseOperation> =
59
+ Op['responses'] extends infer R
60
+ ? R extends Record<string | number, any>
61
+ ? {
62
+ [S in keyof R]: S extends number | string
63
+ ? R[S] extends { content: infer C }
64
+ ? C extends Record<string, any>
65
+ ? { [CT in keyof C]: {
66
+ status: ToNumber<S>,
67
+ contentType: CT,
68
+ body: C[CT]
69
+ } }[keyof C]
70
+ : { status: ToNumber<S>, contentType: null, body: null }
71
+ : { status: ToNumber<S>, contentType: null, body: null }
72
+ : never
73
+ }[keyof R]
74
+ : { status: number; contentType: string; body: any }
75
+ : { status: number; contentType: string; body: any };
76
+
77
+ type ApiFetchOptions<T extends ApiDoc, P extends keyof T, M extends keyof T[P] & HttpMethod, Op = GetOp<T, P, M>> =
78
+ { method: M } &
79
+ (Op extends BaseOperation ?
80
+ OptionalIfEmpty<'path', NonNullable<Op['parameters']>['path']> &
81
+ OptionalIfEmpty<'query', NonNullable<Op['parameters']>['query']> &
82
+ OptionalIfEmpty<'header', NonNullable<Op['parameters']>['header']> &
83
+ OpBody<Op>
84
+ : unknown);
85
+
86
+ type ApiFetch<T extends ApiDoc> = <
87
+ P extends keyof T,
88
+ M extends keyof T[P] & HttpMethod
89
+ >(
90
+ path: P,
91
+ options: ApiFetchOptions<T, P, M>
92
+ ) => Promise<DeepPretty<ApiFetchResponse<GetOp<T, P, M>>>>;
93
+
94
+ export type Request = {
95
+ url: URL;
96
+ init: RequestInit;
97
+ }
98
+
99
+ export function buildApiRequest(
100
+ baseUrl: string | URL,
101
+ path: string,
102
+ options: BaseRequest
103
+ ): Request {
104
+ const headers = new Headers();
105
+ const opts = options as any;
106
+ let body: any = opts.body;
107
+ const url = new URL(path as string, baseUrl);
108
+
109
+ if (opts.header) {
110
+ Object.entries(opts.header).forEach(([key, value]) => {
111
+ if (Array.isArray(value)) {
112
+ value.forEach(v => {
113
+ headers.append(key, v)
114
+ });
115
+ } else {
116
+ headers.set(key, value as string);
117
+ }
118
+ });
119
+ }
120
+
121
+ if (opts.body) {
122
+ headers.append('Content-Type', opts.contentType);
123
+
124
+ if (opts.contentType === 'application/json') {
125
+ body = JSON.stringify(body);
126
+ } else if (opts.contentType === 'multipart/form-data' && !(body instanceof FormData)) {
127
+ const form = new FormData();
128
+
129
+ Object.entries(body).forEach(([key, value]) => {
130
+ if (Array.isArray(value)) {
131
+ value.forEach(v => {
132
+ form.append(key, v)
133
+ });
134
+ } else {
135
+ form.set(key, value as string);
136
+ }
137
+ });
138
+
139
+ body = form;
140
+ } else if (opts.contentType === 'application/x-www-form-urlencoded') {
141
+ body = new URLSearchParams(body);
142
+ }
143
+ }
144
+
145
+ if (opts.path) {
146
+ url.pathname = url.pathname.replace(/\{(.+?)\}/g, (text, key) => key in opts.path ? opts.path[key] : text);
147
+ }
148
+
149
+ if (opts.query) {
150
+ Object.entries(opts.query).forEach(([key, value]) => {
151
+ if (Array.isArray(value)) {
152
+ value.forEach(v => {
153
+ url.searchParams.append(key, v)
154
+ });
155
+ } else {
156
+ url.searchParams.set(key, value as string);
157
+ }
158
+ });
159
+ }
160
+
161
+ return {
162
+ url,
163
+ init: {
164
+ method: opts.method.toUpperCase(),
165
+ headers,
166
+ body,
167
+ }
168
+ };
169
+ }
170
+
171
+ export async function buildResponse<T extends ApiFetchResponse<any>>(response: Response): Promise<T> {
172
+ const contentType = response.headers.get('Content-Type') || null;
173
+
174
+ if (contentType?.startsWith('application/json')) {
175
+ return {
176
+ status: response.status,
177
+ contentType,
178
+ body: await response.json(),
179
+ } as T;
180
+ }
181
+
182
+ if (contentType?.startsWith('application/xml')) {
183
+ return {
184
+ status: response.status,
185
+ contentType,
186
+ body: new DOMParser().parseFromString(await response.text(), 'application/xml'),
187
+ } as T;
188
+ }
189
+
190
+ if (contentType?.startsWith('text/plain')) {
191
+ return {
192
+ status: response.status,
193
+ contentType,
194
+ body: await response.text(),
195
+ } as T;
196
+ }
197
+
198
+ return {
199
+ status: response.status,
200
+ contentType,
201
+ body: await response.blob(),
202
+ } as T;
203
+ }
204
+
205
+ export function createApi<T extends ApiDoc>(
206
+ baseUrl: string | URL,
207
+ adapter?: (request: Request) => Promise<Response>
208
+ ) {
209
+ adapter ??= (request) => fetch(request.url, request.init);
210
+
211
+ const api = async (path: string, options: any) => buildResponse(await adapter(buildApiRequest(baseUrl, path, options)));
212
+
213
+ return api as ApiFetch<T>;
214
+ }
package/templates/core.ts CHANGED
@@ -27,188 +27,5 @@ type Pretty<T> = {
27
27
  [K in keyof T]: T[K];
28
28
  };
29
29
 
30
- type DeepPretty<T> = T extends object ? Pretty<{
31
- [K in keyof T]: DeepPretty<T[K]>;
32
- }> : T;
33
-
34
- type GetOp<T extends ApiDoc, P extends keyof T, M extends keyof T[P] & HttpMethod> =
35
- T[P][M] extends infer Op ? (Op extends BaseOperation ? Op : never) : never;
36
-
37
- type RequiredKeys<T> = { [K in keyof T]-?: (Record<string, never>) extends Pick<T, K> ? never : K }[keyof T];
38
-
39
- type OptionalIfEmpty<K extends string, T> =
40
- [T] extends [never] ? unknown :
41
- [T] extends [undefined] ? unknown :
42
- RequiredKeys<T> extends never
43
- ? (keyof T extends never ? unknown : { [P in K]?: T })
44
- : { [P in K]: T };
45
-
46
- type OpBody<Op extends BaseOperation> =
47
- Op['requestBody'] extends infer RB
48
- ? RB extends Record<string, unknown>
49
- ? { [K in keyof RB]: {
50
- contentType: K,
51
- body: K extends 'multipart/form-data' ? (RB[K] | FormData) : RB[K]
52
- } }[keyof RB]
53
- : unknown
54
- : unknown;
55
-
56
- type ToNumber<S> = S extends number ? S : S extends `${infer N extends number}` ? N : S;
57
-
58
- type ApiFetchResponse<Op extends BaseOperation> =
59
- Op['responses'] extends infer R
60
- ? R extends Record<string | number, any>
61
- ? {
62
- [S in keyof R]: S extends number | string
63
- ? R[S] extends { content: infer C }
64
- ? C extends Record<string, any>
65
- ? { [CT in keyof C]: {
66
- status: ToNumber<S>,
67
- contentType: CT,
68
- body: C[CT]
69
- } }[keyof C]
70
- : { status: ToNumber<S>, contentType: null, body: null }
71
- : { status: ToNumber<S>, contentType: null, body: null }
72
- : never
73
- }[keyof R]
74
- : { status: number; contentType: string; body: any }
75
- : { status: number; contentType: string; body: any };
76
-
77
- type ApiFetchOptions<T extends ApiDoc, P extends keyof T, M extends keyof T[P] & HttpMethod, Op = GetOp<T, P, M>> =
78
- { method: M } &
79
- (Op extends BaseOperation ?
80
- OptionalIfEmpty<'path', NonNullable<Op['parameters']>['path']> &
81
- OptionalIfEmpty<'query', NonNullable<Op['parameters']>['query']> &
82
- OptionalIfEmpty<'header', NonNullable<Op['parameters']>['header']> &
83
- OpBody<Op>
84
- : unknown);
85
-
86
- type ApiFetch<T extends ApiDoc> = <
87
- P extends keyof T,
88
- M extends keyof T[P] & HttpMethod
89
- >(
90
- path: P,
91
- options: ApiFetchOptions<T, P, M>
92
- ) => Promise<DeepPretty<ApiFetchResponse<GetOp<T, P, M>>>>;
93
-
94
- export type Request = {
95
- url: URL;
96
- init: RequestInit;
97
- }
98
-
99
- export function buildApiRequest(
100
- baseUrl: string | URL,
101
- path: string,
102
- options: BaseRequest
103
- ): Request {
104
- const headers = new Headers();
105
- const opts = options as any;
106
- let body: any = opts.body;
107
- const url = new URL(path as string, baseUrl);
108
-
109
- if (opts.header) {
110
- Object.entries(opts.header).forEach(([key, value]) => {
111
- if (Array.isArray(value)) {
112
- value.forEach(v => {
113
- headers.append(key, v)
114
- });
115
- } else {
116
- headers.set(key, value as string);
117
- }
118
- });
119
- }
120
-
121
- if (opts.body) {
122
- headers.append('Content-Type', opts.contentType);
123
-
124
- if (opts.contentType === 'application/json') {
125
- body = JSON.stringify(body);
126
- } else if (opts.contentType === 'multipart/form-data' && !(body instanceof FormData)) {
127
- const form = new FormData();
128
-
129
- Object.entries(body).forEach(([key, value]) => {
130
- if (Array.isArray(value)) {
131
- value.forEach(v => {
132
- form.append(key, v)
133
- });
134
- } else {
135
- form.set(key, value as string);
136
- }
137
- });
138
-
139
- body = form;
140
- } else if (opts.contentType === 'application/x-www-form-urlencoded') {
141
- body = new URLSearchParams(body);
142
- }
143
- }
144
-
145
- if (opts.path) {
146
- url.pathname = url.pathname.replace(/\{(.+?)\}/g, (text, key) => key in opts.path ? opts.path[key] : text);
147
- }
148
-
149
- if (opts.query) {
150
- Object.entries(opts.query).forEach(([key, value]) => {
151
- if (Array.isArray(value)) {
152
- value.forEach(v => {
153
- url.searchParams.append(key, v)
154
- });
155
- } else {
156
- url.searchParams.set(key, value as string);
157
- }
158
- });
159
- }
160
-
161
- return {
162
- url,
163
- init: {
164
- method: opts.method.toUpperCase(),
165
- headers,
166
- body,
167
- }
168
- };
169
- }
170
-
171
- export async function buildResponse<T extends ApiFetchResponse<any>>(response: Response): Promise<T> {
172
- const contentType = response.headers.get('Content-Type') || null;
173
-
174
- if (contentType?.startsWith('application/json')) {
175
- return {
176
- status: response.status,
177
- contentType,
178
- body: await response.json(),
179
- } as T;
180
- }
181
-
182
- if (contentType?.startsWith('application/xml')) {
183
- return {
184
- status: response.status,
185
- contentType,
186
- body: new DOMParser().parseFromString(await response.text(), 'application/xml'),
187
- } as T;
188
- }
189
-
190
- if (contentType?.startsWith('text/plain')) {
191
- return {
192
- status: response.status,
193
- contentType,
194
- body: await response.text(),
195
- } as T;
196
- }
197
-
198
- return {
199
- status: response.status,
200
- contentType,
201
- body: await response.blob(),
202
- } as T;
203
- }
204
-
205
- export function createApi<T extends ApiDoc>(
206
- baseUrl: string | URL,
207
- adapter?: (request: Request) => Promise<Response>
208
- ) {
209
- adapter ??= (request) => fetch(request.url, request.init);
210
-
211
- const api = async (path: string, options: any) => buildResponse(await adapter(buildApiRequest(baseUrl, path, options)));
212
-
213
- return api as ApiFetch<T>;
214
- }
30
+ export type QueryParams<Api extends ApiDoc, Path extends keyof Api, Method extends keyof Api[Path]> = Api[Path][Method] extends { parameters: infer P } ? P : never
31
+ export type ResponseStatus<Api extends ApiDoc, Path extends keyof Api, Method extends keyof Api[Path], Status extends keyof Api[Path][Method]['responses']> = Api[Path][Method]['responses'][Status]