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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions scripts/api-sync/__tests__/apply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ describe('applyResourcesField', () => {
expect(patched).toContain('externalId?: string')
expect(patched).toContain('if (options.externalId !== undefined) body.external_id = options.externalId')
})

// Regression: createQuote's body starts as a plain object literal
// (`const body = {...}`), not `Record<string, any>`. Assigning a new key
// to it after the fact doesn't typecheck (TS2339), which is exactly what
// broke CI when this generator added refund_wallet_address to it.
test('widens an untyped "const body = {" to Record<string, any> so the new field assignment typechecks', () => {
const change: ApplicableChange = { resource: 'quotes', op: 'create', fn: 'createQuote', field: 'refund_wallet_address', type: 'string', required: false }
const patched = applyResourcesField(real('src/commands/resources.ts'), change)
const fnStart = patched.indexOf('export async function createQuote(')
const fnRegion = patched.slice(fnStart, patched.indexOf('export async function', fnStart + 1))
expect(fnRegion).toContain('const body: Record<string, any> = {')
expect(fnRegion).toContain('if (options.refundWalletAddress !== undefined) body.refund_wallet_address = options.refundWalletAddress')
})

test('leaves an already-typed "const body: Record<string, any> =" untouched', () => {
const change: ApplicableChange = { resource: 'bank_accounts', op: 'create', fn: 'createBankAccount', field: 'clabe', type: 'string', required: false }
const patched = applyResourcesField(real('src/commands/resources.ts'), change)
const fnStart = patched.indexOf('export async function createBankAccount(')
const fnRegion = patched.slice(fnStart, patched.indexOf('export async function', fnStart + 1))
expect(fnRegion).not.toContain('Record<string, any> = Record<string, any>')
})
})

describe('applyIndexOption', () => {
Expand Down
9 changes: 9 additions & 0 deletions scripts/api-sync/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ export function applyResourcesField(src: string, change: ApplicableChange): stri
const requestIndent = leadingWhitespace(lines[requestIdx])
lines.splice(requestIdx, 0, `${requestIndent}if (options.${fieldCamel} !== undefined) body.${change.field} = options.${fieldCamel}`)

// The pass-through line above assigns a key that isn't in body's initial
// literal, so body needs a permissive type or TS rejects the assignment
// (an untyped `const body = {...}` infers a closed object type). Widen it
// the same way hand-written conditional-field functions already do.
const bodyDeclRe = /^(\s*)const body = /
const bodyDeclIdx = lines.findIndex(l => bodyDeclRe.test(l))
if (bodyDeclIdx !== -1)
lines[bodyDeclIdx] = lines[bodyDeclIdx].replace('const body = ', 'const body: Record<string, any> = ')

const patchedFn = lines.join('\n')
return src.slice(0, declIdx) + patchedFn + src.slice(fnEnd)
}
Expand Down
Loading