feat: twenty orm for standard and custom objects (#6178)

### Overview

This PR builds upon #5153, adding the ability to get a repository for
custom objects. The `entitySchema` is now generated for both standard
and custom objects based on metadata stored in the database instead of
the decorated `WorkspaceEntity` in the code. This change ensures that
standard objects with custom fields and relations can also support
custom objects.

### Implementation Details

#### Key Changes:

- **Dynamic Schema Generation:** The `entitySchema` for standard and
custom objects is now dynamically generated from the metadata stored in
the database. This shift allows for greater flexibility and
adaptability, particularly for standard objects with custom fields and
relations.
  
- **Custom Object Repository Retrieval:** A repository for a custom
object can be retrieved using `TwentyORMManager` based on the object's
name. Here's an example of how this can be achieved:

  ```typescript
const repository = await this.twentyORMManager.getRepository('custom');
  /*
* `repository` variable will be typed as follows, ensuring that standard
fields and relations are properly typed:
   * const repository: WorkspaceRepository<CustomWorkspaceEntity & {
   *    [key: string]: any;
   * }>
   */
  const res = await repository.find({});
  ```

Fix #6179

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Jérémy M
2024-07-19 18:23:52 +02:00
committed by GitHub
parent a86cc5cb9c
commit 088d061b3e
33 changed files with 947 additions and 587 deletions

View File

@ -1,14 +1,14 @@
{ {
"$schema": "https://json.schemastore.org/swcrc", "$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true, "sourceMaps": true,
"jsc": { "jsc": {
"parser": { "parser": {
"syntax": "typescript", "syntax": "typescript",
"decorators": true, "decorators": true,
"dynamicImport": true "dynamicImport": true
},
"baseUrl": "./../../"
}, },
"minify": false "baseUrl": "./../../"
} },
"minify": false
}

View File

@ -29,9 +29,7 @@ const coreTypeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
useFactory: coreTypeORMFactory, useFactory: coreTypeORMFactory,
name: 'core', name: 'core',
}), }),
TwentyORMModule.register({ TwentyORMModule.register({}),
workspaceEntities: ['dist/src/**/*.workspace-entity{.ts,.js}'],
}),
EnvironmentModule, EnvironmentModule,
], ],
providers: [TypeORMService], providers: [TypeORMService],

View File

@ -2,11 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceSchemaStorageService } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory'; import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory'; import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
describe('WorkspaceSchemaFactory', () => { describe('WorkspaceSchemaFactory', () => {
let service: WorkspaceSchemaFactory; let service: WorkspaceSchemaFactory;
@ -36,7 +36,7 @@ describe('WorkspaceSchemaFactory', () => {
useValue: {}, useValue: {},
}, },
{ {
provide: WorkspaceSchemaStorageService, provide: WorkspaceCacheStorageService,
useValue: {}, useValue: {},
}, },
], ],

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { WorkspaceSchemaStorageModule } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { WorkspaceSchemaBuilderModule } from 'src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module'; import { WorkspaceSchemaBuilderModule } from 'src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module';
import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module'; import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module';
@ -20,7 +20,7 @@ import { WorkspaceSchemaFactory } from './workspace-schema.factory';
MetadataEngineModule, MetadataEngineModule,
WorkspaceSchemaBuilderModule, WorkspaceSchemaBuilderModule,
WorkspaceResolverBuilderModule, WorkspaceResolverBuilderModule,
WorkspaceSchemaStorageModule, WorkspaceCacheStorageModule,
], ],
providers: [WorkspaceSchemaFactory, ScalarsExplorerService], providers: [WorkspaceSchemaFactory, ScalarsExplorerService],
exports: [WorkspaceSchemaFactory], exports: [WorkspaceSchemaFactory],

View File

@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
import { WorkspaceSchemaStorageService } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
@Module({
imports: [ObjectMetadataModule, WorkspaceCacheVersionModule],
providers: [WorkspaceSchemaStorageService],
exports: [WorkspaceSchemaStorageService],
})
export class WorkspaceSchemaStorageModule {}

View File

@ -5,7 +5,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema';
import { gql } from 'graphql-tag'; import { gql } from 'graphql-tag';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceSchemaStorageService } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
@ -20,7 +20,7 @@ export class WorkspaceSchemaFactory {
private readonly scalarsExplorerService: ScalarsExplorerService, private readonly scalarsExplorerService: ScalarsExplorerService,
private readonly workspaceGraphQLSchemaFactory: WorkspaceGraphQLSchemaFactory, private readonly workspaceGraphQLSchemaFactory: WorkspaceGraphQLSchemaFactory,
private readonly workspaceResolverFactory: WorkspaceResolverFactory, private readonly workspaceResolverFactory: WorkspaceResolverFactory,
private readonly workspaceSchemaStorageService: WorkspaceSchemaStorageService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {} ) {}
async createGraphQLSchema( async createGraphQLSchema(
@ -42,11 +42,11 @@ export class WorkspaceSchemaFactory {
} }
// Validate cache version // Validate cache version
await this.workspaceSchemaStorageService.validateCacheVersion(workspaceId); await this.workspaceCacheStorageService.validateCacheVersion(workspaceId);
// Get object metadata from cache // Get object metadata from cache
let objectMetadataCollection = let objectMetadataCollection =
await this.workspaceSchemaStorageService.getObjectMetadataCollection( await this.workspaceCacheStorageService.getObjectMetadataCollection(
workspaceId, workspaceId,
); );
@ -55,7 +55,7 @@ export class WorkspaceSchemaFactory {
objectMetadataCollection = objectMetadataCollection =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId); await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
await this.workspaceSchemaStorageService.setObjectMetadataCollection( await this.workspaceCacheStorageService.setObjectMetadataCollection(
workspaceId, workspaceId,
objectMetadataCollection, objectMetadataCollection,
); );
@ -63,9 +63,9 @@ export class WorkspaceSchemaFactory {
// Get typeDefs from cache // Get typeDefs from cache
let typeDefs = let typeDefs =
await this.workspaceSchemaStorageService.getTypeDefs(workspaceId); await this.workspaceCacheStorageService.getTypeDefs(workspaceId);
let usedScalarNames = let usedScalarNames =
await this.workspaceSchemaStorageService.getUsedScalarNames(workspaceId); await this.workspaceCacheStorageService.getUsedScalarNames(workspaceId);
// If typeDefs are not cached, generate them // If typeDefs are not cached, generate them
if (!typeDefs || !usedScalarNames) { if (!typeDefs || !usedScalarNames) {
@ -79,11 +79,11 @@ export class WorkspaceSchemaFactory {
this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema); this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema);
typeDefs = printSchema(autoGeneratedSchema); typeDefs = printSchema(autoGeneratedSchema);
await this.workspaceSchemaStorageService.setTypeDefs( await this.workspaceCacheStorageService.setTypeDefs(
workspaceId, workspaceId,
typeDefs, typeDefs,
); );
await this.workspaceSchemaStorageService.setUsedScalarNames( await this.workspaceCacheStorageService.setUsedScalarNames(
workspaceId, workspaceId,
usedScalarNames, usedScalarNames,
); );

View File

@ -1,24 +1,18 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Any } from 'typeorm';
import omit from 'lodash.omit'; import omit from 'lodash.omit';
import { Any } from 'typeorm';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants'; import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto'; import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@Injectable() @Injectable()
export class TimelineCalendarEventService { export class TimelineCalendarEventService {
constructor( constructor(private readonly twentyORMManager: TwentyORMManager) {}
@InjectWorkspaceRepository(CalendarEventWorkspaceEntity)
private readonly calendarEventRepository: WorkspaceRepository<CalendarEventWorkspaceEntity>,
@InjectWorkspaceRepository(PersonWorkspaceEntity)
private readonly personRepository: WorkspaceRepository<PersonWorkspaceEntity>,
) {}
// TODO: Align return type with the entities to avoid mapping // TODO: Align return type with the entities to avoid mapping
async getCalendarEventsFromPersonIds( async getCalendarEventsFromPersonIds(
@ -28,7 +22,12 @@ export class TimelineCalendarEventService {
): Promise<TimelineCalendarEventsWithTotal> { ): Promise<TimelineCalendarEventsWithTotal> {
const offset = (page - 1) * pageSize; const offset = (page - 1) * pageSize;
const calendarEventIds = await this.calendarEventRepository.find({ const calendarEventRepository =
await this.twentyORMManager.getRepository<CalendarEventWorkspaceEntity>(
'calendarEvent',
);
const calendarEventIds = await calendarEventRepository.find({
where: { where: {
calendarEventParticipants: { calendarEventParticipants: {
personId: Any(personIds), personId: Any(personIds),
@ -55,7 +54,7 @@ export class TimelineCalendarEventService {
} }
// We've split the query into two parts, because we want to fetch all the participants without any filtering // We've split the query into two parts, because we want to fetch all the participants without any filtering
const [events, total] = await this.calendarEventRepository.findAndCount({ const [events, total] = await calendarEventRepository.findAndCount({
where: { where: {
id: Any(ids), id: Any(ids),
}, },
@ -131,7 +130,12 @@ export class TimelineCalendarEventService {
page = 1, page = 1,
pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE, pageSize: number = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
): Promise<TimelineCalendarEventsWithTotal> { ): Promise<TimelineCalendarEventsWithTotal> {
const personIds = await this.personRepository.find({ const personRepository =
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
'person',
);
const personIds = await personRepository.find({
where: { where: {
companyId, companyId,
}, },

View File

@ -2,12 +2,15 @@ import { Inject, Type } from '@nestjs/common';
import { ModuleRef, createContextId } from '@nestjs/core'; import { ModuleRef, createContextId } from '@nestjs/core';
import { Injector } from '@nestjs/core/injector/injector'; import { Injector } from '@nestjs/core/injector/injector';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
export class LoadServiceWithWorkspaceContext { export class LoadServiceWithWorkspaceContext {
private readonly injector = new Injector(); private readonly injector = new Injector();
constructor( constructor(
@Inject(ModuleRef) @Inject(ModuleRef)
private readonly moduleRef: ModuleRef, private readonly moduleRef: ModuleRef,
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
) {} ) {}
async load<T>(service: T, workspaceId: string): Promise<T> { async load<T>(service: T, workspaceId: string): Promise<T> {
@ -21,10 +24,12 @@ export class LoadServiceWithWorkspaceContext {
} }
const contextId = createContextId(); const contextId = createContextId();
const cacheVersion =
await this.workspaceCacheVersionService.getVersion(workspaceId);
if (this.moduleRef.registerRequestByContextId) { if (this.moduleRef.registerRequestByContextId) {
this.moduleRef.registerRequestByContextId( this.moduleRef.registerRequestByContextId(
{ req: { workspaceId } }, { req: { workspaceId, cacheVersion } },
contextId, contextId,
); );
} }

View File

@ -1,24 +1,39 @@
import { import {
DataSource, DataSource,
EntityManager, DataSourceOptions,
EntityTarget, EntityTarget,
ObjectLiteral, ObjectLiteral,
QueryRunner, QueryRunner,
} from 'typeorm'; } from 'typeorm';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/entity.manager'; import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/entity.manager';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
export class WorkspaceDataSource extends DataSource { export class WorkspaceDataSource extends DataSource {
readonly internalContext: WorkspaceInternalContext;
readonly manager: WorkspaceEntityManager; readonly manager: WorkspaceEntityManager;
constructor(
internalContext: WorkspaceInternalContext,
options: DataSourceOptions,
) {
super(options);
this.internalContext = internalContext;
// Recreate manager after internalContext has been initialized
this.manager = this.createEntityManager();
}
override getRepository<Entity extends ObjectLiteral>( override getRepository<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>, target: EntityTarget<Entity>,
): WorkspaceRepository<Entity> { ): WorkspaceRepository<Entity> {
return this.manager.getRepository(target); return this.manager.getRepository(target);
} }
override createEntityManager(queryRunner?: QueryRunner): EntityManager { override createEntityManager(
return new WorkspaceEntityManager(this, queryRunner); queryRunner?: QueryRunner,
): WorkspaceEntityManager {
return new WorkspaceEntityManager(this.internalContext, this, queryRunner);
} }
} }

View File

@ -1,8 +1,27 @@
import { EntityManager, EntityTarget, ObjectLiteral } from 'typeorm'; import {
DataSource,
EntityManager,
EntityTarget,
ObjectLiteral,
QueryRunner,
} from 'typeorm';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
export class WorkspaceEntityManager extends EntityManager { export class WorkspaceEntityManager extends EntityManager {
private readonly internalContext: WorkspaceInternalContext;
constructor(
internalContext: WorkspaceInternalContext,
connection: DataSource,
queryRunner?: QueryRunner,
) {
super(connection, queryRunner);
this.internalContext = internalContext;
}
override getRepository<Entity extends ObjectLiteral>( override getRepository<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>, target: EntityTarget<Entity>,
): WorkspaceRepository<Entity> { ): WorkspaceRepository<Entity> {
@ -14,6 +33,7 @@ export class WorkspaceEntityManager extends EntityManager {
} }
const newRepository = new WorkspaceRepository<Entity>( const newRepository = new WorkspaceRepository<Entity>(
this.internalContext,
target, target,
this, this,
this.queryRunner, this.queryRunner,

View File

@ -2,18 +2,17 @@ import { Injectable } from '@nestjs/common';
import { ColumnType, EntitySchemaColumnOptions } from 'typeorm'; import { ColumnType, EntitySchemaColumnOptions } from 'typeorm';
import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface';
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import {
import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util'; FieldMetadataEntity,
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
type EntitySchemaColumnMap = { type EntitySchemaColumnMap = {
[key: string]: EntitySchemaColumnOptions; [key: string]: EntitySchemaColumnOptions;
@ -22,17 +21,45 @@ type EntitySchemaColumnMap = {
@Injectable() @Injectable()
export class EntitySchemaColumnFactory { export class EntitySchemaColumnFactory {
create( create(
fieldMetadataArgsCollection: WorkspaceFieldMetadataArgs[], workspaceId: string,
relationMetadataArgsCollection: WorkspaceRelationMetadataArgs[], fieldMetadataCollection: FieldMetadataEntity[],
joinColumnsMetadataArgsCollection: WorkspaceJoinColumnsMetadataArgs[],
): EntitySchemaColumnMap { ): EntitySchemaColumnMap {
let entitySchemaColumnMap: EntitySchemaColumnMap = {}; let entitySchemaColumnMap: EntitySchemaColumnMap = {};
for (const fieldMetadataArgs of fieldMetadataArgsCollection) { for (const fieldMetadata of fieldMetadataCollection) {
const key = fieldMetadataArgs.name; const key = fieldMetadata.name;
if (isCompositeFieldMetadataType(fieldMetadataArgs.type)) { if (isRelationFieldMetadataType(fieldMetadata.type)) {
const compositeColumns = this.createCompositeColumns(fieldMetadataArgs); const relationMetadata =
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata;
if (!relationMetadata) {
throw new Error(
`Relation metadata is missing for field ${fieldMetadata.name}`,
);
}
const joinColumnKey = fieldMetadata.name + 'Id';
const joinColumn = fieldMetadataCollection.find(
(field) => field.name === joinColumnKey,
)
? joinColumnKey
: null;
if (joinColumn) {
entitySchemaColumnMap[joinColumn] = {
name: joinColumn,
type: 'uuid',
nullable: fieldMetadata.isNullable,
};
}
continue;
}
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const compositeColumns = this.createCompositeColumns(fieldMetadata);
entitySchemaColumnMap = { entitySchemaColumnMap = {
...entitySchemaColumnMap, ...entitySchemaColumnMap,
@ -42,39 +69,23 @@ export class EntitySchemaColumnFactory {
continue; continue;
} }
const columnType = fieldMetadataTypeToColumnType(fieldMetadataArgs.type); const columnType = fieldMetadataTypeToColumnType(fieldMetadata.type);
const defaultValue = serializeDefaultValue( const defaultValue = serializeDefaultValue(fieldMetadata.defaultValue);
fieldMetadataArgs.defaultValue,
);
entitySchemaColumnMap[key] = { entitySchemaColumnMap[key] = {
name: key, name: key,
type: columnType as ColumnType, type: columnType as ColumnType,
primary: fieldMetadataArgs.isPrimary, // TODO: We should double check that
nullable: fieldMetadataArgs.isNullable, primary: key === 'id',
nullable: fieldMetadata.isNullable,
createDate: key === 'createdAt', createDate: key === 'createdAt',
updateDate: key === 'updatedAt', updateDate: key === 'updatedAt',
array: fieldMetadataArgs.type === FieldMetadataType.MULTI_SELECT, array: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
default: defaultValue, default: defaultValue,
}; };
for (const relationMetadataArgs of relationMetadataArgsCollection) { if (isEnumFieldMetadataType(fieldMetadata.type)) {
const joinColumn = getJoinColumn( const values = fieldMetadata.options?.map((option) => option.value);
joinColumnsMetadataArgsCollection,
relationMetadataArgs,
);
if (joinColumn) {
entitySchemaColumnMap[joinColumn] = {
name: joinColumn,
type: 'uuid',
nullable: relationMetadataArgs.isNullable,
};
}
}
if (isEnumFieldMetadataType(fieldMetadataArgs.type)) {
const values = fieldMetadataArgs.options?.map((option) => option.value);
if (values && values.length > 0) { if (values && values.length > 0) {
entitySchemaColumnMap[key].enum = values; entitySchemaColumnMap[key].enum = values;
@ -86,25 +97,25 @@ export class EntitySchemaColumnFactory {
} }
private createCompositeColumns( private createCompositeColumns(
fieldMetadataArgs: WorkspaceFieldMetadataArgs, fieldMetadata: FieldMetadataEntity,
): EntitySchemaColumnMap { ): EntitySchemaColumnMap {
const entitySchemaColumnMap: EntitySchemaColumnMap = {}; const entitySchemaColumnMap: EntitySchemaColumnMap = {};
const compositeType = compositeTypeDefintions.get(fieldMetadataArgs.type); const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
if (!compositeType) { if (!compositeType) {
throw new Error( throw new Error(
`Composite type ${fieldMetadataArgs.type} is not defined in compositeTypeDefintions`, `Composite type ${fieldMetadata.type} is not defined in compositeTypeDefintions`,
); );
} }
for (const compositeProperty of compositeType.properties) { for (const compositeProperty of compositeType.properties) {
const columnName = computeCompositeColumnName( const columnName = computeCompositeColumnName(
fieldMetadataArgs.name, fieldMetadata.name,
compositeProperty, compositeProperty,
); );
const columnType = fieldMetadataTypeToColumnType(compositeProperty.type); const columnType = fieldMetadataTypeToColumnType(compositeProperty.type);
const defaultValue = serializeDefaultValue( const defaultValue = serializeDefaultValue(
fieldMetadataArgs.defaultValue?.[compositeProperty.name], fieldMetadata.defaultValue?.[compositeProperty.name],
); );
entitySchemaColumnMap[columnName] = { entitySchemaColumnMap[columnName] = {

View File

@ -1,14 +1,11 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EntitySchemaRelationOptions } from 'typeorm'; import { EntitySchemaRelationOptions } from 'typeorm';
import { RelationType } from 'typeorm/metadata/types/RelationTypes';
import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceJoinColumnsMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-join-columns-metadata-args.interface'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { determineRelationDetails } from 'src/engine/twenty-orm/utils/determine-relation-details.util';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util';
type EntitySchemaRelationMap = { type EntitySchemaRelationMap = {
[key: string]: EntitySchemaRelationOptions; [key: string]: EntitySchemaRelationOptions;
@ -16,55 +13,45 @@ type EntitySchemaRelationMap = {
@Injectable() @Injectable()
export class EntitySchemaRelationFactory { export class EntitySchemaRelationFactory {
create( constructor(
// eslint-disable-next-line @typescript-eslint/ban-types private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
target: Function, ) {}
relationMetadataArgsCollection: WorkspaceRelationMetadataArgs[],
joinColumnsMetadataArgsCollection: WorkspaceJoinColumnsMetadataArgs[], async create(
): EntitySchemaRelationMap { workspaceId: string,
fieldMetadataCollection: FieldMetadataEntity[],
): Promise<EntitySchemaRelationMap> {
const entitySchemaRelationMap: EntitySchemaRelationMap = {}; const entitySchemaRelationMap: EntitySchemaRelationMap = {};
for (const relationMetadataArgs of relationMetadataArgsCollection) { for (const fieldMetadata of fieldMetadataCollection) {
const objectName = convertClassNameToObjectMetadataName(target.name); if (!isRelationFieldMetadataType(fieldMetadata.type)) {
const oppositeTarget = relationMetadataArgs.inverseSideTarget(); continue;
const oppositeObjectName = convertClassNameToObjectMetadataName( }
oppositeTarget.name,
); const relationMetadata =
const relationType = this.getRelationType(relationMetadataArgs); fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
const joinColumn = getJoinColumn(
joinColumnsMetadataArgsCollection, if (!relationMetadata) {
relationMetadataArgs, throw new Error(
`Relation metadata is missing for field ${fieldMetadata.name}`,
);
}
const relationDetails = await determineRelationDetails(
workspaceId,
fieldMetadata,
relationMetadata,
this.workspaceCacheStorageService,
); );
entitySchemaRelationMap[relationMetadataArgs.name] = { entitySchemaRelationMap[fieldMetadata.name] = {
type: relationType, type: relationDetails.relationType,
target: oppositeObjectName, target: relationDetails.target,
inverseSide: relationMetadataArgs.inverseSideFieldKey ?? objectName, inverseSide: relationDetails.inverseSide,
joinColumn: joinColumn joinColumn: relationDetails.joinColumn,
? {
name: joinColumn,
}
: undefined,
}; };
} }
return entitySchemaRelationMap; return entitySchemaRelationMap;
} }
private getRelationType(
relationMetadataArgs: WorkspaceRelationMetadataArgs,
): RelationType {
switch (relationMetadataArgs.type) {
case RelationMetadataType.ONE_TO_MANY:
return 'one-to-many';
case RelationMetadataType.MANY_TO_ONE:
return 'many-to-one';
case RelationMetadataType.ONE_TO_ONE:
return 'one-to-one';
case RelationMetadataType.MANY_TO_MANY:
return 'many-to-many';
default:
throw new Error('Invalid relation type');
}
}
} }

View File

@ -1,53 +1,79 @@
import { Injectable, Type } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EntitySchema } from 'typeorm'; import { EntitySchema } from 'typeorm';
import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; 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 { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectLiteralStorage } from 'src/engine/twenty-orm/storage/object-literal.storage'; import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable() @Injectable()
export class EntitySchemaFactory { export class EntitySchemaFactory {
constructor( constructor(
private readonly entitySchemaColumnFactory: EntitySchemaColumnFactory, private readonly entitySchemaColumnFactory: EntitySchemaColumnFactory,
private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory, private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {} ) {}
create<T>(target: Type<T>): EntitySchema { async create(
const entityMetadataArgs = metadataArgsStorage.filterEntities(target); workspaceId: string,
objectMetadata: ObjectMetadataEntity,
): Promise<EntitySchema>;
if (!entityMetadataArgs) { async create(
throw new Error('Entity metadata args are missing on this target'); workspaceId: string,
objectMetadataName: string,
): Promise<EntitySchema>;
async create(
workspaceId: string,
objectMetadataOrObjectMetadataName: ObjectMetadataEntity | string,
): Promise<EntitySchema> {
let objectMetadata: ObjectMetadataEntity | null =
typeof objectMetadataOrObjectMetadataName !== 'string'
? objectMetadataOrObjectMetadataName
: null;
if (typeof objectMetadataOrObjectMetadataName === 'string') {
objectMetadata =
(await this.workspaceCacheStorageService.getObjectMetadata(
workspaceId,
(objectMetadata) =>
objectMetadata.nameSingular === objectMetadataOrObjectMetadataName,
)) ?? null;
} }
const fieldMetadataArgsCollection = if (!objectMetadata) {
metadataArgsStorage.filterFields(target); throw new Error('Object metadata not found');
const joinColumnsMetadataArgsCollection = }
metadataArgsStorage.filterJoinColumns(target);
const relationMetadataArgsCollection =
metadataArgsStorage.filterRelations(target);
const columns = this.entitySchemaColumnFactory.create( const columns = this.entitySchemaColumnFactory.create(
fieldMetadataArgsCollection, workspaceId,
relationMetadataArgsCollection, objectMetadata.fields,
joinColumnsMetadataArgsCollection,
); );
const relations = this.entitySchemaRelationFactory.create( const relations = await this.entitySchemaRelationFactory.create(
target, workspaceId,
relationMetadataArgsCollection, objectMetadata.fields,
joinColumnsMetadataArgsCollection,
); );
const entitySchema = new EntitySchema({ const entitySchema = new EntitySchema({
name: entityMetadataArgs.nameSingular, name: objectMetadata.nameSingular,
tableName: entityMetadataArgs.nameSingular, tableName: computeTableName(
objectMetadata.nameSingular,
objectMetadata.isCustom,
),
columns, columns,
relations, relations,
}); });
ObjectLiteralStorage.setObjectLiteral(entitySchema, target); WorkspaceEntitiesStorage.setEntitySchema(
workspaceId,
objectMetadata.nameSingular,
entitySchema,
);
return entitySchema; return entitySchema;
} }

View File

@ -1,7 +1,7 @@
import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; 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 { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { ScopedWorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-datasource.factory'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
export const entitySchemaFactories = [ export const entitySchemaFactories = [
@ -9,5 +9,5 @@ export const entitySchemaFactories = [
EntitySchemaRelationFactory, EntitySchemaRelationFactory,
EntitySchemaFactory, EntitySchemaFactory,
WorkspaceDatasourceFactory, WorkspaceDatasourceFactory,
ScopedWorkspaceDatasourceFactory, ScopedWorkspaceContextFactory,
]; ];

View File

@ -0,0 +1,26 @@
import { Inject, Injectable, Optional, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
@Injectable({ scope: Scope.REQUEST })
export class ScopedWorkspaceContextFactory {
constructor(
@Optional()
@Inject(REQUEST)
private readonly request: Request | null,
) {}
public create(): {
workspaceId: string | null;
cacheVersion: string | null;
} {
const workspaceId: string | undefined =
this.request?.['req']?.['workspaceId'];
const cacheVersion: string | undefined =
this.request?.['req']?.['cacheVersion'];
return {
workspaceId: workspaceId ?? null,
cacheVersion: cacheVersion ?? null,
};
}
}

View File

@ -1,27 +0,0 @@
import { Inject, Injectable, Optional, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { EntitySchema } from 'typeorm';
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
@Injectable({ scope: Scope.REQUEST })
export class ScopedWorkspaceDatasourceFactory {
constructor(
@Optional()
@Inject(REQUEST)
private readonly request: Request | null,
private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory,
) {}
public async create(entities: EntitySchema[]) {
const workspaceId: string | undefined =
this.request?.['req']?.['workspaceId'];
if (!workspaceId) {
return null;
}
return this.workspaceDataSourceFactory.create(entities, workspaceId);
}
}

View File

@ -4,27 +4,21 @@ import { EntitySchema } from 'typeorm';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { DataSourceStorage } from 'src/engine/twenty-orm/storage/data-source.storage';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable() @Injectable()
export class WorkspaceDatasourceFactory { export class WorkspaceDatasourceFactory {
constructor( constructor(
private readonly dataSourceService: DataSourceService, private readonly dataSourceService: DataSourceService,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {} ) {}
public async create( public async create(
entities: EntitySchema[], entities: EntitySchema[],
workspaceId: string, workspaceId: string,
): Promise<WorkspaceDataSource | null> { ): Promise<WorkspaceDataSource | null> {
const storedWorkspaceDataSource =
DataSourceStorage.getDataSource(workspaceId);
if (storedWorkspaceDataSource) {
return storedWorkspaceDataSource;
}
const dataSourceMetadata = const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
workspaceId, workspaceId,
@ -34,27 +28,31 @@ export class WorkspaceDatasourceFactory {
return null; return null;
} }
const workspaceDataSource = new WorkspaceDataSource({ const workspaceDataSource = new WorkspaceDataSource(
url: {
dataSourceMetadata.url ?? workspaceId,
this.environmentService.get('PG_DATABASE_URL'), workspaceCacheStorage: this.workspaceCacheStorageService,
type: 'postgres', },
logging: this.environmentService.get('DEBUG_MODE') {
? ['query', 'error'] url:
: ['error'], dataSourceMetadata.url ??
schema: dataSourceMetadata.schema, this.environmentService.get('PG_DATABASE_URL'),
entities, type: 'postgres',
ssl: this.environmentService.get('PG_SSL_ALLOW_SELF_SIGNED') logging: this.environmentService.get('DEBUG_MODE')
? { ? ['query', 'error']
rejectUnauthorized: false, : ['error'],
} schema: dataSourceMetadata.schema,
: undefined, entities,
}); ssl: this.environmentService.get('PG_SSL_ALLOW_SELF_SIGNED')
? {
rejectUnauthorized: false,
}
: undefined,
},
);
await workspaceDataSource.initialize(); await workspaceDataSource.initialize();
DataSourceStorage.setDataSource(workspaceId, workspaceDataSource);
return workspaceDataSource; return workspaceDataSource;
} }
} }

View File

@ -1,10 +1,7 @@
import { FactoryProvider, ModuleMetadata, Type } from '@nestjs/common'; import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; // eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TwentyORMOptions {}
export interface TwentyORMOptions {
workspaceEntities: (Type<BaseWorkspaceEntity> | string)[];
}
export type TwentyORMModuleAsyncOptions = { export type TwentyORMModuleAsyncOptions = {
useFactory: (...args: any[]) => TwentyORMOptions | Promise<TwentyORMOptions>; useFactory: (...args: any[]) => TwentyORMOptions | Promise<TwentyORMOptions>;

View File

@ -0,0 +1,6 @@
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
export interface WorkspaceInternalContext {
workspaceId: string;
workspaceCacheStorage: WorkspaceCacheStorageService;
}

View File

@ -2,12 +2,15 @@ import {
DeepPartial, DeepPartial,
DeleteResult, DeleteResult,
EntityManager, EntityManager,
EntitySchema,
EntityTarget,
FindManyOptions, FindManyOptions,
FindOneOptions, FindOneOptions,
FindOptionsWhere, FindOptionsWhere,
InsertResult, InsertResult,
ObjectId, ObjectId,
ObjectLiteral, ObjectLiteral,
QueryRunner,
RemoveOptions, RemoveOptions,
Repository, Repository,
SaveOptions, SaveOptions,
@ -17,16 +20,31 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity
import { UpsertOptions } from 'typeorm/repository/UpsertOptions'; import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
import { PickKeysByType } from 'typeorm/common/PickKeysByType'; import { PickKeysByType } from 'typeorm/common/PickKeysByType';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { ObjectLiteralStorage } from 'src/engine/twenty-orm/storage/object-literal.storage';
import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { 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 { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { isPlainObject } from 'src/utils/is-plain-object'; import { isPlainObject } from 'src/utils/is-plain-object';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export class WorkspaceRepository< export class WorkspaceRepository<
Entity extends ObjectLiteral, Entity extends ObjectLiteral,
> extends Repository<Entity> { > extends Repository<Entity> {
private readonly internalContext: WorkspaceInternalContext;
constructor(
internalContext: WorkspaceInternalContext,
target: EntityTarget<Entity>,
manager: EntityManager,
queryRunner?: QueryRunner,
) {
super(target, manager, queryRunner);
this.internalContext = internalContext;
}
/** /**
* FIND METHODS * FIND METHODS
*/ */
@ -35,9 +53,9 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<Entity[]> { ): Promise<Entity[]> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options); const computedOptions = await this.transformOptions(options);
const result = await manager.find(this.target, computedOptions); const result = await manager.find(this.target, computedOptions);
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -47,9 +65,9 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<Entity[]> { ): Promise<Entity[]> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
const result = await manager.findBy(this.target, computedOptions.where); const result = await manager.findBy(this.target, computedOptions.where);
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -59,9 +77,9 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<[Entity[], number]> { ): Promise<[Entity[], number]> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options); const computedOptions = await this.transformOptions(options);
const result = await manager.findAndCount(this.target, computedOptions); const result = await manager.findAndCount(this.target, computedOptions);
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -71,12 +89,12 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<[Entity[], number]> { ): Promise<[Entity[], number]> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
const result = await manager.findAndCountBy( const result = await manager.findAndCountBy(
this.target, this.target,
computedOptions.where, computedOptions.where,
); );
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -86,9 +104,9 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<Entity | null> { ): Promise<Entity | null> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options); const computedOptions = await this.transformOptions(options);
const result = await manager.findOne(this.target, computedOptions); const result = await manager.findOne(this.target, computedOptions);
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -98,9 +116,9 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<Entity | null> { ): Promise<Entity | null> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
const result = await manager.findOneBy(this.target, computedOptions.where); const result = await manager.findOneBy(this.target, computedOptions.where);
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -110,9 +128,9 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<Entity> { ): Promise<Entity> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options); const computedOptions = await this.transformOptions(options);
const result = await manager.findOneOrFail(this.target, computedOptions); const result = await manager.findOneOrFail(this.target, computedOptions);
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -122,12 +140,12 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<Entity> { ): Promise<Entity> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
const result = await manager.findOneByOrFail( const result = await manager.findOneByOrFail(
this.target, this.target,
computedOptions.where, computedOptions.where,
); );
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -165,14 +183,25 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<T | T[]> { ): Promise<T | T[]> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities); const formattedEntityOrEntities = await this.formatData(entityOrEntities);
const result = await manager.save( let result: T | T[];
this.target,
formattedEntityOrEntities as any,
options,
);
const formattedResult = this.formatResult(result); // Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion
if (Array.isArray(formattedEntityOrEntities)) {
result = await manager.save(
this.target,
formattedEntityOrEntities,
options,
);
} else {
result = await manager.save(
this.target,
formattedEntityOrEntities,
options,
);
}
const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -198,18 +227,18 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<Entity | Entity[]> { ): Promise<Entity | Entity[]> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities); const formattedEntityOrEntities = await this.formatData(entityOrEntities);
const result = await manager.remove( const result = await manager.remove(
this.target, this.target,
formattedEntityOrEntities, formattedEntityOrEntities,
options, options,
); );
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
override delete( override async delete(
criteria: criteria:
| string | string
| string[] | string[]
@ -225,7 +254,7 @@ export class WorkspaceRepository<
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
if (typeof criteria === 'object' && 'where' in criteria) { if (typeof criteria === 'object' && 'where' in criteria) {
criteria = this.transformOptions(criteria); criteria = await this.transformOptions(criteria);
} }
return manager.delete(this.target, criteria); return manager.delete(this.target, criteria);
@ -261,18 +290,30 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<T | T[]> { ): Promise<T | T[]> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities); const formattedEntityOrEntities = await this.formatData(entityOrEntities);
const result = await manager.softRemove( let result: T | T[];
this.target,
formattedEntityOrEntities as any, // Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion
options, if (Array.isArray(formattedEntityOrEntities)) {
); result = await manager.softRemove(
const formattedResult = this.formatResult(result); this.target,
formattedEntityOrEntities,
options,
);
} else {
result = await manager.softRemove(
this.target,
formattedEntityOrEntities,
options,
);
}
const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
override softDelete( override async softDelete(
criteria: criteria:
| string | string
| string[] | string[]
@ -288,7 +329,7 @@ export class WorkspaceRepository<
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
if (typeof criteria === 'object' && 'where' in criteria) { if (typeof criteria === 'object' && 'where' in criteria) {
criteria = this.transformOptions(criteria); criteria = await this.transformOptions(criteria);
} }
return manager.softDelete(this.target, criteria); return manager.softDelete(this.target, criteria);
@ -327,18 +368,30 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<T | T[]> { ): Promise<T | T[]> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities); const formattedEntityOrEntities = await this.formatData(entityOrEntities);
const result = await manager.recover( let result: T | T[];
this.target,
formattedEntityOrEntities as any, // Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion
options, if (Array.isArray(formattedEntityOrEntities)) {
); result = await manager.recover(
const formattedResult = this.formatResult(result); this.target,
formattedEntityOrEntities,
options,
);
} else {
result = await manager.recover(
this.target,
formattedEntityOrEntities,
options,
);
}
const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
override restore( override async restore(
criteria: criteria:
| string | string
| string[] | string[]
@ -354,7 +407,7 @@ export class WorkspaceRepository<
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
if (typeof criteria === 'object' && 'where' in criteria) { if (typeof criteria === 'object' && 'where' in criteria) {
criteria = this.transformOptions(criteria); criteria = await this.transformOptions(criteria);
} }
return manager.restore(this.target, criteria); return manager.restore(this.target, criteria);
@ -368,9 +421,9 @@ export class WorkspaceRepository<
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<InsertResult> { ): Promise<InsertResult> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const formatedEntity = this.formatData(entity); const formatedEntity = await this.formatData(entity);
const result = await manager.insert(this.target, formatedEntity); const result = await manager.insert(this.target, formatedEntity);
const formattedResult = this.formatResult(result); const formattedResult = await this.formatResult(result);
return formattedResult; return formattedResult;
} }
@ -378,7 +431,7 @@ export class WorkspaceRepository<
/** /**
* UPDATE METHODS * UPDATE METHODS
*/ */
override update( override async update(
criteria: criteria:
| string | string
| string[] | string[]
@ -395,13 +448,13 @@ export class WorkspaceRepository<
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
if (typeof criteria === 'object' && 'where' in criteria) { if (typeof criteria === 'object' && 'where' in criteria) {
criteria = this.transformOptions(criteria); criteria = await this.transformOptions(criteria);
} }
return manager.update(this.target, criteria, partialEntity); return manager.update(this.target, criteria, partialEntity);
} }
override upsert( override async upsert(
entityOrEntities: entityOrEntities:
| QueryDeepPartialEntity<Entity> | QueryDeepPartialEntity<Entity>
| QueryDeepPartialEntity<Entity>[], | QueryDeepPartialEntity<Entity>[],
@ -410,7 +463,7 @@ export class WorkspaceRepository<
): Promise<InsertResult> { ): Promise<InsertResult> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities); const formattedEntityOrEntities = await this.formatData(entityOrEntities);
return manager.upsert( return manager.upsert(
this.target, this.target,
@ -422,22 +475,22 @@ export class WorkspaceRepository<
/** /**
* EXIST METHODS * EXIST METHODS
*/ */
override exists( override async exists(
options?: FindManyOptions<Entity>, options?: FindManyOptions<Entity>,
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<boolean> { ): Promise<boolean> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options); const computedOptions = await this.transformOptions(options);
return manager.exists(this.target, computedOptions); return manager.exists(this.target, computedOptions);
} }
override existsBy( override async existsBy(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[], where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<boolean> { ): Promise<boolean> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
return manager.existsBy(this.target, computedOptions.where); return manager.existsBy(this.target, computedOptions.where);
} }
@ -445,22 +498,22 @@ export class WorkspaceRepository<
/** /**
* COUNT METHODS * COUNT METHODS
*/ */
override count( override async count(
options?: FindManyOptions<Entity>, options?: FindManyOptions<Entity>,
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<number> { ): Promise<number> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options); const computedOptions = await this.transformOptions(options);
return manager.count(this.target, computedOptions); return manager.count(this.target, computedOptions);
} }
override countBy( override async countBy(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[], where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<number> { ): Promise<number> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
return manager.countBy(this.target, computedOptions.where); return manager.countBy(this.target, computedOptions.where);
} }
@ -468,58 +521,60 @@ export class WorkspaceRepository<
/** /**
* MATH METHODS * MATH METHODS
*/ */
override sum( override async sum(
columnName: PickKeysByType<Entity, number>, columnName: PickKeysByType<Entity, number>,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[], where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<number | null> { ): Promise<number | null> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
return manager.sum(this.target, columnName, computedOptions.where); return manager.sum(this.target, columnName, computedOptions.where);
} }
override average( override async average(
columnName: PickKeysByType<Entity, number>, columnName: PickKeysByType<Entity, number>,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[], where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<number | null> { ): Promise<number | null> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
return manager.average(this.target, columnName, computedOptions.where); return manager.average(this.target, columnName, computedOptions.where);
} }
override minimum( override async minimum(
columnName: PickKeysByType<Entity, number>, columnName: PickKeysByType<Entity, number>,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[], where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<number | null> { ): Promise<number | null> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
return manager.minimum(this.target, columnName, computedOptions.where); return manager.minimum(this.target, columnName, computedOptions.where);
} }
override maximum( override async maximum(
columnName: PickKeysByType<Entity, number>, columnName: PickKeysByType<Entity, number>,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[], where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<number | null> { ): Promise<number | null> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where }); const computedOptions = await this.transformOptions({ where });
return manager.maximum(this.target, columnName, computedOptions.where); return manager.maximum(this.target, columnName, computedOptions.where);
} }
override increment( override async increment(
conditions: FindOptionsWhere<Entity>, conditions: FindOptionsWhere<Entity>,
propertyPath: string, propertyPath: string,
value: number | string, value: number | string,
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<UpdateResult> { ): Promise<UpdateResult> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedConditions = this.transformOptions({ where: conditions }); const computedConditions = await this.transformOptions({
where: conditions,
});
return manager.increment( return manager.increment(
this.target, this.target,
@ -529,14 +584,16 @@ export class WorkspaceRepository<
); );
} }
override decrement( override async decrement(
conditions: FindOptionsWhere<Entity>, conditions: FindOptionsWhere<Entity>,
propertyPath: string, propertyPath: string,
value: number | string, value: number | string,
entityManager?: EntityManager, entityManager?: EntityManager,
): Promise<UpdateResult> { ): Promise<UpdateResult> {
const manager = entityManager || this.manager; const manager = entityManager || this.manager;
const computedConditions = this.transformOptions({ where: conditions }); const computedConditions = await this.transformOptions({
where: conditions,
});
return manager.decrement( return manager.decrement(
this.target, this.target,
@ -549,70 +606,85 @@ export class WorkspaceRepository<
/** /**
* PRIVATE METHODS * PRIVATE METHODS
*/ */
private getCompositeFieldMetadataArgs() { private async getObjectMetadataFromTarget() {
const objectLiteral = ObjectLiteralStorage.getObjectLiteral( const objectMetadataName = WorkspaceEntitiesStorage.getObjectMetadataName(
this.target as any, this.internalContext.workspaceId,
this.target as EntitySchema,
); );
if (!objectLiteral) { if (!objectMetadataName) {
throw new Error('Object literal is missing'); throw new Error('Object metadata name is missing');
} }
const fieldMetadataArgsCollection = return this.internalContext.workspaceCacheStorage.getObjectMetadata(
metadataArgsStorage.filterFields(objectLiteral); this.internalContext.workspaceId,
const compositeFieldMetadataArgsCollection = (objectMetadata) => objectMetadata.nameSingular === objectMetadataName,
fieldMetadataArgsCollection.filter((fieldMetadataArg) => );
isCompositeFieldMetadataType(fieldMetadataArg.type),
);
return compositeFieldMetadataArgsCollection;
} }
private transformOptions< private async getCompositeFieldMetadata(
objectMetadata?: ObjectMetadataEntity,
) {
objectMetadata ??= await this.getObjectMetadataFromTarget();
if (!objectMetadata) {
throw new Error('Object metadata entity is missing');
}
const compositeFieldMetadataCollection = objectMetadata.fields.filter(
(fieldMetadata) => isCompositeFieldMetadataType(fieldMetadata.type),
);
return compositeFieldMetadataCollection;
}
private async transformOptions<
T extends FindManyOptions<Entity> | FindOneOptions<Entity> | undefined, T extends FindManyOptions<Entity> | FindOneOptions<Entity> | undefined,
>(options: T): T { >(options: T): Promise<T> {
if (!options) { if (!options) {
return options; return options;
} }
const transformedOptions = { ...options }; const transformedOptions = { ...options };
transformedOptions.where = this.formatData(options.where); transformedOptions.where = await this.formatData(options.where);
return transformedOptions; return transformedOptions;
} }
private formatData<T>(data: T): T { private async formatData<T>(data: T): Promise<T> {
if (!data) { if (!data) {
return data; return data;
} }
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data.map((item) => this.formatData(item)) as T; return Promise.all(
data.map((item) => this.formatData(item)),
) as Promise<T>;
} }
const compositeFieldMetadataArgsCollection = const compositeFieldMetadataCollection =
this.getCompositeFieldMetadataArgs(); await this.getCompositeFieldMetadata();
const compositeFieldMetadataArgsMap = new Map( const compositeFieldMetadataMap = new Map(
compositeFieldMetadataArgsCollection.map((fieldMetadataArg) => [ compositeFieldMetadataCollection.map((fieldMetadata) => [
fieldMetadataArg.name, fieldMetadata.name,
fieldMetadataArg, fieldMetadata,
]), ]),
); );
const newData: object = {}; const newData: object = {};
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
const fieldMetadataArgs = compositeFieldMetadataArgsMap.get(key); const fieldMetadata = compositeFieldMetadataMap.get(key);
if (!fieldMetadataArgs) { if (!fieldMetadata) {
if (isPlainObject(value)) { if (isPlainObject(value)) {
newData[key] = this.formatData(value); newData[key] = await this.formatData(value);
} else { } else {
newData[key] = value; newData[key] = value;
} }
continue; continue;
} }
const compositeType = compositeTypeDefintions.get(fieldMetadataArgs.type); const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
if (!compositeType) { if (!compositeType) {
continue; continue;
@ -620,7 +692,7 @@ export class WorkspaceRepository<
for (const compositeProperty of compositeType.properties) { for (const compositeProperty of compositeType.properties) {
const compositeKey = computeCompositeColumnName( const compositeKey = computeCompositeColumnName(
fieldMetadataArgs.name, fieldMetadata.name,
compositeProperty, compositeProperty,
); );
const value = data?.[key]?.[compositeProperty.name]; const value = data?.[key]?.[compositeProperty.name];
@ -636,78 +708,90 @@ export class WorkspaceRepository<
return newData as T; return newData as T;
} }
private formatResult<T>( private async formatResult<T>(
data: T, data: T,
target = ObjectLiteralStorage.getObjectLiteral(this.target as any), objectMetadata?: ObjectMetadataEntity,
): T { ): Promise<T> {
objectMetadata ??= await this.getObjectMetadataFromTarget();
if (!data) { if (!data) {
return data; return data;
} }
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data.map((item) => this.formatResult(item, target)) as T; // If the data is an array, map each item in the array, format result is a promise
return Promise.all(
data.map((item) => this.formatResult(item, objectMetadata)),
) as Promise<T>;
} }
if (!isPlainObject(data)) { if (!isPlainObject(data)) {
return data; return data;
} }
if (!target) { if (!objectMetadata) {
throw new Error('Object literal is missing'); throw new Error('Object metadata is missing');
} }
const fieldMetadataArgsCollection = const compositeFieldMetadataCollection =
metadataArgsStorage.filterFields(target); await this.getCompositeFieldMetadata(objectMetadata);
const relationMetadataArgsCollection = const compositeFieldMetadataMap = new Map(
metadataArgsStorage.filterRelations(target); compositeFieldMetadataCollection.flatMap((fieldMetadata) => {
const compositeFieldMetadataArgsCollection = const compositeType = compositeTypeDefintions.get(fieldMetadata.type);
fieldMetadataArgsCollection.filter((fieldMetadataArg) =>
isCompositeFieldMetadataType(fieldMetadataArg.type),
);
const compositeFieldMetadataArgsMap = new Map(
compositeFieldMetadataArgsCollection.flatMap((fieldMetadataArg) => {
const compositeType = compositeTypeDefintions.get(
fieldMetadataArg.type,
);
if (!compositeType) return []; if (!compositeType) return [];
// Map each composite property to a [key, value] pair // Map each composite property to a [key, value] pair
return compositeType.properties.map((compositeProperty) => [ return compositeType.properties.map((compositeProperty) => [
computeCompositeColumnName(fieldMetadataArg.name, compositeProperty), computeCompositeColumnName(fieldMetadata.name, compositeProperty),
{ {
parentField: fieldMetadataArg.name, parentField: fieldMetadata.name,
...compositeProperty, ...compositeProperty,
}, },
]); ]);
}), }),
); );
const relationMetadataArgsMap = new Map( const relationMetadataMap = new Map(
relationMetadataArgsCollection.map((relationMetadataArgs) => [ objectMetadata.fields
relationMetadataArgs.name, .filter(({ type }) => isRelationFieldMetadataType(type))
relationMetadataArgs, .map((fieldMetadata) => [
]), fieldMetadata.name,
fieldMetadata.fromRelationMetadata ??
fieldMetadata.toRelationMetadata,
]),
); );
const newData: object = {}; const newData: object = {};
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
const compositePropertyArgs = compositeFieldMetadataArgsMap.get(key); const compositePropertyArgs = compositeFieldMetadataMap.get(key);
const relationMetadataArgs = relationMetadataArgsMap.get(key); const relationMetadata = relationMetadataMap.get(key);
if (!compositePropertyArgs && !relationMetadataArgs) { if (!compositePropertyArgs && !relationMetadata) {
if (isPlainObject(value)) { if (isPlainObject(value)) {
newData[key] = this.formatResult(value); newData[key] = await this.formatResult(value);
} else { } else {
newData[key] = value; newData[key] = value;
} }
continue; continue;
} }
if (relationMetadataArgs) { if (relationMetadata) {
newData[key] = this.formatResult( const inverseSideObjectName =
value, relationMetadata.toObjectMetadata.nameSingular;
relationMetadataArgs.inverseSideTarget() as any, const objectMetadata =
); await this.internalContext.workspaceCacheStorage.getObjectMetadata(
this.internalContext.workspaceId,
(objectMetadata) =>
objectMetadata.nameSingular === inverseSideObjectName,
);
if (!objectMetadata) {
throw new Error(
`Object metadata for object metadata "${inverseSideObjectName}" is missing`,
);
}
newData[key] = await this.formatResult(value, objectMetadata);
continue; continue;
} }

View File

@ -0,0 +1,47 @@
type CacheKey = `${string}-${string}`;
type AsyncFactoryCallback<T> = () => Promise<T | null>;
export class CacheManager<T> {
private cache = new Map<CacheKey, T>();
async execute(
cacheKey: CacheKey,
factory: AsyncFactoryCallback<T>,
onDelete?: (value: T) => Promise<void> | void,
): Promise<T | null> {
const [workspaceId] = cacheKey.split('-');
// If the cacheKey exists, return the cached value
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
// Remove old entries with the same workspaceId
for (const key of this.cache.keys()) {
if (key.startsWith(`${workspaceId}-`)) {
await onDelete?.(this.cache.get(key)!);
this.cache.delete(key);
}
}
// Create a new value using the factory callback
const value = await factory();
if (!value) {
return null;
}
this.cache.set(cacheKey, value);
return value;
}
async clear(onDelete?: (value: T) => Promise<void> | void): Promise<void> {
for (const value of this.cache.values()) {
await onDelete?.(value);
this.cache.delete(value as any);
}
this.cache.clear();
}
}

View File

@ -1,23 +0,0 @@
import { DataSource } from 'typeorm';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
export class DataSourceStorage {
private static readonly dataSources: Map<string, WorkspaceDataSource> =
new Map();
public static getDataSource(key: string): WorkspaceDataSource | undefined {
return this.dataSources.get(key);
}
public static setDataSource(
key: string,
dataSource: WorkspaceDataSource,
): void {
this.dataSources.set(key, dataSource);
}
public static getAllDataSources(): DataSource[] {
return Array.from(this.dataSources.values());
}
}

View File

@ -1,30 +0,0 @@
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

@ -0,0 +1,49 @@
import { EntitySchema } from 'typeorm';
export class WorkspaceEntitiesStorage {
private static workspaceEntities = new Map<
string,
Map<string, EntitySchema>
>();
static getEntitySchema(
workspaceId: string,
objectMetadataName: string,
): EntitySchema | undefined {
const workspace = this.workspaceEntities.get(workspaceId);
return workspace?.get(objectMetadataName);
}
static setEntitySchema(
workspaceId: string,
objectMetadataName: string,
schema: EntitySchema,
): void {
if (!this.workspaceEntities.has(workspaceId)) {
this.workspaceEntities.set(workspaceId, new Map<string, EntitySchema>());
}
const workspace = this.workspaceEntities.get(workspaceId);
workspace?.set(objectMetadataName, schema);
}
static getObjectMetadataName(
workspaceId: string,
target: EntitySchema,
): string | undefined {
const workspace = this.workspaceEntities.get(workspaceId);
return Array.from(workspace?.entries() || []).find(
([, schema]) => schema.options.name === target.options.name,
)?.[0];
}
static getEntities(workspaceId: string): EntitySchema[] {
return Array.from(this.workspaceEntities.get(workspaceId)?.values() || []);
}
static clearWorkspace(workspaceId: string): void {
this.workspaceEntities.delete(workspaceId);
}
}

View File

@ -5,36 +5,46 @@ import {
Module, Module,
OnApplicationShutdown, OnApplicationShutdown,
Provider, Provider,
Type,
} from '@nestjs/common'; } from '@nestjs/common';
import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';
import { importClassesFromDirectories } from 'typeorm/util/DirectoryExportedClassesLoader'; import { Repository } from 'typeorm';
import { Logger as TypeORMLogger } from 'typeorm/logger/Logger';
import { import {
TwentyORMModuleAsyncOptions, TwentyORMModuleAsyncOptions,
TwentyORMOptions, TwentyORMOptions,
} from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface'; } from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { entitySchemaFactories } from 'src/engine/twenty-orm/factories'; import { entitySchemaFactories } from 'src/engine/twenty-orm/factories';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage';
import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants'; import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { DataSourceStorage } from 'src/engine/twenty-orm/storage/data-source.storage';
import { ScopedWorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-datasource.factory';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { splitClassesAndStrings } from 'src/engine/twenty-orm/utils/split-classes-and-strings.util';
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
import { import {
ConfigurableModuleClass, ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN, MODULE_OPTIONS_TOKEN,
} from 'src/engine/twenty-orm/twenty-orm.module-definition'; } from 'src/engine/twenty-orm/twenty-orm.module-definition';
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
export const workspaceDataSourceCacheInstance =
new CacheManager<WorkspaceDataSource>();
@Global() @Global()
@Module({ @Module({
imports: [DataSourceModule], imports: [
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
DataSourceModule,
WorkspaceCacheVersionModule,
WorkspaceCacheStorageModule,
],
providers: [ providers: [
...entitySchemaFactories, ...entitySchemaFactories,
TwentyORMManager, TwentyORMManager,
@ -55,27 +65,18 @@ export class TwentyORMCoreModule
static register(options: TwentyORMOptions): DynamicModule { static register(options: TwentyORMOptions): DynamicModule {
const dynamicModule = super.register(options); const dynamicModule = super.register(options);
// TODO: Avoid code duplication here
const providers: Provider[] = [ const providers: Provider[] = [
{ {
provide: TWENTY_ORM_WORKSPACE_DATASOURCE, provide: TWENTY_ORM_WORKSPACE_DATASOURCE,
useFactory: async ( useFactory: this.createWorkspaceDataSource,
entitySchemaFactory: EntitySchemaFactory, inject: [
scopedWorkspaceDatasourceFactory: ScopedWorkspaceDatasourceFactory, WorkspaceCacheStorageService,
) => { getRepositoryToken(ObjectMetadataEntity, 'metadata'),
const workspaceEntities = await this.loadEntities( EntitySchemaFactory,
options.workspaceEntities, ScopedWorkspaceContextFactory,
); WorkspaceDatasourceFactory,
],
const entities = workspaceEntities.map((entityClass) =>
entitySchemaFactory.create(entityClass),
);
const scopedWorkspaceDataSource =
await scopedWorkspaceDatasourceFactory.create(entities);
return scopedWorkspaceDataSource;
},
inject: [EntitySchemaFactory, ScopedWorkspaceDatasourceFactory],
}, },
]; ];
@ -96,27 +97,13 @@ export class TwentyORMCoreModule
const providers: Provider[] = [ const providers: Provider[] = [
{ {
provide: TWENTY_ORM_WORKSPACE_DATASOURCE, provide: TWENTY_ORM_WORKSPACE_DATASOURCE,
useFactory: async ( useFactory: this.createWorkspaceDataSource,
entitySchemaFactory: EntitySchemaFactory,
scopedWorkspaceDatasourceFactory: ScopedWorkspaceDatasourceFactory,
options: TwentyORMOptions,
) => {
const workspaceEntities = await this.loadEntities(
options.workspaceEntities,
);
const entities = workspaceEntities.map((entityClass) =>
entitySchemaFactory.create(entityClass),
);
const scopedWorkspaceDataSource =
await scopedWorkspaceDatasourceFactory.create(entities);
return scopedWorkspaceDataSource;
},
inject: [ inject: [
WorkspaceCacheStorageService,
getRepositoryToken(ObjectMetadataEntity, 'metadata'),
EntitySchemaFactory, EntitySchemaFactory,
ScopedWorkspaceDatasourceFactory, ScopedWorkspaceContextFactory,
WorkspaceDatasourceFactory,
MODULE_OPTIONS_TOKEN, MODULE_OPTIONS_TOKEN,
], ],
}, },
@ -132,45 +119,70 @@ export class TwentyORMCoreModule
}; };
} }
static async createWorkspaceDataSource(
workspaceCacheStorageService: WorkspaceCacheStorageService,
objectMetadataRepository: Repository<ObjectMetadataEntity>,
entitySchemaFactory: EntitySchemaFactory,
scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
workspaceDataSourceFactory: WorkspaceDatasourceFactory,
_options?: TwentyORMOptions,
) {
const { workspaceId, cacheVersion } =
scopedWorkspaceContextFactory.create();
if (!workspaceId) {
return null;
}
return workspaceDataSourceCacheInstance.execute(
`${workspaceId}-${cacheVersion}`,
async () => {
let objectMetadataCollection =
await workspaceCacheStorageService.getObjectMetadataCollection(
workspaceId,
);
if (!objectMetadataCollection) {
objectMetadataCollection = await objectMetadataRepository.find({
where: { workspaceId },
relations: [
'fields.object',
'fields',
'fields.fromRelationMetadata',
'fields.toRelationMetadata',
'fields.fromRelationMetadata.toObjectMetadata',
],
});
await workspaceCacheStorageService.setObjectMetadataCollection(
workspaceId,
objectMetadataCollection,
);
}
const entities = await Promise.all(
objectMetadataCollection.map((objectMetadata) =>
entitySchemaFactory.create(workspaceId, objectMetadata),
),
);
const workspaceDataSource = await workspaceDataSourceFactory.create(
entities,
workspaceId,
);
return workspaceDataSource;
},
(dataSource) => dataSource.destroy(),
);
}
/** /**
* Destroys all data sources on application shutdown * Destroys all data sources on application shutdown
*/ */
async onApplicationShutdown() { async onApplicationShutdown() {
const dataSources = DataSourceStorage.getAllDataSources(); workspaceDataSourceCacheInstance.clear((dataSource) =>
dataSource.destroy(),
for (const dataSource of dataSources) {
try {
if (dataSource && dataSource.isInitialized) {
await dataSource.destroy();
}
} catch (e) {
this.logger.error(e?.message);
}
}
}
private static async loadEntities(
workspaceEntities: (Type<BaseWorkspaceEntity> | string)[],
): Promise<Type<BaseWorkspaceEntity>[]> {
const [entityClassesOrSchemas, entityDirectories] = splitClassesAndStrings(
workspaceEntities || [],
);
const importedEntities = await importClassesFromDirectories(
// Only `log` function is used under importClassesFromDirectories function
this.logger as unknown as TypeORMLogger,
entityDirectories,
);
const entities = [
...entityClassesOrSchemas,
...(importedEntities as Type<BaseWorkspaceEntity>[]),
];
return entities.filter(
(entity) =>
// Filter out CustomWorkspaceEntity as it's a partial entity handled separately
entity.name !== CustomWorkspaceEntity.name &&
// Filter out BaseWorkspaceEntity as it's a base entity and should not be included in the workspace entities
entity.name !== BaseWorkspaceEntity.name,
); );
} }
} }

View File

@ -1,113 +1,152 @@
import { Injectable, Optional, Type } from '@nestjs/common'; import { Injectable, Optional, Type } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ObjectLiteral } from 'typeorm'; import { ObjectLiteral, Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { workspaceDataSourceCacheInstance } from 'src/engine/twenty-orm/twenty-orm-core.module';
import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/activity.workspace-entity'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity'; import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
import { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-objects/behavioral-event.workspace-entity';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable() @Injectable()
export class TwentyORMManager { export class TwentyORMManager {
constructor( constructor(
@Optional() @Optional()
@InjectWorkspaceDatasource()
private readonly workspaceDataSource: WorkspaceDataSource | null, private readonly workspaceDataSource: WorkspaceDataSource | null,
private readonly entitySchemaFactory: EntitySchemaFactory, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory, private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory,
private readonly entitySchemaFactory: EntitySchemaFactory,
) {} ) {}
getRepository<T extends ObjectLiteral>( async getRepository<T extends ObjectLiteral>(
objectMetadataName: string,
): Promise<WorkspaceRepository<T>>;
async getRepository<T extends ObjectLiteral>(
entityClass: Type<T>, entityClass: Type<T>,
): WorkspaceRepository<T> { ): Promise<WorkspaceRepository<T>>;
const entitySchema = this.entitySchemaFactory.create(entityClass);
async getRepository<T extends ObjectLiteral>(
entityClassOrobjectMetadataName: Type<T> | string,
): Promise<WorkspaceRepository<T>> {
let objectMetadataName: string;
if (typeof entityClassOrobjectMetadataName === 'string') {
objectMetadataName = entityClassOrobjectMetadataName;
} else {
objectMetadataName = convertClassNameToObjectMetadataName(
entityClassOrobjectMetadataName.name,
);
}
if (!this.workspaceDataSource) { if (!this.workspaceDataSource) {
throw new Error('Workspace data source not found'); throw new Error('Workspace data source not found');
} }
const entitySchema = await this.entitySchemaFactory.create(
this.workspaceDataSource.internalContext.workspaceId,
objectMetadataName,
);
if (!entitySchema) {
throw new Error('Entity schema not found');
}
return this.workspaceDataSource.getRepository<T>(entitySchema); return this.workspaceDataSource.getRepository<T>(entitySchema);
} }
async getRepositoryForWorkspace<T extends ObjectLiteral>( async getRepositoryForWorkspace<T extends ObjectLiteral>(
workspaceId: string, workspaceId: string,
entityClass: Type<T>, entityClass: Type<T>,
): Promise<WorkspaceRepository<T>> { ): Promise<WorkspaceRepository<T>>;
// TODO: This is a temporary solution to get all workspace entities
const workspaceEntities = [
ActivityTargetWorkspaceEntity,
ActivityWorkspaceEntity,
ApiKeyWorkspaceEntity,
AttachmentWorkspaceEntity,
BlocklistWorkspaceEntity,
BehavioralEventWorkspaceEntity,
CalendarChannelEventAssociationWorkspaceEntity,
CalendarChannelWorkspaceEntity,
CalendarEventParticipantWorkspaceEntity,
CalendarEventWorkspaceEntity,
CommentWorkspaceEntity,
CompanyWorkspaceEntity,
ConnectedAccountWorkspaceEntity,
FavoriteWorkspaceEntity,
AuditLogWorkspaceEntity,
MessageChannelMessageAssociationWorkspaceEntity,
MessageChannelWorkspaceEntity,
MessageParticipantWorkspaceEntity,
MessageThreadWorkspaceEntity,
MessageWorkspaceEntity,
OpportunityWorkspaceEntity,
PersonWorkspaceEntity,
TimelineActivityWorkspaceEntity,
ViewFieldWorkspaceEntity,
ViewFilterWorkspaceEntity,
ViewSortWorkspaceEntity,
ViewWorkspaceEntity,
WebhookWorkspaceEntity,
WorkspaceMemberWorkspaceEntity,
];
const entities = workspaceEntities.map((workspaceEntity) => async getRepositoryForWorkspace(
this.entitySchemaFactory.create(workspaceEntity as any), workspaceId: string,
objectMetadataName: string,
): Promise<WorkspaceRepository<CustomWorkspaceEntity>>;
async getRepositoryForWorkspace<T extends ObjectLiteral>(
workspaceId: string,
entityClassOrobjectMetadataName: Type<T> | string,
): Promise<
WorkspaceRepository<T> | WorkspaceRepository<CustomWorkspaceEntity>
> {
const cacheVersion =
await this.workspaceCacheVersionService.getVersion(workspaceId);
let objectMetadataName: string;
if (typeof entityClassOrobjectMetadataName === 'string') {
objectMetadataName = entityClassOrobjectMetadataName;
} else {
objectMetadataName = convertClassNameToObjectMetadataName(
entityClassOrobjectMetadataName.name,
);
}
const workspaceDataSource = await workspaceDataSourceCacheInstance.execute(
`${workspaceId}-${cacheVersion}`,
async () => {
let objectMetadataCollection =
await this.workspaceCacheStorageService.getObjectMetadataCollection(
workspaceId,
);
if (!objectMetadataCollection) {
objectMetadataCollection = await this.objectMetadataRepository.find({
where: { workspaceId },
relations: [
'fields.object',
'fields',
'fields.fromRelationMetadata',
'fields.toRelationMetadata',
'fields.fromRelationMetadata.toObjectMetadata',
],
});
await this.workspaceCacheStorageService.setObjectMetadataCollection(
workspaceId,
objectMetadataCollection,
);
}
const entities = await Promise.all(
objectMetadataCollection.map((objectMetadata) =>
this.entitySchemaFactory.create(workspaceId, objectMetadata),
),
);
const workspaceDataSource =
await this.workspaceDataSourceFactory.create(entities, workspaceId);
return workspaceDataSource;
},
(dataSource) => dataSource.destroy(),
); );
const workspaceDataSource = await this.workspaceDataSourceFactory.create( const entitySchema = await this.entitySchemaFactory.create(
entities,
workspaceId, workspaceId,
objectMetadataName,
); );
if (!workspaceDataSource) { if (!workspaceDataSource) {
throw new Error('Workspace data source not found'); throw new Error('Workspace data source not found');
} }
const entitySchema = this.entitySchemaFactory.create(entityClass); if (!entitySchema) {
throw new Error('Entity schema not found');
}
return workspaceDataSource.getRepository<T>(entitySchema); return workspaceDataSource.getRepository<T>(entitySchema);
} }

View File

@ -5,6 +5,7 @@ import { getWorkspaceRepositoryToken } from 'src/engine/twenty-orm/utils/get-wor
import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants'; import { TWENTY_ORM_WORKSPACE_DATASOURCE } from 'src/engine/twenty-orm/twenty-orm.constants';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
/** /**
* Create providers for the given entities. * Create providers for the given entities.
@ -14,17 +15,28 @@ export function createTwentyORMProviders(
): Provider[] { ): Provider[] {
return (objects || []).map((object) => ({ return (objects || []).map((object) => ({
provide: getWorkspaceRepositoryToken(object), provide: getWorkspaceRepositoryToken(object),
useFactory: ( useFactory: async (
dataSource: WorkspaceDataSource | null, dataSource: WorkspaceDataSource | null,
entitySchemaFactory: EntitySchemaFactory, entitySchemaFactory: EntitySchemaFactory,
) => { ) => {
const entity = entitySchemaFactory.create(object as Type); const objectMetadataName = convertClassNameToObjectMetadataName(
(object as Type).name,
);
if (!dataSource) { if (!dataSource) {
return null; return null;
} }
return dataSource.getRepository(entity); const entitySchema = await entitySchemaFactory.create(
dataSource.internalContext.workspaceId,
objectMetadataName,
);
if (!entitySchema) {
throw new Error('Entity schema not found');
}
return dataSource.getRepository(entitySchema);
}, },
inject: [TWENTY_ORM_WORKSPACE_DATASOURCE, EntitySchemaFactory], inject: [TWENTY_ORM_WORKSPACE_DATASOURCE, EntitySchemaFactory],
})); }));

View File

@ -0,0 +1,33 @@
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
RelationMetadataEntity,
RelationMetadataType,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import {
deduceRelationDirection,
RelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util';
export const computeRelationType = (
fieldMetadata: FieldMetadataEntity,
relationMetadata: RelationMetadataEntity,
) => {
const relationDirection = deduceRelationDirection(
fieldMetadata,
relationMetadata,
);
switch (relationMetadata.relationType) {
case RelationMetadataType.ONE_TO_MANY: {
return relationDirection === RelationDirection.FROM
? 'one-to-many'
: 'many-to-one';
}
case RelationMetadataType.ONE_TO_ONE:
return 'one-to-one';
case RelationMetadataType.MANY_TO_MANY:
return 'many-to-many';
default:
throw new Error('Invalid relation type');
}
};

View File

@ -0,0 +1,59 @@
import { RelationType } from 'typeorm/metadata/types/RelationTypes';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
interface RelationDetails {
relationType: RelationType;
target: string;
inverseSide: string;
joinColumn: { name: string } | undefined;
}
export async function determineRelationDetails(
workspaceId: string,
fieldMetadata: FieldMetadataEntity,
relationMetadata: RelationMetadataEntity,
workspaceCacheStorageService: WorkspaceCacheStorageService,
): Promise<RelationDetails> {
const relationType = computeRelationType(fieldMetadata, relationMetadata);
let fromObjectMetadata: ObjectMetadataEntity | undefined =
fieldMetadata.object;
let toObjectMetadata: ObjectMetadataEntity | undefined =
relationMetadata.toObjectMetadata;
// RelationMetadata always store the relation from the perspective of the `from` object, MANY_TO_ONE relations are not stored yet
if (relationType === 'many-to-one') {
fromObjectMetadata = fieldMetadata.object;
toObjectMetadata = await workspaceCacheStorageService.getObjectMetadata(
workspaceId,
(objectMetadata) =>
objectMetadata.id === relationMetadata.toObjectMetadataId,
);
}
if (!fromObjectMetadata || !toObjectMetadata) {
throw new Error('Object metadata not found');
}
// TODO: Support many to many relations
if (relationType === 'many-to-many') {
throw new Error('Many to many relations are not supported yet');
}
return {
relationType,
target: toObjectMetadata.nameSingular,
inverseSide: fromObjectMetadata.nameSingular,
joinColumn:
// TODO: This will work for now but we need to handle this better in the future for custom names on the join column
relationType === 'many-to-one' ||
(relationType === 'one-to-one' &&
relationMetadata.toObjectMetadataId === fieldMetadata.objectMetadataId)
? { name: `${fieldMetadata.name}` + 'Id' }
: undefined,
};
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Module({
imports: [WorkspaceCacheVersionModule],
providers: [WorkspaceCacheStorageService],
exports: [WorkspaceCacheStorageService],
})
export class WorkspaceCacheStorageModule {}

View File

@ -7,11 +7,10 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
@Injectable() @Injectable()
export class WorkspaceSchemaStorageService { export class WorkspaceCacheStorageService {
constructor( constructor(
@InjectCacheStorage(CacheStorageNamespace.WorkspaceSchema) @InjectCacheStorage(CacheStorageNamespace.WorkspaceSchema)
private readonly workspaceSchemaCache: CacheStorageService, private readonly workspaceSchemaCache: CacheStorageService,
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
) {} ) {}
@ -58,6 +57,21 @@ export class WorkspaceSchemaStorageService {
); );
} }
async getObjectMetadata(
workspaceId: string,
predicate: (objectMetadata: ObjectMetadataEntity) => boolean,
): Promise<ObjectMetadataEntity | undefined> {
const objectMetadataCollection = await this.workspaceSchemaCache.get<
ObjectMetadataEntity[]
>(`objectMetadataCollection:${workspaceId}`);
if (!objectMetadataCollection) {
return;
}
return objectMetadataCollection.find(predicate);
}
setTypeDefs(workspaceId: string, typeDefs: string): Promise<void> { setTypeDefs(workspaceId: string, typeDefs: string): Promise<void> {
return this.workspaceSchemaCache.set<string>( return this.workspaceSchemaCache.set<string>(
`typeDefs:${workspaceId}`, `typeDefs:${workspaceId}`,

View File

@ -7,9 +7,7 @@ import { MessageQueueModule } from 'src/engine/integrations/message-queue/messag
@Module({ @Module({
imports: [ imports: [
TwentyORMModule.register({ TwentyORMModule.register({}),
workspaceEntities: ['dist/src/**/*.workspace-entity{.ts,.js}'],
}),
IntegrationsModule, IntegrationsModule,
MessageQueueModule.registerExplorer(), MessageQueueModule.registerExplorer(),
JobsModule, JobsModule,

View File

@ -12,6 +12,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"target": "es2017", "target": "es2017",
"sourceMap": true, "sourceMap": true,
"inlineSources": true,
"outDir": "./dist", "outDir": "./dist",
"incremental": true, "incremental": true,
"skipLibCheck": true, "skipLibCheck": true,