diff --git a/packages/twenty-server/.swcrc b/packages/twenty-server/.swcrc index e51680b8a..ad272e265 100644 --- a/packages/twenty-server/.swcrc +++ b/packages/twenty-server/.swcrc @@ -1,14 +1,14 @@ { - "$schema": "https://json.schemastore.org/swcrc", - "sourceMaps": true, - "jsc": { - "parser": { - "syntax": "typescript", - "decorators": true, - "dynamicImport": true - }, - "baseUrl": "./../../" + "$schema": "https://json.schemastore.org/swcrc", + "sourceMaps": true, + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true }, - "minify": false - } + "baseUrl": "./../../" + }, + "minify": false +} diff --git a/packages/twenty-server/src/database/typeorm/typeorm.module.ts b/packages/twenty-server/src/database/typeorm/typeorm.module.ts index bda843eae..957f6ca59 100644 --- a/packages/twenty-server/src/database/typeorm/typeorm.module.ts +++ b/packages/twenty-server/src/database/typeorm/typeorm.module.ts @@ -29,9 +29,7 @@ const coreTypeORMFactory = async (): Promise => ({ useFactory: coreTypeORMFactory, name: 'core', }), - TwentyORMModule.register({ - workspaceEntities: ['dist/src/**/*.workspace-entity{.ts,.js}'], - }), + TwentyORMModule.register({}), EnvironmentModule, ], providers: [TypeORMService], diff --git a/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts index 29fde8f18..f3554cdde 100644 --- a/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts @@ -2,11 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { WorkspaceSchemaStorageService } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory'; import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; describe('WorkspaceSchemaFactory', () => { let service: WorkspaceSchemaFactory; @@ -36,7 +36,7 @@ describe('WorkspaceSchemaFactory', () => { useValue: {}, }, { - provide: WorkspaceSchemaStorageService, + provide: WorkspaceCacheStorageService, useValue: {}, }, ], diff --git a/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts b/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts index 0ea069944..33c24c621 100644 --- a/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { WorkspaceSchemaStorageModule } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.module'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { WorkspaceSchemaBuilderModule } from 'src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module'; import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module'; @@ -20,7 +20,7 @@ import { WorkspaceSchemaFactory } from './workspace-schema.factory'; MetadataEngineModule, WorkspaceSchemaBuilderModule, WorkspaceResolverBuilderModule, - WorkspaceSchemaStorageModule, + WorkspaceCacheStorageModule, ], providers: [WorkspaceSchemaFactory, ScalarsExplorerService], exports: [WorkspaceSchemaFactory], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.module.ts deleted file mode 100644 index 8d7d4d483..000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; -import { WorkspaceSchemaStorageService } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; - -@Module({ - imports: [ObjectMetadataModule, WorkspaceCacheVersionModule], - providers: [WorkspaceSchemaStorageService], - exports: [WorkspaceSchemaStorageService], -}) -export class WorkspaceSchemaStorageModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 608e7a952..531baba6a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -5,7 +5,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { gql } from 'graphql-tag'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { WorkspaceSchemaStorageService } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; @@ -20,7 +20,7 @@ export class WorkspaceSchemaFactory { private readonly scalarsExplorerService: ScalarsExplorerService, private readonly workspaceGraphQLSchemaFactory: WorkspaceGraphQLSchemaFactory, private readonly workspaceResolverFactory: WorkspaceResolverFactory, - private readonly workspaceSchemaStorageService: WorkspaceSchemaStorageService, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, ) {} async createGraphQLSchema( @@ -42,11 +42,11 @@ export class WorkspaceSchemaFactory { } // Validate cache version - await this.workspaceSchemaStorageService.validateCacheVersion(workspaceId); + await this.workspaceCacheStorageService.validateCacheVersion(workspaceId); // Get object metadata from cache let objectMetadataCollection = - await this.workspaceSchemaStorageService.getObjectMetadataCollection( + await this.workspaceCacheStorageService.getObjectMetadataCollection( workspaceId, ); @@ -55,7 +55,7 @@ export class WorkspaceSchemaFactory { objectMetadataCollection = await this.objectMetadataService.findManyWithinWorkspace(workspaceId); - await this.workspaceSchemaStorageService.setObjectMetadataCollection( + await this.workspaceCacheStorageService.setObjectMetadataCollection( workspaceId, objectMetadataCollection, ); @@ -63,9 +63,9 @@ export class WorkspaceSchemaFactory { // Get typeDefs from cache let typeDefs = - await this.workspaceSchemaStorageService.getTypeDefs(workspaceId); + await this.workspaceCacheStorageService.getTypeDefs(workspaceId); let usedScalarNames = - await this.workspaceSchemaStorageService.getUsedScalarNames(workspaceId); + await this.workspaceCacheStorageService.getUsedScalarNames(workspaceId); // If typeDefs are not cached, generate them if (!typeDefs || !usedScalarNames) { @@ -79,11 +79,11 @@ export class WorkspaceSchemaFactory { this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema); typeDefs = printSchema(autoGeneratedSchema); - await this.workspaceSchemaStorageService.setTypeDefs( + await this.workspaceCacheStorageService.setTypeDefs( workspaceId, typeDefs, ); - await this.workspaceSchemaStorageService.setUsedScalarNames( + await this.workspaceCacheStorageService.setUsedScalarNames( workspaceId, usedScalarNames, ); diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts index 1ef6370ab..a12c745fc 100644 --- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts +++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts @@ -1,24 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { Any } from 'typeorm'; import omit from 'lodash.omit'; +import { Any } from 'typeorm'; import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants'; import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto'; -import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; -import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; +import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; @Injectable() export class TimelineCalendarEventService { - constructor( - @InjectWorkspaceRepository(CalendarEventWorkspaceEntity) - private readonly calendarEventRepository: WorkspaceRepository, - @InjectWorkspaceRepository(PersonWorkspaceEntity) - private readonly personRepository: WorkspaceRepository, - ) {} + constructor(private readonly twentyORMManager: TwentyORMManager) {} // TODO: Align return type with the entities to avoid mapping async getCalendarEventsFromPersonIds( @@ -28,7 +22,12 @@ export class TimelineCalendarEventService { ): Promise { const offset = (page - 1) * pageSize; - const calendarEventIds = await this.calendarEventRepository.find({ + const calendarEventRepository = + await this.twentyORMManager.getRepository( + 'calendarEvent', + ); + + const calendarEventIds = await calendarEventRepository.find({ where: { calendarEventParticipants: { personId: Any(personIds), @@ -55,7 +54,7 @@ export class TimelineCalendarEventService { } // We've split the query into two parts, because we want to fetch all the participants without any filtering - const [events, total] = await this.calendarEventRepository.findAndCount({ + const [events, total] = await calendarEventRepository.findAndCount({ where: { id: Any(ids), }, @@ -131,7 +130,12 @@ export class TimelineCalendarEventService { page = 1, pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE, ): Promise { - const personIds = await this.personRepository.find({ + const personRepository = + await this.twentyORMManager.getRepository( + 'person', + ); + + const personIds = await personRepository.find({ where: { companyId, }, diff --git a/packages/twenty-server/src/engine/twenty-orm/context/load-service-with-workspace.context.ts b/packages/twenty-server/src/engine/twenty-orm/context/load-service-with-workspace.context.ts index 02af97805..144f47598 100644 --- a/packages/twenty-server/src/engine/twenty-orm/context/load-service-with-workspace.context.ts +++ b/packages/twenty-server/src/engine/twenty-orm/context/load-service-with-workspace.context.ts @@ -2,12 +2,15 @@ import { Inject, Type } from '@nestjs/common'; import { ModuleRef, createContextId } from '@nestjs/core'; import { Injector } from '@nestjs/core/injector/injector'; +import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; + export class LoadServiceWithWorkspaceContext { private readonly injector = new Injector(); constructor( @Inject(ModuleRef) private readonly moduleRef: ModuleRef, + private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, ) {} async load(service: T, workspaceId: string): Promise { @@ -21,10 +24,12 @@ export class LoadServiceWithWorkspaceContext { } const contextId = createContextId(); + const cacheVersion = + await this.workspaceCacheVersionService.getVersion(workspaceId); if (this.moduleRef.registerRequestByContextId) { this.moduleRef.registerRequestByContextId( - { req: { workspaceId } }, + { req: { workspaceId, cacheVersion } }, contextId, ); } diff --git a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts index 71ca21909..076ef7f7e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -1,24 +1,39 @@ import { DataSource, - EntityManager, + DataSourceOptions, EntityTarget, ObjectLiteral, QueryRunner, } from 'typeorm'; +import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; + import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/entity.manager'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; export class WorkspaceDataSource extends DataSource { + readonly internalContext: WorkspaceInternalContext; readonly manager: WorkspaceEntityManager; + constructor( + internalContext: WorkspaceInternalContext, + options: DataSourceOptions, + ) { + super(options); + this.internalContext = internalContext; + // Recreate manager after internalContext has been initialized + this.manager = this.createEntityManager(); + } + override getRepository( target: EntityTarget, ): WorkspaceRepository { return this.manager.getRepository(target); } - override createEntityManager(queryRunner?: QueryRunner): EntityManager { - return new WorkspaceEntityManager(this, queryRunner); + override createEntityManager( + queryRunner?: QueryRunner, + ): WorkspaceEntityManager { + return new WorkspaceEntityManager(this.internalContext, this, queryRunner); } } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts index 7d526046c..4be629755 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts @@ -1,8 +1,27 @@ -import { EntityManager, EntityTarget, ObjectLiteral } from 'typeorm'; +import { + DataSource, + EntityManager, + EntityTarget, + ObjectLiteral, + QueryRunner, +} from 'typeorm'; + +import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; export class WorkspaceEntityManager extends EntityManager { + private readonly internalContext: WorkspaceInternalContext; + + constructor( + internalContext: WorkspaceInternalContext, + connection: DataSource, + queryRunner?: QueryRunner, + ) { + super(connection, queryRunner); + this.internalContext = internalContext; + } + override getRepository( target: EntityTarget, ): WorkspaceRepository { @@ -14,6 +33,7 @@ export class WorkspaceEntityManager extends EntityManager { } const newRepository = new WorkspaceRepository( + this.internalContext, target, this, this.queryRunner, diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts index 83106f92f..6fa77853b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts @@ -2,18 +2,17 @@ import { Injectable } from '@nestjs/common'; import { ColumnType, EntitySchemaColumnOptions } from 'typeorm'; -import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface'; -import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface'; -import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface'; - import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; type EntitySchemaColumnMap = { [key: string]: EntitySchemaColumnOptions; @@ -22,17 +21,45 @@ type EntitySchemaColumnMap = { @Injectable() export class EntitySchemaColumnFactory { create( - fieldMetadataArgsCollection: WorkspaceFieldMetadataArgs[], - relationMetadataArgsCollection: WorkspaceRelationMetadataArgs[], - joinColumnsMetadataArgsCollection: WorkspaceJoinColumnsMetadataArgs[], + workspaceId: string, + fieldMetadataCollection: FieldMetadataEntity[], ): EntitySchemaColumnMap { let entitySchemaColumnMap: EntitySchemaColumnMap = {}; - for (const fieldMetadataArgs of fieldMetadataArgsCollection) { - const key = fieldMetadataArgs.name; + for (const fieldMetadata of fieldMetadataCollection) { + const key = fieldMetadata.name; - if (isCompositeFieldMetadataType(fieldMetadataArgs.type)) { - const compositeColumns = this.createCompositeColumns(fieldMetadataArgs); + if (isRelationFieldMetadataType(fieldMetadata.type)) { + const relationMetadata = + fieldMetadata.fromRelationMetadata ?? + fieldMetadata.toRelationMetadata; + + if (!relationMetadata) { + throw new Error( + `Relation metadata is missing for field ${fieldMetadata.name}`, + ); + } + + const joinColumnKey = fieldMetadata.name + 'Id'; + const joinColumn = fieldMetadataCollection.find( + (field) => field.name === joinColumnKey, + ) + ? joinColumnKey + : null; + + if (joinColumn) { + entitySchemaColumnMap[joinColumn] = { + name: joinColumn, + type: 'uuid', + nullable: fieldMetadata.isNullable, + }; + } + + continue; + } + + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + const compositeColumns = this.createCompositeColumns(fieldMetadata); entitySchemaColumnMap = { ...entitySchemaColumnMap, @@ -42,39 +69,23 @@ export class EntitySchemaColumnFactory { continue; } - const columnType = fieldMetadataTypeToColumnType(fieldMetadataArgs.type); - const defaultValue = serializeDefaultValue( - fieldMetadataArgs.defaultValue, - ); + const columnType = fieldMetadataTypeToColumnType(fieldMetadata.type); + const defaultValue = serializeDefaultValue(fieldMetadata.defaultValue); entitySchemaColumnMap[key] = { name: key, type: columnType as ColumnType, - primary: fieldMetadataArgs.isPrimary, - nullable: fieldMetadataArgs.isNullable, + // TODO: We should double check that + primary: key === 'id', + nullable: fieldMetadata.isNullable, createDate: key === 'createdAt', updateDate: key === 'updatedAt', - array: fieldMetadataArgs.type === FieldMetadataType.MULTI_SELECT, + array: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, default: defaultValue, }; - for (const relationMetadataArgs of relationMetadataArgsCollection) { - const joinColumn = getJoinColumn( - joinColumnsMetadataArgsCollection, - relationMetadataArgs, - ); - - if (joinColumn) { - entitySchemaColumnMap[joinColumn] = { - name: joinColumn, - type: 'uuid', - nullable: relationMetadataArgs.isNullable, - }; - } - } - - if (isEnumFieldMetadataType(fieldMetadataArgs.type)) { - const values = fieldMetadataArgs.options?.map((option) => option.value); + if (isEnumFieldMetadataType(fieldMetadata.type)) { + const values = fieldMetadata.options?.map((option) => option.value); if (values && values.length > 0) { entitySchemaColumnMap[key].enum = values; @@ -86,25 +97,25 @@ export class EntitySchemaColumnFactory { } private createCompositeColumns( - fieldMetadataArgs: WorkspaceFieldMetadataArgs, + fieldMetadata: FieldMetadataEntity, ): EntitySchemaColumnMap { const entitySchemaColumnMap: EntitySchemaColumnMap = {}; - const compositeType = compositeTypeDefintions.get(fieldMetadataArgs.type); + const compositeType = compositeTypeDefintions.get(fieldMetadata.type); if (!compositeType) { throw new Error( - `Composite type ${fieldMetadataArgs.type} is not defined in compositeTypeDefintions`, + `Composite type ${fieldMetadata.type} is not defined in compositeTypeDefintions`, ); } for (const compositeProperty of compositeType.properties) { const columnName = computeCompositeColumnName( - fieldMetadataArgs.name, + fieldMetadata.name, compositeProperty, ); const columnType = fieldMetadataTypeToColumnType(compositeProperty.type); const defaultValue = serializeDefaultValue( - fieldMetadataArgs.defaultValue?.[compositeProperty.name], + fieldMetadata.defaultValue?.[compositeProperty.name], ); entitySchemaColumnMap[columnName] = { diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts index bc83b460b..cdd1a5585 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts @@ -1,14 +1,11 @@ import { Injectable } from '@nestjs/common'; import { EntitySchemaRelationOptions } from 'typeorm'; -import { RelationType } from 'typeorm/metadata/types/RelationTypes'; -import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface'; -import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface'; - -import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; type EntitySchemaRelationMap = { [key: string]: EntitySchemaRelationOptions; @@ -16,55 +13,45 @@ type EntitySchemaRelationMap = { @Injectable() export class EntitySchemaRelationFactory { - create( - // eslint-disable-next-line @typescript-eslint/ban-types - target: Function, - relationMetadataArgsCollection: WorkspaceRelationMetadataArgs[], - joinColumnsMetadataArgsCollection: WorkspaceJoinColumnsMetadataArgs[], - ): EntitySchemaRelationMap { + constructor( + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + ) {} + + async create( + workspaceId: string, + fieldMetadataCollection: FieldMetadataEntity[], + ): Promise { const entitySchemaRelationMap: EntitySchemaRelationMap = {}; - for (const relationMetadataArgs of relationMetadataArgsCollection) { - const objectName = convertClassNameToObjectMetadataName(target.name); - const oppositeTarget = relationMetadataArgs.inverseSideTarget(); - const oppositeObjectName = convertClassNameToObjectMetadataName( - oppositeTarget.name, - ); - const relationType = this.getRelationType(relationMetadataArgs); - const joinColumn = getJoinColumn( - joinColumnsMetadataArgsCollection, - relationMetadataArgs, + for (const fieldMetadata of fieldMetadataCollection) { + if (!isRelationFieldMetadataType(fieldMetadata.type)) { + continue; + } + + const relationMetadata = + fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; + + if (!relationMetadata) { + throw new Error( + `Relation metadata is missing for field ${fieldMetadata.name}`, + ); + } + + const relationDetails = await determineRelationDetails( + workspaceId, + fieldMetadata, + relationMetadata, + this.workspaceCacheStorageService, ); - entitySchemaRelationMap[relationMetadataArgs.name] = { - type: relationType, - target: oppositeObjectName, - inverseSide: relationMetadataArgs.inverseSideFieldKey ?? objectName, - joinColumn: joinColumn - ? { - name: joinColumn, - } - : undefined, + entitySchemaRelationMap[fieldMetadata.name] = { + type: relationDetails.relationType, + target: relationDetails.target, + inverseSide: relationDetails.inverseSide, + joinColumn: relationDetails.joinColumn, }; } return entitySchemaRelationMap; } - - private getRelationType( - relationMetadataArgs: WorkspaceRelationMetadataArgs, - ): RelationType { - switch (relationMetadataArgs.type) { - case RelationMetadataType.ONE_TO_MANY: - return 'one-to-many'; - case RelationMetadataType.MANY_TO_ONE: - return 'many-to-one'; - case RelationMetadataType.ONE_TO_ONE: - return 'one-to-one'; - case RelationMetadataType.MANY_TO_MANY: - return 'many-to-many'; - default: - throw new Error('Invalid relation type'); - } - } } diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts index 75f63dcb2..daf83b09f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts @@ -1,53 +1,79 @@ -import { Injectable, Type } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { EntitySchema } from 'typeorm'; import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory'; -import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; -import { ObjectLiteralStorage } from 'src/engine/twenty-orm/storage/object-literal.storage'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @Injectable() export class EntitySchemaFactory { constructor( private readonly entitySchemaColumnFactory: EntitySchemaColumnFactory, private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, ) {} - create(target: Type): EntitySchema { - const entityMetadataArgs = metadataArgsStorage.filterEntities(target); + async create( + workspaceId: string, + objectMetadata: ObjectMetadataEntity, + ): Promise; - if (!entityMetadataArgs) { - throw new Error('Entity metadata args are missing on this target'); + async create( + workspaceId: string, + objectMetadataName: string, + ): Promise; + + async create( + workspaceId: string, + objectMetadataOrObjectMetadataName: ObjectMetadataEntity | string, + ): Promise { + let objectMetadata: ObjectMetadataEntity | null = + typeof objectMetadataOrObjectMetadataName !== 'string' + ? objectMetadataOrObjectMetadataName + : null; + + if (typeof objectMetadataOrObjectMetadataName === 'string') { + objectMetadata = + (await this.workspaceCacheStorageService.getObjectMetadata( + workspaceId, + (objectMetadata) => + objectMetadata.nameSingular === objectMetadataOrObjectMetadataName, + )) ?? null; } - const fieldMetadataArgsCollection = - metadataArgsStorage.filterFields(target); - const joinColumnsMetadataArgsCollection = - metadataArgsStorage.filterJoinColumns(target); - const relationMetadataArgsCollection = - metadataArgsStorage.filterRelations(target); + if (!objectMetadata) { + throw new Error('Object metadata not found'); + } const columns = this.entitySchemaColumnFactory.create( - fieldMetadataArgsCollection, - relationMetadataArgsCollection, - joinColumnsMetadataArgsCollection, + workspaceId, + objectMetadata.fields, ); - const relations = this.entitySchemaRelationFactory.create( - target, - relationMetadataArgsCollection, - joinColumnsMetadataArgsCollection, + const relations = await this.entitySchemaRelationFactory.create( + workspaceId, + objectMetadata.fields, ); const entitySchema = new EntitySchema({ - name: entityMetadataArgs.nameSingular, - tableName: entityMetadataArgs.nameSingular, + name: objectMetadata.nameSingular, + tableName: computeTableName( + objectMetadata.nameSingular, + objectMetadata.isCustom, + ), columns, relations, }); - ObjectLiteralStorage.setObjectLiteral(entitySchema, target); + WorkspaceEntitiesStorage.setEntitySchema( + workspaceId, + objectMetadata.nameSingular, + entitySchema, + ); return entitySchema; } diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/index.ts b/packages/twenty-server/src/engine/twenty-orm/factories/index.ts index cb8ce526f..6f8a8477b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/index.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/index.ts @@ -1,7 +1,7 @@ import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; -import { ScopedWorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-datasource.factory'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; export const entitySchemaFactories = [ @@ -9,5 +9,5 @@ export const entitySchemaFactories = [ EntitySchemaRelationFactory, EntitySchemaFactory, WorkspaceDatasourceFactory, - ScopedWorkspaceDatasourceFactory, + ScopedWorkspaceContextFactory, ]; diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts new file mode 100644 index 000000000..88603807a --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts @@ -0,0 +1,26 @@ +import { Inject, Injectable, Optional, Scope } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; + +@Injectable({ scope: Scope.REQUEST }) +export class ScopedWorkspaceContextFactory { + constructor( + @Optional() + @Inject(REQUEST) + private readonly request: Request | null, + ) {} + + public create(): { + workspaceId: string | null; + cacheVersion: string | null; + } { + const workspaceId: string | undefined = + this.request?.['req']?.['workspaceId']; + const cacheVersion: string | undefined = + this.request?.['req']?.['cacheVersion']; + + return { + workspaceId: workspaceId ?? null, + cacheVersion: cacheVersion ?? null, + }; + } +} diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts deleted file mode 100644 index 096be2194..000000000 --- a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Inject, Injectable, Optional, Scope } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; - -import { EntitySchema } from 'typeorm'; - -import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; - -@Injectable({ scope: Scope.REQUEST }) -export class ScopedWorkspaceDatasourceFactory { - constructor( - @Optional() - @Inject(REQUEST) - private readonly request: Request | null, - private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory, - ) {} - - public async create(entities: EntitySchema[]) { - const workspaceId: string | undefined = - this.request?.['req']?.['workspaceId']; - - if (!workspaceId) { - return null; - } - - return this.workspaceDataSourceFactory.create(entities, workspaceId); - } -} diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 2c733961a..0408e74c5 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -4,27 +4,21 @@ import { EntitySchema } from 'typeorm'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { DataSourceStorage } from 'src/engine/twenty-orm/storage/data-source.storage'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @Injectable() export class WorkspaceDatasourceFactory { constructor( private readonly dataSourceService: DataSourceService, private readonly environmentService: EnvironmentService, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, ) {} public async create( entities: EntitySchema[], workspaceId: string, ): Promise { - const storedWorkspaceDataSource = - DataSourceStorage.getDataSource(workspaceId); - - if (storedWorkspaceDataSource) { - return storedWorkspaceDataSource; - } - const dataSourceMetadata = await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( workspaceId, @@ -34,27 +28,31 @@ export class WorkspaceDatasourceFactory { return null; } - const workspaceDataSource = new WorkspaceDataSource({ - url: - dataSourceMetadata.url ?? - this.environmentService.get('PG_DATABASE_URL'), - type: 'postgres', - logging: this.environmentService.get('DEBUG_MODE') - ? ['query', 'error'] - : ['error'], - schema: dataSourceMetadata.schema, - entities, - ssl: this.environmentService.get('PG_SSL_ALLOW_SELF_SIGNED') - ? { - rejectUnauthorized: false, - } - : undefined, - }); + const workspaceDataSource = new WorkspaceDataSource( + { + workspaceId, + workspaceCacheStorage: this.workspaceCacheStorageService, + }, + { + url: + dataSourceMetadata.url ?? + this.environmentService.get('PG_DATABASE_URL'), + type: 'postgres', + logging: this.environmentService.get('DEBUG_MODE') + ? ['query', 'error'] + : ['error'], + schema: dataSourceMetadata.schema, + entities, + ssl: this.environmentService.get('PG_SSL_ALLOW_SELF_SIGNED') + ? { + rejectUnauthorized: false, + } + : undefined, + }, + ); await workspaceDataSource.initialize(); - DataSourceStorage.setDataSource(workspaceId, workspaceDataSource); - return workspaceDataSource; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/twenty-orm-options.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/twenty-orm-options.interface.ts index b0b06f754..fa88b9fd2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/twenty-orm-options.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/twenty-orm-options.interface.ts @@ -1,10 +1,7 @@ -import { FactoryProvider, ModuleMetadata, Type } from '@nestjs/common'; +import { FactoryProvider, ModuleMetadata } from '@nestjs/common'; -import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; - -export interface TwentyORMOptions { - workspaceEntities: (Type | string)[]; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TwentyORMOptions {} export type TwentyORMModuleAsyncOptions = { useFactory: (...args: any[]) => TwentyORMOptions | Promise; diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts new file mode 100644 index 000000000..07bfeabd1 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts @@ -0,0 +1,6 @@ +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; + +export interface WorkspaceInternalContext { + workspaceId: string; + workspaceCacheStorage: WorkspaceCacheStorageService; +} diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 0f3367685..2c77677f3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -2,12 +2,15 @@ import { DeepPartial, DeleteResult, EntityManager, + EntitySchema, + EntityTarget, FindManyOptions, FindOneOptions, FindOptionsWhere, InsertResult, ObjectId, ObjectLiteral, + QueryRunner, RemoveOptions, Repository, SaveOptions, @@ -17,16 +20,31 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity import { UpsertOptions } from 'typeorm/repository/UpsertOptions'; import { PickKeysByType } from 'typeorm/common/PickKeysByType'; -import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; -import { ObjectLiteralStorage } from 'src/engine/twenty-orm/storage/object-literal.storage'; +import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; + +import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isPlainObject } from 'src/utils/is-plain-object'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export class WorkspaceRepository< Entity extends ObjectLiteral, > extends Repository { + private readonly internalContext: WorkspaceInternalContext; + + constructor( + internalContext: WorkspaceInternalContext, + target: EntityTarget, + manager: EntityManager, + queryRunner?: QueryRunner, + ) { + super(target, manager, queryRunner); + this.internalContext = internalContext; + } + /** * FIND METHODS */ @@ -35,9 +53,9 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions(options); + const computedOptions = await this.transformOptions(options); const result = await manager.find(this.target, computedOptions); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -47,9 +65,9 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); const result = await manager.findBy(this.target, computedOptions.where); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -59,9 +77,9 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise<[Entity[], number]> { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions(options); + const computedOptions = await this.transformOptions(options); const result = await manager.findAndCount(this.target, computedOptions); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -71,12 +89,12 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise<[Entity[], number]> { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); const result = await manager.findAndCountBy( this.target, computedOptions.where, ); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -86,9 +104,9 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions(options); + const computedOptions = await this.transformOptions(options); const result = await manager.findOne(this.target, computedOptions); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -98,9 +116,9 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); const result = await manager.findOneBy(this.target, computedOptions.where); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -110,9 +128,9 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions(options); + const computedOptions = await this.transformOptions(options); const result = await manager.findOneOrFail(this.target, computedOptions); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -122,12 +140,12 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); const result = await manager.findOneByOrFail( this.target, computedOptions.where, ); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -165,14 +183,25 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const formattedEntityOrEntities = this.formatData(entityOrEntities); - const result = await manager.save( - this.target, - formattedEntityOrEntities as any, - options, - ); + const formattedEntityOrEntities = await this.formatData(entityOrEntities); + let result: T | T[]; - const formattedResult = this.formatResult(result); + // Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion + if (Array.isArray(formattedEntityOrEntities)) { + result = await manager.save( + this.target, + formattedEntityOrEntities, + options, + ); + } else { + result = await manager.save( + this.target, + formattedEntityOrEntities, + options, + ); + } + + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -198,18 +227,18 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const formattedEntityOrEntities = this.formatData(entityOrEntities); + const formattedEntityOrEntities = await this.formatData(entityOrEntities); const result = await manager.remove( this.target, formattedEntityOrEntities, options, ); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } - override delete( + override async delete( criteria: | string | string[] @@ -225,7 +254,7 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; if (typeof criteria === 'object' && 'where' in criteria) { - criteria = this.transformOptions(criteria); + criteria = await this.transformOptions(criteria); } return manager.delete(this.target, criteria); @@ -261,18 +290,30 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const formattedEntityOrEntities = this.formatData(entityOrEntities); - const result = await manager.softRemove( - this.target, - formattedEntityOrEntities as any, - options, - ); - const formattedResult = this.formatResult(result); + const formattedEntityOrEntities = await this.formatData(entityOrEntities); + let result: T | T[]; + + // Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion + if (Array.isArray(formattedEntityOrEntities)) { + result = await manager.softRemove( + this.target, + formattedEntityOrEntities, + options, + ); + } else { + result = await manager.softRemove( + this.target, + formattedEntityOrEntities, + options, + ); + } + + const formattedResult = await this.formatResult(result); return formattedResult; } - override softDelete( + override async softDelete( criteria: | string | string[] @@ -288,7 +329,7 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; if (typeof criteria === 'object' && 'where' in criteria) { - criteria = this.transformOptions(criteria); + criteria = await this.transformOptions(criteria); } return manager.softDelete(this.target, criteria); @@ -327,18 +368,30 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const formattedEntityOrEntities = this.formatData(entityOrEntities); - const result = await manager.recover( - this.target, - formattedEntityOrEntities as any, - options, - ); - const formattedResult = this.formatResult(result); + const formattedEntityOrEntities = await this.formatData(entityOrEntities); + let result: T | T[]; + + // Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion + if (Array.isArray(formattedEntityOrEntities)) { + result = await manager.recover( + this.target, + formattedEntityOrEntities, + options, + ); + } else { + result = await manager.recover( + this.target, + formattedEntityOrEntities, + options, + ); + } + + const formattedResult = await this.formatResult(result); return formattedResult; } - override restore( + override async restore( criteria: | string | string[] @@ -354,7 +407,7 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; if (typeof criteria === 'object' && 'where' in criteria) { - criteria = this.transformOptions(criteria); + criteria = await this.transformOptions(criteria); } return manager.restore(this.target, criteria); @@ -368,9 +421,9 @@ export class WorkspaceRepository< entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const formatedEntity = this.formatData(entity); + const formatedEntity = await this.formatData(entity); const result = await manager.insert(this.target, formatedEntity); - const formattedResult = this.formatResult(result); + const formattedResult = await this.formatResult(result); return formattedResult; } @@ -378,7 +431,7 @@ export class WorkspaceRepository< /** * UPDATE METHODS */ - override update( + override async update( criteria: | string | string[] @@ -395,13 +448,13 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; if (typeof criteria === 'object' && 'where' in criteria) { - criteria = this.transformOptions(criteria); + criteria = await this.transformOptions(criteria); } return manager.update(this.target, criteria, partialEntity); } - override upsert( + override async upsert( entityOrEntities: | QueryDeepPartialEntity | QueryDeepPartialEntity[], @@ -410,7 +463,7 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; - const formattedEntityOrEntities = this.formatData(entityOrEntities); + const formattedEntityOrEntities = await this.formatData(entityOrEntities); return manager.upsert( this.target, @@ -422,22 +475,22 @@ export class WorkspaceRepository< /** * EXIST METHODS */ - override exists( + override async exists( options?: FindManyOptions, entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions(options); + const computedOptions = await this.transformOptions(options); return manager.exists(this.target, computedOptions); } - override existsBy( + override async existsBy( where: FindOptionsWhere | FindOptionsWhere[], entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); return manager.existsBy(this.target, computedOptions.where); } @@ -445,22 +498,22 @@ export class WorkspaceRepository< /** * COUNT METHODS */ - override count( + override async count( options?: FindManyOptions, entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions(options); + const computedOptions = await this.transformOptions(options); return manager.count(this.target, computedOptions); } - override countBy( + override async countBy( where: FindOptionsWhere | FindOptionsWhere[], entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); return manager.countBy(this.target, computedOptions.where); } @@ -468,58 +521,60 @@ export class WorkspaceRepository< /** * MATH METHODS */ - override sum( + override async sum( columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); return manager.sum(this.target, columnName, computedOptions.where); } - override average( + override async average( columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); return manager.average(this.target, columnName, computedOptions.where); } - override minimum( + override async minimum( columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); return manager.minimum(this.target, columnName, computedOptions.where); } - override maximum( + override async maximum( columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedOptions = this.transformOptions({ where }); + const computedOptions = await this.transformOptions({ where }); return manager.maximum(this.target, columnName, computedOptions.where); } - override increment( + override async increment( conditions: FindOptionsWhere, propertyPath: string, value: number | string, entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedConditions = this.transformOptions({ where: conditions }); + const computedConditions = await this.transformOptions({ + where: conditions, + }); return manager.increment( this.target, @@ -529,14 +584,16 @@ export class WorkspaceRepository< ); } - override decrement( + override async decrement( conditions: FindOptionsWhere, propertyPath: string, value: number | string, entityManager?: EntityManager, ): Promise { const manager = entityManager || this.manager; - const computedConditions = this.transformOptions({ where: conditions }); + const computedConditions = await this.transformOptions({ + where: conditions, + }); return manager.decrement( this.target, @@ -549,70 +606,85 @@ export class WorkspaceRepository< /** * PRIVATE METHODS */ - private getCompositeFieldMetadataArgs() { - const objectLiteral = ObjectLiteralStorage.getObjectLiteral( - this.target as any, + private async getObjectMetadataFromTarget() { + const objectMetadataName = WorkspaceEntitiesStorage.getObjectMetadataName( + this.internalContext.workspaceId, + this.target as EntitySchema, ); - if (!objectLiteral) { - throw new Error('Object literal is missing'); + if (!objectMetadataName) { + throw new Error('Object metadata name is missing'); } - const fieldMetadataArgsCollection = - metadataArgsStorage.filterFields(objectLiteral); - const compositeFieldMetadataArgsCollection = - fieldMetadataArgsCollection.filter((fieldMetadataArg) => - isCompositeFieldMetadataType(fieldMetadataArg.type), - ); - - return compositeFieldMetadataArgsCollection; + return this.internalContext.workspaceCacheStorage.getObjectMetadata( + this.internalContext.workspaceId, + (objectMetadata) => objectMetadata.nameSingular === objectMetadataName, + ); } - private transformOptions< + private async getCompositeFieldMetadata( + objectMetadata?: ObjectMetadataEntity, + ) { + objectMetadata ??= await this.getObjectMetadataFromTarget(); + + if (!objectMetadata) { + throw new Error('Object metadata entity is missing'); + } + + const compositeFieldMetadataCollection = objectMetadata.fields.filter( + (fieldMetadata) => isCompositeFieldMetadataType(fieldMetadata.type), + ); + + return compositeFieldMetadataCollection; + } + + private async transformOptions< T extends FindManyOptions | FindOneOptions | undefined, - >(options: T): T { + >(options: T): Promise { if (!options) { return options; } const transformedOptions = { ...options }; - transformedOptions.where = this.formatData(options.where); + transformedOptions.where = await this.formatData(options.where); return transformedOptions; } - private formatData(data: T): T { + private async formatData(data: T): Promise { if (!data) { return data; } if (Array.isArray(data)) { - return data.map((item) => this.formatData(item)) as T; + return Promise.all( + data.map((item) => this.formatData(item)), + ) as Promise; } - const compositeFieldMetadataArgsCollection = - this.getCompositeFieldMetadataArgs(); - const compositeFieldMetadataArgsMap = new Map( - compositeFieldMetadataArgsCollection.map((fieldMetadataArg) => [ - fieldMetadataArg.name, - fieldMetadataArg, + const compositeFieldMetadataCollection = + await this.getCompositeFieldMetadata(); + const compositeFieldMetadataMap = new Map( + compositeFieldMetadataCollection.map((fieldMetadata) => [ + fieldMetadata.name, + fieldMetadata, ]), ); const newData: object = {}; for (const [key, value] of Object.entries(data)) { - const fieldMetadataArgs = compositeFieldMetadataArgsMap.get(key); + const fieldMetadata = compositeFieldMetadataMap.get(key); - if (!fieldMetadataArgs) { + if (!fieldMetadata) { if (isPlainObject(value)) { - newData[key] = this.formatData(value); + newData[key] = await this.formatData(value); } else { newData[key] = value; } continue; } - const compositeType = compositeTypeDefintions.get(fieldMetadataArgs.type); + const compositeType = compositeTypeDefintions.get(fieldMetadata.type); if (!compositeType) { continue; @@ -620,7 +692,7 @@ export class WorkspaceRepository< for (const compositeProperty of compositeType.properties) { const compositeKey = computeCompositeColumnName( - fieldMetadataArgs.name, + fieldMetadata.name, compositeProperty, ); const value = data?.[key]?.[compositeProperty.name]; @@ -636,78 +708,90 @@ export class WorkspaceRepository< return newData as T; } - private formatResult( + private async formatResult( data: T, - target = ObjectLiteralStorage.getObjectLiteral(this.target as any), - ): T { + objectMetadata?: ObjectMetadataEntity, + ): Promise { + objectMetadata ??= await this.getObjectMetadataFromTarget(); + if (!data) { return data; } if (Array.isArray(data)) { - return data.map((item) => this.formatResult(item, target)) as T; + // If the data is an array, map each item in the array, format result is a promise + return Promise.all( + data.map((item) => this.formatResult(item, objectMetadata)), + ) as Promise; } if (!isPlainObject(data)) { return data; } - if (!target) { - throw new Error('Object literal is missing'); + if (!objectMetadata) { + throw new Error('Object metadata is missing'); } - const fieldMetadataArgsCollection = - metadataArgsStorage.filterFields(target); - const relationMetadataArgsCollection = - metadataArgsStorage.filterRelations(target); - const compositeFieldMetadataArgsCollection = - fieldMetadataArgsCollection.filter((fieldMetadataArg) => - isCompositeFieldMetadataType(fieldMetadataArg.type), - ); - const compositeFieldMetadataArgsMap = new Map( - compositeFieldMetadataArgsCollection.flatMap((fieldMetadataArg) => { - const compositeType = compositeTypeDefintions.get( - fieldMetadataArg.type, - ); + const compositeFieldMetadataCollection = + await this.getCompositeFieldMetadata(objectMetadata); + const compositeFieldMetadataMap = new Map( + compositeFieldMetadataCollection.flatMap((fieldMetadata) => { + const compositeType = compositeTypeDefintions.get(fieldMetadata.type); if (!compositeType) return []; // Map each composite property to a [key, value] pair return compositeType.properties.map((compositeProperty) => [ - computeCompositeColumnName(fieldMetadataArg.name, compositeProperty), + computeCompositeColumnName(fieldMetadata.name, compositeProperty), { - parentField: fieldMetadataArg.name, + parentField: fieldMetadata.name, ...compositeProperty, }, ]); }), ); - const relationMetadataArgsMap = new Map( - relationMetadataArgsCollection.map((relationMetadataArgs) => [ - relationMetadataArgs.name, - relationMetadataArgs, - ]), + const relationMetadataMap = new Map( + objectMetadata.fields + .filter(({ type }) => isRelationFieldMetadataType(type)) + .map((fieldMetadata) => [ + fieldMetadata.name, + fieldMetadata.fromRelationMetadata ?? + fieldMetadata.toRelationMetadata, + ]), ); const newData: object = {}; for (const [key, value] of Object.entries(data)) { - const compositePropertyArgs = compositeFieldMetadataArgsMap.get(key); - const relationMetadataArgs = relationMetadataArgsMap.get(key); + const compositePropertyArgs = compositeFieldMetadataMap.get(key); + const relationMetadata = relationMetadataMap.get(key); - if (!compositePropertyArgs && !relationMetadataArgs) { + if (!compositePropertyArgs && !relationMetadata) { if (isPlainObject(value)) { - newData[key] = this.formatResult(value); + newData[key] = await this.formatResult(value); } else { newData[key] = value; } continue; } - if (relationMetadataArgs) { - newData[key] = this.formatResult( - value, - relationMetadataArgs.inverseSideTarget() as any, - ); + if (relationMetadata) { + const inverseSideObjectName = + relationMetadata.toObjectMetadata.nameSingular; + const objectMetadata = + await this.internalContext.workspaceCacheStorage.getObjectMetadata( + this.internalContext.workspaceId, + (objectMetadata) => + objectMetadata.nameSingular === inverseSideObjectName, + ); + + if (!objectMetadata) { + throw new Error( + `Object metadata for object metadata "${inverseSideObjectName}" is missing`, + ); + } + + newData[key] = await this.formatResult(value, objectMetadata); continue; } diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/cache-manager.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/cache-manager.storage.ts new file mode 100644 index 000000000..6fe25c595 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/storage/cache-manager.storage.ts @@ -0,0 +1,47 @@ +type CacheKey = `${string}-${string}`; + +type AsyncFactoryCallback = () => Promise; + +export class CacheManager { + private cache = new Map(); + + async execute( + cacheKey: CacheKey, + factory: AsyncFactoryCallback, + onDelete?: (value: T) => Promise | void, + ): Promise { + const [workspaceId] = cacheKey.split('-'); + + // If the cacheKey exists, return the cached value + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey)!; + } + + // Remove old entries with the same workspaceId + for (const key of this.cache.keys()) { + if (key.startsWith(`${workspaceId}-`)) { + await onDelete?.(this.cache.get(key)!); + this.cache.delete(key); + } + } + + // Create a new value using the factory callback + const value = await factory(); + + if (!value) { + return null; + } + + this.cache.set(cacheKey, value); + + return value; + } + + async clear(onDelete?: (value: T) => Promise | void): Promise { + for (const value of this.cache.values()) { + await onDelete?.(value); + this.cache.delete(value as any); + } + this.cache.clear(); + } +} diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/data-source.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/data-source.storage.ts deleted file mode 100644 index b924974e5..000000000 --- a/packages/twenty-server/src/engine/twenty-orm/storage/data-source.storage.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DataSource } from 'typeorm'; - -import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; - -export class DataSourceStorage { - private static readonly dataSources: Map = - new Map(); - - public static getDataSource(key: string): WorkspaceDataSource | undefined { - return this.dataSources.get(key); - } - - public static setDataSource( - key: string, - dataSource: WorkspaceDataSource, - ): void { - this.dataSources.set(key, dataSource); - } - - public static getAllDataSources(): DataSource[] { - return Array.from(this.dataSources.values()); - } -} diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/object-literal.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/object-literal.storage.ts deleted file mode 100644 index 17d486468..000000000 --- a/packages/twenty-server/src/engine/twenty-orm/storage/object-literal.storage.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Type } from '@nestjs/common'; - -import { EntitySchema } from 'typeorm'; - -export class ObjectLiteralStorage { - private static readonly objects: Map> = new Map(); - - public static getObjectLiteral(target: EntitySchema): Type | undefined { - return this.objects.get(target); - } - - public static setObjectLiteral( - target: EntitySchema, - objectLiteral: Type, - ): void { - this.objects.set(target, objectLiteral); - } - - public static getAllObjects(): Type[] { - return Array.from(this.objects.values()); - } - - public static getAllEntitySchemas(): EntitySchema[] { - return Array.from(this.objects.keys()); - } - - public static clear(): void { - this.objects.clear(); - } -} diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/workspace-entities.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/workspace-entities.storage.ts new file mode 100644 index 000000000..aa8f277fa --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/storage/workspace-entities.storage.ts @@ -0,0 +1,49 @@ +import { EntitySchema } from 'typeorm'; + +export class WorkspaceEntitiesStorage { + private static workspaceEntities = new Map< + string, + Map + >(); + + static getEntitySchema( + workspaceId: string, + objectMetadataName: string, + ): EntitySchema | undefined { + const workspace = this.workspaceEntities.get(workspaceId); + + return workspace?.get(objectMetadataName); + } + + static setEntitySchema( + workspaceId: string, + objectMetadataName: string, + schema: EntitySchema, + ): void { + if (!this.workspaceEntities.has(workspaceId)) { + this.workspaceEntities.set(workspaceId, new Map()); + } + const workspace = this.workspaceEntities.get(workspaceId); + + workspace?.set(objectMetadataName, schema); + } + + static getObjectMetadataName( + workspaceId: string, + target: EntitySchema, + ): string | undefined { + const workspace = this.workspaceEntities.get(workspaceId); + + return Array.from(workspace?.entries() || []).find( + ([, schema]) => schema.options.name === target.options.name, + )?.[0]; + } + + static getEntities(workspaceId: string): EntitySchema[] { + return Array.from(this.workspaceEntities.get(workspaceId)?.values() || []); + } + + static clearWorkspace(workspaceId: string): void { + this.workspaceEntities.delete(workspaceId); + } +} diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts index efed8ca7b..f9f959d1e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts @@ -5,36 +5,46 @@ import { Module, OnApplicationShutdown, Provider, - Type, } from '@nestjs/common'; +import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'; -import { importClassesFromDirectories } from 'typeorm/util/DirectoryExportedClassesLoader'; -import { Logger as TypeORMLogger } from 'typeorm/logger/Logger'; +import { Repository } from 'typeorm'; import { TwentyORMModuleAsyncOptions, TwentyORMOptions, } from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; +import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { entitySchemaFactories } from 'src/engine/twenty-orm/factories'; +import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; +import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage'; import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; -import { DataSourceStorage } from 'src/engine/twenty-orm/storage/data-source.storage'; -import { ScopedWorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-datasource.factory'; -import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; -import { splitClassesAndStrings } from 'src/engine/twenty-orm/utils/split-classes-and-strings.util'; -import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, } from 'src/engine/twenty-orm/twenty-orm.module-definition'; -import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; + +export const workspaceDataSourceCacheInstance = + new CacheManager(); @Global() @Module({ - imports: [DataSourceModule], + imports: [ + TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), + DataSourceModule, + WorkspaceCacheVersionModule, + WorkspaceCacheStorageModule, + ], providers: [ ...entitySchemaFactories, TwentyORMManager, @@ -55,27 +65,18 @@ export class TwentyORMCoreModule static register(options: TwentyORMOptions): DynamicModule { const dynamicModule = super.register(options); + // TODO: Avoid code duplication here const providers: Provider[] = [ { provide: TWENTY_ORM_WORKSPACE_DATASOURCE, - useFactory: async ( - entitySchemaFactory: EntitySchemaFactory, - scopedWorkspaceDatasourceFactory: ScopedWorkspaceDatasourceFactory, - ) => { - const workspaceEntities = await this.loadEntities( - options.workspaceEntities, - ); - - const entities = workspaceEntities.map((entityClass) => - entitySchemaFactory.create(entityClass), - ); - - const scopedWorkspaceDataSource = - await scopedWorkspaceDatasourceFactory.create(entities); - - return scopedWorkspaceDataSource; - }, - inject: [EntitySchemaFactory, ScopedWorkspaceDatasourceFactory], + useFactory: this.createWorkspaceDataSource, + inject: [ + WorkspaceCacheStorageService, + getRepositoryToken(ObjectMetadataEntity, 'metadata'), + EntitySchemaFactory, + ScopedWorkspaceContextFactory, + WorkspaceDatasourceFactory, + ], }, ]; @@ -96,27 +97,13 @@ export class TwentyORMCoreModule const providers: Provider[] = [ { provide: TWENTY_ORM_WORKSPACE_DATASOURCE, - useFactory: async ( - entitySchemaFactory: EntitySchemaFactory, - scopedWorkspaceDatasourceFactory: ScopedWorkspaceDatasourceFactory, - options: TwentyORMOptions, - ) => { - const workspaceEntities = await this.loadEntities( - options.workspaceEntities, - ); - - const entities = workspaceEntities.map((entityClass) => - entitySchemaFactory.create(entityClass), - ); - - const scopedWorkspaceDataSource = - await scopedWorkspaceDatasourceFactory.create(entities); - - return scopedWorkspaceDataSource; - }, + useFactory: this.createWorkspaceDataSource, inject: [ + WorkspaceCacheStorageService, + getRepositoryToken(ObjectMetadataEntity, 'metadata'), EntitySchemaFactory, - ScopedWorkspaceDatasourceFactory, + ScopedWorkspaceContextFactory, + WorkspaceDatasourceFactory, MODULE_OPTIONS_TOKEN, ], }, @@ -132,45 +119,70 @@ export class TwentyORMCoreModule }; } + static async createWorkspaceDataSource( + workspaceCacheStorageService: WorkspaceCacheStorageService, + objectMetadataRepository: Repository, + entitySchemaFactory: EntitySchemaFactory, + scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, + workspaceDataSourceFactory: WorkspaceDatasourceFactory, + _options?: TwentyORMOptions, + ) { + const { workspaceId, cacheVersion } = + scopedWorkspaceContextFactory.create(); + + if (!workspaceId) { + return null; + } + + return workspaceDataSourceCacheInstance.execute( + `${workspaceId}-${cacheVersion}`, + async () => { + let objectMetadataCollection = + await workspaceCacheStorageService.getObjectMetadataCollection( + workspaceId, + ); + + if (!objectMetadataCollection) { + objectMetadataCollection = await objectMetadataRepository.find({ + where: { workspaceId }, + relations: [ + 'fields.object', + 'fields', + 'fields.fromRelationMetadata', + 'fields.toRelationMetadata', + 'fields.fromRelationMetadata.toObjectMetadata', + ], + }); + + await workspaceCacheStorageService.setObjectMetadataCollection( + workspaceId, + objectMetadataCollection, + ); + } + + const entities = await Promise.all( + objectMetadataCollection.map((objectMetadata) => + entitySchemaFactory.create(workspaceId, objectMetadata), + ), + ); + + const workspaceDataSource = await workspaceDataSourceFactory.create( + entities, + workspaceId, + ); + + return workspaceDataSource; + }, + (dataSource) => dataSource.destroy(), + ); + } + /** * Destroys all data sources on application shutdown */ async onApplicationShutdown() { - const dataSources = DataSourceStorage.getAllDataSources(); - - for (const dataSource of dataSources) { - try { - if (dataSource && dataSource.isInitialized) { - await dataSource.destroy(); - } - } catch (e) { - this.logger.error(e?.message); - } - } - } - - private static async loadEntities( - workspaceEntities: (Type | string)[], - ): Promise[]> { - const [entityClassesOrSchemas, entityDirectories] = splitClassesAndStrings( - workspaceEntities || [], - ); - const importedEntities = await importClassesFromDirectories( - // Only `log` function is used under importClassesFromDirectories function - this.logger as unknown as TypeORMLogger, - entityDirectories, - ); - const entities = [ - ...entityClassesOrSchemas, - ...(importedEntities as Type[]), - ]; - - return entities.filter( - (entity) => - // Filter out CustomWorkspaceEntity as it's a partial entity handled separately - entity.name !== CustomWorkspaceEntity.name && - // Filter out BaseWorkspaceEntity as it's a base entity and should not be included in the workspace entities - entity.name !== BaseWorkspaceEntity.name, + workspaceDataSourceCacheInstance.clear((dataSource) => + dataSource.destroy(), ); } } diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts index 668379a0a..a6852503c 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts @@ -1,113 +1,152 @@ import { Injectable, Optional, Type } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; -import { ObjectLiteral } from 'typeorm'; +import { ObjectLiteral, Repository } from 'typeorm'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; +import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; -import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/activity.workspace-entity'; -import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity'; -import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity'; -import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; -import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; -import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; -import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; -import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; -import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; -import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; -import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; -import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; -import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity'; -import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; -import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity'; -import { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-objects/behavioral-event.workspace-entity'; -import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; -import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; -import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; -import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity'; -import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; -import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { workspaceDataSourceCacheInstance } from 'src/engine/twenty-orm/twenty-orm-core.module'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; @Injectable() export class TwentyORMManager { constructor( @Optional() + @InjectWorkspaceDatasource() private readonly workspaceDataSource: WorkspaceDataSource | null, - private readonly entitySchemaFactory: EntitySchemaFactory, + private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory, + private readonly entitySchemaFactory: EntitySchemaFactory, ) {} - getRepository( + async getRepository( + objectMetadataName: string, + ): Promise>; + + async getRepository( entityClass: Type, - ): WorkspaceRepository { - const entitySchema = this.entitySchemaFactory.create(entityClass); + ): Promise>; + + async getRepository( + entityClassOrobjectMetadataName: Type | string, + ): Promise> { + let objectMetadataName: string; + + if (typeof entityClassOrobjectMetadataName === 'string') { + objectMetadataName = entityClassOrobjectMetadataName; + } else { + objectMetadataName = convertClassNameToObjectMetadataName( + entityClassOrobjectMetadataName.name, + ); + } if (!this.workspaceDataSource) { throw new Error('Workspace data source not found'); } + const entitySchema = await this.entitySchemaFactory.create( + this.workspaceDataSource.internalContext.workspaceId, + objectMetadataName, + ); + + if (!entitySchema) { + throw new Error('Entity schema not found'); + } + return this.workspaceDataSource.getRepository(entitySchema); } async getRepositoryForWorkspace( workspaceId: string, entityClass: Type, - ): Promise> { - // TODO: This is a temporary solution to get all workspace entities - const workspaceEntities = [ - ActivityTargetWorkspaceEntity, - ActivityWorkspaceEntity, - ApiKeyWorkspaceEntity, - AttachmentWorkspaceEntity, - BlocklistWorkspaceEntity, - BehavioralEventWorkspaceEntity, - CalendarChannelEventAssociationWorkspaceEntity, - CalendarChannelWorkspaceEntity, - CalendarEventParticipantWorkspaceEntity, - CalendarEventWorkspaceEntity, - CommentWorkspaceEntity, - CompanyWorkspaceEntity, - ConnectedAccountWorkspaceEntity, - FavoriteWorkspaceEntity, - AuditLogWorkspaceEntity, - MessageChannelMessageAssociationWorkspaceEntity, - MessageChannelWorkspaceEntity, - MessageParticipantWorkspaceEntity, - MessageThreadWorkspaceEntity, - MessageWorkspaceEntity, - OpportunityWorkspaceEntity, - PersonWorkspaceEntity, - TimelineActivityWorkspaceEntity, - ViewFieldWorkspaceEntity, - ViewFilterWorkspaceEntity, - ViewSortWorkspaceEntity, - ViewWorkspaceEntity, - WebhookWorkspaceEntity, - WorkspaceMemberWorkspaceEntity, - ]; + ): Promise>; - const entities = workspaceEntities.map((workspaceEntity) => - this.entitySchemaFactory.create(workspaceEntity as any), + async getRepositoryForWorkspace( + workspaceId: string, + objectMetadataName: string, + ): Promise>; + + async getRepositoryForWorkspace( + workspaceId: string, + entityClassOrobjectMetadataName: Type | string, + ): Promise< + WorkspaceRepository | WorkspaceRepository + > { + const cacheVersion = + await this.workspaceCacheVersionService.getVersion(workspaceId); + + let objectMetadataName: string; + + if (typeof entityClassOrobjectMetadataName === 'string') { + objectMetadataName = entityClassOrobjectMetadataName; + } else { + objectMetadataName = convertClassNameToObjectMetadataName( + entityClassOrobjectMetadataName.name, + ); + } + + const workspaceDataSource = await workspaceDataSourceCacheInstance.execute( + `${workspaceId}-${cacheVersion}`, + async () => { + let objectMetadataCollection = + await this.workspaceCacheStorageService.getObjectMetadataCollection( + workspaceId, + ); + + if (!objectMetadataCollection) { + objectMetadataCollection = await this.objectMetadataRepository.find({ + where: { workspaceId }, + relations: [ + 'fields.object', + 'fields', + 'fields.fromRelationMetadata', + 'fields.toRelationMetadata', + 'fields.fromRelationMetadata.toObjectMetadata', + ], + }); + + await this.workspaceCacheStorageService.setObjectMetadataCollection( + workspaceId, + objectMetadataCollection, + ); + } + + const entities = await Promise.all( + objectMetadataCollection.map((objectMetadata) => + this.entitySchemaFactory.create(workspaceId, objectMetadata), + ), + ); + + const workspaceDataSource = + await this.workspaceDataSourceFactory.create(entities, workspaceId); + + return workspaceDataSource; + }, + (dataSource) => dataSource.destroy(), ); - const workspaceDataSource = await this.workspaceDataSourceFactory.create( - entities, + const entitySchema = await this.entitySchemaFactory.create( workspaceId, + objectMetadataName, ); if (!workspaceDataSource) { throw new Error('Workspace data source not found'); } - const entitySchema = this.entitySchemaFactory.create(entityClass); + if (!entitySchema) { + throw new Error('Entity schema not found'); + } return workspaceDataSource.getRepository(entitySchema); } diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.providers.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.providers.ts index 652692ace..4668364bf 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.providers.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.providers.ts @@ -5,6 +5,7 @@ import { getWorkspaceRepositoryToken } from 'src/engine/twenty-orm/utils/get-wor import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; /** * Create providers for the given entities. @@ -14,17 +15,28 @@ export function createTwentyORMProviders( ): Provider[] { return (objects || []).map((object) => ({ provide: getWorkspaceRepositoryToken(object), - useFactory: ( + useFactory: async ( dataSource: WorkspaceDataSource | null, entitySchemaFactory: EntitySchemaFactory, ) => { - const entity = entitySchemaFactory.create(object as Type); + const objectMetadataName = convertClassNameToObjectMetadataName( + (object as Type).name, + ); if (!dataSource) { return null; } - return dataSource.getRepository(entity); + const entitySchema = await entitySchemaFactory.create( + dataSource.internalContext.workspaceId, + objectMetadataName, + ); + + if (!entitySchema) { + throw new Error('Entity schema not found'); + } + + return dataSource.getRepository(entitySchema); }, inject: [TWENTY_ORM_WORKSPACE_DATASOURCE, EntitySchemaFactory], })); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-type.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-type.util.ts new file mode 100644 index 000000000..8836edbf2 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-type.util.ts @@ -0,0 +1,33 @@ +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + RelationMetadataEntity, + RelationMetadataType, +} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { + deduceRelationDirection, + RelationDirection, +} from 'src/engine/utils/deduce-relation-direction.util'; + +export const computeRelationType = ( + fieldMetadata: FieldMetadataEntity, + relationMetadata: RelationMetadataEntity, +) => { + const relationDirection = deduceRelationDirection( + fieldMetadata, + relationMetadata, + ); + + switch (relationMetadata.relationType) { + case RelationMetadataType.ONE_TO_MANY: { + return relationDirection === RelationDirection.FROM + ? 'one-to-many' + : 'many-to-one'; + } + case RelationMetadataType.ONE_TO_ONE: + return 'one-to-one'; + case RelationMetadataType.MANY_TO_MANY: + return 'many-to-many'; + default: + throw new Error('Invalid relation type'); + } +}; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/determine-relation-details.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/determine-relation-details.util.ts new file mode 100644 index 000000000..282c2ac26 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/determine-relation-details.util.ts @@ -0,0 +1,59 @@ +import { RelationType } from 'typeorm/metadata/types/RelationTypes'; + +import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util'; + +interface RelationDetails { + relationType: RelationType; + target: string; + inverseSide: string; + joinColumn: { name: string } | undefined; +} + +export async function determineRelationDetails( + workspaceId: string, + fieldMetadata: FieldMetadataEntity, + relationMetadata: RelationMetadataEntity, + workspaceCacheStorageService: WorkspaceCacheStorageService, +): Promise { + const relationType = computeRelationType(fieldMetadata, relationMetadata); + let fromObjectMetadata: ObjectMetadataEntity | undefined = + fieldMetadata.object; + let toObjectMetadata: ObjectMetadataEntity | undefined = + relationMetadata.toObjectMetadata; + + // RelationMetadata always store the relation from the perspective of the `from` object, MANY_TO_ONE relations are not stored yet + if (relationType === 'many-to-one') { + fromObjectMetadata = fieldMetadata.object; + toObjectMetadata = await workspaceCacheStorageService.getObjectMetadata( + workspaceId, + (objectMetadata) => + objectMetadata.id === relationMetadata.toObjectMetadataId, + ); + } + + if (!fromObjectMetadata || !toObjectMetadata) { + throw new Error('Object metadata not found'); + } + + // TODO: Support many to many relations + if (relationType === 'many-to-many') { + throw new Error('Many to many relations are not supported yet'); + } + + return { + relationType, + target: toObjectMetadata.nameSingular, + inverseSide: fromObjectMetadata.nameSingular, + joinColumn: + // TODO: This will work for now but we need to handle this better in the future for custom names on the join column + relationType === 'many-to-one' || + (relationType === 'one-to-one' && + relationMetadata.toObjectMetadataId === fieldMetadata.objectMetadataId) + ? { name: `${fieldMetadata.name}` + 'Id' } + : undefined, + }; +} diff --git a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.module.ts b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.module.ts new file mode 100644 index 000000000..62d8be6fd --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; + +@Module({ + imports: [WorkspaceCacheVersionModule], + providers: [WorkspaceCacheStorageService], + exports: [WorkspaceCacheStorageService], +}) +export class WorkspaceCacheStorageModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service.ts b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts similarity index 87% rename from packages/twenty-server/src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service.ts rename to packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts index c92f911a2..dde679100 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts @@ -7,11 +7,10 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; @Injectable() -export class WorkspaceSchemaStorageService { +export class WorkspaceCacheStorageService { constructor( @InjectCacheStorage(CacheStorageNamespace.WorkspaceSchema) private readonly workspaceSchemaCache: CacheStorageService, - private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, ) {} @@ -58,6 +57,21 @@ export class WorkspaceSchemaStorageService { ); } + async getObjectMetadata( + workspaceId: string, + predicate: (objectMetadata: ObjectMetadataEntity) => boolean, + ): Promise { + const objectMetadataCollection = await this.workspaceSchemaCache.get< + ObjectMetadataEntity[] + >(`objectMetadataCollection:${workspaceId}`); + + if (!objectMetadataCollection) { + return; + } + + return objectMetadataCollection.find(predicate); + } + setTypeDefs(workspaceId: string, typeDefs: string): Promise { return this.workspaceSchemaCache.set( `typeDefs:${workspaceId}`, diff --git a/packages/twenty-server/src/queue-worker/queue-worker.module.ts b/packages/twenty-server/src/queue-worker/queue-worker.module.ts index 1cf307ffe..6f3374a1c 100644 --- a/packages/twenty-server/src/queue-worker/queue-worker.module.ts +++ b/packages/twenty-server/src/queue-worker/queue-worker.module.ts @@ -7,9 +7,7 @@ import { MessageQueueModule } from 'src/engine/integrations/message-queue/messag @Module({ imports: [ - TwentyORMModule.register({ - workspaceEntities: ['dist/src/**/*.workspace-entity{.ts,.js}'], - }), + TwentyORMModule.register({}), IntegrationsModule, MessageQueueModule.registerExplorer(), JobsModule, diff --git a/packages/twenty-server/tsconfig.json b/packages/twenty-server/tsconfig.json index c0fc8b9bb..7b2af2c49 100644 --- a/packages/twenty-server/tsconfig.json +++ b/packages/twenty-server/tsconfig.json @@ -12,6 +12,7 @@ "esModuleInterop": true, "target": "es2017", "sourceMap": true, + "inlineSources": true, "outDir": "./dist", "incremental": true, "skipLibCheck": true,