From 1f4c9b3debace12a1c911f4037f80b4beede4bd5 Mon Sep 17 00:00:00 2001 From: priosshrsth Date: Mon, 24 Aug 2026 07:18:33 +0000 Subject: [PATCH] fix: accept a custom-field create response with no object field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /v1/custom-fields omits `object` from the fields it echoes back, though GET includes it and the API reference documents it on both. CustomFieldSchema requires it, so a successful create threw AssemblyResponseParseError *after* the field existed — and the API has no delete for custom fields, so retrying left an undeletable duplicate behind. The create response now parses through CreatedCustomFieldSchema, where `object` is optional. CustomFieldSchema keeps requiring it so list responses don't lose the guarantee. Revert both once the API sends the field. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 5 +++ src/lib/modules/custom-fields/schema.ts | 23 +++++++++-- tests/schemas.test.ts | 52 +++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc4804b..e89a4b0 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,11 @@ import type { } from "assembly-kit/schemas"; ``` +`customFields.create()` resolves to `CreatedCustomField[]`, not `CustomField[]`: `POST /v1/custom-fields` +omits `object` from the fields it echoes back, where `GET` includes it. Narrow it yourself if you need +the discriminant, or re-read the field through `customFields.list()`, which is authoritative for the +derived `key` anyway. + #### Response schemas ```typescript diff --git a/src/lib/modules/custom-fields/schema.ts b/src/lib/modules/custom-fields/schema.ts index 4c2dae7..099c92e 100644 --- a/src/lib/modules/custom-fields/schema.ts +++ b/src/lib/modules/custom-fields/schema.ts @@ -42,7 +42,7 @@ export interface CustomField { type: CustomFieldType; } -export const CustomFieldSchema: z.ZodType = z.object({ +const customFieldShape = { entityType: CustomFieldEntityTypeSchema, id: z.string(), key: z.string(), @@ -51,6 +51,23 @@ export const CustomFieldSchema: z.ZodType = z.object({ options: z.array(CustomFieldOptionSchema).optional(), order: z.number(), type: CustomFieldTypeSchema, +}; + +export const CustomFieldSchema: z.ZodType = z.object(customFieldShape); + +/** + * `POST /v1/custom-fields` omits `object` from the fields it echoes back, though `GET` includes it + * and the API reference documents it on both. Requiring it made a successful create throw + * `AssemblyResponseParseError` after the field had been created — and a custom field cannot be + * deleted through the API, so a retry left a duplicate behind. Drop this once the API sends it. + */ +export interface CreatedCustomField extends Omit { + object?: "customField"; +} + +export const CreatedCustomFieldSchema: z.ZodType = z.object({ + ...customFieldShape, + object: z.literal("customField").optional(), }); // ─── Custom field value types ───────────────────────────────────────────────── @@ -125,9 +142,9 @@ export const CustomFieldsCreateRequestSchema: z.ZodType = z.object({ - customFields: z.array(CustomFieldSchema).transform((v) => v || []), + customFields: z.array(CreatedCustomFieldSchema).transform((v) => v || []), }); diff --git a/tests/schemas.test.ts b/tests/schemas.test.ts index 2e73212..ab264f3 100644 --- a/tests/schemas.test.ts +++ b/tests/schemas.test.ts @@ -9,6 +9,7 @@ import { CustomFieldEntityTypeSchema, CustomFieldSchema, CustomFieldTypeSchema, + CustomFieldsCreateResponseSchema, InternalUserSchema, ListCustomFieldResponseSchema, NotificationCreateRequestSchema, @@ -303,6 +304,57 @@ describe("CustomFieldSchema", () => { }); expect(result.success).toBe(false); }); + + it("still requires object, since GET sends it", () => { + const { object, ...withoutObject } = valid; + expect(object).toBe("customField"); + expect(CustomFieldSchema.safeParse(withoutObject).success).toBe(false); + }); +}); + +// ─── CustomFieldsCreateResponseSchema ───────────────────────────────────────── + +describe("CustomFieldsCreateResponseSchema", () => { + /** Verbatim from POST /v1/custom-fields, which omits `object` on the field and its options. */ + const createResponse = { + customFields: [ + { + entityType: "company", + id: "388ac3ed-dbb3-45c4-823d-3f847a35f706", + key: "propertyType", + name: "Property Type", + options: [ + { + color: "rgba(144, 149, 157, 1)", + id: "option-8dfa39d0-3a78-48ea-93c5-02bb33fb5811", + key: "hotel", + label: "Hotel", + }, + ], + order: 3, + type: "multiSelect", + }, + ], + }; + + it("accepts a create response with no object field", () => { + const result = CustomFieldsCreateResponseSchema.safeParse(createResponse); + expect(result.success).toBe(true); + }); + + it("still accepts a create response that does send object", () => { + const withObject = { + customFields: [{ ...createResponse.customFields[0], object: "customField" }], + }; + expect(CustomFieldsCreateResponseSchema.safeParse(withObject).success).toBe(true); + }); + + it("rejects a wrong object literal", () => { + const wrong = { + customFields: [{ ...createResponse.customFields[0], object: "customFields" }], + }; + expect(CustomFieldsCreateResponseSchema.safeParse(wrong).success).toBe(false); + }); }); // ─── TaskStatusSchema ─────────────────────────────────────────────────────────