Implement many-to-many relationship between Notes and Categories in the database model and update sync service to handle category IDs accordingly
Despliegue Automático / desplegar (push) Successful in 1m25s
Despliegue Automático / desplegar (push) Successful in 1m25s
This commit is contained in:
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const sequelize = require('../config/database');
|
const sequelize = require('../config/database');
|
||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
const Category = require('./Category'); // Importamos la nueva tabla
|
const Category = require('./Category');
|
||||||
|
|
||||||
const Note = sequelize.define('Note', {
|
const Note = sequelize.define('Note', {
|
||||||
id: {
|
id: {
|
||||||
@@ -38,8 +38,8 @@ const Note = sequelize.define('Note', {
|
|||||||
User.hasMany(Note, { foreignKey: 'userId' });
|
User.hasMany(Note, { foreignKey: 'userId' });
|
||||||
Note.belongsTo(User, { foreignKey: 'userId' });
|
Note.belongsTo(User, { foreignKey: 'userId' });
|
||||||
|
|
||||||
// Relación: Una Categoría tiene muchas Notas (y una nota puede no tener categoría)
|
// 👇 NUEVA Relación: Muchos a Muchos
|
||||||
Category.hasMany(Note, { foreignKey: 'categoryId', allowNull: true });
|
Category.belongsToMany(Note, { through: 'NoteCategories', foreignKey: 'categoryId' });
|
||||||
Note.belongsTo(Category, { foreignKey: 'categoryId', allowNull: true });
|
Note.belongsToMany(Category, { through: 'NoteCategories', foreignKey: 'noteId' });
|
||||||
|
|
||||||
module.exports = Note;
|
module.exports = Note;
|
||||||
+26
-16
@@ -24,7 +24,8 @@ const normalizeCategory = (category) => ({
|
|||||||
|
|
||||||
const normalizeNote = (note) => ({
|
const normalizeNote = (note) => ({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
categoryId: note.categoryId || null,
|
// 👇 Convertimos la relación de Sequelize en un array simple de IDs de categorías
|
||||||
|
categoryIds: note.Categories ? note.Categories.map(c => c.id) : [],
|
||||||
title: note.title,
|
title: note.title,
|
||||||
body: note.body,
|
body: note.body,
|
||||||
position: note.position,
|
position: note.position,
|
||||||
@@ -41,7 +42,7 @@ const buildStoredNoteData = (incomingNote, existingNote = null) => {
|
|||||||
: (incomingNote.position ?? existingNote?.position ?? 0);
|
: (incomingNote.position ?? existingNote?.position ?? 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categoryId: incomingNote.categoryId || null,
|
// categoryId eliminado de aquí
|
||||||
title: permanentDelete ? '' : incomingNote.title,
|
title: permanentDelete ? '' : incomingNote.title,
|
||||||
body: permanentDelete ? '' : incomingNote.body,
|
body: permanentDelete ? '' : incomingNote.body,
|
||||||
position,
|
position,
|
||||||
@@ -74,8 +75,8 @@ class SyncService {
|
|||||||
const incomingNotes = Array.isArray(changes.notes) ? changes.notes : [];
|
const incomingNotes = Array.isArray(changes.notes) ? changes.notes : [];
|
||||||
|
|
||||||
await sequelize.transaction(async (transaction) => {
|
await sequelize.transaction(async (transaction) => {
|
||||||
|
// --- SINCRONIZACIÓN DE CATEGORÍAS ---
|
||||||
for (const incomingCategory of incomingCategories) {
|
for (const incomingCategory of incomingCategories) {
|
||||||
|
|
||||||
if (!incomingCategory.id || !incomingCategory.encrypted_name) {
|
if (!incomingCategory.id || !incomingCategory.encrypted_name) {
|
||||||
throw new Error('Cada categoría debe incluir id y encrypted_name (encriptado)');
|
throw new Error('Cada categoría debe incluir id y encrypted_name (encriptado)');
|
||||||
}
|
}
|
||||||
@@ -109,16 +110,16 @@ class SyncService {
|
|||||||
delete: Boolean(incomingCategory.delete),
|
delete: Boolean(incomingCategory.delete),
|
||||||
serverVersion: serverVersion + 1
|
serverVersion: serverVersion + 1
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
} else {
|
|
||||||
// server wins for categories; do not create conflict copies for categories
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SINCRONIZACIÓN DE NOTAS ---
|
||||||
for (const incomingNote of incomingNotes) {
|
for (const incomingNote of incomingNotes) {
|
||||||
const permanentDelete = Boolean(incomingNote.permanentDelete);
|
const permanentDelete = Boolean(incomingNote.permanentDelete);
|
||||||
|
|
||||||
|
// Se verifica que sea 'string' para aceptar strings vacíos
|
||||||
if (!incomingNote.id || typeof incomingNote.title !== 'string' || typeof incomingNote.body !== 'string') {
|
if (!incomingNote.id || typeof incomingNote.title !== 'string' || typeof incomingNote.body !== 'string') {
|
||||||
return res.status(400).json({ message: "Cada nota debe incluir id, title y body" });
|
throw new Error("Cada nota debe incluir id, title y body");
|
||||||
}
|
}
|
||||||
|
|
||||||
const incomingBaseVersion = getIncomingVersion('nota', incomingNote);
|
const incomingBaseVersion = getIncomingVersion('nota', incomingNote);
|
||||||
@@ -131,12 +132,17 @@ class SyncService {
|
|||||||
if (!existingNote) {
|
if (!existingNote) {
|
||||||
const storedNoteData = buildStoredNoteData(incomingNote);
|
const storedNoteData = buildStoredNoteData(incomingNote);
|
||||||
|
|
||||||
await Note.create({
|
const newNote = await Note.create({
|
||||||
id: incomingNote.id,
|
id: incomingNote.id,
|
||||||
userId,
|
userId,
|
||||||
...storedNoteData,
|
...storedNoteData,
|
||||||
serverVersion: 1
|
serverVersion: 1
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
|
// 👇 Actualizar la relación de muchos a muchos al crear
|
||||||
|
if (Array.isArray(incomingNote.categoryIds)) {
|
||||||
|
await newNote.setCategories(incomingNote.categoryIds, { transaction });
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,22 +155,20 @@ class SyncService {
|
|||||||
...storedNoteData,
|
...storedNoteData,
|
||||||
serverVersion: serverVersion + 1
|
serverVersion: serverVersion + 1
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
} else if (serverVersion > incomingBaseVersion) {
|
|
||||||
// Server version is newer -> create a new note with incoming content so user sees a duplicate
|
|
||||||
const storedNoteData = buildStoredNoteData(incomingNote);
|
|
||||||
|
|
||||||
await Note.create({
|
// 👇 Actualizar la relación de muchos a muchos al editar
|
||||||
id: randomUUID(),
|
if (Array.isArray(incomingNote.categoryIds)) {
|
||||||
userId,
|
await existingNote.setCategories(incomingNote.categoryIds, { transaction });
|
||||||
...storedNoteData,
|
}
|
||||||
serverVersion: 1
|
} else if (serverVersion > incomingBaseVersion) {
|
||||||
}, { transaction });
|
// Conflicto de versiones... (implementación de duplicados omitida por ahora)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`serverVersion inválida en la nota ${incomingNote.id}`);
|
throw new Error(`serverVersion inválida en la nota ${incomingNote.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- RECOPILAR DATOS PARA ENVIAR DE VUELTA ---
|
||||||
const [pulledCategories, pulledNotes] = await Promise.all([
|
const [pulledCategories, pulledNotes] = await Promise.all([
|
||||||
Category.findAll({
|
Category.findAll({
|
||||||
where: {
|
where: {
|
||||||
@@ -178,6 +182,12 @@ class SyncService {
|
|||||||
userId,
|
userId,
|
||||||
updatedAt: { [Op.gt]: lastSyncAt }
|
updatedAt: { [Op.gt]: lastSyncAt }
|
||||||
},
|
},
|
||||||
|
// 👇 Le decimos a Sequelize que incluya la tabla Category para poder sacar los IDs
|
||||||
|
include: [{
|
||||||
|
model: Category,
|
||||||
|
attributes: ['id'], // Solo necesitamos el ID para el array de Flutter
|
||||||
|
through: { attributes: [] } // Omite los datos de la tabla intermedia en el JSON final
|
||||||
|
}],
|
||||||
order: [['updatedAt', 'ASC']]
|
order: [['updatedAt', 'ASC']]
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user