Skip to content

enhance: Add consolidate callbacks for queryKey and normalize in 'delegates' #3449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 7, 2025
Merged
6 changes: 6 additions & 0 deletions .changeset/cold-walls-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@data-client/core': patch
'@data-client/react': patch
---

Fix controller.get and controller.getQueryMeta 'state' argument types
7 changes: 7 additions & 0 deletions .changeset/forty-masks-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@data-client/endpoint': patch
'@data-client/graphql': patch
'@data-client/rest': patch
---

Fix: ensure string id in Entity set when process returns undefined (meaning INVALID)
49 changes: 49 additions & 0 deletions .changeset/proud-insects-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
'@data-client/normalizr': minor
'@data-client/endpoint': minor
'@data-client/core': minor
'@data-client/graphql': minor
'@data-client/react': minor
'@data-client/rest': minor
---

BREAKING CHANGE: schema.normalize(...args, addEntity, getEntity, checkLoop) -> schema.normalize(...args, delegate)

We consolidate all 'callback' functions during recursion calls into a single 'delegate' argument.

```ts
/** Helpers during schema.normalize() */
export interface INormalizeDelegate {
/** Action meta-data for this normalize call */
readonly meta: { fetchedAt: number; date: number; expiresAt: number };
/** Gets any previously normalized entity from store */
getEntity: GetEntity;
/** Updates an entity using merge lifecycles when it has previously been set */
mergeEntity(
schema: Mergeable & { indexes?: any },
pk: string,
incomingEntity: any,
): void;
/** Sets an entity overwriting any previously set values */
setEntity(
schema: { key: string; indexes?: any },
pk: string,
entity: any,
meta?: { fetchedAt: number; date: number; expiresAt: number },
): void;
/** Returns true when we're in a cycle, so we should not continue recursing */
checkLoop(key: string, pk: string, input: object): boolean;
}
```

#### Before

```ts
addEntity(this, processedEntity, id);
```

#### After

```ts
delegate.mergeEntity(this, id, processedEntity);
```
43 changes: 43 additions & 0 deletions .changeset/wicked-bags-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@data-client/normalizr': minor
'@data-client/endpoint': minor
'@data-client/core': minor
'@data-client/graphql': minor
'@data-client/react': minor
'@data-client/rest': minor
---

BREAKING CHANGE: schema.queryKey(args, queryKey, getEntity, getIndex) -> schema.queryKey(args, unvisit, delegate)
BREAKING CHANGE: delegate.getIndex() returns the index directly, rather than object.

We consolidate all 'callback' functions during recursion calls into a single 'delegate' argument.

Our recursive call is renamed from queryKey to unvisit, and does not require the last two arguments.

```ts
/** Accessors to the currently processing state while building query */
export interface IQueryDelegate {
getEntity: GetEntity;
getIndex: GetIndex;
}
```

#### Before

```ts
queryKey(args, queryKey, getEntity, getIndex) {
getIndex(schema.key, indexName, value)[value];
getEntity(this.key, id);
return queryKey(this.schema, args, getEntity, getIndex);
}
```

#### After

```ts
queryKey(args, unvisit, delegate) {
delegate.getIndex(schema.key, indexName, value);
delegate.getEntity(this.key, id);
return unvisit(this.schema, args);
}
```
4 changes: 2 additions & 2 deletions packages/core/src/controller/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ export default class Controller<
schema: S,
...rest: readonly [
...SchemaArgs<S>,
Pick<State<unknown>, 'entities' | 'entityMeta'>,
Pick<State<unknown>, 'entities' | 'indexes'>,
]
): DenormalizeNullable<S> | undefined {
const state = rest[rest.length - 1] as State<any>;
Expand All @@ -600,7 +600,7 @@ export default class Controller<
schema: S,
...rest: readonly [
...SchemaArgs<S>,
Pick<State<unknown>, 'entities' | 'entityMeta'>,
Pick<State<unknown>, 'entities' | 'indexes'>,
]
): {
data: DenormalizeNullable<S> | undefined;
Expand Down
23 changes: 16 additions & 7 deletions packages/core/src/controller/__tests__/__snapshots__/get.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ Group {
}
`;

exports[`Controller.get() indexes query Entity based on index 1`] = `
User {
"id": "1",
"staff": false,
"username": "bob",
}
`;

exports[`Controller.get() indexes query indexes after empty state 1`] = `
User {
"id": "1",
"staff": false,
"username": "bob",
}
`;

exports[`Controller.get() query All should get all entities 1`] = `
[
Tacos {
Expand Down Expand Up @@ -105,13 +121,6 @@ exports[`Controller.get() query Collection based on args 2`] = `
]
`;

exports[`Controller.get() query Entity based on index 1`] = `
User {
"id": "1",
"username": "bob",
}
`;

exports[`Controller.get() query Entity based on pk 1`] = `
Tacos {
"id": "1",
Expand Down
117 changes: 97 additions & 20 deletions packages/core/src/controller/__tests__/get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Entity, schema } from '@data-client/endpoint';

import { initialState } from '../../state/reducer/createReducer';
import { State } from '../../types';
import Controller from '../Controller';

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

it('query Entity based on index', () => {
describe('indexes', () => {
class User extends Entity {
id = '';
username = '';
staff = false;

static indexes = ['username'] as const;
}
it('query Entity based on index', () => {
const controller = new Controller();
const state: State<unknown> = {
...initialState,
entities: {
User: {
'1': { id: '1', username: 'bob' },
'2': { id: '2', username: 'george' },
},
},
indexes: {
User: {
username: {
bob: '1',
george: '2',
},
},
},
};

const controller = new Controller();
const state = {
...initialState,
entities: {
User: {
'1': { id: '1', username: 'bob' },
const bob = controller.get(User, { username: 'bob' }, state);
expect(bob).toBeDefined();
expect(bob).toBeInstanceOf(User);
expect(bob).toMatchSnapshot();
// stability
expect(controller.get(User, { username: 'bob' }, state)).toBe(bob);
// should be same as id lookup
expect(controller.get(User, { id: '1' }, state)).toBe(bob);
// update index
let nextState: State<unknown> = {
...state,
entities: {
...state.entities,
User: {
...state.entities.User,
'1': { id: '1', username: 'george' },
'2': { id: '2', username: 'bob' },
},
},
},
indexes: {
User: {
username: {
bob: '1',
indexes: {
...state.indexes,
User: {
...state.indexes.User,
username: {
...state.indexes.User.username,
bob: '2',
george: '1',
},
},
},
},
};
};
expect(controller.get(User, { username: 'bob' }, nextState)).not.toBe(
bob,
);
nextState = {
...state,
entities: {
...state.entities,
User: {
...state.entities.User,
'1': { id: '1', username: 'bob', staff: true },
},
},
};
// update entity keep index
const nextBob = controller.get(User, { username: 'bob' }, nextState);
expect(nextBob).not.toBe(bob);
expect(nextBob).toBeDefined();
expect(nextBob).toBeInstanceOf(User);
expect(nextBob?.staff).toBe(true);
});

const bob = controller.get(User, { username: 'bob' }, state);
expect(bob).toBeDefined();
expect(bob).toBeInstanceOf(User);
expect(bob).toMatchSnapshot();
// should be same as id lookup
expect(bob).toBe(controller.get(User, { id: '1' }, state));
it('query indexes after empty state', () => {
const controller = new Controller();
expect(
controller.get(User, { username: 'bob' }, initialState),
).toBeUndefined();
const state: State<unknown> = {
...initialState,
entities: {
User: {
'1': { id: '1', username: 'bob' },
'2': { id: '2', username: 'george' },
},
},
indexes: {
User: {
username: {
bob: '1',
george: '2',
},
},
},
};
const bob = controller.get(User, { username: 'bob' }, state);
expect(bob).toBeDefined();
expect(bob).toBeInstanceOf(User);
expect(bob).toMatchSnapshot();
});
});

it('query Collection based on args', () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export type {
EndpointExtraOptions,
Queryable,
SchemaArgs,
Mergeable,
IQueryDelegate,
INormalizeDelegate,
NI,
} from '@data-client/normalizr';
export { ExpiryStatus } from '@data-client/normalizr';
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/state/__tests__/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ describe('reducer', () => {
[id]: { id, counter: 5 },
},
},
entityMeta: {
[Counter.key]: {
[id]: { date: 0, fetchedAt: 0, expiresAt: 0 },
},
},
};
const newState = reducer(state, action);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down
4 changes: 2 additions & 2 deletions packages/endpoint/src-4.0-types/schemaArgs.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Schema, EntityInterface } from './interface.js';
import type { Schema } from './interface.js';
import type { EntityFields } from './schemas/EntityFields.js';
export type SchemaArgs<S extends Schema> = S extends EntityInterface<infer U> ? [EntityFields<U>] : S extends ({
export type SchemaArgs<S extends Schema> = S extends { createIfValid: any; pk: any; key: string; prototype: infer U } ? [EntityFields<U>] : S extends ({
queryKey(args: infer Args, ...rest: any): any;
}) ? Args : S extends {
[K: string]: any;
Expand Down
2 changes: 1 addition & 1 deletion packages/endpoint/src-4.0-types/schemas/Entity.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ declare const Entity_base: import("./EntityTypes.js").IEntityClass<new (...args:
pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined;
});
/**
* Represents data that should be deduped by specifying a primary key.
* Entity defines a single (globally) unique object.
* @see https://dataclient.io/rest/api/Entity
*/
export default abstract class Entity extends Entity_base {
Expand Down
Loading