From 3cada58908edfc6f4ebaa8a08aed404816b5dd4e Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:59:05 +0530 Subject: [PATCH] Migrate from Zod v3 to v4 (#14639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes [#1526](https://github.com/twentyhq/core-team-issues/issues/1526) --------- Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- package.json | 2 +- packages/twenty-cli/package.json | 2 +- packages/twenty-front/package.json | 2 +- .../auth/sign-in-up/hooks/useSignInUpForm.ts | 5 +- .../fieldMetadataItemSchema.ts | 25 +- .../indexFieldMetadataItemSchema.ts | 2 +- .../indexMetadataItemSchema.ts | 6 +- .../objectMetadataItemSchema.ts | 13 +- .../validation-schemas/selectOptionsSchema.ts | 11 +- .../meta-types/utils/orderWorkflowRunState.ts | 2 +- .../record-field/ui/types/FieldMetadata.ts | 2 +- .../ui/types/guards/isFieldCurrencyValue.ts | 2 +- .../ui/types/guards/isFieldRawJsonValue.ts | 8 +- .../validation-schemas/currencyCodeSchema.ts | 2 +- .../currencyFieldDefaultValueSchema.ts | 4 +- .../ui/validation-schemas/emailSchema.ts | 2 +- .../SettingsAccountsBlocklistInput.tsx | 2 +- .../connectionImapSmtpCalDav.ts | 10 +- ...SettingsDataModelFieldSettingsFormCard.tsx | 36 +- ...ettingsDataModelFieldMorphRelationForm.tsx | 2 +- .../SettingsDataModelFieldRelationForm.tsx | 6 +- .../settingsFieldFormSchema.ts | 8 +- .../utils/getMultiSelectFieldPreviewValue.ts | 5 +- .../utils/getSelectFieldPreviewValue.ts | 2 +- ...SettingsDataModelObjectIdentifiersForm.tsx | 4 +- ...ataModelObjectAboutFormSchema.test.ts.snap | 47 +- .../settingsDataModelObjectAboutFormSchema.ts | 4 +- .../validation-schemas/webhookFormSchema.ts | 4 +- ...tingsIntegrationDatabaseTablesListCard.tsx | 2 +- .../components/PlaygroundSetupForm.tsx | 4 +- .../utils/parseSAMLMetadataFromXMLFile.ts | 4 +- .../SSOIdentityProviderSchema.ts | 4 +- .../hooks/internal/useViewFromQueryParams.ts | 5 +- .../utils/resolveDateViewFilterValue.ts | 2 +- .../arrayOfStringsOrVariablesSchema.ts | 2 +- .../jsonRelationFilterValueSchema.ts | 2 +- .../modules/workflow/hooks/useWorkflowRun.ts | 2 +- .../src/modules/workflow/types/Workflow.ts | 2 +- ...ValidateVariableFriendlyStringifiedJson.ts | 4 +- .../validation-schemas/workflowSchema.ts | 390 -------------- .../useWorkflowActionHeader.test.tsx | 6 +- .../components/WorkspaceInviteTeam.tsx | 8 +- .../src/pages/onboarding/CreateProfile.tsx | 8 +- .../src/pages/onboarding/InviteTeam.tsx | 4 +- .../SettingsSecurityApprovedAccessDomain.tsx | 30 +- .../__tests__/camelCaseStringSchema.test.ts | 4 +- .../simpleQuotesStringSchema.test.ts | 4 +- .../camelCaseStringSchema.ts | 2 +- .../simpleQuotesStringSchema.ts | 8 +- .../schemas/workflow-tool-schemas.ts | 6 +- .../workflow-tools/schemas/workflow.schema.ts | 492 ------------------ packages/twenty-shared/package.json | 2 +- .../utils/safeParseRelativeDateFilterValue.ts | 2 +- .../src/utils/url/absoluteUrlSchema.ts | 6 +- packages/twenty-shared/src/workflow/index.ts | 40 ++ .../schemas/ai-agent-action-schema.ts | 8 + .../ai-agent-action-settings-schema.ts | 10 + .../workflow/schemas/base-trigger-schema.ts | 23 + .../schemas/base-workflow-action-schema.ts | 23 + .../base-workflow-action-settings-schema.ts | 22 + .../workflow/schemas/code-action-schema.ts | 8 + .../schemas/code-action-settings-schema.ts | 11 + .../schemas/create-record-action-schema.ts | 8 + .../create-record-action-settings-schema.ts | 18 + .../workflow/schemas/cron-trigger-schema.ts | 35 ++ .../schemas/database-event-trigger-schema.ts | 29 ++ .../schemas/delete-record-action-schema.ts | 8 + .../delete-record-action-settings-schema.ts | 10 + .../workflow/schemas/empty-action-schema.ts | 8 + .../schemas/empty-action-settings-schema.ts | 7 + .../workflow/schemas/filter-action-schema.ts | 8 + .../schemas/filter-action-settings-schema.ts | 31 ++ .../schemas/find-records-action-schema.ts | 8 + .../find-records-action-settings-schema.ts | 17 + .../workflow/schemas/form-action-schema.ts | 8 + .../schemas/form-action-settings-schema.ts | 24 + .../schemas/http-request-action-schema.ts | 8 + .../http-request-action-settings-schema.ts | 24 + .../schemas/iterator-action-schema.ts | 8 + .../iterator-action-settings-schema.ts | 24 + .../workflow/schemas/manual-trigger-schema.ts | 20 + .../workflow/schemas/object-record-schema.ts | 10 + .../schemas/send-email-action-schema.ts | 8 + .../send-email-action-settings-schema.ts | 12 + .../schemas/update-record-action-schema.ts | 8 + .../update-record-action-settings-schema.ts | 13 + .../schemas/webhook-trigger-schema.ts | 19 + .../schemas/workflow-action-schema.ts | 28 + .../workflow/schemas/workflow-run-schema.ts | 16 + .../schemas/workflow-run-state-schema.ts | 13 + .../workflow-run-state-step-info-schema.ts | 8 + .../workflow-run-state-step-infos-schema.ts | 7 + .../schemas/workflow-run-status-schema.ts | 9 + .../workflow-run-step-status-schema.ts | 4 + .../schemas/workflow-trigger-schema.ts | 12 + packages/twenty-ui/package.json | 2 +- packages/twenty-zapier/package.json | 2 +- yarn.lock | 40 +- 98 files changed, 809 insertions(+), 1075 deletions(-) delete mode 100644 packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts delete mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow.schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/ai-agent-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/ai-agent-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/base-trigger-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/base-workflow-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/base-workflow-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/code-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/code-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/create-record-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/create-record-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/cron-trigger-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/database-event-trigger-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/delete-record-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/delete-record-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/empty-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/empty-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/filter-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/filter-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/find-records-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/find-records-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/form-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/form-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/http-request-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/http-request-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/iterator-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/iterator-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/manual-trigger-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/object-record-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/send-email-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/send-email-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/update-record-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/update-record-action-settings-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/webhook-trigger-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-action-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-run-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-run-state-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-info-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-infos-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-run-status-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-run-step-status-schema.ts create mode 100644 packages/twenty-shared/src/workflow/schemas/workflow-trigger-schema.ts diff --git a/package.json b/package.json index 56d2cbdbfde..4c59e19fcf9 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "uuid": "^9.0.0", "vite-tsconfig-paths": "^4.2.1", "xlsx-ugnis": "^0.19.3", - "zod": "3.23.8" + "zod": "^4.1.11" }, "devDependencies": { "@babel/core": "^7.14.5", diff --git a/packages/twenty-cli/package.json b/packages/twenty-cli/package.json index ebee1c806f2..2d9517691be 100644 --- a/packages/twenty-cli/package.json +++ b/packages/twenty-cli/package.json @@ -32,7 +32,7 @@ "jsonc-parser": "^3.2.0", "ora": "^8.0.0", "yaml": "^2.4.0", - "zod": "^3.22.0" + "zod": "^4.1.11" }, "devDependencies": { "@types/fs-extra": "^11.0.0", diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 40bc0e346fc..d5b9073b566 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -41,7 +41,7 @@ "@graphiql/plugin-explorer": "^1.0.2", "@graphiql/react": "^0.23.0", "@hello-pangea/dnd": "^16.2.0", - "@hookform/resolvers": "^3.1.1", + "@hookform/resolvers": "^5.2.2", "@lingui/core": "^5.1.2", "@lingui/detect-locale": "^5.2.0", "@lingui/react": "^5.1.2", diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts index dcef06c2c37..cf71ceee0c2 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts @@ -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 diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts index 674bf4ae7e5..e026ea2bffb 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts @@ -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; + type: z.enum(FieldMetadataType), + updatedAt: z.iso.datetime(), + }); }; diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/indexFieldMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/indexFieldMetadataItemSchema.ts index 673cf87a79f..b188d00a42f 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/indexFieldMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/indexFieldMetadataItemSchema.ts @@ -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(), diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/indexMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/indexMetadataItemSchema.ts index eb390435a64..a569753945a 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/indexMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/indexMetadataItemSchema.ts @@ -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(), diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts index 88951876adc..95c2ef7e100 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts @@ -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; +}); diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/selectOptionsSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/selectOptionsSchema.ts index 146ac96e399..f334b727d6f 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/selectOptionsSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/selectOptionsSchema.ts @@ -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; + ); 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', }, ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/utils/orderWorkflowRunState.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/utils/orderWorkflowRunState.ts index 8d74e6cef8d..71f22435a4c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/utils/orderWorkflowRunState.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/utils/orderWorkflowRunState.ts @@ -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) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts index 86f99e0fbff..29347525dc4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts @@ -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(), }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldCurrencyValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldCurrencyValue.ts index c75a1d75742..ae2d006f5f7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldCurrencyValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldCurrencyValue.ts @@ -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(), }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRawJsonValue.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRawJsonValue.ts index dbab3c8f3b3..5c238ae22e5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRawJsonValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/isFieldRawJsonValue.ts @@ -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 = 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 = z.union([ z.null(), // Exclude literal values other than null z.array(jsonSchema), - z.record(jsonSchema), + z.record(z.string(), jsonSchema), ]); export const isFieldRawJsonValue = ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyCodeSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyCodeSchema.ts index 1a046ea6b6f..90dd9710198 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyCodeSchema.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyCodeSchema.ts @@ -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); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyFieldDefaultValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyFieldDefaultValueSchema.ts index 0277440c9ec..f1ceca165c7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyFieldDefaultValueSchema.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/currencyFieldDefaultValueSchema.ts @@ -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', + }, ), }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/emailSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/emailSchema.ts index fae5812b3ad..adcbf8eaf03 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/emailSchema.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/validation-schemas/emailSchema.ts @@ -1,3 +1,3 @@ import { z } from 'zod'; -export const emailSchema = z.string().email(); +export const emailSchema = z.email(); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx index 9822990a975..625abb1d778 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx @@ -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) => diff --git a/packages/twenty-front/src/modules/settings/accounts/validation-schemas/connectionImapSmtpCalDav.ts b/packages/twenty-front/src/modules/settings/accounts/validation-schemas/connectionImapSmtpCalDav.ts index fd78bee25a1..f264851baa6 100644 --- a/packages/twenty-front/src/modules/settings/accounts/validation-schemas/connectionImapSmtpCalDav.ts +++ b/packages/twenty-front/src/modules/settings/accounts/validation-schemas/connectionImapSmtpCalDav.ts @@ -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', }, ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx index f4a213e90fb..a118f7aab6e 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx @@ -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', diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm.tsx index 5de6d86d34d..61504f7c4a8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm.tsx @@ -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[]], ), diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx index 4b6493527d4..057e1b6272a 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx @@ -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[]], ), diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts index 13a90ecbc70..7a9ed559929 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts @@ -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); }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue.ts index 68786e7d3f9..0f4bc9cc318 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue.ts @@ -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) diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue.ts index c701a0819e3..b44915bab44 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue.ts @@ -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); diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx index c62c488a1dc..c5e0d2fea8e 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -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; } diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap index 565b921e265..d5bcff76ad3 100644 --- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap @@ -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" } ]] `; diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts index 453589600dd..92f28dc9f1b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts @@ -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], }), diff --git a/packages/twenty-front/src/modules/settings/developers/validation-schemas/webhookFormSchema.ts b/packages/twenty-front/src/modules/settings/developers/validation-schemas/webhookFormSchema.ts index 0cd85cb8317..9dd9684c815 100644 --- a/packages/twenty-front/src/modules/settings/developers/validation-schemas/webhookFormSchema.ts +++ b/packages/twenty-front/src/modules/settings/developers/validation-schemas/webhookFormSchema.ts @@ -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(), diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseTablesListCard.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseTablesListCard.tsx index 3be0a7c1eb8..de815aa58f4 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseTablesListCard.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseTablesListCard.tsx @@ -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< diff --git a/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx b/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx index 6a5edf89c66..103f23aaf45 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx +++ b/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts b/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts index 39c1469f3b8..1356bd432d5 100644 --- a/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts +++ b/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts @@ -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), }); diff --git a/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts b/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts index d4dc0f6486a..a8dbc63a83b 100644 --- a/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts +++ b/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts @@ -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(), ); diff --git a/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts b/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts index cd406f0387e..b9140bbe40a 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts @@ -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), ), ) diff --git a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts index 32735ff0477..9633fdb0723 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts @@ -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) => { diff --git a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfStringsOrVariablesSchema.ts b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfStringsOrVariablesSchema.ts index f248ae25562..d17ea019bac 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfStringsOrVariablesSchema.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/arrayOfStringsOrVariablesSchema.ts @@ -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', }, ); diff --git a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema.ts b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema.ts index b47023bc11c..317a4e8652d 100644 --- a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema.ts @@ -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; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts index abf82937606..cb0424c2d98 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.ts @@ -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, diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 4c9eb36b9bb..bc94c37b1f4 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -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; diff --git a/packages/twenty-front/src/modules/workflow/utils/parseAndValidateVariableFriendlyStringifiedJson.ts b/packages/twenty-front/src/modules/workflow/utils/parseAndValidateVariableFriendlyStringifiedJson.ts index ae516ab1a13..ac6390d2499 100644 --- a/packages/twenty-front/src/modules/workflow/utils/parseAndValidateVariableFriendlyStringifiedJson.ts +++ b/packages/twenty-front/src/modules/workflow/utils/parseAndValidateVariableFriendlyStringifiedJson.ts @@ -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 = ( diff --git a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts deleted file mode 100644 index 69e2a81e2be..00000000000 --- a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts +++ /dev/null @@ -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(); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx index 3d83c4382b2..93293eed01e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx @@ -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', () => ({ diff --git a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx index 2d58e6cb915..df3af2f2ead 100644 --- a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx +++ b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx @@ -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', }); } }), diff --git a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx index 31156f46ceb..db93af46508 100644 --- a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx +++ b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx @@ -49,8 +49,12 @@ const StyledComboInputContainer = styled.div` const validationSchema = z .object({ - firstName: z.string().min(1, { message: 'First name can not be empty' }), - lastName: z.string().min(1, { message: 'Last name can not be empty' }), + firstName: z.string().min(1, { + error: 'First name can not be empty', + }), + lastName: z.string().min(1, { + error: 'Last name can not be empty', + }), }) .required(); diff --git a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx index abf9da8d551..16c5ce81210 100644 --- a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx +++ b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx @@ -54,9 +54,7 @@ const StyledActionSkipLinkContainer = styled.div` `; const validationSchema = z.object({ - emails: z.array( - z.object({ email: z.union([z.literal(''), z.string().email()]) }), - ), + emails: z.array(z.object({ email: z.union([z.literal(''), z.email()]) })), }); type FormInput = z.infer; diff --git a/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx b/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx index 8b394b20877..19adb1886cd 100644 --- a/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx +++ b/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx @@ -27,22 +27,20 @@ export const SettingsSecurityApprovedAccessDomain = () => { const form = useForm<{ domain: string; email: string }>({ mode: 'onSubmit', resolver: zodResolver( - z - .object({ - domain: z - .string() - .regex( - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])\.[a-zA-Z]{2,}$/, - { - message: t`Domains have to be smaller than 256 characters, cannot contain spaces and cannot contain any special characters.`, - }, - ) - .max(256), - email: z.string().min(1, { - message: t`Email cannot be empty`, - }), - }) - .strict(), + z.strictObject({ + domain: z + .string() + .regex( + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])\.[a-zA-Z]{2,}$/, + { + message: t`Domains have to be smaller than 256 characters, cannot contain spaces and cannot contain any special characters.`, + }, + ) + .max(256), + email: z.string().min(1, { + message: t`Email cannot be empty`, + }), + }), ), defaultValues: { email: '', diff --git a/packages/twenty-front/src/utils/validation-schemas/__tests__/camelCaseStringSchema.test.ts b/packages/twenty-front/src/utils/validation-schemas/__tests__/camelCaseStringSchema.test.ts index e3bfa43b361..edc3857d9ba 100644 --- a/packages/twenty-front/src/utils/validation-schemas/__tests__/camelCaseStringSchema.test.ts +++ b/packages/twenty-front/src/utils/validation-schemas/__tests__/camelCaseStringSchema.test.ts @@ -1,4 +1,4 @@ -import { type SafeParseError } from 'zod'; +import { type ZodSafeParseError } from 'zod'; import { camelCaseStringSchema } from '../camelCaseStringSchema'; @@ -11,7 +11,7 @@ describe('camelCaseStringSchema', () => { it('fails for non-camel case strings', () => { const result = camelCaseStringSchema.safeParse('NotCamelCase'); expect(result.success).toBe(false); - expect((result as SafeParseError).error.errors).toEqual([ + expect((result as ZodSafeParseError).error.issues).toEqual([ { code: 'custom', message: 'String should be camel case', diff --git a/packages/twenty-front/src/utils/validation-schemas/__tests__/simpleQuotesStringSchema.test.ts b/packages/twenty-front/src/utils/validation-schemas/__tests__/simpleQuotesStringSchema.test.ts index 76017f5a2d3..90adcfba9c9 100644 --- a/packages/twenty-front/src/utils/validation-schemas/__tests__/simpleQuotesStringSchema.test.ts +++ b/packages/twenty-front/src/utils/validation-schemas/__tests__/simpleQuotesStringSchema.test.ts @@ -1,4 +1,4 @@ -import { type SafeParseError } from 'zod'; +import { type ZodSafeParseError } from 'zod'; import { simpleQuotesStringSchema } from '../simpleQuotesStringSchema'; @@ -27,7 +27,7 @@ describe('simpleQuotesStringSchema', () => { // Then expect(result.success).toBe(false); - expect((result as SafeParseError).error.errors).toEqual([ + expect((result as ZodSafeParseError).error.issues).toEqual([ { code: 'custom', message: 'String should be wrapped in simple quotes', diff --git a/packages/twenty-front/src/utils/validation-schemas/camelCaseStringSchema.ts b/packages/twenty-front/src/utils/validation-schemas/camelCaseStringSchema.ts index 0e96c167ca0..8af768cf950 100644 --- a/packages/twenty-front/src/utils/validation-schemas/camelCaseStringSchema.ts +++ b/packages/twenty-front/src/utils/validation-schemas/camelCaseStringSchema.ts @@ -4,5 +4,5 @@ import { z } from 'zod'; export const camelCaseStringSchema = z .string() .refine((value) => camelCase(value) === value, { - message: 'String should be camel case', + error: 'String should be camel case', }); diff --git a/packages/twenty-front/src/utils/validation-schemas/simpleQuotesStringSchema.ts b/packages/twenty-front/src/utils/validation-schemas/simpleQuotesStringSchema.ts index 62f4a667ac7..8defa50cadd 100644 --- a/packages/twenty-front/src/utils/validation-schemas/simpleQuotesStringSchema.ts +++ b/packages/twenty-front/src/utils/validation-schemas/simpleQuotesStringSchema.ts @@ -1,15 +1,11 @@ import { z } from 'zod'; -export const simpleQuotesStringSchema: z.ZodType< - `'${string}'`, - z.ZodTypeDef, - string -> = z +export const simpleQuotesStringSchema = z .string() .refine( (value: string): value is `'${string}'` => value.startsWith("'") && value.endsWith("'"), { - message: 'String should be wrapped in simple quotes', + error: 'String should be wrapped in simple quotes', }, ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas.ts index 97951f4c1be..31394818104 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas.ts @@ -1,9 +1,11 @@ +import { + workflowActionSchema, + workflowTriggerSchema, +} from 'twenty-shared/workflow'; import { z } from 'zod'; import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; -import { workflowActionSchema, workflowTriggerSchema } from './workflow.schema'; - export const createWorkflowVersionStepSchema = z.object({ workflowVersionId: z .string() diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow.schema.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow.schema.ts deleted file mode 100644 index 9a6e9b2f191..00000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/schemas/workflow.schema.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { - FieldMetadataType, - StepLogicalOperator, - ViewFilterOperand, -} from 'twenty-shared/types'; -import { StepStatus } from 'twenty-shared/workflow'; -import { z } from 'zod'; - -export const objectRecordSchema = z - .record(z.string(), z.any()) - .describe( - 'Record data object. Use nested objects for relationships (e.g., "company": {"id": "{{reference}}"}). Common patterns:\n' + - '- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' + - '- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' + - '- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}', - ); - -export const baseWorkflowActionSettingsSchema = z.object({ - input: z - .looseObject({}) - .describe( - 'Input data for the workflow action. Structure depends on the action type.', - ), - outputSchema: z - .looseObject({}) - .describe( - 'Schema defining the output data structure. This data can be referenced in subsequent steps using {{stepId.fieldName}}.', - ), - errorHandlingOptions: z.object({ - retryOnFailure: z.object({ - value: z.boolean().describe('Whether to retry the action if it fails.'), - }), - continueOnFailure: z.object({ - value: z - .boolean() - .describe('Whether to continue to the next step if this action fails.'), - }), - }), -}); - -export const baseWorkflowActionSchema = z.object({ - id: z - .string() - .describe( - 'Unique identifier for the workflow step. Must be unique within the workflow.', - ), - name: z - .string() - .describe( - 'Human-readable name for the workflow step. Should clearly describe what the step does.', - ), - valid: z - .boolean() - .describe( - 'Whether the step configuration is valid. Set to true when all required fields are properly configured.', - ), - nextStepIds: z - .array(z.string()) - .optional() - .nullable() - .describe( - 'Array of step IDs that this step connects to. Leave empty or null for the final step.', - ), - position: z - .object({ x: z.number(), y: z.number() }) - .optional() - .nullable() - .describe('Position coordinates for the step in the workflow diagram.'), -}); - -export const baseTriggerSchema = z.object({ - name: z - .string() - .optional() - .describe( - 'Human-readable name for the trigger. Optional but recommended for clarity.', - ), - type: z - .enum(['DATABASE_EVENT', 'MANUAL', 'CRON', 'WEBHOOK']) - .describe( - 'Type of trigger. DATABASE_EVENT for record changes, MANUAL for user-initiated, CRON for scheduled, WEBHOOK for external calls.', - ), - position: z - .object({ x: z.number(), y: z.number() }) - .optional() - .nullable() - .describe( - 'Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.', - ), - nextStepIds: z - .array(z.string()) - .optional() - .nullable() - .describe( - 'Array of step IDs that the trigger connects to. These are the first steps in the workflow.', - ), -}); - -export const workflowCodeActionSettingsSchema = - baseWorkflowActionSettingsSchema.extend({ - input: z.object({ - serverlessFunctionId: z.string(), - serverlessFunctionVersion: z.string(), - serverlessFunctionInput: z.record(z.string(), 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() - .describe( - 'The name of the object to create a record in. Must be lowercase (e.g., "person", "company", "task").', - ), - objectRecord: objectRecordSchema.describe('The record data to create.'), - }), - }); - -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.string(), 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(), z.string()).optional(), - body: z - .record( - z.string(), - 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.object({ - id: z.string(), - logicalOperator: z.enum(StepLogicalOperator), - parentStepFilterGroupId: z.string().optional(), - positionInStepFilterGroup: z.number().optional(), - }), - ), - stepFilters: z.array( - z.object({ - id: z.string(), - type: z.string(), - stepOutputKey: z.string(), - operand: z.enum(ViewFilterOperand), - value: z.string(), - stepFilterGroupId: z.string(), - positionInStepFilterGroup: z.number().optional(), - fieldMetadataId: z.string().optional(), - compositeFieldSubFieldName: z.string().optional(), - }), - ), - }), - }); - -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.string(), 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() - .regex( - /^[a-z][a-zA-Z0-9_]*\.(created|updated|deleted)$/, - 'Event name must follow the pattern: objectName.action (e.g., "company.created", "person.updated")', - ) - .describe( - 'Event name in format: objectName.action (e.g., "company.created", "person.updated", "task.deleted"). Use lowercase object names.', - ), - input: z.looseObject({}).optional(), - outputSchema: z - .looseObject({}) - .describe( - 'Schema defining the output data structure. For database events, this includes the record that triggered the workflow accessible via {{trigger.object.fieldName}}.', - ), - objectType: z.string().optional(), - fields: z.array(z.string()).optional().nullable(), - }), - }) - .describe( - 'Database event trigger that fires when a record is created, updated, or deleted. The triggered record is accessible in workflow steps via {{trigger.object.fieldName}}.', - ); - -export const workflowManualTriggerSchema = baseTriggerSchema - .extend({ - type: z.literal('MANUAL'), - settings: z.object({ - objectType: z.string().optional(), - outputSchema: z - .looseObject({}) - .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.looseObject({}), - }), - z.object({ - type: z.literal('HOURS'), - schedule: z.object({ - hour: z.number().min(1), - minute: z.number().min(0).max(59), - }), - outputSchema: z.looseObject({}), - }), - z.object({ - type: z.literal('MINUTES'), - schedule: z.object({ minute: z.number().min(1) }), - outputSchema: z.looseObject({}), - }), - z.object({ - type: z.literal('CUSTOM'), - pattern: z.string(), - outputSchema: z.looseObject({}), - }), - ]), -}); - -export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({ - type: z.literal('WEBHOOK'), - settings: z.discriminatedUnion('httpMethod', [ - z.object({ - outputSchema: z.looseObject({}), - httpMethod: z.literal('GET'), - authentication: z.literal('API_KEY').nullable(), - }), - z.object({ - outputSchema: z.looseObject({}), - httpMethod: z.literal('POST'), - expectedBody: z.looseObject({}), - authentication: z.literal('API_KEY').nullable(), - }), - ]), -}); - -export const workflowTriggerSchema = z.discriminatedUnion('type', [ - workflowDatabaseEventTriggerSchema, - workflowManualTriggerSchema, - workflowCronTriggerSchema, - workflowWebhookTriggerSchema, -]); - -export const workflowRunStepStatusSchema = z.enum(StepStatus); - -export const workflowRunStateStepInfoSchema = z.object({ - result: z.any().optional(), - error: z.any().optional(), - status: workflowRunStepStatusSchema, -}); - -export const workflowRunStateStepInfosSchema = z.record( - z.string(), - 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.looseObject({ - __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(), -}); diff --git a/packages/twenty-shared/package.json b/packages/twenty-shared/package.json index 8e2d1f9af48..5f56ac51ea7 100644 --- a/packages/twenty-shared/package.json +++ b/packages/twenty-shared/package.json @@ -30,7 +30,7 @@ "libphonenumber-js": "^1.10.26", "qs": "^6.11.2", "react-router-dom": "^6.4.4", - "zod": "3.23.8" + "zod": "^4.1.11" }, "exports": { ".": { diff --git a/packages/twenty-shared/src/utils/safeParseRelativeDateFilterValue.ts b/packages/twenty-shared/src/utils/safeParseRelativeDateFilterValue.ts index 88e445a33b1..6c1a8994ec7 100644 --- a/packages/twenty-shared/src/utils/safeParseRelativeDateFilterValue.ts +++ b/packages/twenty-shared/src/utils/safeParseRelativeDateFilterValue.ts @@ -15,7 +15,7 @@ const RelativeDateValueSchema = z.object({ } return true; }, { - message: 'Amount is required for NEXT and PAST directions and must be positive', + error: 'Amount is required for NEXT and PAST directions and must be positive' }); export const safeParseRelativeDateFilterValue = ( diff --git a/packages/twenty-shared/src/utils/url/absoluteUrlSchema.ts b/packages/twenty-shared/src/utils/url/absoluteUrlSchema.ts index 6b12072c58e..54f6259f47f 100644 --- a/packages/twenty-shared/src/utils/url/absoluteUrlSchema.ts +++ b/packages/twenty-shared/src/utils/url/absoluteUrlSchema.ts @@ -16,7 +16,7 @@ export const absoluteUrlSchema = z.string().transform((value, ctx) => { // if the hostname is a number, it's not a valid url // if we let URL() parse it, it will throw cast an IP address and we lose the information ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: 'domain is not a valid url', }); @@ -28,14 +28,14 @@ export const absoluteUrlSchema = z.string().transform((value, ctx) => { return absoluteUrl; } ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: 'domain is not a valid url', }); return z.NEVER; } catch { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: "custom", message: 'domain is not a valid url', }); diff --git a/packages/twenty-shared/src/workflow/index.ts b/packages/twenty-shared/src/workflow/index.ts index 7ce7f6b0b29..0bdd029fce0 100644 --- a/packages/twenty-shared/src/workflow/index.ts +++ b/packages/twenty-shared/src/workflow/index.ts @@ -9,6 +9,46 @@ export { CONTENT_TYPE_VALUES_HTTP_REQUEST } from './constants/contentTypeValuesHttpRequest'; export { TRIGGER_STEP_ID } from './constants/TriggerStepId'; +export { workflowAiAgentActionSchema } from './schemas/ai-agent-action-schema'; +export { workflowAiAgentActionSettingsSchema } from './schemas/ai-agent-action-settings-schema'; +export { baseTriggerSchema } from './schemas/base-trigger-schema'; +export { baseWorkflowActionSchema } from './schemas/base-workflow-action-schema'; +export { baseWorkflowActionSettingsSchema } from './schemas/base-workflow-action-settings-schema'; +export { workflowCodeActionSchema } from './schemas/code-action-schema'; +export { workflowCodeActionSettingsSchema } from './schemas/code-action-settings-schema'; +export { workflowCreateRecordActionSchema } from './schemas/create-record-action-schema'; +export { workflowCreateRecordActionSettingsSchema } from './schemas/create-record-action-settings-schema'; +export { workflowCronTriggerSchema } from './schemas/cron-trigger-schema'; +export { workflowDatabaseEventTriggerSchema } from './schemas/database-event-trigger-schema'; +export { workflowDeleteRecordActionSchema } from './schemas/delete-record-action-schema'; +export { workflowDeleteRecordActionSettingsSchema } from './schemas/delete-record-action-settings-schema'; +export { workflowEmptyActionSchema } from './schemas/empty-action-schema'; +export { workflowEmptyActionSettingsSchema } from './schemas/empty-action-settings-schema'; +export { workflowFilterActionSchema } from './schemas/filter-action-schema'; +export { workflowFilterActionSettingsSchema } from './schemas/filter-action-settings-schema'; +export { workflowFindRecordsActionSchema } from './schemas/find-records-action-schema'; +export { workflowFindRecordsActionSettingsSchema } from './schemas/find-records-action-settings-schema'; +export { workflowFormActionSchema } from './schemas/form-action-schema'; +export { workflowFormActionSettingsSchema } from './schemas/form-action-settings-schema'; +export { workflowHttpRequestActionSchema } from './schemas/http-request-action-schema'; +export { workflowHttpRequestActionSettingsSchema } from './schemas/http-request-action-settings-schema'; +export { workflowIteratorActionSchema } from './schemas/iterator-action-schema'; +export { workflowIteratorActionSettingsSchema } from './schemas/iterator-action-settings-schema'; +export { workflowManualTriggerSchema } from './schemas/manual-trigger-schema'; +export { objectRecordSchema } from './schemas/object-record-schema'; +export { workflowSendEmailActionSchema } from './schemas/send-email-action-schema'; +export { workflowSendEmailActionSettingsSchema } from './schemas/send-email-action-settings-schema'; +export { workflowUpdateRecordActionSchema } from './schemas/update-record-action-schema'; +export { workflowUpdateRecordActionSettingsSchema } from './schemas/update-record-action-settings-schema'; +export { workflowWebhookTriggerSchema } from './schemas/webhook-trigger-schema'; +export { workflowActionSchema } from './schemas/workflow-action-schema'; +export { workflowRunSchema } from './schemas/workflow-run-schema'; +export { workflowRunStateSchema } from './schemas/workflow-run-state-schema'; +export { workflowRunStateStepInfoSchema } from './schemas/workflow-run-state-step-info-schema'; +export { workflowRunStateStepInfosSchema } from './schemas/workflow-run-state-step-infos-schema'; +export { workflowRunStatusSchema } from './schemas/workflow-run-status-schema'; +export { workflowRunStepStatusSchema } from './schemas/workflow-run-step-status-schema'; +export { workflowTriggerSchema } from './schemas/workflow-trigger-schema'; export type { BodyType } from './types/workflowHttpRequestStep'; export type { WorkflowRunStepInfo, diff --git a/packages/twenty-shared/src/workflow/schemas/ai-agent-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/ai-agent-action-schema.ts new file mode 100644 index 00000000000..e333987a48c --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/ai-agent-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { workflowAiAgentActionSettingsSchema } from './ai-agent-action-settings-schema'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; + +export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('AI_AGENT'), + settings: workflowAiAgentActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/ai-agent-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/ai-agent-action-settings-schema.ts new file mode 100644 index 00000000000..085cbb645a3 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/ai-agent-action-settings-schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +export const workflowAiAgentActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + agentId: z.string().optional(), + prompt: z.string().optional(), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/base-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/base-trigger-schema.ts new file mode 100644 index 00000000000..43f5aa4c3b8 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/base-trigger-schema.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +export const baseTriggerSchema = z.object({ + name: z + .string() + .optional() + .describe('Human-readable name for the trigger. Optional but recommended for clarity.'), + type: z + .enum(['DATABASE_EVENT', 'MANUAL', 'CRON', 'WEBHOOK']) + .describe( + 'Type of trigger. DATABASE_EVENT for record changes, MANUAL for user-initiated, CRON for scheduled, WEBHOOK for external calls.', + ), + position: z + .object({ x: z.number(), y: z.number() }) + .optional() + .nullable() + .describe('Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.'), + nextStepIds: z + .array(z.string()) + .optional() + .nullable() + .describe('Array of step IDs that the trigger connects to. These are the first steps in the workflow.'), +}); diff --git a/packages/twenty-shared/src/workflow/schemas/base-workflow-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/base-workflow-action-schema.ts new file mode 100644 index 00000000000..adf0dd16799 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/base-workflow-action-schema.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +export const baseWorkflowActionSchema = z.object({ + id: z + .string() + .describe('Unique identifier for the workflow step. Must be unique within the workflow.'), + name: z + .string() + .describe('Human-readable name for the workflow step. Should clearly describe what the step does.'), + valid: z + .boolean() + .describe('Whether the step configuration is valid. Set to true when all required fields are properly configured.'), + nextStepIds: z + .array(z.string()) + .optional() + .nullable() + .describe('Array of step IDs that this step connects to. Leave empty or null for the final step.'), + position: z + .object({ x: z.number(), y: z.number() }) + .optional() + .nullable() + .describe('Position coordinates for the step in the workflow diagram.'), +}); diff --git a/packages/twenty-shared/src/workflow/schemas/base-workflow-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/base-workflow-action-settings-schema.ts new file mode 100644 index 00000000000..02ddba3a53e --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/base-workflow-action-settings-schema.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +export const baseWorkflowActionSettingsSchema = z.object({ + input: z + .looseObject({}) + .describe( + 'Input data for the workflow action. Structure depends on the action type.', + ), + outputSchema: z + .looseObject({}) + .describe( + 'Schema defining the output data structure. This data can be referenced in subsequent steps using {{stepId.fieldName}}.', + ), + errorHandlingOptions: z.object({ + retryOnFailure: z.object({ + value: z.boolean().describe('Whether to retry the action if it fails.'), + }), + continueOnFailure: z.object({ + value: z.boolean().describe('Whether to continue to the next step if this action fails.'), + }), + }), +}); diff --git a/packages/twenty-shared/src/workflow/schemas/code-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/code-action-schema.ts new file mode 100644 index 00000000000..6d2fe827e8a --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/code-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowCodeActionSettingsSchema } from './code-action-settings-schema'; + +export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('CODE'), + settings: workflowCodeActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/code-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/code-action-settings-schema.ts new file mode 100644 index 00000000000..a463830c631 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/code-action-settings-schema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +export const workflowCodeActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + serverlessFunctionId: z.string(), + serverlessFunctionVersion: z.string(), + serverlessFunctionInput: z.record(z.string(), z.any()), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/create-record-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/create-record-action-schema.ts new file mode 100644 index 00000000000..f6ed390f269 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/create-record-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowCreateRecordActionSettingsSchema } from './create-record-action-settings-schema'; + +export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('CREATE_RECORD'), + settings: workflowCreateRecordActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/create-record-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/create-record-action-settings-schema.ts new file mode 100644 index 00000000000..8208dcfa212 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/create-record-action-settings-schema.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; +import { objectRecordSchema } from './object-record-schema'; + +export const workflowCreateRecordActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + objectName: z + .string() + .describe( + 'The name of the object to create a record in. Must be lowercase (e.g., "person", "company", "task").', + ), + objectRecord: objectRecordSchema + .describe( + 'The record data to create.', + ) + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/cron-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/cron-trigger-schema.ts new file mode 100644 index 00000000000..d6b2362ce7c --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/cron-trigger-schema.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { baseTriggerSchema } from './base-trigger-schema'; + +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.looseObject({}), + }), + z.object({ + type: z.literal('HOURS'), + schedule: z.object({ + hour: z.number().min(1), + minute: z.number().min(0).max(59), + }), + outputSchema: z.looseObject({}), + }), + z.object({ + type: z.literal('MINUTES'), + schedule: z.object({ minute: z.number().min(1) }), + outputSchema: z.looseObject({}), + }), + z.object({ + type: z.literal('CUSTOM'), + pattern: z.string(), + outputSchema: z.looseObject({}), + }), + ]), +}); diff --git a/packages/twenty-shared/src/workflow/schemas/database-event-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/database-event-trigger-schema.ts new file mode 100644 index 00000000000..b9024b1e7e7 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/database-event-trigger-schema.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; +import { baseTriggerSchema } from './base-trigger-schema'; + +export const workflowDatabaseEventTriggerSchema = baseTriggerSchema + .extend({ + type: z.literal('DATABASE_EVENT'), + settings: z.object({ + eventName: z + .string() + .regex( + /^[a-z][a-zA-Z0-9_]*\.(created|updated|deleted)$/, + 'Event name must follow the pattern: objectName.action (e.g., "company.created", "person.updated")', + ) + .describe( + 'Event name in format: objectName.action (e.g., "company.created", "person.updated", "task.deleted"). Use lowercase object names.', + ), + input: z.looseObject({}).optional(), + outputSchema: z + .looseObject({}) + .describe( + 'Schema defining the output data structure. For database events, this includes the record that triggered the workflow accessible via {{trigger.object.fieldName}}.', + ), + objectType: z.string().optional(), + fields: z.array(z.string()).optional().nullable(), + }), + }) + .describe( + 'Database event trigger that fires when a record is created, updated, or deleted. The triggered record is accessible in workflow steps via {{trigger.object.fieldName}}.', + ); diff --git a/packages/twenty-shared/src/workflow/schemas/delete-record-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/delete-record-action-schema.ts new file mode 100644 index 00000000000..2a87aae5639 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/delete-record-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowDeleteRecordActionSettingsSchema } from './delete-record-action-settings-schema'; + +export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('DELETE_RECORD'), + settings: workflowDeleteRecordActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/delete-record-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/delete-record-action-settings-schema.ts new file mode 100644 index 00000000000..28d81a44d76 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/delete-record-action-settings-schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +export const workflowDeleteRecordActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + objectName: z.string(), + objectRecordId: z.string(), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/empty-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/empty-action-schema.ts new file mode 100644 index 00000000000..41ba96992be --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/empty-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowEmptyActionSettingsSchema } from './empty-action-settings-schema'; + +export const workflowEmptyActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('EMPTY'), + settings: workflowEmptyActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/empty-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/empty-action-settings-schema.ts new file mode 100644 index 00000000000..b3a5af198d8 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/empty-action-settings-schema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +export const workflowEmptyActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({}), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/filter-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/filter-action-schema.ts new file mode 100644 index 00000000000..80272b2be49 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/filter-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowFilterActionSettingsSchema } from './filter-action-settings-schema'; + +export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('FILTER'), + settings: workflowFilterActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/filter-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/filter-action-settings-schema.ts new file mode 100644 index 00000000000..4393a9a6bc0 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/filter-action-settings-schema.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; +import { StepLogicalOperator } from '../../types/StepFilters'; +import { ViewFilterOperand } from '../../types/ViewFilterOperand'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +export const workflowFilterActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + stepFilterGroups: z.array( + z.object({ + id: z.string(), + logicalOperator: z.enum(StepLogicalOperator), + parentStepFilterGroupId: z.string().optional(), + positionInStepFilterGroup: z.number().optional(), + }), + ), + stepFilters: z.array( + z.object({ + id: z.string(), + type: z.string(), + stepOutputKey: z.string(), + operand: z.enum(ViewFilterOperand), + value: z.string(), + stepFilterGroupId: z.string(), + positionInStepFilterGroup: z.number().optional(), + fieldMetadataId: z.string().optional(), + compositeFieldSubFieldName: z.string().optional(), + }), + ), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/find-records-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/find-records-action-schema.ts new file mode 100644 index 00000000000..5a73c3036ec --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/find-records-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowFindRecordsActionSettingsSchema } from './find-records-action-settings-schema'; + +export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('FIND_RECORDS'), + settings: workflowFindRecordsActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/find-records-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/find-records-action-settings-schema.ts new file mode 100644 index 00000000000..9ea59be2cba --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/find-records-action-settings-schema.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +export const workflowFindRecordsActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + objectName: z.string(), + limit: z.number().optional(), + filter: z + .object({ + recordFilterGroups: z.array(z.any()).optional(), + recordFilters: z.array(z.any()).optional(), + gqlOperationFilter: z.any().optional().nullable(), + }) + .optional(), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/form-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/form-action-schema.ts new file mode 100644 index 00000000000..d0805ce29a1 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/form-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowFormActionSettingsSchema } from './form-action-settings-schema'; + +export const workflowFormActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('FORM'), + settings: workflowFormActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/form-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/form-action-settings-schema.ts new file mode 100644 index 00000000000..149afb1b48a --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/form-action-settings-schema.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +import { FieldMetadataType } from '../../types/FieldMetadataType'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +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.string(), z.any()).optional(), + value: z.any().optional(), + }), + ), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/http-request-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/http-request-action-schema.ts new file mode 100644 index 00000000000..9d5410be78a --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/http-request-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowHttpRequestActionSettingsSchema } from './http-request-action-settings-schema'; + +export const workflowHttpRequestActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('HTTP_REQUEST'), + settings: workflowHttpRequestActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/http-request-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/http-request-action-settings-schema.ts new file mode 100644 index 00000000000..5ad5cd91003 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/http-request-action-settings-schema.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +export const workflowHttpRequestActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + url: z.string(), + method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']), + headers: z.record(z.string(), z.string()).optional(), + body: z + .record( + z.string(), + 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(), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/iterator-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/iterator-action-schema.ts new file mode 100644 index 00000000000..15abcdf2964 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/iterator-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowIteratorActionSettingsSchema } from './iterator-action-settings-schema'; + +export const workflowIteratorActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('ITERATOR'), + settings: workflowIteratorActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/iterator-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/iterator-action-settings-schema.ts new file mode 100644 index 00000000000..0d3709ca663 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/iterator-action-settings-schema.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +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.string(), z.any()), + z.any(), + ]), + ), + z.string(), + ]) + .optional(), + initialLoopStepIds: z.array(z.string()).optional(), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/manual-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/manual-trigger-schema.ts new file mode 100644 index 00000000000..8beb5fcdca9 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/manual-trigger-schema.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; +import { baseTriggerSchema } from './base-trigger-schema'; + +export const workflowManualTriggerSchema = baseTriggerSchema + .extend({ + type: z.literal('MANUAL'), + settings: z.object({ + objectType: z.string().optional(), + outputSchema: z + .looseObject({}) + .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.', + ); diff --git a/packages/twenty-shared/src/workflow/schemas/object-record-schema.ts b/packages/twenty-shared/src/workflow/schemas/object-record-schema.ts new file mode 100644 index 00000000000..f5e91668702 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/object-record-schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const objectRecordSchema = z + .record(z.string(), z.any()) + .describe( + 'Record data object. Use nested objects for relationships (e.g., "company": {"id": "{{reference}}"}). Common patterns:\n' + + '- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' + + '- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' + + '- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}', + ); diff --git a/packages/twenty-shared/src/workflow/schemas/send-email-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/send-email-action-schema.ts new file mode 100644 index 00000000000..e012e62df80 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/send-email-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowSendEmailActionSettingsSchema } from './send-email-action-settings-schema'; + +export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('SEND_EMAIL'), + settings: workflowSendEmailActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/send-email-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/send-email-action-settings-schema.ts new file mode 100644 index 00000000000..18f9c7169a0 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/send-email-action-settings-schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; + +export const workflowSendEmailActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + connectedAccountId: z.string(), + email: z.string(), + subject: z.string().optional(), + body: z.string().optional(), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/update-record-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/update-record-action-schema.ts new file mode 100644 index 00000000000..228f000d943 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/update-record-action-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { baseWorkflowActionSchema } from './base-workflow-action-schema'; +import { workflowUpdateRecordActionSettingsSchema } from './update-record-action-settings-schema'; + +export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend({ + type: z.literal('UPDATE_RECORD'), + settings: workflowUpdateRecordActionSettingsSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/update-record-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/update-record-action-settings-schema.ts new file mode 100644 index 00000000000..0711e9c2799 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/update-record-action-settings-schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; +import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema'; +import { objectRecordSchema } from './object-record-schema'; + +export const workflowUpdateRecordActionSettingsSchema = + baseWorkflowActionSettingsSchema.extend({ + input: z.object({ + objectName: z.string(), + objectRecord: objectRecordSchema, + objectRecordId: z.string(), + fieldsToUpdate: z.array(z.string()), + }), + }); diff --git a/packages/twenty-shared/src/workflow/schemas/webhook-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/webhook-trigger-schema.ts new file mode 100644 index 00000000000..70496b5333d --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/webhook-trigger-schema.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; +import { baseTriggerSchema } from './base-trigger-schema'; + +export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({ + type: z.literal('WEBHOOK'), + settings: z.discriminatedUnion('httpMethod', [ + z.object({ + outputSchema: z.looseObject({}), + httpMethod: z.literal('GET'), + authentication: z.literal('API_KEY').nullable(), + }), + z.object({ + outputSchema: z.looseObject({}), + httpMethod: z.literal('POST'), + expectedBody: z.looseObject({}), + authentication: z.literal('API_KEY').nullable(), + }), + ]), +}); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-action-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-action-schema.ts new file mode 100644 index 00000000000..ec61e220c02 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/workflow-action-schema.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; +import { workflowAiAgentActionSchema } from './ai-agent-action-schema'; +import { workflowCodeActionSchema } from './code-action-schema'; +import { workflowCreateRecordActionSchema } from './create-record-action-schema'; +import { workflowDeleteRecordActionSchema } from './delete-record-action-schema'; +import { workflowEmptyActionSchema } from './empty-action-schema'; +import { workflowFilterActionSchema } from './filter-action-schema'; +import { workflowFindRecordsActionSchema } from './find-records-action-schema'; +import { workflowFormActionSchema } from './form-action-schema'; +import { workflowHttpRequestActionSchema } from './http-request-action-schema'; +import { workflowIteratorActionSchema } from './iterator-action-schema'; +import { workflowSendEmailActionSchema } from './send-email-action-schema'; +import { workflowUpdateRecordActionSchema } from './update-record-action-schema'; + +export const workflowActionSchema = z.discriminatedUnion('type', [ + workflowCodeActionSchema, + workflowSendEmailActionSchema, + workflowCreateRecordActionSchema, + workflowUpdateRecordActionSchema, + workflowDeleteRecordActionSchema, + workflowFindRecordsActionSchema, + workflowFormActionSchema, + workflowHttpRequestActionSchema, + workflowAiAgentActionSchema, + workflowFilterActionSchema, + workflowIteratorActionSchema, + workflowEmptyActionSchema, +]); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-run-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-run-schema.ts new file mode 100644 index 00000000000..28eae417291 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/workflow-run-schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; +import { workflowRunStateSchema } from './workflow-run-state-schema'; +import { workflowRunStatusSchema } from './workflow-run-status-schema'; + +export const workflowRunSchema = z.looseObject({ + __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(), +}); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-run-state-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-run-state-schema.ts new file mode 100644 index 00000000000..dfd71b5be1a --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/workflow-run-state-schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; +import { workflowActionSchema } from './workflow-action-schema'; +import { workflowRunStateStepInfosSchema } from './workflow-run-state-step-infos-schema'; +import { workflowTriggerSchema } from './workflow-trigger-schema'; + +export const workflowRunStateSchema = z.object({ + flow: z.object({ + trigger: workflowTriggerSchema, + steps: z.array(workflowActionSchema), + }), + stepInfos: workflowRunStateStepInfosSchema, + workflowRunError: z.any().optional(), +}); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-info-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-info-schema.ts new file mode 100644 index 00000000000..5419f7009dc --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-info-schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { workflowRunStepStatusSchema } from './workflow-run-step-status-schema'; + +export const workflowRunStateStepInfoSchema = z.object({ + result: z.any().optional(), + error: z.any().optional(), + status: workflowRunStepStatusSchema, +}); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-infos-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-infos-schema.ts new file mode 100644 index 00000000000..3c86d0eaffb --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/workflow-run-state-step-infos-schema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +import { workflowRunStateStepInfoSchema } from './workflow-run-state-step-info-schema'; + +export const workflowRunStateStepInfosSchema = z.record( + z.string(), + workflowRunStateStepInfoSchema, +); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-run-status-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-run-status-schema.ts new file mode 100644 index 00000000000..e7067f8ffbe --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/workflow-run-status-schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const workflowRunStatusSchema = z.enum([ + 'NOT_STARTED', + 'RUNNING', + 'COMPLETED', + 'FAILED', + 'ENQUEUED', +]); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-run-step-status-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-run-step-status-schema.ts new file mode 100644 index 00000000000..2b3adb8d5f7 --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/workflow-run-step-status-schema.ts @@ -0,0 +1,4 @@ +import { z } from 'zod'; +import { StepStatus } from '../types/WorkflowRunStateStepInfos'; + +export const workflowRunStepStatusSchema = z.enum(StepStatus); diff --git a/packages/twenty-shared/src/workflow/schemas/workflow-trigger-schema.ts b/packages/twenty-shared/src/workflow/schemas/workflow-trigger-schema.ts new file mode 100644 index 00000000000..66006fe36bb --- /dev/null +++ b/packages/twenty-shared/src/workflow/schemas/workflow-trigger-schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; +import { workflowCronTriggerSchema } from './cron-trigger-schema'; +import { workflowDatabaseEventTriggerSchema } from './database-event-trigger-schema'; +import { workflowManualTriggerSchema } from './manual-trigger-schema'; +import { workflowWebhookTriggerSchema } from './webhook-trigger-schema'; + +export const workflowTriggerSchema = z.discriminatedUnion('type', [ + workflowDatabaseEventTriggerSchema, + workflowManualTriggerSchema, + workflowCronTriggerSchema, + workflowWebhookTriggerSchema, +]); diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index d36989f015c..2e0d9860ef8 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -39,7 +39,7 @@ "react-tooltip": "^5.13.1", "recoil": "^0.7.7", "twenty-shared": "workspace:*", - "zod": "3.23.8" + "zod": "^4.1.11" }, "scripts": { "build": "npx vite build" diff --git a/packages/twenty-zapier/package.json b/packages/twenty-zapier/package.json index 2a8e4bf5a13..5a11533c656 100644 --- a/packages/twenty-zapier/package.json +++ b/packages/twenty-zapier/package.json @@ -17,7 +17,7 @@ "dotenv": "^16.4.5", "libphonenumber-js": "^1.10.26", "zapier-platform-core": "15.5.1", - "zod": "3.23.8" + "zod": "^4.1.11" }, "devDependencies": { "jest": "29.7.0", diff --git a/yarn.lock b/yarn.lock index ce5a1c97798..dae40335434 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7044,12 +7044,14 @@ __metadata: languageName: node linkType: hard -"@hookform/resolvers@npm:^3.1.1": - version: 3.9.0 - resolution: "@hookform/resolvers@npm:3.9.0" +"@hookform/resolvers@npm:^5.2.2": + version: 5.2.2 + resolution: "@hookform/resolvers@npm:5.2.2" + dependencies: + "@standard-schema/utils": "npm:^0.3.0" peerDependencies: - react-hook-form: ^7.0.0 - checksum: 10c0/0e0e55f63abbd212cf14abbd39afad1f9b6105d6b25ce827fc651b624ed2be467ebe9b186026e0f032062db59ce2370b14e9583b436ae2d057738bdd6f04356c + react-hook-form: ^7.55.0 + checksum: 10c0/0692cd61dcc2a70cbb27b88a37f733c39e97f555c036ba04a81bd42b0467461cfb6bafacb46c16f173672f9c8a216bd7928a2330d4e49c700d130622bf1defaf languageName: node linkType: hard @@ -18828,6 +18830,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/utils@npm:^0.3.0": + version: 0.3.0 + resolution: "@standard-schema/utils@npm:0.3.0" + checksum: 10c0/6eb74cd13e52d5fc74054df51e37d947ef53f3ab9e02c085665dcca3c38c60ece8d735cebbdf18fbb13c775fbcb9becb3f53109b0e092a63f0f7389ce0993fd0 + languageName: node + linkType: hard + "@stitches/core@npm:^1.2.6": version: 1.2.8 resolution: "@stitches/core@npm:1.2.8" @@ -52362,7 +52371,7 @@ __metadata: tsx: "npm:^4.7.0" typescript: "npm:^5.3.0" yaml: "npm:^2.4.0" - zod: "npm:^3.22.0" + zod: "npm:^4.1.11" bin: twenty: dist/cli.js languageName: unknown @@ -52412,7 +52421,7 @@ __metadata: "@graphiql/plugin-explorer": "npm:^1.0.2" "@graphiql/react": "npm:^0.23.0" "@hello-pangea/dnd": "npm:^16.2.0" - "@hookform/resolvers": "npm:^3.1.1" + "@hookform/resolvers": "npm:^5.2.2" "@lingui/cli": "npm:^5.1.2" "@lingui/core": "npm:^5.1.2" "@lingui/detect-locale": "npm:^5.2.0" @@ -52747,7 +52756,7 @@ __metadata: vite: "npm:^7.0.0" vite-plugin-dts: "npm:3.8.1" vite-tsconfig-paths: "npm:^4.2.1" - zod: "npm:3.23.8" + zod: "npm:^4.1.11" languageName: unknown linkType: soft @@ -52787,7 +52796,7 @@ __metadata: tsx: "npm:^4.19.3" twenty-shared: "workspace:*" vite-plugin-svgr: "npm:^4.3.0" - zod: "npm:3.23.8" + zod: "npm:^4.1.11" languageName: unknown linkType: soft @@ -52834,7 +52843,7 @@ __metadata: rimraf: "npm:^3.0.2" zapier-platform-cli: "npm:^15.4.1" zapier-platform-core: "npm:15.5.1" - zod: "npm:3.23.8" + zod: "npm:^4.1.11" languageName: unknown linkType: soft @@ -53046,7 +53055,7 @@ __metadata: vite-tsconfig-paths: "npm:^4.2.1" vitest: "npm:1.4.0" xlsx-ugnis: "npm:^0.19.3" - zod: "npm:3.23.8" + zod: "npm:^4.1.11" languageName: unknown linkType: soft @@ -56244,14 +56253,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:3.23.8": - version: 3.23.8 - resolution: "zod@npm:3.23.8" - checksum: 10c0/8f14c87d6b1b53c944c25ce7a28616896319d95bc46a9660fe441adc0ed0a81253b02b5abdaeffedbeb23bdd25a0bf1c29d2c12dd919aef6447652dd295e3e69 - languageName: node - linkType: hard - -"zod@npm:^3.20.2, zod@npm:^3.22.0, zod@npm:^3.23.8": +"zod@npm:^3.20.2, zod@npm:^3.23.8": version: 3.25.76 resolution: "zod@npm:3.25.76" checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c