Skip to content

Commit aaa21fa

Browse files
committed
don't use sdk
1 parent 33fd93c commit aaa21fa

8 files changed

Lines changed: 75 additions & 297 deletions

File tree

packages/openapi/__tests__/listFnResolver.test.ts

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
1-
import { describe, expect, it, vi } from 'vitest'
2-
import { discoverListEndpoints, buildListFn } from '../listFnResolver'
1+
import { describe, expect, it } from 'vitest'
2+
import { discoverListEndpoints } from '../listFnResolver'
33
import { minimalStripeOpenApiSpec } from './fixtures/minimalSpec'
4-
import type Stripe from 'stripe'
5-
6-
function mockStripe() {
7-
const listFn = vi.fn().mockResolvedValue({ data: [], has_more: false })
8-
return {
9-
customers: { list: listFn },
10-
plans: { list: listFn },
11-
products: { list: listFn },
12-
subscriptionItems: { list: listFn },
13-
checkout: { sessions: { list: listFn } },
14-
radar: { earlyFraudWarnings: { list: listFn } },
15-
entitlements: {
16-
activeEntitlements: { list: listFn },
17-
features: { list: listFn },
18-
},
19-
_listFn: listFn,
20-
}
21-
}
224

235
describe('discoverListEndpoints', () => {
246
it('maps table names to their API paths', () => {
@@ -106,39 +88,3 @@ describe('discoverListEndpoints', () => {
10688
expect(endpoints.size).toBe(0)
10789
})
10890
})
109-
110-
describe('buildListFn', () => {
111-
it('resolves a simple top-level path', async () => {
112-
const mock = mockStripe()
113-
const listFn = buildListFn(mock as unknown as Stripe, '/v1/customers')
114-
await listFn({ limit: 10 })
115-
expect(mock._listFn).toHaveBeenCalledWith({ limit: 10 })
116-
})
117-
118-
it('resolves a nested namespace path', async () => {
119-
const mock = mockStripe()
120-
const listFn = buildListFn(mock as unknown as Stripe, '/v1/checkout/sessions')
121-
await listFn({ limit: 5 })
122-
expect(mock._listFn).toHaveBeenCalledWith({ limit: 5 })
123-
})
124-
125-
it('converts snake_case segments to camelCase', async () => {
126-
const mock = mockStripe()
127-
const listFn = buildListFn(mock as unknown as Stripe, '/v1/subscription_items')
128-
await listFn({ limit: 1 })
129-
expect(mock._listFn).toHaveBeenCalled()
130-
})
131-
132-
it('resolves deeply nested snake_case paths', async () => {
133-
const mock = mockStripe()
134-
const listFn = buildListFn(mock as unknown as Stripe, '/v1/radar/early_fraud_warnings')
135-
await listFn({ limit: 1 })
136-
expect(mock._listFn).toHaveBeenCalled()
137-
})
138-
139-
it('throws when a path segment does not exist on the SDK', async () => {
140-
const mock = mockStripe()
141-
const listFn = buildListFn(mock as unknown as Stripe, '/v1/nonexistent_resource')
142-
await expect(() => listFn({ limit: 1 })).toThrow(/Stripe SDK has no property/)
143-
})
144-
})

packages/openapi/__tests__/writePathPlanner.test.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

packages/openapi/index.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
export type * from './types'
2-
export { SpecParser, OPENAPI_RESOURCE_TABLE_ALIASES, RUNTIME_RESOURCE_ALIASES } from './specParser'
2+
export { SpecParser, OPENAPI_RESOURCE_TABLE_ALIASES } from './specParser'
33
export { OPENAPI_COMPATIBILITY_COLUMNS } from './runtimeMappings'
4-
export { WritePathPlanner } from './writePathPlanner'
54
export { resolveOpenApiSpec } from './specFetchHelper'
65
export {
7-
buildListFn,
8-
buildRetrieveFn,
9-
buildV2ListFn,
10-
buildV2RetrieveFn,
116
discoverListEndpoints,
127
discoverNestedEndpoints,
13-
canResolveSdkResource,
148
isV2Path,
9+
buildListFn,
10+
buildRetrieveFn,
1511
} from './listFnResolver'
16-
export type { NestedEndpoint } from './listFnResolver'
12+
export type { ListEndpoint, NestedEndpoint, ListFn, RetrieveFn, ListParams } from './listFnResolver'
1713
export { parsedTableToJsonSchema } from './jsonSchemaConverter'
1814
export { RUNTIME_REQUIRED_TABLES } from './runtimeMappings'

packages/openapi/listFnResolver.ts

Lines changed: 68 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import type Stripe from 'stripe'
21
import type { OpenApiSchemaObject, OpenApiSpec } from './types'
32
import { OPENAPI_RESOURCE_TABLE_ALIASES } from './runtimeMappings'
43

54
const SCHEMA_REF_PREFIX = '#/components/schemas/'
65

7-
type ListFn = (
8-
params: Stripe.PaginationParams & { created?: Stripe.RangeQueryParam }
9-
) => Promise<{ data: unknown[]; has_more: boolean; pageCursor?: string }>
6+
export type ListParams = {
7+
limit?: number
8+
starting_after?: string
9+
ending_before?: string
10+
created?: { gt?: number; gte?: number; lt?: number; lte?: number }
11+
}
12+
13+
export type ListResult = { data: unknown[]; has_more: boolean; pageCursor?: string }
14+
15+
export type ListFn = (params: ListParams) => Promise<ListResult>
16+
17+
export type RetrieveFn = (id: string) => Promise<unknown>
1018

1119
export type ListEndpoint = {
1220
tableName: string
@@ -25,10 +33,6 @@ export type NestedEndpoint = {
2533
supportsPagination: boolean
2634
}
2735

28-
function snakeToCamel(s: string): string {
29-
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
30-
}
31-
3236
function resolveTableName(resourceId: string, aliases: Record<string, string>): string {
3337
const alias = aliases[resourceId]
3438
if (alias) return alias
@@ -197,161 +201,86 @@ export function isV2Path(apiPath: string): boolean {
197201
return apiPath.startsWith('/v2/')
198202
}
199203

200-
function pathToSdkSegments(apiPath: string): string[] {
201-
if (isV2Path(apiPath)) {
202-
return [
203-
'v2',
204-
...apiPath
205-
.replace(/^\/v2\//, '')
206-
.split('/')
207-
.filter((s) => !s.startsWith('{'))
208-
.map(snakeToCamel),
209-
]
210-
}
211-
return apiPath
212-
.replace(/^\/v[12]\//, '')
213-
.split('/')
214-
.filter((s) => !s.startsWith('{'))
215-
.map(snakeToCamel)
216-
}
204+
// ---------------------------------------------------------------------------
205+
// HTTP-based list / retrieve builders (no Stripe SDK dependency)
206+
// ---------------------------------------------------------------------------
217207

218-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
219-
function resolveStripeResource(stripe: Stripe, segments: string[], apiPath: string): any {
220-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
221-
let resource: any = stripe
222-
for (const segment of segments) {
223-
resource = resource?.[segment]
224-
if (!resource) {
225-
throw new Error(`Stripe SDK has no property "${segment}" when resolving path "${apiPath}"`)
226-
}
227-
}
228-
return resource
208+
const STRIPE_API_BASE = 'https://api.stripe.com'
209+
const V2_STRIPE_VERSION = '2026-02-25.clover'
210+
211+
function authHeaders(apiKey: string): Record<string, string> {
212+
return { Authorization: `Bearer ${apiKey}` }
229213
}
230214

231215
/**
232-
* Check whether an API path can be resolved to a Stripe SDK resource.
233-
* v1 requires both `.list()` and `.retrieve()`.
234-
* v2 only requires `.list()` (retrieve may not be available on all v2 resources).
216+
* Build a callable list function that hits the Stripe HTTP API directly.
217+
* Supports both v1 (has_more pagination) and v2 (next_page_url pagination).
235218
*/
236-
export function canResolveSdkResource(stripe: Stripe, apiPath: string): boolean {
237-
try {
238-
const segments = pathToSdkSegments(apiPath)
239-
const resource = resolveStripeResource(stripe, segments, apiPath)
240-
if (isV2Path(apiPath)) {
241-
return typeof resource.list === 'function'
219+
export function buildListFn(apiKey: string, apiPath: string): ListFn {
220+
if (isV2Path(apiPath)) {
221+
return async (params) => {
222+
const qs = new URLSearchParams()
223+
qs.set('limit', String(Math.min(params.limit ?? 20, 20)))
224+
if (params.starting_after) qs.set('page', params.starting_after)
225+
226+
const response = await fetch(`${STRIPE_API_BASE}${apiPath}?${qs}`, {
227+
headers: { ...authHeaders(apiKey), 'Stripe-Version': V2_STRIPE_VERSION },
228+
})
229+
const body = (await response.json()) as {
230+
data: unknown[]
231+
next_page_url?: string | null
232+
}
233+
const pageCursor = extractPageToken(body.next_page_url)
234+
return { data: body.data ?? [], has_more: !!body.next_page_url, pageCursor }
242235
}
243-
return typeof resource.list === 'function' && typeof resource.retrieve === 'function'
244-
} catch {
245-
return false
246236
}
247-
}
248237

249-
/**
250-
* Build a callable list function by navigating the Stripe SDK object using
251-
* the API path segments converted from snake_case to camelCase.
252-
* Path parameters (e.g. `{customer}`) are stripped automatically.
253-
*/
254-
export function buildListFn(stripe: Stripe, apiPath: string, apiKey: string = ''): ListFn {
255-
const v2 = isV2Path(apiPath)
256-
if (v2) {
257-
return buildV2ListFn(apiKey, apiPath)
258-
}
259-
const segments = pathToSdkSegments(apiPath)
260-
return (params) => {
261-
const resource = resolveStripeResource(stripe, segments, apiPath)
262-
if (typeof resource.list !== 'function') {
263-
throw new Error(`Stripe SDK resource at "${apiPath}" has no list() method`)
238+
return async (params) => {
239+
const qs = new URLSearchParams()
240+
if (params.limit != null) qs.set('limit', String(params.limit))
241+
if (params.starting_after) qs.set('starting_after', params.starting_after)
242+
if (params.ending_before) qs.set('ending_before', params.ending_before)
243+
if (params.created) {
244+
for (const [op, val] of Object.entries(params.created)) {
245+
if (val != null) qs.set(`created[${op}]`, String(val))
246+
}
264247
}
265-
return resource.list(params)
248+
249+
const response = await fetch(`${STRIPE_API_BASE}${apiPath}?${qs}`, {
250+
headers: authHeaders(apiKey),
251+
})
252+
const body = (await response.json()) as { data: unknown[]; has_more: boolean }
253+
return { data: body.data ?? [], has_more: body.has_more }
266254
}
267255
}
268256

269-
type RetrieveFn = (id: string) => Promise<Stripe.Response<unknown>>
270-
271257
/**
272-
* Build a callable retrieve function by navigating the Stripe SDK object using
273-
* the API path segments converted from snake_case to camelCase.
274-
* Path parameters (e.g. `{customer}`) are stripped automatically.
258+
* Build a callable retrieve function that hits the Stripe HTTP API directly.
275259
*/
276-
export function buildRetrieveFn(stripe: Stripe, apiPath: string, apiKey: string): RetrieveFn {
277-
const v2 = isV2Path(apiPath)
278-
if (v2) {
279-
return buildV2RetrieveFn(apiKey, apiPath)
280-
}
281-
const segments = pathToSdkSegments(apiPath)
282-
return (id: string) => {
283-
const resource = resolveStripeResource(stripe, segments, apiPath)
284-
if (typeof resource.retrieve !== 'function') {
285-
throw new Error(`Stripe SDK resource at "${apiPath}" has no retrieve() method`)
260+
export function buildRetrieveFn(apiKey: string, apiPath: string): RetrieveFn {
261+
if (isV2Path(apiPath)) {
262+
return async (id) => {
263+
const response = await fetch(`${STRIPE_API_BASE}${apiPath}/${id}`, {
264+
headers: { ...authHeaders(apiKey), 'Stripe-Version': V2_STRIPE_VERSION },
265+
})
266+
return await response.json()
286267
}
287-
return resource.retrieve(id)
288268
}
289-
}
290269

291-
/**
292-
* Build a list function that calls Stripe rawRequest directly for a fixed endpoint.
293-
* Useful when the Stripe SDK does not expose a matching namespace.
294-
*/
295-
export function buildRawRequestListFn(stripe: Stripe, apiPath: string): ListFn {
296-
return (params) =>
297-
stripe.rawRequest('GET', apiPath, { ...params }) as unknown as Promise<{
298-
data: unknown[]
299-
has_more: boolean
300-
}>
270+
return async (id) => {
271+
const response = await fetch(`${STRIPE_API_BASE}${apiPath}/${id}`, {
272+
headers: authHeaders(apiKey),
273+
})
274+
return await response.json()
275+
}
301276
}
302277

303278
function extractPageToken(nextPageUrl: string | null | undefined): string | undefined {
304279
if (!nextPageUrl) return undefined
305280
try {
306-
const url = new URL(nextPageUrl, 'https://api.stripe.com')
281+
const url = new URL(nextPageUrl, STRIPE_API_BASE)
307282
return url.searchParams.get('page') ?? undefined
308283
} catch {
309284
return undefined
310285
}
311286
}
312-
313-
/**
314-
* Build a list function for v2 API endpoints.
315-
* V2 uses `page` token pagination and returns `next_page_url` instead of `has_more`.
316-
* The response is normalized to the v1 shape so the sync worker can process it uniformly.
317-
*/
318-
export function buildV2ListFn(apiKey: string, apiPath: string): ListFn {
319-
return async (params) => {
320-
const qs = new URLSearchParams()
321-
qs.set('limit', String(Math.min(params.limit ?? 20, 20)))
322-
if (params.starting_after) qs.set('page', params.starting_after)
323-
const url = `https://api.stripe.com${apiPath}?${qs.toString()}`
324-
325-
const response = await fetch(url, {
326-
headers: {
327-
Authorization: `Bearer ${apiKey}`,
328-
'Stripe-Version': '2026-02-25.clover',
329-
},
330-
})
331-
332-
const raw = await response.text()
333-
334-
const body = JSON.parse(raw) as {
335-
data: unknown[]
336-
next_page_url?: string | null
337-
}
338-
const nextToken = extractPageToken(body.next_page_url)
339-
return {
340-
data: body.data ?? [],
341-
has_more: !!body.next_page_url,
342-
pageCursor: nextToken,
343-
}
344-
}
345-
}
346-
347-
export function buildV2RetrieveFn(apiKey: string, apiPath: string): RetrieveFn {
348-
return async (id: string) => {
349-
const response = await fetch(`https://api.stripe.com${apiPath}/${id}`, {
350-
headers: {
351-
Authorization: `Bearer ${apiKey}`,
352-
'Stripe-Version': '2026-02-25.clover',
353-
},
354-
})
355-
return (await response.json()) as Stripe.Response<unknown>
356-
}
357-
}

packages/openapi/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
"files": [
1919
"dist"
2020
],
21-
"dependencies": {
22-
"stripe": "^17.7.0"
23-
},
21+
"dependencies": {},
2422
"devDependencies": {
2523
"@types/node": "^24.5.0",
2624
"vitest": "^3.2.4"

0 commit comments

Comments
 (0)