Refactor API documentation and model attributes for consistency: rename 'isDeleted' to 'delete' and 'encrypted_title'/'encrypted_body' to 'title'/'body'
Despliegue Automático / desplegar (push) Successful in 4m25s
Despliegue Automático / desplegar (push) Successful in 4m25s
This commit is contained in:
+21
-21
@@ -170,7 +170,7 @@ Endpoint único de sincronización offline-first.
|
|||||||
|
|
||||||
Autenticación requerida:
|
Autenticación requerida:
|
||||||
|
|
||||||
- Enviar `Authorization: Bearer <accessToken>` (obligatorio). El servidor ya no acepta `userId` en el body.
|
- Enviar `Authorization: Bearer <accessToken>` (obligatorio).
|
||||||
|
|
||||||
#### Request
|
#### Request
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ Body ejemplo:
|
|||||||
"id": "uuid-categoria-1",
|
"id": "uuid-categoria-1",
|
||||||
"encrypted_name": "texto_cifrado...",
|
"encrypted_name": "texto_cifrado...",
|
||||||
"serverVersion": 1,
|
"serverVersion": 1,
|
||||||
"isDeleted": false,
|
"delete": false,
|
||||||
"updatedAt": "2026-05-18T10:05:00.000Z"
|
"updatedAt": "2026-05-18T10:05:00.000Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -193,12 +193,12 @@ Body ejemplo:
|
|||||||
{
|
{
|
||||||
"id": "uuid-nota-1",
|
"id": "uuid-nota-1",
|
||||||
"categoryId": "uuid-categoria-1",
|
"categoryId": "uuid-categoria-1",
|
||||||
"encrypted_title": "titulo_cifrado...",
|
"title": "titulo_cifrado...",
|
||||||
"encrypted_body": "cuerpo_cifrado...",
|
"body": "cuerpo_cifrado...",
|
||||||
"serverVersion": 1,
|
"serverVersion": 1,
|
||||||
"position": 2000,
|
"position": 2000,
|
||||||
"isDeleted": true,
|
"delete": true,
|
||||||
"isPermanentlyDeleted": false,
|
"permanentDelete": false,
|
||||||
"updatedAt": "2026-05-18T10:10:00.000Z"
|
"updatedAt": "2026-05-18T10:10:00.000Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -220,7 +220,7 @@ Campos:
|
|||||||
"id": "uuid",
|
"id": "uuid",
|
||||||
"name": "texto_cifrado",
|
"name": "texto_cifrado",
|
||||||
"serverVersion": 1,
|
"serverVersion": 1,
|
||||||
"isDeleted": false,
|
"delete": false,
|
||||||
"isDirty": true,
|
"isDirty": true,
|
||||||
"colorValue": 4281558681,
|
"colorValue": 4281558681,
|
||||||
"iconCodePoint": 58896,
|
"iconCodePoint": 58896,
|
||||||
@@ -233,7 +233,7 @@ Campos:
|
|||||||
- `id`: `UUID`, obligatorio.
|
- `id`: `UUID`, obligatorio.
|
||||||
- `name`: `string` (cifrado), obligatorio. Equivale a `encrypted_name`; ambos contienen el nombre encriptado.
|
- `name`: `string` (cifrado), obligatorio. Equivale a `encrypted_name`; ambos contienen el nombre encriptado.
|
||||||
- `serverVersion`: `number` entero >= 0, obligatorio. Es la versión base local con la que se hizo el cambio.
|
- `serverVersion`: `number` entero >= 0, obligatorio. Es la versión base local con la que se hizo el cambio.
|
||||||
- `isDeleted`: `boolean`, opcional, por defecto `false`.
|
- `delete`: `boolean`, opcional, por defecto `false`.
|
||||||
- `isDirty`: `boolean`, opcional. El servidor lo ignora en la escritura y devuelve `false` en la respuesta de sync.
|
- `isDirty`: `boolean`, opcional. El servidor lo ignora en la escritura y devuelve `false` en la respuesta de sync.
|
||||||
- `colorValue`: `number`, opcional.
|
- `colorValue`: `number`, opcional.
|
||||||
- `iconCodePoint`: `number`, opcional.
|
- `iconCodePoint`: `number`, opcional.
|
||||||
@@ -245,11 +245,11 @@ Campos:
|
|||||||
{
|
{
|
||||||
"id": "uuid",
|
"id": "uuid",
|
||||||
"categoryId": "uuid-categoria",
|
"categoryId": "uuid-categoria",
|
||||||
"encrypted_title": "titulo_cifrado",
|
"title": "titulo_cifrado",
|
||||||
"encrypted_body": "cuerpo_cifrado",
|
"body": "cuerpo_cifrado",
|
||||||
"serverVersion": 1,
|
"serverVersion": 1,
|
||||||
"position": 2000,
|
"position": 2000,
|
||||||
"isDeleted": false,
|
"delete": false,
|
||||||
"updatedAt": "2026-05-18T10:10:00.000Z"
|
"updatedAt": "2026-05-18T10:10:00.000Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -258,12 +258,12 @@ Campos:
|
|||||||
|
|
||||||
- `id`: `UUID`, obligatorio.
|
- `id`: `UUID`, obligatorio.
|
||||||
- `categoryId`: `UUID | null`, opcional.
|
- `categoryId`: `UUID | null`, opcional.
|
||||||
- `encrypted_title`: `string`, obligatorio.
|
- `title`: `string`, obligatorio.
|
||||||
- `encrypted_body`: `string`, obligatorio.
|
- `body`: `string`, obligatorio.
|
||||||
- `serverVersion`: `number` entero >= 0, obligatorio. Es la versión base local con la que se hizo el cambio.
|
- `serverVersion`: `number` entero >= 0, obligatorio. Es la versión base local con la que se hizo el cambio.
|
||||||
- `position`: `number`, opcional.
|
- `position`: `number`, opcional.
|
||||||
- `isDeleted`: `boolean`, opcional, por defecto `false`.
|
- `delete`: `boolean`, opcional, por defecto `false`.
|
||||||
- `isPermanentlyDeleted`: `boolean`, opcional, por defecto `false`. Si llega en `true`, el servidor guarda la nota con `encrypted_title` y `encrypted_body` vacíos, `position` en `0` y `isDeleted` en `true`.
|
- `permanentDelete`: `boolean`, opcional, por defecto `false`. Si llega en `true`, el servidor guarda la nota con `title` y `body` vacíos, `position` en `0` y `delete` en `true`.
|
||||||
- `updatedAt`: `ISO date string`, opcional (solo informativo para UI).
|
- `updatedAt`: `ISO date string`, opcional (solo informativo para UI).
|
||||||
|
|
||||||
#### Response
|
#### Response
|
||||||
@@ -280,12 +280,12 @@ Respuesta ejemplo (nuevo contrato):
|
|||||||
{
|
{
|
||||||
"id": "uuid-nota-2",
|
"id": "uuid-nota-2",
|
||||||
"categoryId": null,
|
"categoryId": null,
|
||||||
"encrypted_title": "titulo_cifrado...",
|
"title": "titulo_cifrado...",
|
||||||
"encrypted_body": "cuerpo_cifrado...",
|
"body": "cuerpo_cifrado...",
|
||||||
"serverVersion": 3,
|
"serverVersion": 3,
|
||||||
"position": 0,
|
"position": 0,
|
||||||
"isDeleted": false,
|
"delete": false,
|
||||||
"isPermanentlyDeleted": false,
|
"permanentDelete": false,
|
||||||
"updatedAt": "2026-05-18T10:15:00.000Z"
|
"updatedAt": "2026-05-18T10:15:00.000Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -303,7 +303,7 @@ Campos de salida:
|
|||||||
## Reglas de sincronización
|
## Reglas de sincronización
|
||||||
|
|
||||||
- El cliente debe guardar su último `lastSyncAt`.
|
- El cliente debe guardar su último `lastSyncAt`.
|
||||||
- Un borrado no elimina el registro localmente: se marca con `isDeleted: true`.
|
- Un borrado no elimina el registro localmente: se marca con `delete: true`.
|
||||||
- El servidor decide conflictos comparando `serverVersion` (no `updatedAt`).
|
- El servidor decide conflictos comparando `serverVersion` (no `updatedAt`).
|
||||||
- En categorías, solo se actualiza si `incoming.serverVersion === serverVersion`. Si no coincide, gana el servidor y no se crea duplicado.
|
- En categorías, solo se actualiza si `incoming.serverVersion === serverVersion`. Si no coincide, gana el servidor y no se crea duplicado.
|
||||||
- Si la versión entrante de una nota coincide con la versión actual del servidor, acepta el cambio y sube versión (`v -> v+1`).
|
- Si la versión entrante de una nota coincide con la versión actual del servidor, acepta el cambio y sube versión (`v -> v+1`).
|
||||||
@@ -312,4 +312,4 @@ Campos de salida:
|
|||||||
## Notas importantes
|
## Notas importantes
|
||||||
|
|
||||||
- Las notas y categorías están pensadas para contenido cifrado del lado del cliente.
|
- Las notas y categorías están pensadas para contenido cifrado del lado del cliente.
|
||||||
- El servidor guarda `encrypted_title`, `encrypted_body` y `encrypted_name`, pero no interpreta el contenido.
|
- El servidor guarda `title`, `body` y `encrypted_name`, pero no interpreta el contenido.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const Category = sequelize.define('Category', {
|
|||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
},
|
},
|
||||||
isDeleted: {
|
delete: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -8,11 +8,11 @@ const Note = sequelize.define('Note', {
|
|||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
encrypted_title: {
|
title: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
encrypted_body: {
|
body: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
@@ -24,11 +24,11 @@ const Note = sequelize.define('Note', {
|
|||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
},
|
},
|
||||||
isDeleted: {
|
delete: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
},
|
},
|
||||||
isPermanentlyDeleted: {
|
permanentDelete: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -16,7 +16,7 @@ const normalizeCategory = (category) => ({
|
|||||||
encrypted_name: category.encrypted_name,
|
encrypted_name: category.encrypted_name,
|
||||||
colorValue: category.colorValue ?? null,
|
colorValue: category.colorValue ?? null,
|
||||||
iconCodePoint: category.iconCodePoint ?? null,
|
iconCodePoint: category.iconCodePoint ?? null,
|
||||||
isDeleted: Boolean(category.isDeleted),
|
delete: Boolean(category.delete),
|
||||||
serverVersion: category.serverVersion || 1,
|
serverVersion: category.serverVersion || 1,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
updatedAt: category.updatedAt instanceof Date ? category.updatedAt.toISOString() : toIso(category.updatedAt)
|
updatedAt: category.updatedAt instanceof Date ? category.updatedAt.toISOString() : toIso(category.updatedAt)
|
||||||
@@ -25,28 +25,28 @@ const normalizeCategory = (category) => ({
|
|||||||
const normalizeNote = (note) => ({
|
const normalizeNote = (note) => ({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
categoryId: note.categoryId || null,
|
categoryId: note.categoryId || null,
|
||||||
encrypted_title: note.encrypted_title,
|
title: note.title,
|
||||||
encrypted_body: note.encrypted_body,
|
body: note.body,
|
||||||
position: note.position,
|
position: note.position,
|
||||||
isDeleted: Boolean(note.isDeleted),
|
delete: Boolean(note.delete),
|
||||||
isPermanentlyDeleted: Boolean(note.isPermanentlyDeleted),
|
permanentDelete: Boolean(note.permanentDelete),
|
||||||
serverVersion: note.serverVersion || 1,
|
serverVersion: note.serverVersion || 1,
|
||||||
updatedAt: note.updatedAt instanceof Date ? note.updatedAt.toISOString() : toIso(note.updatedAt)
|
updatedAt: note.updatedAt instanceof Date ? note.updatedAt.toISOString() : toIso(note.updatedAt)
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildStoredNoteData = (incomingNote, existingNote = null) => {
|
const buildStoredNoteData = (incomingNote, existingNote = null) => {
|
||||||
const isPermanentlyDeleted = Boolean(incomingNote.isPermanentlyDeleted);
|
const permanentDelete = Boolean(incomingNote.permanentDelete);
|
||||||
const position = isPermanentlyDeleted
|
const position = permanentDelete
|
||||||
? 0
|
? 0
|
||||||
: (incomingNote.position ?? existingNote?.position ?? 0);
|
: (incomingNote.position ?? existingNote?.position ?? 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categoryId: incomingNote.categoryId || null,
|
categoryId: incomingNote.categoryId || null,
|
||||||
encrypted_title: isPermanentlyDeleted ? '' : incomingNote.encrypted_title,
|
title: permanentDelete ? '' : incomingNote.title,
|
||||||
encrypted_body: isPermanentlyDeleted ? '' : incomingNote.encrypted_body,
|
body: permanentDelete ? '' : incomingNote.body,
|
||||||
position,
|
position,
|
||||||
isDeleted: Boolean(incomingNote.isDeleted) || isPermanentlyDeleted,
|
delete: Boolean(incomingNote.delete) || permanentDelete,
|
||||||
isPermanentlyDeleted
|
permanentDelete
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class SyncService {
|
|||||||
encrypted_name: incomingCategory.encrypted_name,
|
encrypted_name: incomingCategory.encrypted_name,
|
||||||
colorValue: incomingCategory.colorValue ?? null,
|
colorValue: incomingCategory.colorValue ?? null,
|
||||||
iconCodePoint: incomingCategory.iconCodePoint ?? null,
|
iconCodePoint: incomingCategory.iconCodePoint ?? null,
|
||||||
isDeleted: Boolean(incomingCategory.isDeleted),
|
delete: Boolean(incomingCategory.delete),
|
||||||
serverVersion: 1
|
serverVersion: 1
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
continue;
|
continue;
|
||||||
@@ -106,7 +106,7 @@ class SyncService {
|
|||||||
encrypted_name: incomingCategory.encrypted_name,
|
encrypted_name: incomingCategory.encrypted_name,
|
||||||
colorValue: incomingCategory.colorValue ?? existingCategory.colorValue ?? null,
|
colorValue: incomingCategory.colorValue ?? existingCategory.colorValue ?? null,
|
||||||
iconCodePoint: incomingCategory.iconCodePoint ?? existingCategory.iconCodePoint ?? null,
|
iconCodePoint: incomingCategory.iconCodePoint ?? existingCategory.iconCodePoint ?? null,
|
||||||
isDeleted: Boolean(incomingCategory.isDeleted),
|
delete: Boolean(incomingCategory.delete),
|
||||||
serverVersion: serverVersion + 1
|
serverVersion: serverVersion + 1
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
} else {
|
} else {
|
||||||
@@ -115,10 +115,10 @@ class SyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const incomingNote of incomingNotes) {
|
for (const incomingNote of incomingNotes) {
|
||||||
const isPermanentlyDeleted = Boolean(incomingNote.isPermanentlyDeleted);
|
const permanentDelete = Boolean(incomingNote.permanentDelete);
|
||||||
|
|
||||||
if (!incomingNote.id || (!isPermanentlyDeleted && (!incomingNote.encrypted_title || !incomingNote.encrypted_body))) {
|
if (!incomingNote.id || (!permanentDelete && (!incomingNote.title || !incomingNote.body))) {
|
||||||
throw new Error('Cada nota debe incluir id, encrypted_title y encrypted_body');
|
throw new Error('Cada nota debe incluir id, title y body');
|
||||||
}
|
}
|
||||||
|
|
||||||
const incomingBaseVersion = getIncomingVersion('nota', incomingNote);
|
const incomingBaseVersion = getIncomingVersion('nota', incomingNote);
|
||||||
|
|||||||
Reference in New Issue
Block a user