Custom scalars#13270
Conversation
…ApolloCache` (#13252) _Part of the custom scalars work: https://github.com/apollographql/apollo-client/issues/13227_ Adds the plumbing needed for types in `ApolloCache` as well as `InMemoryCache` to handle custom scalars. Implements the `getScalar` method that will be used by this feature to convert a raw value to a scalar. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> * **New Features** * Added custom GraphQL scalar support to InMemoryCache with type-safe configuration for parse, serialize, type-guard, and devtools integration. * Added `getScalar()` method to retrieve configured scalar instances by name. * **Documentation** * Added guides for declaring and configuring custom scalars with usage examples. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
🦋 Changeset detectedLatest commit: 12960d8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Docs preview readyThe preview is ready to be viewed. View the preview File Changes 0 new, 1 changed, 0 removedBuild ID: 523ecfd063c949ea6b9d9dcd URL: https://www.apollographql.com/docs/deploy-preview/523ecfd063c949ea6b9d9dcd ✅ AI Style Review — No Changes DetectedNo MDX files were changed in this pull request. Review Log: View detailed log
|
📝 WalkthroughWalkthroughThis PR adds first-class custom scalar support to Apollo Client. It introduces a generic ChangesCustom GraphQL Scalar Support
Sequence Diagram(s)sequenceDiagram
participant App
participant ApolloClient
participant QueryManager
participant InMemoryCache
participant serializeVariables
participant EntityStore
participant NetworkLink
App->>ApolloClient: query/mutate/subscribe(options with Date variables)
ApolloClient->>QueryManager: execute operation
QueryManager->>InMemoryCache: serializeVariables(document, variables)
InMemoryCache->>serializeVariables: recurse scalars/arrays/inputObjects
serializeVariables-->>QueryManager: {date: "2026-01-01"} (serialized)
QueryManager->>NetworkLink: forward operation (serialized vars)
NetworkLink-->>QueryManager: {startDate: "2026-01-01"} response
QueryManager->>InMemoryCache: write(response)
InMemoryCache->>EntityStore: merge → coerceToParsed (string → Date)
EntityStore-->>InMemoryCache: stored as Date
InMemoryCache->>EntityStore: diff/read → getScalarForField
EntityStore-->>App: {startDate: Date(2026,0,1)} parsed result
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
config/jest.config.tsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. integration-tests/type-tests/customScalars/all-any/differentTypes/index.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. integration-tests/type-tests/customScalars/all-any/empty/index.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…13259) _Part of the custom scalars work: https://github.com/apollographql/apollo-client/issues/13227_ Adds scalar value serialization for scalar fields during cache writes. This allows users to provide either the parsed or serialized value to the cache and have it work as expected.
ccaf686 to
6031987
Compare
Fixes the build issue on the custom scalars feature branch.
commit: |
Adds a new `Scalar.fromGraphQLScalarType` helper that is used to create a `Scalar` instance from an existing `GraphQLScalarType` instance from the `graphql` package. This makes it easier to use scalar definitions from packages such as [`graphql-scalars`](https://the-guild.dev/graphql/scalars)
_Part of the custom scalars work: https://github.com/apollographql/apollo-client/issues/13227_ Adds automatic variable serialization for custom scalars. > [!NOTE] > This PR does not support the fragment APIs (e.g. `writeFragment`, `readFragment`, `watchFragment`. Fragment definitions don't have variable definitions and thus cannot be serialized. A followup PR will be opened to address this in some form. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Automatic serialization of custom scalar values in GraphQL operation variables across queries, mutations, subscriptions, and cache operations * New `inputObjects` configuration option for InMemoryCache to specify scalar field locations within complex input object types <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…l fetch policies (#13275) Adds tests for all APIs to test parsing scalars for all fetch policies. This revealed an issue with mutations and subscriptions where the parsed scalar value wasn't used correctly. This has been fixed in this PR.
|
This is in a good enough place that we can start getting some alpha testing. I'm going to go ahead and get this in the 4.3 branch so we can get an alpha out and start getting feedback. |
78fbc75 to
12960d8
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (2)
src/cache/inmemory/entityStore.ts (1)
404-416: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winShort-circuit
extractwhen no scalars are configured.When
config.scalarsis unset,coerceStoreObjectreturns eachstoreObjectunchanged, but this still rebuilds the entire entry map (new top-level object + per-entry tuples) on everyextract()call — a needless O(n) allocation on a path that can be hot during SSR. Consider falling back to the previoustoObject()form when scalar coercion isn't enabled.♻️ Proposed guard
- const obj = Object.fromEntries( - Object.entries(this.toObject()).map(([dataId, storeObject]) => [ - dataId, - storeObject && - this.coerceStoreObject( - storeObject, - (scalar, value) => scalar.coerceToSerialized(value), - storeObject?.__typename || this.policies.rootTypenamesById[dataId] - ), - ]) - ); + const obj = this.policies.cache["config"].scalars ? + Object.fromEntries( + Object.entries(this.toObject()).map(([dataId, storeObject]) => [ + dataId, + storeObject && + this.coerceStoreObject( + storeObject, + (scalar, value) => scalar.coerceToSerialized(value), + storeObject?.__typename || + this.policies.rootTypenamesById[dataId] + ), + ]) + ) + : this.toObject();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/cache/inmemory/entityStore.ts` around lines 404 - 416, The `extract()` method in `entityStore.ts` always rebuilds a new entry map even when `config.scalars` is unset and `coerceStoreObject` would be a no-op. Update `extract()` to short-circuit to the plain `toObject()` result when scalar coercion is not configured, and only run the `Object.entries(...).map(...)` path when scalars are available. Use the `extract()` and `coerceStoreObject()` paths to locate the change.src/cache/inmemory/readFromStore.ts (1)
364-382: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReuse the already-computed
typenameinstead of re-reading it.The enclosing
execSelectionSetImplalready derivestypenamefrom the sameobjectOrReference(line 294). The new block re-invokescontext.store.getFieldValue(..., "__typename")and shadows the outertypename, which is redundant.♻️ Proposed simplification
if (__DEV__) { - const typename = context.store.getFieldValue<string>( - objectOrReference, - "__typename" - ); const fieldName = selection.name.value; if (typename) { const policy = policies["getFieldPolicy"](typename, fieldName);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/cache/inmemory/readFromStore.ts` around lines 364 - 382, The new __DEV__ warning block in readFromStore should reuse the already-derived typename from execSelectionSetImpl instead of calling context.store.getFieldValue(..., "__typename") again and shadowing it. Update the selection handling in execSelectionSetImpl to use the existing typename variable when looking up the policy and emitting the warning, keeping the logic in the same block but removing the redundant read.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@integration-tests/type-tests/customScalars/all-structured/differentTypes/index.ts`:
- Around line 5-10: The ApolloCache.Scalars augmentation is too strict because
the Record<string, { serialized: string; parsed: string }> index signature
forces every scalar, including DateTime, to use parsed: string and prevents the
intended cache-type mismatch from being tested. Update the custom scalar
declaration around ApolloCache.Scalars so the index value type is widened enough
for DateTime to fit, or move the conflicting DateTime shape into the
InMemoryCache assertion instead, keeping the mismatch where the test is meant to
validate it.
In `@src/cache/core/Scalar.ts`:
- Around line 51-56: The fallback implementation of Scalar.is is guessing the
runtime shape and breaks scalar pairs with overlapping or primitive
serialized/parsed types. Update Scalar’s runtime discrimination in the Scalar
class so it does not rely on typeof value === "object" when no explicit
predicate is provided; instead, require Scalar.Options.is for ambiguous cases or
make the default behavior conservative enough that coerceToParsed and
coerceToSerialized don’t misclassify values. Use the existing Scalar.is,
coerceToParsed, and coerceToSerialized logic to ensure already-parsed values and
serialized values are handled correctly without silent runtime swapping.
In `@src/core/__tests__/client.watchQuery/customScalars.test.ts`:
- Around line 859-922: The no-cache watchQuery path is still bypassing custom
scalar parsing, so the result stays serialized instead of being converted
through the scalar runtime. Update the watchQuery/no-cache handling in
ApolloClient and the related result processing so fields configured via
InMemoryCache typePolicies and scalars are parsed consistently before emitting
values, using the existing custom scalar plumbing around watchQuery and
ObservableStream. After the fix, this customScalars test should no longer need
test.failing and should pass with a Date value for startDate.
In `@src/react/hooks/__tests__/useMutation/customScalars.test.tsx`:
- Around line 391-447: The no-cache path in useMutation still bypasses custom
scalar coercion, so mutation results differ from the cached/network-only flows.
Update the mutation result handling in useMutation (and any helper it uses for
result normalization) so scalar parsing still runs when fetchPolicy is
"no-cache", using the same scalar/type policy logic exercised by the custom
scalar tests and the Date field mapping on Event.startDate. Ensure the mutate()
result shape stays consistent with the existing parsed behavior.
In `@src/react/hooks/__tests__/useQuery/customScalars.test.tsx`:
- Around line 633-698: The no-cache query result path is still skipping custom
scalar parsing, so `useQuery` returns wire-format values instead of runtime
scalar values. Update the underlying `QueryManager`/`QueryInfo` no-cache flow so
it applies the same scalar coercion used by other fetch policies, and verify the
fix against `useQuery`, `ApolloClient`, and the `Date` scalar setup in
`customScalars.test.tsx`. Avoid keeping this as `test.failing`; the expected
behavior should be that `fetchPolicy: "no-cache"` returns parsed scalar values
too.
---
Nitpick comments:
In `@src/cache/inmemory/entityStore.ts`:
- Around line 404-416: The `extract()` method in `entityStore.ts` always
rebuilds a new entry map even when `config.scalars` is unset and
`coerceStoreObject` would be a no-op. Update `extract()` to short-circuit to the
plain `toObject()` result when scalar coercion is not configured, and only run
the `Object.entries(...).map(...)` path when scalars are available. Use the
`extract()` and `coerceStoreObject()` paths to locate the change.
In `@src/cache/inmemory/readFromStore.ts`:
- Around line 364-382: The new __DEV__ warning block in readFromStore should
reuse the already-derived typename from execSelectionSetImpl instead of calling
context.store.getFieldValue(..., "__typename") again and shadowing it. Update
the selection handling in execSelectionSetImpl to use the existing typename
variable when looking up the policy and emitting the warning, keeping the logic
in the same block but removing the redundant read.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: a80a52ab-f084-4059-b5c8-6697434ebc89
⛔ Files ignored due to path filters (1)
src/__tests__/__snapshots__/exports.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (90)
.api-reports/api-report-cache.api.md.api-reports/api-report-core.api.md.api-reports/api-report-utilities_internal.api.md.api-reports/api-report.api.md.changeset/cold-comics-add.md.changeset/eighty-files-sort.md.changeset/strong-shoes-sell.md.changeset/thin-tips-fold.md.changeset/wet-experts-end.md.circleci/config.ymlconfig/jest.config.tsintegration-tests/type-tests/customScalars/all-any/differentTypes/index.tsintegration-tests/type-tests/customScalars/all-any/differentTypes/tsconfig.jsonintegration-tests/type-tests/customScalars/all-any/empty/index.tsintegration-tests/type-tests/customScalars/all-any/empty/tsconfig.jsonintegration-tests/type-tests/customScalars/all-any/matchingTypes/index.tsintegration-tests/type-tests/customScalars/all-any/matchingTypes/tsconfig.jsonintegration-tests/type-tests/customScalars/all-any/mixed/index.tsintegration-tests/type-tests/customScalars/all-any/mixed/tsconfig.jsonintegration-tests/type-tests/customScalars/all-structured/differentTypes/index.tsintegration-tests/type-tests/customScalars/all-structured/differentTypes/tsconfig.jsonintegration-tests/type-tests/customScalars/all-structured/empty/index.tsintegration-tests/type-tests/customScalars/all-structured/empty/tsconfig.jsonintegration-tests/type-tests/customScalars/all-structured/matchingTypes/index.tsintegration-tests/type-tests/customScalars/all-structured/matchingTypes/tsconfig.jsonintegration-tests/type-tests/customScalars/all-structured/mixed/index.tsintegration-tests/type-tests/customScalars/all-structured/mixed/tsconfig.jsonintegration-tests/type-tests/customScalars/all-unknown/differentTypes/index.tsintegration-tests/type-tests/customScalars/all-unknown/differentTypes/tsconfig.jsonintegration-tests/type-tests/customScalars/all-unknown/empty/index.tsintegration-tests/type-tests/customScalars/all-unknown/empty/tsconfig.jsonintegration-tests/type-tests/customScalars/all-unknown/matchingTypes/index.tsintegration-tests/type-tests/customScalars/all-unknown/matchingTypes/tsconfig.jsonintegration-tests/type-tests/customScalars/all-unknown/mixed/index.tsintegration-tests/type-tests/customScalars/all-unknown/mixed/tsconfig.jsonintegration-tests/type-tests/customScalars/differentTypes/index.tsintegration-tests/type-tests/customScalars/differentTypes/tsconfig.jsonintegration-tests/type-tests/customScalars/empty/index.tsintegration-tests/type-tests/customScalars/empty/tsconfig.jsonintegration-tests/type-tests/customScalars/matchingTypes/index.tsintegration-tests/type-tests/customScalars/matchingTypes/tsconfig.jsonintegration-tests/type-tests/customScalars/mixed/index.tsintegration-tests/type-tests/customScalars/mixed/tsconfig.jsonintegration-tests/type-tests/tsconfig.jsonpackage.jsonsrc/cache/core/Scalar.tssrc/cache/core/__tests__/cache.tssrc/cache/core/cache.tssrc/cache/index.tssrc/cache/inmemory/__tests__/cache.diff/customScalars.test.tssrc/cache/inmemory/__tests__/cache.readFragment/customScalars.test.tssrc/cache/inmemory/__tests__/cache.readQuery/customScalars.test.tssrc/cache/inmemory/__tests__/cache.writeQuery/customScalars.test.tssrc/cache/inmemory/__tests__/scalars.tssrc/cache/inmemory/__tests__/variablesSerialization.tssrc/cache/inmemory/entityStore.tssrc/cache/inmemory/inMemoryCache.tssrc/cache/inmemory/policies.tssrc/cache/inmemory/readFromStore.tssrc/cache/inmemory/types.tssrc/core/QueryInfo.tssrc/core/QueryManager.tssrc/core/__tests__/client.mutate/customScalars.test.tssrc/core/__tests__/client.query/customScalars.test.tssrc/core/__tests__/client.readFragment/customScalars.test.tssrc/core/__tests__/client.readQuery/customScalars.test.tssrc/core/__tests__/client.refetchQueries/customScalars.test.tssrc/core/__tests__/client.subscribe/customScalars.test.tssrc/core/__tests__/client.watchFragment/customScalars.test.tssrc/core/__tests__/client.watchQuery/customScalars.test.tssrc/core/__tests__/client.writeQuery/customScalars.test.tssrc/core/index.tssrc/declarations.d.tssrc/react/hooks/__tests__/useBackgroundQuery/customScalars.test.tsxsrc/react/hooks/__tests__/useFragment/customScalars.test.tsxsrc/react/hooks/__tests__/useLazyQuery/customScalars.test.tsxsrc/react/hooks/__tests__/useLoadableQuery/customScalars.test.tsxsrc/react/hooks/__tests__/useMutation/customScalars.test.tsxsrc/react/hooks/__tests__/useQuery/customScalars.test.tsxsrc/react/hooks/__tests__/useQueryRefHandlers/customScalars.test.tsxsrc/react/hooks/__tests__/useSubscription/customScalars.test.tsxsrc/react/hooks/__tests__/useSuspenseFragment/customScalars.test.tsxsrc/react/hooks/__tests__/useSuspenseQuery/customScalars.test.tsxsrc/react/query-preloader/__tests__/createQueryPreloader/customScalars.test.tsxsrc/react/query-preloader/__tests__/createQueryPreloader/testUtils.tsxsrc/testing/internal/index.tssrc/testing/internal/scalars.tssrc/utilities/internal/getUnwrappedType.tssrc/utilities/internal/index.tssrc/utilities/internal/types/IsLooselyEqual.ts
| test.failing( | ||
| "parses custom scalar fields with a no-cache fetch policy", | ||
| async () => { | ||
| const query = gql` | ||
| query Event { | ||
| event { | ||
| id | ||
| startDate | ||
| } | ||
| } | ||
| `; | ||
| const client = new ApolloClient({ | ||
| cache: new InMemoryCache({ | ||
| scalars: { Date: dateScalar }, | ||
| typePolicies: { | ||
| Event: { | ||
| fields: { | ||
| startDate: { scalar: "Date" }, | ||
| }, | ||
| }, | ||
| }, | ||
| }), | ||
| link: new ApolloLink(() => | ||
| of({ | ||
| data: { | ||
| event: { | ||
| __typename: "Event", | ||
| id: "1", | ||
| startDate: "2026-01-01", | ||
| }, | ||
| }, | ||
| }).pipe(delay(20)) | ||
| ), | ||
| }); | ||
|
|
||
| using stream = new ObservableStream( | ||
| client.watchQuery({ | ||
| query, | ||
| fetchPolicy: "no-cache", | ||
| }) | ||
| ); | ||
|
|
||
| await expect(stream).toEmitTypedValue({ | ||
| data: undefined, | ||
| dataState: "empty", | ||
| loading: true, | ||
| networkStatus: NetworkStatus.loading, | ||
| partial: true, | ||
| }); | ||
| await expect(stream).toEmitTypedValue({ | ||
| data: { | ||
| event: { | ||
| __typename: "Event", | ||
| id: "1", | ||
| startDate: new Date(2026, 0, 1), | ||
| }, | ||
| }, | ||
| dataState: "complete", | ||
| loading: false, | ||
| networkStatus: NetworkStatus.ready, | ||
| partial: false, | ||
| }); | ||
| } | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
no-cache still bypasses scalar parsing.
Keeping this as test.failing means watchQuery({ fetchPolicy: "no-cache" }) still produces serialized scalar payloads instead of parsed values. That leaves one fetch policy outside the new custom-scalar runtime contract and will surface inconsistent result types to callers.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/core/__tests__/client.watchQuery/customScalars.test.ts` around lines 859
- 922, The no-cache watchQuery path is still bypassing custom scalar parsing, so
the result stays serialized instead of being converted through the scalar
runtime. Update the watchQuery/no-cache handling in ApolloClient and the related
result processing so fields configured via InMemoryCache typePolicies and
scalars are parsed consistently before emitting values, using the existing
custom scalar plumbing around watchQuery and ObservableStream. After the fix,
this customScalars test should no longer need test.failing and should pass with
a Date value for startDate.
There was a problem hiding this comment.
Correct, this is intentional and will be addressed in a future PR.
There was a problem hiding this comment.
@jerelmiller Got it, thanks for confirming. Sounds good — I'll leave this as-is for now, given it's tracked for a future PR.
(=^・ω・^=)
🧠 Learnings used
Learnt from: phryneas
Repo: apollographql/apollo-client PR: 13132
File: src/core/defaultOptions.ts:5-7
Timestamp: 2026-03-09T13:28:57.460Z
Learning: In the apollographql/apollo-client repository, do not flag lint issues related to Biome for TypeScript files. There is no biome.json/biome.jsonc and no Biome dependency in package.json. Do not suggest biome-ignore comments or Biome-specific lint fixes for any TS file.
| test.failing( | ||
| "parses custom scalar fields with a no-cache fetch policy", | ||
| async () => { | ||
| const mutation = gql` | ||
| mutation CreateEvent { | ||
| createEvent { | ||
| id | ||
| startDate | ||
| } | ||
| } | ||
| `; | ||
| const client = new ApolloClient({ | ||
| cache: new InMemoryCache({ | ||
| scalars: { Date: dateScalar }, | ||
| typePolicies: { | ||
| Event: { | ||
| fields: { | ||
| startDate: { scalar: "Date" }, | ||
| }, | ||
| }, | ||
| }, | ||
| }), | ||
| link: new ApolloLink(() => | ||
| of({ | ||
| data: { | ||
| createEvent: { | ||
| __typename: "Event", | ||
| id: "1", | ||
| startDate: "2026-01-01", | ||
| }, | ||
| }, | ||
| }).pipe(delay(20)) | ||
| ), | ||
| }); | ||
|
|
||
| using _disabledAct = disableActEnvironment(); | ||
| const { takeSnapshot, getCurrentSnapshot } = | ||
| await renderHookToSnapshotStream(() => useMutation(mutation), { | ||
| wrapper: createClientWrapper(client), | ||
| }); | ||
|
|
||
| await takeSnapshot(); | ||
| const [mutate] = getCurrentSnapshot(); | ||
|
|
||
| await expect( | ||
| mutate({ fetchPolicy: "no-cache" }) | ||
| ).resolves.toStrictEqualTyped({ | ||
| data: { | ||
| createEvent: { | ||
| __typename: "Event", | ||
| id: "1", | ||
| startDate: new Date(2026, 0, 1), | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
mutate({ fetchPolicy: "no-cache" }) still skips scalar coercion.
This failing test shows mutation results returned without caching still bypass custom-scalar parsing, so the no-cache path exposes a different runtime shape than the cached/network-only paths.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/react/hooks/__tests__/useMutation/customScalars.test.tsx` around lines
391 - 447, The no-cache path in useMutation still bypasses custom scalar
coercion, so mutation results differ from the cached/network-only flows. Update
the mutation result handling in useMutation (and any helper it uses for result
normalization) so scalar parsing still runs when fetchPolicy is "no-cache",
using the same scalar/type policy logic exercised by the custom scalar tests and
the Date field mapping on Event.startDate. Ensure the mutate() result shape
stays consistent with the existing parsed behavior.
| test.failing( | ||
| "parses custom scalar fields with a no-cache fetch policy", | ||
| async () => { | ||
| const query = gql` | ||
| query Event { | ||
| event { | ||
| id | ||
| startDate | ||
| } | ||
| } | ||
| `; | ||
| const client = new ApolloClient({ | ||
| cache: new InMemoryCache({ | ||
| scalars: { Date: dateScalar }, | ||
| typePolicies: { | ||
| Event: { | ||
| fields: { | ||
| startDate: { scalar: "Date" }, | ||
| }, | ||
| }, | ||
| }, | ||
| }), | ||
| link: new ApolloLink(() => | ||
| of({ | ||
| data: { | ||
| event: { | ||
| __typename: "Event", | ||
| id: "1", | ||
| startDate: "2026-01-01", | ||
| }, | ||
| }, | ||
| }).pipe(delay(20)) | ||
| ), | ||
| }); | ||
|
|
||
| using _disabledAct = disableActEnvironment(); | ||
| const { takeSnapshot } = await renderHookToSnapshotStream( | ||
| () => useQuery(query, { fetchPolicy: "no-cache" }), | ||
| { wrapper: createClientWrapper(client) } | ||
| ); | ||
|
|
||
| await expect(takeSnapshot()).resolves.toStrictEqualTyped({ | ||
| data: undefined, | ||
| dataState: "empty", | ||
| loading: true, | ||
| networkStatus: NetworkStatus.loading, | ||
| previousData: undefined, | ||
| variables: {}, | ||
| }); | ||
|
|
||
| await expect(takeSnapshot()).resolves.toStrictEqualTyped({ | ||
| data: { | ||
| event: { | ||
| __typename: "Event", | ||
| id: "1", | ||
| startDate: new Date(2026, 0, 1), | ||
| }, | ||
| }, | ||
| dataState: "complete", | ||
| loading: false, | ||
| networkStatus: NetworkStatus.ready, | ||
| previousData: undefined, | ||
| variables: {}, | ||
| }); | ||
| } | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
no-cache still bypasses custom scalar parsing.
This test codifies an unresolved behavior gap: useQuery(..., { fetchPolicy: "no-cache" }) still returns wire-format scalar values while the other fetch policies return parsed runtime values. That policy-dependent type drift undercuts the PR’s “first-class custom scalar support” contract and will leak straight to callers using Date-like scalars. Please fix the no-cache result path in the underlying QueryManager/QueryInfo pipeline instead of merging with an expected failure here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/react/hooks/__tests__/useQuery/customScalars.test.tsx` around lines 633 -
698, The no-cache query result path is still skipping custom scalar parsing, so
`useQuery` returns wire-format values instead of runtime scalar values. Update
the underlying `QueryManager`/`QueryInfo` no-cache flow so it applies the same
scalar coercion used by other fetch policies, and verify the fix against
`useQuery`, `ApolloClient`, and the `Date` scalar setup in
`customScalars.test.tsx`. Avoid keeping this as `test.failing`; the expected
behavior should be that `fetchPolicy: "no-cache"` returns parsed scalar values
too.
Small improvements mentioned in #13270 (review). - Keeps `cache.extract()` cheap when no scalars are configured - Reuses an existing variable to remove unneeded duplicate code <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved cache extraction so scalar values are only normalized when scalar handling is enabled. * Preserved stored values unchanged when scalar handling is disabled. * Refined store field checks to avoid redundant lookups during validation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Feature branch for custom scalars.
Summary by CodeRabbit