feat: simplification of default-value specification in FieldMetadata (#4592)
* feat: wip refactor default-value * feat: health check to migrate default value * fix: tests * fix: refactor defaultValue to make it more clean * fix: unit tests * fix: front-end default value
This commit is contained in:
@ -23,13 +23,19 @@ export const useFieldMetadataItem = () => {
|
||||
options?: Omit<FieldMetadataOption, 'id'>[];
|
||||
type: FieldMetadataType;
|
||||
},
|
||||
) =>
|
||||
createOneFieldMetadataItem({
|
||||
...formatFieldMetadataItemInput(input),
|
||||
defaultValue: input.defaultValue,
|
||||
) => {
|
||||
const formatedInput = formatFieldMetadataItemInput(input);
|
||||
const defaultValue = input.defaultValue
|
||||
? `'${input.defaultValue}'`
|
||||
: formatedInput.defaultValue ?? undefined;
|
||||
|
||||
return createOneFieldMetadataItem({
|
||||
...formatedInput,
|
||||
defaultValue,
|
||||
objectMetadataId: input.objectMetadataId,
|
||||
type: input.type,
|
||||
});
|
||||
};
|
||||
|
||||
const editMetadataField = (
|
||||
input: Pick<Field, 'id' | 'label' | 'icon' | 'description'> & {
|
||||
|
||||
@ -74,7 +74,7 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
value: 'OPTION_2',
|
||||
},
|
||||
],
|
||||
defaultValue: 'OPTION_1',
|
||||
defaultValue: "'OPTION_1'",
|
||||
};
|
||||
|
||||
const result = formatFieldMetadataItemInput(input);
|
||||
|
||||
@ -44,7 +44,7 @@ export const formatFieldMetadataItemInput = (
|
||||
|
||||
return {
|
||||
defaultValue: defaultOption
|
||||
? getOptionValueFromLabel(defaultOption.label)
|
||||
? `'${getOptionValueFromLabel(defaultOption.label)}'`
|
||||
: undefined,
|
||||
description: input.description?.trim() ?? null,
|
||||
icon: input.icon,
|
||||
|
||||
@ -107,7 +107,7 @@ export const SettingsObjectFieldEdit = () => {
|
||||
|
||||
const selectOptions = activeMetadataField.options?.map((option) => ({
|
||||
...option,
|
||||
isDefault: defaultValue?.value === option.value,
|
||||
isDefault: defaultValue === `'${option.value}'`,
|
||||
}));
|
||||
selectOptions?.sort(
|
||||
(optionA, optionB) => optionA.position - optionB.position,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { DynamicModule, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { DevtoolsModule } from '@nestjs/devtools-integration';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
@ -11,7 +10,6 @@ import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';
|
||||
|
||||
import { ApiRestModule } from 'src/engine/api/rest/api-rest.module';
|
||||
import { ModulesModule } from 'src/modules/modules.module';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.module';
|
||||
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
|
||||
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module';
|
||||
@ -23,13 +21,13 @@ import { IntegrationsModule } from './engine/integrations/integrations.module';
|
||||
@Module({
|
||||
imports: [
|
||||
// Nest.js devtools, use devtools.nestjs.com to debug
|
||||
DevtoolsModule.registerAsync({
|
||||
useFactory: (environmentService: EnvironmentService) => ({
|
||||
http: environmentService.get('DEBUG_MODE'),
|
||||
port: environmentService.get('DEBUG_PORT'),
|
||||
}),
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
// DevtoolsModule.registerAsync({
|
||||
// useFactory: (environmentService: EnvironmentService) => ({
|
||||
// http: environmentService.get('DEBUG_MODE'),
|
||||
// port: environmentService.get('DEBUG_PORT'),
|
||||
// }),
|
||||
// inject: [EnvironmentService],
|
||||
// }),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
|
||||
@ -34,9 +34,8 @@ export const currencyFields = (
|
||||
isNullable: true,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.amountMicros ?? null,
|
||||
},
|
||||
defaultValue:
|
||||
inferredFieldMetadata.defaultValue?.amountMicros ?? null,
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.NUMERIC>,
|
||||
@ -52,9 +51,8 @@ export const currencyFields = (
|
||||
isNullable: true,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.currencyCode ?? null,
|
||||
},
|
||||
defaultValue:
|
||||
inferredFieldMetadata.defaultValue?.currencyCode ?? null,
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
|
||||
@ -34,9 +34,7 @@ export const fullNameFields = (
|
||||
isNullable: true,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.firstName ?? null,
|
||||
},
|
||||
defaultValue: inferredFieldMetadata.defaultValue?.firstName ?? null,
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
@ -52,9 +50,7 @@ export const fullNameFields = (
|
||||
isNullable: true,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.lastName ?? null,
|
||||
},
|
||||
defaultValue: inferredFieldMetadata.defaultValue?.lastName ?? null,
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
|
||||
@ -34,9 +34,7 @@ export const linkFields = (
|
||||
isNullable: true,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.label ?? null,
|
||||
},
|
||||
defaultValue: inferredFieldMetadata.defaultValue?.label ?? null,
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
@ -52,9 +50,7 @@ export const linkFields = (
|
||||
isNullable: true,
|
||||
...(inferredFieldMetadata
|
||||
? {
|
||||
defaultValue: {
|
||||
value: inferredFieldMetadata.defaultValue?.url ?? null,
|
||||
},
|
||||
defaultValue: inferredFieldMetadata.defaultValue?.url ?? null,
|
||||
}
|
||||
: {}),
|
||||
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||
|
||||
@ -6,88 +6,95 @@ import {
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsNumberString,
|
||||
IsString,
|
||||
Matches,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
|
||||
import { IsQuotedString } from 'src/engine/metadata-modules/field-metadata/validators/is-quoted-string.validator';
|
||||
|
||||
export const fieldMetadataDefaultValueFunctionName = {
|
||||
UUID: 'uuid',
|
||||
NOW: 'now',
|
||||
} as const;
|
||||
|
||||
export type FieldMetadataDefaultValueFunctionNames =
|
||||
(typeof fieldMetadataDefaultValueFunctionName)[keyof typeof fieldMetadataDefaultValueFunctionName];
|
||||
|
||||
export class FieldMetadataDefaultValueString {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueRawJson {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsJSON()
|
||||
value: JSON | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueNumber {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsNumber()
|
||||
value: number | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueBoolean {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsBoolean()
|
||||
value: boolean | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueStringArray {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsQuotedString({ each: true })
|
||||
value: string[] | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueDateTime {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsDate()
|
||||
value: Date | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueLink {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
label: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueCurrency {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsNumberString()
|
||||
amountMicros: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
currencyCode: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueFullName {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
firstName: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
@ValidateIf((object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
lastName: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDynamicDefaultValueUuid {
|
||||
@Matches('uuid')
|
||||
export class FieldMetadataDefaultValueUuidFunction {
|
||||
@Matches(fieldMetadataDefaultValueFunctionName.UUID)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
type: 'uuid';
|
||||
value: typeof fieldMetadataDefaultValueFunctionName.UUID;
|
||||
}
|
||||
|
||||
export class FieldMetadataDynamicDefaultValueNow {
|
||||
@Matches('now')
|
||||
export class FieldMetadataDefaultValueNowFunction {
|
||||
@Matches(fieldMetadataDefaultValueFunctionName.NOW)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
type: 'now';
|
||||
value: typeof fieldMetadataDefaultValueFunctionName.NOW;
|
||||
}
|
||||
|
||||
@ -300,8 +300,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
existingFieldMetadata.type !== FieldMetadataType.SELECT
|
||||
? existingFieldMetadata.defaultValue
|
||||
: updatableFieldInput.defaultValue
|
||||
? // Todo: we need to rework DefaultValue typing and format to be simpler, there is no need to have this complexity
|
||||
{ value: updatableFieldInput.defaultValue as unknown as string }
|
||||
? updatableFieldInput.defaultValue
|
||||
: null,
|
||||
// If the name is updated, the targetColumnMap should be updated as well
|
||||
targetColumnMap: updatableFieldInput.name
|
||||
|
||||
@ -8,38 +8,25 @@ import {
|
||||
FieldMetadataDefaultValueNumber,
|
||||
FieldMetadataDefaultValueString,
|
||||
FieldMetadataDefaultValueStringArray,
|
||||
FieldMetadataDynamicDefaultValueNow,
|
||||
FieldMetadataDynamicDefaultValueUuid,
|
||||
FieldMetadataDefaultValueUuidFunction,
|
||||
FieldMetadataDefaultValueNowFunction,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
type FieldMetadataScalarDefaultValue =
|
||||
| FieldMetadataDefaultValueString
|
||||
| FieldMetadataDefaultValueNumber
|
||||
| FieldMetadataDefaultValueBoolean
|
||||
| FieldMetadataDefaultValueDateTime;
|
||||
type ExtractValueType<T> = T extends { value: infer V } ? V : T;
|
||||
|
||||
export type FieldMetadataDynamicDefaultValue =
|
||||
| FieldMetadataDynamicDefaultValueUuid
|
||||
| FieldMetadataDynamicDefaultValueNow;
|
||||
|
||||
type AllFieldMetadataDefaultValueTypes =
|
||||
| FieldMetadataScalarDefaultValue
|
||||
| FieldMetadataDynamicDefaultValue
|
||||
| FieldMetadataDefaultValueLink
|
||||
| FieldMetadataDefaultValueCurrency
|
||||
| FieldMetadataDefaultValueFullName;
|
||||
type UnionOfValues<T> = T[keyof T];
|
||||
|
||||
type FieldMetadataDefaultValueMapping = {
|
||||
[FieldMetadataType.UUID]:
|
||||
| FieldMetadataDefaultValueString
|
||||
| FieldMetadataDynamicDefaultValueUuid;
|
||||
| FieldMetadataDefaultValueUuidFunction;
|
||||
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.DATE_TIME]:
|
||||
| FieldMetadataDefaultValueDateTime
|
||||
| FieldMetadataDynamicDefaultValueNow;
|
||||
| FieldMetadataDefaultValueNowFunction;
|
||||
[FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean;
|
||||
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
|
||||
[FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber;
|
||||
@ -54,35 +41,31 @@ type FieldMetadataDefaultValueMapping = {
|
||||
[FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
|
||||
};
|
||||
|
||||
export type FieldMetadataClassValidation =
|
||||
UnionOfValues<FieldMetadataDefaultValueMapping>;
|
||||
|
||||
export type FieldMetadataFunctionDefaultValue = ExtractValueType<
|
||||
FieldMetadataDefaultValueUuidFunction | FieldMetadataDefaultValueNowFunction
|
||||
>;
|
||||
|
||||
type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [
|
||||
T,
|
||||
] extends [keyof FieldMetadataDefaultValueMapping]
|
||||
? FieldMetadataDefaultValueMapping[T] | null
|
||||
? ExtractValueType<FieldMetadataDefaultValueMapping[T]> | null
|
||||
: T extends 'default'
|
||||
? AllFieldMetadataDefaultValueTypes | null
|
||||
? ExtractValueType<UnionOfValues<FieldMetadataDefaultValueMapping>> | null
|
||||
: never;
|
||||
|
||||
export type FieldMetadataDefaultValue<
|
||||
T extends FieldMetadataType | 'default' = 'default',
|
||||
> = DefaultValueByFieldMetadata<T>;
|
||||
|
||||
type FieldMetadataDefaultValueExtractNestedType<T> = T extends {
|
||||
value: infer U;
|
||||
}
|
||||
? U
|
||||
: T extends object
|
||||
? { [K in keyof T]: T[K] } extends { value: infer V }
|
||||
? V
|
||||
: T[keyof T]
|
||||
: never;
|
||||
|
||||
type FieldMetadataDefaultValueExtractedTypes = {
|
||||
[K in keyof FieldMetadataDefaultValueMapping]: FieldMetadataDefaultValueExtractNestedType<
|
||||
[K in keyof FieldMetadataDefaultValueMapping]: ExtractValueType<
|
||||
FieldMetadataDefaultValueMapping[K]
|
||||
>;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultSerializableValue =
|
||||
| FieldMetadataDefaultValueExtractedTypes[keyof FieldMetadataDefaultValueExtractedTypes]
|
||||
| FieldMetadataDynamicDefaultValue
|
||||
| null;
|
||||
|
||||
@ -8,25 +8,19 @@ describe('serializeDefaultValue', () => {
|
||||
});
|
||||
|
||||
it('should handle uuid dynamic default value', () => {
|
||||
expect(serializeDefaultValue({ type: 'uuid' })).toBe(
|
||||
'public.uuid_generate_v4()',
|
||||
);
|
||||
expect(serializeDefaultValue('uuid')).toBe('public.uuid_generate_v4()');
|
||||
});
|
||||
|
||||
it('should handle now dynamic default value', () => {
|
||||
expect(serializeDefaultValue({ type: 'now' })).toBe('now()');
|
||||
expect(serializeDefaultValue('now')).toBe('now()');
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for invalid dynamic default value type', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error Just for testing purposes
|
||||
expect(() => serializeDefaultValue({ type: 'invalid' })).toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(() => serializeDefaultValue('invalid')).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should handle string static default value', () => {
|
||||
expect(serializeDefaultValue('test')).toBe("'test'");
|
||||
expect(serializeDefaultValue("'test'")).toBe("'test'");
|
||||
});
|
||||
|
||||
it('should handle number static default value', () => {
|
||||
|
||||
@ -10,110 +10,105 @@ describe('validateDefaultValueForType', () => {
|
||||
|
||||
// Dynamic default values
|
||||
it('should validate uuid dynamic default value for UUID type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.UUID, { type: 'uuid' }),
|
||||
).toBe(true);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.UUID, 'uuid')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate now dynamic default value for DATE_TIME type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.DATE_TIME, { type: 'now' }),
|
||||
validateDefaultValueForType(FieldMetadataType.DATE_TIME, 'now'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for mismatched dynamic default value', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.UUID, { type: 'now' }),
|
||||
).toBe(false);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.UUID, 'now')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// Static default values
|
||||
it('should validate string default value for TEXT type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.TEXT, { value: 'test' }),
|
||||
).toBe(true);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.TEXT, "'test'")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for invalid string default value for TEXT type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.TEXT, { value: 123 }),
|
||||
).toBe(false);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.TEXT, 123)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate string default value for PHONE type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.PHONE, {
|
||||
value: '+123456789',
|
||||
}),
|
||||
validateDefaultValueForType(FieldMetadataType.PHONE, "'+123456789'"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid string default value for PHONE type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.PHONE, { value: 123 }),
|
||||
).toBe(false);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.PHONE, 123)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate string default value for EMAIL type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.EMAIL, {
|
||||
value: 'test@example.com',
|
||||
}),
|
||||
validateDefaultValueForType(
|
||||
FieldMetadataType.EMAIL,
|
||||
"'test@example.com'",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid string default value for EMAIL type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.EMAIL, { value: 123 }),
|
||||
).toBe(false);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.EMAIL, 123)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate number default value for NUMBER type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.NUMBER, { value: 100 }),
|
||||
).toBe(true);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.NUMBER, 100)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for invalid number default value for NUMBER type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.NUMBER, { value: '100' }),
|
||||
).toBe(false);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.NUMBER, '100')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate number default value for PROBABILITY type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.PROBABILITY, {
|
||||
value: 0.5,
|
||||
}),
|
||||
validateDefaultValueForType(FieldMetadataType.PROBABILITY, 0.5),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid number default value for PROBABILITY type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.PROBABILITY, {
|
||||
value: '50%',
|
||||
}),
|
||||
validateDefaultValueForType(FieldMetadataType.PROBABILITY, '50%'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate boolean default value for BOOLEAN type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.BOOLEAN, { value: true }),
|
||||
).toBe(true);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.BOOLEAN, true)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for invalid boolean default value for BOOLEAN type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.BOOLEAN, { value: 'true' }),
|
||||
).toBe(false);
|
||||
expect(validateDefaultValueForType(FieldMetadataType.BOOLEAN, 'true')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// LINK type
|
||||
it('should validate LINK default value', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.LINK, {
|
||||
label: 'http://example.com',
|
||||
url: 'Example',
|
||||
label: "'http://example.com'",
|
||||
url: "'Example'",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -134,7 +129,7 @@ describe('validateDefaultValueForType', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.CURRENCY, {
|
||||
amountMicros: '100',
|
||||
currencyCode: 'USD',
|
||||
currencyCode: "'USD'",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -144,7 +139,7 @@ describe('validateDefaultValueForType', () => {
|
||||
validateDefaultValueForType(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error Just for testing purposes
|
||||
{ amountMicros: 100, currencyCode: 'USD' },
|
||||
{ amountMicros: 100, currencyCode: "'USD'" },
|
||||
FieldMetadataType.CURRENCY,
|
||||
),
|
||||
).toBe(false);
|
||||
@ -153,9 +148,7 @@ describe('validateDefaultValueForType', () => {
|
||||
// Unknown type
|
||||
it('should return false for unknown type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType('unknown' as FieldMetadataType, {
|
||||
value: 'test',
|
||||
}),
|
||||
validateDefaultValueForType('unknown' as FieldMetadataType, "'test'"),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,23 +9,21 @@ export function generateDefaultValue(
|
||||
case FieldMetadataType.TEXT:
|
||||
case FieldMetadataType.PHONE:
|
||||
case FieldMetadataType.EMAIL:
|
||||
return {
|
||||
value: '',
|
||||
};
|
||||
return "''";
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
return {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
firstName: "''",
|
||||
lastName: "''",
|
||||
};
|
||||
case FieldMetadataType.LINK:
|
||||
return {
|
||||
url: '',
|
||||
label: '',
|
||||
url: "''",
|
||||
label: "''",
|
||||
};
|
||||
case FieldMetadataType.CURRENCY:
|
||||
return {
|
||||
amountMicros: null,
|
||||
currencyCode: '',
|
||||
currencyCode: "''",
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import {
|
||||
FieldMetadataDefaultSerializableValue,
|
||||
FieldMetadataFunctionDefaultValue,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import {
|
||||
FieldMetadataDefaultValueFunctionNames,
|
||||
fieldMetadataDefaultValueFunctionName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
|
||||
export const isFunctionDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultSerializableValue,
|
||||
): defaultValue is FieldMetadataFunctionDefaultValue => {
|
||||
return (
|
||||
typeof defaultValue === 'string' &&
|
||||
!defaultValue.startsWith("'") &&
|
||||
Object.values(fieldMetadataDefaultValueFunctionName).includes(
|
||||
defaultValue as FieldMetadataDefaultValueFunctionNames,
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -2,7 +2,8 @@ import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { serializeTypeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-type-default-value.util';
|
||||
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';
|
||||
|
||||
export const serializeDefaultValue = (
|
||||
defaultValue?: FieldMetadataDefaultSerializableValue,
|
||||
@ -11,13 +12,10 @@ export const serializeDefaultValue = (
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dynamic default values
|
||||
if (
|
||||
!Array.isArray(defaultValue) &&
|
||||
typeof defaultValue === 'object' &&
|
||||
'type' in defaultValue
|
||||
) {
|
||||
const serializedTypeDefaultValue = serializeTypeDefaultValue(defaultValue);
|
||||
// Function default values
|
||||
if (isFunctionDefaultValue(defaultValue)) {
|
||||
const serializedTypeDefaultValue =
|
||||
serializeFunctionDefaultValue(defaultValue);
|
||||
|
||||
if (!serializedTypeDefaultValue) {
|
||||
throw new BadRequestException('Invalid default value');
|
||||
@ -27,8 +25,8 @@ export const serializeDefaultValue = (
|
||||
}
|
||||
|
||||
// Static default values
|
||||
if (typeof defaultValue === 'string') {
|
||||
return `'${defaultValue}'`;
|
||||
if (typeof defaultValue === 'string' && defaultValue.startsWith("'")) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'number') {
|
||||
@ -51,5 +49,5 @@ export const serializeDefaultValue = (
|
||||
return `'${JSON.stringify(defaultValue)}'`;
|
||||
}
|
||||
|
||||
throw new BadRequestException('Invalid default value');
|
||||
throw new BadRequestException(`Invalid default value "${defaultValue}"`);
|
||||
};
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { FieldMetadataFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
export const serializeFunctionDefaultValue = (
|
||||
defaultValue?: FieldMetadataFunctionDefaultValue,
|
||||
) => {
|
||||
switch (defaultValue) {
|
||||
case 'uuid':
|
||||
return 'public.uuid_generate_v4()';
|
||||
case 'now':
|
||||
return 'now()';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@ -1,18 +0,0 @@
|
||||
import { FieldMetadataDynamicDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
export const serializeTypeDefaultValue = (
|
||||
defaultValue?: FieldMetadataDynamicDefaultValue,
|
||||
) => {
|
||||
if (!defaultValue?.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (defaultValue.type) {
|
||||
case 'uuid':
|
||||
return 'public.uuid_generate_v4()';
|
||||
case 'now':
|
||||
return 'now()';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@ -1,7 +1,10 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync } from 'class-validator';
|
||||
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import {
|
||||
FieldMetadataClassValidation,
|
||||
FieldMetadataDefaultValue,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
@ -14,21 +17,22 @@ import {
|
||||
FieldMetadataDefaultValueNumber,
|
||||
FieldMetadataDefaultValueString,
|
||||
FieldMetadataDefaultValueStringArray,
|
||||
FieldMetadataDynamicDefaultValueNow,
|
||||
FieldMetadataDynamicDefaultValueUuid,
|
||||
FieldMetadataDefaultValueNowFunction,
|
||||
FieldMetadataDefaultValueUuidFunction,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
export const defaultValueValidatorsMap = {
|
||||
[FieldMetadataType.UUID]: [
|
||||
FieldMetadataDefaultValueString,
|
||||
FieldMetadataDynamicDefaultValueUuid,
|
||||
FieldMetadataDefaultValueUuidFunction,
|
||||
],
|
||||
[FieldMetadataType.TEXT]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.PHONE]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.EMAIL]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.DATE_TIME]: [
|
||||
FieldMetadataDefaultValueDateTime,
|
||||
FieldMetadataDynamicDefaultValueNow,
|
||||
FieldMetadataDefaultValueNowFunction,
|
||||
],
|
||||
[FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean],
|
||||
[FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber],
|
||||
@ -54,10 +58,14 @@ export const validateDefaultValueForType = (
|
||||
if (!validators) return false;
|
||||
|
||||
const isValid = validators.some((validator) => {
|
||||
const conputedDefaultValue = isCompositeFieldMetadataType(type)
|
||||
? defaultValue
|
||||
: { value: defaultValue };
|
||||
|
||||
const defaultValueInstance = plainToInstance<
|
||||
any,
|
||||
FieldMetadataDefaultValue
|
||||
>(validator, defaultValue);
|
||||
FieldMetadataClassValidation
|
||||
>(validator, conputedDefaultValue as FieldMetadataClassValidation);
|
||||
|
||||
return (
|
||||
validateSync(defaultValueInstance, {
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import {
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
registerDecorator,
|
||||
} from 'class-validator';
|
||||
|
||||
export function IsQuotedString(validationOptions?: ValidationOptions) {
|
||||
return function (object: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isQuotedString',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any) {
|
||||
return typeof value === 'string' && /^'.*'$/.test(value);
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${args.property} must be a quoted string`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -256,7 +256,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
defaultValue: { type: 'uuid' },
|
||||
defaultValue: 'uuid',
|
||||
},
|
||||
{
|
||||
standardId: customObjectStandardFieldIds.name,
|
||||
@ -272,7 +272,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
defaultValue: { value: 'Untitled' },
|
||||
defaultValue: "'Untitled'",
|
||||
},
|
||||
{
|
||||
standardId: baseObjectStandardFieldIds.createdAt,
|
||||
@ -288,7 +288,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
defaultValue: { type: 'now' },
|
||||
defaultValue: 'now',
|
||||
},
|
||||
{
|
||||
standardId: baseObjectStandardFieldIds.updatedAt,
|
||||
@ -305,7 +305,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
defaultValue: { type: 'now' },
|
||||
defaultValue: 'now',
|
||||
},
|
||||
{
|
||||
standardId: customObjectStandardFieldIds.position,
|
||||
|
||||
@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
@ -35,8 +34,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
||||
fieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
|
||||
options?: WorkspaceColumnActionOptions,
|
||||
): WorkspaceMigrationColumnCreate {
|
||||
const defaultValue =
|
||||
this.getDefaultValue(fieldMetadata.defaultValue) ?? options?.defaultValue;
|
||||
const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue;
|
||||
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
||||
|
||||
return {
|
||||
@ -54,8 +52,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
||||
options?: WorkspaceColumnActionOptions,
|
||||
): WorkspaceMigrationColumnAlter {
|
||||
const defaultValue =
|
||||
this.getDefaultValue(alteredFieldMetadata.defaultValue) ??
|
||||
options?.defaultValue;
|
||||
alteredFieldMetadata.defaultValue ?? options?.defaultValue;
|
||||
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
||||
const currentColumnName = currentFieldMetadata.targetColumnMap.value;
|
||||
const alteredColumnName = alteredFieldMetadata.targetColumnMap.value;
|
||||
@ -75,9 +72,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
||||
columnName: currentColumnName,
|
||||
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
|
||||
isNullable: currentFieldMetadata.isNullable,
|
||||
defaultValue: serializeDefaultValue(
|
||||
this.getDefaultValue(currentFieldMetadata.defaultValue),
|
||||
),
|
||||
defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue),
|
||||
},
|
||||
alteredColumnDefinition: {
|
||||
columnName: alteredColumnName,
|
||||
@ -87,19 +82,4 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getDefaultValue(
|
||||
defaultValue:
|
||||
| FieldMetadataDefaultValue<BasicFieldMetadataType>
|
||||
| undefined
|
||||
| null,
|
||||
) {
|
||||
if (!defaultValue) return null;
|
||||
|
||||
if ('type' in defaultValue) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return defaultValue?.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,8 +26,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
|
||||
fieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
|
||||
options: WorkspaceColumnActionOptions,
|
||||
): WorkspaceMigrationColumnCreate {
|
||||
const defaultValue =
|
||||
fieldMetadata.defaultValue?.value ?? options?.defaultValue;
|
||||
const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue;
|
||||
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
||||
const enumOptions = fieldMetadata.options
|
||||
? [...fieldMetadata.options.map((option) => option.value)]
|
||||
@ -50,7 +49,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
|
||||
options: WorkspaceColumnActionOptions,
|
||||
): WorkspaceMigrationColumnAlter {
|
||||
const defaultValue =
|
||||
alteredFieldMetadata.defaultValue?.value ?? options?.defaultValue;
|
||||
alteredFieldMetadata.defaultValue ?? options?.defaultValue;
|
||||
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
||||
|
||||
const enumOptions = alteredFieldMetadata.options
|
||||
@ -94,9 +93,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
|
||||
: undefined,
|
||||
isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
isNullable: currentFieldMetadata.isNullable,
|
||||
defaultValue: serializeDefaultValue(
|
||||
currentFieldMetadata.defaultValue?.value,
|
||||
),
|
||||
defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue),
|
||||
},
|
||||
alteredColumnDefinition: {
|
||||
columnName: alteredColumnName,
|
||||
|
||||
@ -12,30 +12,72 @@ import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-met
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadataDefaultValueFunctionNames,
|
||||
fieldMetadataDefaultValueFunctionName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
|
||||
import { AbstractWorkspaceFixer } from './abstract-workspace.fixer';
|
||||
import {
|
||||
AbstractWorkspaceFixer,
|
||||
CompareEntity,
|
||||
} from './abstract-workspace.fixer';
|
||||
|
||||
type WorkspaceDefaultValueFixerType =
|
||||
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT
|
||||
| WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID;
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT> {
|
||||
export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<WorkspaceDefaultValueFixerType> {
|
||||
constructor(
|
||||
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
|
||||
) {
|
||||
super(WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT);
|
||||
super(
|
||||
WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
|
||||
WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
|
||||
);
|
||||
}
|
||||
|
||||
async createWorkspaceMigrations(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceDefaultValueFixerType>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
if (issues.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
const splittedIssues = this.splitIssuesByType(issues);
|
||||
const issueNeedingMigration =
|
||||
splittedIssues[WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT] ??
|
||||
[];
|
||||
|
||||
return this.fixColumnDefaultValueIssues(objectMetadataCollection, issues);
|
||||
return this.fixColumnDefaultValueConflictIssues(
|
||||
objectMetadataCollection,
|
||||
issueNeedingMigration as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
|
||||
);
|
||||
}
|
||||
|
||||
private async fixColumnDefaultValueIssues(
|
||||
async createMetadataUpdates(
|
||||
manager: EntityManager,
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceDefaultValueFixerType>[],
|
||||
): Promise<CompareEntity<FieldMetadataEntity>[]> {
|
||||
if (issues.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const splittedIssues = this.splitIssuesByType(issues);
|
||||
const issueNeedingMetadataUpdate =
|
||||
splittedIssues[WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID] ??
|
||||
[];
|
||||
|
||||
return this.fixColumnDefaultValueNotValidIssues(
|
||||
manager,
|
||||
issueNeedingMetadataUpdate as WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID>[],
|
||||
);
|
||||
}
|
||||
|
||||
private async fixColumnDefaultValueConflictIssues(
|
||||
objectMetadataCollection: ObjectMetadataEntity[],
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
|
||||
): Promise<Partial<WorkspaceMigrationEntity>[]> {
|
||||
@ -61,6 +103,90 @@ export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<Workspace
|
||||
);
|
||||
}
|
||||
|
||||
private async fixColumnDefaultValueNotValidIssues(
|
||||
manager: EntityManager,
|
||||
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID>[],
|
||||
): Promise<CompareEntity<FieldMetadataEntity>[]> {
|
||||
const fieldMetadataRepository = manager.getRepository(FieldMetadataEntity);
|
||||
const updatedEntities: CompareEntity<FieldMetadataEntity>[] = [];
|
||||
|
||||
for (const issue of issues) {
|
||||
const currentDefaultValue:
|
||||
| FieldMetadataDefaultValue<'default'>
|
||||
// Old format for default values
|
||||
// TODO: Remove this after all workspaces are migrated
|
||||
| { type: FieldMetadataDefaultValueFunctionNames }
|
||||
| null = issue.fieldMetadata.defaultValue;
|
||||
let alteredDefaultValue: FieldMetadataDefaultValue<'default'> | null =
|
||||
null;
|
||||
|
||||
// Check if it's an old function default value
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (currentDefaultValue && 'type' in currentDefaultValue) {
|
||||
alteredDefaultValue =
|
||||
currentDefaultValue.type as FieldMetadataDefaultValueFunctionNames;
|
||||
}
|
||||
|
||||
// Check if it's an old string default value
|
||||
if (currentDefaultValue) {
|
||||
for (const key of Object.keys(currentDefaultValue)) {
|
||||
if (key === 'type') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = currentDefaultValue[key];
|
||||
|
||||
const newValue =
|
||||
typeof value === 'string' &&
|
||||
!value.startsWith("'") &&
|
||||
!Object.values(fieldMetadataDefaultValueFunctionName).includes(
|
||||
value as FieldMetadataDefaultValueFunctionNames,
|
||||
)
|
||||
? `'${value}'`
|
||||
: value;
|
||||
|
||||
alteredDefaultValue = {
|
||||
...(currentDefaultValue as any),
|
||||
...(alteredDefaultValue as any),
|
||||
[key]: newValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Old formart default values
|
||||
if (
|
||||
alteredDefaultValue &&
|
||||
typeof alteredDefaultValue === 'object' &&
|
||||
'value' in alteredDefaultValue
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
alteredDefaultValue = alteredDefaultValue.value;
|
||||
}
|
||||
|
||||
if (alteredDefaultValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await fieldMetadataRepository.update(issue.fieldMetadata.id, {
|
||||
defaultValue: alteredDefaultValue,
|
||||
});
|
||||
const alteredEntity = await fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
id: issue.fieldMetadata.id,
|
||||
},
|
||||
});
|
||||
|
||||
updatedEntities.push({
|
||||
current: issue.fieldMetadata,
|
||||
altered: alteredEntity as FieldMetadataEntity | null,
|
||||
});
|
||||
}
|
||||
|
||||
return updatedEntities;
|
||||
}
|
||||
|
||||
private computeFieldMetadataDefaultValueFromColumnDefault(
|
||||
columnDefault: string | undefined,
|
||||
): FieldMetadataDefaultValue<'default'> {
|
||||
@ -73,29 +199,29 @@ export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<Workspace
|
||||
}
|
||||
|
||||
if (!isNaN(Number(columnDefault))) {
|
||||
return { value: +columnDefault };
|
||||
return +columnDefault;
|
||||
}
|
||||
|
||||
if (columnDefault === 'true') {
|
||||
return { value: true };
|
||||
return true;
|
||||
}
|
||||
|
||||
if (columnDefault === 'false') {
|
||||
return { value: false };
|
||||
return false;
|
||||
}
|
||||
|
||||
if (columnDefault === '') {
|
||||
return { value: '' };
|
||||
return "''";
|
||||
}
|
||||
|
||||
if (columnDefault === 'now()') {
|
||||
return { type: 'now' };
|
||||
return 'now';
|
||||
}
|
||||
|
||||
if (columnDefault.startsWith('public.uuid_generate_v4')) {
|
||||
return { type: 'uuid' };
|
||||
return 'uuid';
|
||||
}
|
||||
|
||||
return { value: columnDefault };
|
||||
return columnDefault;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,10 @@ import {
|
||||
WorkspaceTableStructure,
|
||||
WorkspaceTableStructureResult,
|
||||
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface';
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import {
|
||||
FieldMetadataDefaultValue,
|
||||
FieldMetadataFunctionDefaultValue,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import {
|
||||
@ -15,9 +18,11 @@ import {
|
||||
FieldMetadataType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||
import { serializeTypeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-type-default-value.util';
|
||||
import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util';
|
||||
import { FieldMetadataDefaultValueFunctionNames } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseStructureService {
|
||||
@ -204,31 +209,49 @@ export class DatabaseStructureService {
|
||||
|
||||
getPostgresDefault(
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
defaultValue: FieldMetadataDefaultValue | null,
|
||||
defaultValue:
|
||||
| FieldMetadataDefaultValue
|
||||
// Old format for default values
|
||||
// TODO: Should be removed once all default values are migrated
|
||||
| { type: FieldMetadataDefaultValueFunctionNames }
|
||||
| null,
|
||||
): string | null | undefined {
|
||||
const typeORMType = fieldMetadataTypeToColumnType(
|
||||
fieldMetadataType,
|
||||
) as ColumnType;
|
||||
const mainDataSource = this.typeORMService.getMainDataSource();
|
||||
|
||||
if (defaultValue && 'type' in defaultValue) {
|
||||
const serializedDefaultValue = serializeTypeDefaultValue(defaultValue);
|
||||
let value: any =
|
||||
// Old formart default values
|
||||
defaultValue &&
|
||||
typeof defaultValue === 'object' &&
|
||||
'value' in defaultValue
|
||||
? defaultValue.value
|
||||
: defaultValue;
|
||||
|
||||
// Special case for uuid_generate_v4() default value
|
||||
if (serializedDefaultValue === 'public.uuid_generate_v4()') {
|
||||
return 'uuid_generate_v4()';
|
||||
}
|
||||
|
||||
return serializedDefaultValue;
|
||||
// Old format for default values
|
||||
// TODO: Should be removed once all default values are migrated
|
||||
if (
|
||||
defaultValue &&
|
||||
typeof defaultValue === 'object' &&
|
||||
'type' in defaultValue
|
||||
) {
|
||||
return this.computeFunctionDefaultValue(defaultValue.type);
|
||||
}
|
||||
|
||||
const value =
|
||||
defaultValue && 'value' in defaultValue ? defaultValue.value : null;
|
||||
if (isFunctionDefaultValue(value)) {
|
||||
return this.computeFunctionDefaultValue(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
// Remove leading and trailing single quotes for string default values as it's already handled by TypeORM
|
||||
if (typeof value === 'string' && value.match(/^'.*'$/)) {
|
||||
value = value.replace(/^'/, '').replace(/'$/, '');
|
||||
}
|
||||
|
||||
return mainDataSource.driver.normalizeDefault({
|
||||
type: typeORMType,
|
||||
default: value,
|
||||
@ -236,4 +259,17 @@ export class DatabaseStructureService {
|
||||
// Workaround to use normalizeDefault without a complete ColumnMetadata object
|
||||
} as ColumnMetadata);
|
||||
}
|
||||
|
||||
private computeFunctionDefaultValue(
|
||||
value: FieldMetadataFunctionDefaultValue,
|
||||
) {
|
||||
const serializedDefaultValue = serializeFunctionDefaultValue(value);
|
||||
|
||||
// Special case for uuid_generate_v4() default value
|
||||
if (serializedDefaultValue === 'public.uuid_generate_v4()') {
|
||||
return 'uuid_generate_v4()';
|
||||
}
|
||||
|
||||
return serializedDefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
@ -67,18 +67,30 @@ export class FieldMetadataHealthService {
|
||||
issues.push(...defaultValueIssues);
|
||||
}
|
||||
|
||||
for (const compositeFieldMetadata of compositeFieldMetadataCollection) {
|
||||
const compositeFieldIssues = await this.healthCheckField(
|
||||
// Only check structure on nested composite fields
|
||||
if (options.mode === 'structure' || options.mode === 'all') {
|
||||
for (const compositeFieldMetadata of compositeFieldMetadataCollection) {
|
||||
const compositeFieldStructureIssues = this.structureFieldCheck(
|
||||
tableName,
|
||||
workspaceTableColumns,
|
||||
computeCompositeFieldMetadata(
|
||||
compositeFieldMetadata,
|
||||
fieldMetadata,
|
||||
),
|
||||
);
|
||||
|
||||
issues.push(...compositeFieldStructureIssues);
|
||||
}
|
||||
}
|
||||
|
||||
// Only check metadata on the parent composite field
|
||||
if (options.mode === 'metadata' || options.mode === 'all') {
|
||||
const compositeFieldMetadataIssues = this.metadataFieldCheck(
|
||||
tableName,
|
||||
workspaceTableColumns,
|
||||
computeCompositeFieldMetadata(
|
||||
compositeFieldMetadata,
|
||||
fieldMetadata,
|
||||
),
|
||||
options,
|
||||
fieldMetadata,
|
||||
);
|
||||
|
||||
issues.push(...compositeFieldIssues);
|
||||
issues.push(...compositeFieldMetadataIssues);
|
||||
}
|
||||
} else {
|
||||
const fieldIssues = await this.healthCheckField(
|
||||
@ -137,6 +149,7 @@ export class FieldMetadataHealthService {
|
||||
fieldMetadata.type,
|
||||
fieldMetadata.defaultValue,
|
||||
);
|
||||
|
||||
// Check if column exist in database
|
||||
const columnStructure = workspaceTableColumns.find(
|
||||
(tableDefinition) => tableDefinition.columnName === columnName,
|
||||
@ -178,7 +191,7 @@ export class FieldMetadataHealthService {
|
||||
|
||||
if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||
const enumValues = fieldMetadata.options?.map((option) =>
|
||||
serializeDefaultValue(option.value),
|
||||
serializeDefaultValue(`'${option.value}'`),
|
||||
);
|
||||
|
||||
if (!enumValues.includes(columnDefaultValue)) {
|
||||
@ -325,10 +338,11 @@ export class FieldMetadataHealthService {
|
||||
isEnumFieldMetadataType(fieldMetadata.type) &&
|
||||
fieldMetadata.defaultValue
|
||||
) {
|
||||
const enumValues = fieldMetadata.options?.map((option) => option.value);
|
||||
const metadataDefaultValue = (
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<EnumFieldMetadataUnionType>
|
||||
)?.value;
|
||||
const enumValues = fieldMetadata.options?.map((option) =>
|
||||
serializeDefaultValue(`'${option.value}'`),
|
||||
);
|
||||
const metadataDefaultValue =
|
||||
fieldMetadata.defaultValue as FieldMetadataDefaultValue<EnumFieldMetadataUnionType>;
|
||||
|
||||
if (metadataDefaultValue && !enumValues.includes(metadataDefaultValue)) {
|
||||
issues.push({
|
||||
@ -341,29 +355,4 @@ export class FieldMetadataHealthService {
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
private isCompositeObjectWellStructured(
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
object: any,
|
||||
): boolean {
|
||||
const subFields = compositeDefinitions.get(fieldMetadataType)?.() ?? [];
|
||||
|
||||
if (!object) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subFields.length === 0) {
|
||||
throw new InternalServerErrorException(
|
||||
`The composite field type ${fieldMetadataType} doesn't have any sub fields, it seems this one is not implemented in the composite definitions map`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const subField of subFields) {
|
||||
if (!object[subField.name]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,6 +90,16 @@ export class WorkspaceFixService {
|
||||
filteredIssues,
|
||||
);
|
||||
}
|
||||
case WorkspaceHealthFixKind.DefaultValue: {
|
||||
const filteredIssues =
|
||||
this.workspaceDefaultValueFixer.filterIssues(issues);
|
||||
|
||||
return this.workspaceDefaultValueFixer.createMetadataUpdates(
|
||||
manager,
|
||||
objectMetadataCollection,
|
||||
filteredIssues,
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export class CustomObjectMetadata extends BaseObjectMetadata {
|
||||
description: 'Name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
icon: 'IconAbc',
|
||||
defaultValue: { value: 'Untitled' },
|
||||
defaultValue: "'Untitled'",
|
||||
})
|
||||
name: string;
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ export abstract class BaseObjectMetadata {
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
defaultValue: { type: 'uuid' },
|
||||
defaultValue: 'uuid',
|
||||
icon: 'Icon123',
|
||||
})
|
||||
@IsSystem()
|
||||
@ -21,7 +21,7 @@ export abstract class BaseObjectMetadata {
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
defaultValue: { type: 'now' },
|
||||
defaultValue: 'now',
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@ -31,7 +31,7 @@ export abstract class BaseObjectMetadata {
|
||||
label: 'Update date',
|
||||
description: 'Update date',
|
||||
icon: 'IconCalendar',
|
||||
defaultValue: { type: 'now' },
|
||||
defaultValue: 'now',
|
||||
})
|
||||
@IsSystem()
|
||||
updatedAt: Date;
|
||||
|
||||
@ -47,7 +47,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Type',
|
||||
description: 'Activity type',
|
||||
icon: 'IconCheckbox',
|
||||
defaultValue: { value: 'Note' },
|
||||
defaultValue: "'Note'",
|
||||
})
|
||||
type: string;
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ export class CalendarChannelObjectMetadata extends BaseObjectMetadata {
|
||||
color: 'orange',
|
||||
},
|
||||
],
|
||||
defaultValue: { value: CalendarChannelVisibility.SHARE_EVERYTHING },
|
||||
defaultValue: `'${CalendarChannelVisibility.SHARE_EVERYTHING}'`,
|
||||
})
|
||||
visibility: string;
|
||||
|
||||
@ -82,7 +82,7 @@ export class CalendarChannelObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Is Contact Auto Creation Enabled',
|
||||
description: 'Is Contact Auto Creation Enabled',
|
||||
icon: 'IconUserCircle',
|
||||
defaultValue: { value: true },
|
||||
defaultValue: true,
|
||||
})
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@ -92,7 +92,7 @@ export class CalendarChannelObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Is Sync Enabled',
|
||||
description: 'Is Sync Enabled',
|
||||
icon: 'IconRefresh',
|
||||
defaultValue: { value: true },
|
||||
defaultValue: true,
|
||||
})
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
|
||||
@ -100,7 +100,7 @@ export class CalendarEventAttendeeObjectMetadata extends BaseObjectMetadata {
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
defaultValue: { value: CalendarEventAttendeeResponseStatus.NEEDS_ACTION },
|
||||
defaultValue: `'${CalendarEventAttendeeResponseStatus.NEEDS_ACTION}'`,
|
||||
})
|
||||
responseStatus: string;
|
||||
|
||||
|
||||
@ -106,7 +106,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata {
|
||||
description:
|
||||
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
|
||||
icon: 'IconTarget',
|
||||
defaultValue: { value: false },
|
||||
defaultValue: false,
|
||||
})
|
||||
idealCustomerProfile: boolean;
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ export class FavoriteObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Position',
|
||||
description: 'Favorite position',
|
||||
icon: 'IconList',
|
||||
defaultValue: { value: 0 },
|
||||
defaultValue: 0,
|
||||
})
|
||||
position: number;
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
|
||||
color: 'orange',
|
||||
},
|
||||
],
|
||||
defaultValue: { value: 'share_everything' },
|
||||
defaultValue: "'share_everything'",
|
||||
})
|
||||
visibility: string;
|
||||
|
||||
@ -73,7 +73,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
|
||||
{ value: 'email', label: 'Email', position: 0, color: 'green' },
|
||||
{ value: 'sms', label: 'SMS', position: 1, color: 'blue' },
|
||||
],
|
||||
defaultValue: { value: 'email' },
|
||||
defaultValue: "'email'",
|
||||
})
|
||||
type: string;
|
||||
|
||||
@ -83,7 +83,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Is Contact Auto Creation Enabled',
|
||||
description: 'Is Contact Auto Creation Enabled',
|
||||
icon: 'IconUserCircle',
|
||||
defaultValue: { value: true },
|
||||
defaultValue: true,
|
||||
})
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ export class MessageParticipantObjectMetadata extends BaseObjectMetadata {
|
||||
{ value: 'cc', label: 'Cc', position: 2, color: 'orange' },
|
||||
{ value: 'bcc', label: 'Bcc', position: 3, color: 'red' },
|
||||
],
|
||||
defaultValue: { value: 'from' },
|
||||
defaultValue: "'from'",
|
||||
})
|
||||
role: string;
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ export class MessageObjectMetadata extends BaseObjectMetadata {
|
||||
{ value: 'incoming', label: 'Incoming', position: 0, color: 'green' },
|
||||
{ value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' },
|
||||
],
|
||||
defaultValue: { value: 'incoming' },
|
||||
defaultValue: "'incoming'",
|
||||
})
|
||||
direction: string;
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Probability',
|
||||
description: 'Opportunity probability',
|
||||
icon: 'IconProgressCheck',
|
||||
defaultValue: { value: '0' },
|
||||
defaultValue: "'0'",
|
||||
})
|
||||
probability: string;
|
||||
|
||||
@ -85,7 +85,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
|
||||
},
|
||||
{ value: 'CUSTOMER', label: 'Customer', position: 4, color: 'yellow' },
|
||||
],
|
||||
defaultValue: { value: 'NEW' },
|
||||
defaultValue: "'NEW'",
|
||||
})
|
||||
stage: string;
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ export class ViewFieldObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Visible',
|
||||
description: 'View Field visibility',
|
||||
icon: 'IconEye',
|
||||
defaultValue: { value: true },
|
||||
defaultValue: true,
|
||||
})
|
||||
isVisible: boolean;
|
||||
|
||||
@ -43,7 +43,7 @@ export class ViewFieldObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Size',
|
||||
description: 'View Field size',
|
||||
icon: 'IconEye',
|
||||
defaultValue: { value: 0 },
|
||||
defaultValue: 0,
|
||||
})
|
||||
size: number;
|
||||
|
||||
@ -53,7 +53,7 @@ export class ViewFieldObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Position',
|
||||
description: 'View Field position',
|
||||
icon: 'IconList',
|
||||
defaultValue: { value: 0 },
|
||||
defaultValue: 0,
|
||||
})
|
||||
position: number;
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ export class ViewFilterObjectMetadata extends BaseObjectMetadata {
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Operand',
|
||||
description: 'View Filter operand',
|
||||
defaultValue: { value: 'Contains' },
|
||||
defaultValue: "'Contains'",
|
||||
})
|
||||
operand: string;
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ export class ViewSortObjectMetadata extends BaseObjectMetadata {
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Direction',
|
||||
description: 'View Sort direction',
|
||||
defaultValue: { value: 'asc' },
|
||||
defaultValue: "'asc'",
|
||||
})
|
||||
direction: string;
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Type',
|
||||
description: 'View type',
|
||||
defaultValue: { value: 'table' },
|
||||
defaultValue: "'table'",
|
||||
})
|
||||
type: string;
|
||||
|
||||
@ -53,7 +53,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Key',
|
||||
description: 'View key',
|
||||
options: [{ value: 'INDEX', label: 'Index', position: 0, color: 'red' }],
|
||||
defaultValue: { value: 'INDEX' },
|
||||
defaultValue: "'INDEX'",
|
||||
})
|
||||
@IsNullable()
|
||||
key: string;
|
||||
@ -88,7 +88,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Compact View',
|
||||
description: 'Describes if the view is in compact mode',
|
||||
defaultValue: { value: false },
|
||||
defaultValue: false,
|
||||
})
|
||||
isCompact: boolean;
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Color Scheme',
|
||||
description: 'Preferred color scheme',
|
||||
icon: 'IconColorSwatch',
|
||||
defaultValue: { value: 'Light' },
|
||||
defaultValue: "'Light'",
|
||||
})
|
||||
colorScheme: string;
|
||||
|
||||
@ -59,7 +59,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
|
||||
label: 'Language',
|
||||
description: 'Preferred language',
|
||||
icon: 'IconLanguage',
|
||||
defaultValue: { value: 'en' },
|
||||
defaultValue: "'en'",
|
||||
})
|
||||
locale: string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user