Integration testing v2 enum field types fail and success path (#14010)
# Introduction Migrating and improving performances of field enum integrations tests success and failing tests cases to be using the new v2 api ## Discovered issue When deleting an object in v1 it will leave related enums until the object is re-created Something not done anymore within the create in v2 but in the delete operation We should implem an upgrade command to remove such relicas ## Bugs - Update/create default value multi select runner wrong sql query -> FIX - Update default value multi select regression, we should allow option without an id to be inserted -> FIX - default value compare dynamic json stringify convertion or not in compare tools for object and fields
This commit is contained in:
@@ -21,6 +21,7 @@ const jestConfig: JestConfigWithTsJest = {
|
||||
prettierPath: null,
|
||||
silent: false,
|
||||
errorOnDeprecated: true,
|
||||
maxConcurrency: 1,
|
||||
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||
rootDir: '.',
|
||||
testEnvironment: 'node',
|
||||
|
||||
+1
-5
@@ -36,7 +36,6 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
|
||||
import { ViewModule } from 'src/modules/view/view.module';
|
||||
import { WorkspaceMigrationBuilderExceptionV2Interceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor';
|
||||
|
||||
import { FieldMetadataEntity } from './field-metadata.entity';
|
||||
|
||||
@@ -101,10 +100,7 @@ import { FieldMetadataService } from './services/field-metadata.service';
|
||||
},
|
||||
delete: { disabled: true },
|
||||
guards: [WorkspaceAuthGuard],
|
||||
interceptors: [
|
||||
WorkspaceMigrationBuilderExceptionV2Interceptor,
|
||||
FieldMetadataGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
interceptors: [FieldMetadataGraphqlApiExceptionInterceptor],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
+2
-6
@@ -93,18 +93,14 @@ export class FieldMetadataResolver {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
try {
|
||||
try {
|
||||
if (isWorkspaceMigrationV2Enabled) {
|
||||
return await this.fieldMetadataServiceV2.updateOne({
|
||||
updateFieldInput: { ...input.update, id: input.id },
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedInput = (await this.beforeUpdateOneField.run(input, {
|
||||
workspaceId,
|
||||
locale: context.req.locale,
|
||||
|
||||
+1
-3
@@ -61,7 +61,5 @@ export function computeCompositeColumnName<T extends FieldMetadataType>(
|
||||
);
|
||||
}
|
||||
|
||||
return `${fieldMetadataOrFieldName.name}${pascalCase(
|
||||
compositeProperty.name,
|
||||
)}`;
|
||||
return generateName(fieldMetadataOrFieldName.name);
|
||||
}
|
||||
|
||||
+6
@@ -11,8 +11,14 @@ import {
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
|
||||
|
||||
export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
|
||||
workspaceMigrationBuilderExceptionV2Formatter(error);
|
||||
}
|
||||
|
||||
if (error instanceof InvalidMetadataException) {
|
||||
throw new UserInputError(error);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
export const FLAT_FIELD_METADATA_JSONB_PROPERTIES = [
|
||||
'defaultValue',
|
||||
'options',
|
||||
'settings',
|
||||
'standardOverrides',
|
||||
'defaultValue',
|
||||
] as const satisfies (keyof FlatFieldMetadata)[];
|
||||
|
||||
+2
-2
@@ -92,8 +92,8 @@ export class FlatFieldMetadataValidatorService {
|
||||
}
|
||||
|
||||
const updates = compareTwoFlatFieldMetadata({
|
||||
from: existingFlatFieldMetadataToUpdate,
|
||||
to: updatedFlatFieldMetadata,
|
||||
fromFlatFieldMetadata: existingFlatFieldMetadataToUpdate,
|
||||
toFlatFieldMetadata: updatedFlatFieldMetadata,
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
+16
-31
@@ -1,36 +1,28 @@
|
||||
import diff from 'microdiff';
|
||||
import { FieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
|
||||
import { FLAT_FIELD_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant';
|
||||
import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant';
|
||||
import { FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-relation-properties-to-compare.constant';
|
||||
import { type FlatFieldMetadataEntityJsonbProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-jsonb-properties.type';
|
||||
import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
|
||||
import { type FlatFieldMetadataRelationPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-relation-properties-to-compare.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataJsonbProperty } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-jsonb-property.util';
|
||||
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
|
||||
import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
|
||||
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
|
||||
|
||||
const shouldNotOverrideDefaultValue = (type: FieldMetadataType) => {
|
||||
return [
|
||||
FieldMetadataType.BOOLEAN,
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.CURRENCY,
|
||||
FieldMetadataType.PHONES,
|
||||
FieldMetadataType.ADDRESS,
|
||||
].includes(type);
|
||||
};
|
||||
|
||||
type GetWorkspaceMigrationUpdateFieldActionArgs = FromTo<FlatFieldMetadata>;
|
||||
type GetWorkspaceMigrationUpdateFieldActionArgs = FromTo<
|
||||
FlatFieldMetadata,
|
||||
'FlatFieldMetadata'
|
||||
>;
|
||||
/**
|
||||
* This comparator handles update on colliding uniqueIdentifier flatFieldMetadata
|
||||
*/
|
||||
export const compareTwoFlatFieldMetadata = ({
|
||||
from,
|
||||
to,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
}: GetWorkspaceMigrationUpdateFieldActionArgs) => {
|
||||
const transformMetadataForComparisonParameters = {
|
||||
shouldIgnoreProperty: (
|
||||
@@ -45,14 +37,6 @@ export const compareTwoFlatFieldMetadata = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
property === 'defaultValue' &&
|
||||
isDefined(fieldMetadata.type) &&
|
||||
shouldNotOverrideDefaultValue(fieldMetadata.type)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove below assertion when we authorize relation edition, see https://github.com/twentyhq/twenty/commit/39f6f3c4bb101272a9014e142a842d0801a3c33b
|
||||
const isRelationFieldType =
|
||||
isDefined(fieldMetadata.type) &&
|
||||
@@ -80,11 +64,11 @@ export const compareTwoFlatFieldMetadata = ({
|
||||
propertiesToStringify: FLAT_FIELD_METADATA_JSONB_PROPERTIES,
|
||||
};
|
||||
const fromCompare = transformMetadataForComparison(
|
||||
from,
|
||||
fromFlatFieldMetadata,
|
||||
transformMetadataForComparisonParameters,
|
||||
);
|
||||
const toCompare = transformMetadataForComparison(
|
||||
to,
|
||||
toFlatFieldMetadata,
|
||||
transformMetadataForComparisonParameters,
|
||||
);
|
||||
|
||||
@@ -97,14 +81,15 @@ export const compareTwoFlatFieldMetadata = ({
|
||||
case 'CHANGE': {
|
||||
const { oldValue, path, value } = difference;
|
||||
const property = path[0] as FlatFieldMetadataPropertiesToCompare;
|
||||
const isJsonb = FLAT_FIELD_METADATA_JSONB_PROPERTIES.includes(
|
||||
property as FlatFieldMetadataEntityJsonbProperties,
|
||||
);
|
||||
const isJsonb = isFlatFieldMetadataJsonbProperty({
|
||||
flatFieldMetadata: toFlatFieldMetadata,
|
||||
property,
|
||||
});
|
||||
|
||||
if (isJsonb) {
|
||||
return {
|
||||
from: isDefined(oldValue) ? JSON.parse(oldValue) : oldValue,
|
||||
to: isDefined(value) ? JSON.parse(value) : value,
|
||||
from: parseJson(oldValue),
|
||||
to: parseJson(value),
|
||||
property,
|
||||
};
|
||||
}
|
||||
|
||||
+1
@@ -100,6 +100,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
|
||||
const options = (createFieldInput?.options ?? []).map<
|
||||
FieldMetadataOptions<typeof createFieldInput.type>[number]
|
||||
>((option) => ({
|
||||
id: v4(),
|
||||
...trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
option as FieldMetadataOptions<typeof createFieldInput.type>[number],
|
||||
['label', 'value', 'id', 'color'],
|
||||
|
||||
+10
-5
@@ -3,6 +3,7 @@ import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant';
|
||||
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
@@ -132,14 +133,18 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
|
||||
const updatedFlatFieldMetadata = fieldMetadataEditableProperties.reduce(
|
||||
(acc, property) => {
|
||||
const isPropertyUpdated =
|
||||
updatedEditableFieldProperties[property] !== undefined;
|
||||
let newValue = updatedEditableFieldProperties[property];
|
||||
|
||||
if (property === 'options' && isDefined(newValue)) {
|
||||
newValue = updatedEditableFieldProperties[property]?.map((option) => ({
|
||||
id: v4(),
|
||||
...option,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...(isPropertyUpdated
|
||||
? { [property]: updatedEditableFieldProperties[property] }
|
||||
: {}),
|
||||
...(newValue !== undefined ? { [property]: newValue } : {}),
|
||||
};
|
||||
},
|
||||
relatedFlatFieldMetadata,
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FLAT_FIELD_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-jsonb-properties.constant';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataEntityOfTypes } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-types.util';
|
||||
|
||||
export const isFlatFieldMetadataJsonbProperty = ({
|
||||
flatFieldMetadata,
|
||||
property,
|
||||
}: {
|
||||
flatFieldMetadata: FlatFieldMetadata;
|
||||
property: keyof FlatFieldMetadata;
|
||||
}): boolean => {
|
||||
if (property === 'defaultValue') {
|
||||
return isFlatFieldMetadataEntityOfTypes(flatFieldMetadata, [
|
||||
FieldMetadataType.LINKS,
|
||||
FieldMetadataType.PHONES,
|
||||
FieldMetadataType.EMAILS,
|
||||
FieldMetadataType.CURRENCY,
|
||||
FieldMetadataType.FULL_NAME,
|
||||
FieldMetadataType.ADDRESS,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.ACTOR,
|
||||
FieldMetadataType.ARRAY,
|
||||
]);
|
||||
}
|
||||
|
||||
const isJsonB = FLAT_FIELD_METADATA_JSONB_PROPERTIES.includes(
|
||||
property as (typeof FLAT_FIELD_METADATA_JSONB_PROPERTIES)[number],
|
||||
);
|
||||
|
||||
return isJsonB;
|
||||
};
|
||||
+47
-10
@@ -45,12 +45,31 @@ const validateMetadataOptionId = (sanitizedId?: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const validateMetadataOptionLabel = (sanitizedLabel: string) => {
|
||||
const validateMetadataOptionLabel = (
|
||||
sanitizedLabel: string,
|
||||
): FlatFieldMetadataValidationError[] => {
|
||||
if (!isDefined(sanitizedLabel)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option label is required`,
|
||||
userFriendlyMessage: t`Option label is required`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(sanitizedLabel)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option label must be a string of at least one character`,
|
||||
userFriendlyMessage: t`Option label format not supported`,
|
||||
value: sanitizedLabel,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const validators: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
validator: (label) => !isDefined(label),
|
||||
message: t`Option label is required`,
|
||||
},
|
||||
{
|
||||
validator: exceedsDatabaseIdentifierMaximumLength,
|
||||
message: t`Option label exceeds 63 characters`,
|
||||
@@ -75,12 +94,30 @@ const validateMetadataOptionLabel = (sanitizedLabel: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const validateMetadataOptionValue = (sanitizedValue: string) => {
|
||||
const validateMetadataOptionValue = (
|
||||
sanitizedValue: string,
|
||||
): FlatFieldMetadataValidationError[] => {
|
||||
if (!isDefined(sanitizedValue)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option value is required`,
|
||||
userFriendlyMessage: t`Option value is required`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(sanitizedValue)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option value must be a string of at least one character`,
|
||||
userFriendlyMessage: t`Option value format not supported`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const validators: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
validator: (value) => !isDefined(value),
|
||||
message: t`Option value is required`,
|
||||
},
|
||||
{
|
||||
validator: exceedsDatabaseIdentifierMaximumLength,
|
||||
message: t`Option value exceeds 63 characters`,
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import omit from 'lodash.omit';
|
||||
import diff from 'microdiff';
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { assertUnreachable, parseJson } from 'twenty-shared/utils';
|
||||
|
||||
import { FLAT_OBJECT_METADATA_JSONB_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-jsonb-properties.constant';
|
||||
import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant';
|
||||
@@ -63,8 +63,8 @@ export const compareTwoFlatObjectMetadata = ({
|
||||
|
||||
if (isJsonb) {
|
||||
return {
|
||||
from: isDefined(oldValue) ? JSON.parse(oldValue) : oldValue,
|
||||
to: isDefined(value) ? JSON.parse(value) : value,
|
||||
from: parseJson(oldValue),
|
||||
to: parseJson(value),
|
||||
property,
|
||||
};
|
||||
}
|
||||
|
||||
+1
-5
@@ -44,7 +44,6 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceMigrationBuilderExceptionV2Interceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2.interceptor';
|
||||
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
|
||||
|
||||
@Module({
|
||||
@@ -100,10 +99,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
guards: [WorkspaceAuthGuard],
|
||||
interceptors: [
|
||||
WorkspaceMigrationBuilderExceptionV2Interceptor,
|
||||
ObjectMetadataGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
interceptors: [ObjectMetadataGraphqlApiExceptionInterceptor],
|
||||
filters: [PermissionsGraphqlApiExceptionFilter],
|
||||
},
|
||||
],
|
||||
|
||||
+6
@@ -12,8 +12,14 @@ import {
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
|
||||
|
||||
export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
|
||||
workspaceMigrationBuilderExceptionV2Formatter(error);
|
||||
}
|
||||
|
||||
if (error instanceof InvalidMetadataException) {
|
||||
throw new UserInputError(error);
|
||||
}
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
|
||||
fieldMetadataType: Type,
|
||||
// Should be columnType
|
||||
): string => {
|
||||
/**
|
||||
* Composite types are not implemented here, as they are flattened by their composite definitions.
|
||||
|
||||
+14
-13
@@ -1,8 +1,7 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
import { type ColumnType, type QueryRunner } from 'typeorm';
|
||||
|
||||
import { type WorkspaceSchemaColumnDefinition } from 'src/engine/twenty-orm/workspace-schema-manager/types/workspace-schema-column-definition.type';
|
||||
import { buildSqlColumnDefinition } from 'src/engine/twenty-orm/workspace-schema-manager/utils/build-sql-column-definition.util';
|
||||
import { sanitizeDefaultValue } from 'src/engine/twenty-orm/workspace-schema-manager/utils/sanitize-default-value.util';
|
||||
import { removeSqlDDLInjection } from 'src/engine/workspace-manager/workspace-migration-runner/utils/remove-sql-injection.util';
|
||||
|
||||
export class WorkspaceSchemaColumnManagerService {
|
||||
@@ -88,23 +87,25 @@ export class WorkspaceSchemaColumnManagerService {
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
defaultValue?: string | number | boolean | null;
|
||||
columnType?: ColumnType;
|
||||
}): Promise<void> {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
const safeColumnName = removeSqlDDLInjection(columnName);
|
||||
let sql: string;
|
||||
|
||||
if (defaultValue !== undefined) {
|
||||
if (typeof defaultValue === 'string') {
|
||||
const safeDefaultValue = sanitizeDefaultValue(defaultValue);
|
||||
|
||||
sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ALTER COLUMN "${safeColumnName}" SET DEFAULT ${safeDefaultValue}`;
|
||||
} else {
|
||||
sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ALTER COLUMN "${safeColumnName}" SET DEFAULT ${defaultValue}`;
|
||||
const computeDefaultValueSqlQuery = () => {
|
||||
if (defaultValue === undefined) {
|
||||
return `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ALTER COLUMN "${safeColumnName}" DROP DEFAULT`;
|
||||
}
|
||||
} else {
|
||||
sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ALTER COLUMN "${safeColumnName}" DROP DEFAULT`;
|
||||
}
|
||||
|
||||
if (defaultValue === null) {
|
||||
return `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ALTER COLUMN "${safeColumnName}" SET DEFAULT NULL`;
|
||||
}
|
||||
|
||||
return `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ALTER COLUMN "${safeColumnName}" SET DEFAULT ${defaultValue}`;
|
||||
};
|
||||
|
||||
const sql = computeDefaultValueSqlQuery();
|
||||
|
||||
await queryRunner.query(sql);
|
||||
}
|
||||
|
||||
+1
-4
@@ -1,7 +1,6 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkspaceSchemaColumnDefinition } from 'src/engine/twenty-orm/workspace-schema-manager/types/workspace-schema-column-definition.type';
|
||||
import { sanitizeDefaultValue } from 'src/engine/twenty-orm/workspace-schema-manager/utils/sanitize-default-value.util';
|
||||
import { removeSqlDDLInjection } from 'src/engine/workspace-manager/workspace-migration-runner/utils/remove-sql-injection.util';
|
||||
|
||||
export const buildSqlColumnDefinition = (
|
||||
@@ -32,9 +31,7 @@ export const buildSqlColumnDefinition = (
|
||||
}
|
||||
|
||||
if (isDefined(column.default) && column.type !== 'tsvector') {
|
||||
const safeDefault = sanitizeDefaultValue(column.default);
|
||||
|
||||
parts.push(`DEFAULT ${safeDefault}`);
|
||||
parts.push(`DEFAULT ${column.default}`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
ErrorCode,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { type WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { fromWorkspaceMigrationBuilderExceptionToValidationResponseError } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-validation-response-error.util';
|
||||
|
||||
export const workspaceMigrationBuilderExceptionV2Formatter = (
|
||||
error: WorkspaceMigrationBuilderExceptionV2,
|
||||
) => {
|
||||
const { errors, summary } =
|
||||
fromWorkspaceMigrationBuilderExceptionToValidationResponseError(error);
|
||||
|
||||
throw new BaseGraphQLError(error.message, ErrorCode.BAD_USER_INPUT, {
|
||||
code: 'METADATA_VALIDATION_ERROR',
|
||||
errors,
|
||||
summary,
|
||||
message: `Validation failed for ${summary.invalidObjects} object(s) and ${summary.invalidFields} field(s)`,
|
||||
userFriendlyMessage: t`Validation failed for ${summary.invalidObjects} object(s) and ${summary.invalidFields} field(s)`,
|
||||
});
|
||||
};
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { catchError, type Observable } from 'rxjs';
|
||||
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
ErrorCode,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { fromWorkspaceMigrationBuilderExceptionToValidationResponseError } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/from-workspace-migration-builder-exception-to-validation-response-error.util';
|
||||
|
||||
export class WorkspaceMigrationBuilderExceptionV2Interceptor
|
||||
implements NestInterceptor
|
||||
{
|
||||
intercept(_: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
return next.handle().pipe(
|
||||
catchError((error) => {
|
||||
if (!(error instanceof WorkspaceMigrationBuilderExceptionV2)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { errors, summary } =
|
||||
fromWorkspaceMigrationBuilderExceptionToValidationResponseError(
|
||||
error,
|
||||
);
|
||||
|
||||
throw new BaseGraphQLError(error.message, ErrorCode.BAD_USER_INPUT, {
|
||||
code: 'METADATA_VALIDATION_ERROR',
|
||||
errors,
|
||||
summary,
|
||||
message: `Validation failed for ${summary.invalidObjects} object(s) and ${summary.invalidFields} field(s)`,
|
||||
userFriendlyMessage: t`Validation failed for ${summary.invalidObjects} object(s) and ${summary.invalidFields} field(s)`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -68,8 +68,8 @@ export class WorkspaceMigrationV2FieldActionsBuilderService {
|
||||
to: toFlatFieldMetadata,
|
||||
} of updatedFlatFieldMetadatas) {
|
||||
const updates = compareTwoFlatFieldMetadata({
|
||||
from: fromFlatFieldMetadata,
|
||||
to: toFlatFieldMetadata,
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
});
|
||||
|
||||
if (updates.length === 0) {
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ export type CreatedDeletedUpdatedObjectMetadataInputMatrix = FromTo<
|
||||
FlatObjectMetadata
|
||||
> & {
|
||||
buildOptions: WorkspaceMigrationV2BuilderOptions;
|
||||
fromFlatObjectMetadataMaps: FlatObjectMetadataMaps; // should be renamed to from
|
||||
fromFlatObjectMetadataMaps: FlatObjectMetadataMaps;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
|
||||
|
||||
export type FlatFieldMetadataPropertyUpdate<
|
||||
T extends FlatFieldMetadataPropertiesToCompare,
|
||||
> = {
|
||||
property: T;
|
||||
} & FromTo<FieldMetadataEntity[T]>;
|
||||
+2
-6
@@ -1,8 +1,6 @@
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatFieldMetadataPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-field-metadata-property-update.type';
|
||||
|
||||
export type CreateFieldAction = {
|
||||
type: 'create_field';
|
||||
@@ -16,9 +14,7 @@ export type UpdateFieldAction = {
|
||||
objectMetadataId: string;
|
||||
updates: Array<
|
||||
{
|
||||
[P in FlatFieldMetadataPropertiesToCompare]: {
|
||||
property: P;
|
||||
} & FromTo<FieldMetadataEntity[P]>;
|
||||
[P in FlatFieldMetadataPropertiesToCompare]: FlatFieldMetadataPropertyUpdate<P>;
|
||||
}[FlatFieldMetadataPropertiesToCompare]
|
||||
>;
|
||||
};
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { type ColumnType } from 'typeorm';
|
||||
|
||||
import { type FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util';
|
||||
import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util';
|
||||
import { removeSqlDDLInjection } from 'src/engine/workspace-manager/workspace-migration-runner/utils/remove-sql-injection.util';
|
||||
|
||||
type SerializeDefaultValueV2Args = {
|
||||
defaultValue?: FieldMetadataDefaultSerializableValue;
|
||||
columnType?: ColumnType;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
};
|
||||
export const serializeDefaultValueV2 = ({
|
||||
columnType,
|
||||
defaultValue,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnName,
|
||||
}: SerializeDefaultValueV2Args) => {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
const safeColumnName = removeSqlDDLInjection(columnName);
|
||||
|
||||
if (defaultValue === undefined || defaultValue === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
// Function default values
|
||||
if (isFunctionDefaultValue(defaultValue)) {
|
||||
const serializedTypeDefaultValue =
|
||||
serializeFunctionDefaultValue(defaultValue);
|
||||
|
||||
if (!serializedTypeDefaultValue) {
|
||||
throw new FieldMetadataException(
|
||||
'Invalid default value',
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return serializedTypeDefaultValue;
|
||||
}
|
||||
|
||||
const castSuffix =
|
||||
columnType === 'enum'
|
||||
? `::${safeSchemaName}."${safeTableName}_${safeColumnName}_enum"`
|
||||
: `::${columnType}`;
|
||||
|
||||
const sanitizeAndAddCastPrefix = (defaultValue: string) =>
|
||||
`'${removeSqlDDLInjection(defaultValue)}'` + castSuffix;
|
||||
|
||||
switch (typeof defaultValue) {
|
||||
case 'string': {
|
||||
if (!defaultValue.startsWith("'") || !defaultValue.endsWith("'")) {
|
||||
throw new FieldMetadataException(
|
||||
`Invalid string default value "${defaultValue}" should be single quoted`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return sanitizeAndAddCastPrefix(defaultValue);
|
||||
}
|
||||
case 'boolean':
|
||||
case 'number': {
|
||||
return sanitizeAndAddCastPrefix(`${defaultValue}`);
|
||||
}
|
||||
case 'object': {
|
||||
if (defaultValue instanceof Date) {
|
||||
return sanitizeAndAddCastPrefix(`'${defaultValue.toISOString()}'`);
|
||||
}
|
||||
|
||||
if (Array.isArray(defaultValue)) {
|
||||
const arrayValues = defaultValue
|
||||
.map((val) => `'${removeSqlDDLInjection(val)}'`)
|
||||
.join(',');
|
||||
|
||||
return `ARRAY[${arrayValues}]${castSuffix}[]`;
|
||||
}
|
||||
|
||||
return sanitizeAndAddCastPrefix(`'${JSON.stringify(defaultValue)}'`); // Won't work :thinking: at all will remove every brackets and so on
|
||||
}
|
||||
default: {
|
||||
throw new FieldMetadataException(
|
||||
`Invalid default value "${defaultValue}"`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
+5
-5
@@ -63,6 +63,11 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
operation: EnumOperation.CREATE,
|
||||
});
|
||||
|
||||
const columnDefinitions = generateColumnDefinitions({
|
||||
flatFieldMetadata: flatFieldMetadata,
|
||||
flatObjectMetadataWithoutFields: flatObjectMetadataWithFlatFieldMaps,
|
||||
});
|
||||
|
||||
await executeBatchEnumOperations({
|
||||
enumOperations,
|
||||
queryRunner,
|
||||
@@ -70,11 +75,6 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
workspaceSchemaManagerService: this.workspaceSchemaManagerService,
|
||||
});
|
||||
|
||||
const columnDefinitions = generateColumnDefinitions({
|
||||
flatFieldMetadata: flatFieldMetadata,
|
||||
flatObjectMetadataWithoutFields: flatObjectMetadataWithFlatFieldMaps,
|
||||
});
|
||||
|
||||
await this.workspaceSchemaManagerService.columnManager.addColumns({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
|
||||
+72
-59
@@ -1,21 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type EnumFieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
import { ColumnType, type QueryRunner } from 'typeorm';
|
||||
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultOption,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/get-composite-type-or-throw.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { unserializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/unserialize-default-value';
|
||||
import { FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
|
||||
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
|
||||
@@ -23,9 +17,13 @@ import { isRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-fi
|
||||
import { findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util';
|
||||
import { findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-with-flat-field-maps-in-flat-object-metadata-maps-or-throw.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util';
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { FlatFieldMetadataPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-field-metadata-property-update.type';
|
||||
import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
|
||||
import { serializeDefaultValueV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/serialize-default-value-v2.util';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
@@ -40,6 +38,16 @@ import {
|
||||
executeBatchEnumOperations,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/workspace-schema-enum-operations.util';
|
||||
|
||||
type UpdateFieldPropertyUpdateHandlerArgs<
|
||||
T extends FlatFieldMetadataPropertiesToCompare,
|
||||
> = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
flatFieldMetadata: FlatFieldMetadata;
|
||||
update: FlatFieldMetadataPropertyUpdate<T>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'update_field',
|
||||
@@ -85,12 +93,6 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
flatObjectMetadata: flatObjectMetadataWithFlatFieldMaps,
|
||||
});
|
||||
|
||||
const flatObjectMetadata =
|
||||
findFlatObjectMetadataWithFlatFieldMapsInFlatObjectMetadataMapsOrThrow({
|
||||
flatObjectMetadataMaps,
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
const currentFlatFieldMetadata =
|
||||
findFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({
|
||||
flatObjectMetadataMaps,
|
||||
@@ -102,51 +104,52 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
|
||||
for (const update of updates) {
|
||||
if (update.property === 'name') {
|
||||
await this.handleFieldNameUpdate(
|
||||
await this.handleFieldNameUpdate({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
optimisticFlatFieldMetadata,
|
||||
flatFieldMetadata: optimisticFlatFieldMetadata,
|
||||
update,
|
||||
);
|
||||
});
|
||||
optimisticFlatFieldMetadata.name = update.to;
|
||||
}
|
||||
if (update.property === 'defaultValue') {
|
||||
await this.handleFieldDefaultValueUpdate(
|
||||
await this.handleFieldDefaultValueUpdate({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
optimisticFlatFieldMetadata,
|
||||
flatFieldMetadata: optimisticFlatFieldMetadata,
|
||||
update,
|
||||
);
|
||||
});
|
||||
optimisticFlatFieldMetadata.defaultValue = update.to;
|
||||
}
|
||||
if (
|
||||
update.property === 'options' &&
|
||||
isEnumFlatFieldMetadata(optimisticFlatFieldMetadata)
|
||||
) {
|
||||
await this.handleFieldOptionsUpdate(
|
||||
await this.handleFieldOptionsUpdate({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
flatObjectMetadata,
|
||||
optimisticFlatFieldMetadata,
|
||||
flatObjectMetadata:
|
||||
fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata(
|
||||
flatObjectMetadataWithFlatFieldMaps,
|
||||
),
|
||||
flatFieldMetadata: optimisticFlatFieldMetadata,
|
||||
update,
|
||||
);
|
||||
});
|
||||
optimisticFlatFieldMetadata.options = update.to ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFieldNameUpdate(
|
||||
queryRunner: QueryRunner,
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
flatFieldMetadata: FlatFieldMetadata,
|
||||
update: {
|
||||
property: 'name';
|
||||
} & FromTo<string>,
|
||||
) {
|
||||
private async handleFieldNameUpdate({
|
||||
flatFieldMetadata,
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
update,
|
||||
}: UpdateFieldPropertyUpdateHandlerArgs<'name'>) {
|
||||
if (isCompositeFlatFieldMetadata(flatFieldMetadata)) {
|
||||
const compositeType = getCompositeTypeOrThrow(flatFieldMetadata.type);
|
||||
|
||||
@@ -208,15 +211,17 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
});
|
||||
}
|
||||
|
||||
private async handleFieldDefaultValueUpdate(
|
||||
queryRunner: QueryRunner,
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
flatFieldMetadata: FlatFieldMetadata,
|
||||
update: {
|
||||
property: 'defaultValue';
|
||||
} & FromTo<FieldMetadataDefaultValueForAnyType>,
|
||||
) {
|
||||
private async handleFieldDefaultValueUpdate({
|
||||
flatFieldMetadata,
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
update,
|
||||
}: UpdateFieldPropertyUpdateHandlerArgs<'defaultValue'>) {
|
||||
const columnType = fieldMetadataTypeToColumnType(
|
||||
flatFieldMetadata.type,
|
||||
) as ColumnType;
|
||||
|
||||
if (isCompositeFieldMetadataType(flatFieldMetadata.type)) {
|
||||
const compositeType = getCompositeTypeOrThrow(flatFieldMetadata.type);
|
||||
|
||||
@@ -233,11 +238,15 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
property,
|
||||
);
|
||||
// @ts-expect-error - TODO: fix this
|
||||
let compositeDefaultValue = update.to?.[property.name];
|
||||
let compositeDefaultValue = update.to?.[property.name]; // not valid should be serialized
|
||||
|
||||
const serializedNewDefaultValue = unserializeDefaultValue(
|
||||
compositeDefaultValue,
|
||||
);
|
||||
const serializedNewDefaultValue = serializeDefaultValueV2({
|
||||
columnName: compositeColumnName,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnType,
|
||||
defaultValue: compositeDefaultValue,
|
||||
});
|
||||
|
||||
await this.workspaceSchemaManagerService.columnManager.alterColumnDefault(
|
||||
{
|
||||
@@ -250,7 +259,13 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const serializedNewDefaultValue = unserializeDefaultValue(update.to);
|
||||
const serializedNewDefaultValue = serializeDefaultValueV2({
|
||||
columnName: flatFieldMetadata.name,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnType,
|
||||
defaultValue: update.to,
|
||||
});
|
||||
|
||||
await this.workspaceSchemaManagerService.columnManager.alterColumnDefault(
|
||||
{
|
||||
@@ -264,18 +279,16 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFieldOptionsUpdate(
|
||||
queryRunner: QueryRunner,
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
flatObjectMetadata: FlatObjectMetadata,
|
||||
flatFieldMetadata: FlatFieldMetadata<EnumFieldMetadataType>,
|
||||
update: {
|
||||
property: 'options';
|
||||
} & FromTo<
|
||||
FieldMetadataDefaultOption[] | FieldMetadataComplexOption[] | null
|
||||
>,
|
||||
) {
|
||||
private async handleFieldOptionsUpdate({
|
||||
flatFieldMetadata,
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
update,
|
||||
flatObjectMetadata,
|
||||
}: UpdateFieldPropertyUpdateHandlerArgs<'options'> & {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
}) {
|
||||
const fromOptionsById = new Map(
|
||||
(update.from ?? [])
|
||||
.filter((opt) => isDefined(opt.id))
|
||||
|
||||
+10
-10
@@ -55,7 +55,7 @@ describe('Generate Column Definitions', () => {
|
||||
isArray: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
default: null,
|
||||
default: 'NULL',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('Generate Column Definitions', () => {
|
||||
isArray: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
default: null,
|
||||
default: 'NULL',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -188,7 +188,7 @@ describe('Generate Column Definitions', () => {
|
||||
columns.forEach((column) => {
|
||||
expect(column.isNullable).toBe(true);
|
||||
expect(column.isUnique).toBe(false);
|
||||
expect(column.default).toBe(null);
|
||||
expect(column.default).toBe('NULL');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ describe('Generate Column Definitions', () => {
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
name: 'price',
|
||||
defaultValue: {
|
||||
amountMicros: '100000000',
|
||||
amountMicros: "'100000000'",
|
||||
currencyCode: "'USD'",
|
||||
},
|
||||
});
|
||||
@@ -220,7 +220,7 @@ describe('Generate Column Definitions', () => {
|
||||
type: 'numeric',
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
default: '100000000',
|
||||
default: "'100000000'::numeric",
|
||||
});
|
||||
|
||||
expect(columns[1]).toMatchObject({
|
||||
@@ -228,7 +228,7 @@ describe('Generate Column Definitions', () => {
|
||||
type: 'text',
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
default: 'USD',
|
||||
default: "'USD'::text",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -254,7 +254,7 @@ describe('Generate Column Definitions', () => {
|
||||
type: 'text',
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
default: null,
|
||||
default: 'NULL',
|
||||
isArray: false,
|
||||
},
|
||||
]);
|
||||
@@ -280,7 +280,7 @@ describe('Generate Column Definitions', () => {
|
||||
type: 'boolean',
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
default: true,
|
||||
default: "'true'::boolean",
|
||||
isArray: false,
|
||||
},
|
||||
]);
|
||||
@@ -307,7 +307,7 @@ describe('Generate Column Definitions', () => {
|
||||
type: 'text',
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
default: null,
|
||||
default: 'NULL',
|
||||
isArray: false,
|
||||
},
|
||||
]);
|
||||
@@ -332,7 +332,7 @@ describe('Generate Column Definitions', () => {
|
||||
type: 'uuid',
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
default: null,
|
||||
default: 'NULL',
|
||||
isArray: false,
|
||||
},
|
||||
]);
|
||||
|
||||
+23
-15
@@ -1,4 +1,5 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type ColumnType } from 'typeorm';
|
||||
|
||||
import { type CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
@@ -8,8 +9,6 @@ import {
|
||||
computeCompositeColumnName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/get-composite-type-or-throw.util';
|
||||
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
|
||||
import { unserializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/unserialize-default-value';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
|
||||
import { isFlatFieldMetadataEntityOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
@@ -17,6 +16,7 @@ import { type FlatObjectMetadataWithoutFields } from 'src/engine/metadata-module
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||
import { type WorkspaceSchemaColumnDefinition } from 'src/engine/twenty-orm/workspace-schema-manager/types/workspace-schema-column-definition.type';
|
||||
import { computePostgresEnumName } from 'src/engine/workspace-manager/workspace-migration-runner/utils/compute-postgres-enum-name.util';
|
||||
import { serializeDefaultValueV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/serialize-default-value-v2.util';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
WorkspaceMigrationRunnerExceptionCode,
|
||||
@@ -54,10 +54,14 @@ export const generateCompositeColumnDefinition = ({
|
||||
const defaultValue =
|
||||
// @ts-expect-error - TODO: fix this
|
||||
parentFieldMetadata.defaultValue?.[compositeProperty.name];
|
||||
|
||||
const unserializedDefaultValue = unserializeDefaultValue(defaultValue);
|
||||
|
||||
const columnType = fieldMetadataTypeToColumnType(compositeProperty.type);
|
||||
const serializedDefaultValue = serializeDefaultValueV2({
|
||||
columnName,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnType: columnType as ColumnType,
|
||||
defaultValue,
|
||||
});
|
||||
|
||||
const isArrayFlag =
|
||||
compositeProperty.type === FieldMetadataType.ARRAY ||
|
||||
@@ -72,7 +76,7 @@ export const generateCompositeColumnDefinition = ({
|
||||
: columnType,
|
||||
isNullable: parentFieldMetadata.isNullable || !compositeProperty.isRequired,
|
||||
isUnique: parentFieldMetadata.isUnique ?? false,
|
||||
default: unserializedDefaultValue,
|
||||
default: serializedDefaultValue,
|
||||
isArray: isArrayFlag,
|
||||
};
|
||||
|
||||
@@ -120,23 +124,29 @@ const generateRelationColumnDefinition = (
|
||||
};
|
||||
};
|
||||
|
||||
const generateStandardColumnDefinition = (
|
||||
const generateColumnDefinition = (
|
||||
flatFieldMetadata: FlatFieldMetadata,
|
||||
tableName: string,
|
||||
schemaName: string,
|
||||
): WorkspaceSchemaColumnDefinition => {
|
||||
const columnName = computeColumnName(flatFieldMetadata.name);
|
||||
const serializedDefaultValue = serializeDefaultValue(
|
||||
flatFieldMetadata.defaultValue,
|
||||
);
|
||||
const columnType = fieldMetadataTypeToColumnType(flatFieldMetadata.type);
|
||||
const columnType = fieldMetadataTypeToColumnType(
|
||||
flatFieldMetadata.type,
|
||||
) as ColumnType;
|
||||
const serializedDefaultValue = serializeDefaultValueV2({
|
||||
columnName,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnType,
|
||||
defaultValue: flatFieldMetadata.defaultValue,
|
||||
});
|
||||
|
||||
return {
|
||||
name: columnName,
|
||||
type:
|
||||
columnType === 'enum'
|
||||
? `"${schemaName}"."${computePostgresEnumName({ tableName, columnName })}"`
|
||||
: columnType,
|
||||
: (columnType as string),
|
||||
isNullable: flatFieldMetadata.isNullable ?? true,
|
||||
isArray:
|
||||
flatFieldMetadata.type === FieldMetadataType.ARRAY ||
|
||||
@@ -194,7 +204,5 @@ export const generateColumnDefinitions = ({
|
||||
return relationColumn ? [relationColumn] : [];
|
||||
}
|
||||
|
||||
return [
|
||||
generateStandardColumnDefinition(flatFieldMetadata, tableName, schemaName),
|
||||
];
|
||||
return [generateColumnDefinition(flatFieldMetadata, tableName, schemaName)];
|
||||
};
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export const BASIC_FAILING_STRING_EDGE_CASE_INPUTS: {
|
||||
label: string;
|
||||
input: string | undefined | number | null;
|
||||
}[] = [
|
||||
{ input: ' ', label: 'only white spaces' },
|
||||
{ input: '', label: 'empty string' },
|
||||
{ input: null, label: 'null' },
|
||||
{ input: 22222, label: 'not a string' },
|
||||
{ input: 'a'.repeat(64), label: 'too long' },
|
||||
] as const;
|
||||
-20
@@ -12,7 +12,6 @@ import { findManyOperationFactory } from 'test/integration/graphql/utils/find-ma
|
||||
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithMemberRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-member-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
import { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util';
|
||||
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
|
||||
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
|
||||
@@ -21,7 +20,6 @@ import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/m
|
||||
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
@@ -136,15 +134,6 @@ describe('Field permissions restrictions', () => {
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
// Enable the feature flag
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
// Get the original Member role ID for restoration later
|
||||
const getRolesQuery = {
|
||||
query: `
|
||||
@@ -243,15 +232,6 @@ describe('Field permissions restrictions', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Restore the feature flag
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
|
||||
// Restore original role
|
||||
const restoreMemberRoleQuery = {
|
||||
query: `
|
||||
|
||||
-20
@@ -8,7 +8,6 @@ import { createOneOperationFactory } from 'test/integration/graphql/utils/create
|
||||
import { deleteRole } from 'test/integration/graphql/utils/delete-one-role.util';
|
||||
import { makeGraphqlAPIRequestWithMemberRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-member-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
import { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util';
|
||||
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
|
||||
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
|
||||
@@ -17,7 +16,6 @@ import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/m
|
||||
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
@@ -82,15 +80,6 @@ describe('Field update permissions restrictions', () => {
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
// Enable the feature flag
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
// Get the original Member role ID for restoration later
|
||||
const getRolesQuery = {
|
||||
query: `
|
||||
@@ -181,15 +170,6 @@ describe('Field update permissions restrictions', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Restore the feature flag
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
|
||||
// Restore original role
|
||||
const restoreMemberRoleQuery = {
|
||||
query: `
|
||||
|
||||
+2
@@ -443,6 +443,7 @@ describe('roles permissions', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: 'house',
|
||||
namePlural: 'houses',
|
||||
@@ -457,6 +458,7 @@ describe('roles permissions', () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: listingObjectId },
|
||||
});
|
||||
});
|
||||
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
export const updateFeatureFlagFactory = (
|
||||
workspaceId: string,
|
||||
featureFlag: string,
|
||||
featureFlag: FeatureFlagKey,
|
||||
value: boolean,
|
||||
) => ({
|
||||
query: gql`
|
||||
|
||||
+3
@@ -22,6 +22,7 @@ describe('deleteOne', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: LISTING_NAME_SINGULAR,
|
||||
namePlural: LISTING_NAME_PLURAL,
|
||||
@@ -33,6 +34,7 @@ describe('deleteOne', () => {
|
||||
|
||||
listingObjectId = data.createOneObject.id;
|
||||
const { data: createdFieldData } = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'house',
|
||||
type: FieldMetadataType.TEXT,
|
||||
@@ -69,6 +71,7 @@ describe('deleteOne', () => {
|
||||
});
|
||||
afterEach(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: listingObjectId },
|
||||
});
|
||||
});
|
||||
|
||||
+2798
File diff suppressed because it is too large
Load Diff
+564
-564
File diff suppressed because it is too large
Load Diff
+2720
File diff suppressed because it is too large
Load Diff
+549
-549
File diff suppressed because it is too large
Load Diff
+46
-22
@@ -1,34 +1,58 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { BASIC_FAILING_STRING_EDGE_CASE_INPUTS } from 'test/constants/basic-failing-string-edge-case-inputs.constant';
|
||||
import { SELECT_AND_MULTI_SELECT_OPERATION_AGNOSTIC_SUCCESSFUL_AND_FAILING_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/select-and-multi-select-operation-agnostic-tests-cases';
|
||||
import { type FieldMetadataEnumSuccessfulAndFailingTestCases } from 'test/integration/metadata/suites/field-metadata/enum/types/fieldMetadataEnumSuccessfulAndFailingTestCases';
|
||||
import { type UpdateCreateFieldMetadataSelectTestCase } from 'test/integration/metadata/suites/field-metadata/enum/types/update-create-field-metadata-enum-test-case';
|
||||
|
||||
const fuzzedDefaultValueFailingTestCases: UpdateCreateFieldMetadataSelectTestCase[] =
|
||||
BASIC_FAILING_STRING_EDGE_CASE_INPUTS.filter((el) => isDefined(el.input)).map(
|
||||
({ input, label }) => ({
|
||||
title: `should fail with ${label} defaultValue`,
|
||||
context: {
|
||||
input: {
|
||||
defaultValue: [input],
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const MUTLI_SELECT_OPERATION_AGNOSTIC_TEST_CASES: FieldMetadataEnumSuccessfulAndFailingTestCases =
|
||||
{
|
||||
successful: [
|
||||
...SELECT_AND_MULTI_SELECT_OPERATION_AGNOSTIC_SUCCESSFUL_AND_FAILING_TEST_CASES.successful,
|
||||
// {
|
||||
// title: 'should succeed with valid multi-select default values',
|
||||
// context: {
|
||||
// input: {
|
||||
// defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
||||
// options: [
|
||||
// {
|
||||
// label: 'Option 1',
|
||||
// value: 'OPTION_1',
|
||||
// color: 'green',
|
||||
// position: 1,
|
||||
// },
|
||||
// {
|
||||
// label: 'Option 2',
|
||||
// value: 'OPTION_2',
|
||||
// color: 'blue',
|
||||
// position: 2,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: 'should succeed with valid multi-select default values',
|
||||
context: {
|
||||
input: {
|
||||
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'OPTION_2',
|
||||
color: 'blue',
|
||||
position: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
failing: [
|
||||
...fuzzedDefaultValueFailingTestCases,
|
||||
...SELECT_AND_MULTI_SELECT_OPERATION_AGNOSTIC_SUCCESSFUL_AND_FAILING_TEST_CASES.failing,
|
||||
{
|
||||
title: 'should fail with non stringified array default value',
|
||||
|
||||
+3
-35
@@ -1,21 +1,10 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { BASIC_FAILING_STRING_EDGE_CASE_INPUTS } from 'test/constants/basic-failing-string-edge-case-inputs.constant';
|
||||
import { type FieldMetadataEnumSuccessfulAndFailingTestCases } from 'test/integration/metadata/suites/field-metadata/enum/types/fieldMetadataEnumSuccessfulAndFailingTestCases';
|
||||
import { type UpdateCreateFieldMetadataSelectTestCase } from 'test/integration/metadata/suites/field-metadata/enum/types/update-create-field-metadata-enum-test-case';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
|
||||
const basicFailingStringEdgeCaseInputs: {
|
||||
label: string;
|
||||
input: string | undefined | number | null;
|
||||
}[] = [
|
||||
{ input: ' ', label: 'only white spaces' },
|
||||
{ input: '', label: 'empty string' },
|
||||
{ input: null, label: 'null' },
|
||||
{ input: 22222, label: 'not a string' },
|
||||
{ input: 'a'.repeat(64), label: 'too long' },
|
||||
];
|
||||
|
||||
const stringFields: (keyof FieldMetadataComplexOption)[] = [
|
||||
'id',
|
||||
'label',
|
||||
@@ -23,7 +12,7 @@ const stringFields: (keyof FieldMetadataComplexOption)[] = [
|
||||
];
|
||||
const fuzzedOptionsStringFieldFailingTestCases: UpdateCreateFieldMetadataSelectTestCase[] =
|
||||
stringFields.flatMap((field) => {
|
||||
return basicFailingStringEdgeCaseInputs.map<UpdateCreateFieldMetadataSelectTestCase>(
|
||||
return BASIC_FAILING_STRING_EDGE_CASE_INPUTS.map<UpdateCreateFieldMetadataSelectTestCase>(
|
||||
({ input, label }) => ({
|
||||
title: `should fail with ${label} ${field}`,
|
||||
context: {
|
||||
@@ -43,30 +32,9 @@ const fuzzedOptionsStringFieldFailingTestCases: UpdateCreateFieldMetadataSelectT
|
||||
);
|
||||
});
|
||||
|
||||
const fuzzedDefaultValueFailingTestCases: UpdateCreateFieldMetadataSelectTestCase[] =
|
||||
basicFailingStringEdgeCaseInputs
|
||||
.filter((el) => isDefined(el.input))
|
||||
.map(({ input, label }) => ({
|
||||
title: `should fail with ${label} defaultValue`,
|
||||
context: {
|
||||
input: {
|
||||
defaultValue: input,
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const SELECT_AND_MULTI_SELECT_OPERATION_AGNOSTIC_SUCCESSFUL_AND_FAILING_TEST_CASES: FieldMetadataEnumSuccessfulAndFailingTestCases =
|
||||
{
|
||||
failing: [
|
||||
...fuzzedDefaultValueFailingTestCases,
|
||||
...fuzzedOptionsStringFieldFailingTestCases,
|
||||
{
|
||||
title: 'should fail with invalid option id',
|
||||
|
||||
+24
@@ -1,9 +1,33 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { BASIC_FAILING_STRING_EDGE_CASE_INPUTS } from 'test/constants/basic-failing-string-edge-case-inputs.constant';
|
||||
import { SELECT_AND_MULTI_SELECT_OPERATION_AGNOSTIC_SUCCESSFUL_AND_FAILING_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/select-and-multi-select-operation-agnostic-tests-cases';
|
||||
import { type FieldMetadataEnumSuccessfulAndFailingTestCases } from 'test/integration/metadata/suites/field-metadata/enum/types/fieldMetadataEnumSuccessfulAndFailingTestCases';
|
||||
import { type UpdateCreateFieldMetadataSelectTestCase } from 'test/integration/metadata/suites/field-metadata/enum/types/update-create-field-metadata-enum-test-case';
|
||||
|
||||
const fuzzedDefaultValueFailingTestCases: UpdateCreateFieldMetadataSelectTestCase[] =
|
||||
BASIC_FAILING_STRING_EDGE_CASE_INPUTS.filter((el) => isDefined(el.input)).map(
|
||||
({ input, label }) => ({
|
||||
title: `should fail with ${label} defaultValue`,
|
||||
context: {
|
||||
input: {
|
||||
defaultValue: input,
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const SELECT_OPERATION_AGNOSTIC_TEST_CASES: FieldMetadataEnumSuccessfulAndFailingTestCases =
|
||||
{
|
||||
failing: [
|
||||
...fuzzedDefaultValueFailingTestCases,
|
||||
...SELECT_AND_MULTI_SELECT_OPERATION_AGNOSTIC_SUCCESSFUL_AND_FAILING_TEST_CASES.failing,
|
||||
],
|
||||
successful: [
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { eachTestingContextFilter } from 'twenty-shared/testing';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Failing create field metadata %s tests suite v2',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
const testCases =
|
||||
CREATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases } = testCases;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
|
||||
const {
|
||||
labelPlural,
|
||||
description,
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
} = CUSTOM_OBJECT_DISHES;
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular,
|
||||
labelPlural,
|
||||
nameSingular,
|
||||
description,
|
||||
namePlural,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
test.each(eachTestingContextFilter(failingTestCases))(
|
||||
'Create $title',
|
||||
async ({ context: { input } }) => {
|
||||
const { data, errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
...input,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny(errors),
|
||||
);
|
||||
expect(errors[0].extensions.code).not.toBe('INTERNAL_SERVER_ERROR');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import {
|
||||
LISTING_NAME_PLURAL,
|
||||
LISTING_NAME_SINGULAR,
|
||||
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Failing create field metadata %s tests suite',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
const testCases =
|
||||
CREATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases } = testCases;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: LISTING_NAME_SINGULAR,
|
||||
labelPlural: LISTING_NAME_PLURAL,
|
||||
nameSingular: LISTING_NAME_SINGULAR,
|
||||
namePlural: LISTING_NAME_PLURAL,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
test.each(failingTestCases)(
|
||||
'Create $title',
|
||||
async ({ context: { input } }) => {
|
||||
const { data, errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
...input,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/update-enum-field-metadata-test-cases';
|
||||
import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { eachTestingContextFilter } from 'twenty-shared/testing';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Failing update field metadata %s tests suite v2',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
let createdFieldMetadataId: string;
|
||||
const testCases =
|
||||
UPDATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases } = testCases;
|
||||
const initialOptions: CreateOneFieldFactoryInput['options'] = [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'OPTION_2',
|
||||
color: 'green',
|
||||
position: 2,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
|
||||
const {
|
||||
labelPlural,
|
||||
description,
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
} = CUSTOM_OBJECT_DISHES;
|
||||
const { data } = await createOneObjectMetadata({
|
||||
input: {
|
||||
labelSingular,
|
||||
labelPlural,
|
||||
nameSingular,
|
||||
description,
|
||||
namePlural,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata({
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
options: initialOptions,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
createdFieldMetadataId = createOneField.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await deleteOneObjectMetadata({
|
||||
input: {
|
||||
idToDelete: createdObjectMetadataId,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
test.each(eachTestingContextFilter(failingTestCases))(
|
||||
'Update $title',
|
||||
async ({ context: { input } }) => {
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload: input,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny(errors),
|
||||
);
|
||||
expect(errors[0].extensions.code).not.toBe('INTERNAL_SERVER_ERROR');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/update-enum-field-metadata-test-cases';
|
||||
import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import {
|
||||
LISTING_NAME_PLURAL,
|
||||
LISTING_NAME_SINGULAR,
|
||||
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Failing update field metadata %s tests suite',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
let createdFieldMetadataId: string;
|
||||
const testCases =
|
||||
UPDATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases } = testCases;
|
||||
const initialOptions: CreateOneFieldFactoryInput['options'] = [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'OPTION_2',
|
||||
color: 'green',
|
||||
position: 2,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: LISTING_NAME_SINGULAR,
|
||||
labelPlural: LISTING_NAME_PLURAL,
|
||||
nameSingular: LISTING_NAME_SINGULAR,
|
||||
namePlural: LISTING_NAME_PLURAL,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata({
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
options: initialOptions,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
createdFieldMetadataId = createOneField.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await deleteOneObjectMetadata({
|
||||
input: {
|
||||
idToDelete: createdObjectMetadataId,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
test.each(failingTestCases)(
|
||||
'Update $title',
|
||||
async ({ context: { input } }) => {
|
||||
const { ...updatePayload } = input;
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { eachTestingContextFilter } from 'twenty-shared/testing';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Successful create field metadata %s tests suite v2',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
let createdFieldMetadataId: string | undefined = undefined;
|
||||
const testCases =
|
||||
CREATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { successful: successfulTestCases } = testCases;
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
|
||||
const {
|
||||
labelPlural,
|
||||
description,
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
} = CUSTOM_OBJECT_DISHES;
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelPlural,
|
||||
description,
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
createdFieldMetadataId = undefined;
|
||||
});
|
||||
afterEach(async () => {
|
||||
if (!isDefined(createdFieldMetadataId)) {
|
||||
return;
|
||||
}
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
await deleteOneFieldMetadata({
|
||||
input: {
|
||||
idToDelete: createdFieldMetadataId,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
test.each(eachTestingContextFilter(successfulTestCases))(
|
||||
'Create $title',
|
||||
async ({ context: { input, expectedOptions } }) => {
|
||||
const { data, errors } = await createOneFieldMetadata<
|
||||
typeof testedFieldMetadataType
|
||||
>({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
...input,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).not.toBeNull();
|
||||
expect(data.createOneField).toBeDefined();
|
||||
expect(data.createOneField.type).toEqual(testedFieldMetadataType);
|
||||
createdFieldMetadataId = data.createOneField.id;
|
||||
const createdOptions = data.createOneField.options;
|
||||
const optionsToCompare = expectedOptions ?? input.options ?? [];
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(createdOptions?.length).toBe(optionsToCompare.length);
|
||||
createdOptions?.forEach((option) => expect(option.id).toBeDefined());
|
||||
expect(createdOptions).toMatchObject(optionsToCompare);
|
||||
|
||||
if (isDefined(input.defaultValue)) {
|
||||
expect(data.createOneField.defaultValue).toEqual(input.defaultValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+17
-31
@@ -1,17 +1,18 @@
|
||||
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/create-enum-field-metadata-test-cases';
|
||||
import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import {
|
||||
LISTING_NAME_PLURAL,
|
||||
LISTING_NAME_SINGULAR,
|
||||
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Create field metadata %s tests suite',
|
||||
'Successful create field metadata %s tests suite',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
const testCases =
|
||||
@@ -20,11 +21,11 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases, successful: successfulTestCases } =
|
||||
testCases;
|
||||
const { successful: successfulTestCases } = testCases;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { data } = await forceCreateOneObjectMetadata({
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: LISTING_NAME_SINGULAR,
|
||||
labelPlural: LISTING_NAME_PLURAL,
|
||||
@@ -39,7 +40,17 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
@@ -83,30 +94,5 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each(failingTestCases)(
|
||||
'Create $title',
|
||||
async ({ context: { input } }) => {
|
||||
const { data, errors } = await createOneFieldMetadata({
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
...input,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/update-enum-field-metadata-test-cases';
|
||||
import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { eachTestingContextFilter } from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultOption,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Successful update field metadata %s tests suite v2',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
let createdFieldMetadataId: string;
|
||||
const testCases =
|
||||
UPDATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { successful: successfulTestCases } = testCases;
|
||||
const initialOptions: CreateOneFieldFactoryInput['options'] = [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'OPTION_1',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'OPTION_2',
|
||||
color: 'green',
|
||||
position: 2,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
});
|
||||
|
||||
const {
|
||||
labelPlural,
|
||||
description,
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
} = CUSTOM_OBJECT_DISHES;
|
||||
const { data } = await createOneObjectMetadata({
|
||||
input: {
|
||||
labelPlural,
|
||||
description,
|
||||
labelSingular,
|
||||
namePlural,
|
||||
nameSingular,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
options: initialOptions,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
createdFieldMetadataId = createOneField.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdFieldMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('Should update default value to null even if it was set before', async () => {
|
||||
const isMultiSelect =
|
||||
testedFieldMetadataType === FieldMetadataType.MULTI_SELECT;
|
||||
const rawDefaultValue = `'${initialOptions[0].value}'`;
|
||||
const expectedDefaultValue = isMultiSelect
|
||||
? [rawDefaultValue]
|
||||
: rawDefaultValue;
|
||||
|
||||
const { data: firstUpdate } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload: {
|
||||
defaultValue: expectedDefaultValue,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(firstUpdate.updateOneField.defaultValue).toEqual(
|
||||
expectedDefaultValue,
|
||||
);
|
||||
|
||||
const updatedOptions = initialOptions.slice(1);
|
||||
const { data: secondUpdate, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload: {
|
||||
defaultValue: null,
|
||||
options: updatedOptions,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(secondUpdate.updateOneField.defaultValue).toBeNull();
|
||||
expect(secondUpdate.updateOneField.options).toMatchObject(updatedOptions);
|
||||
});
|
||||
|
||||
test.each(eachTestingContextFilter(successfulTestCases))(
|
||||
'Update $title',
|
||||
async ({ context: { input, expectedOptions } }) => {
|
||||
const { ...updatePayload } = input;
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
defaultValue
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data.updateOneField).toBeDefined();
|
||||
const updatedOptions:
|
||||
| FieldMetadataComplexOption[]
|
||||
| FieldMetadataDefaultOption[]
|
||||
| null = data.updateOneField.options;
|
||||
|
||||
expect(updatedOptions).toBeDefined();
|
||||
if (!isDefined(updatedOptions))
|
||||
throw new Error(
|
||||
'Should never occur, type invariant post test assertion',
|
||||
);
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
updatedOptions.forEach((option) => expect(option.id).toBeDefined());
|
||||
|
||||
const optionsToCompare = expectedOptions ?? input.options ?? [];
|
||||
|
||||
expect(updatedOptions.length).toBe(optionsToCompare.length);
|
||||
expect(updatedOptions).toMatchObject(optionsToCompare);
|
||||
|
||||
if (isDefined(input.defaultValue)) {
|
||||
expect(data.updateOneField.defaultValue).toEqual(input.defaultValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+33
-79
@@ -1,4 +1,4 @@
|
||||
import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/update-enum-field-metadata-test-cases';
|
||||
import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/update-enum-field-metadata-test-cases';
|
||||
import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -18,17 +19,17 @@ import {
|
||||
import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
describe.each(fieldMetadataEnumTypes)(
|
||||
'Update field metadata %s tests suite',
|
||||
'Successful update field metadata %s tests suite',
|
||||
(testedFieldMetadataType) => {
|
||||
let createdObjectMetadataId: string;
|
||||
let createdFieldMetadataId: string;
|
||||
const testCases =
|
||||
UPDATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType];
|
||||
|
||||
if (!isDefined(testCases)) {
|
||||
return;
|
||||
}
|
||||
const { failing: failingTestCases, successful: successfulTestCases } =
|
||||
testCases;
|
||||
const { successful: successfulTestCases } = testCases;
|
||||
const initialOptions: CreateOneFieldFactoryInput['options'] = [
|
||||
{
|
||||
label: 'Option 1',
|
||||
@@ -46,6 +47,7 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
|
||||
beforeEach(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: LISTING_NAME_SINGULAR,
|
||||
labelPlural: LISTING_NAME_PLURAL,
|
||||
@@ -57,18 +59,11 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
});
|
||||
|
||||
createdObjectMetadataId = data.createOneObject.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('Should update default value to null even if it was set before', async () => {
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
@@ -78,12 +73,31 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
options: initialOptions,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
`,
|
||||
id
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
const createdFieldMetadata = createOneField.id;
|
||||
createdFieldMetadataId = createOneField.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: createdObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('Should update default value to null even if it was set before', async () => {
|
||||
const isMultiSelect =
|
||||
testedFieldMetadataType === FieldMetadataType.MULTI_SELECT;
|
||||
const rawDefaultValue = `'${initialOptions[0].value}'`;
|
||||
@@ -93,7 +107,7 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
|
||||
const { data: firstUpdate } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadata,
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload: {
|
||||
defaultValue: expectedDefaultValue,
|
||||
},
|
||||
@@ -111,7 +125,7 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
const updatedOptions = initialOptions.slice(1);
|
||||
const { data: secondUpdate, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadata,
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload: {
|
||||
defaultValue: null,
|
||||
options: updatedOptions,
|
||||
@@ -132,30 +146,11 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
test.each(successfulTestCases)(
|
||||
'Update $title',
|
||||
async ({ context: { input, expectedOptions } }) => {
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata({
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
options: initialOptions,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
const createdFieldMetadata = createOneField.id;
|
||||
|
||||
const { ...updatePayload } = input;
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadata,
|
||||
idToUpdate: createdFieldMetadataId,
|
||||
updatePayload,
|
||||
},
|
||||
gqlFields: `
|
||||
@@ -190,46 +185,5 @@ describe.each(fieldMetadataEnumTypes)(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each(failingTestCases)(
|
||||
'Update $title',
|
||||
async ({ context: { input } }) => {
|
||||
const {
|
||||
data: { createOneField },
|
||||
} = await createOneFieldMetadata({
|
||||
input: {
|
||||
objectMetadataId: createdObjectMetadataId,
|
||||
type: testedFieldMetadataType,
|
||||
name: 'testField',
|
||||
label: 'Test Field',
|
||||
isLabelSyncedWithName: false,
|
||||
options: initialOptions,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
type
|
||||
`,
|
||||
});
|
||||
|
||||
const createdFieldMetadata = createOneField.id;
|
||||
|
||||
const { ...updatePayload } = input;
|
||||
|
||||
const { data, errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: createdFieldMetadata,
|
||||
updatePayload,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
options
|
||||
`,
|
||||
});
|
||||
|
||||
expect(data).toBeNull();
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+2
-2
@@ -3,9 +3,9 @@ import { type CountryCode } from 'libphonenumber-js';
|
||||
import { createOneOperation } from 'test/integration/graphql/utils/create-one-operation.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
import {
|
||||
type AdditionalPhoneMetadata,
|
||||
@@ -324,7 +324,7 @@ describe('Phone field metadata tests suite', () => {
|
||||
let createdObjectMetadataId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await forceCreateOneObjectMetadata({
|
||||
const { data } = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'myTestObject',
|
||||
namePlural: 'myTestObjects',
|
||||
|
||||
+1
-1
@@ -3,7 +3,6 @@ import { createOneOperation } from 'test/integration/graphql/utils/create-one-op
|
||||
import { findOneOperation } from 'test/integration/graphql/utils/find-one-operation.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
|
||||
+68
-2
@@ -4,17 +4,18 @@ import {
|
||||
LISTING_NAME_PLURAL,
|
||||
LISTING_NAME_SINGULAR,
|
||||
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
describe('updateOne', () => {
|
||||
describe('FieldMetadataService name/label sync', () => {
|
||||
describe('successful fieldMetadataService name/label sync', () => {
|
||||
let listingObjectId = '';
|
||||
let testFieldId = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: LISTING_NAME_SINGULAR,
|
||||
labelPlural: LISTING_NAME_PLURAL,
|
||||
@@ -28,6 +29,7 @@ describe('updateOne', () => {
|
||||
listingObjectId = data.createOneObject.id;
|
||||
|
||||
const { data: createdFieldMetadata } = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
objectMetadataId: listingObjectId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
@@ -41,6 +43,7 @@ describe('updateOne', () => {
|
||||
});
|
||||
afterEach(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: listingObjectId },
|
||||
});
|
||||
});
|
||||
@@ -90,6 +93,46 @@ describe('updateOne', () => {
|
||||
// Assert
|
||||
expect(data.updateOneField.name).toBe('differentName');
|
||||
});
|
||||
});
|
||||
|
||||
describe('failing update', () => {
|
||||
let listingObjectId = '';
|
||||
let testFieldId = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular: LISTING_NAME_SINGULAR,
|
||||
labelPlural: LISTING_NAME_PLURAL,
|
||||
nameSingular: LISTING_NAME_SINGULAR,
|
||||
namePlural: LISTING_NAME_PLURAL,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isLabelSyncedWithName: true,
|
||||
},
|
||||
});
|
||||
|
||||
listingObjectId = data.createOneObject.id;
|
||||
|
||||
const { data: createdFieldMetadata } = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
objectMetadataId: listingObjectId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'testName',
|
||||
label: 'Test name',
|
||||
isLabelSyncedWithName: true,
|
||||
},
|
||||
});
|
||||
|
||||
testFieldId = createdFieldMetadata.createOneField.id;
|
||||
});
|
||||
afterAll(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: listingObjectId },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update a field name if it is not synced correctly with label and labelSync is true', async () => {
|
||||
// Arrange
|
||||
@@ -115,5 +158,28 @@ describe('updateOne', () => {
|
||||
'Name is not synced with label. Expected name: "testName", got newName',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the field name is not available because of other field with the same name', async () => {
|
||||
await createOneFieldMetadata({
|
||||
input: {
|
||||
objectMetadataId: listingObjectId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'otherTestName',
|
||||
label: 'Test name',
|
||||
},
|
||||
});
|
||||
|
||||
const { errors } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: testFieldId,
|
||||
updatePayload: { name: 'testName' },
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(errors[0].message).toBe(
|
||||
'Name "testName" is not available, check that it is not duplicating another field\'s name.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-2
@@ -5,6 +5,7 @@ import {
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
@@ -13,7 +14,7 @@ import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadat
|
||||
export const createOneFieldMetadata = async <T extends FieldMetadataType>({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
expectToFail,
|
||||
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>): CommonResponseBody<{
|
||||
createOneField: FieldMetadataDTO<T>;
|
||||
}> => {
|
||||
@@ -24,12 +25,19 @@ export const createOneFieldMetadata = async <T extends FieldMetadataType>({
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail) {
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Field Metadata creation should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
errorMessage: 'Field metadata creation should not have failed',
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
|
||||
+10
-2
@@ -4,12 +4,13 @@ import {
|
||||
deleteOneFieldMetadataQueryFactory,
|
||||
} from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata-query-factory.util';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
export const deleteOneFieldMetadata = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
expectToFail,
|
||||
}: PerformMetadataQueryParams<DeleteOneFieldFactoryInput>) => {
|
||||
const graphqlOperation = deleteOneFieldMetadataQueryFactory({
|
||||
input,
|
||||
@@ -18,12 +19,19 @@ export const deleteOneFieldMetadata = async ({
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail) {
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Field Metadata deletion should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
errorMessage: 'Field metadata deletion should not have failed',
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
|
||||
+10
-2
@@ -4,12 +4,13 @@ import {
|
||||
} from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
export const findManyFieldsMetadata = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
expectToFail,
|
||||
}: PerformMetadataQueryParams<FindManyFieldsMetadataFactoryInput>) => {
|
||||
const graphqlOperation = findManyFieldsMetadataQueryFactory({
|
||||
input,
|
||||
@@ -18,13 +19,20 @@ export const findManyFieldsMetadata = async ({
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail) {
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Field Metadata retrieval should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
errorMessage: 'Field metadata retrieval should not have failed',
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
errors: response.body.errors,
|
||||
fields: response.body.data.fields?.edges,
|
||||
|
||||
+10
-2
@@ -5,6 +5,7 @@ import {
|
||||
} from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-query-factory.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
@@ -12,7 +13,7 @@ import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-meta
|
||||
export const updateOneFieldMetadata = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
expectToFail,
|
||||
}: PerformMetadataQueryParams<UpdateOneFieldFactoryInput>): CommonResponseBody<{
|
||||
updateOneField: FieldMetadataEntity;
|
||||
}> => {
|
||||
@@ -23,12 +24,19 @@ export const updateOneFieldMetadata = async ({
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail) {
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Field Metadata update should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
errorMessage: 'Field metadata update should not have failed',
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
export const CUSTOM_OBJECT_DISHES = {
|
||||
namePlural: 'dishes',
|
||||
nameSingular: 'dish',
|
||||
description: 'My favorite dishes',
|
||||
labelPlural: 'Dishes I love',
|
||||
labelSingular: 'Dish I love',
|
||||
} as const satisfies Partial<FlatObjectMetadata>;
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
|
||||
import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('Delete Object metadata with morph relation should succeed', () => {
|
||||
data: {
|
||||
createOneObject: { id: aId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'opportunityForDelete',
|
||||
namePlural: 'opportunitiesForDelete',
|
||||
@@ -33,7 +33,7 @@ describe('Delete Object metadata with morph relation should succeed', () => {
|
||||
data: {
|
||||
createOneObject: { id: bId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'personForDelete',
|
||||
namePlural: 'peopleForDelete',
|
||||
@@ -48,7 +48,7 @@ describe('Delete Object metadata with morph relation should succeed', () => {
|
||||
data: {
|
||||
createOneObject: { id: cId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'companyForDelete',
|
||||
namePlural: 'companiesForDelete',
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
|
||||
import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Rename an object metadata with morph relation should succeed', () => {
|
||||
data: {
|
||||
createOneObject: { id: aId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'opportunityForRename',
|
||||
namePlural: 'opportunitiesForRename',
|
||||
@@ -39,7 +39,7 @@ describe('Rename an object metadata with morph relation should succeed', () => {
|
||||
data: {
|
||||
createOneObject: { id: bId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'personForRename',
|
||||
namePlural: 'peopleForRename',
|
||||
@@ -54,7 +54,7 @@ describe('Rename an object metadata with morph relation should succeed', () => {
|
||||
data: {
|
||||
createOneObject: { id: cId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'companyForRename',
|
||||
namePlural: 'companiesForRename',
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('Delete Object metadata with relation should succeed', () => {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataPersonId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'personForRelation',
|
||||
namePlural: 'peopleForRelation',
|
||||
@@ -73,7 +73,7 @@ describe('Delete Object metadata with relation should succeed', () => {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataOpportunityId },
|
||||
},
|
||||
} = await forceCreateOneObjectMetadata({
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'opportunityForRelation',
|
||||
namePlural: 'opportunitiesForRelation',
|
||||
|
||||
+9
-11
@@ -6,8 +6,6 @@ import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const LISTING_NAME_SINGULAR = 'listing';
|
||||
|
||||
describe('Custom object renaming', () => {
|
||||
let listingObjectId = '';
|
||||
|
||||
@@ -80,19 +78,19 @@ describe('Custom object renaming', () => {
|
||||
|
||||
fillStandardObjectRelationsMapObjectMetadataId(standardObjects);
|
||||
|
||||
const LISTING_OBJECT = {
|
||||
namePlural: 'listings',
|
||||
nameSingular: LISTING_NAME_SINGULAR,
|
||||
labelPlural: 'Listings',
|
||||
labelSingular: 'Listing',
|
||||
description: 'Listing object',
|
||||
const CUSTOM_OBJECT = {
|
||||
namePlural: 'customObjectNamePlural',
|
||||
nameSingular: 'customObjectNameSingular',
|
||||
labelPlural: 'customObjectLabelPlural',
|
||||
labelSingular: 'customObjectLabelSingular',
|
||||
description: 'Custom object description',
|
||||
icon: 'IconListNumbers',
|
||||
isLabelSyncedWithName: false,
|
||||
};
|
||||
|
||||
// Act
|
||||
const { data } = await createOneObjectMetadata({
|
||||
input: LISTING_OBJECT,
|
||||
input: CUSTOM_OBJECT,
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
@@ -100,7 +98,7 @@ describe('Custom object renaming', () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(data.createOneObject.nameSingular).toBe(LISTING_NAME_SINGULAR);
|
||||
expect(data.createOneObject.nameSingular).toBe(CUSTOM_OBJECT.nameSingular);
|
||||
|
||||
listingObjectId = data.createOneObject.id;
|
||||
|
||||
@@ -110,7 +108,7 @@ describe('Custom object renaming', () => {
|
||||
.filter(
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
(field) =>
|
||||
field.node.name === `${LISTING_NAME_SINGULAR}` &&
|
||||
field.node.name === `${CUSTOM_OBJECT.nameSingular}` &&
|
||||
field.node.type === FieldMetadataType.RELATION,
|
||||
)
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
|
||||
+10
-2
@@ -5,6 +5,7 @@ import {
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -12,7 +13,7 @@ import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-me
|
||||
export const createOneObjectMetadata = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
expectToFail,
|
||||
}: PerformMetadataQueryParams<CreateOneObjectFactoryInput>): CommonResponseBody<{
|
||||
createOneObject: ObjectMetadataEntity; // not accurate
|
||||
}> => {
|
||||
@@ -23,12 +24,19 @@ export const createOneObjectMetadata = async ({
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail) {
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object Metadata creation should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object Metadata creation has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
|
||||
+10
-2
@@ -4,12 +4,13 @@ import {
|
||||
deleteOneObjectMetadataQueryFactory,
|
||||
} from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-query-factory.util';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
export const deleteOneObjectMetadata = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
expectToFail,
|
||||
}: PerformMetadataQueryParams<DeleteOneObjectFactoryInput>) => {
|
||||
const graphqlOperation = deleteOneObjectMetadataQueryFactory({
|
||||
input,
|
||||
@@ -18,12 +19,19 @@ export const deleteOneObjectMetadata = async ({
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail) {
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object Metadata deletion should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object Metadata deletion has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
|
||||
+10
-2
@@ -4,6 +4,7 @@ import {
|
||||
} from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
@@ -12,7 +13,7 @@ import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metad
|
||||
export const findManyObjectMetadata = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
expectToFail,
|
||||
}: PerformMetadataQueryParams<FindManyObjectMetadataFactoryInput>): Promise<{
|
||||
errors: BaseGraphQLError[];
|
||||
objects: ObjectMetadataDTO[];
|
||||
@@ -24,13 +25,20 @@ export const findManyObjectMetadata = async ({
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail) {
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object Metadata retrieval should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object Metadata retrieval has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
errors: response.body.errors,
|
||||
objects: response.body.data.objects?.edges.map(
|
||||
|
||||
+29
-23
@@ -1,25 +1,27 @@
|
||||
import {
|
||||
LISTING_NAME_PLURAL,
|
||||
LISTING_NAME_SINGULAR,
|
||||
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
|
||||
import { type CreateOneObjectFactoryInput } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
/**
|
||||
* This testing function util should be used for local debugging only
|
||||
* Please do not call this method in prod ci
|
||||
* Each test should handle and adopt black box testing pattern
|
||||
*/
|
||||
export const forceCreateOneObjectMetadata = async ({
|
||||
input: {
|
||||
labelSingular = LISTING_NAME_SINGULAR,
|
||||
labelPlural = LISTING_NAME_PLURAL,
|
||||
nameSingular = LISTING_NAME_SINGULAR,
|
||||
namePlural = LISTING_NAME_PLURAL,
|
||||
isLabelSyncedWithName = true,
|
||||
labelSingular,
|
||||
labelPlural,
|
||||
nameSingular,
|
||||
namePlural,
|
||||
isLabelSyncedWithName = false,
|
||||
icon = 'IconBuildingSkyscraper',
|
||||
...rest
|
||||
},
|
||||
}: {
|
||||
input: Partial<CreateOneObjectFactoryInput>;
|
||||
input: CreateOneObjectFactoryInput;
|
||||
}) => {
|
||||
const result = await createOneObjectMetadata({
|
||||
input: {
|
||||
@@ -36,7 +38,9 @@ export const forceCreateOneObjectMetadata = async ({
|
||||
if (!isDefined(result.errors)) {
|
||||
return result;
|
||||
}
|
||||
const { objects, errors } = await findManyObjectMetadata({
|
||||
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: {
|
||||
@@ -49,12 +53,6 @@ export const forceCreateOneObjectMetadata = async ({
|
||||
`,
|
||||
});
|
||||
|
||||
if (isDefined(errors) || !isDefined(objects)) {
|
||||
throw new Error(
|
||||
'Force create object metadata find many failed, should never occur',
|
||||
);
|
||||
}
|
||||
|
||||
const match = objects.find((object) => object.nameSingular === nameSingular);
|
||||
|
||||
if (!isDefined(match)) {
|
||||
@@ -63,15 +61,18 @@ export const forceCreateOneObjectMetadata = async ({
|
||||
);
|
||||
}
|
||||
|
||||
const { errors: deleteErrors } = await deleteOneObjectMetadata({
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToUpdate: match.id, updatePayload: { isActive: false } },
|
||||
});
|
||||
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: match.id },
|
||||
});
|
||||
|
||||
if (isDefined(deleteErrors)) {
|
||||
throw new Error(JSON.stringify(deleteErrors));
|
||||
}
|
||||
|
||||
return await createOneObjectMetadata({
|
||||
const { errors: createErrors, data } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
labelSingular,
|
||||
labelPlural,
|
||||
@@ -82,4 +83,9 @@ export const forceCreateOneObjectMetadata = async ({
|
||||
...rest,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
errors: createErrors,
|
||||
};
|
||||
};
|
||||
|
||||
+10
-2
@@ -4,12 +4,13 @@ import {
|
||||
updateOneObjectMetadataQueryFactory,
|
||||
} from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata-query-factory.util';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
export const updateOneObjectMetadata = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
expectToFail,
|
||||
}: PerformMetadataQueryParams<UpdateOneObjectFactoryInput>) => {
|
||||
const graphqlOperation = updateOneObjectMetadataQueryFactory({
|
||||
input,
|
||||
@@ -18,12 +19,19 @@ export const updateOneObjectMetadata = async ({
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail) {
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object Metadata update should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object Metadata update has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
|
||||
export const updateFeatureFlag = async ({
|
||||
featureFlag,
|
||||
value,
|
||||
workspaceId = SEED_APPLE_WORKSPACE_ID,
|
||||
expectToFail,
|
||||
}: {
|
||||
featureFlag: FeatureFlagKey;
|
||||
value: boolean;
|
||||
workspaceId?: string;
|
||||
expectToFail: boolean;
|
||||
}) => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
workspaceId,
|
||||
featureFlag,
|
||||
value,
|
||||
);
|
||||
|
||||
const response = await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
errorMessage: 'Update feature flag should not have failed',
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
errorMessage: 'Update feature flag should have failed',
|
||||
response,
|
||||
});
|
||||
}
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { type Response } from 'supertest';
|
||||
|
||||
type WarnIfErrorButNotExpectedToFailInput = {
|
||||
response: Response;
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
export const warnIfErrorButNotExpectedToFail = ({
|
||||
response,
|
||||
errorMessage,
|
||||
}: WarnIfErrorButNotExpectedToFailInput) => {
|
||||
if (isDefined(response.body.errors) && response.body.errors.length > 0) {
|
||||
expect(response.body.errors).toEqual(errorMessage);
|
||||
}
|
||||
expect(response.body.data).toBeDefined();
|
||||
};
|
||||
+1
-1
@@ -10,7 +10,7 @@ export const warnIfNoErrorButExpectedToFail = ({
|
||||
response,
|
||||
errorMessage,
|
||||
}: WarnIfNoErrorButExpectedToFailInput) => {
|
||||
if (isDefined(response.body.data)) {
|
||||
if (!isDefined(response.body.errors) || response.body.errors.length === 0) {
|
||||
expect(false).toEqual(errorMessage);
|
||||
}
|
||||
expect(response.body.errors.length).toBeGreaterThan(0);
|
||||
|
||||
Reference in New Issue
Block a user