feat: drop target column map (#4670)

This PR is dropping the column `targetColumnMap` of fieldMetadata
entities.
The goal of this column was to properly map field to their respecting
column in the table.
We decide to drop it and instead compute the column name on the fly when
we need it, as it's more easier to support.
Some parts of the code has been refactored to try making implementation
of composite type more easier to understand and maintain.

Fix #3760

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2024-04-08 16:00:28 +02:00
committed by GitHub
co-authored by Charles Bochet
parent 84f8c14e52
commit 5019b5febc
72 changed files with 1432 additions and 1853 deletions
@@ -1,188 +1,61 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
export const addressFields = (
fieldMetadata?: FieldMetadataInterface,
): FieldMetadataInterface[] => {
const inferredFieldMetadata = fieldMetadata as
| FieldMetadataInterface<FieldMetadataType.ADDRESS>
| undefined;
const targetColumnMap = inferredFieldMetadata
? generateTargetColumnMap(
inferredFieldMetadata.type,
inferredFieldMetadata.isCustom ?? false,
inferredFieldMetadata.name,
)
: {
addressStreet1: 'addressStreet1',
addressStreet2: 'addressStreet2',
addressCity: 'addressCity',
addressPostcode: 'addressPostcode',
addressState: 'addressState',
addressCountry: 'addressCountry',
addressLat: 'addressLat',
addressLng: 'addressLng',
};
return [
export const addressCompositeType: CompositeType = {
type: FieldMetadataType.ADDRESS,
properties: [
{
id: 'addressStreet1',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
name: 'addressStreet1',
label: 'Address',
targetColumnMap: {
value: targetColumnMap.addressStreet1,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressStreet1 ?? undefined,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressStreet2',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
hidden: false,
isRequired: false,
},
{
name: 'addressStreet2',
label: 'Address 2',
targetColumnMap: {
value: targetColumnMap.addressStreet2,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressStreet2 ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressCity',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
hidden: false,
isRequired: false,
},
{
name: 'addressCity',
label: 'City',
targetColumnMap: {
value: targetColumnMap.addressCity,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressCity ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressPostcode',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
hidden: false,
isRequired: false,
},
{
name: 'addressPostcode',
label: 'Postcode',
targetColumnMap: {
value: targetColumnMap.addressPostcode,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressPostcode ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressState',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
hidden: false,
isRequired: false,
},
{
name: 'addressState',
label: 'State',
targetColumnMap: {
value: targetColumnMap.addressState,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressState ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'addressCountry',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
hidden: false,
isRequired: false,
},
{
name: 'addressCountry',
label: 'Country',
targetColumnMap: {
value: targetColumnMap.addressCountry,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressCountry ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
{
id: 'addressLat',
type: FieldMetadataType.NUMBER,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
name: 'addressLat',
label: 'Latitude',
targetColumnMap: {
value: targetColumnMap.addressLat,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressLat ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.NUMBER>,
type: FieldMetadataType.NUMERIC,
hidden: false,
isRequired: false,
},
{
id: 'addressLng',
type: FieldMetadataType.NUMBER,
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
name: 'addressLng',
label: 'Longitude',
targetColumnMap: {
value: targetColumnMap.addressLng,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.addressLng ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.NUMBER>,
];
type: FieldMetadataType.NUMERIC,
hidden: false,
isRequired: false,
},
],
};
export const addressObjectDefinition = {
id: FieldMetadataType.ADDRESS.toString(),
nameSingular: 'address',
namePlural: 'address',
labelSingular: 'Address',
labelPlural: 'Addresses',
targetTableName: '',
fields: addressFields(),
fromRelations: [],
toRelations: [],
isActive: true,
isSystem: true,
isCustom: false,
isRemote: false,
} satisfies ObjectMetadataInterface;
export type AddressMetadata = {
addressStreet1: string;
addressStreet2: string;
@@ -1,80 +1,25 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
export const currencyFields = (
fieldMetadata?: FieldMetadataInterface,
): FieldMetadataInterface[] => {
const inferredFieldMetadata = fieldMetadata as
| FieldMetadataInterface<FieldMetadataType.CURRENCY>
| undefined;
const targetColumnMap = inferredFieldMetadata
? generateTargetColumnMap(
inferredFieldMetadata.type,
inferredFieldMetadata.isCustom ?? false,
inferredFieldMetadata.name,
)
: {
amountMicros: 'amountMicros',
currencyCode: 'currencyCode',
};
return [
export const currencyCompositeType: CompositeType = {
type: FieldMetadataType.CURRENCY,
properties: [
{
id: 'amountMicros',
type: FieldMetadataType.NUMERIC,
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'amountMicros',
label: 'AmountMicros',
targetColumnMap: {
value: targetColumnMap.amountMicros,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.amountMicros ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.NUMERIC>,
type: FieldMetadataType.NUMERIC,
hidden: false,
isRequired: false,
},
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'currencyCode',
label: 'Currency Code',
targetColumnMap: {
value: targetColumnMap.currencyCode,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue:
inferredFieldMetadata.defaultValue?.currencyCode ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
];
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
],
};
export const currencyObjectDefinition = {
id: FieldMetadataType.CURRENCY.toString(),
nameSingular: 'currency',
namePlural: 'currency',
labelSingular: 'Currency',
labelPlural: 'Currency',
targetTableName: '',
fields: currencyFields(),
fromRelations: [],
toRelations: [],
isActive: true,
isSystem: true,
isCustom: false,
isRemote: false,
} satisfies ObjectMetadataInterface;
export type CurrencyMetadata = {
amountMicros: number;
currencyCode: string;
@@ -1,78 +1,25 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
export const fullNameFields = (
fieldMetadata?: FieldMetadataInterface,
): FieldMetadataInterface[] => {
const inferredFieldMetadata = fieldMetadata as
| FieldMetadataInterface<FieldMetadataType.FULL_NAME>
| undefined;
const targetColumnMap = inferredFieldMetadata
? generateTargetColumnMap(
inferredFieldMetadata.type,
inferredFieldMetadata.isCustom ?? false,
inferredFieldMetadata.name,
)
: {
firstName: 'firstName',
lastName: 'lastName',
};
return [
export const fullNameCompositeType: CompositeType = {
type: FieldMetadataType.FULL_NAME,
properties: [
{
id: 'firstName',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.FULL_NAME.toString(),
name: 'firstName',
label: 'First Name',
targetColumnMap: {
value: targetColumnMap.firstName,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue: inferredFieldMetadata.defaultValue?.firstName ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'lastName',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.FULL_NAME.toString(),
hidden: false,
isRequired: false,
},
{
name: 'lastName',
label: 'Last Name',
targetColumnMap: {
value: targetColumnMap.lastName,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue: inferredFieldMetadata.defaultValue?.lastName ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
];
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
],
};
export const fullNameObjectDefinition = {
id: FieldMetadataType.FULL_NAME.toString(),
nameSingular: 'fullName',
namePlural: 'fullName',
labelSingular: 'FullName',
labelPlural: 'FullName',
targetTableName: '',
fields: fullNameFields(),
fromRelations: [],
toRelations: [],
isActive: true,
isSystem: true,
isCustom: false,
isRemote: false,
} satisfies ObjectMetadataInterface;
export type FullNameMetadata = {
firstName: string;
lastName: string;
@@ -1,21 +1,22 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { currencyFields } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
import { fullNameFields } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
import { linkFields } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
import { currencyCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
import { linkCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { addressFields } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
import { addressCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
export type CompositeFieldsDefinitionFunction = (
fieldMetadata?: FieldMetadataInterface,
) => FieldMetadataInterface[];
export const compositeDefinitions = new Map<
string,
CompositeFieldsDefinitionFunction
export const compositeTypeDefintions = new Map<
FieldMetadataType,
CompositeType
>([
[FieldMetadataType.LINK, linkFields],
[FieldMetadataType.CURRENCY, currencyFields],
[FieldMetadataType.FULL_NAME, fullNameFields],
[FieldMetadataType.ADDRESS, addressFields],
[FieldMetadataType.LINK, linkCompositeType],
[FieldMetadataType.CURRENCY, currencyCompositeType],
[FieldMetadataType.FULL_NAME, fullNameCompositeType],
[FieldMetadataType.ADDRESS, addressCompositeType],
]);
@@ -1,78 +1,25 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
export const linkFields = (
fieldMetadata?: FieldMetadataInterface,
): FieldMetadataInterface[] => {
const inferredFieldMetadata = fieldMetadata as
| FieldMetadataInterface<FieldMetadataType.LINK>
| undefined;
const targetColumnMap = inferredFieldMetadata
? generateTargetColumnMap(
inferredFieldMetadata.type,
inferredFieldMetadata.isCustom ?? false,
inferredFieldMetadata.name,
)
: {
label: 'label',
url: 'url',
};
return [
export const linkCompositeType: CompositeType = {
type: FieldMetadataType.LINK,
properties: [
{
id: 'label',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.LINK.toString(),
name: 'label',
label: 'Label',
targetColumnMap: {
value: targetColumnMap.label,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue: inferredFieldMetadata.defaultValue?.label ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
{
id: 'url',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.LINK.toString(),
hidden: false,
isRequired: false,
},
{
name: 'url',
label: 'Url',
targetColumnMap: {
value: targetColumnMap.url,
},
isNullable: true,
...(inferredFieldMetadata
? {
defaultValue: inferredFieldMetadata.defaultValue?.url ?? null,
}
: {}),
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
];
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
],
};
export const linkObjectDefinition = {
id: FieldMetadataType.LINK.toString(),
nameSingular: 'link',
namePlural: 'link',
labelSingular: 'Link',
labelPlural: 'Link',
targetTableName: '',
fields: linkFields(),
fromRelations: [],
toRelations: [],
isActive: true,
isSystem: true,
isCustom: false,
isRemote: false,
} satisfies ObjectMetadataInterface;
export type LinkMetadata = {
label: string;
url: string;
@@ -11,7 +11,6 @@ import {
} from 'typeorm';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
@@ -74,9 +73,6 @@ export class FieldMetadataEntity<
@Column({ nullable: false })
label: string;
@Column({ nullable: false, type: 'jsonb' })
targetColumnMap: FieldMetadataTargetColumnMap<T>;
@Column({ nullable: true, type: 'jsonb' })
defaultValue: FieldMetadataDefaultValue<T>;
@@ -19,7 +19,6 @@ import {
WorkspaceMigrationColumnDrop,
WorkspaceMigrationTableAction,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
@@ -37,7 +36,7 @@ import {
RelationMetadataType,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import {
@@ -128,11 +127,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
const createdFieldMetadata = await fieldMetadataRepository.save({
...fieldMetadataInput,
targetColumnMap: generateTargetColumnMap(
fieldMetadataInput.type,
!fieldMetadataInput.isRemoteCreation,
fieldMetadataInput.name,
),
isNullable: generateNullable(
fieldMetadataInput.type,
fieldMetadataInput.isNullable,
@@ -318,14 +312,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
: updatableFieldInput.defaultValue !== null
? updatableFieldInput.defaultValue
: null,
// If the name is updated, the targetColumnMap should be updated as well
targetColumnMap: updatableFieldInput.name
? generateTargetColumnMap(
existingFieldMetadata.type,
existingFieldMetadata.isCustom,
updatableFieldInput.name,
)
: existingFieldMetadata.targetColumnMap,
});
const updatedFieldMetadata = await fieldMetadataRepository.findOneOrFail({
where: { id },
@@ -417,10 +403,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeCustomName(
fieldMetadata.name,
fieldMetadata.isCustom,
),
columnName: computeColumnName(fieldMetadata),
} satisfies WorkspaceMigrationColumnDrop,
],
} satisfies WorkspaceMigrationTableAction,
@@ -0,0 +1,14 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export interface CompositeProperty {
name: string;
description?: string;
type: FieldMetadataType;
hidden: 'input' | 'output' | true | false;
isRequired: boolean;
}
export interface CompositeType {
type: FieldMetadataType;
properties: CompositeProperty[];
}
@@ -1,54 +0,0 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export interface FieldMetadataTargetColumnMapValue {
value: string;
}
export interface FieldMetadataTargetColumnMapLink {
label: string;
url: string;
}
export interface FieldMetadataTargetColumnMapCurrency {
amountMicros: string;
currencyCode: string;
}
export interface FieldMetadataTargetColumnMapFullName {
firstName: string;
lastName: string;
}
export type FieldMetadataTargetColumnMapAddress = {
addressStreet1: string;
addressStreet2: string;
addressCity: string;
addressState: string;
addressZipCode: string;
addressCountry: string;
addressLat: number;
addressLng: number;
};
type AllFieldMetadataTypes = {
[key: string]: string;
};
type FieldMetadataTypeMapping = {
[FieldMetadataType.LINK]: FieldMetadataTargetColumnMapLink;
[FieldMetadataType.CURRENCY]: FieldMetadataTargetColumnMapCurrency;
[FieldMetadataType.FULL_NAME]: FieldMetadataTargetColumnMapFullName;
[FieldMetadataType.ADDRESS]: FieldMetadataTargetColumnMapAddress;
};
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> = [
T,
] extends [keyof FieldMetadataTypeMapping]
? FieldMetadataTypeMapping[T]
: T extends 'default'
? AllFieldMetadataTypes
: FieldMetadataTargetColumnMapValue;
export type FieldMetadataTargetColumnMap<
T extends FieldMetadataType | 'default' = 'default',
> = TypeByFieldMetadata<T>;
@@ -1,4 +1,3 @@
import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
@@ -12,7 +11,6 @@ export interface FieldMetadataInterface<
type: FieldMetadataType;
name: string;
label: string;
targetColumnMap: FieldMetadataTargetColumnMap<T>;
defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>;
objectMetadataId: string;
@@ -1,41 +0,0 @@
import { BadRequestException } from '@nestjs/common';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
describe('generateTargetColumnMap', () => {
it('should generate a target column map for a given type', () => {
const textMap = generateTargetColumnMap(
FieldMetadataType.TEXT,
false,
'name',
);
expect(textMap).toEqual({ value: 'name' });
const linkMap = generateTargetColumnMap(
FieldMetadataType.LINK,
false,
'website',
);
expect(linkMap).toEqual({ label: 'websiteLabel', url: 'websiteUrl' });
const currencyMap = generateTargetColumnMap(
FieldMetadataType.CURRENCY,
true,
'price',
);
expect(currencyMap).toEqual({
amountMicros: '_priceAmountMicros',
currencyCode: '_priceCurrencyCode',
});
});
it('should throw an error for an unknown type', () => {
expect(() =>
generateTargetColumnMap('invalid' as FieldMetadataType, false, 'name'),
).toThrow(BadRequestException);
});
});
@@ -0,0 +1,53 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { pascalCase } from 'src/utils/pascal-case';
type ComputeColumnNameOptions = { isForeignKey?: boolean };
export function computeColumnName(
fieldName: string,
options?: ComputeColumnNameOptions,
): string;
export function computeColumnName<T extends FieldMetadataType | 'default'>(
fieldMetadata: FieldMetadataInterface<T>,
ioptions?: ComputeColumnNameOptions,
): string;
// TODO: If we need to implement custom name logic for columns, we can do it here
export function computeColumnName<T extends FieldMetadataType | 'default'>(
fieldMetadataOrFieldName: FieldMetadataInterface<T> | string,
options?: ComputeColumnNameOptions,
): string {
const generateName = (name: string) => {
return options?.isForeignKey ? `${name}Id` : name;
};
if (typeof fieldMetadataOrFieldName === 'string') {
return generateName(fieldMetadataOrFieldName);
}
if (isCompositeFieldMetadataType(fieldMetadataOrFieldName.type)) {
throw new Error(
`Cannot compute column name for composite field metadata type: ${fieldMetadataOrFieldName.type}`,
);
}
return generateName(fieldMetadataOrFieldName.name);
}
export const computeCompositeColumnName = <
T extends FieldMetadataType | 'default',
>(
fieldMetadata: FieldMetadataInterface<T>,
compositeProperty: CompositeProperty,
): string => {
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
throw new Error(
`Cannot compute composite column name for non-composite field metadata type: ${fieldMetadata.type}`,
);
}
return `${fieldMetadata.name}${pascalCase(compositeProperty.name)}`;
};
@@ -1,73 +0,0 @@
import { BadRequestException } from '@nestjs/common';
import { FieldMetadataTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { createCustomColumnName } from 'src/engine/utils/create-custom-column-name.util';
/**
* Generate a target column map for a given type, this is used to map the field to the correct column(s) in the database.
* This is used to support fields that map to multiple columns in the database.
*
* @param type string
* @returns FieldMetadataTargetColumnMap
*/
export function generateTargetColumnMap(
type: FieldMetadataType,
isCustomField: boolean,
fieldName: string,
): FieldMetadataTargetColumnMap {
const columnName = isCustomField
? createCustomColumnName(fieldName)
: fieldName;
switch (type) {
case FieldMetadataType.UUID:
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:
case FieldMetadataType.PROBABILITY:
case FieldMetadataType.BOOLEAN:
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.RATING:
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
case FieldMetadataType.POSITION:
case FieldMetadataType.RAW_JSON:
return {
value: columnName,
};
case FieldMetadataType.LINK:
return {
label: `${columnName}Label`,
url: `${columnName}Url`,
};
case FieldMetadataType.CURRENCY:
return {
amountMicros: `${columnName}AmountMicros`,
currencyCode: `${columnName}CurrencyCode`,
};
case FieldMetadataType.FULL_NAME:
return {
firstName: `${columnName}FirstName`,
lastName: `${columnName}LastName`,
};
case FieldMetadataType.ADDRESS:
return {
addressStreet1: `${columnName}AddressStreet1`,
addressStreet2: `${columnName}AddressStreet2`,
addressCity: `${columnName}AddressCity`,
addressPostcode: `${columnName}AddressPostcode`,
addressState: `${columnName}AddressState`,
addressCountry: `${columnName}AddressCountry`,
addressLat: `${columnName}AddressLat`,
addressLng: `${columnName}AddressLng`,
};
case FieldMetadataType.RELATION:
return {};
default:
throw new BadRequestException(`Unknown type ${type}`);
}
}
@@ -28,7 +28,7 @@ import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete';
@@ -47,6 +47,7 @@ import {
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
import { buildWorkspaceMigrationsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-custom-object.util';
import { buildWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from './object-metadata.entity';
@@ -172,7 +173,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId,
[
{
name: computeCustomName(
name: computeTableName(
relationToDelete.toObjectName,
relationToDelete.toObjectMetadataIsCustom,
),
@@ -180,9 +181,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: computeCustomName(
`${relationToDelete.toFieldMetadataName}Id`,
relationToDelete.toFieldMetadataIsCustom,
columnName: computeColumnName(
relationToDelete.toFieldMetadataName,
{ isForeignKey: true },
),
} satisfies WorkspaceMigrationColumnDrop,
],
@@ -252,9 +253,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID,
name: 'id',
label: 'Id',
targetColumnMap: {
value: 'id',
},
icon: 'Icon123',
description: 'Id',
isNullable: false,
@@ -269,9 +267,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
icon: 'IconAbc',
description: 'Name',
isNullable: false,
@@ -285,9 +280,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.DATE_TIME,
name: 'createdAt',
label: 'Creation date',
targetColumnMap: {
value: 'createdAt',
},
icon: 'IconCalendar',
description: 'Creation date',
isNullable: false,
@@ -301,9 +293,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.DATE_TIME,
name: 'updatedAt',
label: 'Update date',
targetColumnMap: {
value: 'updatedAt',
},
icon: 'IconCalendar',
description: 'Update date',
isNullable: false,
@@ -318,9 +307,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.POSITION,
name: 'position',
label: 'Position',
targetColumnMap: {
value: 'position',
},
icon: 'IconHierarchy2',
description: 'Position',
isNullable: true,
@@ -518,7 +504,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION,
name: 'activityTargets',
label: 'Activities',
targetColumnMap: {},
description: `Activities tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconCheckbox',
isNullable: true,
@@ -536,7 +521,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
targetColumnMap: {},
description: `ActivityTarget ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
@@ -554,12 +538,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: {
value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
},
description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
@@ -621,7 +599,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
targetColumnMap: {},
description: `Attachments tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconFileImport',
isNullable: true,
@@ -639,7 +616,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
targetColumnMap: {},
description: `Attachment ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
@@ -657,12 +633,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: {
value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
},
description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
@@ -721,7 +691,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION,
name: 'events',
label: 'Events',
targetColumnMap: {},
description: `Events tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconFileImport',
isNullable: true,
@@ -739,7 +708,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
targetColumnMap: {},
description: `Event ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
@@ -757,12 +725,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: {
value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
},
description: `Event ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
@@ -822,7 +784,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
targetColumnMap: {},
description: `Favorites tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconHeart',
isNullable: true,
@@ -840,7 +801,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
targetColumnMap: {},
description: `Favorite ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
isNullable: true,
@@ -858,12 +818,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: {
value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
},
description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
@@ -1,3 +1,4 @@
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import {
@@ -5,7 +6,6 @@ import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const buildWorkspaceMigrationsForCustomObject = (
@@ -26,10 +26,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: 'uuid',
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
@@ -41,10 +40,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
referencedTableName: computeObjectTargetTable(createdObjectMetadata),
referencedTableColumnName: 'id',
onDelete: RelationOnDeleteAction.CASCADE,
@@ -58,10 +56,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: 'uuid',
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
@@ -73,10 +70,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
referencedTableName: computeObjectTargetTable(createdObjectMetadata),
referencedTableColumnName: 'id',
onDelete: RelationOnDeleteAction.CASCADE,
@@ -90,10 +86,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: 'uuid',
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
@@ -105,10 +100,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
referencedTableName: computeObjectTargetTable(createdObjectMetadata),
referencedTableColumnName: 'id',
onDelete: RelationOnDeleteAction.CASCADE,
@@ -122,10 +116,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: 'uuid',
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
@@ -137,10 +130,9 @@ export const buildWorkspaceMigrationsForCustomObject = (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
referencedTableName: computeObjectTargetTable(createdObjectMetadata),
referencedTableColumnName: 'id',
onDelete: RelationOnDeleteAction.CASCADE,
@@ -1,12 +1,12 @@
import { DataSource } from 'typeorm';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
WorkspaceMigrationTableAction,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
const buildCommentForRemoteObjectForeignKey = async (
@@ -65,10 +65,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
@@ -80,10 +79,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
},
],
@@ -110,10 +108,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
@@ -125,10 +122,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
},
],
@@ -155,10 +151,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
@@ -170,10 +165,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
},
],
@@ -200,10 +194,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
@@ -215,10 +208,9 @@ export const buildWorkspaceMigrationsForRemoteObject = async (
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
},
],
@@ -19,9 +19,7 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceMigrationColumnActionType } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { createCustomColumnName } from 'src/engine/utils/create-custom-column-name.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { createRelationForeignKeyColumnName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-column-name.util';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
@@ -57,11 +55,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
// NOTE: this logic is called to create relation through metadata graphql endpoint (so only for custom field relations)
const isCustom = true;
const baseColumnName = `${camelCase(relationMetadataInput.toName)}Id`;
const foreignKeyColumnName = createRelationForeignKeyColumnName(
relationMetadataInput.toName,
isCustom,
);
const columnName = `${camelCase(relationMetadataInput.toName)}Id`;
const fromId = uuidV4();
const toId = uuidV4();
@@ -79,11 +73,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
isCustom,
toId,
),
this.createForeignKeyFieldMetadata(
relationMetadataInput,
baseColumnName,
foreignKeyColumnName,
),
this.createForeignKeyFieldMetadata(relationMetadataInput, columnName),
]);
const createdRelationMetadata = await super.createOne({
@@ -95,7 +85,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
await this.createWorkspaceCustomMigration(
relationMetadataInput,
objectMetadataMap,
foreignKeyColumnName,
columnName,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
@@ -170,7 +160,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
private async createWorkspaceCustomMigration(
relationMetadataInput: CreateRelationInput,
objectMetadataMap: { [key: string]: ObjectMetadataEntity },
foreignKeyColumnName: string,
columnName: string,
) {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-${relationMetadataInput.fromName}`),
@@ -185,7 +175,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: foreignKeyColumnName,
columnName,
columnType: 'uuid',
isNullable: true,
},
@@ -200,7 +190,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: foreignKeyColumnName,
columnName,
referencedTableName: computeObjectTargetTable(
objectMetadataMap[relationMetadataInput.fromObjectMetadataId],
),
@@ -229,12 +219,6 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
description: relationMetadataInput[`${relationDirection}Description`],
icon: relationMetadataInput[`${relationDirection}Icon`],
isCustom: true,
targetColumnMap:
relationDirection === 'to'
? isCustom
? createCustomColumnName(relationMetadataInput.toName)
: relationMetadataInput.toName
: {},
isActive: true,
isNullable: true,
type: FieldMetadataType.RELATION,
@@ -246,18 +230,16 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
private createForeignKeyFieldMetadata(
relationMetadataInput: CreateRelationInput,
baseColumnName: string,
foreignKeyColumnName: string,
columnName: string,
) {
return {
name: baseColumnName,
name: columnName,
label: `${relationMetadataInput.toLabel} Foreign Key`,
description: relationMetadataInput.toDescription
? `${relationMetadataInput.toDescription} Foreign Key`
: undefined,
icon: undefined,
isCustom: true,
targetColumnMap: { value: foreignKeyColumnName },
isActive: true,
isNullable: true,
isSystem: true,
@@ -1,15 +0,0 @@
import { createCustomColumnName } from 'src/engine/utils/create-custom-column-name.util';
import { camelCase } from 'src/utils/camel-case';
export const createRelationForeignKeyColumnName = (
name: string,
isCustom: boolean,
) => {
const baseColumnName = `${camelCase(name)}Id`;
const foreignKeyColumnName = isCustom
? createCustomColumnName(baseColumnName)
: baseColumnName;
return foreignKeyColumnName;
};
@@ -12,6 +12,7 @@ import {
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
export type BasicFieldMetadataType =
| FieldMetadataType.UUID
@@ -33,29 +34,32 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate {
): WorkspaceMigrationColumnCreate[] {
const columnName = computeColumnName(fieldMetadata);
const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue);
return {
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
isNullable: fieldMetadata.isNullable,
defaultValue: serializedDefaultValue,
};
return [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName,
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
isNullable: fieldMetadata.isNullable,
defaultValue: serializedDefaultValue,
},
];
}
protected handleAlterAction(
currentFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
alteredFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter {
): WorkspaceMigrationColumnAlter[] {
const currentColumnName = computeColumnName(currentFieldMetadata);
const alteredColumnName = computeColumnName(alteredFieldMetadata);
const defaultValue =
alteredFieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue);
const currentColumnName = currentFieldMetadata.targetColumnMap.value;
const alteredColumnName = alteredFieldMetadata.targetColumnMap.value;
if (!currentColumnName || !alteredColumnName) {
this.logger.error(
@@ -66,20 +70,24 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
);
}
return {
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName: currentColumnName,
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
isNullable: currentFieldMetadata.isNullable,
defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue),
return [
{
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName: currentColumnName,
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
isNullable: currentFieldMetadata.isNullable,
defaultValue: serializeDefaultValue(
currentFieldMetadata.defaultValue,
),
},
alteredColumnDefinition: {
columnName: alteredColumnName,
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
isNullable: alteredFieldMetadata.isNullable,
defaultValue: serializedDefaultValue,
},
},
alteredColumnDefinition: {
columnName: alteredColumnName,
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
isNullable: alteredFieldMetadata.isNullable,
defaultValue: serializedDefaultValue,
},
};
];
}
}
@@ -26,7 +26,7 @@ export class ColumnActionAbstractFactory<
currentFieldMetadata: FieldMetadataInterface<T> | undefined,
alteredFieldMetadata: FieldMetadataInterface<T>,
options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAction {
): WorkspaceMigrationColumnAction[] {
switch (action) {
case WorkspaceMigrationColumnActionType.CREATE:
return this.handleCreateAction(alteredFieldMetadata, options);
@@ -52,7 +52,7 @@ export class ColumnActionAbstractFactory<
protected handleCreateAction(
_fieldMetadata: FieldMetadataInterface<T>,
_options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate {
): WorkspaceMigrationColumnCreate[] {
throw new Error('handleCreateAction method not implemented.');
}
@@ -60,7 +60,7 @@ export class ColumnActionAbstractFactory<
_currentFieldMetadata: FieldMetadataInterface<T>,
_alteredFieldMetadata: FieldMetadataInterface<T>,
_options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter {
): WorkspaceMigrationColumnAlter[] {
throw new Error('handleAlterAction method not implemented.');
}
}
@@ -0,0 +1,132 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnAlter,
WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
export type CompositeFieldMetadataType =
| FieldMetadataType.ADDRESS
| FieldMetadataType.CURRENCY
| FieldMetadataType.FULL_NAME
| FieldMetadataType.LINK;
@Injectable()
export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<CompositeFieldMetadataType> {
protected readonly logger = new Logger(CompositeColumnActionFactory.name);
protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<CompositeFieldMetadataType>,
): WorkspaceMigrationColumnCreate[] {
const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
if (!compositeType) {
this.logger.error(
`Composite type not found for field metadata type: ${fieldMetadata.type}`,
);
throw new Error(
`Composite type not found for field metadata type: ${fieldMetadata.type}`,
);
}
const columnActions: WorkspaceMigrationColumnCreate[] = [];
for (const property of compositeType.properties) {
const columnName = computeCompositeColumnName(fieldMetadata, property);
const defaultValue = fieldMetadata.defaultValue?.[property.name];
const serializedDefaultValue = serializeDefaultValue(defaultValue);
columnActions.push({
action: WorkspaceMigrationColumnActionType.CREATE,
columnName,
columnType: fieldMetadataTypeToColumnType(property.type),
isNullable: fieldMetadata.isNullable || !property.isRequired,
defaultValue: serializedDefaultValue,
});
}
return columnActions;
}
protected handleAlterAction(
currentFieldMetadata: FieldMetadataInterface<CompositeFieldMetadataType>,
alteredFieldMetadata: FieldMetadataInterface<CompositeFieldMetadataType>,
): WorkspaceMigrationColumnAlter[] {
const currentCompositeType = compositeTypeDefintions.get(
currentFieldMetadata.type,
);
const alteredCompositeType = compositeTypeDefintions.get(
alteredFieldMetadata.type,
);
if (!currentCompositeType || !alteredCompositeType) {
this.logger.error(
`Composite type not found for field metadata type: ${currentFieldMetadata.type} or ${alteredFieldMetadata.type}`,
);
throw new Error(
`Composite type not found for field metadata type: ${currentFieldMetadata.type} or ${alteredFieldMetadata.type}`,
);
}
const columnActions: WorkspaceMigrationColumnAlter[] = [];
for (const alteredProperty of alteredCompositeType.properties) {
// TODO: Based on the name for now, we can add a more robust check in the future
const currentProperty = currentCompositeType.properties.find(
(p) => p.name === alteredProperty.name,
);
if (!currentProperty) {
this.logger.error(
`Current property not found for altered property: ${alteredProperty.name}`,
);
throw new Error(
`Current property not found for altered property: ${alteredProperty.name}`,
);
}
const currentColumnName = computeCompositeColumnName(
currentFieldMetadata,
currentProperty,
);
const alteredColumnName = computeCompositeColumnName(
alteredFieldMetadata,
alteredProperty,
);
const defaultValue =
alteredFieldMetadata.defaultValue?.[alteredProperty.name];
const serializedDefaultValue = serializeDefaultValue(defaultValue);
columnActions.push({
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName: currentColumnName,
columnType: fieldMetadataTypeToColumnType(currentProperty.type),
isNullable:
currentFieldMetadata.isNullable || !currentProperty.isRequired,
defaultValue: serializeDefaultValue(
currentFieldMetadata.defaultValue?.[currentProperty.name],
),
},
alteredColumnDefinition: {
columnName: alteredColumnName,
columnType: fieldMetadataTypeToColumnType(alteredProperty.type),
isNullable:
alteredFieldMetadata.isNullable || !alteredProperty.isRequired,
defaultValue: serializedDefaultValue,
},
});
}
return columnActions;
}
}
@@ -12,6 +12,7 @@ import {
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
export type EnumFieldMetadataType =
| FieldMetadataType.RATING
@@ -25,29 +26,34 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
protected handleCreateAction(
fieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
options: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate {
): WorkspaceMigrationColumnCreate[] {
const columnName = computeColumnName(fieldMetadata);
const defaultValue = fieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue);
const enumOptions = fieldMetadata.options
? [...fieldMetadata.options.map((option) => option.value)]
: undefined;
return {
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: fieldMetadata.targetColumnMap.value,
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
enum: enumOptions,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: fieldMetadata.isNullable,
defaultValue: serializedDefaultValue,
};
return [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName,
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
enum: enumOptions,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: fieldMetadata.isNullable,
defaultValue: serializedDefaultValue,
},
];
}
protected handleAlterAction(
currentFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
alteredFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
options: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter {
): WorkspaceMigrationColumnAlter[] {
const currentColumnName = computeColumnName(currentFieldMetadata);
const alteredColumnName = computeColumnName(alteredFieldMetadata);
const defaultValue =
alteredFieldMetadata.defaultValue ?? options?.defaultValue;
const serializedDefaultValue = serializeDefaultValue(defaultValue);
@@ -71,8 +77,6 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
}),
]
: undefined;
const currentColumnName = currentFieldMetadata.targetColumnMap.value;
const alteredColumnName = alteredFieldMetadata.targetColumnMap.value;
if (!currentColumnName || !alteredColumnName) {
this.logger.error(
@@ -83,26 +87,30 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
);
}
return {
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName: currentColumnName,
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
enum: currentFieldMetadata.options
? [...currentFieldMetadata.options.map((option) => option.value)]
: undefined,
isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: currentFieldMetadata.isNullable,
defaultValue: serializeDefaultValue(currentFieldMetadata.defaultValue),
return [
{
action: WorkspaceMigrationColumnActionType.ALTER,
currentColumnDefinition: {
columnName: currentColumnName,
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
enum: currentFieldMetadata.options
? [...currentFieldMetadata.options.map((option) => option.value)]
: undefined,
isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: currentFieldMetadata.isNullable,
defaultValue: serializeDefaultValue(
currentFieldMetadata.defaultValue,
),
},
alteredColumnDefinition: {
columnName: alteredColumnName,
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
enum: enumOptions,
isArray: alteredFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: alteredFieldMetadata.isNullable,
defaultValue: serializedDefaultValue,
},
},
alteredColumnDefinition: {
columnName: alteredColumnName,
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
enum: enumOptions,
isArray: alteredFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: alteredFieldMetadata.isNullable,
defaultValue: serializedDefaultValue,
},
};
];
}
}
@@ -1,7 +1,9 @@
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
export const workspaceColumnActionFactories = [
BasicColumnActionFactory,
EnumColumnActionFactory,
CompositeColumnActionFactory,
];
@@ -17,5 +17,5 @@ export interface WorkspaceColumnActionFactory<
currentFieldMetadata: FieldMetadataInterface<T> | undefined,
alteredFieldMetadata: FieldMetadataInterface<T>,
options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAction;
): WorkspaceMigrationColumnAction[];
}
@@ -5,14 +5,13 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
import {
WorkspaceMigrationColumnAction,
WorkspaceMigrationColumnActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { compositeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
@Injectable()
export class WorkspaceMigrationFactory {
@@ -28,6 +27,7 @@ export class WorkspaceMigrationFactory {
constructor(
private readonly basicColumnActionFactory: BasicColumnActionFactory,
private readonly enumColumnActionFactory: EnumColumnActionFactory,
private readonly compositeColumnActionFactory: CompositeColumnActionFactory,
) {
this.factoriesMap = new Map<
FieldMetadataType,
@@ -80,6 +80,19 @@ export class WorkspaceMigrationFactory {
FieldMetadataType.MULTI_SELECT,
{ factory: this.enumColumnActionFactory },
],
[FieldMetadataType.LINK, { factory: this.compositeColumnActionFactory }],
[
FieldMetadataType.CURRENCY,
{ factory: this.compositeColumnActionFactory },
],
[
FieldMetadataType.ADDRESS,
{ factory: this.compositeColumnActionFactory },
],
[
FieldMetadataType.FULL_NAME,
{ factory: this.compositeColumnActionFactory },
],
]);
}
@@ -119,41 +132,13 @@ export class WorkspaceMigrationFactory {
throw new Error(`No field metadata provided for action ${action}`);
}
// If it's a composite field type, we need to create a column action for each of the fields
if (isCompositeFieldMetadataType(alteredFieldMetadata.type)) {
const fieldMetadataSplitterFunction = compositeDefinitions.get(
alteredFieldMetadata.type,
);
if (!fieldMetadataSplitterFunction) {
this.logger.error(
`No composite definition found for type ${alteredFieldMetadata.type}`,
{
alteredFieldMetadata,
},
);
throw new Error(
`No composite definition found for type ${alteredFieldMetadata.type}`,
);
}
const fieldMetadataCollection =
fieldMetadataSplitterFunction(alteredFieldMetadata);
return fieldMetadataCollection.map((fieldMetadata) =>
this.createColumnAction(action, fieldMetadata, fieldMetadata),
);
}
// Otherwise, we create a single column action
const columnAction = this.createColumnAction(
const columnActions = this.createColumnAction(
action,
currentFieldMetadata,
alteredFieldMetadata,
);
return [columnAction];
return columnActions;
}
private createColumnAction(
@@ -162,7 +147,7 @@ export class WorkspaceMigrationFactory {
| WorkspaceMigrationColumnActionType.ALTER,
currentFieldMetadata: FieldMetadataInterface | undefined,
alteredFieldMetadata: FieldMetadataInterface,
): WorkspaceMigrationColumnAction {
): WorkspaceMigrationColumnAction[] {
const { factory, options } =
this.factoriesMap.get(alteredFieldMetadata.type) ?? {};