common api - null equivalence (#15926)
closes https://github.com/twentyhq/core-team-issues/issues/1629 To do before requesting review : - filter update Migration to come in an other PR Strat : 1/ Null transformation - [x] Transform NULL equivalent value to NULL in field validation in common api - pre-query - with feature flag - [ ] Same logic in ORM (Not done, complex to handle feature flag here) - [x] Transform NULL value to equivalent in data formatting in ORM - post-query 2/ Migration (in other PR) for fieldMetadata not nullable with default defaultValue (empty string, ...) - [ ] Remove NOT NULL db constraint - [ ] Update record value to NULL - [ ] Update field metadata : isNullable:true - [ ] Update uniqueIndex whereClause (also for standard uniqueIndex) - [ ] Activate feature flag 3/ Update metadata creation - [x] No more default default value - [x] Update standard field nullability - [x] Remove index default whereClause for standard field 4/ Update filter - [x] When filtering on NULL or empty string, be sure all records are returned (the one with NULL + the one with "") 5/ Test - [ ] Strat. to do
This commit is contained in:
+3
-13
@@ -1,22 +1,12 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable';
|
||||
|
||||
describe('generateNullable', () => {
|
||||
it('should generate a nullable value false for TEXT, EMAIL, PHONE no matter what the input is', () => {
|
||||
expect(generateNullable(FieldMetadataType.TEXT, false)).toEqual(false);
|
||||
|
||||
expect(generateNullable(FieldMetadataType.TEXT, true)).toEqual(false);
|
||||
|
||||
expect(generateNullable(FieldMetadataType.TEXT)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should should return true if no input is given', () => {
|
||||
expect(generateNullable(FieldMetadataType.DATE_TIME)).toEqual(true);
|
||||
expect(generateNullable()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should should return the input value if the input value is given', () => {
|
||||
expect(generateNullable(FieldMetadataType.DATE_TIME, true)).toEqual(true);
|
||||
expect(generateNullable(FieldMetadataType.DATE_TIME, false)).toEqual(false);
|
||||
expect(generateNullable(true)).toEqual(true);
|
||||
expect(generateNullable(false)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { isValidUniqueFieldDefaultValueCombination } from 'src/engine/metadata-modules/field-metadata/utils/is-valid-unique-input.util';
|
||||
|
||||
describe('isValidUniqueFieldDefaultValueCombination', () => {
|
||||
it('should return true if the field has a custom default value and is not unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "'custom value'",
|
||||
isUnique: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the field has standard default value and is unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "''",
|
||||
isUnique: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the field has custom default value and is unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "'custom value'",
|
||||
isUnique: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+1
-42
@@ -1,4 +1,4 @@
|
||||
import { FieldMetadataType, FieldActorSource } from 'twenty-shared/types';
|
||||
import { FieldActorSource, FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
@@ -7,47 +7,6 @@ export function generateDefaultValue(
|
||||
type: FieldMetadataType,
|
||||
): FieldMetadataDefaultValue {
|
||||
switch (type) {
|
||||
case FieldMetadataType.TEXT:
|
||||
return "''" satisfies FieldMetadataDefaultValue<FieldMetadataType.TEXT>;
|
||||
case FieldMetadataType.EMAILS:
|
||||
return {
|
||||
primaryEmail: "''",
|
||||
additionalEmails: null,
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.EMAILS>;
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
return {
|
||||
firstName: "''",
|
||||
lastName: "''",
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.FULL_NAME>;
|
||||
case FieldMetadataType.ADDRESS:
|
||||
return {
|
||||
addressStreet1: "''",
|
||||
addressStreet2: "''",
|
||||
addressCity: "''",
|
||||
addressState: "''",
|
||||
addressCountry: "''",
|
||||
addressPostcode: "''",
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.ADDRESS>;
|
||||
case FieldMetadataType.CURRENCY:
|
||||
return {
|
||||
amountMicros: null,
|
||||
currencyCode: "''",
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.CURRENCY>;
|
||||
case FieldMetadataType.LINKS:
|
||||
return {
|
||||
primaryLinkLabel: "''",
|
||||
primaryLinkUrl: "''",
|
||||
secondaryLinks: null,
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.LINKS>;
|
||||
case FieldMetadataType.PHONES:
|
||||
return {
|
||||
primaryPhoneNumber: "''",
|
||||
primaryPhoneCountryCode: "''",
|
||||
primaryPhoneCallingCode: "''",
|
||||
additionalPhones: null,
|
||||
} satisfies FieldMetadataDefaultValue<FieldMetadataType.PHONES>;
|
||||
case FieldMetadataType.ACTOR:
|
||||
return {
|
||||
source: `'${FieldActorSource.MANUAL}'`,
|
||||
|
||||
+1
-8
@@ -1,6 +1,4 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
export function generateNullable(
|
||||
type: FieldMetadataType,
|
||||
inputNullableValue?: boolean,
|
||||
isRemoteCreation?: boolean,
|
||||
): boolean {
|
||||
@@ -8,10 +6,5 @@ export function generateNullable(
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case FieldMetadataType.TEXT:
|
||||
return false;
|
||||
default:
|
||||
return inputNullableValue ?? true;
|
||||
}
|
||||
return inputNullableValue ?? true;
|
||||
}
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import {
|
||||
compositeTypeDefinitions,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
export const isValidUniqueFieldDefaultValueCombination = ({
|
||||
defaultValue,
|
||||
isUnique,
|
||||
type,
|
||||
}: {
|
||||
defaultValue: FieldMetadataDefaultValue;
|
||||
isUnique: boolean;
|
||||
type: FieldMetadataType;
|
||||
}) => {
|
||||
if (!isUnique) return true;
|
||||
|
||||
const defaultDefaultValue = generateDefaultValue(type);
|
||||
|
||||
if (!isCompositeFieldMetadataType(type))
|
||||
return defaultValue === defaultDefaultValue;
|
||||
|
||||
const doUniquePropertiesHaveDefaultValues =
|
||||
compositeTypeDefinitions
|
||||
.get(type)
|
||||
?.properties.filter((property) => property.isIncludedInUniqueConstraint)
|
||||
.every(
|
||||
({ name }) =>
|
||||
(defaultValue as Record<string, string | null>)?.[name] ===
|
||||
(defaultDefaultValue as Record<string, string | null>)?.[name],
|
||||
) ?? false;
|
||||
|
||||
return doUniquePropertiesHaveDefaultValues;
|
||||
};
|
||||
-1
@@ -27,7 +27,6 @@ export const prepareCustomFieldMetadataForCreation = (
|
||||
objectMetadataId: fieldMetadataInput.objectMetadataId,
|
||||
workspaceId: fieldMetadataInput.workspaceId,
|
||||
isNullable: generateNullable(
|
||||
fieldMetadataInput.type,
|
||||
fieldMetadataInput.isNullable,
|
||||
fieldMetadataInput.isRemoteCreation,
|
||||
),
|
||||
|
||||
-1
@@ -34,7 +34,6 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
isCustom: true,
|
||||
isLabelSyncedWithName: createFieldInput.isLabelSyncedWithName ?? false,
|
||||
isNullable: generateNullable(
|
||||
createFieldInput.type,
|
||||
createFieldInput.isNullable,
|
||||
createFieldInput.isRemoteCreation,
|
||||
),
|
||||
|
||||
+1
-3
@@ -3,9 +3,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import {
|
||||
type FieldMetadataType,
|
||||
type CompositeType,
|
||||
compositeTypeDefinitions,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, type QueryRunner, Repository } from 'typeorm';
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from 'src/engine/metadata-modules/index-metadata/index-field-metadata.exception';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { type IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
@@ -197,7 +196,6 @@ export class IndexMetadataService {
|
||||
computeObjectTargetTable(objectMetadata),
|
||||
updatedFieldMetadata.name,
|
||||
])}`,
|
||||
indexWhereClause: computeUniqueIndexWhereClause(updatedFieldMetadata),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
describe('computeUniqueIndexWhereClause', () => {
|
||||
it('should return undefined if standard default value is not defined', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'testField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return a where clause for a an atomic type field', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'testTextField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBe('"testTextField" != \'\'');
|
||||
});
|
||||
|
||||
it('should return a where clause for a composite type field', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.EMAILS,
|
||||
name: 'testEmailsField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBe('"testEmailsFieldPrimaryEmail" != \'\'');
|
||||
});
|
||||
});
|
||||
+10
-10
@@ -11,19 +11,19 @@ import {
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import {
|
||||
IndexMetadataException,
|
||||
IndexMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/index-metadata/index-field-metadata.exception';
|
||||
|
||||
//TODO : To delete once IS_NULL_EQUIVALENCE_ENABLED feature flag removed
|
||||
export const computeUniqueIndexWhereClause = (
|
||||
fieldMetadata: Pick<FieldMetadataEntity, 'type' | 'name'>,
|
||||
fieldMetadata: Pick<FieldMetadataEntity, 'type' | 'name' | 'defaultValue'>,
|
||||
) => {
|
||||
const defaultDefaultValue = generateDefaultValue(fieldMetadata.type);
|
||||
const defaultValue = fieldMetadata.defaultValue;
|
||||
|
||||
if (!isDefined(defaultDefaultValue)) return;
|
||||
if (!isDefined(defaultValue)) return;
|
||||
|
||||
if (
|
||||
fieldMetadata.type === FieldMetadataType.RELATION ||
|
||||
@@ -36,7 +36,7 @@ export const computeUniqueIndexWhereClause = (
|
||||
}
|
||||
|
||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
return `"${fieldMetadata.name}" != ${defaultDefaultValue}`;
|
||||
return `"${fieldMetadata.name}" != ${defaultValue}`;
|
||||
}
|
||||
|
||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
||||
@@ -48,7 +48,7 @@ export const computeUniqueIndexWhereClause = (
|
||||
);
|
||||
}
|
||||
|
||||
const defaultDefaultValueProperties = Object.keys(defaultDefaultValue);
|
||||
const defaultDefaultValueProperties = Object.keys(defaultValue);
|
||||
|
||||
const columnNamesWithDefaultValues = compositeType.properties
|
||||
.filter(
|
||||
@@ -57,13 +57,13 @@ export const computeUniqueIndexWhereClause = (
|
||||
defaultDefaultValueProperties.includes(property.name),
|
||||
)
|
||||
.map((property) => {
|
||||
const defaultValue =
|
||||
defaultDefaultValue[property.name as keyof typeof defaultDefaultValue];
|
||||
const defaultValueProperty =
|
||||
defaultValue[property.name as keyof typeof defaultValue];
|
||||
|
||||
if (isNonEmptyString(defaultValue)) {
|
||||
if (isNonEmptyString(defaultValueProperty)) {
|
||||
return [
|
||||
computeCompositeColumnName(fieldMetadata, property),
|
||||
defaultValue,
|
||||
defaultValueProperty,
|
||||
];
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user