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:
Paul Rastoin
2025-08-28 12:23:04 +02:00
committed by GitHub
parent 533d7fe49a
commit 59d90bedaa
72 changed files with 8236 additions and 1616 deletions
@@ -21,6 +21,7 @@ const jestConfig: JestConfigWithTsJest = {
prettierPath: null,
silent: false,
errorOnDeprecated: true,
maxConcurrency: 1,
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testEnvironment: 'node',
@@ -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],
},
],
}),
@@ -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,
@@ -61,7 +61,5 @@ export function computeCompositeColumnName<T extends FieldMetadataType>(
);
}
return `${fieldMetadataOrFieldName.name}${pascalCase(
compositeProperty.name,
)}`;
return generateName(fieldMetadataOrFieldName.name);
}
@@ -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,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)[];
@@ -92,8 +92,8 @@ export class FlatFieldMetadataValidatorService {
}
const updates = compareTwoFlatFieldMetadata({
from: existingFlatFieldMetadataToUpdate,
to: updatedFlatFieldMetadata,
fromFlatFieldMetadata: existingFlatFieldMetadataToUpdate,
toFlatFieldMetadata: updatedFlatFieldMetadata,
});
if (
@@ -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,
};
}
@@ -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'],
@@ -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,
@@ -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;
};
@@ -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`,
@@ -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,
};
}
@@ -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],
},
],
@@ -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);
}
@@ -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.
@@ -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,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(' ');
@@ -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)`,
});
};
@@ -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)`,
});
}),
);
}
}
@@ -68,8 +68,8 @@ export class WorkspaceMigrationV2FieldActionsBuilderService {
to: toFlatFieldMetadata,
} of updatedFlatFieldMetadatas) {
const updates = compareTwoFlatFieldMetadata({
from: fromFlatFieldMetadata,
to: toFlatFieldMetadata,
fromFlatFieldMetadata,
toFlatFieldMetadata,
});
if (updates.length === 0) {
@@ -31,7 +31,7 @@ export type CreatedDeletedUpdatedObjectMetadataInputMatrix = FromTo<
FlatObjectMetadata
> & {
buildOptions: WorkspaceMigrationV2BuilderOptions;
fromFlatObjectMetadataMaps: FlatObjectMetadataMaps; // should be renamed to from
fromFlatObjectMetadataMaps: FlatObjectMetadataMaps;
};
@Injectable()
@@ -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]>;
@@ -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]
>;
};
@@ -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,
);
}
}
};
@@ -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,
@@ -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))
@@ -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,
},
]);
@@ -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)];
};
@@ -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;
@@ -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: `
@@ -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: `
@@ -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 },
});
});
@@ -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`
@@ -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 },
});
});
@@ -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',
@@ -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',
@@ -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: [
@@ -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');
},
);
},
);
@@ -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();
},
);
},
);
@@ -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');
},
);
},
);
@@ -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();
},
);
},
);
@@ -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);
}
},
);
},
);
@@ -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();
},
);
},
);
@@ -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);
}
},
);
},
);
@@ -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();
},
);
},
);
@@ -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',
@@ -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,
@@ -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.',
);
});
});
});
@@ -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 };
};
@@ -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 };
};
@@ -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,
@@ -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 };
};
@@ -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>;
@@ -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',
@@ -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',
@@ -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',
@@ -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
@@ -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 };
};
@@ -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 };
};
@@ -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(
@@ -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,
};
};
@@ -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 };
};
@@ -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,
});
}
};
@@ -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();
};
@@ -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);