feat: standard fields on custom (#4332)

* feat: add ability to sync standard fields on custom object

* fix: clean

* fix: wrong compute during object creation

* fix: missing cascade delete

* fix: remove unused injected class

* fix: naming

* fix: rename factory to paramsFactory and clean

* fix: rename ExtendCustomObjectMetadata to BaseCustomObjectMetadata

* fix: partial fix inconsistent label and description

* Fixes

* Fix

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M 2024-03-07 17:21:50 +01:00 committed by GitHub
parent c3a024b047
commit af6ffbcc68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 923 additions and 250 deletions

View File

@ -42,10 +42,59 @@
}, },
], ],
"settings": { "settings": {
"editor.formatOnSave": false,
"files.eol": "auto",
"[typescript]": {
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.addMissingImports": "always"
}
},
"[javascript]": {
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.addMissingImports": "always"
}
},
"[typescriptreact]": {
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.addMissingImports": "always"
}
},
"[json]": {
"editor.formatOnSave": true
},
"javascript.format.enable": false,
"typescript.format.enable": false,
"cSpell.enableFiletypes": [
"!javascript",
"!json",
"!typescript",
"!typescriptreact",
"md",
"mdx"
],
"cSpell.words": [
"twentyhq"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"[javascript][typescript][typescriptreact]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.addMissingImports": "always"
}
},
"search.exclude": {
"**/.yarn": true,
},
"files.exclude": { "files.exclude": {
"packages/": true "packages/": true
}, },
"jest.autoEnable": false, "jest.runMode": "on-demand",
"jest.disabledWorkspaceFolders": [ "jest.disabledWorkspaceFolders": [
"ROOT", "ROOT",
"packages/twenty-zapier", "packages/twenty-zapier",

View File

@ -72,6 +72,7 @@ export const useInjectIntoActivityTargetsQueries = () => {
overwriteFindManyActivityTargetsQueryInCache({ overwriteFindManyActivityTargetsQueryInCache({
objectRecordsToOverwrite: newActivityTargets, objectRecordsToOverwrite: newActivityTargets,
queryVariables: findManyActivitiyTargetsQueryVariables, queryVariables: findManyActivitiyTargetsQueryVariables,
depth: 2,
}); });
}; };

View File

@ -63,6 +63,7 @@ export const useRemoveFromActivityTargetsQueries = () => {
overwriteFindManyActivityTargetsQueryInCache({ overwriteFindManyActivityTargetsQueryInCache({
objectRecordsToOverwrite: newActivityTargetsForTargetableObject, objectRecordsToOverwrite: newActivityTargetsForTargetableObject,
queryVariables: findManyActivityTargetsQueryVariables, queryVariables: findManyActivityTargetsQueryVariables,
depth: 2,
}); });
}; };

View File

@ -34,6 +34,7 @@ export const useInjectIntoActivityTargetInlineCellCache = () => {
overwriteFindManyActivityTargetsQueryInCache({ overwriteFindManyActivityTargetsQueryInCache({
queryVariables: activityTargetInlineCellQueryVariables, queryVariables: activityTargetInlineCellQueryVariables,
objectRecordsToOverwrite: activityTargetsToInject, objectRecordsToOverwrite: activityTargetsToInject,
depth: 2,
}); });
}; };

View File

@ -20,14 +20,16 @@ export const useUpsertFindManyRecordsQueryInCache = ({
T extends ObjectRecord = ObjectRecord, T extends ObjectRecord = ObjectRecord,
>({ >({
queryVariables, queryVariables,
depth = MAX_QUERY_DEPTH_FOR_CACHE_INJECTION,
objectRecordsToOverwrite, objectRecordsToOverwrite,
}: { }: {
queryVariables: ObjectRecordQueryVariables; queryVariables: ObjectRecordQueryVariables;
depth?: number;
objectRecordsToOverwrite: T[]; objectRecordsToOverwrite: T[];
}) => { }) => {
const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({ const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({
objectMetadataItem, objectMetadataItem,
depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, // TODO: fix this depth, // TODO: fix this
}); });
const newObjectRecordConnection = getRecordConnectionFromRecords({ const newObjectRecordConnection = getRecordConnectionFromRecords({

View File

@ -3,3 +3,6 @@ type DeepPartial<T> = {
? Array<DeepPartial<R>> ? Array<DeepPartial<R>>
: DeepPartial<T[K]>; : DeepPartial<T[K]>;
}; };
// eslint-disable-next-line @typescript-eslint/ban-types
type ExcludeFunctions<T> = T extends Function ? never : T;

View File

@ -28,6 +28,7 @@
"database:seed:dev": "npx nx command -- workspace:seed:dev", "database:seed:dev": "npx nx command -- workspace:seed:dev",
"database:seed:demo": "npx nx command -- workspace:seed:demo", "database:seed:demo": "npx nx command -- workspace:seed:demo",
"database:reset": "npx nx database:truncate && npx nx database:init", "database:reset": "npx nx database:truncate && npx nx database:init",
"command": "node dist/src/command",
"queue:work": "node dist/src/queue-worker" "queue:work": "node dist/src/queue-worker"
}, },
"dependencies": { "dependencies": {

View File

@ -61,12 +61,10 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
schemaName, schemaName,
); );
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata( await this.workspaceSyncMetadataService.synchronize({
{ workspaceId: this.workspaceId,
workspaceId: this.workspaceId, dataSourceId: dataSourceMetadata.id,
dataSourceId: dataSourceMetadata.id, });
},
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -340,8 +340,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeObjectTargetTable( columnName: `${computeCustomName(
createdObjectMetadata, createdObjectMetadata.nameSingular,
false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: 'uuid',
isNullable: true, isNullable: true,
@ -354,8 +355,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeObjectTargetTable( columnName: `${computeCustomName(
createdObjectMetadata, createdObjectMetadata.nameSingular,
false,
)}Id`, )}Id`,
referencedTableName: computeObjectTargetTable( referencedTableName: computeObjectTargetTable(
createdObjectMetadata, createdObjectMetadata,
@ -372,8 +374,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeObjectTargetTable( columnName: `${computeCustomName(
createdObjectMetadata, createdObjectMetadata.nameSingular,
false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: 'uuid',
isNullable: true, isNullable: true,
@ -386,8 +389,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeObjectTargetTable( columnName: `${computeCustomName(
createdObjectMetadata, createdObjectMetadata.nameSingular,
false,
)}Id`, )}Id`,
referencedTableName: computeObjectTargetTable( referencedTableName: computeObjectTargetTable(
createdObjectMetadata, createdObjectMetadata,
@ -403,8 +407,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${computeObjectTargetTable( columnName: `${computeCustomName(
createdObjectMetadata, createdObjectMetadata.nameSingular,
false,
)}Id`, )}Id`,
columnType: 'uuid', columnType: 'uuid',
isNullable: true, isNullable: true,
@ -417,8 +422,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
columns: [ columns: [
{ {
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${computeObjectTargetTable( columnName: `${computeCustomName(
createdObjectMetadata, createdObjectMetadata.nameSingular,
false,
)}Id`, )}Id`,
referencedTableName: computeObjectTargetTable( referencedTableName: computeObjectTargetTable(
createdObjectMetadata, createdObjectMetadata,
@ -582,7 +588,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: createdObjectMetadata.id, objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'activityTargets', name: 'activityTargets',
@ -596,7 +602,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: activityTargetObjectMetadata.id, objectMetadataId: activityTargetObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular, name: createdObjectMetadata.nameSingular,
@ -610,13 +616,16 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: activityTargetObjectMetadata.id, objectMetadataId: activityTargetObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: { targetColumnMap: {
value: `${computeObjectTargetTable(createdObjectMetadata)}Id`, value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
}, },
description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`, description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined, icon: undefined,
@ -673,7 +682,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: createdObjectMetadata.id, objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'attachments', name: 'attachments',
@ -687,7 +696,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: attachmentObjectMetadata.id, objectMetadataId: attachmentObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular, name: createdObjectMetadata.nameSingular,
@ -701,13 +710,16 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: attachmentObjectMetadata.id, objectMetadataId: attachmentObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: { targetColumnMap: {
value: `${computeObjectTargetTable(createdObjectMetadata)}Id`, value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
}, },
description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`, description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined, icon: undefined,
@ -739,6 +751,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
attachmentRelationFieldMetadataMap[createdObjectMetadata.id].id, attachmentRelationFieldMetadataMap[createdObjectMetadata.id].id,
toFieldMetadataId: toFieldMetadataId:
attachmentRelationFieldMetadataMap[attachmentObjectMetadata.id].id, attachmentRelationFieldMetadataMap[attachmentObjectMetadata.id].id,
onDeleteAction: RelationOnDeleteAction.CASCADE,
}, },
]); ]);
@ -761,7 +774,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: createdObjectMetadata.id, objectMetadataId: createdObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'favorites', name: 'favorites',
@ -775,7 +788,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: favoriteObjectMetadata.id, objectMetadataId: favoriteObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: createdObjectMetadata.nameSingular, name: createdObjectMetadata.nameSingular,
@ -789,13 +802,16 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{ {
objectMetadataId: favoriteObjectMetadata.id, objectMetadataId: favoriteObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
isCustom: true, isCustom: false,
isActive: true, isActive: true,
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: `${createdObjectMetadata.nameSingular}Id`, name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`, label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
targetColumnMap: { targetColumnMap: {
value: `${computeObjectTargetTable(createdObjectMetadata)}Id`, value: `${computeCustomName(
createdObjectMetadata.nameSingular,
false,
)}Id`,
}, },
description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`, description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined, icon: undefined,

View File

@ -1,14 +1,18 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface'; import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface';
import { ReflectBaseCustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-custom-object-metadata.interface';
import { ReflectDynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface';
import { ReflectFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface'; import { ReflectFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface';
import { ReflectObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-object-metadata.interface'; import { ReflectObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-object-metadata.interface';
import { ReflectRelationMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface'; import { ReflectRelationMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface';
export interface ReflectMetadataTypeMap { export interface ReflectMetadataTypeMap {
objectMetadata: ReflectObjectMetadata; objectMetadata: ReflectObjectMetadata;
extendObjectMetadata: ReflectBaseCustomObjectMetadata;
fieldMetadataMap: ReflectFieldMetadata; fieldMetadataMap: ReflectFieldMetadata;
relationMetadataCollection: ReflectRelationMetadata[]; dynamicRelationFieldMetadataMap: ReflectDynamicRelationFieldMetadata;
reflectRelationMetadataCollection: ReflectRelationMetadata[];
gate: GateDecoratorParams; gate: GateDecoratorParams;
isNullable: true; isNullable: true;
isSystem: true; isSystem: true;

View File

@ -38,12 +38,10 @@ export class WorkspaceManagerService {
await this.setWorkspaceMaxRow(workspaceId, schemaName); await this.setWorkspaceMaxRow(workspaceId, schemaName);
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata( await this.workspaceSyncMetadataService.synchronize({
{ workspaceId,
workspaceId, dataSourceId: dataSourceMetadata.id,
dataSourceId: dataSourceMetadata.id, });
},
);
await this.prefillWorkspaceWithStandardObjects( await this.prefillWorkspaceWithStandardObjects(
dataSourceMetadata, dataSourceMetadata,
@ -70,12 +68,10 @@ export class WorkspaceManagerService {
await this.setWorkspaceMaxRow(workspaceId, schemaName); await this.setWorkspaceMaxRow(workspaceId, schemaName);
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata( await this.workspaceSyncMetadataService.synchronize({
{ workspaceId,
workspaceId, dataSourceId: dataSourceMetadata.id,
dataSourceId: dataSourceMetadata.id, });
},
);
await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId); await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId);
} }

View File

@ -72,7 +72,7 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner {
); );
const { storage, workspaceMigrations } = const { storage, workspaceMigrations } =
await this.workspaceSyncMetadataService.syncStandardObjectsAndFieldsMetadata( await this.workspaceSyncMetadataService.synchronize(
{ {
workspaceId, workspaceId,
dataSourceId: dataSourceMetadata.id, dataSourceId: dataSourceMetadata.id,

View File

@ -6,8 +6,8 @@ import {
ComparatorAction, ComparatorAction,
FieldComparatorResult, FieldComparatorResult,
} from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface'; } from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface';
import { PartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { ComputedPartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { PartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; import { ComputedPartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { transformMetadataForComparison } from 'src/workspace/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; import { transformMetadataForComparison } from 'src/workspace/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
@ -30,12 +30,12 @@ export class WorkspaceFieldComparator {
public compare( public compare(
originalObjectMetadata: ObjectMetadataEntity, originalObjectMetadata: ObjectMetadataEntity,
standardObjectMetadata: PartialObjectMetadata, standardObjectMetadata: ComputedPartialObjectMetadata,
): FieldComparatorResult[] { ): FieldComparatorResult[] {
const result: FieldComparatorResult[] = []; const result: FieldComparatorResult[] = [];
const fieldPropertiesToUpdateMap: Record< const fieldPropertiesToUpdateMap: Record<
string, string,
Partial<PartialFieldMetadata> Partial<ComputedPartialFieldMetadata>
> = {}; > = {};
// Double security to only compare non-custom fields // Double security to only compare non-custom fields
@ -69,7 +69,7 @@ export class WorkspaceFieldComparator {
standardObjectMetadata.fields, standardObjectMetadata.fields,
{ {
shouldIgnoreProperty: (property, originalMetadata) => { shouldIgnoreProperty: (property, originalMetadata) => {
if (['options'].includes(property)) { if (['options', 'gate'].includes(property)) {
return true; return true;
} }

View File

@ -7,7 +7,7 @@ import {
ComparatorAction, ComparatorAction,
ObjectComparatorResult, ObjectComparatorResult,
} from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface'; } from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface';
import { PartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; import { ComputedPartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
import { transformMetadataForComparison } from 'src/workspace/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; import { transformMetadataForComparison } from 'src/workspace/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
@ -28,7 +28,7 @@ export class WorkspaceObjectComparator {
public compare( public compare(
originalObjectMetadata: ObjectMetadataEntity | undefined, originalObjectMetadata: ObjectMetadataEntity | undefined,
standardObjectMetadata: PartialObjectMetadata, standardObjectMetadata: ComputedPartialObjectMetadata,
): ObjectComparatorResult { ): ObjectComparatorResult {
// If the object doesn't exist in the original metadata, we need to create it // If the object doesn't exist in the original metadata, we need to create it
if (!originalObjectMetadata) { if (!originalObjectMetadata) {
@ -38,7 +38,7 @@ export class WorkspaceObjectComparator {
}; };
} }
const objectPropertiesToUpdate: Partial<PartialObjectMetadata> = {}; const objectPropertiesToUpdate: Partial<ComputedPartialObjectMetadata> = {};
// Only compare properties that are not ignored // Only compare properties that are not ignored
const partialOriginalObjectMetadata = transformMetadataForComparison( const partialOriginalObjectMetadata = transformMetadataForComparison(

View File

@ -0,0 +1,81 @@
import { BaseCustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/base-custom-object-metadata.decorator';
import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator';
import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/metadata/relation-metadata/relation-metadata.entity';
import { ActivityTargetObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata';
import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator';
import { FavoriteObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata';
import { AttachmentObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/attachment.object-metadata';
@BaseCustomObjectMetadata()
export class CustomObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
label: 'Name',
description: 'Name',
type: FieldMetadataType.TEXT,
icon: 'IconAbc',
defaultValue: { value: 'Untitled' },
})
name: string;
@FieldMetadata({
label: 'Position',
description: 'Position',
type: FieldMetadataType.POSITION,
icon: 'IconHierarchy2',
})
@IsNullable()
@IsSystem()
position: number;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Activities',
description: (objectMetadata) =>
`Activities tied to the ${objectMetadata.labelSingular}`,
icon: 'IconCheckbox',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => ActivityTargetObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
@IsNullable()
activityTargets: ActivityTargetObjectMetadata[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Favorites',
description: (objectMetadata) =>
`Favorites tied to the ${objectMetadata.labelSingular}`,
icon: 'IconHeart',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => FavoriteObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
@IsNullable()
favorites: FavoriteObjectMetadata[];
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Attachments',
description: (objectMetadata) =>
`Attachments tied to the ${objectMetadata.labelSingular}`,
icon: 'IconFileImport',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => AttachmentObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
@IsNullable()
attachments: AttachmentObjectMetadata[];
}

View File

@ -0,0 +1,20 @@
import { BaseCustomObjectMetadataDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-custom-object-metadata.interface';
import { TypedReflect } from 'src/utils/typed-reflect';
export function BaseCustomObjectMetadata(
params?: BaseCustomObjectMetadataDecoratorParams,
): ClassDecorator {
return (target) => {
const gate = TypedReflect.getMetadata('gate', target);
TypedReflect.defineMetadata(
'extendObjectMetadata',
{
...params,
gate,
},
target,
);
};
}

View File

@ -0,0 +1,27 @@
import { DynamicRelationFieldMetadataDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface';
import { TypedReflect } from 'src/utils/typed-reflect';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export function DynamicRelationFieldMetadata(
params: DynamicRelationFieldMetadataDecoratorParams,
): PropertyDecorator {
return (target: object, fieldKey: string) => {
const isSystem =
TypedReflect.getMetadata('isSystem', target, fieldKey) ?? false;
const gate = TypedReflect.getMetadata('gate', target, fieldKey);
TypedReflect.defineMetadata(
'dynamicRelationFieldMetadataMap',
{
type: FieldMetadataType.RELATION,
paramsFactory: params,
isCustom: false,
isNullable: true,
isSystem,
gate,
},
target.constructor,
);
};
}

View File

@ -6,33 +6,27 @@ import {
} from 'src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface'; } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-relation-metadata.interface';
import { TypedReflect } from 'src/utils/typed-reflect'; import { TypedReflect } from 'src/utils/typed-reflect';
import { convertClassNameToObjectMetadataName } from 'src/workspace/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
import { RelationOnDeleteAction } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { RelationOnDeleteAction } from 'src/metadata/relation-metadata/relation-metadata.entity';
export function RelationMetadata( export function RelationMetadata<TClass extends object>(
params: RelationMetadataDecoratorParams, params: RelationMetadataDecoratorParams<TClass>,
): PropertyDecorator { ): PropertyDecorator {
return (target: object, fieldKey: string) => { return (target: object, fieldKey: string) => {
const relationMetadataCollection = const relationMetadataCollection =
TypedReflect.getMetadata( TypedReflect.getMetadata(
'relationMetadataCollection', 'reflectRelationMetadataCollection',
target.constructor, target.constructor,
) ?? []; ) ?? [];
const gate = TypedReflect.getMetadata('gate', target, fieldKey); const gate = TypedReflect.getMetadata('gate', target, fieldKey);
const objectName = convertClassNameToObjectMetadataName(
target.constructor.name,
);
Reflect.defineMetadata( TypedReflect.defineMetadata(
'relationMetadataCollection', 'reflectRelationMetadataCollection',
[ [
...relationMetadataCollection, ...relationMetadataCollection,
{ {
type: params.type, target,
fromObjectNameSingular: objectName, fieldKey,
toObjectNameSingular: params.objectName, ...params,
fromFieldMetadataName: fieldKey,
toFieldMetadataName: params.inverseSideFieldName ?? objectName,
onDelete: params.onDelete ?? RelationOnDeleteAction.SET_NULL, onDelete: params.onDelete ?? RelationOnDeleteAction.SET_NULL,
gate, gate,
} satisfies ReflectRelationMetadata, } satisfies ReflectRelationMetadata,

View File

@ -1,9 +1,11 @@
import { FeatureFlagFactory } from './feature-flags.factory'; import { FeatureFlagFactory } from './feature-flags.factory';
import { StandardFieldFactory } from './standard-field.factory';
import { StandardObjectFactory } from './standard-object.factory'; import { StandardObjectFactory } from './standard-object.factory';
import { StandardRelationFactory } from './standard-relation.factory'; import { StandardRelationFactory } from './standard-relation.factory';
export const workspaceSyncMetadataFactories = [ export const workspaceSyncMetadataFactories = [
FeatureFlagFactory, FeatureFlagFactory,
StandardFieldFactory,
StandardObjectFactory, StandardObjectFactory,
StandardRelationFactory, StandardRelationFactory,
]; ];

View File

@ -0,0 +1,102 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface';
import {
PartialComputedFieldMetadata,
PartialFieldMetadata,
} from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { ReflectFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface';
import { ReflectObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-object-metadata.interface';
import { ReflectDynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface';
import { TypedReflect } from 'src/utils/typed-reflect';
import { isGatedAndNotEnabled } from 'src/workspace/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
@Injectable()
export class StandardFieldFactory {
create(
target: object,
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): (PartialFieldMetadata | PartialComputedFieldMetadata)[] {
const reflectObjectMetadata = TypedReflect.getMetadata(
'objectMetadata',
target,
);
const reflectFieldMetadataMap =
TypedReflect.getMetadata('fieldMetadataMap', target) ?? [];
const reflectDynamicRelationFieldMetadataMap = TypedReflect.getMetadata(
'dynamicRelationFieldMetadataMap',
target,
);
const partialFieldMetadataCollection: (
| PartialFieldMetadata
| PartialComputedFieldMetadata
)[] = Object.values(reflectFieldMetadataMap)
.map((reflectFieldMetadata) =>
this.createFieldMetadata(
reflectObjectMetadata,
reflectFieldMetadata,
context,
workspaceFeatureFlagsMap,
),
)
.filter((metadata): metadata is PartialFieldMetadata => !!metadata);
const partialComputedFieldMetadata = this.createComputedFieldMetadata(
reflectDynamicRelationFieldMetadataMap,
context,
workspaceFeatureFlagsMap,
);
if (partialComputedFieldMetadata) {
partialFieldMetadataCollection.push(partialComputedFieldMetadata);
}
return partialFieldMetadataCollection;
}
private createFieldMetadata(
reflectObjectMetadata: ReflectObjectMetadata | undefined,
reflectFieldMetadata: ReflectFieldMetadata[string],
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): PartialFieldMetadata | undefined {
if (
isGatedAndNotEnabled(reflectFieldMetadata.gate, workspaceFeatureFlagsMap)
) {
return undefined;
}
return {
...reflectFieldMetadata,
workspaceId: context.workspaceId,
isSystem:
reflectObjectMetadata?.isSystem || reflectFieldMetadata.isSystem,
};
}
private createComputedFieldMetadata(
reflectDynamicRelationFieldMetadata:
| ReflectDynamicRelationFieldMetadata
| undefined,
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): PartialComputedFieldMetadata | undefined {
if (
!reflectDynamicRelationFieldMetadata ||
isGatedAndNotEnabled(
reflectDynamicRelationFieldMetadata.gate,
workspaceFeatureFlagsMap,
)
) {
return undefined;
}
return {
...reflectDynamicRelationFieldMetadata,
workspaceId: context.workspaceId,
isSystem: reflectDynamicRelationFieldMetadata.isSystem,
};
}
}

View File

@ -3,20 +3,23 @@ import { Injectable } from '@nestjs/common';
import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { PartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; import { PartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface'; import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface';
import { PartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
import { standardObjectMetadataCollection } from 'src/workspace/workspace-sync-metadata/standard-objects';
import { TypedReflect } from 'src/utils/typed-reflect'; import { TypedReflect } from 'src/utils/typed-reflect';
import { isGatedAndNotEnabled } from 'src/workspace/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; import { isGatedAndNotEnabled } from 'src/workspace/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
import { StandardFieldFactory } from './standard-field.factory';
@Injectable() @Injectable()
export class StandardObjectFactory { export class StandardObjectFactory {
constructor(private readonly standardFieldFactory: StandardFieldFactory) {}
create( create(
standardObjectMetadataDefinitions: (typeof BaseObjectMetadata)[],
context: WorkspaceSyncContext, context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap, workspaceFeatureFlagsMap: FeatureFlagMap,
): PartialObjectMetadata[] { ): PartialObjectMetadata[] {
return standardObjectMetadataCollection return standardObjectMetadataDefinitions
.map((metadata) => .map((metadata) =>
this.createObjectMetadata(metadata, context, workspaceFeatureFlagsMap), this.createObjectMetadata(metadata, context, workspaceFeatureFlagsMap),
) )
@ -29,8 +32,6 @@ export class StandardObjectFactory {
workspaceFeatureFlagsMap: FeatureFlagMap, workspaceFeatureFlagsMap: FeatureFlagMap,
): PartialObjectMetadata | undefined { ): PartialObjectMetadata | undefined {
const objectMetadata = TypedReflect.getMetadata('objectMetadata', metadata); const objectMetadata = TypedReflect.getMetadata('objectMetadata', metadata);
const fieldMetadataMap =
TypedReflect.getMetadata('fieldMetadataMap', metadata) ?? [];
if (!objectMetadata) { if (!objectMetadata) {
throw new Error( throw new Error(
@ -42,22 +43,10 @@ export class StandardObjectFactory {
return undefined; return undefined;
} }
const fields = Object.values(fieldMetadataMap).reduce( const fields = this.standardFieldFactory.create(
// Omit gate as we don't want to store it in the DB metadata,
(acc, { gate, ...fieldMetadata }) => { context,
if (isGatedAndNotEnabled(gate, workspaceFeatureFlagsMap)) { workspaceFeatureFlagsMap,
return acc;
}
acc.push({
...fieldMetadata,
workspaceId: context.workspaceId,
isSystem: objectMetadata.isSystem || fieldMetadata.isSystem,
});
return acc;
},
[] as PartialFieldMetadata[],
); );
return { return {

View File

@ -4,42 +4,80 @@ import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/inte
import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface'; import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface';
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata'; import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
import { standardObjectMetadataCollection } from 'src/workspace/workspace-sync-metadata/standard-objects';
import { TypedReflect } from 'src/utils/typed-reflect'; import { TypedReflect } from 'src/utils/typed-reflect';
import { isGatedAndNotEnabled } from 'src/workspace/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; import { isGatedAndNotEnabled } from 'src/workspace/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { convertClassNameToObjectMetadataName } from 'src/workspace/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
interface CustomRelationFactory {
object: ObjectMetadataEntity;
metadata: typeof BaseObjectMetadata;
}
@Injectable() @Injectable()
export class StandardRelationFactory { export class StandardRelationFactory {
create( create(
customObjectFactories: CustomRelationFactory[],
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<RelationMetadataEntity>[];
create(
standardObjectMetadataDefinitions: (typeof BaseObjectMetadata)[],
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<RelationMetadataEntity>[];
create(
standardObjectMetadataDefinitionsOrCustomObjectFactories:
| (typeof BaseObjectMetadata)[]
| {
object: ObjectMetadataEntity;
metadata: typeof BaseObjectMetadata;
}[],
context: WorkspaceSyncContext, context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>, originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap, workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<RelationMetadataEntity>[] { ): Partial<RelationMetadataEntity>[] {
return standardObjectMetadataCollection.flatMap((standardObjectMetadata) => return standardObjectMetadataDefinitionsOrCustomObjectFactories.flatMap(
this.createRelationMetadata( (
standardObjectMetadata, standardObjectMetadata:
context, | typeof BaseObjectMetadata
originalObjectMetadataMap, | CustomRelationFactory,
workspaceFeatureFlagsMap, ) =>
), this.createRelationMetadata(
standardObjectMetadata,
context,
originalObjectMetadataMap,
workspaceFeatureFlagsMap,
),
); );
} }
private createRelationMetadata( private createRelationMetadata(
standardObjectMetadata: typeof BaseObjectMetadata, standardObjectMetadataOrCustomRelationFactory:
| typeof BaseObjectMetadata
| CustomRelationFactory,
context: WorkspaceSyncContext, context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>, originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap, workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<RelationMetadataEntity>[] { ): Partial<RelationMetadataEntity>[] {
const standardObjectMetadata =
'metadata' in standardObjectMetadataOrCustomRelationFactory
? standardObjectMetadataOrCustomRelationFactory.metadata
: standardObjectMetadataOrCustomRelationFactory;
const objectMetadata = TypedReflect.getMetadata( const objectMetadata = TypedReflect.getMetadata(
'objectMetadata', 'metadata' in standardObjectMetadataOrCustomRelationFactory
? 'extendObjectMetadata'
: 'objectMetadata',
standardObjectMetadata, standardObjectMetadata,
); );
const relationMetadataCollection = TypedReflect.getMetadata( const reflectRelationMetadataCollection = TypedReflect.getMetadata(
'relationMetadataCollection', 'reflectRelationMetadataCollection',
standardObjectMetadata, standardObjectMetadata,
); );
@ -50,67 +88,81 @@ export class StandardRelationFactory {
} }
if ( if (
!relationMetadataCollection || !reflectRelationMetadataCollection ||
isGatedAndNotEnabled(objectMetadata.gate, workspaceFeatureFlagsMap) isGatedAndNotEnabled(objectMetadata?.gate, workspaceFeatureFlagsMap)
) { ) {
return []; return [];
} }
return relationMetadataCollection return reflectRelationMetadataCollection
.filter( .filter(
(relationMetadata) => (reflectRelationMetadata) =>
!isGatedAndNotEnabled( !isGatedAndNotEnabled(
relationMetadata.gate, reflectRelationMetadata.gate,
workspaceFeatureFlagsMap, workspaceFeatureFlagsMap,
), ),
) )
.map((relationMetadata) => { .map((reflectRelationMetadata) => {
// Compute reflect relation metadata
const fromObjectNameSingular =
'object' in standardObjectMetadataOrCustomRelationFactory
? standardObjectMetadataOrCustomRelationFactory.object.nameSingular
: convertClassNameToObjectMetadataName(
reflectRelationMetadata.target.constructor.name,
);
const toObjectNameSingular = convertClassNameToObjectMetadataName(
reflectRelationMetadata.inverseSideTarget().name,
);
const fromFieldMetadataName = reflectRelationMetadata.fieldKey;
const toFieldMetadataName =
(reflectRelationMetadata.inverseSideFieldKey as string | undefined) ??
fromObjectNameSingular;
const fromObjectMetadata = const fromObjectMetadata =
originalObjectMetadataMap[relationMetadata.fromObjectNameSingular]; originalObjectMetadataMap[fromObjectNameSingular];
assert( assert(
fromObjectMetadata, fromObjectMetadata,
`Object ${relationMetadata.fromObjectNameSingular} not found in DB `Object ${fromObjectNameSingular} not found in DB
for relation FROM defined in class ${objectMetadata.nameSingular}`, for relation FROM defined in class ${fromObjectNameSingular}`,
); );
const toObjectMetadata = const toObjectMetadata =
originalObjectMetadataMap[relationMetadata.toObjectNameSingular]; originalObjectMetadataMap[toObjectNameSingular];
assert( assert(
toObjectMetadata, toObjectMetadata,
`Object ${relationMetadata.toObjectNameSingular} not found in DB `Object ${toObjectNameSingular} not found in DB
for relation TO defined in class ${objectMetadata.nameSingular}`, for relation TO defined in class ${fromObjectNameSingular}`,
); );
const fromFieldMetadata = fromObjectMetadata?.fields.find( const fromFieldMetadata = fromObjectMetadata?.fields.find(
(field) => field.name === relationMetadata.fromFieldMetadataName, (field) => field.name === fromFieldMetadataName,
); );
assert( assert(
fromFieldMetadata, fromFieldMetadata,
`Field ${relationMetadata.fromFieldMetadataName} not found in object ${relationMetadata.fromObjectNameSingular} `Field ${fromFieldMetadataName} not found in object ${fromObjectNameSingular}
for relation FROM defined in class ${objectMetadata.nameSingular}`, for relation FROM defined in class ${fromObjectNameSingular}`,
); );
const toFieldMetadata = toObjectMetadata?.fields.find( const toFieldMetadata = toObjectMetadata?.fields.find(
(field) => field.name === relationMetadata.toFieldMetadataName, (field) => field.name === toFieldMetadataName,
); );
assert( assert(
toFieldMetadata, toFieldMetadata,
`Field ${relationMetadata.toFieldMetadataName} not found in object ${relationMetadata.toObjectNameSingular} `Field ${toFieldMetadataName} not found in object ${toObjectNameSingular}
for relation TO defined in class ${objectMetadata.nameSingular}`, for relation TO defined in class ${fromObjectNameSingular}`,
); );
return { return {
relationType: relationMetadata.type, relationType: reflectRelationMetadata.type,
fromObjectMetadataId: fromObjectMetadata?.id, fromObjectMetadataId: fromObjectMetadata?.id,
toObjectMetadataId: toObjectMetadata?.id, toObjectMetadataId: toObjectMetadata?.id,
fromFieldMetadataId: fromFieldMetadata?.id, fromFieldMetadataId: fromFieldMetadata?.id,
toFieldMetadataId: toFieldMetadata?.id, toFieldMetadataId: toFieldMetadata?.id,
workspaceId: context.workspaceId, workspaceId: context.workspaceId,
onDeleteAction: relationMetadata.onDelete, onDeleteAction: reflectRelationMetadata.onDelete,
}; };
}); });
} }

View File

@ -1,8 +1,8 @@
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { PartialFieldMetadata } from './partial-field-metadata.interface'; import { ComputedPartialFieldMetadata } from './partial-field-metadata.interface';
import { PartialObjectMetadata } from './partial-object-metadata.interface'; import { ComputedPartialObjectMetadata } from './partial-object-metadata.interface';
export const enum ComparatorAction { export const enum ComparatorAction {
SKIP = 'SKIP', SKIP = 'SKIP',
@ -32,13 +32,15 @@ export interface ComparatorDeleteResult<T> {
export type ObjectComparatorResult = export type ObjectComparatorResult =
| ComparatorSkipResult | ComparatorSkipResult
| ComparatorCreateResult<PartialObjectMetadata> | ComparatorCreateResult<ComputedPartialObjectMetadata>
| ComparatorUpdateResult<Partial<PartialObjectMetadata>>; | ComparatorUpdateResult<Partial<ComputedPartialObjectMetadata>>;
export type FieldComparatorResult = export type FieldComparatorResult =
| ComparatorSkipResult | ComparatorSkipResult
| ComparatorCreateResult<PartialFieldMetadata> | ComparatorCreateResult<ComputedPartialFieldMetadata>
| ComparatorUpdateResult<Partial<PartialFieldMetadata> & { id: string }> | ComparatorUpdateResult<
Partial<ComputedPartialFieldMetadata> & { id: string }
>
| ComparatorDeleteResult<FieldMetadataEntity>; | ComparatorDeleteResult<FieldMetadataEntity>;
export type RelationComparatorResult = export type RelationComparatorResult =

View File

@ -1,6 +1,20 @@
import { ReflectDynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-computed-relation-field-metadata.interface';
import { ReflectFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface'; import { ReflectFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-field-metadata.interface';
export type PartialFieldMetadata = ReflectFieldMetadata[string] & { export type PartialFieldMetadata = Omit<
ReflectFieldMetadata[string],
'joinColumn'
> & {
workspaceId: string; workspaceId: string;
objectMetadataId?: string; objectMetadataId?: string;
}; };
export type PartialComputedFieldMetadata =
ReflectDynamicRelationFieldMetadata & {
workspaceId: string;
objectMetadataId?: string;
};
export type ComputedPartialFieldMetadata = {
[K in keyof PartialFieldMetadata]: ExcludeFunctions<PartialFieldMetadata[K]>;
};

View File

@ -1,9 +1,20 @@
import { PartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import {
ComputedPartialFieldMetadata,
PartialComputedFieldMetadata,
PartialFieldMetadata,
} from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { ReflectObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-object-metadata.interface'; import { ReflectObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/reflect-object-metadata.interface';
export type PartialObjectMetadata = ReflectObjectMetadata & { export type PartialObjectMetadata = ReflectObjectMetadata & {
id?: string; id?: string;
workspaceId: string; workspaceId: string;
dataSourceId: string; dataSourceId: string;
fields: PartialFieldMetadata[]; fields: (PartialFieldMetadata | PartialComputedFieldMetadata)[];
};
export type ComputedPartialObjectMetadata = Omit<
PartialObjectMetadata,
'fields'
> & {
fields: ComputedPartialFieldMetadata[];
}; };

View File

@ -0,0 +1,23 @@
import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
export type DynamicRelationFieldMetadataDecoratorParams = (
oppositeObjectMetadata: ObjectMetadataEntity,
) => {
name: string;
label: string;
joinColumn: string;
description?: string;
icon?: string;
};
export interface ReflectDynamicRelationFieldMetadata {
type: FieldMetadataType.RELATION;
paramsFactory: DynamicRelationFieldMetadataDecoratorParams;
isNullable: boolean;
isSystem: boolean;
isCustom: boolean;
gate?: GateDecoratorParams;
}

View File

@ -0,0 +1,10 @@
import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface';
export type BaseCustomObjectMetadataDecoratorParams =
| { allowObjectNameList?: string[] }
| { denyObjectNameList?: string[] };
export type ReflectBaseCustomObjectMetadata =
BaseCustomObjectMetadataDecoratorParams & {
gate?: GateDecoratorParams;
};

View File

@ -4,13 +4,14 @@ import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/fie
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
export interface FieldMetadataDecoratorParams< export interface FieldMetadataDecoratorParams<
T extends FieldMetadataType | 'default', T extends FieldMetadataType | 'default',
> { > {
type: T; type: T;
label: string; label: string | ((objectMetadata: ObjectMetadataEntity) => string);
description?: string; description?: string | ((objectMetadata: ObjectMetadataEntity) => string);
icon?: string; icon?: string;
defaultValue?: FieldMetadataDefaultValue<T>; defaultValue?: FieldMetadataDefaultValue<T>;
joinColumn?: string; joinColumn?: string;
@ -28,7 +29,6 @@ export interface ReflectFieldMetadata {
isNullable: boolean; isNullable: boolean;
isSystem: boolean; isSystem: boolean;
isCustom: boolean; isCustom: boolean;
description?: string;
defaultValue: FieldMetadataDefaultValue<'default'> | null; defaultValue: FieldMetadataDefaultValue<'default'> | null;
gate?: GateDecoratorParams; gate?: GateDecoratorParams;
options?: FieldMetadataOptions<'default'> | null; options?: FieldMetadataOptions<'default'> | null;

View File

@ -1,3 +1,5 @@
import { ObjectType } from 'typeorm';
import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface'; import { GateDecoratorParams } from 'src/workspace/workspace-sync-metadata/interfaces/gate-decorator.interface';
import { import {
@ -5,19 +7,17 @@ import {
RelationMetadataType, RelationMetadataType,
} from 'src/metadata/relation-metadata/relation-metadata.entity'; } from 'src/metadata/relation-metadata/relation-metadata.entity';
export interface RelationMetadataDecoratorParams { export interface RelationMetadataDecoratorParams<T> {
type: RelationMetadataType; type: RelationMetadataType;
objectName: string; inverseSideTarget: () => ObjectType<T>;
inverseSideFieldName?: string; inverseSideFieldKey?: keyof T;
onDelete?: RelationOnDeleteAction; onDelete?: RelationOnDeleteAction;
} }
export interface ReflectRelationMetadata { export interface ReflectRelationMetadata
type: RelationMetadataType; extends RelationMetadataDecoratorParams<any> {
fromObjectNameSingular: string; target: object;
toObjectNameSingular: string; fieldKey: string;
fromFieldMetadataName: string;
toFieldMetadataName: string;
gate?: GateDecoratorParams; gate?: GateDecoratorParams;
onDelete: RelationOnDeleteAction; onDelete: RelationOnDeleteAction;
} }

View File

@ -153,8 +153,8 @@ export class WorkspaceMetadataUpdaterService {
// https://github.com/typeorm/typeorm/issues/3490 // https://github.com/typeorm/typeorm/issues/3490
// To avoid calling update in a for loop, we did this hack. // To avoid calling update in a for loop, we did this hack.
return { return {
...oldFieldMetadata, ...omit(oldFieldMetadata, ['objectMetadataId', 'workspaceId']),
...updateFieldMetadata, ...omit(updateFieldMetadata, ['objectMetadataId', 'workspaceId']),
options: updateFieldMetadata.options ?? oldFieldMetadata.options, options: updateFieldMetadata.options ?? oldFieldMetadata.options,
}; };
}); });
@ -197,7 +197,11 @@ export class WorkspaceMetadataUpdaterService {
return { return {
current: oldFieldMetadata as FieldMetadataEntity, current: oldFieldMetadata as FieldMetadataEntity,
altered: alteredFieldMetadata as FieldMetadataEntity, altered: {
...alteredFieldMetadata,
objectMetadataId: oldFieldMetadata.objectMetadataId,
workspaceId: oldFieldMetadata.workspaceId,
} as FieldMetadataEntity,
}; };
}, },
), ),

View File

@ -0,0 +1,140 @@
import { Injectable, Logger } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { ComparatorAction } from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface';
import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface';
import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
import { WorkspaceFieldComparator } from 'src/workspace/workspace-sync-metadata/comparators/workspace-field.comparator';
import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service';
import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage';
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory';
import { StandardFieldFactory } from 'src/workspace/workspace-sync-metadata/factories/standard-field.factory';
import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata';
import { computeStandardObject } from 'src/workspace/workspace-sync-metadata/utils/compute-standard-object.util';
@Injectable()
export class WorkspaceSyncFieldMetadataService {
private readonly logger = new Logger(WorkspaceSyncFieldMetadataService.name);
constructor(
private readonly standardFieldFactory: StandardFieldFactory,
private readonly workspaceFieldComparator: WorkspaceFieldComparator,
private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService,
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
) {}
async synchronize(
context: WorkspaceSyncContext,
manager: EntityManager,
storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const objectMetadataRepository =
manager.getRepository(ObjectMetadataEntity);
// Retrieve object metadata collection from DB
const originalObjectMetadataCollection =
await objectMetadataRepository.find({
where: {
workspaceId: context.workspaceId,
// We're only interested in standard fields
fields: { isCustom: false },
},
relations: ['dataSource', 'fields'],
});
// Filter out custom objects
const customObjectMetadataCollection =
originalObjectMetadataCollection.filter(
(objectMetadata) => objectMetadata.isCustom,
);
// Create standard field metadata collection
const standardFieldMetadataCollection = this.standardFieldFactory.create(
CustomObjectMetadata,
context,
workspaceFeatureFlagsMap,
);
// Loop over all standard objects and compare them with the objects in DB
for (const customObjectMetadata of customObjectMetadataCollection) {
// Also, maybe it's better to refactor a bit and move generation part into a separate module ?
const standardObjectMetadata = computeStandardObject(
{
...customObjectMetadata,
fields: standardFieldMetadataCollection,
},
customObjectMetadata,
);
/**
* COMPARE FIELD METADATA
*/
const fieldComparatorResults = this.workspaceFieldComparator.compare(
customObjectMetadata,
standardObjectMetadata,
);
for (const fieldComparatorResult of fieldComparatorResults) {
switch (fieldComparatorResult.action) {
case ComparatorAction.CREATE: {
storage.addCreateFieldMetadata(fieldComparatorResult.object);
break;
}
case ComparatorAction.UPDATE: {
storage.addUpdateFieldMetadata(fieldComparatorResult.object);
break;
}
case ComparatorAction.DELETE: {
storage.addDeleteFieldMetadata(fieldComparatorResult.object);
break;
}
}
}
}
this.logger.log('Updating workspace metadata');
const metadataFieldUpdaterResult =
await this.workspaceMetadataUpdaterService.updateFieldMetadata(
manager,
storage,
);
this.logger.log('Generating migrations');
const createFieldWorkspaceMigrations =
await this.workspaceMigrationFieldFactory.create(
originalObjectMetadataCollection,
metadataFieldUpdaterResult.createdFieldMetadataCollection,
WorkspaceMigrationBuilderAction.CREATE,
);
const updateFieldWorkspaceMigrations =
await this.workspaceMigrationFieldFactory.create(
originalObjectMetadataCollection,
metadataFieldUpdaterResult.updatedFieldMetadataCollection,
WorkspaceMigrationBuilderAction.UPDATE,
);
const deleteFieldWorkspaceMigrations =
await this.workspaceMigrationFieldFactory.create(
originalObjectMetadataCollection,
storage.fieldMetadataDeleteCollection,
WorkspaceMigrationBuilderAction.DELETE,
);
this.logger.log('Saving migrations');
return [
...createFieldWorkspaceMigrations,
...updateFieldWorkspaceMigrations,
...deleteFieldWorkspaceMigrations,
];
}
}

View File

@ -16,7 +16,8 @@ import { WorkspaceFieldComparator } from 'src/workspace/workspace-sync-metadata/
import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service'; import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service';
import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage';
import { WorkspaceMigrationObjectFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-object.factory'; import { WorkspaceMigrationObjectFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-object.factory';
import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory'; import { computeStandardObject } from 'src/workspace/workspace-sync-metadata/utils/compute-standard-object.util';
import { standardObjectMetadataDefinitions } from 'src/workspace/workspace-sync-metadata/standard-objects';
@Injectable() @Injectable()
export class WorkspaceSyncObjectMetadataService { export class WorkspaceSyncObjectMetadataService {
@ -28,7 +29,6 @@ export class WorkspaceSyncObjectMetadataService {
private readonly workspaceFieldComparator: WorkspaceFieldComparator, private readonly workspaceFieldComparator: WorkspaceFieldComparator,
private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService, private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService,
private readonly workspaceMigrationObjectFactory: WorkspaceMigrationObjectFactory, private readonly workspaceMigrationObjectFactory: WorkspaceMigrationObjectFactory,
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
) {} ) {}
async synchronize( async synchronize(
@ -45,14 +45,18 @@ export class WorkspaceSyncObjectMetadataService {
await objectMetadataRepository.find({ await objectMetadataRepository.find({
where: { where: {
workspaceId: context.workspaceId, workspaceId: context.workspaceId,
isCustom: false,
fields: { isCustom: false }, fields: { isCustom: false },
}, },
relations: ['dataSource', 'fields'], relations: ['dataSource', 'fields'],
}); });
const customObjectMetadataCollection =
originalObjectMetadataCollection.filter(
(objectMetadata) => objectMetadata.isCustom,
);
// Create standard object metadata collection // Create standard object metadata collection
const standardObjectMetadataCollection = this.standardObjectFactory.create( const standardObjectMetadataCollection = this.standardObjectFactory.create(
standardObjectMetadataDefinitions,
context, context,
workspaceFeatureFlagsMap, workspaceFeatureFlagsMap,
); );
@ -68,7 +72,9 @@ export class WorkspaceSyncObjectMetadataService {
this.logger.log('Comparing standard objects and fields metadata'); this.logger.log('Comparing standard objects and fields metadata');
// Store object that need to be deleted // Store object that need to be deleted
for (const originalObjectMetadata of originalObjectMetadataCollection) { for (const originalObjectMetadata of originalObjectMetadataCollection.filter(
(object) => !object.isCustom,
)) {
if (!standardObjectMetadataMap[originalObjectMetadata.nameSingular]) { if (!standardObjectMetadataMap[originalObjectMetadata.nameSingular]) {
storage.addDeleteObjectMetadata(originalObjectMetadata); storage.addDeleteObjectMetadata(originalObjectMetadata);
} }
@ -78,8 +84,11 @@ export class WorkspaceSyncObjectMetadataService {
for (const standardObjectName in standardObjectMetadataMap) { for (const standardObjectName in standardObjectMetadataMap) {
const originalObjectMetadata = const originalObjectMetadata =
originalObjectMetadataMap[standardObjectName]; originalObjectMetadataMap[standardObjectName];
const standardObjectMetadata = const standardObjectMetadata = computeStandardObject(
standardObjectMetadataMap[standardObjectName]; standardObjectMetadataMap[standardObjectName],
originalObjectMetadata,
customObjectMetadataCollection,
);
/** /**
* COMPARE OBJECT METADATA * COMPARE OBJECT METADATA
@ -132,11 +141,6 @@ export class WorkspaceSyncObjectMetadataService {
manager, manager,
storage, storage,
); );
const metadataFieldUpdaterResult =
await this.workspaceMetadataUpdaterService.updateFieldMetadata(
manager,
storage,
);
this.logger.log('Generating migrations'); this.logger.log('Generating migrations');
@ -153,35 +157,11 @@ export class WorkspaceSyncObjectMetadataService {
WorkspaceMigrationBuilderAction.DELETE, WorkspaceMigrationBuilderAction.DELETE,
); );
const createFieldWorkspaceMigrations =
await this.workspaceMigrationFieldFactory.create(
originalObjectMetadataCollection,
metadataFieldUpdaterResult.createdFieldMetadataCollection,
WorkspaceMigrationBuilderAction.CREATE,
);
const updateFieldWorkspaceMigrations =
await this.workspaceMigrationFieldFactory.create(
originalObjectMetadataCollection,
metadataFieldUpdaterResult.updatedFieldMetadataCollection,
WorkspaceMigrationBuilderAction.UPDATE,
);
const deleteFieldWorkspaceMigrations =
await this.workspaceMigrationFieldFactory.create(
originalObjectMetadataCollection,
storage.fieldMetadataDeleteCollection,
WorkspaceMigrationBuilderAction.DELETE,
);
this.logger.log('Saving migrations'); this.logger.log('Saving migrations');
return [ return [
...createObjectWorkspaceMigrations, ...createObjectWorkspaceMigrations,
...deleteObjectWorkspaceMigrations, ...deleteObjectWorkspaceMigrations,
...createFieldWorkspaceMigrations,
...updateFieldWorkspaceMigrations,
...deleteFieldWorkspaceMigrations,
]; ];
} }
} }

View File

@ -16,6 +16,8 @@ import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-me
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage';
import { WorkspaceMigrationRelationFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-relation.factory'; import { WorkspaceMigrationRelationFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-relation.factory';
import { standardObjectMetadataDefinitions } from 'src/workspace/workspace-sync-metadata/standard-objects';
import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata';
@Injectable() @Injectable()
export class WorkspaceSyncRelationMetadataService { export class WorkspaceSyncRelationMetadataService {
@ -44,11 +46,14 @@ export class WorkspaceSyncRelationMetadataService {
await objectMetadataRepository.find({ await objectMetadataRepository.find({
where: { where: {
workspaceId: context.workspaceId, workspaceId: context.workspaceId,
isCustom: false,
fields: { isCustom: false }, fields: { isCustom: false },
}, },
relations: ['dataSource', 'fields'], relations: ['dataSource', 'fields'],
}); });
const customObjectMetadataCollection =
originalObjectMetadataCollection.filter(
(objectMetadata) => objectMetadata.isCustom,
);
// Create map of object metadata & field metadata by unique identifier // Create map of object metadata & field metadata by unique identifier
const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier( const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier(
@ -71,6 +76,18 @@ export class WorkspaceSyncRelationMetadataService {
// Create standard relation metadata collection // Create standard relation metadata collection
const standardRelationMetadataCollection = const standardRelationMetadataCollection =
this.standardRelationFactory.create( this.standardRelationFactory.create(
standardObjectMetadataDefinitions,
context,
originalObjectMetadataMap,
workspaceFeatureFlagsMap,
);
const customRelationMetadataCollection =
this.standardRelationFactory.create(
customObjectMetadataCollection.map((objectMetadata) => ({
object: objectMetadata,
metadata: CustomObjectMetadata,
})),
context, context,
originalObjectMetadataMap, originalObjectMetadataMap,
workspaceFeatureFlagsMap, workspaceFeatureFlagsMap,
@ -78,7 +95,10 @@ export class WorkspaceSyncRelationMetadataService {
const relationComparatorResults = this.workspaceRelationComparator.compare( const relationComparatorResults = this.workspaceRelationComparator.compare(
originalRelationMetadataCollection, originalRelationMetadataCollection,
standardRelationMetadataCollection, [
...standardRelationMetadataCollection,
...customRelationMetadataCollection,
],
); );
for (const relationComparatorResult of relationComparatorResults) { for (const relationComparatorResult of relationComparatorResults) {

View File

@ -1,4 +1,6 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata';
import { DynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface';
import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator';
import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator';
import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator';
@ -57,4 +59,13 @@ export class ActivityTargetObjectMetadata extends BaseObjectMetadata {
}) })
@IsNullable() @IsNullable()
opportunity: OpportunityObjectMetadata; opportunity: OpportunityObjectMetadata;
@DynamicRelationFieldMetadata((oppositeObjectMetadata) => ({
name: oppositeObjectMetadata.nameSingular,
label: oppositeObjectMetadata.labelSingular,
description: `ActivityTarget ${oppositeObjectMetadata.labelSingular}`,
joinColumn: `${oppositeObjectMetadata.nameSingular}Id`,
icon: 'IconBuildingSkyscraper',
}))
custom: CustomObjectMetadata;
} }

View File

@ -80,7 +80,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activityTarget', inverseSideTarget: () => ActivityTargetObjectMetadata,
}) })
@IsNullable() @IsNullable()
activityTargets: ActivityTargetObjectMetadata[]; activityTargets: ActivityTargetObjectMetadata[];
@ -93,7 +93,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment', inverseSideTarget: () => AttachmentObjectMetadata,
}) })
@IsNullable() @IsNullable()
attachments: AttachmentObjectMetadata[]; attachments: AttachmentObjectMetadata[];
@ -106,7 +106,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'comment', inverseSideTarget: () => CommentObjectMetadata,
}) })
@IsNullable() @IsNullable()
comments: CommentObjectMetadata[]; comments: CommentObjectMetadata[];
@ -123,7 +123,7 @@ export class ActivityObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({ @FieldMetadata({
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
label: 'Assignee', label: 'Assignee',
description: 'Acitivity assignee', description: 'Activity assignee',
icon: 'IconUserCircle', icon: 'IconUserCircle',
joinColumn: 'assigneeId', joinColumn: 'assigneeId',
}) })

View File

@ -1,4 +1,6 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata';
import { DynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface';
import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator';
import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator';
import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator';
@ -91,4 +93,13 @@ export class AttachmentObjectMetadata extends BaseObjectMetadata {
}) })
@IsNullable() @IsNullable()
opportunity: OpportunityObjectMetadata; opportunity: OpportunityObjectMetadata;
@DynamicRelationFieldMetadata((oppositeObjectMetadata) => ({
name: oppositeObjectMetadata.nameSingular,
label: oppositeObjectMetadata.labelSingular,
description: `Attachment ${oppositeObjectMetadata.labelSingular}`,
joinColumn: `${oppositeObjectMetadata.nameSingular}Id`,
icon: 'IconBuildingSkyscraper',
}))
custom: CustomObjectMetadata;
} }

View File

@ -6,7 +6,9 @@ export abstract class BaseObjectMetadata {
@FieldMetadata({ @FieldMetadata({
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
label: 'Id', label: 'Id',
description: 'Id',
defaultValue: { type: 'uuid' }, defaultValue: { type: 'uuid' },
icon: 'Icon123',
}) })
@IsSystem() @IsSystem()
id: string; id: string;
@ -14,6 +16,7 @@ export abstract class BaseObjectMetadata {
@FieldMetadata({ @FieldMetadata({
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
label: 'Creation date', label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar', icon: 'IconCalendar',
defaultValue: { type: 'now' }, defaultValue: { type: 'now' },
}) })
@ -22,6 +25,7 @@ export abstract class BaseObjectMetadata {
@FieldMetadata({ @FieldMetadata({
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
label: 'Update date', label: 'Update date',
description: 'Update date',
icon: 'IconCalendar', icon: 'IconCalendar',
defaultValue: { type: 'now' }, defaultValue: { type: 'now' },
}) })

View File

@ -136,7 +136,7 @@ export class CalendarEventObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'calendarEventAttendee', inverseSideTarget: () => CalendarEventAttendeeObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
eventAttendees: CalendarEventAttendeeObjectMetadata[]; eventAttendees: CalendarEventAttendeeObjectMetadata[];

View File

@ -117,7 +117,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'person', inverseSideTarget: () => PersonObjectMetadata,
}) })
@IsNullable() @IsNullable()
people: PersonObjectMetadata[]; people: PersonObjectMetadata[];
@ -141,7 +141,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activityTarget', inverseSideTarget: () => ActivityTargetObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()
@ -155,7 +155,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'opportunity', inverseSideTarget: () => OpportunityObjectMetadata,
}) })
@IsNullable() @IsNullable()
opportunities: OpportunityObjectMetadata[]; opportunities: OpportunityObjectMetadata[];
@ -168,7 +168,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'favorite', inverseSideTarget: () => FavoriteObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()
@ -182,7 +182,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment', inverseSideTarget: () => AttachmentObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()

View File

@ -80,7 +80,7 @@ export class ConnectedAccountObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'messageChannel', inverseSideTarget: () => MessageChannelObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
messageChannels: MessageChannelObjectMetadata[]; messageChannels: MessageChannelObjectMetadata[];
@ -93,7 +93,7 @@ export class ConnectedAccountObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'calendarChannel', inverseSideTarget: () => CalendarChannelObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@Gate({ @Gate({

View File

@ -1,4 +1,6 @@
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
import { CustomObjectMetadata } from 'src/workspace/workspace-sync-metadata/custom-objects/custom.object-metadata';
import { DynamicRelationFieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/dynamic-field-metadata.interface';
import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator';
import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator'; import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator';
import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator';
@ -66,4 +68,13 @@ export class FavoriteObjectMetadata extends BaseObjectMetadata {
}) })
@IsNullable() @IsNullable()
opportunity: OpportunityObjectMetadata; opportunity: OpportunityObjectMetadata;
@DynamicRelationFieldMetadata((oppositeObjectMetadata) => ({
name: oppositeObjectMetadata.nameSingular,
label: oppositeObjectMetadata.labelSingular,
description: `Favorite ${oppositeObjectMetadata.labelSingular}`,
joinColumn: `${oppositeObjectMetadata.nameSingular}Id`,
icon: 'IconBuildingSkyscraper',
}))
custom: CustomObjectMetadata;
} }

View File

@ -25,7 +25,7 @@ import { ViewObjectMetadata } from 'src/workspace/workspace-sync-metadata/standa
import { WebhookObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/webhook.object-metadata'; import { WebhookObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/webhook.object-metadata';
import { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata'; import { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata';
export const standardObjectMetadataCollection = [ export const standardObjectMetadataDefinitions = [
ActivityTargetObjectMetadata, ActivityTargetObjectMetadata,
ActivityObjectMetadata, ActivityObjectMetadata,
ApiKeyObjectMetadata, ApiKeyObjectMetadata,

View File

@ -87,7 +87,7 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'messageChannelMessageAssociation', inverseSideTarget: () => MessageChannelMessageAssociationObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()

View File

@ -29,7 +29,7 @@ export class MessageThreadObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'message', inverseSideTarget: () => MessageObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()
@ -43,7 +43,7 @@ export class MessageThreadObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'messageChannelMessageAssociation', inverseSideTarget: () => MessageChannelMessageAssociationObjectMetadata,
onDelete: RelationOnDeleteAction.RESTRICT, onDelete: RelationOnDeleteAction.RESTRICT,
}) })
@IsNullable() @IsNullable()

View File

@ -86,8 +86,8 @@ export class MessageObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'messageParticipant', inverseSideTarget: () => MessageParticipantObjectMetadata,
inverseSideFieldName: 'message', inverseSideFieldKey: 'message',
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()
@ -101,7 +101,7 @@ export class MessageObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'messageChannelMessageAssociation', inverseSideTarget: () => MessageChannelMessageAssociationObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()

View File

@ -130,7 +130,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'favorite', inverseSideTarget: () => FavoriteObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()
@ -144,7 +144,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activityTarget', inverseSideTarget: () => ActivityTargetObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
@IsNullable() @IsNullable()
@ -158,7 +158,7 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment', inverseSideTarget: () => AttachmentObjectMetadata,
}) })
@IsNullable() @IsNullable()
attachments: AttachmentObjectMetadata[]; attachments: AttachmentObjectMetadata[];

View File

@ -124,8 +124,8 @@ export class PersonObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'opportunity', inverseSideTarget: () => OpportunityObjectMetadata,
inverseSideFieldName: 'pointOfContact', inverseSideFieldKey: 'pointOfContact',
}) })
pointOfContactForOpportunities: OpportunityObjectMetadata[]; pointOfContactForOpportunities: OpportunityObjectMetadata[];
@ -137,7 +137,7 @@ export class PersonObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activityTarget', inverseSideTarget: () => ActivityTargetObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
activityTargets: ActivityTargetObjectMetadata[]; activityTargets: ActivityTargetObjectMetadata[];
@ -150,7 +150,7 @@ export class PersonObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'favorite', inverseSideTarget: () => FavoriteObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
favorites: FavoriteObjectMetadata[]; favorites: FavoriteObjectMetadata[];
@ -163,7 +163,7 @@ export class PersonObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment', inverseSideTarget: () => AttachmentObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
attachments: AttachmentObjectMetadata[]; attachments: AttachmentObjectMetadata[];
@ -176,8 +176,8 @@ export class PersonObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'messageParticipant', inverseSideTarget: () => MessageParticipantObjectMetadata,
inverseSideFieldName: 'person', inverseSideFieldKey: 'person',
}) })
messageParticipants: MessageParticipantObjectMetadata[]; messageParticipants: MessageParticipantObjectMetadata[];
@ -189,8 +189,8 @@ export class PersonObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'calendarEventAttendee', inverseSideTarget: () => CalendarEventAttendeeObjectMetadata,
inverseSideFieldName: 'person', inverseSideFieldKey: 'person',
}) })
@Gate({ @Gate({
featureFlag: 'IS_CALENDAR_ENABLED', featureFlag: 'IS_CALENDAR_ENABLED',

View File

@ -52,7 +52,7 @@ export class PipelineStepObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'opportunity', inverseSideTarget: () => OpportunityObjectMetadata,
}) })
@IsNullable() @IsNullable()
opportunities: OpportunityObjectMetadata[]; opportunities: OpportunityObjectMetadata[];

View File

@ -57,7 +57,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'viewField', inverseSideTarget: () => ViewFieldObjectMetadata,
}) })
@IsNullable() @IsNullable()
viewFields: ViewFieldObjectMetadata[]; viewFields: ViewFieldObjectMetadata[];
@ -70,7 +70,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'viewFilter', inverseSideTarget: () => ViewFilterObjectMetadata,
}) })
@IsNullable() @IsNullable()
viewFilters: ViewFilterObjectMetadata[]; viewFilters: ViewFilterObjectMetadata[];
@ -83,7 +83,7 @@ export class ViewObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'viewSort', inverseSideTarget: () => ViewSortObjectMetadata,
}) })
@IsNullable() @IsNullable()
viewSorts: ViewSortObjectMetadata[]; viewSorts: ViewSortObjectMetadata[];

View File

@ -6,7 +6,6 @@ import {
} from 'src/metadata/relation-metadata/relation-metadata.entity'; } from 'src/metadata/relation-metadata/relation-metadata.entity';
import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator'; import { FieldMetadata } from 'src/workspace/workspace-sync-metadata/decorators/field-metadata.decorator';
import { Gate } from 'src/workspace/workspace-sync-metadata/decorators/gate.decorator'; import { Gate } from 'src/workspace/workspace-sync-metadata/decorators/gate.decorator';
import { IsNullable } from 'src/workspace/workspace-sync-metadata/decorators/is-nullable.decorator';
import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator'; import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-system.decorator';
import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator'; import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator';
import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator'; import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorators/relation-metadata.decorator';
@ -89,8 +88,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activity', inverseSideTarget: () => ActivityObjectMetadata,
inverseSideFieldName: 'author', inverseSideFieldKey: 'author',
}) })
authoredActivities: ActivityObjectMetadata[]; authoredActivities: ActivityObjectMetadata[];
@ -102,10 +101,11 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'activity', inverseSideTarget: () => ActivityObjectMetadata,
inverseSideFieldName: 'assignee', inverseSideFieldKey: 'assignee',
}) })
@IsNullable() assignedActivities: ActivityObjectMetadata[];
@FieldMetadata({ @FieldMetadata({
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
label: 'Favorites', label: 'Favorites',
@ -114,7 +114,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'favorite', inverseSideTarget: () => FavoriteObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE, onDelete: RelationOnDeleteAction.CASCADE,
}) })
favorites: FavoriteObjectMetadata[]; favorites: FavoriteObjectMetadata[];
@ -127,8 +127,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'company', inverseSideTarget: () => CompanyObjectMetadata,
inverseSideFieldName: 'accountOwner', inverseSideFieldKey: 'accountOwner',
}) })
accountOwnerForCompanies: CompanyObjectMetadata[]; accountOwnerForCompanies: CompanyObjectMetadata[];
@ -140,8 +140,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'attachment', inverseSideTarget: () => AttachmentObjectMetadata,
inverseSideFieldName: 'author', inverseSideFieldKey: 'author',
}) })
authoredAttachments: AttachmentObjectMetadata[]; authoredAttachments: AttachmentObjectMetadata[];
@ -153,8 +153,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'comment', inverseSideTarget: () => CommentObjectMetadata,
inverseSideFieldName: 'author', inverseSideFieldKey: 'author',
}) })
authoredComments: CommentObjectMetadata[]; authoredComments: CommentObjectMetadata[];
@ -166,8 +166,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'connectedAccount', inverseSideTarget: () => ConnectedAccountObjectMetadata,
inverseSideFieldName: 'accountOwner', inverseSideFieldKey: 'accountOwner',
}) })
connectedAccounts: ConnectedAccountObjectMetadata[]; connectedAccounts: ConnectedAccountObjectMetadata[];
@ -179,8 +179,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'messageParticipant', inverseSideTarget: () => MessageParticipantObjectMetadata,
inverseSideFieldName: 'workspaceMember', inverseSideFieldKey: 'workspaceMember',
}) })
messageParticipants: MessageParticipantObjectMetadata[]; messageParticipants: MessageParticipantObjectMetadata[];
@ -192,8 +192,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'blocklist', inverseSideTarget: () => BlocklistObjectMetadata,
inverseSideFieldName: 'workspaceMember', inverseSideFieldKey: 'workspaceMember',
}) })
blocklist: BlocklistObjectMetadata[]; blocklist: BlocklistObjectMetadata[];
@ -205,8 +205,8 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata {
}) })
@RelationMetadata({ @RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY, type: RelationMetadataType.ONE_TO_MANY,
objectName: 'calendarEventAttendee', inverseSideTarget: () => CalendarEventAttendeeObjectMetadata,
inverseSideFieldName: 'workspaceMember', inverseSideFieldKey: 'workspaceMember',
}) })
@Gate({ @Gate({
featureFlag: 'IS_CALENDAR_ENABLED', featureFlag: 'IS_CALENDAR_ENABLED',

View File

@ -1,5 +1,5 @@
import { PartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface'; import { ComputedPartialObjectMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
import { PartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { ComputedPartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { PartialRelationMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-relation-metadata.interface'; import { PartialRelationMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-relation-metadata.interface';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
@ -8,15 +8,16 @@ import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-
export class WorkspaceSyncStorage { export class WorkspaceSyncStorage {
// Object metadata // Object metadata
private readonly _objectMetadataCreateCollection: PartialObjectMetadata[] = private readonly _objectMetadataCreateCollection: ComputedPartialObjectMetadata[] =
[]; [];
private readonly _objectMetadataUpdateCollection: Partial<PartialObjectMetadata>[] = private readonly _objectMetadataUpdateCollection: Partial<ComputedPartialObjectMetadata>[] =
[]; [];
private readonly _objectMetadataDeleteCollection: ObjectMetadataEntity[] = []; private readonly _objectMetadataDeleteCollection: ObjectMetadataEntity[] = [];
// Field metadata // Field metadata
private readonly _fieldMetadataCreateCollection: PartialFieldMetadata[] = []; private readonly _fieldMetadataCreateCollection: ComputedPartialFieldMetadata[] =
private readonly _fieldMetadataUpdateCollection: (Partial<PartialFieldMetadata> & { [];
private readonly _fieldMetadataUpdateCollection: (Partial<ComputedPartialFieldMetadata> & {
id: string; id: string;
})[] = []; })[] = [];
private readonly _fieldMetadataDeleteCollection: FieldMetadataEntity[] = []; private readonly _fieldMetadataDeleteCollection: FieldMetadataEntity[] = [];
@ -67,11 +68,11 @@ export class WorkspaceSyncStorage {
return this._relationMetadataDeleteCollection; return this._relationMetadataDeleteCollection;
} }
addCreateObjectMetadata(object: PartialObjectMetadata) { addCreateObjectMetadata(object: ComputedPartialObjectMetadata) {
this._objectMetadataCreateCollection.push(object); this._objectMetadataCreateCollection.push(object);
} }
addUpdateObjectMetadata(object: Partial<PartialObjectMetadata>) { addUpdateObjectMetadata(object: Partial<ComputedPartialObjectMetadata>) {
this._objectMetadataUpdateCollection.push(object); this._objectMetadataUpdateCollection.push(object);
} }
@ -79,12 +80,12 @@ export class WorkspaceSyncStorage {
this._objectMetadataDeleteCollection.push(object); this._objectMetadataDeleteCollection.push(object);
} }
addCreateFieldMetadata(field: PartialFieldMetadata) { addCreateFieldMetadata(field: ComputedPartialFieldMetadata) {
this._fieldMetadataCreateCollection.push(field); this._fieldMetadataCreateCollection.push(field);
} }
addUpdateFieldMetadata( addUpdateFieldMetadata(
field: Partial<PartialFieldMetadata> & { id: string }, field: Partial<ComputedPartialFieldMetadata> & { id: string },
) { ) {
this._fieldMetadataUpdateCollection.push(field); this._fieldMetadataUpdateCollection.push(field);
} }

View File

@ -0,0 +1,72 @@
import {
ComputedPartialObjectMetadata,
PartialObjectMetadata,
} from 'src/workspace/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
import { ComputedPartialFieldMetadata } from 'src/workspace/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { generateTargetColumnMap } from 'src/metadata/field-metadata/utils/generate-target-column-map.util';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
export const computeStandardObject = (
standardObjectMetadata: PartialObjectMetadata,
originalObjectMetadata: ObjectMetadataEntity,
customObjectMetadataCollection: ObjectMetadataEntity[] = [],
): ComputedPartialObjectMetadata => {
const fields: ComputedPartialFieldMetadata[] = [];
for (const partialFieldMetadata of standardObjectMetadata.fields) {
if ('paramsFactory' in partialFieldMetadata) {
// Compute standard fields of custom object
for (const customObjectMetadata of customObjectMetadataCollection) {
const { paramsFactory, ...rest } = partialFieldMetadata;
const { joinColumn, ...data } = paramsFactory(customObjectMetadata);
// Relation
fields.push({
...data,
...rest,
defaultValue: null,
targetColumnMap: {},
});
// Foreign key
fields.push({
...rest,
name: joinColumn,
type: FieldMetadataType.UUID,
label: `${data.label} ID (foreign key)`,
description: `${data.description} id foreign key`,
defaultValue: null,
icon: undefined,
targetColumnMap: generateTargetColumnMap(
FieldMetadataType.UUID,
rest.isCustom,
joinColumn,
),
isSystem: true,
});
}
} else {
const labelText =
typeof partialFieldMetadata.label === 'function'
? partialFieldMetadata.label(originalObjectMetadata)
: partialFieldMetadata.label;
const descriptionText =
typeof partialFieldMetadata.description === 'function'
? partialFieldMetadata.description(originalObjectMetadata)
: partialFieldMetadata.description;
fields.push({
...partialFieldMetadata,
label: labelText,
description: descriptionText,
});
}
}
return {
...standardObjectMetadata,
fields,
};
};

View File

@ -13,6 +13,7 @@ import { workspaceSyncMetadataComparators } from 'src/workspace/workspace-sync-m
import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service'; import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service';
import { WorkspaceSyncObjectMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service'; import { WorkspaceSyncObjectMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service';
import { WorkspaceSyncRelationMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service'; import { WorkspaceSyncRelationMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service';
import { WorkspaceSyncFieldMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migration-builder/workspace-migration-builder.module'; import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migration-builder/workspace-migration-builder.module';
@Module({ @Module({
@ -36,6 +37,7 @@ import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migrati
WorkspaceMetadataUpdaterService, WorkspaceMetadataUpdaterService,
WorkspaceSyncObjectMetadataService, WorkspaceSyncObjectMetadataService,
WorkspaceSyncRelationMetadataService, WorkspaceSyncRelationMetadataService,
WorkspaceSyncFieldMetadataService,
WorkspaceSyncMetadataService, WorkspaceSyncMetadataService,
], ],
exports: [WorkspaceSyncMetadataService], exports: [WorkspaceSyncMetadataService],

View File

@ -9,9 +9,14 @@ import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migrati
import { FeatureFlagFactory } from 'src/workspace/workspace-sync-metadata/factories/feature-flags.factory'; import { FeatureFlagFactory } from 'src/workspace/workspace-sync-metadata/factories/feature-flags.factory';
import { WorkspaceSyncObjectMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service'; import { WorkspaceSyncObjectMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service';
import { WorkspaceSyncRelationMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service'; import { WorkspaceSyncRelationMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service';
import { WorkspaceSyncFieldMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage';
import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity';
interface SynchronizeOptions {
applyChanges?: boolean;
}
@Injectable() @Injectable()
export class WorkspaceSyncMetadataService { export class WorkspaceSyncMetadataService {
private readonly logger = new Logger(WorkspaceSyncMetadataService.name); private readonly logger = new Logger(WorkspaceSyncMetadataService.name);
@ -23,6 +28,7 @@ export class WorkspaceSyncMetadataService {
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceSyncObjectMetadataService: WorkspaceSyncObjectMetadataService, private readonly workspaceSyncObjectMetadataService: WorkspaceSyncObjectMetadataService,
private readonly workspaceSyncRelationMetadataService: WorkspaceSyncRelationMetadataService, private readonly workspaceSyncRelationMetadataService: WorkspaceSyncRelationMetadataService,
private readonly workspaceSyncFieldMetadataService: WorkspaceSyncFieldMetadataService,
) {} ) {}
/** /**
@ -33,9 +39,9 @@ export class WorkspaceSyncMetadataService {
* @param dataSourceId * @param dataSourceId
* @param workspaceId * @param workspaceId
*/ */
public async syncStandardObjectsAndFieldsMetadata( public async synchronize(
context: WorkspaceSyncContext, context: WorkspaceSyncContext,
options: { applyChanges?: boolean } = { applyChanges: true }, options: SynchronizeOptions = { applyChanges: true },
): Promise<{ ): Promise<{
workspaceMigrations: WorkspaceMigrationEntity[]; workspaceMigrations: WorkspaceMigrationEntity[];
storage: WorkspaceSyncStorage; storage: WorkspaceSyncStorage;
@ -62,6 +68,7 @@ export class WorkspaceSyncMetadataService {
this.logger.log('Syncing standard objects and fields metadata'); this.logger.log('Syncing standard objects and fields metadata');
// 1 - Sync standard objects
const workspaceObjectMigrations = const workspaceObjectMigrations =
await this.workspaceSyncObjectMetadataService.synchronize( await this.workspaceSyncObjectMetadataService.synchronize(
context, context,
@ -70,6 +77,16 @@ export class WorkspaceSyncMetadataService {
workspaceFeatureFlagsMap, workspaceFeatureFlagsMap,
); );
// 2 - Sync standard fields on custom objects
const workspaceFieldMigrations =
await this.workspaceSyncFieldMetadataService.synchronize(
context,
manager,
storage,
workspaceFeatureFlagsMap,
);
// 3 - Sync standard relations on standard and custom objects
const workspaceRelationMigrations = const workspaceRelationMigrations =
await this.workspaceSyncRelationMetadataService.synchronize( await this.workspaceSyncRelationMetadataService.synchronize(
context, context,
@ -81,6 +98,7 @@ export class WorkspaceSyncMetadataService {
// Save workspace migrations into the database // Save workspace migrations into the database
workspaceMigrations = await workspaceMigrationRepository.save([ workspaceMigrations = await workspaceMigrationRepository.save([
...workspaceObjectMigrations, ...workspaceObjectMigrations,
...workspaceFieldMigrations,
...workspaceRelationMigrations, ...workspaceRelationMigrations,
]); ]);