Refactored and improved seeds (#8695)
- Added a new Seeder service to help with custom object seeds - Added RichTextFieldInput to edit a rich text field directly on the table, but deactivated it for now.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,7 @@
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
|
||||
export type ObjectMetadataSeed = Omit<
|
||||
CreateObjectInput,
|
||||
'workspaceId' | 'dataSourceId'
|
||||
> & { fields: Omit<CreateFieldInput, 'objectMetadataId' | 'workspaceId'>[] };
|
||||
@ -0,0 +1,41 @@
|
||||
import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
export const PETS_METADATA_SEEDS: ObjectMetadataSeed = {
|
||||
labelPlural: 'Pets',
|
||||
labelSingular: 'Pet',
|
||||
namePlural: 'pets',
|
||||
nameSingular: 'pet',
|
||||
icon: 'IconCat',
|
||||
fields: [
|
||||
{
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Species',
|
||||
name: 'species',
|
||||
options: [
|
||||
{ label: 'Dog', value: 'dog', position: 0, color: 'blue' },
|
||||
{ label: 'Cat', value: 'cat', position: 1, color: 'red' },
|
||||
{ label: 'Bird', value: 'bird', position: 2, color: 'green' },
|
||||
{ label: 'Fish', value: 'fish', position: 3, color: 'yellow' },
|
||||
{ label: 'Rabbit', value: 'rabbit', position: 4, color: 'purple' },
|
||||
{ label: 'Hamster', value: 'hamster', position: 5, color: 'orange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Comments',
|
||||
name: 'comments',
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Age',
|
||||
name: 'age',
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
label: 'Location',
|
||||
name: 'location',
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -0,0 +1,72 @@
|
||||
import {
|
||||
FieldMetadataNumberSettings,
|
||||
FieldMetadataTextSettings,
|
||||
NumberDataType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
export const SURVEY_RESULTS_METADATA_SEEDS: ObjectMetadataSeed = {
|
||||
labelPlural: 'Survey results',
|
||||
labelSingular: 'Survey result',
|
||||
namePlural: 'surveyResults',
|
||||
nameSingular: 'surveyResult',
|
||||
icon: 'IconRulerMeasure',
|
||||
fields: [
|
||||
{
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Score (Float 3 decimals)',
|
||||
name: 'score',
|
||||
settings: {
|
||||
dataType: NumberDataType.FLOAT,
|
||||
decimals: 3,
|
||||
type: 'number',
|
||||
} as FieldMetadataNumberSettings,
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Percentage of completion (Float 3 decimals + percentage)',
|
||||
name: 'percentageOfCompletion',
|
||||
settings: {
|
||||
dataType: NumberDataType.FLOAT,
|
||||
decimals: 6,
|
||||
type: 'percentage',
|
||||
} as FieldMetadataNumberSettings,
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Participants (Int)',
|
||||
name: 'participants',
|
||||
settings: {
|
||||
dataType: NumberDataType.INT,
|
||||
type: 'number',
|
||||
} as FieldMetadataNumberSettings,
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Average estimated number of atoms in the universe (BigInt)',
|
||||
name: 'averageEstimatedNumberOfAtomsInTheUniverse',
|
||||
settings: {
|
||||
dataType: NumberDataType.BIGINT,
|
||||
type: 'number',
|
||||
} as FieldMetadataNumberSettings,
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Comments (Max 5 rows)',
|
||||
name: 'comments',
|
||||
settings: {
|
||||
displayedMaxRows: 5,
|
||||
} as FieldMetadataTextSettings,
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Short notes (Max 1 row)',
|
||||
name: 'shortNotes',
|
||||
settings: {
|
||||
displayedMaxRows: 1,
|
||||
} as FieldMetadataTextSettings,
|
||||
},
|
||||
],
|
||||
};
|
||||
17
packages/twenty-server/src/engine/seeder/seeder.module.ts
Normal file
17
packages/twenty-server/src/engine/seeder/seeder.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { SeederService } from 'src/engine/seeder/seeder.service';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ObjectMetadataModule,
|
||||
FieldMetadataModule,
|
||||
WorkspaceDataSourceModule,
|
||||
],
|
||||
exports: [SeederService],
|
||||
providers: [SeederService],
|
||||
})
|
||||
export class SeederModule {}
|
||||
161
packages/twenty-server/src/engine/seeder/seeder.service.ts
Normal file
161
packages/twenty-server/src/engine/seeder/seeder.service.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
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 { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
@Injectable()
|
||||
export class SeederService {
|
||||
constructor(
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async seedCustomObjects(
|
||||
dataSourceId: string,
|
||||
workspaceId: string,
|
||||
metadataSeeds: ObjectMetadataSeed,
|
||||
dataSeeds: Record<string, any>[],
|
||||
): Promise<void> {
|
||||
const createdObjectMetadata = await this.objectMetadataService.createOne({
|
||||
...metadataSeeds,
|
||||
dataSourceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!createdObjectMetadata) {
|
||||
throw new Error("Object metadata couldn't be created");
|
||||
}
|
||||
|
||||
for (const customField of metadataSeeds.fields) {
|
||||
await this.fieldMetadataService.createOne({
|
||||
...customField,
|
||||
objectMetadataId: createdObjectMetadata.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
const objectMetadataAfterFieldCreation =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
where: { nameSingular: metadataSeeds.nameSingular },
|
||||
});
|
||||
|
||||
if (!objectMetadataAfterFieldCreation) {
|
||||
throw new Error(
|
||||
"Object metadata couldn't be found after field creation.",
|
||||
);
|
||||
}
|
||||
|
||||
const schemaName =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const entityManager = workspaceDataSource.createEntityManager();
|
||||
|
||||
const filteredFields = metadataSeeds.fields.filter((field) =>
|
||||
objectMetadataAfterFieldCreation.fields.some(
|
||||
(f) => f.name === field.name || f.name === `name`,
|
||||
),
|
||||
);
|
||||
|
||||
if (filteredFields.length === 0) {
|
||||
throw new Error('No fields found for seeding, check metadata');
|
||||
}
|
||||
|
||||
filteredFields.unshift({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
});
|
||||
|
||||
const fieldMetadataMap = filteredFields
|
||||
.map((field) => {
|
||||
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,
|
||||
);
|
||||
|
||||
return (
|
||||
fieldNames?.map(
|
||||
(subFieldName: string) =>
|
||||
`${field.name}${capitalize(subFieldName)}`,
|
||||
) ?? []
|
||||
);
|
||||
} else {
|
||||
return field.name;
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
.filter(isDefined);
|
||||
|
||||
const flattenedSeeds = dataSeeds.map((seed) => {
|
||||
const flattenedSeed = {};
|
||||
|
||||
for (const field of filteredFields) {
|
||||
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) {
|
||||
flattenedSeed[`${field.name}${capitalize(subFieldName)}`] =
|
||||
seed?.[field.name]?.[subFieldName];
|
||||
}
|
||||
} else {
|
||||
flattenedSeed[field.name] = seed[field.name];
|
||||
}
|
||||
}
|
||||
|
||||
return flattenedSeed;
|
||||
});
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}._${objectMetadataAfterFieldCreation.nameSingular}`, [
|
||||
...fieldMetadataMap,
|
||||
'position',
|
||||
])
|
||||
.orIgnore()
|
||||
.values(
|
||||
flattenedSeeds.map((flattenedSeed, index) => ({
|
||||
...flattenedSeed,
|
||||
position: index,
|
||||
})),
|
||||
)
|
||||
.returning('*')
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user