fix(twenty-partners): coerce null fields in TFT opportunity import (#22017)
## What The TFT `HTTP Request` action POSTs `null` for empty fields (e.g. `amountMicros:null`, `closeDate:null`). The import schema typed those as `z.number()/z.string().optional()`, which reject `null` (it is not `undefined`), so the endpoint returned `ok:false / invalid_input` before any API call. ## Fix A `dropNulls` preprocessor on the request schema converts `null` (top-level or nested) to "field absent" before validation. Null optional fields are simply omitted from the created opportunity; required `name` still fails correctly if null. No schema-shape or behaviour-contract change. ## Tests Added a case feeding the failing payload shape (`amountMicros:null`, `closeDate:null`) → `created:true` with `amount`/`closeDate` omitted. 42/42 unit pass, lint clean. Patch bump `1.1.1 → 1.1.2`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22017?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-partners",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
|
||||
+24
@@ -95,4 +95,28 @@ describe('import-opportunity-from-tft handler', () => {
|
||||
pointOfContactId: 'person-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('drops null amountMicros/closeDate instead of failing validation', async () => {
|
||||
queryMock
|
||||
.mockResolvedValueOnce({ opportunities: { edges: [] } })
|
||||
.mockResolvedValueOnce({ companies: { edges: [] } })
|
||||
.mockResolvedValueOnce({ people: { edges: [] } });
|
||||
mutationMock
|
||||
.mockResolvedValueOnce({ createCompany: { id: 'company-1' } })
|
||||
.mockResolvedValueOnce({ createPerson: { id: 'person-1' } })
|
||||
.mockResolvedValueOnce({ createOpportunity: { id: 'opp-1' } });
|
||||
|
||||
const body = { ...baseInput(), amountMicros: null, closeDate: null };
|
||||
const result = await handler({
|
||||
body,
|
||||
headers: { 'x-application-secret': SECRET },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true, created: true, id: 'opp-1' });
|
||||
const data = mutationMock.mock.calls.find(
|
||||
([arg]) => 'createOpportunity' in arg,
|
||||
)?.[0].createOpportunity.__args.data;
|
||||
expect(data).not.toHaveProperty('amount');
|
||||
expect(data).not.toHaveProperty('closeDate');
|
||||
});
|
||||
});
|
||||
|
||||
+14
-2
@@ -12,8 +12,20 @@ export const IMPORT_OPPORTUNITY_FROM_TFT_LOGIC_FUNCTION_ID =
|
||||
|
||||
const APPLICATION_SECRET_HEADER = 'x-application-secret';
|
||||
|
||||
// TFT POSTs `null` for empty fields; treat null as "field absent".
|
||||
const dropNulls = (value: unknown): unknown =>
|
||||
value === null
|
||||
? undefined
|
||||
: Array.isArray(value)
|
||||
? value.map(dropNulls)
|
||||
: typeof value === 'object'
|
||||
? Object.fromEntries(
|
||||
Object.entries(value).map(([key, val]) => [key, dropNulls(val)]),
|
||||
)
|
||||
: value;
|
||||
|
||||
// Request contract — the JSON the TFT workflow POSTs.
|
||||
export const importOpportunityFromTftSchema = z.object({
|
||||
export const importOpportunityFromTftSchema = z.preprocess(dropNulls, z.object({
|
||||
tftOpportunityId: z.string().optional(),
|
||||
name: z.string().trim().min(1),
|
||||
amountMicros: z.number().optional(),
|
||||
@@ -33,7 +45,7 @@ export const importOpportunityFromTftSchema = z.object({
|
||||
lastName: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
}));
|
||||
|
||||
export type ImportOpportunityFromTftInput = z.infer<
|
||||
typeof importOpportunityFromTftSchema
|
||||
|
||||
Reference in New Issue
Block a user