Saltar al contenido principal

Collection

Collections definen listas (Array) o mapas (Values) mutables.

Esto significa que pueden crecer y encogerse. Puedes añadir a un Collection(Array) con .push o .unshift, eliminar de un Collection(Array) con .remove, añadir a un Collections(Values) con .assign, y mover elementos entre collections con .move.

RestEndpoint proporciona .push, .unshift, .assign, .remove, .move y los extensores .getPage/ .paginated() cuando se usan Collections

Uso​

import React from 'react';
import { useController } from '@data-client/react';
import { getTodos } from './api/Todo';

export default function NewTodo({ userId }: { userId?: string }) {
  const ctrl = useController();
  const [unshift, setUnshift] = React.useState(false);

  const handlePress = async e => {
    if (e.key === 'Enter') {
      const createTodo = unshift ? getTodos.unshift : getTodos.push;
      ctrl.fetch(createTodo, {
        title: e.currentTarget.value,
        userId,
      });
      e.currentTarget.value = '';
    }
  };

  return (
    <div className="listItem nogap">
      <TextInput size="small" onKeyDown={handlePress} />
      <label>
        <input
          type="checkbox"
          checked={unshift}
          onChange={e => setUnshift(e.currentTarget.checked)}
        />{' '}
        unshift
      </label>
    </div>
  );
}
Resultado
Store▶

Collection con Values​

Cuando una API devuelve objetos con claves en lugar de arrays, combina Collection con Values para habilitar mutaciones sobre el resultado.

import { Entity, resource, Collection, Values } from '@data-client/rest';

class Stats extends Entity {
product_id = '';
volume = 0;
price = 0;

pk() {
return this.product_id;
}

static key = 'Stats';
}

export const StatsResource = resource({
urlPrefix: 'https://api.exchange.example.com',
path: '/products/:product_id/stats',
schema: Stats,
}).extend({
getList: {
path: '/products/stats',
// Collection wraps Values to enable .push, .assign, etc.
schema: new Collection(new Values(Stats)),
process(value) {
// Transform nested response structure
Object.keys(value).forEach(key => {
value[key] = {
...value[key].stats_24hour,
product_id: key,
};
});
return value;
},
},
});

Esto permite añadir o actualizar entradas con .assign. El body es un objeto cuyas claves son las claves de la collection y cuyos valores son los datos de la entity que se van a fusionar:

// Local-only update with ctrl.set()
ctrl.set(StatsResource.getList.schema.assign, {}, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
});

// Network request with ctrl.fetch() - see RestEndpoint.assign
await ctrl.fetch(StatsResource.getList.assign, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
});

Opciones​

argsKey y nestKey calculan el pk de un Collection. argsKey se usa cuando un Collection se normaliza como resultado de un endpoint de nivel superior; nestKey se usa cuando el mismo Collection está anidado en una Entity. Proporciona ambos para reutilizar una misma definición de Collection en los dos contextos.

argsKey(...args): Object​

Devuelve un Object serializable cuyos miembros definen de forma única esta collection según los argumentos del Endpoint.

import { RestEndpoint, Collection } from '@data-client/rest';

const userTodos = new Collection([Todo], {
argsKey: (urlParams: { userId?: string }) => ({
...urlParams,
}),
nestKey: (parent: { id: string }) => ({
userId: parent.id,
}),
});

const getTodos = new RestEndpoint({
path: '/todos',
searchParams: {} as { userId?: string },
schema: userTodos,
});

Cuando se omite, argsKey toma por defecto params => ({ ...params }).

nestKey(parent, key): Object​

Devuelve un Object serializable cuyos miembros definen de forma única esta collection según el padre dentro del que está anidada.

El pk de un Collection anidado suele definirse mejor por aquello dentro de lo que está anidado. Esto permite que las instancias anidadas de Collection compartan estado cuando sus claves tienen el mismo valor. Cuando argsKey y nestKey devuelven objetos con la misma forma, las lecturas de nivel superior y las anidadas se resuelven al mismo estado de la collection.

import { Entity } from '@data-client/rest';
import { Todo, userTodos } from './Todo';

class User extends Entity {
id = '';
name = '';
username = '';
email = '';
todos: Todo[] = [];

static key = 'User';
static schema = {
todos: userTodos,
};
}

En este caso, user.todos y la respuesta de getTodos() del ejemplo de argsKey son siempre el mismo array (iguales por referencia). Añade ambas funciones de clave a la definición compartida de Collection:

const userTodos = new Collection([Todo], {
argsKey: ({ userId }: { userId?: string }) => ({ userId }),
nestKey: (parent: User) => ({ userId: parent.id }),
});

Opción nonFilterArgumentKeys?​

Una alternativa cómoda a argsKey

nonFilterArgumentKeys define una prueba para determinar qué claves de argumentos no se usan para filtrar los resultados. Por ejemplo, si tu API usa 'orderBy' para elegir un orden, este argumento no influiría en qué entities se incluyen en la respuesta.

const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys(key) {
return key === 'orderBy';
},
}),
});

Por comodidad, también puedes usar una RegExp o una lista de strings:

const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys: /orderBy/,
}),
});
const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: string; author?: string },
schema: new Collection([Post], {
nonFilterArgumentKeys: ['orderBy'],
}),
});

En este caso, author y group se consideran claves de argumentos de 'filtro', lo que significa que influirán en si un elemento recién creado debe añadirse a esas listas. En cambio, orderBy no necesita coincidir cuando se llama a push.

import { Entity, Query, Collection, RestEndpoint } from '@data-client/rest';

class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';
}
export const getPosts = new RestEndpoint({
  path: '/:group/posts',
  searchParams: {} as { orderBy?: string; author?: string },
  schema: new Query(
    new Collection([Post], {
      nonFilterArgumentKeys: /orderBy/,
    }),
    (posts, { orderBy } = {}) => {
      if (orderBy) {
        return [...posts].sort((a, b) => a[orderBy].localeCompare(b[orderBy]));
      }
      return posts;
    },
  )
});
Resultado
Store▶

Opción createCollectionFilter?​

Establece un createCollectionFilter por defecto para addWith(), push, unshift y assign.

Estos schemas de creación lo usan para determinar a qué collections se debe añadir.

Por defecto:

createCollectionFilter(...args: Args) {
return (collectionKey: Record<string, string>) =>
Object.entries(collectionKey).every(
([key, value]) =>
this.nonFilterArgumentKeys(key) ||
// strings are canonical form. See pk() above for value transformation
`${args[0][key]}` === value ||
`${args[1]?.[key]}` === value,
);
}

Métodos​

Estos schemas de creación/eliminación se pueden usar con Controller.set() para actualizaciones solo locales sin peticiones de red. Para mutaciones basadas en red, consulta los extensores especializados de RestEndpoint.

push​

Un schema de creación que coloca los nuevos elementos al final de esta collection.

// Add a new todo to the end of the list (local only, no network request)
ctrl.set(getTodos.schema.push, { userId: '1' }, { id: '999', title: 'New Todo' });

unshift​

Un schema de creación que coloca los nuevos elementos al principio de esta collection.

// Add a new todo to the beginning of the list (local only)
ctrl.set(getTodos.schema.unshift, { userId: '1' }, { id: '999', title: 'New Todo' });

remove​

Un schema que elimina elementos de una collection por valor.

El valor de la entity se normaliza para extraer su pk, que luego se compara con los miembros de la collection. Los elementos se eliminan de todas las collections que coinciden con los args proporcionados (filtradas por createCollectionFilter).

// Remove from collections matching { userId: '1' } (local only)
ctrl.set(getTodos.schema.remove, { userId: '1' }, { id: '123' });
// Remove from all collections (empty args matches all)
ctrl.set(getTodos.schema.remove, {}, { id: '123' });

Para una eliminación basada en red que además actualiza la entity, consulta RestEndpoint.remove.

move​

Un schema que mueve elementos entre collections. Elimina la entity de las collections que coinciden con su estado existente y la añade a las collections que coinciden con el nuevo estado de la entity (derivado del último arg).

Funciona tanto con Collection(Array) como con Collection(Values).

// Move todo from userId '1' collection to userId '2' collection (local only)
ctrl.set(
getTodos.schema.move,
{ id: '10', userId: '2', title: 'Moved todo' },
[{ id: '10' }, { userId: '2' }],
);

El filtro de eliminación usa los valores existentes de la entity en el store para determinar a qué collections pertenece actualmente. El filtro de adición usa los valores fusionados de la entity (existentes + último arg) para determinar dónde debe colocarse.

Para movimientos basados en red, consulta RestEndpoint.move.

assign​

Un schema de creación que asigna sus miembros a un Collection(Values). Solo está disponible para Collections que envuelven Values.

const getStats = new RestEndpoint({
path: '/products/stats',
schema: new Collection(new Values(Stats)),
});

// Add/update entries in a Values collection (local only)
ctrl.set(getStats.schema.assign, {}, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
'ETH-USD': { product_id: 'ETH-USD', volume: 500 },
});

addWith(merge, createCollectionFilter): CreationSchema​

Construye un schema de creación personalizado para esta collection. Lo usan push, unshift, assign y paginate

merge(collection, creation)​

Esto fusiona el valor con la collection existente

createCollectionFilter​

Esta función se usa para determinar a qué collections se debe añadir. Usa el Object devuelto por argsKey o nestKey para determinar si esa collection debe recibir los valores recién creados de este schema.

Como los argumentos pueden ser tipos serializables como number, recomendamos usar comparaciones con ==, por ejemplo, '10' == 10

(...args) =>
collectionKey =>
boolean;

moveWith(merge): MoveSchema​

Construye un schema de movimiento personalizado para esta collection. Es análogo a addWith pero para operaciones de move. La función merge controla cómo se añaden las entities a su collection de destino, mientras que el comportamiento de eliminación se deriva automáticamente del tipo de collection (Array o Values).

Esto es útil cuando necesitas controlar la posición de inserción de los elementos movidos (por ejemplo, anteponer en lugar de añadir al final).

merge(collection, moved)​

Controla cómo se añade la entity movida a su collection de destino.

La función de fusión unshift exportada coloca los elementos al principio:

import { Collection, unshift, type CollectionOptions } from '@data-client/rest';
import type { PolymorphicInterface } from '@data-client/endpoint';

class MyCollection<
S extends any[] | PolymorphicInterface = any,
Args extends any[] = any[],
Parent = any,
> extends Collection<S, Args, Parent> {
constructor(schema: S, options?: CollectionOptions<Args, Parent>) {
super(schema, options);
// Prepend moved items instead of appending
this.move = this.moveWith(unshift);
}
}

unshift (función de fusión)​

Una función de fusión que coloca los elementos entrantes al principio de la collection. Úsala con moveWith o addWith para controlar el orden de inserción.

import { unshift } from '@data-client/rest';

Métodos del ciclo de vida​

Método estático shouldReorder(existingMeta, incomingMeta, existing, incoming): boolean​

static shouldReorder(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return incomingMeta.fetchedAt < existingMeta.fetchedAt;
}

Un valor de retorno true reordenará el orden de los argumentos de la entity entrante frente a la almacenada en el store durante la fusión. Con la fusión por defecto, esto hará que los campos de las entities existentes sobrescriban los de las entrantes, en lugar de al revés.

Método estático merge(existing, incoming): mergedValue​

static merge(existing: any, incoming: any) {
return incoming;
}

Método estático mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue​

static mergeWithStore(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
): any;

mergeWithStore() se llama durante la normalización cuando una entity procesada ya se encuentra en el store.

pk: (parent?, key?, args?, parentEntity?): pk?​

pk() llama a nestKey cuando está anidado en una Entity y está disponible; en caso contrario llama a argsKey. Luego serializa el resultado para obtener el string del pk.

pk(
value: any,
parent: any,
key: string,
args: readonly any[],
parentEntity?: any,
) {
const obj =
parentEntity && this.nestKey
? this.nestKey(parent, key)
: this.argsKey(...args);
for (const key in obj) {
if (typeof obj[key] !== 'string') obj[key] = `${obj[key]}`;
}
return JSON.stringify(obj);
}