Explanation order collection code
Explicación de la lógica para ordenar colecciones
Section titled “Explicación de la lógica para ordenar colecciones”Guía detallada para entender el funcionamiento de ordenación e implementarlo en un lifecycle.
Objetivo
Section titled “Objetivo”Ordenar las colecciones gracias al campo order para que puedan tener un sentido lógico.
Esta función incluye cambios cuando se crea, actualiza y borra las colecciones. Además controla que el status (Draft, Modified, Published) de cada colección se mantenga a pesar de cambiar su campo order.
Funcionamiento del código
Section titled “Funcionamiento del código”createOrderCollection
Section titled “createOrderCollection”La función recibe los siguientes parámetros:
- numOrder = el campo order que pone el usuario.
- documentId = es un id correspondiente a la colección que esta siendo creada/editada.
- collection = es el tipo de colección correspondiente para aplicar los cambios.
- collectionDraft = es un boolean que indica si se esta publicando o no.
export async function createOrderCollection( numOrder, documentId, collection, coleccionDraft);Al principio realizamos una búsqueda con findMany para tener las colecciones actualmente existentes.
Antes de pasar a ordenar se tiene que pasar por algunas validaciones:
- En caso de que se esté creando en algún idioma diferente al local.
- En caso de que el order que se este pasando sea null, se le asignará automáticamente la última posición.
- En caso de que la colección este en Draft o Modified.
const entries = await findMany(collection);
if (entries[0].locale !== DEFAULT_LOCALE) { return;}
if (numOrder === null) { assignLastOrderNumber(collection, documentId, entries); return;}
if (!coleccionDraft) { return;}Pasadas las validaciones duplicamos las colecciones que existen para poder encontrar la colección que coincida con el documentId que recibe la función.
const collections = [...entries];
const collectionEdited = collections.find( (collection) => collection.documentId === documentId);Ahora se tiene un array llamado otherCollections con las colecciones existentes menos la descartada. Además dicho array se ordenará teniendo en cuenta el order que tengan, si existe un empate se ordenará por el más antiguo que se actualizó.
const otherCollections = collections.filter( (collection) => collection.documentId !== documentId);
otherCollections.sort((firstCollection, secondCollection) => { if (firstCollection.order !== secondCollection.order) return firstCollection.order - secondCollection.order; return ( new Date(firstCollection.updatedAt).getTime() - new Date(secondCollection.updatedAt).getTime() );});Una vez ordenado se coloca cada colección en un nuevo array que tenga un newOrder empezando desde el 1, en caso de que el currentOrder coincida con el order de la colección descartada se pondrá en el array como corresponde ya que ese order no debe ser cambiado. Si se ha terminado el for y todavía no se ha insertado el elemento descartado significa que es el último, por lo que se agrega al nuevo array en la última posicion.
const result = [];let currentOrder = 1;let inserted = false;
for (const item of otherCollections) { if (!inserted && currentOrder === collectionEdited.order) { result.push({ ...collectionEdited, newOrder: currentOrder++ }); inserted = true; } result.push({ ...item, newOrder: currentOrder++ });}
if (!inserted) { result.push({ ...collectionEdited, newOrder: currentOrder++ });}Finalmente se realiza el update en la base de datos en caso de que el order y newOrder no coincidan. Además se le pasa un flag llamado _internalUpdate que sirve para que los lifecycles no se ejecuten más veces de las necesarias.
const updatePromises = [];
for (const item of result) { if (item.order !== item.newOrder) { const where = { documentId: item.documentId, }; const data = { order: item.newOrder, _internalUpdate: true, }; updatePromises.push(updateAndPublish(collection, where, data)); }}await Promise.all(updatePromises);updateOrderCollection
Section titled “updateOrderCollection”La función recibe los siguientes parámetros:
- numOrder = el campo order que pone el usuario.
- documentId = es un id correspondiente a la colección que esta siendo creada/editada.
- collection = es el tipo de colección correspondiente para aplicar los cambios.
- previousOrder = es el anterior order que tenía la colección antes de ser editado.
export async function updateOrderCollection( numOrder, documentId, collection, previousOrder){A diferencia de la anterior función aquí solamente se tienen dos validaciones:
- En caso de que se este editando en un idioma diferente al local.
- En el caso especial de que el numOrder sea null y el previousOrder sea cualquier número diferente a null. En este último caso se tiene una función (assignPreviousOrder) para devolver de manera automática el anterior order que tenia la colección.
const entries = await findMany(collection);
if (entries[0].locale !== DEFAULT_LOCALE) { return 0; }
if (numOrder === null && previousOrder !== null) { assignPreviousOrder(collection, documentId, previousOrder); return 0; }}En este método no existe lógica de ordenación ya que al querer actualizar los cambios en el order únicamente cuando la colección se encuentra en status Published. Solamente se realizará la lógica de ordenación en la función createOrderCollection.
deleteOrderCollection
Section titled “deleteOrderCollection”La función recibe los siguientes parametros:
- collection = es el tipo de colección correspondiente para aplicar los cambios.
- documentId = es un id correspondiente a la colección que esta siendo creada/editada.
- order = el campo order que pone el usuario.
export async function deleteOrderCollection(collection, documentId, order) {Al principio se busca mediante un findOne las versiones publicada y draft de la colección correspondiente al documentId que se le pasa en la cabecera. Para verificar que sea un delete completo y no se trate de procesos como unpublish o una republicacion.
const versionPublish = await findOne(collection, [documentIdSelect], where, []);
const versionDraft = await findOne( collection, [documentIdSelect], whereDraft, []);
if (versionPublish === NOT_FOUND && versionDraft !== NOT_FOUND) { return;}Luego se busca mediante un filter las colecciones que tengan un order mayor que el order pasado en la cabecera de la funcion.
const filters = { order: { $gt: order },};
const entriesToUpdate = await findMany(collection, null, filters, null);Para finalmente restarle el order a todas las colecciones encontradas
const updatePromises = [];for (const item of entriesToUpdate) { const where = { documentId: item.documentId }; const newOrder = item.order - 1;
const data = { order: newOrder, _internalUpdate: true, }; updatePromises.push(updateAndPublish(collection, where, data));}await Promise.all(updatePromises);Bug descubierto
Section titled “Bug descubierto”Durante la realización del código, se ha descubierto un bug a la hora de actualizar el order de las colecciones en el método de createOrderCollection y deleteOrderCollection. Especificamente el metodo updateAndPublish, que es el encargado de actualizar el order y controlar que el status de cada colección se mantenga:
async function updateAndPublish(collection, where, data);El error que ha mostrado es el siguiente:
Error: Transaction query already complete, run with DEBUG=knex:tx for more infoPor lo que por el momento para conservar su uso y utilización se ha decidido crear una función update personalizada dentro del archivo orderCollection
async function update(collection, where, data) { try { const updated = await strapi.db.query(collection).update({ where: where, data: data, }); if (!updated || updated.length === 0) { return NOT_FOUND; } else { return updated; } } catch (error) { console.log(ERROR_UPDATE + error); }}Implementacion en un lifecycle
Section titled “Implementacion en un lifecycle”Para implementar correctamente la lógica de ordenamiento en un lifecycle se necesita editar los siguientes hooks:
beforeCreate
Section titled “beforeCreate”Mediante la función isPublishingStatusUpdate se puede saber cuando una colección esta siendo publicada o no. Esta información se guardará en el event.state para poder recuperarla en el afterCreate.
const { data } = event.params;
const willBePublished = await isPublishingStatusUpdate(data, productStatusAt);
event.state = { ...event.state, isPublishing: willBePublished,};Además antes de pasar al after, se realizarán una serie de validaciones mediante la función specialChecksOrder.
await specialChecksOrder(EXPERT_SERVICE, data.order, false);Las validaciones son las siguientes:
Campo order | Resultado |
|---|---|
| N = 0 | Error, número 0 no válido |
| N = -1 | Error, solo se aceptan números positivos |
| N > length + 1 | Error, el número máximo para asignar es length + 1 |
| N = 2147483648 | Error, número fuera del rango entero permitido (int32) |
afterCreate
Section titled “afterCreate”En el after únicamente se le pasa los paramteros que necesita el metodo de createOrderCollection, asi como también se recupera del event.state del beforeCreate
if (event.result.locale !== DEFAULT_LOCALE) { return;}const { documentId, order } = event.result;
await createOrderCollection( order, documentId, EXPERT_SERVICE, event.state.isPublishing);beforeUpdate
Section titled “beforeUpdate”Al tratarse del update, al principio se necesita controlar si es un update interno con el flag que se ha creado (_internalUpdate) o si el cambio es de otro campo que no sea el order, esto se comprueba con la función hasFieldInPayload. Además de las validaciones pertinentes.
const { data, where } = event.params;
if (data._internalUpdate) { return;}
if (!hasFieldInPayload(event.params.data, orderSelect)) { return;}await specialChecksOrder(EXPERT_SERVICE, data.order, true);En este caso las validaciones tienen un ligero cambio.
Campo order | Resultado |
|---|---|
| N = 0 | Error, número 0 no válido |
| N = -1 | Error, solo se aceptan números positivos |
| N > length | Error, el número máximo para asignar es length |
| N = 9999999999 | Error, número fuera del rango entero permitido (int32) |
| Ya que al tratarse de editar colecciones el máximo order que se le puede asignar es el length de las colecciones. No como en el create que el máximo número es length + 1. | |
| Además es aquí donde se obtendrá el previousOrder que tenía la colección antes de ser modificado, mediante el método searchPreviousCollection que saca la coleccion anteriormente a ser actualizada. Y se pasará por el event.state para usarlo en el afterUpdate. |
const previousCollection = await searchPreviousCollection( EXPERT_SERVICE, where.id);
event.state = { previousOrder: previousCollection?.order,};afterUpdate
Section titled “afterUpdate”De igual forma que en el beforeUpdate primero comprobar que no se trate de un update interno o algún campo que se ha cambiado que no sea el order.
const { data } = event.params;const { documentId } = event.result;
if (data._internalUpdate) { return;}
if (!hasFieldInPayload(event.params.data, orderSelect)) { return;}Finalmente pasarle los parámetros necesarios a la funcion updateOrderCollection.
const { previousOrder } = event.state ?? {};const newOrder = data.order ?? null;
await updateOrderCollection( newOrder, documentId, EXPERT_SERVICE, previousOrder);afterDelete
Section titled “afterDelete”Como último hook, añadir los parametros necesarios para la función deleteOrderCollection.
afterDelete: async (event) => { await deleteOrderCollection( EXPERT_SERVICE, event.result.documentId, event.result.order, ); },