Migrate from Zod v3 to v4 (#14639)

Closes [#1526](https://github.com/twentyhq/core-team-issues/issues/1526)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Abdul Rahman
2025-09-24 18:29:05 +02:00
committed by GitHub
co-authored by Félix Malfait Félix Malfait
parent 8d78032357
commit 3cada58908
98 changed files with 809 additions and 1075 deletions
@@ -17,7 +17,10 @@ const makeValidationSchema = (signInUpStep: SignInUpStep) =>
z
.object({
exist: z.boolean(),
email: z.string().trim().email('Email must be a valid email'),
email: z
.string()
.trim()
.pipe(z.email({ error: 'Email must be a valid email' })),
password:
signInUpStep === SignInUpStep.Password
? z
@@ -1,22 +1,21 @@
import { z } from 'zod';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metadataLabelSchema';
import { themeColorSchema } from 'twenty-ui/theme';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { FieldMetadataType, RelationType } from '~/generated/graphql';
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
return z.object({
__typename: z.literal('Field').optional(),
createdAt: z.string().datetime(),
createdAt: z.iso.datetime(),
defaultValue: z.any().optional(),
description: z.string().trim().nullable().optional(),
icon: z
.union([z.string().startsWith('Icon').trim(), z.literal('')])
.nullable()
.optional(),
id: z.string().uuid(),
id: z.uuid(),
isActive: z.boolean(),
isCustom: z.boolean(),
isNullable: z.boolean(),
@@ -30,7 +29,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
.array(
z.object({
color: themeColorSchema,
id: z.string().uuid(),
id: z.uuid(),
label: z.string().trim().min(1),
position: z.number(),
value: z.string().trim().min(1),
@@ -42,33 +41,33 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
relation: z
.object({
__typename: z.literal('Relation').optional(),
type: z.nativeEnum(RelationType),
type: z.enum(RelationType),
sourceFieldMetadata: z.object({
__typename: z.literal('Field').optional(),
id: z.string().uuid(),
id: z.uuid(),
name: z.string().trim().min(1),
}),
sourceObjectMetadata: z.object({
__typename: z.literal('Object').optional(),
id: z.string().uuid(),
id: z.uuid(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
targetFieldMetadata: z.object({
__typename: z.literal('Field').optional(),
id: z.string().uuid(),
id: z.uuid(),
name: z.string().trim().min(1),
}),
targetObjectMetadata: z.object({
__typename: z.literal('Object').optional(),
id: z.string().uuid(),
id: z.uuid(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
})
.nullable()
.optional(),
type: z.nativeEnum(FieldMetadataType),
updatedAt: z.string().datetime(),
}) satisfies z.ZodType<FieldMetadataItem>;
type: z.enum(FieldMetadataType),
updatedAt: z.iso.datetime(),
});
};
@@ -4,7 +4,7 @@ import { type IndexFieldMetadataItem } from '@/object-metadata/types/IndexFieldM
export const indexFieldMetadataItemSchema = z.object({
__typename: z.literal('IndexField'),
fieldMetadataId: z.string().uuid(),
fieldMetadataId: z.uuid(),
id: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
@@ -2,16 +2,16 @@ import { z } from 'zod';
import { type IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
import { indexFieldMetadataItemSchema } from '@/object-metadata/validation-schemas/indexFieldMetadataItemSchema';
import { IndexType } from '~/generated-metadata/graphql';
import { IndexType } from '~/generated/graphql';
export const indexMetadataItemSchema = z.object({
__typename: z.literal('Index'),
id: z.string().uuid(),
id: z.uuid(),
name: z.string(),
indexFieldMetadatas: z.array(indexFieldMetadataItemSchema),
createdAt: z.string(),
updatedAt: z.string(),
indexType: z.nativeEnum(IndexType),
indexType: z.enum(IndexType),
indexWhereClause: z.string().nullable(),
isUnique: z.boolean(),
objectMetadata: z.any(),
@@ -1,6 +1,5 @@
import { z } from 'zod';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
import { indexMetadataItemSchema } from '@/object-metadata/validation-schemas/indexMetadataItemSchema';
import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metadataLabelSchema';
@@ -8,28 +7,28 @@ import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStrin
export const objectMetadataItemSchema = z.object({
__typename: z.literal('Object').optional(),
createdAt: z.string().datetime(),
createdAt: z.iso.datetime(),
description: z.string().trim().nullable().optional(),
fields: z.array(fieldMetadataItemSchema()),
readableFields: z.array(fieldMetadataItemSchema()),
updatableFields: z.array(fieldMetadataItemSchema()),
indexMetadatas: z.array(indexMetadataItemSchema),
icon: z.string().startsWith('Icon').trim(),
id: z.string().uuid(),
id: z.uuid(),
duplicateCriteria: z.array(z.array(z.string())),
imageIdentifierFieldMetadataId: z.string().uuid().nullable(),
imageIdentifierFieldMetadataId: z.uuid().nullable(),
isActive: z.boolean(),
isCustom: z.boolean(),
isRemote: z.boolean(),
isSystem: z.boolean(),
isUIReadOnly: z.boolean(),
isSearchable: z.boolean(),
labelIdentifierFieldMetadataId: z.string().uuid(),
labelIdentifierFieldMetadataId: z.uuid(),
labelPlural: metadataLabelSchema(),
labelSingular: metadataLabelSchema(),
namePlural: camelCaseStringSchema,
nameSingular: camelCaseStringSchema,
updatedAt: z.string().datetime(),
updatedAt: z.iso.datetime(),
shortcut: z.string().nullable().optional(),
isLabelSyncedWithName: z.boolean(),
}) satisfies z.ZodType<ObjectMetadataItem>;
});
@@ -1,6 +1,5 @@
import { z } from 'zod';
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { themeColorSchema } from 'twenty-ui/theme';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/computeOptionValueFromLabel';
@@ -22,9 +21,9 @@ const selectOptionSchema = z
}
},
{
message: 'Label is not transliterable',
error: 'Label is not transliterable',
},
) satisfies z.ZodType<FieldMetadataItemOption>;
);
export const selectOptionsSchema = z
.array(selectOptionSchema)
@@ -35,7 +34,7 @@ export const selectOptionsSchema = z
return new Set(optionIds).size === options.length;
},
{
message: 'Options must have unique ids',
error: 'Options must have unique ids',
},
)
.refine(
@@ -44,13 +43,13 @@ export const selectOptionsSchema = z
return new Set(optionValues).size === options.length;
},
{
message: 'Options must have unique values',
error: 'Options must have unique values',
},
)
.refine(
(options) =>
[...options].sort().every((option, index) => option.position === index),
{
message: 'Options positions must be sequential',
error: 'Options positions must be sequential',
},
);
@@ -1,6 +1,6 @@
import { type WorkflowRunState } from '@/workflow/types/Workflow';
import { workflowRunStateSchema } from '@/workflow/validation-schemas/workflowSchema';
import { isDefined } from 'twenty-shared/utils';
import { workflowRunStateSchema } from 'twenty-shared/workflow';
import { type JsonValue } from 'type-fest';
export const orderWorkflowRunState = (value: JsonValue) => {
@@ -290,7 +290,7 @@ export const FieldActorValueSchema = z.object({
name: z.string(),
context: z
.object({
provider: z.nativeEnum(ConnectedAccountProvider).optional(),
provider: z.enum(ConnectedAccountProvider).optional(),
})
.nullable(),
});
@@ -4,7 +4,7 @@ import { CurrencyCode } from '../CurrencyCode';
import { type FieldCurrencyValue } from '../FieldMetadata';
const currencySchema = z.object({
currencyCode: z.nativeEnum(CurrencyCode).nullable(),
currencyCode: z.enum(CurrencyCode).nullable(),
amountMicros: z.number().nullable(),
});
@@ -5,13 +5,17 @@ import { type FieldJsonValue, type Json } from '../FieldMetadata';
// See https://zod.dev/?id=json-type
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]),
z.union([
literalSchema,
z.array(jsonSchema),
z.record(z.string(), jsonSchema),
]),
);
export const jsonWithoutLiteralsSchema: z.ZodType<FieldJsonValue> = z.union([
z.null(), // Exclude literal values other than null
z.array(jsonSchema),
z.record(jsonSchema),
z.record(z.string(), jsonSchema),
]);
export const isFieldRawJsonValue = (
@@ -2,4 +2,4 @@ import { z } from 'zod';
import { CurrencyCode } from '@/object-record/record-field/ui/types/CurrencyCode';
export const currencyCodeSchema = z.nativeEnum(CurrencyCode);
export const currencyCodeSchema = z.enum(CurrencyCode);
@@ -10,6 +10,8 @@ export const currencyFieldDefaultValueSchema = z.object({
currencyCode: simpleQuotesStringSchema.refine(
(value): value is `'${CurrencyCode}'` =>
currencyCodeSchema.safeParse(stripSimpleQuotesFromString(value)).success,
{ message: 'String is not a valid currencyCode' },
{
error: 'String is not a valid currencyCode',
},
),
});
@@ -1,3 +1,3 @@
import { z } from 'zod';
export const emailSchema = z.string().email();
export const emailSchema = z.email();
@@ -41,7 +41,7 @@ export const SettingsAccountsBlocklistInput = ({
emailOrDomain: z
.string()
.trim()
.email(t`Invalid email or domain`)
.pipe(z.email({ error: t`Invalid email or domain` }))
.or(
z.string().refine(
(value) =>
@@ -5,7 +5,7 @@ import { type ConnectionParameters } from '~/generated/graphql';
const connectionParameters = z
.object({
host: z.string().default(''),
port: z.number().int().nullable().default(null),
port: z.int().nullable().default(null),
username: z.string().optional(),
password: z.string().default(''),
secure: z.boolean().default(true),
@@ -18,14 +18,14 @@ const connectionParameters = z
return true;
},
{
message: 'Port must be a positive number when configuring this protocol',
path: ['port'],
error: 'Port must be a positive number when configuring this protocol',
},
);
export const connectionImapSmtpCalDav = z
.object({
handle: z.string().email('Invalid email address'),
handle: z.email('Invalid email address'),
IMAP: connectionParameters.optional(),
SMTP: connectionParameters.optional(),
CALDAV: connectionParameters.optional(),
@@ -37,9 +37,9 @@ export const connectionImapSmtpCalDav = z
);
},
{
message:
'At least one account type (IMAP, SMTP, or CalDAV) must be completely configured',
path: ['handle'],
error:
'At least one account type (IMAP, SMTP, or CalDAV) must be completely configured',
},
);
@@ -39,56 +39,56 @@ const isUniqueFieldFormSchema = z.object({
const booleanFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.BOOLEAN) })
.merge(settingsDataModelFieldBooleanFormSchema);
.extend(settingsDataModelFieldBooleanFormSchema.shape);
const currencyFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.CURRENCY) })
.merge(settingsDataModelFieldCurrencyFormSchema);
.extend(settingsDataModelFieldCurrencyFormSchema.shape);
const dateFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.DATE) })
.merge(settingsDataModelFieldDateFormSchema)
.merge(isUniqueFieldFormSchema);
.extend(settingsDataModelFieldDateFormSchema.shape)
.extend(isUniqueFieldFormSchema.shape);
const dateTimeFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.DATE_TIME) })
.merge(settingsDataModelFieldDateFormSchema)
.merge(isUniqueFieldFormSchema);
.extend(settingsDataModelFieldDateFormSchema.shape)
.extend(isUniqueFieldFormSchema.shape);
const relationFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.RELATION) })
.merge(settingsDataModelFieldRelationFormSchema);
.extend(settingsDataModelFieldRelationFormSchema.shape);
const morphRelationFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.MORPH_RELATION) })
.merge(settingsDataModelFieldMorphRelationFormSchema);
.extend(settingsDataModelFieldMorphRelationFormSchema.shape);
const selectFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.SELECT) })
.merge(settingsDataModelFieldSelectFormSchema);
.extend(settingsDataModelFieldSelectFormSchema.shape);
const multiSelectFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.MULTI_SELECT) })
.merge(settingsDataModelFieldMultiSelectFormSchema);
.extend(settingsDataModelFieldMultiSelectFormSchema.shape);
const numberFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.NUMBER) })
.merge(settingsDataModelFieldNumberFormSchema)
.merge(isUniqueFieldFormSchema);
.extend(settingsDataModelFieldNumberFormSchema.shape)
.extend(isUniqueFieldFormSchema.shape);
const textFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.TEXT) })
.merge(settingsDataModelFieldTextFormSchema)
.merge(isUniqueFieldFormSchema);
.extend(settingsDataModelFieldTextFormSchema.shape)
.extend(isUniqueFieldFormSchema.shape);
const addressFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.ADDRESS) })
.merge(settingsDataModelFieldAddressFormSchema);
.extend(settingsDataModelFieldAddressFormSchema.shape);
const phonesFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.PHONES) })
.merge(settingsDataModelFieldPhonesFormSchema)
.merge(isUniqueFieldFormSchema);
.extend(settingsDataModelFieldPhonesFormSchema.shape)
.extend(isUniqueFieldFormSchema.shape);
const otherFieldsFormSchema = z
.object({
@@ -111,7 +111,7 @@ const otherFieldsFormSchema = z
) as [FieldMetadataType, ...FieldMetadataType[]],
),
})
.merge(isUniqueFieldFormSchema);
.extend(isUniqueFieldFormSchema.shape);
export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
'type',
@@ -25,7 +25,7 @@ import { isDefined } from 'twenty-shared/utils';
import { RelationType } from '~/generated-metadata/graphql';
export const settingsDataModelFieldMorphRelationFormSchema = z.object({
morphRelationObjectMetadataIds: z.array(z.string().uuid()).min(2),
morphRelationObjectMetadataIds: z.array(z.uuid()).min(2),
relationType: z.enum(
Object.keys(RELATION_TYPES) as [RelationType, ...RelationType[]],
),
@@ -25,15 +25,15 @@ export const settingsDataModelFieldRelationFormSchema = z.object({
label: true,
})
// NOT SURE IF THIS IS CORRECT
.merge(
.extend(
fieldMetadataItemSchema()
.pick({
name: true,
isLabelSyncedWithName: true,
})
.partial(),
.partial().shape,
),
objectMetadataId: z.string().uuid(),
objectMetadataId: z.uuid(),
type: z.enum(
Object.keys(RELATION_TYPES) as [RelationType, ...RelationType[]],
),
@@ -7,8 +7,10 @@ import { settingsDataModelFieldTypeFormSchema } from '~/pages/settings/data-mode
export const settingsFieldFormSchema = (existingOtherLabels?: string[]) => {
return z
.object({})
.merge(settingsDataModelFieldIconLabelFormSchema(existingOtherLabels))
.merge(settingsDataModelFieldDescriptionFormSchema())
.merge(settingsDataModelFieldTypeFormSchema)
.extend(
settingsDataModelFieldIconLabelFormSchema(existingOtherLabels).shape,
)
.extend(settingsDataModelFieldDescriptionFormSchema().shape)
.extend(settingsDataModelFieldTypeFormSchema.shape)
.and(settingsDataModelFieldSettingsFormSchema);
};
@@ -27,8 +27,9 @@ export const getMultiSelectFieldPreviewValue = ({
return multiSelectFieldDefaultValueSchema(fieldMetadataItem.options)
.refine(isDefined)
.transform((value) =>
value.map(stripSimpleQuotesFromString).filter(isNonEmptyString),
.transform(
(value) =>
value?.map(stripSimpleQuotesFromString).filter(isNonEmptyString) ?? [],
)
.refine(isNonEmptyArray)
.catch(allOptionValues)
@@ -26,7 +26,7 @@ export const getSelectFieldPreviewValue = ({
return selectFieldDefaultValueSchema(fieldMetadataItem.options)
.refine(isDefined)
.transform(stripSimpleQuotesFromString)
.transform((value) => stripSimpleQuotesFromString(value ?? ''))
.refine(isNonEmptyString)
.catch(firstOptionValue)
.parse(fieldMetadataItem.defaultValue);
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { ZodError, isDirty, type z } from 'zod';
import { ZodError, type z } from 'zod';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@@ -54,7 +54,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({
const handleSave = async (
formValues: SettingsDataModelObjectIdentifiersFormValues,
) => {
if (!isDirty) {
if (!formConfig.formState.isDirty) {
return;
}
@@ -3,13 +3,12 @@
exports[`settingsDataModelObjectAboutFormSchema fails when isLabelSyncedWithName is not a boolean 1`] = `
[ZodError: [
{
"code": "invalid_type",
"expected": "boolean",
"received": "string",
"code": "invalid_type",
"path": [
"isLabelSyncedWithName"
],
"message": "Expected boolean, received string"
"message": "Invalid input: expected boolean, received string"
}
]]
`;
@@ -17,26 +16,24 @@ exports[`settingsDataModelObjectAboutFormSchema fails when isLabelSyncedWithName
exports[`settingsDataModelObjectAboutFormSchema fails when labels are empty strings 1`] = `
[ZodError: [
{
"origin": "string",
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": true,
"exact": false,
"message": "String must contain at least 1 character(s)",
"path": [
"labelSingular"
]
],
"message": "Too small: expected string to have >=1 characters"
},
{
"origin": "string",
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": true,
"exact": false,
"message": "String must contain at least 1 character(s)",
"path": [
"labelPlural"
]
],
"message": "Too small: expected string to have >=1 characters"
},
{
"code": "custom",
@@ -59,17 +56,17 @@ exports[`settingsDataModelObjectAboutFormSchema fails when names are not in came
[ZodError: [
{
"code": "custom",
"message": "String should be camel case",
"path": [
"namePlural"
]
],
"message": "String should be camel case"
},
{
"code": "custom",
"message": "String should be camel case",
"path": [
"nameSingular"
]
],
"message": "String should be camel case"
}
]]
`;
@@ -77,22 +74,20 @@ exports[`settingsDataModelObjectAboutFormSchema fails when names are not in came
exports[`settingsDataModelObjectAboutFormSchema fails when required fields are missing 1`] = `
[ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"code": "invalid_type",
"path": [
"labelSingular"
],
"message": "Required"
"message": "Invalid input: expected string, received undefined"
},
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"code": "invalid_type",
"path": [
"labelPlural"
],
"message": "Required"
"message": "Invalid input: expected string, received undefined"
}
]]
`;
@@ -138,22 +133,20 @@ exports[`settingsDataModelObjectAboutFormSchema fails when singular and plural n
exports[`settingsDataModelObjectAboutFormSchema fails with invalid types for optional fields 1`] = `
[ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"code": "invalid_type",
"path": [
"description"
],
"message": "Expected string, received number"
"message": "Invalid input: expected string, received number"
},
{
"code": "invalid_type",
"expected": "string",
"received": "boolean",
"code": "invalid_type",
"path": [
"icon"
],
"message": "Expected string, received boolean"
"message": "Invalid input: expected string, received boolean"
}
]]
`;
@@ -39,7 +39,7 @@ export const settingsDataModelObjectAboutFormSchema =
];
labelFields.forEach((field) =>
ctx.addIssue({
code: z.ZodIssueCode.custom,
code: 'custom',
message: t`Singular and plural labels must be different`,
path: [field],
}),
@@ -55,7 +55,7 @@ export const settingsDataModelObjectAboutFormSchema =
];
nameFields.forEach((field) =>
ctx.addIssue({
code: z.ZodIssueCode.custom,
code: 'custom',
message: t`Singular and plural names must be different`,
path: [field],
}),
@@ -7,7 +7,7 @@ export const webhookFormSchema = z.object({
.trim()
.min(1, 'URL is required')
.refine((url) => isValidUrl(url), {
message: 'Please enter a valid URL',
error: 'Please enter a valid URL',
}),
description: z.string().optional(),
operations: z
@@ -22,7 +22,7 @@ export const webhookFormSchema = z.object({
(operations) =>
operations.some((op) => op.object !== null && op.action !== null),
{
message: 'At least one complete operation is required',
error: 'At least one complete operation is required',
},
),
secret: z.string().optional(),
@@ -15,7 +15,7 @@ import {
} from '~/generated-metadata/graphql';
export const settingsIntegrationsDatabaseTablesSchema = z.object({
syncedTablesByName: z.record(z.boolean()),
syncedTablesByName: z.record(z.string(), z.boolean()),
});
export type SettingsIntegrationsDatabaseTablesFormValues = z.infer<
@@ -19,8 +19,8 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const playgroundSetupFormSchema = z.object({
apiKeyForPlayground: z.string(),
schema: z.nativeEnum(PlaygroundSchemas),
playgroundType: z.nativeEnum(PlaygroundTypes),
schema: z.enum(PlaygroundSchemas),
playgroundType: z.enum(PlaygroundTypes),
});
type PlaygroundSetupFormValues = z.infer<typeof playgroundSetupFormSchema>;
@@ -3,8 +3,8 @@
import { z } from 'zod';
const validator = z.object({
entityID: z.string().url(),
ssoUrl: z.string().url(),
entityID: z.url(),
ssoUrl: z.url(),
certificate: z.string().min(1),
});
@@ -14,7 +14,7 @@ export const SSOIdentitiesProvidersSAMLParamsSchema = z
.object({
type: z.literal('SAML'),
id: z.string().nonempty(),
ssoURL: z.string().url().nonempty(),
ssoURL: z.url().nonempty(),
certificate: z.string().nonempty(),
})
.required();
@@ -28,7 +28,7 @@ export const SSOIdentitiesProvidersParamsSchema = z
z
.object({
name: z.string().nonempty(),
issuer: z.string().url().nonempty(),
issuer: z.url().nonempty(),
})
.required(),
);
@@ -23,8 +23,9 @@ const filterQueryParamsSchema = z.object({
viewId: z.string().optional(),
filter: z
.record(
z.record(
z.nativeEnum(ViewFilterOperand),
z.string(),
z.partialRecord(
z.enum(ViewFilterOperand),
z.string().or(z.array(z.string())).or(relationFilterValueSchemaObject),
),
)
@@ -53,7 +53,7 @@ export const variableDateViewFilterValuePartsSchema = z
unit: variableDateViewFilterValueUnitSchema,
})
.refine((data) => !(data.amount === undefined && data.direction !== 'THIS'), {
message: "Amount cannot be 'undefined' unless direction is 'THIS'",
error: "Amount cannot be 'undefined' unless direction is 'THIS'",
});
const variableDateViewFilterValueSchema = z.string().transform((value) => {
@@ -14,6 +14,6 @@ export const arrayOfStringsOrVariablesSchema = z
(parsed) =>
Array.isArray(parsed) && parsed.every((item) => typeof item === 'string'),
{
message: 'Expected an array of strings',
error: 'Expected an array of strings',
},
);
@@ -12,7 +12,7 @@ export const jsonRelationFilterValueSchema = z
return JSON.parse(value);
} catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
code: 'custom',
message: (error as Error).message,
});
return z.NEVER;
@@ -1,9 +1,9 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { type WorkflowRun } from '@/workflow/types/Workflow';
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { workflowRunSchema } from 'twenty-shared/workflow';
export const useWorkflowRun = ({
workflowRunId,
@@ -20,7 +20,7 @@ import {
type workflowTriggerSchema,
type workflowUpdateRecordActionSchema,
type workflowWebhookTriggerSchema,
} from '@/workflow/validation-schemas/workflowSchema';
} from 'twenty-shared/workflow';
import { type z } from 'zod';
export type WorkflowCodeAction = z.infer<typeof workflowCodeActionSchema>;
@@ -1,9 +1,9 @@
import { z } from 'zod';
const schema = z
.record(z.any())
.record(z.string(), z.any())
.refine((data) => Object.keys(data).every((key) => !key.match(/\s/)), {
message: 'JSON keys cannot contain spaces',
error: 'JSON keys cannot contain spaces',
});
export const parseAndValidateVariableFriendlyStringifiedJson = (
@@ -1,390 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { StepStatus } from 'twenty-shared/workflow';
import { z } from 'zod';
export const objectRecordSchema = z.record(z.any());
export const baseWorkflowActionSettingsSchema = z.object({
input: z.object({}).passthrough(),
outputSchema: z.object({}).passthrough(),
errorHandlingOptions: z.object({
retryOnFailure: z.object({
value: z.boolean(),
}),
continueOnFailure: z.object({
value: z.boolean(),
}),
}),
});
export const baseWorkflowActionSchema = z.object({
id: z.string(),
name: z.string(),
valid: z.boolean(),
nextStepIds: z.array(z.string()).optional().nullable(),
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
});
export const baseTriggerSchema = z.object({
name: z.string().optional(),
type: z.string(),
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
nextStepIds: z.array(z.string()).optional().nullable(),
});
export const workflowCodeActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
serverlessFunctionId: z.string(),
serverlessFunctionVersion: z.string(),
serverlessFunctionInput: z.record(z.any()),
}),
});
export const workflowSendEmailActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
connectedAccountId: z.string(),
email: z.string(),
subject: z.string().optional(),
body: z.string().optional(),
}),
});
export const workflowCreateRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecord: objectRecordSchema,
}),
});
export const workflowUpdateRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecord: objectRecordSchema,
objectRecordId: z.string(),
fieldsToUpdate: z.array(z.string()),
}),
});
export const workflowDeleteRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecordId: z.string(),
}),
});
export const workflowFindRecordsActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
limit: z.number().optional(),
filter: z
.object({
recordFilterGroups: z.array(z.object({})).optional(),
recordFilters: z.array(z.object({})).optional(),
gqlOperationFilter: z.object({}).optional().nullable(),
})
.optional(),
}),
});
export const workflowFormActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.array(
z.object({
id: z.string(),
name: z.string(),
label: z.string(),
type: z.union([
z.literal(FieldMetadataType.TEXT),
z.literal(FieldMetadataType.NUMBER),
z.literal(FieldMetadataType.DATE),
z.literal(FieldMetadataType.SELECT),
z.literal('RECORD'),
]),
placeholder: z.string().optional(),
settings: z.record(z.any()).optional(),
value: z.any().optional(),
}),
),
});
export const workflowHttpRequestActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
url: z.string(),
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
headers: z.record(z.string()).optional(),
body: z
.record(
z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])),
]),
)
.or(z.string())
.optional(),
}),
});
export const workflowAiAgentActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
agentId: z.string().optional(),
prompt: z.string().optional(),
}),
});
export const workflowFilterActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
stepFilterGroups: z.array(z.any()),
stepFilters: z.array(z.any()),
}),
});
export const workflowIteratorActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
items: z
.union([
z.array(
z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.record(z.any()),
z.any(),
]),
),
z.string(),
])
.optional(),
initialLoopStepIds: z.array(z.string()).optional(),
}),
});
export const workflowEmptyActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({}),
});
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CODE'),
settings: workflowCodeActionSettingsSchema,
});
export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('SEND_EMAIL'),
settings: workflowSendEmailActionSettingsSchema,
});
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('CREATE_RECORD'),
settings: workflowCreateRecordActionSettingsSchema,
},
);
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('UPDATE_RECORD'),
settings: workflowUpdateRecordActionSettingsSchema,
},
);
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('DELETE_RECORD'),
settings: workflowDeleteRecordActionSettingsSchema,
},
);
export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FIND_RECORDS'),
settings: workflowFindRecordsActionSettingsSchema,
});
export const workflowFormActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FORM'),
settings: workflowFormActionSettingsSchema,
});
export const workflowHttpRequestActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('HTTP_REQUEST'),
settings: workflowHttpRequestActionSettingsSchema,
});
export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('AI_AGENT'),
settings: workflowAiAgentActionSettingsSchema,
});
export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FILTER'),
settings: workflowFilterActionSettingsSchema,
});
export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('ITERATOR'),
settings: workflowIteratorActionSettingsSchema,
});
export const workflowEmptyActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('EMPTY'),
settings: workflowEmptyActionSettingsSchema,
});
export const workflowActionSchema = z.discriminatedUnion('type', [
workflowCodeActionSchema,
workflowSendEmailActionSchema,
workflowCreateRecordActionSchema,
workflowUpdateRecordActionSchema,
workflowDeleteRecordActionSchema,
workflowFindRecordsActionSchema,
workflowFormActionSchema,
workflowHttpRequestActionSchema,
workflowAiAgentActionSchema,
workflowFilterActionSchema,
workflowIteratorActionSchema,
workflowEmptyActionSchema,
]);
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({
type: z.literal('DATABASE_EVENT'),
settings: z.object({
eventName: z.string(),
input: z.object({}).passthrough().optional(),
outputSchema: z.object({}).passthrough(),
objectType: z.string().optional(),
fields: z.array(z.string()).optional().nullable(),
}),
});
export const workflowManualTriggerSchema = baseTriggerSchema
.extend({
type: z.literal('MANUAL'),
settings: z.object({
objectType: z.string().optional(),
outputSchema: z
.object({})
.passthrough()
.describe(
'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.',
),
icon: z.string().optional(),
isPinned: z.boolean().optional(),
}),
})
.describe(
'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.',
);
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
type: z.literal('CRON'),
settings: z.discriminatedUnion('type', [
z.object({
type: z.literal('DAYS'),
schedule: z.object({
day: z.number().min(1),
hour: z.number().min(0).max(23),
minute: z.number().min(0).max(59),
}),
outputSchema: z.object({}).passthrough(),
}),
z.object({
type: z.literal('HOURS'),
schedule: z.object({
hour: z.number().min(1),
minute: z.number().min(0).max(59),
}),
outputSchema: z.object({}).passthrough(),
}),
z.object({
type: z.literal('MINUTES'),
schedule: z.object({ minute: z.number().min(1) }),
outputSchema: z.object({}).passthrough(),
}),
z.object({
type: z.literal('CUSTOM'),
pattern: z.string(),
outputSchema: z.object({}).passthrough(),
}),
]),
});
export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({
type: z.literal('WEBHOOK'),
settings: z.discriminatedUnion('httpMethod', [
z.object({
outputSchema: z.object({}).passthrough(),
httpMethod: z.literal('GET'),
authentication: z.literal('API_KEY').nullable(),
}),
z.object({
outputSchema: z.object({}).passthrough(),
httpMethod: z.literal('POST'),
expectedBody: z.object({}).passthrough(),
authentication: z.literal('API_KEY').nullable(),
}),
]),
});
export const workflowTriggerSchema = z.discriminatedUnion('type', [
workflowDatabaseEventTriggerSchema,
workflowManualTriggerSchema,
workflowCronTriggerSchema,
workflowWebhookTriggerSchema,
]);
export const workflowRunStepStatusSchema = z.nativeEnum(StepStatus);
export const workflowRunStateStepInfoSchema = z.object({
result: z.any().optional(),
error: z.any().optional(),
status: workflowRunStepStatusSchema,
});
export const workflowRunStateStepInfosSchema = z.record(
workflowRunStateStepInfoSchema,
);
export const workflowRunStateSchema = z.object({
flow: z.object({
trigger: workflowTriggerSchema,
steps: z.array(workflowActionSchema),
}),
stepInfos: workflowRunStateStepInfosSchema,
workflowRunError: z.any().optional(),
});
export const workflowRunStatusSchema = z.enum([
'NOT_STARTED',
'RUNNING',
'COMPLETED',
'FAILED',
'ENQUEUED',
]);
export const workflowRunSchema = z
.object({
__typename: z.literal('WorkflowRun'),
id: z.string(),
workflowVersionId: z.string(),
workflowId: z.string(),
state: workflowRunStateSchema.nullable(),
status: workflowRunStatusSchema,
createdAt: z.string(),
deletedAt: z.string().nullable(),
endedAt: z.string().nullable(),
name: z.string(),
})
.passthrough();
@@ -3,13 +3,13 @@ import {
type WorkflowHttpRequestAction,
type WorkflowSendEmailAction,
} from '@/workflow/types/Workflow';
import { renderHook } from '@testing-library/react';
import { FieldMetadataType } from 'twenty-shared/types';
import {
workflowFormActionSettingsSchema,
workflowHttpRequestActionSettingsSchema,
workflowSendEmailActionSettingsSchema,
} from '@/workflow/validation-schemas/workflowSchema';
import { renderHook } from '@testing-library/react';
import { FieldMetadataType } from 'twenty-shared/types';
} from 'twenty-shared/workflow';
import { useWorkflowActionHeader } from '../useWorkflowActionHeader';
jest.mock('../useActionIconColorOrThrow', () => ({
@@ -25,7 +25,7 @@ const StyledLinkContainer = styled.div`
`;
const emailValidationSchema = (email: string) =>
z.string().email(`Invalid email '${email}'`);
z.email(`Invalid email '${email}'`);
const validationSchema = () =>
z
@@ -37,9 +37,8 @@ const validationSchema = () =>
const emails = sanitizeEmailList(value.split(','));
if (emails.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_string,
code: 'custom',
message: 'Emails should not be empty',
validation: 'email',
});
}
const invalidEmails: string[] = [];
@@ -51,12 +50,11 @@ const validationSchema = () =>
}
if (invalidEmails.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_string,
code: 'custom',
message:
invalidEmails.length > 1
? 'Emails "' + invalidEmails.join('", "') + '" are invalid'
: 'Email "' + invalidEmails.join('", "') + '" is invalid',
validation: 'email',
});
}
}),