feat: add custom object create and update (#1869)

This commit is contained in:
Jérémy M
2023-10-05 14:33:13 +02:00
committed by GitHub
parent b2dd868046
commit 047bb8014b
3 changed files with 361 additions and 25 deletions

View File

@ -6,12 +6,20 @@ import {
import { GraphQLResolveInfo } from 'graphql'; import { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields'; import graphqlFields from 'graphql-fields';
import { v4 as uuidv4 } from 'uuid';
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 { EnvironmentService } from 'src/integrations/environment/environment.service';
import { pascalCase } from 'src/utils/pascal-case';
import { convertFieldsToGraphQL } from './entity-resolver.util'; 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() @Injectable()
export class EntityResolverService { export class EntityResolverService {
constructor( constructor(
@ -49,7 +57,7 @@ export class EntityResolverService {
const graphqlResult = await workspaceDataSource?.query(` const graphqlResult = await workspaceDataSource?.query(`
SELECT graphql.resolve($$ SELECT graphql.resolve($$
{ {
${entityName}Collection: ${tableName}Collection { findAll${pascalCase(entityName)}: ${tableName}Collection {
${graphqlQuery} ${graphqlQuery}
} }
} }
@ -57,7 +65,7 @@ export class EntityResolverService {
`); `);
const result = const result =
graphqlResult?.[0]?.resolve?.data?.[`${entityName}Collection`]; graphqlResult?.[0]?.resolve?.data?.[`findAll${pascalCase(entityName)}`];
if (!result) { if (!result) {
throw new BadRequestException('Malformed result from GraphQL query'); throw new BadRequestException('Malformed result from GraphQL query');
@ -93,7 +101,9 @@ export class EntityResolverService {
const graphqlResult = await workspaceDataSource?.query(` const graphqlResult = await workspaceDataSource?.query(`
SELECT graphql.resolve($$ SELECT graphql.resolve($$
{ {
${entityName}Collection: : ${tableName}Collection(filter: { id: { eq: "${args.id}" } }) { findOne${pascalCase(
entityName,
)}: ${tableName}Collection(filter: { id: { eq: "${args.id}" } }) {
${graphqlQuery} ${graphqlQuery}
} }
} }
@ -101,10 +111,168 @@ export class EntityResolverService {
`); `);
const result = const result =
graphqlResult?.[0]?.resolve?.data?.[`${entityName}Collection`]; graphqlResult?.[0]?.resolve?.data?.[`findOne${pascalCase(entityName)}`];
if (!result) { if (!result) {
return null; 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; return result;

View File

@ -2,6 +2,7 @@ import {
GraphQLBoolean, GraphQLBoolean,
GraphQLEnumType, GraphQLEnumType,
GraphQLID, GraphQLID,
GraphQLInputObjectType,
GraphQLInt, GraphQLInt,
GraphQLNonNull, GraphQLNonNull,
GraphQLObjectType, GraphQLObjectType,
@ -51,6 +52,7 @@ const mapColumnTypeToGraphQLType = (column: FieldMetadata): any => {
* Generate a GraphQL object type based on the name and columns. * Generate a GraphQL object type based on the name and columns.
* @param name Name for the GraphQL object. * @param name Name for the GraphQL object.
* @param columns Array of FieldMetadata columns. * @param columns Array of FieldMetadata columns.
* @returns GraphQLObjectType
*/ */
export const generateObjectType = <TSource = any, TContext = any>( export const generateObjectType = <TSource = any, TContext = any>(
name: string, name: string,
@ -82,6 +84,64 @@ export const generateObjectType = <TSource = any, TContext = any>(
}); });
}; };
/**
* 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. * Generate multiple GraphQL object types based on an array of object metadata.
* @param objectMetadata Array of ObjectMetadata. * @param objectMetadata Array of ObjectMetadata.

View File

@ -2,6 +2,8 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { import {
GraphQLID, GraphQLID,
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull, GraphQLNonNull,
GraphQLObjectType, GraphQLObjectType,
GraphQLResolveInfo, GraphQLResolveInfo,
@ -16,7 +18,11 @@ import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.ent
import { generateEdgeType } from './graphql-types/edge.graphql-type'; import { generateEdgeType } from './graphql-types/edge.graphql-type';
import { generateConnectionType } from './graphql-types/connection.graphql-type'; import { generateConnectionType } from './graphql-types/connection.graphql-type';
import { generateObjectTypes } from './graphql-types/object.graphql-type'; import {
generateCreateInputType,
generateObjectType,
generateUpdateInputType,
} from './graphql-types/object.graphql-type';
@Injectable() @Injectable()
export class SchemaGenerationService { export class SchemaGenerationService {
@ -46,7 +52,7 @@ export class SchemaGenerationService {
const ConnectionType = generateConnectionType(EdgeType); const ConnectionType = generateConnectionType(EdgeType);
return { return {
[`findAll${pascalCase(entityName)}`]: { [`findMany${pascalCase(entityName)}`]: {
type: ConnectionType, type: ConnectionType,
resolve: async (root, args, context, info: GraphQLResolveInfo) => { resolve: async (root, args, context, info: GraphQLResolveInfo) => {
return this.entityResolverService.findAll( return this.entityResolverService.findAll(
@ -77,39 +83,142 @@ export class SchemaGenerationService {
}; };
} }
private generateQueryType( private generateMutationFieldForEntity(
ObjectTypes: Record<string, GraphQLObjectType>, entityName: string,
tableName: string,
ObjectType: GraphQLObjectType,
CreateInputType: GraphQLInputObjectType,
UpdateInputType: GraphQLInputObjectType,
objectDefinition: ObjectMetadata,
workspaceId: string,
) {
const fieldAliases =
objectDefinition?.fields.reduce(
(acc, field) => ({
...acc,
[field.displayName]: field.targetColumnName,
}),
{},
) || {};
return {
[`createOne${pascalCase(entityName)}`]: {
type: new GraphQLNonNull(ObjectType),
args: {
data: { type: new GraphQLNonNull(CreateInputType) },
},
resolve: (root, args, context, info) => {
return this.entityResolverService.createOne(
entityName,
tableName,
args,
workspaceId,
info,
fieldAliases,
);
},
},
[`createMany${pascalCase(entityName)}`]: {
type: new GraphQLList(ObjectType),
args: {
data: {
type: new GraphQLNonNull(
new GraphQLList(new GraphQLNonNull(CreateInputType)),
),
},
},
resolve: (root, args, context, info) => {
return this.entityResolverService.createMany(
entityName,
tableName,
args,
workspaceId,
info,
fieldAliases,
);
},
},
[`updateOne${pascalCase(entityName)}`]: {
type: new GraphQLNonNull(ObjectType),
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
data: { type: new GraphQLNonNull(UpdateInputType) },
},
resolve: (root, args, context, info) => {
return this.entityResolverService.updateOne(
entityName,
tableName,
args,
workspaceId,
info,
fieldAliases,
);
},
},
};
}
private generateQueryAndMutationTypes(
objectMetadata: ObjectMetadata[], objectMetadata: ObjectMetadata[],
workspaceId: string, workspaceId: string,
): GraphQLObjectType { ): { query: GraphQLObjectType; mutation: GraphQLObjectType } {
const fields: any = {}; const queryFields: any = {};
const mutationFields: any = {};
for (const [entityName, ObjectType] of Object.entries(ObjectTypes)) { for (const objectDefinition of objectMetadata) {
const objectDefinition = objectMetadata.find(
(object) => object.displayName === entityName,
);
const tableName = objectDefinition?.targetTableName ?? ''; const tableName = objectDefinition?.targetTableName ?? '';
const ObjectType = generateObjectType(
objectDefinition.displayName,
objectDefinition.fields,
);
const CreateInputType = generateCreateInputType(
objectDefinition.displayName,
objectDefinition.fields,
);
const UpdateInputType = generateUpdateInputType(
objectDefinition.displayName,
objectDefinition.fields,
);
if (!objectDefinition) { if (!objectDefinition) {
throw new InternalServerErrorException('Object definition not found'); throw new InternalServerErrorException('Object definition not found');
} }
Object.assign( Object.assign(
fields, queryFields,
this.generateQueryFieldForEntity( this.generateQueryFieldForEntity(
entityName, objectDefinition.displayName,
tableName, tableName,
ObjectType, ObjectType,
objectDefinition, objectDefinition,
workspaceId, workspaceId,
), ),
); );
Object.assign(
mutationFields,
this.generateMutationFieldForEntity(
objectDefinition.displayName,
tableName,
ObjectType,
CreateInputType,
UpdateInputType,
objectDefinition,
workspaceId,
),
);
} }
return new GraphQLObjectType({ return {
name: 'Query', query: new GraphQLObjectType({
fields, name: 'Query',
}); fields: queryFields,
}),
mutation: new GraphQLObjectType({
name: 'Mutation',
fields: mutationFields,
}),
};
} }
async generateSchema( async generateSchema(
@ -136,15 +245,14 @@ export class SchemaGenerationService {
dataSourceMetadata.id, dataSourceMetadata.id,
); );
const ObjectTypes = generateObjectTypes(objectMetadata); const { query, mutation } = this.generateQueryAndMutationTypes(
const QueryType = this.generateQueryType(
ObjectTypes,
objectMetadata, objectMetadata,
workspaceId, workspaceId,
); );
return new GraphQLSchema({ return new GraphQLSchema({
query: QueryType, query,
mutation,
}); });
} }
} }