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 <charles@twenty.com>
This commit is contained in:
@ -17,12 +17,12 @@ import { PrismaModule } from './database/prisma.module';
|
|||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
import { AbilityModule } from './ability/ability.module';
|
import { AbilityModule } from './ability/ability.module';
|
||||||
import { TenantModule } from './tenant/tenant.module';
|
import { TenantModule } from './tenant/tenant.module';
|
||||||
import { SchemaGenerationService } from './tenant/schema-generation/schema-generation.service';
|
|
||||||
import { EnvironmentService } from './integrations/environment/environment.service';
|
import { EnvironmentService } from './integrations/environment/environment.service';
|
||||||
import {
|
import {
|
||||||
JwtAuthStrategy,
|
JwtAuthStrategy,
|
||||||
JwtPayload,
|
JwtPayload,
|
||||||
} from './core/auth/strategies/jwt.auth.strategy';
|
} from './core/auth/strategies/jwt.auth.strategy';
|
||||||
|
import { TenantService } from './tenant/tenant.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -37,7 +37,7 @@ import {
|
|||||||
conditionalSchema: async (request) => {
|
conditionalSchema: async (request) => {
|
||||||
try {
|
try {
|
||||||
// Get the SchemaGenerationService from the AppModule
|
// Get the SchemaGenerationService from the AppModule
|
||||||
const service = AppModule.moduleRef.get(SchemaGenerationService, {
|
const tenantService = AppModule.moduleRef.get(TenantService, {
|
||||||
strict: false,
|
strict: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -57,8 +57,8 @@ import {
|
|||||||
// Extract JWT from the request
|
// Extract JWT from the request
|
||||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request.req);
|
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request.req);
|
||||||
|
|
||||||
// If there is no token, return an empty schema
|
// If there is no token or flexible backend is disabled, return an empty schema
|
||||||
if (!token) {
|
if (!token || !environmentService.isFlexibleBackendEnabled()) {
|
||||||
return new GraphQLSchema({});
|
return new GraphQLSchema({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +73,9 @@ import {
|
|||||||
decoded as JwtPayload,
|
decoded as JwtPayload,
|
||||||
);
|
);
|
||||||
|
|
||||||
const conditionalSchema = await service.generateSchema(workspace.id);
|
const conditionalSchema = await tenantService.createTenantSchema(
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
return conditionalSchema;
|
return conditionalSchema;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
|
||||||
|
|
||||||
import { EntityResolverService } from './entity-resolver.service';
|
import { EntityResolverService } from './entity-resolver.service';
|
||||||
|
|
||||||
@ -16,10 +15,6 @@ describe('EntityResolverService', () => {
|
|||||||
provide: DataSourceService,
|
provide: DataSourceService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: EnvironmentService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -1,295 +1,84 @@
|
|||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { GraphQLResolveInfo } from 'graphql';
|
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 { 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';
|
import { PGGraphQLQueryRunner } from './utils/pg-graphql-query-runner.util';
|
||||||
|
|
||||||
function stringify(obj: any) {
|
|
||||||
const jsonString = JSON.stringify(obj);
|
|
||||||
const jsonWithoutQuotes = jsonString.replace(/"(\w+)"\s*:/g, '$1:');
|
|
||||||
return jsonWithoutQuotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EntityResolverService {
|
export class EntityResolverService {
|
||||||
constructor(
|
constructor(private readonly dataSourceService: DataSourceService) {}
|
||||||
private readonly dataSourceService: DataSourceService,
|
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async findAll(
|
async findMany(context: SchemaBuilderContext, info: GraphQLResolveInfo) {
|
||||||
entityName: string,
|
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
|
||||||
tableName: string,
|
entityName: context.entityName,
|
||||||
workspaceId: string,
|
tableName: context.tableName,
|
||||||
info: GraphQLResolveInfo,
|
workspaceId: context.workspaceId,
|
||||||
fieldAliases: Record<string, string>,
|
|
||||||
) {
|
|
||||||
if (!this.environmentService.isFlexibleBackendEnabled()) {
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceDataSource =
|
|
||||||
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
|
|
||||||
|
|
||||||
const graphqlQuery = await this.prepareGrapQLQuery(
|
|
||||||
workspaceId,
|
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
fieldAliases: context.fieldAliases,
|
||||||
);
|
});
|
||||||
|
|
||||||
/* TODO: This is a temporary solution to set the schema before each raw query.
|
return runner.findMany();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(
|
async findOne(
|
||||||
entityName: string,
|
|
||||||
tableName: string,
|
|
||||||
args: { id: string },
|
args: { id: string },
|
||||||
workspaceId: string,
|
context: SchemaBuilderContext,
|
||||||
info: GraphQLResolveInfo,
|
info: GraphQLResolveInfo,
|
||||||
fieldAliases: Record<string, string>,
|
|
||||||
) {
|
) {
|
||||||
if (!this.environmentService.isFlexibleBackendEnabled()) {
|
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
|
||||||
throw new ForbiddenException();
|
entityName: context.entityName,
|
||||||
}
|
tableName: context.tableName,
|
||||||
|
workspaceId: context.workspaceId,
|
||||||
const workspaceDataSource =
|
|
||||||
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
|
|
||||||
|
|
||||||
const graphqlQuery = await this.prepareGrapQLQuery(
|
|
||||||
workspaceId,
|
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
fieldAliases: context.fieldAliases,
|
||||||
);
|
});
|
||||||
|
|
||||||
await workspaceDataSource?.query(`
|
return runner.findOne(args);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOne(
|
async createOne(
|
||||||
entityName: string,
|
|
||||||
tableName: string,
|
|
||||||
args: { data: any },
|
args: { data: any },
|
||||||
workspaceId: string,
|
context: SchemaBuilderContext,
|
||||||
info: GraphQLResolveInfo,
|
info: GraphQLResolveInfo,
|
||||||
fieldAliases: Record<string, string>,
|
|
||||||
) {
|
) {
|
||||||
if (!this.environmentService.isFlexibleBackendEnabled()) {
|
const records = await this.createMany({ data: [args.data] }, context, info);
|
||||||
throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceDataSource =
|
return records?.[0];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMany(
|
async createMany(
|
||||||
entityName: string,
|
|
||||||
tableName: string,
|
|
||||||
args: { data: any[] },
|
args: { data: any[] },
|
||||||
workspaceId: string,
|
context: SchemaBuilderContext,
|
||||||
info: GraphQLResolveInfo,
|
info: GraphQLResolveInfo,
|
||||||
fieldAliases: Record<string, string>,
|
|
||||||
) {
|
) {
|
||||||
if (!this.environmentService.isFlexibleBackendEnabled()) {
|
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
|
||||||
throw new ForbiddenException();
|
entityName: context.entityName,
|
||||||
}
|
tableName: context.tableName,
|
||||||
|
workspaceId: context.workspaceId,
|
||||||
const workspaceDataSource =
|
|
||||||
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
|
|
||||||
|
|
||||||
const graphqlQuery = await this.prepareGrapQLQuery(
|
|
||||||
workspaceId,
|
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
fieldAliases: context.fieldAliases,
|
||||||
);
|
});
|
||||||
|
|
||||||
await workspaceDataSource?.query(`
|
return runner.createMany(args);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOne(
|
async updateOne(
|
||||||
entityName: string,
|
|
||||||
tableName: string,
|
|
||||||
args: { id: string; data: any },
|
args: { id: string; data: any },
|
||||||
workspaceId: string,
|
context: SchemaBuilderContext,
|
||||||
info: GraphQLResolveInfo,
|
info: GraphQLResolveInfo,
|
||||||
fieldAliases: Record<string, string>,
|
|
||||||
) {
|
) {
|
||||||
if (!this.environmentService.isFlexibleBackendEnabled()) {
|
const runner = new PGGraphQLQueryRunner(this.dataSourceService, {
|
||||||
throw new ForbiddenException();
|
entityName: context.entityName,
|
||||||
}
|
tableName: context.tableName,
|
||||||
|
workspaceId: context.workspaceId,
|
||||||
const workspaceDataSource =
|
|
||||||
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
|
|
||||||
|
|
||||||
const graphqlQuery = await this.prepareGrapQLQuery(
|
|
||||||
workspaceId,
|
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
fieldAliases: context.fieldAliases,
|
||||||
);
|
});
|
||||||
|
|
||||||
await workspaceDataSource?.query(`
|
return runner.updateOne(args);
|
||||||
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<string, string>,
|
|
||||||
): Promise<string> {
|
|
||||||
// Extract requested fields from GraphQL resolve info
|
|
||||||
const fields = graphqlFields(info);
|
|
||||||
|
|
||||||
await this.dataSourceService.createWorkspaceSchema(workspaceId);
|
|
||||||
|
|
||||||
const graphqlQuery = convertFieldsToGraphQL(fields, fieldAliases);
|
|
||||||
|
|
||||||
return graphqlQuery;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<any> {
|
||||||
|
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<any[]> {
|
||||||
|
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<any> {
|
||||||
|
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<any[]> {
|
||||||
|
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<any> {
|
||||||
|
const records = await this.createMany({ data: [args.data] });
|
||||||
|
|
||||||
|
return records?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOne(args: { id: string; data: any }): Promise<any> {
|
||||||
|
const query = this.queryBuilder.updateOne(args).build();
|
||||||
|
const result = await this.execute(query, this.options.workspaceId);
|
||||||
|
|
||||||
|
return this.parseResults(result, 'updateOne')?.records?.[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export const stringifyWithoutKeyQuote = (obj: any) => {
|
||||||
|
const jsonString = JSON.stringify(obj);
|
||||||
|
const jsonWithoutQuotes = jsonString.replace(/"(\w+)"\s*:/g, '$1:');
|
||||||
|
return jsonWithoutQuotes;
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
export interface SchemaBuilderContext {
|
||||||
|
entityName: string;
|
||||||
|
tableName: string;
|
||||||
|
workspaceId: string;
|
||||||
|
fieldAliases: Record<string, string>;
|
||||||
|
}
|
||||||
13
server/src/tenant/schema-builder/schema-builder.module.ts
Normal file
13
server/src/tenant/schema-builder/schema-builder.module.ts
Normal file
@ -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 {}
|
||||||
@ -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>(SchemaBuilderService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,43 +1,37 @@
|
|||||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
GraphQLFieldConfigMap,
|
||||||
GraphQLID,
|
GraphQLID,
|
||||||
GraphQLInputObjectType,
|
GraphQLInputObjectType,
|
||||||
GraphQLList,
|
GraphQLList,
|
||||||
GraphQLNonNull,
|
GraphQLNonNull,
|
||||||
GraphQLObjectType,
|
GraphQLObjectType,
|
||||||
GraphQLResolveInfo,
|
|
||||||
GraphQLSchema,
|
GraphQLSchema,
|
||||||
} from 'graphql';
|
} from 'graphql';
|
||||||
|
|
||||||
import { EntityResolverService } from 'src/tenant/entity-resolver/entity-resolver.service';
|
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 { 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 { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||||
|
|
||||||
import { generateEdgeType } from './graphql-types/edge.graphql-type';
|
import { generateEdgeType } from './utils/generate-edge-type.util';
|
||||||
import { generateConnectionType } from './graphql-types/connection.graphql-type';
|
import { generateConnectionType } from './utils/generate-connection-type.util';
|
||||||
import {
|
import { generateObjectType } from './utils/generate-object-type.util';
|
||||||
generateCreateInputType,
|
import { generateCreateInputType } from './utils/generate-create-input-type.util';
|
||||||
generateObjectType,
|
import { generateUpdateInputType } from './utils/generate-update-input-type.util';
|
||||||
generateUpdateInputType,
|
import { SchemaBuilderContext } from './interfaces/schema-builder-context.interface';
|
||||||
} from './graphql-types/object.graphql-type';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchemaGenerationService {
|
export class SchemaBuilderService {
|
||||||
constructor(
|
workspaceId: string;
|
||||||
private readonly dataSourceMetadataService: DataSourceMetadataService,
|
|
||||||
private readonly objectMetadataService: ObjectMetadataService,
|
constructor(private readonly entityResolverService: EntityResolverService) {}
|
||||||
private readonly entityResolverService: EntityResolverService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private generateQueryFieldForEntity(
|
private generateQueryFieldForEntity(
|
||||||
entityName: string,
|
entityName: string,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
ObjectType: GraphQLObjectType,
|
ObjectType: GraphQLObjectType,
|
||||||
objectDefinition: ObjectMetadata,
|
objectDefinition: ObjectMetadata,
|
||||||
workspaceId: string,
|
|
||||||
) {
|
) {
|
||||||
const fieldAliases =
|
const fieldAliases =
|
||||||
objectDefinition?.fields.reduce(
|
objectDefinition?.fields.reduce(
|
||||||
@ -47,6 +41,12 @@ export class SchemaGenerationService {
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
) || {};
|
) || {};
|
||||||
|
const schemaBuilderContext: SchemaBuilderContext = {
|
||||||
|
entityName,
|
||||||
|
tableName,
|
||||||
|
workspaceId: this.workspaceId,
|
||||||
|
fieldAliases,
|
||||||
|
};
|
||||||
|
|
||||||
const EdgeType = generateEdgeType(ObjectType);
|
const EdgeType = generateEdgeType(ObjectType);
|
||||||
const ConnectionType = generateConnectionType(EdgeType);
|
const ConnectionType = generateConnectionType(EdgeType);
|
||||||
@ -54,13 +54,10 @@ export class SchemaGenerationService {
|
|||||||
return {
|
return {
|
||||||
[`findMany${pascalCase(entityName)}`]: {
|
[`findMany${pascalCase(entityName)}`]: {
|
||||||
type: ConnectionType,
|
type: ConnectionType,
|
||||||
resolve: async (root, args, context, info: GraphQLResolveInfo) => {
|
resolve: async (root, args, context, info) => {
|
||||||
return this.entityResolverService.findAll(
|
return this.entityResolverService.findMany(
|
||||||
entityName,
|
schemaBuilderContext,
|
||||||
tableName,
|
|
||||||
workspaceId,
|
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -71,16 +68,13 @@ export class SchemaGenerationService {
|
|||||||
},
|
},
|
||||||
resolve: (root, args, context, info) => {
|
resolve: (root, args, context, info) => {
|
||||||
return this.entityResolverService.findOne(
|
return this.entityResolverService.findOne(
|
||||||
entityName,
|
|
||||||
tableName,
|
|
||||||
args,
|
args,
|
||||||
workspaceId,
|
schemaBuilderContext,
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as GraphQLFieldConfigMap<any, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateMutationFieldForEntity(
|
private generateMutationFieldForEntity(
|
||||||
@ -90,7 +84,6 @@ export class SchemaGenerationService {
|
|||||||
CreateInputType: GraphQLInputObjectType,
|
CreateInputType: GraphQLInputObjectType,
|
||||||
UpdateInputType: GraphQLInputObjectType,
|
UpdateInputType: GraphQLInputObjectType,
|
||||||
objectDefinition: ObjectMetadata,
|
objectDefinition: ObjectMetadata,
|
||||||
workspaceId: string,
|
|
||||||
) {
|
) {
|
||||||
const fieldAliases =
|
const fieldAliases =
|
||||||
objectDefinition?.fields.reduce(
|
objectDefinition?.fields.reduce(
|
||||||
@ -100,6 +93,12 @@ export class SchemaGenerationService {
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
) || {};
|
) || {};
|
||||||
|
const schemaBuilderContext: SchemaBuilderContext = {
|
||||||
|
entityName,
|
||||||
|
tableName,
|
||||||
|
workspaceId: this.workspaceId,
|
||||||
|
fieldAliases,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[`createOne${pascalCase(entityName)}`]: {
|
[`createOne${pascalCase(entityName)}`]: {
|
||||||
@ -109,12 +108,9 @@ export class SchemaGenerationService {
|
|||||||
},
|
},
|
||||||
resolve: (root, args, context, info) => {
|
resolve: (root, args, context, info) => {
|
||||||
return this.entityResolverService.createOne(
|
return this.entityResolverService.createOne(
|
||||||
entityName,
|
|
||||||
tableName,
|
|
||||||
args,
|
args,
|
||||||
workspaceId,
|
schemaBuilderContext,
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -129,12 +125,9 @@ export class SchemaGenerationService {
|
|||||||
},
|
},
|
||||||
resolve: (root, args, context, info) => {
|
resolve: (root, args, context, info) => {
|
||||||
return this.entityResolverService.createMany(
|
return this.entityResolverService.createMany(
|
||||||
entityName,
|
|
||||||
tableName,
|
|
||||||
args,
|
args,
|
||||||
workspaceId,
|
schemaBuilderContext,
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -146,22 +139,19 @@ export class SchemaGenerationService {
|
|||||||
},
|
},
|
||||||
resolve: (root, args, context, info) => {
|
resolve: (root, args, context, info) => {
|
||||||
return this.entityResolverService.updateOne(
|
return this.entityResolverService.updateOne(
|
||||||
entityName,
|
|
||||||
tableName,
|
|
||||||
args,
|
args,
|
||||||
workspaceId,
|
schemaBuilderContext,
|
||||||
info,
|
info,
|
||||||
fieldAliases,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as GraphQLFieldConfigMap<any, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateQueryAndMutationTypes(
|
private generateQueryAndMutationTypes(objectMetadata: ObjectMetadata[]): {
|
||||||
objectMetadata: ObjectMetadata[],
|
query: GraphQLObjectType;
|
||||||
workspaceId: string,
|
mutation: GraphQLObjectType;
|
||||||
): { query: GraphQLObjectType; mutation: GraphQLObjectType } {
|
} {
|
||||||
const queryFields: any = {};
|
const queryFields: any = {};
|
||||||
const mutationFields: any = {};
|
const mutationFields: any = {};
|
||||||
|
|
||||||
@ -191,7 +181,6 @@ export class SchemaGenerationService {
|
|||||||
tableName,
|
tableName,
|
||||||
ObjectType,
|
ObjectType,
|
||||||
objectDefinition,
|
objectDefinition,
|
||||||
workspaceId,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -204,7 +193,6 @@ export class SchemaGenerationService {
|
|||||||
CreateInputType,
|
CreateInputType,
|
||||||
UpdateInputType,
|
UpdateInputType,
|
||||||
objectDefinition,
|
objectDefinition,
|
||||||
workspaceId,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -222,33 +210,13 @@ export class SchemaGenerationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateSchema(
|
async generateSchema(
|
||||||
workspaceId: string | undefined,
|
workspaceId: string,
|
||||||
|
objectMetadata: ObjectMetadata[],
|
||||||
): Promise<GraphQLSchema> {
|
): Promise<GraphQLSchema> {
|
||||||
if (!workspaceId) {
|
this.workspaceId = workspaceId;
|
||||||
return new GraphQLSchema({});
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataSourcesMetadata =
|
const { query, mutation } =
|
||||||
await this.dataSourceMetadataService.getDataSourcesMetadataFromWorkspaceId(
|
this.generateQueryAndMutationTypes(objectMetadata);
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
return new GraphQLSchema({
|
return new GraphQLSchema({
|
||||||
query,
|
query,
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql';
|
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.
|
* Generate a GraphQL connection type based on the EdgeType.
|
||||||
@ -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<string, any> = {};
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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 = <TSource = any, TContext = any>(
|
||||||
|
name: string,
|
||||||
|
columns: FieldMetadata[],
|
||||||
|
): GraphQLObjectType<TSource, TContext> => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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<string, any> = {};
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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 = <TSource = any, TContext = any>(
|
|
||||||
name: string,
|
|
||||||
columns: FieldMetadata[],
|
|
||||||
): GraphQLObjectType<TSource, TContext> => {
|
|
||||||
const fields: Record<string, any> = {
|
|
||||||
// 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<string, any> = {};
|
|
||||||
|
|
||||||
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<string, any> = {};
|
|
||||||
|
|
||||||
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<string, GraphQLObjectType> = {};
|
|
||||||
|
|
||||||
for (const object of objectMetadata) {
|
|
||||||
const ObjectType = generateObjectType(object.displayName, object.fields);
|
|
||||||
|
|
||||||
objectTypes[object.displayName] = ObjectType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return objectTypes;
|
|
||||||
};
|
|
||||||
@ -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 {}
|
|
||||||
@ -1,11 +1,21 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { MetadataModule } from 'src/metadata/metadata.module';
|
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 { TenantService } from './tenant.service';
|
||||||
import { SchemaGenerationModule } from './schema-generation/schema-generation.module';
|
|
||||||
|
import { SchemaBuilderModule } from './schema-builder/schema-builder.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MetadataModule, UniversalModule, SchemaGenerationModule],
|
imports: [
|
||||||
|
MetadataModule,
|
||||||
|
SchemaBuilderModule,
|
||||||
|
DataSourceMetadataModule,
|
||||||
|
ObjectMetadataModule,
|
||||||
|
],
|
||||||
|
providers: [TenantService],
|
||||||
|
exports: [TenantService],
|
||||||
})
|
})
|
||||||
export class TenantModule {}
|
export class TenantModule {}
|
||||||
|
|||||||
@ -2,17 +2,22 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
|
|
||||||
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
||||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-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', () => {
|
import { SchemaBuilderService } from './schema-builder/schema-builder.service';
|
||||||
let service: SchemaGenerationService;
|
|
||||||
|
describe('TenantService', () => {
|
||||||
|
let service: TenantService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
SchemaGenerationService,
|
TenantService,
|
||||||
|
{
|
||||||
|
provide: SchemaBuilderService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: DataSourceMetadataService,
|
provide: DataSourceMetadataService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
@ -21,14 +26,10 @@ describe('SchemaGenerationService', () => {
|
|||||||
provide: ObjectMetadataService,
|
provide: ObjectMetadataService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: EntityResolverService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<SchemaGenerationService>(SchemaGenerationService);
|
service = module.get<TenantService>(TenantService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
45
server/src/tenant/tenant.service.ts
Normal file
45
server/src/tenant/tenant.service.ts
Normal file
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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<string, keyof typeof TypeORMSortOrder>;
|
|
||||||
|
|
||||||
@Field(() => TypeORMSortOrder, { nullable: true })
|
|
||||||
createdAt?: keyof typeof TypeORMSortOrder;
|
|
||||||
|
|
||||||
@Field(() => TypeORMSortOrder, { nullable: true })
|
|
||||||
updatedAt?: keyof typeof TypeORMSortOrder;
|
|
||||||
}
|
|
||||||
@ -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<string, unknown>;
|
|
||||||
|
|
||||||
@Field(() => Date, { nullable: true })
|
|
||||||
createdAt?: Date;
|
|
||||||
|
|
||||||
@Field(() => Date, { nullable: true })
|
|
||||||
updatedAt?: Date;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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<string, unknown>;
|
|
||||||
|
|
||||||
@Field(() => Date, { nullable: false })
|
|
||||||
createdAt!: Date;
|
|
||||||
|
|
||||||
@Field(() => Date, { nullable: false })
|
|
||||||
updatedAt!: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class PaginatedUniversalEntity extends Paginated<UniversalEntity>(
|
|
||||||
UniversalEntity,
|
|
||||||
) {}
|
|
||||||
@ -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 {}
|
|
||||||
@ -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>(UniversalResolver);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(resolver).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<string, TypeORMSortOrder> = {};
|
|
||||||
|
|
||||||
for (const key of Object.keys(flattenWhere)) {
|
|
||||||
orderByClause[`${entity}.${snakeCase(key)}`] = flattenWhere[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderByClause;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user