[POC] add graphql query runner (#6747)
## Context The goal is to replace pg_graphql with our own ORM wrapper (TwentyORM). This PR tries to add some parsing logic to convert graphql requests to send to the ORM to replace pg_graphql implementation. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -32,11 +32,19 @@ export default defineConfig(({ command, mode }) => {
|
||||
overlay: false,
|
||||
};
|
||||
|
||||
console.log(
|
||||
`VITE_DISABLE_TYPESCRIPT_CHECKER: ${VITE_DISABLE_TYPESCRIPT_CHECKER}`,
|
||||
);
|
||||
console.log(`VITE_DISABLE_ESLINT_CHECKER: ${VITE_DISABLE_ESLINT_CHECKER}`);
|
||||
console.log(`VITE_BUILD_SOURCEMAP: ${VITE_BUILD_SOURCEMAP}`);
|
||||
if (VITE_DISABLE_TYPESCRIPT_CHECKER === 'true') {
|
||||
console.log(
|
||||
`VITE_DISABLE_TYPESCRIPT_CHECKER: ${VITE_DISABLE_TYPESCRIPT_CHECKER}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (VITE_DISABLE_ESLINT_CHECKER === 'true') {
|
||||
console.log(`VITE_DISABLE_ESLINT_CHECKER: ${VITE_DISABLE_ESLINT_CHECKER}`);
|
||||
}
|
||||
|
||||
if (VITE_BUILD_SOURCEMAP === 'true') {
|
||||
console.log(`VITE_BUILD_SOURCEMAP: ${VITE_BUILD_SOURCEMAP}`);
|
||||
}
|
||||
|
||||
if (VITE_DISABLE_TYPESCRIPT_CHECKER !== 'true') {
|
||||
checkers['typescript'] = {
|
||||
|
||||
@ -22,6 +22,7 @@ import { MessageQueueDriverType } from 'src/engine/integrations/message-queue/in
|
||||
import { MessageQueueModule } from 'src/engine/integrations/message-queue/message-queue.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { ModulesModule } from 'src/modules/modules.module';
|
||||
|
||||
import { CoreEngineModule } from './engine/core-modules/core-engine.module';
|
||||
@ -45,6 +46,7 @@ import { IntegrationsModule } from './engine/integrations/integrations.module';
|
||||
imports: [CoreEngineModule, GraphQLConfigModule],
|
||||
useClass: GraphQLConfigService,
|
||||
}),
|
||||
TwentyORMModule,
|
||||
// Integrations module, contains all the integrations with other services
|
||||
IntegrationsModule,
|
||||
// Core engine module, contains all the core modules
|
||||
|
||||
@ -3,7 +3,6 @@ import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
|
||||
import { typeORMCoreModuleOptions } from 'src/database/typeorm/core/core.datasource';
|
||||
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
|
||||
import { TypeORMService } from './typeorm.service';
|
||||
|
||||
@ -29,7 +28,6 @@ const coreTypeORMFactory = async (): Promise<TypeOrmModuleOptions> => ({
|
||||
useFactory: coreTypeORMFactory,
|
||||
name: 'core',
|
||||
}),
|
||||
TwentyORMModule.register({}),
|
||||
EnvironmentModule,
|
||||
],
|
||||
providers: [TypeORMService],
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const CONNECTION_MAX_DEPTH = 5;
|
||||
@ -0,0 +1 @@
|
||||
export const QUERY_MAX_RECORDS = 60;
|
||||
@ -0,0 +1,17 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class GraphqlQueryRunnerException extends CustomException {
|
||||
code: GraphqlQueryRunnerExceptionCode;
|
||||
constructor(message: string, code: GraphqlQueryRunnerExceptionCode) {
|
||||
super(message, code);
|
||||
}
|
||||
}
|
||||
|
||||
export enum GraphqlQueryRunnerExceptionCode {
|
||||
MAX_DEPTH_REACHED = 'MAX_DEPTH_REACHED',
|
||||
INVALID_CURSOR = 'INVALID_CURSOR',
|
||||
INVALID_DIRECTION = 'INVALID_DIRECTION',
|
||||
UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR',
|
||||
ARGS_CONFLICT = 'ARGS_CONFLICT',
|
||||
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
|
||||
|
||||
@Module({
|
||||
providers: [GraphqlQueryRunnerService],
|
||||
exports: [GraphqlQueryRunnerService],
|
||||
})
|
||||
export class GraphqlQueryRunnerModule {}
|
||||
@ -0,0 +1,130 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { FindManyOptions, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import {
|
||||
Record as IRecord,
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser';
|
||||
import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util';
|
||||
import {
|
||||
createConnection,
|
||||
decodeCursor,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/connection.util';
|
||||
import { convertObjectMetadataToMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryRunnerService {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
@LogExecutionTime()
|
||||
async findManyWithTwentyOrm<
|
||||
ObjectRecord extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
>(
|
||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<ObjectRecord>> {
|
||||
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
|
||||
options;
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
const objectMetadataMap = convertObjectMetadataToMap(
|
||||
objectMetadataCollection,
|
||||
);
|
||||
|
||||
const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular];
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new Error(
|
||||
`Object metadata for ${objectMetadataItem.nameSingular} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadataMap = objectMetadata.fields;
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
fieldMetadataMap,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const { select, relations } = graphqlQueryParser.parseSelectedFields(
|
||||
objectMetadataItem,
|
||||
selectedFields,
|
||||
);
|
||||
|
||||
const order = args.orderBy
|
||||
? graphqlQueryParser.parseOrder(args.orderBy)
|
||||
: undefined;
|
||||
|
||||
const where = args.filter
|
||||
? graphqlQueryParser.parseFilter(args.filter)
|
||||
: {};
|
||||
|
||||
let cursor: Record<string, any> | undefined;
|
||||
|
||||
if (args.after) {
|
||||
cursor = decodeCursor(args.after);
|
||||
} else if (args.before) {
|
||||
cursor = decodeCursor(args.before);
|
||||
}
|
||||
|
||||
if (args.first && args.last) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Cannot provide both first and last',
|
||||
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
|
||||
);
|
||||
}
|
||||
|
||||
const take = args.first ?? args.last ?? QUERY_MAX_RECORDS;
|
||||
|
||||
const findOptions: FindManyOptions<ObjectLiteral> = {
|
||||
where,
|
||||
order,
|
||||
select,
|
||||
relations,
|
||||
take,
|
||||
};
|
||||
|
||||
const totalCount = await repository.count({
|
||||
where,
|
||||
});
|
||||
|
||||
if (cursor) {
|
||||
applyRangeFilter(where, order, cursor);
|
||||
}
|
||||
|
||||
const objectRecords = await repository.find(findOptions);
|
||||
|
||||
return createConnection(
|
||||
(objectRecords as ObjectRecord[]) ?? [],
|
||||
take,
|
||||
totalCount,
|
||||
order,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import { FindOperator, Not } from 'typeorm';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { GraphqlQueryFilterFieldParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
describe('GraphqlQueryFilterFieldParser', () => {
|
||||
let parser: GraphqlQueryFilterFieldParser;
|
||||
let mockFieldMetadataMap: Record<string, FieldMetadataInterface>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFieldMetadataMap = {
|
||||
simpleField: {
|
||||
id: '1',
|
||||
name: 'simpleField',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Simple Field',
|
||||
objectMetadataId: 'obj1',
|
||||
},
|
||||
};
|
||||
parser = new GraphqlQueryFilterFieldParser(mockFieldMetadataMap);
|
||||
});
|
||||
it('should parse simple field correctly', () => {
|
||||
const result = parser.parse('simpleField', 'value', false);
|
||||
|
||||
expect(result).toEqual({ simpleField: 'value' });
|
||||
});
|
||||
|
||||
it('should negate simple field correctly', () => {
|
||||
const result = parser.parse('simpleField', 'value', true);
|
||||
|
||||
expect(result).toEqual({ simpleField: Not('value') });
|
||||
});
|
||||
|
||||
it('should parse object value using operator parser', () => {
|
||||
const result = parser.parse('simpleField', { like: '%value%' }, false);
|
||||
|
||||
expect(result).toEqual({
|
||||
simpleField: new FindOperator('like', '%%value%%'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,130 @@
|
||||
import {
|
||||
FindOperator,
|
||||
ILike,
|
||||
In,
|
||||
IsNull,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
Like,
|
||||
MoreThan,
|
||||
MoreThanOrEqual,
|
||||
Not,
|
||||
} from 'typeorm';
|
||||
|
||||
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { GraphqlQueryFilterOperatorParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser';
|
||||
|
||||
describe('GraphqlQueryFilterOperatorParser', () => {
|
||||
let parser: GraphqlQueryFilterOperatorParser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new GraphqlQueryFilterOperatorParser();
|
||||
});
|
||||
|
||||
describe('parseOperator', () => {
|
||||
it('should parse eq operator correctly', () => {
|
||||
const result = parser.parseOperator({ eq: 'value' }, false);
|
||||
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
|
||||
it('should parse neq operator correctly', () => {
|
||||
const result = parser.parseOperator({ neq: 'value' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(Not('value'));
|
||||
});
|
||||
|
||||
it('should parse gt operator correctly', () => {
|
||||
const result = parser.parseOperator({ gt: 5 }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(MoreThan(5));
|
||||
});
|
||||
|
||||
it('should parse gte operator correctly', () => {
|
||||
const result = parser.parseOperator({ gte: 5 }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(MoreThanOrEqual(5));
|
||||
});
|
||||
|
||||
it('should parse lt operator correctly', () => {
|
||||
const result = parser.parseOperator({ lt: 5 }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(LessThan(5));
|
||||
});
|
||||
|
||||
it('should parse lte operator correctly', () => {
|
||||
const result = parser.parseOperator({ lte: 5 }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(LessThanOrEqual(5));
|
||||
});
|
||||
|
||||
it('should parse in operator correctly', () => {
|
||||
const result = parser.parseOperator({ in: [1, 2, 3] }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(In([1, 2, 3]));
|
||||
});
|
||||
|
||||
it('should parse is operator with NULL correctly', () => {
|
||||
const result = parser.parseOperator({ is: 'NULL' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(IsNull());
|
||||
});
|
||||
|
||||
it('should parse is operator with non-NULL value correctly', () => {
|
||||
const result = parser.parseOperator({ is: 'NOT_NULL' }, false);
|
||||
|
||||
expect(result).toBe('NOT_NULL');
|
||||
});
|
||||
|
||||
it('should parse like operator correctly', () => {
|
||||
const result = parser.parseOperator({ like: 'test' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(Like('%test%'));
|
||||
});
|
||||
|
||||
it('should parse ilike operator correctly', () => {
|
||||
const result = parser.parseOperator({ ilike: 'test' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(ILike('%test%'));
|
||||
});
|
||||
|
||||
it('should parse startsWith operator correctly', () => {
|
||||
const result = parser.parseOperator({ startsWith: 'test' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(ILike('test%'));
|
||||
});
|
||||
|
||||
it('should parse endsWith operator correctly', () => {
|
||||
const result = parser.parseOperator({ endsWith: 'test' }, false);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(ILike('%test'));
|
||||
});
|
||||
|
||||
it('should negate the operator when isNegated is true', () => {
|
||||
const result = parser.parseOperator({ eq: 'value' }, true);
|
||||
|
||||
expect(result).toBeInstanceOf(FindOperator);
|
||||
expect(result).toEqual(Not('value'));
|
||||
});
|
||||
|
||||
it('should throw an exception for unsupported operator', () => {
|
||||
expect(() =>
|
||||
parser.parseOperator({ unsupported: 'value' }, false),
|
||||
).toThrow(GraphqlQueryRunnerException);
|
||||
expect(() =>
|
||||
parser.parseOperator({ unsupported: 'value' }, false),
|
||||
).toThrow('Operator "unsupported" is not supported');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,107 @@
|
||||
import { FindOptionsWhere, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
|
||||
import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser';
|
||||
|
||||
export class GraphqlQueryFilterConditionParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
private fieldConditionParser: GraphqlQueryFilterFieldParser;
|
||||
|
||||
constructor(fieldMetadataMap: FieldMetadataMap) {
|
||||
this.fieldMetadataMap = fieldMetadataMap;
|
||||
this.fieldConditionParser = new GraphqlQueryFilterFieldParser(
|
||||
this.fieldMetadataMap,
|
||||
);
|
||||
}
|
||||
|
||||
public parse(
|
||||
conditions: RecordFilter,
|
||||
isNegated = false,
|
||||
): FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[] {
|
||||
if (Array.isArray(conditions)) {
|
||||
return this.parseAndCondition(conditions, isNegated);
|
||||
}
|
||||
|
||||
const result: FindOptionsWhere<ObjectLiteral> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(conditions)) {
|
||||
switch (key) {
|
||||
case 'and':
|
||||
return this.parseAndCondition(value, isNegated);
|
||||
case 'or':
|
||||
return this.parseOrCondition(value, isNegated);
|
||||
case 'not':
|
||||
return this.parse(value, !isNegated);
|
||||
default:
|
||||
Object.assign(
|
||||
result,
|
||||
this.fieldConditionParser.parse(key, value, isNegated),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseAndCondition(
|
||||
conditions: RecordFilter[],
|
||||
isNegated: boolean,
|
||||
): FindOptionsWhere<ObjectLiteral>[] {
|
||||
const parsedConditions = conditions.map((condition) =>
|
||||
this.parse(condition, isNegated),
|
||||
);
|
||||
|
||||
return this.combineConditions(parsedConditions, isNegated ? 'or' : 'and');
|
||||
}
|
||||
|
||||
private parseOrCondition(
|
||||
conditions: RecordFilter[],
|
||||
isNegated: boolean,
|
||||
): FindOptionsWhere<ObjectLiteral>[] {
|
||||
const parsedConditions = conditions.map((condition) =>
|
||||
this.parse(condition, isNegated),
|
||||
);
|
||||
|
||||
return this.combineConditions(parsedConditions, isNegated ? 'and' : 'or');
|
||||
}
|
||||
|
||||
private combineConditions(
|
||||
conditions: (
|
||||
| FindOptionsWhere<ObjectLiteral>
|
||||
| FindOptionsWhere<ObjectLiteral>[]
|
||||
)[],
|
||||
combineType: 'and' | 'or',
|
||||
): FindOptionsWhere<ObjectLiteral>[] {
|
||||
if (combineType === 'and') {
|
||||
let result: FindOptionsWhere<ObjectLiteral>[] = [{}];
|
||||
|
||||
for (const condition of conditions) {
|
||||
if (Array.isArray(condition)) {
|
||||
const newResult: FindOptionsWhere<ObjectLiteral>[] = [];
|
||||
|
||||
for (const existingCondition of result) {
|
||||
for (const orCondition of condition) {
|
||||
newResult.push({
|
||||
...existingCondition,
|
||||
...orCondition,
|
||||
});
|
||||
}
|
||||
}
|
||||
result = newResult;
|
||||
} else {
|
||||
result = result.map((existingCondition) => ({
|
||||
...existingCondition,
|
||||
...condition,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return conditions.flat();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
import { FindOptionsWhere, Not, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
||||
|
||||
import { GraphqlQueryFilterConditionParser } from './graphql-query-filter-condition.parser';
|
||||
import { GraphqlQueryFilterOperatorParser } from './graphql-query-filter-operator.parser';
|
||||
|
||||
export class GraphqlQueryFilterFieldParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
private operatorParser: GraphqlQueryFilterOperatorParser;
|
||||
|
||||
constructor(fieldMetadataMap: FieldMetadataMap) {
|
||||
this.fieldMetadataMap = fieldMetadataMap;
|
||||
this.operatorParser = new GraphqlQueryFilterOperatorParser();
|
||||
}
|
||||
|
||||
public parse(
|
||||
key: string,
|
||||
value: any,
|
||||
isNegated: boolean,
|
||||
): FindOptionsWhere<ObjectLiteral> {
|
||||
const fieldMetadata = this.fieldMetadataMap[key];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return {
|
||||
[key]: (value: RecordFilter, isNegated: boolean) => {
|
||||
const conditionParser = new GraphqlQueryFilterConditionParser(
|
||||
this.fieldMetadataMap,
|
||||
);
|
||||
|
||||
return conditionParser.parse(value, isNegated);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
return this.parseCompositeFieldForFilter(fieldMetadata, value, isNegated);
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
const parsedValue = this.operatorParser.parseOperator(value, isNegated);
|
||||
|
||||
return { [key]: parsedValue };
|
||||
}
|
||||
|
||||
return { [key]: isNegated ? Not(value) : value };
|
||||
}
|
||||
|
||||
private parseCompositeFieldForFilter(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
fieldValue: any,
|
||||
isNegated: boolean,
|
||||
): FindOptionsWhere<ObjectLiteral> {
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
fieldMetadata.type as CompositeFieldMetadataType,
|
||||
);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new Error(
|
||||
`Composite type definition not found for type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.entries(fieldValue).reduce(
|
||||
(result, [subFieldKey, subFieldValue]) => {
|
||||
const subFieldMetadata = compositeType.properties.find(
|
||||
(property) => property.name === subFieldKey,
|
||||
);
|
||||
|
||||
if (!subFieldMetadata) {
|
||||
throw new Error(
|
||||
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
|
||||
|
||||
if (isPlainObject(subFieldValue)) {
|
||||
result[fullFieldName] = this.operatorParser.parseOperator(
|
||||
subFieldValue,
|
||||
isNegated,
|
||||
);
|
||||
} else {
|
||||
result[fullFieldName] = isNegated
|
||||
? Not(subFieldValue)
|
||||
: subFieldValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
{} as FindOptionsWhere<ObjectLiteral>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import {
|
||||
FindOperator,
|
||||
ILike,
|
||||
In,
|
||||
IsNull,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
Like,
|
||||
MoreThan,
|
||||
MoreThanOrEqual,
|
||||
Not,
|
||||
} from 'typeorm';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
|
||||
export class GraphqlQueryFilterOperatorParser {
|
||||
private operatorMap: { [key: string]: (value: any) => FindOperator<any> };
|
||||
|
||||
constructor() {
|
||||
this.operatorMap = {
|
||||
eq: (value: any) => value,
|
||||
neq: (value: any) => Not(value),
|
||||
gt: (value: any) => MoreThan(value),
|
||||
gte: (value: any) => MoreThanOrEqual(value),
|
||||
lt: (value: any) => LessThan(value),
|
||||
lte: (value: any) => LessThanOrEqual(value),
|
||||
in: (value: any) => In(value),
|
||||
is: (value: any) => (value === 'NULL' ? IsNull() : value),
|
||||
like: (value: string) => Like(`%${value}%`),
|
||||
ilike: (value: string) => ILike(`%${value}%`),
|
||||
startsWith: (value: string) => ILike(`${value}%`),
|
||||
endsWith: (value: string) => ILike(`%${value}`),
|
||||
};
|
||||
}
|
||||
|
||||
public parseOperator(
|
||||
operatorObj: Record<string, any>,
|
||||
isNegated: boolean,
|
||||
): FindOperator<any> {
|
||||
const [[operator, value]] = Object.entries(operatorObj);
|
||||
|
||||
if (operator in this.operatorMap) {
|
||||
const operatorFunction = this.operatorMap[operator];
|
||||
|
||||
return isNegated ? Not(operatorFunction(value)) : operatorFunction(value);
|
||||
}
|
||||
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Operator "${operator}" is not supported`,
|
||||
GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser';
|
||||
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
describe('GraphqlQueryOrderFieldParser', () => {
|
||||
let parser: GraphqlQueryOrderFieldParser;
|
||||
const fieldMetadataMap: FieldMetadataMap = {};
|
||||
|
||||
beforeEach(() => {
|
||||
fieldMetadataMap['name'] = {
|
||||
id: 'name-id',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
objectMetadataId: 'object-id',
|
||||
};
|
||||
fieldMetadataMap['age'] = {
|
||||
id: 'age-id',
|
||||
name: 'age',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Age',
|
||||
objectMetadataId: 'object-id',
|
||||
};
|
||||
fieldMetadataMap['address'] = {
|
||||
id: 'address-id',
|
||||
name: 'address',
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
label: 'Address',
|
||||
objectMetadataId: 'object-id',
|
||||
};
|
||||
|
||||
parser = new GraphqlQueryOrderFieldParser(fieldMetadataMap);
|
||||
});
|
||||
describe('parse', () => {
|
||||
it('should parse simple order by fields', () => {
|
||||
const orderBy = [
|
||||
{ name: OrderByDirection.AscNullsFirst },
|
||||
{ age: OrderByDirection.DescNullsLast },
|
||||
];
|
||||
const result = parser.parse(orderBy);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: { direction: 'ASC', nulls: 'FIRST' },
|
||||
age: { direction: 'DESC', nulls: 'LAST' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,122 @@
|
||||
import { FindOptionsOrderValue } from 'typeorm';
|
||||
|
||||
import {
|
||||
OrderByDirection,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
export class GraphqlQueryOrderFieldParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
|
||||
constructor(fieldMetadataMap: FieldMetadataMap) {
|
||||
this.fieldMetadataMap = fieldMetadataMap;
|
||||
}
|
||||
|
||||
parse(orderBy: RecordOrderBy): Record<string, FindOptionsOrderValue> {
|
||||
return orderBy.reduce(
|
||||
(acc, item) => {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
const fieldMetadata = this.fieldMetadataMap[key];
|
||||
|
||||
if (!fieldMetadata || value === undefined) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Field "${key}" does not exist or is not sortable`,
|
||||
GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeOrder = this.parseCompositeFieldForOrder(
|
||||
fieldMetadata,
|
||||
value,
|
||||
);
|
||||
|
||||
Object.assign(acc, compositeOrder);
|
||||
} else {
|
||||
acc[key] = this.convertOrderByToFindOptionsOrder(value);
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, FindOptionsOrderValue>,
|
||||
);
|
||||
}
|
||||
|
||||
private parseCompositeFieldForOrder(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
value: any,
|
||||
): Record<string, FindOptionsOrderValue> {
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
fieldMetadata.type as CompositeFieldMetadataType,
|
||||
);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new Error(
|
||||
`Composite type definition not found for type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.entries(value).reduce(
|
||||
(acc, [subFieldKey, subFieldValue]) => {
|
||||
const subFieldMetadata = compositeType.properties.find(
|
||||
(property) => property.name === subFieldKey,
|
||||
);
|
||||
|
||||
if (!subFieldMetadata) {
|
||||
throw new Error(
|
||||
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
|
||||
|
||||
if (!this.isOrderByDirection(subFieldValue)) {
|
||||
throw new Error(
|
||||
`Sub field order by value must be of type OrderByDirection, but got: ${subFieldValue}`,
|
||||
);
|
||||
}
|
||||
acc[fullFieldName] =
|
||||
this.convertOrderByToFindOptionsOrder(subFieldValue);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, FindOptionsOrderValue>,
|
||||
);
|
||||
}
|
||||
|
||||
private convertOrderByToFindOptionsOrder(
|
||||
direction: OrderByDirection,
|
||||
): FindOptionsOrderValue {
|
||||
switch (direction) {
|
||||
case OrderByDirection.AscNullsFirst:
|
||||
return { direction: 'ASC', nulls: 'FIRST' };
|
||||
case OrderByDirection.AscNullsLast:
|
||||
return { direction: 'ASC', nulls: 'LAST' };
|
||||
case OrderByDirection.DescNullsFirst:
|
||||
return { direction: 'DESC', nulls: 'FIRST' };
|
||||
case OrderByDirection.DescNullsLast:
|
||||
return { direction: 'DESC', nulls: 'LAST' };
|
||||
default:
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Invalid direction: ${direction}`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private isOrderByDirection(value: unknown): value is OrderByDirection {
|
||||
return Object.values(OrderByDirection).includes(value as OrderByDirection);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import {
|
||||
FindOptionsOrderValue,
|
||||
FindOptionsWhere,
|
||||
ObjectLiteral,
|
||||
} from 'typeorm';
|
||||
|
||||
import {
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { GraphqlQueryFilterConditionParser as GraphqlQueryFilterParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser';
|
||||
import { GraphqlQueryOrderFieldParser as GraphqlQueryOrderParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser';
|
||||
import { GraphQLSelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser';
|
||||
import {
|
||||
FieldMetadataMap,
|
||||
ObjectMetadataMap,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
|
||||
export class GraphqlQueryParser {
|
||||
private fieldMetadataMap: FieldMetadataMap;
|
||||
private objectMetadataMap: ObjectMetadataMap;
|
||||
|
||||
constructor(
|
||||
fieldMetadataMap: FieldMetadataMap,
|
||||
objectMetadataMap: ObjectMetadataMap,
|
||||
) {
|
||||
this.objectMetadataMap = objectMetadataMap;
|
||||
this.fieldMetadataMap = fieldMetadataMap;
|
||||
}
|
||||
|
||||
parseFilter(
|
||||
recordFilter: RecordFilter,
|
||||
): FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[] {
|
||||
const graphqlQueryFilterParser = new GraphqlQueryFilterParser(
|
||||
this.fieldMetadataMap,
|
||||
);
|
||||
|
||||
return graphqlQueryFilterParser.parse(recordFilter);
|
||||
}
|
||||
|
||||
parseOrder(orderBy: RecordOrderBy): Record<string, FindOptionsOrderValue> {
|
||||
const graphqlQueryOrderParser = new GraphqlQueryOrderParser(
|
||||
this.fieldMetadataMap,
|
||||
);
|
||||
|
||||
return graphqlQueryOrderParser.parse(orderBy);
|
||||
}
|
||||
|
||||
parseSelectedFields(
|
||||
parentObjectMetadata: ObjectMetadataInterface,
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
): { select: Record<string, any>; relations: Record<string, any> } {
|
||||
const parentFields =
|
||||
this.objectMetadataMap[parentObjectMetadata.nameSingular]?.fields;
|
||||
|
||||
if (!parentFields) {
|
||||
throw new Error(
|
||||
`Could not find object metadata for ${parentObjectMetadata.nameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const selectedFieldsParser = new GraphQLSelectedFieldsParser(
|
||||
this.objectMetadataMap,
|
||||
);
|
||||
|
||||
return selectedFieldsParser.parse(graphqlSelectedFields, parentFields);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { GraphQLSelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser';
|
||||
import {
|
||||
ObjectMetadataMap,
|
||||
ObjectMetadataMapItem,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
deduceRelationDirection,
|
||||
RelationDirection,
|
||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
||||
|
||||
export class GraphqlSelectedFieldsRelationParser {
|
||||
private objectMetadataMap: ObjectMetadataMap;
|
||||
|
||||
constructor(objectMetadataMap: ObjectMetadataMap) {
|
||||
this.objectMetadataMap = objectMetadataMap;
|
||||
}
|
||||
|
||||
parseRelationField(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
fieldKey: string,
|
||||
fieldValue: any,
|
||||
result: { select: Record<string, any>; relations: Record<string, any> },
|
||||
): void {
|
||||
result.relations[fieldKey] = true;
|
||||
|
||||
if (!fieldValue || typeof fieldValue !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||
|
||||
if (!relationMetadata) {
|
||||
throw new Error(
|
||||
`Relation metadata not found for field ${fieldMetadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
);
|
||||
const referencedObjectMetadata = this.getReferencedObjectMetadata(
|
||||
relationMetadata,
|
||||
relationDirection,
|
||||
);
|
||||
|
||||
const relationFields = referencedObjectMetadata.fields;
|
||||
const fieldParser = new GraphQLSelectedFieldsParser(this.objectMetadataMap);
|
||||
const subResult = fieldParser.parse(fieldValue, relationFields);
|
||||
|
||||
result.select[fieldKey] = {
|
||||
id: true,
|
||||
...subResult.select,
|
||||
};
|
||||
result.relations[fieldKey] = subResult.relations;
|
||||
}
|
||||
|
||||
private getReferencedObjectMetadata(
|
||||
relationMetadata: RelationMetadataEntity,
|
||||
relationDirection: RelationDirection,
|
||||
): ObjectMetadataMapItem {
|
||||
const referencedObjectMetadata =
|
||||
relationDirection === RelationDirection.TO
|
||||
? this.objectMetadataMap[relationMetadata.fromObjectMetadataId]
|
||||
: this.objectMetadataMap[relationMetadata.toObjectMetadataId];
|
||||
|
||||
if (!referencedObjectMetadata) {
|
||||
throw new Error(
|
||||
`Referenced object metadata not found for relation ${relationMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return referencedObjectMetadata;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { GraphqlSelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser';
|
||||
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
||||
|
||||
export class GraphQLSelectedFieldsParser {
|
||||
private graphqlSelectedFieldsRelationParser: GraphqlSelectedFieldsRelationParser;
|
||||
|
||||
constructor(objectMetadataMap: ObjectMetadataMap) {
|
||||
this.graphqlSelectedFieldsRelationParser =
|
||||
new GraphqlSelectedFieldsRelationParser(objectMetadataMap);
|
||||
}
|
||||
|
||||
parse(
|
||||
graphqlSelectedFields: Partial<Record<string, any>>,
|
||||
fieldMetadataMap: Record<string, FieldMetadataInterface>,
|
||||
): { select: Record<string, any>; relations: Record<string, any> } {
|
||||
const result: {
|
||||
select: Record<string, any>;
|
||||
relations: Record<string, any>;
|
||||
} = {
|
||||
select: {},
|
||||
relations: {},
|
||||
};
|
||||
|
||||
for (const [fieldKey, fieldValue] of Object.entries(
|
||||
graphqlSelectedFields,
|
||||
)) {
|
||||
if (this.shouldNotParseField(fieldKey)) {
|
||||
continue;
|
||||
}
|
||||
if (this.isConnectionField(fieldKey, fieldValue)) {
|
||||
const subResult = this.parse(fieldValue, fieldMetadataMap);
|
||||
|
||||
Object.assign(result.select, subResult.select);
|
||||
Object.assign(result.relations, subResult.relations);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldMetadata = fieldMetadataMap[fieldKey];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Field "${fieldKey}" does not exist or is not selectable`,
|
||||
GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||
this.graphqlSelectedFieldsRelationParser.parseRelationField(
|
||||
fieldMetadata,
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
result,
|
||||
);
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeResult = this.parseCompositeField(
|
||||
fieldMetadata,
|
||||
fieldValue,
|
||||
);
|
||||
|
||||
Object.assign(result.select, compositeResult);
|
||||
} else {
|
||||
result.select[fieldKey] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private isConnectionField(fieldKey: string, fieldValue: any): boolean {
|
||||
return ['edges', 'node'].includes(fieldKey) && isPlainObject(fieldValue);
|
||||
}
|
||||
|
||||
private shouldNotParseField(fieldKey: string): boolean {
|
||||
return ['__typename', 'totalCount', 'pageInfo', 'cursor'].includes(
|
||||
fieldKey,
|
||||
);
|
||||
}
|
||||
|
||||
private parseCompositeField(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
fieldValue: any,
|
||||
): Record<string, any> {
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
fieldMetadata.type as CompositeFieldMetadataType,
|
||||
);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new Error(
|
||||
`Composite type definition not found for type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.keys(fieldValue)
|
||||
.filter((subFieldKey) => subFieldKey !== '__typename')
|
||||
.reduce(
|
||||
(acc, subFieldKey) => {
|
||||
const subFieldMetadata = compositeType.properties.find(
|
||||
(property) => property.name === subFieldKey,
|
||||
);
|
||||
|
||||
if (!subFieldMetadata) {
|
||||
throw new Error(
|
||||
`Sub field metadata not found for composite type: ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`;
|
||||
|
||||
acc[fullFieldName] = true;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import {
|
||||
FindOptionsOrderValue,
|
||||
FindOptionsWhere,
|
||||
LessThan,
|
||||
MoreThan,
|
||||
ObjectLiteral,
|
||||
} from 'typeorm';
|
||||
|
||||
export const applyRangeFilter = (
|
||||
where: FindOptionsWhere<ObjectLiteral>,
|
||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||
cursor: Record<string, any>,
|
||||
): FindOptionsWhere<ObjectLiteral> => {
|
||||
if (!order) return where;
|
||||
|
||||
const orderEntries = Object.entries(order);
|
||||
|
||||
orderEntries.forEach(([column, order], index) => {
|
||||
if (typeof order !== 'object' || !('direction' in order)) {
|
||||
return;
|
||||
}
|
||||
where[column] =
|
||||
order.direction === 'ASC'
|
||||
? MoreThan(cursor[index])
|
||||
: LessThan(cursor[index]);
|
||||
});
|
||||
|
||||
return where;
|
||||
};
|
||||
@ -0,0 +1,114 @@
|
||||
import { FindOptionsOrderValue } from 'typeorm';
|
||||
|
||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
|
||||
import { CONNECTION_MAX_DEPTH } from 'src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant';
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { isPlainObject } from 'src/utils/is-plain-object';
|
||||
|
||||
export const createConnection = <ObjectRecord extends IRecord = IRecord>(
|
||||
objectRecords: ObjectRecord[],
|
||||
take: number,
|
||||
totalCount: number,
|
||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||
depth = 0,
|
||||
): IConnection<ObjectRecord> => {
|
||||
const edges = (objectRecords ?? []).map((objectRecord) => ({
|
||||
node: processNestedConnections(
|
||||
objectRecord,
|
||||
take,
|
||||
totalCount,
|
||||
order,
|
||||
depth,
|
||||
),
|
||||
cursor: encodeCursor(objectRecord, order),
|
||||
}));
|
||||
|
||||
return {
|
||||
edges,
|
||||
pageInfo: {
|
||||
hasNextPage: objectRecords.length === take && totalCount > take,
|
||||
hasPreviousPage: false,
|
||||
startCursor: edges[0]?.cursor,
|
||||
endCursor: edges[edges.length - 1]?.cursor,
|
||||
},
|
||||
totalCount: totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
const processNestedConnections = <T extends Record<string, any>>(
|
||||
objectRecord: T,
|
||||
take: number,
|
||||
totalCount: number,
|
||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||
depth = 0,
|
||||
): T => {
|
||||
if (depth >= CONNECTION_MAX_DEPTH) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Maximum depth of ${CONNECTION_MAX_DEPTH} reached`,
|
||||
GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED,
|
||||
);
|
||||
}
|
||||
|
||||
const processedObjectRecords: Record<string, any> = { ...objectRecord };
|
||||
|
||||
for (const [key, value] of Object.entries(objectRecord)) {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0 && typeof value[0] !== 'object') {
|
||||
processedObjectRecords[key] = value;
|
||||
} else {
|
||||
processedObjectRecords[key] = createConnection(
|
||||
value,
|
||||
take,
|
||||
value.length,
|
||||
order,
|
||||
depth + 1,
|
||||
);
|
||||
}
|
||||
} else if (value instanceof Date) {
|
||||
processedObjectRecords[key] = value.toISOString();
|
||||
} else if (isPlainObject(value)) {
|
||||
processedObjectRecords[key] = processNestedConnections(
|
||||
value,
|
||||
take,
|
||||
totalCount,
|
||||
order,
|
||||
depth + 1,
|
||||
);
|
||||
} else {
|
||||
processedObjectRecords[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return processedObjectRecords as T;
|
||||
};
|
||||
|
||||
export const decodeCursor = (cursor: string): Record<string, any> => {
|
||||
try {
|
||||
return JSON.parse(Buffer.from(cursor, 'base64').toString());
|
||||
} catch (err) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Invalid cursor: ${cursor}`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const encodeCursor = <ObjectRecord extends IRecord = IRecord>(
|
||||
objectRecord: ObjectRecord,
|
||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||
): string => {
|
||||
const cursor = {};
|
||||
|
||||
Object.keys(order ?? []).forEach((key) => {
|
||||
cursor[key] = objectRecord[key];
|
||||
});
|
||||
|
||||
cursor['id'] = objectRecord.id;
|
||||
|
||||
return Buffer.from(JSON.stringify(Object.values(cursor))).toString('base64');
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
export type FieldMetadataMap = Record<string, FieldMetadataInterface>;
|
||||
|
||||
export type ObjectMetadataMapItem = Omit<ObjectMetadataInterface, 'fields'> & {
|
||||
fields: FieldMetadataMap;
|
||||
};
|
||||
|
||||
export type ObjectMetadataMap = Record<string, ObjectMetadataMapItem>;
|
||||
|
||||
export const convertObjectMetadataToMap = (
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
): ObjectMetadataMap => {
|
||||
const objectMetadataMap: ObjectMetadataMap = {};
|
||||
|
||||
for (const objectMetadata of objectMetadataCollection) {
|
||||
const fieldsMap: FieldMetadataMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
fieldsMap[fieldMetadata.name] = fieldMetadata;
|
||||
}
|
||||
|
||||
const processedObjectMetadata: ObjectMetadataMapItem = {
|
||||
...objectMetadata,
|
||||
fields: fieldsMap,
|
||||
};
|
||||
|
||||
objectMetadataMap[objectMetadata.id] = processedObjectMetadata;
|
||||
objectMetadataMap[objectMetadata.nameSingular] = processedObjectMetadata;
|
||||
objectMetadataMap[objectMetadata.namePlural] = processedObjectMetadata;
|
||||
}
|
||||
|
||||
return objectMetadataMap;
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { GraphqlQueryRunnerModule } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module';
|
||||
import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module';
|
||||
import { RecordPositionBackfillCommand } from 'src/engine/api/graphql/workspace-query-runner/commands/0-20-record-position-backfill.command';
|
||||
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
|
||||
@ -8,6 +10,8 @@ import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query
|
||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
@ -24,9 +28,12 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceQueryHookModule,
|
||||
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
AnalyticsModule,
|
||||
DuplicateModule,
|
||||
FileModule,
|
||||
GraphqlQueryRunnerModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
WorkspaceQueryRunnerService,
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
|
||||
import { WorkspaceQueryBuilderFactory } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory';
|
||||
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
@ -42,6 +43,8 @@ import {
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||
@ -63,8 +66,8 @@ import {
|
||||
} from './interfaces/pg-graphql.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
|
||||
import {
|
||||
PgGraphQLConfig,
|
||||
computePgGraphQLError,
|
||||
PgGraphQLConfig,
|
||||
} from './utils/compute-pg-graphql-error.util';
|
||||
|
||||
@Injectable()
|
||||
@ -83,6 +86,8 @@ export class WorkspaceQueryRunnerService {
|
||||
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly duplicateService: DuplicateService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
|
||||
) {}
|
||||
|
||||
async findMany<
|
||||
@ -96,6 +101,12 @@ export class WorkspaceQueryRunnerService {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
const start = performance.now();
|
||||
|
||||
const isQueryRunnerTwentyORMEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
@ -110,6 +121,13 @@ export class WorkspaceQueryRunnerService {
|
||||
ResolverArgsType.FindMany,
|
||||
)) as FindManyResolverArgs<Filter, OrderBy>;
|
||||
|
||||
if (isQueryRunnerTwentyORMEnabled) {
|
||||
return this.graphqlQueryRunnerService.findManyWithTwentyOrm(
|
||||
computedArgs,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.findMany(
|
||||
computedArgs,
|
||||
{
|
||||
@ -119,6 +137,7 @@ export class WorkspaceQueryRunnerService {
|
||||
);
|
||||
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const end = performance.now();
|
||||
|
||||
this.logger.log(
|
||||
|
||||
@ -13,7 +13,6 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSchemaFactory {
|
||||
constructor(
|
||||
|
||||
@ -24,10 +24,8 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
@ -55,7 +53,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
UserWorkspaceModule,
|
||||
WorkspaceModule,
|
||||
OnboardingModule,
|
||||
TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]),
|
||||
WorkspaceDataSourceModule,
|
||||
ConnectedAccountModule,
|
||||
],
|
||||
|
||||
@ -5,7 +5,7 @@ import { isDefined } from 'class-validator';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
@ -14,7 +14,7 @@ export class BillingService {
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly isFeatureEnabledService: IsFeatureEnabledService,
|
||||
private readonly isFeatureEnabledService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
isBillingEnabled() {
|
||||
|
||||
@ -1,20 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { TimelineCalendarEventResolver } from 'src/engine/core-modules/calendar/timeline-calendar-event.resolver';
|
||||
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TwentyORMModule.forFeature([
|
||||
CalendarEventWorkspaceEntity,
|
||||
PersonWorkspaceEntity,
|
||||
]),
|
||||
UserModule,
|
||||
],
|
||||
imports: [UserModule],
|
||||
exports: [],
|
||||
providers: [TimelineCalendarEventResolver, TimelineCalendarEventService],
|
||||
})
|
||||
|
||||
@ -11,4 +11,5 @@ export enum FeatureFlagKey {
|
||||
IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED',
|
||||
IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED',
|
||||
IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED',
|
||||
IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED',
|
||||
}
|
||||
|
||||
@ -5,8 +5,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagFactory } from 'src/engine/core-modules/feature-flag/services/feature-flags.factory';
|
||||
import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -19,7 +18,7 @@ import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/se
|
||||
resolvers: [],
|
||||
}),
|
||||
],
|
||||
exports: [IsFeatureEnabledService, FeatureFlagFactory],
|
||||
providers: [IsFeatureEnabledService, FeatureFlagFactory],
|
||||
exports: [FeatureFlagService],
|
||||
providers: [FeatureFlagService],
|
||||
})
|
||||
export class FeatureFlagModule {}
|
||||
|
||||
@ -5,16 +5,32 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FeatureFlagFactory {
|
||||
export class FeatureFlagService {
|
||||
constructor(
|
||||
@InjectRepository(FeatureFlagEntity, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
) {}
|
||||
|
||||
async create(workspaceId: string): Promise<FeatureFlagMap> {
|
||||
public async isFeatureEnabled(
|
||||
key: FeatureFlagKey,
|
||||
workspaceId: string,
|
||||
): Promise<boolean> {
|
||||
const featureFlag = await this.featureFlagRepository.findOneBy({
|
||||
workspaceId,
|
||||
key,
|
||||
value: true,
|
||||
});
|
||||
|
||||
return !!featureFlag?.value;
|
||||
}
|
||||
|
||||
public async getWorkspaceFeatureFlags(
|
||||
workspaceId: string,
|
||||
): Promise<FeatureFlagMap> {
|
||||
const workspaceFeatureFlags = await this.featureFlagRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
@ -1,28 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
|
||||
@Injectable()
|
||||
export class IsFeatureEnabledService {
|
||||
constructor(
|
||||
@InjectRepository(FeatureFlagEntity, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
) {}
|
||||
|
||||
public async isFeatureEnabled(
|
||||
key: FeatureFlagKey,
|
||||
workspaceId: string,
|
||||
): Promise<boolean> {
|
||||
const featureFlag = await this.featureFlagRepository.findOneBy({
|
||||
workspaceId,
|
||||
key,
|
||||
value: true,
|
||||
});
|
||||
|
||||
return !!featureFlag?.value;
|
||||
}
|
||||
}
|
||||
@ -6,11 +6,9 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -23,7 +21,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
],
|
||||
services: [UserWorkspaceService],
|
||||
}),
|
||||
TwentyORMModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||
],
|
||||
exports: [UserWorkspaceService],
|
||||
providers: [UserWorkspaceService],
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* A decorator function that logs the execution time of the decorated method.
|
||||
*
|
||||
* @param target The target class of the decorated method.
|
||||
* @param propertyKey The name of the decorated method.
|
||||
* @param descriptor The property descriptor of the decorated method.
|
||||
* @returns The modified property descriptor with the execution time logging functionality.
|
||||
*/
|
||||
export function LogExecutionTime() {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
const logger = new Logger(`${target.constructor.name}:${propertyKey}`);
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const start = performance.now();
|
||||
|
||||
const result = await originalMethod.apply(this, args);
|
||||
const end = performance.now();
|
||||
const executionTime = end - start;
|
||||
|
||||
logger.log(`Execution time: ${executionTime.toFixed(2)}ms`);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
@ -5,7 +5,6 @@ import {
|
||||
registerEnumType,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import {
|
||||
Authorize,
|
||||
FilterableField,
|
||||
@ -23,17 +22,19 @@ import {
|
||||
IsUUID,
|
||||
Validate,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
|
||||
|
||||
import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator';
|
||||
import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
|
||||
|
||||
registerEnumType(FieldMetadataType, {
|
||||
name: 'FieldMetadataType',
|
||||
@ -57,6 +58,9 @@ registerEnumType(FieldMetadataType, {
|
||||
@Relation('fromRelationMetadata', () => RelationMetadataDTO, {
|
||||
nullable: true,
|
||||
})
|
||||
@Relation('object', () => ObjectMetadataDTO, {
|
||||
nullable: true,
|
||||
})
|
||||
export class FieldMetadataDTO<
|
||||
T extends FieldMetadataType | 'default' = 'default',
|
||||
> {
|
||||
|
||||
@ -19,7 +19,6 @@ import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metada
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
||||
|
||||
@ -36,7 +35,6 @@ import { UpdateFieldInput } from './dtos/update-field.input';
|
||||
NestjsQueryTypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceStatusModule,
|
||||
TwentyORMModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
ObjectMetadataModule,
|
||||
|
||||
@ -57,7 +57,7 @@ export class EntitySchemaRelationFactory {
|
||||
target: relationDetails.target,
|
||||
inverseSide: relationDetails.inverseSide,
|
||||
joinColumn: relationDetails.joinColumn,
|
||||
};
|
||||
} satisfies EntitySchemaRelationOptions;
|
||||
}
|
||||
|
||||
return entitySchemaRelationMap;
|
||||
|
||||
@ -9,11 +9,13 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
import { workspaceDataSourceCacheInstance } from 'src/engine/twenty-orm/twenty-orm-core.module';
|
||||
import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceDatasourceFactory {
|
||||
private cacheManager = new CacheManager<WorkspaceDataSource>();
|
||||
|
||||
constructor(
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@ -48,7 +50,7 @@ export class WorkspaceDatasourceFactory {
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceDataSource = await workspaceDataSourceCacheInstance.execute(
|
||||
const workspaceDataSource = await this.cacheManager.execute(
|
||||
`${workspaceId}-${latestWorkspaceMetadataVersion}`,
|
||||
async () => {
|
||||
let cachedObjectMetadataCollection =
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import {
|
||||
DynamicModule,
|
||||
Global,
|
||||
Logger,
|
||||
Module,
|
||||
OnApplicationShutdown,
|
||||
} from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
TwentyORMModuleAsyncOptions,
|
||||
TwentyORMOptions,
|
||||
} from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface';
|
||||
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { entitySchemaFactories } from 'src/engine/twenty-orm/factories';
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
import { CacheManager } from 'src/engine/twenty-orm/storage/cache-manager.storage';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { ConfigurableModuleClass } from 'src/engine/twenty-orm/twenty-orm.module-definition';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
export const workspaceDataSourceCacheInstance =
|
||||
new CacheManager<WorkspaceDataSource>();
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
],
|
||||
providers: [
|
||||
...entitySchemaFactories,
|
||||
TwentyORMManager,
|
||||
TwentyORMGlobalManager,
|
||||
],
|
||||
exports: [EntitySchemaFactory, TwentyORMManager, TwentyORMGlobalManager],
|
||||
})
|
||||
export class TwentyORMCoreModule
|
||||
extends ConfigurableModuleClass
|
||||
implements OnApplicationShutdown
|
||||
{
|
||||
private static readonly logger = new Logger(TwentyORMCoreModule.name);
|
||||
|
||||
static register(options: TwentyORMOptions): DynamicModule {
|
||||
const dynamicModule = super.register(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? [])],
|
||||
exports: [...(dynamicModule.exports ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
static registerAsync(
|
||||
asyncOptions: TwentyORMModuleAsyncOptions,
|
||||
): DynamicModule {
|
||||
const dynamicModule = super.registerAsync(asyncOptions);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? [])],
|
||||
exports: [...(dynamicModule.exports ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys all data sources on application shutdown
|
||||
*/
|
||||
async onApplicationShutdown() {
|
||||
workspaceDataSourceCacheInstance.clear((dataSource) =>
|
||||
dataSource.destroy(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,40 +1,28 @@
|
||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
||||
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
TwentyORMModuleAsyncOptions,
|
||||
TwentyORMOptions,
|
||||
} from 'src/engine/twenty-orm/interfaces/twenty-orm-options.interface';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { entitySchemaFactories } from 'src/engine/twenty-orm/factories';
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
import { TwentyORMCoreModule } from 'src/engine/twenty-orm/twenty-orm-core.module';
|
||||
|
||||
// Todo: remove this file
|
||||
@Global()
|
||||
@Module({})
|
||||
export class TwentyORMModule {
|
||||
static register(options: TwentyORMOptions): DynamicModule {
|
||||
return {
|
||||
module: TwentyORMModule,
|
||||
imports: [TwentyORMCoreModule.register(options)],
|
||||
};
|
||||
}
|
||||
|
||||
static forFeature(_objects: EntityClassOrSchema[] = []): DynamicModule {
|
||||
const providers = [];
|
||||
|
||||
return {
|
||||
module: TwentyORMModule,
|
||||
providers: providers,
|
||||
exports: providers,
|
||||
};
|
||||
}
|
||||
|
||||
static registerAsync(
|
||||
asyncOptions: TwentyORMModuleAsyncOptions,
|
||||
): DynamicModule {
|
||||
return {
|
||||
module: TwentyORMModule,
|
||||
imports: [TwentyORMCoreModule.registerAsync(asyncOptions)],
|
||||
};
|
||||
}
|
||||
}
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
],
|
||||
providers: [
|
||||
...entitySchemaFactories,
|
||||
TwentyORMManager,
|
||||
TwentyORMGlobalManager,
|
||||
],
|
||||
exports: [EntitySchemaFactory, TwentyORMManager, TwentyORMGlobalManager],
|
||||
})
|
||||
export class TwentyORMModule {}
|
||||
|
||||
@ -21,7 +21,10 @@ export async function determineRelationDetails(
|
||||
let fromObjectMetadata: ObjectMetadataEntity | undefined =
|
||||
fieldMetadata.object;
|
||||
let toObjectMetadata: ObjectMetadataEntity | undefined =
|
||||
relationMetadata.toObjectMetadata;
|
||||
objectMetadataCollection.find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.id === relationMetadata.toObjectMetadataId,
|
||||
);
|
||||
|
||||
// RelationMetadata always store the relation from the perspective of the `from` object, MANY_TO_ONE relations are not stored yet
|
||||
if (relationType === 'many-to-one') {
|
||||
@ -37,6 +40,16 @@ export async function determineRelationDetails(
|
||||
throw new Error('Object metadata not found');
|
||||
}
|
||||
|
||||
const toFieldMetadata = toObjectMetadata.fields.find((field) =>
|
||||
relationType === 'many-to-one'
|
||||
? field.id === relationMetadata.fromFieldMetadataId
|
||||
: field.id === relationMetadata.toFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!toFieldMetadata) {
|
||||
throw new Error('To field metadata not found');
|
||||
}
|
||||
|
||||
// TODO: Support many to many relations
|
||||
if (relationType === 'many-to-many') {
|
||||
throw new Error('Many to many relations are not supported yet');
|
||||
@ -45,7 +58,7 @@ export async function determineRelationDetails(
|
||||
return {
|
||||
relationType,
|
||||
target: toObjectMetadata.nameSingular,
|
||||
inverseSide: fromObjectMetadata.nameSingular,
|
||||
inverseSide: toFieldMetadata.name,
|
||||
joinColumn:
|
||||
// TODO: This will work for now but we need to handle this better in the future for custom names on the join column
|
||||
relationType === 'many-to-one' ||
|
||||
|
||||
@ -5,7 +5,7 @@ import { DataSource, QueryFailedError } from 'typeorm';
|
||||
|
||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { FeatureFlagFactory } from 'src/engine/core-modules/feature-flag/services/feature-flags.factory';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.service';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
@ -27,7 +27,7 @@ export class WorkspaceSyncMetadataService {
|
||||
constructor(
|
||||
@InjectDataSource('metadata')
|
||||
private readonly metadataDataSource: DataSource,
|
||||
private readonly featureFlagFactory: FeatureFlagFactory,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceSyncObjectMetadataService: WorkspaceSyncObjectMetadataService,
|
||||
private readonly workspaceSyncRelationMetadataService: WorkspaceSyncRelationMetadataService,
|
||||
@ -69,9 +69,10 @@ export class WorkspaceSyncMetadataService {
|
||||
);
|
||||
|
||||
// Retrieve feature flags
|
||||
const workspaceFeatureFlagsMap = await this.featureFlagFactory.create(
|
||||
context.workspaceId,
|
||||
);
|
||||
const workspaceFeatureFlagsMap =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlags(
|
||||
context.workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log('Syncing standard objects and fields metadata');
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
|
||||
import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event-cleaner/calendar-event-cleaner.module';
|
||||
@ -20,10 +19,6 @@ import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event
|
||||
import { CalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
|
||||
import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service';
|
||||
import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module';
|
||||
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { RefreshAccessTokenManagerModule } from 'src/modules/connected-account/refresh-access-token-manager/refresh-access-token-manager.module';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
@ -31,12 +26,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TwentyORMModule.forFeature([
|
||||
CalendarEventWorkspaceEntity,
|
||||
CalendarChannelWorkspaceEntity,
|
||||
CalendarChannelEventAssociationWorkspaceEntity,
|
||||
CalendarEventParticipantWorkspaceEntity,
|
||||
]),
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
BlocklistWorkspaceEntity,
|
||||
|
||||
@ -7,7 +7,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { CalendarCreateCompanyAndContactAfterSyncJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job';
|
||||
import { CalendarEventParticipantMatchParticipantJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job';
|
||||
@ -16,7 +15,6 @@ import { CalendarEventParticipantPersonListener } from 'src/modules/calendar/cal
|
||||
import { CalendarEventParticipantWorkspaceMemberListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener';
|
||||
import { CalendarEventParticipantListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener';
|
||||
import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service';
|
||||
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||
import { ContactCreationManagerModule } from 'src/modules/contact-creation-manager/contact-creation-manager.module';
|
||||
import { MatchParticipantModule } from 'src/modules/match-participant/match-participant.module';
|
||||
|
||||
@ -24,7 +22,6 @@ import { MatchParticipantModule } from 'src/modules/match-participant/match-part
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceModule,
|
||||
TwentyORMModule.forFeature([CalendarEventParticipantWorkspaceEntity]),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
||||
'metadata',
|
||||
|
||||
@ -1,21 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
|
||||
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
|
||||
import { CanAccessCalendarEventService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service';
|
||||
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TwentyORMModule.forFeature([
|
||||
CalendarChannelEventAssociationWorkspaceEntity,
|
||||
CalendarChannelWorkspaceEntity,
|
||||
]),
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
WorkspaceMemberWorkspaceEntity,
|
||||
|
||||
@ -5,12 +5,10 @@ import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module';
|
||||
import { RefreshAccessTokenManagerModule } from 'src/modules/connected-account/refresh-access-token-manager/refresh-access-token-manager.module';
|
||||
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessagingSingleMessageImportCommand } from 'src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command';
|
||||
import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-message-list-fetch.cron.command';
|
||||
import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command';
|
||||
@ -42,7 +40,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
|
||||
MessagingCommonModule,
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
|
||||
TwentyORMModule.forFeature([MessageChannelWorkspaceEntity]),
|
||||
BillingModule,
|
||||
EmailAliasManagerModule,
|
||||
FeatureFlagModule,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service';
|
||||
import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum';
|
||||
@ -41,7 +41,7 @@ export class MessagingMessagesImportService {
|
||||
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
|
||||
private readonly blocklistRepository: BlocklistRepository,
|
||||
private readonly emailAliasManagerService: EmailAliasManagerService,
|
||||
private readonly isFeatureEnabledService: IsFeatureEnabledService,
|
||||
private readonly isFeatureEnabledService: FeatureFlagService,
|
||||
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
||||
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
|
||||
@ -6,9 +6,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { ContactCreationManagerModule } from 'src/modules/contact-creation-manager/contact-creation-manager.module';
|
||||
import { MatchParticipantModule } from 'src/modules/match-participant/match-participant.module';
|
||||
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
|
||||
@ -31,7 +29,6 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
||||
TimelineActivityWorkspaceEntity,
|
||||
]),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]),
|
||||
MessagingCommonModule,
|
||||
MatchParticipantModule,
|
||||
],
|
||||
|
||||
@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
|
||||
@ -25,7 +25,7 @@ export class DatabaseEventTriggerListener {
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectMessageQueue(MessageQueue.workflowQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly isFeatureFlagEnabledService: IsFeatureEnabledService,
|
||||
private readonly isFeatureFlagEnabledService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
@OnEvent('*.created')
|
||||
|
||||
@ -1,18 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity';
|
||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook';
|
||||
import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TwentyORMModule.forFeature([
|
||||
AttachmentWorkspaceEntity,
|
||||
CommentWorkspaceEntity,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
WorkspaceMemberDeleteOnePreQueryHook,
|
||||
WorkspaceMemberDeleteManyPreQueryHook,
|
||||
|
||||
@ -8,11 +8,11 @@ import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TwentyORMModule.register({}),
|
||||
IntegrationsModule,
|
||||
MessageQueueModule.registerExplorer(),
|
||||
WorkspaceEventEmitterModule,
|
||||
JobsModule,
|
||||
TwentyORMModule,
|
||||
],
|
||||
})
|
||||
export class QueueWorkerModule {}
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
export const isPlainObject = <T>(value: T): boolean => {
|
||||
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
|
||||
return prototype === null || prototype === Object.prototype;
|
||||
export const isPlainObject = (
|
||||
input: unknown,
|
||||
): input is Record<string, unknown> => {
|
||||
return typeof input === 'object' && input !== null && !Array.isArray(input);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user