Skip to content

Commit

Permalink
feat(gatsby): Add aggregation resolvers
Browse files Browse the repository at this point in the history
  • Loading branch information
ascorbic committed Apr 9, 2021
1 parent 172cf4d commit c81b74c
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 4 deletions.
4 changes: 4 additions & 0 deletions packages/gatsby/src/schema/__tests__/fixtures/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const nodes = [
},
frontmatter: {
title: `Markdown File 1`,
views: 200,
price: "1.99",
tags: [],
date: new Date(Date.UTC(2019, 0, 1)),
authors: [`author2@example.com`, `author1@example.com`],
Expand All @@ -57,6 +59,8 @@ const nodes = [
frontmatter: {
title: `Markdown File 2`,
tags: [`constructor`],
views: 100,
price: "3.99",
published: false,
authors: [`author1@example.com`],
reviewer___NODE: null,
Expand Down
120 changes: 119 additions & 1 deletion packages/gatsby/src/schema/__tests__/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,124 @@ describe(`Query schema`, () => {
`)
})
})
describe(`aggregation fields`, () => {
it(`calculates max value of numeric field`, async () => {
const query = `
{
allMarkdown {
max(field: frontmatter___views)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.max).toEqual(200)
})

it(`calculates max value of numeric string field`, async () => {
const query = `
{
allMarkdown {
max(field: frontmatter___price)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.max).toEqual(3.99)
})

it(`calculates min value of numeric field`, async () => {
const query = `
{
allMarkdown {
min(field: frontmatter___views)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.min).toEqual(100)
})

it(`calculates min value of numeric string field`, async () => {
const query = `
{
allMarkdown {
min(field: frontmatter___price)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.min).toEqual(1.99)
})
})

it(`calculates sum of numeric field`, async () => {
const query = `
{
allMarkdown {
sum(field: frontmatter___views)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.sum).toEqual(300)
})

it(`calculates sum of numeric string field`, async () => {
const query = `
{
allMarkdown {
sum(field: frontmatter___price)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.sum).toEqual(5.98)
})

it(`returns null for min of non-numeric fields`, async () => {
const query = `
{
allMarkdown {
min(field: frontmatter___title)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.min).toBeNull()
})

it(`returns null for max of non-numeric fields`, async () => {
const query = `
{
allMarkdown {
max(field: frontmatter___title)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.max).toBeNull()
})

it(`returns null for sum of non-numeric fields`, async () => {
const query = `
{
allMarkdown {
sum(field: frontmatter___title)
}
}
`
const results = await runQuery(query)
expect(results.errors).toBeUndefined()
expect(results.data.allMarkdown.sum).toBeNull()
})
})

describe(`on fields added by setFieldsOnGraphQLNodeType API`, () => {
Expand Down Expand Up @@ -1646,7 +1764,7 @@ describe(`Query schema`, () => {
/**
* queries are read from file and parsed with babel
*/
it.only(`escape sequences work when correctly escaped`, async () => {
it(`escape sequences work when correctly escaped`, async () => {
const fs = require(`fs`)
const path = require(`path`)
const babel = require(`@babel/parser`)
Expand Down
28 changes: 26 additions & 2 deletions packages/gatsby/src/schema/node-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ class LocalNodeModel {
sort: query.sort,
group: query.group,
distinct: query.distinct,
max: query.max,
min: query.min,
sum: query.sum,
})
const fieldsToResolve = determineResolvableFields(
this.schemaComposer,
Expand Down Expand Up @@ -593,7 +596,7 @@ const toNodeTypeNames = (schema, gqlTypeName) => {
.map(type => type.name)
}

const getQueryFields = ({ filter, sort, group, distinct }) => {
const getQueryFields = ({ filter, sort, group, distinct, max, min, sum }) => {
const filterFields = filter ? dropQueryOperators(filter) : {}
const sortFields = (sort && sort.fields) || []

Expand All @@ -609,11 +612,32 @@ const getQueryFields = ({ filter, sort, group, distinct }) => {
distinct = []
}

if (max && !Array.isArray(max)) {
max = [max]
} else if (max == null) {
max = []
}

if (min && !Array.isArray(min)) {
min = [min]
} else if (min == null) {
min = []
}

if (sum && !Array.isArray(sum)) {
sum = [sum]
} else if (sum == null) {
sum = []
}

return _.merge(
filterFields,
...sortFields.map(pathToObject),
...group.map(pathToObject),
...distinct.map(pathToObject)
...distinct.map(pathToObject),
...max.map(pathToObject),
...min.map(pathToObject),
...sum.map(pathToObject)
)
}

Expand Down
74 changes: 74 additions & 0 deletions packages/gatsby/src/schema/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,12 @@ export function findManyPaginated<TSource, TArgs, TNodeType>(
// `distinct` which might need to be resolved.
const group = getProjectedField(info, `group`)
const distinct = getProjectedField(info, `distinct`)
const max = getProjectedField(info, `max`)
const extendedArgs = {
...args,
group: group || [],
distinct: distinct || [],
max: max || [],
}

const result = await findMany<TSource, PaginatedArgs<TArgs>>(typeName)(
Expand Down Expand Up @@ -135,6 +137,78 @@ export const distinct: GatsbyResolver<
return Array.from(values).sort()
}

export const min: GatsbyResolver<
IGatsbyConnection<any>,
IFieldConnectionArgs
> = function minResolver(source, args): number | null {
const { field } = args
const { edges } = source

let min = Number.MAX_SAFE_INTEGER

edges.forEach(({ node }) => {
let value =
getValueAt(node, `__gatsby_resolved.${field}`) || getValueAt(node, field)

if (typeof value !== `number`) {
value = parseFloat(value)
}
if (!isNaN(value) && value < min) {
min = value
}
})
if (min === Number.MAX_SAFE_INTEGER) {
return null
}
return min
}

export const max: GatsbyResolver<
IGatsbyConnection<any>,
IFieldConnectionArgs
> = function maxResolver(source, args): number | null {
const { field } = args
const { edges } = source

let max = Number.MIN_SAFE_INTEGER

edges.forEach(({ node }) => {
let value =
getValueAt(node, `__gatsby_resolved.${field}`) || getValueAt(node, field)
if (typeof value !== `number`) {
value = parseFloat(value)
}
if (!isNaN(value) && value > max) {
max = value
}
})
if (max === Number.MIN_SAFE_INTEGER) {
return null
}
return max
}

export const sum: GatsbyResolver<
IGatsbyConnection<any>,
IFieldConnectionArgs
> = function sumResolver(source, args): number | null {
const { field } = args
const { edges } = source

return edges.reduce<number | null>((prev, { node }) => {
let value =
getValueAt(node, `__gatsby_resolved.${field}`) || getValueAt(node, field)

if (typeof value !== `number`) {
value = parseFloat(value)
}
if (!isNaN(value)) {
return (prev || 0) + value
}
return prev
}, null)
}

type IGatsbyGroupReturnValue<NodeType> = Array<
IGatsbyConnection<NodeType> & {
field: string
Expand Down
23 changes: 22 additions & 1 deletion packages/gatsby/src/schema/types/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "graphql-compose"
import { getFieldsEnum } from "./sort"
import { addDerivedType } from "./derived-types"
import { distinct, group } from "../resolvers"
import { distinct, group, max, min, sum } from "../resolvers"

export const getPageInfo = <TContext = any>({
schemaComposer,
Expand Down Expand Up @@ -110,6 +110,27 @@ export const getPagination = <TContext = any>({
},
resolve: distinct,
},
max: {
type: `Float`,
args: {
field: fieldsEnumTC.getTypeNonNull(),
},
resolve: max,
},
min: {
type: `Float`,
args: {
field: fieldsEnumTC.getTypeNonNull(),
},
resolve: min,
},
sum: {
type: `Float`,
args: {
field: fieldsEnumTC.getTypeNonNull(),
},
resolve: sum,
},
group: {
type: [getGroup({ schemaComposer, typeComposer }).getTypeNonNull()],
args: {
Expand Down

0 comments on commit c81b74c

Please sign in to comment.