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
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
feat(sdk): throw on error and export types in JavaScript SDK
tothandras committed Jan 30, 2025
commit 73b0b0da9d0247074fa577263d5585d0a484f2e1
2 changes: 1 addition & 1 deletion api/client/javascript/package.json
Original file line number Diff line number Diff line change
@@ -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",
2 changes: 1 addition & 1 deletion api/client/javascript/scripts/generate.ts
Original file line number Diff line number Diff line change
@@ -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,
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,
@@ -11,7 +12,6 @@ import type {
VoidInvoiceActionInput,
} from './schemas.js'
import type { Client } from 'openapi-fetch'

/**
* Billing
*/
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,
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,
4 changes: 2 additions & 2 deletions api/client/javascript/src/client/events.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})

@@ -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'

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'

3 changes: 2 additions & 1 deletion api/client/javascript/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -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
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'

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,
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,
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'

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'

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,
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 =
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
*/

Unchanged files with check annotations Beta

access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Set up magic Nix cache
uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9

Check warning on line 30 in .github/workflows/ci.yaml

GitHub Actions / Build

Magic Nix Cache is deprecated

Magic Nix Cache has been deprecated due to a change in the underlying GitHub APIs and will stop working on 1 February 2025. To continue caching Nix builds in GitHub Actions, use FlakeHub Cache instead. Replace... uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 ...with... uses: DeterminateSystems/flakehub-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 For more details: https://dtr.mn/magic-nix-cache-eol

Check warning on line 30 in .github/workflows/ci.yaml

GitHub Actions / Commit hooks

Magic Nix Cache is deprecated

Magic Nix Cache has been deprecated due to a change in the underlying GitHub APIs and will stop working on 1 February 2025. To continue caching Nix builds in GitHub Actions, use FlakeHub Cache instead. Replace... uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 ...with... uses: DeterminateSystems/flakehub-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 For more details: https://dtr.mn/magic-nix-cache-eol
- name: Prepare Nix shell
run: nix develop --impure .#ci
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Set up magic Nix cache
uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9

Check warning on line 159 in .github/workflows/ci.yaml

GitHub Actions / Build

Magic Nix Cache is deprecated

Magic Nix Cache has been deprecated due to a change in the underlying GitHub APIs and will stop working on 1 February 2025. To continue caching Nix builds in GitHub Actions, use FlakeHub Cache instead. Replace... uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 ...with... uses: DeterminateSystems/flakehub-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 For more details: https://dtr.mn/magic-nix-cache-eol

Check warning on line 159 in .github/workflows/ci.yaml

GitHub Actions / Commit hooks

Magic Nix Cache is deprecated

Magic Nix Cache has been deprecated due to a change in the underlying GitHub APIs and will stop working on 1 February 2025. To continue caching Nix builds in GitHub Actions, use FlakeHub Cache instead. Replace... uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 ...with... uses: DeterminateSystems/flakehub-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 For more details: https://dtr.mn/magic-nix-cache-eol
- name: Prepare Nix shell
run: nix develop --impure .#ci
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Set up magic Nix cache
uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9

Check warning on line 185 in .github/workflows/ci.yaml

GitHub Actions / Build

Magic Nix Cache is deprecated

Magic Nix Cache has been deprecated due to a change in the underlying GitHub APIs and will stop working on 1 February 2025. To continue caching Nix builds in GitHub Actions, use FlakeHub Cache instead. Replace... uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 ...with... uses: DeterminateSystems/flakehub-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 For more details: https://dtr.mn/magic-nix-cache-eol

Check warning on line 185 in .github/workflows/ci.yaml

GitHub Actions / Commit hooks

Magic Nix Cache is deprecated

Magic Nix Cache has been deprecated due to a change in the underlying GitHub APIs and will stop working on 1 February 2025. To continue caching Nix builds in GitHub Actions, use FlakeHub Cache instead. Replace... uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 ...with... uses: DeterminateSystems/flakehub-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9 For more details: https://dtr.mn/magic-nix-cache-eol
- name: Check
run: nix flake check --impure