diff --git a/.changeset/rotten-cobras-grin.md b/.changeset/rotten-cobras-grin.md new file mode 100644 index 000000000..44d98356e --- /dev/null +++ b/.changeset/rotten-cobras-grin.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +Mutation list operations now work if you need to pass a `@with` directive to the fragment spread diff --git a/e2e/kit/src/lib/utils/routes.ts b/e2e/kit/src/lib/utils/routes.ts index 39863eda3..2d7f66bf0 100644 --- a/e2e/kit/src/lib/utils/routes.ts +++ b/e2e/kit/src/lib/utils/routes.ts @@ -20,6 +20,7 @@ export const routes = { Lists_all: '/lists/all?limit=15', Lists_fragment: '/lists/fragment', + Lists_mutation_insert: '/lists/mutation-insert', blocking: '/blocking', Stores_SSR: '/stores/ssr', diff --git a/e2e/kit/src/routes/lists/mutation-insert/+page.gql b/e2e/kit/src/routes/lists/mutation-insert/+page.gql new file mode 100644 index 000000000..cd6dfe2f5 --- /dev/null +++ b/e2e/kit/src/routes/lists/mutation-insert/+page.gql @@ -0,0 +1,11 @@ +query UsersListMutationInsertUsers($someParam: Boolean!) { + usersConnection(first: 5, snapshot: "users-list-mutation-insert") @list(name: "MyList") { + edges { + node { + id + name + testField(someParam: $someParam) + } + } + } +} diff --git a/e2e/kit/src/routes/lists/mutation-insert/+page.svelte b/e2e/kit/src/routes/lists/mutation-insert/+page.svelte new file mode 100644 index 000000000..029159714 --- /dev/null +++ b/e2e/kit/src/routes/lists/mutation-insert/+page.svelte @@ -0,0 +1,38 @@ + + + + +
+ {#if $UsersListMutationInsertUsers.data} + + {/if} +
diff --git a/e2e/kit/src/routes/lists/mutation-insert/+page.ts b/e2e/kit/src/routes/lists/mutation-insert/+page.ts new file mode 100644 index 000000000..138ed737b --- /dev/null +++ b/e2e/kit/src/routes/lists/mutation-insert/+page.ts @@ -0,0 +1,7 @@ +import type { UsersListMutationInsertUsersVariables } from './$houdini'; + +export const _UsersListMutationInsertUsersVariables: UsersListMutationInsertUsersVariables = () => { + return { + someParam: true + }; +}; diff --git a/e2e/kit/src/routes/lists/mutation-insert/spec.ts b/e2e/kit/src/routes/lists/mutation-insert/spec.ts new file mode 100644 index 000000000..98505b460 --- /dev/null +++ b/e2e/kit/src/routes/lists/mutation-insert/spec.ts @@ -0,0 +1,22 @@ +import { test } from '@playwright/test'; +import { routes } from '../../../lib/utils/routes.js'; +import { expect_to_be, goto, locator_click } from '../../../lib/utils/testsHelper.js'; + +test('mutation list insert with @with directive', async ({ page }) => { + await goto(page, routes.Lists_mutation_insert); + + // Verify the initial page data + await expect_to_be( + page, + 'Bruce Willis - Hello worldSamuel Jackson - Hello worldMorgan Freeman - Hello worldTom Hanks - Hello worldWill Smith - Hello world' + ); + + // Add a user + await locator_click(page, `button[id="addusers"]`); + + // Verify new user is at the top + await expect_to_be( + page, + 'Test User - Hello worldBruce Willis - Hello worldSamuel Jackson - Hello worldMorgan Freeman - Hello worldTom Hanks - Hello worldWill Smith - Hello world' + ); +}); diff --git a/packages/houdini/src/codegen/generators/artifacts/operations.ts b/packages/houdini/src/codegen/generators/artifacts/operations.ts index 6d75bde49..08293c80c 100644 --- a/packages/houdini/src/codegen/generators/artifacts/operations.ts +++ b/packages/houdini/src/codegen/generators/artifacts/operations.ts @@ -28,8 +28,19 @@ export function operationsByPath( // inside of the mutation could contain operations graphql.visit(definition, { FragmentSpread(node, _, __, ___, ancestors) { + // at this point, the fragment spread can contain the hashed parameters from an `@with` directive in it. + // if this fragment spread has a `@with` directive, strip the last `_asdf` from the name and check if that is a list fragment + let nameWithoutHash = node.name.value + + if ( + node.directives && + node.directives.find((directive) => directive.name.value === 'with') + ) { + nameWithoutHash = nameWithoutHash.substring(0, nameWithoutHash.lastIndexOf('_')) + } + // if the fragment is not a list operation, we don't care about it now - if (!config.isListFragment(node.name.value)) { + if (!config.isListFragment(nameWithoutHash)) { return } @@ -44,8 +55,8 @@ export function operationsByPath( operationObject({ config, filepath, - listName: config.listNameFromFragment(node.name.value), - operationKind: config.listOperationFromFragment(node.name.value), + listName: config.listNameFromFragment(nameWithoutHash), + operationKind: config.listOperationFromFragment(nameWithoutHash), type: parentTypeFromAncestors(config.schema, filepath, ancestors).name, selection: node, }) diff --git a/packages/houdini/src/codegen/generators/artifacts/tests/artifacts.test.ts b/packages/houdini/src/codegen/generators/artifacts/tests/artifacts.test.ts index 1ba82893f..283a27345 100644 --- a/packages/houdini/src/codegen/generators/artifacts/tests/artifacts.test.ts +++ b/packages/houdini/src/codegen/generators/artifacts/tests/artifacts.test.ts @@ -1984,6 +1984,124 @@ describe('mutation artifacts', function () { `) }) + test('insert operation and @with directive', async function () { + // the config to use in tests + const config = testConfig() + const docs = [ + mockCollectedDoc( + `mutation A { + addFriend { + friend { + ...All_Users_insert @with(filter: "Hello World") + } + } + }` + ), + mockCollectedDoc( + `query TestQuery($filter: String) { + users(stringValue: "foo") @list(name: "All_Users") { + firstName + field(filter: $filter) + } + }` + ), + ] + + // execute the generator + await runPipeline(config, docs) + + // load the contents of the file + expect(docs[0]).toMatchInlineSnapshot(` + export default { + "name": "A", + "kind": "HoudiniMutation", + "hash": "b1ad7b854a43149aedac6832e6a5bff625e125f516e1fea5806bf4123c4ee687", + + "raw": \`mutation A { + addFriend { + friend { + ...All_Users_insert_1oDy9M + id + } + } + } + + fragment All_Users_insert_1oDy9M on User { + firstName + field(filter: "Hello World") + id + } + \`, + + "rootType": "Mutation", + + "selection": { + "fields": { + "addFriend": { + "type": "AddFriendOutput", + "keyRaw": "addFriend", + + "selection": { + "fields": { + "friend": { + "type": "User", + "keyRaw": "friend", + + "operations": [{ + "action": "insert", + "list": "All_Users", + "position": "last" + }], + + "selection": { + "fields": { + "firstName": { + "type": "String", + "keyRaw": "firstName" + }, + + "field": { + "type": "String", + "keyRaw": "field(filter: \\"Hello World\\")", + "nullable": true + }, + + "id": { + "type": "ID", + "keyRaw": "id", + "visible": true + } + }, + + "fragments": { + "All_Users_insert": { + "arguments": { + "filter": { + "kind": "StringValue", + "value": "Hello World" + } + } + } + } + }, + + "visible": true + } + } + }, + + "visible": true + } + } + }, + + "pluginData": {} + }; + + "HoudiniHash=ba51f7673207e362cd0ba18f1dee123fc094a90123d0657b0d56c26d021426df"; + `) + }) + test('insert operation allList', async function () { // the config to use in tests const config = testConfig() @@ -2089,6 +2207,125 @@ describe('mutation artifacts', function () { `) }) + test('insert operation allList and @with directive', async function () { + // the config to use in tests + const config = testConfig() + const docs = [ + mockCollectedDoc( + `mutation A { + addFriend { + friend { + ...All_Users_insert @with(filter: "Hello World") @allLists + } + } + }` + ), + mockCollectedDoc( + `query TestQuery($filter: String) { + users(stringValue: "foo") @list(name: "All_Users") { + firstName + field(filter: $filter) + } + }` + ), + ] + + // execute the generator + await runPipeline(config, docs) + + // load the contents of the file + expect(docs[0]).toMatchInlineSnapshot(` + export default { + "name": "A", + "kind": "HoudiniMutation", + "hash": "b1ad7b854a43149aedac6832e6a5bff625e125f516e1fea5806bf4123c4ee687", + + "raw": \`mutation A { + addFriend { + friend { + ...All_Users_insert_1oDy9M + id + } + } + } + + fragment All_Users_insert_1oDy9M on User { + firstName + field(filter: "Hello World") + id + } + \`, + + "rootType": "Mutation", + + "selection": { + "fields": { + "addFriend": { + "type": "AddFriendOutput", + "keyRaw": "addFriend", + + "selection": { + "fields": { + "friend": { + "type": "User", + "keyRaw": "friend", + + "operations": [{ + "action": "insert", + "list": "All_Users", + "position": "last", + "target": "all" + }], + + "selection": { + "fields": { + "firstName": { + "type": "String", + "keyRaw": "firstName" + }, + + "field": { + "type": "String", + "keyRaw": "field(filter: \\"Hello World\\")", + "nullable": true + }, + + "id": { + "type": "ID", + "keyRaw": "id", + "visible": true + } + }, + + "fragments": { + "All_Users_insert": { + "arguments": { + "filter": { + "kind": "StringValue", + "value": "Hello World" + } + } + } + } + }, + + "visible": true + } + } + }, + + "visible": true + } + } + }, + + "pluginData": {} + }; + + "HoudiniHash=f6b965893f6a89f0d97c9a63645b36de599756e3e135d8912b1e0b741164caeb"; + `) + }) + test('remove operation allList', async function () { // the config to use in tests const config = testConfig() @@ -2292,6 +2529,125 @@ describe('mutation artifacts', function () { `) }) + test('toggle operation allList and @with directive', async function () { + // the config to use in tests + const config = testConfig() + const docs = [ + mockCollectedDoc( + `mutation A { + addFriend { + friend { + ...All_Users_toggle @with(filter: "Hello World") @allLists @prepend + } + } + }` + ), + mockCollectedDoc( + `query TestQuery($filter: String) { + users(stringValue: "foo") @list(name: "All_Users") { + firstName + field(filter: $filter) + } + }` + ), + ] + + // execute the generator + await runPipeline(config, docs) + + // load the contents of the file + expect(docs[0]).toMatchInlineSnapshot(` + export default { + "name": "A", + "kind": "HoudiniMutation", + "hash": "d7187de06687137a262178ad23eecf315461cd5cef17e2b384cbcdd25fe1e752", + + "raw": \`mutation A { + addFriend { + friend { + ...All_Users_toggle_1oDy9M + id + } + } + } + + fragment All_Users_toggle_1oDy9M on User { + firstName + field(filter: "Hello World") + id + } + \`, + + "rootType": "Mutation", + + "selection": { + "fields": { + "addFriend": { + "type": "AddFriendOutput", + "keyRaw": "addFriend", + + "selection": { + "fields": { + "friend": { + "type": "User", + "keyRaw": "friend", + + "operations": [{ + "action": "toggle", + "list": "All_Users", + "position": "first", + "target": "all" + }], + + "selection": { + "fields": { + "firstName": { + "type": "String", + "keyRaw": "firstName" + }, + + "field": { + "type": "String", + "keyRaw": "field(filter: \\"Hello World\\")", + "nullable": true + }, + + "id": { + "type": "ID", + "keyRaw": "id", + "visible": true + } + }, + + "fragments": { + "All_Users_toggle": { + "arguments": { + "filter": { + "kind": "StringValue", + "value": "Hello World" + } + } + } + } + }, + + "visible": true + } + } + }, + + "visible": true + } + } + }, + + "pluginData": {} + }; + + "HoudiniHash=2b2f4aafb54ec3bdba80389398aff1d4b6f478a5e58ec714bb6aa82c48e987b5"; + `) + }) + test('insert operation allList by default in config', async function () { // the config to use in tests const docs = [ @@ -2612,6 +2968,124 @@ describe('mutation artifacts', function () { `) }) + test('toggle operation and @with directive', async function () { + // the config to use in tests + const config = testConfig() + const docs = [ + mockCollectedDoc( + `mutation A { + addFriend { + friend { + ...All_Users_toggle @with(filter: "Hello World") + } + } + }` + ), + mockCollectedDoc( + `query TestQuery($filter: String) { + users(stringValue: "foo") @list(name: "All_Users") { + firstName + field(filter: $filter) + } + }` + ), + ] + + // execute the generator + await runPipeline(config, docs) + + // load the contents of the file + expect(docs[0]).toMatchInlineSnapshot(` + export default { + "name": "A", + "kind": "HoudiniMutation", + "hash": "d7187de06687137a262178ad23eecf315461cd5cef17e2b384cbcdd25fe1e752", + + "raw": \`mutation A { + addFriend { + friend { + ...All_Users_toggle_1oDy9M + id + } + } + } + + fragment All_Users_toggle_1oDy9M on User { + firstName + field(filter: "Hello World") + id + } + \`, + + "rootType": "Mutation", + + "selection": { + "fields": { + "addFriend": { + "type": "AddFriendOutput", + "keyRaw": "addFriend", + + "selection": { + "fields": { + "friend": { + "type": "User", + "keyRaw": "friend", + + "operations": [{ + "action": "toggle", + "list": "All_Users", + "position": "last" + }], + + "selection": { + "fields": { + "firstName": { + "type": "String", + "keyRaw": "firstName" + }, + + "field": { + "type": "String", + "keyRaw": "field(filter: \\"Hello World\\")", + "nullable": true + }, + + "id": { + "type": "ID", + "keyRaw": "id", + "visible": true + } + }, + + "fragments": { + "All_Users_toggle": { + "arguments": { + "filter": { + "kind": "StringValue", + "value": "Hello World" + } + } + } + } + }, + + "visible": true + } + } + }, + + "visible": true + } + } + }, + + "pluginData": {} + }; + + "HoudiniHash=4a95f1e6dc9fdce153311e84965a99e72f76fc56a063fced1e28efefc50f143a"; + `) + }) + test('remove operation', async function () { // the config to use in tests const config = testConfig()