Skip to content

Commit ce164d2

Browse files
authored
enhance: Queries passthrough suspense rather than being undefined (#3017)
1 parent 5469db0 commit ce164d2

File tree

13 files changed

+125
-29
lines changed

13 files changed

+125
-29
lines changed

.changeset/short-experts-wave.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@data-client/endpoint": patch
3+
"@data-client/rest": patch
4+
"@data-client/graphql": patch
5+
---
6+
7+
Queries pass-through suspense rather than ever being undefined
8+
9+
- [useSuspense()](https://dataclient.io/docs/api/useSuspense) return values will not be nullable
10+
- [useQuery()](https://dataclient.io/docs/api/useQuery) will still be nullable due to it handling `INVALID` as `undefined` return
11+
- [Query.process](https://dataclient.io/rest/api/Query#process) does not need to handle nullable cases

.circleci/config.yml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ jobs:
1010
- image: cimg/node:20.12
1111
resource_class: large
1212
steps:
13-
- checkout
13+
- checkout:
14+
depth: 1
1415
- run:
1516
name: examples use local packages
1617
command: |
@@ -37,8 +38,21 @@ jobs:
3738
- persist_to_workspace:
3839
root: ~/
3940
paths:
40-
- project
41-
41+
# excplitily list so we can ignore some directories that are not needed
42+
- project/packages
43+
- project/examples
44+
- project/node_modules
45+
- project/__tests__
46+
- project/scripts
47+
- project/babel.config.js
48+
- project/jest.config.js
49+
- project/tsconfig-base.json
50+
- project/tsconfig.json
51+
- project/yarn.lock
52+
- project/tsconfig.test.json
53+
- project/package.json
54+
- project/.yarnrc.yml
55+
- project/.yarn
4256
lint:
4357
docker: *docker
4458
resource_class: medium+
@@ -80,7 +94,7 @@ jobs:
8094
if [ "<< parameters.react-version >>" == "^17.0.0" ]; then
8195
curl -Os https://uploader.codecov.io/latest/linux/codecov;
8296
chmod +x codecov;
83-
yarn run test:coverage --ci --maxWorkers=3 --coverageReporters=text-lcov > ./lcov.info;
97+
yarn run test:coverage --ci --maxWorkers=4 --coverageReporters=text-lcov > ./lcov.info;
8498
if [ "$CODECOV_TOKEN" != "" ]; then
8599
./codecov -t ${CODECOV_TOKEN} < ./lcov.info || true;
86100
else

docs/rest/api/Collection.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ export const getPosts = new RestEndpoint({
317317
nonFilterArgumentKeys: /orderBy/,
318318
}),
319319
(posts, { orderBy } = {}) => {
320-
if (orderBy && posts) {
320+
if (orderBy) {
321321
return [...posts].sort((a, b) => a[orderBy].localeCompare(b[orderBy]));
322322
}
323323
return posts;

docs/rest/api/Query.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const getPosts = new RestEndpoint({
6464
nonFilterArgumentKeys: /orderBy/,
6565
}),
6666
(posts, { orderBy } = {}) => {
67-
if (orderBy && posts) {
67+
if (orderBy) {
6868
return [...posts].sort((a, b) =>
6969
a[orderBy].localeCompare(b[orderBy]),
7070
);

packages/core/src/controller/__tests__/get.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ describe('Controller.get()', () => {
212212
entities,
213213
};
214214
const tacoCount = new schema.Query(TacoList, tacos => {
215-
return tacos?.length ?? 0;
215+
return tacos.length ?? 0;
216216
});
217217

218218
expect(controller.get(tacoCount, { type: 'foo' }, state)).toBe(1);

packages/endpoint/src/schema.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ export class All<
127127
| (S extends EntityMap ? UnionResult<S> : Normalize<S>)[]
128128
| undefined;
129129

130-
_denormalizeNullable(): (S extends EntityMap<infer T> ? T : Denormalize<S>)[];
130+
_denormalizeNullable():
131+
| (S extends EntityMap<infer T> ? T : Denormalize<S>)[]
132+
| undefined;
131133

132134
denormalize(
133135
input: {},

packages/endpoint/src/schemas/Query.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
SchemaSimple,
66
} from '../interface.js';
77
import type {
8+
Denormalize,
89
DenormalizeNullable,
910
NormalizeNullable,
1011
SchemaArgs,
@@ -17,7 +18,7 @@ import type {
1718
*/
1819
export default class Query<
1920
S extends Queryable,
20-
P extends (entries: DenormalizeNullable<S>, ...args: any) => any,
21+
P extends (entries: Denormalize<S>, ...args: any) => any,
2122
> implements SchemaSimple<ReturnType<P> | undefined, ProcessParameters<P, S>>
2223
{
2324
declare schema: S;
@@ -32,9 +33,9 @@ export default class Query<
3233
return (this.schema as any).normalize(...args);
3334
}
3435

35-
denormalize(input: {}, args: any, unvisit: any): ReturnType<P> | undefined {
36+
denormalize(input: {}, args: any, unvisit: any): ReturnType<P> {
3637
const value = unvisit(input, this.schema);
37-
return typeof value === 'symbol' ? undefined : this.process(value, ...args);
38+
return typeof value === 'symbol' ? value : this.process(value, ...args);
3839
}
3940

4041
queryKey(

packages/endpoint/src/schemas/__tests__/Query.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// eslint-env jest
22
import { MemoCache } from '@data-client/normalizr';
3+
import { useQuery, useSuspense } from '@data-client/react';
4+
import { RestEndpoint } from '@data-client/rest';
35
import { IDEntity } from '__tests__/new';
46
import { fromJS } from 'immutable';
57

@@ -188,7 +190,6 @@ describe('top level schema', () => {
188190
const sortedUsers = new schema.Query(
189191
new schema.Collection([User]),
190192
(results, { asc } = { asc: false }, ...args) => {
191-
if (!results) return results;
192193
const sorted = [...results].sort((a, b) => a.name.localeCompare(b.name));
193194
if (asc) return sorted;
194195
return sorted.reverse();
@@ -214,6 +215,49 @@ describe('top level schema', () => {
214215
expect(users).toMatchSnapshot();
215216
});
216217

218+
test('works if base entity suspends', () => {
219+
const entities = {
220+
User: {
221+
1: { id: '1', name: 'Milo' },
222+
2: { id: '2', name: 'Jake' },
223+
3: { id: '3', name: 'Zeta' },
224+
4: { id: '4', name: 'Alpha' },
225+
},
226+
};
227+
const users = new MemoCache().query('', sortedUsers, [], entities, {});
228+
expect(users).toBeUndefined();
229+
});
230+
231+
test('works if base entity suspends', () => {
232+
const allSortedUsers = new schema.Query(
233+
new schema.All(User),
234+
(results, { asc } = { asc: false }, ...args) => {
235+
const sorted = [...results].sort((a, b) =>
236+
a.name.localeCompare(b.name),
237+
);
238+
if (asc) return sorted;
239+
return sorted.reverse();
240+
},
241+
);
242+
const users = new MemoCache().query('', allSortedUsers, [], {}, {});
243+
expect(users).toBeUndefined();
244+
});
245+
246+
test('works with nested schemas', () => {
247+
const allSortedUsers = new schema.Query(
248+
new schema.All(User),
249+
(results, { asc } = { asc: false }, ...args) => {
250+
const sorted = [...results].sort((a, b) =>
251+
a.name.localeCompare(b.name),
252+
);
253+
if (asc) return sorted;
254+
return sorted.reverse();
255+
},
256+
);
257+
const users = new MemoCache().query('', allSortedUsers, [], {}, {});
258+
expect(users).toBeUndefined();
259+
});
260+
217261
test('denormalizes should not be found when no entities are present', () => {
218262
const entities = {
219263
DOG: {
@@ -226,4 +270,20 @@ describe('top level schema', () => {
226270

227271
expect(value).toEqual(undefined);
228272
});
273+
274+
test('', () => {
275+
const getUsers = new RestEndpoint({
276+
path: '/users',
277+
searchParams: {} as { asc?: boolean },
278+
schema: sortedUsers,
279+
});
280+
() => {
281+
const users = useSuspense(getUsers, { asc: true });
282+
users.map(user => user.name);
283+
const others = useQuery(getUsers.schema, { asc: true });
284+
// @ts-expect-error
285+
others.map(user => user.name);
286+
others?.map(user => user.name);
287+
};
288+
});
229289
});

packages/rest/src/__tests__/createResource.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ describe('createResource()', () => {
11221122

11231123
const queryRemainingTodos = new schema.Query(
11241124
TodoResource.getList.schema,
1125-
entries => entries && entries.filter(todo => !todo.completed).length,
1125+
entries => entries.filter(todo => !todo.completed).length,
11261126
);
11271127

11281128
() => useQuery(queryRemainingTodos, { userId: 1 });

website/src/components/Playground/editor-types/@data-client/endpoint.d.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -459,12 +459,12 @@ declare class Invalidate<E extends EntityInterface & {
459459
*
460460
* @see https://dataclient.io/rest/api/Query
461461
*/
462-
declare class Query<S extends Queryable, P extends (entries: DenormalizeNullable<S>, ...args: any) => any> implements SchemaSimple<ReturnType<P> | undefined, ProcessParameters<P, S>> {
462+
declare class Query<S extends Queryable, P extends (entries: Denormalize<S>, ...args: any) => any> implements SchemaSimple<ReturnType<P> | undefined, ProcessParameters<P, S>> {
463463
schema: S;
464464
process: P;
465465
constructor(schema: S, process: P);
466466
normalize(...args: any): any;
467-
denormalize(input: {}, args: any, unvisit: any): ReturnType<P> | undefined;
467+
denormalize(input: {}, args: any, unvisit: any): ReturnType<P>;
468468
queryKey(args: ProcessParameters<P, S>, queryKey: (schema: any, args: any, getEntity: GetEntity, getIndex: GetIndex) => any, getEntity: GetEntity, getIndex: GetIndex): any;
469469
_denormalizeNullable: (input: {}, args: readonly any[], unvisit: (input: any, schema: any) => any) => ReturnType<P> | undefined;
470470
_normalizeNullable: () => NormalizeNullable<S>;
@@ -636,7 +636,9 @@ declare class All<
636636
| (S extends EntityMap ? UnionResult<S> : Normalize<S>)[]
637637
| undefined;
638638

639-
_denormalizeNullable(): (S extends EntityMap<infer T> ? T : Denormalize<S>)[];
639+
_denormalizeNullable():
640+
| (S extends EntityMap<infer T> ? T : Denormalize<S>)[]
641+
| undefined;
640642

641643
denormalize(
642644
input: {},
@@ -915,7 +917,7 @@ type schema_d_Invalidate<E extends EntityInterface & {
915917
process: any;
916918
}> = Invalidate<E>;
917919
declare const schema_d_Invalidate: typeof Invalidate;
918-
type schema_d_Query<S extends Queryable, P extends (entries: DenormalizeNullable<S>, ...args: any) => any> = Query<S, P>;
920+
type schema_d_Query<S extends Queryable, P extends (entries: Denormalize<S>, ...args: any) => any> = Query<S, P>;
919921
declare const schema_d_Query: typeof Query;
920922
type schema_d_SchemaClass<T = any, N = T | undefined, Args extends any[] = any[]> = SchemaClass<T, N, Args>;
921923
type schema_d_All<S extends EntityMap | EntityInterface = EntityMap | EntityInterface> = All<S>;

0 commit comments

Comments
 (0)