Files
twenty/server/src/tenant/entity-resolver/entity-resolver.service.ts
T

296 lines
7.8 KiB
TypeScript

import {
BadRequestException,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields';
import { v4 as uuidv4 } from 'uuid';
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;
}
@Injectable()
export class EntityResolverService {
constructor(
private readonly dataSourceService: DataSourceService,
private readonly environmentService: EnvironmentService,
) {}
async findAll(
entityName: string,
tableName: string,
workspaceId: string,
info: GraphQLResolveInfo,
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,
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;
}
async findOne(
entityName: string,
tableName: string,
args: { id: string },
workspaceId: string,
info: GraphQLResolveInfo,
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,
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;
}
async createOne(
entityName: string,
tableName: string,
args: { data: any },
workspaceId: string,
info: GraphQLResolveInfo,
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,
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(
entityName: string,
tableName: string,
args: { data: any[] },
workspaceId: string,
info: GraphQLResolveInfo,
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,
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;
}
async updateOne(
entityName: string,
tableName: string,
args: { id: string; data: any },
workspaceId: string,
info: GraphQLResolveInfo,
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,
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<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;
}
}