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]);