Skip to content

chore(client): move misc public files to new core/ directory, deprecate old paths #40

New issue

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

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

Already on GitHub? # to your account

Merged
merged 1 commit into from
Mar 27, 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
94 changes: 2 additions & 92 deletions src/api-promise.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,2 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { type Lightswitch } from './client';

import { type PromiseOrValue } from './internal/types';
import { APIResponseProps, defaultParseResponse } from './internal/parse';

/**
* A subclass of `Promise` providing additional helper methods
* for interacting with the SDK.
*/
export class APIPromise<T> extends Promise<T> {
private parsedPromise: Promise<T> | undefined;
#client: Lightswitch;

constructor(
client: Lightswitch,
private responsePromise: Promise<APIResponseProps>,
private parseResponse: (
client: Lightswitch,
props: APIResponseProps,
) => PromiseOrValue<T> = defaultParseResponse,
) {
super((resolve) => {
// this is maybe a bit weird but this has to be a no-op to not implicitly
// parse the response body; instead .then, .catch, .finally are overridden
// to parse the response
resolve(null as any);
});
this.#client = client;
}

_thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
transform(await this.parseResponse(client, props), props),
);
}

/**
* Gets the raw `Response` instance instead of parsing the response
* data.
*
* If you want to parse the response body but still get the `Response`
* instance, you can use {@link withResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
* to your `tsconfig.json`.
*/
asResponse(): Promise<Response> {
return this.responsePromise.then((p) => p.response);
}

/**
* Gets the parsed response data and the raw `Response` instance.
*
* If you just want to get the raw `Response` instance without parsing it,
* you can use {@link asResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
* to your `tsconfig.json`.
*/
async withResponse(): Promise<{ data: T; response: Response }> {
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
return { data, response };
}

private parse(): Promise<T> {
if (!this.parsedPromise) {
this.parsedPromise = this.responsePromise.then((data) => this.parseResponse(this.#client, data));
}
return this.parsedPromise;
}

override then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
return this.parse().then(onfulfilled, onrejected);
}

override catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
): Promise<T | TResult> {
return this.parse().catch(onrejected);
}

override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return this.parse().finally(onfinally);
}
}
/** @deprecated Import from ./core/api-promise instead */
export * from './core/api-promise';
6 changes: 3 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { getPlatformHeaders } from './internal/detect-platform';
import * as Shims from './internal/shims';
import * as Opts from './internal/request-options';
import { VERSION } from './version';
import * as Errors from './error';
import * as Uploads from './uploads';
import * as Errors from './core/error';
import * as Uploads from './core/uploads';
import * as API from './resources/index';
import { APIPromise } from './api-promise';
import { APIPromise } from './core/api-promise';
import { type Fetch } from './internal/builtin-types';
import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
import { FinalRequestOptions, RequestOptions } from './internal/request-options';
Expand Down
3 changes: 3 additions & 0 deletions src/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `core`

This directory holds public modules implementing non-resource-specific SDK functionality.
92 changes: 92 additions & 0 deletions src/core/api-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { type Lightswitch } from '../client';

import { type PromiseOrValue } from '../internal/types';
import { APIResponseProps, defaultParseResponse } from '../internal/parse';

/**
* A subclass of `Promise` providing additional helper methods
* for interacting with the SDK.
*/
export class APIPromise<T> extends Promise<T> {
private parsedPromise: Promise<T> | undefined;
#client: Lightswitch;

constructor(
client: Lightswitch,
private responsePromise: Promise<APIResponseProps>,
private parseResponse: (
client: Lightswitch,
props: APIResponseProps,
) => PromiseOrValue<T> = defaultParseResponse,
) {
super((resolve) => {
// this is maybe a bit weird but this has to be a no-op to not implicitly
// parse the response body; instead .then, .catch, .finally are overridden
// to parse the response
resolve(null as any);
});
this.#client = client;
}

_thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
transform(await this.parseResponse(client, props), props),
);
}

/**
* Gets the raw `Response` instance instead of parsing the response
* data.
*
* If you want to parse the response body but still get the `Response`
* instance, you can use {@link withResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
* to your `tsconfig.json`.
*/
asResponse(): Promise<Response> {
return this.responsePromise.then((p) => p.response);
}

/**
* Gets the parsed response data and the raw `Response` instance.
*
* If you just want to get the raw `Response` instance without parsing it,
* you can use {@link asResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
* to your `tsconfig.json`.
*/
async withResponse(): Promise<{ data: T; response: Response }> {
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
return { data, response };
}

private parse(): Promise<T> {
if (!this.parsedPromise) {
this.parsedPromise = this.responsePromise.then((data) => this.parseResponse(this.#client, data));
}
return this.parsedPromise;
}

override then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
return this.parse().then(onfulfilled, onrejected);
}

override catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
): Promise<T | TResult> {
return this.parse().catch(onrejected);
}

override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return this.parse().finally(onfinally);
}
}
130 changes: 130 additions & 0 deletions src/core/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { castToError } from '../internal/errors';

export class LightswitchError extends Error {}

export class APIError<
TStatus extends number | undefined = number | undefined,
THeaders extends Headers | undefined = Headers | undefined,
TError extends Object | undefined = Object | undefined,
> extends LightswitchError {
/** HTTP status for the response that caused the error */
readonly status: TStatus;
/** HTTP headers for the response that caused the error */
readonly headers: THeaders;
/** JSON body of the response that caused the error */
readonly error: TError;

constructor(status: TStatus, error: TError, message: string | undefined, headers: THeaders) {
super(`${APIError.makeMessage(status, error, message)}`);
this.status = status;
this.headers = headers;
this.error = error;
}

private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
const msg =
error?.message ?
typeof error.message === 'string' ?
error.message
: JSON.stringify(error.message)
: error ? JSON.stringify(error)
: message;

if (status && msg) {
return `${status} ${msg}`;
}
if (status) {
return `${status} status code (no body)`;
}
if (msg) {
return msg;
}
return '(no status code or body)';
}

static generate(
status: number | undefined,
errorResponse: Object | undefined,
message: string | undefined,
headers: Headers | undefined,
): APIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}

const error = errorResponse as Record<string, any>;

if (status === 400) {
return new BadRequestError(status, error, message, headers);
}

if (status === 401) {
return new AuthenticationError(status, error, message, headers);
}

if (status === 403) {
return new PermissionDeniedError(status, error, message, headers);
}

if (status === 404) {
return new NotFoundError(status, error, message, headers);
}

if (status === 409) {
return new ConflictError(status, error, message, headers);
}

if (status === 422) {
return new UnprocessableEntityError(status, error, message, headers);
}

if (status === 429) {
return new RateLimitError(status, error, message, headers);
}

if (status >= 500) {
return new InternalServerError(status, error, message, headers);
}

return new APIError(status, error, message, headers);
}
}

export class APIUserAbortError extends APIError<undefined, undefined, undefined> {
constructor({ message }: { message?: string } = {}) {
super(undefined, undefined, message || 'Request was aborted.', undefined);
}
}

export class APIConnectionError extends APIError<undefined, undefined, undefined> {
constructor({ message, cause }: { message?: string | undefined; cause?: Error | undefined }) {
super(undefined, undefined, message || 'Connection error.', undefined);
// in some environments the 'cause' property is already declared
// @ts-ignore
if (cause) this.cause = cause;
}
}

export class APIConnectionTimeoutError extends APIConnectionError {
constructor({ message }: { message?: string } = {}) {
super({ message: message ?? 'Request timed out.' });
}
}

export class BadRequestError extends APIError<400, Headers> {}

export class AuthenticationError extends APIError<401, Headers> {}

export class PermissionDeniedError extends APIError<403, Headers> {}

export class NotFoundError extends APIError<404, Headers> {}

export class ConflictError extends APIError<409, Headers> {}

export class UnprocessableEntityError extends APIError<422, Headers> {}

export class RateLimitError extends APIError<429, Headers> {}

export class InternalServerError extends APIError<number, Headers> {}
11 changes: 11 additions & 0 deletions src/core/resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import type { Lightswitch } from '../client';

export class APIResource {
protected _client: Lightswitch;

constructor(client: Lightswitch) {
this._client = client;
}
}
2 changes: 2 additions & 0 deletions src/core/uploads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { type Uploadable } from '../internal/uploads';
export { toFile, type ToFileInput } from '../internal/to-file';
Loading