diff --git a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml index b97e9851c..87b69781b 100644 --- a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml +++ b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml @@ -630,109 +630,114 @@ imageId 1 "" - + + 10 + int|0s + 0 + + 1 relationId 1 - + sourceNoteId - + targetNoteId - + relationId 1 sqlite_autoindex_relations_1 - + 1 TEXT|0s 1 - + 2 TEXT|0s 1 - + 1 sourceId 1 - + sourceId 1 sqlite_autoindex_source_ids_1 - + 1 text|0s - + 2 text|0s - + 3 text|0s - + 4 integer|0s - + 5 text|0s - + 1 - + 2 - + 1 INTEGER|0s 1 1 - + 2 TEXT|0s 1 - + 3 TEXT|0s 1 - + 4 TEXT|0s 1 - + 5 TEXT|0s 1 - + entityName entityId 1 - + syncDate - + id 1 diff --git a/db/migrations/0109__create_attributes.sql b/db/migrations/0109__create_attributes.sql new file mode 100644 index 000000000..286615014 --- /dev/null +++ b/db/migrations/0109__create_attributes.sql @@ -0,0 +1,27 @@ +create table attributes +( + attributeId TEXT not null primary key, + noteId TEXT not null, + type TEXT not null, + name TEXT not null, + value TEXT default '' not null, + position INT default 0 not null, + dateCreated TEXT not null, + dateModified TEXT not null, + isDeleted INT not null, + hash TEXT default "" not null); + +create index IDX_attributes_name_value + on labels (name, value); + +create index IDX_attributes_value + on labels (value); + +create index IDX_attributes_noteId + on labels (noteId); + +INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash) +SELECT labelId, noteId, 'label', name, value, position, dateCreated, dateModified, isDeleted, hash FROM labels; + +INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash) +SELECT relationId, sourceNoteId, 'relation', name, targetNoteId, position, dateCreated, dateModified, isDeleted, hash FROM relations; diff --git a/src/entities/attribute.js b/src/entities/attribute.js new file mode 100644 index 000000000..1c2eefb05 --- /dev/null +++ b/src/entities/attribute.js @@ -0,0 +1,41 @@ +"use strict"; + +const Entity = require('./entity'); +const repository = require('../services/repository'); +const dateUtils = require('../services/date_utils'); +const sql = require('../services/sql'); + +class Attribute extends Entity { + static get tableName() { return "attributes"; } + static get primaryKeyName() { return "attributeId"; } + static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "dateModified", "dateCreated"]; } + + async getNote() { + return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); + } + + async beforeSaving() { + super.beforeSaving(); + + if (!this.value) { + // null value isn't allowed + this.value = ""; + } + + if (this.position === undefined) { + this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]); + } + + if (!this.isDeleted) { + this.isDeleted = false; + } + + if (!this.dateCreated) { + this.dateCreated = dateUtils.nowDate(); + } + + this.dateModified = dateUtils.nowDate(); + } +} + +module.exports = Attribute; \ No newline at end of file diff --git a/src/entities/entity_constructor.js b/src/entities/entity_constructor.js index b0faf65d9..78022dbc4 100644 --- a/src/entities/entity_constructor.js +++ b/src/entities/entity_constructor.js @@ -3,6 +3,7 @@ const NoteRevision = require('../entities/note_revision'); const Image = require('../entities/image'); const NoteImage = require('../entities/note_image'); const Branch = require('../entities/branch'); +const Attribute = require('../entities/attribute'); const Label = require('../entities/label'); const Relation = require('../entities/relation'); const RecentNote = require('../entities/recent_note'); @@ -13,7 +14,10 @@ const repository = require('../services/repository'); function createEntityFromRow(row) { let entity; - if (row.labelId) { + if (row.attributeId) { + entity = new Attribute(row); + } + else if (row.labelId) { entity = new Label(row); } else if (row.relationId) { diff --git a/src/routes/api/attributes.js b/src/routes/api/attributes.js new file mode 100644 index 000000000..174eb05eb --- /dev/null +++ b/src/routes/api/attributes.js @@ -0,0 +1,70 @@ +"use strict"; + +const sql = require('../../services/sql'); +const attributeService = require('../../services/attributes'); +const repository = require('../../services/repository'); +const Attribute = require('../../entities/attribute'); + +async function getNoteAttributes(req) { + const noteId = req.params.noteId; + + return await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]); +} + +async function updateNoteAttributes(req) { + const noteId = req.params.noteId; + const attributes = req.body; + + for (const attribute of attributes) { + let attributeEntity; + + if (attribute.attributeId) { + attributeEntity = await repository.getAttribute(attribute.attributeId); + } + else { + // if it was "created" and then immediatelly deleted, we just don't create it at all + if (attribute.isDeleted) { + continue; + } + + attributeEntity = new Attribute(); + attributeEntity.noteId = noteId; + } + + attributeEntity.name = attribute.name; + attributeEntity.value = attribute.value; + attributeEntity.position = attribute.position; + attributeEntity.isDeleted = attribute.isDeleted; + + await attributeEntity.save(); + } + + return await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]); +} + +async function getAllAttributeNames() { + const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0"); + + for (const attribute of attributeService.BUILTIN_ATTRIBUTES) { + if (!names.includes(attribute)) { + names.push(attribute); + } + } + + names.sort(); + + return names; +} + +async function getValuesForAttribute(req) { + const attributeName = req.params.attributeName; + + return await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [attributeName]); +} + +module.exports = { + getNoteAttributes, + updateNoteAttributes, + getAllAttributeNames, + getValuesForAttribute +}; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index b422fac0d..1cc74a817 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -25,6 +25,7 @@ const sqlRoute = require('./api/sql'); const anonymizationRoute = require('./api/anonymization'); const cleanupRoute = require('./api/cleanup'); const imageRoute = require('./api/image'); +const attributesRoute = require('./api/attributes'); const labelsRoute = require('./api/labels'); const relationsRoute = require('./api/relations'); const scriptRoute = require('./api/script'); @@ -133,6 +134,11 @@ function register(app) { route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); + apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getNoteAttributes); + apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes); + apiRoute(GET, '/api/attributes/names', attributesRoute.getAllAttributeNames); + apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); + apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels); apiRoute(PUT, '/api/notes/:noteId/labels', labelsRoute.updateNoteLabels); apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames); diff --git a/src/services/app_info.js b/src/services/app_info.js index 6ec279739..dc5fa5984 100644 --- a/src/services/app_info.js +++ b/src/services/app_info.js @@ -3,7 +3,7 @@ const build = require('./build'); const packageJson = require('../../package'); -const APP_DB_VERSION = 108; +const APP_DB_VERSION = 109; const SYNC_VERSION = 1; module.exports = { diff --git a/src/services/attributes.js b/src/services/attributes.js new file mode 100644 index 000000000..069df17ba --- /dev/null +++ b/src/services/attributes.js @@ -0,0 +1,52 @@ +"use strict"; + +const repository = require('./repository'); +const Attribute = require('../entities/attribute'); + +const BUILTIN_ATTRIBUTES = [ + 'disableVersioning', + 'calendarRoot', + 'archived', + 'excludeFromExport', + 'run', + 'manualTransactionHandling', + 'disableInclusion', + 'appCss', + 'hideChildrenOverview' +]; + +async function getNotesWithAttribute(name, value) { + let notes; + + if (value !== undefined) { + notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) + WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]); + } + else { + notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) + WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]); + } + + return notes; +} + +async function getNoteWithAttribute(name, value) { + const notes = await getNotesWithAttribute(name, value); + + return notes.length > 0 ? notes[0] : null; +} + +async function createAttribute(noteId, name, value = "") { + return await new Attribute({ + noteId: noteId, + name: name, + value: value + }).save(); +} + +module.exports = { + getNotesWithAttribute, + getNoteWithAttribute, + createAttribute, + BUILTIN_ATTRIBUTES +}; \ No newline at end of file diff --git a/src/services/sync_table.js b/src/services/sync_table.js index c425fb514..e81bb7372 100644 --- a/src/services/sync_table.js +++ b/src/services/sync_table.js @@ -38,6 +38,10 @@ async function addNoteImageSync(noteImageId, sourceId) { await addEntitySync("note_images", noteImageId, sourceId); } +async function addAttributeSync(attributeId, sourceId) { + await addEntitySync("attributes", attributeId, sourceId); +} + async function addLabelSync(labelId, sourceId) { await addEntitySync("labels", labelId, sourceId); } @@ -104,6 +108,7 @@ async function fillAllSyncRows() { await fillSyncRows("recent_notes", "branchId"); await fillSyncRows("images", "imageId"); await fillSyncRows("note_images", "noteImageId"); + await fillSyncRows("attributes", "attributeId"); await fillSyncRows("labels", "labelId"); await fillSyncRows("relations", "relationId"); await fillSyncRows("api_tokens", "apiTokenId"); @@ -119,6 +124,7 @@ module.exports = { addRecentNoteSync, addImageSync, addNoteImageSync, + addAttributeSync, addLabelSync, addRelationSync, addApiTokenSync, diff --git a/src/services/sync_update.js b/src/services/sync_update.js index 6fad891ab..a9724fa05 100644 --- a/src/services/sync_update.js +++ b/src/services/sync_update.js @@ -30,6 +30,9 @@ async function updateEntity(sync, entity, sourceId) { else if (entityName === 'note_images') { await updateNoteImage(entity, sourceId); } + else if (entityName === 'attributes') { + await updateAttribute(entity, sourceId); + } else if (entityName === 'labels') { await updateLabel(entity, sourceId); } @@ -174,6 +177,20 @@ async function updateNoteImage(entity, sourceId) { } } +async function updateAttribute(entity, sourceId) { + const origAttribute = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [entity.attributeId]); + + if (!origAttribute || origAttribute.dateModified <= entity.dateModified) { + await sql.transactional(async () => { + await sql.replace("attributes", entity); + + await syncTableService.addAttributeSync(entity.attributeId, sourceId); + }); + + log.info("Update/sync attribute " + entity.attributeId); + } +} + async function updateLabel(entity, sourceId) { const origLabel = await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [entity.labelId]);