Skip to content

Commit 62939dd

Browse files
committed
Add fromClient static method
1 parent 12671b0 commit 62939dd

24 files changed

+1050
-192
lines changed

generate-routes.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { writeFile } from 'node:fs/promises'
1+
import { readFile, writeFile } from 'node:fs/promises'
22
import { resolve } from 'node:path'
33

44
import { openapi } from '@seamapi/types/connect'
55
import { camelCase, paramCase, pascalCase, snakeCase } from 'change-case'
66
import { ESLint } from 'eslint'
77
import { format, resolveConfig } from 'prettier'
88

9+
const rootClassPath = resolve('src', 'lib', 'seam', 'connect', 'client.ts')
910
const routeOutputPath = resolve('src', 'lib', 'seam', 'connect', 'routes')
1011

1112
const routePaths: string[] = [
@@ -53,6 +54,10 @@ interface Endpoint {
5354
requestFormat: 'params' | 'body'
5455
}
5556

57+
interface ClassMeta {
58+
constructors: string
59+
}
60+
5661
const exampleRoute: Route = {
5762
namespace: 'workspaces',
5863
endpoints: [
@@ -105,15 +110,15 @@ const createRoute = (routePath: string): Route => {
105110

106111
const routes = createRoutes()
107112

108-
const renderRoute = (route: Route): string => `
113+
const renderRoute = (route: Route, { constructors }: ClassMeta): string => `
109114
/*
110115
* Automatically generated by generate-routes.ts.
111116
* Do not edit this file or add other files to this directory.
112117
*/
113118
114119
${renderImports()}
115120
116-
${renderClass(route)}
121+
${renderClass(route, { constructors })}
117122
118123
${renderExports(route)}
119124
`
@@ -125,24 +130,31 @@ import { Axios } from 'axios'
125130
import type { SetNonNullable } from 'type-fest'
126131
127132
import { createAxiosClient } from 'lib/seam/connect/axios.js'
128-
import type { SeamHttpOptions } from 'lib/seam/connect/client-options.js'
133+
import {
134+
isSeamHttpOptionsWithApiKey,
135+
isSeamHttpOptionsWithClient,
136+
isSeamHttpOptionsWithClientSessionToken,
137+
SeamHttpInvalidOptionsError,
138+
type SeamHttpOptions,
139+
type SeamHttpOptionsWithApiKey,
140+
type SeamHttpOptionsWithClient,
141+
type SeamHttpOptionsWithClientSessionToken,
142+
} from 'lib/seam/connect/client-options.js'
129143
import { parseOptions } from 'lib/seam/connect/parse-options.js'
130144
`
131145

132-
const renderClass = ({ namespace, endpoints }: Route): string =>
146+
const renderClass = (
147+
{ namespace, endpoints }: Route,
148+
{ constructors }: ClassMeta,
149+
): string =>
133150
`
134151
export class SeamHttp${pascalCase(namespace)} {
135152
client: Axios
136153
137-
constructor(apiKeyOrOptionsOrClient: Axios | string | SeamHttpOptions) {
138-
if (apiKeyOrOptionsOrClient instanceof Axios) {
139-
this.client = apiKeyOrOptionsOrClient
140-
return
141-
}
142-
143-
const options = parseOptions(apiKeyOrOptionsOrClient)
144-
this.client = createAxiosClient(options)
145-
}
154+
${constructors
155+
.replace(/.*this\.#legacy.*\n/, '')
156+
.replaceAll(': SeamHttp ', `: SeamHttp${pascalCase(namespace)} `)
157+
.replaceAll('new SeamHttp(', `new SeamHttp${pascalCase(namespace)}(`)}
146158
147159
${endpoints.map(renderClassMethod).join('\n')}
148160
}
@@ -222,6 +234,24 @@ const write = async (data: string, ...path: string[]): Promise<void> => {
222234
await writeFile(filePath, prettyOutput)
223235
}
224236

237+
const getClassConstructors = (data: string): string => {
238+
const lines = data.split('\n')
239+
240+
const startIdx = lines.findIndex((line) =>
241+
line.trim().startsWith('constructor('),
242+
)
243+
if (startIdx === -1) {
244+
throw new Error('Could not find start of class constructors')
245+
}
246+
247+
const endIdx = lines.findIndex((line) => line.trim().startsWith('get '))
248+
if (endIdx === -1) {
249+
throw new Error('Could not find end of class constructors')
250+
}
251+
252+
return lines.slice(startIdx, endIdx).join('\n')
253+
}
254+
225255
const prettierOutput = async (
226256
data: string,
227257
filepath: string,
@@ -259,8 +289,10 @@ const eslintFixOutput = async (
259289
}
260290

261291
const writeRoute = async (route: Route): Promise<void> => {
292+
const rootClass = await readFile(rootClassPath)
293+
const constructors = getClassConstructors(rootClass.toString())
262294
await write(
263-
renderRoute(route),
295+
renderRoute(route, { constructors }),
264296
routeOutputPath,
265297
`${paramCase(route.namespace)}.ts`,
266298
)

src/lib/seam/connect/client-options.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { AxiosRequestConfig } from 'axios'
1+
import type { Axios, AxiosRequestConfig } from 'axios'
22

33
export type SeamHttpOptions =
4+
| SeamHttpOptionsWithClient
45
| SeamHttpOptionsWithApiKey
56
| SeamHttpOptionsWithClientSessionToken
67

@@ -10,6 +11,31 @@ interface SeamHttpCommonOptions {
1011
enableLegacyMethodBehaivor?: boolean
1112
}
1213

14+
export interface SeamHttpOptionsWithClient
15+
extends Pick<SeamHttpCommonOptions, 'enableLegacyMethodBehaivor'> {
16+
client: Axios
17+
}
18+
19+
export const isSeamHttpOptionsWithClient = (
20+
options: SeamHttpOptions,
21+
): options is SeamHttpOptionsWithClient => {
22+
if (!('client' in options)) return false
23+
if (options.client == null) return false
24+
25+
const keys = Object.keys(options).filter(
26+
(k) => !['client', 'enableLegacyMethodBehaivor'].includes(k),
27+
)
28+
if (keys.length > 0) {
29+
throw new SeamHttpInvalidOptionsError(
30+
`The client option cannot be used with any other option except enableLegacyMethodBehaivor, but received: ${keys.join(
31+
', ',
32+
)}`,
33+
)
34+
}
35+
36+
return true
37+
}
38+
1339
export interface SeamHttpOptionsWithApiKey extends SeamHttpCommonOptions {
1440
apiKey: string
1541
}
@@ -18,6 +44,7 @@ export const isSeamHttpOptionsWithApiKey = (
1844
options: SeamHttpOptions,
1945
): options is SeamHttpOptionsWithApiKey => {
2046
if (!('apiKey' in options)) return false
47+
if (options.apiKey == null) return false
2148

2249
if ('clientSessionToken' in options && options.clientSessionToken != null) {
2350
throw new SeamHttpInvalidOptionsError(
@@ -37,6 +64,7 @@ export const isSeamHttpOptionsWithClientSessionToken = (
3764
options: SeamHttpOptions,
3865
): options is SeamHttpOptionsWithClientSessionToken => {
3966
if (!('clientSessionToken' in options)) return false
67+
if (options.clientSessionToken == null) return false
4068

4169
if ('apiKey' in options && options.apiKey != null) {
4270
throw new SeamHttpInvalidOptionsError(

src/lib/seam/connect/client.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import type { Axios } from 'axios'
33
import { createAxiosClient } from './axios.js'
44
import {
55
isSeamHttpOptionsWithApiKey,
6+
isSeamHttpOptionsWithClient,
67
isSeamHttpOptionsWithClientSessionToken,
78
SeamHttpInvalidOptionsError,
89
type SeamHttpOptions,
910
type SeamHttpOptionsWithApiKey,
11+
type SeamHttpOptionsWithClient,
1012
type SeamHttpOptionsWithClientSessionToken,
1113
} from './client-options.js'
1214
import { SeamHttpLegacyWorkspaces } from './legacy/workspaces.js'
@@ -21,7 +23,19 @@ export class SeamHttp {
2123
constructor(apiKeyOrOptions: string | SeamHttpOptions) {
2224
const options = parseOptions(apiKeyOrOptions)
2325
this.#legacy = options.enableLegacyMethodBehaivor
24-
this.client = createAxiosClient(options)
26+
const client = 'client' in options ? options.client : null
27+
this.client = client ?? createAxiosClient(options)
28+
}
29+
30+
static fromClient(
31+
client: SeamHttpOptionsWithClient['client'],
32+
options: Omit<SeamHttpOptionsWithClient, 'client'> = {},
33+
): SeamHttp {
34+
const opts = { ...options, client }
35+
if (!isSeamHttpOptionsWithClient(opts)) {
36+
throw new SeamHttpInvalidOptionsError('Missing client')
37+
}
38+
return new SeamHttp(opts)
2539
}
2640

2741
static fromApiKey(
@@ -49,15 +63,15 @@ export class SeamHttp {
4963
return new SeamHttp(opts)
5064
}
5165

52-
// TODO
53-
// static fromPublishableKey and deprecate getClientSessionToken
54-
55-
// TODO: Should we keep makeRequest?
56-
// Better to implement error handling and wrapping in an error handler.
57-
// makeRequest
58-
5966
get workspaces(): SeamHttpWorkspaces {
6067
if (this.#legacy) return new SeamHttpLegacyWorkspaces(this.client)
61-
return new SeamHttpWorkspaces(this.client)
68+
return new SeamHttpWorkspaces({ client: this.client })
6269
}
6370
}
71+
72+
// TODO
73+
// static fromPublishableKey and deprecate getClientSessionToken
74+
75+
// TODO: Should we keep makeRequest?
76+
// Better to implement error handling and wrapping in an error handler.
77+
// makeRequest

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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
import type { RouteRequestParams, RouteResponse } from '@seamapi/types/connect'
2-
import { Axios } from 'axios'
2+
import type { Axios } from 'axios'
33
import type { SetNonNullable } from 'type-fest'
44

5-
import { createAxiosClient } from 'lib/seam/connect/axios.js'
6-
import type { SeamHttpOptions } from 'lib/seam/connect/client-options.js'
7-
import { parseOptions } from 'lib/seam/connect/parse-options.js'
8-
95
export class SeamHttpLegacyWorkspaces {
106
client: Axios
117

12-
constructor(apiKeyOrOptionsOrClient: Axios | string | SeamHttpOptions) {
13-
if (apiKeyOrOptionsOrClient instanceof Axios) {
14-
this.client = apiKeyOrOptionsOrClient
15-
return
16-
}
17-
18-
const options = parseOptions(apiKeyOrOptionsOrClient)
19-
this.client = createAxiosClient(options)
8+
constructor(client: Axios) {
9+
this.client = client
2010
}
2111

2212
async get(
@@ -31,6 +21,7 @@ export class SeamHttpLegacyWorkspaces {
3121
}
3222
}
3323

24+
// TODO: Import from routes so no need to redefine here
3425
type WorkspacesGetParams = SetNonNullable<
3526
Required<RouteRequestParams<'/workspaces/get'>>
3627
>

src/lib/seam/connect/parse-options.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { SeamHttpOptions } from './client-options.js'
1+
import {
2+
isSeamHttpOptionsWithClient,
3+
type SeamHttpOptions,
4+
} from './client-options.js'
5+
6+
const enableLegacyMethodBehaivorDefault = true
7+
28
export const parseOptions = (
39
apiKeyOrOptions: string | SeamHttpOptions,
410
): Required<SeamHttpOptions> => {
@@ -7,6 +13,13 @@ export const parseOptions = (
713
? { apiKey: apiKeyOrOptions }
814
: apiKeyOrOptions
915

16+
if (isSeamHttpOptionsWithClient(options))
17+
return {
18+
...options,
19+
enableLegacyMethodBehaivor:
20+
options.enableLegacyMethodBehaivor ?? enableLegacyMethodBehaivorDefault,
21+
}
22+
1023
const endpoint =
1124
options.endpoint ??
1225
globalThis.process?.env?.['SEAM_ENDPOINT'] ??
@@ -23,6 +36,7 @@ export const parseOptions = (
2336
...(apiKey != null ? { apiKey } : {}),
2437
endpoint,
2538
axiosOptions: options.axiosOptions ?? {},
26-
enableLegacyMethodBehaivor: false,
39+
enableLegacyMethodBehaivor:
40+
options.enableLegacyMethodBehaivor ?? enableLegacyMethodBehaivorDefault,
2741
}
2842
}

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

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

6-
import { Axios } from 'axios'
6+
import type { Axios } from 'axios'
77

88
import { createAxiosClient } from 'lib/seam/connect/axios.js'
9-
import type { SeamHttpOptions } from 'lib/seam/connect/client-options.js'
9+
import {
10+
isSeamHttpOptionsWithApiKey,
11+
isSeamHttpOptionsWithClient,
12+
isSeamHttpOptionsWithClientSessionToken,
13+
SeamHttpInvalidOptionsError,
14+
type SeamHttpOptions,
15+
type SeamHttpOptionsWithApiKey,
16+
type SeamHttpOptionsWithClient,
17+
type SeamHttpOptionsWithClientSessionToken,
18+
} from 'lib/seam/connect/client-options.js'
1019
import { parseOptions } from 'lib/seam/connect/parse-options.js'
1120

1221
export class SeamHttpAccessCodesUnmanaged {
1322
client: Axios
1423

15-
constructor(apiKeyOrOptionsOrClient: Axios | string | SeamHttpOptions) {
16-
if (apiKeyOrOptionsOrClient instanceof Axios) {
17-
this.client = apiKeyOrOptionsOrClient
18-
return
24+
constructor(apiKeyOrOptions: string | SeamHttpOptions) {
25+
const options = parseOptions(apiKeyOrOptions)
26+
const client = 'client' in options ? options.client : null
27+
this.client = client ?? createAxiosClient(options)
28+
}
29+
30+
static fromClient(
31+
client: SeamHttpOptionsWithClient['client'],
32+
options: Omit<SeamHttpOptionsWithClient, 'client'> = {},
33+
): SeamHttpAccessCodesUnmanaged {
34+
const opts = { ...options, client }
35+
if (!isSeamHttpOptionsWithClient(opts)) {
36+
throw new SeamHttpInvalidOptionsError('Missing client')
1937
}
38+
return new SeamHttpAccessCodesUnmanaged(opts)
39+
}
2040

21-
const options = parseOptions(apiKeyOrOptionsOrClient)
22-
this.client = createAxiosClient(options)
41+
static fromApiKey(
42+
apiKey: SeamHttpOptionsWithApiKey['apiKey'],
43+
options: Omit<SeamHttpOptionsWithApiKey, 'apiKey'> = {},
44+
): SeamHttpAccessCodesUnmanaged {
45+
const opts = { ...options, apiKey }
46+
if (!isSeamHttpOptionsWithApiKey(opts)) {
47+
throw new SeamHttpInvalidOptionsError('Missing apiKey')
48+
}
49+
return new SeamHttpAccessCodesUnmanaged(opts)
50+
}
51+
52+
static fromClientSessionToken(
53+
clientSessionToken: SeamHttpOptionsWithClientSessionToken['clientSessionToken'],
54+
options: Omit<
55+
SeamHttpOptionsWithClientSessionToken,
56+
'clientSessionToken'
57+
> = {},
58+
): SeamHttpAccessCodesUnmanaged {
59+
const opts = { ...options, clientSessionToken }
60+
if (!isSeamHttpOptionsWithClientSessionToken(opts)) {
61+
throw new SeamHttpInvalidOptionsError('Missing clientSessionToken')
62+
}
63+
return new SeamHttpAccessCodesUnmanaged(opts)
2364
}
2465
}

0 commit comments

Comments
 (0)