From b207d10312997ac73ecaf16c0a0c08d254299a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Mon, 6 May 2024 14:12:11 +0200 Subject: [PATCH] feat: extend twenty orm (#5238) This PR is a follow up of PR #5153. This one introduce some changes on how we're querying composite fields. We can do: ```typescript export class CompanyService { constructor( @InjectWorkspaceRepository(CompanyObjectMetadata) private readonly companyObjectMetadataRepository: WorkspaceRepository, ) {} async companies(): Promise { // Old way // const companiesFilteredByLinkLabel = await this.companyObjectMetadataRepository.find({ // where: { xLinkLabel: 'MyLabel' }, // }); // Result will return xLinkLabel property // New way const companiesFilteredByLinkLabel = await this.companyObjectMetadataRepository.find({ where: { xLink: { label: 'MyLabel' } }, }); // Result will return { xLink: { label: 'MyLabel' } } property instead of { xLinkLabel: 'MyLabel' } return companiesFilteredByLinkLabel; } } ``` Also we can now inject `TwentyORMManage` class to manually create a repository based on a given `workspaceId` using `getRepositoryForWorkspace` function that way: ```typescript export class CompanyService { constructor( // TwentyORMModule should be initialized private readonly twentyORMManager, ) {} async companies(): Promise { const repository = await this.twentyORMManager.getRepositoryForWorkspace( '8bb6e872-a71f-4341-82b5-6b56fa81cd77', CompanyObjectMetadata, ); const companies = await repository.find(); return companies; } } ``` --- .../datasource/workspace.datasource.ts | 24 + .../inject-workspace-datasource.decorator.ts | 1 + .../inject-workspace-repository.decorator.ts | 1 + .../entity-manager/entity.manager.ts | 24 + .../factories/entity-schema.factory.ts | 7 +- .../src/engine/twenty-orm/factories/index.ts | 2 + .../scoped-workspace-datasource.factory.ts | 25 + .../factories/workspace-datasource.factory.ts | 29 +- .../repository/workspace.repository.ts | 592 +++++++++++++++++- .../twenty-orm/storage/data-source.storage.ts | 14 +- .../storage/object-literal.storage.ts | 30 + .../twenty-orm/twenty-orm-core.module.ts | 32 +- .../engine/twenty-orm/twenty-orm.manager.ts | 42 ++ .../engine/twenty-orm/twenty-orm.providers.ts | 9 +- .../engine/twenty-orm/twenty-orm.service.ts | 27 - 15 files changed, 784 insertions(+), 75 deletions(-) create mode 100644 packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts create 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/storage/object-literal.storage.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts delete mode 100644 packages/twenty-server/src/engine/twenty-orm/twenty-orm.service.ts 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 new file mode 100644 index 000000000..ab51a2492 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -0,0 +1,24 @@ +import { + DataSource, + EntityManager, + EntityTarget, + ObjectLiteral, + QueryRunner, +} from 'typeorm'; + +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/entity.manager'; + +export class WorkspaceDataSource extends DataSource { + readonly manager: WorkspaceEntityManager; + + override getRepository( + target: EntityTarget, + ): WorkspaceRepository { + return this.manager.getRepository(target); + } + + override createEntityManager(queryRunner?: QueryRunner): EntityManager { + return new WorkspaceEntityManager(this, queryRunner); + } +} diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator.ts index f9bae643f..810dc1515 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator.ts @@ -2,5 +2,6 @@ import { Inject } from '@nestjs/common'; import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants'; +// nit: The datasource can be null if it's used outside of an authenticated request context export const InjectWorkspaceDatasource = () => Inject(TWENTY_ORM_WORKSPACE_DATASOURCE); diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/inject-workspace-repository.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/inject-workspace-repository.decorator.ts index 4c2047a06..88050a06a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/inject-workspace-repository.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/inject-workspace-repository.decorator.ts @@ -3,6 +3,7 @@ import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-clas import { getWorkspaceRepositoryToken } from 'src/engine/twenty-orm/utils/get-workspace-repository-token.util'; +// nit: The repository can be null if it's used outside of an authenticated request context export const InjectWorkspaceRepository = ( entity: EntityClassOrSchema, ): ReturnType => Inject(getWorkspaceRepositoryToken(entity)); 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 new file mode 100644 index 000000000..989bd1550 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts @@ -0,0 +1,24 @@ +import { EntityManager, EntityTarget, ObjectLiteral } from 'typeorm'; + +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; + +export class WorkspaceEntityManager extends EntityManager { + override getRepository( + target: EntityTarget, + ): WorkspaceRepository { + // find already created repository instance and return it if found + const repoFromMap = this.repositories.get(target); + + if (repoFromMap) return repoFromMap as WorkspaceRepository; + + const newRepository = new WorkspaceRepository( + target, + this, + this.queryRunner, + ); + + this.repositories.set(target, newRepository); + + return newRepository; + } +} 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 0bfdc985d..060f2a224 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 @@ -5,6 +5,7 @@ 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'; @Injectable() export class EntitySchemaFactory { @@ -33,11 +34,15 @@ export class EntitySchemaFactory { relationMetadataArgsCollection, ); - return new EntitySchema({ + const entitySchema = new EntitySchema({ name: objectMetadataArgs.nameSingular, tableName: objectMetadataArgs.nameSingular, columns, relations, }); + + ObjectLiteralStorage.setObjectLiteral(entitySchema, target); + + 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 b07da0d7b..cb8ce526f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/index.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/index.ts @@ -1,6 +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 { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; export const entitySchemaFactories = [ @@ -8,4 +9,5 @@ export const entitySchemaFactories = [ EntitySchemaRelationFactory, EntitySchemaFactory, WorkspaceDatasourceFactory, + ScopedWorkspaceDatasourceFactory, ]; 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 new file mode 100644 index 000000000..0c21b6e74 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-datasource.factory.ts @@ -0,0 +1,25 @@ +import { Inject, Injectable, Scope } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; + +import { EntitySchema } from 'typeorm'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; + +@Injectable({ scope: Scope.REQUEST }) +export class ScopedWorkspaceDatasourceFactory { + constructor( + @Inject(REQUEST) private readonly request: Request, + private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory, + ) {} + + public async create(entities: EntitySchema[]) { + const workspace: Workspace | undefined = this.request['req']?.['workspace']; + + if (!workspace) { + return null; + } + + return this.workspaceDataSourceFactory.create(entities, workspace.id); + } +} 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 1094e38cc..ac90f2c8b 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 @@ -1,31 +1,22 @@ -import { Inject, Injectable, Scope } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; +import { Injectable } from '@nestjs/common'; -import { DataSource, EntitySchema } from 'typeorm'; +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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceStorage } from 'src/engine/twenty-orm/storage/data-source.storage'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -@Injectable({ scope: Scope.REQUEST }) +@Injectable() export class WorkspaceDatasourceFactory { constructor( - @Inject(REQUEST) private readonly request: Request, private readonly dataSourceService: DataSourceService, private readonly environmentService: EnvironmentService, ) {} - public async createWorkspaceDatasource(entities: EntitySchema[]) { - const workspace: Workspace = this.request['req']['workspace']; - - if (!workspace) { - return null; - } - - const storedWorkspaceDataSource = DataSourceStorage.getDataSource( - workspace.id, - ); + public async create(entities: EntitySchema[], workspaceId: string) { + const storedWorkspaceDataSource = + DataSourceStorage.getDataSource(workspaceId); if (storedWorkspaceDataSource) { return storedWorkspaceDataSource; @@ -33,10 +24,10 @@ export class WorkspaceDatasourceFactory { const dataSourceMetadata = await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspace.id, + workspaceId, ); - const workspaceDataSource = new DataSource({ + const workspaceDataSource = new WorkspaceDataSource({ url: dataSourceMetadata.url ?? this.environmentService.get('PG_DATABASE_URL'), @@ -51,7 +42,7 @@ export class WorkspaceDatasourceFactory { await workspaceDataSource.initialize(); - DataSourceStorage.setDataSource(workspace.id, workspaceDataSource); + DataSourceStorage.setDataSource(workspaceId, workspaceDataSource); return workspaceDataSource; } 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 5731b808a..ef405aa5c 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 @@ -1,7 +1,593 @@ -import { ObjectLiteral, Repository } from 'typeorm'; +import { + DeepPartial, + DeleteResult, + FindManyOptions, + FindOneOptions, + FindOptionsWhere, + InsertResult, + ObjectId, + ObjectLiteral, + RemoveOptions, + Repository, + SaveOptions, + UpdateResult, +} from 'typeorm'; +import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; +import { UpsertOptions } from 'typeorm/repository/UpsertOptions'; +import { PickKeysByType } from 'typeorm/common/PickKeysByType'; -import { FlattenCompositeTypes } from 'src/engine/twenty-orm/interfaces/flatten-composite-types.interface'; +import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; +import { ObjectLiteralStorage } from 'src/engine/twenty-orm/storage/object-literal.storage'; +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'; export class WorkspaceRepository< Entity extends ObjectLiteral, -> extends Repository> {} +> extends Repository { + /** + * FIND METHODS + */ + override async find(options?: FindManyOptions): Promise { + const computedOptions = this.transformOptions(options); + const result = await super.find(computedOptions); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override async findBy( + where: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + const result = await super.findBy(computedOptions.where); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override async findAndCount( + options?: FindManyOptions, + ): Promise<[Entity[], number]> { + const computedOptions = this.transformOptions(options); + const result = await super.findAndCount(computedOptions); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override async findAndCountBy( + where: FindOptionsWhere | FindOptionsWhere[], + ): Promise<[Entity[], number]> { + const computedOptions = this.transformOptions({ where }); + const result = await super.findAndCountBy(computedOptions.where); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override async findOne( + options: FindOneOptions, + ): Promise { + const computedOptions = this.transformOptions(options); + const result = await super.findOne(computedOptions); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override async findOneBy( + where: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + const result = await super.findOneBy(computedOptions.where); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override async findOneOrFail( + options: FindOneOptions, + ): Promise { + const computedOptions = this.transformOptions(options); + const result = await super.findOneOrFail(computedOptions); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override async findOneByOrFail( + where: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + const result = await super.findOneByOrFail(computedOptions.where); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + /** + * SAVE METHODS + */ + override save>( + entities: T[], + options: SaveOptions & { reload: false }, + ): Promise; + + override save>( + entities: T[], + options?: SaveOptions, + ): Promise<(T & Entity)[]>; + + override save>( + entity: T, + options: SaveOptions & { reload: false }, + ): Promise; + + override save>( + entity: T, + options?: SaveOptions, + ): Promise; + + override async save>( + entityOrEntities: T | T[], + options?: SaveOptions, + ): Promise { + const formattedEntityOrEntities = this.formatData(entityOrEntities); + const result = await super.save(formattedEntityOrEntities as any, options); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + /** + * REMOVE METHODS + */ + override remove( + entities: Entity[], + options?: RemoveOptions, + ): Promise; + + override remove(entity: Entity, options?: RemoveOptions): Promise; + + override async remove( + entityOrEntities: Entity | Entity[], + ): Promise { + const formattedEntityOrEntities = this.formatData(entityOrEntities); + const result = await super.remove(formattedEntityOrEntities as any); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override delete( + criteria: + | string + | string[] + | number + | number[] + | Date + | Date[] + | ObjectId + | ObjectId[] + | FindOptionsWhere, + ): Promise { + if (typeof criteria === 'object' && 'where' in criteria) { + criteria = this.transformOptions(criteria); + } + + return this.delete(criteria); + } + + override softRemove>( + entities: T[], + options: SaveOptions & { reload: false }, + ): Promise; + + override softRemove>( + entities: T[], + options?: SaveOptions, + ): Promise<(T & Entity)[]>; + + override softRemove>( + entity: T, + options: SaveOptions & { reload: false }, + ): Promise; + + override softRemove>( + entity: T, + options?: SaveOptions, + ): Promise; + + override async softRemove>( + entityOrEntities: T | T[], + options?: SaveOptions, + ): Promise { + const formattedEntityOrEntities = this.formatData(entityOrEntities); + const result = await super.softRemove( + formattedEntityOrEntities as any, + options, + ); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override softDelete( + criteria: + | string + | string[] + | number + | number[] + | Date + | Date[] + | ObjectId + | ObjectId[] + | FindOptionsWhere, + ): Promise { + if (typeof criteria === 'object' && 'where' in criteria) { + criteria = this.transformOptions(criteria); + } + + return this.softDelete(criteria); + } + + /** + * RECOVERY METHODS + */ + override recover>( + entities: T[], + options: SaveOptions & { reload: false }, + ): Promise; + + override recover>( + entities: T[], + options?: SaveOptions, + ): Promise<(T & Entity)[]>; + + override recover>( + entity: T, + options: SaveOptions & { reload: false }, + ): Promise; + + override recover>( + entity: T, + options?: SaveOptions, + ): Promise; + + override async recover>( + entityOrEntities: T | T[], + options?: SaveOptions, + ): Promise { + const formattedEntityOrEntities = this.formatData(entityOrEntities); + const result = await super.recover( + formattedEntityOrEntities as any, + options, + ); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + override restore( + criteria: + | string + | string[] + | number + | number[] + | Date + | Date[] + | ObjectId + | ObjectId[] + | FindOptionsWhere, + ): Promise { + if (typeof criteria === 'object' && 'where' in criteria) { + criteria = this.transformOptions(criteria); + } + + return this.restore(criteria); + } + + /** + * INSERT METHODS + */ + override async insert( + entity: QueryDeepPartialEntity | QueryDeepPartialEntity[], + ): Promise { + const formatedEntity = this.formatData(entity); + const result = await super.insert(formatedEntity); + const formattedResult = this.formatResult(result); + + return formattedResult; + } + + /** + * UPDATE METHODS + */ + override update( + criteria: + | string + | string[] + | number + | number[] + | Date + | Date[] + | ObjectId + | ObjectId[] + | FindOptionsWhere, + partialEntity: QueryDeepPartialEntity, + ): Promise { + if (typeof criteria === 'object' && 'where' in criteria) { + criteria = this.transformOptions(criteria); + } + + return this.update(criteria, partialEntity); + } + + override upsert( + entityOrEntities: + | QueryDeepPartialEntity + | QueryDeepPartialEntity[], + conflictPathsOrOptions: string[] | UpsertOptions, + ): Promise { + const formattedEntityOrEntities = this.formatData(entityOrEntities); + + return this.upsert(formattedEntityOrEntities, conflictPathsOrOptions); + } + + /** + * EXIST METHODS + */ + override exist(options?: FindManyOptions): Promise { + const computedOptions = this.transformOptions(options); + + return super.exist(computedOptions); + } + + override exists(options?: FindManyOptions): Promise { + const computedOptions = this.transformOptions(options); + + return super.exists(computedOptions); + } + + override existsBy( + where: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + + return super.existsBy(computedOptions.where); + } + + /** + * COUNT METHODS + */ + override count(options?: FindManyOptions): Promise { + const computedOptions = this.transformOptions(options); + + return super.count(computedOptions); + } + + override countBy( + where: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + + return super.countBy(computedOptions.where); + } + + /** + * MATH METHODS + */ + override sum( + columnName: PickKeysByType, + where?: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + + return super.sum(columnName, computedOptions.where); + } + + override average( + columnName: PickKeysByType, + where?: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + + return super.average(columnName, computedOptions.where); + } + + override minimum( + columnName: PickKeysByType, + where?: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + + return super.minimum(columnName, computedOptions.where); + } + + override maximum( + columnName: PickKeysByType, + where?: FindOptionsWhere | FindOptionsWhere[], + ): Promise { + const computedOptions = this.transformOptions({ where }); + + return super.maximum(columnName, computedOptions.where); + } + + override increment( + conditions: FindOptionsWhere, + propertyPath: string, + value: number | string, + ): Promise { + const computedConditions = this.transformOptions({ where: conditions }); + + return this.increment(computedConditions.where, propertyPath, value); + } + + override decrement( + conditions: FindOptionsWhere, + propertyPath: string, + value: number | string, + ): Promise { + const computedConditions = this.transformOptions({ where: conditions }); + + return this.decrement(computedConditions.where, propertyPath, value); + } + + /** + * PRIVATE METHODS + */ + private getCompositeFieldMetadataArgs() { + const objectLiteral = ObjectLiteralStorage.getObjectLiteral( + this.target as any, + ); + + if (!objectLiteral) { + throw new Error('Object literal is missing'); + } + + const fieldMetadataArgsCollection = + metadataArgsStorage.filterFields(objectLiteral); + const compositeFieldMetadataArgsCollection = + fieldMetadataArgsCollection.filter((fieldMetadataArg) => + isCompositeFieldMetadataType(fieldMetadataArg.type), + ); + + return compositeFieldMetadataArgsCollection; + } + + private transformOptions< + T extends FindManyOptions | FindOneOptions | undefined, + >(options: T): T { + if (!options) { + return options; + } + + const transformedOptions = { ...options }; + + transformedOptions.where = this.formatData(options.where); + + return transformedOptions; + } + + private formatData(data: T): T { + if (!data) { + return data; + } + + if (Array.isArray(data)) { + return data.map((item) => this.formatData(item)) as T; + } + const compositeFieldMetadataArgsCollection = + this.getCompositeFieldMetadataArgs(); + const compositeFieldMetadataArgsMap = new Map( + compositeFieldMetadataArgsCollection.map((fieldMetadataArg) => [ + fieldMetadataArg.name, + fieldMetadataArg, + ]), + ); + const newData: object = {}; + + for (const [key, value] of Object.entries(data)) { + const fieldMetadataArgs = compositeFieldMetadataArgsMap.get(key); + + if (!fieldMetadataArgs) { + if (typeof value === 'object') { + newData[key] = this.formatData(value); + } else { + newData[key] = value; + } + continue; + } + + const compositeType = compositeTypeDefintions.get(fieldMetadataArgs.type); + + if (!compositeType) { + continue; + } + + for (const compositeProperty of compositeType.properties) { + const compositeKey = computeCompositeColumnName( + fieldMetadataArgs.name, + compositeProperty, + ); + const value = data?.[key]?.[compositeProperty.name]; + + if (value === undefined || value === null) { + continue; + } + + newData[compositeKey] = data[key][compositeProperty.name]; + } + } + + return newData as T; + } + + private formatResult(data: T): T { + if (!data) { + return data; + } + + if (Array.isArray(data)) { + return data.map((item) => this.formatResult(item)) as T; + } + + const objectLiteral = ObjectLiteralStorage.getObjectLiteral( + this.target as any, + ); + + if (!objectLiteral) { + throw new Error('Object literal is missing'); + } + + const fieldMetadataArgsCollection = + metadataArgsStorage.filterFields(objectLiteral); + const compositeFieldMetadataArgsCollection = + fieldMetadataArgsCollection.filter((fieldMetadataArg) => + isCompositeFieldMetadataType(fieldMetadataArg.type), + ); + const compositeFieldMetadataArgsMap = new Map( + compositeFieldMetadataArgsCollection.flatMap((fieldMetadataArg) => { + const compositeType = compositeTypeDefintions.get( + fieldMetadataArg.type, + ); + + if (!compositeType) return []; + + // Map each composite property to a [key, value] pair + return compositeType.properties.map((compositeProperty) => [ + computeCompositeColumnName(fieldMetadataArg.name, compositeProperty), + { + parentField: fieldMetadataArg.name, + ...compositeProperty, + }, + ]); + }), + ); + const newData: object = {}; + + for (const [key, value] of Object.entries(data)) { + const compositePropertyArgs = compositeFieldMetadataArgsMap.get(key); + + if (!compositePropertyArgs) { + if (typeof value === 'object') { + newData[key] = this.formatResult(value); + } else { + newData[key] = value; + } + continue; + } + + const { parentField, ...compositeProperty } = compositePropertyArgs; + + if (!newData[parentField]) { + newData[parentField] = {}; + } + + newData[parentField][compositeProperty.name] = value; + } + + return newData as T; + } +} 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 index e09b9bdbf..b924974e5 100644 --- 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 @@ -1,13 +1,19 @@ import { DataSource } from 'typeorm'; -export class DataSourceStorage { - private static readonly dataSources: Map = new Map(); +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; - public static getDataSource(key: string): DataSource | undefined { +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: DataSource): void { + public static setDataSource( + key: string, + dataSource: WorkspaceDataSource, + ): void { this.dataSources.set(key, dataSource); } 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 new file mode 100644 index 000000000..17d486468 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/storage/object-literal.storage.ts @@ -0,0 +1,30 @@ +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/twenty-orm-core.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-core.module.ts index 2f73b260d..72c50e747 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 @@ -18,17 +18,17 @@ import { import { entitySchemaFactories } from 'src/engine/twenty-orm/factories'; import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants'; -import { TwentyORMService } from 'src/engine/twenty-orm/twenty-orm.service'; -import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; +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'; @Global() @Module({ imports: [DataSourceModule], - providers: [...entitySchemaFactories, TwentyORMService], - exports: [EntitySchemaFactory, TwentyORMService], + providers: [...entitySchemaFactories, TwentyORMManager], + exports: [EntitySchemaFactory, TwentyORMManager], }) export class TwentyORMCoreModule extends ConfigurableModuleClass @@ -43,20 +43,18 @@ export class TwentyORMCoreModule provide: TWENTY_ORM_WORKSPACE_DATASOURCE, useFactory: async ( entitySchemaFactory: EntitySchemaFactory, - workspaceDatasourceFactory: WorkspaceDatasourceFactory, + scopedWorkspaceDatasourceFactory: ScopedWorkspaceDatasourceFactory, ) => { const entities = options.objects.map((entityClass) => entitySchemaFactory.create(entityClass), ); - const dataSource = - await workspaceDatasourceFactory.createWorkspaceDatasource( - entities, - ); + const scopedWorkspaceDataSource = + await scopedWorkspaceDatasourceFactory.create(entities); - return dataSource; + return scopedWorkspaceDataSource; }, - inject: [EntitySchemaFactory, WorkspaceDatasourceFactory], + inject: [EntitySchemaFactory, ScopedWorkspaceDatasourceFactory], }, ]; @@ -79,23 +77,21 @@ export class TwentyORMCoreModule provide: TWENTY_ORM_WORKSPACE_DATASOURCE, useFactory: async ( entitySchemaFactory: EntitySchemaFactory, - workspaceDatasourceFactory: WorkspaceDatasourceFactory, + scopedWorkspaceDatasourceFactory: ScopedWorkspaceDatasourceFactory, options: TwentyORMOptions, ) => { const entities = options.objects.map((entityClass) => entitySchemaFactory.create(entityClass), ); - const dataSource = - await workspaceDatasourceFactory.createWorkspaceDatasource( - entities, - ); + const scopedWorkspaceDataSource = + await scopedWorkspaceDatasourceFactory.create(entities); - return dataSource; + return scopedWorkspaceDataSource; }, inject: [ EntitySchemaFactory, - WorkspaceDatasourceFactory, + ScopedWorkspaceDatasourceFactory, MODULE_OPTIONS_TOKEN, ], }, 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 new file mode 100644 index 000000000..caaa4c216 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts @@ -0,0 +1,42 @@ +import { Injectable, Type } from '@nestjs/common'; + +import { ObjectLiteral } from 'typeorm'; + +import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; +import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; +import { ObjectLiteralStorage } from 'src/engine/twenty-orm/storage/object-literal.storage'; + +@Injectable() +export class TwentyORMManager { + constructor( + @InjectWorkspaceDatasource() + private readonly workspaceDataSource: WorkspaceDataSource, + private readonly entitySchemaFactory: EntitySchemaFactory, + private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory, + ) {} + + getRepository( + entityClass: Type, + ): WorkspaceRepository { + const entitySchema = this.entitySchemaFactory.create(entityClass); + + return this.workspaceDataSource.getRepository(entitySchema); + } + + async getRepositoryForWorkspace( + workspaceId: string, + entityClass: Type, + ): Promise> { + const entities = ObjectLiteralStorage.getAllEntitySchemas(); + const workspaceDataSource = await this.workspaceDataSourceFactory.create( + entities, + workspaceId, + ); + const entitySchema = this.entitySchemaFactory.create(entityClass); + + 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 22621e362..652692ace 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 @@ -1,11 +1,10 @@ import { Provider, Type } from '@nestjs/common'; import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { DataSource } from 'typeorm'; - import { getWorkspaceRepositoryToken } from 'src/engine/twenty-orm/utils/get-workspace-repository-token.util'; 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'; /** * Create providers for the given entities. @@ -16,11 +15,15 @@ export function createTwentyORMProviders( return (objects || []).map((object) => ({ provide: getWorkspaceRepositoryToken(object), useFactory: ( - dataSource: DataSource, + dataSource: WorkspaceDataSource | null, entitySchemaFactory: EntitySchemaFactory, ) => { const entity = entitySchemaFactory.create(object as Type); + if (!dataSource) { + return null; + } + return dataSource.getRepository(entity); }, inject: [TWENTY_ORM_WORKSPACE_DATASOURCE, EntitySchemaFactory], diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.service.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.service.ts deleted file mode 100644 index b77ac8c63..000000000 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable, Type } from '@nestjs/common'; - -import { DataSource, ObjectLiteral, Repository } from 'typeorm'; - -import { FlattenCompositeTypes } from 'src/engine/twenty-orm/interfaces/flatten-composite-types.interface'; - -import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; -import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; - -@Injectable() -export class TwentyORMService { - constructor( - @InjectWorkspaceDatasource() - private readonly workspaceDataSource: DataSource, - private readonly entitySchemaFactory: EntitySchemaFactory, - ) {} - - getRepository( - entityClass: Type, - ): Repository> { - const entitySchema = this.entitySchemaFactory.create(entityClass); - - return this.workspaceDataSource.getRepository>( - entitySchema, - ); - } -}