Skip to content

Commit ca4536f

Browse files
authored
enhance: Add consolidate callbacks for queryKey and normalize in 'delegates' (#3427)
1 parent fdfb34d commit ca4536f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+2463
-1624
lines changed

.changeset/cold-walls-like.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@data-client/core': patch
3+
'@data-client/react': patch
4+
---
5+
6+
Fix controller.get and controller.getQueryMeta 'state' argument types

.changeset/forty-masks-join.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@data-client/endpoint': patch
3+
'@data-client/graphql': patch
4+
'@data-client/rest': patch
5+
---
6+
7+
Fix: ensure string id in Entity set when process returns undefined (meaning INVALID)

.changeset/proud-insects-smile.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
'@data-client/normalizr': minor
3+
'@data-client/endpoint': minor
4+
'@data-client/core': minor
5+
'@data-client/graphql': minor
6+
'@data-client/react': minor
7+
'@data-client/rest': minor
8+
---
9+
10+
BREAKING CHANGE: schema.normalize(...args, addEntity, getEntity, checkLoop) -> schema.normalize(...args, delegate)
11+
12+
We consolidate all 'callback' functions during recursion calls into a single 'delegate' argument.
13+
14+
```ts
15+
/** Helpers during schema.normalize() */
16+
export interface INormalizeDelegate {
17+
/** Action meta-data for this normalize call */
18+
readonly meta: { fetchedAt: number; date: number; expiresAt: number };
19+
/** Gets any previously normalized entity from store */
20+
getEntity: GetEntity;
21+
/** Updates an entity using merge lifecycles when it has previously been set */
22+
mergeEntity(
23+
schema: Mergeable & { indexes?: any },
24+
pk: string,
25+
incomingEntity: any,
26+
): void;
27+
/** Sets an entity overwriting any previously set values */
28+
setEntity(
29+
schema: { key: string; indexes?: any },
30+
pk: string,
31+
entity: any,
32+
meta?: { fetchedAt: number; date: number; expiresAt: number },
33+
): void;
34+
/** Returns true when we're in a cycle, so we should not continue recursing */
35+
checkLoop(key: string, pk: string, input: object): boolean;
36+
}
37+
```
38+
39+
#### Before
40+
41+
```ts
42+
addEntity(this, processedEntity, id);
43+
```
44+
45+
#### After
46+
47+
```ts
48+
delegate.mergeEntity(this, id, processedEntity);
49+
```

.changeset/wicked-bags-appear.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@data-client/normalizr': minor
3+
'@data-client/endpoint': minor
4+
'@data-client/core': minor
5+
'@data-client/graphql': minor
6+
'@data-client/react': minor
7+
'@data-client/rest': minor
8+
---
9+
10+
BREAKING CHANGE: schema.queryKey(args, queryKey, getEntity, getIndex) -> schema.queryKey(args, unvisit, delegate)
11+
BREAKING CHANGE: delegate.getIndex() returns the index directly, rather than object.
12+
13+
We consolidate all 'callback' functions during recursion calls into a single 'delegate' argument.
14+
15+
Our recursive call is renamed from queryKey to unvisit, and does not require the last two arguments.
16+
17+
```ts
18+
/** Accessors to the currently processing state while building query */
19+
export interface IQueryDelegate {
20+
getEntity: GetEntity;
21+
getIndex: GetIndex;
22+
}
23+
```
24+
25+
#### Before
26+
27+
```ts
28+
queryKey(args, queryKey, getEntity, getIndex) {
29+
getIndex(schema.key, indexName, value)[value];
30+
getEntity(this.key, id);
31+
return queryKey(this.schema, args, getEntity, getIndex);
32+
}
33+
```
34+
35+
#### After
36+
37+
```ts
38+
queryKey(args, unvisit, delegate) {
39+
delegate.getIndex(schema.key, indexName, value);
40+
delegate.getEntity(this.key, id);
41+
return unvisit(this.schema, args);
42+
}
43+
```

packages/core/src/controller/Controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ export default class Controller<
580580
schema: S,
581581
...rest: readonly [
582582
...SchemaArgs<S>,
583-
Pick<State<unknown>, 'entities' | 'entityMeta'>,
583+
Pick<State<unknown>, 'entities' | 'indexes'>,
584584
]
585585
): DenormalizeNullable<S> | undefined {
586586
const state = rest[rest.length - 1] as State<any>;
@@ -600,7 +600,7 @@ export default class Controller<
600600
schema: S,
601601
...rest: readonly [
602602
...SchemaArgs<S>,
603-
Pick<State<unknown>, 'entities' | 'entityMeta'>,
603+
Pick<State<unknown>, 'entities' | 'indexes'>,
604604
]
605605
): {
606606
data: DenormalizeNullable<S> | undefined;

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

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

56+
exports[`Controller.get() indexes query Entity based on index 1`] = `
57+
User {
58+
"id": "1",
59+
"staff": false,
60+
"username": "bob",
61+
}
62+
`;
63+
64+
exports[`Controller.get() indexes query indexes after empty state 1`] = `
65+
User {
66+
"id": "1",
67+
"staff": false,
68+
"username": "bob",
69+
}
70+
`;
71+
5672
exports[`Controller.get() query All should get all entities 1`] = `
5773
[
5874
Tacos {
@@ -105,13 +121,6 @@ exports[`Controller.get() query Collection based on args 2`] = `
105121
]
106122
`;
107123

108-
exports[`Controller.get() query Entity based on index 1`] = `
109-
User {
110-
"id": "1",
111-
"username": "bob",
112-
}
113-
`;
114-
115124
exports[`Controller.get() query Entity based on pk 1`] = `
116125
Tacos {
117126
"id": "1",

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

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Entity, schema } from '@data-client/endpoint';
22

33
import { initialState } from '../../state/reducer/createReducer';
4+
import { State } from '../../types';
45
import Controller from '../Controller';
56

67
class Tacos extends Entity {
@@ -46,37 +47,113 @@ describe('Controller.get()', () => {
4647
() => controller.get(Tacos, { doesnotexist: 5 }, state);
4748
});
4849

49-
it('query Entity based on index', () => {
50+
describe('indexes', () => {
5051
class User extends Entity {
5152
id = '';
5253
username = '';
54+
staff = false;
5355

5456
static indexes = ['username'] as const;
5557
}
58+
it('query Entity based on index', () => {
59+
const controller = new Controller();
60+
const state: State<unknown> = {
61+
...initialState,
62+
entities: {
63+
User: {
64+
'1': { id: '1', username: 'bob' },
65+
'2': { id: '2', username: 'george' },
66+
},
67+
},
68+
indexes: {
69+
User: {
70+
username: {
71+
bob: '1',
72+
george: '2',
73+
},
74+
},
75+
},
76+
};
5677

57-
const controller = new Controller();
58-
const state = {
59-
...initialState,
60-
entities: {
61-
User: {
62-
'1': { id: '1', username: 'bob' },
78+
const bob = controller.get(User, { username: 'bob' }, state);
79+
expect(bob).toBeDefined();
80+
expect(bob).toBeInstanceOf(User);
81+
expect(bob).toMatchSnapshot();
82+
// stability
83+
expect(controller.get(User, { username: 'bob' }, state)).toBe(bob);
84+
// should be same as id lookup
85+
expect(controller.get(User, { id: '1' }, state)).toBe(bob);
86+
// update index
87+
let nextState: State<unknown> = {
88+
...state,
89+
entities: {
90+
...state.entities,
91+
User: {
92+
...state.entities.User,
93+
'1': { id: '1', username: 'george' },
94+
'2': { id: '2', username: 'bob' },
95+
},
6396
},
64-
},
65-
indexes: {
66-
User: {
67-
username: {
68-
bob: '1',
97+
indexes: {
98+
...state.indexes,
99+
User: {
100+
...state.indexes.User,
101+
username: {
102+
...state.indexes.User.username,
103+
bob: '2',
104+
george: '1',
105+
},
69106
},
70107
},
71-
},
72-
};
108+
};
109+
expect(controller.get(User, { username: 'bob' }, nextState)).not.toBe(
110+
bob,
111+
);
112+
nextState = {
113+
...state,
114+
entities: {
115+
...state.entities,
116+
User: {
117+
...state.entities.User,
118+
'1': { id: '1', username: 'bob', staff: true },
119+
},
120+
},
121+
};
122+
// update entity keep index
123+
const nextBob = controller.get(User, { username: 'bob' }, nextState);
124+
expect(nextBob).not.toBe(bob);
125+
expect(nextBob).toBeDefined();
126+
expect(nextBob).toBeInstanceOf(User);
127+
expect(nextBob?.staff).toBe(true);
128+
});
73129

74-
const bob = controller.get(User, { username: 'bob' }, state);
75-
expect(bob).toBeDefined();
76-
expect(bob).toBeInstanceOf(User);
77-
expect(bob).toMatchSnapshot();
78-
// should be same as id lookup
79-
expect(bob).toBe(controller.get(User, { id: '1' }, state));
130+
it('query indexes after empty state', () => {
131+
const controller = new Controller();
132+
expect(
133+
controller.get(User, { username: 'bob' }, initialState),
134+
).toBeUndefined();
135+
const state: State<unknown> = {
136+
...initialState,
137+
entities: {
138+
User: {
139+
'1': { id: '1', username: 'bob' },
140+
'2': { id: '2', username: 'george' },
141+
},
142+
},
143+
indexes: {
144+
User: {
145+
username: {
146+
bob: '1',
147+
george: '2',
148+
},
149+
},
150+
},
151+
};
152+
const bob = controller.get(User, { username: 'bob' }, state);
153+
expect(bob).toBeDefined();
154+
expect(bob).toBeInstanceOf(User);
155+
expect(bob).toMatchSnapshot();
156+
});
80157
});
81158

82159
it('query Collection based on args', () => {

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export type {
1616
EndpointExtraOptions,
1717
Queryable,
1818
SchemaArgs,
19+
Mergeable,
20+
IQueryDelegate,
21+
INormalizeDelegate,
1922
NI,
2023
} from '@data-client/normalizr';
2124
export { ExpiryStatus } from '@data-client/normalizr';

packages/core/src/state/__tests__/reducer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ describe('reducer', () => {
259259
[id]: { id, counter: 5 },
260260
},
261261
},
262+
entityMeta: {
263+
[Counter.key]: {
264+
[id]: { date: 0, fetchedAt: 0, expiresAt: 0 },
265+
},
266+
},
262267
};
263268
const newState = reducer(state, action);
264269
// eslint-disable-next-line @typescript-eslint/ban-ts-comment

packages/endpoint/src-4.0-types/schemaArgs.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { Schema, EntityInterface } from './interface.js';
1+
import type { Schema } from './interface.js';
22
import type { EntityFields } from './schemas/EntityFields.js';
3-
export type SchemaArgs<S extends Schema> = S extends EntityInterface<infer U> ? [EntityFields<U>] : S extends ({
3+
export type SchemaArgs<S extends Schema> = S extends { createIfValid: any; pk: any; key: string; prototype: infer U } ? [EntityFields<U>] : S extends ({
44
queryKey(args: infer Args, ...rest: any): any;
55
}) ? Args : S extends {
66
[K: string]: any;

packages/endpoint/src-4.0-types/schemas/Entity.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ declare const Entity_base: import("./EntityTypes.js").IEntityClass<new (...args:
66
pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined;
77
});
88
/**
9-
* Represents data that should be deduped by specifying a primary key.
9+
* Entity defines a single (globally) unique object.
1010
* @see https://dataclient.io/rest/api/Entity
1111
*/
1212
export default abstract class Entity extends Entity_base {

0 commit comments

Comments
 (0)