From 386fc4c5b604a40586aba47533f83a1f5a3723d9 Mon Sep 17 00:00:00 2001 From: JYC Date: Mon, 25 Sep 2023 06:27:47 +0200 Subject: [PATCH] Enable custom arg to the node interface (#1172) --- .changeset/large-donuts-melt.md | 5 + .../src/codegen/transforms/paginate.test.ts | 146 +++++++++++++++++- .../src/codegen/transforms/paginate.ts | 4 +- .../src/codegen/validators/typeCheck.ts | 36 ++--- 4 files changed, 171 insertions(+), 20 deletions(-) create mode 100644 .changeset/large-donuts-melt.md diff --git a/.changeset/large-donuts-melt.md b/.changeset/large-donuts-melt.md new file mode 100644 index 0000000000..121fff94fc --- /dev/null +++ b/.changeset/large-donuts-melt.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +Node interface arg can be customized with the first defaultKeys defined in houdini.config.js (this allows "nodeId" for instance.) diff --git a/packages/houdini/src/codegen/transforms/paginate.test.ts b/packages/houdini/src/codegen/transforms/paginate.test.ts index c4bfad3543..73b23f3242 100644 --- a/packages/houdini/src/codegen/transforms/paginate.test.ts +++ b/packages/houdini/src/codegen/transforms/paginate.test.ts @@ -1,4 +1,4 @@ -import { test, expect } from 'vitest' +import { expect, test } from 'vitest' import { runPipeline } from '../../codegen' import { mockCollectedDoc, testConfig } from '../../test' @@ -1998,3 +1998,147 @@ test('default defaultPaginateMode to SinglePage', async function () { "HoudiniHash=c0276291ccf0e89ecf3e2c0fd68314703c62c8dca06915e602f931297be94c3c"; `) }) + +const nodeSchema = ` +interface Node { + mySuperId: ID! +} + +type Query { + node(mySuperId: ID!): Node + friends(first: Int, after: String, last: Int, before: String): UserConnection! +} + +type UserConnection { + edges: [UserEdge!]! + pageInfo: PageInfo! +} + +type UserEdge { + node: User + cursor: String! +} + +type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String + endCursor: String +} + +type User implements Node { + id: ID! + mySuperId: ID! + name: String! + friends(first: Int, after: String, last: Int, before: String): UserConnection! +} +` + +test('paginate with a wrong node interface should throw', async function () { + const docs = [ + mockCollectedDoc( + `fragment UserFriends on User { + friends(first: 1) @paginate { + edges { + node { + name + } + } + } + } + ` + ), + ] + + // run the pipeline + const config = testConfig({ + schema: nodeSchema, + logLevel: 'full', + }) + + try { + await runPipeline(config, docs) + expect('We should').toBe('never be here') + } catch (error) { + if (Array.isArray(error) && error[0] instanceof Error) { + expect(error[0].message).toMatchInlineSnapshot( + ` + "It looks like you are trying to use @paginate on a document that does not have a valid type resolver. + If this is happening inside of a fragment, make sure that the fragment either implements the Node interface or you + have defined a resolver entry for the fragment type. + + For more information, please visit these links: + - https://houdinigraphql.com/guides/pagination#paginated-fragments + - https://houdinigraphql.com/guides/caching-data#custom-ids + " + ` + ) + } else { + expect('We should').toBe('never be here') + } + } +}) + +test('paginate with a strange node interface, but well configured locally', async function () { + const docs = [ + mockCollectedDoc( + `fragment UserFriends on User { + friends(first: 1) @paginate { + edges { + node { + name + } + } + } + } + ` + ), + ] + + // run the pipeline + const config = testConfig({ + schema: nodeSchema, + types: { + User: { + keys: ['mySuperId'], + }, + }, + }) + + try { + await runPipeline(config, docs) + } catch (error) { + expect('We should').toBe('never be here') + } +}) + +test('paginate with a strange node interface, but well configured globally', async function () { + const docs = [ + mockCollectedDoc( + `fragment UserFriends on User { + friends(first: 1) @paginate { + edges { + node { + name + } + } + } + } + ` + ), + ] + + // run the pipeline + const config = testConfig({ + schema: nodeSchema, + defaultKeys: ['mySuperId'], + }) + + try { + await runPipeline(config, docs) + } catch (error) { + console.log(`error`, error) + + expect('We should').toBe('never be here') + } +}) diff --git a/packages/houdini/src/codegen/transforms/paginate.ts b/packages/houdini/src/codegen/transforms/paginate.ts index e5419bf8ca..a686bdb92e 100644 --- a/packages/houdini/src/codegen/transforms/paginate.ts +++ b/packages/houdini/src/codegen/transforms/paginate.ts @@ -503,7 +503,9 @@ export default async function paginate(config: Config, documents: Document[]): P }, }, // make sure every key field is present - ...(typeConfig?.keys || ['id']).map((key) => ({ + ...( + typeConfig?.keys || [config.defaultKeys[0]] + ).map((key) => ({ kind: graphql.Kind.FIELD, name: { kind: graphql.Kind.NAME, diff --git a/packages/houdini/src/codegen/validators/typeCheck.ts b/packages/houdini/src/codegen/validators/typeCheck.ts index 8567b12c7f..f1d9062369 100755 --- a/packages/houdini/src/codegen/validators/typeCheck.ts +++ b/packages/houdini/src/codegen/validators/typeCheck.ts @@ -1,16 +1,16 @@ import { logGreen } from '@kitql/helper' import * as graphql from 'graphql' +import type { Config, Document, PaginateModes } from '../../lib' import { - parentField, - definitionFromAncestors, + HoudiniError, LogLevel, + definitionFromAncestors, + parentField, parentTypeFromAncestors, - HoudiniError, siteURL, unwrapType, } from '../../lib' -import type { Config, Document, LogLevels, PaginateModes } from '../../lib' import type { FragmentArgument } from '../transforms/fragmentVariables' import { fragmentArguments as collectFragmentArguments, @@ -1062,7 +1062,7 @@ function nodeDirectives(config: Config, directives: string[]) { // if the fragment is not on the query type or an implementor of node if (!possibleNodes.includes(definitionType)) { ctx.reportError( - new graphql.GraphQLError(paginateOnNonNodeMessage(config, node.name.value)) + new graphql.GraphQLError(paginateOnNonNodeMessage(node.name.value)) ) } }, @@ -1234,35 +1234,35 @@ export function getAndVerifyNodeInterface(config: Config): graphql.GraphQLInterf // make sure its an interface if (!graphql.isInterfaceType(nodeInterface)) { - displayInvalidNodeFieldMessage(config.logLevel) + displayInvalidNodeFieldMessage(config) return null } // look for a field on the query type to look up a node by id const queryType = schema.getQueryType() if (!queryType) { - displayInvalidNodeFieldMessage(config.logLevel) + displayInvalidNodeFieldMessage(config) return null } // look for a node field const nodeField = queryType.getFields()['node'] if (!nodeField) { - displayInvalidNodeFieldMessage(config.logLevel) + displayInvalidNodeFieldMessage(config) return null } // there needs to be an arg on the field called id const args = nodeField.args if (args.length === 0) { - displayInvalidNodeFieldMessage(config.logLevel) + displayInvalidNodeFieldMessage(config) return null } // look for the id arg - const idArg = args.find((arg) => arg.name === 'id') + const idArg = args.find((arg) => arg.name === config.defaultKeys[0]) if (!idArg) { - displayInvalidNodeFieldMessage(config.logLevel) + displayInvalidNodeFieldMessage(config) return null } @@ -1270,14 +1270,14 @@ export function getAndVerifyNodeInterface(config: Config): graphql.GraphQLInterf const idType = unwrapType(config, idArg.type) // make sure its an ID if (idType.type.name !== 'ID') { - displayInvalidNodeFieldMessage(config.logLevel) + displayInvalidNodeFieldMessage(config) return null } // make sure that the node field returns a Node const fieldReturnType = unwrapType(config, nodeField.type) if (fieldReturnType.type.name !== 'Node') { - displayInvalidNodeFieldMessage(config.logLevel) + displayInvalidNodeFieldMessage(config) return null } @@ -1285,10 +1285,10 @@ export function getAndVerifyNodeInterface(config: Config): graphql.GraphQLInterf } let nbInvalidNodeFieldMessageDisplayed = 0 -function displayInvalidNodeFieldMessage(logLevel: LogLevels) { +function displayInvalidNodeFieldMessage(config: Config) { // We want to display the message only once. if (nbInvalidNodeFieldMessageDisplayed === 0) { - if (logLevel === LogLevel.Full) { + if (config.logLevel === LogLevel.Full) { console.warn(invalidNodeFieldMessage) } else { console.warn(invalidNodeFieldMessageLight) @@ -1304,11 +1304,11 @@ const invalidNodeFieldMessage = `⚠️ Your project defines a Node interface b If you are trying to provide the Node interface and its field, they must look like the following: interface Node { - id: ID! + id: ID! } extend type Query { - node(id: ID!): Node + node(id: ID!): Node } For more information, please visit these links: @@ -1316,7 +1316,7 @@ For more information, please visit these links: - ${siteURL}/guides/caching-data#custom-ids ` -const paginateOnNonNodeMessage = (config: Config, directiveName: string) => +const paginateOnNonNodeMessage = (directiveName: string) => `It looks like you are trying to use @${directiveName} on a document that does not have a valid type resolver. If this is happening inside of a fragment, make sure that the fragment either implements the Node interface or you have defined a resolver entry for the fragment type.