Skip to content

Commit

Permalink
feat(aggregations): Add aggregations to graphql
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Jul 16, 2020
1 parent c37b7ae commit af075d2
Show file tree
Hide file tree
Showing 29 changed files with 834 additions and 9 deletions.
118 changes: 118 additions & 0 deletions packages/core/__tests__/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
import {
AggregateResponse,
applyFilter,
Filter,
Query,
QueryFieldMap,
SortDirection,
SortField,
transformAggregateQuery,
transformAggregateResponse,
transformFilter,
transformQuery,
transformSort,
} from '../src';
import { getFilterFields } from '../src/helpers/query.helpers';
import { AggregateQuery } from '../src/interfaces/aggregate-query.interface';

class TestDTO {
first!: string;

last!: string;

age?: number;
}

class TestEntity {
firstName!: string;

lastName!: string;

ageInYears?: number;
}

const fieldMap: QueryFieldMap<TestDTO, TestEntity> = {
first: 'firstName',
last: 'lastName',
age: 'ageInYears',
};

describe('transformSort', () => {
Expand Down Expand Up @@ -386,3 +395,112 @@ describe('getFilterFields', () => {
expect(getFilterFields(filter).sort()).toEqual(['boolField', 'strField', 'testRelation']);
});
});

describe('transformAggregateQuery', () => {
it('should transform an aggregate query', () => {
const aggQuery: AggregateQuery<TestDTO> = {
count: ['first'],
sum: ['age'],
max: ['first', 'last', 'age'],
min: ['first', 'last', 'age'],
};
const entityAggQuery: AggregateQuery<TestEntity> = {
count: ['firstName'],
sum: ['ageInYears'],
max: ['firstName', 'lastName', 'ageInYears'],
min: ['firstName', 'lastName', 'ageInYears'],
};
expect(transformAggregateQuery(aggQuery, fieldMap)).toEqual(entityAggQuery);
});

it('should throw an error if an unknown field is encountered', () => {
const aggQuery: AggregateQuery<TestDTO> = {
count: ['first'],
sum: ['age'],
max: ['first', 'last', 'age'],
min: ['first', 'last', 'age'],
};
// @ts-ignore
expect(() => transformAggregateQuery(aggQuery, { last: 'lastName' })).toThrow(
"No corresponding field found for 'first' when transforming aggregateQuery",
);
});
});

describe('transformAggregateResponse', () => {
it('should transform an aggregate query', () => {
const aggResponse: AggregateResponse<TestDTO> = {
count: {
first: 2,
},
sum: {
age: 101,
},
max: {
first: 'firstz',
last: 'lastz',
age: 100,
},
min: {
first: 'firsta',
last: 'lasta',
age: 1,
},
};
const entityAggResponse: AggregateResponse<TestEntity> = {
count: {
firstName: 2,
},
sum: {
ageInYears: 101,
},
max: {
firstName: 'firstz',
lastName: 'lastz',
ageInYears: 100,
},
min: {
firstName: 'firsta',
lastName: 'lasta',
ageInYears: 1,
},
};
expect(transformAggregateResponse(aggResponse, fieldMap)).toEqual(entityAggResponse);
});

it('should handle empty aggregate fields', () => {
const aggResponse: AggregateResponse<TestDTO> = {
count: {
first: 2,
},
};
const entityAggResponse: AggregateResponse<TestEntity> = {
count: {
firstName: 2,
},
};
expect(transformAggregateResponse(aggResponse, fieldMap)).toEqual(entityAggResponse);
});

it('should throw an error if the field is not found', () => {
let aggResponse: AggregateResponse<TestDTO> = {
count: {
first: 2,
},
};
// @ts-ignore
expect(() => transformAggregateResponse(aggResponse, { last: 'lastName' })).toThrow(
"No corresponding field found for 'first' when transforming aggregateQuery",
);

aggResponse = {
max: {
age: 10,
},
};
// @ts-ignore
expect(() => transformAggregateResponse(aggResponse, { last: 'lastName' })).toThrow(
"No corresponding field found for 'age' when transforming aggregateQuery",
);
});
});
4 changes: 4 additions & 0 deletions packages/core/__tests__/services/noop-query.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ describe('NoOpQueryService', () => {
return expect(instance.query({})).rejects.toThrow('query is not implemented');
});

it('should throw a NotImplementedException when calling aggregate', () => {
return expect(instance.aggregate({}, {})).rejects.toThrow('aggregate is not implemented');
});

it('should throw a NotImplementedException when calling count', () => {
return expect(instance.count({})).rejects.toThrow('count is not implemented');
});
Expand Down
10 changes: 9 additions & 1 deletion packages/core/__tests__/services/proxy-query.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mock, reset, instance, when } from 'ts-mockito';
import { QueryService } from '../../src';
import { QueryService, AggregateQuery } from '../../src';
import { ProxyQueryService } from '../../src/services/proxy-query.service';

describe('NoOpQueryService', () => {
Expand Down Expand Up @@ -73,6 +73,14 @@ describe('NoOpQueryService', () => {
when(mockQueryService.query(query)).thenResolve(result);
return expect(queryService.query(query)).resolves.toBe(result);
});

it('should proxy to the underlying service when calling aggregate', () => {
const filter = {};
const aggregate: AggregateQuery<TestType> = { count: ['foo'] };
const result = { count: { foo: 1 } };
when(mockQueryService.aggregate(filter, aggregate)).thenResolve(result);
return expect(queryService.aggregate(filter, aggregate)).resolves.toBe(result);
});
it('should proxy to the underlying service when calling count', () => {
const query = {};
const result = 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
type Query {
aggregate(
"""Filter to find records to aggregate on"""
filter: FakeTypeAggregateFilter
): Int!
}

input FakeTypeAggregateFilter {
and: [FakeTypeAggregateFilter!]
or: [FakeTypeAggregateFilter!]
stringField: StringFieldComparison
numberField: NumberFieldComparison
boolField: BooleanFieldComparison
dateField: DateFieldComparison
}

input StringFieldComparison {
is: Boolean
isNot: Boolean
eq: String
neq: String
gt: String
gte: String
lt: String
lte: String
like: String
notLike: String
iLike: String
notILike: String
in: [String!]
notIn: [String!]
}

input NumberFieldComparison {
is: Boolean
isNot: Boolean
eq: Float
neq: Float
gt: Float
gte: Float
lt: Float
lte: Float
in: [Float!]
notIn: [Float!]
between: NumberFieldComparisonBetween
notBetween: NumberFieldComparisonBetween
}

input NumberFieldComparisonBetween {
lower: Float!
upper: Float!
}

input BooleanFieldComparison {
is: Boolean
isNot: Boolean
}

input DateFieldComparison {
is: Boolean
isNot: Boolean
eq: DateTime
neq: DateTime
gt: DateTime
gte: DateTime
lt: DateTime
lte: DateTime
in: [DateTime!]
notIn: [DateTime!]
between: DateFieldComparisonBetween
notBetween: DateFieldComparisonBetween
}

"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

input DateFieldComparisonBetween {
lower: DateTime!
upper: DateTime!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
type FakeTypeCountAggregate {
stringField: Int
numberField: Int
boolField: Int
dateField: Int
}

type FakeTypeSumAggregate {
numberField: Float
}

type FakeTypeAvgAggregate {
numberField: Float
}

type FakeTypeMinAggregate {
stringField: String
numberField: Float
dateField: DateTime
}

"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

type FakeTypeMaxAggregate {
stringField: String
numberField: Float
dateField: DateTime
}

type CustomPrefixCountAggregate {
stringField: Int
numberField: Int
boolField: Int
dateField: Int
}

type CustomPrefixSumAggregate {
numberField: Float
}

type CustomPrefixAvgAggregate {
numberField: Float
}

type CustomPrefixMinAggregate {
stringField: String
numberField: Float
dateField: DateTime
}

type CustomPrefixMaxAggregate {
stringField: String
numberField: Float
dateField: DateTime
}

type CustomPrefixAggregateResponse {
count: CustomPrefixCountAggregate
sum: CustomPrefixSumAggregate
avg: CustomPrefixAvgAggregate
min: CustomPrefixMinAggregate
max: CustomPrefixMaxAggregate
}

type Query {
aggregate: CustomPrefixAggregateResponse!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
type FakeTypeCountAggregate {
stringField: Int
numberField: Int
boolField: Int
dateField: Int
}

type FakeTypeSumAggregate {
numberField: Float
}

type FakeTypeAvgAggregate {
numberField: Float
}

type FakeTypeMinAggregate {
stringField: String
numberField: Float
dateField: DateTime
}

"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

type FakeTypeMaxAggregate {
stringField: String
numberField: Float
dateField: DateTime
}

type FakeTypeAggregateResponse {
count: FakeTypeCountAggregate
sum: FakeTypeSumAggregate
avg: FakeTypeAvgAggregate
min: FakeTypeMinAggregate
max: FakeTypeMaxAggregate
}

type Query {
aggregate: FakeTypeAggregateResponse!
}
5 changes: 5 additions & 0 deletions packages/query-graphql/__tests__/__fixtures__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export const expectSDL = async (resolvers: Function[], sdl: string): Promise<voi
return expect(printSchema(schema)).toEqual(sdl);
};

export const aggregateArgsTypeSDL = readGraphql(resolve(__dirname, './aggregate-args-type.graphql'));
export const aggregateResponseTypeSDL = readGraphql(resolve(__dirname, './aggregate-response-type.graphql'));
export const aggregateResponseTypeWithCustomNameSDL = readGraphql(
resolve(__dirname, './aggregate-response-type-with-custom-name.graphql'),
);
export const deleteManyResponseTypeSDL = readGraphql(resolve(__dirname, './delete-many-response-type.graphql'));
export const updateManyResponseTypeSDL = readGraphql(resolve(__dirname, './update-many-response-type.graphql'));
export const createOneInputTypeSDL = readGraphql(resolve(__dirname, './create-one-input-type.graphql'));
Expand Down
Loading

0 comments on commit af075d2

Please sign in to comment.