cleanup pg_graphql #1 (#7673)

## Context
This PR removes workspace-query-runner/builder in preparation for fully
deprecating pg_graphql

next steps: Remove from the setup and make a command to remove comments
on schema/tables related to pg_graphql
This commit is contained in:
Weiko
2024-10-14 14:19:13 +02:00
committed by GitHub
parent a64635a9db
commit efba3b14be
34 changed files with 138 additions and 2790 deletions

View File

@ -1,119 +0,0 @@
import { TestingModule, Test } from '@nestjs/testing';
import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory';
import { ArgsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-string.factory';
describe('ArgsStringFactory', () => {
let service: ArgsStringFactory;
const argsAliasCreate = jest.fn();
beforeEach(async () => {
jest.resetAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [
ArgsStringFactory,
{
provide: ArgsAliasFactory,
useValue: {
create: argsAliasCreate,
},
},
],
}).compile();
service = module.get<ArgsStringFactory>(ArgsStringFactory);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should return null when args are missing', () => {
const args = undefined;
const result = service.create(args, []);
expect(result).toBeNull();
});
it('should return a string with the args when args are present', () => {
const args = {
id: '1',
name: 'field_name',
};
argsAliasCreate.mockReturnValue(args);
const result = service.create(args, []);
expect(result).toEqual('id: "1", name: "field_name"');
});
it('should return a string with the args when args are present and the value is an object', () => {
const args = {
id: '1',
name: {
firstName: 'test',
},
};
argsAliasCreate.mockReturnValue(args);
const result = service.create(args, []);
expect(result).toEqual('id: "1", name: {firstName:"test"}');
});
it('when orderBy is present, should return an array of objects', () => {
const args = {
orderBy: [{ id: 'AscNullsFirst' }, { name: 'AscNullsFirst' }],
};
argsAliasCreate.mockReturnValue(args);
const result = service.create(args, []);
expect(result).toEqual(
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}]',
);
});
it('when orderBy is present with position criteria, should return position at the end of the list', () => {
const args = {
orderBy: [
{ position: 'AscNullsFirst' },
{ id: 'AscNullsFirst' },
{ name: 'AscNullsFirst' },
],
};
argsAliasCreate.mockReturnValue(args);
const result = service.create(args, []);
expect(result).toEqual(
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]',
);
});
it('when orderBy is present with position in the middle, should return position at the end of the list', () => {
const args = {
orderBy: [
{ id: 'AscNullsFirst' },
{ position: 'AscNullsFirst' },
{ name: 'AscNullsFirst' },
],
};
argsAliasCreate.mockReturnValue(args);
const result = service.create(args, []);
expect(result).toEqual(
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]',
);
});
});
});

View File

@ -1,92 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
@Injectable()
export class ArgsAliasFactory {
private readonly logger = new Logger(ArgsAliasFactory.name);
create(
args: Record<string, any>,
fieldMetadataCollection: FieldMetadataInterface[],
): Record<string, any> {
const fieldMetadataMap = new Map(
fieldMetadataCollection.map((fieldMetadata) => [
fieldMetadata.name,
fieldMetadata,
]),
);
return this.createArgsObjectRecursive(args, fieldMetadataMap);
}
private createArgsObjectRecursive(
args: Record<string, any>,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
) {
// If it's not an object, we don't need to do anything
if (typeof args !== 'object' || args === null) {
return args;
}
// If it's an array, we need to map all items
if (Array.isArray(args)) {
return args.map((arg) =>
this.createArgsObjectRecursive(arg, fieldMetadataMap),
);
}
const newArgs = {};
for (const [key, value] of Object.entries(args)) {
const fieldMetadata = fieldMetadataMap.get(key);
// If it's a composite type, we need to transform args to properly map column name
if (
fieldMetadata &&
value !== null &&
isCompositeFieldMetadataType(fieldMetadata.type)
) {
// Get composite type definition
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) {
this.logger.error(
`Composite type definition not found for type: ${fieldMetadata.type}`,
);
throw new Error(
`Composite type definition not found for type: ${fieldMetadata.type}`,
);
}
// Loop through sub values and map them to composite property
for (const [subKey, subValue] of Object.entries(value)) {
// Find composite property
const compositeProperty = compositeType.properties.find(
(property) => property.name === subKey,
);
if (compositeProperty) {
const columnName = computeCompositeColumnName(
fieldMetadata,
compositeProperty,
);
newArgs[columnName] = subValue;
}
}
} else if (fieldMetadata) {
newArgs[key] = value;
} else {
// Recurse if value is a nested object, otherwise append field or alias
newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap);
}
}
return newArgs;
}
}

View File

@ -1,98 +0,0 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
import { isDefined } from 'src/utils/is-defined';
import { ArgsAliasFactory } from './args-alias.factory';
@Injectable()
export class ArgsStringFactory {
constructor(private readonly argsAliasFactory: ArgsAliasFactory) {}
create(
initialArgs: Record<string, any> | undefined,
fieldMetadataCollection: FieldMetadataInterface[],
softDeletable?: boolean,
): string | null {
if (!initialArgs) {
return null;
}
if (softDeletable) {
initialArgs.filter = {
and: [initialArgs.filter, { deletedAt: { is: 'NULL' } }].filter(
isDefined,
),
};
}
let argsString = '';
const computedArgs = this.argsAliasFactory.create(
initialArgs,
fieldMetadataCollection,
);
for (const key in computedArgs) {
// Check if the value is not undefined
if (computedArgs[key] === undefined) {
continue;
}
if (typeof computedArgs[key] === 'string') {
// If it's a string, add quotes
argsString += `${key}: "${computedArgs[key]}", `;
} else if (
typeof computedArgs[key] === 'object' &&
computedArgs[key] !== null
) {
if (key === 'orderBy') {
argsString += `${key}: ${this.buildStringifiedOrderBy(
computedArgs[key],
)}, `;
} else {
// If it's an object (and not null), stringify it
argsString += `${key}: ${stringifyWithoutKeyQuote(
computedArgs[key],
)}, `;
}
} else {
// For other types (number, boolean), add as is
argsString += `${key}: ${computedArgs[key]}, `;
}
}
// Remove trailing comma and space, if present
if (argsString.endsWith(', ')) {
argsString = argsString.slice(0, -2);
}
return argsString;
}
private buildStringifiedOrderBy(
keyValuePairArray: Array<Record<string, any>>,
): string {
if (
keyValuePairArray.length !== 0 &&
Object.keys(keyValuePairArray[0]).length === 0
) {
return `[]`;
}
// if position argument is present we want to put it at the very last
let orderByString = keyValuePairArray
.sort((_, obj) => (Object.hasOwnProperty.call(obj, 'position') ? -1 : 0))
.map((obj) => {
const [key] = Object.keys(obj);
const value = obj[key];
return `{${key}: ${value}}`;
})
.join(', ');
if (orderByString.endsWith(', ')) {
orderByString = orderByString.slice(0, -2);
}
return `[${orderByString}]`;
}
}

View File

@ -1,55 +0,0 @@
import { Injectable } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { ArgsAliasFactory } from './args-alias.factory';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class CreateManyQueryFactory {
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsAliasFactory: ArgsAliasFactory,
) {}
async create<Record extends IRecord = IRecord>(
args: CreateManyResolverArgs<Partial<Record>>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
);
const computedArgsData = this.argsAliasFactory.create(
args.data,
options.fieldMetadataCollection,
);
return `
mutation {
insertInto${computeObjectTargetTable(
options.objectMetadataItem,
)}Collection(objects: ${stringifyWithoutKeyQuote(
computedArgsData.map((datum) => ({
id: uuidv4(),
...datum,
})),
)}) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -1,45 +0,0 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { FieldsStringFactory } from './fields-string.factory';
export interface DeleteManyQueryFactoryOptions
extends WorkspaceQueryBuilderOptions {
atMost?: number;
}
@Injectable()
export class DeleteManyQueryFactory {
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
async create(
args: DeleteManyResolverArgs,
options: DeleteManyQueryFactoryOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
);
return `
mutation {
deleteFrom${computeObjectTargetTable(
options.objectMetadataItem,
)}Collection(filter: ${stringifyWithoutKeyQuote(
args.filter,
)}, atMost: ${options.atMost ?? 1}) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -1,37 +0,0 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class DeleteOneQueryFactory {
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
async create(
args: DeleteOneResolverArgs,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
);
return `
mutation {
deleteFrom${computeObjectTargetTable(
options.objectMetadataItem,
)}Collection(filter: { id: { eq: "${args.id}" } }) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -1,34 +1,8 @@
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
import { ArgsAliasFactory } from './args-alias.factory';
import { ArgsStringFactory } from './args-string.factory';
import { CreateManyQueryFactory } from './create-many-query.factory';
import { DeleteManyQueryFactory } from './delete-many-query.factory';
import { DeleteOneQueryFactory } from './delete-one-query.factory';
import { FieldAliasFactory } from './field-alias.factory';
import { FieldsStringFactory } from './fields-string.factory';
import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory';
import { FindManyQueryFactory } from './find-many-query.factory';
import { FindOneQueryFactory } from './find-one-query.factory';
import { RecordPositionQueryFactory } from './record-position-query.factory';
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
import { UpdateManyQueryFactory } from './update-many-query.factory';
import { UpdateOneQueryFactory } from './update-one-query.factory';
export const workspaceQueryBuilderFactories = [
ArgsAliasFactory,
ArgsStringFactory,
RelationFieldAliasFactory,
CreateManyQueryFactory,
DeleteOneQueryFactory,
FieldAliasFactory,
FieldsStringFactory,
FindManyQueryFactory,
FindOneQueryFactory,
FindDuplicatesQueryFactory,
RecordPositionQueryFactory,
UpdateOneQueryFactory,
UpdateManyQueryFactory,
DeleteManyQueryFactory,
ForeignDataWrapperServerQueryFactory,
];

View File

@ -1,50 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { createCompositeFieldKey } from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import {
computeColumnName,
computeCompositeColumnName,
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
@Injectable()
export class FieldAliasFactory {
private readonly logger = new Logger(FieldAliasFactory.name);
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
// If it's not a composite field, we can just return the alias
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
const alias = computeColumnName(fieldMetadata);
return `${fieldKey}: ${alias}`;
}
// If it's a composite field, we need to get the definition
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) {
this.logger.error(
`Composite type not found for field metadata type: ${fieldMetadata.type}`,
);
throw new Error(
`Composite type not found for field metadata type: ${fieldMetadata.type}`,
);
}
return compositeType.properties
.map((property) => {
// Generate a prefixed key for the composite field, this will be computed when the query has ran
const compositeKey = createCompositeFieldKey(
fieldMetadata.name,
property.name,
);
const alias = computeCompositeColumnName(fieldMetadata, property);
return `${compositeKey}: ${alias}`;
})
.join('\n');
}
}

View File

@ -1,109 +0,0 @@
import { Injectable } from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields';
import isEmpty from 'lodash.isempty';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { FieldAliasFactory } from './field-alias.factory';
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
@Injectable()
export class FieldsStringFactory {
constructor(
private readonly fieldAliasFactory: FieldAliasFactory,
private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
) {}
async create(
info: GraphQLResolveInfo,
fieldMetadataCollection: FieldMetadataInterface[],
objectMetadataCollection: ObjectMetadataInterface[],
withSoftDeleted?: boolean,
): Promise<string> {
const selectedFields: Partial<Record> = graphqlFields(info);
const res = await this.createFieldsStringRecursive(
info,
selectedFields,
fieldMetadataCollection,
objectMetadataCollection,
withSoftDeleted ?? false,
);
return res;
}
async createFieldsStringRecursive(
info: GraphQLResolveInfo,
selectedFields: Partial<Record>,
fieldMetadataCollection: FieldMetadataInterface[],
objectMetadataCollection: ObjectMetadataInterface[],
withSoftDeleted: boolean,
accumulator = '',
): Promise<string> {
const fieldMetadataMap = new Map(
fieldMetadataCollection.map((metadata) => [metadata.name, metadata]),
);
for (const [fieldKey, fieldValue] of Object.entries(selectedFields)) {
let fieldAlias: string | null;
if (fieldMetadataMap.has(fieldKey)) {
// We're sure that the field exists in the map after this if condition
// ES6 should tackle that more properly
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fieldMetadata = fieldMetadataMap.get(fieldKey)!;
// If the field is a relation field, we need to create a special alias
if (isRelationFieldMetadataType(fieldMetadata.type)) {
const alias = await this.relationFieldAliasFactory.create(
fieldKey,
fieldValue,
fieldMetadata,
objectMetadataCollection,
info,
withSoftDeleted,
);
fieldAlias = alias;
} else {
// Otherwise we just need to create a simple alias
const alias = this.fieldAliasFactory.create(fieldKey, fieldMetadata);
fieldAlias = alias;
}
}
fieldAlias ??= fieldKey;
// Recurse if value is a nested object, otherwise append field or alias
if (
!fieldMetadataMap.has(fieldKey) &&
fieldValue &&
typeof fieldValue === 'object' &&
!isEmpty(fieldValue)
) {
accumulator += `${fieldKey} {\n`;
accumulator = await this.createFieldsStringRecursive(
info,
fieldValue,
fieldMetadataCollection,
objectMetadataCollection,
withSoftDeleted,
accumulator,
);
accumulator += `}\n`;
} else {
accumulator += `${fieldAlias}\n`;
}
}
return accumulator;
}
}

View File

@ -1,97 +0,0 @@
import { Injectable } from '@nestjs/common';
import isEmpty from 'lodash.isempty';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory';
import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class FindDuplicatesQueryFactory {
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsAliasFactory: ArgsAliasFactory,
private readonly duplicateService: DuplicateService,
) {}
async create(
args: FindDuplicatesResolverArgs,
options: WorkspaceQueryBuilderOptions,
existingRecords?: Record[],
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
);
if (existingRecords) {
const query = existingRecords.reduce((acc, record, index) => {
return (
acc + this.buildQuery(fieldsString, options, undefined, record, index)
);
}, '');
return `query {
${query}
}`;
}
const query = args.data?.reduce((acc, dataItem, index) => {
const argsData = this.argsAliasFactory.create(
dataItem ?? {},
options.fieldMetadataCollection,
);
return (
acc +
this.buildQuery(
fieldsString,
options,
argsData as Record,
undefined,
index,
)
);
}, '');
return `query {
${query}
}`;
}
buildQuery(
fieldsString: string,
options: WorkspaceQueryBuilderOptions,
data?: Record,
existingRecord?: Record,
index?: number,
) {
const duplicateCondition =
this.duplicateService.buildDuplicateConditionForGraphQL(
options.objectMetadataItem,
data ?? existingRecord,
existingRecord?.id,
);
const filters = stringifyWithoutKeyQuote(duplicateCondition);
return `${computeObjectTargetTable(
options.objectMetadataItem,
)}Collection${index}: ${computeObjectTargetTable(
options.objectMetadataItem,
)}Collection${
isEmpty(duplicateCondition?.or) ? '(first: 0)' : `(filter: ${filters})`
} {
${fieldsString}
}
`;
}
}

View File

@ -1,50 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { ArgsStringFactory } from './args-string.factory';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class FindManyQueryFactory {
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsStringFactory: ArgsStringFactory,
) {}
async create<
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
);
const argsString = this.argsStringFactory.create(
args,
options.fieldMetadataCollection,
!options.withSoftDeleted,
);
return `
query {
${computeObjectTargetTable(options.objectMetadataItem)}Collection${
argsString ? `(${argsString})` : ''
} {
${fieldsString}
}
}
`;
}
}

View File

@ -1,49 +0,0 @@
import { Injectable } from '@nestjs/common';
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { ArgsStringFactory } from './args-string.factory';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class FindOneQueryFactory {
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsStringFactory: ArgsStringFactory,
) {}
async create<Filter extends RecordFilter = RecordFilter>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
options.withSoftDeleted,
);
const argsString = this.argsStringFactory.create(
args,
options.fieldMetadataCollection,
!options.withSoftDeleted,
);
return `
query {
${computeObjectTargetTable(options.objectMetadataItem)}Collection${
argsString ? `(${argsString})` : ''
} {
edges {
node {
${fieldsString}
}
}
}
}
`;
}
}

View File

@ -1,152 +0,0 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { GraphQLResolveInfo } from 'graphql';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import {
deduceRelationDirection,
RelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { ArgsStringFactory } from './args-string.factory';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class RelationFieldAliasFactory {
constructor(
@Inject(forwardRef(() => FieldsStringFactory))
private readonly fieldsStringFactory: CircularDep<FieldsStringFactory>,
private readonly argsStringFactory: ArgsStringFactory,
) {}
create(
fieldKey: string,
fieldValue: any,
fieldMetadata: FieldMetadataInterface,
objectMetadataCollection: ObjectMetadataInterface[],
info: GraphQLResolveInfo,
withSoftDeleted?: boolean,
): Promise<string> {
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
throw new Error(`Field ${fieldMetadata.name} is not a relation field`);
}
return this.createRelationAlias(
fieldKey,
fieldValue,
fieldMetadata,
objectMetadataCollection,
info,
withSoftDeleted,
);
}
private async createRelationAlias(
fieldKey: string,
fieldValue: any,
fieldMetadata: FieldMetadataInterface,
objectMetadataCollection: ObjectMetadataInterface[],
info: GraphQLResolveInfo,
withSoftDeleted?: boolean,
): Promise<string> {
const relationMetadata =
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
if (!relationMetadata) {
throw new Error(
`Relation metadata not found for field ${fieldMetadata.name}`,
);
}
if (!fieldMetadata.workspaceId) {
throw new Error(
`Workspace id not found for field ${fieldMetadata.name} in object metadata ${fieldMetadata.objectMetadataId}`,
);
}
const relationDirection = deduceRelationDirection(
fieldMetadata,
relationMetadata,
);
// Retrieve the referenced object metadata based on the relation direction
// Mandatory to handle n+n relations
const referencedObjectMetadata = objectMetadataCollection.find(
(objectMetadata) =>
objectMetadata.id ===
(relationDirection == RelationDirection.TO
? relationMetadata.fromObjectMetadataId
: relationMetadata.toObjectMetadataId),
);
if (!referencedObjectMetadata) {
throw new Error(
`Referenced object metadata not found for relation ${relationMetadata.id}`,
);
}
// If it's a relation destination is of kind MANY, we need to add the collection suffix and extract the args
if (
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY &&
relationDirection === RelationDirection.FROM
) {
const args = getFieldArgumentsByKey(info, fieldKey);
const argsString = this.argsStringFactory.create(
args,
referencedObjectMetadata.fields ?? [],
!withSoftDeleted,
);
const fieldsString =
await this.fieldsStringFactory.createFieldsStringRecursive(
info,
fieldValue,
referencedObjectMetadata.fields ?? [],
objectMetadataCollection,
withSoftDeleted ?? false,
);
return `
${fieldKey}: ${computeObjectTargetTable(
referencedObjectMetadata,
)}Collection${argsString ? `(${argsString})` : ''} {
${fieldsString}
}
`;
}
let relationAlias = `${fieldKey}: ${computeColumnName(fieldMetadata)}`;
// For one to one relations, pg_graphql use the target TableName on the side that is not storing the foreign key
// so we need to alias it to the field key
if (
relationMetadata.relationType === RelationMetadataType.ONE_TO_ONE &&
relationDirection === RelationDirection.FROM
) {
relationAlias = `${fieldKey}: ${computeObjectTargetTable(
referencedObjectMetadata,
)}`;
}
const fieldsString =
await this.fieldsStringFactory.createFieldsStringRecursive(
info,
fieldValue,
referencedObjectMetadata.fields ?? [],
objectMetadataCollection,
withSoftDeleted ?? false,
);
// Otherwise it means it's a relation destination is of kind ONE
return `
${relationAlias} {
${fieldsString}
}
`;
}
}

View File

@ -1,64 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
Record as IRecord,
RecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory';
import { FieldsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory';
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export interface UpdateManyQueryFactoryOptions
extends WorkspaceQueryBuilderOptions {
atMost?: number;
}
@Injectable()
export class UpdateManyQueryFactory {
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsAliasFactory: ArgsAliasFactory,
) {}
async create<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: UpdateManyResolverArgs<Partial<Record>, Filter>,
options: UpdateManyQueryFactoryOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
);
const computedArgsData = this.argsAliasFactory.create(
args.data,
options.fieldMetadataCollection,
);
const argsData = {
...computedArgsData,
updatedAt: new Date().toISOString(),
};
return `
mutation {
update${computeObjectTargetTable(options.objectMetadataItem)}Collection(
set: ${stringifyWithoutKeyQuote(argsData)},
filter: ${stringifyWithoutKeyQuote(args.filter)},
atMost: ${options.atMost ?? 1},
) {
affectedCount
records {
${fieldsString}
}
}
}`;
}
}

View File

@ -1,56 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { ArgsAliasFactory } from './args-alias.factory';
import { FieldsStringFactory } from './fields-string.factory';
@Injectable()
export class UpdateOneQueryFactory {
constructor(
private readonly fieldsStringFactory: FieldsStringFactory,
private readonly argsAliasFactory: ArgsAliasFactory,
) {}
async create<Record extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Partial<Record>>,
options: WorkspaceQueryBuilderOptions,
) {
const fieldsString = await this.fieldsStringFactory.create(
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
);
const computedArgsData = this.argsAliasFactory.create(
args.data,
options.fieldMetadataCollection,
);
const argsData = {
...computedArgsData,
id: undefined, // do not allow updating an existing object's id
updatedAt: new Date().toISOString(),
};
return `
mutation {
update${computeObjectTargetTable(
options.objectMetadataItem,
)}Collection(set: ${stringifyWithoutKeyQuote(
argsData,
)}, filter: { id: { eq: "${args.id}" } }) {
affectedCount
records {
${fieldsString}
}
}
}
`;
}
}

View File

@ -1,114 +0,0 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
import {
Record as IRecord,
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import {
FindManyResolverArgs,
FindOneResolverArgs,
CreateManyResolverArgs,
UpdateOneResolverArgs,
DeleteOneResolverArgs,
UpdateManyResolverArgs,
DeleteManyResolverArgs,
FindDuplicatesResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FindManyQueryFactory } from './factories/find-many-query.factory';
import { FindOneQueryFactory } from './factories/find-one-query.factory';
import { CreateManyQueryFactory } from './factories/create-many-query.factory';
import { UpdateOneQueryFactory } from './factories/update-one-query.factory';
import { DeleteOneQueryFactory } from './factories/delete-one-query.factory';
import {
UpdateManyQueryFactory,
UpdateManyQueryFactoryOptions,
} from './factories/update-many-query.factory';
import {
DeleteManyQueryFactory,
DeleteManyQueryFactoryOptions,
} from './factories/delete-many-query.factory';
import { FindDuplicatesQueryFactory } from './factories/find-duplicates-query.factory';
@Injectable()
export class WorkspaceQueryBuilderFactory {
constructor(
private readonly findManyQueryFactory: FindManyQueryFactory,
private readonly findOneQueryFactory: FindOneQueryFactory,
private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory,
private readonly createManyQueryFactory: CreateManyQueryFactory,
private readonly updateOneQueryFactory: UpdateOneQueryFactory,
private readonly deleteOneQueryFactory: DeleteOneQueryFactory,
private readonly updateManyQueryFactory: UpdateManyQueryFactory,
private readonly deleteManyQueryFactory: DeleteManyQueryFactory,
) {}
findMany<
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.findManyQueryFactory.create<Filter, OrderBy>(args, options);
}
findOne<Filter extends RecordFilter = RecordFilter>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.findOneQueryFactory.create<Filter>(args, options);
}
findDuplicates(
args: FindDuplicatesResolverArgs,
options: WorkspaceQueryBuilderOptions,
existingRecords?: IRecord[],
): Promise<string> {
return this.findDuplicatesQueryFactory.create(
args,
options,
existingRecords,
);
}
createMany<Record extends IRecord = IRecord>(
args: CreateManyResolverArgs<Partial<Record>>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.createManyQueryFactory.create<Record>(args, options);
}
updateOne<Record extends IRecord = IRecord>(
initialArgs: UpdateOneResolverArgs<Partial<Record>>,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.updateOneQueryFactory.create<Record>(initialArgs, options);
}
deleteOne(
args: DeleteOneResolverArgs,
options: WorkspaceQueryBuilderOptions,
): Promise<string> {
return this.deleteOneQueryFactory.create(args, options);
}
updateMany<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: UpdateManyResolverArgs<Partial<Record>, Filter>,
options: UpdateManyQueryFactoryOptions,
): Promise<string> {
return this.updateManyQueryFactory.create(args, options);
}
deleteMany<Filter extends RecordFilter = RecordFilter>(
args: DeleteManyResolverArgs<Filter>,
options: DeleteManyQueryFactoryOptions,
): Promise<string> {
return this.deleteManyQueryFactory.create(args, options);
}
}

View File

@ -1,21 +1,13 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { FieldsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory';
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module';
import { WorkspaceQueryBuilderFactory } from './workspace-query-builder.factory';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { workspaceQueryBuilderFactories } from './factories/factories';
@Module({
imports: [ObjectMetadataModule, DuplicateModule],
providers: [...workspaceQueryBuilderFactories, WorkspaceQueryBuilderFactory],
exports: [
WorkspaceQueryBuilderFactory,
FieldsStringFactory,
RecordPositionQueryFactory,
],
imports: [ObjectMetadataModule],
providers: [...workspaceQueryBuilderFactories],
exports: [RecordPositionQueryFactory],
})
export class WorkspaceQueryBuilderModule {}