From 38ecbe70737c5121da0d38d50b6e7f9135bf7e55 Mon Sep 17 00:00:00 2001 From: Jonas Jacobsen Date: Sat, 5 Nov 2022 08:41:19 +0100 Subject: [PATCH 1/5] 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 --- packages/houdini/src/runtime/cache/cache.ts | 2 ++ packages/houdini/src/runtime/cache/subscription.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/houdini/src/runtime/cache/cache.ts b/packages/houdini/src/runtime/cache/cache.ts index 7193e2d48c..f827f6a340 100644 --- a/packages/houdini/src/runtime/cache/cache.ts +++ b/packages/houdini/src/runtime/cache/cache.ts @@ -357,6 +357,7 @@ class CacheInternal { selection: fields, subscribers: currentSubscribers, variables, + parentType: linkedType, }) toNotify.push(...currentSubscribers) @@ -545,6 +546,7 @@ class CacheInternal { selection: fields, subscribers: currentSubscribers, variables, + parentType: linkedType, }) } } diff --git a/packages/houdini/src/runtime/cache/subscription.ts b/packages/houdini/src/runtime/cache/subscription.ts index 58a2f4c57c..49a71b6afa 100644 --- a/packages/houdini/src/runtime/cache/subscription.ts +++ b/packages/houdini/src/runtime/cache/subscription.ts @@ -156,16 +156,17 @@ export class InMemorySubscriptions { selection, variables, subscribers, + parentType, }: { parent: string selection: SubscriptionSelection variables: {} subscribers: SubscriptionSpec[] + parentType: string }) { // look at every field in the selection and add the subscribers for (const fieldSelection of Object.values(selection)) { - const { keyRaw, fields } = fieldSelection - + const { type: linkedType, keyRaw, fields } = fieldSelection const key = evaluateKey(keyRaw, variables) // add the subscriber to the @@ -175,7 +176,7 @@ export class InMemorySubscriptions { key, selection: fieldSelection, spec, - parentType: 'asdf', + parentType: parentType, variables, }) } @@ -183,6 +184,7 @@ export class InMemorySubscriptions { // if there are fields under this if (fields) { const { value: link } = this.cache._internal_unstable.storage.get(parent, key) + // figure out who else needs subscribers const children = !Array.isArray(link) ? ([link] as string[]) @@ -192,13 +194,13 @@ export class InMemorySubscriptions { if (!linkedRecord) { continue } - // insert the subscriber this.addMany({ parent: linkedRecord, selection: fields, variables, subscribers, + parentType: linkedType, }) } } From 57b37770e18aed7afbc115f7bfdd590813b7b9b0 Mon Sep 17 00:00:00 2001 From: Jonas Jacobsen Date: Sat, 5 Nov 2022 12:52:12 +0100 Subject: [PATCH 2/5] 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 --- e2e/_api/graphql.mjs | 107 +++++++++++++++++- e2e/_api/schema.graphql | 39 +++++++ .../graphql/operations/MUTATION.AddBook.gql | 6 + .../graphql/operations/MUTATION.AddCity.gql | 6 + .../operations/MUTATION.AddLibrary.gql | 6 + .../operations/MUTATION.DeleteBook.gql | 5 + .../operations/MUTATION.DeleteCity.gql | 5 + .../operations/MUTATION.DeleteLibrary.gql | 5 + .../lib/graphql/operations/QUERY.Cities.gql | 14 +++ e2e/sveltekit/src/lib/utils/routes.ts | 2 + .../routes/stores/nested-list/+page.svelte | 98 ++++++++++++++++ 11 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddBook.gql create mode 100644 e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddCity.gql create mode 100644 e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddLibrary.gql create mode 100644 e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteBook.gql create mode 100644 e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteCity.gql create mode 100644 e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteLibrary.gql create mode 100644 e2e/sveltekit/src/lib/graphql/operations/QUERY.Cities.gql create mode 100644 e2e/sveltekit/src/routes/stores/nested-list/+page.svelte diff --git a/e2e/_api/graphql.mjs b/e2e/_api/graphql.mjs index 0fc0070b50..8ee4c8d86b 100644 --- a/e2e/_api/graphql.mjs +++ b/e2e/_api/graphql.mjs @@ -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) }, @@ -100,6 +135,9 @@ export const resolvers = { __typename: 'User', } }, + cities: () => { + return cities; + }, }, User: { @@ -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 }) => { @@ -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({ diff --git a/e2e/_api/schema.graphql b/e2e/_api/schema.graphql index 7467df3987..df08501c06 100644 --- a/e2e/_api/schema.graphql +++ b/e2e/_api/schema.graphql @@ -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 { @@ -52,6 +72,7 @@ type Query { ): UserConnection! usersList(limit: Int = 4, offset: Int, snapshot: String!): [User!]! session: String + cities: [City]! } type User implements Node { @@ -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]! +} \ No newline at end of file diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddBook.gql b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddBook.gql new file mode 100644 index 0000000000..4cdf9211c7 --- /dev/null +++ b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddBook.gql @@ -0,0 +1,6 @@ +mutation AddBook($library: ID!, $title: String!) { + addBook(library: $library, title: $title) { + id + ...Book_List_insert @append(parentID: $library) + } +} diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddCity.gql b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddCity.gql new file mode 100644 index 0000000000..8a94c88c68 --- /dev/null +++ b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddCity.gql @@ -0,0 +1,6 @@ +mutation AddCity($name: String!) { + addCity(name: $name) { + id + ...City_List_insert + } +} diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddLibrary.gql b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddLibrary.gql new file mode 100644 index 0000000000..63e61e4b98 --- /dev/null +++ b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddLibrary.gql @@ -0,0 +1,6 @@ +mutation AddLibrary($city: ID!, $name: String!) { + addLibrary(city: $city, name: $name) { + id + ...Library_List_insert @append(parentID: $city) + } +} diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteBook.gql b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteBook.gql new file mode 100644 index 0000000000..480cd6b517 --- /dev/null +++ b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteBook.gql @@ -0,0 +1,5 @@ +mutation DeleteBook($book: ID!) { + deleteBook(book: $book) { + id @Book_delete + } +} diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteCity.gql b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteCity.gql new file mode 100644 index 0000000000..f14b960c15 --- /dev/null +++ b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteCity.gql @@ -0,0 +1,5 @@ +mutation DeleteCity($city: ID!) { + deleteCity(city: $city) { + id @City_delete + } +} diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteLibrary.gql b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteLibrary.gql new file mode 100644 index 0000000000..a0ffc88195 --- /dev/null +++ b/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteLibrary.gql @@ -0,0 +1,5 @@ +mutation DeleteLibrary($library: ID!) { + deleteLibrary(library: $library) { + id @Library_delete + } +} diff --git a/e2e/sveltekit/src/lib/graphql/operations/QUERY.Cities.gql b/e2e/sveltekit/src/lib/graphql/operations/QUERY.Cities.gql new file mode 100644 index 0000000000..546b8dc57b --- /dev/null +++ b/e2e/sveltekit/src/lib/graphql/operations/QUERY.Cities.gql @@ -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 + } + } + } +} diff --git a/e2e/sveltekit/src/lib/utils/routes.ts b/e2e/sveltekit/src/lib/utils/routes.ts index 86f053c005..0cc1bc11cd 100644 --- a/e2e/sveltekit/src/lib/utils/routes.ts +++ b/e2e/sveltekit/src/lib/utils/routes.ts @@ -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', diff --git a/e2e/sveltekit/src/routes/stores/nested-list/+page.svelte b/e2e/sveltekit/src/routes/stores/nested-list/+page.svelte new file mode 100644 index 0000000000..f645063aca --- /dev/null +++ b/e2e/sveltekit/src/routes/stores/nested-list/+page.svelte @@ -0,0 +1,98 @@ + + +

Nested - List

+ +
    + {#each $GQL_Cities.data?.cities ?? [] as city} +
  • + {city?.id}: {city?.name} + +
      + {#each city?.libraries ?? [] as library} +
    • + {library?.id}: {library?.name} + +
        + {#each library?.books ?? [] as book} +
      • + {book?.id}: {book?.title} + +
      • + {/each} +
      • +
      +
    • + {/each} +
    • +
    +
  • + {/each} +
  • + +
  • +
+ +
{JSON.stringify($GQL_Cities?.data, null, 4)}
From cc13343796da64a528ab6e842ddd2491eec2938e Mon Sep 17 00:00:00 2001 From: Jonas Jacobsen Date: Sat, 5 Nov 2022 13:39:12 +0100 Subject: [PATCH 3/5] Changeset Signed-off-by: Jonas Jacobsen --- .changeset/rotten-timers-exist.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rotten-timers-exist.md diff --git a/.changeset/rotten-timers-exist.md b/.changeset/rotten-timers-exist.md new file mode 100644 index 0000000000..ea85c086d8 --- /dev/null +++ b/.changeset/rotten-timers-exist.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +Provide parentType to addMany subscriptions From b338751319bfe85a3ec0c06f893a60308161cece Mon Sep 17 00:00:00 2001 From: Jonas Jacobsen Date: Sat, 5 Nov 2022 15:07:40 +0100 Subject: [PATCH 4/5] 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 --- .../operations => routes/stores/nested-list}/MUTATION.AddBook.gql | 0 .../operations => routes/stores/nested-list}/MUTATION.AddCity.gql | 0 .../stores/nested-list}/MUTATION.AddLibrary.gql | 0 .../stores/nested-list}/MUTATION.DeleteBook.gql | 0 .../stores/nested-list}/MUTATION.DeleteCity.gql | 0 .../stores/nested-list}/MUTATION.DeleteLibrary.gql | 0 .../operations => routes/stores/nested-list}/QUERY.Cities.gql | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename e2e/sveltekit/src/{lib/graphql/operations => routes/stores/nested-list}/MUTATION.AddBook.gql (100%) rename e2e/sveltekit/src/{lib/graphql/operations => routes/stores/nested-list}/MUTATION.AddCity.gql (100%) rename e2e/sveltekit/src/{lib/graphql/operations => routes/stores/nested-list}/MUTATION.AddLibrary.gql (100%) rename e2e/sveltekit/src/{lib/graphql/operations => routes/stores/nested-list}/MUTATION.DeleteBook.gql (100%) rename e2e/sveltekit/src/{lib/graphql/operations => routes/stores/nested-list}/MUTATION.DeleteCity.gql (100%) rename e2e/sveltekit/src/{lib/graphql/operations => routes/stores/nested-list}/MUTATION.DeleteLibrary.gql (100%) rename e2e/sveltekit/src/{lib/graphql/operations => routes/stores/nested-list}/QUERY.Cities.gql (100%) diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddBook.gql b/e2e/sveltekit/src/routes/stores/nested-list/MUTATION.AddBook.gql similarity index 100% rename from e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddBook.gql rename to e2e/sveltekit/src/routes/stores/nested-list/MUTATION.AddBook.gql diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddCity.gql b/e2e/sveltekit/src/routes/stores/nested-list/MUTATION.AddCity.gql similarity index 100% rename from e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddCity.gql rename to e2e/sveltekit/src/routes/stores/nested-list/MUTATION.AddCity.gql diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddLibrary.gql b/e2e/sveltekit/src/routes/stores/nested-list/MUTATION.AddLibrary.gql similarity index 100% rename from e2e/sveltekit/src/lib/graphql/operations/MUTATION.AddLibrary.gql rename to e2e/sveltekit/src/routes/stores/nested-list/MUTATION.AddLibrary.gql diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteBook.gql b/e2e/sveltekit/src/routes/stores/nested-list/MUTATION.DeleteBook.gql similarity index 100% rename from e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteBook.gql rename to e2e/sveltekit/src/routes/stores/nested-list/MUTATION.DeleteBook.gql diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteCity.gql b/e2e/sveltekit/src/routes/stores/nested-list/MUTATION.DeleteCity.gql similarity index 100% rename from e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteCity.gql rename to e2e/sveltekit/src/routes/stores/nested-list/MUTATION.DeleteCity.gql diff --git a/e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteLibrary.gql b/e2e/sveltekit/src/routes/stores/nested-list/MUTATION.DeleteLibrary.gql similarity index 100% rename from e2e/sveltekit/src/lib/graphql/operations/MUTATION.DeleteLibrary.gql rename to e2e/sveltekit/src/routes/stores/nested-list/MUTATION.DeleteLibrary.gql diff --git a/e2e/sveltekit/src/lib/graphql/operations/QUERY.Cities.gql b/e2e/sveltekit/src/routes/stores/nested-list/QUERY.Cities.gql similarity index 100% rename from e2e/sveltekit/src/lib/graphql/operations/QUERY.Cities.gql rename to e2e/sveltekit/src/routes/stores/nested-list/QUERY.Cities.gql From 3a9f2b52d21d6b339c3b739de5ac76764a18fa43 Mon Sep 17 00:00:00 2001 From: Jonas Jacobsen Date: Sun, 6 Nov 2022 19:35:19 +0100 Subject: [PATCH 5/5] 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 --- .../runtime/cache/tests/subscriptions.test.ts | 557 ++++++++++++++++++ 1 file changed, 557 insertions(+) diff --git a/packages/houdini/src/runtime/cache/tests/subscriptions.test.ts b/packages/houdini/src/runtime/cache/tests/subscriptions.test.ts index 84051a7b97..7ed43f00f9 100644 --- a/packages/houdini/src/runtime/cache/tests/subscriptions.test.ts +++ b/packages/houdini/src/runtime/cache/tests/subscriptions.test.ts @@ -1637,6 +1637,563 @@ test('clearing a display layer updates subscribers', function () { }) }) + +test('nested list receiving update from adding item to inner list', function () { + // instantiate a cache + const cache = new Cache(config); + + const selection = { + cities: { + type: "City", + keyRaw: "cities", + list: { + name: "City_List", + connection: false, + type: "City" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + name: { + type: "String", + keyRaw: "name" + }, + libraries: { + type: "Library", + keyRaw: "libraries", + list: { + name: "Library_List", + connection: false, + type: "Library" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + name: { + type: "String", + keyRaw: "name" + }, + books: { + type: "Book", + keyRaw: "books", + list: { + name: "Book_List", + connection: false, + type: "Book" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + title: { + type: "String", + keyRaw: "title" + } + } + } + } + } + } + } + }; + + const data = { + cities: [ + { + id: "1", + name: "Alexandria", + libraries: [ + { + id: "1", + name: "The Library of Alexandria", + books: [ + { + id: "1", + title: "Callimachus Pinakes" + }, + { + id: "2", + title: "Kutubkhana-i-lskandriyya" + } + ] + }, + { + id: "2", + name: "Bibliotheca Alexandrina", + books: [ + { + id: "3", + title: "Analyze your own personality" + } + ] + } + ] + }, + { + id: "2", + name: "Istanbul", + libraries: [ + { + id: "3", + name: "The Imperial Library of Constantinople", + books: [ + { + id: "4", + title: "Homer" + }, + { + id: "5", + title: "The Hellenistic History" + } + ] + } + ] + } + ] + }; + + // write the database + cache.write({ selection, data }); + + // add city + cache.write({ + selection: { + addCity: { + type: "City", + keyRaw: "addCity(name: $name)", + operations: [ + { + action: "insert", + list: "City_List", + position: "last" + } + ], + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + name: { + type: "String", + keyRaw: "name" + }, + libraries: { + type: "Library", + keyRaw: "libraries", + list: { + name: "Library_List", + connection: false, + type: "Library" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + name: { + type: "String", + keyRaw: "name" + }, + books: { + type: "Book", + keyRaw: "books", + list: { + name: "Book_List", + connection: false, + type: "Book" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + title: { + type: "String", + keyRaw: "title" + } + } + } + } + } + } + } + }, + data: { + addCity: { + id: "3", + name: "Aalborg", + libraries: [] + } + }, + variables: { + name: "Aalborg" + }, + forceNotify: true, + }); + cache.write({ + selection: { + newEntries: { + keyRaw: "cities", + type: "City", + update: "append", + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + name: { + type: "String", + keyRaw: "name" + }, + libraries: { + type: "Library", + keyRaw: "libraries", + list: { + name: "Library_List", + connection: false, + type: "Library" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + name: { + type: "String", + keyRaw: "name" + }, + books: { + type: "Book", + keyRaw: "books", + list: { + name: "Book_List", + connection: false, + type: "Book" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + title: { + type: "String", + keyRaw: "title" + } + } + } + } + }, + __typename: { + keyRaw: "__typename", + type: "String" + } + } + } + }, + data: { + newEntries: [ + { + id: "3", + name: "Aalborg", + libraries: [], + __typename: "City" + } + ] + }, + variables: { + name: "Aalborg" + }, + applyUpdates: true, + }); + + // add library + cache.write({ + selection: { + addLibrary: { + type: "Library", + keyRaw: "addLibrary(city: $city, name: $name)", + operations: [ + { + action: "insert", + list: "Library_List", + position: "last", + parentID: { + kind: "Variable", + value: "city" + } + } + ], + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + name: { + type: "String", + keyRaw: "name" + }, + books: { + type: "Book", + keyRaw: "books", + list: { + name: "Book_List", + connection: false, + type: "Book" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + title: { + type: "String", + keyRaw: "title" + } + } + } + } + } + }, + data: { + addLibrary: { + id: "4", + name: "Aalborg Bibliotekerne", + books: [] + } + }, + variables: { + city: "3", + name: "Aalborg Bibliotekerne" + }, + forceNotify: true, + }); + cache.write({ + selection: { + newEntries: { + keyRaw: "libraries", + type: "Library", + update: "append", + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + name: { + type: "String", + keyRaw: "name" + }, + books: { + type: "Book", + keyRaw: "books", + list: { + name: "Book_List", + connection: false, + type: "Book" + }, + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + title: { + type: "String", + keyRaw: "title" + } + } + }, + __typename: { + keyRaw: "__typename", + type: "String" + } + } + } + }, + data: { + newEntries: [ + { + id: "4", + name: "Aalborg Bibliotekerne", + books: [], + __typename: "Library" + } + ] + }, + variables: { + city: "3", + name: "Aalborg Bibliotekerne" + }, + parent: 'City:3', + applyUpdates: true, + }); + + // a function to spy on that will play the role of set + const set = vi.fn(); + + // subscribe to the fields + cache.subscribe({ + rootType: 'Query', + selection, + set, + }); + + // add book + cache.write({ + selection: { + addBook: { + type: "Book", + keyRaw: "addBook(library: $library, title: $title)", + operations: [ + { + action: "insert", + list: "Book_List", + position: "last", + parentID: { + kind: "Variable", + value: "library" + } + } + ], + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + title: { + type: "String", + keyRaw: "title" + } + } + } + }, + data: { + addBook: { + id: "6", + title: "Stone Blind" + } + }, + variables: { + library: "4", + title: "Stone Blind" + }, + forceNotify: true, + }); + cache.write({ + selection: { + newEntries: { + keyRaw: "books", + type: "Book", + update: "append", + fields: { + id: { + type: "ID", + keyRaw: "id" + }, + title: { + type: "String", + keyRaw: "title" + }, + __typename: { + keyRaw: "__typename", + type: "String" + } + } + } + }, + data: { + newEntries: [ + { + id: "6", + title: "Stone Blind", + __typename: "Book" + } + ] + }, + variables: { + library: "4", + title: "Stone Blind" + }, + parent: 'Parent:4', + applyUpdates: true, + }); + + // make sure that set got called with the full response containing new city, library, and book + expect(set).toHaveBeenCalledWith({ + cities: [ + { + id: "1", + name: "Alexandria", + libraries: [ + { + id: "1", + name: "The Library of Alexandria", + books: [ + { + id: "1", + title: "Callimachus Pinakes" + }, + { + id: "2", + title: "Kutubkhana-i-lskandriyya" + } + ] + }, + { + id: "2", + name: "Bibliotheca Alexandrina", + books: [ + { + id: "3", + title: "Analyze your own personality" + } + ] + } + ] + }, + { + id: "2", + name: "Istanbul", + libraries: [ + { + id: "3", + name: "The Imperial Library of Constantinople", + books: [ + { + id: "4", + title: "Homer" + }, + { + id: "5", + title: "The Hellenistic History" + } + ] + } + ] + }, + { + id: "3", + name: "Aalborg", + libraries: [ + { + id: "4", + name: "Aalborg Bibliotekerne", + books: [ + { + id: "6", + title: "Stone Blind" + } + ] + } + ] + } + ] + }); +}); + test.todo('can write to and resolve layers') test.todo("resolving a layer with the same value as the most recent doesn't notify subscribers")