From 088d061b3e6e5460d4b8fc9d581f4972110fa8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Fri, 19 Jul 2024 18:23:52 +0200 Subject: [PATCH] feat: twenty orm for standard and custom objects (#6178) ### Overview This PR builds upon #5153, adding the ability to get a repository for custom objects. The `entitySchema` is now generated for both standard and custom objects based on metadata stored in the database instead of the decorated `WorkspaceEntity` in the code. This change ensures that standard objects with custom fields and relations can also support custom objects. ### Implementation Details #### Key Changes: - **Dynamic Schema Generation:** The `entitySchema` for standard and custom objects is now dynamically generated from the metadata stored in the database. This shift allows for greater flexibility and adaptability, particularly for standard objects with custom fields and relations. - **Custom Object Repository Retrieval:** A repository for a custom object can be retrieved using `TwentyORMManager` based on the object's name. Here's an example of how this can be achieved: ```typescript const repository = await this.twentyORMManager.getRepository('custom'); /* * `repository` variable will be typed as follows, ensuring that standard fields and relations are properly typed: * const repository: WorkspaceRepository */ const res = await repository.find({}); ``` Fix #6179 --------- Co-authored-by: Charles Bochet Co-authored-by: Weiko --- packages/twenty-server/.swcrc | 22 +- .../src/database/typeorm/typeorm.module.ts | 4 +- .../__tests__/workspace.factory.spec.ts | 4 +- .../api/graphql/core-graphql-api.module.ts | 4 +- .../workspace-schema-storage.module.ts | 12 - .../api/graphql/workspace-schema.factory.ts | 18 +- .../timeline-calendar-event.service.ts | 30 +- .../load-service-with-workspace.context.ts | 7 +- .../datasource/workspace.datasource.ts | 21 +- .../entity-manager/entity.manager.ts | 22 +- .../factories/entity-schema-column.factory.ts | 95 +++-- .../entity-schema-relation.factory.ts | 85 ++--- .../factories/entity-schema.factory.ts | 72 ++-- .../src/engine/twenty-orm/factories/index.ts | 4 +- .../scoped-workspace-context.factory.ts | 26 ++ .../scoped-workspace-datasource.factory.ts | 27 -- .../factories/workspace-datasource.factory.ts | 50 ++- .../twenty-orm-options.interface.ts | 9 +- .../workspace-internal-context.interface.ts | 6 + .../repository/workspace.repository.ts | 360 +++++++++++------- .../storage/cache-manager.storage.ts | 47 +++ .../twenty-orm/storage/data-source.storage.ts | 23 -- .../storage/object-literal.storage.ts | 30 -- .../storage/workspace-entities.storage.ts | 49 +++ .../twenty-orm/twenty-orm-core.module.ts | 180 +++++---- .../engine/twenty-orm/twenty-orm.manager.ts | 183 +++++---- .../engine/twenty-orm/twenty-orm.providers.ts | 18 +- .../utils/compute-relation-type.util.ts | 33 ++ .../utils/determine-relation-details.util.ts | 59 +++ .../workspace-cache-storage.module.ts | 11 + .../workspace-cache-storage.service.ts} | 18 +- .../src/queue-worker/queue-worker.module.ts | 4 +- packages/twenty-server/tsconfig.json | 1 + 33 files changed, 947 insertions(+), 587 deletions(-) delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.module.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts delete mode 100644 packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/storage/cache-manager.storage.ts delete mode 100644 packages/twenty-server/src/engine/twenty-orm/storage/data-source.storage.ts delete mode 100644 packages/twenty-server/src/engine/twenty-orm/storage/object-literal.storage.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/storage/workspace-entities.storage.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-type.util.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/determine-relation-details.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.module.ts rename packages/twenty-server/src/engine/{api/graphql/workspace-schema-storage/workspace-schema-storage.service.ts => workspace-cache-storage/workspace-cache-storage.service.ts} (87%) 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,