Skip to content

Commit 1fe40fc

Browse files
committed
Generate methods
1 parent 1b3d888 commit 1fe40fc

23 files changed

+1749
-33
lines changed

generate-routes.ts

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolve } from 'node:path'
44
import { openapi } from '@seamapi/types/connect'
55
import { camelCase, paramCase, pascalCase, snakeCase } from 'change-case'
66
import { ESLint } from 'eslint'
7+
import pluralize from 'pluralize'
78
import { format, resolveConfig } from 'prettier'
89

910
const rootClassPath = resolve('src', 'lib', 'seam', 'connect', 'client.ts')
@@ -49,36 +50,17 @@ interface Endpoint {
4950
name: string
5051
path: string
5152
namespace: string
52-
resource: string
53-
method: 'GET' | 'POST'
53+
resource: string | null
54+
method: Method
5455
requestFormat: 'params' | 'body'
5556
}
5657

58+
type Method = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH'
59+
5760
interface ClassMeta {
5861
constructors: string
5962
}
6063

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-
8264
const createRoutes = (): Route[] => {
8365
const paths = Object.keys(openapi.paths)
8466

@@ -102,13 +84,75 @@ const createRoutes = (): Route[] => {
10284
}
10385

10486
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+
10593
return {
106-
namespace: routePath.split('/').join('_').slice(1),
107-
endpoints: [],
94+
namespace,
95+
endpoints: endpointPaths.map((endpointPath) =>
96+
createEndpoint(namespace, routePath, endpointPath),
97+
),
10898
}
10999
}
110100

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
112156

113157
const renderRoute = (route: Route, { constructors }: ClassMeta): string => `
114158
/*
@@ -125,7 +169,7 @@ ${renderExports(route)}
125169

126170
const renderImports = (): string =>
127171
`
128-
import type { RouteRequestParams, RouteResponse } from '@seamapi/types/connect'
172+
import type { RouteRequestParams, RouteResponse, RouteRequestBody } from '@seamapi/types/connect'
129173
import { Axios } from 'axios'
130174
import type { SetNonNullable } from 'type-fest'
131175
@@ -173,9 +217,15 @@ const renderClassMethod = ({
173217
name,
174218
namespace,
175219
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({
179229
name,
180230
namespace,
181231
})}>({
@@ -184,7 +234,7 @@ const renderClassMethod = ({
184234
requestFormat === 'params' ? 'params,' : ''
185235
} ${requestFormat === 'body' ? 'data: body,' : ''}
186236
})
187-
return data.${resource}
237+
${resource === null ? '' : `return data.${resource}`}
188238
}
189239
`
190240

@@ -298,4 +348,4 @@ const writeRoute = async (route: Route): Promise<void> => {
298348
)
299349
}
300350

301-
await Promise.all(routes.map(writeRoute))
351+
await Promise.all(createRoutes().map(writeRoute))

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"@seamapi/types": "^1.14.0",
9191
"@types/eslint": "^8.44.2",
9292
"@types/node": "^18.11.18",
93+
"@types/pluralize": "^0.0.31",
9394
"ava": "^5.0.1",
9495
"c8": "^8.0.0",
9596
"change-case": "^4.1.2",
@@ -102,6 +103,7 @@
102103
"eslint-plugin-simple-import-sort": "^10.0.0",
103104
"eslint-plugin-unused-imports": "^3.0.0",
104105
"landlubber": "^1.0.0",
106+
"pluralize": "^8.0.0",
105107
"prettier": "^3.0.0",
106108
"tsc-alias": "^1.8.2",
107109
"tsup": "^7.2.0",

src/lib/seam/connect/legacy/workspaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export class SeamHttpLegacyWorkspaces {
1010
}
1111

1212
async get(
13-
params: WorkspacesGetParams = {},
13+
params: WorkspacesGetParams,
1414
): Promise<WorkspacesGetResponse['workspace']> {
1515
const { data } = await this.client.request<WorkspacesGetResponse>({
1616
url: '/workspaces/get',

src/lib/seam/connect/routes/access-codes-unmanaged.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
* Do not edit this file or add other files to this directory.
44
*/
55

6+
import type { RouteRequestBody, RouteResponse } from '@seamapi/types/connect'
67
import type { Axios } from 'axios'
8+
import type { SetNonNullable } from 'type-fest'
79

810
import { createAxiosClient } from 'lib/seam/connect/axios.js'
911
import {
@@ -61,4 +63,95 @@ export class SeamHttpAccessCodesUnmanaged {
6163
}
6264
return new SeamHttpAccessCodesUnmanaged(opts)
6365
}
66+
67+
async convertToManaged(
68+
body: AccessCodesUnmanagedConvertToManagedBody,
69+
): Promise<void> {
70+
await this.client.request<AccessCodesUnmanagedConvertToManagedResponse>({
71+
url: '/access_codes/unmanaged/convert_to_managed',
72+
method: 'patch',
73+
data: body,
74+
})
75+
}
76+
77+
async delete(body: AccessCodesUnmanagedDeleteBody): Promise<void> {
78+
await this.client.request<AccessCodesUnmanagedDeleteResponse>({
79+
url: '/access_codes/unmanaged/delete',
80+
method: 'post',
81+
data: body,
82+
})
83+
}
84+
85+
async get(
86+
body: AccessCodesUnmanagedGetBody,
87+
): Promise<AccessCodesUnmanagedGetResponse['access_code']> {
88+
const { data } = await this.client.request<AccessCodesUnmanagedGetResponse>(
89+
{
90+
url: '/access_codes/unmanaged/get',
91+
method: 'post',
92+
data: body,
93+
},
94+
)
95+
return data.access_code
96+
}
97+
98+
async list(
99+
body: AccessCodesUnmanagedListBody,
100+
): Promise<AccessCodesUnmanagedListResponse['access_codes']> {
101+
const { data } =
102+
await this.client.request<AccessCodesUnmanagedListResponse>({
103+
url: '/access_codes/unmanaged/list',
104+
method: 'post',
105+
data: body,
106+
})
107+
return data.access_codes
108+
}
109+
110+
async update(body: AccessCodesUnmanagedUpdateBody): Promise<void> {
111+
await this.client.request<AccessCodesUnmanagedUpdateResponse>({
112+
url: '/access_codes/unmanaged/update',
113+
method: 'patch',
114+
data: body,
115+
})
116+
}
64117
}
118+
119+
type AccessCodesUnmanagedConvertToManagedBody = SetNonNullable<
120+
Required<RouteRequestBody<'/access_codes/unmanaged/convert_to_managed'>>
121+
>
122+
123+
type AccessCodesUnmanagedConvertToManagedResponse = SetNonNullable<
124+
Required<RouteResponse<'/access_codes/unmanaged/convert_to_managed'>>
125+
>
126+
127+
type AccessCodesUnmanagedDeleteBody = SetNonNullable<
128+
Required<RouteRequestBody<'/access_codes/unmanaged/delete'>>
129+
>
130+
131+
type AccessCodesUnmanagedDeleteResponse = SetNonNullable<
132+
Required<RouteResponse<'/access_codes/unmanaged/delete'>>
133+
>
134+
135+
type AccessCodesUnmanagedGetBody = SetNonNullable<
136+
Required<RouteRequestBody<'/access_codes/unmanaged/get'>>
137+
>
138+
139+
type AccessCodesUnmanagedGetResponse = SetNonNullable<
140+
Required<RouteResponse<'/access_codes/unmanaged/get'>>
141+
>
142+
143+
type AccessCodesUnmanagedListBody = SetNonNullable<
144+
Required<RouteRequestBody<'/access_codes/unmanaged/list'>>
145+
>
146+
147+
type AccessCodesUnmanagedListResponse = SetNonNullable<
148+
Required<RouteResponse<'/access_codes/unmanaged/list'>>
149+
>
150+
151+
type AccessCodesUnmanagedUpdateBody = SetNonNullable<
152+
Required<RouteRequestBody<'/access_codes/unmanaged/update'>>
153+
>
154+
155+
type AccessCodesUnmanagedUpdateResponse = SetNonNullable<
156+
Required<RouteResponse<'/access_codes/unmanaged/update'>>
157+
>

0 commit comments

Comments
 (0)