feat: conditional filtering & aggregation support & data ordering support (#2107)
* feat: wip * feat: add filter on findOne * fix: tests & small bug * feat: add test and support aggregation * feat: add order by support * fix: fix comments * fix: tests
This commit is contained in:
@ -0,0 +1,24 @@
|
||||
import { GraphQLEnumType } from 'graphql';
|
||||
|
||||
export const OrderByDirectionType = new GraphQLEnumType({
|
||||
name: 'OrderByDirection',
|
||||
description: 'This enum is used to specify the order of results',
|
||||
values: {
|
||||
AscNullsFirst: {
|
||||
value: 'AscNullsFirst',
|
||||
description: 'Ascending order, nulls first',
|
||||
},
|
||||
AscNullsLast: {
|
||||
value: 'AscNullsLast',
|
||||
description: 'Ascending order, nulls last',
|
||||
},
|
||||
DescNullsFirst: {
|
||||
value: 'DescNullsFirst',
|
||||
description: 'Descending order, nulls first',
|
||||
},
|
||||
DescNullsLast: {
|
||||
value: 'DescNullsLast',
|
||||
description: 'Descending order, nulls last',
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
|
||||
import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date-time.scalar';
|
||||
|
||||
import { FilterIsEnumType } from './filter-is-enum-filter.type';
|
||||
|
||||
export const DatetimeFilterType = new GraphQLInputObjectType({
|
||||
name: 'DateTimeFilter',
|
||||
fields: {
|
||||
eq: { type: DateTimeScalarType },
|
||||
gt: { type: DateTimeScalarType },
|
||||
gte: { type: DateTimeScalarType },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(DateTimeScalarType)) },
|
||||
lt: { type: DateTimeScalarType },
|
||||
lte: { type: DateTimeScalarType },
|
||||
neq: { type: DateTimeScalarType },
|
||||
is: { type: FilterIsEnumType },
|
||||
},
|
||||
});
|
||||
@ -1,19 +0,0 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
|
||||
import { DatetimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/datetime.scalar';
|
||||
|
||||
import { FilterIsEnumType } from './filter-is-enum-filter.type';
|
||||
|
||||
export const DatetimeFilterInputType = new GraphQLInputObjectType({
|
||||
name: 'DatetimeFilter',
|
||||
fields: {
|
||||
eq: { type: DatetimeScalarType },
|
||||
gt: { type: DatetimeScalarType },
|
||||
gte: { type: DatetimeScalarType },
|
||||
in: { type: new GraphQLList(new GraphQLNonNull(DatetimeScalarType)) },
|
||||
lt: { type: DatetimeScalarType },
|
||||
lte: { type: DatetimeScalarType },
|
||||
neq: { type: DatetimeScalarType },
|
||||
is: { type: FilterIsEnumType },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { GraphQLScalarType, Kind } from 'graphql';
|
||||
|
||||
export const CursorScalarType = new GraphQLScalarType({
|
||||
name: 'Cursor',
|
||||
description: 'A custom scalar that represents a cursor for pagination',
|
||||
serialize(value) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Cursor must be a string');
|
||||
}
|
||||
return value;
|
||||
},
|
||||
parseValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Cursor must be a string');
|
||||
}
|
||||
return value;
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new Error('Cursor must be a string');
|
||||
}
|
||||
return ast.value;
|
||||
},
|
||||
});
|
||||
@ -1,8 +1,8 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
import { Kind } from 'graphql/language';
|
||||
|
||||
export const DatetimeScalarType = new GraphQLScalarType({
|
||||
name: 'Datetime',
|
||||
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);
|
||||
@ -0,0 +1,17 @@
|
||||
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 { TimeScalarType } from './time.scalar';
|
||||
import { UUIDScalarType } from './uuid.scalar';
|
||||
|
||||
export const scalars = [
|
||||
BigFloatScalarType,
|
||||
BigIntScalarType,
|
||||
DateScalarType,
|
||||
DateTimeScalarType,
|
||||
TimeScalarType,
|
||||
UUIDScalarType,
|
||||
CursorScalarType,
|
||||
];
|
||||
@ -4,6 +4,7 @@ import {
|
||||
GraphQLFieldConfigMap,
|
||||
GraphQLID,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInt,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType,
|
||||
@ -21,6 +22,10 @@ import { generateCreateInputType } from './utils/generate-create-input-type.util
|
||||
import { generateUpdateInputType } from './utils/generate-update-input-type.util';
|
||||
import { SchemaBuilderContext } from './interfaces/schema-builder-context.interface';
|
||||
import { cleanEntityName } from './utils/clean-entity-name.util';
|
||||
import { scalars } from './graphql-types/scalars';
|
||||
import { CursorScalarType } from './graphql-types/scalars/cursor.scalar';
|
||||
import { generateFilterInputType } from './utils/generate-filter-input-type.util';
|
||||
import { generateOrderByInputType } from './utils/generate-order-by-input-type.util';
|
||||
|
||||
@Injectable()
|
||||
export class SchemaBuilderService {
|
||||
@ -45,12 +50,29 @@ export class SchemaBuilderService {
|
||||
|
||||
const EdgeType = generateEdgeType(ObjectType);
|
||||
const ConnectionType = generateConnectionType(EdgeType);
|
||||
const FilterInputType = generateFilterInputType(
|
||||
entityName.singular,
|
||||
objectDefinition.fields,
|
||||
);
|
||||
const OrderByInputType = generateOrderByInputType(
|
||||
entityName.singular,
|
||||
objectDefinition.fields,
|
||||
);
|
||||
|
||||
return {
|
||||
[`${entityName.plural}`]: {
|
||||
type: ConnectionType,
|
||||
args: {
|
||||
first: { type: GraphQLInt },
|
||||
last: { type: GraphQLInt },
|
||||
before: { type: CursorScalarType },
|
||||
after: { type: CursorScalarType },
|
||||
filter: { type: FilterInputType },
|
||||
orderBy: { type: OrderByInputType },
|
||||
},
|
||||
resolve: async (root, args, context, info) => {
|
||||
return this.entityResolverService.findMany(
|
||||
args,
|
||||
schemaBuilderContext,
|
||||
info,
|
||||
);
|
||||
@ -59,7 +81,7 @@ export class SchemaBuilderService {
|
||||
[`${entityName.singular}`]: {
|
||||
type: ObjectType,
|
||||
args: {
|
||||
id: { type: new GraphQLNonNull(GraphQLID) },
|
||||
filter: { type: FilterInputType },
|
||||
},
|
||||
resolve: (root, args, context, info) => {
|
||||
return this.entityResolverService.findOne(
|
||||
@ -211,6 +233,7 @@ export class SchemaBuilderService {
|
||||
return new GraphQLSchema({
|
||||
query,
|
||||
mutation,
|
||||
types: [...scalars],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { GraphQLList, GraphQLNonNull, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { generateFilterInputType } from 'src/tenant/schema-builder/utils/generate-filter-input-type.util';
|
||||
import { mapColumnTypeToFilterType } from 'src/tenant/schema-builder/utils/map-column-type-to-filter-type.util';
|
||||
|
||||
describe('generateFilterInputType', () => {
|
||||
it('handles empty columns array', () => {
|
||||
const FilterInputType = generateFilterInputType('EmptyTest', []);
|
||||
|
||||
expect(FilterInputType.name).toBe('EmptyTestFilterInput');
|
||||
|
||||
expect(FilterInputType.getFields()).toHaveProperty('id');
|
||||
expect(FilterInputType.getFields()).toHaveProperty('createdAt');
|
||||
expect(FilterInputType.getFields()).toHaveProperty('updatedAt');
|
||||
expect(FilterInputType.getFields()).toHaveProperty('and');
|
||||
expect(FilterInputType.getFields()).toHaveProperty('or');
|
||||
expect(FilterInputType.getFields()).toHaveProperty('not');
|
||||
});
|
||||
|
||||
it('handles various column types', () => {
|
||||
const columns = [
|
||||
{ name: 'stringField', type: 'text' },
|
||||
{ name: 'intField', type: 'number' },
|
||||
{ name: 'booleanField', type: 'boolean' },
|
||||
] as FieldMetadata[];
|
||||
|
||||
const FilterInputType = generateFilterInputType('MultiTypeTest', columns);
|
||||
|
||||
columns.forEach((column) => {
|
||||
const expectedType = mapColumnTypeToFilterType(column);
|
||||
|
||||
expect(FilterInputType.getFields()[column.name].type).toBe(expectedType);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles nested logical fields', () => {
|
||||
const FilterInputType = generateFilterInputType('NestedTest', []);
|
||||
|
||||
const andFieldType = FilterInputType.getFields().and.type;
|
||||
const orFieldType = FilterInputType.getFields().or.type;
|
||||
const notFieldType = FilterInputType.getFields().not.type;
|
||||
|
||||
expect(andFieldType).toBeInstanceOf(GraphQLList);
|
||||
expect(orFieldType).toBeInstanceOf(GraphQLList);
|
||||
|
||||
if (notFieldType instanceof GraphQLNonNull) {
|
||||
expect(notFieldType.ofType).toBe(FilterInputType);
|
||||
} else {
|
||||
expect(notFieldType).toBeInstanceOf(GraphQLInputObjectType);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -8,6 +8,7 @@ import {
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { generateObjectType } from 'src/tenant/schema-builder/utils/generate-object-type.util';
|
||||
import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date-time.scalar';
|
||||
|
||||
describe('generateObjectType', () => {
|
||||
test('should generate a GraphQLObjectType with correct name', () => {
|
||||
@ -31,13 +32,13 @@ describe('generateObjectType', () => {
|
||||
}
|
||||
|
||||
if (fields.createdAt.type instanceof GraphQLNonNull) {
|
||||
expect(fields.createdAt.type.ofType).toBe(GraphQLString);
|
||||
expect(fields.createdAt.type.ofType).toBe(DateTimeScalarType);
|
||||
} else {
|
||||
fail('createdAt.type is not an instance of GraphQLNonNull');
|
||||
}
|
||||
|
||||
if (fields.updatedAt.type instanceof GraphQLNonNull) {
|
||||
expect(fields.updatedAt.type.ofType).toBe(GraphQLString);
|
||||
expect(fields.updatedAt.type.ofType).toBe(DateTimeScalarType);
|
||||
} else {
|
||||
fail('updatedAt.type is not an instance of GraphQLNonNull');
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { OrderByDirectionType } from 'src/tenant/schema-builder/graphql-types/enum/order-by-direction.type';
|
||||
import { generateOrderByInputType } from 'src/tenant/schema-builder/utils/generate-order-by-input-type.util';
|
||||
|
||||
describe('generateOrderByInputType', () => {
|
||||
it('includes default fields', () => {
|
||||
const result = generateOrderByInputType('SampleType', []);
|
||||
const fields = result.getFields();
|
||||
|
||||
expect(fields.id.type).toBe(OrderByDirectionType);
|
||||
expect(fields.createdAt.type).toBe(OrderByDirectionType);
|
||||
expect(fields.updatedAt.type).toBe(OrderByDirectionType);
|
||||
});
|
||||
|
||||
it('adds fields from provided FieldMetadata columns', () => {
|
||||
const testColumns = [
|
||||
{ name: 'customField1' },
|
||||
{ name: 'customField2' },
|
||||
] as FieldMetadata[];
|
||||
const result = generateOrderByInputType('SampleType', testColumns);
|
||||
const fields = result.getFields();
|
||||
|
||||
expect(fields.customField1.type).toBe(OrderByDirectionType);
|
||||
expect(fields.customField2.type).toBe(OrderByDirectionType);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,52 @@
|
||||
import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { UUIDFilterType } from 'src/tenant/schema-builder/graphql-types/input/uuid-filter.type';
|
||||
import { DatetimeFilterType } from 'src/tenant/schema-builder/graphql-types/input/date-time-filter.type';
|
||||
|
||||
import { mapColumnTypeToFilterType } from './map-column-type-to-filter-type.util';
|
||||
|
||||
const defaultFields = {
|
||||
id: { type: UUIDFilterType },
|
||||
createdAt: { type: DatetimeFilterType },
|
||||
updatedAt: { type: DatetimeFilterType },
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a GraphQL filter input type with filters based on the columns from metadata.
|
||||
* @param name Name for the GraphQL object.
|
||||
* @param columns Array of FieldMetadata columns.
|
||||
* @returns GraphQLInputObjectType
|
||||
*/
|
||||
export const generateFilterInputType = (
|
||||
name: string,
|
||||
columns: FieldMetadata[],
|
||||
): GraphQLInputObjectType => {
|
||||
const filterInputType = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(name)}FilterInput`,
|
||||
fields: () => ({
|
||||
...defaultFields,
|
||||
...columns.reduce((fields, column) => {
|
||||
const graphqlType = mapColumnTypeToFilterType(column);
|
||||
|
||||
fields[column.name] = {
|
||||
type: graphqlType,
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, {}),
|
||||
and: {
|
||||
type: new GraphQLList(new GraphQLNonNull(filterInputType)),
|
||||
},
|
||||
or: {
|
||||
type: new GraphQLList(new GraphQLNonNull(filterInputType)),
|
||||
},
|
||||
not: {
|
||||
type: filterInputType,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return filterInputType;
|
||||
};
|
||||
@ -1,19 +1,15 @@
|
||||
import {
|
||||
GraphQLID,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType,
|
||||
GraphQLString,
|
||||
} from 'graphql';
|
||||
import { GraphQLID, GraphQLNonNull, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date-time.scalar';
|
||||
|
||||
import { mapColumnTypeToGraphQLType } from './map-column-type-to-graphql-type.util';
|
||||
|
||||
const defaultFields = {
|
||||
id: { type: new GraphQLNonNull(GraphQLID) },
|
||||
createdAt: { type: new GraphQLNonNull(GraphQLString) },
|
||||
updatedAt: { type: new GraphQLNonNull(GraphQLString) },
|
||||
createdAt: { type: new GraphQLNonNull(DateTimeScalarType) },
|
||||
updatedAt: { type: new GraphQLNonNull(DateTimeScalarType) },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { OrderByDirectionType } from 'src/tenant/schema-builder/graphql-types/enum/order-by-direction.type';
|
||||
|
||||
const defaultFields = {
|
||||
id: { type: OrderByDirectionType },
|
||||
createdAt: { type: OrderByDirectionType },
|
||||
updatedAt: { type: OrderByDirectionType },
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a GraphQL order by input type with order by fields based on the columns from metadata.
|
||||
* @param name Name for the GraphQL object.
|
||||
* @param columns Array of FieldMetadata columns.
|
||||
* @returns GraphQLInputObjectType
|
||||
*/
|
||||
export const generateOrderByInputType = (
|
||||
name: string,
|
||||
columns: FieldMetadata[],
|
||||
): GraphQLInputObjectType => {
|
||||
const fields = {
|
||||
...defaultFields,
|
||||
};
|
||||
|
||||
columns.forEach((column) => {
|
||||
fields[column.name] = {
|
||||
type: OrderByDirectionType,
|
||||
};
|
||||
});
|
||||
|
||||
return new GraphQLInputObjectType({
|
||||
name: `${pascalCase(name)}OrderBy`,
|
||||
fields,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user