Refactor backend folder structure (#4505)
* Refactor backend folder structure Co-authored-by: Charles Bochet <charles@twenty.com> * fix tests * fix * move yoga hooks --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,44 @@
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine-metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
export const fieldNumberMock = {
|
||||
name: 'fieldNumber',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
isNullable: false,
|
||||
defaultValue: null,
|
||||
targetColumnMap: { value: 'fieldNumber' },
|
||||
};
|
||||
|
||||
export const fieldStringMock = {
|
||||
name: 'fieldString',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
targetColumnMap: { value: 'fieldString' },
|
||||
};
|
||||
|
||||
export const fieldLinkMock = {
|
||||
name: 'fieldLink',
|
||||
type: FieldMetadataType.LINK,
|
||||
isNullable: false,
|
||||
defaultValue: { label: '', url: '' },
|
||||
targetColumnMap: { label: 'fieldLinkLabel', url: 'fieldLinkUrl' },
|
||||
};
|
||||
|
||||
export const fieldCurrencyMock = {
|
||||
name: 'fieldCurrency',
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
targetColumnMap: {
|
||||
amountMicros: 'fieldCurrencyAmountMicros',
|
||||
currencyCode: 'fieldCurrencyCurrencyCode',
|
||||
},
|
||||
};
|
||||
|
||||
export const objectMetadataItemMock: DeepPartial<ObjectMetadataEntity> = {
|
||||
targetTableName: 'testingObject',
|
||||
nameSingular: 'objectName',
|
||||
namePlural: 'objectsName',
|
||||
fields: [fieldNumberMock, fieldStringMock, fieldLinkMock, fieldCurrencyMock],
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { DataSourceService } from 'src/engine-metadata/data-source/data-source.service';
|
||||
import { ObjectMetadataService } from 'src/engine-metadata/object-metadata/object-metadata.service';
|
||||
import { WorkspaceSchemaStorageService } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.service';
|
||||
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
|
||||
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
|
||||
import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
|
||||
import { WorkspaceFactory } from 'src/engine/api/graphql/workspace.factory';
|
||||
|
||||
describe('WorkspaceFactory', () => {
|
||||
let service: WorkspaceFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
WorkspaceFactory,
|
||||
{
|
||||
provide: DataSourceService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ScalarsExplorerService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceGraphQLSchemaFactory,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceResolverFactory,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceSchemaStorageService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<WorkspaceFactory>(WorkspaceFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { MetadataModule } from 'src/engine-metadata/metadata.module';
|
||||
import { DataSourceModule } from 'src/engine-metadata/data-source/data-source.module';
|
||||
import { WorkspaceSchemaStorageModule } from 'src/engine/api/graphql/workspace-schema-storage/workspace-schema-storage.module';
|
||||
import { ObjectMetadataModule } from 'src/engine-metadata/object-metadata/object-metadata.module';
|
||||
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
|
||||
import { WorkspaceSchemaBuilderModule } from 'src/engine/api/graphql/workspace-schema-builder/workspace-schema-builder.module';
|
||||
import { WorkspaceResolverBuilderModule } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module';
|
||||
|
||||
import { WorkspaceFactory } from './workspace.factory';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MetadataModule,
|
||||
DataSourceModule,
|
||||
ObjectMetadataModule,
|
||||
WorkspaceSchemaBuilderModule,
|
||||
WorkspaceResolverBuilderModule,
|
||||
WorkspaceSchemaStorageModule,
|
||||
],
|
||||
providers: [WorkspaceFactory, ScalarsExplorerService],
|
||||
exports: [WorkspaceFactory],
|
||||
})
|
||||
export class CoreGraphqlApiModule {}
|
||||
@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLScalarType, GraphQLSchema, isScalarType } from 'graphql';
|
||||
|
||||
import { scalars } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@Injectable()
|
||||
export class ScalarsExplorerService {
|
||||
private scalarImplementations: Record<string, GraphQLScalarType>;
|
||||
|
||||
constructor() {
|
||||
this.scalarImplementations = scalars.reduce((acc, scalar) => {
|
||||
acc[scalar.name] = scalar;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
getScalarImplementation(scalarName: string): GraphQLScalarType | undefined {
|
||||
return this.scalarImplementations[scalarName];
|
||||
}
|
||||
|
||||
getUsedScalarNames(schema: GraphQLSchema): string[] {
|
||||
const typeMap = schema.getTypeMap();
|
||||
const usedScalarNames: string[] = [];
|
||||
|
||||
for (const typeName in typeMap) {
|
||||
const type = typeMap[typeName];
|
||||
|
||||
if (isScalarType(type) && !typeName.startsWith('__')) {
|
||||
usedScalarNames.push(type.name);
|
||||
}
|
||||
}
|
||||
|
||||
return usedScalarNames;
|
||||
}
|
||||
|
||||
getScalarResolvers(
|
||||
usedScalarNames: string[],
|
||||
): Record<string, GraphQLScalarType> {
|
||||
const scalarResolvers: Record<string, GraphQLScalarType> = {};
|
||||
|
||||
for (const scalarName of usedScalarNames) {
|
||||
const scalarImplementation = this.getScalarImplementation(scalarName);
|
||||
|
||||
if (scalarImplementation) {
|
||||
scalarResolvers[scalarName] = scalarImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
return scalarResolvers;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
|
||||
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
|
||||
export const workspaceQueryBuilderOptionsMock: WorkspaceQueryBuilderOptions = {
|
||||
fieldMetadataCollection: [],
|
||||
info: {} as GraphQLResolveInfo,
|
||||
objectMetadataCollection: [],
|
||||
objectMetadataItem: objectMetadataItemMock as ObjectMetadataInterface,
|
||||
};
|
||||
@ -0,0 +1,122 @@
|
||||
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}]',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,206 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { RecordFilter } 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 { 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 { FindDuplicatesQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/find-duplicates-query.factory';
|
||||
import { workspaceQueryBuilderOptionsMock } from 'src/engine/api/graphql/workspace-query-builder/__mocks__/workspace-query-builder-options.mock';
|
||||
|
||||
describe('FindDuplicatesQueryFactory', () => {
|
||||
let service: FindDuplicatesQueryFactory;
|
||||
const argAliasCreate = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FindDuplicatesQueryFactory,
|
||||
{
|
||||
provide: FieldsStringFactory,
|
||||
useValue: {
|
||||
create: jest.fn().mockResolvedValue('fieldsString'),
|
||||
// Mock implementation of FieldsStringFactory methods if needed
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ArgsAliasFactory,
|
||||
useValue: {
|
||||
create: argAliasCreate,
|
||||
// Mock implementation of ArgsAliasFactory methods if needed
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FindDuplicatesQueryFactory>(
|
||||
FindDuplicatesQueryFactory,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should return (first: 0) as a filter when args are missing', async () => {
|
||||
const args: FindDuplicatesResolverArgs<RecordFilter> = {};
|
||||
|
||||
const query = await service.create(
|
||||
args,
|
||||
workspaceQueryBuilderOptionsMock,
|
||||
);
|
||||
|
||||
expect(query.trim()).toEqual(`query {
|
||||
objectNameCollection(first: 0) {
|
||||
fieldsString
|
||||
}
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should use firstName and lastName as a filter when both args are present', async () => {
|
||||
argAliasCreate.mockReturnValue({
|
||||
nameFirstName: 'John',
|
||||
nameLastName: 'Doe',
|
||||
});
|
||||
|
||||
const args: FindDuplicatesResolverArgs<RecordFilter> = {
|
||||
data: {
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
} as unknown as RecordFilter,
|
||||
};
|
||||
|
||||
const query = await service.create(args, {
|
||||
...workspaceQueryBuilderOptionsMock,
|
||||
objectMetadataItem: {
|
||||
...workspaceQueryBuilderOptionsMock.objectMetadataItem,
|
||||
nameSingular: 'person',
|
||||
},
|
||||
});
|
||||
|
||||
expect(query.trim()).toEqual(`query {
|
||||
personCollection(filter: {or:[{nameFirstName:{eq:\"John\"},nameLastName:{eq:\"Doe\"}}]}) {
|
||||
fieldsString
|
||||
}
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should ignore an argument if the string length is less than 3', async () => {
|
||||
argAliasCreate.mockReturnValue({
|
||||
linkedinLinkUrl: 'ab',
|
||||
email: 'test@test.com',
|
||||
});
|
||||
|
||||
const args: FindDuplicatesResolverArgs<RecordFilter> = {
|
||||
data: {
|
||||
linkedinLinkUrl: 'ab',
|
||||
email: 'test@test.com',
|
||||
} as unknown as RecordFilter,
|
||||
};
|
||||
|
||||
const query = await service.create(args, {
|
||||
...workspaceQueryBuilderOptionsMock,
|
||||
objectMetadataItem: {
|
||||
...workspaceQueryBuilderOptionsMock.objectMetadataItem,
|
||||
nameSingular: 'person',
|
||||
},
|
||||
});
|
||||
|
||||
expect(query.trim()).toEqual(`query {
|
||||
personCollection(filter: {or:[{email:{eq:"test@test.com"}}]}) {
|
||||
fieldsString
|
||||
}
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should return (first: 0) as a filter when only firstName is present', async () => {
|
||||
argAliasCreate.mockReturnValue({
|
||||
nameFirstName: 'John',
|
||||
});
|
||||
|
||||
const args: FindDuplicatesResolverArgs<RecordFilter> = {
|
||||
data: {
|
||||
name: {
|
||||
firstName: 'John',
|
||||
},
|
||||
} as unknown as RecordFilter,
|
||||
};
|
||||
|
||||
const query = await service.create(args, {
|
||||
...workspaceQueryBuilderOptionsMock,
|
||||
objectMetadataItem: {
|
||||
...workspaceQueryBuilderOptionsMock.objectMetadataItem,
|
||||
nameSingular: 'person',
|
||||
},
|
||||
});
|
||||
|
||||
expect(query.trim()).toEqual(`query {
|
||||
personCollection(first: 0) {
|
||||
fieldsString
|
||||
}
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should use "currentRecord" as query args when its present', async () => {
|
||||
argAliasCreate.mockReturnValue({
|
||||
nameFirstName: 'John',
|
||||
});
|
||||
|
||||
const args: FindDuplicatesResolverArgs<RecordFilter> = {
|
||||
id: 'uuid',
|
||||
};
|
||||
|
||||
const query = await service.create(
|
||||
args,
|
||||
{
|
||||
...workspaceQueryBuilderOptionsMock,
|
||||
objectMetadataItem: {
|
||||
...workspaceQueryBuilderOptionsMock.objectMetadataItem,
|
||||
nameSingular: 'person',
|
||||
},
|
||||
},
|
||||
{
|
||||
nameFirstName: 'Peter',
|
||||
nameLastName: 'Parker',
|
||||
},
|
||||
);
|
||||
|
||||
expect(query.trim()).toEqual(`query {
|
||||
personCollection(filter: {id:{neq:\"uuid\"},or:[{nameFirstName:{eq:\"Peter\"},nameLastName:{eq:\"Parker\"}}]}) {
|
||||
fieldsString
|
||||
}
|
||||
}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildQueryForExistingRecord', () => {
|
||||
it(`should include all the fields that exist for person inside "duplicateCriteriaCollection" constant`, async () => {
|
||||
const query = service.buildQueryForExistingRecord('uuid', {
|
||||
...workspaceQueryBuilderOptionsMock,
|
||||
objectMetadataItem: {
|
||||
...workspaceQueryBuilderOptionsMock.objectMetadataItem,
|
||||
nameSingular: 'person',
|
||||
},
|
||||
});
|
||||
|
||||
expect(query.trim()).toEqual(`query {
|
||||
personCollection(filter: { id: { eq: \"uuid\" }}){
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
nameFirstName
|
||||
nameLastName
|
||||
linkedinLinkUrl
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,89 @@
|
||||
import {
|
||||
RecordPositionQueryFactory,
|
||||
RecordPositionQueryType,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
|
||||
|
||||
describe('RecordPositionQueryFactory', () => {
|
||||
const objectMetadataItem = {
|
||||
isCustom: false,
|
||||
nameSingular: 'company',
|
||||
};
|
||||
const dataSourceSchema = 'workspace_test';
|
||||
const factory: RecordPositionQueryFactory = new RecordPositionQueryFactory();
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(factory).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
describe('createForGet', () => {
|
||||
it('should return a string with the position when positionValue is first', async () => {
|
||||
const positionValue = 'first';
|
||||
|
||||
const result = await factory.create(
|
||||
RecordPositionQueryType.GET,
|
||||
positionValue,
|
||||
objectMetadataItem,
|
||||
dataSourceSchema,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
`SELECT position FROM workspace_test."company"
|
||||
WHERE "position" IS NOT NULL ORDER BY "position" ASC LIMIT 1`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a string with the position when positionValue is last', async () => {
|
||||
const positionValue = 'last';
|
||||
|
||||
const result = await factory.create(
|
||||
RecordPositionQueryType.GET,
|
||||
positionValue,
|
||||
objectMetadataItem,
|
||||
dataSourceSchema,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
`SELECT position FROM workspace_test."company"
|
||||
WHERE "position" IS NOT NULL ORDER BY "position" DESC LIMIT 1`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a string with the position when positionValue is a number', async () => {
|
||||
const positionValue = 1;
|
||||
|
||||
try {
|
||||
await factory.create(
|
||||
RecordPositionQueryType.GET,
|
||||
positionValue,
|
||||
objectMetadataItem,
|
||||
dataSourceSchema,
|
||||
);
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
'RecordPositionQueryType.GET requires positionValue to be a number',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('createForUpdate', () => {
|
||||
it('should return a string when RecordPositionQueryType is UPDATE', async () => {
|
||||
const positionValue = 1;
|
||||
|
||||
const result = await factory.create(
|
||||
RecordPositionQueryType.UPDATE,
|
||||
positionValue,
|
||||
objectMetadataItem,
|
||||
dataSourceSchema,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
`UPDATE workspace_test."company"
|
||||
SET "position" = $1
|
||||
WHERE "id" = $2`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,69 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
@Injectable()
|
||||
export class ArgsAliasFactory {
|
||||
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 special complex field, we need to map all columns
|
||||
if (
|
||||
fieldMetadata &&
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
Object.values(fieldMetadata.targetColumnMap).length > 1
|
||||
) {
|
||||
for (const [subKey, subValue] of Object.entries(value)) {
|
||||
const mappedKey = fieldMetadata.targetColumnMap[subKey];
|
||||
|
||||
if (mappedKey) {
|
||||
newArgs[mappedKey] = subValue;
|
||||
}
|
||||
}
|
||||
} else if (fieldMetadata) {
|
||||
// Otherwise we just need to map the value
|
||||
const mappedKey = fieldMetadata.targetColumnMap.value;
|
||||
|
||||
newArgs[mappedKey ?? key] = value;
|
||||
} else {
|
||||
// Recurse if value is a nested object, otherwise append field or alias
|
||||
newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap);
|
||||
}
|
||||
}
|
||||
|
||||
return newArgs;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
||||
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
|
||||
@Injectable()
|
||||
export class ArgsStringFactory {
|
||||
constructor(private readonly argsAliasFactory: ArgsAliasFactory) {}
|
||||
|
||||
create(
|
||||
initialArgs: Record<string, any> | undefined,
|
||||
fieldMetadataCollection: FieldMetadataInterface[],
|
||||
): string | null {
|
||||
if (!initialArgs) {
|
||||
return null;
|
||||
}
|
||||
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 it's an object (and not null), stringify it
|
||||
argsString += `${key}: ${this.buildStringifiedObject(
|
||||
key,
|
||||
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 buildStringifiedObject(
|
||||
key: string,
|
||||
obj: Record<string, any>,
|
||||
): string {
|
||||
// PgGraphql is expecting the orderBy argument to be an array of objects
|
||||
if (key === 'orderBy') {
|
||||
const orderByString = Object.keys(obj)
|
||||
.sort((_, b) => {
|
||||
return b === 'position' ? -1 : 0;
|
||||
})
|
||||
.map((orderByKey) => `{${orderByKey}: ${obj[orderByKey]}}`)
|
||||
.join(', ');
|
||||
|
||||
return `[${orderByString}]`;
|
||||
}
|
||||
|
||||
return stringifyWithoutKeyQuote(obj);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.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 { FieldsStringFactory } from './fields-string.factory';
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
|
||||
@Injectable()
|
||||
export class CreateManyQueryFactory {
|
||||
private readonly logger = new Logger(CreateManyQueryFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
||||
) {}
|
||||
|
||||
async create<Record extends IRecord = IRecord>(
|
||||
args: CreateManyResolverArgs<Record>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
) {
|
||||
const fieldsString = await this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
options.objectMetadataCollection,
|
||||
);
|
||||
const computedArgs = this.argsAliasFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
return `
|
||||
mutation {
|
||||
insertInto${computeObjectTargetTable(
|
||||
options.objectMetadataItem,
|
||||
)}Collection(objects: ${stringifyWithoutKeyQuote(
|
||||
computedArgs.data.map((datum) => ({
|
||||
id: uuidv4(),
|
||||
...datum,
|
||||
})),
|
||||
)}) {
|
||||
affectedCount
|
||||
records {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { Injectable, Logger } 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 {
|
||||
private readonly logger = new Logger(DeleteOneQueryFactory.name);
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
import { ArgsStringFactory } from './args-string.factory';
|
||||
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
|
||||
import { CreateManyQueryFactory } from './create-many-query.factory';
|
||||
import { DeleteOneQueryFactory } from './delete-one-query.factory';
|
||||
import { FieldAliasFactory } from './field-alias.factory';
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
import { FindManyQueryFactory } from './find-many-query.factory';
|
||||
import { FindOneQueryFactory } from './find-one-query.factory';
|
||||
import { UpdateOneQueryFactory } from './update-one-query.factory';
|
||||
import { UpdateManyQueryFactory } from './update-many-query.factory';
|
||||
import { DeleteManyQueryFactory } from './delete-many-query.factory';
|
||||
import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory';
|
||||
import { RecordPositionQueryFactory } from './record-position-query.factory';
|
||||
|
||||
export const workspaceQueryBuilderFactories = [
|
||||
ArgsAliasFactory,
|
||||
ArgsStringFactory,
|
||||
RelationFieldAliasFactory,
|
||||
CreateManyQueryFactory,
|
||||
DeleteOneQueryFactory,
|
||||
FieldAliasFactory,
|
||||
FieldsStringFactory,
|
||||
FindManyQueryFactory,
|
||||
FindOneQueryFactory,
|
||||
FindDuplicatesQueryFactory,
|
||||
RecordPositionQueryFactory,
|
||||
UpdateOneQueryFactory,
|
||||
UpdateManyQueryFactory,
|
||||
DeleteManyQueryFactory,
|
||||
];
|
||||
@ -0,0 +1,30 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
@Injectable()
|
||||
export class FieldAliasFactory {
|
||||
private readonly logger = new Logger(FieldAliasFactory.name);
|
||||
|
||||
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
|
||||
const entries = Object.entries(fieldMetadata.targetColumnMap);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entries.length === 1) {
|
||||
// If there is only one value, use it as the alias
|
||||
const alias = entries[0][1];
|
||||
|
||||
return `${fieldKey}: ${alias}`;
|
||||
}
|
||||
|
||||
// Otherwise it means it's a special type with multiple values, so we need map all columns
|
||||
return `
|
||||
${entries
|
||||
.map(([key, value]) => `___${fieldMetadata.name}_${key}: ${value}`)
|
||||
.join('\n')}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.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 {
|
||||
private readonly logger = new Logger(FieldsStringFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldAliasFactory: FieldAliasFactory,
|
||||
private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
|
||||
) {}
|
||||
|
||||
create(
|
||||
info: GraphQLResolveInfo,
|
||||
fieldMetadataCollection: FieldMetadataInterface[],
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
): Promise<string> {
|
||||
const selectedFields: Record<string, any> = graphqlFields(info);
|
||||
|
||||
return this.createFieldsStringRecursive(
|
||||
info,
|
||||
selectedFields,
|
||||
fieldMetadataCollection,
|
||||
objectMetadataCollection,
|
||||
);
|
||||
}
|
||||
|
||||
async createFieldsStringRecursive(
|
||||
info: GraphQLResolveInfo,
|
||||
selectedFields: Record<string, any>,
|
||||
fieldMetadataCollection: FieldMetadataInterface[],
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
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,
|
||||
);
|
||||
|
||||
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,
|
||||
accumulator,
|
||||
);
|
||||
accumulator += `}\n`;
|
||||
} else {
|
||||
accumulator += `${fieldAlias}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
import { Injectable, Logger } 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 { RecordFilter } 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 { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.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 { duplicateCriteriaCollection } from 'src/engine/api/graphql/workspace-resolver-builder/constants/duplicate-criteria.constants';
|
||||
import { settings } from 'src/engine/constants/settings';
|
||||
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FindDuplicatesQueryFactory {
|
||||
private readonly logger = new Logger(FindDuplicatesQueryFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
||||
) {}
|
||||
|
||||
async create<Filter extends RecordFilter = RecordFilter>(
|
||||
args: FindDuplicatesResolverArgs<Filter>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
currentRecord?: Record<string, unknown>,
|
||||
) {
|
||||
const fieldsString = await this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
options.objectMetadataCollection,
|
||||
);
|
||||
|
||||
const argsData = this.getFindDuplicateBy<Filter>(
|
||||
args,
|
||||
options,
|
||||
currentRecord,
|
||||
);
|
||||
|
||||
const duplicateCondition = this.buildDuplicateCondition(
|
||||
options.objectMetadataItem,
|
||||
argsData,
|
||||
args.id,
|
||||
);
|
||||
|
||||
const filters = stringifyWithoutKeyQuote(duplicateCondition);
|
||||
|
||||
return `
|
||||
query {
|
||||
${computeObjectTargetTable(options.objectMetadataItem)}Collection${
|
||||
isEmpty(duplicateCondition?.or)
|
||||
? '(first: 0)'
|
||||
: `(filter: ${filters})`
|
||||
} {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
getFindDuplicateBy<Filter extends RecordFilter = RecordFilter>(
|
||||
args: FindDuplicatesResolverArgs<Filter>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
currentRecord?: Record<string, unknown>,
|
||||
) {
|
||||
if (currentRecord) {
|
||||
return currentRecord;
|
||||
}
|
||||
|
||||
return this.argsAliasFactory.create(
|
||||
args.data ?? {},
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
}
|
||||
|
||||
buildQueryForExistingRecord(
|
||||
id: string,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
) {
|
||||
return `
|
||||
query {
|
||||
${computeObjectTargetTable(
|
||||
options.objectMetadataItem,
|
||||
)}Collection(filter: { id: { eq: "${id}" }}){
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
${this.getApplicableDuplicateCriteriaCollection(
|
||||
options.objectMetadataItem,
|
||||
)
|
||||
.flatMap((dc) => dc.columnNames)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private buildDuplicateCondition(
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
argsData?: Record<string, unknown>,
|
||||
filteringByExistingRecordId?: string,
|
||||
) {
|
||||
if (!argsData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const criteriaCollection =
|
||||
this.getApplicableDuplicateCriteriaCollection(objectMetadataItem);
|
||||
|
||||
const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) =>
|
||||
criteria.columnNames.every((columnName) => {
|
||||
const value = argsData[columnName] as string | undefined;
|
||||
|
||||
return (
|
||||
!!value && value.length >= settings.minLengthOfStringForDuplicateCheck
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const filterCriteria = criteriaWithMatchingArgs.map((criteria) =>
|
||||
Object.fromEntries(
|
||||
criteria.columnNames.map((columnName) => [
|
||||
columnName,
|
||||
{ eq: argsData[columnName] },
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
// when filtering by an existing record, we need to filter that explicit record out
|
||||
...(filteringByExistingRecordId && {
|
||||
id: { neq: filteringByExistingRecordId },
|
||||
}),
|
||||
// keep condition as "or" to get results by more duplicate criteria
|
||||
or: filterCriteria,
|
||||
};
|
||||
}
|
||||
|
||||
private getApplicableDuplicateCriteriaCollection(
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
) {
|
||||
return duplicateCriteriaCollection.filter(
|
||||
(duplicateCriteria) =>
|
||||
duplicateCriteria.objectName === objectMetadataItem.nameSingular,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import {
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.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 {
|
||||
private readonly logger = new Logger(FindManyQueryFactory.name);
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
return `
|
||||
query {
|
||||
${computeObjectTargetTable(options.objectMetadataItem)}Collection${
|
||||
argsString ? `(${argsString})` : ''
|
||||
} {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.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 {
|
||||
private readonly logger = new Logger(FindOneQueryFactory.name);
|
||||
|
||||
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,
|
||||
);
|
||||
const argsString = this.argsStringFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
return `
|
||||
query {
|
||||
${computeObjectTargetTable(options.objectMetadataItem)}Collection${
|
||||
argsString ? `(${argsString})` : ''
|
||||
} {
|
||||
edges {
|
||||
node {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
export enum RecordPositionQueryType {
|
||||
GET = 'GET',
|
||||
UPDATE = 'UPDATE',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RecordPositionQueryFactory {
|
||||
async create(
|
||||
recordPositionQueryType: RecordPositionQueryType,
|
||||
positionValue: 'first' | 'last' | number,
|
||||
objectMetadata: { isCustom: boolean; nameSingular: string },
|
||||
dataSourceSchema: string,
|
||||
): Promise<string> {
|
||||
const name =
|
||||
(objectMetadata.isCustom ? '_' : '') + objectMetadata.nameSingular;
|
||||
|
||||
switch (recordPositionQueryType) {
|
||||
case RecordPositionQueryType.GET:
|
||||
if (typeof positionValue === 'number') {
|
||||
throw new Error(
|
||||
'RecordPositionQueryType.GET requires positionValue to be a number',
|
||||
);
|
||||
}
|
||||
|
||||
return this.createForGet(positionValue, name, dataSourceSchema);
|
||||
case RecordPositionQueryType.UPDATE:
|
||||
return this.createForUpdate(name, dataSourceSchema);
|
||||
default:
|
||||
throw new Error('Invalid RecordPositionQueryType');
|
||||
}
|
||||
}
|
||||
|
||||
private async createForGet(
|
||||
positionValue: 'first' | 'last',
|
||||
name: string,
|
||||
dataSourceSchema: string,
|
||||
): Promise<string> {
|
||||
const orderByDirection = positionValue === 'first' ? 'ASC' : 'DESC';
|
||||
|
||||
return `SELECT position FROM ${dataSourceSchema}."${name}"
|
||||
WHERE "position" IS NOT NULL ORDER BY "position" ${orderByDirection} LIMIT 1`;
|
||||
}
|
||||
|
||||
private async createForUpdate(
|
||||
name: string,
|
||||
dataSourceSchema: string,
|
||||
): Promise<string> {
|
||||
return `UPDATE ${dataSourceSchema}."${name}"
|
||||
SET "position" = $1
|
||||
WHERE "id" = $2`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { RelationMetadataType } from 'src/engine-metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
deduceRelationDirection,
|
||||
RelationDirection,
|
||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
||||
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util';
|
||||
import { ObjectMetadataService } from 'src/engine-metadata/object-metadata/object-metadata.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
import { ArgsStringFactory } from './args-string.factory';
|
||||
|
||||
@Injectable()
|
||||
export class RelationFieldAliasFactory {
|
||||
private logger = new Logger(RelationFieldAliasFactory.name);
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => FieldsStringFactory))
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsStringFactory: ArgsStringFactory,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
fieldKey: string,
|
||||
fieldValue: any,
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
info: GraphQLResolveInfo,
|
||||
): 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,
|
||||
);
|
||||
}
|
||||
|
||||
private async createRelationAlias(
|
||||
fieldKey: string,
|
||||
fieldValue: any,
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
info: GraphQLResolveInfo,
|
||||
): 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 ?? [],
|
||||
);
|
||||
const fieldsString =
|
||||
await this.fieldsStringFactory.createFieldsStringRecursive(
|
||||
info,
|
||||
fieldValue,
|
||||
referencedObjectMetadata.fields ?? [],
|
||||
objectMetadataCollection,
|
||||
);
|
||||
|
||||
return `
|
||||
${fieldKey}: ${computeObjectTargetTable(
|
||||
referencedObjectMetadata,
|
||||
)}Collection${argsString ? `(${argsString})` : ''} {
|
||||
${fieldsString}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
let relationAlias = fieldMetadata.isCustom
|
||||
? `${fieldKey}: _${fieldMetadata.name}`
|
||||
: fieldKey;
|
||||
|
||||
// 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,
|
||||
);
|
||||
|
||||
// Otherwise it means it's a relation destination is of kind ONE
|
||||
return `
|
||||
${relationAlias} {
|
||||
${fieldsString}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
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 { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
||||
import { FieldsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory';
|
||||
import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory';
|
||||
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<Record, Filter>,
|
||||
options: UpdateManyQueryFactoryOptions,
|
||||
) {
|
||||
const fieldsString = await this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
options.objectMetadataCollection,
|
||||
);
|
||||
|
||||
const computedArgs = this.argsAliasFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
const argsData = {
|
||||
...computedArgs.data,
|
||||
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}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.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 { FieldsStringFactory } from './fields-string.factory';
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateOneQueryFactory {
|
||||
private readonly logger = new Logger(UpdateOneQueryFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
||||
) {}
|
||||
|
||||
async create<Record extends IRecord = IRecord>(
|
||||
args: UpdateOneResolverArgs<Record>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
) {
|
||||
const fieldsString = await this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
options.objectMetadataCollection,
|
||||
);
|
||||
const computedArgs = this.argsAliasFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
const argsData = {
|
||||
...computedArgs.data,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return `
|
||||
mutation {
|
||||
update${computeObjectTargetTable(
|
||||
options.objectMetadataItem,
|
||||
)}Collection(set: ${stringifyWithoutKeyQuote(
|
||||
argsData,
|
||||
)}, filter: { id: { eq: "${computedArgs.id}" } }) {
|
||||
affectedCount
|
||||
records {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
export interface Record {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type RecordFilter = {
|
||||
[Property in keyof Record]: any;
|
||||
};
|
||||
|
||||
export enum OrderByDirection {
|
||||
AscNullsFirst = 'AscNullsFirst',
|
||||
AscNullsLast = 'AscNullsLast',
|
||||
DescNullsFirst = 'DescNullsFirst',
|
||||
DescNullsLast = 'DescNullsLast',
|
||||
}
|
||||
|
||||
export type RecordOrderBy = {
|
||||
[Property in keyof Record]?: OrderByDirection;
|
||||
};
|
||||
|
||||
export interface RecordDuplicateCriteria {
|
||||
objectName: string;
|
||||
columnNames: string[];
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
export interface WorkspaceQueryBuilderOptions {
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
info: GraphQLResolveInfo;
|
||||
fieldMetadataCollection: FieldMetadataInterface[];
|
||||
objectMetadataCollection: ObjectMetadataInterface[];
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
||||
|
||||
describe('stringifyWithoutKeyQuote', () => {
|
||||
test('should stringify object correctly without quotes around keys', () => {
|
||||
const obj = { name: 'John', age: 30, isAdmin: false };
|
||||
const result = stringifyWithoutKeyQuote(obj);
|
||||
|
||||
expect(result).toBe('{name:"John",age:30,isAdmin:false}');
|
||||
});
|
||||
|
||||
test('should handle nested objects', () => {
|
||||
const obj = {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
address: { city: 'New York', zipCode: 10001 },
|
||||
};
|
||||
const result = stringifyWithoutKeyQuote(obj);
|
||||
|
||||
expect(result).toBe(
|
||||
'{name:"John",age:30,address:{city:"New York",zipCode:10001}}',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle arrays', () => {
|
||||
const obj = {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
hobbies: ['reading', 'movies', 'hiking'],
|
||||
};
|
||||
const result = stringifyWithoutKeyQuote(obj);
|
||||
|
||||
expect(result).toBe(
|
||||
'{name:"John",age:30,hobbies:["reading","movies","hiking"]}',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle empty objects', () => {
|
||||
const obj = {};
|
||||
const result = stringifyWithoutKeyQuote(obj);
|
||||
|
||||
expect(result).toBe('{}');
|
||||
});
|
||||
|
||||
test('should handle numbers, strings, and booleans', () => {
|
||||
const num = 10;
|
||||
const str = 'Hello';
|
||||
const bool = false;
|
||||
|
||||
expect(stringifyWithoutKeyQuote(num)).toBe('10');
|
||||
expect(stringifyWithoutKeyQuote(str)).toBe('"Hello"');
|
||||
expect(stringifyWithoutKeyQuote(bool)).toBe('false');
|
||||
});
|
||||
|
||||
test('should handle null and undefined', () => {
|
||||
expect(stringifyWithoutKeyQuote(null)).toBe('null');
|
||||
expect(stringifyWithoutKeyQuote(undefined)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,96 @@
|
||||
import {
|
||||
GraphQLResolveInfo,
|
||||
SelectionSetNode,
|
||||
Kind,
|
||||
SelectionNode,
|
||||
FieldNode,
|
||||
InlineFragmentNode,
|
||||
ValueNode,
|
||||
} from 'graphql';
|
||||
|
||||
const isFieldNode = (node: SelectionNode): node is FieldNode =>
|
||||
node.kind === Kind.FIELD;
|
||||
|
||||
const isInlineFragmentNode = (
|
||||
node: SelectionNode,
|
||||
): node is InlineFragmentNode => node.kind === Kind.INLINE_FRAGMENT;
|
||||
|
||||
const findFieldNode = (
|
||||
selectionSet: SelectionSetNode | undefined,
|
||||
key: string,
|
||||
): FieldNode | null => {
|
||||
if (!selectionSet) return null;
|
||||
|
||||
let field: FieldNode | null = null;
|
||||
|
||||
for (const selection of selectionSet.selections) {
|
||||
// We've found the field
|
||||
if (isFieldNode(selection) && selection.name.value === key) {
|
||||
return selection;
|
||||
}
|
||||
|
||||
// Recursively search for the field in nested selections
|
||||
if (
|
||||
(isFieldNode(selection) || isInlineFragmentNode(selection)) &&
|
||||
selection.selectionSet
|
||||
) {
|
||||
field = findFieldNode(selection.selectionSet, key);
|
||||
|
||||
// If we find the field in a nested selection, stop searching
|
||||
if (field) break;
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
const parseValueNode = (
|
||||
valueNode: ValueNode,
|
||||
variables: GraphQLResolveInfo['variableValues'],
|
||||
) => {
|
||||
switch (valueNode.kind) {
|
||||
case Kind.VARIABLE:
|
||||
return variables[valueNode.name.value];
|
||||
case Kind.INT:
|
||||
case Kind.FLOAT:
|
||||
return Number(valueNode.value);
|
||||
case Kind.STRING:
|
||||
case Kind.BOOLEAN:
|
||||
case Kind.ENUM:
|
||||
return valueNode.value;
|
||||
case Kind.LIST:
|
||||
return valueNode.values.map((value) => parseValueNode(value, variables));
|
||||
case Kind.OBJECT:
|
||||
return valueNode.fields.reduce((obj, field) => {
|
||||
obj[field.name.value] = parseValueNode(field.value, variables);
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFieldArgumentsByKey = (
|
||||
info: GraphQLResolveInfo,
|
||||
fieldKey: string,
|
||||
): Record<string, any> => {
|
||||
// Start from the first top-level field node and search recursively
|
||||
const targetField = findFieldNode(info.fieldNodes[0].selectionSet, fieldKey);
|
||||
|
||||
// If the field is not found, throw an error
|
||||
if (!targetField) {
|
||||
throw new Error(`Field "${fieldKey}" not found.`);
|
||||
}
|
||||
|
||||
// Extract the arguments from the field we've found
|
||||
const args: Record<string, any> = {};
|
||||
|
||||
if (targetField.arguments && targetField.arguments.length) {
|
||||
for (const arg of targetField.arguments) {
|
||||
args[arg.name.value] = parseValueNode(arg.value, info.variableValues);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
export const stringifyWithoutKeyQuote = (obj: any) => {
|
||||
const jsonString = JSON.stringify(obj);
|
||||
const jsonWithoutQuotes = jsonString?.replace(/"(\w+)"\s*:/g, '$1:');
|
||||
|
||||
return jsonWithoutQuotes;
|
||||
};
|
||||
@ -0,0 +1,126 @@
|
||||
import { Injectable, Logger } 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 {
|
||||
private readonly logger = new Logger(WorkspaceQueryBuilderFactory.name);
|
||||
|
||||
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<Filter extends RecordFilter = RecordFilter>(
|
||||
args: FindDuplicatesResolverArgs<Filter>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
existingRecord?: Record<string, unknown>,
|
||||
): Promise<string> {
|
||||
return this.findDuplicatesQueryFactory.create<Filter>(
|
||||
args,
|
||||
options,
|
||||
existingRecord,
|
||||
);
|
||||
}
|
||||
|
||||
findDuplicatesExistingRecord(
|
||||
id: string,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): string {
|
||||
return this.findDuplicatesQueryFactory.buildQueryForExistingRecord(
|
||||
id,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
createMany<Record extends IRecord = IRecord>(
|
||||
args: CreateManyResolverArgs<Record>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): Promise<string> {
|
||||
return this.createManyQueryFactory.create<Record>(args, options);
|
||||
}
|
||||
|
||||
updateOne<Record extends IRecord = IRecord>(
|
||||
initialArgs: UpdateOneResolverArgs<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<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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataModule } from 'src/engine-metadata/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 { WorkspaceQueryBuilderFactory } from './workspace-query-builder.factory';
|
||||
|
||||
import { workspaceQueryBuilderFactories } from './factories/factories';
|
||||
|
||||
@Module({
|
||||
imports: [ObjectMetadataModule],
|
||||
providers: [...workspaceQueryBuilderFactories, WorkspaceQueryBuilderFactory],
|
||||
exports: [
|
||||
WorkspaceQueryBuilderFactory,
|
||||
FieldsStringFactory,
|
||||
RecordPositionQueryFactory,
|
||||
],
|
||||
})
|
||||
export class WorkspaceQueryBuilderModule {}
|
||||
@ -0,0 +1,63 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
|
||||
|
||||
describe('QueryRunnerArgsFactory', () => {
|
||||
const recordPositionFactory = {
|
||||
create: jest.fn().mockResolvedValue(2),
|
||||
};
|
||||
const options = {
|
||||
fieldMetadataCollection: [
|
||||
{ name: 'position', type: FieldMetadataType.POSITION },
|
||||
] as FieldMetadataInterface[],
|
||||
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
||||
} as WorkspaceQueryRunnerOptions;
|
||||
|
||||
let factory: QueryRunnerArgsFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
QueryRunnerArgsFactory,
|
||||
{
|
||||
provide: RecordPositionFactory,
|
||||
useValue: {
|
||||
create: recordPositionFactory.create,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
factory = module.get<QueryRunnerArgsFactory>(QueryRunnerArgsFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(factory).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should simply return the args when data is an empty array', async () => {
|
||||
const args = {
|
||||
data: [],
|
||||
};
|
||||
const result = await factory.create(args, options);
|
||||
|
||||
expect(result).toEqual(args);
|
||||
});
|
||||
|
||||
it('should override args when of type array', async () => {
|
||||
const args = { data: [{ id: 1 }, { position: 'last' }] };
|
||||
|
||||
const result = await factory.create(args, options);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: [{ id: 1 }, { position: 2 }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,64 @@
|
||||
import { TestingModule, Test } from '@nestjs/testing';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
|
||||
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
|
||||
|
||||
describe('RecordPositionFactory', () => {
|
||||
const recordPositionQueryFactory = {
|
||||
create: jest.fn().mockResolvedValue('query'),
|
||||
};
|
||||
|
||||
const workspaceDataSourceService = {
|
||||
getSchemaName: jest.fn().mockReturnValue('schemaName'),
|
||||
executeRawQuery: jest.fn().mockResolvedValue([{ position: 1 }]),
|
||||
};
|
||||
|
||||
let factory: RecordPositionFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
RecordPositionFactory,
|
||||
{
|
||||
provide: RecordPositionQueryFactory,
|
||||
useValue: recordPositionQueryFactory,
|
||||
},
|
||||
{
|
||||
provide: WorkspaceDataSourceService,
|
||||
useValue: workspaceDataSourceService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
factory = module.get<RecordPositionFactory>(RecordPositionFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(factory).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
const objectMetadata = { isCustom: false, nameSingular: 'company' };
|
||||
const workspaceId = 'workspaceId';
|
||||
|
||||
it('should return the value when value is a number', async () => {
|
||||
const value = 1;
|
||||
const result = await factory.create(value, objectMetadata, workspaceId);
|
||||
|
||||
expect(result).toEqual(value);
|
||||
});
|
||||
it('should return the existing position / 2 when value is first', async () => {
|
||||
const value = 'first';
|
||||
const result = await factory.create(value, objectMetadata, workspaceId);
|
||||
|
||||
expect(result).toEqual(0.5);
|
||||
});
|
||||
it('should return the existing position + 1 when value is last', async () => {
|
||||
const value = 'last';
|
||||
const result = await factory.create(value, objectMetadata, workspaceId);
|
||||
|
||||
expect(result).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { RecordPositionFactory } from './record-position.factory';
|
||||
import { QueryRunnerArgsFactory } from './query-runner-args.factory';
|
||||
|
||||
export const workspaceQueryRunnerFactories = [
|
||||
QueryRunnerArgsFactory,
|
||||
RecordPositionFactory,
|
||||
];
|
||||
@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import { RecordPositionFactory } from './record-position.factory';
|
||||
|
||||
@Injectable()
|
||||
export class QueryRunnerArgsFactory {
|
||||
constructor(private readonly recordPositionFactory: RecordPositionFactory) {}
|
||||
|
||||
async create(
|
||||
args: Record<string, any>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
) {
|
||||
const fieldMetadataCollection = options.fieldMetadataCollection;
|
||||
|
||||
const fieldMetadataMap = new Map(
|
||||
fieldMetadataCollection.map((fieldMetadata) => [
|
||||
fieldMetadata.name,
|
||||
fieldMetadata,
|
||||
]),
|
||||
);
|
||||
|
||||
return {
|
||||
data: await Promise.all(
|
||||
args.data.map((arg) =>
|
||||
this.overrideArgByFieldMetadata(arg, options, fieldMetadataMap),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private async overrideArgByFieldMetadata(
|
||||
arg: Record<string, any>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
fieldMetadataMap: Map<string, FieldMetadataInterface>,
|
||||
) {
|
||||
const createArgPromiseByArgKey = Object.entries(arg).map(
|
||||
async ([key, value]) => {
|
||||
const fieldMetadata = fieldMetadataMap.get(key);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return [key, await Promise.resolve(value)];
|
||||
}
|
||||
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.POSITION:
|
||||
return [
|
||||
key,
|
||||
await this.recordPositionFactory.create(
|
||||
value,
|
||||
{
|
||||
isCustom: options.objectMetadataItem.isCustom,
|
||||
nameSingular: options.objectMetadataItem.nameSingular,
|
||||
},
|
||||
options.workspaceId,
|
||||
),
|
||||
];
|
||||
default:
|
||||
return [key, await Promise.resolve(value)];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const newArgEntries = await Promise.all(createArgPromiseByArgKey);
|
||||
|
||||
return Object.fromEntries(newArgEntries);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import {
|
||||
RecordPositionQueryFactory,
|
||||
RecordPositionQueryType,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
|
||||
|
||||
@Injectable()
|
||||
export class RecordPositionFactory {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
private readonly recordPositionQueryFactory: RecordPositionQueryFactory,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
value: number | 'first' | 'last',
|
||||
objectMetadata: { isCustom: boolean; nameSingular: string },
|
||||
workspaceId: string,
|
||||
): Promise<number> {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const query = await this.recordPositionQueryFactory.create(
|
||||
RecordPositionQueryType.GET,
|
||||
value,
|
||||
objectMetadata,
|
||||
dataSourceSchema,
|
||||
);
|
||||
|
||||
const records = await this.workspaceDataSourceService.executeRawQuery(
|
||||
query,
|
||||
[],
|
||||
workspaceId,
|
||||
undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
(value === 'first'
|
||||
? records[0]?.position / 2
|
||||
: records[0]?.position + 1) || 1
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { IPageInfo } from './page-info.interface';
|
||||
import { IEdge } from './edge.interface';
|
||||
|
||||
export interface IConnection<T, CustomEdge extends IEdge<T> = IEdge<T>> {
|
||||
edges: Array<CustomEdge>;
|
||||
pageInfo: IPageInfo;
|
||||
totalCount: number;
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export interface IEdge<T> {
|
||||
cursor: string;
|
||||
node: T;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
export interface IPageInfo {
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
startCursor?: string;
|
||||
endCursor?: string;
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
export interface PGGraphQLResponse<Data = any> {
|
||||
resolve: {
|
||||
data: Data;
|
||||
errors: any[];
|
||||
};
|
||||
}
|
||||
|
||||
export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>];
|
||||
|
||||
export interface PGGraphQLMutation<Record = IRecord> {
|
||||
affectedRows: number;
|
||||
records: Record[];
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
export interface WorkspaceQueryRunnerOptions {
|
||||
workspaceId: string;
|
||||
userId: string | undefined;
|
||||
info: GraphQLResolveInfo;
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
fieldMetadataCollection: FieldMetadataInterface[];
|
||||
objectMetadataCollection: ObjectMetadataInterface[];
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { ObjectMetadataService } from 'src/engine-metadata/object-metadata/object-metadata.service';
|
||||
import { DataSourceService } from 'src/engine-metadata/data-source/data-source.service';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import {
|
||||
CallWebhookJob,
|
||||
CallWebhookJobData,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job';
|
||||
|
||||
export enum CallWebhookJobsJobOperation {
|
||||
create = 'create',
|
||||
update = 'update',
|
||||
delete = 'delete',
|
||||
}
|
||||
|
||||
export type CallWebhookJobsJobData = {
|
||||
workspaceId: string;
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
record: any;
|
||||
operation: CallWebhookJobsJobOperation;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CallWebhookJobsJob
|
||||
implements MessageQueueJob<CallWebhookJobsJobData>
|
||||
{
|
||||
private readonly logger = new Logger(CallWebhookJobsJob.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
@Inject(MessageQueue.webhookQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
async handle(data: CallWebhookJobsJobData): Promise<void> {
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
data.workspaceId,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
data.workspaceId,
|
||||
);
|
||||
const nameSingular = data.objectMetadataItem.nameSingular;
|
||||
const operation = data.operation;
|
||||
const eventType = `${operation}.${nameSingular}`;
|
||||
const webhooks: { id: string; targetUrl: string }[] =
|
||||
await workspaceDataSource?.query(
|
||||
`
|
||||
SELECT * FROM ${dataSourceMetadata.schema}."webhook"
|
||||
WHERE operation LIKE '%${eventType}%'
|
||||
OR operation LIKE '%*.${nameSingular}%'
|
||||
OR operation LIKE '%${operation}.*%'
|
||||
OR operation LIKE '%*.*%'
|
||||
`,
|
||||
);
|
||||
|
||||
webhooks.forEach((webhook) => {
|
||||
this.messageQueueService.add<CallWebhookJobData>(
|
||||
CallWebhookJob.name,
|
||||
{
|
||||
targetUrl: webhook.targetUrl,
|
||||
eventType,
|
||||
objectMetadata: {
|
||||
id: data.objectMetadataItem.id,
|
||||
nameSingular: data.objectMetadataItem.nameSingular,
|
||||
},
|
||||
workspaceId: data.workspaceId,
|
||||
webhookId: webhook.id,
|
||||
eventDate: new Date(),
|
||||
record: data.record,
|
||||
},
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
});
|
||||
|
||||
if (webhooks.length) {
|
||||
this.logger.log(
|
||||
`CallWebhookJobsJob on eventType '${eventType}' called on webhooks ids [\n"${webhooks
|
||||
.map((webhook) => webhook.id)
|
||||
.join('",\n"')}"\n]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
|
||||
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||
|
||||
export type CallWebhookJobData = {
|
||||
targetUrl: string;
|
||||
eventType: string;
|
||||
objectMetadata: { id: string; nameSingular: string };
|
||||
workspaceId: string;
|
||||
webhookId: string;
|
||||
eventDate: Date;
|
||||
record: any;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CallWebhookJob implements MessageQueueJob<CallWebhookJobData> {
|
||||
private readonly logger = new Logger(CallWebhookJob.name);
|
||||
|
||||
constructor(private readonly httpService: HttpService) {}
|
||||
|
||||
async handle(data: CallWebhookJobData): Promise<void> {
|
||||
try {
|
||||
await this.httpService.axiosRef.post(data.targetUrl, data);
|
||||
this.logger.log(
|
||||
`CallWebhookJob successfully called on targetUrl '${data.targetUrl}'`,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Error calling webhook on targetUrl '${data.targetUrl}': ${err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||
|
||||
import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
|
||||
|
||||
export type RecordPositionBackfillJobData = {
|
||||
workspaceId: string;
|
||||
objectMetadata: { nameSingular: string; isCustom: boolean };
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RecordPositionBackfillJob
|
||||
implements MessageQueueJob<RecordPositionBackfillJobData>
|
||||
{
|
||||
constructor(
|
||||
private readonly recordPositionBackfillService: RecordPositionBackfillService,
|
||||
) {}
|
||||
|
||||
async handle(data: RecordPositionBackfillJobData): Promise<void> {
|
||||
this.recordPositionBackfillService.backfill(
|
||||
data.workspaceId,
|
||||
data.objectMetadata,
|
||||
data.recordId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import {
|
||||
CreatedObjectMetadata,
|
||||
ObjectRecordCreateEvent,
|
||||
} from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import {
|
||||
RecordPositionBackfillJob,
|
||||
RecordPositionBackfillJobData,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/jobs/record-position-backfill.job';
|
||||
|
||||
@Injectable()
|
||||
export class RecordPositionListener {
|
||||
constructor(
|
||||
@Inject(MessageQueue.recordPositionBackfillQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
@OnEvent('*.created')
|
||||
async handleAllCreate(payload: ObjectRecordCreateEvent<any>) {
|
||||
if (!hasPositionField(payload.createdObjectMetadata)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasPositionSet(payload.createdRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageQueueService.add<RecordPositionBackfillJobData>(
|
||||
RecordPositionBackfillJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
recordId: payload.createdRecord.id,
|
||||
objectMetadata: payload.createdObjectMetadata,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hasPositionField = (
|
||||
createdObjectMetadata: CreatedObjectMetadata,
|
||||
): boolean => {
|
||||
return (
|
||||
createdObjectMetadata.isCustom ||
|
||||
['opportunity', 'company', 'people'].includes(
|
||||
createdObjectMetadata.nameSingular,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const hasPositionSet = (createdRecord: any): boolean => {
|
||||
return !!createdRecord?.position;
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
|
||||
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
|
||||
import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule],
|
||||
providers: [
|
||||
RecordPositionFactory,
|
||||
RecordPositionQueryFactory,
|
||||
RecordPositionBackfillService,
|
||||
],
|
||||
exports: [RecordPositionBackfillService],
|
||||
})
|
||||
export class RecordPositionBackfillModule {}
|
||||
@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import {
|
||||
RecordPositionQueryFactory,
|
||||
RecordPositionQueryType,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
|
||||
import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory';
|
||||
|
||||
@Injectable()
|
||||
export class RecordPositionBackfillService {
|
||||
constructor(
|
||||
private readonly recordPositionFactory: RecordPositionFactory,
|
||||
private readonly recordPositionQueryFactory: RecordPositionQueryFactory,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
async backfill(
|
||||
workspaceId: string,
|
||||
objectMetadata: { nameSingular: string; isCustom: boolean },
|
||||
recordId: string,
|
||||
) {
|
||||
const position = await this.recordPositionFactory.create(
|
||||
'last',
|
||||
objectMetadata as ObjectMetadataInterface,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const query = await this.recordPositionQueryFactory.create(
|
||||
RecordPositionQueryType.UPDATE,
|
||||
position,
|
||||
objectMetadata as ObjectMetadataInterface,
|
||||
dataSourceSchema,
|
||||
);
|
||||
|
||||
this.workspaceDataSourceService.executeRawQuery(
|
||||
query,
|
||||
[position, recordId],
|
||||
workspaceId,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
import {
|
||||
isSpecialKey,
|
||||
handleSpecialKey,
|
||||
parseResult,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util';
|
||||
|
||||
describe('isSpecialKey', () => {
|
||||
test('should return true if the key starts with "___"', () => {
|
||||
expect(isSpecialKey('___specialKey')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if the key does not start with "___"', () => {
|
||||
expect(isSpecialKey('normalKey')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSpecialKey', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
result = {};
|
||||
});
|
||||
|
||||
test('should correctly process a special key and add it to the result object', () => {
|
||||
handleSpecialKey(result, '___complexField_link', 'value1');
|
||||
expect(result).toEqual({
|
||||
complexField: {
|
||||
link: 'value1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should add values under the same newKey if called multiple times', () => {
|
||||
handleSpecialKey(result, '___complexField_link', 'value1');
|
||||
handleSpecialKey(result, '___complexField_text', 'value2');
|
||||
expect(result).toEqual({
|
||||
complexField: {
|
||||
link: 'value1',
|
||||
text: 'value2',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should not create a new field if the special key is not correctly formed', () => {
|
||||
handleSpecialKey(result, '___complexField', 'value1');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResult', () => {
|
||||
test('should recursively parse an object and handle special keys', () => {
|
||||
const obj = {
|
||||
normalField: 'value1',
|
||||
___specialField_part1: 'value2',
|
||||
nested: {
|
||||
___specialFieldNested_part2: 'value3',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedResult = {
|
||||
normalField: 'value1',
|
||||
specialField: {
|
||||
part1: 'value2',
|
||||
},
|
||||
nested: {
|
||||
specialFieldNested: {
|
||||
part2: 'value3',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(parseResult(obj)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('should handle arrays and parse each element', () => {
|
||||
const objArray = [
|
||||
{
|
||||
___specialField_part1: 'value1',
|
||||
},
|
||||
{
|
||||
___specialField_part2: 'value2',
|
||||
},
|
||||
];
|
||||
|
||||
const expectedResult = [
|
||||
{
|
||||
specialField: {
|
||||
part1: 'value1',
|
||||
},
|
||||
},
|
||||
{
|
||||
specialField: {
|
||||
part2: 'value2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(parseResult(objArray)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('should return the original value if it is not an object or array', () => {
|
||||
expect(parseResult('stringValue')).toBe('stringValue');
|
||||
expect(parseResult(12345)).toBe(12345);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
HttpException,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
interface PgGraphQLErrorMapping {
|
||||
[key: string]: (command: string, objectName: string) => HttpException;
|
||||
}
|
||||
|
||||
const pgGraphQLErrorMapping: PgGraphQLErrorMapping = {
|
||||
'delete impacts too many records': (command, objectName) =>
|
||||
new BadRequestException(
|
||||
`Cannot ${command} ${objectName} because it impacts too many records.`,
|
||||
),
|
||||
};
|
||||
|
||||
export const computePgGraphQLError = (
|
||||
command: string,
|
||||
objectName: string,
|
||||
errors: any[],
|
||||
) => {
|
||||
const error = errors[0];
|
||||
const errorMessage = error?.message;
|
||||
const mappedError = pgGraphQLErrorMapping[errorMessage];
|
||||
|
||||
if (mappedError) {
|
||||
return mappedError(command, objectName);
|
||||
}
|
||||
|
||||
return new InternalServerErrorException(
|
||||
`GraphQL errors on ${command}${objectName}: ${JSON.stringify(error)}`,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,53 @@
|
||||
export const isSpecialKey = (key: string): boolean => {
|
||||
return key.startsWith('___');
|
||||
};
|
||||
|
||||
export const handleSpecialKey = (
|
||||
result: any,
|
||||
key: string,
|
||||
value: any,
|
||||
): void => {
|
||||
const parts = key.split('_').filter((part) => part);
|
||||
|
||||
// If parts don't contain enough information, return without altering result
|
||||
if (parts.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newKey = parts.slice(0, -1).join('');
|
||||
const subKey = parts[parts.length - 1];
|
||||
|
||||
if (!result[newKey]) {
|
||||
result[newKey] = {};
|
||||
}
|
||||
|
||||
result[newKey][subKey] = value;
|
||||
};
|
||||
|
||||
export const parseResult = (obj: any): any => {
|
||||
if (obj === null || typeof obj !== 'object' || typeof obj === 'function') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => parseResult(item));
|
||||
}
|
||||
|
||||
const result: any = {};
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
result[key] = parseResult(obj[key]);
|
||||
} else if (key === '__typename') {
|
||||
result[key] = obj[key].replace(/^_*/, '');
|
||||
} else if (isSpecialKey(key)) {
|
||||
handleSpecialKey(result, key, obj[key]);
|
||||
} else {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { ResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
export interface WorkspacePreQueryHook {
|
||||
execute(
|
||||
userId: string | undefined,
|
||||
workspaceId: string,
|
||||
payload: ResolverArgs,
|
||||
): Promise<void>;
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
import {
|
||||
CreateManyResolverArgs,
|
||||
CreateOneResolverArgs,
|
||||
DeleteManyResolverArgs,
|
||||
DeleteOneResolverArgs,
|
||||
FindDuplicatesResolverArgs,
|
||||
FindManyResolverArgs,
|
||||
FindOneResolverArgs,
|
||||
UpdateManyResolverArgs,
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
export type ExecutePreHookMethod =
|
||||
| 'createMany'
|
||||
| 'createOne'
|
||||
| 'deleteMany'
|
||||
| 'deleteOne'
|
||||
| 'findMany'
|
||||
| 'findOne'
|
||||
| 'findDuplicates'
|
||||
| 'updateMany'
|
||||
| 'updateOne';
|
||||
|
||||
export type ObjectName = string;
|
||||
|
||||
export type HookName = string;
|
||||
|
||||
export type WorkspaceQueryHook = {
|
||||
[key in ObjectName]: {
|
||||
[key in ExecutePreHookMethod]?: HookName[];
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkspacePreQueryHookPayload<T> = T extends 'createMany'
|
||||
? CreateManyResolverArgs
|
||||
: T extends 'createOne'
|
||||
? CreateOneResolverArgs
|
||||
: T extends 'deleteMany'
|
||||
? DeleteManyResolverArgs
|
||||
: T extends 'deleteOne'
|
||||
? DeleteOneResolverArgs
|
||||
: T extends 'findMany'
|
||||
? FindManyResolverArgs
|
||||
: T extends 'findOne'
|
||||
? FindOneResolverArgs
|
||||
: T extends 'updateMany'
|
||||
? UpdateManyResolverArgs
|
||||
: T extends 'updateOne'
|
||||
? UpdateOneResolverArgs
|
||||
: T extends 'findDuplicates'
|
||||
? FindDuplicatesResolverArgs
|
||||
: never;
|
||||
@ -0,0 +1,11 @@
|
||||
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/query-hooks/message/message-find-many.pre-query.hook';
|
||||
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/query-hooks/message/message-find-one.pre-query-hook';
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type';
|
||||
|
||||
// TODO: move to a decorator
|
||||
export const workspacePreQueryHooks: WorkspaceQueryHook = {
|
||||
message: {
|
||||
findOne: [MessageFindOnePreQueryHook.name],
|
||||
findMany: [MessageFindManyPreQueryHook.name],
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { MessagingQueryHookModule } from 'src/modules/messaging/query-hooks/messaging-query-hook.module';
|
||||
import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
|
||||
|
||||
@Module({
|
||||
imports: [MessagingQueryHookModule],
|
||||
providers: [WorkspacePreQueryHookService],
|
||||
exports: [WorkspacePreQueryHookService],
|
||||
})
|
||||
export class WorkspacePreQueryHookModule {}
|
||||
@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
|
||||
|
||||
import {
|
||||
ExecutePreHookMethod,
|
||||
WorkspacePreQueryHookPayload,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type';
|
||||
import { workspacePreQueryHooks } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspacePreQueryHookService {
|
||||
constructor(private readonly workspaceQueryHookModuleRef: ModuleRef) {}
|
||||
|
||||
public async executePreHooks<T extends ExecutePreHookMethod>(
|
||||
userId: string | undefined,
|
||||
workspaceId: string,
|
||||
objectName: string,
|
||||
method: T,
|
||||
payload: WorkspacePreQueryHookPayload<T>,
|
||||
): Promise<void> {
|
||||
const hooks = workspacePreQueryHooks[objectName] || [];
|
||||
|
||||
for (const hookName of Object.values(hooks[method] ?? [])) {
|
||||
const hook: WorkspacePreQueryHook =
|
||||
await this.workspaceQueryHookModuleRef.get(hookName, {
|
||||
strict: false,
|
||||
});
|
||||
|
||||
await hook.execute(userId, workspaceId, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspacePreQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module';
|
||||
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
|
||||
import { RecordPositionListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceQueryBuilderModule,
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspacePreQueryHookModule,
|
||||
],
|
||||
providers: [
|
||||
WorkspaceQueryRunnerService,
|
||||
...workspaceQueryRunnerFactories,
|
||||
RecordPositionListener,
|
||||
],
|
||||
exports: [WorkspaceQueryRunnerService],
|
||||
})
|
||||
export class WorkspaceQueryRunnerModule {}
|
||||
@ -0,0 +1,519 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import {
|
||||
Record as IRecord,
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import {
|
||||
CreateManyResolverArgs,
|
||||
CreateOneResolverArgs,
|
||||
DeleteManyResolverArgs,
|
||||
DeleteOneResolverArgs,
|
||||
FindDuplicatesResolverArgs,
|
||||
FindManyResolverArgs,
|
||||
FindOneResolverArgs,
|
||||
UpdateManyResolverArgs,
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { WorkspaceQueryBuilderFactory } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import {
|
||||
CallWebhookJobsJob,
|
||||
CallWebhookJobsJobData,
|
||||
CallWebhookJobsJobOperation,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job';
|
||||
import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
|
||||
import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { NotFoundError } from 'src/engine/filters/utils/graphql-errors.util';
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
|
||||
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
|
||||
import {
|
||||
PGGraphQLMutation,
|
||||
PGGraphQLResult,
|
||||
} from './interfaces/pg-graphql.interface';
|
||||
import { computePgGraphQLError } from './utils/compute-pg-graphql-error.util';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceQueryRunnerService {
|
||||
private readonly logger = new Logger(WorkspaceQueryRunnerService.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
|
||||
@Inject(MessageQueue.webhookQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly workspacePreQueryHookService: WorkspacePreQueryHookService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async findMany<
|
||||
Record extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
>(
|
||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<Record> | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const start = performance.now();
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.findMany(
|
||||
args,
|
||||
options,
|
||||
);
|
||||
|
||||
await this.workspacePreQueryHookService.executePreHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const end = performance.now();
|
||||
|
||||
this.logger.log(
|
||||
`query time: ${end - start} ms on query ${
|
||||
options.objectMetadataItem.nameSingular
|
||||
}`,
|
||||
);
|
||||
|
||||
return this.parseResult<IConnection<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne<
|
||||
Record extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
>(
|
||||
args: FindOneResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
if (!args.filter || Object.keys(args.filter).length === 0) {
|
||||
throw new BadRequestException('Missing filter argument');
|
||||
}
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const query = await this.workspaceQueryBuilderFactory.findOne(
|
||||
args,
|
||||
options,
|
||||
);
|
||||
|
||||
await this.workspacePreQueryHookService.executePreHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const parsedResult = this.parseResult<IConnection<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
);
|
||||
|
||||
return parsedResult?.edges?.[0]?.node;
|
||||
}
|
||||
|
||||
async findDuplicates<TRecord extends IRecord = IRecord>(
|
||||
args: FindDuplicatesResolverArgs<TRecord>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<TRecord> | undefined> {
|
||||
if (!args.data && !args.id) {
|
||||
throw new BadRequestException(
|
||||
'You have to provide either "data" or "id" argument',
|
||||
);
|
||||
}
|
||||
|
||||
if (!args.id && isEmpty(args.data)) {
|
||||
throw new BadRequestException(
|
||||
'The "data" condition can not be empty when ID input not provided',
|
||||
);
|
||||
}
|
||||
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
|
||||
let existingRecord: Record<string, unknown> | undefined;
|
||||
|
||||
if (args.id) {
|
||||
const existingRecordQuery =
|
||||
this.workspaceQueryBuilderFactory.findDuplicatesExistingRecord(
|
||||
args.id,
|
||||
options,
|
||||
);
|
||||
|
||||
const existingRecordResult = await this.execute(
|
||||
existingRecordQuery,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const parsedResult = this.parseResult<Record<string, unknown>>(
|
||||
existingRecordResult,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
);
|
||||
|
||||
existingRecord = parsedResult?.edges?.[0]?.node;
|
||||
|
||||
if (!existingRecord) {
|
||||
throw new NotFoundError(`Object with id ${args.id} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.findDuplicates(
|
||||
args,
|
||||
options,
|
||||
existingRecord,
|
||||
);
|
||||
|
||||
await this.workspacePreQueryHookService.executePreHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findDuplicates',
|
||||
args,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
|
||||
return this.parseResult<IConnection<TRecord>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
async createMany<Record extends IRecord = IRecord>(
|
||||
args: CreateManyResolverArgs<Record>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
const computedArgs = await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.createMany(
|
||||
computedArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
|
||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'insertInto',
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.create,
|
||||
options,
|
||||
);
|
||||
|
||||
parsedResults.forEach((record) => {
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
|
||||
workspaceId,
|
||||
createdRecord: this.removeNestedProperties(record),
|
||||
createdObjectMetadata: {
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isCustom: objectMetadataItem.isCustom,
|
||||
},
|
||||
} satisfies ObjectRecordCreateEvent<any>);
|
||||
});
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
async createOne<Record extends IRecord = IRecord>(
|
||||
args: CreateOneResolverArgs<Record>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const results = await this.createMany({ data: [args.data] }, options);
|
||||
|
||||
return results?.[0];
|
||||
}
|
||||
|
||||
async updateOne<Record extends IRecord = IRecord>(
|
||||
args: UpdateOneResolverArgs<Record>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
|
||||
const existingRecord = await this.findOne(
|
||||
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
||||
args,
|
||||
options,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
|
||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.update,
|
||||
options,
|
||||
);
|
||||
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
|
||||
workspaceId,
|
||||
previousRecord: this.removeNestedProperties(existingRecord as Record),
|
||||
updatedRecord: this.removeNestedProperties(parsedResults?.[0]),
|
||||
} satisfies ObjectRecordUpdateEvent<any>);
|
||||
|
||||
return parsedResults?.[0];
|
||||
}
|
||||
|
||||
async updateMany<Record extends IRecord = IRecord>(
|
||||
args: UpdateManyResolverArgs<Record>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_RECORD_AFFECTED',
|
||||
);
|
||||
const query = await this.workspaceQueryBuilderFactory.updateMany(args, {
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
});
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
|
||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.update,
|
||||
options,
|
||||
);
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
async deleteMany<
|
||||
Record extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
>(
|
||||
args: DeleteManyResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_RECORD_AFFECTED',
|
||||
);
|
||||
const query = await this.workspaceQueryBuilderFactory.deleteMany(args, {
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
});
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
|
||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'deleteFrom',
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.delete,
|
||||
options,
|
||||
);
|
||||
|
||||
parsedResults.forEach((record) => {
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
||||
workspaceId,
|
||||
deletedRecord: [this.removeNestedProperties(record)],
|
||||
} satisfies ObjectRecordDeleteEvent<any>);
|
||||
});
|
||||
|
||||
return parsedResults;
|
||||
}
|
||||
|
||||
async deleteOne<Record extends IRecord = IRecord>(
|
||||
args: DeleteOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
||||
args,
|
||||
options,
|
||||
);
|
||||
const result = await this.execute(query, workspaceId);
|
||||
|
||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'deleteFrom',
|
||||
)?.records;
|
||||
|
||||
await this.triggerWebhooks<Record>(
|
||||
parsedResults,
|
||||
CallWebhookJobsJobOperation.delete,
|
||||
options,
|
||||
);
|
||||
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
||||
workspaceId,
|
||||
deletedRecord: this.removeNestedProperties(parsedResults?.[0]),
|
||||
} satisfies ObjectRecordDeleteEvent<any>);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
sanitizedRecord[key] = value;
|
||||
}
|
||||
|
||||
return sanitizedRecord;
|
||||
}
|
||||
|
||||
async execute(
|
||||
query: string,
|
||||
workspaceId: string,
|
||||
): Promise<PGGraphQLResult | undefined> {
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await workspaceDataSource?.query(`
|
||||
SET search_path TO ${this.workspaceDataSourceService.getSchemaName(
|
||||
workspaceId,
|
||||
)};
|
||||
`);
|
||||
|
||||
const results = await workspaceDataSource?.query<PGGraphQLResult>(`
|
||||
SELECT graphql.resolve($$
|
||||
${query}
|
||||
$$);
|
||||
`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private parseResult<Result>(
|
||||
graphqlResult: PGGraphQLResult | undefined,
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
command: string,
|
||||
): Result {
|
||||
const entityKey = `${command}${computeObjectTargetTable(
|
||||
objectMetadataItem,
|
||||
)}Collection`;
|
||||
const result = graphqlResult?.[0]?.resolve?.data?.[entityKey];
|
||||
const errors = graphqlResult?.[0]?.resolve?.errors;
|
||||
|
||||
if (!result) {
|
||||
this.logger.log(
|
||||
`No result found for ${entityKey}, graphqlResult: ` +
|
||||
JSON.stringify(graphqlResult, null, 3),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
result &&
|
||||
['update', 'deleteFrom'].includes(command) &&
|
||||
!result.affectedCount
|
||||
) {
|
||||
throw new BadRequestException('No rows were affected.');
|
||||
}
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
const error = computePgGraphQLError(
|
||||
command,
|
||||
objectMetadataItem.nameSingular,
|
||||
errors,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return parseResult(result);
|
||||
}
|
||||
|
||||
async executeAndParse<Result>(
|
||||
query: string,
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
command: string,
|
||||
workspaceId: string,
|
||||
): Promise<Result> {
|
||||
const result = await this.execute(query, workspaceId);
|
||||
|
||||
return this.parseResult(result, objectMetadataItem, command);
|
||||
}
|
||||
|
||||
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.workspaceId,
|
||||
operation,
|
||||
objectMetadataItem: options.objectMetadataItem,
|
||||
},
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import { RecordDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
/**
|
||||
* objectName: directly reference the name of the object from the metadata tables.
|
||||
* columnNames: reference the column names not the field names.
|
||||
* So if we need to reference a custom field, we should directly add the column name like `_customColumn`.
|
||||
* If we need to terence a composite field, we should add all children of the composite like `nameFirstName` and `nameLastName`
|
||||
*/
|
||||
export const duplicateCriteriaCollection: RecordDuplicateCriteria[] = [
|
||||
{
|
||||
objectName: 'company',
|
||||
columnNames: ['domainName'],
|
||||
},
|
||||
{
|
||||
objectName: 'company',
|
||||
columnNames: ['name'],
|
||||
},
|
||||
{
|
||||
objectName: 'person',
|
||||
columnNames: ['nameFirstName', 'nameLastName'],
|
||||
},
|
||||
{
|
||||
objectName: 'person',
|
||||
columnNames: ['linkedinLinkUrl'],
|
||||
},
|
||||
{
|
||||
objectName: 'person',
|
||||
columnNames: ['email'],
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CreateManyResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreateManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'createMany' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<CreateManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.createMany(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CreateOneResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreateOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'createOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<CreateOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.createOne(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
DeleteManyResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'deleteMany' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<DeleteManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.deleteMany(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
DeleteOneResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'deleteOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<DeleteOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.deleteOne(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
Resolver,
|
||||
FindOneResolverArgs,
|
||||
ExecuteQuickActionOnOneResolverArgs,
|
||||
DeleteOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
import { QuickActionsService } from 'src/engine/modules/quick-actions/quick-actions.service';
|
||||
|
||||
@Injectable()
|
||||
export class ExecuteQuickActionOnOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'executeQuickActionOnOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
private readonly quickActionsService: QuickActionsService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<ExecuteQuickActionOnOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.executeQuickActionOnOne(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
userId: internalContext.userId,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private async executeQuickActionOnOne<Record extends IRecord = IRecord>(
|
||||
args: DeleteOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
switch (options.objectMetadataItem.nameSingular) {
|
||||
case 'company': {
|
||||
await this.quickActionsService.executeQuickActionOnCompany(
|
||||
args.id,
|
||||
options.workspaceId,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'person': {
|
||||
await this.quickActionsService.createCompanyFromPerson(
|
||||
args.id,
|
||||
options.workspaceId,
|
||||
options.objectMetadataCollection,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// TODO: different quick actions per object on frontend
|
||||
break;
|
||||
}
|
||||
|
||||
return this.workspaceQueryRunnerService.findOne(
|
||||
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||
|
||||
import { FindDuplicatesResolverFactory } from './find-duplicates-resolver.factory';
|
||||
import { FindManyResolverFactory } from './find-many-resolver.factory';
|
||||
import { FindOneResolverFactory } from './find-one-resolver.factory';
|
||||
import { CreateManyResolverFactory } from './create-many-resolver.factory';
|
||||
import { CreateOneResolverFactory } from './create-one-resolver.factory';
|
||||
import { UpdateOneResolverFactory } from './update-one-resolver.factory';
|
||||
import { DeleteOneResolverFactory } from './delete-one-resolver.factory';
|
||||
import { DeleteManyResolverFactory } from './delete-many-resolver.factory';
|
||||
import { ExecuteQuickActionOnOneResolverFactory } from './execute-quick-action-on-one-resolver.factory';
|
||||
|
||||
export const workspaceResolverBuilderFactories = [
|
||||
FindManyResolverFactory,
|
||||
FindOneResolverFactory,
|
||||
FindDuplicatesResolverFactory,
|
||||
CreateManyResolverFactory,
|
||||
CreateOneResolverFactory,
|
||||
UpdateOneResolverFactory,
|
||||
DeleteOneResolverFactory,
|
||||
ExecuteQuickActionOnOneResolverFactory,
|
||||
UpdateManyResolverFactory,
|
||||
DeleteManyResolverFactory,
|
||||
];
|
||||
|
||||
export const workspaceResolverBuilderMethodNames = {
|
||||
queries: [
|
||||
FindManyResolverFactory.methodName,
|
||||
FindOneResolverFactory.methodName,
|
||||
FindDuplicatesResolverFactory.methodName,
|
||||
],
|
||||
mutations: [
|
||||
CreateManyResolverFactory.methodName,
|
||||
CreateOneResolverFactory.methodName,
|
||||
UpdateOneResolverFactory.methodName,
|
||||
DeleteOneResolverFactory.methodName,
|
||||
ExecuteQuickActionOnOneResolverFactory.methodName,
|
||||
UpdateManyResolverFactory.methodName,
|
||||
DeleteManyResolverFactory.methodName,
|
||||
],
|
||||
} as const;
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
FindDuplicatesResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class FindDuplicatesResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'findDuplicates' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<FindDuplicatesResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.findDuplicates(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
FindManyResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class FindManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'findMany' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<FindManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.findMany(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
FindOneResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class FindOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'findOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<FindOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.findOne(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
Resolver,
|
||||
UpdateManyResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'updateMany' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<UpdateManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.updateMany(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
Resolver,
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'updateOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<UpdateOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.updateOne(args, {
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
export interface PGGraphQLResponse<Data = any> {
|
||||
resolve: {
|
||||
data: Data;
|
||||
};
|
||||
}
|
||||
|
||||
export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>];
|
||||
|
||||
export interface PGGraphQLMutation<Record = IRecord> {
|
||||
affectedRows: number;
|
||||
records: Record[];
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { Resolver } from './workspace-resolvers-builder.interface';
|
||||
|
||||
export interface WorkspaceResolverBuilderFactoryInterface {
|
||||
create(context: WorkspaceSchemaBuilderContext): Resolver;
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import { GraphQLFieldResolver } from 'graphql';
|
||||
|
||||
import {
|
||||
Record,
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
||||
|
||||
export type Resolver<Args = any> = GraphQLFieldResolver<any, any, Args>;
|
||||
|
||||
export interface FindManyResolverArgs<
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
> {
|
||||
first?: number;
|
||||
last?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
filter?: Filter;
|
||||
orderBy?: OrderBy;
|
||||
}
|
||||
|
||||
export interface FindOneResolverArgs<Filter = any> {
|
||||
filter?: Filter;
|
||||
}
|
||||
|
||||
export interface FindDuplicatesResolverArgs<Data extends Record = Record> {
|
||||
id?: string;
|
||||
data?: Data;
|
||||
}
|
||||
|
||||
export interface CreateOneResolverArgs<Data extends Record = Record> {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export interface CreateManyResolverArgs<Data extends Record = Record> {
|
||||
data: Data[];
|
||||
}
|
||||
|
||||
export interface UpdateOneResolverArgs<Data extends Record = Record> {
|
||||
id: string;
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export interface UpdateManyResolverArgs<
|
||||
Data extends Record = Record,
|
||||
Filter = any,
|
||||
> {
|
||||
filter: Filter;
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export interface DeleteOneResolverArgs {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ExecuteQuickActionOnOneResolverArgs {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DeleteManyResolverArgs<Filter = any> {
|
||||
filter: Filter;
|
||||
}
|
||||
|
||||
export type WorkspaceResolverBuilderQueryMethodNames =
|
||||
(typeof workspaceResolverBuilderMethodNames.queries)[number];
|
||||
|
||||
export type WorkspaceResolverBuilderMutationMethodNames =
|
||||
(typeof workspaceResolverBuilderMethodNames.mutations)[number];
|
||||
|
||||
export type WorkspaceResolverBuilderMethodNames =
|
||||
| WorkspaceResolverBuilderQueryMethodNames
|
||||
| WorkspaceResolverBuilderMutationMethodNames;
|
||||
|
||||
export interface WorkspaceResolverBuilderMethods {
|
||||
readonly queries: readonly WorkspaceResolverBuilderQueryMethodNames[];
|
||||
readonly mutations: readonly WorkspaceResolverBuilderMutationMethodNames[];
|
||||
}
|
||||
|
||||
export type ResolverArgs =
|
||||
| CreateManyResolverArgs
|
||||
| CreateOneResolverArgs
|
||||
| DeleteManyResolverArgs
|
||||
| DeleteOneResolverArgs
|
||||
| FindManyResolverArgs
|
||||
| FindOneResolverArgs
|
||||
| FindDuplicatesResolverArgs
|
||||
| UpdateManyResolverArgs
|
||||
| UpdateOneResolverArgs;
|
||||
@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
||||
import { QuickActionsModule } from 'src/engine/modules/quick-actions/quick-actions.module';
|
||||
|
||||
import { WorkspaceResolverFactory } from './workspace-resolver.factory';
|
||||
|
||||
import { workspaceResolverBuilderFactories } from './factories/factories';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceQueryRunnerModule, QuickActionsModule],
|
||||
providers: [...workspaceResolverBuilderFactories, WorkspaceResolverFactory],
|
||||
exports: [WorkspaceResolverFactory],
|
||||
})
|
||||
export class WorkspaceResolverBuilderModule {}
|
||||
@ -0,0 +1,120 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { IResolvers } from '@graphql-tools/utils';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
||||
import { ExecuteQuickActionOnOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory';
|
||||
|
||||
import { FindDuplicatesResolverFactory } from './factories/find-duplicates-resolver.factory';
|
||||
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
|
||||
import { FindOneResolverFactory } from './factories/find-one-resolver.factory';
|
||||
import { CreateManyResolverFactory } from './factories/create-many-resolver.factory';
|
||||
import { CreateOneResolverFactory } from './factories/create-one-resolver.factory';
|
||||
import { UpdateOneResolverFactory } from './factories/update-one-resolver.factory';
|
||||
import { DeleteOneResolverFactory } from './factories/delete-one-resolver.factory';
|
||||
import {
|
||||
WorkspaceResolverBuilderMethodNames,
|
||||
WorkspaceResolverBuilderMethods,
|
||||
} from './interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from './interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceResolverFactory {
|
||||
private readonly logger = new Logger(WorkspaceResolverFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly findManyResolverFactory: FindManyResolverFactory,
|
||||
private readonly findOneResolverFactory: FindOneResolverFactory,
|
||||
private readonly findDuplicatesResolverFactory: FindDuplicatesResolverFactory,
|
||||
private readonly createManyResolverFactory: CreateManyResolverFactory,
|
||||
private readonly createOneResolverFactory: CreateOneResolverFactory,
|
||||
private readonly updateOneResolverFactory: UpdateOneResolverFactory,
|
||||
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
|
||||
private readonly executeQuickActionOnOneResolverFactory: ExecuteQuickActionOnOneResolverFactory,
|
||||
private readonly updateManyResolverFactory: UpdateManyResolverFactory,
|
||||
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
workspaceId: string,
|
||||
userId: string | undefined,
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
|
||||
): Promise<IResolvers> {
|
||||
const factories = new Map<
|
||||
WorkspaceResolverBuilderMethodNames,
|
||||
WorkspaceResolverBuilderFactoryInterface
|
||||
>([
|
||||
['findMany', this.findManyResolverFactory],
|
||||
['findOne', this.findOneResolverFactory],
|
||||
['findDuplicates', this.findDuplicatesResolverFactory],
|
||||
['createMany', this.createManyResolverFactory],
|
||||
['createOne', this.createOneResolverFactory],
|
||||
['updateOne', this.updateOneResolverFactory],
|
||||
['deleteOne', this.deleteOneResolverFactory],
|
||||
['executeQuickActionOnOne', this.executeQuickActionOnOneResolverFactory],
|
||||
['updateMany', this.updateManyResolverFactory],
|
||||
['deleteMany', this.deleteManyResolverFactory],
|
||||
]);
|
||||
const resolvers: IResolvers = {
|
||||
Query: {},
|
||||
Mutation: {},
|
||||
};
|
||||
|
||||
for (const objectMetadata of objectMetadataCollection) {
|
||||
// Generate query resolvers
|
||||
for (const methodName of workspaceResolverBuilderMethods.queries) {
|
||||
const resolverName = getResolverName(objectMetadata, methodName);
|
||||
const resolverFactory = factories.get(methodName);
|
||||
|
||||
if (!resolverFactory) {
|
||||
this.logger.error(`Unknown query resolver type: ${methodName}`, {
|
||||
objectMetadata,
|
||||
methodName,
|
||||
resolverName,
|
||||
});
|
||||
|
||||
throw new Error(`Unknown query resolver type: ${methodName}`);
|
||||
}
|
||||
|
||||
resolvers.Query[resolverName] = resolverFactory.create({
|
||||
workspaceId,
|
||||
userId,
|
||||
objectMetadataItem: objectMetadata,
|
||||
fieldMetadataCollection: objectMetadata.fields,
|
||||
objectMetadataCollection: objectMetadataCollection,
|
||||
});
|
||||
}
|
||||
|
||||
// Generate mutation resolvers
|
||||
for (const methodName of workspaceResolverBuilderMethods.mutations) {
|
||||
const resolverName = getResolverName(objectMetadata, methodName);
|
||||
const resolverFactory = factories.get(methodName);
|
||||
|
||||
if (!resolverFactory) {
|
||||
this.logger.error(`Unknown mutation resolver type: ${methodName}`, {
|
||||
objectMetadata,
|
||||
methodName,
|
||||
resolverName,
|
||||
});
|
||||
|
||||
throw new Error(`Unknown mutation resolver type: ${methodName}`);
|
||||
}
|
||||
|
||||
resolvers.Mutation[resolverName] = resolverFactory.create({
|
||||
workspaceId,
|
||||
userId,
|
||||
objectMetadataItem: objectMetadata,
|
||||
fieldMetadataCollection: objectMetadata.fields,
|
||||
objectMetadataCollection: objectMetadataCollection,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return resolvers;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigArgumentMap } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ArgsMetadata } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
|
||||
@Injectable()
|
||||
export class ArgsFactory {
|
||||
private readonly logger = new Logger(ArgsFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
{ args, objectMetadataId }: ArgsMetadata,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigArgumentMap {
|
||||
const fieldConfigMap: GraphQLFieldConfigArgumentMap = {};
|
||||
|
||||
for (const key in args) {
|
||||
if (!args.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
const arg = args[key];
|
||||
|
||||
// Argument is a scalar type
|
||||
if (arg.type) {
|
||||
const fieldType = this.typeMapperService.mapToScalarType(
|
||||
arg.type,
|
||||
options.dateScalarMode,
|
||||
options.numberScalarMode,
|
||||
);
|
||||
|
||||
if (!fieldType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${arg.type.toString()}`,
|
||||
{
|
||||
arg,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${arg.type.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const gqlType = this.typeMapperService.mapToGqlType(fieldType, {
|
||||
defaultValue: arg.defaultValue,
|
||||
nullable: arg.isNullable,
|
||||
isArray: arg.isArray,
|
||||
});
|
||||
|
||||
fieldConfigMap[key] = {
|
||||
type: gqlType,
|
||||
};
|
||||
}
|
||||
|
||||
// Argument is an input type
|
||||
if (arg.kind) {
|
||||
const inputType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
objectMetadataId,
|
||||
arg.kind,
|
||||
);
|
||||
|
||||
if (!inputType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL input type for ${objectMetadataId}`,
|
||||
{
|
||||
objectMetadataId,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL input type for ${objectMetadataId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const gqlType = this.typeMapperService.mapToGqlType(inputType, {
|
||||
nullable: arg.isNullable,
|
||||
isArray: arg.isArray,
|
||||
});
|
||||
|
||||
fieldConfigMap[key] = {
|
||||
type: gqlType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fieldConfigMap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLInt, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import {
|
||||
ObjectTypeDefinition,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from './object-type-definition.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
|
||||
export enum ConnectionTypeDefinitionKind {
|
||||
Edge = 'Edge',
|
||||
PageInfo = 'PageInfo',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(ConnectionTypeDefinitionFactory.name);
|
||||
|
||||
constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Connection;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
fields.edges = {
|
||||
type: this.connectionTypeFactory.create(
|
||||
objectMetadata,
|
||||
ConnectionTypeDefinitionKind.Edge,
|
||||
options,
|
||||
{
|
||||
isArray: true,
|
||||
arrayDepth: 1,
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
fields.pageInfo = {
|
||||
type: this.connectionTypeFactory.create(
|
||||
objectMetadata,
|
||||
ConnectionTypeDefinitionKind.PageInfo,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
fields.totalCount = {
|
||||
type: GraphQLInt,
|
||||
description: 'Total number of records in the connection',
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { PageInfoType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/object';
|
||||
|
||||
import { ConnectionTypeDefinitionKind } from './connection-type-definition.factory';
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionTypeFactory {
|
||||
private readonly logger = new Logger(ConnectionTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ConnectionTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
if (kind === ConnectionTypeDefinitionKind.PageInfo) {
|
||||
return this.typeMapperService.mapToGqlType(PageInfoType, typeOptions);
|
||||
}
|
||||
|
||||
const edgeType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
kind as unknown as ObjectTypeDefinitionKind,
|
||||
);
|
||||
|
||||
if (!edgeType) {
|
||||
this.logger.error(
|
||||
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
{
|
||||
objectMetadata,
|
||||
buildOtions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(edgeType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import {
|
||||
ObjectTypeDefinition,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from './object-type-definition.factory';
|
||||
import { EdgeTypeFactory } from './edge-type.factory';
|
||||
|
||||
export enum EdgeTypeDefinitionKind {
|
||||
Node = 'Node',
|
||||
Cursor = 'Cursor',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EdgeTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(EdgeTypeDefinitionFactory.name);
|
||||
|
||||
constructor(private readonly edgeTypeFactory: EdgeTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Edge;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
fields.node = {
|
||||
type: this.edgeTypeFactory.create(
|
||||
objectMetadata,
|
||||
EdgeTypeDefinitionKind.Node,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
fields.cursor = {
|
||||
type: this.edgeTypeFactory.create(
|
||||
objectMetadata,
|
||||
EdgeTypeDefinitionKind.Cursor,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { CursorScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
import { EdgeTypeDefinitionKind } from './edge-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class EdgeTypeFactory {
|
||||
private readonly logger = new Logger(EdgeTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: EdgeTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
if (kind === EdgeTypeDefinitionKind.Cursor) {
|
||||
return this.typeMapperService.mapToGqlType(CursorScalarType, typeOptions);
|
||||
}
|
||||
|
||||
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
ObjectTypeDefinitionKind.Plain,
|
||||
);
|
||||
|
||||
if (!objectType) {
|
||||
this.logger.error(
|
||||
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
{
|
||||
objectMetadata,
|
||||
buildOtions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(objectType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLEnumType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import {
|
||||
FieldMetadataComplexOption,
|
||||
FieldMetadataDefaultOption,
|
||||
} from 'src/engine-metadata/field-metadata/dtos/options.input';
|
||||
import { isEnumFieldMetadataType } from 'src/engine-metadata/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
|
||||
export interface EnumTypeDefinition {
|
||||
target: string;
|
||||
type: GraphQLEnumType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EnumTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(EnumTypeDefinitionFactory.name);
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): EnumTypeDefinition[] {
|
||||
const enumTypeDefinitions: EnumTypeDefinition[] = [];
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
if (!isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
enumTypeDefinitions.push({
|
||||
target: fieldMetadata.id,
|
||||
type: this.generateEnum(
|
||||
objectMetadata.nameSingular,
|
||||
fieldMetadata,
|
||||
options,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return enumTypeDefinitions;
|
||||
}
|
||||
|
||||
private generateEnum(
|
||||
objectName: string,
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLEnumType {
|
||||
// FixMe: It's a hack until Typescript get fixed on union types for reduce function
|
||||
// https://github.com/microsoft/TypeScript/issues/36390
|
||||
const enumOptions = fieldMetadata.options as Array<
|
||||
FieldMetadataDefaultOption | FieldMetadataComplexOption
|
||||
>;
|
||||
|
||||
if (!enumOptions) {
|
||||
this.logger.error(
|
||||
`Enum options are not defined for ${fieldMetadata.name}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(`Enum options are not defined for ${fieldMetadata.name}`);
|
||||
}
|
||||
|
||||
return new GraphQLEnumType({
|
||||
name: `${pascalCase(objectName)}${pascalCase(fieldMetadata.name)}Enum`,
|
||||
description: fieldMetadata.description,
|
||||
values: enumOptions.reduce(
|
||||
(acc, enumOption) => {
|
||||
// Key must match this regex: /^[_A-Za-z][_0-9A-Za-z]+$/
|
||||
acc[enumOption.value] = {
|
||||
value: enumOption.value,
|
||||
description: enumOption.label,
|
||||
};
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: { value: string; description: string } },
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,166 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
GraphQLFieldConfigArgumentMap,
|
||||
GraphQLFieldConfigMap,
|
||||
GraphQLObjectType,
|
||||
} from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field';
|
||||
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import {
|
||||
RelationDirection,
|
||||
deduceRelationDirection,
|
||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
||||
import { RelationMetadataType } from 'src/engine-metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
import { RelationTypeFactory } from './relation-type.factory';
|
||||
import { ArgsFactory } from './args.factory';
|
||||
|
||||
export enum ObjectTypeDefinitionKind {
|
||||
Connection = 'Connection',
|
||||
Edge = 'Edge',
|
||||
Plain = '',
|
||||
}
|
||||
|
||||
export interface ObjectTypeDefinition {
|
||||
target: string;
|
||||
kind: ObjectTypeDefinitionKind;
|
||||
type: GraphQLObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ExtendObjectTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(ExtendObjectTypeDefinitionFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly relationTypeFactory: RelationTypeFactory,
|
||||
private readonly argsFactory: ArgsFactory,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Plain;
|
||||
const gqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
kind,
|
||||
);
|
||||
const containsRelationField = objectContainsRelationField(objectMetadata);
|
||||
|
||||
if (!gqlType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
|
||||
{
|
||||
objectMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Security check to avoid extending an object that does not need to be extended
|
||||
if (!containsRelationField) {
|
||||
this.logger.error(
|
||||
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
|
||||
{
|
||||
objectMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Extract current object config to extend it
|
||||
const config = gqlType.toConfig();
|
||||
|
||||
// Recreate the same object type with the new fields
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
...config,
|
||||
fields: () => ({
|
||||
...config.fields,
|
||||
...this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Ignore relation fields as they are already defined
|
||||
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||
|
||||
if (!relationMetadata) {
|
||||
this.logger.error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
{ fieldMetadata },
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
);
|
||||
const relationType = this.relationTypeFactory.create(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
relationDirection,
|
||||
);
|
||||
let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined;
|
||||
|
||||
// Args are only needed when relation is of kind `oneToMany` and the relation direction is `from`
|
||||
if (
|
||||
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY &&
|
||||
relationDirection === RelationDirection.FROM
|
||||
) {
|
||||
const args = getResolverArgs('findMany');
|
||||
|
||||
argsType = this.argsFactory.create(
|
||||
{
|
||||
args,
|
||||
objectMetadataId: relationMetadata.toObjectMetadataId,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type: relationType,
|
||||
args: argsType,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||
|
||||
import { ArgsFactory } from './args.factory';
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
import { InputTypeDefinitionFactory } from './input-type-definition.factory';
|
||||
import { ObjectTypeDefinitionFactory } from './object-type-definition.factory';
|
||||
import { OutputTypeFactory } from './output-type.factory';
|
||||
import { QueryTypeFactory } from './query-type.factory';
|
||||
import { RootTypeFactory } from './root-type.factory';
|
||||
import { FilterTypeFactory } from './filter-type.factory';
|
||||
import { FilterTypeDefinitionFactory } from './filter-type-definition.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
|
||||
import { EdgeTypeFactory } from './edge-type.factory';
|
||||
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
|
||||
import { MutationTypeFactory } from './mutation-type.factory';
|
||||
import { OrderByTypeFactory } from './order-by-type.factory';
|
||||
import { OrderByTypeDefinitionFactory } from './order-by-type-definition.factory';
|
||||
import { RelationTypeFactory } from './relation-type.factory';
|
||||
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
|
||||
import { OrphanedTypesFactory } from './orphaned-types.factory';
|
||||
|
||||
export const workspaceSchemaBuilderFactories = [
|
||||
ArgsFactory,
|
||||
InputTypeFactory,
|
||||
InputTypeDefinitionFactory,
|
||||
OutputTypeFactory,
|
||||
ObjectTypeDefinitionFactory,
|
||||
EnumTypeDefinitionFactory,
|
||||
RelationTypeFactory,
|
||||
ExtendObjectTypeDefinitionFactory,
|
||||
FilterTypeFactory,
|
||||
FilterTypeDefinitionFactory,
|
||||
OrderByTypeFactory,
|
||||
OrderByTypeDefinitionFactory,
|
||||
ConnectionTypeFactory,
|
||||
ConnectionTypeDefinitionFactory,
|
||||
EdgeTypeFactory,
|
||||
EdgeTypeDefinitionFactory,
|
||||
RootTypeFactory,
|
||||
QueryTypeFactory,
|
||||
MutationTypeFactory,
|
||||
OrphanedTypesFactory,
|
||||
];
|
||||
@ -0,0 +1,91 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
|
||||
import { FilterTypeFactory } from './filter-type.factory';
|
||||
import {
|
||||
InputTypeDefinition,
|
||||
InputTypeDefinitionKind,
|
||||
} from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FilterTypeDefinitionFactory {
|
||||
constructor(
|
||||
private readonly filterTypeFactory: FilterTypeFactory,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
const kind = InputTypeDefinitionKind.Filter;
|
||||
const filterInputType = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => {
|
||||
const andOrType = this.typeMapperService.mapToGqlType(filterInputType, {
|
||||
isArray: true,
|
||||
arrayDepth: 1,
|
||||
nullable: true,
|
||||
});
|
||||
|
||||
return {
|
||||
...this.generateFields(objectMetadata, options),
|
||||
and: {
|
||||
type: andOrType,
|
||||
},
|
||||
or: {
|
||||
type: andOrType,
|
||||
},
|
||||
not: {
|
||||
type: this.typeMapperService.mapToGqlType(filterInputType, {
|
||||
nullable: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: filterInputType,
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Relation types are generated during extension of object type definition
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.filterTypeFactory.create(fieldMetadata, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInputType,
|
||||
GraphQLList,
|
||||
GraphQLScalarType,
|
||||
} from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine-metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isEnumFieldMetadataType } from 'src/engine-metadata/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FilterTypeFactory {
|
||||
private readonly logger = new Logger(FilterTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
buildOptions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||
? fieldMetadata.type.toString()
|
||||
: fieldMetadata.id;
|
||||
let filterType: GraphQLInputObjectType | GraphQLScalarType | undefined =
|
||||
undefined;
|
||||
|
||||
if (isEnumFieldMetadataType(fieldMetadata.type)) {
|
||||
filterType = this.createEnumFilterType(fieldMetadata);
|
||||
} else {
|
||||
filterType = this.typeMapperService.mapToFilterType(
|
||||
fieldMetadata.type,
|
||||
buildOptions.dateScalarMode,
|
||||
buildOptions.numberScalarMode,
|
||||
);
|
||||
|
||||
filterType ??= this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
target,
|
||||
InputTypeDefinitionKind.Filter,
|
||||
);
|
||||
}
|
||||
|
||||
if (!filterType) {
|
||||
this.logger.error(`Could not find a GraphQL type for ${target}`, {
|
||||
fieldMetadata,
|
||||
buildOptions,
|
||||
typeOptions,
|
||||
});
|
||||
|
||||
throw new Error(`Could not find a GraphQL type for ${target}`);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(filterType, typeOptions);
|
||||
}
|
||||
|
||||
private createEnumFilterType(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
): GraphQLInputObjectType {
|
||||
const enumType = this.typeDefinitionsStorage.getEnumTypeByKey(
|
||||
fieldMetadata.id,
|
||||
);
|
||||
|
||||
if (!enumType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL enum type for ${fieldMetadata.id}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL enum type for ${fieldMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return new GraphQLInputObjectType({
|
||||
name: `${enumType.name}Filter`,
|
||||
fields: () => ({
|
||||
eq: { type: enumType },
|
||||
neq: { type: enumType },
|
||||
in: { type: new GraphQLList(enumType) },
|
||||
is: { type: FilterIs },
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
|
||||
export enum InputTypeDefinitionKind {
|
||||
Create = 'Create',
|
||||
Update = 'Update',
|
||||
Filter = 'Filter',
|
||||
OrderBy = 'OrderBy',
|
||||
}
|
||||
|
||||
export interface InputTypeDefinition {
|
||||
target: string;
|
||||
kind: InputTypeDefinitionKind;
|
||||
type: GraphQLInputObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InputTypeDefinitionFactory {
|
||||
constructor(private readonly inputTypeFactory: InputTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${pascalCase(
|
||||
objectMetadata.nameSingular,
|
||||
)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, kind, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Relation field types are generated during extension of object type definition
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.inputTypeFactory.create(fieldMetadata, kind, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine-metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class InputTypeFactory {
|
||||
private readonly logger = new Logger(InputTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||
? fieldMetadata.type.toString()
|
||||
: fieldMetadata.id;
|
||||
let inputType: GraphQLInputType | undefined =
|
||||
this.typeMapperService.mapToScalarType(
|
||||
fieldMetadata.type,
|
||||
buildOtions.dateScalarMode,
|
||||
buildOtions.numberScalarMode,
|
||||
);
|
||||
|
||||
inputType ??= this.typeDefinitionsStorage.getInputTypeByKey(target, kind);
|
||||
|
||||
inputType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
|
||||
|
||||
if (!inputType) {
|
||||
this.logger.error(`Could not find a GraphQL type for ${target}`, {
|
||||
fieldMetadata,
|
||||
kind,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
});
|
||||
|
||||
throw new Error(`Could not find a GraphQL type for ${target}`);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(inputType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { WorkspaceResolverBuilderMutationMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class MutationTypeFactory {
|
||||
constructor(private readonly rootTypeFactory: RootTypeFactory) {}
|
||||
|
||||
create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderMutationMethodNames[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLObjectType {
|
||||
return this.rootTypeFactory.create(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverMethodNames,
|
||||
ObjectTypeName.Mutation,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import { OutputTypeFactory } from './output-type.factory';
|
||||
|
||||
export enum ObjectTypeDefinitionKind {
|
||||
Connection = 'Connection',
|
||||
Edge = 'Edge',
|
||||
Plain = '',
|
||||
}
|
||||
|
||||
export interface ObjectTypeDefinition {
|
||||
target: string;
|
||||
kind: ObjectTypeDefinitionKind;
|
||||
type: GraphQLObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ObjectTypeDefinitionFactory {
|
||||
constructor(private readonly outputTypeFactory: OutputTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, kind, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Relation field types are generated during extension of object type definition
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.outputTypeFactory.create(fieldMetadata, kind, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
|
||||
import {
|
||||
InputTypeDefinition,
|
||||
InputTypeDefinitionKind,
|
||||
} from './input-type-definition.factory';
|
||||
import { OrderByTypeFactory } from './order-by-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OrderByTypeDefinitionFactory {
|
||||
constructor(private readonly orderByTypeFactory: OrderByTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
const kind = InputTypeDefinitionKind.OrderBy;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${pascalCase(
|
||||
objectMetadata.nameSingular,
|
||||
)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Relation field types are generated during extension of object type definition
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.orderByTypeFactory.create(fieldMetadata, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine-metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OrderByTypeFactory {
|
||||
private readonly logger = new Logger(OrderByTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||
? fieldMetadata.type.toString()
|
||||
: fieldMetadata.id;
|
||||
let orderByType = this.typeMapperService.mapToOrderByType(
|
||||
fieldMetadata.type,
|
||||
);
|
||||
|
||||
orderByType ??= this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
target,
|
||||
InputTypeDefinitionKind.OrderBy,
|
||||
);
|
||||
|
||||
if (!orderByType) {
|
||||
this.logger.error(`Could not find a GraphQL type for ${target}`, {
|
||||
fieldMetadata,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
});
|
||||
|
||||
throw new Error(`Could not find a GraphQL type for ${target}`);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(orderByType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLNamedType } from 'graphql';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
@Injectable()
|
||||
export class OrphanedTypesFactory {
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(): GraphQLNamedType[] {
|
||||
const objectTypeDefs =
|
||||
this.typeDefinitionsStorage.getAllObjectTypeDefinitions();
|
||||
const inputTypeDefs =
|
||||
this.typeDefinitionsStorage.getAllInputTypeDefinitions();
|
||||
const classTypeDefs = [...objectTypeDefs, ...inputTypeDefs];
|
||||
|
||||
return [...classTypeDefs.map(({ type }) => type)];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine-metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OutputTypeFactory {
|
||||
private readonly logger = new Logger(OutputTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
const target = isCompositeFieldMetadataType(fieldMetadata.type)
|
||||
? fieldMetadata.type.toString()
|
||||
: fieldMetadata.id;
|
||||
let gqlType: GraphQLOutputType | undefined =
|
||||
this.typeMapperService.mapToScalarType(
|
||||
fieldMetadata.type,
|
||||
buildOtions.dateScalarMode,
|
||||
buildOtions.numberScalarMode,
|
||||
);
|
||||
|
||||
gqlType ??= this.typeDefinitionsStorage.getObjectTypeByKey(target, kind);
|
||||
|
||||
gqlType ??= this.typeDefinitionsStorage.getEnumTypeByKey(target);
|
||||
|
||||
if (!gqlType) {
|
||||
this.logger.error(`Could not find a GraphQL type for ${target}`, {
|
||||
fieldMetadata,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
});
|
||||
|
||||
throw new Error(`Could not find a GraphQL type for ${target}`);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(gqlType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { WorkspaceResolverBuilderQueryMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class QueryTypeFactory {
|
||||
constructor(private readonly rootTypeFactory: RootTypeFactory) {}
|
||||
|
||||
create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderQueryMethodNames[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLObjectType {
|
||||
return this.rootTypeFactory.create(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverMethodNames,
|
||||
ObjectTypeName.Query,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
import { RelationMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/relation-metadata.interface';
|
||||
|
||||
import { RelationMetadataType } from 'src/engine-metadata/relation-metadata/relation-metadata.entity';
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { RelationDirection } from 'src/engine/utils/deduce-relation-direction.util';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class RelationTypeFactory {
|
||||
private readonly logger = new Logger(RelationTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
relationMetadata: RelationMetadataInterface,
|
||||
relationDirection: RelationDirection,
|
||||
): GraphQLOutputType {
|
||||
let relationQqlType: GraphQLOutputType | undefined = undefined;
|
||||
|
||||
if (
|
||||
relationDirection === RelationDirection.FROM &&
|
||||
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY
|
||||
) {
|
||||
relationQqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
relationMetadata.toObjectMetadataId,
|
||||
ObjectTypeDefinitionKind.Connection,
|
||||
);
|
||||
} else {
|
||||
const relationObjectId =
|
||||
relationDirection === RelationDirection.FROM
|
||||
? relationMetadata.toObjectMetadataId
|
||||
: relationMetadata.fromObjectMetadataId;
|
||||
|
||||
relationQqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
relationObjectId,
|
||||
ObjectTypeDefinitionKind.Plain,
|
||||
);
|
||||
}
|
||||
|
||||
if (!relationQqlType) {
|
||||
this.logger.error(
|
||||
`Could not find a relation type for ${fieldMetadata.id}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(`Could not find a relation type for ${fieldMetadata.id}`);
|
||||
}
|
||||
|
||||
return relationQqlType;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
|
||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||
|
||||
import { ArgsFactory } from './args.factory';
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
export enum ObjectTypeName {
|
||||
Query = 'Query',
|
||||
Mutation = 'Mutation',
|
||||
Subscription = 'Subscription',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RootTypeFactory {
|
||||
private readonly logger = new Logger(RootTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly argsFactory: ArgsFactory,
|
||||
) {}
|
||||
|
||||
create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderMethodNames[],
|
||||
objectTypeName: ObjectTypeName,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLObjectType {
|
||||
if (workspaceResolverMethodNames.length === 0) {
|
||||
this.logger.error(
|
||||
`No resolver methods were found for ${objectTypeName.toString()}`,
|
||||
{
|
||||
workspaceResolverMethodNames,
|
||||
objectTypeName,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`No resolvers were found for ${objectTypeName.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
return new GraphQLObjectType({
|
||||
name: objectTypeName.toString(),
|
||||
fields: this.generateFields(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverMethodNames,
|
||||
options,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private generateFields<T = any, U = any>(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderMethodNames[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<T, U> {
|
||||
const fieldConfigMap: GraphQLFieldConfigMap<T, U> = {};
|
||||
|
||||
for (const objectMetadata of objectMetadataCollection) {
|
||||
for (const methodName of workspaceResolverMethodNames) {
|
||||
const name = getResolverName(objectMetadata, methodName);
|
||||
const args = getResolverArgs(methodName);
|
||||
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
['findMany', 'findDuplicates'].includes(methodName)
|
||||
? ObjectTypeDefinitionKind.Connection
|
||||
: ObjectTypeDefinitionKind.Plain,
|
||||
);
|
||||
const argsType = this.argsFactory.create(
|
||||
{
|
||||
args,
|
||||
objectMetadataId: objectMetadata.id,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
if (!objectType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`,
|
||||
{
|
||||
objectMetadata,
|
||||
methodName,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const outputType = this.typeMapperService.mapToGqlType(objectType, {
|
||||
isArray: ['updateMany', 'deleteMany', 'createMany'].includes(
|
||||
methodName,
|
||||
),
|
||||
});
|
||||
|
||||
fieldConfigMap[name] = {
|
||||
type: outputType,
|
||||
args: argsType,
|
||||
resolve: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fieldConfigMap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './order-by-direction.enum-type';
|
||||
@ -0,0 +1,24 @@
|
||||
import { GraphQLEnumType } from 'graphql';
|
||||
|
||||
export const OrderByDirectionType = new GraphQLEnumType({
|
||||
name: 'OrderByDirection',
|
||||
description: 'This enum is used to specify the order of results',
|
||||
values: {
|
||||
AscNullsFirst: {
|
||||
value: 'AscNullsFirst',
|
||||
description: 'Ascending order, nulls first',
|
||||
},
|
||||
AscNullsLast: {
|
||||
value: 'AscNullsLast',
|
||||
description: 'Ascending order, nulls last',
|
||||
},
|
||||
DescNullsFirst: {
|
||||
value: 'DescNullsFirst',
|
||||
description: 'Descending order, nulls first',
|
||||
},
|
||||
DescNullsLast: {
|
||||
value: 'DescNullsLast',
|
||||
description: 'Descending order, nulls last',
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
import { BigFloatScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
export const BigFloatFilterType = new GraphQLInputObjectType({
|
||||
name: 'BigFloatFilter',
|
||||
fields: {
|
||||
eq: { type: BigFloatScalarType },
|
||||
gt: { type: BigFloatScalarType },
|
||||
gte: { type: BigFloatScalarType },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(BigFloatScalarType)) },
|
||||
lt: { type: BigFloatScalarType },
|
||||
lte: { type: BigFloatScalarType },
|
||||
neq: { type: BigFloatScalarType },
|
||||
is: { type: FilterIs },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
import {
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLInt,
|
||||
} from 'graphql';
|
||||
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
|
||||
export const BigIntFilterType = new GraphQLInputObjectType({
|
||||
name: 'BigIntFilter',
|
||||
fields: {
|
||||
eq: { type: GraphQLInt },
|
||||
gt: { type: GraphQLInt },
|
||||
gte: { type: GraphQLInt },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(GraphQLInt)) },
|
||||
lt: { type: GraphQLInt },
|
||||
lte: { type: GraphQLInt },
|
||||
neq: { type: GraphQLInt },
|
||||
is: { type: FilterIs },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { GraphQLBoolean, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
|
||||
export const BooleanFilterType = new GraphQLInputObjectType({
|
||||
name: 'BooleanFilter',
|
||||
fields: {
|
||||
eq: { type: GraphQLBoolean },
|
||||
is: { type: FilterIs },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
import { DateScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
export const DateFilterType = new GraphQLInputObjectType({
|
||||
name: 'DateFilter',
|
||||
fields: {
|
||||
eq: { type: DateScalarType },
|
||||
gt: { type: DateScalarType },
|
||||
gte: { type: DateScalarType },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(DateScalarType)) },
|
||||
lt: { type: DateScalarType },
|
||||
lte: { type: DateScalarType },
|
||||
neq: { type: DateScalarType },
|
||||
is: { type: FilterIs },
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user