[Custom scalars] Handle scalar parsing during cache reads and writes - #13259
Conversation
acd4729 to
31b8365
Compare
31b8365 to
9ba29b6
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends the InMemoryCache custom-scalars work by allowing scalar fields (configured via a new scalar field policy option) to be coerced during cache writes/merges and during cache.extract()/cache.restore(), so users can provide either parsed or serialized values and get consistent behavior.
Changes:
- Adds
scalar?: ScalarNamesto field policies and plumbing to resolve a scalar per(typename, fieldName). - Coerces scalar values to parsed form during writes/merges, and serializes them during cache extraction (with complementary parsing during restore via the merge path).
- Adds extensive runtime tests and TypeScript type-tests validating scalar policy typing and scalar coercion behavior.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/cache/inmemory/writeToStore.ts | Coerces scalar field values during write paths (including post-merge-function). |
| src/cache/inmemory/readFromStore.ts | Adds a DEV warning when scalar is configured on a field that actually has a selection set. |
| src/cache/inmemory/policies.ts | Adds scalar to FieldPolicy plus scalar lookup/coercion helpers (getScalarForField, maybeCoerce*). |
| src/cache/inmemory/entityStore.ts | Parses scalar values before merging and serializes scalar values during extract(). |
| src/cache/inmemory/types.ts | Introduces KnownScalars/ScalarNames typing helpers used by the new FieldPolicy.scalar option. |
| src/cache/inmemory/inMemoryCache.ts | Refactors scalar-related types to reuse KnownScalars from types.ts. |
| src/cache/inmemory/tests/scalars.ts | Adds comprehensive behavioral tests for scalar parsing/serialization across cache APIs. |
| integration-tests/type-tests/customScalars/mixed/index.ts | Adds type-test coverage restricting FieldPolicy.scalar to known scalar names in this scenario. |
| integration-tests/type-tests/customScalars/matchingTypes/index.ts | Adds type-test coverage for allowed/disallowed scalar names in field policies. |
| integration-tests/type-tests/customScalars/empty/index.ts | Adds type-test coverage ensuring no scalar names are allowed when none are declared. |
| integration-tests/type-tests/customScalars/differentTypes/index.ts | Adds type-test coverage restricting scalar names when only some scalars are declared. |
| integration-tests/type-tests/customScalars/all-unknown/mixed/index.ts | Adds type-test coverage allowing any scalar name when scalar typing is fully unknown. |
| integration-tests/type-tests/customScalars/all-unknown/matchingTypes/index.ts | Adds type-test coverage allowing any scalar name when scalar typing is fully unknown. |
| integration-tests/type-tests/customScalars/all-unknown/empty/index.ts | Adds type-test coverage allowing any scalar name when scalar typing is fully unknown. |
| integration-tests/type-tests/customScalars/all-unknown/differentTypes/index.ts | Adds type-test coverage allowing any scalar name when scalar typing is fully unknown. |
| integration-tests/type-tests/customScalars/all-structured/mixed/index.ts | Adds type-test coverage allowing any scalar name in the “all-structured” scenario. |
| integration-tests/type-tests/customScalars/all-structured/matchingTypes/index.ts | Adds type-test coverage allowing any scalar name in the “all-structured” scenario. |
| integration-tests/type-tests/customScalars/all-structured/empty/index.ts | Adds type-test coverage allowing any scalar name in the “all-structured” scenario. |
| integration-tests/type-tests/customScalars/all-structured/differentTypes/index.ts | Adds type-test coverage allowing any scalar name in the “all-structured” scenario. |
| integration-tests/type-tests/customScalars/all-any/mixed/index.ts | Adds type-test coverage allowing any scalar name when scalar typing is any. |
| integration-tests/type-tests/customScalars/all-any/matchingTypes/index.ts | Adds type-test coverage allowing any scalar name when scalar typing is any. |
| integration-tests/type-tests/customScalars/all-any/empty/index.ts | Fixes imports and adds type-test coverage allowing any scalar name when scalar typing is any. |
| integration-tests/type-tests/customScalars/all-any/differentTypes/index.ts | Adds type-test coverage allowing any scalar name when scalar typing is any. |
| .changeset/strong-shoes-sell.md | Documents the new FieldPolicy.scalar option and its effect on reads/writes/extract/restore. |
| .api-reports/api-report.api.md | Updates API report for new public types/methods introduced by scalar coercion support. |
| .api-reports/api-report-cache.api.md | Updates cache API report for the new FieldPolicy.scalar option and Policies APIs. |
| public maybeCoerceToScalarValue( | ||
| value: StoreValue, | ||
| options: CoerceValueOptions | ||
| ): StoreValue { |
There was a problem hiding this comment.
Turns out I don't need these anymore now that all of the logic lives in EntityStore. Removed in 03945ef
adcaf51 to
260a649
Compare
…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.
| coerce: (scalar: Scalar<any, any>, value: unknown) => unknown, | ||
| typename = obj.__typename | ||
| ): StoreObject { | ||
| if (!typename) { |
There was a problem hiding this comment.
@jerelmiller could we add an early bailout for a "no scalars defined" case?
There was a problem hiding this comment.
I caught this after I merged this PR and put it in #13273: https://github.com/apollographql/apollo-client/pull/13273/changes#diff-246cd7915328b250bf54f58d58c38b3101c8c65be631f0cf93318d79405432c4 🙂
| this.policies.rootTypenamesById[dataId] | ||
| ); | ||
|
|
||
| const merged: StoreObject = new DeepMerger({ |
There was a problem hiding this comment.
Does DeepMerger need new safeguards so it doesn't smush together the implementation details of user-defined distinct scalar values?
There was a problem hiding this comment.
I don't think so? We would have had this problem already I would think since you still have the possibility of e.g. object scalars (like JSON), and that seems to be working fine 🤔
There was a problem hiding this comment.
I would assume that this has been wonky all the time until now, and nobody ever noticed/reported it.
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to release-4.3, this PR will be updated.⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ `release-4.3` is currently in **pre mode** so this branch has prereleases rather than normal releases. If you want to exit prereleases, run `changeset pre exit` on `release-4.3`.⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ # Releases ## @apollo/client@4.3.0-alpha.2 ### Minor Changes - [#13274](#13274) [`7b10078`](7b10078) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds `Scalar.fromGraphQLScalarType` helper to create a `Scalar` instance from an existing graphql.js `GraphQLScalarType`. ```ts import { GraphQLScalarType } from "graphql"; import { Scalar } from "@apollo/client"; const dateTimeScalarType = new GraphQLScalarType<Date, string>({ // ... }); const dateTimeScalar = Scalar.fromGraphQLScalarType(dateTimeScalarType, { is: (value) => value instanceof Date, }); ``` - [#13252](#13252) [`ed86234`](ed86234) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds the plumbing and types implementation for declaring custom scalars and configuring custom scalars in `InMemoryCache`. You can declare custom scalar types with declaration merging on the `ApolloCache.Scalars` interface: ```ts // apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloCache { interface Scalars { Date: { serialized: string; parsed: Date }; } } } ``` This enables the `scalars` option in `InMemoryCache`: ```ts import { Scalar } from "@apollo/client"; const cache = new InMemoryCache({ scalars: { Date: new Scalar({ parse: (dateString) => new Date(dateString), serialize: (date) => date.toISOString(), is: (value) => value instanceof Date, }), }, }); ``` - [#13259](#13259) [`ccaf686`](ccaf686) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds a `scalar` option to `InMemoryCache` field policies that tells the cache which scalar to use when parsing or serializing the field value. ```ts import { Scalar } from "@apollo/client"; new InMemoryCache({ scalars: { DateTime: new Scalar({ parse: (dateString) => new Date(dateString), serialize: (date) => date.toISOString(), }), }, typePolicies: { Event: { fields: { startTime: { // Parse this field using the DateTime scalar scalar: "DateTime", }, }, }, }, }); ``` This scalar definition is now used to properly parse or serialize the field value for cache reads and writes as well as `cache.extract()` and `cache.restore()`. - [#13273](#13273) [`0886de1`](0886de1) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Automatically serialize variables that include custom scalar values. This includes cache reads and writes as well as requests to the network. For more complex input objects, a new `inputObjects` option is available to `InMemoryCache` that specifies where nested scalar fields are found. ```ts const cache = new InMemoryCache({ scalars: { DateTime: new Scalar({ parse: (value) => new Date(value), serialize: (value) => value.toISOString(), is: (value) => value instanceof Date, }), }, inputObjects: { EventFilter: { fields: { date: "DateTime", }, }, }, }); const client = new ApolloClient({ cache, link }); await client.query({ query: gql` query Event($filter: EventFilter!) { event(filter: $filter) { name } } `, variables: { filter: { date: new Date("2026-01-01T00:00:00.000Z"), }, }, }); // The link receives: // { filter: { date: "2026-01-01T00:00:00.000Z" } } ``` - [#13252](#13252) [`ed86234`](ed86234) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds the `getScalar` abstract method to `ApolloCache` that cache subclasses override to provide scalar behavior to Apollo Client. Defaults to unconditionally return `undefined` if not specified. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jerel Miller <jerelmiller@gmail.com>
Part of the custom scalars work: #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.