Skip to content

Commit

Permalink
Fix inserts into lists of unions (#1103)
Browse files Browse the repository at this point in the history
Co-authored-by: jycouet <jycouet@gmail.com>
Co-authored-by: Alec Aivazis <alec@aivazis.com>
  • Loading branch information
3 people committed Jun 22, 2023
1 parent 1fc47b8 commit 891a8c7
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-pears-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'houdini': patch
---

fix: insert in list works for list of union
30 changes: 30 additions & 0 deletions e2e/_api/graphql.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ let dataRentedBooks = [
{ userId: '1', bookId: 1, rate: 9 },
]

const listA = []
const listB = []

const userSnapshots = {}
function getUserSnapshot(snapshot) {
if (!userSnapshots[snapshot]) {
Expand Down Expand Up @@ -112,6 +115,23 @@ export const resolvers = {
hello: () => {
return 'Hello World! // From Houdini!'
},
aOrB: () => {
const toRet = []

toRet.push(
...listA.map((a) => {
return { __typename: 'A', ...a }
})
)

toRet.push(
...listB.map((b) => {
return { __typename: 'B', ...b }
})
)

return toRet
},
usersList: (_, args) => {
return [...getUserSnapshot(args.snapshot)].splice(args.offset || 0, args.limit)
},
Expand Down Expand Up @@ -429,6 +449,16 @@ export const resolvers = {

throw new GraphQLError('RentedBook not found', { code: 403 })
},
createA: (_, args) => {
const a = { id: listA.length + 1, a: args.a }
listA.push(a)
return a
},
createB: (_, args) => {
const b = { id: listB.length + 1, b: args.b }
listB.push(b)
return b
},
},

DateTime: new GraphQLScalarType({
Expand Down
15 changes: 15 additions & 0 deletions e2e/_api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type Mutation {
deleteLibrary(library: ID!): Library!
deleteBook(book: ID!, delay: Int, force: ForceReturn): Book
updateRentedBook(userId: String!, bookId: Int!, rate: Int!): RentedBook
createA(a: String!): A!
createB(b: String!): B!
}

interface Node {
Expand All @@ -66,7 +68,10 @@ input UserNameFilter {
name: String!
}

union UnionAorB = A | B

type Query {
aOrB: [UnionAorB!]!
avgYearsBirthDate: Float!
node(id: ID!): Node
user(id: ID!, snapshot: String!, tmp: Boolean, delay: Int, forceNullDate: Boolean): User!
Expand Down Expand Up @@ -178,6 +183,16 @@ type RentedBook {
rate: Int!
}

type A {
id: ID!
a: String!
}

type B {
id: ID!
b: String!
}

union UserNodesResult = UserNodes | Message1
union UserResult = User | Message1

Expand Down
3 changes: 3 additions & 0 deletions e2e/kit/houdini.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const config = {
types: {
RentedBook: {
keys: ['userId', 'bookId']
},
UnionAorB: {
keys: []
}
},

Expand Down
2 changes: 2 additions & 0 deletions e2e/kit/src/lib/utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const routes = {
Stores_Pagination_query_forward_cursor: '/stores/pagination/query/forward-cursor',
Stores_Directives: '/stores/directives',

union_insert: 'union-insert',

Plugin_query_simple: '/plugin/query/simple',
Plugin_query_variable_1: '/plugin/query/variable-1',
Plugin_query_variable_2: '/plugin/query/variable-2',
Expand Down
12 changes: 12 additions & 0 deletions e2e/kit/src/routes/union-insert/+page.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
query AorB {
aOrB @list(name: "All_AorB") {
... on A {
id
a
}
... on B {
id
b
}
}
}
31 changes: 31 additions & 0 deletions e2e/kit/src/routes/union-insert/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import { graphql } from '$houdini';
import type { PageData } from './$houdini';
export let data: PageData;
$: ({ AorB } = data);
const addA = graphql(`
mutation CreateA($a: String!) {
createA(a: $a) {
...All_AorB_insert
}
}
`);
const addB = graphql(`
mutation CreateB($b: String!) {
createB(b: $b) {
...All_AorB_insert
}
}
`);
</script>

<button id="addA" on:click={() => addA.mutate({ a: 'MyA' })}>Add A</button>
<button id="addB" on:click={() => addB.mutate({ b: 'MyB' })}>Add B</button>

<div id="result">
<pre>{JSON.stringify($AorB?.data, null, 0)}</pre>
</div>
27 changes: 27 additions & 0 deletions e2e/kit/src/routes/union-insert/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { test } from '@playwright/test';
import { routes } from '../../lib/utils/routes.js';
import { expect_1_gql, expect_to_be, goto } from '../../lib/utils/testsHelper.js';

test.describe('union-result', () => {
test('Get two stores and not resetting', async ({ page }) => {
await goto(page, routes.union_insert);

// When we arrive on the page, we expect to see null in the result div
await expect_to_be(page, '{"aOrB":[]}');

// we click on the button to getAllUsers
await expect_1_gql(page, 'button[id="addA"]');

// expect 1 element in the array
await expect_to_be(page, '{"aOrB":[{"id":"1","a":"MyA","__typename":"A"}]}');

// we click on the button to getAllUsers
await expect_1_gql(page, 'button[id="addB"]');

// expect 2 elements in the array
await expect_to_be(
page,
'{"aOrB":[{"id":"1","a":"MyA","__typename":"A"},{"id":"1","b":"MyB","__typename":"B"}]}'
);
});
});
38 changes: 33 additions & 5 deletions packages/houdini/src/codegen/generators/artifacts/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,22 +121,50 @@ function prepareSelection({
// look up the field
const type = config.schema.getType(rootType) as graphql.GraphQLObjectType
if (!type) {
throw new HoudiniError({ filepath, message: 'Could not find type' })
throw new HoudiniError({
filepath,
message: 'Could not find type. Looking for ' + JSON.stringify(rootType),
})
}

const attributeName = field.alias?.value || field.name.value
// if we are looking at __typename, its a string (not defined in the schema)
let fieldType: graphql.GraphQLType
let fieldType: graphql.GraphQLType | null = null
let nullable = false

// if we are looking at __typename, its a string (not defined in the schema)
if (field.name.value === '__typename') {
fieldType = config.schema.getType('String')!
} else {
}
// if the type is something that has definite fields then we can look up the field
// type in the schema
else if ('getFields' in type) {
let typeRef = type.getFields()[field.name.value].type
fieldType = getRootType(typeRef)
nullable = !graphql.isNonNullType(typeRef)
}
// if we are looking at an abstract type that doesn't have well-defined fields (ie a union)
// then we are safe to look at any possible type (i think)
else if (graphql.isAbstractType(type)) {
for (const possible of config.schema.getPossibleTypes(type)) {
if (graphql.isObjectType(possible)) {
if (possible.getFields()[field.name.value]) {
fieldType = possible.getFields()[field.name.value].type
nullable = !graphql.isNonNullType(fieldType)
break
}
}
}
}

// make sure we identified a type
if (!fieldType) {
throw {
message: "Could not identify field's type",
description: `Missing definition for ${field.name.value} in ${type.name}`,
}
}

const typeName = fieldType.toString()
const typeName = (getRootType(fieldType) as graphql.GraphQLObjectType).name

// make sure we include the attribute in the path
const pathSoFar = path.concat(attributeName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,151 @@ test('fragments of unions inject correctly', function () {
`)
})

test('list of fragment unions', async function () {
// the config to use in tests
const config = testConfig()
const docs = [
mockCollectedDoc(
`query Entities {
entities @list(name: "list_entities") {
... on User {
name
}
... on Cat {
name
}
}
}`
),
mockCollectedDoc(
`mutation CatMutation {
catMutation {
...list_entities_insert
}
}`
),
]

// execute the generator
await runPipeline(config, docs)

expect(docs[0]).toMatchInlineSnapshot(`
export default {
"name": "Entities",
"kind": "HoudiniQuery",
"hash": "d7db6b92dac1893d8640c0f9d3535d1f75cdb1017cf4c8960c0bdaefbe119229",
"raw": \`query Entities {
entities {
... on User {
name
id
}
... on Cat {
name
id
}
__typename
}
}
\`,
"rootType": "Query",
"selection": {
"fields": {
"entities": {
"type": "Entity",
"keyRaw": "entities",
"directives": [{
"name": "list",
"arguments": {
"name": {
"kind": "StringValue",
"value": "list_entities"
}
}
}],
"list": {
"name": "list_entities",
"connection": false,
"type": "Entity"
},
"selection": {
"abstractFields": {
"fields": {
"User": {
"name": {
"type": "String",
"keyRaw": "name",
"visible": true
},
"id": {
"type": "ID",
"keyRaw": "id",
"visible": true
},
"__typename": {
"type": "String",
"keyRaw": "__typename",
"visible": true
}
},
"Cat": {
"name": {
"type": "String",
"keyRaw": "name",
"visible": true
},
"id": {
"type": "ID",
"keyRaw": "id",
"visible": true
},
"__typename": {
"type": "String",
"keyRaw": "__typename",
"visible": true
}
}
},
"typeMap": {}
},
"fields": {
"__typename": {
"type": "String",
"keyRaw": "__typename",
"visible": true
}
}
},
"abstract": true,
"visible": true
}
}
},
"pluginData": {},
"policy": "CacheOrNetwork",
"partial": false
};
"HoudiniHash=d7db6b92dac1893d8640c0f9d3535d1f75cdb1017cf4c8960c0bdaefbe119229";
`)
})

test('fragments in lists', async function () {
// the config to use in tests
const config = testConfig()
Expand Down

0 comments on commit 891a8c7

Please sign in to comment.