diff --git a/scripts/api-sync/__tests__/apply.test.ts b/scripts/api-sync/__tests__/apply.test.ts index a1f6237..59302e6 100644 --- a/scripts/api-sync/__tests__/apply.test.ts +++ b/scripts/api-sync/__tests__/apply.test.ts @@ -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`. 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 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 = {') + expect(fnRegion).toContain('if (options.refundWalletAddress !== undefined) body.refund_wallet_address = options.refundWalletAddress') + }) + + test('leaves an already-typed "const body: Record =" 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 = Record') + }) }) describe('applyIndexOption', () => { diff --git a/scripts/api-sync/apply.ts b/scripts/api-sync/apply.ts index 8ab09c4..b4d4b56 100644 --- a/scripts/api-sync/apply.ts +++ b/scripts/api-sync/apply.ts @@ -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 = ') + const patchedFn = lines.join('\n') return src.slice(0, declIdx) + patchedFn + src.slice(fnEnd) }