# Invalidate

Describes entities to be marked as [INVALID](https://dataclient.io/docs/concepts/expiry-policy.md#invalid). This removes items from a
collection, or [forces suspense](https://dataclient.io/docs/concepts/expiry-policy.md#invalidate-entity) for endpoints where the entity is required.

## Constructor

```typescript
new Invalidate(entity)
new Invalidate(union)
new Invalidate(entityMap, schemaAttribute)
```

- `entity`: A singular [Entity](https://dataclient.io/rest/api/Entity.md) to invalidate.
- `union`: A [Union](https://dataclient.io/rest/api/Union.md) schema for polymorphic invalidation.
- `entityMap`: A mapping of schema keys to [Entities](https://dataclient.io/rest/api/Entity.md).
- `schemaAttribute`: _optional_ (required if `entityMap` is used) The attribute on each entity found that defines what schema, per the entityMap, to use when normalizing.
  Can be a string or a function. If given a function, accepts the following arguments:
  - `value`: The input value of the entity.
  - `parent`: The parent object of the input array.
  - `key`: The key at which the input array appears on the parent object.

## Usage

```typescript title="api/User"
import { Entity, RestEndpoint, Collection, Invalidate } from '@data-client/rest';

class User extends Entity {
  id = '';
  name = '';
}
export const getUsers = new RestEndpoint({
  path: '/users',
  schema: new Collection([User]),
});
export const deleteUser = new RestEndpoint({
  path: '/users/:id',
  method: 'DELETE',
  schema: new Invalidate(User),
});
```

```tsx title="UserPage"
import { useSuspense, useController } from '@data-client/react';
import { getUsers, deleteUser } from './api/User';

function UsersPage() {
  const users = useSuspense(getUsers);
  const ctrl = useController();
  return (
    <div>
      {users.map(user => (
        <div key={user.pk()}>
          {user.name}{' '}
          <span
            style={{ cursor: 'pointer' }}
            onClick={() => ctrl.fetch(deleteUser, { id: user.id })}
          >
            ❌
          </span>
        </div>
      ))}
    </div>
  );
}
render(<UsersPage />);
```

### Batch Invalidation

Here we add another endpoint for deleting many entities at a time by wrapping
`Invalidate` in an array. `Data Client` can then `invalidate` every
entity from the response.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  author = '';
}
```

```typescript title="Resource" {9}
import { resource, Invalidate } from '@data-client/rest';
import Post from './Post';

export const PostResource = resource({
  schema: Post,
  path: '/posts/:id',
}).extend('deleteMany', {
  path: '/posts',
  body: [] as string[],
  method: 'DELETE',
  schema: [new Invalidate(Post)],
});
```

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.deleteMany(['5', '13', '7']);
```

Sometimes our backend returns nothing for 'DELETE'. In this
case, we can use [process](https://dataclient.io/rest/api/RestEndpoint.md#process) to build
a usable response from the argument `body`.

```typescript title="Post"
import { Entity } from '@data-client/rest';

export default class Post extends Entity {
  id = '';
  title = '';
  author = '';
}
```

```typescript title="Resource" {10-13}
import { resource, Invalidate } from '@data-client/rest';
import Post from './Post';

export const PostResource = resource({
  schema: Post,
  path: '/posts/:id',
}).extend('deleteMany', {
  path: '/posts',
  body: [] as string[],
  method: 'DELETE',
  schema: [new Invalidate(Post)],
  process(value, body) {
    // use the body payload to inform which entities to delete
    return body.map(id => ({ id }));
  }
});
```

```typescript title="Usage" column
import { PostResource } from './Resource';
PostResource.deleteMany(['5', '13', '7']);
```

To delete many entities without an endpoint, such as from a websocket message, pass the same schema to
[Controller.set()](https://dataclient.io/docs/api/Controller.md#set-array):

```ts
ctrl.set([new Invalidate(Post)], [{ id: '5' }, { id: '13' }, { id: '7' }]);
```

Or delete a single entity:

```ts
ctrl.set(new Invalidate(Post), { id: '5' });
```

### Polymorphic types

If your endpoint can delete more than one type of entity, you can use polymorphic invalidation.

#### With Union schema

The simplest approach is to pass an existing [Union](https://dataclient.io/rest/api/Union.md) schema directly:

```typescript
import { Entity, RestEndpoint, Union, Invalidate } from '@data-client/rest';

class User extends Entity {
  id = '';
  name = '';
  readonly type = 'users';
}
class Group extends Entity {
  id = '';
  groupname = '';
  readonly type = 'groups';
}

const MemberUnion = new Union(
  { users: User, groups: Group },
  'type'
);

const deleteMember = new RestEndpoint({
  path: '/members/:id',
  method: 'DELETE',
  schema: new Invalidate(MemberUnion),
});
```

#### string schemaAttribute

Alternatively, define the polymorphic mapping inline with a string attribute:

```typescript
import { RestEndpoint, Invalidate } from '@data-client/rest';

const deleteMember = new RestEndpoint({
  path: '/members/:id',
  method: 'DELETE',
  schema: new Invalidate(
    { users: User, groups: Group },
    'type'
  ),
});
```

#### function schemaAttribute

The return values should match a key in the entity map. This is useful for more complex discrimination logic:

```typescript
import { RestEndpoint, Invalidate } from '@data-client/rest';

const deleteMember = new RestEndpoint({
  path: '/members/:id',
  method: 'DELETE',
  schema: new Invalidate(
    { users: User, groups: Group },
    (input, parent, key) => input.memberType === 'user' ? 'users' : 'groups'
  ),
});
```

### Impact on useSuspense()

When entities are invalidated in a result currently being presented in React, useSuspense()
will consider them invalid

- For optional Entities, they are simply removed
- For required Entities, this invalidates the entire response re-triggering suspense.
