Skip to content

Commit d84899d

Browse files
authored
feat: Support dynamic invalidation/deletes (#3407)
1 parent d085b5e commit d84899d

File tree

6 files changed

+101
-1
lines changed

6 files changed

+101
-1
lines changed

.changeset/wide-berries-smell.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@data-client/endpoint': patch
3+
'@data-client/graphql': patch
4+
'@data-client/rest': patch
5+
---
6+
7+
Support dynamic invalidation/deletes
8+
9+
Returning `undefined` from [Entity.process](https://dataclient.io/rest/api/Entity#process)
10+
will cause the [Entity](https://dataclient.io/rest/api/Entity) to be [invalidated](https://dataclient.io/docs/concepts/expiry-policy#invalidate-entity).
11+
This this allows us to invalidate dynamically; based on the particular response data.
12+
13+
```ts
14+
class PriceLevel extends Entity {
15+
price = 0;
16+
amount = 0;
17+
18+
pk() {
19+
return this.price;
20+
}
21+
22+
static process(
23+
input: [number, number],
24+
parent: any,
25+
key: string | undefined,
26+
): any {
27+
const [price, amount] = input;
28+
if (amount === 0) return undefined;
29+
return { price, amount };
30+
}
31+
}
32+
```

docs/rest/api/Entity.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,34 @@ class Stream extends Entity {
411411
}
412412
```
413413

414+
#### Dynamic Invalidation
415+
416+
Returning `undefined` from [Entity.process](https://dataclient.io/rest/api/Entity#process)
417+
will cause the [Entity](https://dataclient.io/rest/api/Entity) to be [invalidated](https://dataclient.io/docs/concepts/expiry-policy#invalidate-entity).
418+
This this allows us to invalidate dynamically; based on the particular response data.
419+
420+
```ts
421+
class PriceLevel extends Entity {
422+
price = 0;
423+
amount = 0;
424+
425+
pk() {
426+
return this.price;
427+
}
428+
429+
static process(
430+
input: [number, number],
431+
parent: any,
432+
key: string | undefined,
433+
): any {
434+
const [price, amount] = input;
435+
// highlight-next-line
436+
if (amount === 0) return undefined;
437+
return { price, amount };
438+
}
439+
}
440+
```
441+
414442
### static mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue {#mergeWithStore}
415443

416444
```typescript

packages/endpoint/src/schemas/EntityMixin.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
Visit,
77
} from '../interface.js';
88
import { AbstractInstanceType } from '../normal.js';
9+
import { INVALID } from '../special.js';
910
import type {
1011
IEntityClass,
1112
IEntityInstance,
@@ -252,7 +253,13 @@ export default function EntityMixin<TBase extends Constructor>(
252253
checkLoop: CheckLoop,
253254
): any {
254255
const processedEntity = this.process(input, parent, key, args);
255-
let id = this.pk(processedEntity, parent, key, args);
256+
let id: string | number | undefined;
257+
if (typeof processedEntity === 'undefined') {
258+
id = this.pk(input, parent, key, args);
259+
addEntity(this, INVALID, id);
260+
return id;
261+
}
262+
id = this.pk(processedEntity, parent, key, args);
256263
if (id === undefined || id === '' || id === 'undefined') {
257264
// create a random id if a valid one cannot be computed
258265
// this is useful for optimistic creates that don't need real ids - just something to hold their place

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,39 @@ describe(`${Entity.name} normalization`, () => {
590590
expect(final?.type).toEqual('message');
591591
expect(final).toMatchSnapshot();
592592
});
593+
594+
test('when undefined is returned, INVALIDate the entity', () => {
595+
class ProcessTaco extends Tacos {
596+
readonly slug: string = '';
597+
static process(input: any, parent: any, key: string | undefined): any {
598+
if (input.id === 'DELETE') return undefined;
599+
return {
600+
...input,
601+
slug: `thing-${(input as unknown as ProcessTaco).id}`,
602+
};
603+
}
604+
}
605+
let { entities, result } = normalize(ProcessTaco, {
606+
id: 'DELETE',
607+
name: 'foo',
608+
});
609+
let final = new SimpleMemoCache().denormalize(
610+
ProcessTaco,
611+
result,
612+
entities,
613+
);
614+
expect(final).toEqual(expect.any(Symbol));
615+
616+
// still work in normal cases
617+
({ entities, result } = normalize(ProcessTaco, {
618+
id: '1',
619+
name: 'foo',
620+
}));
621+
final = new SimpleMemoCache().denormalize(ProcessTaco, result, entities);
622+
expect(final).not.toEqual(expect.any(Symbol));
623+
if (typeof final === 'symbol') return;
624+
expect(final?.slug).toEqual('thing-1');
625+
});
593626
});
594627
});
595628

packages/endpoint/src/schemas/__tests__/EntitySchema.test.ts renamed to packages/endpoint/src/schemas/__tests__/EntityMixin.test.ts

File renamed without changes.

packages/endpoint/src/schemas/__tests__/__snapshots__/EntitySchema.test.ts.snap renamed to packages/endpoint/src/schemas/__tests__/__snapshots__/EntityMixin.test.ts.snap

File renamed without changes.

0 commit comments

Comments
 (0)