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> ); }
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; }, ) });
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);
}