Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
gcp: ${{ steps.force.outputs.all || steps.changes.outputs.gcp }}
neon: ${{ steps.force.outputs.all || steps.changes.outputs.neon }}
planetscale: ${{ steps.force.outputs.all || steps.changes.outputs.planetscale }}
polar: ${{ steps.force.outputs.all || steps.changes.outputs.polar }}
prisma-postgres: ${{ steps.force.outputs.all || steps.changes.outputs.prisma-postgres }}
stripe: ${{ steps.force.outputs.all || steps.changes.outputs.stripe }}
supabase: ${{ steps.force.outputs.all || steps.changes.outputs.supabase }}
Expand Down Expand Up @@ -68,6 +69,9 @@ jobs:
planetscale:
- 'packages/planetscale/**'
- 'packages/core/**'
polar:
- 'packages/polar/**'
- 'packages/core/**'
prisma-postgres:
- 'packages/prisma-postgres/**'
- 'packages/core/**'
Expand Down Expand Up @@ -124,6 +128,27 @@ jobs:
- run: bun run check
working-directory: packages/core

ci-polar:
needs: detect-changes
if: needs.detect-changes.outputs.polar == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run build
working-directory: packages/core
- run: bun run check
working-directory: packages/polar
# Live tests are gated on a Polar sandbox token; they skip without it.
- run: bun run test
working-directory: packages/polar
env:
POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
POLAR_SERVER: sandbox

ci-aws:
needs: detect-changes
if: needs.detect-changes.outputs.aws == 'true'
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ distilled/
│ ├── mongodb-atlas/ # @distilled.cloud/mongodb-atlas — MongoDB Atlas SDK from OpenAPI spec
│ ├── neon/ # @distilled.cloud/neon — Neon SDK from OpenAPI spec
│ ├── planetscale/ # @distilled.cloud/planetscale — PlanetScale SDK from OpenAPI spec
│ ├── polar/ # @distilled.cloud/polar — Polar billing SDK from OpenAPI spec
│ ├── prisma-postgres/ # @distilled.cloud/prisma-postgres — Prisma Postgres SDK from OpenAPI spec
│ ├── stripe/ # @distilled.cloud/stripe — Stripe SDK from OpenAPI spec
│ ├── supabase/ # @distilled.cloud/supabase — Supabase SDK from OpenAPI spec
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ For larger SDKs its very likely you hit 5hr limits on claude, you can just run t
| [`@distilled.cloud/mongodb-atlas`](./packages/mongodb-atlas) | MongoDB Atlas SDK from OpenAPI spec |
| [`@distilled.cloud/neon`](./packages/neon) | Neon serverless Postgres SDK from OpenAPI spec |
| [`@distilled.cloud/planetscale`](./packages/planetscale) | PlanetScale MySQL SDK from OpenAPI spec |
| [`@distilled.cloud/polar`](./packages/polar) | Polar billing SDK from OpenAPI spec (products, subscriptions, meters, events, checkouts) |
| [`@distilled.cloud/prisma-postgres`](./packages/prisma-postgres) | Prisma Postgres SDK from OpenAPI spec |
| [`@distilled.cloud/stripe`](./packages/stripe) | Stripe SDK from OpenAPI spec |
| [`@distilled.cloud/supabase`](./packages/supabase) | Supabase Management API SDK from OpenAPI spec |
Expand Down
18 changes: 18 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 53 additions & 1 deletion packages/core/scripts/generate-openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1385,7 +1385,12 @@ function generateInputSchema3(
}
if (bodyContent?.schema) {
let bodySchema = bodyContent.schema;
if (bodySchema.$ref) {
// Unwrap chained `$ref`s (e.g. Polar's `CheckoutCreate` → `$ref`
// `CheckoutProductsCreate`). Resolving only once leaves a bare-ref
// schema with no `properties`/`oneOf`, degenerating to an empty body.
const seenBodyRefs = new Set<string>();
while (bodySchema.$ref && !seenBodyRefs.has(bodySchema.$ref)) {
seenBodyRefs.add(bodySchema.$ref);
bodySchema = resolveRef(spec, bodySchema.$ref);
}

Expand Down Expand Up @@ -1416,6 +1421,53 @@ function generateInputSchema3(
};
}

// Flatten a top-level `oneOf`/`anyOf` request body — a discriminated
// union like Polar's `CustomerCreate` / `ProductCreate` — into a single
// permissive struct: the union of every branch's properties, marking a
// field required only when it is required in EVERY branch (so a shared
// field like `name` stays required while variant-only fields like
// `email` / `recurring_interval` become optional). Without this a union
// body has no `.properties` and degenerates to an EMPTY input schema —
// the operation compiles but can never send its body.
const bodyBranches = bodySchema.oneOf ?? bodySchema.anyOf;
if (!bodySchema.properties && bodyBranches && bodyBranches.length > 0) {
const mergedProps: Record<string, SchemaObject> = {};
const requiredSets: Set<string>[] = [];
for (const branch of bodyBranches) {
const resolved = branch.$ref
? (resolveRef(spec, branch.$ref) as SchemaObject)
: branch;
const branchProps: Record<string, SchemaObject> = {
...resolved.properties,
};
const branchRequired: string[] = [...(resolved.required ?? [])];
// A branch may itself be an `allOf` — merge those props too.
for (const sub of resolved.allOf ?? []) {
const resolvedSub = sub.$ref
? (resolveRef(spec, sub.$ref) as SchemaObject)
: sub;
if (resolvedSub.properties) {
Object.assign(branchProps, resolvedSub.properties);
}
if (resolvedSub.required) {
branchRequired.push(...resolvedSub.required);
}
}
Object.assign(mergedProps, branchProps);
requiredSets.push(new Set(branchRequired));
}
// Required only where required in every branch (intersection).
const requiredEverywhere = Object.keys(mergedProps).filter((key) =>
requiredSets.every((set) => set.has(key)),
);
bodySchema = {
...bodySchema,
type: "object",
properties: mergedProps,
required: requiredEverywhere,
};
}

if (bodySchema.properties) {
const required = new Set(bodySchema.required || []);
for (const [key, value] of Object.entries(bodySchema.properties)) {
Expand Down
60 changes: 60 additions & 0 deletions packages/polar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# @distilled.cloud/polar

Effect-native [Polar](https://polar.sh) billing SDK, generated from the [Polar OpenAPI spec](https://api.polar.sh/openapi.json). Products, prices, subscriptions, customers, meters, events, checkouts, and benefits with exhaustive error typing, retry policies, and streaming pagination.

## Installation

```bash
npm install @distilled.cloud/polar effect
```

## Quick Start

```typescript
import { Effect, Layer } from "effect";
import * as Stream from "effect/Stream";
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
import { CredentialsFromEnv } from "@distilled.cloud/polar";
import { organizationslist } from "@distilled.cloud/polar";

const PolarLive = Layer.mergeAll(FetchHttpClient.layer, CredentialsFromEnv);

const program = organizationslist({ limit: 20 }).pipe(Effect.provide(PolarLive));
```

Errors are typed and matchable:

```typescript
import { subscriptionsget } from "@distilled.cloud/polar";

subscriptionsget({ id }).pipe(
Effect.catch("NotFound", () => Effect.succeed(null)),
);
```

## Configuration

```bash
POLAR_ACCESS_TOKEN=polar_pat_... # Personal or Organization Access Token
POLAR_SERVER=production # or "sandbox" (default: production)
POLAR_BASE_URL=... # optional, overrides POLAR_SERVER
```

Create a token in the [Polar dashboard](https://polar.sh/settings) under **Developers**. Or build credentials directly:

```typescript
import { layer } from "@distilled.cloud/polar";

const creds = layer({ accessToken: token, server: "sandbox" });
```

## Regenerating

```bash
bun run specs:update # refresh specs/openapi.json from api.polar.sh
bun run generate # regenerate src/operations from the spec
```

## License

Apache-2.0
89 changes: 89 additions & 0 deletions packages/polar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"name": "@distilled.cloud/polar",
"version": "0.0.0",
"repository": {
"type": "git",
"url": "https://github.com/alchemy-run/distilled",
"directory": "packages/polar"
},
"license": "Apache-2.0",
"type": "module",
"sideEffects": false,
"module": "src/index.ts",
"files": [
"lib",
"src"
],
"exports": {
".": {
"types": "./lib/index.d.ts",
"bun": "./src/index.ts",
"default": "./lib/index.js"
},
"./Category": {
"types": "./lib/category.d.ts",
"bun": "./src/category.ts",
"default": "./lib/category.js"
},
"./Client": {
"types": "./lib/client.d.ts",
"bun": "./src/client.ts",
"default": "./lib/client.js"
},
"./Credentials": {
"types": "./lib/credentials.d.ts",
"bun": "./src/credentials.ts",
"default": "./lib/credentials.js"
},
"./Errors": {
"types": "./lib/errors.d.ts",
"bun": "./src/errors.ts",
"default": "./lib/errors.js"
},
"./Operations": {
"types": "./lib/operations/index.d.ts",
"bun": "./src/operations/index.ts",
"default": "./lib/operations/index.js"
},
"./Retry": {
"types": "./lib/retry.d.ts",
"bun": "./src/retry.ts",
"default": "./lib/retry.js"
},
"./Sensitive": {
"types": "./lib/sensitive.d.ts",
"bun": "./src/sensitive.ts",
"default": "./lib/sensitive.js"
},
"./Traits": {
"types": "./lib/traits.d.ts",
"bun": "./src/traits.ts",
"default": "./lib/traits.js"
}
},
"scripts": {
"typecheck": "tsc",
"build": "tsc -b",
"fmt": "oxfmt --write src",
"lint": "oxlint --fix src",
"check": "tsc && oxlint src && oxfmt --check src",
"nuke": "bun scripts/nuke.ts",
"test": "bunx vitest run test --exclude specs --passWithNoTests",
"publish:npm": "bun run build && bun publish --access public",
"generate": "bun run scripts/generate.ts && oxlint --fix src && oxfmt --write src && oxfmt --write src",
"specs:fetch": "curl -sSL https://api.polar.sh/openapi.json -o specs/openapi.json",
"specs:update": "curl -sSL https://api.polar.sh/openapi.json -o specs/openapi.json"
},
"dependencies": {
"@distilled.cloud/core": "workspace:*"
},
"devDependencies": {
"@types/bun": "catalog:",
"@types/node": "catalog:",
"dotenv": "catalog:",
"vitest": "catalog:"
},
"peerDependencies": {
"effect": "catalog:"
}
}
23 changes: 23 additions & 0 deletions packages/polar/scripts/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Polar SDK Code Generator
*
* Uses the shared OpenAPI generator from sdk-core to generate operations from
* the Polar OpenAPI 3.1 spec (https://api.polar.sh/openapi.json).
*/
import * as path from "path";
import { generateFromOpenAPI } from "@distilled.cloud/core/openapi/generate";

const rootDir = path.join(import.meta.dir, "..");

generateFromOpenAPI({
specPath: path.join(rootDir, "specs/openapi.json"),
patchDir: path.join(rootDir, "patches"),
outputDir: path.join(rootDir, "src/operations"),
importPrefix: "..",
clientImport: "../client",
traitsImport: "../traits",
sensitiveImport: "../sensitive",
errorsImport: "../errors",
includeOperationErrors: false,
skipDeprecated: true,
});
Loading
Loading