Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: twenty orm sync #5266

Merged
merged 10 commits into from
May 15, 2024
16 changes: 7 additions & 9 deletions packages/twenty-server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import { ConfigModule } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static';
import { GraphQLModule } from '@nestjs/graphql';
import { DevtoolsModule } from '@nestjs/devtools-integration';

import { existsSync } from 'fs';
import { join } from 'path';
Expand All @@ -20,7 +19,6 @@ import { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.mo
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module';
import { GraphQLConfigService } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware';

Expand All @@ -30,13 +28,13 @@ import { IntegrationsModule } from './engine/integrations/integrations.module';
@Module({
imports: [
// Nest.js devtools, use devtools.nestjs.com to debug
DevtoolsModule.registerAsync({
useFactory: (environmentService: EnvironmentService) => ({
http: environmentService.get('DEBUG_MODE'),
port: environmentService.get('DEBUG_PORT'),
}),
inject: [EnvironmentService],
}),
// DevtoolsModule.registerAsync({
// useFactory: (environmentService: EnvironmentService) => ({
// http: environmentService.get('DEBUG_MODE'),
// port: environmentService.get('DEBUG_PORT'),
// }),
// inject: [EnvironmentService],
// }),
ConfigModule.forRoot({
isGlobal: true,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/ti
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { HealthModule } from 'src/engine/core-modules/health/health.module';

import { ClientConfigModule } from './client-config/client-config.module';
import { FileModule } from './file/file.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { FileModule } from './file/file.module';
import { ClientConfigModule } from './client-config/client-config.module';

@Module({
imports: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { WorkspaceIsPimaryField } from 'src/engine/twenty-orm/decorators/workspa
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';

export abstract class BaseObjectMetadata {
export abstract class BaseWorkspaceEntity {
@WorkspaceField({
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
type: FieldMetadataType.UUID,
Expand All @@ -25,7 +25,6 @@ export abstract class BaseObjectMetadata {
icon: 'IconCalendar',
defaultValue: 'now',
})
@WorkspaceIsSystem()
createdAt: Date;

@WorkspaceField({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { BaseCustomObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/base-custom-object-metadata.decorator';
import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator';
import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator';
import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { ActivityTargetObjectMetadata } from 'src/modules/activity/standard-objects/activity-target.object-metadata';
import { RelationMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/relation-metadata.decorator';
import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/favorite.object-metadata';
import { AttachmentObjectMetadata } from 'src/modules/attachment/standard-objects/attachment.object-metadata';
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
import { WorkspaceCustomObject } from 'src/engine/twenty-orm/decorators/workspace-custom-object.decorator';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';

@BaseCustomObjectMetadata()
export class CustomObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
@WorkspaceCustomObject()
export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
label: 'Name',
description: 'Name',
Expand All @@ -27,81 +27,68 @@ export class CustomObjectMetadata extends BaseObjectMetadata {
})
name: string;

@FieldMetadata({
@WorkspaceField({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
label: 'Position',
description: 'Position',
type: FieldMetadataType.POSITION,
icon: 'IconHierarchy2',
})
@IsNullable()
@IsSystem()
@WorkspaceIsNullable()
@WorkspaceIsSystem()
position: number;

@FieldMetadata({
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.activityTargets,
type: FieldMetadataType.RELATION,
label: 'Activities',
type: RelationMetadataType.ONE_TO_MANY,
description: (objectMetadata) =>
`Activities tied to the ${objectMetadata.labelSingular}`,
icon: 'IconCheckbox',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => ActivityTargetObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
@IsNullable()
@WorkspaceIsNullable()
activityTargets: ActivityTargetObjectMetadata[];

@FieldMetadata({
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.favorites,
type: FieldMetadataType.RELATION,
label: 'Favorites',
type: RelationMetadataType.ONE_TO_MANY,
description: (objectMetadata) =>
`Favorites tied to the ${objectMetadata.labelSingular}`,
icon: 'IconHeart',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => FavoriteObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
@IsNullable()
@IsSystem()
@WorkspaceIsNullable()
@WorkspaceIsSystem()
favorites: FavoriteObjectMetadata[];

@FieldMetadata({
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.attachments,
type: FieldMetadataType.RELATION,
label: 'Attachments',
type: RelationMetadataType.ONE_TO_MANY,
description: (objectMetadata) =>
`Attachments tied to the ${objectMetadata.labelSingular}`,
icon: 'IconFileImport',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => AttachmentObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
@IsNullable()
@WorkspaceIsNullable()
attachments: AttachmentObjectMetadata[];

@FieldMetadata({
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.timelineActivities,
type: FieldMetadataType.RELATION,
label: 'Timeline Activities',
type: RelationMetadataType.ONE_TO_MANY,
description: (objectMetadata) =>
`Timeline Activities tied to the ${objectMetadata.labelSingular}`,

icon: 'IconIconTimelineEvent',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => TimelineActivityObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
@IsNullable()
@IsSystem()
@WorkspaceIsNullable()
@WorkspaceIsSystem()
timelineActivities: TimelineActivityObjectMetadata[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';

export function WorkspaceCustomObject(): ClassDecorator {
return (target) => {
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',
target,
);

metadataArgsStorage.addExtendedEntities({
target,
gate,
});
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ObjectType } from 'typeorm';

import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface';

import { TypedReflect } from 'src/utils/typed-reflect';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';

interface WorkspaceBaseDynamicRelationOptions<TClass> {
type: RelationMetadataType;
argsFactory: WorkspaceDynamicRelationMetadataArgsFactory;
inverseSideTarget: () => ObjectType<TClass>;
inverseSideFieldKey?: keyof TClass;
onDelete?: RelationOnDeleteAction;
}

export function WorkspaceDynamicRelation<TClass extends object>(
args: WorkspaceBaseDynamicRelationOptions<TClass>,
): PropertyDecorator {
return (target, propertyKey) => {
const isSystem =
TypedReflect.getMetadata(
'workspace:is-system-metadata-args',
target,
propertyKey.toString(),
) ?? false;
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',
target,
propertyKey.toString(),
);

metadataArgsStorage.addDynamicRelations({
target: target.constructor,
argsFactory: args.argsFactory,
type: args.type,
inverseSideTarget: args.inverseSideTarget,
inverseSideFieldKey: args.inverseSideFieldKey as string | undefined,
onDelete: args.onDelete,
isSystem,
isNullable: true,
isPrimary: false,
gate,
});
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';

export interface WorkspaceFieldOptions<
T extends FieldMetadataType | 'default',
Expand All @@ -15,44 +16,50 @@ export interface WorkspaceFieldOptions<
description?: string | ((objectMetadata: ObjectMetadataEntity) => string);
icon?: string;
defaultValue?: FieldMetadataDefaultValue<T>;
joinColumn?: string;
options?: FieldMetadataOptions<T>;
}

export function WorkspaceField<T extends FieldMetadataType>(
options: WorkspaceFieldOptions<T>,
): PropertyDecorator {
return (object, propertyKey) => {
const isPrimary = TypedReflect.getMetadata(
'workspace:is-primary-field-metadata-args',
object,
propertyKey.toString(),
);
const isNullable = TypedReflect.getMetadata(
'workspace:is-nullable-metadata-args',
object,
propertyKey.toString(),
);
const isSystem = TypedReflect.getMetadata(
'workspace:is-system-metadata-args',
object,
propertyKey.toString(),
);
const isPrimary =
TypedReflect.getMetadata(
'workspace:is-primary-field-metadata-args',
object,
propertyKey.toString(),
) ?? false;
const isNullable =
TypedReflect.getMetadata(
'workspace:is-nullable-metadata-args',
object,
propertyKey.toString(),
) ?? false;
const isSystem =
TypedReflect.getMetadata(
'workspace:is-system-metadata-args',
object,
propertyKey.toString(),
) ?? false;
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',
object,
propertyKey.toString(),
);
const defaultValue = (options.defaultValue ??
generateDefaultValue(
options.type,
)) as FieldMetadataDefaultValue<'default'> | null;

metadataArgsStorage.fields.push({
metadataArgsStorage.addFields({
target: object.constructor,
standardId: options.standardId,
name: propertyKey.toString(),
label: options.label,
type: options.type,
description: options.description,
icon: options.icon,
defaultValue: options.defaultValue,
defaultValue,
options: options.options,
isPrimary,
isNullable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
import { TypedReflect } from 'src/utils/typed-reflect';

interface WorkspaceObjectOptions {
interface WorkspaceEntityOptions {
standardId: string;
namePlural: string;
labelSingular: string;
Expand All @@ -11,26 +11,25 @@ interface WorkspaceObjectOptions {
icon?: string;
}

export function WorkspaceObject(
options: WorkspaceObjectOptions,
export function WorkspaceEntity(
options: WorkspaceEntityOptions,
): ClassDecorator {
return (target) => {
const isAuditLogged =
TypedReflect.getMetadata(
'workspace:is-audit-logged-metadata-args',
target,
) ?? true;
const isSystem = TypedReflect.getMetadata(
'workspace:is-system-metadata-args',
target,
);
const isSystem =
TypedReflect.getMetadata('workspace:is-system-metadata-args', target) ??
false;
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',
target,
);
const objectName = convertClassNameToObjectMetadataName(target.name);

metadataArgsStorage.objects.push({
metadataArgsStorage.addEntities({
target,
standardId: options.standardId,
nameSingular: objectName,
Expand Down