Sync metadata generate migrations (#2864)

* Sync Metadata generates migrations

* add execute migrations

* fix relations + add isActive on creation

* fix composite fields migration

* remove dependency

* use new metadata setup for seed-dev

* fix rebase

* remove unused code

* fix viewField dev seeds

* fix isSystem
This commit is contained in:
Weiko
2023-12-07 19:22:34 +01:00
committed by GitHub
parent 590912b30f
commit 5efc2f00b9
66 changed files with 2393 additions and 2943 deletions

View File

@ -4,14 +4,14 @@ import { DatabaseCommandModule } from 'src/database/commands/database-command.mo
import { AppModule } from './app.module';
import { WorkspaceManagerCommandsModule } from './workspace/workspace-manager/commands/workspace-manager-commands.module';
import { WorkspaceSyncMetadataCommandsModule } from './workspace/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
import { WorkspaceMigrationRunnerCommandsModule } from './workspace/workspace-migration-runner/commands/workspace-migration-runner-commands.module';
@Module({
imports: [
AppModule,
WorkspaceMigrationRunnerCommandsModule,
WorkspaceManagerCommandsModule,
WorkspaceSyncMetadataCommandsModule,
DatabaseCommandModule,
],
})

View File

@ -2,19 +2,18 @@ import { Command, CommandRunner } from 'nest-commander';
import { DataSource } from 'typeorm';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
import { seedCompanies } from 'src/database/typeorm-seeds/workspace/companies';
import { seedViewFields } from 'src/database/typeorm-seeds/workspace/view-fields';
import { seedViews } from 'src/database/typeorm-seeds/workspace/views';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { seedMetadataSchema } from 'src/database/typeorm-seeds/metadata';
import { seedOpportunity } from 'src/database/typeorm-seeds/workspace/opportunity';
import { seedPipelineStep } from 'src/database/typeorm-seeds/workspace/pipeline-step';
import { seedWorkspaceMember } from 'src/database/typeorm-seeds/workspace/workspaceMember';
import { seedPeople } from 'src/database/typeorm-seeds/workspace/people';
import { seedCoreSchema } from 'src/database/typeorm-seeds/core';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
// TODO: implement dry-run
@Command({
@ -29,8 +28,9 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
private readonly environmentService: EnvironmentService,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly objectMetadataService: ObjectMetadataService,
) {
super();
}
@ -41,13 +41,30 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
url: this.environmentService.getPGDatabaseUrl(),
type: 'postgres',
logging: true,
schema: 'public',
schema: 'core',
});
await dataSource.initialize();
await seedCoreSchema(dataSource, this.workspaceId);
await seedMetadataSchema(dataSource);
await dataSource.destroy();
const schemaName = await this.workspaceDataSourceService.createWorkspaceDBSchema(
this.workspaceId,
);
const dataSourceMetadata =
await this.dataSourceService.createDataSourceMetadata(
this.workspaceId,
schemaName,
);
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
this.workspaceId,
);
} catch (error) {
console.error(error);
@ -68,20 +85,27 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
}
try {
await this.workspaceMigrationService.insertStandardMigrations(
this.workspaceId,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
this.workspaceId,
);
const objectMetadata = await this.objectMetadataService.findManyWithinWorkspace(this.workspaceId);
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
acc[object.nameSingular] = {
id: object.id,
fields: object.fields.reduce((acc, field) => {
acc[field.name] = field.id;
return acc;
}, {}),
};
return acc;
}, {});
await seedCompanies(workspaceDataSource, dataSourceMetadata.schema);
await seedPeople(workspaceDataSource, dataSourceMetadata.schema);
await seedPipelineStep(workspaceDataSource, dataSourceMetadata.schema);
await seedOpportunity(workspaceDataSource, dataSourceMetadata.schema);
await seedViews(workspaceDataSource, dataSourceMetadata.schema);
await seedViewFields(workspaceDataSource, dataSourceMetadata.schema);
await seedViews(workspaceDataSource, dataSourceMetadata.schema, objectMetadataMap);
await seedWorkspaceMember(workspaceDataSource, dataSourceMetadata.schema);
} catch (error) {
console.error(error);

View File

@ -9,15 +9,19 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-demo-workspace.command';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/worksapce-sync-metadata.module';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
@Module({
imports: [
WorkspaceManagerModule,
DataSourceModule,
TypeORMModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
WorkspaceModule,
WorkspaceDataSourceModule,
WorkspaceSyncMetadataModule,
ObjectMetadataModule,
],
providers: [
DataSeedWorkspaceCommand,

View File

@ -124,19 +124,19 @@ export const seedViewFilterFieldMetadata = async (
isCustom: false,
workspaceId: SeedWorkspaceId,
isActive: true,
type: FieldMetadataType.UUID,
name: 'viewId',
label: 'View Id',
type: FieldMetadataType.RELATION,
name: 'view',
label: 'View',
targetColumnMap: {},
description: 'View Filter related view',
icon: 'IconLayoutCollage',
isNullable: false,
isNullable: true,
isSystem: false,
defaultValue: undefined,
},
{
id: SeedViewFilterFieldMetadataIds.ViewForeignKey,
objectMetadataId: SeedObjectMetadataIds.ViewField,
objectMetadataId: SeedObjectMetadataIds.ViewFilter,
isCustom: false,
workspaceId: SeedWorkspaceId,
isActive: true,

View File

@ -1,169 +0,0 @@
import { DataSource } from 'typeorm';
import { SeedViewIds } from 'src/database/typeorm-seeds/workspace/views';
import { SeedCompanyFieldMetadataIds } from 'src/database/typeorm-seeds/metadata/field-metadata/company';
import { SeedPersonFieldMetadataIds } from 'src/database/typeorm-seeds/metadata/field-metadata/person';
import { SeedOpportunityFieldMetadataIds } from 'src/database/typeorm-seeds/metadata/field-metadata/opportunity';
const tableName = 'viewField';
export const seedViewFields = async (
workspaceDataSource: DataSource,
schemaName: string,
) => {
await workspaceDataSource
.createQueryBuilder()
.insert()
.into(`${schemaName}.${tableName}`, [
'fieldMetadataId',
'viewId',
'position',
'isVisible',
'size',
])
.orIgnore()
.values([
{
fieldMetadataId: SeedCompanyFieldMetadataIds.Name,
viewId: SeedViewIds.Company,
position: 0,
isVisible: true,
size: 180,
},
{
fieldMetadataId: SeedCompanyFieldMetadataIds.DomainName,
viewId: SeedViewIds.Company,
position: 1,
isVisible: true,
size: 100,
},
{
fieldMetadataId: SeedCompanyFieldMetadataIds.AccountOwner,
viewId: SeedViewIds.Company,
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedCompanyFieldMetadataIds.CreatedAt,
viewId: SeedViewIds.Company,
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedCompanyFieldMetadataIds.Employees,
viewId: SeedViewIds.Company,
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedCompanyFieldMetadataIds.LinkedinLink,
viewId: SeedViewIds.Company,
position: 5,
isVisible: true,
size: 170,
},
{
fieldMetadataId: SeedCompanyFieldMetadataIds.Address,
viewId: SeedViewIds.Company,
position: 6,
isVisible: true,
size: 170,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.Name,
viewId: SeedViewIds.Person,
position: 0,
isVisible: true,
size: 210,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.Email,
viewId: SeedViewIds.Person,
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.Company,
viewId: SeedViewIds.Person,
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.Phone,
viewId: SeedViewIds.Person,
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.CreatedAt,
viewId: SeedViewIds.Person,
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.City,
viewId: SeedViewIds.Person,
position: 5,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.JobTitle,
viewId: SeedViewIds.Person,
position: 6,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.LinkedinLink,
viewId: SeedViewIds.Person,
position: 7,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedPersonFieldMetadataIds.XLink,
viewId: SeedViewIds.Person,
position: 8,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedOpportunityFieldMetadataIds.Amount,
viewId: SeedViewIds.Opportunity,
position: 0,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedOpportunityFieldMetadataIds.CloseDate,
viewId: SeedViewIds.Opportunity,
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedOpportunityFieldMetadataIds.Probability,
viewId: SeedViewIds.Opportunity,
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: SeedOpportunityFieldMetadataIds.PointOfContact,
viewId: SeedViewIds.Opportunity,
position: 3,
isVisible: true,
size: 150,
},
])
.execute();
};

View File

@ -1,48 +1,198 @@
import { DataSource } from 'typeorm';
import { SeedObjectMetadataIds } from 'src/database/typeorm-seeds/metadata/object-metadata';
const tableName = 'view';
export const enum SeedViewIds {
Company = '20202020-2441-4424-8163-4002c523d415',
Person = '20202020-1979-447d-8115-593744eb4ead',
Opportunity = '20202020-b2b3-48a5-96ce-0936d6af21f7',
}
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
export const seedViews = async (
workspaceDataSource: DataSource,
schemaName: string,
objectMetadataMap: Record<string, ObjectMetadataEntity>,
) => {
const createdViews = await workspaceDataSource
.createQueryBuilder()
.insert()
.into(`${schemaName}.view`, [
'name',
'objectMetadataId',
'type',
])
.values([
{
name: 'All Companies',
objectMetadataId: objectMetadataMap['company'].id,
type: 'table',
},
{
name: 'All People',
objectMetadataId: objectMetadataMap['person'].id,
type: 'table',
},
{
name: 'All Opportunities',
objectMetadataId: objectMetadataMap['opportunity'].id,
type: 'kanban',
},
])
.returning('*')
.execute();
const viewIdMap = createdViews.raw.reduce((acc, view) => {
acc[view.name] = view.id;
return acc;
}, {});
await workspaceDataSource
.createQueryBuilder()
.insert()
.into(`${schemaName}.${tableName}`, [
'id',
'name',
'objectMetadataId',
'type',
])
.orIgnore()
.values([
{
id: SeedViewIds.Company,
name: 'All Companies',
objectMetadataId: SeedObjectMetadataIds.Company,
type: 'table',
},
{
id: SeedViewIds.Person,
name: 'All People',
objectMetadataId: SeedObjectMetadataIds.Person,
type: 'table',
},
{
id: SeedViewIds.Opportunity,
name: 'All Opportunities',
objectMetadataId: SeedObjectMetadataIds.Opportunity,
type: 'kanban',
},
])
.execute();
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewField`, [
'fieldMetadataId',
'viewId',
'position',
'isVisible',
'size',
])
.values([
{
fieldMetadataId: objectMetadataMap['company'].fields['name'],
viewId: viewIdMap['All Companies'],
position: 0,
isVisible: true,
size: 180,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['domainName'],
viewId: viewIdMap['All Companies'],
position: 1,
isVisible: true,
size: 100,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['accountOwner'],
viewId: viewIdMap['All Companies'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['createdAt'],
viewId: viewIdMap['All Companies'],
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['employees'],
viewId: viewIdMap['All Companies'],
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['linkedinLink'],
viewId: viewIdMap['All Companies'],
position: 5,
isVisible: true,
size: 170,
},
{
fieldMetadataId: objectMetadataMap['company'].fields['address'],
viewId: viewIdMap['All Companies'],
position: 6,
isVisible: true,
size: 170,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['name'],
viewId: viewIdMap['All People'],
position: 0,
isVisible: true,
size: 210,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['email'],
viewId: viewIdMap['All People'],
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['company'],
viewId: viewIdMap['All People'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['phone'],
viewId: viewIdMap['All People'],
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['createdAt'],
viewId: viewIdMap['All People'],
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['city'],
viewId: viewIdMap['All People'],
position: 5,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['jobTitle'],
viewId: viewIdMap['All People'],
position: 6,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['linkedinLink'],
viewId: viewIdMap['All People'],
position: 7,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['person'].fields['xLink'],
viewId: viewIdMap['All People'],
position: 8,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['amount'],
viewId: viewIdMap['All Opportunities'],
position: 0,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['closeDate'],
viewId: viewIdMap['All Opportunities'],
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['probability'],
viewId: viewIdMap['All Opportunities'],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId: objectMetadataMap['opportunity'].fields['pointOfContact'],
viewId: viewIdMap['All Opportunities'],
position: 3,
isVisible: true,
size: 150,
},
])
.execute();
};

View File

@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { typeORMCoreModuleOptions } from 'src/database/typeorm/core/core.datasource';
import { EnvironmentModule } from 'src/integrations/environment/environment.module';
import { TypeORMService } from './typeorm.service';
@ -27,6 +28,7 @@ const coreTypeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
useFactory: coreTypeORMFactory,
name: 'core',
}),
EnvironmentModule,
],
providers: [TypeORMService],
exports: [TypeORMService],

View File

@ -3,21 +3,21 @@ import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/f
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const currencyObjectDefinition = {
id: FieldMetadataType.CURRENCY.toString(),
nameSingular: 'currency',
namePlural: 'currency',
labelSingular: 'Currency',
labelPlural: 'Currency',
targetTableName: '',
fields: [
export const currencyFields = (
fieldMetadata?: FieldMetadataInterface,
): FieldMetadataInterface[] => {
return [
{
id: 'amountMicros',
type: FieldMetadataType.NUMERIC,
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'amountMicros',
label: 'AmountMicros',
targetColumnMap: { value: 'amountMicros' },
targetColumnMap: {
value: fieldMetadata
? `${fieldMetadata.name}AmountMicros`
: 'amountMicros',
},
isNullable: true,
} satisfies FieldMetadataInterface,
{
@ -26,10 +26,24 @@ export const currencyObjectDefinition = {
objectMetadataId: FieldMetadataType.CURRENCY.toString(),
name: 'currencyCode',
label: 'Currency Code',
targetColumnMap: { value: 'currencyCode' },
targetColumnMap: {
value: fieldMetadata
? `${fieldMetadata.name}CurrencyCode`
: 'currencyCode',
},
isNullable: true,
} satisfies FieldMetadataInterface,
],
];
};
export const currencyObjectDefinition = {
id: FieldMetadataType.CURRENCY.toString(),
nameSingular: 'currency',
namePlural: 'currency',
labelSingular: 'Currency',
labelPlural: 'Currency',
targetTableName: '',
fields: currencyFields(),
fromRelations: [],
toRelations: [],
} satisfies ObjectMetadataInterface;

View File

@ -3,21 +3,19 @@ import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/f
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const fullNameObjectDefinition = {
id: FieldMetadataType.FULL_NAME.toString(),
nameSingular: 'fullName',
namePlural: 'fullName',
labelSingular: 'FullName',
labelPlural: 'FullName',
targetTableName: '',
fields: [
export const fullNameFields = (
fieldMetadata?: FieldMetadataInterface,
): FieldMetadataInterface[] => {
return [
{
id: 'firstName',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.FULL_NAME.toString(),
name: 'firstName',
label: 'First Name',
targetColumnMap: { value: 'firstName' },
targetColumnMap: {
value: fieldMetadata ? `${fieldMetadata.name}FirstName` : 'firstName',
},
isNullable: true,
} satisfies FieldMetadataInterface,
{
@ -26,10 +24,22 @@ export const fullNameObjectDefinition = {
objectMetadataId: FieldMetadataType.FULL_NAME.toString(),
name: 'lastName',
label: 'Last Name',
targetColumnMap: { value: 'lastName' },
targetColumnMap: {
value: fieldMetadata ? `${fieldMetadata.name}LastName` : 'lastName',
},
isNullable: true,
} satisfies FieldMetadataInterface,
],
];
};
export const fullNameObjectDefinition = {
id: FieldMetadataType.FULL_NAME.toString(),
nameSingular: 'fullName',
namePlural: 'fullName',
labelSingular: 'FullName',
labelPlural: 'FullName',
targetTableName: '',
fields: fullNameFields(),
fromRelations: [],
toRelations: [],
} satisfies ObjectMetadataInterface;

View File

@ -3,21 +3,19 @@ import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/f
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const linkObjectDefinition = {
id: FieldMetadataType.LINK.toString(),
nameSingular: 'link',
namePlural: 'link',
labelSingular: 'Link',
labelPlural: 'Link',
targetTableName: '',
fields: [
export const linkFields = (
fieldMetadata?: FieldMetadataInterface,
): FieldMetadataInterface[] => {
return [
{
id: 'label',
type: FieldMetadataType.TEXT,
objectMetadataId: FieldMetadataType.LINK.toString(),
name: 'label',
label: 'Label',
targetColumnMap: { value: 'label' },
targetColumnMap: {
value: fieldMetadata ? `${fieldMetadata.name}Label` : 'label',
},
isNullable: true,
} satisfies FieldMetadataInterface,
{
@ -26,10 +24,22 @@ export const linkObjectDefinition = {
objectMetadataId: FieldMetadataType.LINK.toString(),
name: 'url',
label: 'Url',
targetColumnMap: { value: 'url' },
targetColumnMap: {
value: fieldMetadata ? `${fieldMetadata.name}Url` : 'url',
},
isNullable: true,
} satisfies FieldMetadataInterface,
],
];
};
export const linkObjectDefinition = {
id: FieldMetadataType.LINK.toString(),
nameSingular: 'link',
namePlural: 'link',
labelSingular: 'Link',
labelPlural: 'Link',
targetTableName: '',
fields: linkFields(),
fromRelations: [],
toRelations: [],
} satisfies ObjectMetadataInterface;

View File

@ -0,0 +1,23 @@
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export function generateDefaultValue(
type: FieldMetadataType,
): FieldMetadataDefaultValue {
switch (type) {
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
return {
value: '',
};
case FieldMetadataType.FULL_NAME:
return {
firstName: '',
lastName: '',
};
default:
return null;
}
}

View File

@ -12,9 +12,13 @@ import {
WorkspaceMigrationColumnActionType,
} from 'src/metadata/workspace-migration/workspace-migration.entity';
import { isCompositeFieldMetadataType } from 'src/metadata/field-metadata/utils/is-composite-field-metadata-type.util';
import { linkObjectDefinition } from 'src/metadata/field-metadata/composite-types/link.composite-type';
import { currencyObjectDefinition } from 'src/metadata/field-metadata/composite-types/currency.composite-type';
import { fullNameObjectDefinition } from 'src/metadata/field-metadata/composite-types/full-name.composite-type';
import { fullNameFields } from 'src/metadata/field-metadata/composite-types/full-name.composite-type';
import { currencyFields } from 'src/metadata/field-metadata/composite-types/currency.composite-type';
import { linkFields } from 'src/metadata/field-metadata/composite-types/link.composite-type';
type CompositeFieldSplitterFunction = (
fieldMetadata: FieldMetadataInterface,
) => FieldMetadataInterface[];
@Injectable()
export class WorkspaceMigrationFactory {
@ -26,7 +30,10 @@ export class WorkspaceMigrationFactory {
options?: WorkspaceColumnActionOptions;
}
>;
private compositeDefinitions = new Map<string, FieldMetadataInterface[]>();
private compositeDefinitions = new Map<
string,
CompositeFieldSplitterFunction
>();
constructor(
private readonly basicColumnActionFactory: BasicColumnActionFactory,
@ -83,11 +90,13 @@ export class WorkspaceMigrationFactory {
],
]);
this.compositeDefinitions = new Map<string, FieldMetadataInterface[]>([
[FieldMetadataType.LINK, linkObjectDefinition.fields],
[FieldMetadataType.CURRENCY, currencyObjectDefinition.fields],
[FieldMetadataType.FULL_NAME, fullNameObjectDefinition.fields],
]);
this.compositeDefinitions = new Map<string, CompositeFieldSplitterFunction>(
[
[FieldMetadataType.LINK, linkFields],
[FieldMetadataType.CURRENCY, currencyFields],
[FieldMetadataType.FULL_NAME, fullNameFields],
],
);
}
createColumnActions(
@ -128,11 +137,11 @@ export class WorkspaceMigrationFactory {
// If it's a composite field type, we need to create a column action for each of the fields
if (isCompositeFieldMetadataType(alteredFieldMetadata.type)) {
const fieldMetadataCollection = this.compositeDefinitions.get(
const fieldMetadataSplitterFunction = this.compositeDefinitions.get(
alteredFieldMetadata.type,
);
if (!fieldMetadataCollection) {
if (!fieldMetadataSplitterFunction) {
this.logger.error(
`No composite definition found for type ${alteredFieldMetadata.type}`,
{
@ -145,6 +154,9 @@ export class WorkspaceMigrationFactory {
);
}
const fieldMetadataCollection =
fieldMetadataSplitterFunction(alteredFieldMetadata);
return fieldMetadataCollection.map((fieldMetadata) =>
this.createColumnAction(action, fieldMetadata, fieldMetadata),
);

View File

@ -1,103 +0,0 @@
import camelCase from 'lodash.camelcase';
import 'reflect-metadata';
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util';
export type FieldMetadataDecorator = {
type: FieldMetadataType;
label: string;
description?: string | null;
icon?: string | null;
defaultValue?: FieldMetadataDefaultValue | null;
};
export type ObjectMetadataDecorator = {
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string | null;
icon?: string | null;
};
const classSuffix = 'ObjectMetadata';
export function ObjectMetadata(
metadata: ObjectMetadataDecorator,
): ClassDecorator {
return (target) => {
const isSystem = Reflect.getMetadata('isSystem', target) || false;
let objectName = camelCase(target.name);
if (objectName.endsWith(classSuffix)) {
objectName = objectName.slice(0, -classSuffix.length);
}
Reflect.defineMetadata(
'objectMetadata',
{
nameSingular: objectName,
...metadata,
targetTableName: objectName,
isSystem,
isCustom: false,
isActive: true,
},
target,
);
};
}
export function IsNullable() {
return function (target: object, fieldKey: string) {
Reflect.defineMetadata('isNullable', true, target, fieldKey);
};
}
export function IsSystem() {
return function (target: object, fieldKey?: string) {
if (fieldKey) {
Reflect.defineMetadata('isSystem', true, target, fieldKey);
} else {
Reflect.defineMetadata('isSystem', true, target);
}
};
}
export function FieldMetadata(
metadata: FieldMetadataDecorator,
): PropertyDecorator {
return (target: object, fieldKey: string) => {
const existingFieldMetadata =
Reflect.getMetadata('fieldMetadata', target.constructor) || {};
const isNullable =
Reflect.getMetadata('isNullable', target, fieldKey) || false;
const isSystem = Reflect.getMetadata('isSystem', target, fieldKey) || false;
Reflect.defineMetadata(
'fieldMetadata',
{
...existingFieldMetadata,
[fieldKey]: {
name: fieldKey,
...metadata,
targetColumnMap: generateTargetColumnMap(
metadata.type,
false,
fieldKey,
),
isNullable,
isSystem,
isCustom: false,
isActive: true,
},
},
target.constructor,
);
};
}

View File

@ -1,90 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const activityTargetMetadata = {
nameSingular: 'activityTarget',
namePlural: 'activityTargets',
labelSingular: 'Activity Target',
labelPlural: 'Activity Targets',
targetTableName: 'activityTarget',
description: 'An activity target',
icon: 'IconCheckbox',
isActive: true,
isSystem: true,
fields: [
{
// Relations
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activity',
label: 'Activity',
targetColumnMap: {},
description: 'ActivityTarget activity',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'person',
label: 'Person',
targetColumnMap: {},
description: 'ActivityTarget person',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {},
description: 'ActivityTarget company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'activityId',
label: 'Activity id (foreign key)',
targetColumnMap: {},
description: 'ActivityTarget activity id foreign key',
icon: undefined,
isNullable: false,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'personId',
label: 'Person id (foreign key)',
targetColumnMap: {},
description: 'ActivityTarget person id foreign key',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company id (foreign key)',
targetColumnMap: {},
description: 'ActivityTarget company id foreign key',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
],
};
export default activityTargetMetadata;

View File

@ -1,181 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const activityMetadata = {
nameSingular: 'activity',
namePlural: 'activities',
labelSingular: 'Activity',
labelPlural: 'Activities',
targetTableName: 'activity',
description: 'An activity',
icon: 'IconCheckbox',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'title',
label: 'Title',
targetColumnMap: {
value: 'title',
},
description: 'Activity title',
icon: 'IconNotes',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'body',
label: 'Body',
targetColumnMap: {
value: 'body',
},
description: 'Activity body',
icon: 'IconList',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'type',
label: 'Type',
targetColumnMap: {
value: 'type',
},
description: 'Activity type',
icon: 'IconCheckbox',
isNullable: false,
defaultValue: { value: 'Note' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'reminderAt',
label: 'Reminder Date',
targetColumnMap: {
value: 'reminderAt',
},
description: 'Activity reminder date',
icon: 'IconCalendarEvent',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'dueAt',
label: 'Due Date',
targetColumnMap: {
value: 'dueAt',
},
description: 'Activity due date',
icon: 'IconCalendarEvent',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'completedAt',
label: 'Completion Date',
targetColumnMap: {
value: 'completedAt',
},
description: 'Activity completion date',
icon: 'IconCheck',
isNullable: true,
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activityTargets',
label: 'Targets',
targetColumnMap: {},
description: 'Activity targets',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
targetColumnMap: {},
description: 'Activity attachments',
icon: 'IconFileImport',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'comments',
label: 'Comments',
targetColumnMap: {},
description: 'Activity comments',
icon: 'IconComment',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'author',
label: 'Author',
targetColumnMap: {},
description:
'Activity author. This is the person who created the activity',
icon: 'IconUserCircle',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'authorId',
label: 'Author id (foreign key)',
targetColumnMap: {},
description: 'Activity author id foreign key',
icon: undefined,
isNullable: false,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'assignee',
label: 'Assignee',
targetColumnMap: {},
description:
'Acitivity assignee. This is the workspace member assigned to the activity ',
icon: 'IconUserCircle',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'assigneeId',
label: 'Assignee id (foreign key)',
targetColumnMap: {},
description: 'Acitivity assignee id foreign key',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
],
};
export default activityMetadata;

View File

@ -1,57 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const apiKeyMetadata = {
nameSingular: 'apiKey',
namePlural: 'apiKeys',
labelSingular: 'Api Key',
labelPlural: 'Api Keys',
targetTableName: 'apiKey',
description: 'An api key',
icon: 'IconRobot',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'ApiKey name',
icon: 'IconLink',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'expiresAt',
label: 'Expiration date',
targetColumnMap: {
value: 'expiresAt',
},
description: 'ApiKey expiration date',
icon: 'IconCalendar',
isNullable: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'revokedAt',
label: 'Revocation date',
targetColumnMap: {
value: 'revokedAt',
},
description: 'ApiKey revocation date',
icon: 'IconCalendar',
isNullable: true,
},
],
};
export default apiKeyMetadata;

View File

@ -1,124 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const attachmentMetadata = {
nameSingular: 'attachment',
namePlural: 'attachments',
labelSingular: 'Attachment',
labelPlural: 'Attachments',
targetTableName: 'attachment',
description: 'An attachment',
icon: 'IconFileImport',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'Attachment name',
icon: 'IconFileUpload',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'fullPath',
label: 'Full path',
targetColumnMap: {
value: 'fullPath',
},
description: 'Attachment full path',
icon: 'IconLink',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'type',
label: 'Type',
targetColumnMap: {
value: 'type',
},
description: 'Attachment type',
icon: 'IconList',
isNullable: false,
defaultValue: { value: '' },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'author',
label: 'Author',
targetColumnMap: {
value: 'authorId',
},
description: 'Attachment author',
icon: 'IconCircleUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'authorId',
label: 'Author id (foreign key)',
targetColumnMap: {},
description: 'Activity author id foreign key',
icon: undefined,
isNullable: false,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activity',
label: 'Activity',
targetColumnMap: {
value: 'activityId',
},
description: 'Attachment activity',
icon: 'IconNotes',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'person',
label: 'Person',
targetColumnMap: {
value: 'personId',
},
description: 'Attachment person',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {
value: 'companyId',
},
description: 'Attachment company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
],
};
export default attachmentMetadata;

View File

@ -1,78 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const commentMetadata = {
nameSingular: 'comment',
namePlural: 'comments',
labelSingular: 'Comment',
labelPlural: 'Comments',
targetTableName: 'comment',
description: 'A comment',
icon: 'IconMessageCircle',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'body',
label: 'Body',
targetColumnMap: {
value: 'body',
},
description: 'Comment body',
icon: 'IconLink',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'authorId',
label: 'Author',
targetColumnMap: {},
description: 'Comment author',
icon: 'IconCircleUser',
isNullable: true,
isSystem: true,
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'author',
label: 'Author',
targetColumnMap: {},
description: 'Comment author',
icon: 'IconCircleUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activity',
label: 'Activity',
targetColumnMap: {},
description: 'Comment activity',
icon: 'IconNotes',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'activityId',
label: 'Activity',
targetColumnMap: {},
description: 'Comment activity',
icon: 'IconNotes',
isNullable: true,
isSystem: true,
},
],
};
export default commentMetadata;

View File

@ -1,212 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const companyMetadata = {
nameSingular: 'company',
namePlural: 'companies',
labelSingular: 'Company',
labelPlural: 'Companies',
targetTableName: 'company',
description: 'A company',
icon: 'IconBuildingSkyscraper',
isActive: true,
isSystem: false,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'The company name',
icon: 'IconBuildingSkyscraper',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'domainName',
label: 'Domain Name',
targetColumnMap: {
value: 'domainName',
},
description:
'The company website URL. We use this url to fetch the company icon',
icon: 'IconLink',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'address',
label: 'Address',
targetColumnMap: {
value: 'address',
},
description: 'The company address',
icon: 'IconMap',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'employees',
label: 'Employees',
targetColumnMap: {
value: 'employees',
},
description: 'Number of employees in the company',
icon: 'IconUsers',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.LINK,
name: 'linkedinLink',
label: 'Linkedin',
targetColumnMap: {
label: 'linkedinLinkLabel',
url: 'linkedinLinkUrl',
},
description: 'The company Linkedin account',
icon: 'IconBrandLinkedin',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.LINK,
name: 'xLink',
label: 'X',
targetColumnMap: {
label: 'xLinkLabel',
url: 'xLinkUrl',
},
description: 'The company Twitter/X account',
icon: 'IconBrandX',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.CURRENCY,
name: 'annualRecurringRevenue',
label: 'ARR',
targetColumnMap: {
amountMicros: 'annualRecurringRevenueAmountMicros',
currencyCode: 'annualRecurringRevenueCurrencyCode',
},
description:
'Annual Recurring Revenue: The actual or estimated annual revenue of the company',
icon: 'IconMoneybag',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.BOOLEAN,
name: 'idealCustomerProfile',
label: 'ICP',
targetColumnMap: {
value: 'idealCustomerProfile',
},
description:
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
icon: 'IconTarget',
isNullable: true,
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'people',
label: 'People',
targetColumnMap: {},
description: 'People linked to the company.',
icon: 'IconUsers',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'accountOwner',
label: 'Account Owner',
targetColumnMap: {
value: 'accountOwnerId',
},
description:
'Your team member responsible for managing the company account',
icon: 'IconUserCircle',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'accountOwnerId',
label: 'Account Owner ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for account owner',
icon: undefined,
isNullable: true,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activityTargets',
label: 'Activities',
targetColumnMap: {},
description: 'Activities tied to the company',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'opportunities',
label: 'Opportunities',
targetColumnMap: {},
description: 'Opportunities linked to the company.',
icon: 'IconTargetArrow',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
targetColumnMap: {},
description: 'Favorites linked to the company',
icon: 'IconHeart',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
targetColumnMap: {},
description: 'Attachments linked to the company.',
icon: 'IconFileImport',
isNullable: true,
},
],
};
export default companyMetadata;

View File

@ -1,104 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const favoriteMetadata = {
nameSingular: 'favorite',
namePlural: 'favorites',
labelSingular: 'Favorite',
labelPlural: 'Favorites',
targetTableName: 'favorite',
description: 'A favorite',
icon: 'IconHeart',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'position',
label: 'Position',
targetColumnMap: {
value: 'position',
},
description: 'Favorite position',
icon: 'IconList',
isNullable: false,
defaultValue: { value: 0 },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'workspaceMember',
label: 'Workspace Member',
targetColumnMap: {},
description: 'Favorite workspace member',
icon: 'IconCircleUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'person',
label: 'Person',
targetColumnMap: {},
description: 'Favorite person',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {},
description: 'Favorite company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'workspaceMemberId',
label: 'Workspace Member ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for workspace member',
icon: undefined,
isNullable: false,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'personId',
label: 'Person ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for person',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for company',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
],
};
export default favoriteMetadata;

View File

@ -1,165 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const opportunityMetadata = {
nameSingular: 'opportunity',
namePlural: 'opportunities',
labelSingular: 'Opportunity',
labelPlural: 'Opportunities',
targetTableName: 'opportunity',
description: 'An opportunity',
icon: 'IconTargetArrow',
isActive: true,
isSystem: false,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.CURRENCY,
name: 'amount',
label: 'Amount',
targetColumnMap: {
amountMicros: 'amountAmountMicros',
currencyCode: 'amountCurrencyCode',
},
description: 'Opportunity amount',
icon: 'IconCurrencyDollar',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.DATE_TIME,
name: 'closeDate',
label: 'Close date',
targetColumnMap: {
value: 'closeDate',
},
description: 'Opportunity close date',
icon: 'IconCalendarEvent',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'probability',
label: 'Probability',
targetColumnMap: {
value: 'probability',
},
description: 'Opportunity probability',
icon: 'IconProgressCheck',
isNullable: true,
defaultValue: { value: '0' },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'pipelineStep',
label: 'Pipeline Step',
targetColumnMap: {
value: 'pipelineStepId',
},
description: 'Opportunity pipeline step',
icon: 'IconKanban',
isSystem: true,
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'pointOfContact',
label: 'Point of Contact',
targetColumnMap: {
value: 'pointOfContactId',
},
description: 'Opportunity point of contact',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'person',
label: 'Person',
targetColumnMap: {
value: 'personId',
},
description: 'Opportunity person',
icon: 'IconUser',
isNullable: true,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {
value: 'companyId',
},
description: 'Opportunity company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for company',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'personId',
label: 'Person ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for person',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'pointOfContactId',
label: 'Point of Contact ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for point of contact',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'pipelineStepId',
label: 'Pipeline Step ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for pipeline step',
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
},
],
};
export default opportunityMetadata;

View File

@ -1,209 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const personMetadata = {
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
targetTableName: 'person',
description: 'A person',
icon: 'IconUser',
isActive: true,
isSystem: false,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.FULL_NAME,
name: 'name',
label: 'Name',
targetColumnMap: {
firstName: 'nameFirstName',
lastName: 'nameLastName',
},
description: 'Contacts name',
icon: 'IconUser',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.EMAIL,
name: 'email',
label: 'Email',
targetColumnMap: {
value: 'email',
},
description: 'Contacts Email',
icon: 'IconMail',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.LINK,
name: 'linkedinLink',
label: 'Linkedin',
targetColumnMap: {
label: 'linkedinLinkLabel',
url: 'linkedinLinkUrl',
},
description: 'Contacts Linkedin account',
icon: 'IconBrandLinkedin',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.LINK,
name: 'xLink',
label: 'X',
targetColumnMap: {
label: 'xLinkLabel',
url: 'xLinkUrl',
},
description: 'Contacts X/Twitter account',
icon: 'IconBrandX',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'jobTitle',
label: 'Job Title',
targetColumnMap: {
value: 'jobTitle',
},
description: 'Contacts job title',
icon: 'IconBriefcase',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'phone',
label: 'Phone',
targetColumnMap: {
value: 'phone',
},
description: 'Contacts phone number',
icon: 'IconPhone',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'city',
label: 'City',
targetColumnMap: {
value: 'city',
},
description: 'Contacts city',
icon: 'IconMap',
isNullable: true,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'avatarUrl',
label: 'Avatar',
targetColumnMap: {
value: 'avatarUrl',
},
description: 'Contacts avatar',
icon: 'IconFileUpload',
isNullable: true,
isSystem: true,
defaultValue: { value: '' },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
targetColumnMap: {},
description: 'Contacts company',
icon: 'IconBuildingSkyscraper',
isNullable: true,
isSystem: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company ID (foreign key)',
targetColumnMap: {},
description: 'Foreign key for company',
icon: undefined,
isNullable: true,
isSystem: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'pointOfContactForOpportunities',
label: 'POC for Opportunities',
targetColumnMap: {},
description: 'Point of Contact for Opportunities',
icon: 'IconTargetArrow',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'activityTargets',
label: 'Activities',
targetColumnMap: {},
description: 'Activities tied to the contact',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'opportunities',
label: 'Opportunities',
targetColumnMap: {},
description: 'Opportunities linked to the contact.',
icon: 'IconTargetArrow',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
targetColumnMap: {},
description: 'Favorites linked to the contact',
icon: 'IconHeart',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
targetColumnMap: {},
description: 'Attachments linked to the contact.',
icon: 'IconFileImport',
isNullable: true,
},
],
};
export default personMetadata;

View File

@ -1,71 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const pipelineStepMetadata = {
nameSingular: 'pipelineStep',
namePlural: 'pipelineSteps',
labelSingular: 'Pipeline Step',
labelPlural: 'Pipeline Steps',
targetTableName: 'pipelineStep',
description: 'A pipeline step',
icon: 'IconLayoutKanban',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'Pipeline Step name',
icon: 'IconCurrencyDollar',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'color',
label: 'Color',
targetColumnMap: {
value: 'color',
},
description: 'Pipeline Step color',
icon: 'IconColorSwatch',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'position',
label: 'Position',
targetColumnMap: {
value: 'position',
},
description: 'Pipeline Step position',
icon: 'IconHierarchy2',
isNullable: false,
defaultValue: { value: 0 },
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'opportunities',
label: 'Opportunities',
targetColumnMap: {},
description: 'Opportunities linked to the step.',
icon: 'IconTargetArrow',
isNullable: true,
},
],
};
export default pipelineStepMetadata;

View File

@ -1,27 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const activityRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'activity',
toObjectNameSingular: 'activityTarget',
fromFieldMetadataName: 'activityTargets',
toFieldMetadataName: 'activity',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'activity',
toObjectNameSingular: 'attachment',
fromFieldMetadataName: 'attachments',
toFieldMetadataName: 'activity',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'activity',
toObjectNameSingular: 'comment',
fromFieldMetadataName: 'comments',
toFieldMetadataName: 'activity',
},
];
export default activityRelationMetadata;

View File

@ -1,41 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const companyRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'person',
fromFieldMetadataName: 'people',
toFieldMetadataName: 'company',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'favorite',
fromFieldMetadataName: 'favorites',
toFieldMetadataName: 'company',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'attachment',
fromFieldMetadataName: 'attachments',
toFieldMetadataName: 'company',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'opportunity',
fromFieldMetadataName: 'opportunities',
toFieldMetadataName: 'company',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'company',
toObjectNameSingular: 'activityTarget',
fromFieldMetadataName: 'activityTargets',
toFieldMetadataName: 'company',
},
];
export default companyRelationMetadata;

View File

@ -1,41 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const personRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'favorite',
fromFieldMetadataName: 'favorites',
toFieldMetadataName: 'person',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'attachment',
fromFieldMetadataName: 'attachments',
toFieldMetadataName: 'person',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'opportunity',
fromFieldMetadataName: 'opportunities',
toFieldMetadataName: 'person',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'opportunity',
fromFieldMetadataName: 'pointOfContactForOpportunities',
toFieldMetadataName: 'pointOfContact',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'person',
toObjectNameSingular: 'activityTarget',
fromFieldMetadataName: 'activityTargets',
toFieldMetadataName: 'person',
},
];
export default personRelationMetadata;

View File

@ -1,13 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const pipelineStepRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'pipelineStep',
toObjectNameSingular: 'opportunity',
fromFieldMetadataName: 'opportunities',
toFieldMetadataName: 'pipelineStep',
},
];
export default pipelineStepRelationMetadata;

View File

@ -1,27 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const viewRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'view',
toObjectNameSingular: 'viewField',
fromFieldMetadataName: 'viewFields',
toFieldMetadataName: 'view',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'view',
toObjectNameSingular: 'viewFilter',
fromFieldMetadataName: 'viewFilters',
toFieldMetadataName: 'view',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'view',
toObjectNameSingular: 'viewSort',
fromFieldMetadataName: 'viewSorts',
toFieldMetadataName: 'view',
},
];
export default viewRelationMetadata;

View File

@ -1,48 +0,0 @@
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
const workspaceMemberRelationMetadata = [
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'company',
fromFieldMetadataName: 'accountOwnerForCompanies',
toFieldMetadataName: 'accountOwner',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'favorite',
fromFieldMetadataName: 'favorites',
toFieldMetadataName: 'workspaceMember',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'activity',
fromFieldMetadataName: 'authoredActivities',
toFieldMetadataName: 'author',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'activity',
fromFieldMetadataName: 'assignedActivities',
toFieldMetadataName: 'assignee',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'comment',
fromFieldMetadataName: 'authoredComments',
toFieldMetadataName: 'author',
},
{
type: RelationMetadataType.ONE_TO_MANY,
fromObjectNameSingular: 'workspaceMember',
toObjectNameSingular: 'attachment',
fromFieldMetadataName: 'authoredAttachments',
toFieldMetadataName: 'author',
},
];
export default workspaceMemberRelationMetadata;

View File

@ -1,82 +0,0 @@
import apiKeyMetadata from 'src/workspace/workspace-manager/standard-objects/api-key';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import activityMetadata from 'src/workspace/workspace-manager/standard-objects/activity';
import activityTargetMetadata from 'src/workspace/workspace-manager/standard-objects/activity-target';
import attachmentMetadata from 'src/workspace/workspace-manager/standard-objects/attachment';
import commentMetadata from 'src/workspace/workspace-manager/standard-objects/comment';
import companyMetadata from 'src/workspace/workspace-manager/standard-objects/company';
import favoriteMetadata from 'src/workspace/workspace-manager/standard-objects/favorite';
import opportunityMetadata from 'src/workspace/workspace-manager/standard-objects/opportunity';
import personMetadata from 'src/workspace/workspace-manager/standard-objects/person';
import pipelineStepMetadata from 'src/workspace/workspace-manager/standard-objects/pipeline-step';
import viewMetadata from 'src/workspace/workspace-manager/standard-objects/view';
import viewFieldMetadata from 'src/workspace/workspace-manager/standard-objects/view-field';
import viewFilterMetadata from 'src/workspace/workspace-manager/standard-objects/view-filter';
import viewSortMetadata from 'src/workspace/workspace-manager/standard-objects/view-sort';
import workspaceMemberMetadata from 'src/workspace/workspace-manager/standard-objects/workspace-member';
import connectedAccountMetadata from 'src/workspace/workspace-manager/standard-objects/connected-account';
export const standardObjectsMetadata = {
activityTarget: activityTargetMetadata,
activity: activityMetadata,
apiKey: apiKeyMetadata,
attachment: attachmentMetadata,
comment: commentMetadata,
company: companyMetadata,
connectedAccount: connectedAccountMetadata,
favorite: favoriteMetadata,
opportunity: opportunityMetadata,
person: personMetadata,
pipelineStep: pipelineStepMetadata,
viewField: viewFieldMetadata,
viewFilter: viewFilterMetadata,
viewSort: viewSortMetadata,
view: viewMetadata,
workspaceMember: workspaceMemberMetadata,
};
export const basicFieldsMetadata: Partial<FieldMetadataEntity>[] = [
{
name: 'id',
label: 'Id',
type: FieldMetadataType.UUID,
targetColumnMap: {
value: 'id',
},
isNullable: true,
isSystem: true,
isCustom: false,
isActive: true,
defaultValue: { type: 'uuid' },
},
{
name: 'createdAt',
label: 'Creation date',
type: FieldMetadataType.DATE_TIME,
targetColumnMap: {
value: 'createdAt',
},
icon: 'IconCalendar',
isNullable: true,
isCustom: false,
isActive: true,
defaultValue: { type: 'now' },
},
{
name: 'updatedAt',
label: 'Update date',
type: FieldMetadataType.DATE_TIME,
targetColumnMap: {
value: 'updatedAt',
},
icon: 'IconCalendar',
isNullable: true,
isCustom: false,
isSystem: true,
isActive: true,
defaultValue: { type: 'now' },
},
];

View File

@ -1,15 +0,0 @@
import activityRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/activity';
import companyRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/company';
import personRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/person';
import pipelineStepRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/pipeline-step';
import viewRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/view';
import workspaceMemberRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/workspace-member';
export const standardObjectRelationMetadata = [
...activityRelationMetadata,
...companyRelationMetadata,
...personRelationMetadata,
...pipelineStepRelationMetadata,
...viewRelationMetadata,
...workspaceMemberRelationMetadata,
];

View File

@ -1,96 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const viewFieldMetadata = {
nameSingular: 'viewField',
namePlural: 'viewFields',
labelSingular: 'View Field',
labelPlural: 'View Fields',
targetTableName: 'viewField',
description: '(System) View Fields',
icon: 'IconTag',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'fieldMetadataId',
label: 'Field Metadata Id',
targetColumnMap: {
value: 'fieldMetadataId',
},
description: 'View Field target field',
icon: 'IconTag',
isNullable: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.BOOLEAN,
name: 'isVisible',
label: 'Visible',
targetColumnMap: {
value: 'isVisible',
},
description: 'View Field visibility',
icon: 'IconEye',
isNullable: false,
defaultValue: { value: true },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'size',
label: 'Size',
targetColumnMap: {
value: 'size',
},
description: 'View Field size',
icon: 'IconEye',
isNullable: false,
defaultValue: { value: 0 },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.NUMBER,
name: 'position',
label: 'Position',
targetColumnMap: {
value: 'position',
},
description: 'View Field position',
icon: 'IconList',
isNullable: false,
defaultValue: { value: 0 },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'view',
label: 'View',
targetColumnMap: {},
description: 'View Field related view',
icon: 'IconLayoutCollage',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'viewId',
label: 'View Id',
targetColumnMap: {
value: 'viewId',
},
description: 'View field related view',
icon: 'IconLayoutCollage',
isNullable: false,
},
],
};
export default viewFieldMetadata;

View File

@ -1,96 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const viewFilterMetadata = {
nameSingular: 'viewFilter',
namePlural: 'viewFilters',
labelSingular: 'View Filter',
labelPlural: 'View Filters',
targetTableName: 'viewFilter',
description: '(System) View Filters',
icon: 'IconFilterBolt',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'fieldMetadataId',
label: 'Field Metadata Id',
targetColumnMap: {
value: 'fieldMetadataId',
},
description: 'View Filter target field',
icon: null,
isNullable: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'operand',
label: 'Operand',
targetColumnMap: {
value: 'operand',
},
description: 'View Filter operand',
icon: null,
isNullable: false,
defaultValue: { value: 'Contains' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'value',
label: 'Value',
targetColumnMap: {
value: 'value',
},
description: 'View Filter value',
icon: null,
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'displayValue',
label: 'Display Value',
targetColumnMap: {
value: 'displayValue',
},
description: 'View Filter Display Value',
icon: null,
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'view',
label: 'View',
targetColumnMap: {},
description: 'View Filter related view',
icon: 'IconLayoutCollage',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'viewId',
label: 'View Id',
targetColumnMap: {
value: 'viewId',
},
description: 'View Filter related view',
icon: 'IconLayoutCollage',
isNullable: false,
},
],
};
export default viewFilterMetadata;

View File

@ -1,68 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const viewSortMetadata = {
nameSingular: 'viewSort',
namePlural: 'viewSorts',
labelSingular: 'View Sort',
labelPlural: 'View Sorts',
targetTableName: 'viewSort',
description: '(System) View Sorts',
icon: 'IconArrowsSort',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'fieldMetadataId',
label: 'Field Metadata Id',
targetColumnMap: {
value: 'fieldMetadataId',
},
description: 'View Sort target field',
icon: null,
isNullable: false,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'direction',
label: 'Direction',
targetColumnMap: {
value: 'direction',
},
description: 'View Sort direction',
icon: null,
isNullable: false,
defaultValue: { value: 'asc' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'view',
label: 'View',
targetColumnMap: {},
description: 'View Sort related view',
icon: 'IconLayoutCollage',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'viewId',
label: 'View Id',
targetColumnMap: {
value: 'viewId',
},
description: 'View Sort related view',
icon: 'IconLayoutCollage',
isNullable: false,
},
],
};
export default viewSortMetadata;

View File

@ -1,85 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const viewMetadata = {
nameSingular: 'view',
namePlural: 'views',
labelSingular: 'View',
labelPlural: 'Views',
targetTableName: 'view',
description: '(System) Views',
icon: 'IconLayoutCollage',
isActive: true,
isSystem: true,
fields: [
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
targetColumnMap: {
value: 'name',
},
description: 'View name',
icon: null,
isNullable: false,
defaultValue: { value: '' },
},
{
type: FieldMetadataType.UUID,
name: 'objectMetadataId',
label: 'Object Metadata Id',
targetColumnMap: {
value: 'objectMetadataId',
},
description: 'View target object',
icon: null,
isNullable: false,
},
{
type: FieldMetadataType.TEXT,
name: 'type',
label: 'Type',
targetColumnMap: {
value: 'type',
},
description: 'View type',
icon: null,
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'viewFields',
label: 'View Fields',
targetColumnMap: {},
description: 'View Fields',
icon: 'IconTag',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'viewSorts',
label: 'View Sorts',
targetColumnMap: {},
description: 'View Sorts',
icon: 'IconArrowsSort',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'viewFilters',
label: 'View Filters',
targetColumnMap: {},
description: 'View Filters',
icon: 'IconFilterBolt',
isNullable: true,
},
],
};
export default viewMetadata;

View File

@ -1,45 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const webhookMetadata = {
nameSingular: 'webhook',
namePlural: 'webhooks',
labelSingular: 'Webhook',
labelPlural: 'Webhooks',
targetTableName: 'webhook',
description: 'A webhook',
icon: 'IconRobot',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'targetUrl',
label: 'Target Url',
targetColumnMap: {
value: 'targetUrl',
},
description: 'Webhook target url',
icon: 'IconLink',
isNullable: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'operation',
label: 'Operation',
targetColumnMap: {
value: 'operation',
},
description: 'Webhook operation',
icon: 'IconCheckbox',
isNullable: false,
defaultValue: { value: '' },
},
],
};
export default webhookMetadata;

View File

@ -1,156 +0,0 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
const workspaceMemberMetadata = {
nameSingular: 'workspaceMember',
namePlural: 'workspaceMembers',
labelSingular: 'Workspace Member',
labelPlural: 'Workspace Members',
targetTableName: 'workspaceMember',
description: 'A workspace member',
icon: 'IconUserCircle',
isActive: true,
isSystem: true,
fields: [
{
isCustom: false,
isActive: true,
type: FieldMetadataType.FULL_NAME,
name: 'name',
label: 'Name',
targetColumnMap: {
firstName: 'nameFirstName',
lastName: 'nameLastName',
},
description: 'Workspace member name',
icon: 'IconCircleUser',
isNullable: false,
defaultValue: { firstName: '', lastName: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'colorScheme',
label: 'Color Scheme',
targetColumnMap: {
value: 'colorScheme',
},
description: 'Preferred color scheme',
icon: 'IconColorSwatch',
isNullable: false,
defaultValue: { value: 'Light' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'locale',
label: 'Language',
targetColumnMap: {
value: 'locale',
},
description: 'Preferred language',
icon: 'IconLanguage',
isNullable: false,
defaultValue: { value: 'en' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.TEXT,
name: 'avatarUrl',
label: 'Avatar Url',
targetColumnMap: {
value: 'avatarUrl',
},
description: 'Workspace member avatar',
icon: 'IconFileUpload',
isNullable: true,
isSystem: false,
defaultValue: { value: '' },
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
name: 'userId',
label: 'User Id',
targetColumnMap: {
value: 'userId',
},
description: 'Associated User Id',
icon: 'IconCircleUsers',
isNullable: false,
isSystem: false,
},
// Relations
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'authoredActivities',
label: 'Authored activities',
targetColumnMap: {},
description: 'Activities created by the workspace member',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'assignedActivities',
label: 'Assigned activities',
targetColumnMap: {},
description: 'Activities assigned to the workspace member',
icon: 'IconCheckbox',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
targetColumnMap: {},
description: 'Favorites linked to the workspace member',
icon: 'IconHeart',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'accountOwnerForCompanies',
label: 'Account Owner For Companies',
targetColumnMap: {},
description: 'Account owner for companies',
icon: 'IconBriefcase',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'authoredAttachments',
label: 'Authored attachments',
targetColumnMap: {},
description: 'Attachments created by the workspace member',
icon: 'IconFileImport',
isNullable: true,
},
{
isCustom: false,
isActive: true,
type: FieldMetadataType.RELATION,
name: 'authoredComments',
label: 'Authored comments',
targetColumnMap: {},
description: 'Authored comments',
icon: 'IconComment',
isNullable: true,
},
],
};
export default workspaceMemberMetadata;

View File

@ -1,42 +0,0 @@
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { BaseObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/base.object-metadata';
export class MetadataParser {
static parseMetadata(
metadata: typeof BaseObjectMetadata,
workspaceId: string,
dataSourceId: string,
) {
const objectMetadata = Reflect.getMetadata('objectMetadata', metadata);
const fieldMetadata = Reflect.getMetadata('fieldMetadata', metadata);
if (objectMetadata) {
const fields = Object.values(fieldMetadata);
return {
...objectMetadata,
workspaceId,
dataSourceId,
fields: fields.map((field: FieldMetadataEntity) => ({
...field,
workspaceId,
isSystem: objectMetadata.isSystem || field.isSystem,
defaultValue: field.defaultValue || null, // TODO: use default default value based on field type
options: field.options || null,
})),
};
}
return undefined;
}
static parseAllMetadata(
metadata: (typeof BaseObjectMetadata)[],
workspaceId: string,
dataSourceId: string,
) {
return metadata.map((_metadata) =>
MetadataParser.parseMetadata(_metadata, workspaceId, dataSourceId),
);
}
}

View File

@ -1,14 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { RelationMetadataModule } from 'src/metadata/relation-metadata/relation-metadata.module';
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/worksapce-sync-metadata.module';
import { WorkspaceManagerService } from './workspace-manager.service';
@ -16,14 +12,9 @@ import { WorkspaceManagerService } from './workspace-manager.service';
imports: [
WorkspaceDataSourceModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
ObjectMetadataModule,
DataSourceModule,
RelationMetadataModule,
TypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
),
WorkspaceSyncMetadataModule,
],
exports: [WorkspaceManagerService],
providers: [WorkspaceManagerService],

View File

@ -1,8 +1,4 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import diff from 'microdiff';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
@ -12,41 +8,16 @@ import { standardObjectsPrefillData } from 'src/workspace/workspace-manager/stan
import { demoObjectsPrefillData } from 'src/workspace/workspace-manager/demo-objects-prefill-data/demo-objects-prefill-data';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
import { RelationMetadataService } from 'src/metadata/relation-metadata/relation-metadata.service';
import { standardObjectRelationMetadata } from 'src/workspace/workspace-manager/standard-objects/standard-object-relation-metadata';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import { MetadataParser } from 'src/workspace/workspace-manager/utils/metadata.parser';
import { WebhookObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/webook.object-metadata';
import { ApiKeyObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/api-key.object-metadata';
import { ViewSortObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/view-sort.object-metadata';
import {
filterIgnoredProperties,
mapObjectMetadataByUniqueIdentifier,
} from 'src/workspace/workspace-manager/utils/sync-metadata.util';
import {
basicFieldsMetadata,
standardObjectsMetadata,
} from './standard-objects/standard-object-metadata';
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
@Injectable()
export class WorkspaceManagerService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly dataSourceService: DataSourceService,
private readonly relationMetadataService: RelationMetadataService,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
) {}
/**
@ -68,22 +39,14 @@ export class WorkspaceManagerService {
await this.setWorkspaceMaxRow(workspaceId, schemaName);
await this.workspaceMigrationService.insertStandardMigrations(workspaceId);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
const createdObjectMetadata =
await this.createStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
await this.prefillWorkspaceWithStandardObjects(
dataSourceMetadata,
workspaceId,
createdObjectMetadata,
);
}
@ -106,321 +69,12 @@ export class WorkspaceManagerService {
await this.setWorkspaceMaxRow(workspaceId, schemaName);
await this.workspaceMigrationService.insertStandardMigrations(workspaceId);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
const createdObjectMetadata =
await this.createStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
workspaceId,
);
await this.prefillWorkspaceWithDemoObjects(
dataSourceMetadata,
workspaceId,
createdObjectMetadata,
);
}
/**
*
* Create all standard objects and fields metadata for a given workspace
*
* @param dataSourceId
* @param workspaceId
*/
public async createStandardObjectsAndFieldsMetadata(
dataSourceId: string,
workspaceId: string,
): Promise<ObjectMetadataEntity[]> {
const createdObjectMetadata = await this.objectMetadataService.createMany(
Object.values(standardObjectsMetadata).map((objectMetadata: any) => ({
...objectMetadata,
dataSourceId,
workspaceId,
isCustom: false,
isActive: true,
fields: [...basicFieldsMetadata, ...objectMetadata.fields].map(
(field) => ({
...field,
workspaceId,
isCustom: false,
isActive: true,
}),
),
})),
);
await this.relationMetadataService.createMany(
Object.values(standardObjectRelationMetadata).map((relationMetadata) =>
this.createStandardObjectRelations(
workspaceId,
createdObjectMetadata,
relationMetadata,
),
),
);
return createdObjectMetadata;
}
/**
*
* @param workspaceId
* @param createdObjectMetadata
* @param relationMetadata
* @returns Partial<RelationMetadataEntity>
*/
private createStandardObjectRelations(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity[],
relationMetadata: any,
) {
const createdObjectMetadataByNameSingular = createdObjectMetadata.reduce(
(acc, curr) => {
acc[curr.nameSingular] = curr;
return acc;
},
{},
);
const fromObjectMetadata =
createdObjectMetadataByNameSingular[
relationMetadata.fromObjectNameSingular
];
const toObjectMetadata =
createdObjectMetadataByNameSingular[
relationMetadata.toObjectNameSingular
];
if (!fromObjectMetadata) {
throw new Error(
`Could not find created object metadata with
fromObjectNameSingular: ${relationMetadata.fromObjectNameSingular}`,
);
}
if (!toObjectMetadata) {
throw new Error(
`Could not find created object metadata with
toObjectNameSingular: ${relationMetadata.toObjectNameSingular}`,
);
}
const fromFieldMetadata = createdObjectMetadataByNameSingular[
relationMetadata.fromObjectNameSingular
]?.fields.find(
(field: FieldMetadataEntity) =>
field.type === FieldMetadataType.RELATION &&
field.name === relationMetadata.fromFieldMetadataName,
);
const toFieldMetadata = createdObjectMetadataByNameSingular[
relationMetadata.toObjectNameSingular
]?.fields.find(
(field: FieldMetadataEntity) =>
field.type === FieldMetadataType.RELATION &&
field.name === relationMetadata.toFieldMetadataName,
);
if (!fromFieldMetadata) {
throw new Error(
`Could not find created field metadata with
fromFieldMetadataName: ${relationMetadata.fromFieldMetadataName}
for object: ${relationMetadata.fromObjectNameSingular}`,
);
}
if (!toFieldMetadata) {
throw new Error(
`Could not find created field metadata with
toFieldMetadataName: ${relationMetadata.toFieldMetadataName}
for object: ${relationMetadata.toObjectNameSingular}`,
);
}
return {
fromObjectMetadataId: fromObjectMetadata.id,
toObjectMetadataId: toObjectMetadata.id,
workspaceId,
relationType: relationMetadata.type,
fromFieldMetadataId: fromFieldMetadata.id,
toFieldMetadataId: toFieldMetadata.id,
};
}
/**
*
* Sync all standard objects and fields metadata for a given workspace and data source
* This will update the metadata if it has changed and generate migrations based on the diff.
*
* @param dataSourceId
* @param workspaceId
*/
public async syncStandardObjectsAndFieldsMetadata(
dataSourceId: string,
workspaceId: string,
) {
const standardObjects = MetadataParser.parseAllMetadata(
[WebhookObjectMetadata, ApiKeyObjectMetadata, ViewSortObjectMetadata],
workspaceId,
dataSourceId,
);
const objectsInDB = await this.objectMetadataRepository.find({
where: { workspaceId, dataSourceId, isCustom: false },
relations: ['fields'],
});
const objectsInDBByName = mapObjectMetadataByUniqueIdentifier(objectsInDB);
const standardObjectsByName =
mapObjectMetadataByUniqueIdentifier(standardObjects);
const objectsToCreate: ObjectMetadataEntity[] = [];
const objectsToDelete = objectsInDB.filter(
(objectInDB) => !standardObjectsByName[objectInDB.nameSingular],
);
const objectsToUpdate: Record<string, ObjectMetadataEntity> = {};
const fieldsToCreate: FieldMetadataEntity[] = [];
const fieldsToDelete: FieldMetadataEntity[] = [];
const fieldsToUpdate: Record<string, FieldMetadataEntity> = {};
for (const standardObjectName in standardObjectsByName) {
const standardObject = standardObjectsByName[standardObjectName];
const objectInDB = objectsInDBByName[standardObjectName];
if (!objectInDB) {
objectsToCreate.push(standardObject);
continue;
}
// Deconstruct fields and compare objects and fields independently
const { fields: objectInDBFields, ...objectInDBWithoutFields } =
objectInDB;
const { fields: standardObjectFields, ...standardObjectWithoutFields } =
standardObject;
const objectPropertiesToIgnore = [
'id',
'createdAt',
'updatedAt',
'labelIdentifierFieldMetadataId',
'imageIdentifierFieldMetadataId',
];
const objectDiffWithoutIgnoredProperties = filterIgnoredProperties(
objectInDBWithoutFields,
objectPropertiesToIgnore,
);
const fieldPropertiesToIgnore = [
'id',
'createdAt',
'updatedAt',
'objectMetadataId',
];
const objectInDBFieldsWithoutDefaultFields = Object.fromEntries(
Object.entries(objectInDBFields).map(([key, value]) => {
if (value === null || typeof value !== 'object') {
return [key, value];
}
return [key, filterIgnoredProperties(value, fieldPropertiesToIgnore)];
}),
);
// Compare objects
const objectDiff = diff(
objectDiffWithoutIgnoredProperties,
standardObjectWithoutFields,
);
// Compare fields
const fieldsDiff = diff(
objectInDBFieldsWithoutDefaultFields,
standardObjectFields,
);
for (const diff of objectDiff) {
// We only handle CHANGE here as REMOVE and CREATE are handled earlier.
if (diff.type === 'CHANGE') {
const property = diff.path[0];
objectsToUpdate[objectInDB.id] = {
...objectsToUpdate[objectInDB.id],
[property]: diff.value,
};
}
}
for (const diff of fieldsDiff) {
if (diff.type === 'CREATE') {
const fieldName = diff.path[0];
const fieldMetadata = standardObjectFields[fieldName];
fieldsToCreate.push(fieldMetadata);
}
if (diff.type === 'CHANGE') {
const fieldName = diff.path[0];
const property = diff.path[diff.path.length - 1];
const fieldMetadata = objectInDBFields[fieldName];
fieldsToUpdate[fieldMetadata.id] = {
...fieldsToUpdate[fieldMetadata.id],
[property]: diff.value,
};
}
if (diff.type === 'REMOVE') {
const fieldName = diff.path[0];
const fieldMetadata = objectInDBFields[fieldName];
fieldsToDelete.push(fieldMetadata);
}
}
// console.log(standardObjectName + ':objectDiff', objectDiff);
// console.log(standardObjectName + ':fieldsDiff', fieldsDiff);
}
// TODO: Sync relationMetadata
// NOTE: Relations are handled like any field during the diff, so we ignore the relationMetadata table
// during the diff as it depends on the 2 fieldMetadata that we will compare here.
// However we need to make sure the relationMetadata table is in sync with the fieldMetadata table.
// TODO: Use transactions
// CREATE OBJECTS
try {
await this.objectMetadataRepository.save(objectsToCreate);
// UPDATE OBJECTS, this is not optimal as we are running n queries here.
for (const [key, value] of Object.entries(objectsToUpdate)) {
await this.objectMetadataRepository.update(key, value);
}
// DELETE OBJECTS
if (objectsToDelete.length > 0) {
await this.objectMetadataRepository.delete(
objectsToDelete.map((object) => object.id),
);
}
// CREATE FIELDS
await this.fieldMetadataRepository.save(fieldsToCreate);
// UPDATE FIELDS
for (const [key, value] of Object.entries(fieldsToUpdate)) {
await this.fieldMetadataRepository.update(key, value);
}
// DELETE FIELDS
if (fieldsToDelete.length > 0) {
await this.fieldMetadataRepository.delete(
fieldsToDelete.map((field) => field.id),
);
}
} catch (e) {
console.error('Sync of standard objects failed with:', e);
}
// TODO: Create migrations based on diff from above.
await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId);
}
/**
@ -451,7 +105,6 @@ export class WorkspaceManagerService {
private async prefillWorkspaceWithStandardObjects(
dataSourceMetadata: DataSourceEntity,
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity[],
) {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
@ -461,6 +114,10 @@ export class WorkspaceManagerService {
if (!workspaceDataSource) {
throw new Error('Could not connect to workspace data source');
}
const createdObjectMetadata =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
await standardObjectsPrefillData(
workspaceDataSource,
dataSourceMetadata.schema,
@ -478,7 +135,6 @@ export class WorkspaceManagerService {
private async prefillWorkspaceWithDemoObjects(
dataSourceMetadata: DataSourceEntity,
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity[],
) {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
@ -489,6 +145,9 @@ export class WorkspaceManagerService {
throw new Error('Could not connect to workspace data source');
}
const createdObjectMetadata =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
await demoObjectsPrefillData(
workspaceDataSource,
dataSourceMetadata.schema,

View File

@ -1,7 +1,7 @@
import { Command, CommandRunner, Option } from 'nest-commander';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
// TODO: implement dry-run
interface RunWorkspaceMigrationsOptions {
@ -14,7 +14,7 @@ interface RunWorkspaceMigrationsOptions {
})
export class SyncWorkspaceMetadataCommand extends CommandRunner {
constructor(
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly dataSourceService: DataSourceService,
) {
super();
@ -29,9 +29,7 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner {
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
options.workspaceId,
);
// TODO: This solution could be improved, using a diff for example, we should not have to delete all metadata and recreate them.
await this.workspaceManagerService.syncStandardObjectsAndFieldsMetadata(
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata(
dataSourceMetadata.id,
options.workspaceId,
);

View File

@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspace-manager.module';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/worksapce-sync-metadata.module';
import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command';
@Module({
imports: [WorkspaceManagerModule, DataSourceModule],
imports: [WorkspaceSyncMetadataModule, DataSourceModule],
providers: [SyncWorkspaceMetadataCommand],
})
export class WorkspaceManagerCommandsModule {}
export class WorkspaceSyncMetadataCommandsModule {}

View File

@ -0,0 +1,183 @@
import camelCase from 'lodash.camelcase';
import 'reflect-metadata';
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { generateDefaultValue } from 'src/metadata/field-metadata/utils/generate-default-value';
export type FieldMetadataDecorator = {
type: FieldMetadataType;
label: string;
description?: string | null;
icon?: string | null;
defaultValue?: FieldMetadataDefaultValue | null;
joinColumn?: string;
};
export type ObjectMetadataDecorator = {
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string | null;
icon?: string | null;
};
export type RelationMetadataDecorator = {
type: RelationMetadataType;
objectName: string;
inverseSideFieldName?: string;
};
function convertClassNameToObjectMetadataName(name: string): string {
const classSuffix = 'ObjectMetadata';
let objectName = camelCase(name);
if (objectName.endsWith(classSuffix)) {
objectName = objectName.slice(0, -classSuffix.length);
}
return objectName;
}
export function ObjectMetadata(
metadata: ObjectMetadataDecorator,
): ClassDecorator {
return (target) => {
const isSystem = Reflect.getMetadata('isSystem', target) || false;
const objectName = convertClassNameToObjectMetadataName(target.name);
Reflect.defineMetadata(
'objectMetadata',
{
nameSingular: objectName,
...metadata,
targetTableName: objectName,
isSystem,
isCustom: false,
description: metadata.description ?? null,
icon: metadata.icon ?? null,
},
target,
);
};
}
export function IsNullable() {
return function (target: object, fieldKey: string) {
Reflect.defineMetadata('isNullable', true, target, fieldKey);
};
}
export function IsSystem() {
return function (target: object, fieldKey?: string) {
if (fieldKey) {
Reflect.defineMetadata('isSystem', true, target, fieldKey);
} else {
Reflect.defineMetadata('isSystem', true, target);
}
};
}
export function FieldMetadata(
metadata: FieldMetadataDecorator,
): PropertyDecorator {
return (target: object, fieldKey: string) => {
const existingFieldMetadata =
Reflect.getMetadata('fieldMetadata', target.constructor) || {};
const isNullable =
Reflect.getMetadata('isNullable', target, fieldKey) || false;
const isSystem = Reflect.getMetadata('isSystem', target, fieldKey) || false;
const { joinColumn, ...fieldMetadata } = metadata;
Reflect.defineMetadata(
'fieldMetadata',
{
...existingFieldMetadata,
[fieldKey]: generateFieldMetadata(
fieldMetadata,
fieldKey,
isNullable,
isSystem,
),
...(joinColumn && fieldMetadata.type === FieldMetadataType.RELATION
? {
[joinColumn]: generateFieldMetadata(
{
...fieldMetadata,
type: FieldMetadataType.UUID,
label: `${fieldMetadata.label} id (foreign key)`,
description: `${fieldMetadata.description} id foreign key`,
defaultValue: null,
},
joinColumn,
isNullable,
true,
),
}
: {}),
},
target.constructor,
);
};
}
function generateFieldMetadata(
metadata: FieldMetadataDecorator,
fieldKey: string,
isNullable: boolean,
isSystem: boolean,
) {
const targetColumnMap = JSON.stringify(
generateTargetColumnMap(metadata.type, false, fieldKey),
);
const defaultValue =
metadata.defaultValue ?? generateDefaultValue(metadata.type);
return {
name: fieldKey,
...metadata,
targetColumnMap: targetColumnMap,
isNullable,
isSystem,
isCustom: false,
options: null, // TODO: handle options + stringify for the diff.
description: metadata.description ?? null,
icon: metadata.icon ?? null,
defaultValue: defaultValue ? JSON.stringify(defaultValue) : null,
};
}
export function RelationMetadata(
metadata: RelationMetadataDecorator,
): PropertyDecorator {
return (target: object, fieldKey: string) => {
const existingRelationMetadata =
Reflect.getMetadata('relationMetadata', target.constructor) || [];
const objectName = convertClassNameToObjectMetadataName(
target.constructor.name,
);
Reflect.defineMetadata(
'relationMetadata',
[
...existingRelationMetadata,
{
type: metadata.type,
fromObjectNameSingular: objectName,
toObjectNameSingular: metadata.objectName,
fromFieldMetadataName: fieldKey,
toFieldMetadataName: metadata.inverseSideFieldName ?? objectName,
},
],
target.constructor,
);
};
}

View File

@ -0,0 +1,47 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
FieldMetadata,
IsSystem,
IsNullable,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'activityTargets',
labelSingular: 'Activity Target',
labelPlural: 'Activity Targets',
description: 'An activity target',
icon: 'IconCheckbox',
})
@IsSystem()
export class ActivityTargetObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Activity',
description: 'ActivityTarget activity',
icon: 'IconNotes',
joinColumn: 'activityId',
})
activity: object;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Person',
description: 'ActivityTarget person',
icon: 'IconUser',
joinColumn: 'personId',
})
@IsNullable()
person: object;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Company',
description: 'ActivityTarget company',
icon: 'IconBuildingSkyscraper',
joinColumn: 'companyId',
})
@IsNullable()
company: object;
}

View File

@ -0,0 +1,128 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import {
ObjectMetadata,
IsSystem,
IsNullable,
FieldMetadata,
RelationMetadata,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'activities',
labelSingular: 'Activity',
labelPlural: 'Activities',
description: 'An activity',
icon: 'IconCheckbox',
})
@IsSystem()
export class ActivityObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Title',
description: 'Activity title',
icon: 'IconNotes',
})
@IsNullable()
title: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Body',
description: 'Activity body',
icon: 'IconList',
})
@IsNullable()
body: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Type',
description: 'Activity type',
icon: 'IconCheckbox',
defaultValue: { value: 'Note' },
})
type: string;
@FieldMetadata({
type: FieldMetadataType.DATE_TIME,
label: 'Reminder Date',
description: 'Activity reminder date',
icon: 'IconCalendarEvent',
})
@IsNullable()
reminderAt: Date;
@FieldMetadata({
type: FieldMetadataType.DATE_TIME,
label: 'Due Date',
description: 'Activity due date',
icon: 'IconCalendarEvent',
})
@IsNullable()
dueAt: Date;
@FieldMetadata({
type: FieldMetadataType.DATE_TIME,
label: 'Completion Date',
description: 'Activity completion date',
icon: 'IconCheck',
})
@IsNullable()
completedAt: Date;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Targets',
description: 'Activity targets',
icon: 'IconCheckbox',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activityTarget',
})
activityTargets: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Attachments',
description: 'Activity attachments',
icon: 'IconFileImport',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment',
})
attachments: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Comments',
description: 'Activity comments',
icon: 'IconComment',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'comment',
})
comments: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Author',
description: 'Activity author',
icon: 'IconUserCircle',
joinColumn: 'authorId',
})
author: object;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Assignee',
description: 'Acitivity assignee',
icon: 'IconUserCircle',
joinColumn: 'assigneeId',
})
assignee: object;
}

View File

@ -1,17 +1,17 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
FieldMetadata,
IsNullable,
IsSystem,
ObjectMetadata,
} from 'src/workspace/workspace-manager/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/base.object-metadata';
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'apiKeys',
labelSingular: 'Api Key',
labelPlural: 'Api Keys',
description: 'A api key',
description: 'An api key',
icon: 'IconRobot',
})
@IsSystem()

View File

@ -0,0 +1,80 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
IsSystem,
FieldMetadata,
IsNullable,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'attachments',
labelSingular: 'Attachment',
labelPlural: 'Attachments',
description: 'An attachment',
icon: 'IconFileImport',
})
@IsSystem()
export class AttachmentObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Name',
description: 'Attachment name',
icon: 'IconFileUpload',
})
name: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Full path',
description: 'Attachment full path',
icon: 'IconLink',
})
fullPath: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Type',
description: 'Attachment type',
icon: 'IconList',
})
type: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Author',
description: 'Attachment author',
icon: 'IconCircleUser',
joinColumn: 'authorId',
})
author: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Activity',
description: 'Attachment activity',
icon: 'IconNotes',
joinColumn: 'activityId',
})
activity: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Person',
description: 'Attachment person',
icon: 'IconUser',
joinColumn: 'personId',
})
@IsNullable()
person: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Company',
description: 'Attachment company',
icon: 'IconBuildingSkyscraper',
joinColumn: 'companyId',
})
@IsNullable()
company: string;
}

View File

@ -2,7 +2,7 @@ import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.en
import {
FieldMetadata,
IsSystem,
} from 'src/workspace/workspace-manager/decorators/metadata.decorator';
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
export abstract class BaseObjectMetadata {
@FieldMetadata({
@ -22,7 +22,6 @@ export abstract class BaseObjectMetadata {
icon: 'IconCalendar',
defaultValue: { type: 'now' },
})
@IsSystem()
createdAt: Date;
@FieldMetadata({

View File

@ -0,0 +1,44 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
IsSystem,
FieldMetadata,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'comments',
labelSingular: 'Comment',
labelPlural: 'Comments',
description: 'A comment',
icon: 'IconMessageCircle',
})
@IsSystem()
export class CommentObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Body',
description: 'Comment body',
icon: 'IconLink',
defaultValue: { value: '' },
})
body: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Author',
description: 'Comment author',
icon: 'IconCircleUser',
joinColumn: 'authorId',
})
author: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Activity',
description: 'Comment activity',
icon: 'IconNotes',
joinColumn: 'activityId',
})
activity: string;
}

View File

@ -0,0 +1,164 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import {
ObjectMetadata,
FieldMetadata,
IsNullable,
RelationMetadata,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'companies',
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'A company',
icon: 'IconBuildingSkyscraper',
})
export class CompanyObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Name',
description: 'The company name',
icon: 'IconBuildingSkyscraper',
})
name: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Domain Name',
description:
'The company website URL. We use this url to fetch the company icon',
icon: 'IconLink',
})
@IsNullable()
domainName?: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Address',
description: 'The company address',
icon: 'IconMap',
})
@IsNullable()
address: string;
@FieldMetadata({
type: FieldMetadataType.NUMBER,
label: 'Employees',
description: 'Number of employees in the company',
icon: 'IconUsers',
})
@IsNullable()
employees: number;
@FieldMetadata({
type: FieldMetadataType.LINK,
label: 'Linkedin',
description: 'The company Linkedin account',
icon: 'IconBrandLinkedin',
})
@IsNullable()
linkedinLink: string;
@FieldMetadata({
type: FieldMetadataType.LINK,
label: 'X',
description: 'The company Twitter/X account',
icon: 'IconBrandX',
})
@IsNullable()
xLink: string;
@FieldMetadata({
type: FieldMetadataType.CURRENCY,
label: 'ARR',
description:
'Annual Recurring Revenue: The actual or estimated annual revenue of the company',
icon: 'IconMoneybag',
})
@IsNullable()
annualRecurringRevenue: number;
@FieldMetadata({
type: FieldMetadataType.BOOLEAN,
label: 'ICP',
description:
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
icon: 'IconTarget',
})
@IsNullable()
idealCustomerProfile: boolean;
// Relations
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'People',
description: 'People linked to the company.',
icon: 'IconUsers',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'person',
})
people: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Account Owner',
description:
'Your team member responsible for managing the company account',
icon: 'IconUserCircle',
joinColumn: 'accountOwnerId',
})
@IsNullable()
accountOwner: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Activities',
description: 'Activities tied to the company',
icon: 'IconCheckbox',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activityTarget',
})
activityTargets: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Opportunities',
description: 'Opportunities linked to the company.',
icon: 'IconTargetArrow',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'opportunity',
})
opportunities: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Favorites',
description: 'Favorites linked to the company',
icon: 'IconHeart',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'favorite',
})
favorites: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Attachments',
description: 'Attachments linked to the company.',
icon: 'IconFileImport',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment',
})
attachments: object[];
}

View File

@ -0,0 +1,54 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
IsSystem,
FieldMetadata,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'favorites',
labelSingular: 'Favorite',
labelPlural: 'Favorites',
description: 'A favorite',
icon: 'IconHeart',
})
@IsSystem()
export class FavoriteObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.NUMBER,
label: 'Position',
description: 'Favorite position',
icon: 'IconList',
defaultValue: { value: 0 },
})
position: number;
// Relations
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Workspace Member',
description: 'Favorite workspace member',
icon: 'IconCircleUser',
joinColumn: 'workspaceMemberId',
})
workspaceMember: object;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Person',
description: 'Favorite person',
icon: 'IconUser',
joinColumn: 'personId',
})
person: object;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Company',
description: 'Favorite company',
icon: 'IconBuildingSkyscraper',
joinColumn: 'companyId',
})
company: object;
}

View File

@ -0,0 +1,35 @@
import { ActivityTargetObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata';
import { ActivityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity.object-metadata';
import { ApiKeyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/api-key.object-metadata';
import { AttachmentObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/attachment.object-metadata';
import { CommentObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/comment.object-metadata';
import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata';
import { FavoriteObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata';
import { OpportunityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata';
import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata';
import { PipelineStepObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata';
import { ViewFieldObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/view-field.object-metadata';
import { ViewFilterObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/view-filter.object-metadata';
import { ViewSortObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/view-sort.object-metadata';
import { ViewObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/view.object-metadata';
import { WebhookObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/webook.object-metadata';
import { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata';
export const standardObjectMetadata = [
ActivityTargetObjectMetadata,
ActivityObjectMetadata,
ApiKeyObjectMetadata,
AttachmentObjectMetadata,
CommentObjectMetadata,
CompanyObjectMetadata,
FavoriteObjectMetadata,
OpportunityObjectMetadata,
PersonObjectMetadata,
PipelineStepObjectMetadata,
ViewFieldObjectMetadata,
ViewFilterObjectMetadata,
ViewSortObjectMetadata,
ViewObjectMetadata,
WebhookObjectMetadata,
WorkspaceMemberObjectMetadata,
];

View File

@ -0,0 +1,85 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
IsSystem,
FieldMetadata,
IsNullable,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'opportunities',
labelSingular: 'Opportunity',
labelPlural: 'Opportunities',
description: 'An opportunity',
icon: 'IconTargetArrow',
})
export class OpportunityObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.CURRENCY,
label: 'Amount',
description: 'Opportunity amount',
icon: 'IconCurrencyDollar',
})
@IsNullable()
amount: string;
@FieldMetadata({
type: FieldMetadataType.DATE_TIME,
label: 'Close date',
description: 'Opportunity close date',
icon: 'IconCalendarEvent',
})
@IsNullable()
closeDate: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Probability',
description: 'Opportunity probability',
icon: 'IconProgressCheck',
defaultValue: { value: '0' },
})
@IsNullable()
probability: string;
// Relations
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Pipeline Step',
description: 'Opportunity pipeline step',
icon: 'IconKanban',
joinColumn: 'pipelineStepId',
})
@IsNullable()
pipelineStep: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Point of Contact',
description: 'Opportunity point of contact',
icon: 'IconUser',
joinColumn: 'pointOfContactId',
})
@IsNullable()
pointOfContact: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Person',
description: 'Opportunity person',
icon: 'IconUser',
joinColumn: 'personId',
})
person: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Company',
description: 'Opportunity company',
icon: 'IconBuildingSkyscraper',
joinColumn: 'companyId',
})
@IsNullable()
company: string;
}

View File

@ -0,0 +1,164 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import {
ObjectMetadata,
FieldMetadata,
IsNullable,
RelationMetadata,
IsSystem,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
description: 'A person',
icon: 'IconUser',
})
export class PersonObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.FULL_NAME,
label: 'Name',
description: 'Contacts name',
icon: 'IconUser',
})
@IsNullable()
name: string;
@FieldMetadata({
type: FieldMetadataType.EMAIL,
label: 'Email',
description: 'Contacts Email',
icon: 'IconMail',
})
@IsNullable()
email: string;
@FieldMetadata({
type: FieldMetadataType.LINK,
label: 'Linkedin',
description: 'Contacts Linkedin account',
icon: 'IconBrandLinkedin',
})
@IsNullable()
linkedinLink: string;
@FieldMetadata({
type: FieldMetadataType.LINK,
label: 'X',
description: 'Contacts X/Twitter account',
icon: 'IconBrandX',
})
@IsNullable()
xLink: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Job Title',
description: 'Contacts job title',
icon: 'IconBriefcase',
})
@IsNullable()
jobTitle: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Phone',
description: 'Contacts phone number',
icon: 'IconPhone',
})
@IsNullable()
phone: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'City',
description: 'Contacts city',
icon: 'IconMap',
})
@IsNullable()
city: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Avatar',
description: 'Contacts avatar',
icon: 'IconFileUpload',
})
@IsSystem()
@IsNullable()
avatarUrl: string;
// Relations
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Company',
description: 'Contacts company',
icon: 'IconBuildingSkyscraper',
joinColumn: 'companyId',
})
@IsNullable()
company: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'POC for Opportunities',
description: 'Point of Contact for Opportunities',
icon: 'IconTargetArrow',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'opportunity',
inverseSideFieldName: 'pointOfContact',
})
pointOfContactForOpportunities: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Activities',
description: 'Activities tied to the contact',
icon: 'IconCheckbox',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activityTarget',
})
activityTargets: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Opportunities',
description: 'Opportunities linked to the contact.',
icon: 'IconTargetArrow',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'opportunity',
})
opportunities: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Favorites',
description: 'Favorites linked to the contact',
icon: 'IconHeart',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'favorite',
})
favorites: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Attachments',
description: 'Attachments linked to the contact.',
icon: 'IconFileImport',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment',
})
attachments: object[];
}

View File

@ -0,0 +1,62 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import {
ObjectMetadata,
FieldMetadata,
IsNullable,
IsSystem,
RelationMetadata,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'pipelineSteps',
labelSingular: 'Pipeline Step',
labelPlural: 'Pipeline Steps',
description: 'A pipeline step',
icon: 'IconLayoutKanban',
})
@IsSystem()
export class PipelineStepObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Name',
description: 'Pipeline Step name',
icon: 'IconCurrencyDollar',
})
@IsNullable()
name: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Color',
description: 'Pipeline Step color',
icon: 'IconColorSwatch',
})
@IsNullable()
color: string;
@FieldMetadata({
type: FieldMetadataType.NUMBER,
label: 'Position',
description: 'Pipeline Step position',
icon: 'IconHierarchy2',
defaultValue: { value: 0 },
})
@IsNullable()
position: number;
// Relations
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Opportunities',
description: 'Opportunities linked to the step.',
icon: 'IconTargetArrow',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'opportunity',
})
@IsNullable()
opportunities: object[];
}

View File

@ -0,0 +1,63 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
IsSystem,
FieldMetadata,
IsNullable,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'viewFields',
labelSingular: 'View Field',
labelPlural: 'View Fields',
description: '(System) View Fields',
icon: 'IconTag',
})
@IsSystem()
export class ViewFieldObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.UUID,
label: 'Field Metadata Id',
description: 'View Field target field',
icon: 'IconTag',
})
fieldMetadataId: string;
@FieldMetadata({
type: FieldMetadataType.BOOLEAN,
label: 'Visible',
description: 'View Field visibility',
icon: 'IconEye',
defaultValue: { value: true },
})
isVisible: boolean;
@FieldMetadata({
type: FieldMetadataType.NUMBER,
label: 'Size',
description: 'View Field size',
icon: 'IconEye',
defaultValue: { value: 0 },
})
size: number;
@FieldMetadata({
type: FieldMetadataType.NUMBER,
label: 'Position',
description: 'View Field position',
icon: 'IconList',
defaultValue: { value: 0 },
})
position: number;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'View',
description: 'View Field related view',
icon: 'IconLayoutCollage',
joinColumn: 'viewId',
})
@IsNullable()
view?: object;
}

View File

@ -0,0 +1,63 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
ObjectMetadata,
IsSystem,
FieldMetadata,
IsNullable,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'viewFilters',
labelSingular: 'View Filter',
labelPlural: 'View Filters',
description: '(System) View Filters',
icon: 'IconFilterBolt',
})
@IsSystem()
export class ViewFilterObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.UUID,
label: 'Field Metadata Id',
description: 'View Filter target field',
icon: null,
})
fieldMetadataId: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Operand',
description: 'View Filter operand',
icon: null,
defaultValue: { value: 'Contains' },
})
operand: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Value',
description: 'View Filter value',
icon: null,
defaultValue: { value: '' },
})
value: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Display Value',
description: 'View Filter Display Value',
icon: null,
defaultValue: { value: '' },
})
displayValue: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'View',
description: 'View Filter related view',
icon: 'IconLayoutCollage',
joinColumn: 'viewId',
})
@IsNullable()
view: string;
}

View File

@ -4,8 +4,8 @@ import {
FieldMetadata,
IsNullable,
IsSystem,
} from 'src/workspace/workspace-manager/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/base.object-metadata';
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'viewSorts',
@ -20,17 +20,26 @@ export class ViewSortObjectMetadata extends BaseObjectMetadata {
type: FieldMetadataType.UUID,
label: 'Field Metadata Id',
description: 'View Sort target field',
icon: null,
icon: 'IconTag',
})
fieldMetadataId: string;
// TODO: We could create a relation decorator but let's keep it simple for now.
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Direction',
description: 'View Sort direction',
icon: null,
defaultValue: { value: 'asc' },
})
direction: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'View',
description: 'View Sort related view',
icon: 'IconLayoutCollage',
joinColumn: 'viewId',
})
@IsNullable()
view?: object;
view: string;
}

View File

@ -0,0 +1,81 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import {
ObjectMetadata,
IsSystem,
FieldMetadata,
RelationMetadata,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'views',
labelSingular: 'View',
labelPlural: 'Views',
description: '(System) Views',
icon: 'IconLayoutCollage',
})
@IsSystem()
export class ViewObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Name',
description: 'View name',
icon: null,
defaultValue: { value: '' },
})
name: string;
@FieldMetadata({
type: FieldMetadataType.UUID,
label: 'Object Metadata Id',
description: 'View target object',
icon: null,
})
objectMetadataId: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Type',
description: 'View type',
icon: null,
defaultValue: { value: 'table' },
})
type: string;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'View Fields',
description: 'View Fields',
icon: 'IconTag',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'viewField',
})
viewFields: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'View Filters',
description: 'View Filters',
icon: 'IconFilterBolt',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'viewFilter',
})
viewFilters: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'View Sorts',
description: 'View Sorts',
icon: 'IconArrowsSort',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'viewSort',
})
viewSorts: object[];
}

View File

@ -1,10 +1,10 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import {
FieldMetadata,
IsSystem,
ObjectMetadata,
} from 'src/workspace/workspace-manager/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-manager/standard-objects/base.object-metadata';
IsSystem,
FieldMetadata,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'webhooks',

View File

@ -0,0 +1,142 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
import {
ObjectMetadata,
IsSystem,
FieldMetadata,
IsNullable,
RelationMetadata,
} from 'src/workspace/workspace-sync-metadata/decorators/metadata.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
@ObjectMetadata({
namePlural: 'workspaceMembers',
labelSingular: 'Workspace Member',
labelPlural: 'Workspace Members',
description: 'A workspace member',
icon: 'IconUserCircle',
})
@IsSystem()
export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
type: FieldMetadataType.FULL_NAME,
label: 'Name',
description: 'Workspace member name',
icon: 'IconCircleUser',
})
name: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Color Scheme',
description: 'Preferred color scheme',
icon: 'IconColorSwatch',
defaultValue: { value: 'Light' },
})
colorScheme: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Language',
description: 'Preferred language',
icon: 'IconLanguage',
defaultValue: { value: 'en' },
})
locale: string;
@FieldMetadata({
type: FieldMetadataType.TEXT,
label: 'Avatar Url',
description: 'Workspace member avatar',
icon: 'IconFileUpload',
defaultValue: { value: '' },
})
@IsNullable()
avatarUrl: string;
@FieldMetadata({
type: FieldMetadataType.UUID,
label: 'User Id',
description: 'Associated User Id',
icon: 'IconCircleUsers',
})
userId: string;
// Relations
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Authored activities',
description: 'Activities created by the workspace member',
icon: 'IconCheckbox',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activity',
inverseSideFieldName: 'author',
})
authoredActivities: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Assigned activities',
description: 'Activities assigned to the workspace member',
icon: 'IconCheckbox',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activity',
inverseSideFieldName: 'assignee',
})
assignedActivities: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Favorites',
description: 'Favorites linked to the workspace member',
icon: 'IconHeart',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'favorite',
})
favorites: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Account Owner For Companies',
description: 'Account owner for companies',
icon: 'IconBriefcase',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'company',
inverseSideFieldName: 'accountOwner',
})
accountOwnerForCompanies: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Authored attachments',
description: 'Attachments created by the workspace member',
icon: 'IconFileImport',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment',
inverseSideFieldName: 'author',
})
authoredAttachments: object[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Authored comments',
description: 'Authored comments',
icon: 'IconComment',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
objectName: 'comment',
inverseSideFieldName: 'author',
})
authoredComments: object[];
}

View File

@ -0,0 +1,109 @@
import assert from 'assert';
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
export class MetadataParser {
static parseMetadata(
metadata: typeof BaseObjectMetadata,
workspaceId: string,
dataSourceId: string,
) {
const objectMetadata = Reflect.getMetadata('objectMetadata', metadata);
const fieldMetadata = Reflect.getMetadata('fieldMetadata', metadata);
if (objectMetadata) {
const fields = Object.values(fieldMetadata);
return {
...objectMetadata,
workspaceId,
dataSourceId,
fields: fields.map((field: FieldMetadataEntity) => ({
...field,
workspaceId,
isSystem: objectMetadata.isSystem || field.isSystem,
defaultValue: field.defaultValue || null,
options: field.options || null,
})),
};
}
return undefined;
}
static parseAllMetadata(
metadata: (typeof BaseObjectMetadata)[],
workspaceId: string,
dataSourceId: string,
) {
return metadata.map((_metadata) =>
MetadataParser.parseMetadata(_metadata, workspaceId, dataSourceId),
);
}
static parseRelationMetadata(
metadata: typeof BaseObjectMetadata,
workspaceId: string,
objectMetadataFromDB: Record<string, ObjectMetadataEntity>,
) {
const objectMetadata = Reflect.getMetadata('objectMetadata', metadata);
const relationMetadata = Reflect.getMetadata('relationMetadata', metadata);
if (!relationMetadata) return [];
return relationMetadata.map((relation) => {
const fromObjectMetadata =
objectMetadataFromDB[relation.fromObjectNameSingular];
assert(
fromObjectMetadata,
`Object ${relation.fromObjectNameSingular} not found in DB
for relation defined in class ${objectMetadata.nameSingular}`,
);
const toObjectMetadata =
objectMetadataFromDB[relation.toObjectNameSingular];
assert(
toObjectMetadata,
`Object ${relation.toObjectNameSingular} not found in DB
for relation defined in class ${objectMetadata.nameSingular}`,
);
const fromFieldMetadata =
fromObjectMetadata?.fields[relation.fromFieldMetadataName];
assert(
fromFieldMetadata,
`Field ${relation.fromFieldMetadataName} not found in object ${relation.fromObjectNameSingular}
for relation defined in class ${objectMetadata.nameSingular}`,
);
const toFieldMetadata =
toObjectMetadata?.fields[relation.toFieldMetadataName];
assert(
toFieldMetadata,
`Field ${relation.toFieldMetadataName} not found in object ${relation.toObjectNameSingular}
for relation defined in class ${objectMetadata.nameSingular}`,
);
return {
relationType: relation.type,
fromObjectMetadataId: fromObjectMetadata?.id,
toObjectMetadataId: toObjectMetadata?.id,
fromFieldMetadataId: fromFieldMetadata?.id,
toFieldMetadataId: toFieldMetadata?.id,
workspaceId,
};
});
}
static parseAllRelations(
metadata: (typeof BaseObjectMetadata)[],
workspaceId: string,
objectMetadataFromDB: Record<string, ObjectMetadataEntity>,
) {
return metadata.flatMap((_metadata) =>
MetadataParser.parseRelationMetadata(
_metadata,
workspaceId,
objectMetadataFromDB,
),
);
}
}

View File

@ -11,9 +11,12 @@ import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metada
export const filterIgnoredProperties = (
obj: any,
propertiesToIgnore: string[],
mapFunction?: (value: any) => any,
) => {
return Object.fromEntries(
Object.entries(obj).filter(([key]) => !propertiesToIgnore.includes(key)),
Object.entries(obj)
.filter(([key]) => !propertiesToIgnore.includes(key))
.map(([key, value]) => [key, mapFunction ? mapFunction(value) : value]),
);
};
@ -41,3 +44,22 @@ export const mapObjectMetadataByUniqueIdentifier = (
return acc;
}, {});
};
export const convertStringifiedFieldsToJSON = (fieldMetadata) => {
if (fieldMetadata.targetColumnMap) {
fieldMetadata.targetColumnMap = JSON.parse(
fieldMetadata.targetColumnMap as unknown as string,
);
}
if (fieldMetadata.defaultValue) {
fieldMetadata.defaultValue = JSON.parse(
fieldMetadata.defaultValue as unknown as string,
);
}
if (fieldMetadata.options) {
fieldMetadata.options = JSON.parse(
fieldMetadata.options as unknown as string,
);
}
return fieldMetadata;
};

View File

@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync.metadata.service';
@Module({
imports: [
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
TypeOrmModule.forFeature(
[
FieldMetadataEntity,
ObjectMetadataEntity,
RelationMetadataEntity,
WorkspaceMigrationEntity,
],
'metadata',
),
],
exports: [WorkspaceSyncMetadataService],
providers: [WorkspaceSyncMetadataService],
})
export class WorkspaceSyncMetadataModule {}

View File

@ -0,0 +1,435 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import diff from 'microdiff';
import { Repository } from 'typeorm';
import camelCase from 'lodash.camelcase';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import {
RelationMetadataEntity,
RelationMetadataType,
} from 'src/metadata/relation-metadata/relation-metadata.entity';
import { MetadataParser } from 'src/workspace/workspace-sync-metadata/utils/metadata.parser';
import {
mapObjectMetadataByUniqueIdentifier,
filterIgnoredProperties,
convertStringifiedFieldsToJSON,
} from 'src/workspace/workspace-sync-metadata/utils/sync-metadata.util';
import { standardObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnRelation,
WorkspaceMigrationEntity,
WorkspaceMigrationTableAction,
} from 'src/metadata/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationFactory } from 'src/metadata/workspace-migration/workspace-migration.factory';
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
@Injectable()
export class WorkspaceSyncMetadataService {
constructor(
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(RelationMetadataEntity, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
@InjectRepository(WorkspaceMigrationEntity, 'metadata')
private readonly workspaceMigrationRepository: Repository<WorkspaceMigrationEntity>,
) {}
/**
*
* Sync all standard objects and fields metadata for a given workspace and data source
* This will update the metadata if it has changed and generate migrations based on the diff.
*
* @param dataSourceId
* @param workspaceId
*/
public async syncStandardObjectsAndFieldsMetadata(
dataSourceId: string,
workspaceId: string,
) {
const standardObjects = MetadataParser.parseAllMetadata(
standardObjectMetadata,
workspaceId,
dataSourceId,
);
try {
const objectsInDB = await this.objectMetadataRepository.find({
where: { workspaceId, dataSourceId, isCustom: false },
relations: ['fields'],
});
const objectsInDBByName =
mapObjectMetadataByUniqueIdentifier(objectsInDB);
const standardObjectsByName =
mapObjectMetadataByUniqueIdentifier(standardObjects);
const objectsToCreate: ObjectMetadataEntity[] = [];
const objectsToDelete = objectsInDB.filter(
(objectInDB) => !standardObjectsByName[objectInDB.nameSingular],
);
const objectsToUpdate: Record<string, ObjectMetadataEntity> = {};
const fieldsToCreate: FieldMetadataEntity[] = [];
const fieldsToDelete: FieldMetadataEntity[] = [];
const fieldsToUpdate: Record<string, FieldMetadataEntity> = {};
for (const standardObjectName in standardObjectsByName) {
const standardObject = standardObjectsByName[standardObjectName];
const objectInDB = objectsInDBByName[standardObjectName];
if (!objectInDB) {
objectsToCreate.push(standardObject);
continue;
}
// Deconstruct fields and compare objects and fields independently
const { fields: objectInDBFields, ...objectInDBWithoutFields } =
objectInDB;
const { fields: standardObjectFields, ...standardObjectWithoutFields } =
standardObject;
const objectPropertiesToIgnore = [
'id',
'createdAt',
'updatedAt',
'labelIdentifierFieldMetadataId',
'imageIdentifierFieldMetadataId',
'isActive',
];
const objectDiffWithoutIgnoredProperties = filterIgnoredProperties(
objectInDBWithoutFields,
objectPropertiesToIgnore,
);
const fieldPropertiesToIgnore = [
'id',
'createdAt',
'updatedAt',
'objectMetadataId',
'isActive',
];
const objectInDBFieldsWithoutDefaultFields = Object.fromEntries(
Object.entries(objectInDBFields).map(([key, value]) => {
if (value === null || typeof value !== 'object') {
return [key, value];
}
return [
key,
filterIgnoredProperties(
value,
fieldPropertiesToIgnore,
(property) => {
if (property !== null && typeof property === 'object') {
return JSON.stringify(property);
}
return property;
},
),
];
}),
);
// Compare objects
const objectDiff = diff(
objectDiffWithoutIgnoredProperties,
standardObjectWithoutFields,
);
// Compare fields
const fieldsDiff = diff(
objectInDBFieldsWithoutDefaultFields,
standardObjectFields,
);
for (const diff of objectDiff) {
// We only handle CHANGE here as REMOVE and CREATE are handled earlier.
if (diff.type === 'CHANGE') {
const property = diff.path[0];
objectsToUpdate[objectInDB.id] = {
...objectsToUpdate[objectInDB.id],
[property]: diff.value,
};
}
}
for (const diff of fieldsDiff) {
const fieldName = diff.path[0];
if (diff.type === 'CREATE')
fieldsToCreate.push({
...standardObjectFields[fieldName],
objectMetadataId: objectInDB.id,
});
if (diff.type === 'REMOVE' && diff.path.length === 1)
fieldsToDelete.push(objectInDBFields[fieldName]);
if (diff.type === 'CHANGE') {
const property = diff.path[diff.path.length - 1];
fieldsToUpdate[objectInDBFields[fieldName].id] = {
...fieldsToUpdate[objectInDBFields[fieldName].id],
[property]: diff.value,
};
}
}
}
// CREATE OBJECTS
await this.objectMetadataRepository.save(
objectsToCreate.map((object) => ({
...object,
isActive: true,
fields: Object.values(object.fields).map((field) => ({
...convertStringifiedFieldsToJSON(field),
isActive: true,
})),
})),
);
// UPDATE OBJECTS, this is not optimal as we are running n queries here.
for (const [key, value] of Object.entries(objectsToUpdate)) {
await this.objectMetadataRepository.update(key, value);
}
// DELETE OBJECTS
if (objectsToDelete.length > 0) {
await this.objectMetadataRepository.delete(
objectsToDelete.map((object) => object.id),
);
}
// CREATE FIELDS
await this.fieldMetadataRepository.save(
fieldsToCreate.map((field) => convertStringifiedFieldsToJSON(field)),
);
// UPDATE FIELDS
for (const [key, value] of Object.entries(fieldsToUpdate)) {
await this.fieldMetadataRepository.update(
key,
convertStringifiedFieldsToJSON(value),
);
}
// DELETE FIELDS
// TODO: handle relation fields deletion. We need to delete the relation metadata first due to the DB constraint.
const fieldsToDeleteWithoutRelationType = fieldsToDelete.filter(
(field) => field.type !== FieldMetadataType.RELATION,
);
if (fieldsToDeleteWithoutRelationType.length > 0) {
await this.fieldMetadataRepository.delete(
fieldsToDeleteWithoutRelationType.map((field) => field.id),
);
}
// Generate migrations
await this.generateMigrationsFromSync(
objectsToCreate,
objectsToDelete,
fieldsToCreate,
fieldsToDelete,
);
// We run syncRelationMetadata after everything to ensure that all objects and fields are
// in the DB before creating relations.
await this.syncRelationMetadata(workspaceId, dataSourceId);
// Execute migrations
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
} catch (error) {
console.error('Sync of standard objects failed with:', error);
}
}
private async syncRelationMetadata(
workspaceId: string,
dataSourceId: string,
) {
const objectsInDB = await this.objectMetadataRepository.find({
where: { workspaceId, dataSourceId, isCustom: false },
relations: ['fields'],
});
const objectsInDBByName = mapObjectMetadataByUniqueIdentifier(objectsInDB);
const standardRelations = MetadataParser.parseAllRelations(
standardObjectMetadata,
workspaceId,
objectsInDBByName,
).reduce((result, currentObject) => {
const key = `${currentObject.fromObjectMetadataId}->${currentObject.fromFieldMetadataId}`;
result[key] = currentObject;
return result;
}, {});
// TODO: filter out custom relations once isCustom has been added to relationMetadata table
const relationsInDB = await this.relationMetadataRepository.find({
where: { workspaceId },
});
// We filter out 'id' later because we need it to remove the relation from DB
const relationsInDBWithoutIgnoredProperties = relationsInDB
.map((relation) =>
filterIgnoredProperties(relation, ['createdAt', 'updatedAt']),
)
.reduce((result, currentObject) => {
const key = `${currentObject.fromObjectMetadataId}->${currentObject.fromFieldMetadataId}`;
result[key] = currentObject;
return result;
}, {});
// Compare relations
const relationsDiff = diff(
relationsInDBWithoutIgnoredProperties,
standardRelations,
);
const relationsToCreate: RelationMetadataEntity[] = [];
const relationsToDelete: RelationMetadataEntity[] = [];
for (const diff of relationsDiff) {
if (diff.type === 'CREATE') {
relationsToCreate.push(diff.value);
}
if (diff.type === 'REMOVE' && diff.path[diff.path.length - 1] !== 'id') {
relationsToDelete.push(diff.oldValue);
}
}
try {
// CREATE RELATIONS
await this.relationMetadataRepository.save(relationsToCreate);
// DELETE RELATIONS
if (relationsToDelete.length > 0) {
await this.relationMetadataRepository.delete(
relationsToDelete.map((relation) => relation.id),
);
}
await this.generateRelationMigrationsFromSync(
relationsToCreate,
relationsToDelete,
objectsInDB,
);
} catch (error) {
console.error('Sync of standard relations failed with:', error);
}
}
private async generateMigrationsFromSync(
objectsToCreate: ObjectMetadataEntity[],
_objectsToDelete: ObjectMetadataEntity[],
_fieldsToCreate: FieldMetadataEntity[],
_fieldsToDelete: FieldMetadataEntity[],
) {
const migrationsToSave: Partial<WorkspaceMigrationEntity>[] = [];
if (objectsToCreate.length > 0) {
objectsToCreate.map((object) => {
const migrations = [
{
name: object.targetTableName,
action: 'create',
} satisfies WorkspaceMigrationTableAction,
...Object.values(object.fields)
.filter((field) => field.type !== FieldMetadataType.RELATION)
.map(
(field) =>
({
name: object.targetTableName,
action: 'alter',
columns: this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.CREATE,
field,
),
} satisfies WorkspaceMigrationTableAction),
),
];
migrationsToSave.push({
workspaceId: object.workspaceId,
isCustom: false,
migrations,
});
});
}
await this.workspaceMigrationRepository.save(migrationsToSave);
// TODO: handle delete migrations
}
private async generateRelationMigrationsFromSync(
relationsToCreate: RelationMetadataEntity[],
_relationsToDelete: RelationMetadataEntity[],
objectsInDB: ObjectMetadataEntity[],
) {
const relationsMigrationsToSave: Partial<WorkspaceMigrationEntity>[] = [];
if (relationsToCreate.length > 0) {
relationsToCreate.map((relation) => {
const toObjectMetadata = objectsInDB.find(
(object) => object.id === relation.toObjectMetadataId,
);
const fromObjectMetadata = objectsInDB.find(
(object) => object.id === relation.fromObjectMetadataId,
);
if (!toObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${relation.toObjectMetadataId} not found`,
);
}
if (!fromObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${relation.fromObjectMetadataId} not found`,
);
}
const toFieldMetadata = toObjectMetadata.fields.find(
(field) => field.id === relation.toFieldMetadataId,
);
if (!toFieldMetadata) {
throw new Error(
`FieldMetadata with id ${relation.toFieldMetadataId} not found`,
);
}
const migrations = [
{
name: toObjectMetadata.targetTableName,
action: 'alter',
columns: [
{
action: WorkspaceMigrationColumnActionType.RELATION,
columnName: `${camelCase(toFieldMetadata.name)}Id`,
referencedTableName: fromObjectMetadata.targetTableName,
referencedTableColumnName: 'id',
isUnique:
relation.relationType === RelationMetadataType.ONE_TO_ONE,
} satisfies WorkspaceMigrationColumnRelation,
],
} satisfies WorkspaceMigrationTableAction,
];
relationsMigrationsToSave.push({
workspaceId: relation.workspaceId,
isCustom: false,
migrations,
});
});
}
await this.workspaceMigrationRepository.save(relationsMigrationsToSave);
// TODO: handle delete migrations
}
}