From 017a0b15639a78593b7a633ee36095f8977b5ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Tue, 10 Oct 2023 10:50:54 +0200 Subject: [PATCH] feat: refactor custom object (#1887) * chore: drop old universal entity * feat: wip refactor graphql generation custom object * feat: refactor custom object resolvers fix: tests fix: import --------- Co-authored-by: Charles Bochet --- server/src/app.module.ts | 12 +- .../entity-resolver.service.spec.ts | 5 - .../entity-resolver.service.ts | 291 +++--------------- .../convert-fields-to-graphql.util.ts} | 0 .../utils/pg-graphql-query-builder.util.ts | 128 ++++++++ .../utils/pg-graphql-query-runner.util.ts | 94 ++++++ .../utils/stringify-without-key-quote.util.ts | 5 + .../schema-builder-context.interface.ts | 6 + .../schema-builder/schema-builder.module.ts | 13 + .../schema-builder.service.spec.ts | 27 ++ .../schema-builder.service.ts} | 114 +++---- .../utils/generate-connection-type.util.ts} | 2 +- .../utils/generate-create-input-type.util.ts | 33 ++ .../utils/generate-edge-type.util.ts} | 0 .../utils/generate-object-type.util.ts | 46 +++ .../utils/generate-update-input-type.util.ts | 33 ++ .../map-column-type-to-graphql-type.util.ts | 45 +++ .../utils/page-into-type.util.ts} | 0 .../graphql-types/object.graphql-type.ts | 159 ---------- .../schema-generation.module.ts | 19 -- server/src/tenant/tenant.module.ts | 16 +- ...service.spec.ts => tenant.service.spec.ts} | 21 +- server/src/tenant/tenant.service.ts | 45 +++ .../universal/args/base-universal.args.ts | 11 - .../args/delete-one-universal.args.ts | 10 - ...niversal-entity-order-by-relation.input.ts | 29 -- .../universal/args/universal-entity.input.ts | 18 -- .../universal/args/update-one-custom.args.ts | 13 - .../src/tenant/universal/universal.entity.ts | 25 -- .../src/tenant/universal/universal.module.ts | 11 - .../universal/universal.resolver.spec.ts | 27 -- .../tenant/universal/universal.resolver.ts | 41 --- server/src/tenant/universal/universal.util.ts | 59 ---- 33 files changed, 588 insertions(+), 770 deletions(-) rename server/src/tenant/entity-resolver/{entity-resolver.util.ts => utils/convert-fields-to-graphql.util.ts} (100%) create mode 100644 server/src/tenant/entity-resolver/utils/pg-graphql-query-builder.util.ts create mode 100644 server/src/tenant/entity-resolver/utils/pg-graphql-query-runner.util.ts create mode 100644 server/src/tenant/entity-resolver/utils/stringify-without-key-quote.util.ts create mode 100644 server/src/tenant/schema-builder/interfaces/schema-builder-context.interface.ts create mode 100644 server/src/tenant/schema-builder/schema-builder.module.ts create mode 100644 server/src/tenant/schema-builder/schema-builder.service.spec.ts rename server/src/tenant/{schema-generation/schema-generation.service.ts => schema-builder/schema-builder.service.ts} (67%) rename server/src/tenant/{schema-generation/graphql-types/connection.graphql-type.ts => schema-builder/utils/generate-connection-type.util.ts} (91%) create mode 100644 server/src/tenant/schema-builder/utils/generate-create-input-type.util.ts rename server/src/tenant/{schema-generation/graphql-types/edge.graphql-type.ts => schema-builder/utils/generate-edge-type.util.ts} (100%) create mode 100644 server/src/tenant/schema-builder/utils/generate-object-type.util.ts create mode 100644 server/src/tenant/schema-builder/utils/generate-update-input-type.util.ts create mode 100644 server/src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util.ts rename server/src/tenant/{schema-generation/graphql-types/page-info.graphql-type.ts => schema-builder/utils/page-into-type.util.ts} (100%) delete mode 100644 server/src/tenant/schema-generation/graphql-types/object.graphql-type.ts delete mode 100644 server/src/tenant/schema-generation/schema-generation.module.ts rename server/src/tenant/{schema-generation/schema-generation.service.spec.ts => tenant.service.spec.ts} (64%) create mode 100644 server/src/tenant/tenant.service.ts delete mode 100644 server/src/tenant/universal/args/base-universal.args.ts delete mode 100644 server/src/tenant/universal/args/delete-one-universal.args.ts delete mode 100644 server/src/tenant/universal/args/universal-entity-order-by-relation.input.ts delete mode 100644 server/src/tenant/universal/args/universal-entity.input.ts delete mode 100644 server/src/tenant/universal/args/update-one-custom.args.ts delete mode 100644 server/src/tenant/universal/universal.entity.ts delete mode 100644 server/src/tenant/universal/universal.module.ts delete mode 100644 server/src/tenant/universal/universal.resolver.spec.ts delete mode 100644 server/src/tenant/universal/universal.resolver.ts delete mode 100644 server/src/tenant/universal/universal.util.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index b40f38c87..982cb2369 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -17,12 +17,12 @@ import { PrismaModule } from './database/prisma.module'; import { HealthModule } from './health/health.module'; import { AbilityModule } from './ability/ability.module'; import { TenantModule } from './tenant/tenant.module'; -import { SchemaGenerationService } from './tenant/schema-generation/schema-generation.service'; import { EnvironmentService } from './integrations/environment/environment.service'; import { JwtAuthStrategy, JwtPayload, } from './core/auth/strategies/jwt.auth.strategy'; +import { TenantService } from './tenant/tenant.service'; @Module({ imports: [ @@ -37,7 +37,7 @@ import { conditionalSchema: async (request) => { try { // Get the SchemaGenerationService from the AppModule - const service = AppModule.moduleRef.get(SchemaGenerationService, { + const tenantService = AppModule.moduleRef.get(TenantService, { strict: false, }); @@ -57,8 +57,8 @@ import { // Extract JWT from the request const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request.req); - // If there is no token, return an empty schema - if (!token) { + // If there is no token or flexible backend is disabled, return an empty schema + if (!token || !environmentService.isFlexibleBackendEnabled()) { return new GraphQLSchema({}); } @@ -73,7 +73,9 @@ import { decoded as JwtPayload, ); - const conditionalSchema = await service.generateSchema(workspace.id); + const conditionalSchema = await tenantService.createTenantSchema( + workspace.id, + ); return conditionalSchema; } catch (error) { diff --git a/server/src/tenant/entity-resolver/entity-resolver.service.spec.ts b/server/src/tenant/entity-resolver/entity-resolver.service.spec.ts index 59f1a6b8d..b453d0c67 100644 --- a/server/src/tenant/entity-resolver/entity-resolver.service.spec.ts +++ b/server/src/tenant/entity-resolver/entity-resolver.service.spec.ts @@ -1,7 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; -import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { EntityResolverService } from './entity-resolver.service'; @@ -16,10 +15,6 @@ describe('EntityResolverService', () => { provide: DataSourceService, useValue: {}, }, - { - provide: EnvironmentService, - useValue: {}, - }, ], }).compile(); diff --git a/server/src/tenant/entity-resolver/entity-resolver.service.ts b/server/src/tenant/entity-resolver/entity-resolver.service.ts index a2f57b1a1..ac603583e 100644 --- a/server/src/tenant/entity-resolver/entity-resolver.service.ts +++ b/server/src/tenant/entity-resolver/entity-resolver.service.ts @@ -1,295 +1,84 @@ -import { - BadRequestException, - ForbiddenException, - Injectable, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { GraphQLResolveInfo } from 'graphql'; -import graphqlFields from 'graphql-fields'; -import { v4 as uuidv4 } from 'uuid'; + +import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; -import { EnvironmentService } from 'src/integrations/environment/environment.service'; -import { pascalCase } from 'src/utils/pascal-case'; -import { convertFieldsToGraphQL } from './entity-resolver.util'; - -function stringify(obj: any) { - const jsonString = JSON.stringify(obj); - const jsonWithoutQuotes = jsonString.replace(/"(\w+)"\s*:/g, '$1:'); - return jsonWithoutQuotes; -} +import { PGGraphQLQueryRunner } from './utils/pg-graphql-query-runner.util'; @Injectable() export class EntityResolverService { - constructor( - private readonly dataSourceService: DataSourceService, - private readonly environmentService: EnvironmentService, - ) {} + constructor(private readonly dataSourceService: DataSourceService) {} - async findAll( - entityName: string, - tableName: string, - workspaceId: string, - info: GraphQLResolveInfo, - fieldAliases: Record, - ) { - if (!this.environmentService.isFlexibleBackendEnabled()) { - throw new ForbiddenException(); - } - - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - const graphqlQuery = await this.prepareGrapQLQuery( - workspaceId, + async findMany(context: SchemaBuilderContext, info: GraphQLResolveInfo) { + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + entityName: context.entityName, + tableName: context.tableName, + workspaceId: context.workspaceId, info, - fieldAliases, - ); + fieldAliases: context.fieldAliases, + }); - /* TODO: This is a temporary solution to set the schema before each raw query. - getSchemaName is used to avoid a call to metadata.data_source table, - this won't work when we won't be able to dynamically recompute the schema name from its workspace_id only (remote schemas for example) - */ - await workspaceDataSource?.query(` - SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)}; - `); - const graphqlResult = await workspaceDataSource?.query(` - SELECT graphql.resolve($$ - { - findAll${pascalCase(entityName)}: ${tableName}Collection { - ${graphqlQuery} - } - } - $$); - `); - - const result = - graphqlResult?.[0]?.resolve?.data?.[`findAll${pascalCase(entityName)}`]; - - if (!result) { - throw new BadRequestException('Malformed result from GraphQL query'); - } - - return result; + return runner.findMany(); } async findOne( - entityName: string, - tableName: string, args: { id: string }, - workspaceId: string, + context: SchemaBuilderContext, info: GraphQLResolveInfo, - fieldAliases: Record, ) { - if (!this.environmentService.isFlexibleBackendEnabled()) { - throw new ForbiddenException(); - } - - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - const graphqlQuery = await this.prepareGrapQLQuery( - workspaceId, + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + entityName: context.entityName, + tableName: context.tableName, + workspaceId: context.workspaceId, info, - fieldAliases, - ); + fieldAliases: context.fieldAliases, + }); - await workspaceDataSource?.query(` - SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)}; - `); - const graphqlResult = await workspaceDataSource?.query(` - SELECT graphql.resolve($$ - { - findOne${pascalCase( - entityName, - )}: ${tableName}Collection(filter: { id: { eq: "${args.id}" } }) { - ${graphqlQuery} - } - } - $$); - `); - - const result = - graphqlResult?.[0]?.resolve?.data?.[`findOne${pascalCase(entityName)}`]; - - if (!result) { - throw new BadRequestException('Malformed result from GraphQL query'); - } - - return result; + return runner.findOne(args); } async createOne( - entityName: string, - tableName: string, args: { data: any }, - workspaceId: string, + context: SchemaBuilderContext, info: GraphQLResolveInfo, - fieldAliases: Record, ) { - if (!this.environmentService.isFlexibleBackendEnabled()) { - throw new ForbiddenException(); - } + const records = await this.createMany({ data: [args.data] }, context, info); - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - const graphqlQuery = await this.prepareGrapQLQuery( - workspaceId, - info, - fieldAliases, - ); - - await workspaceDataSource?.query(` - SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)}; - `); - const graphqlResult = await workspaceDataSource?.query(` - SELECT graphql.resolve($$ - mutation { - createOne${pascalCase( - entityName, - )}: insertInto${tableName}Collection(objects: [${stringify({ - id: uuidv4(), - ...args.data, - })}]) { - affectedCount - records { - ${graphqlQuery} - } - } - } - $$); - `); - - const result = - graphqlResult?.[0]?.resolve?.data?.[`createOne${pascalCase(entityName)}`] - ?.records[0]; - - if (!result) { - throw new BadRequestException('Malformed result from GraphQL query'); - } - - return result; + return records?.[0]; } async createMany( - entityName: string, - tableName: string, args: { data: any[] }, - workspaceId: string, + context: SchemaBuilderContext, info: GraphQLResolveInfo, - fieldAliases: Record, ) { - if (!this.environmentService.isFlexibleBackendEnabled()) { - throw new ForbiddenException(); - } - - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - const graphqlQuery = await this.prepareGrapQLQuery( - workspaceId, + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + entityName: context.entityName, + tableName: context.tableName, + workspaceId: context.workspaceId, info, - fieldAliases, - ); + fieldAliases: context.fieldAliases, + }); - await workspaceDataSource?.query(` - SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)}; - `); - const graphqlResult = await workspaceDataSource?.query(` - SELECT graphql.resolve($$ - mutation { - insertInto${entityName}Collection: insertInto${tableName}Collection(objects: ${stringify( - args.data.map((datum) => ({ - id: uuidv4(), - ...datum, - })), - )}) { - affectedCount - records { - ${graphqlQuery} - } - } - } - $$); - `); - - const result = - graphqlResult?.[0]?.resolve?.data?.[`insertInto${entityName}Collection`] - ?.records; - - if (!result) { - throw new BadRequestException('Malformed result from GraphQL query'); - } - - return result; + return runner.createMany(args); } async updateOne( - entityName: string, - tableName: string, args: { id: string; data: any }, - workspaceId: string, + context: SchemaBuilderContext, info: GraphQLResolveInfo, - fieldAliases: Record, ) { - if (!this.environmentService.isFlexibleBackendEnabled()) { - throw new ForbiddenException(); - } - - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - const graphqlQuery = await this.prepareGrapQLQuery( - workspaceId, + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + entityName: context.entityName, + tableName: context.tableName, + workspaceId: context.workspaceId, info, - fieldAliases, - ); + fieldAliases: context.fieldAliases, + }); - await workspaceDataSource?.query(` - SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)}; - `); - const graphqlResult = await workspaceDataSource?.query(` - SELECT graphql.resolve($$ - mutation { - updateOne${pascalCase( - entityName, - )}: update${tableName}Collection(set: ${stringify( - args.data, - )}, filter: { id: { eq: "${args.id}" } }) { - affectedCount - records { - ${graphqlQuery} - } - } - } - $$); - `); - - const result = - graphqlResult?.[0]?.resolve?.data?.[`updateOne${pascalCase(entityName)}`] - ?.records[0]; - - if (!result) { - throw new BadRequestException('Malformed result from GraphQL query'); - } - - return result; - } - - private async prepareGrapQLQuery( - workspaceId: string, - info: GraphQLResolveInfo, - fieldAliases: Record, - ): Promise { - // Extract requested fields from GraphQL resolve info - const fields = graphqlFields(info); - - await this.dataSourceService.createWorkspaceSchema(workspaceId); - - const graphqlQuery = convertFieldsToGraphQL(fields, fieldAliases); - - return graphqlQuery; + return runner.updateOne(args); } } diff --git a/server/src/tenant/entity-resolver/entity-resolver.util.ts b/server/src/tenant/entity-resolver/utils/convert-fields-to-graphql.util.ts similarity index 100% rename from server/src/tenant/entity-resolver/entity-resolver.util.ts rename to server/src/tenant/entity-resolver/utils/convert-fields-to-graphql.util.ts diff --git a/server/src/tenant/entity-resolver/utils/pg-graphql-query-builder.util.ts b/server/src/tenant/entity-resolver/utils/pg-graphql-query-builder.util.ts new file mode 100644 index 000000000..0e270bb08 --- /dev/null +++ b/server/src/tenant/entity-resolver/utils/pg-graphql-query-builder.util.ts @@ -0,0 +1,128 @@ +import { GraphQLResolveInfo } from 'graphql'; +import graphqlFields from 'graphql-fields'; +import { v4 as uuidv4 } from 'uuid'; + +import { pascalCase } from 'src/utils/pascal-case'; + +import { stringifyWithoutKeyQuote } from './stringify-without-key-quote.util'; +import { convertFieldsToGraphQL } from './convert-fields-to-graphql.util'; + +type Command = 'findMany' | 'findOne' | 'createMany' | 'updateOne'; + +type CommandArgs = { + findMany: null; + findOne: { id: string }; + createMany: { data: any[] }; + updateOne: { id: string; data: any }; +}; + +export interface PGGraphQLQueryBuilderOptions { + entityName: string; + tableName: string; + info: GraphQLResolveInfo; + fieldAliases: Record; +} + +export class PGGraphQLQueryBuilder { + private options: PGGraphQLQueryBuilderOptions; + private command: Command; + private commandArgs: any; + + constructor(options: PGGraphQLQueryBuilderOptions) { + this.options = options; + } + + private getFields(): string { + const fields = graphqlFields(this.options.info); + + return convertFieldsToGraphQL(fields, this.options.fieldAliases); + } + + // Define command setters + findMany() { + this.command = 'findMany'; + this.commandArgs = null; + return this; + } + + findOne(args: CommandArgs['findOne']) { + this.command = 'findOne'; + this.commandArgs = args; + return this; + } + + createMany(args: CommandArgs['createMany']) { + this.command = 'createMany'; + this.commandArgs = args; + return this; + } + + updateOne(args: CommandArgs['updateOne']) { + this.command = 'updateOne'; + this.commandArgs = args; + return this; + } + + build() { + const { entityName, tableName } = this.options; + const fields = this.getFields(); + + switch (this.command) { + case 'findMany': + return ` + query FindMany${pascalCase(entityName)} { + findMany${pascalCase(entityName)}: ${tableName}Collection { + ${fields} + } + } + `; + case 'findOne': + return ` + query FindOne${pascalCase(entityName)} { + findOne${pascalCase( + entityName, + )}: ${tableName}Collection(filter: { id: { eq: "${ + this.commandArgs.id + }" } }) { + ${fields} + } + } + `; + case 'createMany': + return ` + mutation CreateMany${pascalCase(entityName)} { + createMany${pascalCase( + entityName, + )}: insertInto${tableName}Collection(objects: ${stringifyWithoutKeyQuote( + this.commandArgs.data.map((datum) => ({ + id: uuidv4(), + ...datum, + })), + )}) { + affectedCount + records { + ${fields} + } + } + } + `; + case 'updateOne': + return ` + mutation UpdateOne${pascalCase(entityName)} { + updateOne${pascalCase( + entityName, + )}: update${tableName}Collection(set: ${stringifyWithoutKeyQuote( + this.commandArgs.data, + )}, filter: { id: { eq: "${this.commandArgs.id}" } }) { + affectedCount + records { + ${fields} + } + } + } + `; + default: + throw new Error('Invalid command'); + } + } +} diff --git a/server/src/tenant/entity-resolver/utils/pg-graphql-query-runner.util.ts b/server/src/tenant/entity-resolver/utils/pg-graphql-query-runner.util.ts new file mode 100644 index 000000000..953d21911 --- /dev/null +++ b/server/src/tenant/entity-resolver/utils/pg-graphql-query-runner.util.ts @@ -0,0 +1,94 @@ +import { BadRequestException } from '@nestjs/common'; + +import { GraphQLResolveInfo } from 'graphql'; + +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { pascalCase } from 'src/utils/pascal-case'; + +import { PGGraphQLQueryBuilder } from './pg-graphql-query-builder.util'; + +interface QueryRunnerOptions { + entityName: string; + tableName: string; + workspaceId: string; + info: GraphQLResolveInfo; + fieldAliases: Record; +} + +export class PGGraphQLQueryRunner { + private queryBuilder: PGGraphQLQueryBuilder; + private options: QueryRunnerOptions; + + constructor( + private dataSourceService: DataSourceService, + options: QueryRunnerOptions, + ) { + this.queryBuilder = new PGGraphQLQueryBuilder({ + entityName: options.entityName, + tableName: options.tableName, + info: options.info, + fieldAliases: options.fieldAliases, + }); + this.options = options; + } + + private async execute(query: string, workspaceId: string): Promise { + const workspaceDataSource = + await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); + + await workspaceDataSource?.query(` + SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)}; + `); + + return workspaceDataSource?.query(` + SELECT graphql.resolve($$ + ${query} + $$); + `); + } + + private parseResults(graphqlResult: any, command: string): any { + const entityKey = `${command}${pascalCase(this.options.entityName)}`; + const result = graphqlResult?.[0]?.resolve?.data?.[entityKey]; + + if (!result) { + throw new BadRequestException('Malformed result from GraphQL query'); + } + + return result; + } + + async findMany(): Promise { + const query = this.queryBuilder.findMany().build(); + const result = await this.execute(query, this.options.workspaceId); + + return this.parseResults(result, 'findMany'); + } + + async findOne(args: { id: string }): Promise { + const query = this.queryBuilder.findOne(args).build(); + const result = await this.execute(query, this.options.workspaceId); + + return this.parseResults(result, 'findOne'); + } + + async createMany(args: { data: any[] }): Promise { + const query = this.queryBuilder.createMany(args).build(); + const result = await this.execute(query, this.options.workspaceId); + + return this.parseResults(result, 'createMany')?.records; + } + + async createOne(args: { data: any }): Promise { + const records = await this.createMany({ data: [args.data] }); + + return records?.[0]; + } + + async updateOne(args: { id: string; data: any }): Promise { + const query = this.queryBuilder.updateOne(args).build(); + const result = await this.execute(query, this.options.workspaceId); + + return this.parseResults(result, 'updateOne')?.records?.[0]; + } +} diff --git a/server/src/tenant/entity-resolver/utils/stringify-without-key-quote.util.ts b/server/src/tenant/entity-resolver/utils/stringify-without-key-quote.util.ts new file mode 100644 index 000000000..ff041ed40 --- /dev/null +++ b/server/src/tenant/entity-resolver/utils/stringify-without-key-quote.util.ts @@ -0,0 +1,5 @@ +export const stringifyWithoutKeyQuote = (obj: any) => { + const jsonString = JSON.stringify(obj); + const jsonWithoutQuotes = jsonString.replace(/"(\w+)"\s*:/g, '$1:'); + return jsonWithoutQuotes; +}; diff --git a/server/src/tenant/schema-builder/interfaces/schema-builder-context.interface.ts b/server/src/tenant/schema-builder/interfaces/schema-builder-context.interface.ts new file mode 100644 index 000000000..9deb6118d --- /dev/null +++ b/server/src/tenant/schema-builder/interfaces/schema-builder-context.interface.ts @@ -0,0 +1,6 @@ +export interface SchemaBuilderContext { + entityName: string; + tableName: string; + workspaceId: string; + fieldAliases: Record; +} diff --git a/server/src/tenant/schema-builder/schema-builder.module.ts b/server/src/tenant/schema-builder/schema-builder.module.ts new file mode 100644 index 000000000..93247c2f7 --- /dev/null +++ b/server/src/tenant/schema-builder/schema-builder.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; + +import { EntityResolverModule } from 'src/tenant/entity-resolver/entity-resolver.module'; +import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; + +import { SchemaBuilderService } from './schema-builder.service'; + +@Module({ + imports: [EntityResolverModule], + providers: [SchemaBuilderService, JwtAuthGuard], + exports: [SchemaBuilderService], +}) +export class SchemaBuilderModule {} diff --git a/server/src/tenant/schema-builder/schema-builder.service.spec.ts b/server/src/tenant/schema-builder/schema-builder.service.spec.ts new file mode 100644 index 000000000..37bb294b7 --- /dev/null +++ b/server/src/tenant/schema-builder/schema-builder.service.spec.ts @@ -0,0 +1,27 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { EntityResolverService } from 'src/tenant/entity-resolver/entity-resolver.service'; + +import { SchemaBuilderService } from './schema-builder.service'; + +describe('SchemaBuilderService', () => { + let service: SchemaBuilderService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SchemaBuilderService, + { + provide: EntityResolverService, + useValue: {}, + }, + ], + }).compile(); + + service = module.get(SchemaBuilderService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/server/src/tenant/schema-generation/schema-generation.service.ts b/server/src/tenant/schema-builder/schema-builder.service.ts similarity index 67% rename from server/src/tenant/schema-generation/schema-generation.service.ts rename to server/src/tenant/schema-builder/schema-builder.service.ts index d2c7635ad..b413686ee 100644 --- a/server/src/tenant/schema-generation/schema-generation.service.ts +++ b/server/src/tenant/schema-builder/schema-builder.service.ts @@ -1,43 +1,37 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { + GraphQLFieldConfigMap, GraphQLID, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, GraphQLObjectType, - GraphQLResolveInfo, GraphQLSchema, } from 'graphql'; import { EntityResolverService } from 'src/tenant/entity-resolver/entity-resolver.service'; -import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service'; import { pascalCase } from 'src/utils/pascal-case'; -import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity'; -import { generateEdgeType } from './graphql-types/edge.graphql-type'; -import { generateConnectionType } from './graphql-types/connection.graphql-type'; -import { - generateCreateInputType, - generateObjectType, - generateUpdateInputType, -} from './graphql-types/object.graphql-type'; +import { generateEdgeType } from './utils/generate-edge-type.util'; +import { generateConnectionType } from './utils/generate-connection-type.util'; +import { generateObjectType } from './utils/generate-object-type.util'; +import { generateCreateInputType } from './utils/generate-create-input-type.util'; +import { generateUpdateInputType } from './utils/generate-update-input-type.util'; +import { SchemaBuilderContext } from './interfaces/schema-builder-context.interface'; @Injectable() -export class SchemaGenerationService { - constructor( - private readonly dataSourceMetadataService: DataSourceMetadataService, - private readonly objectMetadataService: ObjectMetadataService, - private readonly entityResolverService: EntityResolverService, - ) {} +export class SchemaBuilderService { + workspaceId: string; + + constructor(private readonly entityResolverService: EntityResolverService) {} private generateQueryFieldForEntity( entityName: string, tableName: string, ObjectType: GraphQLObjectType, objectDefinition: ObjectMetadata, - workspaceId: string, ) { const fieldAliases = objectDefinition?.fields.reduce( @@ -47,6 +41,12 @@ export class SchemaGenerationService { }), {}, ) || {}; + const schemaBuilderContext: SchemaBuilderContext = { + entityName, + tableName, + workspaceId: this.workspaceId, + fieldAliases, + }; const EdgeType = generateEdgeType(ObjectType); const ConnectionType = generateConnectionType(EdgeType); @@ -54,13 +54,10 @@ export class SchemaGenerationService { return { [`findMany${pascalCase(entityName)}`]: { type: ConnectionType, - resolve: async (root, args, context, info: GraphQLResolveInfo) => { - return this.entityResolverService.findAll( - entityName, - tableName, - workspaceId, + resolve: async (root, args, context, info) => { + return this.entityResolverService.findMany( + schemaBuilderContext, info, - fieldAliases, ); }, }, @@ -71,16 +68,13 @@ export class SchemaGenerationService { }, resolve: (root, args, context, info) => { return this.entityResolverService.findOne( - entityName, - tableName, args, - workspaceId, + schemaBuilderContext, info, - fieldAliases, ); }, }, - }; + } as GraphQLFieldConfigMap; } private generateMutationFieldForEntity( @@ -90,7 +84,6 @@ export class SchemaGenerationService { CreateInputType: GraphQLInputObjectType, UpdateInputType: GraphQLInputObjectType, objectDefinition: ObjectMetadata, - workspaceId: string, ) { const fieldAliases = objectDefinition?.fields.reduce( @@ -100,6 +93,12 @@ export class SchemaGenerationService { }), {}, ) || {}; + const schemaBuilderContext: SchemaBuilderContext = { + entityName, + tableName, + workspaceId: this.workspaceId, + fieldAliases, + }; return { [`createOne${pascalCase(entityName)}`]: { @@ -109,12 +108,9 @@ export class SchemaGenerationService { }, resolve: (root, args, context, info) => { return this.entityResolverService.createOne( - entityName, - tableName, args, - workspaceId, + schemaBuilderContext, info, - fieldAliases, ); }, }, @@ -129,12 +125,9 @@ export class SchemaGenerationService { }, resolve: (root, args, context, info) => { return this.entityResolverService.createMany( - entityName, - tableName, args, - workspaceId, + schemaBuilderContext, info, - fieldAliases, ); }, }, @@ -146,22 +139,19 @@ export class SchemaGenerationService { }, resolve: (root, args, context, info) => { return this.entityResolverService.updateOne( - entityName, - tableName, args, - workspaceId, + schemaBuilderContext, info, - fieldAliases, ); }, }, - }; + } as GraphQLFieldConfigMap; } - private generateQueryAndMutationTypes( - objectMetadata: ObjectMetadata[], - workspaceId: string, - ): { query: GraphQLObjectType; mutation: GraphQLObjectType } { + private generateQueryAndMutationTypes(objectMetadata: ObjectMetadata[]): { + query: GraphQLObjectType; + mutation: GraphQLObjectType; + } { const queryFields: any = {}; const mutationFields: any = {}; @@ -191,7 +181,6 @@ export class SchemaGenerationService { tableName, ObjectType, objectDefinition, - workspaceId, ), ); @@ -204,7 +193,6 @@ export class SchemaGenerationService { CreateInputType, UpdateInputType, objectDefinition, - workspaceId, ), ); } @@ -222,33 +210,13 @@ export class SchemaGenerationService { } async generateSchema( - workspaceId: string | undefined, + workspaceId: string, + objectMetadata: ObjectMetadata[], ): Promise { - if (!workspaceId) { - return new GraphQLSchema({}); - } + this.workspaceId = workspaceId; - const dataSourcesMetadata = - await this.dataSourceMetadataService.getDataSourcesMetadataFromWorkspaceId( - workspaceId, - ); - - // Can'f find any data sources for this workspace - if (!dataSourcesMetadata || dataSourcesMetadata.length === 0) { - return new GraphQLSchema({}); - } - - const dataSourceMetadata = dataSourcesMetadata[0]; - - const objectMetadata = - await this.objectMetadataService.getObjectMetadataFromDataSourceId( - dataSourceMetadata.id, - ); - - const { query, mutation } = this.generateQueryAndMutationTypes( - objectMetadata, - workspaceId, - ); + const { query, mutation } = + this.generateQueryAndMutationTypes(objectMetadata); return new GraphQLSchema({ query, diff --git a/server/src/tenant/schema-generation/graphql-types/connection.graphql-type.ts b/server/src/tenant/schema-builder/utils/generate-connection-type.util.ts similarity index 91% rename from server/src/tenant/schema-generation/graphql-types/connection.graphql-type.ts rename to server/src/tenant/schema-builder/utils/generate-connection-type.util.ts index 2db8a18d9..759cfbed8 100644 --- a/server/src/tenant/schema-generation/graphql-types/connection.graphql-type.ts +++ b/server/src/tenant/schema-builder/utils/generate-connection-type.util.ts @@ -1,6 +1,6 @@ import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql'; -import { PageInfoType } from './page-info.graphql-type'; +import { PageInfoType } from './page-into-type.util'; /** * Generate a GraphQL connection type based on the EdgeType. diff --git a/server/src/tenant/schema-builder/utils/generate-create-input-type.util.ts b/server/src/tenant/schema-builder/utils/generate-create-input-type.util.ts new file mode 100644 index 000000000..dd5acefa9 --- /dev/null +++ b/server/src/tenant/schema-builder/utils/generate-create-input-type.util.ts @@ -0,0 +1,33 @@ +import { GraphQLInputObjectType, GraphQLNonNull } from 'graphql'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { pascalCase } from 'src/utils/pascal-case'; + +import { mapColumnTypeToGraphQLType } from './map-column-type-to-graphql-type.util'; + +/** + * Generate a GraphQL create input type based on the name and columns. + * @param name Name for the GraphQL input. + * @param columns Array of FieldMetadata columns. + * @returns GraphQLInputObjectType + */ +export const generateCreateInputType = ( + name: string, + columns: FieldMetadata[], +): GraphQLInputObjectType => { + const fields: Record = {}; + + columns.forEach((column) => { + const graphqlType = mapColumnTypeToGraphQLType(column); + + fields[column.displayName] = { + type: !column.isNullable ? new GraphQLNonNull(graphqlType) : graphqlType, + description: column.targetColumnName, + }; + }); + + return new GraphQLInputObjectType({ + name: `${pascalCase(name)}CreateInput`, + fields, + }); +}; diff --git a/server/src/tenant/schema-generation/graphql-types/edge.graphql-type.ts b/server/src/tenant/schema-builder/utils/generate-edge-type.util.ts similarity index 100% rename from server/src/tenant/schema-generation/graphql-types/edge.graphql-type.ts rename to server/src/tenant/schema-builder/utils/generate-edge-type.util.ts diff --git a/server/src/tenant/schema-builder/utils/generate-object-type.util.ts b/server/src/tenant/schema-builder/utils/generate-object-type.util.ts new file mode 100644 index 000000000..acf7e9482 --- /dev/null +++ b/server/src/tenant/schema-builder/utils/generate-object-type.util.ts @@ -0,0 +1,46 @@ +import { + GraphQLID, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, +} from 'graphql'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { pascalCase } from 'src/utils/pascal-case'; + +import { mapColumnTypeToGraphQLType } from './map-column-type-to-graphql-type.util'; + +const defaultFields = { + id: { type: new GraphQLNonNull(GraphQLID) }, + createdAt: { type: new GraphQLNonNull(GraphQLString) }, + updatedAt: { type: new GraphQLNonNull(GraphQLString) }, +}; + +/** + * Generate a GraphQL object type based on the name and columns. + * @param name Name for the GraphQL object. + * @param columns Array of FieldMetadata columns. + * @returns GraphQLObjectType + */ +export const generateObjectType = ( + name: string, + columns: FieldMetadata[], +): GraphQLObjectType => { + const fields = { + ...defaultFields, + }; + + columns.forEach((column) => { + const graphqlType = mapColumnTypeToGraphQLType(column); + + fields[column.displayName] = { + type: !column.isNullable ? new GraphQLNonNull(graphqlType) : graphqlType, + description: column.targetColumnName, + }; + }); + + return new GraphQLObjectType({ + name: pascalCase(name), + fields, + }); +}; diff --git a/server/src/tenant/schema-builder/utils/generate-update-input-type.util.ts b/server/src/tenant/schema-builder/utils/generate-update-input-type.util.ts new file mode 100644 index 000000000..8b79fe18d --- /dev/null +++ b/server/src/tenant/schema-builder/utils/generate-update-input-type.util.ts @@ -0,0 +1,33 @@ +import { GraphQLInputObjectType } from 'graphql'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { pascalCase } from 'src/utils/pascal-case'; + +import { mapColumnTypeToGraphQLType } from './map-column-type-to-graphql-type.util'; + +/** + * Generate a GraphQL update input type based on the name and columns. + * @param name Name for the GraphQL input. + * @param columns Array of FieldMetadata columns. + * @returns GraphQLInputObjectType + */ +export const generateUpdateInputType = ( + name: string, + columns: FieldMetadata[], +): GraphQLInputObjectType => { + const fields: Record = {}; + + columns.forEach((column) => { + const graphqlType = mapColumnTypeToGraphQLType(column); + // No GraphQLNonNull wrapping here, so all fields are optional + fields[column.displayName] = { + type: graphqlType, + description: column.targetColumnName, + }; + }); + + return new GraphQLInputObjectType({ + name: `${pascalCase(name)}UpdateInput`, + fields, + }); +}; diff --git a/server/src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util.ts b/server/src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util.ts new file mode 100644 index 000000000..7ea92eede --- /dev/null +++ b/server/src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util.ts @@ -0,0 +1,45 @@ +import { + GraphQLBoolean, + GraphQLEnumType, + GraphQLID, + GraphQLInt, + GraphQLString, +} from 'graphql'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { pascalCase } from 'src/utils/pascal-case'; + +/** + * Map the column type from field-metadata to its corresponding GraphQL type. + * @param columnType Type of the column in the database. + */ +export const mapColumnTypeToGraphQLType = (column: FieldMetadata) => { + switch (column.type) { + case 'uuid': + return GraphQLID; + case 'text': + case 'url': + case 'date': + return GraphQLString; + case 'boolean': + return GraphQLBoolean; + case 'number': + return GraphQLInt; + case 'enum': { + if (column.enums && column.enums.length > 0) { + const enumName = `${pascalCase(column.objectId)}${pascalCase( + column.displayName, + )}Enum`; + + return new GraphQLEnumType({ + name: enumName, + values: Object.fromEntries( + column.enums.map((value) => [value, { value }]), + ), + }); + } + } + default: + return GraphQLString; + } +}; diff --git a/server/src/tenant/schema-generation/graphql-types/page-info.graphql-type.ts b/server/src/tenant/schema-builder/utils/page-into-type.util.ts similarity index 100% rename from server/src/tenant/schema-generation/graphql-types/page-info.graphql-type.ts rename to server/src/tenant/schema-builder/utils/page-into-type.util.ts diff --git a/server/src/tenant/schema-generation/graphql-types/object.graphql-type.ts b/server/src/tenant/schema-generation/graphql-types/object.graphql-type.ts deleted file mode 100644 index 88abe166f..000000000 --- a/server/src/tenant/schema-generation/graphql-types/object.graphql-type.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { - GraphQLBoolean, - GraphQLEnumType, - GraphQLID, - GraphQLInputObjectType, - GraphQLInt, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString, -} from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity'; -import { pascalCase } from 'src/utils/pascal-case'; - -/** - * Map the column type from field-metadata to its corresponding GraphQL type. - * @param columnType Type of the column in the database. - */ -const mapColumnTypeToGraphQLType = (column: FieldMetadata): any => { - switch (column.type) { - case 'uuid': - return GraphQLID; - case 'text': - case 'url': - case 'date': - return GraphQLString; - case 'boolean': - return GraphQLBoolean; - case 'number': - return GraphQLInt; - case 'enum': { - if (column.enums && column.enums.length > 0) { - const enumName = `${pascalCase(column.objectId)}${pascalCase( - column.displayName, - )}Enum`; - - return new GraphQLEnumType({ - name: enumName, - values: Object.fromEntries( - column.enums.map((value) => [value, { value }]), - ), - }); - } - } - default: - return GraphQLString; - } -}; - -/** - * Generate a GraphQL object type based on the name and columns. - * @param name Name for the GraphQL object. - * @param columns Array of FieldMetadata columns. - * @returns GraphQLObjectType - */ -export const generateObjectType = ( - name: string, - columns: FieldMetadata[], -): GraphQLObjectType => { - const fields: Record = { - // Default fields - id: { type: new GraphQLNonNull(GraphQLID) }, - createdAt: { type: new GraphQLNonNull(GraphQLString) }, - updatedAt: { type: new GraphQLNonNull(GraphQLString) }, - }; - - columns.forEach((column) => { - let graphqlType = mapColumnTypeToGraphQLType(column); - - if (!column.isNullable) { - graphqlType = new GraphQLNonNull(graphqlType); - } - - fields[column.displayName] = { - type: graphqlType, - description: column.targetColumnName, - }; - }); - - return new GraphQLObjectType({ - name: pascalCase(name), - fields, - }); -}; - -/** - * Generate a GraphQL create input type based on the name and columns. - * @param name Name for the GraphQL input. - * @param columns Array of FieldMetadata columns. - * @returns GraphQLInputObjectType - */ -export const generateCreateInputType = ( - name: string, - columns: FieldMetadata[], -): GraphQLInputObjectType => { - const fields: Record = {}; - - columns.forEach((column) => { - let graphqlType = mapColumnTypeToGraphQLType(column); - - if (!column.isNullable) { - graphqlType = new GraphQLNonNull(graphqlType); - } - - fields[column.displayName] = { - type: graphqlType, - description: column.targetColumnName, - }; - }); - - return new GraphQLInputObjectType({ - name: `${pascalCase(name)}CreateInput`, - fields, - }); -}; - -/** - * Generate a GraphQL update input type based on the name and columns. - * @param name Name for the GraphQL input. - * @param columns Array of FieldMetadata columns. - * @returns GraphQLInputObjectType - */ -export const generateUpdateInputType = ( - name: string, - columns: FieldMetadata[], -): GraphQLInputObjectType => { - const fields: Record = {}; - - columns.forEach((column) => { - const graphqlType = mapColumnTypeToGraphQLType(column); - // No GraphQLNonNull wrapping here, so all fields are optional - fields[column.displayName] = { - type: graphqlType, - description: column.targetColumnName, - }; - }); - - return new GraphQLInputObjectType({ - name: `${pascalCase(name)}UpdateInput`, - fields, - }); -}; - -/** - * Generate multiple GraphQL object types based on an array of object metadata. - * @param objectMetadata Array of ObjectMetadata. - */ -export const generateObjectTypes = (objectMetadata: ObjectMetadata[]) => { - const objectTypes: Record = {}; - - for (const object of objectMetadata) { - const ObjectType = generateObjectType(object.displayName, object.fields); - - objectTypes[object.displayName] = ObjectType; - } - - return objectTypes; -}; diff --git a/server/src/tenant/schema-generation/schema-generation.module.ts b/server/src/tenant/schema-generation/schema-generation.module.ts deleted file mode 100644 index e2709cdc9..000000000 --- a/server/src/tenant/schema-generation/schema-generation.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { EntityResolverModule } from 'src/tenant/entity-resolver/entity-resolver.module'; -import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; -import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module'; -import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; - -import { SchemaGenerationService } from './schema-generation.service'; - -@Module({ - imports: [ - EntityResolverModule, - DataSourceMetadataModule, - ObjectMetadataModule, - ], - providers: [SchemaGenerationService, JwtAuthGuard], - exports: [SchemaGenerationService], -}) -export class SchemaGenerationModule {} diff --git a/server/src/tenant/tenant.module.ts b/server/src/tenant/tenant.module.ts index 4798cab37..e3e670cf5 100644 --- a/server/src/tenant/tenant.module.ts +++ b/server/src/tenant/tenant.module.ts @@ -1,11 +1,21 @@ import { Module } from '@nestjs/common'; import { MetadataModule } from 'src/metadata/metadata.module'; +import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data-source-metadata.module'; +import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; -import { UniversalModule } from './universal/universal.module'; -import { SchemaGenerationModule } from './schema-generation/schema-generation.module'; +import { TenantService } from './tenant.service'; + +import { SchemaBuilderModule } from './schema-builder/schema-builder.module'; @Module({ - imports: [MetadataModule, UniversalModule, SchemaGenerationModule], + imports: [ + MetadataModule, + SchemaBuilderModule, + DataSourceMetadataModule, + ObjectMetadataModule, + ], + providers: [TenantService], + exports: [TenantService], }) export class TenantModule {} diff --git a/server/src/tenant/schema-generation/schema-generation.service.spec.ts b/server/src/tenant/tenant.service.spec.ts similarity index 64% rename from server/src/tenant/schema-generation/schema-generation.service.spec.ts rename to server/src/tenant/tenant.service.spec.ts index 3bfcf4198..51f2467a4 100644 --- a/server/src/tenant/schema-generation/schema-generation.service.spec.ts +++ b/server/src/tenant/tenant.service.spec.ts @@ -2,17 +2,22 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service'; import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; -import { EntityResolverService } from 'src/tenant/entity-resolver/entity-resolver.service'; -import { SchemaGenerationService } from './schema-generation.service'; +import { TenantService } from './tenant.service'; -describe('SchemaGenerationService', () => { - let service: SchemaGenerationService; +import { SchemaBuilderService } from './schema-builder/schema-builder.service'; + +describe('TenantService', () => { + let service: TenantService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - SchemaGenerationService, + TenantService, + { + provide: SchemaBuilderService, + useValue: {}, + }, { provide: DataSourceMetadataService, useValue: {}, @@ -21,14 +26,10 @@ describe('SchemaGenerationService', () => { provide: ObjectMetadataService, useValue: {}, }, - { - provide: EntityResolverService, - useValue: {}, - }, ], }).compile(); - service = module.get(SchemaGenerationService); + service = module.get(TenantService); }); it('should be defined', () => { diff --git a/server/src/tenant/tenant.service.ts b/server/src/tenant/tenant.service.ts new file mode 100644 index 000000000..92fa67363 --- /dev/null +++ b/server/src/tenant/tenant.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLSchema } from 'graphql'; + +import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service'; +import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; + +import { SchemaBuilderService } from './schema-builder/schema-builder.service'; + +@Injectable() +export class TenantService { + constructor( + private readonly schemaBuilderService: SchemaBuilderService, + private readonly dataSourceMetadataService: DataSourceMetadataService, + private readonly objectMetadataService: ObjectMetadataService, + ) {} + + async createTenantSchema(workspaceId: string | undefined) { + if (!workspaceId) { + return new GraphQLSchema({}); + } + + const dataSourcesMetadata = + await this.dataSourceMetadataService.getDataSourcesMetadataFromWorkspaceId( + workspaceId, + ); + + // Can'f find any data sources for this workspace + if (!dataSourcesMetadata || dataSourcesMetadata.length === 0) { + return new GraphQLSchema({}); + } + + const dataSourceMetadata = dataSourcesMetadata[0]; + + const objectMetadata = + await this.objectMetadataService.getObjectMetadataFromDataSourceId( + dataSourceMetadata.id, + ); + + return this.schemaBuilderService.generateSchema( + workspaceId, + objectMetadata, + ); + } +} diff --git a/server/src/tenant/universal/args/base-universal.args.ts b/server/src/tenant/universal/args/base-universal.args.ts deleted file mode 100644 index 8ed90d8f5..000000000 --- a/server/src/tenant/universal/args/base-universal.args.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ArgsType, Field } from '@nestjs/graphql'; - -import { IsNotEmpty, IsString } from 'class-validator'; - -@ArgsType() -export class BaseUniversalArgs { - @Field(() => String) - @IsNotEmpty() - @IsString() - entity: string; -} diff --git a/server/src/tenant/universal/args/delete-one-universal.args.ts b/server/src/tenant/universal/args/delete-one-universal.args.ts deleted file mode 100644 index 998efeaf4..000000000 --- a/server/src/tenant/universal/args/delete-one-universal.args.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ArgsType, Field } from '@nestjs/graphql'; - -import { BaseUniversalArgs } from './base-universal.args'; -import { UniversalEntityInput } from './universal-entity.input'; - -@ArgsType() -export class DeleteOneUniversalArgs extends BaseUniversalArgs { - @Field(() => UniversalEntityInput, { nullable: true }) - where?: UniversalEntityInput; -} diff --git a/server/src/tenant/universal/args/universal-entity-order-by-relation.input.ts b/server/src/tenant/universal/args/universal-entity-order-by-relation.input.ts deleted file mode 100644 index 12f0f46f8..000000000 --- a/server/src/tenant/universal/args/universal-entity-order-by-relation.input.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; -import { registerEnumType } from '@nestjs/graphql'; - -import GraphQLJSON from 'graphql-type-json'; - -export enum TypeORMSortOrder { - ASC = 'ASC', - DESC = 'DESC', -} - -registerEnumType(TypeORMSortOrder, { - name: 'TypeORMSortOrder', - description: undefined, -}); - -@InputType() -export class UniversalEntityOrderByRelationInput { - @Field(() => TypeORMSortOrder, { nullable: true }) - id?: keyof typeof TypeORMSortOrder; - - @Field(() => GraphQLJSON, { nullable: true }) - data?: Record; - - @Field(() => TypeORMSortOrder, { nullable: true }) - createdAt?: keyof typeof TypeORMSortOrder; - - @Field(() => TypeORMSortOrder, { nullable: true }) - updatedAt?: keyof typeof TypeORMSortOrder; -} diff --git a/server/src/tenant/universal/args/universal-entity.input.ts b/server/src/tenant/universal/args/universal-entity.input.ts deleted file mode 100644 index 149a3e0ce..000000000 --- a/server/src/tenant/universal/args/universal-entity.input.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Field, ID, InputType } from '@nestjs/graphql'; - -import GraphQLJSON from 'graphql-type-json'; - -@InputType() -export class UniversalEntityInput { - @Field(() => ID, { nullable: true }) - id?: string; - - @Field(() => GraphQLJSON, { nullable: true }) - data?: Record; - - @Field(() => Date, { nullable: true }) - createdAt?: Date; - - @Field(() => Date, { nullable: true }) - updatedAt?: Date; -} diff --git a/server/src/tenant/universal/args/update-one-custom.args.ts b/server/src/tenant/universal/args/update-one-custom.args.ts deleted file mode 100644 index f8f19eea4..000000000 --- a/server/src/tenant/universal/args/update-one-custom.args.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ArgsType, Field } from '@nestjs/graphql'; - -import { BaseUniversalArgs } from './base-universal.args'; -import { UniversalEntityInput } from './universal-entity.input'; - -@ArgsType() -export class UpdateOneCustomArgs extends BaseUniversalArgs { - @Field(() => UniversalEntityInput, { nullable: false }) - data!: UniversalEntityInput; - - @Field(() => UniversalEntityInput, { nullable: true }) - where?: UniversalEntityInput; -} diff --git a/server/src/tenant/universal/universal.entity.ts b/server/src/tenant/universal/universal.entity.ts deleted file mode 100644 index fe16223f3..000000000 --- a/server/src/tenant/universal/universal.entity.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; - -import GraphQLJSON from 'graphql-type-json'; - -import { Paginated } from 'src/utils/pagination'; - -@ObjectType() -export class UniversalEntity { - @Field(() => ID, { nullable: false }) - id!: string; - - @Field(() => GraphQLJSON, { nullable: false }) - data!: Record; - - @Field(() => Date, { nullable: false }) - createdAt!: Date; - - @Field(() => Date, { nullable: false }) - updatedAt!: Date; -} - -@ObjectType() -export class PaginatedUniversalEntity extends Paginated( - UniversalEntity, -) {} diff --git a/server/src/tenant/universal/universal.module.ts b/server/src/tenant/universal/universal.module.ts deleted file mode 100644 index fe2715522..000000000 --- a/server/src/tenant/universal/universal.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; - -import { UniversalResolver } from './universal.resolver'; - -@Module({ - imports: [DataSourceModule], - providers: [UniversalResolver], -}) -export class UniversalModule {} diff --git a/server/src/tenant/universal/universal.resolver.spec.ts b/server/src/tenant/universal/universal.resolver.spec.ts deleted file mode 100644 index 5a2533800..000000000 --- a/server/src/tenant/universal/universal.resolver.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { EnvironmentService } from 'src/integrations/environment/environment.service'; - -import { UniversalResolver } from './universal.resolver'; - -describe('UniversalResolver', () => { - let resolver: UniversalResolver; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - UniversalResolver, - { - provide: EnvironmentService, - useValue: {}, - }, - ], - }).compile(); - - resolver = module.get(UniversalResolver); - }); - - it('should be defined', () => { - expect(resolver).toBeDefined(); - }); -}); diff --git a/server/src/tenant/universal/universal.resolver.ts b/server/src/tenant/universal/universal.resolver.ts deleted file mode 100644 index cf0ffeee6..000000000 --- a/server/src/tenant/universal/universal.resolver.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Query, Resolver } from '@nestjs/graphql'; -import { ForbiddenException, UseGuards } from '@nestjs/common'; - -import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; -import { EnvironmentService } from 'src/integrations/environment/environment.service'; - -import { UniversalEntity } from './universal.entity'; - -@UseGuards(JwtAuthGuard) -@Resolver(() => UniversalEntity) -export class UniversalResolver { - constructor(private readonly environmentService: EnvironmentService) {} - - @Query(() => UniversalEntity) - updateOneCustom(): UniversalEntity { - if (!this.environmentService.isFlexibleBackendEnabled()) { - throw new ForbiddenException(); - } - - return { - id: 'exampleId', - data: {}, - createdAt: new Date(), - updatedAt: new Date(), - }; - } - - @Query(() => UniversalEntity) - deleteOneCustom(): UniversalEntity { - if (!this.environmentService.isFlexibleBackendEnabled()) { - throw new ForbiddenException(); - } - - return { - id: 'exampleId', - data: {}, - createdAt: new Date(), - updatedAt: new Date(), - }; - } -} diff --git a/server/src/tenant/universal/universal.util.ts b/server/src/tenant/universal/universal.util.ts deleted file mode 100644 index c90b1a8ef..000000000 --- a/server/src/tenant/universal/universal.util.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { snakeCase } from 'src/utils/snake-case'; - -import { UniversalEntityInput } from './args/universal-entity.input'; -import { - UniversalEntityOrderByRelationInput, - TypeORMSortOrder, -} from './args/universal-entity-order-by-relation.input'; - -export const getRawTypeORMWhereClause = ( - entity: string, - where?: UniversalEntityInput | undefined, -) => { - if (!where) { - return { - where: '', - parameters: {}, - }; - } - - const { id, data, createdAt, updatedAt } = where; - const flattenWhere: any = { - ...(id ? { id } : {}), - ...data, - ...(createdAt ? { createdAt } : {}), - ...(updatedAt ? { updatedAt } : {}), - }; - - return { - where: Object.keys(flattenWhere) - .map((key) => `${entity}.${snakeCase(key)} = :${key}`) - .join(' AND '), - parameters: flattenWhere, - }; -}; - -export const getRawTypeORMOrderByClause = ( - entity: string, - orderBy?: UniversalEntityOrderByRelationInput | undefined, -) => { - if (!orderBy) { - return {}; - } - - const { id, data, createdAt, updatedAt } = orderBy; - const flattenWhere: any = { - ...(id ? { id } : {}), - ...data, - ...(createdAt ? { createdAt } : {}), - ...(updatedAt ? { updatedAt } : {}), - }; - - const orderByClause: Record = {}; - - for (const key of Object.keys(flattenWhere)) { - orderByClause[`${entity}.${snakeCase(key)}`] = flattenWhere[key]; - } - - return orderByClause; -};