-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Added
by
option to aggregated date fields so group by DAY
, …
…`MONTH` or `YEAR` (TypeORM)
- Loading branch information
Showing
11 changed files
with
267 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,26 @@ | ||
export enum GroupBy { | ||
DAY = 'DAY', | ||
MONTH = 'MONTH', | ||
YEAR = 'YEAR' | ||
} | ||
|
||
export type AggregateQueryField<DTO, ARGS = unknown> = { | ||
field: keyof DTO | ||
args: ARGS | ||
} | ||
|
||
export type AggregateQueryCountField<DTO> = AggregateQueryField<DTO> | ||
export type AggregateQuerySumField<DTO> = AggregateQueryField<DTO> | ||
export type AggregateQueryAvgField<DTO> = AggregateQueryField<DTO> | ||
export type AggregateQueryMaxField<DTO> = AggregateQueryField<DTO> | ||
export type AggregateQueryMinField<DTO> = AggregateQueryField<DTO> | ||
export type AggregateQueryGroupByField<DTO> = AggregateQueryField<DTO, { by: GroupBy }> | ||
|
||
export type AggregateQuery<DTO> = { | ||
count?: (keyof DTO)[] | ||
sum?: (keyof DTO)[] | ||
avg?: (keyof DTO)[] | ||
max?: (keyof DTO)[] | ||
min?: (keyof DTO)[] | ||
groupBy?: (keyof DTO)[] | ||
count?: AggregateQueryCountField<DTO>[] | ||
sum?: AggregateQuerySumField<DTO>[] | ||
avg?: AggregateQueryAvgField<DTO>[] | ||
max?: AggregateQueryMaxField<DTO>[] | ||
min?: AggregateQueryMinField<DTO>[] | ||
groupBy?: AggregateQueryGroupByField<DTO>[] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
packages/query-graphql/src/decorators/graphql-resolve-info.utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { ASTNode, FieldNode, getArgumentValues, getNamedType, GraphQLField, GraphQLUnionType, isCompositeType } from 'graphql' | ||
|
||
import type { CursorConnectionType, OffsetConnectionType } from '../types' | ||
import type { Query } from '@ptc-org/nestjs-query-core' | ||
import type { GraphQLCompositeType, GraphQLResolveInfo as ResolveInfo, SelectionNode } from 'graphql' | ||
|
||
type QueryResolveFields<DTO> = { | ||
[key in keyof DTO]: QueryResolveTree< | ||
// If the key is a array get the type of the array | ||
DTO[key] extends ArrayLike<unknown> ? DTO[key][number] : DTO[key] | ||
> | ||
} | ||
|
||
export interface QueryResolveTree<DTO> { | ||
name: string | ||
alias: string | ||
args?: Query<DTO> | ||
fields: QueryResolveFields<DTO> | ||
} | ||
|
||
function getFieldFromAST<TContext>( | ||
fieldNode: ASTNode, | ||
parentType: GraphQLCompositeType | ||
): GraphQLField<GraphQLCompositeType, TContext> | undefined { | ||
if (fieldNode.kind === 'Field') { | ||
if (!(parentType instanceof GraphQLUnionType)) { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
return parentType.getFields()[fieldNode.name.value] | ||
} else { | ||
// XXX: TODO: Handle GraphQLUnionType | ||
} | ||
} | ||
return undefined | ||
} | ||
|
||
function parseFieldNodes<DTO>( | ||
inASTs: ReadonlyArray<SelectionNode> | SelectionNode, | ||
resolveInfo: ResolveInfo, | ||
initTree: QueryResolveFields<DTO> | null, | ||
parentType: GraphQLCompositeType | ||
): QueryResolveTree<DTO> | QueryResolveFields<DTO> { | ||
const asts: ReadonlyArray<FieldNode> = Array.isArray(inASTs) ? inASTs : [inASTs] | ||
|
||
return asts.reduce((tree, fieldNode) => { | ||
const alias: string = fieldNode?.alias?.value ?? fieldNode.name.value | ||
|
||
const field = getFieldFromAST(fieldNode, parentType) | ||
if (field == null) { | ||
return tree | ||
} | ||
const fieldGqlTypeOrUndefined = getNamedType(field.type) | ||
if (!fieldGqlTypeOrUndefined) { | ||
return tree | ||
} | ||
|
||
const parsedField = { | ||
name: fieldNode.name.value, | ||
alias, | ||
args: getArgumentValues(field, fieldNode, resolveInfo.variableValues), | ||
|
||
fields: | ||
fieldNode.selectionSet && isCompositeType(fieldGqlTypeOrUndefined) | ||
? parseFieldNodes( | ||
fieldNode.selectionSet.selections, | ||
resolveInfo, | ||
{} as QueryResolveFields<DTO>, | ||
fieldGqlTypeOrUndefined | ||
) | ||
: {} | ||
} as QueryResolveTree<DTO> | ||
|
||
if (tree === null) { | ||
return parsedField | ||
} else { | ||
tree[alias] = parsedField | ||
} | ||
|
||
return tree | ||
}, initTree) | ||
} | ||
|
||
function isOffsetPaging<DTO>(info: unknown): info is QueryResolveTree<OffsetConnectionType<DTO>> { | ||
return typeof (info as QueryResolveTree<OffsetConnectionType<DTO>>).fields.nodes !== 'undefined' | ||
} | ||
|
||
function isCursorPaging<DTO>(info: unknown): info is QueryResolveTree<CursorConnectionType<DTO>> { | ||
return typeof (info as QueryResolveTree<CursorConnectionType<DTO>>).fields.edges !== 'undefined' | ||
} | ||
|
||
export function simplifyResolveInfo<DTO>(resolveInfo: ResolveInfo): QueryResolveTree<DTO> { | ||
const simpleInfo = parseFieldNodes(resolveInfo.fieldNodes, resolveInfo, null, resolveInfo.parentType) as | ||
| QueryResolveTree<DTO> | ||
| QueryResolveTree<OffsetConnectionType<DTO>> | ||
| QueryResolveTree<CursorConnectionType<DTO>> | ||
|
||
if (isOffsetPaging(simpleInfo)) { | ||
return simpleInfo.fields.nodes as QueryResolveTree<DTO> | ||
} else if (isCursorPaging(simpleInfo)) { | ||
return simpleInfo.fields.edges.fields.node as QueryResolveTree<DTO> | ||
} | ||
|
||
return simpleInfo as QueryResolveTree<DTO> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
packages/query-graphql/src/resolvers/aggregate/group-by-aggregate.resolver.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Args, Parent, registerEnumType, Resolver } from '@nestjs/graphql' | ||
import { AggregateResponse, Class, GroupBy, MapReflector, QueryService } from '@ptc-org/nestjs-query-core' | ||
|
||
import { getGraphqlObjectName } from '../../common' | ||
import { getFilterableFields, ResolverField } from '../../decorators' | ||
import { ServiceResolver } from '../resolver.interface' | ||
|
||
const reflector = new MapReflector('nestjs-query:aggregate-response-type') | ||
|
||
registerEnumType(GroupBy, { | ||
name: 'GroupBy', // this one is mandatory | ||
description: 'Group by' // this one is optional | ||
}) | ||
|
||
export const GroupByAggregateMixin = | ||
<DTO>(DTOClass: Class<DTO>, AR: Class<AggregateResponse<DTO>>) => | ||
<B extends Class<ServiceResolver<DTO, QueryService<DTO, unknown, unknown>>>>(Base: B): B => { | ||
const objName = getGraphqlObjectName(DTOClass, 'Unable to make AggregationResponseType.') | ||
|
||
const aggName = `${objName}AggregateGroupBy` | ||
return reflector.memoize(DTOClass, aggName, () => { | ||
const fields = getFilterableFields(DTOClass).filter((field) => field.target === Date) | ||
|
||
if (!fields.length) { | ||
throw new Error( | ||
`No fields found to create AggregationResponseType for ${DTOClass.name}. Ensure fields are annotated with @FilterableField` | ||
) | ||
} | ||
|
||
return fields.reduce((RB, field) => { | ||
@Resolver(() => AR, { isAbstract: true }) | ||
class ReadOneMixin extends RB { | ||
@ResolverField(field.propertyName, () => field.target, { nullable: true }) | ||
[field.propertyName]( | ||
@Parent() dto: DTO, | ||
@Args('by', { | ||
type: () => GroupBy, | ||
defaultValue: GroupBy.DAY, | ||
nullable: true | ||
}) | ||
by: string | ||
): unknown { | ||
return dto[field.propertyName] | ||
} | ||
} | ||
|
||
return ReadOneMixin | ||
}, Base) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.