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:
Jérémy M
2023-10-10 10:50:54 +02:00
committed by GitHub
parent 18c8f26f38
commit 017a0b1563
33 changed files with 588 additions and 770 deletions
@@ -0,0 +1,23 @@
import isEmpty from 'lodash.isempty';
export const convertFieldsToGraphQL = (
fields: any,
fieldAliases: Record<string, string>,
acc = '',
) => {
for (const [key, value] of Object.entries(fields)) {
if (value && !isEmpty(value)) {
acc += `${key} {\n`;
acc = convertFieldsToGraphQL(value, fieldAliases, acc);
acc += `}\n`;
} else {
if (fieldAliases[key]) {
acc += `${key}: ${fieldAliases[key]}\n`;
} else {
acc += `${key}\n`;
}
}
}
return acc;
};
@@ -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;
};