Skip to content

Commit

Permalink
Enable custom arg to the node interface (#1172)
Browse files Browse the repository at this point in the history
  • Loading branch information
jycouet authored Sep 25, 2023
1 parent 8ccdbfb commit 386fc4c
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/large-donuts-melt.md
Original file line number Diff line number Diff line change
@@ -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.)
146 changes: 145 additions & 1 deletion packages/houdini/src/codegen/transforms/paginate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from 'vitest'
import { expect, test } from 'vitest'

import { runPipeline } from '../../codegen'
import { mockCollectedDoc, testConfig } from '../../test'
Expand Down Expand Up @@ -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')
}
})
4 changes: 3 additions & 1 deletion packages/houdini/src/codegen/transforms/paginate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
36 changes: 18 additions & 18 deletions packages/houdini/src/codegen/validators/typeCheck.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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))
)
}
},
Expand Down Expand Up @@ -1234,61 +1234,61 @@ 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
}

// make sure that the id arg takes an ID
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
}

return nodeInterface as graphql.GraphQLInterfaceType
}

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)
Expand All @@ -1304,19 +1304,19 @@ 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:
- https://graphql.org/learn/global-object-identification/
- ${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.
Expand Down

0 comments on commit 386fc4c

Please sign in to comment.