mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-25 13:02:15 +03:00
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<CompanyObjectMetadata>, ) {} async companies(): Promise<CompanyObjectMetadata[]> { // 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<CompanyObjectMetadata[]> { const repository = await this.twentyORMManager.getRepositoryForWorkspace( '8bb6e872-a71f-4341-82b5-6b56fa81cd77', CompanyObjectMetadata, ); const companies = await repository.find(); return companies; } } ```
This commit is contained in:
parent
154ae99ed3
commit
b207d10312
@ -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<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
): WorkspaceRepository<Entity> {
|
||||
return this.manager.getRepository(target);
|
||||
}
|
||||
|
||||
override createEntityManager(queryRunner?: QueryRunner): EntityManager {
|
||||
return new WorkspaceEntityManager(this, queryRunner);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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<typeof Inject> => Inject(getWorkspaceRepositoryToken(entity));
|
||||
|
@ -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<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
): WorkspaceRepository<Entity> {
|
||||
// find already created repository instance and return it if found
|
||||
const repoFromMap = this.repositories.get(target);
|
||||
|
||||
if (repoFromMap) return repoFromMap as WorkspaceRepository<Entity>;
|
||||
|
||||
const newRepository = new WorkspaceRepository<Entity>(
|
||||
target,
|
||||
this,
|
||||
this.queryRunner,
|
||||
);
|
||||
|
||||
this.repositories.set(target, newRepository);
|
||||
|
||||
return newRepository;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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<FlattenCompositeTypes<Entity>> {}
|
||||
> extends Repository<Entity> {
|
||||
/**
|
||||
* FIND METHODS
|
||||
*/
|
||||
override async find(options?: FindManyOptions<Entity>): Promise<Entity[]> {
|
||||
const computedOptions = this.transformOptions(options);
|
||||
const result = await super.find(computedOptions);
|
||||
const formattedResult = this.formatResult(result);
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
|
||||
override async findBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<Entity[]> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
const result = await super.findBy(computedOptions.where);
|
||||
const formattedResult = this.formatResult(result);
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
|
||||
override async findAndCount(
|
||||
options?: FindManyOptions<Entity>,
|
||||
): 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<Entity> | FindOptionsWhere<Entity>[],
|
||||
): 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<Entity>,
|
||||
): Promise<Entity | null> {
|
||||
const computedOptions = this.transformOptions(options);
|
||||
const result = await super.findOne(computedOptions);
|
||||
const formattedResult = this.formatResult(result);
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
|
||||
override async findOneBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<Entity | null> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
const result = await super.findOneBy(computedOptions.where);
|
||||
const formattedResult = this.formatResult(result);
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
|
||||
override async findOneOrFail(
|
||||
options: FindOneOptions<Entity>,
|
||||
): Promise<Entity> {
|
||||
const computedOptions = this.transformOptions(options);
|
||||
const result = await super.findOneOrFail(computedOptions);
|
||||
const formattedResult = this.formatResult(result);
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
|
||||
override async findOneByOrFail(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<Entity> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
const result = await super.findOneByOrFail(computedOptions.where);
|
||||
const formattedResult = this.formatResult(result);
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* SAVE METHODS
|
||||
*/
|
||||
override save<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
options: SaveOptions & { reload: false },
|
||||
): Promise<T[]>;
|
||||
|
||||
override save<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
options?: SaveOptions,
|
||||
): Promise<(T & Entity)[]>;
|
||||
|
||||
override save<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
options: SaveOptions & { reload: false },
|
||||
): Promise<T>;
|
||||
|
||||
override save<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
options?: SaveOptions,
|
||||
): Promise<T & Entity>;
|
||||
|
||||
override async save<T extends DeepPartial<Entity>>(
|
||||
entityOrEntities: T | T[],
|
||||
options?: SaveOptions,
|
||||
): Promise<T | T[]> {
|
||||
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<Entity[]>;
|
||||
|
||||
override remove(entity: Entity, options?: RemoveOptions): Promise<Entity>;
|
||||
|
||||
override async remove(
|
||||
entityOrEntities: Entity | Entity[],
|
||||
): Promise<Entity | Entity[]> {
|
||||
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<Entity>,
|
||||
): Promise<DeleteResult> {
|
||||
if (typeof criteria === 'object' && 'where' in criteria) {
|
||||
criteria = this.transformOptions(criteria);
|
||||
}
|
||||
|
||||
return this.delete(criteria);
|
||||
}
|
||||
|
||||
override softRemove<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
options: SaveOptions & { reload: false },
|
||||
): Promise<T[]>;
|
||||
|
||||
override softRemove<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
options?: SaveOptions,
|
||||
): Promise<(T & Entity)[]>;
|
||||
|
||||
override softRemove<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
options: SaveOptions & { reload: false },
|
||||
): Promise<T>;
|
||||
|
||||
override softRemove<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
options?: SaveOptions,
|
||||
): Promise<T & Entity>;
|
||||
|
||||
override async softRemove<T extends DeepPartial<Entity>>(
|
||||
entityOrEntities: T | T[],
|
||||
options?: SaveOptions,
|
||||
): Promise<T | T[]> {
|
||||
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<Entity>,
|
||||
): Promise<UpdateResult> {
|
||||
if (typeof criteria === 'object' && 'where' in criteria) {
|
||||
criteria = this.transformOptions(criteria);
|
||||
}
|
||||
|
||||
return this.softDelete(criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* RECOVERY METHODS
|
||||
*/
|
||||
override recover<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
options: SaveOptions & { reload: false },
|
||||
): Promise<T[]>;
|
||||
|
||||
override recover<T extends DeepPartial<Entity>>(
|
||||
entities: T[],
|
||||
options?: SaveOptions,
|
||||
): Promise<(T & Entity)[]>;
|
||||
|
||||
override recover<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
options: SaveOptions & { reload: false },
|
||||
): Promise<T>;
|
||||
|
||||
override recover<T extends DeepPartial<Entity>>(
|
||||
entity: T,
|
||||
options?: SaveOptions,
|
||||
): Promise<T & Entity>;
|
||||
|
||||
override async recover<T extends DeepPartial<Entity>>(
|
||||
entityOrEntities: T | T[],
|
||||
options?: SaveOptions,
|
||||
): Promise<T | T[]> {
|
||||
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<Entity>,
|
||||
): Promise<UpdateResult> {
|
||||
if (typeof criteria === 'object' && 'where' in criteria) {
|
||||
criteria = this.transformOptions(criteria);
|
||||
}
|
||||
|
||||
return this.restore(criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* INSERT METHODS
|
||||
*/
|
||||
override async insert(
|
||||
entity: QueryDeepPartialEntity<Entity> | QueryDeepPartialEntity<Entity>[],
|
||||
): Promise<InsertResult> {
|
||||
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<Entity>,
|
||||
partialEntity: QueryDeepPartialEntity<Entity>,
|
||||
): Promise<UpdateResult> {
|
||||
if (typeof criteria === 'object' && 'where' in criteria) {
|
||||
criteria = this.transformOptions(criteria);
|
||||
}
|
||||
|
||||
return this.update(criteria, partialEntity);
|
||||
}
|
||||
|
||||
override upsert(
|
||||
entityOrEntities:
|
||||
| QueryDeepPartialEntity<Entity>
|
||||
| QueryDeepPartialEntity<Entity>[],
|
||||
conflictPathsOrOptions: string[] | UpsertOptions<Entity>,
|
||||
): Promise<InsertResult> {
|
||||
const formattedEntityOrEntities = this.formatData(entityOrEntities);
|
||||
|
||||
return this.upsert(formattedEntityOrEntities, conflictPathsOrOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* EXIST METHODS
|
||||
*/
|
||||
override exist(options?: FindManyOptions<Entity>): Promise<boolean> {
|
||||
const computedOptions = this.transformOptions(options);
|
||||
|
||||
return super.exist(computedOptions);
|
||||
}
|
||||
|
||||
override exists(options?: FindManyOptions<Entity>): Promise<boolean> {
|
||||
const computedOptions = this.transformOptions(options);
|
||||
|
||||
return super.exists(computedOptions);
|
||||
}
|
||||
|
||||
override existsBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<boolean> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
|
||||
return super.existsBy(computedOptions.where);
|
||||
}
|
||||
|
||||
/**
|
||||
* COUNT METHODS
|
||||
*/
|
||||
override count(options?: FindManyOptions<Entity>): Promise<number> {
|
||||
const computedOptions = this.transformOptions(options);
|
||||
|
||||
return super.count(computedOptions);
|
||||
}
|
||||
|
||||
override countBy(
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
|
||||
return super.countBy(computedOptions.where);
|
||||
}
|
||||
|
||||
/**
|
||||
* MATH METHODS
|
||||
*/
|
||||
override sum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
|
||||
return super.sum(columnName, computedOptions.where);
|
||||
}
|
||||
|
||||
override average(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
|
||||
return super.average(columnName, computedOptions.where);
|
||||
}
|
||||
|
||||
override minimum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
|
||||
return super.minimum(columnName, computedOptions.where);
|
||||
}
|
||||
|
||||
override maximum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
const computedOptions = this.transformOptions({ where });
|
||||
|
||||
return super.maximum(columnName, computedOptions.where);
|
||||
}
|
||||
|
||||
override increment(
|
||||
conditions: FindOptionsWhere<Entity>,
|
||||
propertyPath: string,
|
||||
value: number | string,
|
||||
): Promise<UpdateResult> {
|
||||
const computedConditions = this.transformOptions({ where: conditions });
|
||||
|
||||
return this.increment(computedConditions.where, propertyPath, value);
|
||||
}
|
||||
|
||||
override decrement(
|
||||
conditions: FindOptionsWhere<Entity>,
|
||||
propertyPath: string,
|
||||
value: number | string,
|
||||
): Promise<UpdateResult> {
|
||||
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<Entity> | FindOneOptions<Entity> | undefined,
|
||||
>(options: T): T {
|
||||
if (!options) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const transformedOptions = { ...options };
|
||||
|
||||
transformedOptions.where = this.formatData(options.where);
|
||||
|
||||
return transformedOptions;
|
||||
}
|
||||
|
||||
private formatData<T>(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<T>(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;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
export class DataSourceStorage {
|
||||
private static readonly dataSources: Map<string, DataSource> = 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<string, WorkspaceDataSource> =
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { Type } from '@nestjs/common';
|
||||
|
||||
import { EntitySchema } from 'typeorm';
|
||||
|
||||
export class ObjectLiteralStorage {
|
||||
private static readonly objects: Map<EntitySchema, Type<any>> = new Map();
|
||||
|
||||
public static getObjectLiteral(target: EntitySchema): Type<any> | undefined {
|
||||
return this.objects.get(target);
|
||||
}
|
||||
|
||||
public static setObjectLiteral(
|
||||
target: EntitySchema,
|
||||
objectLiteral: Type<any>,
|
||||
): void {
|
||||
this.objects.set(target, objectLiteral);
|
||||
}
|
||||
|
||||
public static getAllObjects(): Type<any>[] {
|
||||
return Array.from(this.objects.values());
|
||||
}
|
||||
|
||||
public static getAllEntitySchemas(): EntitySchema[] {
|
||||
return Array.from(this.objects.keys());
|
||||
}
|
||||
|
||||
public static clear(): void {
|
||||
this.objects.clear();
|
||||
}
|
||||
}
|
@ -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,
|
||||
],
|
||||
},
|
||||
|
@ -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<T extends ObjectLiteral>(
|
||||
entityClass: Type<T>,
|
||||
): WorkspaceRepository<T> {
|
||||
const entitySchema = this.entitySchemaFactory.create(entityClass);
|
||||
|
||||
return this.workspaceDataSource.getRepository<T>(entitySchema);
|
||||
}
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
entityClass: Type<T>,
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
const entities = ObjectLiteralStorage.getAllEntitySchemas();
|
||||
const workspaceDataSource = await this.workspaceDataSourceFactory.create(
|
||||
entities,
|
||||
workspaceId,
|
||||
);
|
||||
const entitySchema = this.entitySchemaFactory.create(entityClass);
|
||||
|
||||
return workspaceDataSource.getRepository<T>(entitySchema);
|
||||
}
|
||||
}
|
@ -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],
|
||||
|
@ -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<T extends ObjectLiteral>(
|
||||
entityClass: Type<T>,
|
||||
): Repository<FlattenCompositeTypes<T>> {
|
||||
const entitySchema = this.entitySchemaFactory.create(entityClass);
|
||||
|
||||
return this.workspaceDataSource.getRepository<FlattenCompositeTypes<T>>(
|
||||
entitySchema,
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user