First step of https://github.com/twentyhq/twenty/issues/6868 Adds min.., max.. queries for DATETIME fields adds min.., max.., avg.., sum.. queries for NUMBER fields (count distinct operation and composite fields such as CURRENCY handling will be dealt with in a future PR) <img width="1422" alt="Capture d’écran 2024-11-06 à 15 48 46" src="https://github.com/user-attachments/assets/4bcdece0-ad3e-4536-9720-fe4044a36719"> --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import {
|
||||
AggregationField,
|
||||
getAvailableAggregationsFromObjectFields,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
|
||||
|
||||
type AggregationGraphQLType = Pick<AggregationField, 'type' | 'description'>;
|
||||
|
||||
@Injectable()
|
||||
export class AggregationTypeFactory {
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
): Record<string, AggregationGraphQLType> {
|
||||
const availableAggregations = getAvailableAggregationsFromObjectFields(
|
||||
objectMetadata.fields,
|
||||
);
|
||||
|
||||
return Object.entries(availableAggregations).reduce<
|
||||
Record<string, AggregationGraphQLType>
|
||||
>((acc, [key, agg]) => {
|
||||
acc[key] = {
|
||||
type: agg.type,
|
||||
description: agg.description,
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLInt, GraphQLObjectType } from 'graphql';
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { AggregationTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
import {
|
||||
ObjectTypeDefinition,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from './object-type-definition.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
|
||||
export enum ConnectionTypeDefinitionKind {
|
||||
Edge = 'Edge',
|
||||
@ -20,7 +21,10 @@ export enum ConnectionTypeDefinitionKind {
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionTypeDefinitionFactory {
|
||||
constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {}
|
||||
constructor(
|
||||
private readonly connectionTypeFactory: ConnectionTypeFactory,
|
||||
private readonly aggregationTypeFactory: AggregationTypeFactory,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
@ -45,6 +49,10 @@ export class ConnectionTypeDefinitionFactory {
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
const aggregatedFields = this.aggregationTypeFactory.create(objectMetadata);
|
||||
|
||||
Object.assign(fields, aggregatedFields);
|
||||
|
||||
fields.edges = {
|
||||
type: this.connectionTypeFactory.create(
|
||||
objectMetadata,
|
||||
@ -69,11 +77,6 @@ export class ConnectionTypeDefinitionFactory {
|
||||
),
|
||||
};
|
||||
|
||||
fields.totalCount = {
|
||||
type: GraphQLInt,
|
||||
description: 'Total number of records in the connection',
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
|
||||
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
||||
import { AggregationTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/aggregation-type.factory';
|
||||
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
|
||||
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
||||
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
|
||||
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||
|
||||
import { ArgsFactory } from './args.factory';
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
|
||||
import { EdgeTypeFactory } from './edge-type.factory';
|
||||
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
|
||||
import { InputTypeDefinitionFactory } from './input-type-definition.factory';
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
import { MutationTypeFactory } from './mutation-type.factory';
|
||||
import { ObjectTypeDefinitionFactory } from './object-type-definition.factory';
|
||||
import { OrphanedTypesFactory } from './orphaned-types.factory';
|
||||
import { OutputTypeFactory } from './output-type.factory';
|
||||
import { QueryTypeFactory } from './query-type.factory';
|
||||
import { RootTypeFactory } from './root-type.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
|
||||
import { EdgeTypeFactory } from './edge-type.factory';
|
||||
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
|
||||
import { MutationTypeFactory } from './mutation-type.factory';
|
||||
import { RelationTypeFactory } from './relation-type.factory';
|
||||
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
|
||||
import { OrphanedTypesFactory } from './orphaned-types.factory';
|
||||
import { RootTypeFactory } from './root-type.factory';
|
||||
|
||||
export const workspaceSchemaBuilderFactories = [
|
||||
ArgsFactory,
|
||||
@ -39,4 +40,5 @@ export const workspaceSchemaBuilderFactories = [
|
||||
QueryTypeFactory,
|
||||
MutationTypeFactory,
|
||||
OrphanedTypesFactory,
|
||||
AggregationTypeFactory,
|
||||
];
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
import { Kind } from 'graphql/language';
|
||||
|
||||
export const DateTimeScalarType = new GraphQLScalarType({
|
||||
name: 'DateTime',
|
||||
description: 'A custom scalar that represents a datetime in ISO format',
|
||||
serialize(value: string): string {
|
||||
const date = new Date(value);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error('Invalid date format, expected ISO date string');
|
||||
}
|
||||
|
||||
return date.toISOString();
|
||||
},
|
||||
parseValue(value: string): Date {
|
||||
const date = new Date(value);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error('Invalid date format, expected ISO date string');
|
||||
}
|
||||
|
||||
return date;
|
||||
},
|
||||
parseLiteral(ast): Date {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new Error('Invalid date format, expected ISO date string');
|
||||
}
|
||||
|
||||
const date = new Date(ast.value);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error('Invalid date format, expected ISO date string');
|
||||
}
|
||||
|
||||
return date;
|
||||
},
|
||||
});
|
||||
@ -1,10 +1,9 @@
|
||||
import { RawJSONScalar } from './raw-json.scalar';
|
||||
import { PositionScalarType } from './position.scalar';
|
||||
import { CursorScalarType } from './cursor.scalar';
|
||||
import { BigFloatScalarType } from './big-float.scalar';
|
||||
import { BigIntScalarType } from './big-int.scalar';
|
||||
import { CursorScalarType } from './cursor.scalar';
|
||||
import { DateScalarType } from './date.scalar';
|
||||
import { DateTimeScalarType } from './date-time.scalar';
|
||||
import { PositionScalarType } from './position.scalar';
|
||||
import { RawJSONScalar } from './raw-json.scalar';
|
||||
import { TimeScalarType } from './time.scalar';
|
||||
import { UUIDScalarType } from './uuid.scalar';
|
||||
|
||||
@ -12,7 +11,6 @@ export * from './big-float.scalar';
|
||||
export * from './big-int.scalar';
|
||||
export * from './cursor.scalar';
|
||||
export * from './date.scalar';
|
||||
export * from './date-time.scalar';
|
||||
export * from './time.scalar';
|
||||
export * from './uuid.scalar';
|
||||
|
||||
@ -20,7 +18,6 @@ export const scalars = [
|
||||
BigFloatScalarType,
|
||||
BigIntScalarType,
|
||||
DateScalarType,
|
||||
DateTimeScalarType,
|
||||
TimeScalarType,
|
||||
UUIDScalarType,
|
||||
CursorScalarType,
|
||||
|
||||
@ -1,17 +1,9 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import {
|
||||
ObjectMetadataMap,
|
||||
ObjectMetadataMapItem,
|
||||
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
export interface WorkspaceSchemaBuilderContext {
|
||||
authContext: AuthContext;
|
||||
fieldMetadataCollection: FieldMetadataInterface[];
|
||||
objectMetadataCollection: ObjectMetadataInterface[];
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
objectMetadataMap: ObjectMetadataMap;
|
||||
objectMetadataMapItem: ObjectMetadataMapItem;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
}
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
import { GraphQLISODateTime } from '@nestjs/graphql';
|
||||
|
||||
import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
enum AGGREGATION_OPERATIONS {
|
||||
min = 'MIN',
|
||||
max = 'MAX',
|
||||
avg = 'AVG',
|
||||
sum = 'SUM',
|
||||
count = 'COUNT',
|
||||
}
|
||||
|
||||
export type AggregationField = {
|
||||
type: GraphQLScalarType;
|
||||
description: string;
|
||||
fromField: string;
|
||||
aggregationOperation: AGGREGATION_OPERATIONS;
|
||||
};
|
||||
|
||||
export const getAvailableAggregationsFromObjectFields = (
|
||||
fields: FieldMetadataInterface[],
|
||||
): Record<string, AggregationField> => {
|
||||
return fields.reduce<Record<string, AggregationField>>((acc, field) => {
|
||||
acc['totalCount'] = {
|
||||
type: GraphQLInt,
|
||||
description: `Total number of records in the connection`,
|
||||
fromField: 'id',
|
||||
aggregationOperation: AGGREGATION_OPERATIONS.count,
|
||||
};
|
||||
|
||||
if (field.type === FieldMetadataType.DATE_TIME) {
|
||||
acc[`min${capitalize(field.name)}`] = {
|
||||
type: GraphQLISODateTime,
|
||||
description: `Oldest date contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
aggregationOperation: AGGREGATION_OPERATIONS.min,
|
||||
};
|
||||
|
||||
acc[`max${capitalize(field.name)}`] = {
|
||||
type: GraphQLISODateTime,
|
||||
description: `Most recent date contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
aggregationOperation: AGGREGATION_OPERATIONS.max,
|
||||
};
|
||||
}
|
||||
|
||||
if (field.type === FieldMetadataType.NUMBER) {
|
||||
acc[`min${capitalize(field.name)}`] = {
|
||||
type: GraphQLFloat,
|
||||
description: `Minimum amount contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
aggregationOperation: AGGREGATION_OPERATIONS.min,
|
||||
};
|
||||
|
||||
acc[`max${capitalize(field.name)}`] = {
|
||||
type: GraphQLFloat,
|
||||
description: `Maximum amount contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
aggregationOperation: AGGREGATION_OPERATIONS.max,
|
||||
};
|
||||
|
||||
acc[`avg${capitalize(field.name)}`] = {
|
||||
type: GraphQLFloat,
|
||||
description: `Average amount contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
aggregationOperation: AGGREGATION_OPERATIONS.avg,
|
||||
};
|
||||
|
||||
acc[`sum${capitalize(field.name)}`] = {
|
||||
type: GraphQLFloat,
|
||||
description: `Sum of amounts contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
aggregationOperation: AGGREGATION_OPERATIONS.sum,
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
@ -7,10 +7,10 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
|
||||
|
||||
import { TypeDefinitionsGenerator } from './type-definitions.generator';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
|
||||
import { QueryTypeFactory } from './factories/query-type.factory';
|
||||
import { MutationTypeFactory } from './factories/mutation-type.factory';
|
||||
import { OrphanedTypesFactory } from './factories/orphaned-types.factory';
|
||||
import { QueryTypeFactory } from './factories/query-type.factory';
|
||||
import { WorkspaceBuildSchemaOptions } from './interfaces/workspace-build-schema-optionts.interface';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceGraphQLSchemaFactory {
|
||||
|
||||
Reference in New Issue
Block a user