Common - Field validation (#15491)

Closes : https://github.com/twentyhq/core-team-issues/issues/1622

To do in other PR : 
- Add migration command for non nullable text, raw_json & array fields
- Add null transformation

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Etienne
2025-11-13 15:45:35 +01:00
committed by GitHub
parent f6ed1e8295
commit 261cba40ef
114 changed files with 3524 additions and 1189 deletions
@@ -32,9 +32,9 @@ describe('computeSchemaComponents', () => {
"lastName": "Osinski",
},
"fieldLinks": {
"additionalLinks": [],
"primaryLinkLabel": "",
"primaryLinkUrl": "https://narrow-help.net/",
"secondaryLinks": [],
},
"fieldMultiSelect": [
"OPTION_1",
@@ -46,9 +46,7 @@ describe('computeSchemaComponents', () => {
"primaryPhoneCountryCode": "FR",
"primaryPhoneNumber": "06 10 20 30 40",
},
"fieldSelect": [
"OPTION_1",
],
"fieldSelect": "OPTION_1",
},
"properties": {
"fieldActor": {
@@ -535,9 +533,9 @@ describe('computeSchemaComponents', () => {
"lastName": "Jones",
},
"fieldLinks": {
"additionalLinks": [],
"primaryLinkLabel": "",
"primaryLinkUrl": "https://unlawful-blowgun.biz",
"secondaryLinks": [],
},
"fieldMultiSelect": [
"OPTION_1",
@@ -549,9 +547,7 @@ describe('computeSchemaComponents', () => {
"primaryPhoneCountryCode": "FR",
"primaryPhoneNumber": "06 10 20 30 40",
},
"fieldSelect": [
"OPTION_1",
],
"fieldSelect": "OPTION_1",
},
"properties": {
"fieldActor": {
@@ -58,7 +58,7 @@ export const generateRandomFieldValue = ({
return {
primaryLinkLabel: '',
primaryLinkUrl: faker.internet.url(),
additionalLinks: [],
secondaryLinks: [],
};
}
@@ -82,10 +82,10 @@ export const generateRandomFieldValue = ({
case FieldMetadataType.SELECT: {
if (!isDefined(field.options) || !isDefined(field.options[0].value)) {
return [];
return null;
}
return [field.options[0].value];
return field.options[0].value;
}
case FieldMetadataType.MULTI_SELECT: {
@@ -2,15 +2,16 @@ import { Injectable } from '@nestjs/common';
import {
FieldMetadataType,
ObjectRecord,
compositeTypeDefinitions,
type RichTextV2Metadata,
richTextV2ValueSchema,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { transformEmailsValue } from 'src/engine/core-modules/record-transformer/utils/transform-emails-value.util';
import { transformLinksValue } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util';
import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
@Injectable()
export class RecordInputTransformerService {
@@ -18,15 +19,9 @@ export class RecordInputTransformerService {
recordInput,
objectMetadataMapItem,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
recordInput: Record<string, any>;
recordInput: Partial<ObjectRecord>;
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}): Promise<Record<string, any>> {
if (!recordInput) {
return recordInput;
}
}): Promise<Partial<ObjectRecord>> {
let transformedEntries = {};
for (const [key, value] of Object.entries(recordInput)) {
@@ -72,11 +67,11 @@ export class RecordInputTransformerService {
'Rich text is not supported, please use RICH_TEXT_V2 instead',
);
case FieldMetadataType.RICH_TEXT_V2:
return this.transformRichTextV2Value(value);
return await transformRichTextV2Value(value);
case FieldMetadataType.LINKS:
return transformLinksValue(value);
case FieldMetadataType.EMAILS:
return this.transformEmailsValue(value);
return transformEmailsValue(value);
case FieldMetadataType.PHONES:
return transformPhonesValue({ input: value });
default:
@@ -84,73 +79,6 @@ export class RecordInputTransformerService {
}
}
private async transformRichTextV2Value(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
richTextValue: any,
): Promise<RichTextV2Metadata> {
const parsedValue = richTextV2ValueSchema.parse(richTextValue);
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
// Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks)
// Todo : This may be resolved once the server-utils library is updated with proper conversion support - #947
let convertedMarkdown: string | null = null;
try {
convertedMarkdown = isDefined(parsedValue.blocknote)
? await serverBlockNoteEditor.blocksToMarkdownLossy(
JSON.parse(parsedValue.blocknote),
)
: null;
} catch {
convertedMarkdown = parsedValue.blocknote || null;
}
const convertedBlocknote = parsedValue.markdown
? JSON.stringify(
await serverBlockNoteEditor.tryParseMarkdownToBlocks(
parsedValue.markdown,
),
)
: null;
return {
markdown: parsedValue.markdown || convertedMarkdown,
blocknote: parsedValue.blocknote || convertedBlocknote,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private transformEmailsValue(value: any): any {
if (!value) {
return value;
}
let additionalEmails = value?.additionalEmails;
const primaryEmail = value?.primaryEmail
? value.primaryEmail.toLowerCase()
: '';
if (additionalEmails) {
try {
const emailArray = JSON.parse(additionalEmails) as string[];
additionalEmails = JSON.stringify(
emailArray.map((email) => email.toLowerCase()),
);
} catch {
/* empty */
}
}
return {
primaryEmail,
additionalEmails,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private stringifySubFields(fieldMetadataType: FieldMetadataType, value: any) {
const compositeType = compositeTypeDefinitions.get(fieldMetadataType);
@@ -0,0 +1,34 @@
import { isNonEmptyString } from '@sniptt/guards';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const transformEmailsValue = (value: any): any => {
if (!value) {
return value;
}
let additionalEmails = value?.additionalEmails;
const primaryEmail = value?.primaryEmail
? value.primaryEmail.toLowerCase()
: '';
if (additionalEmails) {
try {
const emailArray = (
isNonEmptyString(additionalEmails)
? JSON.parse(additionalEmails)
: additionalEmails
) as string[];
additionalEmails = JSON.stringify(
emailArray.map((email) => email.toLowerCase()),
);
} catch {
/* empty */
}
}
return {
primaryEmail,
additionalEmails,
};
};
@@ -32,7 +32,7 @@ export const transformLinksValue = (
const secondaryLinksArray = isNonEmptyString(secondaryLinksRaw)
? parseJson<LinkMetadataNullable[]>(secondaryLinksRaw)
: null;
: secondaryLinksRaw;
const { primaryLinkLabel, primaryLinkUrl, secondaryLinks } = removeEmptyLinks(
{
@@ -0,0 +1,46 @@
import { isNonEmptyString } from '@sniptt/guards';
import {
type RichTextV2Metadata,
richTextV2ValueSchema,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const transformRichTextV2Value = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
richTextValue: any,
): Promise<RichTextV2Metadata> => {
const parsedValue = isNonEmptyString(richTextValue)
? richTextV2ValueSchema.parse(richTextValue)
: richTextValue;
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
// Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks)
// Todo : This may be resolved once the server-utils library is updated with proper conversion support - #947
let convertedMarkdown: string | null = null;
try {
convertedMarkdown = isDefined(parsedValue.blocknote)
? await serverBlockNoteEditor.blocksToMarkdownLossy(
JSON.parse(parsedValue.blocknote),
)
: null;
} catch {
convertedMarkdown = parsedValue.blocknote || null;
}
const convertedBlocknote = parsedValue.markdown
? JSON.stringify(
await serverBlockNoteEditor.tryParseMarkdownToBlocks(
parsedValue.markdown,
),
)
: null;
return {
markdown: parsedValue.markdown || convertedMarkdown,
blocknote: parsedValue.blocknote || convertedBlocknote,
};
};