Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk): throw on error and export types in JavaScript SDK #2180

Merged
merged 1 commit into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading