Skip to content

Commit

Permalink
Fix bug when updating deeply nested lists with @parentID (#682)
Browse files Browse the repository at this point in the history
* Provide parentType to addMany subscriptions

The parentType in addFieldSubscription was hardcoded to 'asdf'.
This causes issues when using multiple nested @list directives,
as using the [ListName]_(insert|remove|toggle) operations with
@parentID attempted to look for a parent with type 'asdf'.

Signed-off-by: Jonas Jacobsen <[email protected]>

* e2e testing of nested lists

Route for testing nested lists has been added to e2e/sveltekit,

Modles have been added to the e2e/_api GQL schema, and a simple
"database" with sample data has been added to the resolvers.

Signed-off-by: Jonas Jacobsen <[email protected]>

* Changeset

Signed-off-by: Jonas Jacobsen <[email protected]>

* Move e2e operation files to appropriate route

Operation files only used by src/routes/stores/nested-list
are now placed appropriately.

Signed-off-by: Jonas Jacobsen <[email protected]>

* Simple test for nested lists

Adds a simple test for testing data with nested lists.
The test adds the cities/books/libraries database to the cache,
and adds first a city, then a library. Next, a subscription is added,
before adding a book to the cache, and it is verified that the book
is added to the library.

Signed-off-by: Jonas Jacobsen <[email protected]>

* test verifies fix

* fix imports

* clarify changeset

* clean up tests more

Signed-off-by: Jonas Jacobsen <[email protected]>
Co-authored-by: Alec Aivazis <[email protected]>
  • Loading branch information
Joklost and AlecAivazis authored Nov 8, 2022
1 parent 878eb37 commit 57577ee
Show file tree
Hide file tree
Showing 15 changed files with 420 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-timers-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'houdini': patch
---

Fix bug when updating deeply nested lists with @parentID
107 changes: 106 additions & 1 deletion e2e/_api/graphql.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,41 @@ export const typeDefs = sourceFiles.map((filepath) =>
fs.readFileSync(path.resolve(filepath), 'utf-8')
)

// Example Cities/Libraries/Books data
// Assume a traditional relational database for storage - each table with unique ID.
let cityId = 1;
let libraryId = 1;
let bookId = 1;

// Allow the "database" to be persistent and mutable
let cities = [
{
id: cityId++, name: 'Alexandria', libraries: [
{
id: libraryId++, name: 'The Library of Alexandria', books: [
{ id: bookId++, title: 'Callimachus Pinakes' },
{ id: bookId++, title: 'Kutubkhana-i-lskandriyya' },
]
},
{
id: libraryId++, name: 'Bibliotheca Alexandrina', books: [
{ id: bookId++, title: 'Analyze your own personality' },
]
},
]
},
{
id: cityId++, name: 'Istanbul', libraries: [
{
id: libraryId++, name: 'The Imperial Library of Constantinople', books: [
{ id: bookId++, title: 'Homer' },
{ id: bookId++, title: 'The Hellenistic History' },
]
},
]
},
];

// example data
const data = [
{ id: '1', name: 'Bruce Willis', birthDate: new Date(1955, 2, 19) },
Expand Down Expand Up @@ -100,6 +135,9 @@ export const resolvers = {
__typename: 'User',
}
},
cities: () => {
return cities;
},
},

User: {
Expand Down Expand Up @@ -149,7 +187,7 @@ export const resolvers = {
try {
let data = await processFile(file)
return data
} catch (e) {}
} catch (e) { }
throw new GraphQLYogaError('ERROR', { code: 500 })
},
multipleUpload: async (_, { files }) => {
Expand All @@ -164,6 +202,73 @@ export const resolvers = {
}
return res
},
addCity: (_, args) => {
const city = {
id: cityId++,
name: args.name,
libraries: [],
}

cities.push(city);
return city;
},
addLibrary: (_, args) => {
const cityId = Number.parseInt(args.city);
const city = cities.find((city) => city.id === cityId);
if (!city) {
throw new GraphQLYogaError('City not found', { code: 404 })
}

const library = {
id: libraryId++,
name: args.name,
books: [],
}
city.libraries.push(library);
return library;
},
addBook: (_, args) => {
const libraryId = Number.parseInt(args.library);
const city = cities.find((city) => city.libraries.find((library) => library.id === libraryId));
if (!city) {
throw new GraphQLYogaError('City/Library not found', { code: 404 })
}
const library = city.libraries.find((library) => library.id === libraryId);

const book = {
id: bookId++,
title: args.title,
}
library.books.push(book);
return book;
},
deleteCity: (_, args) => {
const cityId = Number.parseInt(args.city);
const city = cities.find((city) => city.id === cityId);
cities = cities.filter((city) => city.id !== cityId);
return city;
},
deleteLibrary: (_, args) => {
const libraryId = Number.parseInt(args.library);
const city = cities.find((city) => city.libraries.find((library) => library.id === libraryId));
if (!city) {
throw new GraphQLYogaError('City/Library not found', { code: 404 })
}
const library = city.libraries.find((library) => library.id === libraryId);
city.libraries = city.libraries.filter((library) => library.id !== libraryId);
return library;
},
deleteBook: (_, args) => {
const bookId = Number.parseInt(args.book);
const city = cities.find((city) => city.libraries.find((library) => library.books.find((book) => book.id === bookId)));
if (!city) {
throw new GraphQLYogaError('City/Library/Book not found', { code: 404 })
}
const library = city.libraries.find((library) => library.books.find((book) => book.id === bookId));
const book = library.books.find((book) => book.id === bookId);
library.books = library.books.filter((book) => book.id !== bookId);
return book;
},
},

DateTime: new GraphQLScalarType({
Expand Down
39 changes: 39 additions & 0 deletions e2e/_api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ type Mutation {
updateUser(id: ID!, name: String, snapshot: String!, birthDate: DateTime, delay: Int): User!
singleUpload(file: File!): String!
multipleUpload(files: [File!]!): [String!]!
addCity(
name: String!
): City!
addLibrary(
city: ID!
name: String!
): Library!
addBook(
library: ID!
title: String!
): Book!
deleteCity(
city: ID!
): City!
deleteLibrary(
library: ID!
): Library!
deleteBook(
book: ID!
): Book!
}

interface Node {
Expand All @@ -52,6 +72,7 @@ type Query {
): UserConnection!
usersList(limit: Int = 4, offset: Int, snapshot: String!): [User!]!
session: String
cities: [City]!
}

type User implements Node {
Expand All @@ -73,3 +94,21 @@ type UserEdge {
cursor: String
node: User
}


type Book {
id: ID!
title: String!
}

type Library {
id: ID!
name: String!
books: [Book]!
}

type City {
id: ID!
name: String!
libraries: [Library]!
}
2 changes: 2 additions & 0 deletions e2e/sveltekit/src/lib/utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export const routes = {
Pagination_fragment_backwards_cursor: '/pagination/fragment/backwards-cursor',
Pagination_fragment_offset: '/pagination/fragment/offset',

Stores_Nested_List: '/stores/nested-list',

Stores_subunsub_list: '/stores/subunsub-list',
Stores_subunsub_mutation: '/stores/subunsub-mutation',

Expand Down
98 changes: 98 additions & 0 deletions e2e/sveltekit/src/routes/stores/nested-list/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script lang="ts">
import { browser } from '$app/environment';
import {
GQL_Cities,
GQL_AddCity,
GQL_AddLibrary,
GQL_AddBook,
GQL_DeleteCity,
GQL_DeleteLibrary,
GQL_DeleteBook
} from '$houdini';
$: browser && GQL_Cities.fetch();
const addCity = (event: Event) => {
const target = event?.target as HTMLInputElement;
GQL_AddCity.mutate({ name: target.value });
target.value = '';
};
const deleteCity = (event: Event) => {
const target = event?.target as HTMLButtonElement;
if (!target.dataset.id) {
return;
}
GQL_DeleteCity.mutate({ city: target.dataset.id });
};
const addLibrary = (event: Event) => {
const target = event?.target as HTMLInputElement;
if (!target.dataset.id) {
return;
}
GQL_AddLibrary.mutate({ city: target.dataset.id, name: target.value });
target.value = '';
};
const deleteLibrary = (event: Event) => {
const target = event?.target as HTMLButtonElement;
if (!target.dataset.id) {
return;
}
GQL_DeleteLibrary.mutate({ library: target.dataset.id });
};
const addBook = (event: Event) => {
const target = event?.target as HTMLInputElement;
if (!target.dataset.id) {
return;
}
GQL_AddBook.mutate({ library: target.dataset.id, title: target.value });
target.value = '';
};
const deleteBook = (event: Event) => {
const target = event?.target as HTMLButtonElement;
if (!target.dataset.id) {
return;
}
GQL_DeleteBook.mutate({ book: target.dataset.id });
};
</script>

<h1>Nested - List</h1>

<ul>
{#each $GQL_Cities.data?.cities ?? [] as city}
<li>
{city?.id}: {city?.name}
<button data-id={city?.id} on:click={deleteCity}>Delete</button>
<ul>
{#each city?.libraries ?? [] as library}
<li>
{library?.id}: {library?.name}
<button data-id={library?.id} on:click={deleteLibrary}>Delete</button>
<ul>
{#each library?.books ?? [] as book}
<li>
{book?.id}: {book?.title}
<button data-id={book?.id} on:click={deleteBook}>Delete</button>
</li>
{/each}
<li><input data-id={library?.id} on:change={addBook} /></li>
</ul>
</li>
{/each}
<li><input data-id={city?.id} on:change={addLibrary} /></li>
</ul>
</li>
{/each}
<li>
<input on:change={addCity} />
</li>
</ul>

<pre>{JSON.stringify($GQL_Cities?.data, null, 4)}</pre>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation AddBook($library: ID!, $title: String!) {
addBook(library: $library, title: $title) {
id
...Book_List_insert @append(parentID: $library)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation AddCity($name: String!) {
addCity(name: $name) {
id
...City_List_insert
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation AddLibrary($city: ID!, $name: String!) {
addLibrary(city: $city, name: $name) {
id
...Library_List_insert @append(parentID: $city)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation DeleteBook($book: ID!) {
deleteBook(book: $book) {
id @Book_delete
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation DeleteCity($city: ID!) {
deleteCity(city: $city) {
id @City_delete
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation DeleteLibrary($library: ID!) {
deleteLibrary(library: $library) {
id @Library_delete
}
}
14 changes: 14 additions & 0 deletions e2e/sveltekit/src/routes/stores/nested-list/QUERY.Cities.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
query Cities {
cities @list(name: "City_List") {
id
name
libraries @list(name: "Library_List") {
id
name
books @list(name: "Book_List") {
id
title
}
}
}
}
2 changes: 2 additions & 0 deletions packages/houdini/src/runtime/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ class CacheInternal {
selection: fields,
subscribers: currentSubscribers,
variables,
parentType: linkedType,
})

toNotify.push(...currentSubscribers)
Expand Down Expand Up @@ -545,6 +546,7 @@ class CacheInternal {
selection: fields,
subscribers: currentSubscribers,
variables,
parentType: linkedType,
})
}
}
Expand Down
Loading

2 comments on commit 57577ee

@vercel
Copy link

@vercel vercel bot commented on 57577ee Nov 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 57577ee Nov 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs-next – ./site

docs-next-houdinigraphql.vercel.app
docs-next-git-main-houdinigraphql.vercel.app
docs-next-kohl.vercel.app

Please sign in to comment.