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:
Jérémy M 2024-05-06 14:12:11 +02:00 committed by GitHub
parent 154ae99ed3
commit b207d10312
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 784 additions and 75 deletions

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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));

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,
];

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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,
],
},

View File

@ -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);
}
}

View File

@ -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],

View File

@ -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,
);
}
}