Skip to content

Commit fcb7d7d

Browse files
authored
feat: Unions can query() without type discriminator (#3558)
* feat: Unions can query() without type discriminator * enhance: Normalize delegate.invalidate() first argument only has param.
1 parent beaa03e commit fcb7d7d

File tree

20 files changed

+343
-39
lines changed

20 files changed

+343
-39
lines changed

.changeset/plenty-regions-visit.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@data-client/normalizr': patch
3+
'@data-client/endpoint': patch
4+
'@data-client/react': patch
5+
'@data-client/core': patch
6+
'@data-client/rest': patch
7+
'@data-client/graphql': patch
8+
---
9+
10+
Normalize delegate.invalidate() first argument only has `key` param.
11+
12+
`indexes` optional param no longer provided as it was never used.
13+
14+
15+
```ts
16+
normalize(
17+
input: any,
18+
parent: any,
19+
key: string | undefined,
20+
args: any[],
21+
visit: (...args: any) => any,
22+
delegate: INormalizeDelegate,
23+
): string {
24+
delegate.invalidate({ key: this._entity.key }, pk);
25+
return pk;
26+
}
27+
```

.changeset/weak-grapes-kiss.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@data-client/endpoint': patch
3+
'@data-client/rest': patch
4+
'@data-client/graphql': patch
5+
---
6+
7+
Unions can query() without type discriminator
8+
9+
#### Before
10+
```tsx
11+
// @ts-expect-error
12+
const event = useQuery(EventUnion, { id });
13+
// event is undefined
14+
const newsEvent = useQuery(EventUnion, { id, type: 'news' });
15+
// newsEvent is found
16+
```
17+
18+
#### After
19+
20+
```tsx
21+
const event = useQuery(EventUnion, { id });
22+
// event is found
23+
const newsEvent = useQuery(EventUnion, { id, type: 'news' });
24+
// newsEvent is found
25+
```

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ Group {
5353
}
5454
`;
5555

56+
exports[`Controller.get() Union based on args with function schemaAttribute 1`] = `
57+
User {
58+
"id": "1",
59+
"type": "users",
60+
"username": "bob",
61+
}
62+
`;
63+
64+
exports[`Controller.get() Union based on args with function schemaAttribute 2`] = `
65+
Group {
66+
"groupname": "fast",
67+
"id": "2",
68+
"memberCount": 5,
69+
"type": "groups",
70+
}
71+
`;
72+
5673
exports[`Controller.get() indexes query Entity based on index 1`] = `
5774
User {
5875
"id": "1",

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

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,11 @@ describe('Controller.get()', () => {
295295
id: string = '';
296296
}
297297
class User extends IDEntity {
298-
type = 'user';
298+
type = 'users';
299299
username: string = '';
300300
}
301301
class Group extends IDEntity {
302-
type = 'group';
302+
type = 'groups';
303303
groupname: string = '';
304304
memberCount = 0;
305305
}
@@ -349,6 +349,70 @@ describe('Controller.get()', () => {
349349
// @ts-expect-error
350350
() => controller.get(queryPerson, { id: '1', doesnotexist: 5 }, state);
351351
});
352+
353+
it('Union based on args with function schemaAttribute', () => {
354+
class IDEntity extends Entity {
355+
id: string = '';
356+
}
357+
class User extends IDEntity {
358+
type = 'user';
359+
username: string = '';
360+
}
361+
class Group extends IDEntity {
362+
type = 'group';
363+
groupname: string = '';
364+
memberCount = 0;
365+
}
366+
const queryPerson = new schema.Union(
367+
{
368+
users: User,
369+
groups: Group,
370+
},
371+
(value: { type: 'users' | 'groups' }) => value.type,
372+
);
373+
const controller = new Controller();
374+
const state = {
375+
...initialState,
376+
entities: {
377+
User: {
378+
'1': { id: '1', type: 'users', username: 'bob' },
379+
},
380+
Group: {
381+
'2': { id: '2', type: 'groups', groupname: 'fast', memberCount: 5 },
382+
},
383+
},
384+
};
385+
const user = controller.get(queryPerson, { id: '1', type: 'users' }, state);
386+
expect(user).toBeDefined();
387+
expect(user).toBeInstanceOf(User);
388+
expect(user).toMatchSnapshot();
389+
const group = controller.get(
390+
queryPerson,
391+
{ id: '2', type: 'groups' },
392+
state,
393+
);
394+
expect(group).toBeDefined();
395+
expect(group).toBeInstanceOf(Group);
396+
expect(group).toMatchSnapshot();
397+
398+
// should maintain referential equality
399+
expect(user).toBe(
400+
controller.get(queryPerson, { id: '1', type: 'users' }, state),
401+
);
402+
403+
// these are the 'fallback case' where it cannot determine type discriminator, so just enumerates
404+
() => controller.get(queryPerson, { id: '1' }, state);
405+
// @ts-expect-error
406+
() => controller.get(queryPerson, { id: '1', type: 'notrealtype' }, state);
407+
// @ts-expect-error
408+
() => controller.get(queryPerson, { id: { bob: 5 }, type: 'users' }, state);
409+
// @ts-expect-error
410+
expect(controller.get(queryPerson, 5, state)).toBeUndefined();
411+
// @ts-expect-error
412+
() => controller.get(queryPerson, { doesnotexist: 5 }, state);
413+
// @ts-expect-error
414+
() => controller.get(queryPerson, { id: '1', doesnotexist: 5 }, state);
415+
});
352416
});
353417

354418
describe('Snapshot.getQueryMeta()', () => {

packages/endpoint/src/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export interface INormalizeDelegate {
177177
meta?: { fetchedAt: number; date: number; expiresAt: number },
178178
): void;
179179
/** Invalidates an entity, potentially triggering suspense */
180-
invalidate(schema: { key: string; indexes?: any }, pk: string): void;
180+
invalidate(schema: { key: string }, pk: string): void;
181181
/** Returns true when we're in a cycle, so we should not continue recursing */
182182
checkLoop(key: string, pk: string, input: object): boolean;
183183
}

packages/endpoint/src/schema.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export interface UnionConstructor {
229229
schemaAttribute: SchemaAttribute,
230230
): UnionInstance<
231231
Choices,
232-
UnionSchemaToArgs<Choices, SchemaAttribute> &
232+
Partial<UnionSchemaToArgs<Choices, SchemaAttribute>> &
233233
Partial<AbstractInstanceType<Choices[keyof Choices]>>
234234
>;
235235

packages/endpoint/src/schemas/Invalidate.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default class Invalidate<
3434
this._entity = entity;
3535
}
3636

37-
get key() {
37+
get key(): string {
3838
return this._entity.key;
3939
}
4040

@@ -73,7 +73,7 @@ export default class Invalidate<
7373

7474
// any queued updates are meaningless with delete, so we should just set it
7575
// and creates will have a different pk
76-
delegate.invalidate(this as any, pk);
76+
delegate.invalidate({ key: this._entity.key }, pk);
7777
return pk;
7878
}
7979

@@ -86,6 +86,7 @@ export default class Invalidate<
8686
args: readonly any[],
8787
unvisit: (schema: any, input: any) => any,
8888
): AbstractInstanceType<E> {
89+
// TODO: is this really always going to be the full object - validate that calling fetch will give this even when input is a string
8990
return unvisit(this._entity, id) as any;
9091
}
9192

packages/endpoint/src/schemas/Polymorphic.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Value: ${JSON.stringify(value, undefined, 2)}`,
103103
/* istanbul ignore else */
104104
if (process.env.NODE_ENV !== 'production' && value) {
105105
console.warn(
106-
`TypeError: Unable to infer schema for ${this.constructor.name}
106+
`TypeError: Unable to determine schema for ${this.constructor.name}
107107
Value: ${JSON.stringify(value, undefined, 2)}.`,
108108
);
109109
}

packages/endpoint/src/schemas/Union.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,23 @@ export default class UnionSchema extends PolymorphicSchema {
2929

3030
queryKey(args: any, unvisit: (schema: any, args: any) => any) {
3131
if (!args[0]) return;
32+
// Often we have sufficient information in the first arg like { id, type }
3233
const schema = this.getSchemaAttribute(args[0], undefined, '');
3334
const discriminatedSchema = this.schema[schema];
3435

35-
// Was unable to infer the entity's schema from params
36-
if (discriminatedSchema === undefined) return;
37-
const id = unvisit(discriminatedSchema, args);
38-
if (id === undefined) return;
39-
return { id, schema };
36+
// Fast case - args include type discriminator
37+
if (discriminatedSchema) {
38+
const id = unvisit(discriminatedSchema, args);
39+
if (id === undefined) return;
40+
return { id, schema };
41+
}
42+
43+
// Fallback to trying every possible schema if it cannot be determined
44+
for (const key in this.schema) {
45+
const id = unvisit(this.schema[key], args);
46+
if (id !== undefined) {
47+
return { id, schema: key };
48+
}
49+
}
4050
}
4151
}

0 commit comments

Comments
 (0)