diff --git a/server/package.json b/server/package.json index afb3e0f3c..5506137b5 100644 --- a/server/package.json +++ b/server/package.json @@ -41,6 +41,7 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.2", "@nestjs/core": "^9.0.0", + "@nestjs/event-emitter": "^2.0.3", "@nestjs/graphql": "^12.0.8", "@nestjs/jwt": "^10.0.3", "@nestjs/passport": "^9.0.3", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 960118275..fc5146c86 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ConfigModule } from '@nestjs/config'; import { APP_FILTER, ContextIdFactory, ModuleRef } from '@nestjs/core'; +import { EventEmitterModule } from '@nestjs/event-emitter'; import { YogaDriver, YogaDriverConfig } from '@graphql-yoga/nestjs'; import GraphQLJSON from 'graphql-type-json'; @@ -9,6 +10,8 @@ import { GraphQLError, GraphQLSchema } from 'graphql'; import { ExtractJwt } from 'passport-jwt'; import { TokenExpiredError, JsonWebTokenError, verify } from 'jsonwebtoken'; +import { WorkspaceFactory } from 'src/workspace/workspace.factory'; + import { AppService } from './app.service'; import { CoreModule } from './core/core.module'; @@ -20,7 +23,6 @@ import { JwtAuthStrategy, JwtPayload, } from './core/auth/strategies/jwt.auth.strategy'; -import { WorkspaceFactory } from './workspace/workspace.factory'; import { ExceptionFilter } from './filters/exception.filter'; @Module({ @@ -102,6 +104,7 @@ import { ExceptionFilter } from './filters/exception.filter'; resolvers: { JSON: GraphQLJSON }, plugins: [], }), + EventEmitterModule.forRoot(), HealthModule, IntegrationsModule, CoreModule, diff --git a/server/src/database/typeorm/typeorm.service.ts b/server/src/database/typeorm/typeorm.service.ts index 3fb17f016..b948d5608 100644 --- a/server/src/database/typeorm/typeorm.service.ts +++ b/server/src/database/typeorm/typeorm.service.ts @@ -12,6 +12,7 @@ import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; export class TypeORMService implements OnModuleInit, OnModuleDestroy { private mainDataSource: DataSource; private dataSources: Map = new Map(); + private isDatasourceInitializing: Map = new Map(); constructor(private readonly environmentService: EnvironmentService) { this.mainDataSource = new DataSource({ @@ -35,23 +36,46 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy { public async connectToDataSource( dataSource: DataSourceEntity, ): Promise { + // Wait for a bit before trying again if another initialization is in progress + while (this.isDatasourceInitializing.get(dataSource.id)) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + if (this.dataSources.has(dataSource.id)) { return this.dataSources.get(dataSource.id); } + this.isDatasourceInitializing.set(dataSource.id, true); + + try { + const dataSourceInstance = await this.createAndInitializeDataSource( + dataSource, + ); + + this.dataSources.set(dataSource.id, dataSourceInstance); + + return dataSourceInstance; + } finally { + this.isDatasourceInitializing.delete(dataSource.id); + } + } + + private async createAndInitializeDataSource( + dataSource: DataSourceEntity, + ): Promise { const schema = dataSource.schema; const workspaceDataSource = new DataSource({ url: dataSource.url ?? this.environmentService.getPGDatabaseUrl(), type: 'postgres', - logging: ['query'], + logging: this.environmentService.isDebugMode() + ? ['query', 'error'] + : ['error'], schema, }); await workspaceDataSource.initialize(); - this.dataSources.set(dataSource.id, workspaceDataSource); - return workspaceDataSource; } diff --git a/server/src/integrations/environment/interfaces/memory-storage.interface.ts b/server/src/integrations/environment/interfaces/memory-storage.interface.ts new file mode 100644 index 000000000..bf906df15 --- /dev/null +++ b/server/src/integrations/environment/interfaces/memory-storage.interface.ts @@ -0,0 +1,3 @@ +export enum MemoryStorageType { + Local = 'local', +} diff --git a/server/src/integrations/memory-storage/decorators/inject-memory-storage.decorator.ts b/server/src/integrations/memory-storage/decorators/inject-memory-storage.decorator.ts new file mode 100644 index 000000000..c4d807087 --- /dev/null +++ b/server/src/integrations/memory-storage/decorators/inject-memory-storage.decorator.ts @@ -0,0 +1,9 @@ +import { Inject } from '@nestjs/common'; + +import { createMemoryStorageInjectionToken } from 'src/integrations/memory-storage/memory-storage.util'; + +export const InjectMemoryStorage = (identifier: string) => { + const injectionToken = createMemoryStorageInjectionToken(identifier); + + return Inject(injectionToken); +}; diff --git a/server/src/integrations/memory-storage/drivers/interfaces/memory-storage-driver.interface.ts b/server/src/integrations/memory-storage/drivers/interfaces/memory-storage-driver.interface.ts new file mode 100644 index 000000000..a2f52e8ad --- /dev/null +++ b/server/src/integrations/memory-storage/drivers/interfaces/memory-storage-driver.interface.ts @@ -0,0 +1,5 @@ +export interface MemoryStorageDriver { + read(params: { key: string }): Promise; + write(params: { key: string; data: T }): Promise; + delete(params: { key: string }): Promise; +} diff --git a/server/src/integrations/memory-storage/drivers/local.driver.ts b/server/src/integrations/memory-storage/drivers/local.driver.ts new file mode 100644 index 000000000..f8d1e1601 --- /dev/null +++ b/server/src/integrations/memory-storage/drivers/local.driver.ts @@ -0,0 +1,57 @@ +import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface'; + +import { MemoryStorageDriver } from './interfaces/memory-storage-driver.interface'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface LocalMemoryDriverOptions {} + +export class LocalMemoryDriver implements MemoryStorageDriver { + private identifier: string; + private options: LocalMemoryDriverOptions; + private serializer: MemoryStorageSerializer; + private storage: Map = new Map(); + + constructor( + identifier: string, + options: LocalMemoryDriverOptions, + serializer: MemoryStorageSerializer, + ) { + this.identifier = identifier; + this.options = options; + this.serializer = serializer; + } + + async write(params: { key: string; data: T }): Promise { + const compositeKey = this.generateCompositeKey(params.key); + const serializedData = this.serializer.serialize(params.data); + + this.storage.set(compositeKey, serializedData); + } + + async read(params: { key: string }): Promise { + const compositeKey = this.generateCompositeKey(params.key); + + if (!this.storage.has(compositeKey)) { + return null; + } + + const data = this.storage.get(compositeKey)!; + const deserializeData = this.serializer.deserialize(data); + + return deserializeData; + } + + async delete(params: { key: string }): Promise { + const compositeKey = this.generateCompositeKey(params.key); + + if (!this.storage.has(compositeKey)) { + return; + } + + this.storage.delete(compositeKey); + } + + private generateCompositeKey(key: string): string { + return `${this.identifier}:${key}`; + } +} diff --git a/server/src/integrations/memory-storage/interfaces/index.ts b/server/src/integrations/memory-storage/interfaces/index.ts new file mode 100644 index 000000000..05ba64022 --- /dev/null +++ b/server/src/integrations/memory-storage/interfaces/index.ts @@ -0,0 +1 @@ +export * from './memory-storage.interface'; diff --git a/server/src/integrations/memory-storage/interfaces/memory-storage.interface.ts b/server/src/integrations/memory-storage/interfaces/memory-storage.interface.ts new file mode 100644 index 000000000..a8e4553b0 --- /dev/null +++ b/server/src/integrations/memory-storage/interfaces/memory-storage.interface.ts @@ -0,0 +1,29 @@ +import { FactoryProvider, ModuleMetadata } from '@nestjs/common'; + +import { MemoryStorageType } from 'src/integrations/environment/interfaces/memory-storage.interface'; +import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface'; + +import { LocalMemoryDriverOptions } from 'src/integrations/memory-storage/drivers/local.driver'; + +export interface LocalMemoryDriverFactoryOptions { + type: MemoryStorageType.Local; + options: LocalMemoryDriverOptions; +} + +interface MemoryStorageModuleBaseOptions { + identifier: string; + serializer?: MemoryStorageSerializer; +} + +export type MemoryStorageModuleOptions = MemoryStorageModuleBaseOptions & + LocalMemoryDriverFactoryOptions; + +export type MemoryStorageModuleAsyncOptions = { + identifier: string; + useFactory: ( + ...args: any[] + ) => + | Omit + | Promise>; +} & Pick & + Pick; diff --git a/server/src/integrations/memory-storage/memory-storage.constants.ts b/server/src/integrations/memory-storage/memory-storage.constants.ts new file mode 100644 index 000000000..68881644b --- /dev/null +++ b/server/src/integrations/memory-storage/memory-storage.constants.ts @@ -0,0 +1 @@ +export const MEMORY_STORAGE_SERVICE = 'MEMORY_STORAGE_SERVICE'; diff --git a/server/src/integrations/memory-storage/memory-storage.module.ts b/server/src/integrations/memory-storage/memory-storage.module.ts new file mode 100644 index 000000000..bd9108d73 --- /dev/null +++ b/server/src/integrations/memory-storage/memory-storage.module.ts @@ -0,0 +1,73 @@ +import { DynamicModule, Global } from '@nestjs/common'; + +import { MemoryStorageType } from 'src/integrations/environment/interfaces/memory-storage.interface'; + +import { MemoryStorageDefaultSerializer } from 'src/integrations/memory-storage/serializers/default.serializer'; +import { createMemoryStorageInjectionToken } from 'src/integrations/memory-storage/memory-storage.util'; + +import { + MemoryStorageModuleAsyncOptions, + MemoryStorageModuleOptions, +} from './interfaces'; + +import { LocalMemoryDriver } from './drivers/local.driver'; + +@Global() +export class MemoryStorageModule { + static forRoot(options: MemoryStorageModuleOptions): DynamicModule { + // Dynamic injection token to allow multiple instances of the same driver + const injectionToken = createMemoryStorageInjectionToken( + options.identifier, + ); + const provider = { + provide: injectionToken, + useValue: this.createStorageDriver(options), + }; + + return { + module: MemoryStorageModule, + providers: [provider], + exports: [provider], + }; + } + + static forRootAsync(options: MemoryStorageModuleAsyncOptions): DynamicModule { + // Dynamic injection token to allow multiple instances of the same driver + const injectionToken = createMemoryStorageInjectionToken( + options.identifier, + ); + const provider = { + provide: injectionToken, + useFactory: async (...args: any[]) => { + const config = await options.useFactory(...args); + + return this.createStorageDriver({ + identifier: options.identifier, + ...config, + }); + }, + inject: options.inject || [], + }; + + return { + module: MemoryStorageModule, + imports: options.imports || [], + providers: [provider], + exports: [provider], + }; + } + + private static createStorageDriver(options: MemoryStorageModuleOptions) { + switch (options.type) { + case MemoryStorageType.Local: + return new LocalMemoryDriver( + options.identifier, + options.options, + options.serializer ?? new MemoryStorageDefaultSerializer(), + ); + // Future case for Redis or other types + default: + throw new Error(`Unsupported storage type: ${options.type}`); + } + } +} diff --git a/server/src/integrations/memory-storage/memory-storage.service.spec.ts b/server/src/integrations/memory-storage/memory-storage.service.spec.ts new file mode 100644 index 000000000..b9e4ef90e --- /dev/null +++ b/server/src/integrations/memory-storage/memory-storage.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { MemoryStorageService } from './memory-storage.service'; + +describe('MemoryStorageService', () => { + let service: MemoryStorageService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MemoryStorageService], + }).compile(); + + service = module.get>(MemoryStorageService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/server/src/integrations/memory-storage/memory-storage.service.ts b/server/src/integrations/memory-storage/memory-storage.service.ts new file mode 100644 index 000000000..89fdf0bbd --- /dev/null +++ b/server/src/integrations/memory-storage/memory-storage.service.ts @@ -0,0 +1,21 @@ +import { MemoryStorageDriver } from 'src/integrations/memory-storage/drivers/interfaces/memory-storage-driver.interface'; + +export class MemoryStorageService implements MemoryStorageDriver { + private driver: MemoryStorageDriver; + + constructor(driver: MemoryStorageDriver) { + this.driver = driver; + } + + write(params: { key: string; data: T }): Promise { + return this.driver.write(params); + } + + read(params: { key: string }): Promise { + return this.driver.read(params); + } + + delete(params: { key: string }): Promise { + return this.driver.delete(params); + } +} diff --git a/server/src/integrations/memory-storage/memory-storage.util.ts b/server/src/integrations/memory-storage/memory-storage.util.ts new file mode 100644 index 000000000..6fea200de --- /dev/null +++ b/server/src/integrations/memory-storage/memory-storage.util.ts @@ -0,0 +1,5 @@ +import { MEMORY_STORAGE_SERVICE } from 'src/integrations/memory-storage/memory-storage.constants'; + +export const createMemoryStorageInjectionToken = (identifier: string) => { + return `${MEMORY_STORAGE_SERVICE}_${identifier}`; +}; diff --git a/server/src/integrations/memory-storage/serializers/default.serializer.ts b/server/src/integrations/memory-storage/serializers/default.serializer.ts new file mode 100644 index 000000000..729d065dd --- /dev/null +++ b/server/src/integrations/memory-storage/serializers/default.serializer.ts @@ -0,0 +1,16 @@ +import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface'; + +export class MemoryStorageDefaultSerializer + implements MemoryStorageSerializer +{ + serialize(item: T): string { + if (typeof item !== 'string') { + throw new Error('DefaultSerializer can only serialize strings'); + } + return item; + } + + deserialize(data: string): T { + return data as unknown as T; + } +} diff --git a/server/src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface.ts b/server/src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface.ts new file mode 100644 index 000000000..838de317d --- /dev/null +++ b/server/src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface.ts @@ -0,0 +1,4 @@ +export interface MemoryStorageSerializer { + serialize(item: T): string; + deserialize(data: string): T; +} diff --git a/server/src/integrations/memory-storage/serializers/json.serializer.ts b/server/src/integrations/memory-storage/serializers/json.serializer.ts new file mode 100644 index 000000000..ef0b3e436 --- /dev/null +++ b/server/src/integrations/memory-storage/serializers/json.serializer.ts @@ -0,0 +1,13 @@ +import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface'; + +export class MemoryStorageJsonSerializer + implements MemoryStorageSerializer +{ + serialize(item: T): string { + return JSON.stringify(item); + } + + deserialize(data: string): T { + return JSON.parse(data) as T; + } +} diff --git a/server/src/workspace/workspace-migration-runner/events/workspace-migration-applied.event.ts b/server/src/workspace/workspace-migration-runner/events/workspace-migration-applied.event.ts new file mode 100644 index 000000000..b529b3f12 --- /dev/null +++ b/server/src/workspace/workspace-migration-runner/events/workspace-migration-applied.event.ts @@ -0,0 +1,11 @@ +export class WorkspaceMigrationAppliedEvent { + private readonly _workspaceId: string; + + constructor(worskapceId: string) { + this._workspaceId = worskapceId; + } + + get workspaceId(): string { + return this._workspaceId; + } +} diff --git a/server/src/workspace/workspace-migration-runner/events/workspace-migration-events.ts b/server/src/workspace/workspace-migration-runner/events/workspace-migration-events.ts new file mode 100644 index 000000000..54bb916af --- /dev/null +++ b/server/src/workspace/workspace-migration-runner/events/workspace-migration-events.ts @@ -0,0 +1,3 @@ +export enum WorkspaceMigrationEvents { + MigrationApplied = '@workspace/migration-applied', +} diff --git a/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts b/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts index 0982f6aea..00f6a047f 100644 --- a/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { QueryRunner, @@ -17,6 +18,8 @@ import { WorkspaceMigrationColumnCreate, WorkspaceMigrationColumnRelation, } from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationEvents } from 'src/workspace/workspace-migration-runner/events/workspace-migration-events'; +import { WorkspaceMigrationAppliedEvent } from 'src/workspace/workspace-migration-runner/events/workspace-migration-applied.event'; import { customTableDefaultColumns } from './utils/custom-table-default-column.util'; @@ -25,6 +28,7 @@ export class WorkspaceMigrationRunnerService { constructor( private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceMigrationService: WorkspaceMigrationService, + private readonly eventEmitter: EventEmitter2, ) {} /** @@ -78,6 +82,12 @@ export class WorkspaceMigrationRunnerService { await queryRunner.release(); + // Emit event when migration is applied + this.eventEmitter.emit( + WorkspaceMigrationEvents.MigrationApplied, + new WorkspaceMigrationAppliedEvent(workspaceId), + ); + return flattenedPendingMigrations; } diff --git a/server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts b/server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts index 58cf40032..1961a7ca2 100644 --- a/server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts +++ b/server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts @@ -142,13 +142,9 @@ export class WorkspaceQueryRunnerService { )}; `); - const queryFormatted = query - .replace('neq:null', 'is:NOT_NULL') - .replace('eq:null', 'is:NULL'); - const results = await workspaceDataSource?.query(` SELECT graphql.resolve($$ - ${queryFormatted} + ${query} $$); `); diff --git a/server/src/workspace/workspace-schema-storage/workspace-schema-storage.module.ts b/server/src/workspace/workspace-schema-storage/workspace-schema-storage.module.ts new file mode 100644 index 000000000..9839c8760 --- /dev/null +++ b/server/src/workspace/workspace-schema-storage/workspace-schema-storage.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; + +import { MemoryStorageType } from 'src/integrations/environment/interfaces/memory-storage.interface'; + +import { MemoryStorageModule } from 'src/integrations/memory-storage/memory-storage.module'; +import { MemoryStorageJsonSerializer } from 'src/integrations/memory-storage/serializers/json.serializer'; +import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; +import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; +import { WorkspaceSchemaStorageService } from 'src/workspace/workspace-schema-storage/workspace-schema-storage.service'; + +@Module({ + imports: [ + ObjectMetadataModule, + MemoryStorageModule.forRoot({ + identifier: 'objectMetadataCollection', + type: MemoryStorageType.Local, + options: {}, + serializer: new MemoryStorageJsonSerializer(), + }), + MemoryStorageModule.forRoot({ + identifier: 'typeDefs', + type: MemoryStorageType.Local, + options: {}, + }), + ], + providers: [WorkspaceSchemaStorageService], + exports: [WorkspaceSchemaStorageService], +}) +export class WorkspaceSchemaStorageModule {} diff --git a/server/src/workspace/workspace-schema-storage/workspace-schema-storage.service.ts b/server/src/workspace/workspace-schema-storage/workspace-schema-storage.service.ts new file mode 100644 index 000000000..c72390293 --- /dev/null +++ b/server/src/workspace/workspace-schema-storage/workspace-schema-storage.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +import { InjectMemoryStorage } from 'src/integrations/memory-storage/decorators/inject-memory-storage.decorator'; +import { MemoryStorageService } from 'src/integrations/memory-storage/memory-storage.service'; +import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; +import { WorkspaceMigrationAppliedEvent } from 'src/workspace/workspace-migration-runner/events/workspace-migration-applied.event'; +import { WorkspaceMigrationEvents } from 'src/workspace/workspace-migration-runner/events/workspace-migration-events'; + +@Injectable() +export class WorkspaceSchemaStorageService { + constructor( + @InjectMemoryStorage('objectMetadataCollection') + private readonly objectMetadataMemoryStorageService: MemoryStorageService< + ObjectMetadataEntity[] + >, + @InjectMemoryStorage('typeDefs') + private readonly typeDefsMemoryStorageService: MemoryStorageService, + ) {} + + setObjectMetadata( + workspaceId: string, + objectMetadata: ObjectMetadataEntity[], + ) { + return this.objectMetadataMemoryStorageService.write({ + key: workspaceId, + data: objectMetadata, + }); + } + + getObjectMetadata( + workspaceId: string, + ): Promise { + return this.objectMetadataMemoryStorageService.read({ + key: workspaceId, + }); + } + + setTypeDefs(workspaceId: string, typeDefs: string): Promise { + return this.typeDefsMemoryStorageService.write({ + key: workspaceId, + data: typeDefs, + }); + } + + getTypeDefs(workspaceId: string): Promise { + return this.typeDefsMemoryStorageService.read({ + key: workspaceId, + }); + } + + /** + * Clear the workspace schema storage when new migrations are applied for a specific workspace + */ + @OnEvent(WorkspaceMigrationEvents.MigrationApplied) + handleMigrationAppliedEvent({ workspaceId }: WorkspaceMigrationAppliedEvent) { + this.objectMetadataMemoryStorageService.delete({ key: workspaceId }); + this.typeDefsMemoryStorageService.delete({ key: workspaceId }); + } +} diff --git a/server/src/workspace/workspace.factory.spec.ts b/server/src/workspace/workspace.factory.spec.ts index f14b2e13a..93e7e0631 100644 --- a/server/src/workspace/workspace.factory.spec.ts +++ b/server/src/workspace/workspace.factory.spec.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; +import { WorkspaceSchemaStorageService } from 'src/workspace/workspace-schema-storage/workspace-schema-storage.service'; import { WorkspaceFactory } from './workspace.factory'; @@ -31,6 +32,10 @@ describe('WorkspaceFactory', () => { provide: WorkspaceResolverFactory, useValue: {}, }, + { + provide: WorkspaceSchemaStorageService, + useValue: {}, + }, ], }).compile(); diff --git a/server/src/workspace/workspace.factory.ts b/server/src/workspace/workspace.factory.ts index 80410de7f..638c2f06e 100644 --- a/server/src/workspace/workspace.factory.ts +++ b/server/src/workspace/workspace.factory.ts @@ -5,6 +5,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { gql } from 'graphql-tag'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { WorkspaceSchemaStorageService } from 'src/workspace/workspace-schema-storage/workspace-schema-storage.service'; import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; import { WorkspaceGraphQLSchemaFactory } from './workspace-schema-builder/workspace-graphql-schema.factory'; @@ -18,6 +19,7 @@ export class WorkspaceFactory { private readonly objectMetadataService: ObjectMetadataService, private readonly workspaceGraphQLSchemaFactory: WorkspaceGraphQLSchemaFactory, private readonly workspaceResolverFactory: WorkspaceResolverFactory, + private readonly workspaceSchemaStorageService: WorkspaceSchemaStorageService, ) {} async createGraphQLSchema( @@ -37,15 +39,43 @@ export class WorkspaceFactory { return new GraphQLSchema({}); } - const objectMetadataCollection = - await this.objectMetadataService.getObjectMetadataFromWorkspaceId( - workspaceId, - ); + // Get object metadata from cache + let objectMetadataCollection = + await this.workspaceSchemaStorageService.getObjectMetadata(workspaceId); - const autoGeneratedSchema = await this.workspaceGraphQLSchemaFactory.create( - objectMetadataCollection, - workspaceResolverBuilderMethodNames, + // If object metadata is not cached, get it from the database + if (!objectMetadataCollection) { + objectMetadataCollection = + await this.objectMetadataService.getObjectMetadataFromWorkspaceId( + workspaceId, + ); + + await this.workspaceSchemaStorageService.setObjectMetadata( + workspaceId, + objectMetadataCollection, + ); + } + + // Get typeDefs from cache + let typeDefs = await this.workspaceSchemaStorageService.getTypeDefs( + workspaceId, ); + + // If typeDefs are not cached, generate them + if (!typeDefs) { + const autoGeneratedSchema = + await this.workspaceGraphQLSchemaFactory.create( + objectMetadataCollection, + workspaceResolverBuilderMethodNames, + ); + typeDefs = printSchema(autoGeneratedSchema); + + await this.workspaceSchemaStorageService.setTypeDefs( + workspaceId, + typeDefs, + ); + } + const autoGeneratedResolvers = await this.workspaceResolverFactory.create( workspaceId, objectMetadataCollection, @@ -53,7 +83,6 @@ export class WorkspaceFactory { ); // TODO: Cache the generate type definitions - const typeDefs = printSchema(autoGeneratedSchema); const executableSchema = makeExecutableSchema({ typeDefs: gql` ${typeDefs} diff --git a/server/src/workspace/workspace.module.ts b/server/src/workspace/workspace.module.ts index b60b51050..1fc388c45 100644 --- a/server/src/workspace/workspace.module.ts +++ b/server/src/workspace/workspace.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { MetadataModule } from 'src/metadata/metadata.module'; import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; +import { WorkspaceSchemaStorageModule } from 'src/workspace/workspace-schema-storage/workspace-schema-storage.module'; import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; import { WorkspaceFactory } from './workspace.factory'; @@ -16,6 +17,7 @@ import { WorkspaceResolverBuilderModule } from './workspace-resolver-builder/wor ObjectMetadataModule, WorkspaceSchemaBuilderModule, WorkspaceResolverBuilderModule, + WorkspaceSchemaStorageModule, ], providers: [WorkspaceFactory], exports: [WorkspaceFactory], diff --git a/server/yarn.lock b/server/yarn.lock index a7b2e5534..2138b935b 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1848,6 +1848,13 @@ path-to-regexp "3.2.0" tslib "2.5.3" +"@nestjs/event-emitter@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nestjs/event-emitter/-/event-emitter-2.0.3.tgz#3bfcb7b580f98b2dee3c44c567b45a506767f559" + integrity sha512-Pt7KAERrgK0OjvarSI3wfVhwZ8X1iLq1lXuodyRe+Zx3aLLP7fraFUHirASbFkB6KIQ1Zj+gZ1g8a9eu4GfFhw== + dependencies: + eventemitter2 "6.4.9" + "@nestjs/graphql@^12.0.8": version "12.0.8" resolved "https://registry.yarnpkg.com/@nestjs/graphql/-/graphql-12.0.8.tgz#15143b76dfb5fa4dc880d68a1bf2f7159ea077b6" @@ -5159,6 +5166,11 @@ etag@~1.8.1: resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +eventemitter2@6.4.9: + version "6.4.9" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" + integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== + eventemitter3@^3.1.0: version "3.1.2" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz"