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:
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user