https://github.com/user-attachments/assets/4be785e0-ea8a-4c8e-840e-6fa0a663d7ba Closes #11938 --------- Co-authored-by: martmull <martmull@hotmail.fr>
255 lines
7.9 KiB
TypeScript
255 lines
7.9 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
|
|
import { FieldMetadataType } from 'twenty-shared/types';
|
|
import { capitalize, isDefined } from 'twenty-shared/utils';
|
|
import { DataSource, EntityManager } from 'typeorm';
|
|
|
|
import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed';
|
|
|
|
import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members';
|
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
|
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
|
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
|
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
|
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
|
|
@Injectable()
|
|
export class SeederService {
|
|
constructor(
|
|
private readonly objectMetadataService: ObjectMetadataService,
|
|
private readonly fieldMetadataService: FieldMetadataService,
|
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
) {}
|
|
|
|
public async seedCustomObjectRecords(
|
|
workspaceId: string,
|
|
objectMetadataSeed: ObjectMetadataSeed,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
objectRecordSeeds: Record<string, any>[],
|
|
) {
|
|
const { fieldMetadataSeeds, objectMetadata } = await this.getSeedMetadata(
|
|
workspaceId,
|
|
objectMetadataSeed,
|
|
);
|
|
|
|
const schemaName =
|
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
|
|
|
const mainDataSource: DataSource =
|
|
await this.workspaceDataSourceService.connectToMainDataSource();
|
|
|
|
const entityManager: EntityManager = mainDataSource.createEntityManager();
|
|
|
|
const objectRecordSeedsAsSQLFlattenedSeeds = objectRecordSeeds.map(
|
|
(recordSeed) => {
|
|
const objectRecordSeedsAsSQLFlattenedSeeds = {};
|
|
|
|
for (const field of fieldMetadataSeeds) {
|
|
if (isCompositeFieldMetadataType(field.type)) {
|
|
const compositeFieldTypeDefinition = compositeTypeDefinitions.get(
|
|
field.type,
|
|
);
|
|
|
|
if (!isDefined(compositeFieldTypeDefinition)) {
|
|
throw new Error(
|
|
`Composite field type definition not found for ${field.type}`,
|
|
);
|
|
}
|
|
|
|
const fieldNames = compositeFieldTypeDefinition.properties
|
|
?.map((property) => property.name)
|
|
.filter(isDefined);
|
|
|
|
for (const subFieldName of fieldNames) {
|
|
const subFieldValue = recordSeed?.[field.name]?.[subFieldName];
|
|
|
|
const subFieldValueAsSQLValue =
|
|
this.turnCompositeSubFieldValueAsSQLValue(
|
|
field.type,
|
|
subFieldName,
|
|
subFieldValue,
|
|
);
|
|
|
|
const subFieldNameAsSQLColumnName = `${field.name}${capitalize(subFieldName)}`;
|
|
|
|
// @ts-expect-error legacy noImplicitAny
|
|
objectRecordSeedsAsSQLFlattenedSeeds[
|
|
subFieldNameAsSQLColumnName
|
|
] = subFieldValueAsSQLValue;
|
|
}
|
|
} else {
|
|
const fieldValue = recordSeed[field.name];
|
|
|
|
const fieldValueAsSQLValue = this.turnFieldValueAsSQLValue(
|
|
field.type,
|
|
fieldValue,
|
|
);
|
|
|
|
// @ts-expect-error legacy noImplicitAny
|
|
objectRecordSeedsAsSQLFlattenedSeeds[field.name] =
|
|
fieldValueAsSQLValue;
|
|
}
|
|
}
|
|
|
|
return objectRecordSeedsAsSQLFlattenedSeeds;
|
|
},
|
|
);
|
|
|
|
if (!(objectRecordSeedsAsSQLFlattenedSeeds.length > 0)) {
|
|
return;
|
|
}
|
|
|
|
const fieldMetadataNamesAsFlattenedSQLColumnNames = Object.keys(
|
|
objectRecordSeedsAsSQLFlattenedSeeds[0],
|
|
);
|
|
|
|
const sqlColumnNames = [
|
|
...fieldMetadataNamesAsFlattenedSQLColumnNames,
|
|
'position',
|
|
'createdBySource',
|
|
'createdByWorkspaceMemberId',
|
|
'createdByName',
|
|
];
|
|
|
|
const sqlValues = objectRecordSeedsAsSQLFlattenedSeeds.map(
|
|
(flattenedSeed, index) => ({
|
|
...flattenedSeed,
|
|
position: index,
|
|
createdBySource: 'MANUAL',
|
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
|
createdByName: 'Tim Apple',
|
|
}),
|
|
);
|
|
|
|
await entityManager
|
|
.createQueryBuilder()
|
|
.insert()
|
|
.into(
|
|
`${schemaName}.${computeTableName(objectMetadata.nameSingular, true)}`,
|
|
sqlColumnNames,
|
|
)
|
|
.orIgnore()
|
|
.values(sqlValues)
|
|
.returning('*')
|
|
.execute();
|
|
}
|
|
|
|
public async seedCustomObjects(
|
|
dataSourceId: string,
|
|
workspaceId: string,
|
|
objectMetadataSeed: ObjectMetadataSeed,
|
|
): Promise<void> {
|
|
const createdObjectMetadata = await this.objectMetadataService.createOne({
|
|
...objectMetadataSeed,
|
|
dataSourceId,
|
|
workspaceId,
|
|
});
|
|
|
|
if (!createdObjectMetadata) {
|
|
throw new Error("Object metadata couldn't be created");
|
|
}
|
|
|
|
await this.fieldMetadataService.createMany(
|
|
objectMetadataSeed.fields.map((fieldMetadataSeed) => ({
|
|
...fieldMetadataSeed,
|
|
objectMetadataId: createdObjectMetadata.id,
|
|
workspaceId,
|
|
})),
|
|
);
|
|
|
|
const { fieldMetadataSeeds } = await this.getSeedMetadata(
|
|
workspaceId,
|
|
objectMetadataSeed,
|
|
);
|
|
|
|
this.addNameFieldToFieldMetadataSeeds(fieldMetadataSeeds);
|
|
}
|
|
|
|
private addNameFieldToFieldMetadataSeeds(
|
|
arrayOfMetadataFields: Pick<CreateFieldInput, 'name' | 'type' | 'label'>[],
|
|
) {
|
|
arrayOfMetadataFields.unshift({
|
|
name: 'name',
|
|
type: FieldMetadataType.TEXT,
|
|
label: 'Name',
|
|
});
|
|
}
|
|
|
|
private async getSeedMetadata(
|
|
workspaceId: string,
|
|
objectMetadataSeed: ObjectMetadataSeed,
|
|
) {
|
|
const objectMetadata =
|
|
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
|
where: { nameSingular: objectMetadataSeed.nameSingular },
|
|
});
|
|
|
|
if (!objectMetadata) {
|
|
throw new Error(
|
|
"Object metadata couldn't be found after field creation.",
|
|
);
|
|
}
|
|
|
|
const fieldMetadataSeeds = objectMetadataSeed.fields.filter((field) =>
|
|
objectMetadata.fields.some(
|
|
(f) => f.name === field.name || f.name === `name`,
|
|
),
|
|
);
|
|
|
|
if (fieldMetadataSeeds.length === 0) {
|
|
throw new Error('No fields found for seeding, check metadata file');
|
|
}
|
|
|
|
return { fieldMetadataSeeds, objectMetadata };
|
|
}
|
|
|
|
private turnCompositeSubFieldValueAsSQLValue(
|
|
fieldType: FieldMetadataType,
|
|
subFieldName: string,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
subFieldValue: any,
|
|
) {
|
|
if (!isCompositeFieldMetadataType(fieldType)) {
|
|
throw new Error(
|
|
`${subFieldName} is not a sub field of a composite field type.`,
|
|
);
|
|
}
|
|
|
|
const compositeFieldTypeDefinition =
|
|
compositeTypeDefinitions.get(fieldType);
|
|
|
|
const compositeSubFieldType =
|
|
compositeFieldTypeDefinition?.properties.find(
|
|
(property) => property.name === subFieldName,
|
|
)?.type ?? null;
|
|
|
|
if (!isDefined(compositeSubFieldType)) {
|
|
throw new Error(
|
|
`Cannot find ${subFieldName} in properties of composite type ${fieldType}.`,
|
|
);
|
|
}
|
|
|
|
return this.turnFieldValueAsSQLValue(compositeSubFieldType, subFieldValue);
|
|
}
|
|
|
|
private turnFieldValueAsSQLValue(
|
|
fieldType: FieldMetadataType,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
fieldValue: any,
|
|
) {
|
|
if (fieldType === FieldMetadataType.RAW_JSON) {
|
|
try {
|
|
return JSON.stringify(fieldValue);
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Error while trying to turn field value as stringified JSON : ${error.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return fieldValue;
|
|
}
|
|
}
|