diff --git a/packages/gatsby/src/schema/__tests__/node-model.js b/packages/gatsby/src/schema/__tests__/node-model.js index 19830b85b2637..97829a217db0f 100644 --- a/packages/gatsby/src/schema/__tests__/node-model.js +++ b/packages/gatsby/src/schema/__tests__/node-model.js @@ -676,6 +676,7 @@ describe(`NodeModel`, () => { nested: { foo: `foo1`, bar: `bar1`, + sort_order: 10, }, internal: { type: `Test`, @@ -689,6 +690,7 @@ describe(`NodeModel`, () => { nested: { foo: `foo2`, bar: `bar2`, + sort_order: 9, }, internal: { type: `Test`, @@ -1122,6 +1124,49 @@ describe(`NodeModel`, () => { expect(result2[0].id).toBe(`id2`) }) + it(`findAll sorts using v5 sort fields`, async () => { + nodeModel.replaceFiltersCache() + + const { entries } = await nodeModel.findAll( + { + query: { + sort: [{ nested: { sort_order: `asc` } }], + }, + type: `Test`, + }, + { path: `/` } + ) + + const result = Array.from(entries) + + expect(result.length).toBe(2) + expect(result[0].id).toBe(`id2`) + expect(result[1].id).toBe(`id1`) + }) + + it(`findAll sorts using legacy (pre-v5) sort fields`, async () => { + nodeModel.replaceFiltersCache() + + const { entries } = await nodeModel.findAll( + { + query: { + sort: { + fields: [`nested.sort_order`], + order: [`asc`], + }, + }, + type: `Test`, + }, + { path: `/` } + ) + + const result = Array.from(entries) + + expect(result.length).toBe(2) + expect(result[0].id).toBe(`id2`) + expect(result[1].id).toBe(`id1`) + }) + it(`always uses a custom resolvers for query fields`, async () => { // See https://github.com/gatsbyjs/gatsby/issues/27368 nodeModel.replaceFiltersCache() diff --git a/packages/gatsby/src/schema/node-model.js b/packages/gatsby/src/schema/node-model.js index ff0c00798c59a..c960be78ac470 100644 --- a/packages/gatsby/src/schema/node-model.js +++ b/packages/gatsby/src/schema/node-model.js @@ -16,7 +16,11 @@ import { store } from "../redux" import { getDataStore, getNode, getTypes } from "../datastore" import { GatsbyIterable, isIterable } from "../datastore/common/iterable" import { wrapNode, wrapNodes } from "../utils/detect-node-mutations" -import { toNodeTypeNames, fieldNeedToResolve } from "./utils" +import { + toNodeTypeNames, + fieldNeedToResolve, + maybeConvertSortInputObjectToSortPath, +} from "./utils" import { getMaybeResolvedValue } from "./resolvers" type TypeOrTypeName = string | GraphQLOutputType @@ -193,7 +197,7 @@ class LocalNodeModel { } async _query(args) { - const { query = {}, type, stats, tracer } = args || {} + let { query = {}, type, stats, tracer } = args || {} // We don't support querying union types (yet?), because the combined types // need not have any fields in common. @@ -236,6 +240,8 @@ class LocalNodeModel { } } + query = maybeConvertSortInputObjectToSortPath(query) + let materializationActivity if (tracer) { materializationActivity = reporter.phantomActivity(`Materialization`, { diff --git a/packages/gatsby/src/schema/resolvers.ts b/packages/gatsby/src/schema/resolvers.ts index 61f3fe0944556..9d4d13554738b 100644 --- a/packages/gatsby/src/schema/resolvers.ts +++ b/packages/gatsby/src/schema/resolvers.ts @@ -16,7 +16,6 @@ import { SelectionNode, FieldNode, } from "graphql" -import isPlainObject from "lodash/isPlainObject" import { Path } from "graphql/jsutils/Path" import reporter from "gatsby-cli/lib/reporter" import { pathToArray } from "../query/utils" @@ -29,48 +28,18 @@ import { import { IGatsbyNode } from "../redux/types" import { IQueryResult } from "../datastore/types" import { GatsbyIterable } from "../datastore/common/iterable" -import { getResolvedFields, fieldPathNeedToResolve } from "./utils" +import { + getResolvedFields, + fieldPathNeedToResolve, + INestedPathStructureNode, + pathObjectToPathString, +} from "./utils" type ResolvedLink = IGatsbyNode | Array | null type nestedListOfStrings = Array type nestedListOfNodes = Array -type NestedPathStructure = INestedPathStructureNode | true | "ASC" | "DESC" - -interface INestedPathStructureNode { - [key: string]: NestedPathStructure -} - -function pathObjectToPathString(input: INestedPathStructureNode): { - path: string - leaf: any -} { - const path: Array = [] - let currentValue: NestedPathStructure | undefined = input - let leaf: any = undefined - while (currentValue) { - if (isPlainObject(currentValue)) { - const entries = Object.entries(currentValue) - if (entries.length !== 1) { - throw new Error(`Invalid field arg`) - } - for (const [key, value] of entries) { - path.push(key) - currentValue = value - } - } else { - leaf = currentValue - currentValue = undefined - } - } - - return { - path: path.join(`.`), - leaf, - } -} - export function getMaybeResolvedValue( node: IGatsbyNode, field: string | INestedPathStructureNode, @@ -113,39 +82,6 @@ export function findOne( type PaginatedArgs = TArgs & { skip?: number; limit?: number; sort: any } -function maybeConvertSortInputObjectToSortPath( - args: PaginatedArgs -): any { - if (!args.sort) { - return args - } - - if (_CFLAGS_.GATSBY_MAJOR === `5`) { - let sorts = args.sort - if (!Array.isArray(sorts)) { - sorts = [sorts] - } - - const modifiedSort: any = { - fields: [], - order: [], - } - - for (const sort of sorts) { - const { path, leaf } = pathObjectToPathString(sort) - modifiedSort.fields.push(path) - modifiedSort.order.push(leaf) - } - - return { - ...args, - sort: modifiedSort, - } - } - - return args -} - export function findManyPaginated( typeName: string ): GatsbyResolver> { @@ -169,7 +105,7 @@ export function findManyPaginated( const limit = typeof args.limit === `number` ? args.limit + 2 : undefined const extendedArgs = { - ...maybeConvertSortInputObjectToSortPath(args), + ...args, group: group || [], distinct: distinct || [], max: max || [], diff --git a/packages/gatsby/src/schema/utils.ts b/packages/gatsby/src/schema/utils.ts index c32e8f6e13a85..02fb76a1407b1 100644 --- a/packages/gatsby/src/schema/utils.ts +++ b/packages/gatsby/src/schema/utils.ts @@ -14,6 +14,7 @@ import { ObjectTypeComposer, SchemaComposer, } from "graphql-compose" +import isPlainObject from "lodash/isPlainObject" import type { IGatsbyNodePartial } from "../datastore/in-memory/indexing" import { IGatsbyNode } from "../internal" @@ -145,3 +146,83 @@ export function getResolvedFields( const resolvedNodes = store.getState().resolvedNodesCache.get(typeName) return resolvedNodes?.get(node.id) } + +type NestedPathStructure = INestedPathStructureNode | true | "ASC" | "DESC" + +export interface INestedPathStructureNode { + [key: string]: NestedPathStructure +} + +export function pathObjectToPathString(input: INestedPathStructureNode): { + path: string + leaf: any +} { + const path: Array = [] + let currentValue: NestedPathStructure | undefined = input + let leaf: any = undefined + while (currentValue) { + if (isPlainObject(currentValue)) { + const entries = Object.entries(currentValue) + if (entries.length !== 1) { + throw new Error(`Invalid field arg`) + } + for (const [key, value] of entries) { + path.push(key) + currentValue = value + } + } else { + leaf = currentValue + currentValue = undefined + } + } + + return { + path: path.join(`.`), + leaf, + } +} + +export function maybeConvertSortInputObjectToSortPath(args: any): any { + if (!args.sort) { + return args + } + + if (_CFLAGS_.GATSBY_MAJOR === `5`) { + // check if it's already in expected format + if ( + Array.isArray(args.sort?.fields) && + Array.isArray(args.sort?.order) && + args.sort.order.every( + item => + typeof item === `string` && + (item.toLowerCase() === `asc` || item.toLowerCase() === `desc`) + ) + ) { + return args + } + + let sorts = args.sort + + if (!Array.isArray(sorts)) { + sorts = [sorts] + } + + const modifiedSort: any = { + fields: [], + order: [], + } + + for (const sort of sorts) { + const { path, leaf } = pathObjectToPathString(sort) + modifiedSort.fields.push(path) + modifiedSort.order.push(leaf) + } + + return { + ...args, + sort: modifiedSort, + } + } + + return args +}