feat: add custom object create and update (#1869)
This commit is contained in:
@ -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;
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user