diff --git a/.github/workflows/sdk-ci.yml b/.github/workflows/sdk-ci.yml new file mode 100644 index 0000000..0a3224a --- /dev/null +++ b/.github/workflows/sdk-ci.yml @@ -0,0 +1,17 @@ +name: TypeScript SDK CI + +on: + push: + pull_request: + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + - run: npm install + - run: npm run build diff --git a/.github/workflows/sdk-release.yml b/.github/workflows/sdk-release.yml new file mode 100644 index 0000000..8514903 --- /dev/null +++ b/.github/workflows/sdk-release.yml @@ -0,0 +1,33 @@ +name: TypeScript SDK Release + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + id-token: write + packages: write + +jobs: + release: + runs-on: ubuntu-latest + env: + VERSIONING_POLICY: manual + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + - run: npm install + - run: npm run build + - run: npm install -g npm@latest + - name: Publish to npm + run: | + if npm view "@scalar/galaxy-sdk@0.1.1" version >/dev/null 2>&1; then + echo "@scalar/galaxy-sdk@0.1.1 already published to npm, skipping" + else + npm publish --provenance --access public + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f138a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.tsbuildinfo +.env +.env.* + diff --git a/.scalar/sdk-sync.json b/.scalar/sdk-sync.json new file mode 100644 index 0000000..6167e3a --- /dev/null +++ b/.scalar/sdk-sync.json @@ -0,0 +1,5 @@ +{ + "build": "iNVr8wQMDXnHHupXJ4ats", + "slug": "demo-api-scalar-galaxy-typescript", + "version": "0.1.1" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..14b7c3c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## v0.1.1 (2026-06-24) + +Initial release. diff --git a/README.md b/README.md index 47c57a8..0cab28a 100644 --- a/README.md +++ b/README.md @@ -1 +1,234 @@ -# galaxy-node-sdk \ No newline at end of file +# Scalar Galaxy + +Generated TypeScript SDK for Scalar Galaxy API. +The Scalar Galaxy is an example OpenAPI document to test OpenAPI tools and libraries. It's a fictional universe with fictional planets and fictional data. Get all the data for [all planets](#tag/planets/get/planets). + +## Resources + +* https://github.com/scalar/scalar +* https://github.com/OAI/OpenAPI-Specification +* https://scalar.com + +## Markdown Support + +All descriptions *can* contain ~~tons of text~~ **Markdown**. [If GitHub supports the syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax), chances are we're supporting it, too. You can even create [internal links to reference endpoints](#tag/authentication/post/user/signup). + +
+ Examples + + **Blockquotes** + + > I love OpenAPI. <3 + + **Tables** + + | Feature | Availability | + | ---------------- | ------------ | + | Markdown Support | ✓ | + + **Accordion** + + ```html +
+ Using Details Tags +

HTML Example

+
+ ``` + + **Images** + + Yes, there's support for images, too! + + ![Empty placeholder image showing the width/height](https://images.placeholders.dev/?width=1280&height=720) + + **Alerts** + + > [!tip] + > You can now use markdown alerts in your descriptions. + +
+ +
+ +## Contents + +- [Installation](#installation) +- [Usage](#usage) +- [API Reference](./api.md) +- [Authentication](#authentication) +- [Errors](#errors) +- [Client Options](#client-options) +- [Request Options](#request-options) +- [Retries and Timeouts](#retries-and-timeouts) +- [Helpers](#helpers) +- [Logging](#logging) +- [Requirements](#requirements) + +
+ +## Installation + +```sh +npm install @scalar/galaxy-sdk +``` + +
+ +## Usage + +```ts +import ScalarGalaxy from "@scalar/galaxy-sdk"; + +const client = new ScalarGalaxy({ + bearerAuth: process.env["BEARER_AUTH"], // defaults to the BEARER_AUTH env var + environment: "production", +}); + +const listAllData = await client.planets.listAllData({ + limit: 10, + offset: 0, +}); +console.log(listAllData); +``` + +The examples in the following sections assume a `client` configured as shown above. + +See the [API reference](./api.md) for every available operation. + +
+ +## Authentication + +Pass credentials to the generated client constructor. Environment variables are read automatically when supported by the target runtime. + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `bearerAuth` | `string \| provider` | - | JWT Bearer token authentication Defaults to BEARER_AUTH. | +| `basicAuthUsername` | `string \| provider` | - | Basic HTTP authentication Defaults to BASIC_AUTH_USERNAME. | +| `apiKeyHeader` | `string \| provider` | - | API key request header Defaults to API_KEY_HEADER. | +| `apiKeyQuery` | `string \| provider` | - | API key query parameter Defaults to API_KEY_QUERY. | +| `apiKeyCookie` | `string \| provider` | - | API key browser cookie Defaults to API_KEY_COOKIE. | +| `oAuth2` | `string \| provider` | - | OAuth 2.0 authentication Defaults to SCALAR_OAUTH2. | +| `openIDConnect` | `string \| provider` | - | OpenID Connect Authentication Defaults to SCALAR_OPENIDCONNECT. | + +Declared schemes: + +- `bearerAuth` bearer token +- `basicAuth` basic authentication +- `apiKeyHeader` API key in header `X-API-Key` +- `apiKeyQuery` API key in query `api_key` +- `apiKeyCookie` API key in cookie `api_key` +- `oAuth2` OAuth2/OpenID Connect +- `openIdConnect` OAuth2/OpenID Connect + +
+ +## Errors + +Non-success responses throw generated API errors. Error objects expose status, headers, response body, and request metadata where the target runtime supports it. + +```ts +import { APIError } from "@scalar/galaxy-sdk"; + +try { + const listAllData = await client.planets.listAllData({ + limit: 10, + offset: 0, + }); +} catch (err) { + if (err instanceof APIError) { + console.log(err.status, err.name, err.headers); + } + throw err; +} +``` + +Documented error statuses: `400`, `401`, `403`, `404`, `409`, `422`. + +
+ +## Client Options + +Configure the generated client by setting any of these options when you create it. + +```ts +import ScalarGalaxy from "@scalar/galaxy-sdk"; + +const client = new ScalarGalaxy({ + timeout: 60000, + maxRetries: 2, + logLevel: "debug", +}); +``` + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `bearerAuth` | `string \| AuthTokenProvider` | `process.env["BEARER_AUTH"]` | JWT Bearer token authentication | +| `basicAuthUsername` | `string \| AuthTokenProvider` | `process.env["BASIC_AUTH_USERNAME"]` | Basic HTTP authentication | +| `apiKeyHeader` | `string \| AuthTokenProvider` | `process.env["API_KEY_HEADER"]` | API key request header | +| `apiKeyQuery` | `string \| AuthTokenProvider` | `process.env["API_KEY_QUERY"]` | API key query parameter | +| `apiKeyCookie` | `string \| AuthTokenProvider` | `process.env["API_KEY_COOKIE"]` | API key browser cookie | +| `oAuth2` | `string \| AuthTokenProvider` | `process.env["SCALAR_OAUTH2"]` | OAuth 2.0 authentication | +| `openIDConnect` | `string \| AuthTokenProvider` | `process.env["SCALAR_OPENIDCONNECT"]` | OpenID Connect Authentication | +| `environment` | `Environment` | - | Select one of the configured API environments. | +| `baseURL` | `string \| null` | `process.env["SCALAR_BASE_URL"]` | Override the default API base URL. Pass `null` when selecting a configured environment. | +| `timeout` | `number` | `60000` | Maximum time in milliseconds to wait for a response before aborting a request. | +| `maxRetries` | `number` | `2` | Number of retries for temporary failures. | +| `defaultHeaders` | `HeadersInit` | - | Headers sent with every request. | +| `defaultQuery` | `Record` | - | Query parameters sent with every request. | +| `fetchOptions` | `RequestInit` | - | Additional fetch options sent with every request. | +| `fetch` | `Fetch` | - | Custom fetch implementation. | +| `logLevel` | `"off" \| "error" \| "warn" \| "info" \| "debug" \| null` | `process.env["SCALAR_LOG"]` | Controls request and retry debug logging. | +| `logger` | `Logger \| null` | `console` | Custom logger implementation. | + +
+ +## Request Options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `headers` | `HeadersInit` | - | Per-request headers. | +| `query` | `Record` | - | Per-request query parameters. | +| `body` | `unknown` | - | Override the generated request body. | +| `timeout` | `number` | - | Per-request timeout in milliseconds. | +| `maxRetries` | `number` | - | Per-request retry count. | +| `signal` | `AbortSignal` | - | Abort an in-flight request. | +| `fetchOptions` | `RequestInit` | - | Per-request fetch options. | +| `idempotencyKey` | `string` | - | Idempotency key for retry-safe operations. | + +
+ +## Retries and Timeouts + +Generated clients support request timeouts and retry temporary failures such as network errors, 408, 409, 429, and 5xx responses. Retry delays honor `Retry-After` headers when present. Tune the retry and timeout client options shown above, or override them per request. + +
+ +## Helpers + +- Use `.withResponse()` on any request to inspect both parsed data and the raw `Response` object. +- Every operation returns an `APIPromise`, so you can `await` it directly or chain `.withResponse()`. + +
+ +## Logging + +- Set `logLevel: "debug"` to log request URLs, options, response status, response headers, and retry attempts. +- Pass a custom `logger` to route logs into your own observability pipeline. +- Set `logLevel: null` to disable environment-driven logging. + +
+ +## Requirements + +- Node.js 20+, a modern browser, or any runtime with `fetch` support + +Powered by Scalar. + + +## Contributions + +This SDK is generated programmatically. Manual edits to generated files will be +overwritten on the next build. + +### SDK created by [Scalar](https://www.scalar.com/?utm_source=demo-api-scalar-galaxy-typescript&utm_campaign=sdk) diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..8f16c2c --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,7 @@ +# Versioning + +This SDK is configured with the `manual` versioning policy. + +- `manual`: package versions are set explicitly before release. +- `semver`: releases should follow semantic versioning based on API and SDK surface changes. +- `calendar`: releases should use a calendar-derived version chosen by the release workflow or maintainer. diff --git a/api.md b/api.md new file mode 100644 index 0000000..7c39189 --- /dev/null +++ b/api.md @@ -0,0 +1,163 @@ +# Scalar Galaxy TypeScript API + +Complete reference of every operation, grouped by resource. See [the README](./README.md) for usage and configuration. + +## Contents + +- [`Planets`](#planets) + - [Get all planets](#get-all-planets) + - [Create a planet](#create-a-planet) + - [Get a planet](#get-a-planet) + - [Update a planet](#update-a-planet) + - [Delete a planet](#delete-a-planet) + - [Upload an image to a planet](#upload-an-image-to-a-planet) +- [`CelestialBodies`](#celestialbodies) + - [Create a celestial body](#create-a-celestial-body) +- [`Authentication`](#authentication) + - [Create a user](#create-a-user) + - [Get a token](#get-a-token) + - [Get authenticated user](#get-authenticated-user) + +## Setup + +```ts +import ScalarGalaxy from "@scalar/galaxy-sdk"; + +const client = new ScalarGalaxy({ + bearerAuth: process.env["BEARER_AUTH"], // defaults to the BEARER_AUTH env var + environment: "production", +}); +``` + +## `Planets` + +### Get all planets + +It's easy to say you know them all, but do you really? Retrieve all the planets and check whether you missed one. + +| Direction | Type | +| --- | --- | +| Request | [`PlanetListAllDataParams`](./src/resources/planets.ts) | +| Response | [`PlanetListAllDataResponse`](./src/resources/planets.ts) | + +```ts +const listAllData = await client.planets.listAllData({ + limit: 10, + offset: 0, +}); +``` + +### Create a planet + +Time to play god and create a new planet. What do you think? Ah, don't think too much. What could go wrong anyway? + +| Direction | Type | +| --- | --- | +| Request | [`PlanetCreateParams`](./src/resources/planets.ts) | +| Response | [`Planet`](./src/resources/planets.ts) | + +```ts +const planet = await client.planets.create(); +``` + +### Get a planet + +You'll better learn a little bit more about the planets. It might come in handy once space travel is available for everyone. + +| Direction | Type | +| --- | --- | +| Response | [`Planet`](./src/resources/planets.ts) | + +```ts +const planet = await client.planets.retrieve(1); +``` + +### Update a planet + +Sometimes you make mistakes, that's fine. No worries, you can update all planets. + +| Direction | Type | +| --- | --- | +| Request | [`PlanetUpdateParams`](./src/resources/planets.ts) | +| Response | [`Planet`](./src/resources/planets.ts) | + +```ts +const planet = await client.planets.update(1); +``` + +### Delete a planet + +This endpoint was used to delete planets. Unfortunately, that caused a lot of trouble for planets with life. So, this endpoint is now deprecated and should not be used anymore. + +```ts +await client.planets.delete(1); +``` + +### Upload an image to a planet + +Got a crazy good photo of a planet? Share it with the world! + +| Direction | Type | +| --- | --- | +| Request | [`PlanetUploadImageParams`](./src/resources/planets.ts) | +| Response | [`PlanetUploadImageResponse`](./src/resources/planets.ts) | + +```ts +const uploadImage = await client.planets.uploadImage(1); +``` + +## `CelestialBodies` + +### Create a celestial body + +| Direction | Type | +| --- | --- | +| Request | [`CelestialBodyCreateParams`](./src/resources/celestial-bodies.ts) | +| Response | [`CelestialBody`](./src/resources/celestial-bodies.ts) | + +```ts +const celestialBody = await client.celestialBodies.create({ + name: "", + type: "planet", +}); +``` + +## `Authentication` + +### Create a user + +Time to create a user account, eh? + +| Direction | Type | +| --- | --- | +| Request | [`AuthenticationCreateUserParams`](./src/resources/authentication.ts) | +| Response | [`User`](./src/resources/authentication.ts) | + +```ts +const user = await client.authentication.createUser(); +``` + +### Get a token + +Yeah, this is the boring security stuff. Just get your super secret token and move on. + +| Direction | Type | +| --- | --- | +| Request | [`AuthenticationCreateTokenParams`](./src/resources/authentication.ts) | +| Response | [`AuthenticationCreateTokenResponse`](./src/resources/authentication.ts) | + +```ts +const createToken = await client.authentication.createToken(); +``` + +### Get authenticated user + +Find yourself they say. That's what you can do here. + +| Direction | Type | +| --- | --- | +| Response | [`User`](./src/resources/authentication.ts) | + +```ts +const user = await client.authentication.listMe(); +``` diff --git a/openapi.augmented.json b/openapi.augmented.json new file mode 100644 index 0000000..36dc1df --- /dev/null +++ b/openapi.augmented.json @@ -0,0 +1,1688 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "Demo API (Scalar Galaxy)", + "description": "The Scalar Galaxy is an example OpenAPI document to test OpenAPI tools and libraries. It's a fictional universe with fictional planets and fictional data. Get all the data for [all planets](#tag/planets/get/planets).\n\n## Resources\n\n* https://github.com/scalar/scalar\n* https://github.com/OAI/OpenAPI-Specification\n* https://scalar.com\n\n## Markdown Support\n\nAll descriptions *can* contain ~~tons of text~~ **Markdown**. [If GitHub supports the syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax), chances are we're supporting it, too. You can even create [internal links to reference endpoints](#tag/authentication/post/user/signup).\n\n
\n Examples\n\n **Blockquotes**\n\n > I love OpenAPI. <3\n\n **Tables**\n\n | Feature | Availability |\n | ---------------- | ------------ |\n | Markdown Support | ✓ |\n\n **Accordion**\n\n ```html\n
\n Using Details Tags\n

HTML Example

\n
\n ```\n\n **Images**\n\n Yes, there's support for images, too!\n\n ![Empty placeholder image showing the width/height](https://images.placeholders.dev/?width=1280&height=720)\n\n **Alerts**\n\n > [!tip]\n > You can now use markdown alerts in your descriptions.\n\n
\n", + "version": "1.0.0", + "contact": { + "name": "Scalar Support", + "url": "https://scalar.com", + "email": "marc@scalar.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/license/MIT" + }, + "x-scalar-sdk-installation": [ + { + "lang": "TypeScript", + "description": "```sh\nnpm install @scalar/galaxy-sdk\n```" + } + ] + }, + "externalDocs": { + "description": "Documentation", + "url": "https://github.com/scalar/scalar" + }, + "servers": [ + { + "url": "https://galaxy.scalar.com" + }, + { + "url": "{protocol}://void.scalar.com/{path}", + "description": "Responds with your request data", + "variables": { + "protocol": { + "enum": [ + "https" + ], + "default": "https" + }, + "path": { + "default": "" + } + } + } + ], + "security": [ + { + "bearerAuth": [] + }, + { + "basicAuth": [] + }, + { + "apiKeyQuery": [] + }, + { + "apiKeyHeader": [] + }, + { + "apiKeyCookie": [] + }, + { + "oAuth2": [] + }, + { + "openIdConnect": [] + } + ], + "tags": [ + { + "name": "Authentication", + "description": "Some endpoints are public, but some require authentication. We provide all the required endpoints to create an account and authorize yourself." + }, + { + "name": "Planets", + "description": "Everything about planets" + }, + { + "name": "Celestial Bodies", + "description": "Celestial bodies are the planets and satellites in the Scalar Galaxy." + } + ], + "paths": { + "/planets": { + "get": { + "tags": [ + "Planets" + ], + "summary": "Get all planets", + "description": "It's easy to say you know them all, but do you really? Retrieve all the planets and check whether you missed one.", + "operationId": "getAllData", + "security": [ + {} + ], + "parameters": [ + { + "$ref": "#/components/parameters/limit" + }, + { + "$ref": "#/components/parameters/offset" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + { + "$ref": "#/components/schemas/PaginatedResource" + } + ] + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "type": "object", + "xml": { + "name": "planets" + }, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + { + "$ref": "#/components/schemas/PaginatedResource" + } + ] + } + } + } + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst listAllData = await client.planets.listAllData({\n limit: 10,\n offset: 0,\n});\nconsole.log(listAllData);" + } + ] + }, + "post": { + "tags": [ + "Planets" + ], + "summary": "Create a planet", + "description": "Time to play god and create a new planet. What do you think? Ah, don't think too much. What could go wrong anyway?", + "operationId": "createPlanet", + "callbacks": { + "planetCreated": { + "{$request.body#/successCallbackUrl}": { + "post": { + "requestBody": { + "description": "Information about the newly created planet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + "responses": { + "200": { + "description": "Your server returns this code if it accepts the callback" + }, + "204": { + "description": "Your server should return this HTTP status code if no longer interested in further updates" + } + } + } + } + }, + "planetExploded": { + "{$request.body#/successCallbackUrl}": { + "post": { + "requestBody": { + "description": "Information about the newly exploded planet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + "responses": { + "200": { + "description": "Your server returns this code if it accepts the callback" + } + } + } + } + } + }, + "requestBody": { + "description": "Planet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst planet = await client.planets.create();\nconsole.log(planet);" + } + ] + } + }, + "/planets/{planetId}": { + "get": { + "tags": [ + "Planets" + ], + "summary": "Get a planet", + "description": "You'll better learn a little bit more about the planets. It might come in handy once space travel is available for everyone.", + "operationId": "getPlanet", + "security": [ + {} + ], + "parameters": [ + { + "$ref": "#/components/parameters/planetId" + } + ], + "responses": { + "200": { + "description": "Planet Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst planet = await client.planets.retrieve(1);\nconsole.log(planet);" + } + ] + }, + "put": { + "tags": [ + "Planets" + ], + "summary": "Update a planet", + "description": "Sometimes you make mistakes, that's fine. No worries, you can update all planets.", + "operationId": "updatePlanet", + "requestBody": { + "description": "New information about the planet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/planetId" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst planet = await client.planets.update(1);\nconsole.log(planet);" + } + ] + }, + "delete": { + "tags": [ + "Planets" + ], + "summary": "Delete a planet", + "operationId": "deletePlanet", + "description": "This endpoint was used to delete planets. Unfortunately, that caused a lot of trouble for planets with life. So, this endpoint is now deprecated and should not be used anymore.", + "x-scalar-stability": "experimental", + "parameters": [ + { + "$ref": "#/components/parameters/planetId" + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nawait client.planets.delete(1);" + } + ] + } + }, + "/planets/{planetId}/image": { + "post": { + "tags": [ + "Planets" + ], + "summary": "Upload an image to a planet", + "description": "Got a crazy good photo of a planet? Share it with the world!", + "operationId": "uploadImage", + "parameters": [ + { + "$ref": "#/components/parameters/planetId" + } + ], + "requestBody": { + "description": "Image to upload", + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary", + "description": "The image file to upload", + "examples": [ + "@mars.jpg", + "@jupiter.png" + ] + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/ImageUploaded" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst uploadImage = await client.planets.uploadImage(1);\nconsole.log(uploadImage);" + } + ] + } + }, + "/celestial-bodies": { + "post": { + "tags": [ + "Celestial Bodies" + ], + "summary": "Create a celestial body", + "operationId": "createCelestialBody", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CelestialBody" + } + } + } + }, + "responses": { + "201": { + "description": "Celestial body created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CelestialBody" + } + } + } + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst celestialBody = await client.celestialBodies.create({\n name: \"\",\n type: \"planet\",\n});\nconsole.log(celestialBody);" + } + ] + } + }, + "/user/signup": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Create a user", + "description": "Time to create a user account, eh?", + "operationId": "createUser", + "security": [ + {} + ], + "requestBody": { + "description": "User to create", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/User" + }, + { + "$ref": "#/components/schemas/Credentials" + } + ] + }, + "examples": { + "Marc": { + "value": { + "name": "Marc", + "email": "marc@scalar.com", + "password": "i-love-scalar" + } + }, + "Cam": { + "value": { + "name": "Cam", + "email": "cam@scalar.com", + "password": "scalar-is-cool" + } + } + } + }, + "application/xml": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/User" + }, + { + "$ref": "#/components/schemas/Credentials" + } + ] + }, + "examples": { + "Marc": { + "value": { + "name": "Marc", + "email": "marc@scalar.com", + "password": "i-love-scalar" + } + }, + "Cam": { + "value": { + "name": "Cam", + "email": "cam@scalar.com", + "password": "scalar-is-cool" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/UnprocessableEntity" + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst user = await client.authentication.createUser();\nconsole.log(user);" + } + ] + } + }, + "/auth/token": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Get a token", + "description": "Yeah, this is the boring security stuff. Just get your super secret token and move on.", + "operationId": "getToken", + "security": [ + {} + ], + "requestBody": { + "description": "Credentials to authenticate a user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Credentials" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Credentials" + } + } + } + }, + "responses": { + "201": { + "description": "Token Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Token" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Token" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst createToken = await client.authentication.createToken();\nconsole.log(createToken);" + } + ] + } + }, + "/me": { + "get": { + "tags": [ + "Authentication" + ], + "summary": "Get authenticated user", + "description": "Find yourself they say. That's what you can do here.", + "operationId": "getMe", + "security": [ + { + "basicAuth": [] + }, + { + "oAuth2": [ + "read:account" + ] + }, + { + "bearerAuth": [] + }, + { + "apiKeyHeader": [] + }, + { + "apiKeyQuery": [] + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + }, + "x-scalar-examples": [ + { + "lang": "TypeScript", + "label": "TypeScript", + "source": "import ScalarGalaxy from \"@scalar/galaxy-sdk\";\n\nconst client = new ScalarGalaxy({\n bearerAuth: process.env[\"BEARER_AUTH\"], // defaults to the BEARER_AUTH env var\n environment: \"production\",\n});\n\nconst user = await client.authentication.listMe();\nconsole.log(user);" + } + ] + } + } + }, + "webhooks": { + "newPlanet": { + "post": { + "tags": [ + "Planets" + ], + "requestBody": { + "description": "Information about a new planet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Planet" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status to indicate that the data was received successfully" + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "JWT Bearer token authentication" + }, + "basicAuth": { + "type": "http", + "scheme": "basic", + "description": "Basic HTTP authentication" + }, + "apiKeyHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key", + "description": "API key request header" + }, + "apiKeyQuery": { + "type": "apiKey", + "in": "query", + "name": "api_key", + "description": "API key query parameter" + }, + "apiKeyCookie": { + "type": "apiKey", + "in": "cookie", + "name": "api_key", + "description": "API key browser cookie" + }, + "oAuth2": { + "type": "oauth2", + "description": "OAuth 2.0 authentication", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://galaxy.scalar.com/oauth/authorize", + "tokenUrl": "https://galaxy.scalar.com/oauth/token", + "scopes": { + "read:account": "read your account information", + "write:planets": "modify planets in your account", + "read:planets": "read your planets" + }, + "refreshUrl": "", + "x-usePkce": "SHA-256" + }, + "clientCredentials": { + "tokenUrl": "https://galaxy.scalar.com/oauth/token", + "scopes": { + "read:account": "read your account information", + "write:planets": "modify planets in your account", + "read:planets": "read your planets" + }, + "refreshUrl": "" + }, + "implicit": { + "authorizationUrl": "https://galaxy.scalar.com/oauth/authorize", + "scopes": { + "read:account": "read your account information", + "write:planets": "modify planets in your account", + "read:planets": "read your planets" + }, + "refreshUrl": "" + }, + "password": { + "tokenUrl": "https://galaxy.scalar.com/oauth/token", + "scopes": { + "read:account": "read your account information", + "write:planets": "modify planets in your account", + "read:planets": "read your planets" + }, + "refreshUrl": "" + } + } + }, + "openIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "https://galaxy.scalar.com/.well-known/openid-configuration", + "description": "OpenID Connect Authentication" + } + }, + "parameters": { + "planetId": { + "name": "planetId", + "description": "The ID of the planet to get", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "examples": [ + 1 + ] + } + }, + "satelliteId": { + "name": "satelliteId", + "description": "The ID of the satellite to get", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "examples": [ + 1 + ] + } + }, + "limit": { + "name": "limit", + "description": "The number of items to return", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 10 + } + }, + "offset": { + "name": "offset", + "description": "The number of items to skip before starting to collect the result set", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, + "responses": { + "ImageUploaded": { + "description": "Image uploaded", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageUploadedMessage" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ImageUploadedMessage" + } + } + } + }, + "BadRequest": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "Forbidden": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "NotFound": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + }, + "Unauthorized": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "Conflict": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conflict" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Conflict" + } + } + } + }, + "UnprocessableEntity": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntity" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntity" + } + } + } + } + }, + "schemas": { + "User": { + "description": "A user", + "type": "object", + "xml": { + "name": "user" + }, + "properties": { + "id": { + "type": "integer", + "format": "int64", + "readOnly": true, + "examples": [ + 1 + ] + }, + "name": { + "type": "string", + "examples": [ + "Marc" + ] + } + } + }, + "Credentials": { + "description": "Credentials to authenticate a user", + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "examples": [ + "marc@scalar.com" + ] + }, + "password": { + "type": "string", + "writeOnly": true, + "examples": [ + "i-love-scalar", + "i-love-OSS", + "i-love-code" + ] + } + } + }, + "Token": { + "description": "A token to authenticate a user", + "type": "object", + "properties": { + "token": { + "type": "string", + "examples": [ + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + ] + } + } + }, + "CelestialBody": { + "oneOf": [ + { + "$ref": "#/components/schemas/Planet" + }, + { + "$ref": "#/components/schemas/Satellite" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "planet": "#/components/schemas/Planet", + "satellite": "#/components/schemas/Satellite" + } + }, + "description": "A celestial body which can be either a planet or a satellite" + }, + "Planet": { + "description": "A planet in the Scalar Galaxy", + "type": "object", + "required": [ + "id", + "name", + "type" + ], + "additionalProperties": false, + "xml": { + "name": "planet" + }, + "properties": { + "id": { + "type": "integer", + "format": "int64", + "readOnly": true, + "examples": [ + 1 + ], + "x-variable": "planetId" + }, + "name": { + "type": "string", + "examples": [ + "Mars", + "Jupiter", + "HD 40307g" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "examples": [ + "The red planet", + "A gas giant with a great red spot" + ] + }, + "type": { + "type": "string", + "enum": [ + "planet", + "terrestrial", + "gas_giant", + "ice_giant", + "dwarf", + "super_earth" + ], + "x-enum-varnames": [ + "Planet", + "Terrestrial", + "GasGiant", + "IceGiant", + "Dwarf", + "SuperEarth" + ], + "x-enum-descriptions": { + "planet": "Discriminator value for Planet schema", + "terrestrial": "Rocky planets with solid surfaces, like Earth and Mars", + "gas_giant": "Large planets composed mainly of hydrogen and helium, like Jupiter and Saturn", + "ice_giant": "Planets composed of water, ammonia, and methane ices, like Uranus and Neptune", + "dwarf": "Small planetary bodies that don't meet full planet criteria, like Pluto", + "super_earth": "Rocky planets larger than Earth but smaller than gas giants" + }, + "examples": [ + "terrestrial" + ] + }, + "habitabilityIndex": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 1, + "description": "A score from 0 to 1 indicating potential habitability", + "examples": [ + 0.68 + ] + }, + "physicalProperties": { + "type": "object", + "additionalProperties": { + "x-additionalPropertiesName": "measurement", + "type": "number", + "format": "float", + "description": "Additional physical measurements for the planet" + }, + "properties": { + "mass": { + "type": "number", + "format": "float", + "exclusiveMinimum": 0, + "description": "Mass in Earth masses (must be greater than 0)", + "examples": [ + 0.107 + ] + }, + "radius": { + "type": "number", + "format": "float", + "exclusiveMinimum": 0, + "description": "Radius in Earth radii (must be greater than 0)", + "examples": [ + 0.532 + ] + }, + "gravity": { + "type": "number", + "format": "float", + "description": "Surface gravity in Earth g", + "examples": [ + 0.378 + ] + }, + "temperature": { + "type": "object", + "additionalProperties": { + "x-additionalPropertiesName": "temperatureMetric", + "type": "number", + "format": "float", + "description": "Additional temperature-related measurements in Kelvin" + }, + "properties": { + "min": { + "type": "number", + "format": "float", + "description": "Minimum temperature in Kelvin", + "examples": [ + 130 + ] + }, + "max": { + "type": "number", + "format": "float", + "description": "Maximum temperature in Kelvin", + "examples": [ + 308 + ] + }, + "average": { + "type": "number", + "format": "float", + "description": "Average temperature in Kelvin", + "examples": [ + 210 + ] + } + } + } + } + }, + "atmosphere": { + "type": "array", + "description": "Atmospheric composition", + "items": { + "type": "object", + "additionalProperties": { + "x-additionalPropertiesName": "atmosphericData", + "type": "string", + "description": "Additional atmospheric composition data" + }, + "properties": { + "compound": { + "type": "string", + "examples": [ + "CO2", + "N2" + ] + }, + "percentage": { + "type": "number", + "format": "float", + "exclusiveMaximum": 100, + "examples": [ + 95.3 + ] + } + } + } + }, + "discoveredAt": { + "type": "string", + "format": "date-time", + "examples": [ + "1610-01-07T00:00:00Z" + ] + }, + "image": { + "type": "string", + "nullable": true, + "examples": [ + "https://cdn.scalar.com/photos/mars.jpg" + ] + }, + "satellites": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Satellite" + } + }, + "creator": { + "$ref": "#/components/schemas/User" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "solar-system", + "rocky", + "explored" + ] + ] + }, + "lastUpdated": { + "type": "string", + "format": "date-time", + "readOnly": true, + "examples": [ + "2024-01-15T14:30:00Z" + ] + }, + "successCallbackUrl": { + "type": "string", + "format": "uri", + "description": "URL which gets invoked upon a successful operation", + "examples": [ + "https://example.com/webhook" + ] + }, + "failureCallbackUrl": { + "type": "string", + "format": "uri", + "description": "URL which gets invoked upon a failed operation", + "examples": [ + "https://example.com/webhook" + ] + } + } + }, + "Satellite": { + "description": "Every satellite in the Scalar Galaxy", + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "readOnly": true, + "examples": [ + 1 + ] + }, + "name": { + "type": "string", + "examples": [ + "Phobos" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "examples": [ + "Phobos is the larger and innermost of the two moons of Mars." + ] + }, + "diameter": { + "type": "number", + "format": "float", + "description": "Diameter in kilometers", + "examples": [ + 22.2 + ] + }, + "type": { + "type": "string", + "enum": [ + "satellite", + "moon", + "asteroid", + "comet" + ], + "x-enum-varnames": [ + "Satellite", + "Moon", + "Asteroid", + "Comet" + ], + "x-enum-descriptions": { + "satellite": "Discriminator value for Satellite schema", + "moon": "Natural satellites that orbit planets", + "asteroid": "Rocky objects that orbit the sun, typically found in the asteroid belt", + "comet": "Icy bodies that release gas when approaching the sun, creating visible tails" + }, + "examples": [ + "moon" + ] + }, + "orbit": { + "type": "object", + "properties": { + "planetId": { + "type": "integer", + "format": "int64", + "description": "The ID of the planet this satellite orbits", + "examples": [ + 1 + ] + }, + "orbitalPeriod": { + "type": "number", + "format": "float", + "description": "Orbital period in Earth days", + "examples": [ + 0.319 + ] + }, + "distance": { + "type": "number", + "format": "float", + "description": "Average distance from the planet in kilometers", + "examples": [ + 9376 + ] + } + } + } + } + }, + "PaginatedResource": { + "description": "A paginated resource", + "type": "object", + "properties": { + "meta": { + "type": "object", + "properties": { + "limit": { + "type": "integer", + "format": "int64", + "examples": [ + 10 + ] + }, + "offset": { + "type": "integer", + "format": "int64", + "examples": [ + 0 + ] + }, + "total": { + "type": "integer", + "format": "int64", + "examples": [ + 100 + ] + }, + "next": { + "type": [ + "string", + "null" + ], + "examples": [ + "/planets?limit=10&offset=10" + ] + } + } + } + } + }, + "ImageUploadedMessage": { + "x-scalar-ignore": true, + "description": "Message about an image upload", + "type": "object", + "properties": { + "message": { + "type": "string", + "examples": [ + "Image uploaded successfully" + ] + }, + "imageUrl": { + "type": "string", + "description": "The URL where the uploaded image can be accessed", + "examples": [ + "https://cdn.scalar.com/images/8f47c132-9d1f-4f83-b5a4-91db5ee757ab.jpg" + ] + }, + "uploadedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the image was uploaded", + "examples": [ + "2024-01-15T14:30:00Z" + ] + }, + "fileSize": { + "type": "integer", + "description": "Size of the uploaded image in bytes", + "examples": [ + 1048576 + ] + }, + "mimeType": { + "type": "string", + "description": "The content type of the uploaded image", + "examples": [ + "image/jpeg", + "image/png" + ] + } + } + }, + "BadRequestError": { + "x-scalar-ignore": true, + "description": "RFC 7807 (https://datatracker.ietf.org/doc/html/rfc7807)", + "type": "object", + "properties": { + "type": { + "type": "string", + "examples": [ + "https://example.com/errors/bad-request" + ] + }, + "title": { + "type": "string", + "examples": [ + "Bad Request" + ] + }, + "status": { + "type": "integer", + "format": "int64", + "examples": [ + 400 + ] + }, + "detail": { + "type": "string", + "examples": [ + "The request was invalid." + ] + } + } + }, + "ForbiddenError": { + "x-scalar-ignore": true, + "description": "RFC 7807 (https://datatracker.ietf.org/doc/html/rfc7807)", + "type": "object", + "properties": { + "type": { + "type": "string", + "examples": [ + "https://example.com/errors/forbidden" + ] + }, + "title": { + "type": "string", + "examples": [ + "Forbidden" + ] + }, + "status": { + "type": "integer", + "format": "int64", + "examples": [ + 403 + ] + }, + "detail": { + "type": "string", + "examples": [ + "You are not authorized to access this resource." + ] + } + } + }, + "NotFoundError": { + "x-scalar-ignore": true, + "description": "RFC 7807 (https://datatracker.ietf.org/doc/html/rfc7807)", + "type": "object", + "properties": { + "type": { + "type": "string", + "examples": [ + "https://example.com/errors/not-found" + ] + }, + "title": { + "type": "string", + "examples": [ + "Not Found" + ] + }, + "status": { + "type": "integer", + "format": "int64", + "examples": [ + 404 + ] + }, + "detail": { + "type": "string", + "examples": [ + "The resource you are trying to access does not exist." + ] + } + } + }, + "UnauthorizedError": { + "x-scalar-ignore": true, + "description": "RFC 7807 (https://datatracker.ietf.org/doc/html/rfc7807)", + "type": "object", + "properties": { + "type": { + "type": "string", + "examples": [ + "https://example.com/errors/not-found" + ] + }, + "title": { + "type": "string", + "examples": [ + "Unauthorized" + ] + }, + "status": { + "type": "integer", + "format": "int64", + "examples": [ + 401 + ] + }, + "detail": { + "type": "string", + "examples": [ + "You are not authorized to access this resource." + ] + } + } + }, + "Conflict": { + "x-scalar-ignore": true, + "description": "RFC 7807 (https://datatracker.ietf.org/doc/html/rfc7807)", + "type": "object", + "properties": { + "type": { + "type": "string", + "examples": [ + "https://example.com/errors/conflict" + ] + }, + "title": { + "type": "string", + "examples": [ + "Conflict" + ] + }, + "status": { + "type": "integer", + "format": "int64", + "examples": [ + 409 + ] + }, + "detail": { + "type": "string", + "examples": [ + "The resource you are trying to access is in conflict." + ] + } + } + }, + "UnprocessableEntity": { + "x-scalar-ignore": true, + "description": "RFC 7807 (https://datatracker.ietf.org/doc/html/rfc7807)", + "type": "object", + "properties": { + "type": { + "type": "string", + "examples": [ + "https://example.com/errors/unprocessable-entity" + ] + }, + "title": { + "type": "string", + "examples": [ + "Unprocessable Entity" + ] + }, + "status": { + "type": "integer", + "format": "int64", + "examples": [ + 422 + ] + }, + "detail": { + "type": "string", + "examples": [ + "The request was invalid." + ] + } + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e495ce --- /dev/null +++ b/package.json @@ -0,0 +1,58 @@ +{ + "name": "@scalar/galaxy-sdk", + "version": "0.1.1", + "description": "The Scalar Galaxy is an example OpenAPI document to test OpenAPI tools and libraries. It's a fictional universe with fictional planets and fictional data. Get all the data for all planets.", + "repository": { + "type": "git", + "url": "git+https://github.com/scalar/galaxy-node-sdk.git" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "types": "./dist/esm/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "default": "./dist/esm/index.js" + }, + "./resources/*": { + "types": "./dist/esm/resources/*.d.ts", + "import": "./dist/esm/resources/*.js", + "require": "./dist/cjs/resources/*.js", + "default": "./dist/esm/resources/*.js" + }, + "./*.mjs": { + "types": "./dist/esm/*.d.ts", + "import": "./dist/esm/*.js", + "default": "./dist/esm/*.js" + }, + "./*.js": { + "types": "./dist/esm/*.d.ts", + "require": "./dist/cjs/*.js", + "default": "./dist/cjs/*.js" + }, + "./*": { + "types": "./dist/esm/*.d.ts", + "import": "./dist/esm/*.js", + "require": "./dist/cjs/*.js", + "default": "./dist/esm/*.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "api.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/finalize-build.mjs", + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.cjs.json --noEmit", + "prepublishOnly": "npm run build" + }, + "dependencies": {}, + "devDependencies": { + "typescript": "^6.0.0" + } +} diff --git a/scalar-sdk.manifest.json b/scalar-sdk.manifest.json new file mode 100644 index 0000000..ab40773 --- /dev/null +++ b/scalar-sdk.manifest.json @@ -0,0 +1,1802 @@ +{ + "name": "ScalarGalaxy", + "slug": "scalarGalaxy", + "version": "0.1.1", + "servers": [ + "https://galaxy.scalar.com", + "{protocol}://void.scalar.com/{path}" + ], + "environments": { + "production": "https://galaxy.scalar.com", + "responds_with_your_request_data": "{protocol}://void.scalar.com/{path}" + }, + "environmentOrder": [ + "production", + "responds_with_your_request_data" + ], + "auth": [ + "bearer", + "basic", + "apiKey", + "apiKey", + "apiKey", + "oauth2", + "oauth2" + ], + "authDetails": [ + { + "kind": "bearer", + "id": "bearerAuth" + }, + { + "kind": "basic", + "id": "basicAuth" + }, + { + "kind": "apiKey", + "id": "apiKeyHeader", + "in": "header", + "paramName": "X-API-Key" + }, + { + "kind": "apiKey", + "id": "apiKeyQuery", + "in": "query", + "paramName": "api_key" + }, + { + "kind": "apiKey", + "id": "apiKeyCookie", + "in": "cookie", + "paramName": "api_key" + }, + { + "kind": "oauth2", + "id": "oAuth2", + "flows": [ + { + "kind": "authorizationCode", + "authorizationUrl": "https://galaxy.scalar.com/oauth/authorize", + "tokenUrl": "https://galaxy.scalar.com/oauth/token", + "refreshUrl": "", + "scopes": [ + "read:account", + "write:planets", + "read:planets" + ] + }, + { + "kind": "clientCredentials", + "tokenUrl": "https://galaxy.scalar.com/oauth/token", + "refreshUrl": "", + "scopes": [ + "read:account", + "write:planets", + "read:planets" + ] + }, + { + "kind": "password", + "tokenUrl": "https://galaxy.scalar.com/oauth/token", + "refreshUrl": "", + "scopes": [ + "read:account", + "write:planets", + "read:planets" + ] + }, + { + "kind": "implicit", + "authorizationUrl": "https://galaxy.scalar.com/oauth/authorize", + "refreshUrl": "", + "scopes": [ + "read:account", + "write:planets", + "read:planets" + ] + } + ] + }, + { + "kind": "oauth2", + "id": "openIdConnect", + "flows": [], + "openIdConnectUrl": "https://galaxy.scalar.com/.well-known/openid-configuration" + } + ], + "clientHeaderParams": [], + "schemas": [ + { + "name": "User", + "source": "User", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "id", + "publicName": "id", + "required": false, + "deprecated": false, + "access": "readOnly", + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + }, + { + "name": "name", + "publicName": "name", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "Credentials", + "source": "Credentials", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "email", + "publicName": "email", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "email" + } + }, + { + "name": "password", + "publicName": "password", + "required": true, + "deprecated": false, + "access": "writeOnly", + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "Token", + "source": "Token", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "token", + "publicName": "token", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "CelestialBody", + "source": "CelestialBody", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "ref", + "name": "Planet" + }, + { + "kind": "ref", + "name": "Satellite" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "planet": "Planet", + "satellite": "Satellite" + } + }, + "variantNames": [ + null, + null + ] + } + }, + { + "name": "Planet", + "source": "Planet", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "id", + "publicName": "id", + "required": true, + "deprecated": false, + "access": "readOnly", + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + }, + { + "name": "name", + "publicName": "name", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "description", + "publicName": "description", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } + }, + { + "name": "type", + "publicName": "type", + "required": true, + "deprecated": false, + "type": { + "kind": "enum", + "values": [ + "planet", + "terrestrial", + "gas_giant", + "ice_giant", + "dwarf", + "super_earth" + ], + "names": [ + "Planet", + "Terrestrial", + "GasGiant", + "IceGiant", + "Dwarf", + "SuperEarth" + ], + "deprecations": [ + false, + false, + false, + false, + false, + false + ] + } + }, + { + "name": "habitabilityIndex", + "publicName": "habitabilityIndex", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float", + "validation": {} + } + }, + { + "name": "physicalProperties", + "publicName": "physicalProperties", + "required": false, + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "mass", + "publicName": "mass", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + }, + { + "name": "radius", + "publicName": "radius", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + }, + { + "name": "gravity", + "publicName": "gravity", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + }, + { + "name": "temperature", + "publicName": "temperature", + "required": false, + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "min", + "publicName": "min", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + }, + { + "name": "max", + "publicName": "max", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + }, + { + "name": "average", + "publicName": "average", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + } + ], + "additionalProperties": { + "kind": "primitive", + "type": "number", + "format": "float" + } + } + } + ], + "additionalProperties": { + "kind": "primitive", + "type": "number", + "format": "float" + } + } + }, + { + "name": "atmosphere", + "publicName": "atmosphere", + "required": false, + "deprecated": false, + "type": { + "kind": "array", + "items": { + "kind": "object", + "properties": [ + { + "name": "compound", + "publicName": "compound", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "percentage", + "publicName": "percentage", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + } + ], + "additionalProperties": { + "kind": "primitive", + "type": "string" + } + } + } + }, + { + "name": "discoveredAt", + "publicName": "discoveredAt", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "date-time" + } + }, + { + "name": "image", + "publicName": "image", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } + }, + { + "name": "satellites", + "publicName": "satellites", + "required": false, + "deprecated": false, + "type": { + "kind": "array", + "items": { + "kind": "ref", + "name": "Satellite" + } + } + }, + { + "name": "creator", + "publicName": "creator", + "required": false, + "deprecated": false, + "type": { + "kind": "ref", + "name": "User" + } + }, + { + "name": "tags", + "publicName": "tags", + "required": false, + "deprecated": false, + "type": { + "kind": "array", + "items": { + "kind": "primitive", + "type": "string" + } + } + }, + { + "name": "lastUpdated", + "publicName": "lastUpdated", + "required": false, + "deprecated": false, + "access": "readOnly", + "type": { + "kind": "primitive", + "type": "string", + "format": "date-time" + } + }, + { + "name": "successCallbackUrl", + "publicName": "successCallbackUrl", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "uri" + } + }, + { + "name": "failureCallbackUrl", + "publicName": "failureCallbackUrl", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string", + "format": "uri" + } + } + ], + "additionalProperties": false + } + }, + { + "name": "Satellite", + "source": "Satellite", + "publicAliases": [], + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "id", + "publicName": "id", + "required": false, + "deprecated": false, + "access": "readOnly", + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + }, + { + "name": "name", + "publicName": "name", + "required": true, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "string" + } + }, + { + "name": "description", + "publicName": "description", + "required": false, + "deprecated": false, + "type": { + "kind": "union", + "variants": [ + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "null" + } + ] + } + }, + { + "name": "diameter", + "publicName": "diameter", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + }, + { + "name": "type", + "publicName": "type", + "required": true, + "deprecated": false, + "type": { + "kind": "enum", + "values": [ + "satellite", + "moon", + "asteroid", + "comet" + ], + "names": [ + "Satellite", + "Moon", + "Asteroid", + "Comet" + ], + "deprecations": [ + false, + false, + false, + false + ] + } + }, + { + "name": "orbit", + "publicName": "orbit", + "required": false, + "deprecated": false, + "type": { + "kind": "object", + "properties": [ + { + "name": "planetId", + "publicName": "planetId", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "integer", + "format": "int64" + } + }, + { + "name": "orbitalPeriod", + "publicName": "orbitalPeriod", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + }, + { + "name": "distance", + "publicName": "distance", + "required": false, + "deprecated": false, + "type": { + "kind": "primitive", + "type": "number", + "format": "float" + } + } + ], + "additionalProperties": false + } + } + ], + "additionalProperties": false + } + } + ], + "resources": [ + "planets", + "celestialBodies", + "authentication" + ], + "publicResources": [ + "planets", + "celestialBodies", + "authentication" + ], + "operations": [ + { + "resource": "planets", + "publicResource": "planets", + "operation": "listAllData", + "publicOperation": "listAllData", + "deprecated": false, + "method": "GET", + "path": "/planets", + "pathParams": [], + "publicPathParams": [], + "queryParams": [ + "limit", + "offset" + ], + "publicQueryParams": [ + "limit", + "offset" + ], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [], + "publicBodyParams": [], + "pathParamDetails": [], + "queryParamDetails": [ + { + "name": "limit", + "required": false, + "style": "form", + "explode": true, + "defaultValue": 10 + }, + { + "name": "offset", + "required": false, + "style": "form", + "explode": true, + "defaultValue": 0 + } + ], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "PlanetListAllDataParams" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + "errorResponses": [], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "planets", + "publicResource": "planets", + "operation": "create", + "publicOperation": "create", + "deprecated": false, + "method": "POST", + "path": "/planets", + "pathParams": [], + "publicPathParams": [], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [ + "id", + "name", + "description", + "type", + "habitabilityIndex", + "physicalProperties", + "atmosphere", + "discoveredAt", + "image", + "satellites", + "creator", + "tags", + "lastUpdated", + "successCallbackUrl", + "failureCallbackUrl" + ], + "publicBodyParams": [ + "id", + "name", + "description", + "type", + "habitabilityIndex", + "physicalProperties", + "atmosphere", + "discoveredAt", + "image", + "satellites", + "creator", + "tags", + "lastUpdated", + "successCallbackUrl", + "failureCallbackUrl" + ], + "pathParamDetails": [], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "PlanetCreateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "json" + } + ], + "required": false, + "publicName": "body", + "publicIdentifier": "body" + }, + "response": { + "status": "201", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + "responseModel": { + "name": "Planet", + "publicAliases": [] + }, + "result": { + "successStatus": "201", + "errorStatuses": [ + "400", + "403" + ] + }, + "errorResponses": [ + { + "status": "400", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "planets", + "publicResource": "planets", + "operation": "retrieve", + "publicOperation": "retrieve", + "deprecated": false, + "method": "GET", + "path": "/planets/{planetId}", + "pathParams": [ + "planetId" + ], + "publicPathParams": [ + "planetId" + ], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [], + "publicBodyParams": [], + "pathParamDetails": [ + { + "name": "planetId", + "required": true, + "style": "simple", + "explode": false + } + ], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "PlanetRetrieveParams" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + "responseModel": { + "name": "Planet", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "404" + ] + }, + "errorResponses": [ + { + "status": "404", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "planets", + "publicResource": "planets", + "operation": "update", + "publicOperation": "update", + "deprecated": false, + "method": "PUT", + "path": "/planets/{planetId}", + "pathParams": [ + "planetId" + ], + "publicPathParams": [ + "planetId" + ], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [ + "id", + "name", + "description", + "type", + "habitabilityIndex", + "physicalProperties", + "atmosphere", + "discoveredAt", + "image", + "satellites", + "creator", + "tags", + "lastUpdated", + "successCallbackUrl", + "failureCallbackUrl" + ], + "publicBodyParams": [ + "id", + "name", + "description", + "type", + "habitabilityIndex", + "physicalProperties", + "atmosphere", + "discoveredAt", + "image", + "satellites", + "creator", + "tags", + "lastUpdated", + "successCallbackUrl", + "failureCallbackUrl" + ], + "pathParamDetails": [ + { + "name": "planetId", + "required": true, + "style": "simple", + "explode": false + } + ], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "PlanetUpdateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "json" + } + ], + "required": false, + "publicName": "body", + "publicIdentifier": "body" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + "responseModel": { + "name": "Planet", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "400", + "403", + "404" + ] + }, + "errorResponses": [ + { + "status": "400", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "404", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "planets", + "publicResource": "planets", + "operation": "delete", + "publicOperation": "delete", + "deprecated": false, + "method": "DELETE", + "path": "/planets/{planetId}", + "pathParams": [ + "planetId" + ], + "publicPathParams": [ + "planetId" + ], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [], + "publicBodyParams": [], + "pathParamDetails": [ + { + "name": "planetId", + "required": true, + "style": "simple", + "explode": false + } + ], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "PlanetDeleteParams" + }, + "result": { + "successStatus": "204", + "errorStatuses": [ + "404" + ] + }, + "errorResponses": [ + { + "status": "404", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "planets", + "publicResource": "planets", + "operation": "uploadImage", + "publicOperation": "uploadImage", + "deprecated": false, + "method": "POST", + "path": "/planets/{planetId}/image", + "pathParams": [ + "planetId" + ], + "publicPathParams": [ + "planetId" + ], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [ + "image" + ], + "publicBodyParams": [ + "image" + ], + "pathParamDetails": [ + { + "name": "planetId", + "required": true, + "style": "simple", + "explode": false + } + ], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "PlanetUploadImageParams" + }, + "requestBody": { + "contentType": "multipart/form-data", + "encoding": "multipart", + "contents": [ + { + "contentType": "multipart/form-data", + "encoding": "multipart" + } + ], + "required": false, + "publicName": "body", + "publicIdentifier": "body" + }, + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "400", + "403", + "404" + ] + }, + "errorResponses": [ + { + "status": "400", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "404", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "celestialBodies", + "publicResource": "celestialBodies", + "operation": "create", + "publicOperation": "create", + "deprecated": false, + "method": "POST", + "path": "/celestial-bodies", + "pathParams": [], + "publicPathParams": [], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [ + "body" + ], + "publicBodyParams": [ + "body" + ], + "pathParamDetails": [], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "CelestialBodyCreateParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ], + "required": true, + "publicName": "body", + "publicIdentifier": "body" + }, + "response": { + "status": "201", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + } + ] + }, + "responseModel": { + "name": "CelestialBody", + "publicAliases": [] + }, + "errorResponses": [], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "authentication", + "publicResource": "authentication", + "operation": "createUser", + "publicOperation": "createUser", + "deprecated": false, + "method": "POST", + "path": "/user/signup", + "pathParams": [], + "publicPathParams": [], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [ + "id", + "name", + "email", + "password" + ], + "publicBodyParams": [ + "id", + "name", + "email", + "password" + ], + "pathParamDetails": [], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "AuthenticationCreateUserParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "json" + } + ], + "required": false, + "publicName": "body", + "publicIdentifier": "body" + }, + "response": { + "status": "201", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + "responseModel": { + "name": "User", + "publicAliases": [] + }, + "result": { + "successStatus": "201", + "errorStatuses": [ + "400", + "401", + "403", + "409", + "422" + ] + }, + "errorResponses": [ + { + "status": "400", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "409", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "422", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "authentication", + "publicResource": "authentication", + "operation": "createToken", + "publicOperation": "createToken", + "deprecated": false, + "method": "POST", + "path": "/auth/token", + "pathParams": [], + "publicPathParams": [], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [ + "email", + "password" + ], + "publicBodyParams": [ + "email", + "password" + ], + "pathParamDetails": [], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "paramsModel": { + "publicName": "AuthenticationCreateTokenParams" + }, + "requestBody": { + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "json" + } + ], + "required": false, + "publicName": "body", + "publicIdentifier": "body" + }, + "response": { + "status": "201", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + "responseModel": { + "name": "Token", + "publicAliases": [] + }, + "result": { + "successStatus": "201", + "errorStatuses": [ + "400", + "401", + "403" + ] + }, + "errorResponses": [ + { + "status": "400", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + } + ], + "responseLinks": [], + "transport": "http" + }, + { + "resource": "authentication", + "publicResource": "authentication", + "operation": "listMe", + "publicOperation": "listMe", + "deprecated": false, + "method": "GET", + "path": "/me", + "pathParams": [], + "publicPathParams": [], + "queryParams": [], + "publicQueryParams": [], + "headerParams": [], + "publicHeaderParams": [], + "bodyParams": [], + "publicBodyParams": [], + "pathParamDetails": [], + "queryParamDetails": [], + "headerParamDetails": [], + "cookieParams": [], + "publicCookieParams": [], + "cookieParamDetails": [], + "response": { + "status": "200", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + "responseModel": { + "name": "User", + "publicAliases": [] + }, + "result": { + "successStatus": "200", + "errorStatuses": [ + "401", + "403" + ] + }, + "errorResponses": [ + { + "status": "401", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + }, + { + "status": "403", + "contentType": "application/json", + "encoding": "json", + "contents": [ + { + "contentType": "application/json", + "encoding": "json" + }, + { + "contentType": "application/xml", + "encoding": "binary" + } + ] + } + ], + "responseLinks": [], + "transport": "http" + } + ], + "webhooks": [ + { + "event": "newPlanet", + "name": "NewPlanet", + "source": "webhook", + "method": "POST", + "path": "newPlanet" + }, + { + "event": "{$request.body#/successCallbackUrl}", + "name": "RequestBodySuccessCallbackUrl", + "source": "callback", + "method": "POST", + "path": "POST /planets -> {$request.body#/successCallbackUrl}", + "payloadModel": { + "name": "Planet", + "publicAliases": [] + } + }, + { + "event": "{$request.body#/successCallbackUrl}", + "name": "RequestBodySuccessCallbackUrl", + "source": "callback", + "method": "POST", + "path": "POST /planets -> {$request.body#/successCallbackUrl}", + "payloadModel": { + "name": "Planet", + "publicAliases": [] + } + } + ] +} diff --git a/scripts/finalize-build.mjs b/scripts/finalize-build.mjs new file mode 100644 index 0000000..11472f1 --- /dev/null +++ b/scripts/finalize-build.mjs @@ -0,0 +1,49 @@ +import { readdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, extname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const RELATIVE_SPECIFIER_RE = /(from\s+["']|import\(\s*["'])(\.{1,2}\/[^"']+)(["'])/g + +await Promise.all([ + rewriteRelativeSpecifiers(resolve(root, 'dist/esm')), + markCommonJsOutput(resolve(root, 'dist/cjs')), +]) + +async function rewriteRelativeSpecifiers(dir) { + let entries + try { + entries = await readdir(dir, { withFileTypes: true }) + } catch (error) { + if (error && error.code === 'ENOENT') return + throw error + } + + await Promise.all( + entries.map(async (entry) => { + const path = resolve(dir, entry.name) + if (entry.isDirectory()) { + await rewriteRelativeSpecifiers(path) + return + } + if (!path.endsWith('.js') && !path.endsWith('.d.ts')) return + const source = await readFile(path, 'utf8') + await writeFile(path, source.replace(RELATIVE_SPECIFIER_RE, addJsExtension), 'utf8') + }), + ) +} + +async function markCommonJsOutput(dir) { + try { + await readdir(dir) + await writeFile(resolve(dir, 'package.json'), '{\n "type": "commonjs"\n}\n', 'utf8') + } catch (error) { + if (error && error.code === 'ENOENT') return + throw error + } +} + +function addJsExtension(match, prefix, specifier, suffix) { + if (extname(specifier)) return match + return `${prefix}${specifier}.js${suffix}` +} diff --git a/src/api-promise.ts b/src/api-promise.ts new file mode 100644 index 0000000..e30110d --- /dev/null +++ b/src/api-promise.ts @@ -0,0 +1,81 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import type { ScalarGalaxy } from './client'; +import type { FinalRequestOptions } from './internal/request-options'; + +export type APIResponseProps = { + readonly response: Response; + readonly options: FinalRequestOptions; + readonly controller: AbortController; + readonly requestLogID?: string | undefined; + readonly retryOfRequestLogID?: string | undefined; + readonly startTime?: number | undefined; +}; + +export type ParseResponse = (client: ScalarGalaxy, props: APIResponseProps) => T | Promise; + +export const defaultParseResponse = async (_client: unknown, props: APIResponseProps): Promise => { + const { response } = props; + if (response.status === 204) return null as T; + if (props.options.__binaryResponse) return response as T; + const contentType = response.headers.get('content-type'); + const mediaType = contentType?.split(';')[0]?.trim(); + const isJson = mediaType?.includes('application/json') || mediaType?.endsWith('+json'); + if (isJson && response.headers.get('content-length') === '0') return undefined as T; + if (isJson) return (await response.json()) as T; + return (await response.text()) as unknown as T; +}; + +/** A Promise subclass providing SDK response helpers. */ +export class APIPromise extends Promise { + private parsedPromise: Promise | undefined; + + constructor( + private readonly client: ScalarGalaxy, + private readonly responsePromise: Promise, + private readonly parseResponse: ParseResponse = defaultParseResponse, + ) { + super((resolve) => { + resolve(undefined as T); + }); + } + + _thenUnwrap(transform: (data: T, props: APIResponseProps) => U): APIPromise { + return new APIPromise(this.client, this.responsePromise, async (client, props) => + transform(await this.parseResponse(client, props), props), + ); + } + + asResponse(): Promise { + return this.responsePromise.then((props) => props.response); + } + + async withResponse(): Promise<{ data: T; response: Response }> { + const [data, response] = await Promise.all([this.parse(), this.asResponse()]); + return { data, response }; + } + + private parse(): Promise { + if (!this.parsedPromise) { + this.parsedPromise = this.responsePromise.then((props) => this.parseResponse(this.client, props)); + } + return this.parsedPromise; + } + + override then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | undefined | null, + ): Promise { + return this.parse().then(onfulfilled, onrejected); + } + + override catch( + onrejected?: ((reason: unknown) => TResult | PromiseLike) | undefined | null, + ): Promise { + return this.parse().catch(onrejected); + } + + override finally(onfinally?: (() => void) | undefined | null): Promise { + return this.parse().finally(onfinally); + } +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..565aa10 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,952 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { APIPromise, type APIResponseProps } from './api-promise'; +import * as Errors from './error'; +import { uuid4 } from './internal/utils/uuid'; +import { validatePositiveInteger, isAbsoluteURL, safeJSON, isEmptyObj } from './internal/utils/values'; +import { sleep } from './internal/utils/sleep'; +import { castToError, isAbortError } from './internal/errors'; +import { getPlatformHeaders } from './internal/detect-platform'; +import * as Shims from './internal/shims'; +import * as Opts from './internal/request-options'; +import { readEnv } from './internal/utils/env'; +import { formatRequestDetails, loggerFor, parseLogLevel, type LogLevel, type Logger } from './internal/utils/log'; +export type { Logger, LogLevel } from './internal/utils/log'; +import type { RequestInit, RequestInfo, BodyInit, Fetch } from './internal/builtin-types'; +import { buildHeaders, type HeadersLike } from './internal/headers'; +import type { FinalRequestOptions, RequestOptions } from './internal/request-options'; +import type { HTTPMethod, FinalizedRequestInit, MergedRequestInit, PromiseOrValue } from './internal/types'; +import { stringify as stringifyQuery } from './internal/qs/stringify'; +import type { StringifyOptions } from './internal/qs/types'; +import { toFile } from './core/uploads'; +import { VERSION } from './version'; +import { Planets, type Planet, type PlanetListAllDataResponse, type PlanetUploadImageResponse, type PlanetListAllDataParams, type PlanetCreateParams, type PlanetUpdateParams, type PlanetUploadImageParams } from "./resources/planets"; +import { CelestialBodies, type CelestialBody, type CelestialBodyCreateParams } from "./resources/celestial-bodies"; +import { Authentication, type User, type Credentials, type AuthenticationCreateTokenResponse, type AuthenticationCreateUserParams, type AuthenticationCreateTokenParams } from "./resources/authentication"; + +export type AuthTokenProvider = () => string | Promise; + +const queryArrayFormat: NonNullable = "indices"; +const queryAllowDots = false; + +const environments = { + production: "https://galaxy.scalar.com", + responds_with_your_request_data: "{protocol}://void.scalar.com/{path}", +}; +type Environment = keyof typeof environments; + +export interface ClientOptions { + /** + * JWT Bearer token authentication + */ + bearerAuth?: string | AuthTokenProvider | undefined; + + /** + * Basic HTTP authentication + */ + basicAuthUsername?: string | AuthTokenProvider | undefined; + + /** + * API key request header + */ + apiKeyHeader?: string | AuthTokenProvider | undefined; + + /** + * API key query parameter + */ + apiKeyQuery?: string | AuthTokenProvider | undefined; + + /** + * API key browser cookie + */ + apiKeyCookie?: string | AuthTokenProvider | undefined; + + /** + * OAuth 2.0 authentication + */ + oAuth2?: string | AuthTokenProvider | undefined; + + /** + * OpenID Connect Authentication + */ + openIDConnect?: string | AuthTokenProvider | undefined; + + /** + * Specifies the environment to use for the API. + * + * Each environment maps to a different base URL: + * - `production` corresponds to `https://galaxy.scalar.com` + * - `responds_with_your_request_data` corresponds to `{protocol}://void.scalar.com/{path}` + */ + environment?: Environment | undefined; + + /** + * Override the default base URL for the API, e.g., "https://api.example.com/v2/" + * + * Defaults to process.env["SCALAR_BASE_URL"]. + */ + baseURL?: string | null | undefined; + + /** + * The maximum amount of time (in milliseconds) that the client should wait for a response + * from the server before timing out a single request. + * + * Note that request timeouts are retried by default, so in a worst-case scenario you may wait + * much longer than this timeout before the promise succeeds or fails. + * + * @unit milliseconds + */ + timeout?: number | undefined; + + /** + * Additional `RequestInit` options to be passed to `fetch` calls. + * Properties will be overridden by per-request `fetchOptions`. + */ + fetchOptions?: MergedRequestInit | undefined; + + /** + * Specify a custom `fetch` function implementation. + * + * If not provided, we expect that `fetch` is defined globally. + */ + fetch?: Fetch | undefined; + + /** + * The maximum number of times that the client will retry a request in case of a + * temporary failure, like a network error or a 5XX error from the server. + * + * @default 2 + */ + maxRetries?: number | undefined; + + /** + * Default headers to include with every request to the API. + * + * These can be removed in individual requests by explicitly setting the + * header to `null` in request options. + */ + defaultHeaders?: HeadersLike | undefined; + + /** + * Default query parameters to include with every request to the API. + * + * These can be removed in individual requests by explicitly setting the + * param to `undefined` in request options. + */ + defaultQuery?: Record | undefined; + + /** + * Set the log level. + * + * Defaults to process.env["SCALAR_LOG"] or 'warn' if it isn't set. + */ + logLevel?: LogLevel | undefined; + + /** + * Set the logger. + * + * Defaults to globalThis.console. + */ + logger?: Logger | undefined; +} + +export type ScalarGalaxyOptions = ClientOptions; + +/** + * API Client for interfacing with the ScalarGalaxy API. + */ +export class ScalarGalaxy { + bearerAuth: string | AuthTokenProvider | undefined; + basicAuthUsername: string | AuthTokenProvider | undefined; + apiKeyHeader: string | AuthTokenProvider | undefined; + apiKeyQuery: string | AuthTokenProvider | undefined; + apiKeyCookie: string | AuthTokenProvider | undefined; + oAuth2: string | AuthTokenProvider | undefined; + openIDConnect: string | AuthTokenProvider | undefined; + + baseURL: string; + maxRetries: number; + timeout: number; + logger: Logger; + logLevel: LogLevel | undefined; + fetchOptions: MergedRequestInit | undefined; + private fetch: Fetch; + #encoder: Opts.RequestEncoder; + protected idempotencyHeader?: string; + private _baseURLOverridden: boolean; + private _defaultBaseURL: string; + private _options: ClientOptions; + + /** + * API Client for interfacing with the ScalarGalaxy API. + * + * @param {string | AuthTokenProvider | undefined} [opts.bearerAuth=process.env["BEARER_AUTH"] ?? undefined] + * @param {string | AuthTokenProvider | undefined} [opts.basicAuthUsername=process.env["BASIC_AUTH_USERNAME"] ?? undefined] + * @param {string | AuthTokenProvider | undefined} [opts.apiKeyHeader=process.env["API_KEY_HEADER"] ?? undefined] + * @param {string | AuthTokenProvider | undefined} [opts.apiKeyQuery=process.env["API_KEY_QUERY"] ?? undefined] + * @param {string | AuthTokenProvider | undefined} [opts.apiKeyCookie=process.env["API_KEY_COOKIE"] ?? undefined] + * @param {string | AuthTokenProvider | undefined} [opts.oAuth2=process.env["SCALAR_OAUTH2"] ?? undefined] + * @param {string | AuthTokenProvider | undefined} [opts.openIDConnect=process.env["SCALAR_OPENIDCONNECT"] ?? undefined] + * @param {Environment} [opts.environment=production] - Specifies the environment URL to use for the API. + * @param {string} [opts.baseURL=process.env["SCALAR_BASE_URL"] ?? https://galaxy.scalar.com] - Override the default base URL for the API. + * @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out. + * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls. + * @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation. + * @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request. + * @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API. + * @param {Record} opts.defaultQuery - Default query parameters to include with every request to the API. + */ + constructor({ + baseURL = readEnv("SCALAR_BASE_URL"), + bearerAuth = readEnv("BEARER_AUTH"), + basicAuthUsername = readEnv("BASIC_AUTH_USERNAME"), + apiKeyHeader = readEnv("API_KEY_HEADER"), + apiKeyQuery = readEnv("API_KEY_QUERY"), + apiKeyCookie = readEnv("API_KEY_COOKIE"), + oAuth2 = readEnv("SCALAR_OAUTH2"), + openIDConnect = readEnv("SCALAR_OPENIDCONNECT"), + ...opts + }: ClientOptions = {}) { + const options: ClientOptions = { + bearerAuth, + basicAuthUsername, + apiKeyHeader, + apiKeyQuery, + apiKeyCookie, + oAuth2, + openIDConnect, + ...opts, + baseURL: baseURL || null, + }; + const environment = options.environment ?? "production"; + const baseURLOverridden = baseURL !== null && baseURL !== undefined && baseURL !== ""; + if (baseURLOverridden && options.environment) throw new Errors.ScalarGalaxyError("Ambiguous URL; The `baseURL` option (or SCALAR_BASE_URL env var) and the `environment` option are given. If you want to use the environment you must pass baseURL: null"); + const defaultBaseURL = environments[environment]; + this.baseURL = options.baseURL || defaultBaseURL; + this.timeout = options.timeout ?? ScalarGalaxy.DEFAULT_TIMEOUT /* 1 minute */; + this.logger = options.logger ?? console; + const defaultLogLevel = 'warn'; + // Set default logLevel early so that we can log a warning in parseLogLevel. + this.logLevel = defaultLogLevel; + this.logLevel = + parseLogLevel(options.logLevel, 'ClientOptions.logLevel', this) ?? + parseLogLevel(readEnv("SCALAR_LOG"), "process.env[\"SCALAR_LOG\"]", this) ?? + defaultLogLevel; + this.fetchOptions = options.fetchOptions; + this.maxRetries = options.maxRetries ?? 2; + this.fetch = options.fetch ?? Shims.getDefaultFetch(); + this.#encoder = Opts.FallbackEncoder; + + const customHeadersEnv = readEnv("SCALAR_CUSTOM_HEADERS"); + if (customHeadersEnv) { + const parsed: Record = {}; + for (const line of customHeadersEnv.split('\n')) { + const colon = line.indexOf(':'); + if (colon >= 0) { + parsed[line.substring(0, colon).trim()] = line.substring(colon + 1).trim(); + } + } + options.defaultHeaders = { ...parsed, ...options.defaultHeaders }; + } + + this._options = { ...options, baseURL: baseURLOverridden ? this.baseURL : undefined, environment }; + this._baseURLOverridden = baseURLOverridden; + this._defaultBaseURL = defaultBaseURL; + + this.bearerAuth = bearerAuth; + this.basicAuthUsername = basicAuthUsername; + this.apiKeyHeader = apiKeyHeader; + this.apiKeyQuery = apiKeyQuery; + this.apiKeyCookie = apiKeyCookie; + this.oAuth2 = oAuth2; + this.openIDConnect = openIDConnect; + } + + withOptions(options: Partial): this { + const client = new (this.constructor as new (props: ClientOptions) => this)({ + ...this._options, + ...(this.#baseURLOverridden() ? { baseURL: this.baseURL } : {}), + maxRetries: this.maxRetries, + timeout: this.timeout, + logger: this.logger, + logLevel: this.logLevel, + fetch: this.fetch, + fetchOptions: this.fetchOptions, + bearerAuth: this.bearerAuth, + basicAuthUsername: this.basicAuthUsername, + apiKeyHeader: this.apiKeyHeader, + apiKeyQuery: this.apiKeyQuery, + apiKeyCookie: this.apiKeyCookie, + oAuth2: this.oAuth2, + openIDConnect: this.openIDConnect, + ...options, + }); + return client; + } + + #baseURLOverridden(): boolean { + // A named environment selects a default URL; only explicit overrides should bypass per-request defaults. + return this._baseURLOverridden || this.baseURL !== this._defaultBaseURL; + } + + protected defaultQuery(): Record | undefined { + return this._options.defaultQuery; + } + + protected stringifyQuery(query: object | Record): string { + return stringifyQuery(query, { arrayFormat: queryArrayFormat, allowDots: queryAllowDots }); + } + + private getUserAgent(): string { + return `${this.constructor.name}/JS ${VERSION}`; + } + + protected defaultIdempotencyKey(): string { + return `scalar-node-retry-${uuid4()}`; + } + + protected makeStatusError( + status: number, + error: object | undefined, + message: string | undefined, + headers: Headers, + ): Errors.APIError { + return Errors.APIError.generate(status, error, message, headers); + } + + buildURL( + path: string, + query: Record | null | undefined, + defaultBaseURL?: string | undefined, + ): string { + const baseURL = (!this.#baseURLOverridden() && defaultBaseURL) || this.baseURL; + // Guarantee exactly one "/" between baseURL and path so that bases without a trailing slash + // and paths without a leading slash do not fuse into a malformed URL (e.g. ".../v1" + "widgets"). + const url = + isAbsoluteURL(path) ? + new URL(path) + : new URL((baseURL.endsWith('/') ? baseURL : baseURL + '/') + (path.startsWith('/') ? path.slice(1) : path)); + + const defaultQuery = this.defaultQuery(); + const pathQuery = Object.fromEntries(url.searchParams); + if (!isEmptyObj(defaultQuery) || !isEmptyObj(pathQuery)) { + query = { ...pathQuery, ...defaultQuery, ...query }; + } + + if (typeof query === "object" && query && !Array.isArray(query)) { + url.search = this.stringifyQuery(query); + } + + return url.toString(); + } + + /** + * Used as a callback for mutating the given `FinalRequestOptions` object. + */ + protected async prepareOptions(options: FinalRequestOptions): Promise {} + + /** + * Used as a callback for mutating the given `RequestInit` object. + * + * This is useful for cases where you want to add certain headers based off of + * the request properties, e.g. `method` or `url`. + */ + protected async prepareRequest( + request: RequestInit, + { url, options }: { url: string; options: FinalRequestOptions }, + ): Promise {} + + get(path: string, opts?: PromiseOrValue): APIPromise { + return this.methodRequest('get', path, opts); + } + + post(path: string, opts?: PromiseOrValue): APIPromise { + return this.methodRequest('post', path, opts); + } + + patch(path: string, opts?: PromiseOrValue): APIPromise { + return this.methodRequest('patch', path, opts); + } + + put(path: string, opts?: PromiseOrValue): APIPromise { + return this.methodRequest('put', path, opts); + } + + delete(path: string, opts?: PromiseOrValue): APIPromise { + return this.methodRequest('delete', path, opts); + } + + private methodRequest( + method: HTTPMethod, + path: string, + opts?: PromiseOrValue, + ): APIPromise { + return this.request( + Promise.resolve(opts).then((opts) => { + return { method, path, ...opts } as FinalRequestOptions; + }), + ); + } + + request( + options: PromiseOrValue, + remainingRetries: number | null = null, + ): APIPromise { + return new APIPromise(this, this.makeRequest(options, remainingRetries, undefined)); + } + + private async makeRequest( + optionsInput: PromiseOrValue, + retriesRemaining: number | null, + retryOfRequestLogID: string | undefined, + ): Promise { + const options = await optionsInput; + const maxRetries = options.maxRetries ?? this.maxRetries; + if (retriesRemaining == null) { + retriesRemaining = maxRetries; + } + + await this.prepareOptions(options); + + const { req, url, timeout } = await this.buildRequest(options, { + retryCount: maxRetries - retriesRemaining, + }); + + await this.prepareRequest(req, { url, options }); + + /** Not an API request ID, just for correlating local log entries. */ + const requestLogID = 'log_' + ((Math.random() * (1 << 24)) | 0).toString(16).padStart(6, '0'); + const retryLogStr = retryOfRequestLogID === undefined ? '' : `, retryOf: ${retryOfRequestLogID}`; + const startTime = Date.now(); + + loggerFor(this).debug( + `[${requestLogID}] sending request`, + formatRequestDetails({ + retryOfRequestLogID, + method: options.method, + url, + options, + headers: req.headers, + }), + ); + + if (options.signal?.aborted) { + throw new Errors.APIUserAbortError(); + } + + const controller = new AbortController(); + const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError); + const headersTime = Date.now(); + + if (response instanceof globalThis.Error) { + const retryMessage = `retrying, ${retriesRemaining} attempts remaining`; + if (options.signal?.aborted) { + throw new Errors.APIUserAbortError(); + } + // detect native connection timeout errors + // deno throws "TypeError: error sending request for url (https://example/): client error (Connect): tcp connect error: Operation timed out (os error 60): Operation timed out (os error 60)" + // undici throws "TypeError: fetch failed" with cause "ConnectTimeoutError: Connect Timeout Error (attempted address: example:443, timeout: 1ms)" + // others do not provide enough information to distinguish timeouts from other connection errors + const isTimeout = + isAbortError(response) || + /timed? ?out/i.test(String(response) + ('cause' in response ? String(response.cause) : '')); + if (retriesRemaining) { + loggerFor(this).info( + `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - ${retryMessage}`, + ); + loggerFor(this).debug( + `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (${retryMessage})`, + formatRequestDetails({ + retryOfRequestLogID, + url, + durationMs: headersTime - startTime, + message: response.message, + }), + ); + return this.retryRequest(options, retriesRemaining, retryOfRequestLogID ?? requestLogID); + } + loggerFor(this).info( + `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - error; no more retries left`, + ); + loggerFor(this).debug( + `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (error; no more retries left)`, + formatRequestDetails({ + retryOfRequestLogID, + url, + durationMs: headersTime - startTime, + message: response.message, + }), + ); + if (isTimeout) { + throw new Errors.APIConnectionTimeoutError(); + } + throw new Errors.APIConnectionError({ cause: response }); + } + + const responseInfo = `[${requestLogID}${retryLogStr}] ${req.method} ${url} ${ + response.ok ? 'succeeded' : 'failed' + } with status ${response.status} in ${headersTime - startTime}ms`; + + if (!response.ok) { + const shouldRetry = await this.shouldRetry(response); + if (retriesRemaining && shouldRetry) { + const retryMessage = `retrying, ${retriesRemaining} attempts remaining`; + + // We don't need the body of this response. + await Shims.CancelReadableStream(response.body); + loggerFor(this).info(`${responseInfo} - ${retryMessage}`); + loggerFor(this).debug( + `[${requestLogID}] response error (${retryMessage})`, + formatRequestDetails({ + retryOfRequestLogID, + url: response.url, + status: response.status, + headers: response.headers, + durationMs: headersTime - startTime, + }), + ); + return this.retryRequest( + options, + retriesRemaining, + retryOfRequestLogID ?? requestLogID, + response.headers, + ); + } + + const retryMessage = shouldRetry ? `error; no more retries left` : `error; not retryable`; + + loggerFor(this).info(`${responseInfo} - ${retryMessage}`); + + const errText = await response.text().catch((err: any) => castToError(err).message); + const errJSON = safeJSON(errText) as any; + const errMessage = errJSON ? undefined : errText; + + loggerFor(this).debug( + `[${requestLogID}] response error (${retryMessage})`, + formatRequestDetails({ + retryOfRequestLogID, + url: response.url, + status: response.status, + headers: response.headers, + message: errMessage, + durationMs: Date.now() - startTime, + }), + ); + + const err = this.makeStatusError(response.status, errJSON, errMessage, response.headers); + throw err; + } + + loggerFor(this).info(responseInfo); + loggerFor(this).debug( + `[${requestLogID}] response start`, + formatRequestDetails({ + retryOfRequestLogID, + url: response.url, + status: response.status, + headers: response.headers, + durationMs: headersTime - startTime, + }), + ); + + return { response, options, controller, requestLogID, retryOfRequestLogID, startTime }; + } + + async fetchWithTimeout(url: RequestInfo, init: RequestInit | undefined, ms: number, controller: AbortController): Promise { + const { signal, method, ...options } = init || {}; + const abort = this._makeAbort(controller); + if (signal) signal.addEventListener('abort', abort, { once: true }); + + const timeout = setTimeout(abort, ms); + + const isReadableBody = + ((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) || + (typeof options.body === 'object' && options.body !== null && Symbol.asyncIterator in options.body); + + const fetchOptions: RequestInit = { + signal: controller.signal as any, + ...(isReadableBody ? { duplex: 'half' } : {}), + method: 'GET', + ...options, + }; + if (method) { + // Custom methods like 'patch' need to be uppercased + // See https://github.com/nodejs/undici/issues/2294 + fetchOptions.method = method.toUpperCase(); + } + + try { + // use undefined this binding; fetch errors if bound to something else in browser/cloudflare + return await this.fetch.call(undefined, url, fetchOptions); + } finally { + clearTimeout(timeout); + } + } + + private async shouldRetry(response: Response): Promise { + // Note this is not a standard header. + const shouldRetryHeader = response.headers.get('x-should-retry'); + + // If the server explicitly says whether or not to retry, obey. + if (shouldRetryHeader === 'true') return true; + if (shouldRetryHeader === 'false') return false; + + // Retry on request timeouts. + if (response.status === 408) return true; + + // Retry on lock timeouts. + if (response.status === 409) return true; + + // Retry on rate limits. + if (response.status === 429) return true; + + // Retry internal errors. + if (response.status >= 500) return true; + + return false; + } + + private async retryRequest( + options: FinalRequestOptions, + retriesRemaining: number, + requestLogID: string, + responseHeaders?: Headers | undefined, + ): Promise { + let timeoutMillis: number | undefined; + + // Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it. + const retryAfterMillisHeader = responseHeaders?.get('retry-after-ms'); + if (retryAfterMillisHeader) { + const timeoutMs = parseFloat(retryAfterMillisHeader); + if (!Number.isNaN(timeoutMs)) { + timeoutMillis = timeoutMs; + } + } + + // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + const retryAfterHeader = responseHeaders?.get('retry-after'); + if (retryAfterHeader && !timeoutMillis) { + const timeoutSeconds = parseFloat(retryAfterHeader); + if (!Number.isNaN(timeoutSeconds)) { + timeoutMillis = timeoutSeconds * 1000; + } else { + timeoutMillis = Date.parse(retryAfterHeader) - Date.now(); + } + } + + // If the API asks us to wait a certain amount of time, just do what it says, + // but cap server-provided delays at 60s so an oversized or malformed Retry-After + // (e.g. `retry-after-ms: 999999999`, a past HTTP-date, or a value that Date.parse + // failed on) cannot block retries for an unbounded amount of time. Otherwise fall + // back to the default exponential-backoff calculation. + const maxRetryAfterMillis = 60 * 1000; + if ( + timeoutMillis === undefined || + !Number.isFinite(timeoutMillis) || + timeoutMillis <= 0 || + timeoutMillis > maxRetryAfterMillis + ) { + const maxRetries = options.maxRetries ?? this.maxRetries; + timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries); + } + await sleep(timeoutMillis); + + return this.makeRequest(options, retriesRemaining - 1, requestLogID); + } + + private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number { + const initialRetryDelay = 0.5; + const maxRetryDelay = 8.0; + + const numRetries = maxRetries - retriesRemaining; + + // Apply exponential backoff, but not more than the max. + const sleepSeconds = Math.min(initialRetryDelay * Math.pow(2, numRetries), maxRetryDelay); + + // Apply some jitter, take up to at most 25 percent of the retry time. + const jitter = 1 - Math.random() * 0.25; + + return sleepSeconds * jitter * 1000; + } + + async buildRequest( + inputOptions: FinalRequestOptions, + { retryCount = 0 }: { retryCount?: number } = {}, + ): Promise<{ req: FinalizedRequestInit; url: string; timeout: number }> { + const options = { ...inputOptions }; + const { method, path, query, defaultBaseURL } = options; + + const url = this.buildURL(path!, { ...await this.authQueryAsync(), ...((query as Record) ?? {}) }, defaultBaseURL); + if ('timeout' in options) validatePositiveInteger('timeout', options.timeout); + options.timeout = options.timeout ?? this.timeout; + const { bodyHeaders, body } = this.buildBody({ options }); + const reqHeaders = await this.buildHeaders({ options, method, bodyHeaders, retryCount, url }); + + const req: FinalizedRequestInit = { + method, + headers: reqHeaders, + ...(options.signal && { signal: options.signal }), + ...((globalThis as any).ReadableStream && + body instanceof (globalThis as any).ReadableStream && { duplex: 'half' }), + // `buildBody` already collapses no-body into `undefined`; here we only need to drop that + // sentinel. A truthiness spread would also strip an intentional empty-string body. + ...(body !== undefined && { body }), + ...((this.fetchOptions as any) ?? {}), + ...((options.fetchOptions as any) ?? {}), + }; + return { req, url, timeout: options.timeout }; + } + + private async buildHeaders({ + options, + method, + bodyHeaders, + retryCount, + url, + }: { + options: FinalRequestOptions; + method: HTTPMethod; + bodyHeaders: HeadersLike; + retryCount: number; + url: string; + }): Promise { + let idempotencyHeaders: HeadersLike = {}; + if (this.idempotencyHeader && method !== 'get') { + if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey(); + idempotencyHeaders[this.idempotencyHeader] = options.idempotencyKey; + } + + const headers = buildHeaders([ + idempotencyHeaders, + { + Accept: 'application/json', + 'User-Agent': this.getUserAgent(), + 'X-Scalar-Retry-Count': String(retryCount), + ...(options.timeout ? { 'X-Scalar-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}), + ...getPlatformHeaders(), + }, + await this.authHeaders(options), + this._options.defaultHeaders, + bodyHeaders, + options.headers, + ]); + appendAuthCookies(headers.values, await this.authCookiesAsync()); + + this.validateAuth(url, headers.values, options); + + return headers.values; + } + + private _makeAbort(controller: AbortController) { + // note: we can't just inline this method inside `fetchWithTimeout()` because then the closure + // would capture all request options, and cause a memory leak. + return () => controller.abort(); + } + + private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }): { + bodyHeaders: HeadersLike; + body: BodyInit | undefined; + } { + // Skip only `null`/`undefined` so an intentional empty-string (or 0/false) payload still + // reaches the encoder. A plain `!body` check would silently drop those falsy-but-valid bodies, + // and `null` must be excluded here too because the iterator check below uses `in`, which + // throws on null. + if (body == null) { + return { bodyHeaders: undefined, body: undefined }; + } + const headers = buildHeaders([rawHeaders]); + if ( + // Pass raw type verbatim + ArrayBuffer.isView(body) || + body instanceof ArrayBuffer || + body instanceof DataView || + // Always pass strings through verbatim. The previous guard required a caller-set + // `content-type` and otherwise fell through to `FallbackEncoder`, which JSON.stringifies + // the value and labels it `application/json` — silently quoting plain-text payloads and + // mislabeling them as JSON. fetch defaults a string body to `text/plain;charset=UTF-8` + // when no `content-type` is set, which is a safer default than misclaiming JSON. + typeof body === 'string' || + // `Blob` is superset of `File` + ((globalThis as any).Blob && body instanceof (globalThis as any).Blob) || + // `FormData` -> `multipart/form-data` + body instanceof FormData || + // `URLSearchParams` -> `application/x-www-form-urlencoded` + body instanceof URLSearchParams || + // Send chunked stream (each chunk has own `length`) + ((globalThis as any).ReadableStream && body instanceof (globalThis as any).ReadableStream) + ) { + return { bodyHeaders: undefined, body: body as BodyInit }; + } else if ( + typeof body === 'object' && + (Symbol.asyncIterator in body || + (Symbol.iterator in body && 'next' in body && typeof body.next === 'function')) + ) { + return { bodyHeaders: undefined, body: Shims.ReadableStreamFrom(body as AsyncIterable) }; + } else if ( + typeof body === 'object' && + headers.values.get('content-type') === 'application/x-www-form-urlencoded' + ) { + return { + bodyHeaders: { 'content-type': 'application/x-www-form-urlencoded' }, + body: this.stringifyQuery(body), + }; + } else { + return this.#encoder({ body, headers }); + } + } + + private validateAuth(url: string, headers: Headers, options: FinalRequestOptions): void { + if (headers.has("Authorization")) return; + if (headerExplicitlyOmitted(options.headers, "Authorization")) return; + if (headers.has("X-API-Key")) return; + if (headerExplicitlyOmitted(options.headers, "X-API-Key")) return; + if (new URL(url).searchParams.has("api_key")) return; + if (cookieHeaderHas(headers.get("Cookie"), "api_key")) return; + throw new Errors.AuthenticationError(401, {}, "Could not resolve authentication method. Expected Authorization or X-API-Key or query api_key or cookie api_key to be set.", headers); + } + + authHeadersSync(): Record { + const headers: Record = {}; + const bearerAuth = this.resolveAuthOptionSync("bearerAuth", this.bearerAuth); + if (bearerAuth) headers['Authorization'] = `Bearer ${bearerAuth}`; + const apiKeyHeader = this.resolveAuthOptionSync("apiKeyHeader", this.apiKeyHeader); + if (apiKeyHeader) headers["X-API-Key"] = apiKeyHeader; + const oAuth2 = this.resolveAuthOptionSync("oAuth2", this.oAuth2); + if (oAuth2) headers['Authorization'] = `Bearer ${oAuth2}`; + const openIDConnect = this.resolveAuthOptionSync("openIDConnect", this.openIDConnect); + if (openIDConnect) headers['Authorization'] = `Bearer ${openIDConnect}`; + return headers; + } + + webSocketAuthHeaders(): Record { + const bearerAuth = this.resolveAuthOptionSync("bearerAuth", this.bearerAuth); + if (bearerAuth) return { Authorization: `Bearer ${bearerAuth}` }; + const apiKeyHeader = this.resolveAuthOptionSync("apiKeyHeader", this.apiKeyHeader); + if (apiKeyHeader) return { "X-API-Key": apiKeyHeader }; + return {}; + } + + protected async authHeaders(options: FinalRequestOptions): Promise { + return buildHeaders([await this.authHeadersAsync()]); + } + + private async authQueryAsync(): Promise> { + const query: Record = {}; + const apiKeyQuery = await this.resolveAuthOption("apiKeyQuery", this.apiKeyQuery); + if (apiKeyQuery) query["api_key"] = apiKeyQuery; + return query; + } + + private async authCookiesAsync(): Promise> { + const cookies: Record = {}; + const apiKeyCookie = await this.resolveAuthOption("apiKeyCookie", this.apiKeyCookie); + if (apiKeyCookie) cookies["api_key"] = apiKeyCookie; + return cookies; + } + + private async authHeadersAsync(): Promise> { + const headers: Record = {}; + const bearerAuth = await this.resolveAuthOption("bearerAuth", this.bearerAuth); + if (bearerAuth) headers['Authorization'] = `Bearer ${bearerAuth}`; + const apiKeyHeader = await this.resolveAuthOption("apiKeyHeader", this.apiKeyHeader); + if (apiKeyHeader) headers["X-API-Key"] = apiKeyHeader; + const oAuth2 = await this.resolveAuthOption("oAuth2", this.oAuth2); + if (oAuth2) headers['Authorization'] = `Bearer ${oAuth2}`; + const openIDConnect = await this.resolveAuthOption("openIDConnect", this.openIDConnect); + if (openIDConnect) headers['Authorization'] = `Bearer ${openIDConnect}`; + return headers; + } + + private async resolveAuthOption(optionName: string, value: string | AuthTokenProvider | null | undefined): Promise { + if (value == null) return undefined; + const token = typeof value === "function" ? await value() : value; + if (!token) throw new Errors.ScalarGalaxyError(`Expected '${optionName}' to resolve to a non-empty string.`); + return token; + } + + private resolveAuthOptionSync(optionName: string, value: string | AuthTokenProvider | null | undefined): string | undefined { + if (value == null) return undefined; + const token = typeof value === "function" ? value() : value; + if (typeof token !== "string" || !token) throw new Errors.ScalarGalaxyError(`Expected '${optionName}' to resolve to a non-empty string.`); + return token; + } + + static ScalarGalaxy = this; + static DEFAULT_TIMEOUT = 60000; // 1 minute + + static ScalarGalaxyError = Errors.ScalarGalaxyError; + static APIError = Errors.APIError; + static APIConnectionError = Errors.APIConnectionError; + static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError; + static APIUserAbortError = Errors.APIUserAbortError; + static NotFoundError = Errors.NotFoundError; + static ConflictError = Errors.ConflictError; + static RateLimitError = Errors.RateLimitError; + static BadRequestError = Errors.BadRequestError; + static AuthenticationError = Errors.AuthenticationError; + static InternalServerError = Errors.InternalServerError; + static PermissionDeniedError = Errors.PermissionDeniedError; + static UnprocessableEntityError = Errors.UnprocessableEntityError; + + static toFile = toFile; + + planets: Planets = new Planets(this); + celestialBodies: CelestialBodies = new CelestialBodies(this); + authentication: Authentication = new Authentication(this); +} + +ScalarGalaxy.Planets = Planets; +ScalarGalaxy.CelestialBodies = CelestialBodies; +ScalarGalaxy.Authentication = Authentication; + +export declare namespace ScalarGalaxy { + export type RequestOptions = Opts.RequestOptions; + export { + Planets as Planets, + type Planet as Planet, + type PlanetListAllDataResponse as PlanetListAllDataResponse, + type PlanetUploadImageResponse as PlanetUploadImageResponse, + type PlanetListAllDataParams as PlanetListAllDataParams, + type PlanetCreateParams as PlanetCreateParams, + type PlanetUpdateParams as PlanetUpdateParams, + type PlanetUploadImageParams as PlanetUploadImageParams, + }; + + export { + CelestialBodies as CelestialBodies, + type CelestialBody as CelestialBody, + type CelestialBodyCreateParams as CelestialBodyCreateParams, + }; + + export { + Authentication as Authentication, + type User as User, + type Credentials as Credentials, + type AuthenticationCreateTokenResponse as AuthenticationCreateTokenResponse, + type AuthenticationCreateUserParams as AuthenticationCreateUserParams, + type AuthenticationCreateTokenParams as AuthenticationCreateTokenParams, + }; +} + + +const headerExplicitlyOmitted = (source: HeadersLike | undefined, name: string): boolean => { + if (!source || Array.isArray(source) || source instanceof Headers) return false; + const target = name.toLowerCase(); + return Object.entries(source).some(([key, value]) => key.toLowerCase() === target && value === null); +}; + +const appendAuthCookies = (headers: Headers, cookies: Record): void => { + for (const [name, value] of Object.entries(cookies)) { + if (cookieHeaderHas(headers.get("Cookie"), name)) continue; + const cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value); + const existing = headers.get("Cookie"); + headers.set("Cookie", existing ? existing + "; " + cookie : cookie); + } +}; + +const cookieHeaderHas = (value: string | null, name: string): boolean => { + if (!value) return false; + const target = encodeURIComponent(name) + "="; + return value.split(";").some((cookie) => cookie.trim().startsWith(target)); +}; + diff --git a/src/core/error.ts b/src/core/error.ts new file mode 100644 index 0000000..ae92e15 --- /dev/null +++ b/src/core/error.ts @@ -0,0 +1,130 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { castToError } from '../internal/errors'; + +export class ScalarGalaxyError extends Error {} + +export class APIError< + TStatus extends number | undefined = number | undefined, + THeaders extends Headers | undefined = Headers | undefined, + TError extends Object | undefined = Object | undefined, +> extends ScalarGalaxyError { + /** 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; + + 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 { + constructor({ message }: { message?: string } = {}) { + super(undefined, undefined, message || 'Request was aborted.', undefined); + } +} + +export class APIConnectionError extends APIError { + 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 {} diff --git a/src/core/uploads.ts b/src/core/uploads.ts new file mode 100644 index 0000000..536cfe6 --- /dev/null +++ b/src/core/uploads.ts @@ -0,0 +1,4 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export { type Uploadable } from '../internal/uploads'; +export { toFile, type ToFileInput } from '../internal/to-file'; diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..834f539 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,4 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +/** @deprecated Import from ./core/error instead */ +export * from './core/error'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..724cd2a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,22 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export { ScalarGalaxy as default } from './client.js'; + +export { type Uploadable, toFile } from './core/uploads'; +export { APIPromise } from './api-promise'; +export { ScalarGalaxy, type ClientOptions, type ScalarGalaxyOptions, type Logger, type LogLevel } from './client.js'; +export { + ScalarGalaxyError, + APIError, + APIConnectionError, + APIConnectionTimeoutError, + APIUserAbortError, + NotFoundError, + ConflictError, + RateLimitError, + BadRequestError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, +} from './error'; diff --git a/src/internal/README.md b/src/internal/README.md new file mode 100644 index 0000000..3ef5a25 --- /dev/null +++ b/src/internal/README.md @@ -0,0 +1,3 @@ +# `internal` + +The modules in this directory are not importable outside this package and will change between releases. diff --git a/src/internal/builtin-types.ts b/src/internal/builtin-types.ts new file mode 100644 index 0000000..40d03c4 --- /dev/null +++ b/src/internal/builtin-types.ts @@ -0,0 +1,93 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise; + +/** + * An alias to the builtin `RequestInit` type so we can + * easily alias it in import statements if there are name clashes. + * + * https://developer.mozilla.org/docs/Web/API/RequestInit + */ +type _RequestInit = RequestInit; + +/** + * An alias to the builtin `Response` type so we can + * easily alias it in import statements if there are name clashes. + * + * https://developer.mozilla.org/docs/Web/API/Response + */ +type _Response = Response; + +/** + * The type for the first argument to `fetch`. + * + * https://developer.mozilla.org/docs/Web/API/Window/fetch#resource + */ +type _RequestInfo = Request | URL | string; + +/** + * The type for constructing `RequestInit` Headers. + * + * https://developer.mozilla.org/docs/Web/API/RequestInit#setting_headers + */ +type _HeadersInit = RequestInit['headers']; + +/** + * The type for constructing `RequestInit` body. + * + * https://developer.mozilla.org/docs/Web/API/RequestInit#body + */ +type _BodyInit = RequestInit['body']; + +/** + * An alias to the builtin `Array` type so we can + * easily alias it in import statements if there are name clashes. + */ +type _Array = Array; + +/** + * An alias to the builtin `Record` type so we can + * easily alias it in import statements if there are name clashes. + */ +type _Record = Record; + +export type { + _Array as Array, + _BodyInit as BodyInit, + _HeadersInit as HeadersInit, + _Record as Record, + _RequestInfo as RequestInfo, + _RequestInit as RequestInit, + _Response as Response, +}; + +/** + * A copy of the builtin `EndingType` type as it isn't fully supported in certain + * environments and attempting to reference the global version will error. + * + * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L27941 + */ +type EndingType = 'native' | 'transparent'; + +/** + * A copy of the builtin `BlobPropertyBag` type as it isn't fully supported in certain + * environments and attempting to reference the global version will error. + * + * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L154 + * https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#options + */ +export interface BlobPropertyBag { + endings?: EndingType; + type?: string; +} + +/** + * A copy of the builtin `FilePropertyBag` type as it isn't fully supported in certain + * environments and attempting to reference the global version will error. + * + * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L503 + * https://developer.mozilla.org/en-US/docs/Web/API/File/File#options + */ +export interface FilePropertyBag extends BlobPropertyBag { + lastModified?: number; +} diff --git a/src/internal/detect-platform.ts b/src/internal/detect-platform.ts new file mode 100644 index 0000000..7fc9267 --- /dev/null +++ b/src/internal/detect-platform.ts @@ -0,0 +1,196 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { VERSION } from '../version'; + +export const isRunningInBrowser = () => { + return ( + // @ts-ignore + typeof window !== 'undefined' && + // @ts-ignore + typeof window.document !== 'undefined' && + // @ts-ignore + typeof navigator !== 'undefined' + ); +}; + +type DetectedPlatform = 'deno' | 'node' | 'edge' | 'unknown'; + +/** + * Note this does not detect 'browser'; for that, use getBrowserInfo(). + */ +function getDetectedPlatform(): DetectedPlatform { + if (typeof Deno !== 'undefined' && Deno.build != null) { + return 'deno'; + } + if (typeof EdgeRuntime !== 'undefined') { + return 'edge'; + } + if ( + Object.prototype.toString.call( + typeof (globalThis as any).process !== 'undefined' ? (globalThis as any).process : 0, + ) === '[object process]' + ) { + return 'node'; + } + return 'unknown'; +} + +declare const Deno: any; +declare const EdgeRuntime: any; +type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | `other:${string}` | 'unknown'; +type PlatformName = + | 'MacOS' + | 'Linux' + | 'Windows' + | 'FreeBSD' + | 'OpenBSD' + | 'iOS' + | 'Android' + | `Other:${string}` + | 'Unknown'; +type Browser = 'ie' | 'edge' | 'chrome' | 'firefox' | 'safari'; +type PlatformProperties = { + 'X-Scalar-Lang': 'js'; + 'X-Scalar-Package-Version': string; + 'X-Scalar-OS': PlatformName; + 'X-Scalar-Arch': Arch; + 'X-Scalar-Runtime': 'node' | 'deno' | 'edge' | `browser:${Browser}` | 'unknown'; + 'X-Scalar-Runtime-Version': string; +}; +const getPlatformProperties = (): PlatformProperties => { + const detectedPlatform = getDetectedPlatform(); + if (detectedPlatform === 'deno') { + return { + 'X-Scalar-Lang': 'js', + 'X-Scalar-Package-Version': VERSION, + 'X-Scalar-OS': normalizePlatform(Deno.build.os), + 'X-Scalar-Arch': normalizeArch(Deno.build.arch), + 'X-Scalar-Runtime': 'deno', + 'X-Scalar-Runtime-Version': + typeof Deno.version === 'string' ? Deno.version : Deno.version?.deno ?? 'unknown', + }; + } + if (typeof EdgeRuntime !== 'undefined') { + return { + 'X-Scalar-Lang': 'js', + 'X-Scalar-Package-Version': VERSION, + 'X-Scalar-OS': 'Unknown', + 'X-Scalar-Arch': `other:${EdgeRuntime}`, + 'X-Scalar-Runtime': 'edge', + 'X-Scalar-Runtime-Version': (globalThis as any).process.version, + }; + } + // Check if Node.js + if (detectedPlatform === 'node') { + return { + 'X-Scalar-Lang': 'js', + 'X-Scalar-Package-Version': VERSION, + 'X-Scalar-OS': normalizePlatform((globalThis as any).process.platform ?? 'unknown'), + 'X-Scalar-Arch': normalizeArch((globalThis as any).process.arch ?? 'unknown'), + 'X-Scalar-Runtime': 'node', + 'X-Scalar-Runtime-Version': (globalThis as any).process.version ?? 'unknown', + }; + } + + const browserInfo = getBrowserInfo(); + if (browserInfo) { + return { + 'X-Scalar-Lang': 'js', + 'X-Scalar-Package-Version': VERSION, + 'X-Scalar-OS': 'Unknown', + 'X-Scalar-Arch': 'unknown', + 'X-Scalar-Runtime': `browser:${browserInfo.browser}`, + 'X-Scalar-Runtime-Version': browserInfo.version, + }; + } + + // TODO add support for Cloudflare workers, etc. + return { + 'X-Scalar-Lang': 'js', + 'X-Scalar-Package-Version': VERSION, + 'X-Scalar-OS': 'Unknown', + 'X-Scalar-Arch': 'unknown', + 'X-Scalar-Runtime': 'unknown', + 'X-Scalar-Runtime-Version': 'unknown', + }; +}; + +type BrowserInfo = { + browser: Browser; + version: string; +}; + +declare const navigator: { userAgent: string } | undefined; + +// Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts +function getBrowserInfo(): BrowserInfo | null { + if (typeof navigator === 'undefined' || !navigator) { + return null; + } + + // NOTE: The order matters here! + const browserPatterns = [ + { key: 'edge' as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ }, + { key: 'ie' as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ }, + { key: 'ie' as const, pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/ }, + { key: 'chrome' as const, pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ }, + { key: 'firefox' as const, pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ }, + { key: 'safari' as const, pattern: /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/ }, + ]; + + // Find the FIRST matching browser + for (const { key, pattern } of browserPatterns) { + const match = pattern.exec(navigator.userAgent); + if (match) { + const major = match[1] || 0; + const minor = match[2] || 0; + const patch = match[3] || 0; + + return { browser: key, version: `${major}.${minor}.${patch}` }; + } + } + + return null; +} + +const normalizeArch = (arch: string): Arch => { + // Node docs: + // - https://nodejs.org/api/process.html#processarch + // Deno docs: + // - https://doc.deno.land/deno/stable/~/Deno.build + if (arch === 'x32') return 'x32'; + if (arch === 'x86_64' || arch === 'x64') return 'x64'; + if (arch === 'arm') return 'arm'; + if (arch === 'aarch64' || arch === 'arm64') return 'arm64'; + if (arch) return `other:${arch}`; + return 'unknown'; +}; + +const normalizePlatform = (platform: string): PlatformName => { + // Node platforms: + // - https://nodejs.org/api/process.html#processplatform + // Deno platforms: + // - https://doc.deno.land/deno/stable/~/Deno.build + // - https://github.com/denoland/deno/issues/14799 + + platform = platform.toLowerCase(); + + // NOTE: this iOS check is untested and may not work + // Node does not work natively on IOS, there is a fork at + // https://github.com/nodejs-mobile/nodejs-mobile + // however it is unknown at the time of writing how to detect if it is running + if (platform.includes('ios')) return 'iOS'; + if (platform === 'android') return 'Android'; + if (platform === 'darwin') return 'MacOS'; + if (platform === 'win32') return 'Windows'; + if (platform === 'freebsd') return 'FreeBSD'; + if (platform === 'openbsd') return 'OpenBSD'; + if (platform === 'linux') return 'Linux'; + if (platform) return `Other:${platform}`; + return 'Unknown'; +}; + +let _platformHeaders: PlatformProperties; +export const getPlatformHeaders = () => { + return (_platformHeaders ??= getPlatformProperties()); +}; diff --git a/src/internal/errors.ts b/src/internal/errors.ts new file mode 100644 index 0000000..fed0efd --- /dev/null +++ b/src/internal/errors.ts @@ -0,0 +1,33 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export function isAbortError(err: unknown) { + return ( + typeof err === 'object' && + err !== null && + // Spec-compliant fetch implementations + (('name' in err && (err as any).name === 'AbortError') || + // Expo fetch + ('message' in err && String((err as any).message).includes('FetchRequestCanceledException'))) + ); +} + +export const castToError = (err: any): Error => { + if (err instanceof Error) return err; + if (typeof err === 'object' && err !== null) { + try { + if (Object.prototype.toString.call(err) === '[object Error]') { + // @ts-ignore - not all envs have native support for cause yet + const error = new Error(err.message, err.cause ? { cause: err.cause } : {}); + if (err.stack) error.stack = err.stack; + // @ts-ignore - not all envs have native support for cause yet + if (err.cause && !error.cause) error.cause = err.cause; + if (err.name) error.name = err.name; + return error; + } + } catch {} + try { + return new Error(JSON.stringify(err)); + } catch {} + } + return new Error(err); +}; diff --git a/src/internal/headers.ts b/src/internal/headers.ts new file mode 100644 index 0000000..411434e --- /dev/null +++ b/src/internal/headers.ts @@ -0,0 +1,97 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { isReadonlyArray } from './utils/values'; + +type HeaderValue = string | undefined | null; +export type HeadersLike = + | Headers + | readonly HeaderValue[][] + | Record + | undefined + | null + | NullableHeaders; + +const brand_privateNullableHeaders = /* @__PURE__ */ Symbol('brand.privateNullableHeaders'); + +/** + * @internal + * Users can pass explicit nulls to unset default headers. When we parse them + * into a standard headers type we need to preserve that information. + */ +export type NullableHeaders = { + /** Brand check, prevent users from creating a NullableHeaders. */ + [brand_privateNullableHeaders]: true; + /** Parsed headers. */ + values: Headers; + /** Set of lowercase header names explicitly set to null. */ + nulls: Set; +}; + +function* iterateHeaders(headers: HeadersLike): IterableIterator { + if (!headers) return; + + if (brand_privateNullableHeaders in headers) { + const { values, nulls } = headers; + yield* values.entries(); + for (const name of nulls) { + yield [name, null]; + } + return; + } + + let shouldClear = false; + let iter: Iterable; + if (headers instanceof Headers) { + iter = headers.entries(); + } else if (isReadonlyArray(headers)) { + iter = headers; + } else { + shouldClear = true; + iter = Object.entries(headers ?? {}); + } + for (let row of iter) { + const name = row[0]; + if (typeof name !== 'string') throw new TypeError('expected header name to be a string'); + const values = isReadonlyArray(row[1]) ? row[1] : [row[1]]; + let didClear = false; + for (const value of values) { + if (value === undefined) continue; + + // Objects keys always overwrite older headers, they never append. + // Yield a null to clear the header before adding the new values. + if (shouldClear && !didClear) { + didClear = true; + yield [name, null]; + } + yield [name, value]; + } + } +} + +export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => { + const targetHeaders = new Headers(); + const nullHeaders = new Set(); + for (const headers of newHeaders) { + const seenHeaders = new Set(); + for (const [name, value] of iterateHeaders(headers)) { + const lowerName = name.toLowerCase(); + if (!seenHeaders.has(lowerName)) { + targetHeaders.delete(name); + seenHeaders.add(lowerName); + } + if (value === null) { + targetHeaders.delete(name); + nullHeaders.add(lowerName); + } else { + targetHeaders.append(name, value); + nullHeaders.delete(lowerName); + } + } + } + return { [brand_privateNullableHeaders]: true, values: targetHeaders, nulls: nullHeaders }; +}; + +export const isEmptyHeaders = (headers: HeadersLike) => { + for (const _ of iterateHeaders(headers)) return false; + return true; +}; diff --git a/src/internal/qs/LICENSE.md b/src/internal/qs/LICENSE.md new file mode 100644 index 0000000..3fda157 --- /dev/null +++ b/src/internal/qs/LICENSE.md @@ -0,0 +1,13 @@ +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/puruvj/neoqs/graphs/contributors) All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/internal/qs/README.md b/src/internal/qs/README.md new file mode 100644 index 0000000..67ae04e --- /dev/null +++ b/src/internal/qs/README.md @@ -0,0 +1,3 @@ +# qs + +This is a vendored version of [neoqs](https://github.com/PuruVJ/neoqs) which is a TypeScript rewrite of [qs](https://github.com/ljharb/qs), a query string library. diff --git a/src/internal/qs/formats.ts b/src/internal/qs/formats.ts new file mode 100644 index 0000000..e76a742 --- /dev/null +++ b/src/internal/qs/formats.ts @@ -0,0 +1,10 @@ +import type { Format } from './types'; + +export const default_format: Format = 'RFC3986'; +export const default_formatter = (v: PropertyKey) => String(v); +export const formatters: Record string> = { + RFC1738: (v: PropertyKey) => String(v).replace(/%20/g, '+'), + RFC3986: default_formatter, +}; +export const RFC1738 = 'RFC1738'; +export const RFC3986 = 'RFC3986'; diff --git a/src/internal/qs/index.ts b/src/internal/qs/index.ts new file mode 100644 index 0000000..c3a3620 --- /dev/null +++ b/src/internal/qs/index.ts @@ -0,0 +1,13 @@ +import { default_format, formatters, RFC1738, RFC3986 } from './formats'; + +const formats = { + formatters, + RFC1738, + RFC3986, + default: default_format, +}; + +export { stringify } from './stringify'; +export { formats }; + +export type { DefaultDecoder, DefaultEncoder, Format, ParseOptions, StringifyOptions } from './types'; diff --git a/src/internal/qs/stringify.ts b/src/internal/qs/stringify.ts new file mode 100644 index 0000000..7e71387 --- /dev/null +++ b/src/internal/qs/stringify.ts @@ -0,0 +1,385 @@ +import { encode, is_buffer, maybe_map, has } from './utils'; +import { default_format, default_formatter, formatters } from './formats'; +import type { NonNullableProperties, StringifyOptions } from './types'; +import { isArray } from '../utils/values'; + +const array_prefix_generators = { + brackets(prefix: PropertyKey) { + return String(prefix) + '[]'; + }, + comma: 'comma', + indices(prefix: PropertyKey, key: string) { + return String(prefix) + '[' + key + ']'; + }, + repeat(prefix: PropertyKey) { + return String(prefix); + }, +}; + +const push_to_array = function (arr: any[], value_or_array: any) { + Array.prototype.push.apply(arr, isArray(value_or_array) ? value_or_array : [value_or_array]); +}; + +let toISOString; + +const defaults = { + addQueryPrefix: false, + allowDots: false, + allowEmptyArrays: false, + arrayFormat: 'indices', + charset: 'utf-8', + charsetSentinel: false, + delimiter: '&', + encode: true, + encodeDotInKeys: false, + encoder: encode, + encodeValuesOnly: false, + format: default_format, + formatter: default_formatter, + /** @deprecated */ + indices: false, + serializeDate(date) { + return (toISOString ??= Function.prototype.call.bind(Date.prototype.toISOString))(date); + }, + skipNulls: false, + strictNullHandling: false, +} as NonNullableProperties; + +function is_non_nullish_primitive(v: unknown): v is string | number | boolean | symbol | bigint { + return ( + typeof v === 'string' || + typeof v === 'number' || + typeof v === 'boolean' || + typeof v === 'symbol' || + typeof v === 'bigint' + ); +} + +const sentinel = {}; + +function inner_stringify( + object: any, + prefix: PropertyKey, + generateArrayPrefix: StringifyOptions['arrayFormat'] | ((prefix: string, key: string) => string), + commaRoundTrip: boolean, + allowEmptyArrays: boolean, + strictNullHandling: boolean, + skipNulls: boolean, + encodeDotInKeys: boolean, + encoder: StringifyOptions['encoder'], + filter: StringifyOptions['filter'], + sort: StringifyOptions['sort'], + allowDots: StringifyOptions['allowDots'], + serializeDate: StringifyOptions['serializeDate'], + format: StringifyOptions['format'], + formatter: StringifyOptions['formatter'], + encodeValuesOnly: boolean, + charset: StringifyOptions['charset'], + sideChannel: WeakMap, +) { + let obj = object; + + let tmp_sc = sideChannel; + let step = 0; + let find_flag = false; + while ((tmp_sc = tmp_sc.get(sentinel)) !== void undefined && !find_flag) { + // Where object last appeared in the ref tree + const pos = tmp_sc.get(object); + step += 1; + if (typeof pos !== 'undefined') { + if (pos === step) { + throw new RangeError('Cyclic object value'); + } else { + find_flag = true; // Break while + } + } + if (typeof tmp_sc.get(sentinel) === 'undefined') { + step = 0; + } + } + + if (typeof filter === 'function') { + obj = filter(prefix, obj); + } else if (obj instanceof Date) { + obj = serializeDate?.(obj); + } else if (generateArrayPrefix === 'comma' && isArray(obj)) { + obj = maybe_map(obj, function (value) { + if (value instanceof Date) { + return serializeDate?.(value); + } + return value; + }); + } + + if (obj === null) { + if (strictNullHandling) { + return encoder && !encodeValuesOnly ? + // @ts-expect-error + encoder(prefix, defaults.encoder, charset, 'key', format) + : prefix; + } + + obj = ''; + } + + if (is_non_nullish_primitive(obj) || is_buffer(obj)) { + if (encoder) { + const key_value = + encodeValuesOnly ? prefix + // @ts-expect-error + : encoder(prefix, defaults.encoder, charset, 'key', format); + return [ + formatter?.(key_value) + + '=' + + // @ts-expect-error + formatter?.(encoder(obj, defaults.encoder, charset, 'value', format)), + ]; + } + return [formatter?.(prefix) + '=' + formatter?.(String(obj))]; + } + + const values: string[] = []; + + if (typeof obj === 'undefined') { + return values; + } + + let obj_keys; + if (generateArrayPrefix === 'comma' && isArray(obj)) { + // we need to join elements in + if (encodeValuesOnly && encoder) { + // @ts-expect-error values only + obj = maybe_map(obj, encoder); + } + obj_keys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }]; + } else if (isArray(filter)) { + obj_keys = filter; + } else { + const keys = Object.keys(obj); + obj_keys = sort ? keys.sort(sort) : keys; + } + + const encoded_prefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix); + + const adjusted_prefix = + commaRoundTrip && isArray(obj) && obj.length === 1 ? encoded_prefix + '[]' : encoded_prefix; + + if (allowEmptyArrays && isArray(obj) && obj.length === 0) { + return adjusted_prefix + '[]'; + } + + for (let j = 0; j < obj_keys.length; ++j) { + const key = obj_keys[j]; + const value = + // @ts-ignore + typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key as any]; + + if (skipNulls && value === null) { + continue; + } + + // @ts-ignore + const encoded_key = allowDots && encodeDotInKeys ? (key as any).replace(/\./g, '%2E') : key; + const key_prefix = + isArray(obj) ? + typeof generateArrayPrefix === 'function' ? + generateArrayPrefix(adjusted_prefix, encoded_key) + : adjusted_prefix + : adjusted_prefix + (allowDots ? '.' + encoded_key : '[' + encoded_key + ']'); + + sideChannel.set(object, step); + const valueSideChannel = new WeakMap(); + valueSideChannel.set(sentinel, sideChannel); + push_to_array( + values, + inner_stringify( + value, + key_prefix, + generateArrayPrefix, + commaRoundTrip, + allowEmptyArrays, + strictNullHandling, + skipNulls, + encodeDotInKeys, + // @ts-ignore + generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder, + filter, + sort, + allowDots, + serializeDate, + format, + formatter, + encodeValuesOnly, + charset, + valueSideChannel, + ), + ); + } + + return values; +} + +function normalize_stringify_options( + opts: StringifyOptions = defaults, +): NonNullableProperties> & { indices?: boolean } { + if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') { + throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided'); + } + + if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') { + throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided'); + } + + if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') { + throw new TypeError('Encoder has to be a function.'); + } + + const charset = opts.charset || defaults.charset; + if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { + throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'); + } + + let format = default_format; + if (typeof opts.format !== 'undefined') { + if (!has(formatters, opts.format)) { + throw new TypeError('Unknown format option provided.'); + } + format = opts.format; + } + const formatter = formatters[format]; + + let filter = defaults.filter; + if (typeof opts.filter === 'function' || isArray(opts.filter)) { + filter = opts.filter; + } + + let arrayFormat: StringifyOptions['arrayFormat']; + if (opts.arrayFormat && opts.arrayFormat in array_prefix_generators) { + arrayFormat = opts.arrayFormat; + } else if ('indices' in opts) { + arrayFormat = opts.indices ? 'indices' : 'repeat'; + } else { + arrayFormat = defaults.arrayFormat; + } + + if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') { + throw new TypeError('`commaRoundTrip` must be a boolean, or absent'); + } + + const allowDots = + typeof opts.allowDots === 'undefined' ? + !!opts.encodeDotInKeys === true ? + true + : defaults.allowDots + : !!opts.allowDots; + + return { + addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix, + // @ts-ignore + allowDots: allowDots, + allowEmptyArrays: + typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays, + arrayFormat: arrayFormat, + charset: charset, + charsetSentinel: + typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, + commaRoundTrip: !!opts.commaRoundTrip, + delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter, + encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode, + encodeDotInKeys: + typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys, + encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder, + encodeValuesOnly: + typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly, + filter: filter, + format: format, + formatter: formatter, + serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate, + skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls, + // @ts-ignore + sort: typeof opts.sort === 'function' ? opts.sort : null, + strictNullHandling: + typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling, + }; +} + +export function stringify(object: any, opts: StringifyOptions = {}) { + let obj = object; + const options = normalize_stringify_options(opts); + + let obj_keys: PropertyKey[] | undefined; + let filter; + + if (typeof options.filter === 'function') { + filter = options.filter; + obj = filter('', obj); + } else if (isArray(options.filter)) { + filter = options.filter; + obj_keys = filter; + } + + const keys: string[] = []; + + if (typeof obj !== 'object' || obj === null) { + return ''; + } + + const generateArrayPrefix = array_prefix_generators[options.arrayFormat]; + const commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip; + + if (!obj_keys) { + obj_keys = Object.keys(obj); + } + + if (options.sort) { + obj_keys.sort(options.sort); + } + + const sideChannel = new WeakMap(); + for (let i = 0; i < obj_keys.length; ++i) { + const key = obj_keys[i]!; + + if (options.skipNulls && obj[key] === null) { + continue; + } + push_to_array( + keys, + inner_stringify( + obj[key], + key, + // @ts-expect-error + generateArrayPrefix, + commaRoundTrip, + options.allowEmptyArrays, + options.strictNullHandling, + options.skipNulls, + options.encodeDotInKeys, + options.encode ? options.encoder : null, + options.filter, + options.sort, + options.allowDots, + options.serializeDate, + options.format, + options.formatter, + options.encodeValuesOnly, + options.charset, + sideChannel, + ), + ); + } + + const joined = keys.join(options.delimiter); + let prefix = options.addQueryPrefix === true ? '?' : ''; + + if (options.charsetSentinel) { + if (options.charset === 'iso-8859-1') { + // encodeURIComponent('✓'), the "numeric entity" representation of a checkmark + prefix += 'utf8=%26%2310003%3B&'; + } else { + // encodeURIComponent('✓') + prefix += 'utf8=%E2%9C%93&'; + } + } + + return joined.length > 0 ? prefix + joined : ''; +} diff --git a/src/internal/qs/types.ts b/src/internal/qs/types.ts new file mode 100644 index 0000000..7c28dbb --- /dev/null +++ b/src/internal/qs/types.ts @@ -0,0 +1,71 @@ +export type Format = 'RFC1738' | 'RFC3986'; + +export type DefaultEncoder = (str: any, defaultEncoder?: any, charset?: string) => string; +export type DefaultDecoder = (str: string, decoder?: any, charset?: string) => string; + +export type BooleanOptional = boolean | undefined; + +export type StringifyBaseOptions = { + delimiter?: string; + allowDots?: boolean; + encodeDotInKeys?: boolean; + strictNullHandling?: boolean; + skipNulls?: boolean; + encode?: boolean; + encoder?: ( + str: any, + defaultEncoder: DefaultEncoder, + charset: string, + type: 'key' | 'value', + format?: Format, + ) => string; + filter?: Array | ((prefix: PropertyKey, value: any) => any); + arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma'; + indices?: boolean; + sort?: ((a: PropertyKey, b: PropertyKey) => number) | null; + serializeDate?: (d: Date) => string; + format?: 'RFC1738' | 'RFC3986'; + formatter?: (str: PropertyKey) => string; + encodeValuesOnly?: boolean; + addQueryPrefix?: boolean; + charset?: 'utf-8' | 'iso-8859-1'; + charsetSentinel?: boolean; + allowEmptyArrays?: boolean; + commaRoundTrip?: boolean; +}; + +export type StringifyOptions = StringifyBaseOptions; + +export type ParseBaseOptions = { + comma?: boolean; + delimiter?: string | RegExp; + depth?: number | false; + decoder?: (str: string, defaultDecoder: DefaultDecoder, charset: string, type: 'key' | 'value') => any; + arrayLimit?: number; + parseArrays?: boolean; + plainObjects?: boolean; + allowPrototypes?: boolean; + allowSparse?: boolean; + parameterLimit?: number; + strictDepth?: boolean; + strictNullHandling?: boolean; + ignoreQueryPrefix?: boolean; + charset?: 'utf-8' | 'iso-8859-1'; + charsetSentinel?: boolean; + interpretNumericEntities?: boolean; + allowEmptyArrays?: boolean; + duplicates?: 'combine' | 'first' | 'last'; + allowDots?: boolean; + decodeDotInKeys?: boolean; +}; + +export type ParseOptions = ParseBaseOptions; + +export type ParsedQs = { + [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; +}; + +// Type to remove null or undefined union from each property +export type NonNullableProperties = { + [K in keyof T]-?: Exclude; +}; diff --git a/src/internal/qs/utils.ts b/src/internal/qs/utils.ts new file mode 100644 index 0000000..4cd5657 --- /dev/null +++ b/src/internal/qs/utils.ts @@ -0,0 +1,265 @@ +import { RFC1738 } from './formats'; +import type { DefaultEncoder, Format } from './types'; +import { isArray } from '../utils/values'; + +export let has = (obj: object, key: PropertyKey): boolean => ( + (has = (Object as any).hasOwn ?? Function.prototype.call.bind(Object.prototype.hasOwnProperty)), + has(obj, key) +); + +const hex_table = /* @__PURE__ */ (() => { + const array = []; + for (let i = 0; i < 256; ++i) { + array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase()); + } + + return array; +})(); + +function compact_queue>(queue: Array<{ obj: T; prop: string }>) { + while (queue.length > 1) { + const item = queue.pop(); + if (!item) continue; + + const obj = item.obj[item.prop]; + + if (isArray(obj)) { + const compacted: unknown[] = []; + + for (let j = 0; j < obj.length; ++j) { + if (typeof obj[j] !== 'undefined') { + compacted.push(obj[j]); + } + } + + // @ts-ignore + item.obj[item.prop] = compacted; + } + } +} + +function array_to_object(source: any[], options: { plainObjects: boolean }) { + const obj = options && options.plainObjects ? Object.create(null) : {}; + for (let i = 0; i < source.length; ++i) { + if (typeof source[i] !== 'undefined') { + obj[i] = source[i]; + } + } + + return obj; +} + +export function merge( + target: any, + source: any, + options: { plainObjects?: boolean; allowPrototypes?: boolean } = {}, +) { + if (!source) { + return target; + } + + if (typeof source !== 'object') { + if (isArray(target)) { + target.push(source); + } else if (target && typeof target === 'object') { + if ((options && (options.plainObjects || options.allowPrototypes)) || !has(Object.prototype, source)) { + target[source] = true; + } + } else { + return [target, source]; + } + + return target; + } + + if (!target || typeof target !== 'object') { + return [target].concat(source); + } + + let mergeTarget = target; + if (isArray(target) && !isArray(source)) { + // @ts-ignore + mergeTarget = array_to_object(target, options); + } + + if (isArray(target) && isArray(source)) { + source.forEach(function (item, i) { + if (has(target, i)) { + const targetItem = target[i]; + if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { + target[i] = merge(targetItem, item, options); + } else { + target.push(item); + } + } else { + target[i] = item; + } + }); + return target; + } + + return Object.keys(source).reduce(function (acc, key) { + const value = source[key]; + + if (has(acc, key)) { + acc[key] = merge(acc[key], value, options); + } else { + acc[key] = value; + } + return acc; + }, mergeTarget); +} + +export function assign_single_source(target: any, source: any) { + return Object.keys(source).reduce(function (acc, key) { + acc[key] = source[key]; + return acc; + }, target); +} + +export function decode(str: string, _: any, charset: string) { + const strWithoutPlus = str.replace(/\+/g, ' '); + if (charset === 'iso-8859-1') { + // unescape never throws, no try...catch needed: + return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape); + } + // utf-8 + try { + return decodeURIComponent(strWithoutPlus); + } catch (e) { + return strWithoutPlus; + } +} + +const limit = 1024; + +export const encode: ( + str: any, + defaultEncoder: DefaultEncoder, + charset: string, + type: 'key' | 'value', + format: Format, +) => string = (str, _defaultEncoder, charset, _kind, format: Format) => { + // This code was originally written by Brian White for the io.js core querystring library. + // It has been adapted here for stricter adherence to RFC 3986 + if (str.length === 0) { + return str; + } + + let string = str; + if (typeof str === 'symbol') { + string = Symbol.prototype.toString.call(str); + } else if (typeof str !== 'string') { + string = String(str); + } + + if (charset === 'iso-8859-1') { + return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { + return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; + }); + } + + let out = ''; + for (let j = 0; j < string.length; j += limit) { + const segment = string.length >= limit ? string.slice(j, j + limit) : string; + const arr = []; + + for (let i = 0; i < segment.length; ++i) { + let c = segment.charCodeAt(i); + if ( + c === 0x2d || // - + c === 0x2e || // . + c === 0x5f || // _ + c === 0x7e || // ~ + (c >= 0x30 && c <= 0x39) || // 0-9 + (c >= 0x41 && c <= 0x5a) || // a-z + (c >= 0x61 && c <= 0x7a) || // A-Z + (format === RFC1738 && (c === 0x28 || c === 0x29)) // ( ) + ) { + arr[arr.length] = segment.charAt(i); + continue; + } + + if (c < 0x80) { + arr[arr.length] = hex_table[c]; + continue; + } + + if (c < 0x800) { + arr[arr.length] = hex_table[0xc0 | (c >> 6)]! + hex_table[0x80 | (c & 0x3f)]; + continue; + } + + if (c < 0xd800 || c >= 0xe000) { + arr[arr.length] = + hex_table[0xe0 | (c >> 12)]! + hex_table[0x80 | ((c >> 6) & 0x3f)] + hex_table[0x80 | (c & 0x3f)]; + continue; + } + + i += 1; + c = 0x10000 + (((c & 0x3ff) << 10) | (segment.charCodeAt(i) & 0x3ff)); + + arr[arr.length] = + hex_table[0xf0 | (c >> 18)]! + + hex_table[0x80 | ((c >> 12) & 0x3f)] + + hex_table[0x80 | ((c >> 6) & 0x3f)] + + hex_table[0x80 | (c & 0x3f)]; + } + + out += arr.join(''); + } + + return out; +}; + +export function compact(value: any) { + const queue = [{ obj: { o: value }, prop: 'o' }]; + const refs = []; + + for (let i = 0; i < queue.length; ++i) { + const item = queue[i]; + // @ts-ignore + const obj = item.obj[item.prop]; + + const keys = Object.keys(obj); + for (let j = 0; j < keys.length; ++j) { + const key = keys[j]!; + const val = obj[key]; + if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { + queue.push({ obj: obj, prop: key }); + refs.push(val); + } + } + } + + compact_queue(queue); + + return value; +} + +export function is_regexp(obj: any) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +} + +export function is_buffer(obj: any) { + if (!obj || typeof obj !== 'object') { + return false; + } + + return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); +} + +export function combine(a: any, b: any) { + return [].concat(a, b); +} + +export function maybe_map(val: T[], fn: (v: T) => T) { + if (isArray(val)) { + const mapped = []; + for (let i = 0; i < val.length; i += 1) { + mapped.push(fn(val[i]!)); + } + return mapped; + } + return fn(val); +} diff --git a/src/internal/request-options.ts b/src/internal/request-options.ts new file mode 100644 index 0000000..4dc4258 --- /dev/null +++ b/src/internal/request-options.ts @@ -0,0 +1,39 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import type { BodyInit } from './builtin-types'; +import type { HTTPMethod, MergedRequestInit } from './types'; +import type { HeadersLike, NullableHeaders } from './headers'; + +export type RequestOptions = { + method?: HTTPMethod | undefined; + path?: string | undefined; + headers?: HeadersLike | undefined; + query?: object | undefined | null; + body?: unknown; + timeout?: number | undefined; + maxRetries?: number | undefined; + stream?: boolean | undefined; + signal?: AbortSignal | undefined | null; + fetchOptions?: MergedRequestInit | undefined; + idempotencyKey?: string | undefined; + defaultBaseURL?: string | undefined; + __binaryResponse?: boolean | undefined; +}; + +export type FinalRequestOptions = RequestOptions & { + method: HTTPMethod; + path: string; +}; + +export type EncodedContent = { bodyHeaders: HeadersLike; body: BodyInit }; +export type RequestEncoder = (request: { headers: NullableHeaders; body: unknown }) => EncodedContent; + +/** Fallback JSON encoder used when a request body is not already a fetch body type. */ +export const FallbackEncoder: RequestEncoder = ({ body }) => { + return { + bodyHeaders: { + 'content-type': 'application/json', + }, + body: JSON.stringify(body), + }; +}; diff --git a/src/internal/shim-types.ts b/src/internal/shim-types.ts new file mode 100644 index 0000000..6d992a5 --- /dev/null +++ b/src/internal/shim-types.ts @@ -0,0 +1,26 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +/** + * Shims for types that we can't always rely on being available globally. + * + * Note: these only exist at the type-level, there is no corresponding runtime + * version for any of these symbols. + */ + +type NeverToAny = T extends never ? any : T; + +/** @ts-ignore */ +type _DOMReadableStream = globalThis.ReadableStream; + +/** @ts-ignore */ +type _NodeReadableStream = import('stream/web').ReadableStream; + +type _ConditionalNodeReadableStream = + typeof globalThis extends { ReadableStream: any } ? never : _NodeReadableStream; + +type _ReadableStream = NeverToAny< + | ([0] extends [1 & _DOMReadableStream] ? never : _DOMReadableStream) + | ([0] extends [1 & _ConditionalNodeReadableStream] ? never : _ConditionalNodeReadableStream) +>; + +export type { _ReadableStream as ReadableStream }; diff --git a/src/internal/shims.ts b/src/internal/shims.ts new file mode 100644 index 0000000..c278363 --- /dev/null +++ b/src/internal/shims.ts @@ -0,0 +1,107 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +/** + * This module provides internal shims and utility functions for environments where certain Node.js or global types may not be available. + * + * These are used to ensure we can provide a consistent behaviour between different JavaScript environments and good error + * messages in cases where an environment isn't fully supported. + */ + +import type { Fetch } from './builtin-types'; +import type { ReadableStream } from './shim-types'; + +export function getDefaultFetch(): Fetch { + if (typeof fetch !== 'undefined') { + return fetch as any; + } + + throw new Error( + '`fetch` is not defined as a global; Either pass `fetch` to the client, `new ScalarGalaxy({ fetch })` or polyfill the global, `globalThis.fetch = fetch`', + ); +} + +type ReadableStreamArgs = ConstructorParameters; + +export function makeReadableStream(...args: ReadableStreamArgs): ReadableStream { + const ReadableStream = (globalThis as any).ReadableStream; + if (typeof ReadableStream === 'undefined') { + // Note: All of the platforms / runtimes we officially support already define + // `ReadableStream` as a global, so this should only ever be hit on unsupported runtimes. + throw new Error( + '`ReadableStream` is not defined as a global; You will need to polyfill it, `globalThis.ReadableStream = ReadableStream`', + ); + } + + return new ReadableStream(...args); +} + +export function ReadableStreamFrom(iterable: Iterable | AsyncIterable): ReadableStream { + let iter: AsyncIterator | Iterator = + Symbol.asyncIterator in iterable ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator](); + + return makeReadableStream({ + start() {}, + async pull(controller: any) { + const { done, value } = await iter.next(); + if (done) { + controller.close(); + } else { + controller.enqueue(value); + } + }, + async cancel() { + await iter.return?.(); + }, + }); +} + +/** + * Most browsers don't yet have async iterable support for ReadableStream, + * and Node has a very different way of reading bytes from its "ReadableStream". + * + * This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490 + */ +export function ReadableStreamToAsyncIterable(stream: any): AsyncIterableIterator { + if (stream[Symbol.asyncIterator]) return stream; + + const reader = stream.getReader(); + return { + async next() { + try { + const result = await reader.read(); + if (result?.done) reader.releaseLock(); // release lock when stream becomes closed + return result; + } catch (e) { + reader.releaseLock(); // release lock when stream becomes errored + throw e; + } + }, + async return() { + const cancelPromise = reader.cancel(); + reader.releaseLock(); + await cancelPromise; + return { done: true, value: undefined }; + }, + [Symbol.asyncIterator]() { + return this; + }, + }; +} + +/** + * Cancels a ReadableStream we don't need to consume. + * See https://undici.nodejs.org/#/?id=garbage-collection + */ +export async function CancelReadableStream(stream: any): Promise { + if (stream === null || typeof stream !== 'object') return; + + if (stream[Symbol.asyncIterator]) { + await stream[Symbol.asyncIterator]().return?.(); + return; + } + + const reader = stream.getReader(); + const cancelPromise = reader.cancel(); + reader.releaseLock(); + await cancelPromise; +} diff --git a/src/internal/to-file.ts b/src/internal/to-file.ts new file mode 100644 index 0000000..30eada3 --- /dev/null +++ b/src/internal/to-file.ts @@ -0,0 +1,154 @@ +import { BlobPart, getName, makeFile, isAsyncIterable } from './uploads'; +import type { FilePropertyBag } from './builtin-types'; +import { checkFileSupport } from './uploads'; + +type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | DataView; + +/** + * Intended to match DOM Blob, node-fetch Blob, node:buffer Blob, etc. + * Don't add arrayBuffer here, node-fetch doesn't have it + */ +interface BlobLike { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */ + readonly size: number; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */ + readonly type: string; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */ + text(): Promise; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */ + slice(start?: number, end?: number): BlobLike; +} + +/** + * This check adds the arrayBuffer() method type because it is available and used at runtime + */ +const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Promise } => + value != null && + typeof value === 'object' && + typeof value.size === 'number' && + typeof value.type === 'string' && + typeof value.text === 'function' && + typeof value.slice === 'function' && + typeof value.arrayBuffer === 'function'; + +/** + * Intended to match DOM File, node:buffer File, undici File, etc. + */ +interface FileLike extends BlobLike { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */ + readonly lastModified: number; + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */ + readonly name?: string | undefined; +} + +/** + * This check adds the arrayBuffer() method type because it is available and used at runtime + */ +const isFileLike = (value: any): value is FileLike & { arrayBuffer(): Promise } => + value != null && + typeof value === 'object' && + typeof value.name === 'string' && + typeof value.lastModified === 'number' && + isBlobLike(value); + +/** + * Intended to match DOM Response, node-fetch Response, undici Response, etc. + */ +export interface ResponseLike { + url: string; + blob(): Promise; +} + +const isResponseLike = (value: any): value is ResponseLike => + value != null && + typeof value === 'object' && + typeof value.url === 'string' && + typeof value.blob === 'function'; + +export type ToFileInput = + | FileLike + | ResponseLike + | Exclude + | AsyncIterable; + +/** + * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats + * @param value the raw content of the file. Can be an {@link Uploadable}, BlobLikePart, or AsyncIterable of BlobLikeParts + * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible + * @param {Object=} options additional properties + * @param {string=} options.type the MIME type of the content + * @param {number=} options.lastModified the last modified timestamp + * @returns a {@link File} with the given properties + */ +export async function toFile( + value: ToFileInput | PromiseLike, + name?: string | null | undefined, + options?: FilePropertyBag | undefined, +): Promise { + checkFileSupport(); + + // If it's a promise, resolve it. + value = await value; + + // If we've been given a `File` we don't need to do anything + if (isFileLike(value)) { + if (value instanceof File) { + return value; + } + return makeFile([await value.arrayBuffer()], value.name); + } + + if (isResponseLike(value)) { + const blob = await value.blob(); + name ||= new URL(value.url).pathname.split(/[\\/]/).pop(); + + return makeFile(await getBytes(blob), name, options); + } + + const parts = await getBytes(value); + + name ||= getName(value); + + if (!options?.type) { + const type = parts.find((part) => typeof part === 'object' && 'type' in part && part.type); + if (typeof type === 'string') { + options = { ...options, type }; + } + } + + return makeFile(parts, name, options); +} + +async function getBytes(value: BlobLikePart | AsyncIterable): Promise> { + let parts: Array = []; + if ( + typeof value === 'string' || + ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc. + value instanceof ArrayBuffer + ) { + parts.push(value); + } else if (isBlobLike(value)) { + parts.push(value instanceof Blob ? value : await value.arrayBuffer()); + } else if ( + isAsyncIterable(value) // includes Readable, ReadableStream, etc. + ) { + for await (const chunk of value) { + parts.push(...(await getBytes(chunk as BlobLikePart))); // TODO, consider validating? + } + } else { + const constructor = value?.constructor?.name; + throw new Error( + `Unexpected data type: ${typeof value}${ + constructor ? `; constructor: ${constructor}` : '' + }${propsForError(value)}`, + ); + } + + return parts; +} + +function propsForError(value: unknown): string { + if (typeof value !== 'object' || value === null) return ''; + const props = Object.getOwnPropertyNames(value); + return `; props: [${props.map((p) => `"${p}"`).join(', ')}]`; +} diff --git a/src/internal/types.ts b/src/internal/types.ts new file mode 100644 index 0000000..93a4f5a --- /dev/null +++ b/src/internal/types.ts @@ -0,0 +1,93 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export type PromiseOrValue = T | Promise; +export type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; + +export type KeysEnum = { [P in keyof Required]: true }; + +export type FinalizedRequestInit = RequestInit & { headers: Headers }; + +type NotAny = [0] extends [1 & T] ? never : T; + +/** + * Some environments overload the global fetch function, and Parameters only gets the last signature. + */ +type OverloadedParameters = + T extends ( + { + (...args: infer A): unknown; + (...args: infer B): unknown; + (...args: infer C): unknown; + (...args: infer D): unknown; + } + ) ? + A | B | C | D + : T extends ( + { + (...args: infer A): unknown; + (...args: infer B): unknown; + (...args: infer C): unknown; + } + ) ? + A | B | C + : T extends ( + { + (...args: infer A): unknown; + (...args: infer B): unknown; + } + ) ? + A | B + : T extends (...args: infer A) => unknown ? A + : never; + +/** + * These imports attempt to get types from a parent package's dependencies. + * Unresolved bare specifiers can trigger [automatic type acquisition][1] in some projects, which + * would cause typescript to show types not present at runtime. To avoid this, we import + * directly from parent node_modules folders. + * + * We need to check multiple levels because we don't know what directory structure we'll be in. + * For example, pnpm generates directories like this: + * ``` + * node_modules + * ├── .pnpm + * │ └── pkg@1.0.0 + * │ └── node_modules + * │ └── pkg + * │ └── internal + * │ └── types.d.ts + * ├── pkg -> .pnpm/pkg@1.0.0/node_modules/pkg + * └── undici + * ``` + * + * [1]: https://www.typescriptlang.org/tsconfig/#typeAcquisition + */ +/** @ts-ignore For users with \@types/node */ /* prettier-ignore */ +type UndiciTypesRequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; +/** @ts-ignore For users with undici */ /* prettier-ignore */ +type UndiciRequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; +/** @ts-ignore For users with \@types/bun */ /* prettier-ignore */ +type BunRequestInit = globalThis.FetchRequestInit; +/** @ts-ignore For users with node-fetch@2 */ /* prettier-ignore */ +type NodeFetch2RequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; +/** @ts-ignore For users with node-fetch@3, doesn't need file extension because types are at ./@types/index.d.ts */ /* prettier-ignore */ +type NodeFetch3RequestInit = NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny | NotAny; +/** @ts-ignore For users who use Deno */ /* prettier-ignore */ +type FetchRequestInit = NonNullable[1]>; + +type RequestInits = + | NotAny + | NotAny + | NotAny + | NotAny + | NotAny + | NotAny + | NotAny; + +/** + * This type contains `RequestInit` options that may be available on the current runtime, + * including per-platform extensions like `dispatcher`, `agent`, `client`, etc. + */ +export type MergedRequestInit = RequestInits & + /** We don't include these in the types as they'll be overridden for every request. */ + Partial>; diff --git a/src/internal/uploads.ts b/src/internal/uploads.ts new file mode 100644 index 0000000..bb10918 --- /dev/null +++ b/src/internal/uploads.ts @@ -0,0 +1,201 @@ +import { type RequestOptions } from './request-options'; +import type { FilePropertyBag, Fetch } from './builtin-types'; +import type { ScalarGalaxy } from '../client'; +import { ReadableStreamFrom } from './shims'; + +export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView; +type FsReadStream = AsyncIterable & { path: string | { toString(): string } }; + +// https://github.com/oven-sh/bun/issues/5980 +interface BunFile extends Blob { + readonly name?: string | undefined; +} + +export const checkFileSupport = () => { + if (typeof File === 'undefined') { + const { process } = globalThis as any; + const isOldNode = + typeof process?.versions?.node === 'string' && parseInt(process.versions.node.split('.')) < 20; + throw new Error( + '`File` is not defined as a global, which is required for file uploads.' + + (isOldNode ? + " Update to Node 20 LTS or newer, or set `globalThis.File` to `import('node:buffer').File`." + : ''), + ); + } +}; + +/** + * Typically, this is a native "File" class. + * + * We provide the {@link toFile} utility to convert a variety of objects + * into the File class. + * + * For convenience, you can also pass a fetch Response, or in Node, + * the result of fs.createReadStream(). + */ +export type Uploadable = Blob | File | Response | FsReadStream | BunFile; + +/** + * Construct a `File` instance. This is used to ensure a helpful error is thrown + * for environments that don't define a global `File` yet. + */ +export function makeFile( + fileBits: BlobPart[], + fileName: string | undefined, + options?: FilePropertyBag, +): File { + checkFileSupport(); + return new File(fileBits as any, fileName ?? 'unknown_file', options); +} + +export function getName(value: any): string | undefined { + return ( + ( + (typeof value === 'object' && + value !== null && + (('name' in value && value.name && String(value.name)) || + ('url' in value && value.url && String(value.url)) || + ('filename' in value && value.filename && String(value.filename)) || + ('path' in value && value.path && String(value.path)))) || + '' + ) + .split(/[\\/]/) + .pop() || undefined + ); +} + +export const isAsyncIterable = (value: any): value is AsyncIterable => + value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function'; + +/** + * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value. + * Otherwise returns the request as is. + */ +export const maybeMultipartFormRequestOptions = async ( + opts: RequestOptions, + fetch: ScalarGalaxy | Fetch, +): Promise => { + if (!hasUploadableValue(opts.body)) return opts; + + return { ...opts, body: await createForm(opts.body, fetch) }; +}; + +type MultipartFormRequestOptions = Omit & { body: unknown }; + +export const multipartFormRequestOptions = async ( + opts: MultipartFormRequestOptions, + fetch: ScalarGalaxy | Fetch, +): Promise => { + return { ...opts, body: await createForm(opts.body, fetch) }; +}; + +const supportsFormDataMap = /* @__PURE__ */ new WeakMap>(); + +/** + * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending + * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]". + * This function detects if the fetch function provided supports the global FormData object to avoid + * confusing error messages later on. + */ +function supportsFormData(fetchObject: ScalarGalaxy | Fetch): Promise { + const fetch: Fetch = typeof fetchObject === 'function' ? fetchObject : (fetchObject as any).fetch; + const cached = supportsFormDataMap.get(fetch); + if (cached) return cached; + const promise = (async () => { + try { + // Prefer a `Response` constructor we can reach without a network round-trip: the one attached to + // the fetch function, then the global `Response`. Only fall back to probing `data:,` when neither + // exists, so serializing an already-provided File/Blob never triggers an extra fetch (which would + // otherwise show up as a spurious request to `data:,` before the real API call). + const FetchResponse = ( + 'Response' in fetch ? fetch.Response + : typeof Response !== 'undefined' ? Response + : (await fetch('data:,')).constructor) as typeof Response; + const data = new FormData(); + if (data.toString() === (await new FetchResponse(data).text())) { + return false; + } + return true; + } catch { + // avoid false negatives + return true; + } + })(); + supportsFormDataMap.set(fetch, promise); + return promise; +} + +export const createForm = async >( + body: T | undefined, + fetch: ScalarGalaxy | Fetch, +): Promise => { + if (!(await supportsFormData(fetch))) { + throw new TypeError( + 'The provided fetch function does not support file uploads with the current global FormData class.', + ); + } + const form = new FormData(); + if (isUploadable(body)) { + // Multipart schemas can describe the whole request body as a single binary part. + await addFormValue(form, 'body', body); + return form; + } + await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))); + return form; +}; + +// We check for Blob not File because Bun.File doesn't inherit from File, +// but they both inherit from Blob and have a `name` property at runtime. +const isBlob = (value: unknown): value is Blob => value instanceof Blob; + +const isUploadable = (value: unknown) => + typeof value === 'object' && + value !== null && + (value instanceof Response || isAsyncIterable(value) || isBlob(value)); + +const hasUploadableValue = (value: unknown): boolean => { + if (isUploadable(value)) return true; + if (Array.isArray(value)) return value.some(hasUploadableValue); + if (value && typeof value === 'object') { + for (const k in value) { + if (hasUploadableValue((value as any)[k])) return true; + } + } + return false; +}; + +const addFormValue = async (form: FormData, key: string, value: unknown): Promise => { + if (value === undefined) return; + if (value == null) { + throw new TypeError( + `Received null for "${key}"; to pass null in FormData, you must use the string 'null'`, + ); + } + + // TODO: make nested formats configurable + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + form.append(key, String(value)); + } else if (value instanceof Response) { + form.append(key, makeFile([await value.blob()], getName(value))); + } else if (isAsyncIterable(value)) { + form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value))); + } else if (isBlob(value)) { + const name = getName(value); + if (name === undefined) { + form.append(key, value); + } else { + form.append(key, value, name); + } + } else if (Array.isArray(value)) { + await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry))); + } else if (typeof value === 'object') { + await Promise.all( + Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)), + ); + } else { + throw new TypeError( + `Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`, + ); + } +}; diff --git a/src/internal/utils/env.ts b/src/internal/utils/env.ts new file mode 100644 index 0000000..6212168 --- /dev/null +++ b/src/internal/utils/env.ts @@ -0,0 +1,18 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +/** + * Read an environment variable. + * + * Trims beginning and trailing whitespace. + * + * Will return undefined if the environment variable doesn't exist or cannot be accessed. + */ +export const readEnv = (env: string): string | undefined => { + if (typeof (globalThis as any).process !== 'undefined') { + return (globalThis as any).process.env?.[env]?.trim() || undefined; + } + if (typeof (globalThis as any).Deno !== 'undefined') { + return (globalThis as any).Deno.env?.get?.(env)?.trim() || undefined; + } + return undefined; +}; diff --git a/src/internal/utils/log.ts b/src/internal/utils/log.ts new file mode 100644 index 0000000..0556bf4 --- /dev/null +++ b/src/internal/utils/log.ts @@ -0,0 +1,128 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { hasOwn } from './values'; +import { type ScalarGalaxy } from '../../client'; +import { RequestOptions } from '../request-options'; + +type LogFn = (message: string, ...rest: unknown[]) => void; +export type Logger = { + error: LogFn; + warn: LogFn; + info: LogFn; + debug: LogFn; +}; +export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug'; + +const levelNumbers = { + off: 0, + error: 200, + warn: 300, + info: 400, + debug: 500, +}; + +export const parseLogLevel = ( + maybeLevel: string | undefined, + sourceName: string, + client: ScalarGalaxy, +): LogLevel | undefined => { + if (!maybeLevel) { + return undefined; + } + if (hasOwn(levelNumbers, maybeLevel)) { + return maybeLevel; + } + loggerFor(client).warn( + `${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify( + Object.keys(levelNumbers), + )}`, + ); + return undefined; +}; + +function noop() {} + +function makeLogFn(fnLevel: keyof Logger, logger: Logger | undefined, logLevel: LogLevel) { + if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) { + return noop; + } else { + // Don't wrap logger functions, we want the stacktrace intact! + return logger[fnLevel].bind(logger); + } +} + +const noopLogger = { + error: noop, + warn: noop, + info: noop, + debug: noop, +}; + +let cachedLoggers = /* @__PURE__ */ new WeakMap(); + +export function loggerFor(client: ScalarGalaxy): Logger { + const logger = client.logger; + const logLevel = client.logLevel ?? 'off'; + if (!logger) { + return noopLogger; + } + + const cachedLogger = cachedLoggers.get(logger); + if (cachedLogger && cachedLogger[0] === logLevel) { + return cachedLogger[1]; + } + + const levelLogger = { + error: makeLogFn('error', logger, logLevel), + warn: makeLogFn('warn', logger, logLevel), + info: makeLogFn('info', logger, logLevel), + debug: makeLogFn('debug', logger, logLevel), + }; + + cachedLoggers.set(logger, [logLevel, levelLogger]); + + return levelLogger; +} + +export const formatRequestDetails = (details: { + options?: RequestOptions | undefined; + headers?: Headers | Record | undefined; + retryOfRequestLogID?: string | undefined; + retryOf?: string | undefined; + url?: string | undefined; + status?: number | undefined; + method?: string | undefined; + durationMs?: number | undefined; + message?: unknown; + body?: unknown; +}) => { + if (details.options) { + details.options = { ...details.options }; + delete details.options['headers']; // redundant + leaks internals + } + if (details.headers) { + details.headers = Object.fromEntries( + (details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map( + ([name, value]) => [ + name, + ( + name.toLowerCase() === 'authorization' || + name.toLowerCase() === 'api-key' || + name.toLowerCase() === 'x-api-key' || + name.toLowerCase() === 'cookie' || + name.toLowerCase() === 'set-cookie' + ) ? + '***' + : value, + ], + ), + ); + } + if ('retryOfRequestLogID' in details) { + if (details.retryOfRequestLogID) { + details.retryOf = details.retryOfRequestLogID; + } + delete details.retryOfRequestLogID; + } + return details; +}; diff --git a/src/internal/utils/path.ts b/src/internal/utils/path.ts new file mode 100644 index 0000000..86d5039 --- /dev/null +++ b/src/internal/utils/path.ts @@ -0,0 +1,122 @@ +import { ScalarGalaxyError } from '../../core/error'; + +/** + * Percent-encode everything that isn't safe to have in a path without encoding safe chars. + * + * Taken from https://datatracker.ietf.org/doc/html/rfc3986#section-3.3: + * > unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * > sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + * > pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + */ +export function encodeURIPath(str: string) { + return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent); +} + +/** + * Like {@link encodeURIPath} but leaves `/` unescaped, implementing RFC 6570 reserved expansion + * (`{+var}`) for path-like parameters such as a file path. The slash is part of the route shape for + * these params — `docs/example.txt` must stay nested under `.../files/docs/example.txt` rather than + * collapsing to a single `docs%2Fexample.txt` segment that points at a different backend path. Each + * `/`-separated segment is still run through {@link encodeURIPath}, so every other unsafe character is + * percent-encoded with the exact same rules, and the tag function below still rejects `.`/`..` segments, + * so a reserved value cannot smuggle in path traversal. + */ +export function encodeURIPathReserved(str: string) { + return str.split('/').map(encodeURIPath).join('/'); +} + +/** + * Wrapper marking a path-parameter value for reserved expansion. The {@link createPathTagFunction} tag + * detects this instance and encodes its value with {@link encodeURIPathReserved} (slash-preserving) + * instead of the default per-segment encoder, while leaving all other interpolated params untouched. + */ +class ReservedPathParam { + constructor(readonly value: string) {} + toString(): string { + return this.value; + } +} + +/** + * Marks a value so the `path` tag preserves `/` when encoding it (RFC 6570 reserved expansion). Used by + * generated request paths for parameters the spec flags with `allowReserved` (e.g. file paths). + */ +export const reserved = (value: unknown): ReservedPathParam => new ReservedPathParam('' + value); + +const EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null)); + +export const createPathTagFunction = (pathEncoder = encodeURIPath) => + function path(statics: readonly string[], ...params: readonly unknown[]): string { + // If there are no params, no processing is needed. + if (statics.length === 1) return statics[0]!; + + let postPath = false; + const invalidSegments = []; + const path = statics.reduce((previousValue, currentValue, index) => { + if (/[?#]/.test(currentValue)) { + postPath = true; + } + const param = params[index]; + // Reserved params keep `/` (file-path-like values); everything else uses the default encoder. + const isReserved = param instanceof ReservedPathParam; + const value = isReserved ? param.value : param; + let encoded = (postPath ? encodeURIComponent : isReserved ? encodeURIPathReserved : pathEncoder)('' + value); + if ( + index !== params.length && + (value == null || + (typeof value === 'object' && + // handle values from other realms + value.toString === + Object.getPrototypeOf(Object.getPrototypeOf((value as any).hasOwnProperty ?? EMPTY) ?? EMPTY) + ?.toString)) + ) { + encoded = value + ''; + invalidSegments.push({ + start: previousValue.length + currentValue.length, + length: encoded.length, + error: `Value of type ${Object.prototype.toString + .call(value) + .slice(8, -1)} is not a valid path parameter`, + }); + } + return previousValue + currentValue + (index === params.length ? '' : encoded); + }, ''); + + const pathOnly = path.split(/[?#]/, 1)[0]!; + const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi; + let match; + + // Find all invalid segments + while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) { + invalidSegments.push({ + start: match.index, + length: match[0].length, + error: `Value "${match[0]}" can\'t be safely passed as a path parameter`, + }); + } + + invalidSegments.sort((a, b) => a.start - b.start); + + if (invalidSegments.length > 0) { + let lastEnd = 0; + const underline = invalidSegments.reduce((acc, segment) => { + const spaces = ' '.repeat(segment.start - lastEnd); + const arrows = '^'.repeat(segment.length); + lastEnd = segment.start + segment.length; + return acc + spaces + arrows; + }, ''); + + throw new ScalarGalaxyError( + `Path parameters result in path with invalid segments:\n${invalidSegments + .map((e) => e.error) + .join('\n')}\n${path}\n${underline}`, + ); + } + + return path; + }; + +/** + * URI-encodes path params and ensures no unsafe /./ or /../ path segments are introduced. + */ +export const path = /* @__PURE__ */ createPathTagFunction(encodeURIPath); diff --git a/src/internal/utils/sleep.ts b/src/internal/utils/sleep.ts new file mode 100644 index 0000000..f8d0a12 --- /dev/null +++ b/src/internal/utils/sleep.ts @@ -0,0 +1,3 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/internal/utils/uuid.ts b/src/internal/utils/uuid.ts new file mode 100644 index 0000000..94878fa --- /dev/null +++ b/src/internal/utils/uuid.ts @@ -0,0 +1,17 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +/** + * https://stackoverflow.com/a/2117523 + */ +export let uuid4 = function () { + const { crypto } = globalThis as any; + if (crypto?.randomUUID) { + uuid4 = crypto.randomUUID.bind(crypto); + return crypto.randomUUID(); + } + const u8 = new Uint8Array(1); + const randomByte = crypto ? () => crypto.getRandomValues(u8)[0]! : () => (Math.random() * 0xff) & 0xff; + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => + (+c ^ (randomByte() & (15 >> (+c / 4)))).toString(16), + ); +}; diff --git a/src/internal/utils/values.ts b/src/internal/utils/values.ts new file mode 100644 index 0000000..b9a716f --- /dev/null +++ b/src/internal/utils/values.ts @@ -0,0 +1,105 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { ScalarGalaxyError } from '../../core/error'; + +// https://url.spec.whatwg.org/#url-scheme-string +const startsWithSchemeRegexp = /^[a-z][a-z0-9+.-]*:/i; + +export const isAbsoluteURL = (url: string): boolean => { + return startsWithSchemeRegexp.test(url); +}; + +export let isArray = (val: unknown): val is unknown[] => ((isArray = Array.isArray), isArray(val)); +export let isReadonlyArray = isArray as (val: unknown) => val is readonly unknown[]; + +/** Returns an object if the given value isn't an object, otherwise returns as-is */ +export function maybeObj(x: unknown): object { + if (typeof x !== 'object') { + return {}; + } + + return x ?? {}; +} + +// https://stackoverflow.com/a/34491287 +export function isEmptyObj(obj: Object | null | undefined): boolean { + if (!obj) return true; + for (const _k in obj) return false; + return true; +} + +// https://eslint.org/docs/latest/rules/no-prototype-builtins +export function hasOwn(obj: T, key: PropertyKey): key is keyof T { + return Object.prototype.hasOwnProperty.call(obj, key); +} + +export function isObj(obj: unknown): obj is Record { + return obj != null && typeof obj === 'object' && !Array.isArray(obj); +} + +export const ensurePresent = (value: T | null | undefined): T => { + if (value == null) { + throw new ScalarGalaxyError(`Expected a value to be given but received ${value} instead.`); + } + + return value; +}; + +export const validatePositiveInteger = (name: string, n: unknown): number => { + if (typeof n !== 'number' || !Number.isInteger(n)) { + throw new ScalarGalaxyError(`${name} must be an integer`); + } + if (n < 0) { + throw new ScalarGalaxyError(`${name} must be a positive integer`); + } + return n; +}; + +export const coerceInteger = (value: unknown): number => { + if (typeof value === 'number') return Math.round(value); + if (typeof value === 'string') return parseInt(value, 10); + + throw new ScalarGalaxyError(`Could not coerce ${value} (type: ${typeof value}) into a number`); +}; + +export const coerceFloat = (value: unknown): number => { + if (typeof value === 'number') return value; + if (typeof value === 'string') return parseFloat(value); + + throw new ScalarGalaxyError(`Could not coerce ${value} (type: ${typeof value}) into a number`); +}; + +export const coerceBoolean = (value: unknown): boolean => { + if (typeof value === 'boolean') return value; + if (typeof value === 'string') return value === 'true'; + return Boolean(value); +}; + +export const maybeCoerceInteger = (value: unknown): number | undefined => { + if (value == null) { + return undefined; + } + return coerceInteger(value); +}; + +export const maybeCoerceFloat = (value: unknown): number | undefined => { + if (value == null) { + return undefined; + } + return coerceFloat(value); +}; + +export const maybeCoerceBoolean = (value: unknown): boolean | undefined => { + if (value == null) { + return undefined; + } + return coerceBoolean(value); +}; + +export const safeJSON = (text: string) => { + try { + return JSON.parse(text); + } catch (err) { + return undefined; + } +}; diff --git a/src/resource.ts b/src/resource.ts new file mode 100644 index 0000000..901b7bf --- /dev/null +++ b/src/resource.ts @@ -0,0 +1,11 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import type { ScalarGalaxy } from './client'; + +export class APIResource { + protected _client: ScalarGalaxy; + + constructor(client: ScalarGalaxy) { + this._client = client; + } +} diff --git a/src/resources/authentication.ts b/src/resources/authentication.ts new file mode 100644 index 0000000..144c08d --- /dev/null +++ b/src/resources/authentication.ts @@ -0,0 +1,106 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { APIResource } from "../resource"; +import { APIPromise } from "../api-promise"; +import type { RequestOptions } from "../internal/request-options"; + +export class Authentication extends APIResource { + /** + * Time to create a user account, eh? + * + * @param {AuthenticationCreateUserParams} [body] - The request body to send. + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} Created + * + * @example + * ```ts + * const user = await client.authentication.createUser(); + * ``` + */ + createUser(body: AuthenticationCreateUserParams | null | undefined = undefined, options?: RequestOptions): APIPromise { + return this._client.post("/user/signup", { body: body, ...options }); + } + + /** + * Yeah, this is the boring security stuff. Just get your super secret token and move on. + * + * @param {AuthenticationCreateTokenParams} [body] - The request body to send. + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} Token Created + * + * @example + * ```ts + * const createToken = await client.authentication.createToken(); + * ``` + */ + createToken(body: AuthenticationCreateTokenParams | null | undefined = undefined, options?: RequestOptions): APIPromise { + return this._client.post("/auth/token", { body: body, ...options }); + } + + /** + * Find yourself they say. That's what you can do here. + * + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} OK + * + * @example + * ```ts + * const user = await client.authentication.listMe(); + * ``` + */ + listMe(options?: RequestOptions): APIPromise { + return this._client.get("/me", options); + } +} + +/** + * A user + */ +export interface User { + /** + * @format int64 + */ + id?: number; + name?: string; +} + +/** + * Credentials to authenticate a user + */ +export interface Credentials { + /** + * @format email + */ + email: string; +} + +export interface AuthenticationCreateUserParams { + /** + * @format email + */ + email: string; + password: string; + name?: string; +} + +export interface AuthenticationCreateTokenParams { + /** + * @format email + */ + email: string; + password: string; +} + +export interface AuthenticationCreateTokenResponse { + token?: string; +} +export declare namespace Authentication { + export { + type User as User, + type Credentials as Credentials, + type AuthenticationCreateTokenResponse as AuthenticationCreateTokenResponse, + type AuthenticationCreateUserParams as AuthenticationCreateUserParams, + type AuthenticationCreateTokenParams as AuthenticationCreateTokenParams, + }; +} +export { Authentication as AuthenticationResource }; diff --git a/src/resources/celestial-bodies.ts b/src/resources/celestial-bodies.ts new file mode 100644 index 0000000..9104fac --- /dev/null +++ b/src/resources/celestial-bodies.ts @@ -0,0 +1,135 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { APIResource } from "../resource"; +import { APIPromise } from "../api-promise"; +import type { RequestOptions } from "../internal/request-options"; + +export class CelestialBodies extends APIResource { + /** + * Create a celestial body + * + * @param {CelestialBodyCreateParams} body - The request body to send. + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} Celestial body created + * + * @example + * ```ts + * const celestialBody = await client.celestialBodies.create({ + * name: "", + * type: "planet", + * }); + * ``` + */ + create(body: CelestialBodyCreateParams, options?: RequestOptions): APIPromise { + return this._client.post("/celestial-bodies", { body: body, ...options }); + } +} + +/** + * A celestial body which can be either a planet or a satellite + */ +export type CelestialBody = Planet | { name: string; type: "satellite" | "moon" | "asteroid" | "comet"; id?: number; description?: string | null; diameter?: number; orbit?: { planetId?: number; orbitalPeriod?: number; distance?: number } }; + +/** + * A planet in the Scalar Galaxy + */ +export interface Planet { + /** + * @format int64 + */ + id: number; + name: string; + type: "planet" | "terrestrial" | "gas_giant" | "ice_giant" | "dwarf" | "super_earth"; + description?: string | null; + /** + * A score from 0 to 1 indicating potential habitability + * @format float + * @minimum 0 + * @maximum 1 + */ + habitabilityIndex?: number; + physicalProperties?: { mass?: number; radius?: number; gravity?: number; temperature?: { min?: number; max?: number; average?: number } }; + /** + * Atmospheric composition + */ + atmosphere?: Array<{ compound?: string; percentage?: number }>; + /** + * @format date-time + */ + discoveredAt?: string; + image?: string | null; + satellites?: Array<{ name: string; type: "satellite" | "moon" | "asteroid" | "comet"; id?: number; description?: string | null; diameter?: number; orbit?: { planetId?: number; orbitalPeriod?: number; distance?: number } }>; + /** + * A user + */ + creator?: User; + tags?: Array; + /** + * @format date-time + */ + lastUpdated?: string; + /** + * URL which gets invoked upon a successful operation + * @format uri + */ + successCallbackUrl?: string; + /** + * URL which gets invoked upon a failed operation + * @format uri + */ + failureCallbackUrl?: string; +} + +/** + * A user + */ +export interface User { + /** + * @format int64 + */ + id?: number; + name?: string; +} + +export type CelestialBodyCreateParams = Planet | CelestialBodyCreateParams.CelestialBodyCreateParamsItem; + +export namespace CelestialBodyCreateParams { + export interface CelestialBodyCreateParamsItem { + name: string; + type: "satellite" | "moon" | "asteroid" | "comet"; + description?: string | null; + /** + * Diameter in kilometers + * @format float + */ + diameter?: number; + orbit?: CelestialBodyCreateParamsItem.Orbit; + } + + export namespace CelestialBodyCreateParamsItem { + export interface Orbit { + /** + * The ID of the planet this satellite orbits + * @format int64 + */ + planetId?: number; + /** + * Orbital period in Earth days + * @format float + */ + orbitalPeriod?: number; + /** + * Average distance from the planet in kilometers + * @format float + */ + distance?: number; + } + } +} +export declare namespace CelestialBodies { + export { + type CelestialBody as CelestialBody, + type CelestialBodyCreateParams as CelestialBodyCreateParams, + }; +} +export { CelestialBodies as CelestialBodyResource }; diff --git a/src/resources/index.ts b/src/resources/index.ts new file mode 100644 index 0000000..74d3b8c --- /dev/null +++ b/src/resources/index.ts @@ -0,0 +1,11 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export { Planets } from "./planets"; +export type { Planet, User, PlanetListAllDataParams, PlanetListAllDataResponse, PlanetCreateParams, PlanetUpdateParams, PlanetUploadImageParams, PlanetUploadImageResponse } from "./planets"; +export { Planets as PlanetResource } from "./planets"; +export { CelestialBodies } from "./celestial-bodies"; +export type { CelestialBody, CelestialBodyCreateParams } from "./celestial-bodies"; +export { CelestialBodies as CelestialBodyResource } from "./celestial-bodies"; +export { Authentication } from "./authentication"; +export type { Credentials, AuthenticationCreateUserParams, AuthenticationCreateTokenParams, AuthenticationCreateTokenResponse } from "./authentication"; +export { Authentication as AuthenticationResource } from "./authentication"; diff --git a/src/resources/planets.ts b/src/resources/planets.ts new file mode 100644 index 0000000..d86f70e --- /dev/null +++ b/src/resources/planets.ts @@ -0,0 +1,492 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +import { APIResource } from "../resource"; +import { APIPromise } from "../api-promise"; +import type { RequestOptions } from "../internal/request-options"; +import { multipartFormRequestOptions } from "../internal/uploads"; +import { path as __scalarPath } from "../internal/utils/path"; +import type { Uploadable } from "../core/uploads"; + +export class Planets extends APIResource { + /** + * It's easy to say you know them all, but do you really? Retrieve all the planets and check whether you missed one. + * + * @param {PlanetListAllDataParams} [params] - The parameters to send with the request. + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} OK + * + * @example + * ```ts + * const listAllData = await client.planets.listAllData({ + * limit: 10, + * offset: 0, + * }); + * ``` + */ + listAllData(params: PlanetListAllDataParams | null | undefined = {}, options?: RequestOptions): APIPromise { + const { limit, offset } = params ?? {}; + return this._client.get("/planets", { query: { limit: limit, offset: offset }, ...options }); + } + + /** + * Time to play god and create a new planet. What do you think? Ah, don't think too much. What could go wrong anyway? + * + * @param {PlanetCreateParams} [body] - The request body to send. + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} Created + * + * @example + * ```ts + * const planet = await client.planets.create(); + * ``` + */ + create(body: PlanetCreateParams | null | undefined = undefined, options?: RequestOptions): APIPromise { + return this._client.post("/planets", { body: body, ...options }); + } + + /** + * You'll better learn a little bit more about the planets. It might come in handy once space travel is available for everyone. + * + * @param {number} planetID - The ID of the planet to get + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} Planet Found + * + * @example + * ```ts + * const planet = await client.planets.retrieve(1); + * ``` + */ + retrieve(planetID: number, options?: RequestOptions): APIPromise { + return this._client.get(__scalarPath`/planets/${planetID}`, options); + } + + /** + * Sometimes you make mistakes, that's fine. No worries, you can update all planets. + * + * @param {number} planetID - The ID of the planet to get + * @param {PlanetUpdateParams} [body] - The request body to send. + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} OK + * + * @example + * ```ts + * const planet = await client.planets.update(1); + * ``` + */ + update(planetID: number, body: PlanetUpdateParams | null | undefined = undefined, options?: RequestOptions): APIPromise { + return this._client.put(__scalarPath`/planets/${planetID}`, { body: body, ...options }); + } + + /** + * This endpoint was used to delete planets. Unfortunately, that caused a lot of trouble for planets with life. So, this endpoint is now deprecated and should not be used anymore. + * + * @param {number} planetID - The ID of the planet to get + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns No Content + * + * @example + * ```ts + * await client.planets.delete(1); + * ``` + */ + delete(planetID: number, options?: RequestOptions): APIPromise { + return this._client.delete(__scalarPath`/planets/${planetID}`, { ...options, headers: { Accept: "*/*", ...options?.headers } }); + } + + /** + * Got a crazy good photo of a planet? Share it with the world! + * + * @param {number} planetID - The ID of the planet to get + * @param {PlanetUploadImageParams} [body] - The request body to send. + * @param {RequestOptions} [options] - Options to apply to the request, such as headers and an abort signal. + * @returns {APIPromise} Image uploaded + * + * @example + * ```ts + * const uploadImage = await client.planets.uploadImage(1); + * ``` + */ + uploadImage(planetID: number, body: PlanetUploadImageParams | null | undefined = undefined, options?: RequestOptions): APIPromise { + return this._client.post(__scalarPath`/planets/${planetID}/image`, multipartFormRequestOptions({ body: body, ...options }, this._client)); + } +} + +/** + * A planet in the Scalar Galaxy + */ +export interface Planet { + /** + * @format int64 + */ + id: number; + name: string; + type: "planet" | "terrestrial" | "gas_giant" | "ice_giant" | "dwarf" | "super_earth"; + description?: string | null; + /** + * A score from 0 to 1 indicating potential habitability + * @format float + * @minimum 0 + * @maximum 1 + */ + habitabilityIndex?: number; + physicalProperties?: { mass?: number; radius?: number; gravity?: number; temperature?: { min?: number; max?: number; average?: number } }; + /** + * Atmospheric composition + */ + atmosphere?: Array<{ compound?: string; percentage?: number }>; + /** + * @format date-time + */ + discoveredAt?: string; + image?: string | null; + satellites?: Array<{ name: string; type: "satellite" | "moon" | "asteroid" | "comet"; id?: number; description?: string | null; diameter?: number; orbit?: { planetId?: number; orbitalPeriod?: number; distance?: number } }>; + /** + * A user + */ + creator?: User; + tags?: Array; + /** + * @format date-time + */ + lastUpdated?: string; + /** + * URL which gets invoked upon a successful operation + * @format uri + */ + successCallbackUrl?: string; + /** + * URL which gets invoked upon a failed operation + * @format uri + */ + failureCallbackUrl?: string; +} + +/** + * A user + */ +export interface User { + /** + * @format int64 + */ + id?: number; + name?: string; +} + +export interface PlanetListAllDataParams { + /** + * The number of items to return + * @default 10 + * @format int64 + */ + limit?: number; + /** + * The number of items to skip before starting to collect the result set + * @default 0 + * @format int64 + */ + offset?: number; +} + +export interface PlanetListAllDataResponse { + data?: Array; + meta?: PlanetListAllDataResponse.Meta; +} + +export namespace PlanetListAllDataResponse { + export interface Meta { + /** + * @format int64 + */ + limit?: number; + /** + * @format int64 + */ + offset?: number; + /** + * @format int64 + */ + total?: number; + next?: string | null; + } +} + +export interface PlanetCreateParams { + name: string; + type: "planet" | "terrestrial" | "gas_giant" | "ice_giant" | "dwarf" | "super_earth"; + description?: string | null; + /** + * A score from 0 to 1 indicating potential habitability + * @format float + * @minimum 0 + * @maximum 1 + */ + habitabilityIndex?: number; + physicalProperties?: PlanetCreateParams.PhysicalProperties; + /** + * Atmospheric composition + */ + atmosphere?: Array; + /** + * @format date-time + */ + discoveredAt?: string; + image?: string | null; + satellites?: Array; + /** + * A user + */ + creator?: User; + tags?: Array; + /** + * URL which gets invoked upon a successful operation + * @format uri + */ + successCallbackUrl?: string; + /** + * URL which gets invoked upon a failed operation + * @format uri + */ + failureCallbackUrl?: string; +} + +export namespace PlanetCreateParams { + export interface PhysicalProperties { + /** + * Mass in Earth masses (must be greater than 0) + * @format float + */ + mass?: number; + /** + * Radius in Earth radii (must be greater than 0) + * @format float + */ + radius?: number; + /** + * Surface gravity in Earth g + * @format float + */ + gravity?: number; + temperature?: PhysicalProperties.Temperature; + } + + export namespace PhysicalProperties { + export interface Temperature { + /** + * Minimum temperature in Kelvin + * @format float + */ + min?: number; + /** + * Maximum temperature in Kelvin + * @format float + */ + max?: number; + /** + * Average temperature in Kelvin + * @format float + */ + average?: number; + } + } + + export interface Atmosphere { + compound?: string; + /** + * @format float + */ + percentage?: number; + } + + export interface Satellite { + name: string; + type: "satellite" | "moon" | "asteroid" | "comet"; + description?: string | null; + /** + * Diameter in kilometers + * @format float + */ + diameter?: number; + orbit?: Satellite.Orbit; + } + + export namespace Satellite { + export interface Orbit { + /** + * The ID of the planet this satellite orbits + * @format int64 + */ + planetId?: number; + /** + * Orbital period in Earth days + * @format float + */ + orbitalPeriod?: number; + /** + * Average distance from the planet in kilometers + * @format float + */ + distance?: number; + } + } +} + +export interface PlanetUpdateParams { + name: string; + type: "planet" | "terrestrial" | "gas_giant" | "ice_giant" | "dwarf" | "super_earth"; + description?: string | null; + /** + * A score from 0 to 1 indicating potential habitability + * @format float + * @minimum 0 + * @maximum 1 + */ + habitabilityIndex?: number; + physicalProperties?: PlanetUpdateParams.PhysicalProperties; + /** + * Atmospheric composition + */ + atmosphere?: Array; + /** + * @format date-time + */ + discoveredAt?: string; + image?: string | null; + satellites?: Array; + /** + * A user + */ + creator?: User; + tags?: Array; + /** + * URL which gets invoked upon a successful operation + * @format uri + */ + successCallbackUrl?: string; + /** + * URL which gets invoked upon a failed operation + * @format uri + */ + failureCallbackUrl?: string; +} + +export namespace PlanetUpdateParams { + export interface PhysicalProperties { + /** + * Mass in Earth masses (must be greater than 0) + * @format float + */ + mass?: number; + /** + * Radius in Earth radii (must be greater than 0) + * @format float + */ + radius?: number; + /** + * Surface gravity in Earth g + * @format float + */ + gravity?: number; + temperature?: PhysicalProperties.Temperature; + } + + export namespace PhysicalProperties { + export interface Temperature { + /** + * Minimum temperature in Kelvin + * @format float + */ + min?: number; + /** + * Maximum temperature in Kelvin + * @format float + */ + max?: number; + /** + * Average temperature in Kelvin + * @format float + */ + average?: number; + } + } + + export interface Atmosphere { + compound?: string; + /** + * @format float + */ + percentage?: number; + } + + export interface Satellite { + name: string; + type: "satellite" | "moon" | "asteroid" | "comet"; + description?: string | null; + /** + * Diameter in kilometers + * @format float + */ + diameter?: number; + orbit?: Satellite.Orbit; + } + + export namespace Satellite { + export interface Orbit { + /** + * The ID of the planet this satellite orbits + * @format int64 + */ + planetId?: number; + /** + * Orbital period in Earth days + * @format float + */ + orbitalPeriod?: number; + /** + * Average distance from the planet in kilometers + * @format float + */ + distance?: number; + } + } +} + +export interface PlanetUploadImageParams { + /** + * The image file to upload + * @format binary + */ + image?: Uploadable; +} + +export interface PlanetUploadImageResponse { + message?: string; + /** + * The URL where the uploaded image can be accessed + */ + imageUrl?: string; + /** + * Timestamp when the image was uploaded + * @format date-time + */ + uploadedAt?: string; + /** + * Size of the uploaded image in bytes + */ + fileSize?: number; + /** + * The content type of the uploaded image + */ + mimeType?: string; +} +export declare namespace Planets { + export { + type Planet as Planet, + type PlanetListAllDataResponse as PlanetListAllDataResponse, + type PlanetUploadImageResponse as PlanetUploadImageResponse, + type PlanetListAllDataParams as PlanetListAllDataParams, + type PlanetCreateParams as PlanetCreateParams, + type PlanetUpdateParams as PlanetUpdateParams, + type PlanetUploadImageParams as PlanetUploadImageParams, + }; +} +export { Planets as PlanetResource }; diff --git a/src/uploads.ts b/src/uploads.ts new file mode 100644 index 0000000..86a228e --- /dev/null +++ b/src/uploads.ts @@ -0,0 +1,3 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export { type Uploadable, toFile, type ToFileInput } from './core/uploads'; diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..1bee298 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,3 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +export const VERSION = "0.0.1"; diff --git a/tests/smoke-test.ts b/tests/smoke-test.ts new file mode 100644 index 0000000..2502f1f --- /dev/null +++ b/tests/smoke-test.ts @@ -0,0 +1,180 @@ +// File generated from our OpenAPI spec by Scalar. See README.md for details. + +// Smoke test: calls every generated operation once to confirm the SDK can reach each endpoint. +// Run it from this repo with `bun tests/smoke-test.ts`. Each case below calls one SDK method +// exactly the way the SDK exposes it (positional params, request body, pagination, streaming). +// +// Two environment variables tune a run: +// - SCALAR_SMOKE_FILTER: comma-separated needles; only operations whose name or path contains +// one of them run, so you can smoke-test a subset without editing this file. +// - SCALAR_SMOKE_REPORT: a file path; when set, the run writes a JSON report there instead of +// printing a table. The generator uses this to collect per-operation results. +import { writeFileSync } from 'node:fs' + +// The default export is the client class. The client reads auth and the base URL from the +// environment, so it needs no constructor options to point at a server. +import ScalarGalaxy from "@scalar/galaxy-sdk" + +// One shared client runs every case. +const client = new ScalarGalaxy() + +// The result of running one case, collected for the JSON report or the printed table. +type SmokeResult = { + operation: string + method: string + path: string + status: 'passed' | 'failed' + durationMs: number + error?: string +} + +// One entry per generated operation. `run` performs the real SDK call; the other fields are +// metadata used for filtering and reporting. This list is generated, so it stays in sync with +// the SDK surface. +const cases: { operation: string; method: string; path: string; run: () => Promise }[] = [ + { + operation: "listAllData", + method: "GET", + path: "/planets", + run: async () => { + const listAllData = await client.planets.listAllData({ + limit: 10, + offset: 0, + }); + }, + }, + + { + operation: "create", + method: "POST", + path: "/planets", + run: async () => { + const planet = await client.planets.create(); + }, + }, + + { + operation: "retrieve", + method: "GET", + path: "/planets/{planetId}", + run: async () => { + const planet = await client.planets.retrieve(1); + }, + }, + + { + operation: "update", + method: "PUT", + path: "/planets/{planetId}", + run: async () => { + const planet = await client.planets.update(1); + }, + }, + + { + operation: "delete", + method: "DELETE", + path: "/planets/{planetId}", + run: async () => { + await client.planets.delete(1); + }, + }, + + { + operation: "uploadImage", + method: "POST", + path: "/planets/{planetId}/image", + run: async () => { + const uploadImage = await client.planets.uploadImage(1); + }, + }, + + { + operation: "create", + method: "POST", + path: "/celestial-bodies", + run: async () => { + const celestialBody = await client.celestialBodies.create({ + name: "", + type: "planet", + }); + }, + }, + + { + operation: "createUser", + method: "POST", + path: "/user/signup", + run: async () => { + const user = await client.authentication.createUser(); + }, + }, + + { + operation: "createToken", + method: "POST", + path: "/auth/token", + run: async () => { + const createToken = await client.authentication.createToken(); + }, + }, + + { + operation: "listMe", + method: "GET", + path: "/me", + run: async () => { + const user = await client.authentication.listMe(); + }, + }, + +] + +const main = async (): Promise => { + // SCALAR_SMOKE_FILTER (comma-separated) keeps only cases whose operation name or path matches + // one of the needles, so a caller can smoke-test a subset. With no filter, every case runs. + const filter = process.env['SCALAR_SMOKE_FILTER'] + const needles = filter ? filter.split(',').map((needle) => needle.trim()).filter(Boolean) : [] + const selected = needles.length > 0 ? cases.filter((testCase) => needles.some((needle) => testCase.operation.includes(needle) || testCase.path.includes(needle))) : cases + + // Run every selected case concurrently. Promise.allSettled means one failing operation never + // blocks the others, so a single run reports the status of every endpoint. + const settled = await Promise.allSettled( + selected.map(async (testCase): Promise => { + const startedAt = Date.now() + try { + await testCase.run() + return { operation: testCase.operation, method: testCase.method, path: testCase.path, status: 'passed', durationMs: Date.now() - startedAt } + } catch (error) { + // Prefer the stack so a failure points at the failing SDK call; fall back to the message. + const message = error instanceof Error ? (error.stack ?? error.message) : String(error) + return { operation: testCase.operation, method: testCase.method, path: testCase.path, status: 'failed', durationMs: Date.now() - startedAt, error: message } + } + }), + ) + + // allSettled never rejects, but defensively map any rejected slot to a failed result. + const results: SmokeResult[] = settled.map((result) => (result.status === 'fulfilled' ? result.value : { operation: 'unknown', method: '', path: '', status: 'failed', durationMs: 0, error: String(result.reason) })) + const failed = results.filter((result) => result.status === 'failed') + + // With SCALAR_SMOKE_REPORT set, write a machine-readable report; otherwise print a table. + const reportPath = process.env['SCALAR_SMOKE_REPORT'] + if (reportPath) { + writeFileSync(reportPath, JSON.stringify({ total: results.length, failed: failed.length, results })) + } else { + for (const result of results) { + if (result.status === 'passed') console.log(`\u2714 ${result.operation} (${result.method} ${result.path}) ${result.durationMs}ms`) + else console.error(`\u2718 ${result.operation} (${result.method} ${result.path})\n${result.error ?? ''}`) + } + if (results.length === 0) { + console.error('No code samples ran (empty SDK or a SCALAR_SMOKE_FILTER that matched nothing).') + } else { + console.log(`\n${results.length - failed.length}/${results.length} samples passed`) + } + } + + // An empty run (no operations, or a filter that matched nothing) is a failure, not a vacuous pass. + if (failed.length > 0 || results.length === 0) process.exitCode = 1 +} + +void main() diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..ace7601 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "CommonJS", + "lib": [ + "ES2023", + "DOM" + ], + "rootDir": "src", + "outDir": "./dist/cjs", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a30b112 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": [ + "ES2023", + "DOM" + ], + "rootDir": "src", + "outDir": "./dist/esm", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +}