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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 20 additions & 3 deletions src/lib/modules/custom-fields/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface CustomField {
type: CustomFieldType;
}

export const CustomFieldSchema: z.ZodType<CustomField> = z.object({
const customFieldShape = {
entityType: CustomFieldEntityTypeSchema,
id: z.string(),
key: z.string(),
Expand All @@ -51,6 +51,23 @@ export const CustomFieldSchema: z.ZodType<CustomField> = z.object({
options: z.array(CustomFieldOptionSchema).optional(),
order: z.number(),
type: CustomFieldTypeSchema,
};

export const CustomFieldSchema: z.ZodType<CustomField> = 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<CustomField, "object"> {
object?: "customField";
}

export const CreatedCustomFieldSchema: z.ZodType<CreatedCustomField> = z.object({
...customFieldShape,
object: z.literal("customField").optional(),
});

// ─── Custom field value types ─────────────────────────────────────────────────
Expand Down Expand Up @@ -125,9 +142,9 @@ export const CustomFieldsCreateRequestSchema: z.ZodType<CustomFieldsCreateReques
});

export interface CustomFieldsCreateResponse {
customFields: CustomField[];
customFields: CreatedCustomField[];
}

export const CustomFieldsCreateResponseSchema: z.ZodType<CustomFieldsCreateResponse> = z.object({
customFields: z.array(CustomFieldSchema).transform((v) => v || []),
customFields: z.array(CreatedCustomFieldSchema).transform((v) => v || []),
});
52 changes: 52 additions & 0 deletions tests/schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CustomFieldEntityTypeSchema,
CustomFieldSchema,
CustomFieldTypeSchema,
CustomFieldsCreateResponseSchema,
InternalUserSchema,
ListCustomFieldResponseSchema,
NotificationCreateRequestSchema,
Expand Down Expand Up @@ -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 ─────────────────────────────────────────────────────────
Expand Down
Loading