From 0e91c82ace7eff1c21ec23667021a9f19056c564 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Mon, 12 Apr 2021 09:02:47 +0100 Subject: [PATCH] feat(gatsby): Add aggregation resolvers (#30789) * feat(gatsby): Add aggregation resolvers * Update snapshots * Update snapshot * Update integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap * Update snap --- .../__tests__/__snapshots__/index.js.snap | 60 +++++++++ .../__snapshots__/build-schema.js.snap | 12 ++ .../__snapshots__/rebuild-schema.js.snap | 12 ++ .../src/schema/__tests__/fixtures/queries.js | 4 + .../gatsby/src/schema/__tests__/queries.js | 120 +++++++++++++++++- .../src/schema/__tests__/rebuild-schema.js | 3 + packages/gatsby/src/schema/node-model.js | 28 +++- packages/gatsby/src/schema/resolvers.ts | 74 +++++++++++ .../gatsby/src/schema/types/pagination.ts | 23 +++- 9 files changed, 332 insertions(+), 4 deletions(-) diff --git a/integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap b/integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap index 595396781502d..4f65b12651f6e 100644 --- a/integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap +++ b/integration-tests/gatsby-source-wordpress/__tests__/__snapshots__/index.js.snap @@ -428,6 +428,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpCategoryConnection", @@ -572,6 +575,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpCommentAuthorConnection", @@ -614,6 +620,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpCommentConnection", @@ -713,6 +722,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpConnection", @@ -748,6 +760,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpContentNodeConnection", @@ -870,6 +885,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpContentTypeConnection", @@ -1120,6 +1138,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpMediaItemConnection", @@ -1233,6 +1254,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpMenuConnection", @@ -1297,6 +1321,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpMenuItemConnection", @@ -1551,6 +1578,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpPageConnection", @@ -1759,6 +1789,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpPostConnection", @@ -1812,6 +1845,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpPostFormatConnection", @@ -2051,6 +2087,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpTagConnection", @@ -2156,6 +2195,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpTaxonomyConnection", @@ -2231,6 +2273,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpTermNodeConnection", @@ -2314,6 +2359,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpTranslationFilterTestConnection", @@ -2393,6 +2441,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpTypeLimit0TestConnection", @@ -2472,6 +2523,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpTypeLimitTestConnection", @@ -2556,6 +2610,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpUserConnection", @@ -2607,6 +2664,9 @@ Array [ "nodes", "pageInfo", "distinct", + "max", + "min", + "sum", "group", ], "name": "WpUserRoleConnection", diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/build-schema.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/build-schema.js.snap index 9eb9728497522..c98ac899cbc6a 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/build-schema.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/build-schema.js.snap @@ -501,6 +501,9 @@ type FileConnection { nodes: [File!]! pageInfo: PageInfo! distinct(field: FileFieldsEnum!): [String!]! + max(field: FileFieldsEnum!): Float + min(field: FileFieldsEnum!): Float + sum(field: FileFieldsEnum!): Float group(skip: Int, limit: Int, field: FileFieldsEnum!): [FileGroupConnection!]! } @@ -703,6 +706,9 @@ type DirectoryConnection { nodes: [Directory!]! pageInfo: PageInfo! distinct(field: DirectoryFieldsEnum!): [String!]! + max(field: DirectoryFieldsEnum!): Float + min(field: DirectoryFieldsEnum!): Float + sum(field: DirectoryFieldsEnum!): Float group(skip: Int, limit: Int, field: DirectoryFieldsEnum!): [DirectoryGroupConnection!]! } @@ -895,6 +901,9 @@ type SiteConnection { nodes: [Site!]! pageInfo: PageInfo! distinct(field: SiteFieldsEnum!): [String!]! + max(field: SiteFieldsEnum!): Float + min(field: SiteFieldsEnum!): Float + sum(field: SiteFieldsEnum!): Float group(skip: Int, limit: Int, field: SiteFieldsEnum!): [SiteGroupConnection!]! } @@ -1025,6 +1034,9 @@ type SitePageConnection { nodes: [SitePage!]! pageInfo: PageInfo! distinct(field: SitePageFieldsEnum!): [String!]! + max(field: SitePageFieldsEnum!): Float + min(field: SitePageFieldsEnum!): Float + sum(field: SitePageFieldsEnum!): Float group(skip: Int, limit: Int, field: SitePageFieldsEnum!): [SitePageGroupConnection!]! } diff --git a/packages/gatsby/src/schema/__tests__/__snapshots__/rebuild-schema.js.snap b/packages/gatsby/src/schema/__tests__/__snapshots__/rebuild-schema.js.snap index dc6086a764dba..b23bdc35b2281 100644 --- a/packages/gatsby/src/schema/__tests__/__snapshots__/rebuild-schema.js.snap +++ b/packages/gatsby/src/schema/__tests__/__snapshots__/rebuild-schema.js.snap @@ -501,6 +501,9 @@ type FileConnection { nodes: [File!]! pageInfo: PageInfo! distinct(field: FileFieldsEnum!): [String!]! + max(field: FileFieldsEnum!): Float + min(field: FileFieldsEnum!): Float + sum(field: FileFieldsEnum!): Float group(skip: Int, limit: Int, field: FileFieldsEnum!): [FileGroupConnection!]! } @@ -703,6 +706,9 @@ type DirectoryConnection { nodes: [Directory!]! pageInfo: PageInfo! distinct(field: DirectoryFieldsEnum!): [String!]! + max(field: DirectoryFieldsEnum!): Float + min(field: DirectoryFieldsEnum!): Float + sum(field: DirectoryFieldsEnum!): Float group(skip: Int, limit: Int, field: DirectoryFieldsEnum!): [DirectoryGroupConnection!]! } @@ -895,6 +901,9 @@ type SiteConnection { nodes: [Site!]! pageInfo: PageInfo! distinct(field: SiteFieldsEnum!): [String!]! + max(field: SiteFieldsEnum!): Float + min(field: SiteFieldsEnum!): Float + sum(field: SiteFieldsEnum!): Float group(skip: Int, limit: Int, field: SiteFieldsEnum!): [SiteGroupConnection!]! } @@ -1025,6 +1034,9 @@ type SitePageConnection { nodes: [SitePage!]! pageInfo: PageInfo! distinct(field: SitePageFieldsEnum!): [String!]! + max(field: SitePageFieldsEnum!): Float + min(field: SitePageFieldsEnum!): Float + sum(field: SitePageFieldsEnum!): Float group(skip: Int, limit: Int, field: SitePageFieldsEnum!): [SitePageGroupConnection!]! } diff --git a/packages/gatsby/src/schema/__tests__/fixtures/queries.js b/packages/gatsby/src/schema/__tests__/fixtures/queries.js index d0697fc474309..4abaac2b18cba 100644 --- a/packages/gatsby/src/schema/__tests__/fixtures/queries.js +++ b/packages/gatsby/src/schema/__tests__/fixtures/queries.js @@ -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`], @@ -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, diff --git a/packages/gatsby/src/schema/__tests__/queries.js b/packages/gatsby/src/schema/__tests__/queries.js index 1ad9f441dca4e..99ca8d32e5f97 100644 --- a/packages/gatsby/src/schema/__tests__/queries.js +++ b/packages/gatsby/src/schema/__tests__/queries.js @@ -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`, () => { @@ -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`) diff --git a/packages/gatsby/src/schema/__tests__/rebuild-schema.js b/packages/gatsby/src/schema/__tests__/rebuild-schema.js index f009d892edaf8..042a969fb85c0 100644 --- a/packages/gatsby/src/schema/__tests__/rebuild-schema.js +++ b/packages/gatsby/src/schema/__tests__/rebuild-schema.js @@ -479,6 +479,9 @@ describe(`build and update individual types`, () => { nodes: [Bar!]! pageInfo: PageInfo! distinct(field: BarFieldsEnum!): [String!]! + max(field: BarFieldsEnum!): Float + min(field: BarFieldsEnum!): Float + sum(field: BarFieldsEnum!): Float group(skip: Int, limit: Int, field: BarFieldsEnum!): [BarGroupConnection!]! }" `) diff --git a/packages/gatsby/src/schema/node-model.js b/packages/gatsby/src/schema/node-model.js index 7f42cca71b34c..d7e6ef60aec11 100644 --- a/packages/gatsby/src/schema/node-model.js +++ b/packages/gatsby/src/schema/node-model.js @@ -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, @@ -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) || [] @@ -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) ) } diff --git a/packages/gatsby/src/schema/resolvers.ts b/packages/gatsby/src/schema/resolvers.ts index 879b153a387c0..b2bbb9ee450e3 100644 --- a/packages/gatsby/src/schema/resolvers.ts +++ b/packages/gatsby/src/schema/resolvers.ts @@ -88,10 +88,12 @@ export function findManyPaginated( // `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>(typeName)( @@ -135,6 +137,78 @@ export const distinct: GatsbyResolver< return Array.from(values).sort() } +export const min: GatsbyResolver< + IGatsbyConnection, + 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 = Number(value) + } + if (!isNaN(value) && value < min) { + min = value + } + }) + if (min === Number.MAX_SAFE_INTEGER) { + return null + } + return min +} + +export const max: GatsbyResolver< + IGatsbyConnection, + 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 = Number(value) + } + if (!isNaN(value) && value > max) { + max = value + } + }) + if (max === Number.MIN_SAFE_INTEGER) { + return null + } + return max +} + +export const sum: GatsbyResolver< + IGatsbyConnection, + IFieldConnectionArgs +> = function sumResolver(source, args): number | null { + const { field } = args + const { edges } = source + + return edges.reduce((prev, { node }) => { + let value = + getValueAt(node, `__gatsby_resolved.${field}`) || getValueAt(node, field) + + if (typeof value !== `number`) { + value = Number(value) + } + if (!isNaN(value)) { + return (prev || 0) + value + } + return prev + }, null) +} + type IGatsbyGroupReturnValue = Array< IGatsbyConnection & { field: string diff --git a/packages/gatsby/src/schema/types/pagination.ts b/packages/gatsby/src/schema/types/pagination.ts index 49fc65e0927cf..3c4cca5ec2fd8 100644 --- a/packages/gatsby/src/schema/types/pagination.ts +++ b/packages/gatsby/src/schema/types/pagination.ts @@ -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 = ({ schemaComposer, @@ -110,6 +110,27 @@ export const getPagination = ({ }, 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: {