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:
Jérémy M
2024-03-27 10:56:04 +01:00
committed by GitHub
parent 90ce7709dd
commit 5c0b65eecb
43 changed files with 481 additions and 328 deletions

View File

@ -23,13 +23,19 @@ export const useFieldMetadataItem = () => {
options?: Omit<FieldMetadataOption, 'id'>[]; options?: Omit<FieldMetadataOption, 'id'>[];
type: FieldMetadataType; type: FieldMetadataType;
}, },
) => ) => {
createOneFieldMetadataItem({ const formatedInput = formatFieldMetadataItemInput(input);
...formatFieldMetadataItemInput(input), const defaultValue = input.defaultValue
defaultValue: input.defaultValue, ? `'${input.defaultValue}'`
: formatedInput.defaultValue ?? undefined;
return createOneFieldMetadataItem({
...formatedInput,
defaultValue,
objectMetadataId: input.objectMetadataId, objectMetadataId: input.objectMetadataId,
type: input.type, type: input.type,
}); });
};
const editMetadataField = ( const editMetadataField = (
input: Pick<Field, 'id' | 'label' | 'icon' | 'description'> & { input: Pick<Field, 'id' | 'label' | 'icon' | 'description'> & {

View File

@ -74,7 +74,7 @@ describe('formatFieldMetadataItemInput', () => {
value: 'OPTION_2', value: 'OPTION_2',
}, },
], ],
defaultValue: 'OPTION_1', defaultValue: "'OPTION_1'",
}; };
const result = formatFieldMetadataItemInput(input); const result = formatFieldMetadataItemInput(input);

View File

@ -44,7 +44,7 @@ export const formatFieldMetadataItemInput = (
return { return {
defaultValue: defaultOption defaultValue: defaultOption
? getOptionValueFromLabel(defaultOption.label) ? `'${getOptionValueFromLabel(defaultOption.label)}'`
: undefined, : undefined,
description: input.description?.trim() ?? null, description: input.description?.trim() ?? null,
icon: input.icon, icon: input.icon,

View File

@ -107,7 +107,7 @@ export const SettingsObjectFieldEdit = () => {
const selectOptions = activeMetadataField.options?.map((option) => ({ const selectOptions = activeMetadataField.options?.map((option) => ({
...option, ...option,
isDefault: defaultValue?.value === option.value, isDefault: defaultValue === `'${option.value}'`,
})); }));
selectOptions?.sort( selectOptions?.sort(
(optionA, optionB) => optionA.position - optionB.position, (optionA, optionB) => optionA.position - optionB.position,

View File

@ -1,7 +1,6 @@
import { DynamicModule, Module } from '@nestjs/common'; import { DynamicModule, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { DevtoolsModule } from '@nestjs/devtools-integration';
import { GraphQLModule } from '@nestjs/graphql'; import { GraphQLModule } from '@nestjs/graphql';
import { existsSync } from 'fs'; 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 { ApiRestModule } from 'src/engine/api/rest/api-rest.module';
import { ModulesModule } from 'src/modules/modules.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 { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.module';
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-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'; import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module';
@ -23,13 +21,13 @@ import { IntegrationsModule } from './engine/integrations/integrations.module';
@Module({ @Module({
imports: [ imports: [
// Nest.js devtools, use devtools.nestjs.com to debug // Nest.js devtools, use devtools.nestjs.com to debug
DevtoolsModule.registerAsync({ // DevtoolsModule.registerAsync({
useFactory: (environmentService: EnvironmentService) => ({ // useFactory: (environmentService: EnvironmentService) => ({
http: environmentService.get('DEBUG_MODE'), // http: environmentService.get('DEBUG_MODE'),
port: environmentService.get('DEBUG_PORT'), // port: environmentService.get('DEBUG_PORT'),
}), // }),
inject: [EnvironmentService], // inject: [EnvironmentService],
}), // }),
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
}), }),

View File

@ -34,9 +34,8 @@ export const currencyFields = (
isNullable: true, isNullable: true,
...(inferredFieldMetadata ...(inferredFieldMetadata
? { ? {
defaultValue: { defaultValue:
value: inferredFieldMetadata.defaultValue?.amountMicros ?? null, inferredFieldMetadata.defaultValue?.amountMicros ?? null,
},
} }
: {}), : {}),
} satisfies FieldMetadataInterface<FieldMetadataType.NUMERIC>, } satisfies FieldMetadataInterface<FieldMetadataType.NUMERIC>,
@ -52,9 +51,8 @@ export const currencyFields = (
isNullable: true, isNullable: true,
...(inferredFieldMetadata ...(inferredFieldMetadata
? { ? {
defaultValue: { defaultValue:
value: inferredFieldMetadata.defaultValue?.currencyCode ?? null, inferredFieldMetadata.defaultValue?.currencyCode ?? null,
},
} }
: {}), : {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>, } satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,

View File

@ -34,9 +34,7 @@ export const fullNameFields = (
isNullable: true, isNullable: true,
...(inferredFieldMetadata ...(inferredFieldMetadata
? { ? {
defaultValue: { defaultValue: inferredFieldMetadata.defaultValue?.firstName ?? null,
value: inferredFieldMetadata.defaultValue?.firstName ?? null,
},
} }
: {}), : {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>, } satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
@ -52,9 +50,7 @@ export const fullNameFields = (
isNullable: true, isNullable: true,
...(inferredFieldMetadata ...(inferredFieldMetadata
? { ? {
defaultValue: { defaultValue: inferredFieldMetadata.defaultValue?.lastName ?? null,
value: inferredFieldMetadata.defaultValue?.lastName ?? null,
},
} }
: {}), : {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>, } satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,

View File

@ -34,9 +34,7 @@ export const linkFields = (
isNullable: true, isNullable: true,
...(inferredFieldMetadata ...(inferredFieldMetadata
? { ? {
defaultValue: { defaultValue: inferredFieldMetadata.defaultValue?.label ?? null,
value: inferredFieldMetadata.defaultValue?.label ?? null,
},
} }
: {}), : {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>, } satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
@ -52,9 +50,7 @@ export const linkFields = (
isNullable: true, isNullable: true,
...(inferredFieldMetadata ...(inferredFieldMetadata
? { ? {
defaultValue: { defaultValue: inferredFieldMetadata.defaultValue?.url ?? null,
value: inferredFieldMetadata.defaultValue?.url ?? null,
},
} }
: {}), : {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>, } satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,

View File

@ -6,88 +6,95 @@ import {
IsNotEmpty, IsNotEmpty,
IsNumber, IsNumber,
IsNumberString, IsNumberString,
IsString,
Matches, Matches,
ValidateIf, ValidateIf,
} from 'class-validator'; } 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 { export class FieldMetadataDefaultValueString {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsString() @IsQuotedString()
value: string | null; value: string | null;
} }
export class FieldMetadataDefaultValueRawJson { export class FieldMetadataDefaultValueRawJson {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsJSON() @IsJSON()
value: JSON | null; value: JSON | null;
} }
export class FieldMetadataDefaultValueNumber { export class FieldMetadataDefaultValueNumber {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsNumber() @IsNumber()
value: number | null; value: number | null;
} }
export class FieldMetadataDefaultValueBoolean { export class FieldMetadataDefaultValueBoolean {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsBoolean() @IsBoolean()
value: boolean | null; value: boolean | null;
} }
export class FieldMetadataDefaultValueStringArray { export class FieldMetadataDefaultValueStringArray {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsArray() @IsArray()
@IsString({ each: true }) @IsQuotedString({ each: true })
value: string[] | null; value: string[] | null;
} }
export class FieldMetadataDefaultValueDateTime { export class FieldMetadataDefaultValueDateTime {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsDate() @IsDate()
value: Date | null; value: Date | null;
} }
export class FieldMetadataDefaultValueLink { export class FieldMetadataDefaultValueLink {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsString() @IsQuotedString()
label: string | null; label: string | null;
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsString() @IsQuotedString()
url: string | null; url: string | null;
} }
export class FieldMetadataDefaultValueCurrency { export class FieldMetadataDefaultValueCurrency {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsNumberString() @IsNumberString()
amountMicros: string | null; amountMicros: string | null;
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsString() @IsQuotedString()
currencyCode: string | null; currencyCode: string | null;
} }
export class FieldMetadataDefaultValueFullName { export class FieldMetadataDefaultValueFullName {
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsString() @IsQuotedString()
firstName: string | null; firstName: string | null;
@ValidateIf((_object, value) => value !== null) @ValidateIf((object, value) => value !== null)
@IsString() @IsQuotedString()
lastName: string | null; lastName: string | null;
} }
export class FieldMetadataDynamicDefaultValueUuid { export class FieldMetadataDefaultValueUuidFunction {
@Matches('uuid') @Matches(fieldMetadataDefaultValueFunctionName.UUID)
@IsNotEmpty() @IsNotEmpty()
@IsString() value: typeof fieldMetadataDefaultValueFunctionName.UUID;
type: 'uuid';
} }
export class FieldMetadataDynamicDefaultValueNow { export class FieldMetadataDefaultValueNowFunction {
@Matches('now') @Matches(fieldMetadataDefaultValueFunctionName.NOW)
@IsNotEmpty() @IsNotEmpty()
@IsString() value: typeof fieldMetadataDefaultValueFunctionName.NOW;
type: 'now';
} }

View File

@ -300,8 +300,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
existingFieldMetadata.type !== FieldMetadataType.SELECT existingFieldMetadata.type !== FieldMetadataType.SELECT
? existingFieldMetadata.defaultValue ? existingFieldMetadata.defaultValue
: updatableFieldInput.defaultValue : updatableFieldInput.defaultValue
? // Todo: we need to rework DefaultValue typing and format to be simpler, there is no need to have this complexity ? updatableFieldInput.defaultValue
{ value: updatableFieldInput.defaultValue as unknown as string }
: null, : null,
// If the name is updated, the targetColumnMap should be updated as well // If the name is updated, the targetColumnMap should be updated as well
targetColumnMap: updatableFieldInput.name targetColumnMap: updatableFieldInput.name

View File

@ -8,38 +8,25 @@ import {
FieldMetadataDefaultValueNumber, FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValueString, FieldMetadataDefaultValueString,
FieldMetadataDefaultValueStringArray, FieldMetadataDefaultValueStringArray,
FieldMetadataDynamicDefaultValueNow, FieldMetadataDefaultValueUuidFunction,
FieldMetadataDynamicDefaultValueUuid, FieldMetadataDefaultValueNowFunction,
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
type FieldMetadataScalarDefaultValue = type ExtractValueType<T> = T extends { value: infer V } ? V : T;
| FieldMetadataDefaultValueString
| FieldMetadataDefaultValueNumber
| FieldMetadataDefaultValueBoolean
| FieldMetadataDefaultValueDateTime;
export type FieldMetadataDynamicDefaultValue = type UnionOfValues<T> = T[keyof T];
| FieldMetadataDynamicDefaultValueUuid
| FieldMetadataDynamicDefaultValueNow;
type AllFieldMetadataDefaultValueTypes =
| FieldMetadataScalarDefaultValue
| FieldMetadataDynamicDefaultValue
| FieldMetadataDefaultValueLink
| FieldMetadataDefaultValueCurrency
| FieldMetadataDefaultValueFullName;
type FieldMetadataDefaultValueMapping = { type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.UUID]: [FieldMetadataType.UUID]:
| FieldMetadataDefaultValueString | FieldMetadataDefaultValueString
| FieldMetadataDynamicDefaultValueUuid; | FieldMetadataDefaultValueUuidFunction;
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString; [FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString; [FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString; [FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
[FieldMetadataType.DATE_TIME]: [FieldMetadataType.DATE_TIME]:
| FieldMetadataDefaultValueDateTime | FieldMetadataDefaultValueDateTime
| FieldMetadataDynamicDefaultValueNow; | FieldMetadataDefaultValueNowFunction;
[FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean; [FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean;
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber; [FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
[FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber; [FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber;
@ -54,35 +41,31 @@ type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson; [FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
}; };
export type FieldMetadataClassValidation =
UnionOfValues<FieldMetadataDefaultValueMapping>;
export type FieldMetadataFunctionDefaultValue = ExtractValueType<
FieldMetadataDefaultValueUuidFunction | FieldMetadataDefaultValueNowFunction
>;
type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [ type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [
T, T,
] extends [keyof FieldMetadataDefaultValueMapping] ] extends [keyof FieldMetadataDefaultValueMapping]
? FieldMetadataDefaultValueMapping[T] | null ? ExtractValueType<FieldMetadataDefaultValueMapping[T]> | null
: T extends 'default' : T extends 'default'
? AllFieldMetadataDefaultValueTypes | null ? ExtractValueType<UnionOfValues<FieldMetadataDefaultValueMapping>> | null
: never; : never;
export type FieldMetadataDefaultValue< export type FieldMetadataDefaultValue<
T extends FieldMetadataType | 'default' = 'default', T extends FieldMetadataType | 'default' = 'default',
> = DefaultValueByFieldMetadata<T>; > = 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 = { type FieldMetadataDefaultValueExtractedTypes = {
[K in keyof FieldMetadataDefaultValueMapping]: FieldMetadataDefaultValueExtractNestedType< [K in keyof FieldMetadataDefaultValueMapping]: ExtractValueType<
FieldMetadataDefaultValueMapping[K] FieldMetadataDefaultValueMapping[K]
>; >;
}; };
export type FieldMetadataDefaultSerializableValue = export type FieldMetadataDefaultSerializableValue =
| FieldMetadataDefaultValueExtractedTypes[keyof FieldMetadataDefaultValueExtractedTypes] | FieldMetadataDefaultValueExtractedTypes[keyof FieldMetadataDefaultValueExtractedTypes]
| FieldMetadataDynamicDefaultValue
| null; | null;

View File

@ -8,25 +8,19 @@ describe('serializeDefaultValue', () => {
}); });
it('should handle uuid dynamic default value', () => { it('should handle uuid dynamic default value', () => {
expect(serializeDefaultValue({ type: 'uuid' })).toBe( expect(serializeDefaultValue('uuid')).toBe('public.uuid_generate_v4()');
'public.uuid_generate_v4()',
);
}); });
it('should handle now dynamic default value', () => { 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', () => { it('should throw BadRequestException for invalid dynamic default value type', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment expect(() => serializeDefaultValue('invalid')).toThrow(BadRequestException);
// @ts-expect-error Just for testing purposes
expect(() => serializeDefaultValue({ type: 'invalid' })).toThrow(
BadRequestException,
);
}); });
it('should handle string static default value', () => { it('should handle string static default value', () => {
expect(serializeDefaultValue('test')).toBe("'test'"); expect(serializeDefaultValue("'test'")).toBe("'test'");
}); });
it('should handle number static default value', () => { it('should handle number static default value', () => {

View File

@ -10,110 +10,105 @@ describe('validateDefaultValueForType', () => {
// Dynamic default values // Dynamic default values
it('should validate uuid dynamic default value for UUID type', () => { it('should validate uuid dynamic default value for UUID type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.UUID, 'uuid')).toBe(
validateDefaultValueForType(FieldMetadataType.UUID, { type: 'uuid' }), true,
).toBe(true); );
}); });
it('should validate now dynamic default value for DATE_TIME type', () => { it('should validate now dynamic default value for DATE_TIME type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.DATE_TIME, { type: 'now' }), validateDefaultValueForType(FieldMetadataType.DATE_TIME, 'now'),
).toBe(true); ).toBe(true);
}); });
it('should return false for mismatched dynamic default value', () => { it('should return false for mismatched dynamic default value', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.UUID, 'now')).toBe(
validateDefaultValueForType(FieldMetadataType.UUID, { type: 'now' }), false,
).toBe(false); );
}); });
// Static default values // Static default values
it('should validate string default value for TEXT type', () => { it('should validate string default value for TEXT type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.TEXT, "'test'")).toBe(
validateDefaultValueForType(FieldMetadataType.TEXT, { value: 'test' }), true,
).toBe(true); );
}); });
it('should return false for invalid string default value for TEXT type', () => { it('should return false for invalid string default value for TEXT type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.TEXT, 123)).toBe(
validateDefaultValueForType(FieldMetadataType.TEXT, { value: 123 }), false,
).toBe(false); );
}); });
it('should validate string default value for PHONE type', () => { it('should validate string default value for PHONE type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.PHONE, { validateDefaultValueForType(FieldMetadataType.PHONE, "'+123456789'"),
value: '+123456789',
}),
).toBe(true); ).toBe(true);
}); });
it('should return false for invalid string default value for PHONE type', () => { it('should return false for invalid string default value for PHONE type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.PHONE, 123)).toBe(
validateDefaultValueForType(FieldMetadataType.PHONE, { value: 123 }), false,
).toBe(false); );
}); });
it('should validate string default value for EMAIL type', () => { it('should validate string default value for EMAIL type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.EMAIL, { validateDefaultValueForType(
value: 'test@example.com', FieldMetadataType.EMAIL,
}), "'test@example.com'",
),
).toBe(true); ).toBe(true);
}); });
it('should return false for invalid string default value for EMAIL type', () => { it('should return false for invalid string default value for EMAIL type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.EMAIL, 123)).toBe(
validateDefaultValueForType(FieldMetadataType.EMAIL, { value: 123 }), false,
).toBe(false); );
}); });
it('should validate number default value for NUMBER type', () => { it('should validate number default value for NUMBER type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.NUMBER, 100)).toBe(
validateDefaultValueForType(FieldMetadataType.NUMBER, { value: 100 }), true,
).toBe(true); );
}); });
it('should return false for invalid number default value for NUMBER type', () => { it('should return false for invalid number default value for NUMBER type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.NUMBER, '100')).toBe(
validateDefaultValueForType(FieldMetadataType.NUMBER, { value: '100' }), false,
).toBe(false); );
}); });
it('should validate number default value for PROBABILITY type', () => { it('should validate number default value for PROBABILITY type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.PROBABILITY, { validateDefaultValueForType(FieldMetadataType.PROBABILITY, 0.5),
value: 0.5,
}),
).toBe(true); ).toBe(true);
}); });
it('should return false for invalid number default value for PROBABILITY type', () => { it('should return false for invalid number default value for PROBABILITY type', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.PROBABILITY, { validateDefaultValueForType(FieldMetadataType.PROBABILITY, '50%'),
value: '50%',
}),
).toBe(false); ).toBe(false);
}); });
it('should validate boolean default value for BOOLEAN type', () => { it('should validate boolean default value for BOOLEAN type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.BOOLEAN, true)).toBe(
validateDefaultValueForType(FieldMetadataType.BOOLEAN, { value: true }), true,
).toBe(true); );
}); });
it('should return false for invalid boolean default value for BOOLEAN type', () => { it('should return false for invalid boolean default value for BOOLEAN type', () => {
expect( expect(validateDefaultValueForType(FieldMetadataType.BOOLEAN, 'true')).toBe(
validateDefaultValueForType(FieldMetadataType.BOOLEAN, { value: 'true' }), false,
).toBe(false); );
}); });
// LINK type // LINK type
it('should validate LINK default value', () => { it('should validate LINK default value', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.LINK, { validateDefaultValueForType(FieldMetadataType.LINK, {
label: 'http://example.com', label: "'http://example.com'",
url: 'Example', url: "'Example'",
}), }),
).toBe(true); ).toBe(true);
}); });
@ -134,7 +129,7 @@ describe('validateDefaultValueForType', () => {
expect( expect(
validateDefaultValueForType(FieldMetadataType.CURRENCY, { validateDefaultValueForType(FieldMetadataType.CURRENCY, {
amountMicros: '100', amountMicros: '100',
currencyCode: 'USD', currencyCode: "'USD'",
}), }),
).toBe(true); ).toBe(true);
}); });
@ -144,7 +139,7 @@ describe('validateDefaultValueForType', () => {
validateDefaultValueForType( validateDefaultValueForType(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error Just for testing purposes // @ts-expect-error Just for testing purposes
{ amountMicros: 100, currencyCode: 'USD' }, { amountMicros: 100, currencyCode: "'USD'" },
FieldMetadataType.CURRENCY, FieldMetadataType.CURRENCY,
), ),
).toBe(false); ).toBe(false);
@ -153,9 +148,7 @@ describe('validateDefaultValueForType', () => {
// Unknown type // Unknown type
it('should return false for unknown type', () => { it('should return false for unknown type', () => {
expect( expect(
validateDefaultValueForType('unknown' as FieldMetadataType, { validateDefaultValueForType('unknown' as FieldMetadataType, "'test'"),
value: 'test',
}),
).toBe(false); ).toBe(false);
}); });
}); });

View File

@ -9,23 +9,21 @@ export function generateDefaultValue(
case FieldMetadataType.TEXT: case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE: case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL: case FieldMetadataType.EMAIL:
return { return "''";
value: '',
};
case FieldMetadataType.FULL_NAME: case FieldMetadataType.FULL_NAME:
return { return {
firstName: '', firstName: "''",
lastName: '', lastName: "''",
}; };
case FieldMetadataType.LINK: case FieldMetadataType.LINK:
return { return {
url: '', url: "''",
label: '', label: "''",
}; };
case FieldMetadataType.CURRENCY: case FieldMetadataType.CURRENCY:
return { return {
amountMicros: null, amountMicros: null,
currencyCode: '', currencyCode: "''",
}; };
default: default:
return null; return null;

View File

@ -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,
)
);
};

View File

@ -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 { 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 = ( export const serializeDefaultValue = (
defaultValue?: FieldMetadataDefaultSerializableValue, defaultValue?: FieldMetadataDefaultSerializableValue,
@ -11,13 +12,10 @@ export const serializeDefaultValue = (
return null; return null;
} }
// Dynamic default values // Function default values
if ( if (isFunctionDefaultValue(defaultValue)) {
!Array.isArray(defaultValue) && const serializedTypeDefaultValue =
typeof defaultValue === 'object' && serializeFunctionDefaultValue(defaultValue);
'type' in defaultValue
) {
const serializedTypeDefaultValue = serializeTypeDefaultValue(defaultValue);
if (!serializedTypeDefaultValue) { if (!serializedTypeDefaultValue) {
throw new BadRequestException('Invalid default value'); throw new BadRequestException('Invalid default value');
@ -27,8 +25,8 @@ export const serializeDefaultValue = (
} }
// Static default values // Static default values
if (typeof defaultValue === 'string') { if (typeof defaultValue === 'string' && defaultValue.startsWith("'")) {
return `'${defaultValue}'`; return defaultValue;
} }
if (typeof defaultValue === 'number') { if (typeof defaultValue === 'number') {
@ -51,5 +49,5 @@ export const serializeDefaultValue = (
return `'${JSON.stringify(defaultValue)}'`; return `'${JSON.stringify(defaultValue)}'`;
} }
throw new BadRequestException('Invalid default value'); throw new BadRequestException(`Invalid default value "${defaultValue}"`);
}; };

View File

@ -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;
}
};

View File

@ -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;
}
};

View File

@ -1,7 +1,10 @@
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator'; 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 { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { import {
@ -14,21 +17,22 @@ import {
FieldMetadataDefaultValueNumber, FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValueString, FieldMetadataDefaultValueString,
FieldMetadataDefaultValueStringArray, FieldMetadataDefaultValueStringArray,
FieldMetadataDynamicDefaultValueNow, FieldMetadataDefaultValueNowFunction,
FieldMetadataDynamicDefaultValueUuid, FieldMetadataDefaultValueUuidFunction,
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; } 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 = { export const defaultValueValidatorsMap = {
[FieldMetadataType.UUID]: [ [FieldMetadataType.UUID]: [
FieldMetadataDefaultValueString, FieldMetadataDefaultValueString,
FieldMetadataDynamicDefaultValueUuid, FieldMetadataDefaultValueUuidFunction,
], ],
[FieldMetadataType.TEXT]: [FieldMetadataDefaultValueString], [FieldMetadataType.TEXT]: [FieldMetadataDefaultValueString],
[FieldMetadataType.PHONE]: [FieldMetadataDefaultValueString], [FieldMetadataType.PHONE]: [FieldMetadataDefaultValueString],
[FieldMetadataType.EMAIL]: [FieldMetadataDefaultValueString], [FieldMetadataType.EMAIL]: [FieldMetadataDefaultValueString],
[FieldMetadataType.DATE_TIME]: [ [FieldMetadataType.DATE_TIME]: [
FieldMetadataDefaultValueDateTime, FieldMetadataDefaultValueDateTime,
FieldMetadataDynamicDefaultValueNow, FieldMetadataDefaultValueNowFunction,
], ],
[FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean], [FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean],
[FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber], [FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber],
@ -54,10 +58,14 @@ export const validateDefaultValueForType = (
if (!validators) return false; if (!validators) return false;
const isValid = validators.some((validator) => { const isValid = validators.some((validator) => {
const conputedDefaultValue = isCompositeFieldMetadataType(type)
? defaultValue
: { value: defaultValue };
const defaultValueInstance = plainToInstance< const defaultValueInstance = plainToInstance<
any, any,
FieldMetadataDefaultValue FieldMetadataClassValidation
>(validator, defaultValue); >(validator, conputedDefaultValue as FieldMetadataClassValidation);
return ( return (
validateSync(defaultValueInstance, { validateSync(defaultValueInstance, {

View File

@ -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`;
},
},
});
};
}

View File

@ -256,7 +256,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isCustom: false, isCustom: false,
isSystem: true, isSystem: true,
workspaceId: objectMetadataInput.workspaceId, workspaceId: objectMetadataInput.workspaceId,
defaultValue: { type: 'uuid' }, defaultValue: 'uuid',
}, },
{ {
standardId: customObjectStandardFieldIds.name, standardId: customObjectStandardFieldIds.name,
@ -272,7 +272,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isActive: true, isActive: true,
isCustom: false, isCustom: false,
workspaceId: objectMetadataInput.workspaceId, workspaceId: objectMetadataInput.workspaceId,
defaultValue: { value: 'Untitled' }, defaultValue: "'Untitled'",
}, },
{ {
standardId: baseObjectStandardFieldIds.createdAt, standardId: baseObjectStandardFieldIds.createdAt,
@ -288,7 +288,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isActive: true, isActive: true,
isCustom: false, isCustom: false,
workspaceId: objectMetadataInput.workspaceId, workspaceId: objectMetadataInput.workspaceId,
defaultValue: { type: 'now' }, defaultValue: 'now',
}, },
{ {
standardId: baseObjectStandardFieldIds.updatedAt, standardId: baseObjectStandardFieldIds.updatedAt,
@ -305,7 +305,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isCustom: false, isCustom: false,
isSystem: true, isSystem: true,
workspaceId: objectMetadataInput.workspaceId, workspaceId: objectMetadataInput.workspaceId,
defaultValue: { type: 'now' }, defaultValue: 'now',
}, },
{ {
standardId: customObjectStandardFieldIds.position, standardId: customObjectStandardFieldIds.position,

View File

@ -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 { 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 { 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 { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { import {
@ -35,8 +34,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
fieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>, fieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
options?: WorkspaceColumnActionOptions, options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate { ): WorkspaceMigrationColumnCreate {
const defaultValue = const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue;
this.getDefaultValue(fieldMetadata.defaultValue) ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue); const serializedDefaultValue = serializeDefaultValue(defaultValue);
return { return {
@ -54,8 +52,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
options?: WorkspaceColumnActionOptions, options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter { ): WorkspaceMigrationColumnAlter {
const defaultValue = const defaultValue =
this.getDefaultValue(alteredFieldMetadata.defaultValue) ?? alteredFieldMetadata.defaultValue ?? options?.defaultValue;
options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue); const serializedDefaultValue = serializeDefaultValue(defaultValue);
const currentColumnName = currentFieldMetadata.targetColumnMap.value; const currentColumnName = currentFieldMetadata.targetColumnMap.value;
const alteredColumnName = alteredFieldMetadata.targetColumnMap.value; const alteredColumnName = alteredFieldMetadata.targetColumnMap.value;
@ -75,9 +72,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
columnName: currentColumnName, columnName: currentColumnName,
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type), columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
isNullable: currentFieldMetadata.isNullable, isNullable: currentFieldMetadata.isNullable,
defaultValue: serializeDefaultValue( defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue),
this.getDefaultValue(currentFieldMetadata.defaultValue),
),
}, },
alteredColumnDefinition: { alteredColumnDefinition: {
columnName: alteredColumnName, 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;
}
}
} }

View File

@ -26,8 +26,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
fieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>, fieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
options: WorkspaceColumnActionOptions, options: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate { ): WorkspaceMigrationColumnCreate {
const defaultValue = const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue;
fieldMetadata.defaultValue?.value ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue); const serializedDefaultValue = serializeDefaultValue(defaultValue);
const enumOptions = fieldMetadata.options const enumOptions = fieldMetadata.options
? [...fieldMetadata.options.map((option) => option.value)] ? [...fieldMetadata.options.map((option) => option.value)]
@ -50,7 +49,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
options: WorkspaceColumnActionOptions, options: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter { ): WorkspaceMigrationColumnAlter {
const defaultValue = const defaultValue =
alteredFieldMetadata.defaultValue?.value ?? options?.defaultValue; alteredFieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue); const serializedDefaultValue = serializeDefaultValue(defaultValue);
const enumOptions = alteredFieldMetadata.options const enumOptions = alteredFieldMetadata.options
@ -94,9 +93,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
: undefined, : undefined,
isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT, isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: currentFieldMetadata.isNullable, isNullable: currentFieldMetadata.isNullable,
defaultValue: serializeDefaultValue( defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue),
currentFieldMetadata.defaultValue?.value,
),
}, },
alteredColumnDefinition: { alteredColumnDefinition: {
columnName: alteredColumnName, columnName: alteredColumnName,

View File

@ -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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.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 { 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() @Injectable()
export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT> { export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<WorkspaceDefaultValueFixerType> {
constructor( constructor(
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
) { ) {
super(WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT); super(
WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT,
WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
);
} }
async createWorkspaceMigrations( async createWorkspaceMigrations(
manager: EntityManager, manager: EntityManager,
objectMetadataCollection: ObjectMetadataEntity[], objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[], issues: WorkspaceHealthColumnIssue<WorkspaceDefaultValueFixerType>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> { ): Promise<Partial<WorkspaceMigrationEntity>[]> {
if (issues.length <= 0) { if (issues.length <= 0) {
return []; 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[], objectMetadataCollection: ObjectMetadataEntity[],
issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[], issues: WorkspaceHealthColumnIssue<WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> { ): 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( private computeFieldMetadataDefaultValueFromColumnDefault(
columnDefault: string | undefined, columnDefault: string | undefined,
): FieldMetadataDefaultValue<'default'> { ): FieldMetadataDefaultValue<'default'> {
@ -73,29 +199,29 @@ export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer<Workspace
} }
if (!isNaN(Number(columnDefault))) { if (!isNaN(Number(columnDefault))) {
return { value: +columnDefault }; return +columnDefault;
} }
if (columnDefault === 'true') { if (columnDefault === 'true') {
return { value: true }; return true;
} }
if (columnDefault === 'false') { if (columnDefault === 'false') {
return { value: false }; return false;
} }
if (columnDefault === '') { if (columnDefault === '') {
return { value: '' }; return "''";
} }
if (columnDefault === 'now()') { if (columnDefault === 'now()') {
return { type: 'now' }; return 'now';
} }
if (columnDefault.startsWith('public.uuid_generate_v4')) { if (columnDefault.startsWith('public.uuid_generate_v4')) {
return { type: 'uuid' }; return 'uuid';
} }
return { value: columnDefault }; return columnDefault;
} }
} }

View File

@ -7,7 +7,10 @@ import {
WorkspaceTableStructure, WorkspaceTableStructure,
WorkspaceTableStructureResult, WorkspaceTableStructureResult,
} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface'; } 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 { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { import {
@ -15,9 +18,11 @@ import {
FieldMetadataType, FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; } 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 { 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 { 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 { 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() @Injectable()
export class DatabaseStructureService { export class DatabaseStructureService {
@ -204,31 +209,49 @@ export class DatabaseStructureService {
getPostgresDefault( getPostgresDefault(
fieldMetadataType: FieldMetadataType, 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 { ): string | null | undefined {
const typeORMType = fieldMetadataTypeToColumnType( const typeORMType = fieldMetadataTypeToColumnType(
fieldMetadataType, fieldMetadataType,
) as ColumnType; ) as ColumnType;
const mainDataSource = this.typeORMService.getMainDataSource(); const mainDataSource = this.typeORMService.getMainDataSource();
if (defaultValue && 'type' in defaultValue) { let value: any =
const serializedDefaultValue = serializeTypeDefaultValue(defaultValue); // Old formart default values
defaultValue &&
typeof defaultValue === 'object' &&
'value' in defaultValue
? defaultValue.value
: defaultValue;
// Special case for uuid_generate_v4() default value // Old format for default values
if (serializedDefaultValue === 'public.uuid_generate_v4()') { // TODO: Should be removed once all default values are migrated
return 'uuid_generate_v4()'; if (
} defaultValue &&
typeof defaultValue === 'object' &&
return serializedDefaultValue; 'type' in defaultValue
) {
return this.computeFunctionDefaultValue(defaultValue.type);
} }
const value = if (isFunctionDefaultValue(value)) {
defaultValue && 'value' in defaultValue ? defaultValue.value : null; return this.computeFunctionDefaultValue(value);
}
if (typeof value === 'number') { if (typeof value === 'number') {
return value.toString(); 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({ return mainDataSource.driver.normalizeDefault({
type: typeORMType, type: typeORMType,
default: value, default: value,
@ -236,4 +259,17 @@ export class DatabaseStructureService {
// Workaround to use normalizeDefault without a complete ColumnMetadata object // Workaround to use normalizeDefault without a complete ColumnMetadata object
} as ColumnMetadata); } 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;
}
} }

View File

@ -1,4 +1,4 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
@ -67,18 +67,30 @@ export class FieldMetadataHealthService {
issues.push(...defaultValueIssues); issues.push(...defaultValueIssues);
} }
for (const compositeFieldMetadata of compositeFieldMetadataCollection) { // Only check structure on nested composite fields
const compositeFieldIssues = await this.healthCheckField( 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, tableName,
workspaceTableColumns, fieldMetadata,
computeCompositeFieldMetadata(
compositeFieldMetadata,
fieldMetadata,
),
options,
); );
issues.push(...compositeFieldIssues); issues.push(...compositeFieldMetadataIssues);
} }
} else { } else {
const fieldIssues = await this.healthCheckField( const fieldIssues = await this.healthCheckField(
@ -137,6 +149,7 @@ export class FieldMetadataHealthService {
fieldMetadata.type, fieldMetadata.type,
fieldMetadata.defaultValue, fieldMetadata.defaultValue,
); );
// Check if column exist in database // Check if column exist in database
const columnStructure = workspaceTableColumns.find( const columnStructure = workspaceTableColumns.find(
(tableDefinition) => tableDefinition.columnName === columnName, (tableDefinition) => tableDefinition.columnName === columnName,
@ -178,7 +191,7 @@ export class FieldMetadataHealthService {
if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) { if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) {
const enumValues = fieldMetadata.options?.map((option) => const enumValues = fieldMetadata.options?.map((option) =>
serializeDefaultValue(option.value), serializeDefaultValue(`'${option.value}'`),
); );
if (!enumValues.includes(columnDefaultValue)) { if (!enumValues.includes(columnDefaultValue)) {
@ -325,10 +338,11 @@ export class FieldMetadataHealthService {
isEnumFieldMetadataType(fieldMetadata.type) && isEnumFieldMetadataType(fieldMetadata.type) &&
fieldMetadata.defaultValue fieldMetadata.defaultValue
) { ) {
const enumValues = fieldMetadata.options?.map((option) => option.value); const enumValues = fieldMetadata.options?.map((option) =>
const metadataDefaultValue = ( serializeDefaultValue(`'${option.value}'`),
fieldMetadata.defaultValue as FieldMetadataDefaultValue<EnumFieldMetadataUnionType> );
)?.value; const metadataDefaultValue =
fieldMetadata.defaultValue as FieldMetadataDefaultValue<EnumFieldMetadataUnionType>;
if (metadataDefaultValue && !enumValues.includes(metadataDefaultValue)) { if (metadataDefaultValue && !enumValues.includes(metadataDefaultValue)) {
issues.push({ issues.push({
@ -341,29 +355,4 @@ export class FieldMetadataHealthService {
return issues; 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;
}
} }

View File

@ -90,6 +90,16 @@ export class WorkspaceFixService {
filteredIssues, filteredIssues,
); );
} }
case WorkspaceHealthFixKind.DefaultValue: {
const filteredIssues =
this.workspaceDefaultValueFixer.filterIssues(issues);
return this.workspaceDefaultValueFixer.createMetadataUpdates(
manager,
objectMetadataCollection,
filteredIssues,
);
}
default: { default: {
return []; return [];
} }

View File

@ -23,7 +23,7 @@ export class CustomObjectMetadata extends BaseObjectMetadata {
description: 'Name', description: 'Name',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'IconAbc', icon: 'IconAbc',
defaultValue: { value: 'Untitled' }, defaultValue: "'Untitled'",
}) })
name: string; name: string;

View File

@ -9,7 +9,7 @@ export abstract class BaseObjectMetadata {
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
label: 'Id', label: 'Id',
description: 'Id', description: 'Id',
defaultValue: { type: 'uuid' }, defaultValue: 'uuid',
icon: 'Icon123', icon: 'Icon123',
}) })
@IsSystem() @IsSystem()
@ -21,7 +21,7 @@ export abstract class BaseObjectMetadata {
label: 'Creation date', label: 'Creation date',
description: 'Creation date', description: 'Creation date',
icon: 'IconCalendar', icon: 'IconCalendar',
defaultValue: { type: 'now' }, defaultValue: 'now',
}) })
createdAt: Date; createdAt: Date;
@ -31,7 +31,7 @@ export abstract class BaseObjectMetadata {
label: 'Update date', label: 'Update date',
description: 'Update date', description: 'Update date',
icon: 'IconCalendar', icon: 'IconCalendar',
defaultValue: { type: 'now' }, defaultValue: 'now',
}) })
@IsSystem() @IsSystem()
updatedAt: Date; updatedAt: Date;

View File

@ -47,7 +47,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata {
label: 'Type', label: 'Type',
description: 'Activity type', description: 'Activity type',
icon: 'IconCheckbox', icon: 'IconCheckbox',
defaultValue: { value: 'Note' }, defaultValue: "'Note'",
}) })
type: string; type: string;

View File

@ -72,7 +72,7 @@ export class CalendarChannelObjectMetadata extends BaseObjectMetadata {
color: 'orange', color: 'orange',
}, },
], ],
defaultValue: { value: CalendarChannelVisibility.SHARE_EVERYTHING }, defaultValue: `'${CalendarChannelVisibility.SHARE_EVERYTHING}'`,
}) })
visibility: string; visibility: string;
@ -82,7 +82,7 @@ export class CalendarChannelObjectMetadata extends BaseObjectMetadata {
label: 'Is Contact Auto Creation Enabled', label: 'Is Contact Auto Creation Enabled',
description: 'Is Contact Auto Creation Enabled', description: 'Is Contact Auto Creation Enabled',
icon: 'IconUserCircle', icon: 'IconUserCircle',
defaultValue: { value: true }, defaultValue: true,
}) })
isContactAutoCreationEnabled: boolean; isContactAutoCreationEnabled: boolean;
@ -92,7 +92,7 @@ export class CalendarChannelObjectMetadata extends BaseObjectMetadata {
label: 'Is Sync Enabled', label: 'Is Sync Enabled',
description: 'Is Sync Enabled', description: 'Is Sync Enabled',
icon: 'IconRefresh', icon: 'IconRefresh',
defaultValue: { value: true }, defaultValue: true,
}) })
isSyncEnabled: boolean; isSyncEnabled: boolean;

View File

@ -100,7 +100,7 @@ export class CalendarEventAttendeeObjectMetadata extends BaseObjectMetadata {
color: 'green', color: 'green',
}, },
], ],
defaultValue: { value: CalendarEventAttendeeResponseStatus.NEEDS_ACTION }, defaultValue: `'${CalendarEventAttendeeResponseStatus.NEEDS_ACTION}'`,
}) })
responseStatus: string; responseStatus: string;

View File

@ -106,7 +106,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata {
description: description:
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you', 'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
icon: 'IconTarget', icon: 'IconTarget',
defaultValue: { value: false }, defaultValue: false,
}) })
idealCustomerProfile: boolean; idealCustomerProfile: boolean;

View File

@ -29,7 +29,7 @@ export class FavoriteObjectMetadata extends BaseObjectMetadata {
label: 'Position', label: 'Position',
description: 'Favorite position', description: 'Favorite position',
icon: 'IconList', icon: 'IconList',
defaultValue: { value: 0 }, defaultValue: 0,
}) })
position: number; position: number;

View File

@ -40,7 +40,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
color: 'orange', color: 'orange',
}, },
], ],
defaultValue: { value: 'share_everything' }, defaultValue: "'share_everything'",
}) })
visibility: string; visibility: string;
@ -73,7 +73,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
{ value: 'email', label: 'Email', position: 0, color: 'green' }, { value: 'email', label: 'Email', position: 0, color: 'green' },
{ value: 'sms', label: 'SMS', position: 1, color: 'blue' }, { value: 'sms', label: 'SMS', position: 1, color: 'blue' },
], ],
defaultValue: { value: 'email' }, defaultValue: "'email'",
}) })
type: string; type: string;
@ -83,7 +83,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
label: 'Is Contact Auto Creation Enabled', label: 'Is Contact Auto Creation Enabled',
description: 'Is Contact Auto Creation Enabled', description: 'Is Contact Auto Creation Enabled',
icon: 'IconUserCircle', icon: 'IconUserCircle',
defaultValue: { value: true }, defaultValue: true,
}) })
isContactAutoCreationEnabled: boolean; isContactAutoCreationEnabled: boolean;

View File

@ -42,7 +42,7 @@ export class MessageParticipantObjectMetadata extends BaseObjectMetadata {
{ value: 'cc', label: 'Cc', position: 2, color: 'orange' }, { value: 'cc', label: 'Cc', position: 2, color: 'orange' },
{ value: 'bcc', label: 'Bcc', position: 3, color: 'red' }, { value: 'bcc', label: 'Bcc', position: 3, color: 'red' },
], ],
defaultValue: { value: 'from' }, defaultValue: "'from'",
}) })
role: string; role: string;

View File

@ -55,7 +55,7 @@ export class MessageObjectMetadata extends BaseObjectMetadata {
{ value: 'incoming', label: 'Incoming', position: 0, color: 'green' }, { value: 'incoming', label: 'Incoming', position: 0, color: 'green' },
{ value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' }, { value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' },
], ],
defaultValue: { value: 'incoming' }, defaultValue: "'incoming'",
}) })
direction: string; direction: string;

View File

@ -63,7 +63,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
label: 'Probability', label: 'Probability',
description: 'Opportunity probability', description: 'Opportunity probability',
icon: 'IconProgressCheck', icon: 'IconProgressCheck',
defaultValue: { value: '0' }, defaultValue: "'0'",
}) })
probability: string; probability: string;
@ -85,7 +85,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
}, },
{ value: 'CUSTOMER', label: 'Customer', position: 4, color: 'yellow' }, { value: 'CUSTOMER', label: 'Customer', position: 4, color: 'yellow' },
], ],
defaultValue: { value: 'NEW' }, defaultValue: "'NEW'",
}) })
stage: string; stage: string;

View File

@ -33,7 +33,7 @@ export class ViewFieldObjectMetadata extends BaseObjectMetadata {
label: 'Visible', label: 'Visible',
description: 'View Field visibility', description: 'View Field visibility',
icon: 'IconEye', icon: 'IconEye',
defaultValue: { value: true }, defaultValue: true,
}) })
isVisible: boolean; isVisible: boolean;
@ -43,7 +43,7 @@ export class ViewFieldObjectMetadata extends BaseObjectMetadata {
label: 'Size', label: 'Size',
description: 'View Field size', description: 'View Field size',
icon: 'IconEye', icon: 'IconEye',
defaultValue: { value: 0 }, defaultValue: 0,
}) })
size: number; size: number;
@ -53,7 +53,7 @@ export class ViewFieldObjectMetadata extends BaseObjectMetadata {
label: 'Position', label: 'Position',
description: 'View Field position', description: 'View Field position',
icon: 'IconList', icon: 'IconList',
defaultValue: { value: 0 }, defaultValue: 0,
}) })
position: number; position: number;

View File

@ -31,7 +31,7 @@ export class ViewFilterObjectMetadata extends BaseObjectMetadata {
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
label: 'Operand', label: 'Operand',
description: 'View Filter operand', description: 'View Filter operand',
defaultValue: { value: 'Contains' }, defaultValue: "'Contains'",
}) })
operand: string; operand: string;

View File

@ -32,7 +32,7 @@ export class ViewSortObjectMetadata extends BaseObjectMetadata {
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
label: 'Direction', label: 'Direction',
description: 'View Sort direction', description: 'View Sort direction',
defaultValue: { value: 'asc' }, defaultValue: "'asc'",
}) })
direction: string; direction: string;

View File

@ -43,7 +43,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
label: 'Type', label: 'Type',
description: 'View type', description: 'View type',
defaultValue: { value: 'table' }, defaultValue: "'table'",
}) })
type: string; type: string;
@ -53,7 +53,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
label: 'Key', label: 'Key',
description: 'View key', description: 'View key',
options: [{ value: 'INDEX', label: 'Index', position: 0, color: 'red' }], options: [{ value: 'INDEX', label: 'Index', position: 0, color: 'red' }],
defaultValue: { value: 'INDEX' }, defaultValue: "'INDEX'",
}) })
@IsNullable() @IsNullable()
key: string; key: string;
@ -88,7 +88,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
type: FieldMetadataType.BOOLEAN, type: FieldMetadataType.BOOLEAN,
label: 'Compact View', label: 'Compact View',
description: 'Describes if the view is in compact mode', description: 'Describes if the view is in compact mode',
defaultValue: { value: false }, defaultValue: false,
}) })
isCompact: boolean; isCompact: boolean;

View File

@ -49,7 +49,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
label: 'Color Scheme', label: 'Color Scheme',
description: 'Preferred color scheme', description: 'Preferred color scheme',
icon: 'IconColorSwatch', icon: 'IconColorSwatch',
defaultValue: { value: 'Light' }, defaultValue: "'Light'",
}) })
colorScheme: string; colorScheme: string;
@ -59,7 +59,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
label: 'Language', label: 'Language',
description: 'Preferred language', description: 'Preferred language',
icon: 'IconLanguage', icon: 'IconLanguage',
defaultValue: { value: 'en' }, defaultValue: "'en'",
}) })
locale: string; locale: string;