Skip to content

Commit 61d0fc3

Browse files
authored
fix(stripe): encode Stripe wire shapes (#359)
1 parent 761ccc9 commit 61d0fc3

6 files changed

Lines changed: 281 additions & 54 deletions

File tree

packages/core/scripts/generate-openapi.ts

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ function renderEnumLiterals(
255255
return `Schema.Literals([${literals}])`;
256256
}
257257

258+
function renderParameterSchema3(
259+
schema: SchemaObject | undefined,
260+
spec: OpenAPI3Spec,
261+
ctx: SchemaGenerationContext,
262+
): string {
263+
if (!schema) return "Schema.String";
264+
if (schema.enum && schema.enum.length > 0) {
265+
return renderEnumLiterals(schema.enum, schema.type);
266+
}
267+
return openApiTypeToEffectSchema(schema, spec, "", new Set(), ctx);
268+
}
269+
258270
// ============================================================================
259271
// Version Detection
260272
// ============================================================================
@@ -1323,22 +1335,7 @@ function generateInputSchema3(
13231335
const tsFields: string[] = [];
13241336
const paramSchemaTs = (schema: SchemaObject | undefined): string => {
13251337
if (!schema) return "string";
1326-
if (schema.enum && schema.enum.length > 0) {
1327-
return schema.enum
1328-
.map((v) =>
1329-
schema.type === "integer" ||
1330-
schema.type === "number" ||
1331-
schema.type === "boolean"
1332-
? String(v)
1333-
: JSON.stringify(v),
1334-
)
1335-
.join(" | ");
1336-
}
1337-
return schema.type === "integer" || schema.type === "number"
1338-
? "number"
1339-
: schema.type === "boolean"
1340-
? "boolean"
1341-
: "string";
1338+
return openApiTypeToTsType(schema, spec, new Set(), ctx);
13421339
};
13431340
const usedNames = new Set<string>();
13441341

@@ -1362,14 +1359,7 @@ function generateInputSchema3(
13621359
if (usedNames.has(param.name)) continue;
13631360
usedNames.add(param.name);
13641361
const schema = param.schema;
1365-
let schemaStr =
1366-
schema?.enum && schema.enum.length > 0
1367-
? renderEnumLiterals(schema.enum, schema.type)
1368-
: schema?.type === "integer" || schema?.type === "number"
1369-
? "Schema.Number"
1370-
: schema?.type === "boolean"
1371-
? "Schema.Boolean"
1372-
: "Schema.String";
1362+
let schemaStr = renderParameterSchema3(schema, spec, ctx);
13731363

13741364
if (!param.required) {
13751365
schemaStr = `Schema.optional(${schemaStr})`;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"description": "Allow clearing Product.shippable on update with Stripe's empty-string form value.",
3+
"patches": [
4+
{
5+
"op": "test",
6+
"path": "/paths/~1v1~1products~1{id}/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/shippable",
7+
"value": {
8+
"type": "boolean",
9+
"description": "Whether this product is shipped (i.e., physical goods)."
10+
}
11+
},
12+
{
13+
"op": "replace",
14+
"path": "/paths/~1v1~1products~1{id}/post/requestBody/content/application~1x-www-form-urlencoded/schema/properties/shippable",
15+
"value": {
16+
"description": "Whether this product is shipped (i.e., physical goods).",
17+
"anyOf": [
18+
{
19+
"type": "boolean"
20+
},
21+
{
22+
"type": "string",
23+
"enum": [""]
24+
}
25+
]
26+
}
27+
}
28+
]
29+
}

packages/stripe/src/client.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
* error matching and credential handling.
66
*/
77
import * as Effect from "effect/Effect";
8+
import * as Predicate from "effect/Predicate";
89
import * as Redacted from "effect/Redacted";
910
import * as Schema from "effect/Schema";
1011
import { makeAPI } from "@distilled.cloud/core/client";
12+
import type * as Traits from "./traits.ts";
1113
import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
1214
import { Retry } from "./retry.ts";
1315
import {
@@ -40,6 +42,74 @@ export type StripeRequestOptions = {
4042
readonly apiVersion?: string | undefined;
4143
} & StripeConnectRequestOptions;
4244

45+
const appendQueryValue = (
46+
query: Record<string, string | string[]>,
47+
key: string,
48+
value: unknown,
49+
): void => {
50+
if (Predicate.isNullish(value)) {
51+
return;
52+
}
53+
const encoded = Predicate.isBoolean(value)
54+
? value
55+
? "true"
56+
: "false"
57+
: String(value);
58+
const existing = query[key];
59+
if (Predicate.isUndefined(existing)) {
60+
query[key] = encoded;
61+
} else if (Array.isArray(existing)) {
62+
existing.push(encoded);
63+
} else {
64+
query[key] = [existing, encoded];
65+
}
66+
};
67+
68+
const appendStripeQuery = (
69+
query: Record<string, string | string[]>,
70+
key: string,
71+
value: unknown,
72+
): void => {
73+
if (Predicate.isNullish(value)) {
74+
return;
75+
}
76+
if (Array.isArray(value)) {
77+
for (const item of value) {
78+
appendQueryValue(query, `${key}[]`, item);
79+
}
80+
return;
81+
}
82+
if (Predicate.isObject(value)) {
83+
for (const [nestedKey, nestedValue] of Object.entries(value)) {
84+
appendStripeQuery(query, `${key}[${nestedKey}]`, nestedValue);
85+
}
86+
return;
87+
}
88+
appendQueryValue(query, key, value);
89+
};
90+
91+
const normalizeStripeGetQuery = ({
92+
method,
93+
parts,
94+
}: {
95+
method: string;
96+
parts: Traits.RequestParts;
97+
}): Traits.RequestParts => {
98+
if (
99+
method !== "GET" ||
100+
Predicate.isUndefined(parts.body) ||
101+
!Predicate.isObject(parts.body)
102+
) {
103+
return parts;
104+
}
105+
106+
const query = { ...parts.query };
107+
for (const [key, value] of Object.entries(parts.body)) {
108+
appendStripeQuery(query, key, value);
109+
}
110+
return { ...parts, body: undefined, query };
111+
};
112+
43113
const stripeRequestHeaders = (
44114
options: StripeRequestOptions | undefined,
45115
): Record<string, string> => {
@@ -201,6 +271,7 @@ export const API = makeAPI<Credentials, StripeRequestOptions>({
201271
Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
202272
}),
203273
getRequestHeaders: stripeRequestHeaders,
274+
transformRequestParts: normalizeStripeGetQuery,
204275
matchError,
205276
ParseError: StripeParseError as any,
206277
retry: Retry as any,

packages/stripe/src/operations/GetPrices.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,49 @@ import * as T from "../traits.ts";
55
// Input Schema
66
export interface GetPricesInput {
77
active?: boolean;
8-
created?: string;
8+
created?: { gt?: number; gte?: number; lt?: number; lte?: number } | number;
99
currency?: string;
1010
ending_before?: string;
11-
expand?: string;
11+
expand?: ReadonlyArray<string>;
1212
limit?: number;
13-
lookup_keys?: string;
13+
lookup_keys?: ReadonlyArray<string>;
1414
product?: string;
15-
recurring?: string;
15+
recurring?: {
16+
interval?: "day" | "month" | "week" | "year";
17+
meter?: string;
18+
usage_type?: "licensed" | "metered";
19+
};
1620
starting_after?: string;
1721
type?: "one_time" | "recurring";
1822
}
1923
export const GetPricesInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({
2024
active: Schema.optional(Schema.Boolean),
21-
created: Schema.optional(Schema.String),
25+
created: Schema.optional(
26+
Schema.Union([
27+
Schema.Struct({
28+
gt: Schema.optional(Schema.Number),
29+
gte: Schema.optional(Schema.Number),
30+
lt: Schema.optional(Schema.Number),
31+
lte: Schema.optional(Schema.Number),
32+
}),
33+
Schema.Number,
34+
]),
35+
),
2236
currency: Schema.optional(Schema.String),
2337
ending_before: Schema.optional(Schema.String),
24-
expand: Schema.optional(Schema.String),
38+
expand: Schema.optional(Schema.Array(Schema.String)),
2539
limit: Schema.optional(Schema.Number),
26-
lookup_keys: Schema.optional(Schema.String),
40+
lookup_keys: Schema.optional(Schema.Array(Schema.String)),
2741
product: Schema.optional(Schema.String),
28-
recurring: Schema.optional(Schema.String),
42+
recurring: Schema.optional(
43+
Schema.Struct({
44+
interval: Schema.optional(
45+
Schema.Literals(["day", "month", "week", "year"]),
46+
),
47+
meter: Schema.optional(Schema.String),
48+
usage_type: Schema.optional(Schema.Literals(["licensed", "metered"])),
49+
}),
50+
),
2951
starting_after: Schema.optional(Schema.String),
3052
type: Schema.optional(Schema.Literals(["one_time", "recurring"])),
3153
}).pipe(
@@ -34,7 +56,7 @@ export const GetPricesInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({
3456

3557
// Output Schema
3658
export interface GetPricesOutput {
37-
data: {
59+
data: ReadonlyArray<{
3860
active: boolean;
3961
billing_scheme: "per_unit" | "tiered";
4062
created: number;
@@ -48,13 +70,13 @@ export interface GetPricesOutput {
4870
preset: number | null;
4971
} | null;
5072
tax_behavior: "exclusive" | "inclusive" | "unspecified" | null;
51-
tiers?: {
73+
tiers?: ReadonlyArray<{
5274
flat_amount: number | null;
5375
flat_amount_decimal: string | null;
5476
unit_amount: number | null;
5577
unit_amount_decimal: string | null;
5678
up_to: number | null;
57-
}[];
79+
}>;
5880
unit_amount: number | null;
5981
unit_amount_decimal: string | null;
6082
}
@@ -78,9 +100,9 @@ export interface GetPricesOutput {
78100
default_price?: string | unknown | null;
79101
description: string | null;
80102
id: string;
81-
images: string[];
103+
images: ReadonlyArray<string>;
82104
livemode: boolean;
83-
marketing_features: { name?: string }[];
105+
marketing_features: ReadonlyArray<{ name?: string }>;
84106
metadata: Record<string, string>;
85107
name: string;
86108
object: "product";
@@ -115,19 +137,19 @@ export interface GetPricesOutput {
115137
usage_type: "licensed" | "metered";
116138
} | null;
117139
tax_behavior: "exclusive" | "inclusive" | "unspecified" | null;
118-
tiers?: {
140+
tiers?: ReadonlyArray<{
119141
flat_amount: number | null;
120142
flat_amount_decimal: string | null;
121143
unit_amount: number | null;
122144
unit_amount_decimal: string | null;
123145
up_to: number | null;
124-
}[];
146+
}>;
125147
tiers_mode: "graduated" | "volume" | null;
126148
transform_quantity: { divide_by: number; round: "down" | "up" } | null;
127149
type: "one_time" | "recurring";
128150
unit_amount: number | null;
129151
unit_amount_decimal: string | null;
130-
}[];
152+
}>;
131153
has_more: boolean;
132154
object: "list";
133155
url: string;

packages/stripe/src/operations/PostProductsId.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ export interface PostProductsIdInput {
88
active?: boolean;
99
default_price?: string;
1010
description?: string | "";
11-
expand?: string[];
12-
images?: string[] | "";
13-
marketing_features?: { name: string }[] | "";
11+
expand?: ReadonlyArray<string>;
12+
images?: ReadonlyArray<string> | "";
13+
marketing_features?: ReadonlyArray<{ name: string }> | "";
1414
metadata?: Record<string, string> | "";
1515
name?: string;
1616
package_dimensions?:
1717
| { height: number; length: number; weight: number; width: number }
1818
| "";
19-
shippable?: boolean;
19+
shippable?: boolean | "";
2020
statement_descriptor?: string;
2121
tax_code?: string | "";
2222
unit_label?: string | "";
@@ -61,7 +61,9 @@ export const PostProductsIdInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({
6161
Schema.Literals([""]),
6262
]),
6363
),
64-
shippable: Schema.optional(Schema.Boolean),
64+
shippable: Schema.optional(
65+
Schema.Union([Schema.Boolean, Schema.Literals([""])]),
66+
),
6567
statement_descriptor: Schema.optional(Schema.String),
6668
tax_code: Schema.optional(
6769
Schema.Union([Schema.String, Schema.Literals([""])]),
@@ -98,13 +100,13 @@ export interface PostProductsIdOutput {
98100
preset: number | null;
99101
} | null;
100102
tax_behavior: "exclusive" | "inclusive" | "unspecified" | null;
101-
tiers?: {
103+
tiers?: ReadonlyArray<{
102104
flat_amount: number | null;
103105
flat_amount_decimal: string | null;
104106
unit_amount: number | null;
105107
unit_amount_decimal: string | null;
106108
up_to: number | null;
107-
}[];
109+
}>;
108110
unit_amount: number | null;
109111
unit_amount_decimal: string | null;
110112
}
@@ -128,9 +130,9 @@ export interface PostProductsIdOutput {
128130
default_price?: string | unknown | null;
129131
description: string | null;
130132
id: string;
131-
images: string[];
133+
images: ReadonlyArray<string>;
132134
livemode: boolean;
133-
marketing_features: { name?: string }[];
135+
marketing_features: ReadonlyArray<{ name?: string }>;
134136
metadata: Record<string, string>;
135137
name: string;
136138
object: "product";
@@ -165,13 +167,13 @@ export interface PostProductsIdOutput {
165167
usage_type: "licensed" | "metered";
166168
} | null;
167169
tax_behavior: "exclusive" | "inclusive" | "unspecified" | null;
168-
tiers?: {
170+
tiers?: ReadonlyArray<{
169171
flat_amount: number | null;
170172
flat_amount_decimal: string | null;
171173
unit_amount: number | null;
172174
unit_amount_decimal: string | null;
173175
up_to: number | null;
174-
}[];
176+
}>;
175177
tiers_mode: "graduated" | "volume" | null;
176178
transform_quantity: { divide_by: number; round: "down" | "up" } | null;
177179
type: "one_time" | "recurring";
@@ -181,9 +183,9 @@ export interface PostProductsIdOutput {
181183
| null;
182184
description: string | null;
183185
id: string;
184-
images: string[];
186+
images: ReadonlyArray<string>;
185187
livemode: boolean;
186-
marketing_features: { name?: string }[];
188+
marketing_features: ReadonlyArray<{ name?: string }>;
187189
metadata: Record<string, string>;
188190
name: string;
189191
object: "product";

0 commit comments

Comments
 (0)