Skip to content

Commit

Permalink
feat(sdk): throw on error and export types in JavaScript SDK (#2180)
Browse files Browse the repository at this point in the history
  • Loading branch information
tothandras authored Jan 30, 2025
1 parent a03739a commit 5a5f911
Show file tree
Hide file tree
Showing 20 changed files with 104 additions and 88 deletions.
2 changes: 1 addition & 1 deletion api/client/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"lint": "eslint . --format=pretty",
"format": "prettier --write .",
"build": "duel",
"generate": "node --experimental-strip-types scripts/generate.ts && prettier --write src/client/schemas.d.ts",
"generate": "node --experimental-strip-types scripts/generate.ts && prettier --write src/client/schemas.ts",
"pretest": "pnpm run build",
"test": "vitest --run",
"test:watch": "vitest --watch",
Expand Down
2 changes: 1 addition & 1 deletion api/client/javascript/scripts/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ const ast = await openapiTS(schema, {

const contents = astToString(ast)

fs.writeFileSync('./src/client/schemas.d.ts', contents)
fs.writeFileSync('./src/client/schemas.ts', contents)
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/apps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type {
AppBaseReplaceUpdate,
CreateStripeCheckoutSessionRequest,
Expand Down
4 changes: 2 additions & 2 deletions api/client/javascript/src/client/billing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type {
BillingProfileCreate,
BillingProfileCustomerOverrideCreate,
Expand All @@ -11,7 +12,6 @@ import type {
VoidInvoiceActionInput,
} from './schemas.js'
import type { Client } from 'openapi-fetch'

/**
* Billing
*/
Expand Down
62 changes: 62 additions & 0 deletions api/client/javascript/src/client/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { UnexpectedProblemResponse } from './schemas.js'

/**
* Request options
*/
export type RequestOptions = Pick<RequestInit, 'signal'>

/**
* An error that occurred during an HTTP request
*/
export class HTTPError extends Error {
name = 'HTTPError'

constructor(
public message: string,
public type: string,
public title: string,
public status: number,
protected __raw?: Record<string, any>
) {
super(message)
}

static fromResponse(resp: {
response: Response
error?: UnexpectedProblemResponse
}): HTTPError {
if (
resp.response.headers.get('Content-Type') ===
'application/problem+json' &&
resp.error
) {
return new HTTPError(
resp.error.detail,
resp.error.type,
resp.error.title,
resp.error.status ?? resp.response.status,
resp.error
)
}

return new HTTPError(
`Request failed: ${resp.response.statusText}`,
resp.response.statusText,
resp.response.statusText,
resp.response.status
)
}

getField(key: string) {
return this.__raw?.[key]
}
}

/**
* Check if an error is an HTTPError
* @param error - The error to check
* @returns Whether the error is an HTTPError
*/
export function isHTTPError(error: unknown): error is HTTPError {
return error instanceof HTTPError
}
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/customers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type {
CustomerAppData,
CustomerCreate,
Expand Down
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/entitlements.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type {
EntitlementCreateInputs,
EntitlementGrantCreateInput,
Expand Down
4 changes: 2 additions & 2 deletions api/client/javascript/src/client/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Events', () => {
}
)
const resp = await client.events.ingest(event)
expect(resp.data).toBeUndefined()
expect(resp).toBeUndefined()
expect(fetchMock.callHistory.done(task.name)).toBeTruthy()
})

Expand Down Expand Up @@ -104,7 +104,7 @@ describe('Events', () => {
}
)
const resp = await client.events.list(query)
expect(resp.data).toEqual(respBody)
expect(resp).toEqual(respBody)
expect(fetchMock.callHistory.done(task.name)).toBeTruthy()
})
})
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import crypto from 'crypto'
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type { operations, paths, Event } from './schemas.js'
import type { Client } from 'openapi-fetch'

Expand Down
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/features.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type { FeatureCreateInputs, operations, paths } from './schemas.js'
import type { Client } from 'openapi-fetch'

Expand Down
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { Subscriptions } from './subscriptions.js'
import { encodeDates } from './utils.js'
import type { paths } from './schemas.js'

export type * from './schemas.js'
export * from './schemas.js'
export * from './common.js'

/**
* OpenMeter Config
Expand Down
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/meters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type { MeterCreate, operations, paths } from './schemas.js'
import type { Client } from 'openapi-fetch'

Expand Down
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type {
NotificationChannel,
NotificationRuleCreateRequest,
Expand Down
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/plans.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type {
operations,
paths,
Expand Down
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/portal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type { operations, paths, PortalToken } from './schemas.js'
import type { Client } from 'openapi-fetch'

Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/subjects.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type { operations, paths, SubjectUpsert } from './schemas.js'
import type { Client } from 'openapi-fetch'

Expand Down
3 changes: 2 additions & 1 deletion api/client/javascript/src/client/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { transformResponse, type RequestOptions } from './utils.js'
import { transformResponse } from './utils.js'
import type { RequestOptions } from './common.js'
import type {
operations,
paths,
Expand Down
78 changes: 11 additions & 67 deletions api/client/javascript/src/client/utils.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,37 @@
import type { UnexpectedProblemResponse } from './schemas.js'
import { HTTPError } from './common.js'
import type { FetchResponse, ParseAsResponse } from 'openapi-fetch'
import type {
MediaType,
ResponseObjectMap,
SuccessResponse,
} from 'openapi-typescript-helpers'

// Add more options as needed: 'headers' | 'credentials' | 'mode' | 'referrer' | 'referrerPolicy'
export type RequestOptions = Pick<RequestInit, 'signal'>

export class Problem extends Error {
name = 'Problem'

constructor(
public message: string,
public type: string,
public title: string,
public status: number,

protected __raw?: Record<string, any>
) {
super(message)
}

static fromResponse(resp: {
response: Response
error?: UnexpectedProblemResponse
}): Problem {
if (
resp.response.headers.get('Content-Type') ===
'application/problem+json' &&
resp.error
) {
return new Problem(
resp.error.detail,
resp.error.type,
resp.error.title,
resp.error.status ?? resp.response.status,
resp.error
)
}

return new Problem(
`Request failed: ${resp.response.statusText}`,
resp.response.statusText,
resp.response.statusText,
resp.response.status
)
}

getField(key: string) {
return this.__raw?.[key]
}
}

// Implementation
/**
* Transform a response from the API
* @param resp - The response to transform
* @throws HTTPError if the response is an error
* @returns The transformed response
*/
export function transformResponse<
T extends Record<string | number, any>,
Options,
Media extends MediaType,
>(
resp: FetchResponse<T, Options, Media>
):
| {
data: ParseAsResponse<
SuccessResponse<ResponseObjectMap<T>, Media>,
Options
>
error?: never
response: Response
}
| {
data?: never
error: Problem
response: Response
} {
| ParseAsResponse<SuccessResponse<ResponseObjectMap<T>, Media>, Options>
| never {
// Handle errors
if (resp.error || resp.response.status >= 400) {
const error = Problem.fromResponse(resp)

return { error, response: resp.response }
throw HTTPError.fromResponse(resp)
}

// Decode dates
if (resp.data) {
resp.data = decodeDates(resp.data)
}

return { data: resp.data!, response: resp.response }
return resp.data!
}

const ISODateFormat =
Expand Down
4 changes: 1 addition & 3 deletions api/client/javascript/src/portal/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import createClient from 'openapi-fetch'
import { createQuerySerializer } from 'openapi-fetch/dist/cjs/index.cjs'
import { encodeDates, transformResponse } from '../client/utils.js'
import type { RequestOptions } from '../client/common.js'
import type { paths } from '../client/schemas.js'
import type { RequestOptions } from '../client/utils.js'
import type { Client, ClientOptions } from 'openapi-fetch'

export type { RequestOptions } from '../client/utils.js'

/**
* Portal Config
*/
Expand Down

0 comments on commit 5a5f911

Please sign in to comment.