feat: dynamic graphQL schema generation based on user workspace (#1725)
* wip: refacto and start creating custom resolver * feat: findMany & findUnique of a custom entity * feat: wip pagination * feat: initial metadata migration * feat: universal findAll with pagination * fix: clean small stuff in pagination * fix: test * fix: miss file * feat: rename custom into universal * feat: create metadata schema in default database * Multi-tenant db schemas POC fix tests and use query builders remove synchronize restore updatedAt remove unnecessary import use queryRunner fix camelcase add migrations for standard objects Multi-tenant db schemas POC fix tests and use query builders remove synchronize restore updatedAt remove unnecessary import use queryRunner fix camelcase add migrations for standard objects poc: conditional schema at runtime wip: try to create resolver in Nest.JS context fix * feat: wip add pg_graphql * feat: setup pg_graphql during database init * wip: dynamic resolver * poc: dynamic resolver and query using pg_graphql * feat: pg_graphql use ARG in Dockerfile * feat: clean findMany & findOne dynamic resolver * feat: get correct schema based on access token * fix: remove old file * fix: tests * fix: better comment * fix: e2e test not working, error format change due to yoga * remove typeorm entity generation + fix jwt + fix search_path + remove anon * fix conflict --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: corentin <corentin@twenty.com>
This commit is contained in:
111
server/src/tenant/entity-resolver/entity-resolver.service.ts
Normal file
111
server/src/tenant/entity-resolver/entity-resolver.service.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { DataSourceService } from 'src/tenant/metadata/data-source/data-source.service';
|
||||
|
||||
import { convertFieldsToGraphQL } from './entity-resolver.util';
|
||||
|
||||
@Injectable()
|
||||
export class EntityResolverService {
|
||||
constructor(private readonly dataSourceService: DataSourceService) {}
|
||||
|
||||
async findAll(
|
||||
entityName: string,
|
||||
tableName: string,
|
||||
workspaceId: string,
|
||||
info: GraphQLResolveInfo,
|
||||
fieldAliases: Record<string, string>,
|
||||
) {
|
||||
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($$
|
||||
{
|
||||
${entityName}Collection: ${tableName}Collection {
|
||||
${graphqlQuery}
|
||||
}
|
||||
}
|
||||
$$);
|
||||
`);
|
||||
|
||||
const result =
|
||||
graphqlResult?.[0]?.resolve?.data?.[`${entityName}Collection`];
|
||||
|
||||
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>,
|
||||
) {
|
||||
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($$
|
||||
{
|
||||
${entityName}Collection: : ${tableName}Collection(filter: { id: { eq: "${args.id}" } }) {
|
||||
${graphqlQuery}
|
||||
}
|
||||
}
|
||||
$$);
|
||||
`);
|
||||
|
||||
const result =
|
||||
graphqlResult?.[0]?.resolve?.data?.[`${entityName}Collection`];
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user