From bcb48ac08a7109275b264bec12f5a01ef477bf77 Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 01:00:32 -0700 Subject: [PATCH 01/11] document api url config removal --- site/src/routes/guides/migrating-to-016.svx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/src/routes/guides/migrating-to-016.svx b/site/src/routes/guides/migrating-to-016.svx index ef75fae230..c0257b1848 100644 --- a/site/src/routes/guides/migrating-to-016.svx +++ b/site/src/routes/guides/migrating-to-016.svx @@ -30,6 +30,9 @@ npx svelte-migrate routes so unless you are doing something special, you can probably just delete it. If you _are_ doing something special, make sure that you include `.js` in your extensions so that the generated `+page.js` can be picked up if you use an automatic loader. Keep in mind there is a new `exclude` value that might be better suited to your situation. +- `apiUrl` has a slightly new behavior. It now controls wether or not the vite plugin will poll for schema changes. + If the latest version of your schema is available locally then you should just omit the value. This is common in + monorepos and GraphQL apis that are resolved with a SvelteKit endpoint. ```diff From 1f60522590d49ce7375e41b8942f09ff309b59a8 Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 01:02:20 -0700 Subject: [PATCH 02/11] more doc tweaks --- site/src/routes/api/config.svx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/site/src/routes/api/config.svx b/site/src/routes/api/config.svx index beb91da432..4d9d29506c 100644 --- a/site/src/routes/api/config.svx +++ b/site/src/routes/api/config.svx @@ -17,11 +17,10 @@ It can contain the following values: - `exclude` (optional): a pattern that filters out files that match the include pattern - `projectDir` (optional, default: `process.cwd()`): an absolute path pointing to your SvelteKit project (useful for monorepos) - `schemaPath` (optional, default: `"./schema.graphql"`): the path to the static representation of your schema, can be a glob pointing to multiple files. One of `schema` or `schemaPath` is required -- `schema` (optional): a string containing your entire schema (mostly useful for testing). One of `schema` or `schemaPath` is required +- `apiUrl` (optional): A url to use to pull the schema. If you don't pass an `apiUrl`, the kit plugin will not poll for schema changes. For more information see the [pull-schema command docs](/api/cli#pull-schema). - `framework` (optional, default: `"kit"`): Either `"kit"` or `"svelte"`. Used to tell the preprocessor what kind of loading paradigm to generate for you. (default: `kit`) - `module` (optional, default: `"esm"`): One of `"esm"` or `"commonjs"`. Used to tell the artifact generator what kind of modules to create. (default: `esm`) - `definitionsPath` (optional, default: `"$houdini/graphql"`): a path that the generator will use to write `schema.graphql` and `documents.gql` files containing all of the internal fragment and directive definitions used in the project. -- `apiUrl` (optional): A url to use to pull the schema. If you don't pass an `apiUrl`, the kit plugin will not poll for schema changes. For more information see the [pull-schema command docs](/api/cli#pull-schema). - `scalars` (optional): An object describing custom scalars for your project (see below). - `cacheBufferSize` (optional, default: `10`): The number of queries that must occur before a value is removed from the cache. For more information, see the [Caching Guide](/guides/caching-data). - `defaultCachePolicy` (optional, default: `"CacheOrNetwork"`): The default cache policy to use for queries. For a list of the policies or other information see the [Caching Guide](/guides/caching-data). @@ -32,7 +31,7 @@ It can contain the following values: - `disableMasking` (optional, default: `false`): A boolean indicating whether fields from referenced fragments should be included in a document's selection set - `schemaPollHeaders` (optional): An object specifying the headers to use when pulling your schema. Keys of the object are header names and its values can be either a string pointing to the environment variable to use as the header value or a function that takes the current `process.env` and returns the the value to use. - `schemaPollInterval` (optional, default: `2000`): Configures the schema polling behavior for the kit plugin. If its value is greater than `0`, the plugin will poll the set number of milliseconds. If set to `0`, the plugin will only pull the schema when you first run `dev`. If you set to `null`, the plugin will never look for schema changes. You can see use the [pull-schema command] to get updates. -- `globalStorePrefix` (optional, default: `GQL_`): The default prefix of your global stores. Note: it's nice to have a prefix so that your editor finds all your stores by just typings this prefix. +- `globalStorePrefix` (optional, default: `GQL_`): The default prefix of your global stores. This lets your editor provide autocompletion with just a few characters. ## Custom Scalars From 94d2782e19f9ea424721fd7682f79f6386e078cd Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 03:15:12 -0700 Subject: [PATCH 03/11] generate all optional optimistic update type --- src/cmd/generators/stores/mutation.ts | 5 +- src/cmd/generators/typescript/index.ts | 31 +++++++- src/cmd/generators/typescript/inlineType.ts | 32 ++++++--- .../generators/typescript/typescript.test.ts | 70 +++++++++++++++++++ src/runtime/stores/mutation.ts | 25 +++---- 5 files changed, 134 insertions(+), 29 deletions(-) diff --git a/src/cmd/generators/stores/mutation.ts b/src/cmd/generators/stores/mutation.ts index 6c5a0fedd8..345012668c 100644 --- a/src/cmd/generators/stores/mutation.ts +++ b/src/cmd/generators/stores/mutation.ts @@ -31,11 +31,12 @@ export default ${globalStoreName} const _input = `${artifactName}$input` const _data = `${artifactName}$result` + const _optimistic = `${artifactName}$optimistic` // the type definitions for the store - const typeDefs = `import type { ${_input}, ${_data}, MutationStore } from '$houdini' + const typeDefs = `import type { ${_input}, ${_data}, ${_optimistic}, MutationStore } from '$houdini' -export declare class ${storeName} extends MutationStore<${_data} | undefined, ${_input}>{ +export declare class ${storeName} extends MutationStore<${_data} | undefined, ${_input}, ${_optimistic}>{ constructor() {} } diff --git a/src/cmd/generators/typescript/index.ts b/src/cmd/generators/typescript/index.ts index 9ef0bee11d..abca20ec18 100644 --- a/src/cmd/generators/typescript/index.ts +++ b/src/cmd/generators/typescript/index.ts @@ -179,6 +179,7 @@ async function generateOperationTypeDefs( // the name of the types we will define const inputTypeName = `${definition.name!.value}$input` const shapeTypeName = `${definition.name!.value}$result` + const optimisticTypeName = `${definition.name!.value}$optimistic` // dry const hasInputs = definition.variableDefinitions && definition.variableDefinitions.length > 0 @@ -222,10 +223,11 @@ async function generateOperationTypeDefs( rootType: parentType, selections, root: true, - allowReadonly: true, + readonly: true, visitedTypes, body, missingScalars, + includeFragments: true, }) ) ) @@ -273,6 +275,30 @@ async function generateOperationTypeDefs( ) ) } + + // mutations need to have an optimistic response type defined + if (definition.operation === 'mutation') { + body.push( + AST.exportNamedDeclaration( + AST.tsTypeAliasDeclaration( + AST.identifier(optimisticTypeName), + inlineType({ + config, + filepath, + rootType: parentType, + selections, + root: true, + readonly: true, + visitedTypes, + body, + missingScalars, + includeFragments: false, + allOptional: true, + }) + ) + ) + ) + } } async function generateFragmentTypeDefs( @@ -349,10 +375,11 @@ async function generateFragmentTypeDefs( rootType: type, selections, root: true, - allowReadonly: true, + readonly: true, body, visitedTypes, missingScalars, + includeFragments: true, }) ) ) diff --git a/src/cmd/generators/typescript/inlineType.ts b/src/cmd/generators/typescript/inlineType.ts index 71f44219a3..5fc7ef1036 100644 --- a/src/cmd/generators/typescript/inlineType.ts +++ b/src/cmd/generators/typescript/inlineType.ts @@ -16,20 +16,24 @@ export function inlineType({ rootType, selections, root, - allowReadonly, + readonly, body, visitedTypes, missingScalars, + includeFragments, + allOptional, }: { config: Config filepath: string rootType: graphql.GraphQLNamedType selections: readonly graphql.SelectionNode[] | undefined root: boolean - allowReadonly: boolean + readonly: boolean body: StatementKind[] visitedTypes: Set missingScalars: Set + includeFragments: boolean + allOptional?: boolean }): TSTypeKind { // start unwrapping non-nulls and lists (we'll wrap it back up before we return) const { type, wrappers } = unwrapType(config, rootType) @@ -162,20 +166,28 @@ export function inlineType({ rootType: field.type as graphql.GraphQLNamedType, selections: selection.selectionSet?.selections as graphql.SelectionNode[], root: false, - allowReadonly, + readonly, visitedTypes, body, missingScalars, + includeFragments, + allOptional, }) // we're done - return readonlyProperty( + const prop = readonlyProperty( AST.tsPropertySignature( AST.identifier(attributeName), AST.tsTypeAnnotation(attributeType) ), - allowReadonly + readonly ) + + if (allOptional) { + prop.optional = true + } + + return prop }), ]) @@ -184,7 +196,7 @@ export function inlineType({ | graphql.FragmentSpreadNode[] | undefined - if (fragmentSpreads && fragmentSpreads.length) { + if (includeFragments && fragmentSpreads && fragmentSpreads.length) { result.members.push( readonlyProperty( AST.tsPropertySignature( @@ -202,7 +214,7 @@ export function inlineType({ ) ) ), - allowReadonly + readonly ) ) } @@ -224,11 +236,13 @@ export function inlineType({ filepath, rootType: fragmentRootType, selections: fragment, - allowReadonly, + readonly, visitedTypes, root, body, missingScalars, + includeFragments, + allOptional, }) // we need to handle __typename in the generated type. this means removing @@ -265,7 +279,7 @@ export function inlineType({ AST.identifier('__typename'), AST.tsTypeAnnotation(AST.tsLiteralType(AST.stringLiteral(typeName))) ), - allowReadonly + readonly ) ) } diff --git a/src/cmd/generators/typescript/typescript.test.ts b/src/cmd/generators/typescript/typescript.test.ts index 5558910602..2ea901c529 100644 --- a/src/cmd/generators/typescript/typescript.test.ts +++ b/src/cmd/generators/typescript/typescript.test.ts @@ -453,6 +453,70 @@ describe('typescript', function () { age?: number | null | undefined, weight?: number | null | undefined }; + + export type MyMutation$optimistic = { + readonly doThing?: { + readonly firstName?: string + } | null + }; + `) + }) + + test("mutation optimistic response type doesn't include fragments", async function () { + // the document to test + const docs = [ + mockCollectedDoc( + `mutation MyMutation { + doThing( + list: [], + id: "1" + firstName: "hello" + ) { + firstName + ...TestFragment, + } + }` + ), + mockCollectedDoc( + `fragment TestFragment on User { + firstName + }` + ), + ] + + // execute the generator + await runPipeline(config, docs) + + // look up the files in the artifact directory + const fileContents = await readFile(config.artifactTypePath(docs[0].document)) + + // make sure they match what we expect + expect( + recast.parse(fileContents!, { + parser: typeScriptParser, + }) + ).toMatchInlineSnapshot(` + export type MyMutation = { + readonly "input": MyMutation$input, + readonly "result": MyMutation$result + }; + + export type MyMutation$result = { + readonly doThing: { + readonly firstName: string, + readonly $fragments: { + TestFragment: true + } + } | null + }; + + export type MyMutation$input = null; + + export type MyMutation$optimistic = { + readonly doThing?: { + readonly firstName?: string + } | null + }; `) }) @@ -1104,6 +1168,12 @@ describe('typescript', function () { age?: number | null | undefined, weight?: number | null | undefined }; + + export type MyMutation$optimistic = { + readonly doThing?: { + readonly id?: string + } | null + }; `) }) diff --git a/src/runtime/stores/mutation.ts b/src/runtime/stores/mutation.ts index 95b5e47870..79af1ff5cf 100644 --- a/src/runtime/stores/mutation.ts +++ b/src/runtime/stores/mutation.ts @@ -7,10 +7,14 @@ import type { SubscriptionSpec, MutationArtifact } from '../lib' import { getCurrentConfig } from '../lib/config' import { executeQuery } from '../lib/network' import { marshalInputs, marshalSelection, unmarshalSelection } from '../lib/scalars' -import { GraphQLObject, HoudiniFetchContext } from '../lib/types' +import { GraphQLObject } from '../lib/types' import { BaseStore } from './store' -export class MutationStore<_Data extends GraphQLObject, _Input> extends BaseStore { +export class MutationStore< + _Data extends GraphQLObject, + _Input, + _Optimistic extends GraphQLObject +> extends BaseStore { artifact: MutationArtifact kind = 'HoudiniMutation' as const @@ -32,7 +36,7 @@ export class MutationStore<_Data extends GraphQLObject, _Input> extends BaseStor // @ts-ignore metadata?: App.Metadata fetch?: typeof globalThis.fetch - } & MutationConfig<_Data, _Input> = {} + } & MutationConfig<_Data, _Input, _Optimistic> = {} ): Promise<_Data | null> { const config = await getCurrentConfig() @@ -65,17 +69,6 @@ export class MutationStore<_Data extends GraphQLObject, _Input> extends BaseStor variables, layer: layer.id, }) - - const storeData = { - data: optimisticResponse, - errors: null, - isFetching: true, - isOptimisticResponse: true, - variables, - } - - // update the store value - this.store.set(storeData) } const newVariables = (await marshalInputs({ @@ -176,8 +169,8 @@ export class MutationStore<_Data extends GraphQLObject, _Input> extends BaseStor } } -export type MutationConfig<_Result, _Input> = { - optimisticResponse?: _Result +export type MutationConfig<_Result, _Input, _Optimistic> = { + optimisticResponse?: _Optimistic } export type MutationResult<_Data, _Input> = { From 2a6c22b1eca8396111142a16b9972176f5c29874 Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 03:19:06 -0700 Subject: [PATCH 04/11] undo parameter name tweak --- src/cmd/generators/typescript/inlineType.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cmd/generators/typescript/inlineType.ts b/src/cmd/generators/typescript/inlineType.ts index 5fc7ef1036..e4fec4485e 100644 --- a/src/cmd/generators/typescript/inlineType.ts +++ b/src/cmd/generators/typescript/inlineType.ts @@ -16,7 +16,7 @@ export function inlineType({ rootType, selections, root, - readonly, + allowReadonly, body, visitedTypes, missingScalars, @@ -28,12 +28,12 @@ export function inlineType({ rootType: graphql.GraphQLNamedType selections: readonly graphql.SelectionNode[] | undefined root: boolean - readonly: boolean + allowReadonly: boolean body: StatementKind[] visitedTypes: Set missingScalars: Set includeFragments: boolean - allOptional?: boolean + allOptional: boolean }): TSTypeKind { // start unwrapping non-nulls and lists (we'll wrap it back up before we return) const { type, wrappers } = unwrapType(config, rootType) @@ -166,7 +166,7 @@ export function inlineType({ rootType: field.type as graphql.GraphQLNamedType, selections: selection.selectionSet?.selections as graphql.SelectionNode[], root: false, - readonly, + allowReadonly, visitedTypes, body, missingScalars, @@ -180,7 +180,7 @@ export function inlineType({ AST.identifier(attributeName), AST.tsTypeAnnotation(attributeType) ), - readonly + allowReadonly ) if (allOptional) { @@ -214,7 +214,7 @@ export function inlineType({ ) ) ), - readonly + allowReadonly ) ) } @@ -236,7 +236,7 @@ export function inlineType({ filepath, rootType: fragmentRootType, selections: fragment, - readonly, + allowReadonly, visitedTypes, root, body, @@ -279,7 +279,7 @@ export function inlineType({ AST.identifier('__typename'), AST.tsTypeAnnotation(AST.tsLiteralType(AST.stringLiteral(typeName))) ), - readonly + allowReadonly ) ) } From 62ec76169bfd8d699697998202f4a07962568f4d Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 03:19:52 -0700 Subject: [PATCH 05/11] build errors --- src/cmd/generators/typescript/index.ts | 6 +++--- src/cmd/generators/typescript/inlineType.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cmd/generators/typescript/index.ts b/src/cmd/generators/typescript/index.ts index abca20ec18..89f62d4ce0 100644 --- a/src/cmd/generators/typescript/index.ts +++ b/src/cmd/generators/typescript/index.ts @@ -223,7 +223,7 @@ async function generateOperationTypeDefs( rootType: parentType, selections, root: true, - readonly: true, + allowReadonly: true, visitedTypes, body, missingScalars, @@ -288,7 +288,7 @@ async function generateOperationTypeDefs( rootType: parentType, selections, root: true, - readonly: true, + allowReadonly: true, visitedTypes, body, missingScalars, @@ -375,7 +375,7 @@ async function generateFragmentTypeDefs( rootType: type, selections, root: true, - readonly: true, + allowReadonly: true, body, visitedTypes, missingScalars, diff --git a/src/cmd/generators/typescript/inlineType.ts b/src/cmd/generators/typescript/inlineType.ts index e4fec4485e..593703b2c0 100644 --- a/src/cmd/generators/typescript/inlineType.ts +++ b/src/cmd/generators/typescript/inlineType.ts @@ -33,7 +33,7 @@ export function inlineType({ visitedTypes: Set missingScalars: Set includeFragments: boolean - allOptional: boolean + allOptional?: boolean }): TSTypeKind { // start unwrapping non-nulls and lists (we'll wrap it back up before we return) const { type, wrappers } = unwrapType(config, rootType) From 92241cb77a7081edb3a4344de47ed430f9f453e9 Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 18:35:36 -0700 Subject: [PATCH 06/11] typo --- src/vite/transforms/kit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vite/transforms/kit.ts b/src/vite/transforms/kit.ts index 6dc7f314b2..6062e8c43d 100644 --- a/src/vite/transforms/kit.ts +++ b/src/vite/transforms/kit.ts @@ -376,7 +376,7 @@ async function find_page_query(page: TransformPage): Promise ) as graphql.OperationDefinitionNode // if it doesn't exist, there is an error, but no discovered query either if (!definition) { - formatErrors({ message: 'page.gql must contain a query.', filpath: page_query_path }) + formatErrors({ message: 'page.gql must contain a query.', filepath: page_query_path }) return null } From a493f7dedefc06b6e658c42ec4e738fbcc1bbfae Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 20:22:15 -0700 Subject: [PATCH 07/11] document "incomplete" optimistic response types --- site/src/routes/guides/caching-data.svx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/site/src/routes/guides/caching-data.svx b/site/src/routes/guides/caching-data.svx index 1e3885ad71..afe0121358 100644 --- a/site/src/routes/guides/caching-data.svx +++ b/site/src/routes/guides/caching-data.svx @@ -61,6 +61,20 @@ When the mutation resolves, the old values will be erased entirely and the new v Remember to always request and specify an `id` when dealing with optimistic responses so that the cache can make sure to update the correct records. Also, it's worth mentioning that you don't have to provide a complete response for an optimistic value, the cache will write whatever information you give it (as long as its found in the mutation body). +### Why is typescript missing fields? + +If you are using typescript, you might notice that the generated types for optimistic +responses do not include any fields from fragments that you might have spread in. +While surprising at first, this is by design. We believe that it is a mistake to +tightly couple the invocation of the mutation with a fragment that's defined in +some random file and whose definition might change unknowingly. If it did change, +there would be a nasty error when the runtime tries to look up the schema information +so the generated types are trying to guide you towards a safer practice. + +There's no harm in duplicating a field that is part of a fragment so if you are going to +provide an optimistic value, you should add those fields to the explicit selection +set of the mutation. + ## Partial Data As your users navigate through your application, their cache will build up with the data that they encounter. This means that a lot of the times, they will have already seen the data that a new view wants. Houdini's cache can be told to render a view if only some of the necessary data is present using the `@cache` directive: From 7811be36631b9009c0efcce76813f98991b1b6d3 Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 20:24:38 -0700 Subject: [PATCH 08/11] added changeset --- .changeset/many-horses-provide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/many-horses-provide.md diff --git a/.changeset/many-horses-provide.md b/.changeset/many-horses-provide.md new file mode 100644 index 0000000000..dbdd9f4d11 --- /dev/null +++ b/.changeset/many-horses-provide.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +fix generated types for optimistic responses From 54e997e3e366a830dcec6f0c9893ab7ed3b428ff Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Fri, 26 Aug 2022 20:28:28 -0700 Subject: [PATCH 09/11] add missing type parameter --- src/runtime/lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/lib/types.ts b/src/runtime/lib/types.ts index 79db6f77e1..35c2f1ee60 100644 --- a/src/runtime/lib/types.ts +++ b/src/runtime/lib/types.ts @@ -96,7 +96,7 @@ export type BaseCompiledDocument = { export type GraphQLTagResult = | QueryStore | FragmentStore - | MutationStore + | MutationStore | SubscriptionStore export type HoudiniFetchContext = { From 70d778cb7562203979c6d6980d3ff0a8b316640a Mon Sep 17 00:00:00 2001 From: Alec Aivazis Date: Sat, 27 Aug 2022 12:28:36 -0700 Subject: [PATCH 10/11] update integration tests with new optimistic timing --- integration/api/graphql.mjs | 4 +++ integration/api/schema.graphql | 2 +- .../operations/MUTATION.UpdateUser.gql | 8 ++++- .../routes/stores/mutation-scalars/spec.ts | 2 +- .../stores/mutation-update/+page.svelte | 15 ++++++--- .../routes/stores/mutation-update/+page.ts | 9 ------ .../src/routes/stores/mutation-update/spec.ts | 20 ++++++------ .../src/routes/stores/mutation/+page.svelte | 31 +++++++++++++------ .../src/routes/stores/mutation/spec.ts | 26 +++------------- src/vite/index.ts | 2 +- 10 files changed, 59 insertions(+), 60 deletions(-) delete mode 100644 integration/src/routes/stores/mutation-update/+page.ts diff --git a/integration/api/graphql.mjs b/integration/api/graphql.mjs index 04f59fa818..774bff3b19 100644 --- a/integration/api/graphql.mjs +++ b/integration/api/graphql.mjs @@ -116,6 +116,10 @@ export const resolvers = { return user; }, updateUser: async (_, args) => { + if (args.delay) { + await sleep(args.delay); + } + const list = getSnapshot(args.snapshot); const userIndex = list.findIndex((c) => c.id === `${args.snapshot}:${args.id}`); if (userIndex === -1) { diff --git a/integration/api/schema.graphql b/integration/api/schema.graphql index 772629e48b..bbf1347298 100644 --- a/integration/api/schema.graphql +++ b/integration/api/schema.graphql @@ -22,7 +22,7 @@ type Mutation { enumValue: MyEnum types: [TypeOfUser!] ): User! - updateUser(id: ID!, name: String, snapshot: String!, birthDate: DateTime): User! + updateUser(id: ID!, name: String, snapshot: String!, birthDate: DateTime, delay: Int): User! } interface Node { diff --git a/integration/src/lib/graphql/operations/MUTATION.UpdateUser.gql b/integration/src/lib/graphql/operations/MUTATION.UpdateUser.gql index f3db7bcae6..f558c5786a 100644 --- a/integration/src/lib/graphql/operations/MUTATION.UpdateUser.gql +++ b/integration/src/lib/graphql/operations/MUTATION.UpdateUser.gql @@ -1,5 +1,11 @@ mutation UpdateUser($id: ID!, $name: String, $birthDate: DateTime) { - updateUser(id: $id, name: $name, birthDate: $birthDate, snapshot: "store-user-query") { + updateUser( + id: $id + name: $name + birthDate: $birthDate + snapshot: "update-user-mutation" + delay: 1000 + ) { id name birthDate diff --git a/integration/src/routes/stores/mutation-scalars/spec.ts b/integration/src/routes/stores/mutation-scalars/spec.ts index b3c13303ec..65e35135db 100644 --- a/integration/src/routes/stores/mutation-scalars/spec.ts +++ b/integration/src/routes/stores/mutation-scalars/spec.ts @@ -12,7 +12,7 @@ test.describe('mutation store', function () { // make sure that the result updated with unmarshaled data await expectToBe( page, - '{"updateUser":{"id":"store-user-query:6","name":"Harrison Ford","birthDate":"1986-11-07T00:00:00.000Z"}}' + '{"updateUser":{"id":"update-user-mutation:6","name":"Harrison Ford","birthDate":"1986-11-07T00:00:00.000Z"}}' ); }); }); diff --git a/integration/src/routes/stores/mutation-update/+page.svelte b/integration/src/routes/stores/mutation-update/+page.svelte index 3b0b5a9eab..97e82d3dc8 100644 --- a/integration/src/routes/stores/mutation-update/+page.svelte +++ b/integration/src/routes/stores/mutation-update/+page.svelte @@ -1,11 +1,16 @@ - - -``` +When the mutation resolves, the old values will be erased entirely and the new values will +be committed to the cache. If instead the mutation fails, the optimistic changes will be +reverted and the handler's promise will reject with the error message as usual. + +Remember to always request and specify an `id` when dealing with optimistic responses so +that the cache can make sure to update the correct records. Also, it's worth mentioning that +you don't have to provide a complete response for an optimistic value, the cache will write +whatever information you give it (as long as its found in the mutation body). Because of this, +the store value won't update until the mutation resolves. + +### Why is typescript missing fields? + +If you are using typescript, you might notice that the generated types for optimistic +responses do not include any fields from fragments that you might have spread in. +While surprising at first, this is by design. We believe that it is a mistake to +tightly couple the invocation of the mutation with a fragment that's defined in +some random file and whose definition might change unknowingly. If it did change, +there would be a nasty error when the runtime tries to look up the schema information +so the generated types are trying to guide you towards a safer practice. + +There's no harm in duplicating a field that is part of a fragment so if you are going to +provide an optimistic value, you should add those fields to the explicit selection +set of the mutation.