## 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:
@ -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}]',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}]`;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
];
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
@ -8,7 +8,6 @@ import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner
|
||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
@ -17,8 +16,6 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
|
||||
|
||||
import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener';
|
||||
|
||||
@Module({
|
||||
@ -31,17 +28,15 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
AnalyticsModule,
|
||||
TelemetryModule,
|
||||
DuplicateModule,
|
||||
FileModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
WorkspaceQueryRunnerService,
|
||||
...workspaceQueryRunnerFactories,
|
||||
EntityEventsToDbListener,
|
||||
TelemetryListener,
|
||||
RecordPositionBackfillCommand,
|
||||
],
|
||||
exports: [WorkspaceQueryRunnerService, ...workspaceQueryRunnerFactories],
|
||||
exports: [...workspaceQueryRunnerFactories],
|
||||
})
|
||||
export class WorkspaceQueryRunnerModule {}
|
||||
|
||||
@ -1,942 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
|
||||
import {
|
||||
Record as IRecord,
|
||||
RecordFilter,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import {
|
||||
CreateManyResolverArgs,
|
||||
CreateOneResolverArgs,
|
||||
DeleteManyResolverArgs,
|
||||
DeleteOneResolverArgs,
|
||||
DestroyManyResolverArgs,
|
||||
FindDuplicatesResolverArgs,
|
||||
ResolverArgsType,
|
||||
RestoreManyResolverArgs,
|
||||
UpdateManyResolverArgs,
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { WorkspaceQueryBuilderFactory } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory';
|
||||
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
import {
|
||||
CallWebhookJobsJob,
|
||||
CallWebhookJobsJobData,
|
||||
CallWebhookJobsJobOperation,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job';
|
||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util';
|
||||
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||
import {
|
||||
WorkspaceQueryRunnerException,
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
import {
|
||||
PGGraphQLMutation,
|
||||
PGGraphQLResult,
|
||||
} from './interfaces/pg-graphql.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
|
||||
import {
|
||||
PgGraphQLConfig,
|
||||
computePgGraphQLError,
|
||||
} from './utils/compute-pg-graphql-error.util';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceQueryRunnerService {
|
||||
private readonly logger = new Logger(WorkspaceQueryRunnerService.name);
|
||||
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
|
||||
private readonly queryResultGettersFactory: QueryResultGettersFactory,
|
||||
@InjectMessageQueue(MessageQueue.webhookQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly duplicateService: DuplicateService,
|
||||
) {}
|
||||
|
||||
async findDuplicates<TRecord extends IRecord = IRecord>(
|
||||
args: FindDuplicatesResolverArgs<Partial<TRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<TRecord> | undefined> {
|
||||
if (!args.data && !args.ids) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
'You have to provide either "data" or "id" argument',
|
||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (!args.ids && isEmpty(args.data)) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
'The "data" condition can not be empty when ID input not provided',
|
||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
console.log(
|
||||
`running findDuplicates for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
||||
);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findDuplicates',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.FindDuplicates,
|
||||
)) as FindDuplicatesResolverArgs<TRecord>;
|
||||
|
||||
let existingRecords: IRecord[] | undefined = undefined;
|
||||
|
||||
if (computedArgs.ids && computedArgs.ids.length > 0) {
|
||||
existingRecords = await this.duplicateService.findExistingRecords(
|
||||
computedArgs.ids,
|
||||
objectMetadataItem,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
if (!existingRecords || existingRecords.length === 0) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
`Object with id ${args.ids} not found`,
|
||||
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.findDuplicates(
|
||||
computedArgs,
|
||||
options,
|
||||
existingRecords,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
return this.parseResult<IConnection<TRecord>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
authContext.workspace.id,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async createMany<Record extends IRecord = IRecord>(
|
||||
args: CreateManyResolverArgs<Partial<Record>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
if (args.upsert) {
|
||||
return await this.upsertMany(args, options);
|
||||
}
|
||||
|
||||
args.data.forEach((record) => {
|
||||
if (record?.id) {
|
||||
assertIsValidUuid(record.id);
|
||||
}
|
||||
});
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'createMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.CreateMany,
|
||||
)) as CreateManyResolverArgs<Record>;
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.createMany(
|
||||
computedArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'insertInto',
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
await this.workspaceQueryHookService.executePostQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'createMany',
|
||||
parsedResults,
|
||||
);
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.create,
|
||||
options,
|
||||
);
|
||||
|
||||
this.workspaceEventEmitter.emit(
|
||||
`${objectMetadataItem.nameSingular}.created`,
|
||||
parsedResults.map(
|
||||
(record) =>
|
||||
({
|
||||
userId: authContext.user?.id,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
after: record,
|
||||
},
|
||||
}) satisfies ObjectRecordCreateEvent<any>,
|
||||
),
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
async upsertMany<Record extends IRecord = IRecord>(
|
||||
args: CreateManyResolverArgs<Partial<Record>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
console.log(
|
||||
`running upsertMany for ${options.objectMetadataItem.nameSingular} on workspace ${options.authContext.workspace.id}`,
|
||||
);
|
||||
const ids = args.data
|
||||
.map((item) => item.id)
|
||||
.filter((id) => id !== undefined);
|
||||
|
||||
const existingRecords =
|
||||
ids.length > 0
|
||||
? await this.duplicateService.findExistingRecords(
|
||||
ids as string[],
|
||||
options.objectMetadataItem,
|
||||
options.authContext.workspace.id,
|
||||
)
|
||||
: [];
|
||||
|
||||
const existingRecordsMap = new Map(
|
||||
existingRecords.map((record) => [record.id, record]),
|
||||
);
|
||||
|
||||
const results: Record[] = [];
|
||||
const recordsToCreate: Partial<Record>[] = [];
|
||||
|
||||
for (const payload of args.data) {
|
||||
if (payload.id && existingRecordsMap.has(payload.id)) {
|
||||
const result = await this.updateOne(
|
||||
{ id: payload.id, data: payload },
|
||||
options,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
results.push(result);
|
||||
}
|
||||
} else {
|
||||
recordsToCreate.push(payload);
|
||||
}
|
||||
}
|
||||
|
||||
if (recordsToCreate.length > 0) {
|
||||
const createResults = await this.createMany(
|
||||
{ data: recordsToCreate } as CreateManyResolverArgs<Partial<Record>>,
|
||||
options,
|
||||
);
|
||||
|
||||
if (createResults) {
|
||||
results.push(...createResults);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async createOne<Record extends IRecord = IRecord>(
|
||||
args: CreateOneResolverArgs<Partial<Record>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const results = await this.createMany(
|
||||
{ data: [args.data], upsert: args.upsert },
|
||||
options,
|
||||
);
|
||||
|
||||
return results?.[0];
|
||||
}
|
||||
|
||||
async updateOne<Record extends IRecord = IRecord>(
|
||||
args: UpdateOneResolverArgs<Partial<Record>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
console.log(
|
||||
`running updateOne for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
||||
);
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
assertIsValidUuid(args.id);
|
||||
|
||||
const existingRecord = await repository.findOne({
|
||||
where: { id: args.id },
|
||||
});
|
||||
|
||||
if (!existingRecord) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
`Object with id ${args.id} not found`,
|
||||
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'updateOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
||||
hookedArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.update,
|
||||
options,
|
||||
);
|
||||
|
||||
this.workspaceEventEmitter.emit(
|
||||
`${objectMetadataItem.nameSingular}.updated`,
|
||||
[
|
||||
{
|
||||
userId: authContext.user?.id,
|
||||
recordId: existingRecord.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
updatedFields: Object.keys(args.data),
|
||||
before: this.removeNestedProperties(existingRecord as Record),
|
||||
after: this.removeNestedProperties(parsedResults?.[0]),
|
||||
},
|
||||
} satisfies ObjectRecordUpdateEvent<any>,
|
||||
],
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
return parsedResults?.[0];
|
||||
}
|
||||
|
||||
async updateMany<Record extends IRecord = IRecord>(
|
||||
args: UpdateManyResolverArgs<Partial<Record>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
console.log(
|
||||
`running updateMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
||||
);
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
args.filter?.id?.in?.forEach((id) => assertIsValidUuid(id));
|
||||
|
||||
const existingRecords = await repository.find({
|
||||
where: { id: In(args.filter?.id?.in) },
|
||||
});
|
||||
const mappedRecords = new Map(
|
||||
existingRecords.map((record) => [record.id, record]),
|
||||
);
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||
);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'updateMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
||||
hookedArgs,
|
||||
{
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.update,
|
||||
options,
|
||||
);
|
||||
|
||||
const eventsToEmit: ObjectRecordUpdateEvent<any>[] = parsedResults
|
||||
.map((record) => {
|
||||
const existingRecord = mappedRecords.get(record.id);
|
||||
|
||||
if (!existingRecord) {
|
||||
this.logger.warn(
|
||||
`Record with id ${record.id} not found in the database`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: authContext.user?.id,
|
||||
recordId: existingRecord.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
updatedFields: Object.keys(args.data),
|
||||
before: this.removeNestedProperties(existingRecord as Record),
|
||||
after: this.removeNestedProperties(record),
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
this.workspaceEventEmitter.emit(
|
||||
`${objectMetadataItem.nameSingular}.updated`,
|
||||
eventsToEmit,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
async deleteMany<
|
||||
Record extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
>(
|
||||
args: DeleteManyResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
console.log(
|
||||
`running deleteMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
||||
);
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||
);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'deleteMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
||||
{
|
||||
filter: hookedArgs.filter,
|
||||
data: {
|
||||
deletedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
},
|
||||
);
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
const existingRecords = await repository.find({
|
||||
where: { id: In(args.filter?.id?.in) },
|
||||
});
|
||||
const mappedRecords = new Map(
|
||||
existingRecords.map((record) => [record.id, record]),
|
||||
);
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.delete,
|
||||
options,
|
||||
);
|
||||
|
||||
this.workspaceEventEmitter.emit(
|
||||
`${objectMetadataItem.nameSingular}.deleted`,
|
||||
parsedResults.map((record) => {
|
||||
const existingRecord = mappedRecords.get(record.id);
|
||||
|
||||
return {
|
||||
userId: authContext.user?.id,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
before: this.removeNestedProperties({
|
||||
...existingRecord,
|
||||
...record,
|
||||
}),
|
||||
},
|
||||
} satisfies ObjectRecordDeleteEvent<any>;
|
||||
}),
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
async destroyMany<
|
||||
Record extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
>(
|
||||
args: DestroyManyResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
console.log(
|
||||
`running destroyMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
||||
);
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||
);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'destroyMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.deleteMany(
|
||||
{
|
||||
filter: {
|
||||
...hookedArgs.filter,
|
||||
deletedAt: { is: 'NOT_NULL' },
|
||||
},
|
||||
},
|
||||
{
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'deleteFrom',
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.delete,
|
||||
options,
|
||||
);
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
async restoreMany<
|
||||
Record extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
>(
|
||||
args: RestoreManyResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
console.log(
|
||||
`running restoreMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
||||
);
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||
);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'restoreMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
||||
{
|
||||
filter: {
|
||||
...hookedArgs.filter,
|
||||
deletedAt: { is: 'NOT_NULL' },
|
||||
},
|
||||
data: {
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.create,
|
||||
options,
|
||||
);
|
||||
|
||||
this.workspaceEventEmitter.emit(
|
||||
`${objectMetadataItem.nameSingular}.created`,
|
||||
parsedResults.map(
|
||||
(record) =>
|
||||
({
|
||||
userId: authContext.user?.id,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
after: this.removeNestedProperties(record),
|
||||
},
|
||||
}) satisfies ObjectRecordCreateEvent<any>,
|
||||
),
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
async deleteOne<Record extends IRecord = IRecord>(
|
||||
args: DeleteOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
console.log(
|
||||
`running deleteOne for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
||||
);
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
assertIsValidUuid(args.id);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'deleteOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
||||
{
|
||||
id: hookedArgs.id,
|
||||
data: {
|
||||
deletedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
const existingRecord = await repository.findOne({
|
||||
where: { id: args.id },
|
||||
});
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.delete,
|
||||
options,
|
||||
);
|
||||
|
||||
this.workspaceEventEmitter.emit(
|
||||
`${objectMetadataItem.nameSingular}.deleted`,
|
||||
[
|
||||
{
|
||||
userId: authContext.user?.id,
|
||||
recordId: args.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
before: {
|
||||
...(existingRecord ?? {}),
|
||||
...this.removeNestedProperties(parsedResults?.[0]),
|
||||
},
|
||||
},
|
||||
} satisfies ObjectRecordDeleteEvent<any>,
|
||||
],
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
return parsedResults?.[0];
|
||||
}
|
||||
|
||||
private removeNestedProperties<Record extends IRecord = IRecord>(
|
||||
record: Record,
|
||||
) {
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedRecord = {};
|
||||
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (value && typeof value === 'object' && value['edges']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === '__typename') {
|
||||
continue;
|
||||
}
|
||||
|
||||
sanitizedRecord[key] = value;
|
||||
}
|
||||
|
||||
return sanitizedRecord;
|
||||
}
|
||||
|
||||
async executeSQL(
|
||||
workspaceDataSource: DataSource,
|
||||
workspaceId: string,
|
||||
sqlQuery: string,
|
||||
parameters?: any[],
|
||||
) {
|
||||
try {
|
||||
return await workspaceDataSource?.transaction(
|
||||
async (transactionManager) => {
|
||||
await transactionManager.query(`
|
||||
SET LOCAL search_path TO ${this.workspaceDataSourceService.getSchemaName(
|
||||
workspaceId,
|
||||
)};
|
||||
`);
|
||||
|
||||
const results = transactionManager.query(sqlQuery, parameters);
|
||||
|
||||
return results;
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
if (isQueryTimeoutError(error)) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
'The SQL request took too long to process, resulting in a query read timeout. To resolve this issue, consider modifying your query by reducing the depth of relationships or limiting the number of records being fetched.',
|
||||
WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
query: string,
|
||||
workspaceId: string,
|
||||
): Promise<PGGraphQLResult | undefined> {
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return this.executeSQL(
|
||||
workspaceDataSource,
|
||||
workspaceId,
|
||||
`SELECT graphql.resolve($1);`,
|
||||
[query],
|
||||
);
|
||||
}
|
||||
|
||||
private async parseResult<Result>(
|
||||
graphqlResult: PGGraphQLResult | undefined,
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
command: string,
|
||||
workspaceId: string,
|
||||
isMultiQuery = false,
|
||||
): Promise<Result> {
|
||||
const entityKey = `${command}${computeObjectTargetTable(
|
||||
objectMetadataItem,
|
||||
)}Collection`;
|
||||
const result = !isMultiQuery
|
||||
? graphqlResult?.[0]?.resolve?.data?.[entityKey]
|
||||
: Object.keys(graphqlResult?.[0]?.resolve?.data).reduce(
|
||||
(acc: IRecord[], dataItem, index) => {
|
||||
acc.push(graphqlResult?.[0]?.resolve?.data[`${entityKey}${index}`]);
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const errors = graphqlResult?.[0]?.resolve?.errors;
|
||||
|
||||
if (
|
||||
result &&
|
||||
['update', 'deleteFrom'].includes(command) &&
|
||||
!result.affectedCount
|
||||
) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
'No rows were affected.',
|
||||
WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED,
|
||||
);
|
||||
}
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
const error = computePgGraphQLError(
|
||||
command,
|
||||
objectMetadataItem.nameSingular,
|
||||
errors,
|
||||
{
|
||||
atMost: this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||
),
|
||||
} satisfies PgGraphQLConfig,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const resultWithGetters = await this.queryResultGettersFactory.create(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return parseResult(resultWithGetters);
|
||||
}
|
||||
|
||||
async triggerWebhooks<Record>(
|
||||
jobsData: Record[] | undefined,
|
||||
operation: CallWebhookJobsJobOperation,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
) {
|
||||
if (!Array.isArray(jobsData)) {
|
||||
return;
|
||||
}
|
||||
jobsData.forEach((jobData) => {
|
||||
this.messageQueueService.add<CallWebhookJobsJobData>(
|
||||
CallWebhookJobsJob.name,
|
||||
{
|
||||
record: jobData,
|
||||
workspaceId: options.authContext.workspace.id,
|
||||
operation,
|
||||
objectMetadataItem: options.objectMetadataItem,
|
||||
},
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user