@@ -4,6 +4,7 @@ import { resolve } from 'node:path'
4
4
import { openapi } from '@seamapi/types/connect'
5
5
import { camelCase , paramCase , pascalCase , snakeCase } from 'change-case'
6
6
import { ESLint } from 'eslint'
7
+ import pluralize from 'pluralize'
7
8
import { format , resolveConfig } from 'prettier'
8
9
9
10
const rootClassPath = resolve ( 'src' , 'lib' , 'seam' , 'connect' , 'client.ts' )
@@ -49,36 +50,17 @@ interface Endpoint {
49
50
name : string
50
51
path : string
51
52
namespace : string
52
- resource : string
53
- method : 'GET' | 'POST'
53
+ resource : string | null
54
+ method : Method
54
55
requestFormat : 'params' | 'body'
55
56
}
56
57
58
+ type Method = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH'
59
+
57
60
interface ClassMeta {
58
61
constructors : string
59
62
}
60
63
61
- const exampleRoute : Route = {
62
- namespace : 'workspaces' ,
63
- endpoints : [
64
- {
65
- name : 'get' ,
66
- namespace : 'workspaces' ,
67
- path : '/workspaces/get' ,
68
- method : 'GET' ,
69
- resource : 'workspace' ,
70
- requestFormat : [ 'GET' , 'DELETE' ] . includes ( 'GET' ) ? 'params' : 'body' ,
71
- } ,
72
- ] ,
73
- }
74
-
75
- const isEndpointUnderRoute = (
76
- endpointPath : string ,
77
- routePath : string ,
78
- ) : boolean =>
79
- endpointPath . startsWith ( routePath ) &&
80
- endpointPath . split ( '/' ) . length - 1 === routePath . split ( '/' ) . length
81
-
82
64
const createRoutes = ( ) : Route [ ] => {
83
65
const paths = Object . keys ( openapi . paths )
84
66
@@ -102,13 +84,75 @@ const createRoutes = (): Route[] => {
102
84
}
103
85
104
86
const createRoute = ( routePath : string ) : Route => {
87
+ const endpointPaths = Object . keys ( openapi . paths ) . filter ( ( path ) =>
88
+ isEndpointUnderRoute ( path , routePath ) ,
89
+ )
90
+
91
+ const namespace = routePath . split ( '/' ) . join ( '_' ) . slice ( 1 )
92
+
105
93
return {
106
- namespace : routePath . split ( '/' ) . join ( '_' ) . slice ( 1 ) ,
107
- endpoints : [ ] ,
94
+ namespace,
95
+ endpoints : endpointPaths . map ( ( endpointPath ) =>
96
+ createEndpoint ( namespace , routePath , endpointPath ) ,
97
+ ) ,
108
98
}
109
99
}
110
100
111
- const routes = createRoutes ( )
101
+ const createEndpoint = (
102
+ namespace : string ,
103
+ routePath : string ,
104
+ endpointPath : string ,
105
+ ) : Endpoint => {
106
+ if ( ! isOpenApiPath ( endpointPath ) ) {
107
+ throw new Error ( `Did not find ${ endpointPath } in OpenAPI spec` )
108
+ }
109
+ const spec = openapi . paths [ endpointPath ]
110
+ const method = deriveSemanticMethod ( Object . keys ( spec ) )
111
+ const name = endpointPath . split ( routePath ) [ 1 ] ?. slice ( 1 )
112
+ if ( name == null ) {
113
+ throw new Error ( `Could not parse name from ${ endpointPath } ` )
114
+ }
115
+ return {
116
+ name,
117
+ namespace,
118
+ path : endpointPath ,
119
+ method,
120
+ resource : deriveResource ( routePath , name , method ) ,
121
+ requestFormat : [ 'GET' , 'DELETE' ] . includes ( method ) ? 'params' : 'body' ,
122
+ }
123
+ }
124
+
125
+ const deriveResource = (
126
+ routePath : string ,
127
+ name : string ,
128
+ method : Method ,
129
+ ) : string | null => {
130
+ if ( [ 'DELETE' , 'PATCH' , 'PUT' ] . includes ( method ) ) return null
131
+ if ( [ 'update' , 'delete' ] . includes ( name ) ) return null
132
+ const group = routePath . split ( '/' ) [ 1 ]
133
+ if ( group == null ) throw new Error ( `Could not parse group from ${ routePath } ` )
134
+ if ( name === 'list' ) return group
135
+ return pluralize . singular ( group )
136
+ }
137
+
138
+ const deriveSemanticMethod = ( methods : string [ ] ) : Method => {
139
+ if ( methods . includes ( 'get' ) ) return 'GET'
140
+ if ( methods . includes ( 'delete' ) ) return 'DELETE'
141
+ if ( methods . includes ( 'patch' ) ) return 'PATCH'
142
+ if ( methods . includes ( 'put' ) ) return 'PUT'
143
+ if ( methods . includes ( 'post' ) ) return 'POST'
144
+ throw new Error ( `Could not find valid method in ${ methods . join ( ', ' ) } ` )
145
+ }
146
+
147
+ const isOpenApiPath = ( key : string ) : key is keyof typeof openapi . paths =>
148
+ key in openapi . paths
149
+
150
+ const isEndpointUnderRoute = (
151
+ endpointPath : string ,
152
+ routePath : string ,
153
+ ) : boolean =>
154
+ endpointPath . startsWith ( routePath ) &&
155
+ endpointPath . split ( '/' ) . length - 1 === routePath . split ( '/' ) . length
112
156
113
157
const renderRoute = ( route : Route , { constructors } : ClassMeta ) : string => `
114
158
/*
@@ -125,7 +169,7 @@ ${renderExports(route)}
125
169
126
170
const renderImports = ( ) : string =>
127
171
`
128
- import type { RouteRequestParams, RouteResponse } from '@seamapi/types/connect'
172
+ import type { RouteRequestParams, RouteResponse, RouteRequestBody } from '@seamapi/types/connect'
129
173
import { Axios } from 'axios'
130
174
import type { SetNonNullable } from 'type-fest'
131
175
@@ -173,9 +217,15 @@ const renderClassMethod = ({
173
217
name,
174
218
namespace,
175
219
requestFormat,
176
- } ) } = {},
177
- ): Promise<${ renderResponseType ( { name, namespace } ) } ['${ resource } ']> {
178
- const { data } = await this.client.request<${ renderResponseType ( {
220
+ } ) } ,
221
+ ): Promise<${
222
+ resource === null
223
+ ? 'void'
224
+ : `${ renderResponseType ( { name, namespace } ) } ['${ resource } ']`
225
+ } > {
226
+ ${
227
+ resource === null ? '' : 'const { data } = '
228
+ } await this.client.request<${ renderResponseType ( {
179
229
name,
180
230
namespace,
181
231
} ) } >({
@@ -184,7 +234,7 @@ const renderClassMethod = ({
184
234
requestFormat === 'params' ? 'params,' : ''
185
235
} ${ requestFormat === 'body' ? 'data: body,' : '' }
186
236
})
187
- return data.${ resource }
237
+ ${ resource === null ? '' : ` return data.${ resource } ` }
188
238
}
189
239
`
190
240
@@ -298,4 +348,4 @@ const writeRoute = async (route: Route): Promise<void> => {
298
348
)
299
349
}
300
350
301
- await Promise . all ( routes . map ( writeRoute ) )
351
+ await Promise . all ( createRoutes ( ) . map ( writeRoute ) )
0 commit comments