Skip to content

Commit 29caaee

Browse files
committed
feat: support plural fragments
1 parent f28be48 commit 29caaee

9 files changed

Lines changed: 149 additions & 47 deletions

.changeset/poor-olives-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"solid-relay": minor
3+
---
4+
5+
feat: support plural fragments

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { loadQuery, type LoadQueryOptions, type PreloadedQuery } from "./loadQuery";
2-
export { createFragment } from "./primitives/createFragment";
2+
export { createFragment, type MaybeArray } from "./primitives/createFragment";
33
export { createLazyLoadQuery } from "./primitives/createLazyLoadQuery";
44
export { createMutation } from "./primitives/createMutation";
55
export { createPaginationFragment } from "./primitives/createPaginationFragment.js";

src/primitives/createFragment.ts

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
import type { GraphQLResponse, GraphQLTaggedNode, Subscribable, Subscription } from "relay-runtime";
1+
import type {
2+
GraphQLResponse,
3+
GraphQLTaggedNode,
4+
Observer,
5+
Subscribable,
6+
Subscription,
7+
} from "relay-runtime";
28
import { observeFragment } from "relay-runtime/experimental.js";
9+
import {
10+
ArrayKeyType,
11+
ArrayKeyTypeData,
12+
FragmentState,
13+
KeyType,
14+
KeyTypeData,
15+
} from "relay-runtime/lib/store/FragmentTypes";
316
import type { Accessor, Setter, Signal } from "solid-js";
417
import { batch, createResource, createSignal, untrack } from "solid-js";
518
import { reconcile, type SetStoreFunction, unwrap } from "solid-js/store";
619
import { isServer } from "solid-js/web";
720
import { useRelayEnvironment } from "../RelayEnvironment";
8-
import type { KeyType, KeyTypeData } from "../types/keyType";
921
import { createDataStore, type DataStore } from "../utils/dataStore";
1022

1123
type FragmentResult<T> =
@@ -50,17 +62,56 @@ export function createFragment<TKey extends KeyType>(
5062
deferStream?: boolean;
5163
},
5264
): DataStore<KeyTypeData<TKey> | null | undefined>;
53-
export function createFragment<TKey extends KeyType>(
65+
export function createFragment<TKey extends ArrayKeyType>(
66+
fragment: GraphQLTaggedNode,
67+
key: Accessor<TKey>,
68+
options?: {
69+
deferStream?: boolean;
70+
},
71+
): DataStore<ArrayKeyTypeData<TKey>>;
72+
export function createFragment<TKey extends MaybeArray<ArrayKeyType>>(
73+
fragment: GraphQLTaggedNode,
74+
key: Accessor<TKey>,
75+
options?: {
76+
deferStream?: boolean;
77+
},
78+
): DataStore<MaybeArray<ArrayKeyTypeData<RequiredArray<TKey>>>>;
79+
export function createFragment<TKey extends ArrayKeyType>(
5480
fragment: GraphQLTaggedNode,
5581
key: Accessor<TKey | null | undefined>,
5682
options?: {
5783
deferStream?: boolean;
5884
},
59-
): DataStore<KeyTypeData<TKey> | null | undefined> {
85+
): DataStore<ArrayKeyTypeData<TKey> | null | undefined>;
86+
export function createFragment<TKey extends MaybeArray<ArrayKeyType>>(
87+
fragment: GraphQLTaggedNode,
88+
key: Accessor<TKey | null | undefined>,
89+
options?: {
90+
deferStream?: boolean;
91+
},
92+
): DataStore<MaybeArray<ArrayKeyTypeData<RequiredArray<TKey>>> | null | undefined>;
93+
export function createFragment<TKey extends KeyType | ArrayKeyType>(
94+
fragment: GraphQLTaggedNode,
95+
key: Accessor<TKey | null | undefined>,
96+
options?: {
97+
deferStream?: boolean;
98+
},
99+
): DataStore<Data<TKey> | null | undefined> {
60100
return createFragmentInternal(fragment, key, undefined, options);
61101
}
62102

63-
export function createFragmentInternal<TKey extends KeyType>(
103+
export type MaybeArray<T> =
104+
T extends ReadonlyArray<unknown> ? ReadonlyArray<T[number] | null | undefined> : never;
105+
type RequiredArray<T> =
106+
T extends ReadonlyArray<(infer U) | null | undefined> ? ReadonlyArray<U> : never;
107+
108+
type Data<TKey extends KeyType | ArrayKeyType> = TKey extends KeyType
109+
? KeyTypeData<TKey>
110+
: TKey extends ArrayKeyType
111+
? ArrayKeyTypeData<TKey>
112+
: never;
113+
114+
export function createFragmentInternal<TKey extends KeyType | ArrayKeyType>(
64115
fragment: GraphQLTaggedNode,
65116
key: Accessor<TKey | null | undefined>,
66117
options?: Accessor<{
@@ -69,10 +120,9 @@ export function createFragmentInternal<TKey extends KeyType>(
69120
createResourceOptions?: {
70121
deferStream?: boolean;
71122
},
72-
): DataStore<KeyTypeData<TKey> | null | undefined> {
123+
): DataStore<Data<TKey> | null | undefined> {
73124
const environment = useRelayEnvironment();
74125

75-
type FragmentObserver = Parameters<ReturnType<typeof observeFragment>["subscribe"]>[0];
76126
const resultUpdateObserver = {
77127
next(res) {
78128
queueMicrotask(() => {
@@ -86,7 +136,7 @@ export function createFragmentInternal<TKey extends KeyType>(
86136
setResult("pending", false);
87137
setResult(
88138
"data",
89-
reconcile(res.value as Record<string, unknown>, {
139+
reconcile(res.value, {
90140
key: "__id",
91141
merge: true,
92142
}),
@@ -101,11 +151,11 @@ export function createFragmentInternal<TKey extends KeyType>(
101151
});
102152
});
103153
},
104-
} satisfies FragmentObserver;
154+
} satisfies Observer<FragmentState<unknown>>;
105155
const [subscription, setSubscription] = createSignal<Subscription>();
106156

107157
const setResultQueue: unknown[][] = [];
108-
let setResult: SetStoreFunction<FragmentResult<TKey[" $data"]>> = (...args: unknown[]) => {
158+
let setResult: SetStoreFunction<FragmentResult<unknown>> = (...args: unknown[]) => {
109159
setResultQueue.push(args);
110160
};
111161

@@ -140,7 +190,7 @@ export function createFragmentInternal<TKey extends KeyType>(
140190
});
141191
}
142192

143-
const source = observeFragment(environment(), fragment, key);
193+
const source = observeFragment(environment(), fragment, key as KeyType);
144194

145195
return new Promise<true>((resolve, reject) => {
146196
setSubscription(
@@ -173,7 +223,9 @@ export function createFragmentInternal<TKey extends KeyType>(
173223

174224
if (!fetchedInSameEnv && !current && nextValue && k) {
175225
setSubscription(
176-
observeFragment(environment(), fragment, k).subscribe(resultUpdateObserver),
226+
observeFragment(environment(), fragment, k as KeyType).subscribe(
227+
resultUpdateObserver,
228+
),
177229
);
178230
}
179231

@@ -184,7 +236,7 @@ export function createFragmentInternal<TKey extends KeyType>(
184236
},
185237
);
186238

187-
const store = createDataStore<FragmentResult<TKey[" $data"]>>(
239+
const store = createDataStore<FragmentResult<unknown>>(
188240
{
189241
data: undefined,
190242
error: undefined,
@@ -197,5 +249,5 @@ export function createFragmentInternal<TKey extends KeyType>(
197249
}
198250
setResult = store[1];
199251

200-
return store[0];
252+
return store[0] as DataStore<Data<TKey> | null | undefined>;
201253
}

src/primitives/createPaginationFragment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
import { type Accessor, batch, createEffect, createMemo, createSignal, untrack } from "solid-js";
2121
import invariant from "tiny-invariant";
2222
import { useRelayEnvironment } from "../RelayEnvironment";
23-
import type { KeyType, KeyTypeData } from "../types/keyType";
2423
import { createFetchTracker } from "../utils/createFetchTracker";
2524
import type { DataStore } from "../utils/dataStore";
2625
import { getConnectionState } from "../utils/getConnectionState";
@@ -31,6 +30,7 @@ import {
3130
type RefetchFnDynamic,
3231
type RefetchOptions,
3332
} from "./createRefetchableFragment";
33+
import { KeyType, KeyTypeData } from "relay-runtime/lib/store/FragmentTypes";
3434

3535
type CreatePaginationFragmentReturn<
3636
TQuery extends OperationType,

src/primitives/createRefetchableFragment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ import {
3232
import { unwrap } from "solid-js/store";
3333
import { isServer } from "solid-js/web";
3434
import { useRelayEnvironment } from "../RelayEnvironment";
35-
import type { KeyType, KeyTypeData } from "../types/keyType";
3635
import type { DataStore } from "../utils/dataStore";
3736
import { getQueryRef } from "../utils/getQueryRef";
3837
import { useIsMounted } from "../utils/useIsMounted";
3938
import { createFragmentInternal } from "./createFragment";
4039
import { createQueryLoader } from "./createQueryLoader";
40+
import { KeyType, KeyTypeData } from "relay-runtime/lib/store/FragmentTypes";
4141

4242
export type RefetchFnDynamic<
4343
TQuery extends OperationType,

src/types/keyType.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/types/relayRuntimeExperimental.d.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

tests/createFragment.test.tsx

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import {
66
Store,
77
} from "relay-runtime";
88
import { createMockEnvironment, type MockEnvironment } from "relay-test-utils";
9-
import { createSignal, ErrorBoundary, Suspense, type JSXElement } from "solid-js";
9+
import { createSignal, ErrorBoundary, For, Show, Suspense, type JSXElement } from "solid-js";
1010
import {
1111
createFragment,
1212
createLazyLoadQuery,
1313
type DataStore,
14+
type MaybeArray,
1415
RelayEnvironmentProvider,
1516
} from "solid-relay";
1617
import { page } from "vitest/browser";
@@ -19,6 +20,11 @@ import type {
1920
createFragmentTest_user$key,
2021
} from "./__generated__/createFragmentTest_user.graphql";
2122
import type { createFragmentTestOwnerQuery } from "./__generated__/createFragmentTestOwnerQuery.graphql";
23+
import {
24+
createFragmentTestPlural_users$data,
25+
createFragmentTestPlural_users$key,
26+
} from "./__generated__/createFragmentTestPlural_users.graphql";
27+
import { createFragmentTestPluralQuery } from "./__generated__/createFragmentTestPluralQuery.graphql";
2228
import { renderToBody, wait } from "./utils";
2329

2430
let environment: MockEnvironment;
@@ -209,4 +215,71 @@ describe("createFragment", () => {
209215
expect(store?.()).toBeUndefined();
210216
expect(store?.latest).toBeUndefined();
211217
});
218+
219+
describe("plural", () => {
220+
let pluralStore: DataStore<MaybeArray<createFragmentTestPlural_users$data>> | undefined;
221+
const pluralQuery = graphql`
222+
query createFragmentTestPluralQuery($ids: [ID!]!) {
223+
nodes(ids: $ids) {
224+
...createFragmentTestPlural_users
225+
}
226+
}
227+
` as ConcreteRequest;
228+
const pluralFragment = graphql`
229+
fragment createFragmentTestPlural_users on User @relay(plural: true) {
230+
id
231+
name
232+
}
233+
`;
234+
const pluralOperation = createOperationDescriptor(pluralQuery, { ids: ["1"] });
235+
236+
const PluralChild = (props: {
237+
users: MaybeArray<createFragmentTestPlural_users$key>;
238+
testId?: string;
239+
}) => {
240+
pluralStore = createFragment(pluralFragment, () => props.users);
241+
return (
242+
<ul>
243+
<For each={pluralStore()}>
244+
{(node, i) => <li data-testid={`${props.testId ?? "name"}-${i()}`}>{node?.name}</li>}
245+
</For>
246+
</ul>
247+
);
248+
};
249+
250+
const PluralQueryScreen = (props: {
251+
users?: createFragmentTestPlural_users$key | null | undefined;
252+
}) => {
253+
const data = createLazyLoadQuery<createFragmentTestPluralQuery>(pluralQuery, { ids: ["1"] });
254+
return (
255+
<ErrorBoundary fallback={(err) => <h1 data-testid="error">{err.message}</h1>}>
256+
<Suspense fallback="Fallback">
257+
<Show when={data()}>
258+
{(data) => <PluralChild users={props.users ?? data().nodes} />}
259+
</Show>
260+
</Suspense>
261+
</ErrorBoundary>
262+
);
263+
};
264+
265+
beforeEach(() => {
266+
pluralStore = undefined;
267+
});
268+
269+
it("reads fragment data from a parent query key", async () => {
270+
renderToBody(() => (
271+
<View>
272+
<PluralQueryScreen />
273+
</View>
274+
));
275+
276+
await expect.element(page.getByText("Fallback")).toBeInTheDocument();
277+
environment.mock.resolve(pluralOperation, {
278+
data: { nodes: [{ __typename: "User", id: "1", name: "Alice" }] },
279+
});
280+
await wait(2);
281+
await expect.element(page.getByTestId("name-0")).toHaveTextContent("Alice");
282+
await expect.element(page.getByText("Fallback")).not.toBeInTheDocument();
283+
});
284+
});
212285
});

tests/schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
type Query {
22
node(id: ID!): Node
3+
nodes(ids: [ID!]!): [Node]!
34
users(first: Int, after: String, last: Int, before: String): UserConnection!
45
}
56

0 commit comments

Comments
 (0)