I am working with a Mongoose model, this is the model
import { Router, Request, Response } from 'express'
import UserModel from '../../../models/user.model'
import { error500 } from '../../../global/errors'
import AreaModel from '../../../models/area.model'
const UserChangesRoute = Router()
UserChangesRoute.get( '/:id', ( req: Request, res: Response ) => {
const id: String = req.params.id
const pop_user = { path: 'modification.user', select: 'name last_name' }
UserModel.find( { _id: id }, 'modification' ).sort({ 'modification.date': 1 })
.populate( pop_user )
.lean()
.exec( ( err: any, data: any ) => {
if( err ) {
return res.status( 500 ).json( { message: error500, err } )
}
// I used to: data = data.toObject()
for( let i = 0; i < data[0].modification.length; i++ ) {
if( data[0].modification[ i ][ 'updated' ][ 0 ] === 'area' ) {
AreaModel.findById( data[0].modification[ i ][ 'updated' ][ 1 ], 'name', ( e: any, area: any ) => {
if( !e ) data[0].modification[ i ][ 'updated' ][ 1 ] = area.name
});
AreaModel.findById( data[0].modification[ i ][ 'updated' ][ 2 ], 'name', ( e: any, area: any ) => {
if( !e ) data[0].modification[ i ][ 'updated' ][ 2 ] = area.name
});
}
}
res.status( 200 ).json( { data } )
})
})
export default UserChangesRoute
I'm trying to find and replace parts of the array where it comes data[0].modification
in Here inside stores something like this
[ 'area', '5d144642416ada46385d01b8', '5d144681416ada46385d01b9', 'Área'];
It should be like this in the end
['area', 'Sistemas', 'Recursos Humanos', 'Área'];
Index 1 indicates at the DB structure level that it was moved in the front. The second index is what area (in this case) was removed, the third index is what area was replaced, and the last one is just a tag for the front.
It is a history of changes.
What I'm trying to do is replace those Mongo ids with the name of the area, I tried to do it with a populate
, however it's not an array Schemas
and since it doesn't have a reference to the Areas model, I reiterate it's a history of changes, not only they save areas, if not roles, names, direct bosses, anyway... everything that is changed in the front, must be stored in that way, and each change is an $push
arrangement ofmodifications
Now, the problem is that Mongoose returns me a MongoDocument
, not a JSON
(No matter how similar it is) I need to parse that MongoDocument
a JSON
to be able to work with it.
This is the User model (Requested by Mauricio Contreras)
import mongos from 'mongoose'
import validator from 'mongoose-unique-validator'
const schema = new mongos.Schema({
name: {
type : String,
required : [ true, 'El nombre es necesario' ],
maxlength : [ 50, 'El nombre no puede exceder los 50 caracteres'],
minlength : [ 3, 'El nombre debe contener 3 o más caracteres']
},
last_name: {
type : String,
required : [ true, 'El apellido es necesario' ],
maxlength : [ 100, 'El apellido no puede exceder los 50 caracteres'],
minlength : [ 3, 'El apellido debe contener 3 o más caracteres'],
},
user_name: {
type: String,
unique : [ true, 'El usuario está duplicado' ],
required : [ true, 'El usuario es necesario' ],
maxlength : [ 25, 'El usuario no puede exceder los 25 caracteres'] },
email: {
type : String,
unique : [ true, 'El correo está duplicado'],
required : [ true, 'El correo es necesario' ],
maxlength : [ 100, 'El correo no puede exceder los 100 caracteres'],
match : [/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
'El correo electrónico no tiene el formato adecuado'] },
gender : { type: Number, min: 0, max: 2 },
photo : { type: String },
phone : { type: String },
role : { type: mongos.Schema.Types.ObjectId, ref: 'Role' },
area : { type: mongos.Schema.Types.ObjectId, ref: 'Area' },
boss : { type: mongos.Schema.Types.ObjectId, ref: 'User' },
permissions: [{
module : { type: mongos.Schema.Types.ObjectId, ref: 'Module' },
chmod : { type: String, minlength: 1, maxlength: 5, default: 'r' }
}],
password : { type: String },
status : { type: String, default: 'active' },
last_login : { type: Date },
addedBy : { type: String },
addedDate : { type: Date, default: Date.now },
modification : [{
_id : false,
user : { type: mongos.Types.ObjectId, ref: 'User' },
date : { type: Date, default: Date.now },
updated : { type: Array }
}]
}, { collection: 'users' })
schema.plugin( validator, { message: 'Ya existe el correo o ID {VALUE} en la base de datos' } )
const UserModel = mongos.model('User', schema )
export default UserModel
I tried to use lean() method but it is not working for me.
In the same way I used toObject() But it tells me it is not a function.
How can I replace data from a MongoDocument?
tl;dr
SHORT ANSWER
You can achieve what you set out to do using an aggregation method, I'll try to explain this to you in as much detail as possible. But it will be quite a long explanation.
I will use an aggregation method, which will carry out the task in stages, until the desired result is achieved. You can read more about aggregation in the MongoDB documentation .
Using the data provided in your question as a model, we are going to write an aggregation method that we will execute when the request for changes made to a user is made, as you have it in your route.
One of the main differences when using the aggregation API is that it will return a
Array
containing simple JSON documents, which you can then modify if you still need it. On the other hand, the queries (Queries
) made on the model, return an instance of the model (a Mongoose document in this case), and as such its methods and properties.Using Aggregation:
It can be a bit intimidating at first, however it is very easy once we understand the concepts of stages and pipeline , as part of the MongoDB Aggregation process .
In this way, the documents are returned
Array
as you wish according to the requirement of your question.LONG ANSWER
SHORT ANSWER EXPLANATION
I am opposed to answers like: copy and paste this solution , so I will give the explanation of the aggregation method in stages, so that you understand what we are doing.
Stage
$match
:The first stage receives as a parameter the
id
user document on which we want to perform the Aggregation process. As a note, the Mongoose Aggregation API does not performcast
data type conversion during pipeline stages, which is why we will use the cast from theString
returnreq.params.id
typeObjectId
of to the Mongo type.This stage returns the document whose field
_id
matches the value that we have passed as a parameter. It could be equated to the methodfindById(id)
.Stage
$project
:In this stage we will create the basic structure of the document that we want as a result. In this case I am taking as a reference what you raise in your question.
Since the field is
modification
of typeArray
, in order to work with it we will use the operator$map
, which iterates over each element of our array.For each element of the array, I'm going to indicate the fields that I want to be returned. Thus, if I want the field
user
, I must indicate it as:At this stage, the crucial thing is what we do with the field
updated
, which is a typeArray
.On said field we will use the operator
$map
and convert eachitem
(if possible) to a typeObjectId
. For this we will use the operator$convert
.What we'll do is try to convert each value of
updated
to an ObjectId type. I say try because not all the values in our array are valid strings to convert to aObjectId
Mongo type.Taking into account that the field
updated
contains a value similar to the following:It is clear that the first and last elements of our array are not valid values
String
to be converted toObjectId
. That is why we use the attributeonError
of our operator$convert
, which allows us to return a value in case of an error during the conversion:Now the field
updated
will have the following structure:The answer to this question is given in the stage
$lookup
.Stage
$unwind
:This stage will make things easier for us when working with the array data
modification
. What this stage does is create a document for each element present in our array, and the fieldmodification
is converted to a typedocument
.Since it is a bit difficult to understand the process until you see the result, I will leave some images that can clarify a bit what happens:
Before performing the stage
$unwind
, our aggregation looks like the following image:We can notice that there is only one document that contains all the modification data in the field
modification
.When performing the step
$unwind
, the result will be the following:Ahora claramente podemos observar que se han creado 3 documentos (1 por cada elemento del campo
modification
), además, el campomodification
pasó de ser un tipoArray
a ser un campo que contiene un documento.Ahora podremos trabajar sin problemas sobre el campo
updated
de cada documento.Etapa
$lookup
:En esta etapa lo que hacemos es algo parecido al JOIN de SQL. La idea es traer los datos de otra colección (aunque la misma no tenga una referencia directa con la que estamos procesando). Para ello debemos establecer el campo por el cual deseamos hacer la referencia.
Aquí cobra importancia la primera etapa de
$project
y la etapa de$unwind
, ya que vamos a buscar datos de la colecciónareas
usando como campo de referencia los valoresObjectId
del campoupdated
(el cual es un tipoArray
). Además, haremos una etapa$lookup
adicional para traer los datos de la colecciónusers
(si entiendo bien el propósito del campomodification
: se almacena el_id
del usuario que realizó la modificación).Siendo que
modification.updated
es un campo de tipoArray
, el proceso de$lookup
se llevará a cabo sobre cada elemento de dichoArray
, cada documento devuelto por$lookup
será añadido a unArray
llamadoupdated
que es el nombre que le hemos pasado como valor al atributoas
.Un ejemplo de nuestro documento hasta este momento sería el siguiente:
Como se aprecia en la imagen (sólo se muestra el primer documento), se han añadido 2 campos:
user
yupdated
, ambos contienen documentos de las coleccionesusers
yareas
que corresponden a los_id
sobre los que se ha hecho el$lookup
. Para este ejemplo, el modeloArea
es muy básico, solo contiene el camponame
, nuestro modeloUser
también es básico para usarlo en este ejemplo.Segunda etapa
$project
Ahora que ya tenemos los datos que necesitamos, pasaremos a la siguiente etapa. En esta, vamos a construir lo que se convertirá en nuestro documento final.
Tal vez visualmente esta etapa es muy compleja, pero lo cierto es que los procesos que allí se realizan son muy sencillos.
En la primera parte de la etapa declaramos el campo
modification
e incluimos dentro del mismo los siguientes valores:Debemos recordar que el campo
user
agregado en la etapa anterior es un tipoArray
que contiene los documentos encontrados al realizar la búsqueda sobre el campo_id
. Es por ello que se usa el operador$arrayElemAt
al cual se le pasa como parámetro la ruta delArray
($user
) y la posición del elemento que queremos obtener (en este caso el primer elemento). esto nos devolverá el documento que contiene los datos del usuario, algo equiparable al métodopopulate()
de Mongoose.Esto indica que se incluirá el campo
date
que hemos venido agregando en cada etapa.Por último, crearemos un campo llamado
updated
que contendrá los valores nuevos que hemos obtenido en la etapa de$lookup
.La construcción la haremos usando el operador
$map
, ya que el campoupdated
es de tipoArray
.El
Array
de entrada será el que hemos modificado en nuestra primera etapa de$project
, el cual contiene la siguiente estructura:Luego definiremos 2 variables:
Las mismas hacen referencia a los 2 documentos del campo
updated
que hemos creado durante la etapa de$lookup
. La idea es comparar el valor deObjectId
delArray
de origen con el valor deObjectId
del documento que hemos llenado de la colecciónareas
.Una vez establecidas nuestras variables realizaremos un retorno condicional, es decir, retornaremos un valor de acuerdo a si se cumple una condición o no. Para ello usaremos el operador
$cond
, usando la siguiente sintaxis:Nuestra expresión booleana será:
Donde
$$item
se refiere al valor del iterando de nuestroArray
de entrada, y$$from._id
será el valor del campo_id
de nuestro primer elemento.Si hay coincidencia devolvemos el valor de
$$from.name
, de lo contrario pasamos a la siguiente parte del condicional, donde realizaremos nuevamente una operación de comparación condicional para determinar si se devuelve el valor de$$to.name
o simplemente devolvemos el valor de$item
.Una vez realizadas todas estas tareas, el campo
updated
debe tener la siguiente estructura:Que es la estructura deseada.
Etapa
$sort
:Hasta ahora ya tenemos la estructura armada de nuestro documento, ahora vamos a ordenarla usando el campo
modification.date
en orden ascendente (fecha más antigua hacia la más reciente).Tercera etapa
$project
:En esta tercera y última etapa
$project
, vamos a sanitizar o sanear los datos que deseamos mostrar en el campouser
. Hasta ahora se muestran todos los campos del documento, pero deseamos mostrar solamente los campos_id
,name
ylast_name
. Es por ello que escribimos lo siguiente:En donde le indicamos a MongoDB que deseamos sólo el campo
name
y el campolast_name
. El campo_id
siempre se incluirá a menos que escribamos explícitamente:_id: 0
.Etapa
$group
:Esta será nuestra última etapa. Recordemos que durante la agregación separamos los documentos en tantos ítems como elementos tuviera el campo
modification
. Ahora es tiempo de reagrupar todos estos documentos, en uno solo y convertir nuevamente el campomodification
en unArray
, tal como aparece el documento original.Dado que todos los documentos creados en la etapa
$unwind
corresponden al mismo_id
de usuario, usaremos dicho campo como_id
de nuestra agrupación:En esta etapa le estamos diciendo a MongoDB que agrupe todos los documentos que contengan el mismo campo
_id
, y que en dicha agrupación, cree un campo llamadomodification
que será de tipoArray
ya que usaremos el operador$push
, para insertar cada valor del campomodification
de cada uno de los documentos que estamos agrupando.Dado que todos los documentos tienen el mismo
_id
, el resultado será un documento único, que contiene un campo de tipoArray
cuyos elementos se corresponden con los cambios realizados sobre el usuario, y donde hemos agregado los datos del usuario que realizó los cambios y además hemos cambiado los valores deid
de área por el nombre del área.Ejemplo de agregación antes de realizar la etapa de
$group
:Se puede apreciar que la agregación devuelve varios documentos, todos tienen el mismo valor para el campo
_id
.Ejemplo de agregación después de realizar la etapa de
$group
:Ahora se aprecia que la agregación devuelve un solo documento donde el campo
modification
es un tipoArray
, cuyos elementos se corresponden con cada modificación realizada sobre el usuario.RECOMENDACIONES FINALES
Una de las recomendaciones que puedo hacerte es que escribas la agregación como un método del modelo. De esta forma, puedes usarlo desde cualquier otro método en que lo necesites y así no tienes que repetir nada de código, sólo la llamada.
Puedes consultar la documentación sobre métodos de instancia de Mongoose, para mayor información, o consultar la API de mongoose sobre métodos de
Schema
.Por ejemplo podrías hacer lo siguiente en tu modelo:
user.model.js
Luego, para usar la agregación puedes hacer lo siguiente:
De esta forma tienes todo centralizado en el modelo
User
. Además, así como has escrito una agregación paraarea
, puedes escribirla pararoles
yjefes
, etc. Siempre que el valor deupdate
cumpla el formato especificado. Sólo cambias el valor de la colección sobre la que harás el$lookup
.Incluso puedes hacer una agregación que te devuelva todo, haciendo varias etapas
$lookup
, una por cada colección sobre la que desees obtener la información.Por ejemplo:
Como puedes ver, he encadenado varias etapas
$lookup
, cada una apunta a una colección diferente, luego debes ir refactorizando el código según sea necesario.La ventaja de usar la agregación sobre lo que intentabas hacer (realizando una consulta a la DB por cada
id
en el campoupdated
), es que la agregación implica una sola llamada a la base de datos, todos los procesos se realizan del lado del motor de base de datos, y en teoría serán más rápidos que realizar el proceso manualmente desde tu aplicación.Imagine that a user has 10 (to say few) entries in the field of modifications, that means that you must make 20 requests (2 for each entry) to the database to obtain the information about the name of the field associated with
id
each entry. entry. That's not practical at all, not counting the chaining you have to do, or the use of a functionasync
to be able to useawait
with DB calls.I hope this is a solution to your problem and helps you understand the world of aggregations a bit.