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
parent 84f8c14e52
commit 5019b5febc
72 changed files with 1432 additions and 1853 deletions

View File

@ -1,9 +1,15 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
@Injectable()
export class ArgsAliasFactory {
private readonly logger = new Logger(ArgsAliasFactory.name);
create(
args: Record<string, any>,
fieldMetadataCollection: FieldMetadataInterface[],
@ -39,25 +45,42 @@ export class ArgsAliasFactory {
for (const [key, value] of Object.entries(args)) {
const fieldMetadata = fieldMetadataMap.get(key);
// If it's a special complex field, we need to map all columns
// If it's a composite type, we need to transform args to properly map column name
if (
fieldMetadata &&
typeof value === 'object' &&
value !== null &&
Object.values(fieldMetadata.targetColumnMap).length > 1
isCompositeFieldMetadataType(fieldMetadata.type)
) {
for (const [subKey, subValue] of Object.entries(value)) {
const mappedKey = fieldMetadata.targetColumnMap[subKey];
// Get composite type definition
const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
if (mappedKey) {
newArgs[mappedKey] = subValue;
if (!compositeType) {
this.logger.error(
`Composite type definition not found for type: ${fieldMetadata.type}`,
);
throw new Error(
`Composite type definition not found for type: ${fieldMetadata.type}`,
);
}
// Loop through sub values and map them to composite property
for (const [subKey, subValue] of Object.entries(value)) {
// Find composite property
const compositeProperty = compositeType.properties.find(
(property) => property.name === subKey,
);
if (compositeProperty) {
const columnName = computeCompositeColumnName(
fieldMetadata,
compositeProperty,
);
newArgs[columnName] = subValue;
}
}
} else if (fieldMetadata) {
// Otherwise we just need to map the value
const mappedKey = fieldMetadata.targetColumnMap.value;
newArgs[mappedKey ?? key] = value;
newArgs[key] = value;
} else {
// Recurse if value is a nested object, otherwise append field or alias
newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap);

View File

@ -2,29 +2,49 @@ import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { createCompositeFieldKey } from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util';
import {
computeColumnName,
computeCompositeColumnName,
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
@Injectable()
export class FieldAliasFactory {
private readonly logger = new Logger(FieldAliasFactory.name);
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
const entries = Object.entries(fieldMetadata.targetColumnMap);
if (entries.length === 0) {
return null;
}
if (entries.length === 1) {
// If there is only one value, use it as the alias
const alias = entries[0][1];
// If it's not a composite field, we can just return the alias
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
const alias = computeColumnName(fieldMetadata);
return `${fieldKey}: ${alias}`;
}
// Otherwise it means it's a special type with multiple values, so we need map all columns
return `
${entries
.map(([key, value]) => `___${fieldMetadata.name}_${key}: ${value}`)
.join('\n')}
`;
// If it's a composite field, we need to get the definition
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}`,
);
}
return compositeType.properties
.map((property) => {
// Generate a prefixed key for the composite field, this will be computed when the query has ran
const compositeKey = createCompositeFieldKey(
fieldMetadata.name,
property.name,
);
const alias = computeCompositeColumnName(fieldMetadata, property);
return `${compositeKey}: ${alias}`;
})
.join('\n');
}
}

View File

@ -14,6 +14,7 @@ import {
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { FieldsStringFactory } from './fields-string.factory';
import { ArgsStringFactory } from './args-string.factory';
@ -118,9 +119,7 @@ export class RelationFieldAliasFactory {
`;
}
let relationAlias = fieldMetadata.isCustom
? `${fieldKey}: _${fieldMetadata.name}`
: fieldKey;
let relationAlias = `${fieldKey}: ${computeColumnName(fieldMetadata)}`;
// For one to one relations, pg_graphql use the target TableName on the side that is not storing the foreign key
// so we need to alias it to the field key

View File

@ -0,0 +1,38 @@
/**
* Composite key are structured as follows:
* COMPOSITE___{parentFieldName}_{childFieldName}
* This util are here to pre-process and post-process the composite keys before and after querying the database
*/
export const compositeFieldPrefix = 'COMPOSITE___';
export const createCompositeFieldKey = (
fieldName: string,
propertyName: string,
): string => {
return `${compositeFieldPrefix}${fieldName}_${propertyName}`;
};
export const isPrefixedCompositeField = (key: string): boolean => {
return key.startsWith(compositeFieldPrefix);
};
export const parseCompositeFieldKey = (
key: string,
): {
parentFieldName: string;
childFieldName: string;
} | null => {
const [parentFieldName, childFieldName] = key
.replace(compositeFieldPrefix, '')
.split('_');
if (!parentFieldName || !childFieldName) {
return null;
}
return {
parentFieldName,
childFieldName,
};
};