Skip to content

Commit b224efc

Browse files
authored
Allow read functions to return undefined array items (#13056)
1 parent da0b5ce commit b224efc

4 files changed

Lines changed: 257 additions & 1 deletion

File tree

.changeset/stupid-shrimps-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": minor
3+
---
4+
5+
`InMemoryCache` no longer filters out explicitly returned `undefined` items from `read` functions for array fields. This now makes it possible to create `read` functions on array fields that return partial data and trigger a fetch for the full list.

src/__tests__/client.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ApolloClient, NetworkStatus } from "@apollo/client";
2020
import type {
2121
NormalizedCacheObject,
2222
PossibleTypesMap,
23+
Reference,
2324
} from "@apollo/client/cache";
2425
import {
2526
createFragmentRegistry,
@@ -3201,6 +3202,143 @@ describe("client", () => {
32013202

32023203
expect(actualResult.data).toEqual(result);
32033204
});
3205+
3206+
it("handles read functions on arrays that return undefined items", async () => {
3207+
const query: TypedDocumentNode<{
3208+
books: Array<{ __typename: "Book"; id: number; name: string }>;
3209+
}> = gql`
3210+
query {
3211+
books {
3212+
id
3213+
name
3214+
}
3215+
}
3216+
`;
3217+
3218+
const link = new MockLink([
3219+
{
3220+
request: { query },
3221+
result: {
3222+
data: {
3223+
books: [
3224+
{ __typename: "Book", id: 1, name: "Book 1 (fetch)" },
3225+
{ __typename: "Book", id: 2, name: "Book 2 (fetch)" },
3226+
{ __typename: "Book", id: 3, name: "Book 3 (fetch)" },
3227+
],
3228+
},
3229+
},
3230+
},
3231+
]);
3232+
3233+
const cache = new InMemoryCache({
3234+
typePolicies: {
3235+
Query: {
3236+
fields: {
3237+
books: (existing: Reference[] = [], { canRead }) => {
3238+
return existing.map((book) => (canRead(book) ? book : undefined));
3239+
},
3240+
},
3241+
},
3242+
},
3243+
});
3244+
3245+
const client = new ApolloClient({ link, cache });
3246+
3247+
client.writeQuery({
3248+
query,
3249+
data: {
3250+
books: [
3251+
{ __typename: "Book", id: 1, name: "Book 1 (cache)" },
3252+
{ __typename: "Book", id: 2, name: "Book 2 (cache)" },
3253+
],
3254+
},
3255+
});
3256+
3257+
const stream = new ObservableStream(client.watchQuery({ query }));
3258+
const partialStream = new ObservableStream(
3259+
client.watchQuery({
3260+
query,
3261+
returnPartialData: true,
3262+
})
3263+
);
3264+
3265+
await expect(stream).toEmitTypedValue({
3266+
data: {
3267+
books: [
3268+
{ __typename: "Book", id: 1, name: "Book 1 (cache)" },
3269+
{ __typename: "Book", id: 2, name: "Book 2 (cache)" },
3270+
],
3271+
},
3272+
dataState: "complete",
3273+
loading: false,
3274+
networkStatus: NetworkStatus.ready,
3275+
partial: false,
3276+
});
3277+
3278+
await expect(partialStream).toEmitTypedValue({
3279+
data: {
3280+
books: [
3281+
{ __typename: "Book", id: 1, name: "Book 1 (cache)" },
3282+
{ __typename: "Book", id: 2, name: "Book 2 (cache)" },
3283+
],
3284+
},
3285+
dataState: "complete",
3286+
loading: false,
3287+
networkStatus: NetworkStatus.ready,
3288+
partial: false,
3289+
});
3290+
3291+
cache.evict({ id: cache.identify({ __typename: "Book", id: 2 }) });
3292+
3293+
await expect(stream).toEmitSimilarValue({
3294+
expected: (previous) => ({
3295+
...previous,
3296+
loading: true,
3297+
networkStatus: NetworkStatus.loading,
3298+
}),
3299+
});
3300+
await expect(partialStream).toEmitTypedValue({
3301+
data: {
3302+
books: [{ __typename: "Book", id: 1, name: "Book 1 (cache)" }, {}],
3303+
},
3304+
dataState: "partial",
3305+
loading: true,
3306+
networkStatus: NetworkStatus.loading,
3307+
partial: true,
3308+
});
3309+
3310+
await expect(stream).toEmitSimilarValue({
3311+
expected: (previous) => ({
3312+
...previous,
3313+
data: {
3314+
books: [
3315+
{ __typename: "Book", id: 1, name: "Book 1 (fetch)" },
3316+
{ __typename: "Book", id: 2, name: "Book 2 (fetch)" },
3317+
{ __typename: "Book", id: 3, name: "Book 3 (fetch)" },
3318+
],
3319+
},
3320+
dataState: "complete",
3321+
loading: false,
3322+
networkStatus: NetworkStatus.ready,
3323+
}),
3324+
});
3325+
await expect(partialStream).toEmitTypedValue({
3326+
data: {
3327+
books: [
3328+
{ __typename: "Book", id: 1, name: "Book 1 (fetch)" },
3329+
{ __typename: "Book", id: 2, name: "Book 2 (fetch)" },
3330+
{ __typename: "Book", id: 3, name: "Book 3 (fetch)" },
3331+
],
3332+
},
3333+
dataState: "complete",
3334+
loading: false,
3335+
networkStatus: NetworkStatus.ready,
3336+
partial: false,
3337+
});
3338+
3339+
await expect(stream).not.toEmitAnything();
3340+
await expect(partialStream).not.toEmitAnything();
3341+
});
32043342
});
32053343

32063344
describe("@connection", () => {

src/cache/inmemory/__tests__/readFromStore.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,117 @@ describe("reading from the store", () => {
15161516
});
15171517
});
15181518

1519+
it("does not filter explicit undefined items in array returned from read function", () => {
1520+
const cache = new InMemoryCache({
1521+
typePolicies: {
1522+
Query: {
1523+
fields: {
1524+
ducks(existing: Reference[] = [], { canRead }) {
1525+
return existing.map((duck) => (canRead(duck) ? duck : undefined));
1526+
},
1527+
},
1528+
},
1529+
},
1530+
});
1531+
1532+
cache.writeQuery({
1533+
query: gql`
1534+
query {
1535+
ducks {
1536+
quacking
1537+
}
1538+
}
1539+
`,
1540+
data: {
1541+
ducks: [
1542+
{ __typename: "Duck", id: 1, quacking: true },
1543+
{ __typename: "Duck", id: 2, quacking: false },
1544+
{ __typename: "Duck", id: 3, quacking: false },
1545+
],
1546+
},
1547+
});
1548+
1549+
expect(cache.extract()).toEqual({
1550+
"Duck:1": {
1551+
__typename: "Duck",
1552+
id: 1,
1553+
quacking: true,
1554+
},
1555+
"Duck:2": {
1556+
__typename: "Duck",
1557+
id: 2,
1558+
quacking: false,
1559+
},
1560+
"Duck:3": {
1561+
__typename: "Duck",
1562+
id: 3,
1563+
quacking: false,
1564+
},
1565+
ROOT_QUERY: {
1566+
__typename: "Query",
1567+
ducks: [{ __ref: "Duck:1" }, { __ref: "Duck:2" }, { __ref: "Duck:3" }],
1568+
},
1569+
});
1570+
1571+
function diffDucks() {
1572+
return cache.diff({
1573+
query: gql`
1574+
query {
1575+
ducks {
1576+
id
1577+
quacking
1578+
}
1579+
}
1580+
`,
1581+
optimistic: true,
1582+
});
1583+
}
1584+
1585+
expect(diffDucks()).toEqual({
1586+
complete: true,
1587+
result: {
1588+
ducks: [
1589+
{ __typename: "Duck", id: 1, quacking: true },
1590+
{ __typename: "Duck", id: 2, quacking: false },
1591+
{ __typename: "Duck", id: 3, quacking: false },
1592+
],
1593+
},
1594+
});
1595+
1596+
expect(
1597+
cache.evict({
1598+
id: cache.identify({
1599+
__typename: "Duck",
1600+
id: 3,
1601+
}),
1602+
})
1603+
).toBe(true);
1604+
1605+
expect(diffDucks()).toEqual({
1606+
complete: false,
1607+
result: {
1608+
ducks: [
1609+
{ __typename: "Duck", id: 1, quacking: true },
1610+
{ __typename: "Duck", id: 2, quacking: false },
1611+
{},
1612+
],
1613+
},
1614+
missing: new MissingFieldError(
1615+
"Can't find field 'id' on object undefined",
1616+
{
1617+
ducks: {
1618+
2: {
1619+
id: "Can't find field 'id' on object undefined",
1620+
quacking: "Can't find field 'quacking' on object undefined",
1621+
},
1622+
},
1623+
},
1624+
expect.anything(), // query
1625+
expect.anything() // variables
1626+
),
1627+
});
1628+
});
1629+
15191630
it("propagates eviction signals to parent queries", () => {
15201631
const cache = new InMemoryCache({
15211632
typePolicies: {

src/cache/inmemory/readFromStore.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,9 @@ export class StoreReader {
424424
}
425425

426426
if (field.selectionSet) {
427-
array = array.filter(context.store.canRead);
427+
array = array.filter(
428+
(item) => item === undefined || context.store.canRead(item)
429+
);
428430
}
429431

430432
array = array.map((item, i) => {

0 commit comments

Comments
 (0)