diff --git a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
index 7c8dd0bfb..b97e9851c 100644
--- a/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
+++ b/.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
@@ -21,533 +21,529 @@
-
-
+
+
+
-
+
-
-
+
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
INT|0s
1
0
-
+
5
TEXT|0s
1
""
-
+
1
apiTokenId
1
-
+
apiTokenId
1
sqlite_autoindex_api_tokens_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
INTEGER|0s
1
-
+
5
TEXT|0s
-
+
6
BOOLEAN|0s
-
+
7
INTEGER|0s
1
0
-
+
8
TEXT|0s
1
-
+
9
TEXT|0s
1
""
-
+
10
TEXT|0s
1
'1970-01-01T00:00:00.000Z'
-
+
1
branchId
1
-
+
noteId
parentNoteId
-
+
noteId
-
+
parentNoteId
-
+
branchId
1
sqlite_autoindex_branches_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
-
+
3
TEXT|0s
-
+
4
TEXT|0s
1
-
+
1
eventId
1
-
+
eventId
1
sqlite_autoindex_event_log_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
TEXT|0s
1
-
+
5
BLOB|0s
-
+
6
INT|0s
1
0
-
+
7
TEXT|0s
1
-
+
8
TEXT|0s
1
-
+
9
TEXT|0s
1
""
-
+
1
imageId
1
-
+
imageId
1
sqlite_autoindex_images_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
TEXT|0s
1
''
-
+
5
INT|0s
1
0
-
+
6
TEXT|0s
1
-
+
7
TEXT|0s
1
-
+
8
INT|0s
1
-
+
9
TEXT|0s
1
""
-
+
1
labelId
1
-
+
noteId
-
+
name
value
-
+
labelId
1
sqlite_autoindex_labels_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
1
-
+
4
INT|0s
1
0
-
+
5
TEXT|0s
1
-
+
6
TEXT|0s
1
-
+
7
TEXT|0s
1
""
-
+
1
noteImageId
1
-
+
noteId
imageId
-
+
noteId
-
+
imageId
-
+
noteImageId
1
sqlite_autoindex_note_images_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
3
TEXT|0s
-
+
4
TEXT|0s
-
+
5
INT|0s
1
0
-
+
6
TEXT|0s
1
-
+
7
TEXT|0s
1
-
+
8
TEXT|0s
1
''
-
+
9
TEXT|0s
1
''
-
+
10
TEXT|0s
1
""
-
+
1
noteRevisionId
1
-
+
noteId
-
+
dateModifiedFrom
-
+
dateModifiedTo
-
+
noteRevisionId
1
sqlite_autoindex_note_revisions_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
"unnamed"
-
+
3
TEXT|0s
1
""
-
+
4
INT|0s
1
0
-
+
5
INT|0s
1
0
-
+
6
TEXT|0s
1
-
+
7
TEXT|0s
1
-
+
8
TEXT|0s
1
'text'
-
+
9
TEXT|0s
1
'text/html'
-
+
10
TEXT|0s
1
""
-
+
1
noteId
1
-
+
type
-
+
noteId
1
sqlite_autoindex_notes_1
-
+
1
TEXT|0s
1
-
- 2
- TEXT|0s
- 1
-
- 3
+ 2
TEXT|0s
- 4
+ 3
INT|0s
- 5
+ 4
INTEGER|0s
1
0
- 6
+ 5
TEXT|0s
1
""
- 7
+ 6
TEXT|0s
1
'1970-01-01T00:00:00.000Z'
1
- optionId
+ name
1
- optionId
+ name
1
sqlite_autoindex_options_1
@@ -587,90 +583,156 @@ imageId
1
sqlite_autoindex_recent_notes_1
-
+
1
TEXT|0s
1
-
+
2
TEXT|0s
1
-
+
+ 3
+ TEXT|0s
+ 1
+
+
+ 4
+ TEXT|0s
+ 1
+
+
+ 5
+ INT|0s
+ 1
+ 0
+
+
+ 6
+ TEXT|0s
+ 1
+
+
+ 7
+ TEXT|0s
+ 1
+
+
+ 8
+ INT|0s
+ 1
+
+
+ 9
+ TEXT|0s
+ 1
+ ""
+
+
+ 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/0106__add_relations_table.sql b/db/migrations/0106__add_relations_table.sql
new file mode 100644
index 000000000..9cc5cb84e
--- /dev/null
+++ b/db/migrations/0106__add_relations_table.sql
@@ -0,0 +1,15 @@
+CREATE TABLE relations
+(
+ relationId TEXT not null primary key,
+ sourceNoteId TEXT not null,
+ name TEXT not null,
+ targetNoteId TEXT 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_relation_sourceNoteId
+ on relations (sourceNoteId);
+CREATE INDEX IDX_relation_targetNoteId
+ on relations (targetNoteId);
\ No newline at end of file
diff --git a/src/entities/entity_constructor.js b/src/entities/entity_constructor.js
index 5db7c0963..b0faf65d9 100644
--- a/src/entities/entity_constructor.js
+++ b/src/entities/entity_constructor.js
@@ -4,6 +4,7 @@ const Image = require('../entities/image');
const NoteImage = require('../entities/note_image');
const Branch = require('../entities/branch');
const Label = require('../entities/label');
+const Relation = require('../entities/relation');
const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token');
const Option = require('../entities/option');
@@ -15,6 +16,9 @@ function createEntityFromRow(row) {
if (row.labelId) {
entity = new Label(row);
}
+ else if (row.relationId) {
+ entity = new Relation(row);
+ }
else if (row.noteRevisionId) {
entity = new NoteRevision(row);
}
diff --git a/src/entities/relation.js b/src/entities/relation.js
new file mode 100644
index 000000000..25c2760e8
--- /dev/null
+++ b/src/entities/relation.js
@@ -0,0 +1,40 @@
+"use strict";
+
+const Entity = require('./entity');
+const repository = require('../services/repository');
+const dateUtils = require('../services/date_utils');
+const sql = require('../services/sql');
+
+class Relation extends Entity {
+ static get tableName() { return "relations"; }
+ static get primaryKeyName() { return "relationId"; }
+ static get hashedProperties() { return ["relationId", "sourceNoteId", "name", "targetNoteId", "dateModified", "dateCreated"]; }
+
+ async getSourceNote() {
+ return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.sourceNoteId]);
+ }
+
+ async getTargetNote() {
+ return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.targetNoteId]);
+ }
+
+ async beforeSaving() {
+ super.beforeSaving();
+
+ if (this.position === undefined) {
+ this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM relations WHERE sourceNoteId = ?`, [this.sourceNoteId]);
+ }
+
+ if (!this.isDeleted) {
+ this.isDeleted = false;
+ }
+
+ if (!this.dateCreated) {
+ this.dateCreated = dateUtils.nowDate();
+ }
+
+ this.dateModified = dateUtils.nowDate();
+ }
+}
+
+module.exports = Relation;
\ No newline at end of file
diff --git a/src/public/javascripts/dialogs/relations.js b/src/public/javascripts/dialogs/relations.js
new file mode 100644
index 000000000..5455bf731
--- /dev/null
+++ b/src/public/javascripts/dialogs/relations.js
@@ -0,0 +1,222 @@
+import noteDetailService from '../services/note_detail.js';
+import server from '../services/server.js';
+import infoService from "../services/info.js";
+
+const $dialog = $("#relations-dialog");
+const $saveRelationsButton = $("#save-relations-button");
+const $relationsBody = $('#relations-table tbody');
+
+const relationsModel = new RelationsModel();
+let relationNames = [];
+
+function RelationsModel() {
+ const self = this;
+
+ this.relations = ko.observableArray();
+
+ this.updateRelationPositions = function() {
+ let position = 0;
+
+ // we need to update positions by searching in the DOM, because order of the
+ // relations in the viewmodel (self.relations()) stays the same
+ $relationsBody.find('input[name="position"]').each(function() {
+ const relation = self.getTargetRelation(this);
+
+ relation().position = position++;
+ });
+ };
+
+ this.loadRelations = async function() {
+ const noteId = noteDetailService.getCurrentNoteId();
+
+ const relations = await server.get('notes/' + noteId + '/relations');
+
+ self.relations(relations.map(ko.observable));
+
+ addLastEmptyRow();
+
+ relationNames = await server.get('relations/names');
+
+ // relation might not be rendered immediatelly so could not focus
+ setTimeout(() => $(".relation-name:last").focus(), 100);
+
+ $relationsBody.sortable({
+ handle: '.handle',
+ containment: $relationsBody,
+ update: this.updateRelationPositions
+ });
+ };
+
+ this.deleteRelation = function(data, event) {
+ const relation = self.getTargetRelation(event.target);
+ const relationData = relation();
+
+ if (relationData) {
+ relationData.isDeleted = 1;
+
+ relation(relationData);
+
+ addLastEmptyRow();
+ }
+ };
+
+ function isValid() {
+ for (let relations = self.relations(), i = 0; i < relations.length; i++) {
+ if (self.isEmptyName(i)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ this.save = async function() {
+ // we need to defocus from input (in case of enter-triggered save) because value is updated
+ // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
+ // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
+ $saveRelationsButton.focus();
+
+ if (!isValid()) {
+ alert("Please fix all validation errors and try saving again.");
+ return;
+ }
+
+ self.updateRelationPositions();
+
+ const noteId = noteDetailService.getCurrentNoteId();
+
+ const relationsToSave = self.relations()
+ .map(relation => relation())
+ .filter(relation => relation.relationId !== "" || relation.name !== "");
+
+ const relations = await server.put('notes/' + noteId + '/relations', relationsToSave);
+
+ self.relations(relations.map(ko.observable));
+
+ addLastEmptyRow();
+
+ infoService.showMessage("Relations have been saved.");
+
+ noteDetailService.loadRelationList();
+ };
+
+ function addLastEmptyRow() {
+ const relations = self.relations().filter(attr => attr().isDeleted === 0);
+ const last = relations.length === 0 ? null : relations[relations.length - 1]();
+
+ if (!last || last.name.trim() !== "" || last.value !== "") {
+ self.relations.push(ko.observable({
+ relationId: '',
+ name: '',
+ value: '',
+ isDeleted: 0,
+ position: 0
+ }));
+ }
+ }
+
+ this.relationChanged = function (data, event) {
+ addLastEmptyRow();
+
+ const relation = self.getTargetRelation(event.target);
+
+ relation.valueHasMutated();
+ };
+
+ this.isNotUnique = function(index) {
+ const cur = self.relations()[index]();
+
+ if (cur.name.trim() === "") {
+ return false;
+ }
+
+ for (let relations = self.relations(), i = 0; i < relations.length; i++) {
+ const relation = relations[i]();
+
+ if (index !== i && cur.name === relation.name) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ this.isEmptyName = function(index) {
+ const cur = self.relations()[index]();
+
+ return cur.name.trim() === "" && (cur.relationId !== "" || cur.value !== "");
+ };
+
+ this.getTargetRelation = function(target) {
+ const context = ko.contextFor(target);
+ const index = context.$index();
+
+ return self.relations()[index];
+ }
+}
+
+async function showDialog() {
+ glob.activeDialog = $dialog;
+
+ await relationsModel.loadRelations();
+
+ $dialog.dialog({
+ modal: true,
+ width: 800,
+ height: 500
+ });
+}
+
+ko.applyBindings(relationsModel, document.getElementById('relations-dialog'));
+
+$(document).on('focus', '.relation-name', function (e) {
+ if (!$(this).hasClass("ui-autocomplete-input")) {
+ $(this).autocomplete({
+ // shouldn't be required and autocomplete should just accept array of strings, but that fails
+ // because we have overriden filter() function in autocomplete.js
+ source: relationNames.map(relation => {
+ return {
+ relation: relation,
+ value: relation
+ }
+ }),
+ minLength: 0
+ });
+ }
+
+ $(this).autocomplete("search", $(this).val());
+});
+
+$(document).on('focus', '.relation-value', async function (e) {
+ if (!$(this).hasClass("ui-autocomplete-input")) {
+ const relationName = $(this).parent().parent().find('.relation-name').val();
+
+ if (relationName.trim() === "") {
+ return;
+ }
+
+ const relationValues = await server.get('relations/values/' + encodeURIComponent(relationName));
+
+ if (relationValues.length === 0) {
+ return;
+ }
+
+ $(this).autocomplete({
+ // shouldn't be required and autocomplete should just accept array of strings, but that fails
+ // because we have overriden filter() function in autocomplete.js
+ source: relationValues.map(relation => {
+ return {
+ label: relation,
+ value: relation
+ }
+ }),
+ minLength: 0
+ });
+ }
+
+ $(this).autocomplete("search", $(this).val());
+});
+
+export default {
+ showDialog
+};
\ No newline at end of file
diff --git a/src/public/javascripts/services/entrypoints.js b/src/public/javascripts/services/entrypoints.js
index d9bc7fd23..d25f79785 100644
--- a/src/public/javascripts/services/entrypoints.js
+++ b/src/public/javascripts/services/entrypoints.js
@@ -12,6 +12,7 @@ import recentChangesDialog from "../dialogs/recent_changes.js";
import sqlConsoleDialog from "../dialogs/sql_console.js";
import searchNotesService from "./search_notes.js";
import labelsDialog from "../dialogs/labels.js";
+import relationsDialog from "../dialogs/relations.js";
import protectedSessionService from "./protected_session.js";
function registerEntrypoints() {
@@ -40,6 +41,9 @@ function registerEntrypoints() {
$(".show-labels-button").click(labelsDialog.showDialog);
utils.bindShortcut('alt+l', labelsDialog.showDialog);
+ $(".show-relations-button").click(relationsDialog.showDialog);
+ utils.bindShortcut('alt+r', relationsDialog.showDialog);
+
$("#options-button").click(optionsDialog.showDialog);
utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
diff --git a/src/routes/api/relations.js b/src/routes/api/relations.js
new file mode 100644
index 000000000..64ef88d2c
--- /dev/null
+++ b/src/routes/api/relations.js
@@ -0,0 +1,63 @@
+"use strict";
+
+const sql = require('../../services/sql');
+const relationService = require('../../services/relations');
+const repository = require('../../services/repository');
+const Relation = require('../../entities/relation');
+
+async function getNoteRelations(req) {
+ const noteId = req.params.noteId;
+
+ return await repository.getEntities("SELECT * FROM relations WHERE isDeleted = 0 AND sourceNoteId = ? ORDER BY position, dateCreated", [noteId]);
+}
+
+async function updateNoteRelations(req) {
+ const noteId = req.params.noteId;
+ const relations = req.body;
+
+ for (const relation of relations) {
+ let relationEntity;
+
+ if (relation.labelId) {
+ relationEntity = await repository.getRelation(relation.relationId);
+ }
+ else {
+ // if it was "created" and then immediatelly deleted, we just don't create it at all
+ if (relation.isDeleted) {
+ continue;
+ }
+
+ relationEntity = new Relation();
+ relationEntity.sourceNoteId = noteId;
+ }
+
+ relationEntity.name = relation.name;
+ relationEntity.targetNoteId = relation.targetNoteId;
+ relationEntity.position = relation.position;
+ relationEntity.isDeleted = relation.isDeleted;
+
+ await relationEntity.save();
+ }
+
+ return await repository.getEntities("SELECT * FROM relations WHERE isDeleted = 0 AND sourceNoteId = ? ORDER BY position, dateCreated", [noteId]);
+}
+
+async function getAllRelationNames() {
+ const names = await sql.getColumn("SELECT DISTINCT name FROM relations WHERE isDeleted = 0");
+
+ for (const relationName of relationService.BUILTIN_RELATIONS) {
+ if (!names.includes(relationName)) {
+ names.push(relationName);
+ }
+ }
+
+ names.sort();
+
+ return names;
+}
+
+module.exports = {
+ getNoteRelations,
+ updateNoteRelations,
+ getAllRelationNames
+};
\ No newline at end of file
diff --git a/src/routes/routes.js b/src/routes/routes.js
index 0b6b01433..8d2169dcb 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -26,6 +26,7 @@ const anonymizationRoute = require('./api/anonymization');
const cleanupRoute = require('./api/cleanup');
const imageRoute = require('./api/image');
const labelsRoute = require('./api/labels');
+const relationsRoute = require('./api/relations');
const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender');
const filesRoute = require('./api/file_upload');
@@ -137,6 +138,10 @@ function register(app) {
apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames);
apiRoute(GET, '/api/labels/values/:labelName', labelsRoute.getValuesForLabel);
+ apiRoute(GET, '/api/notes/:noteId/relations', relationsRoute.getNoteRelations);
+ apiRoute(PUT, '/api/notes/:noteId/relations', relationsRoute.updateNoteRelations);
+ apiRoute(GET, '/api/relations/names', relationsRoute.getAllRelationNames);
+
route(GET, '/api/images/:imageId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware], imageRoute.uploadImage, apiResultHandler);
diff --git a/src/services/app_info.js b/src/services/app_info.js
index 99c63a630..10b6c9bf9 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 = 105;
+const APP_DB_VERSION = 106;
const SYNC_VERSION = 1;
module.exports = {
diff --git a/src/services/relations.js b/src/services/relations.js
new file mode 100644
index 000000000..64f554289
--- /dev/null
+++ b/src/services/relations.js
@@ -0,0 +1,44 @@
+"use strict";
+
+const repository = require('./repository');
+const Relation = require('../entities/relation');
+
+const BUILTIN_RELATIONS = [
+ 'exampleBuiltIn'
+];
+
+async function getNotesWithRelation(name, targetNoteId) {
+ let notes;
+
+ if (targetNoteId !== undefined) {
+ notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN relations ON notes.noteId = relations.sourceNoteId
+ WHERE notes.isDeleted = 0 AND relations.isDeleted = 0 AND relations.name = ? AND relations.targetNoteId = ?`, [name, targetNoteId]);
+ }
+ else {
+ notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN relations ON notes.noteId = relations.sourceNoteId
+ WHERE notes.isDeleted = 0 AND relations.isDeleted = 0 AND relations.name = ?`, [name]);
+ }
+
+ return notes;
+}
+
+async function getNoteWithRelation(name, value) {
+ const notes = await getNotesWithRelation(name, value);
+
+ return notes.length > 0 ? notes[0] : null;
+}
+
+async function createRelation(sourceNoteId, name, targetNoteId) {
+ return await new Relation({
+ sourceNoteId: sourceNoteId,
+ name: name,
+ targetNoteId: targetNoteId
+ }).save();
+}
+
+module.exports = {
+ getNotesWithRelation,
+ getNoteWithRelation,
+ createRelation,
+ BUILTIN_RELATIONS
+};
\ No newline at end of file
diff --git a/src/services/repository.js b/src/services/repository.js
index ff9f299ce..0f5f5e042 100644
--- a/src/services/repository.js
+++ b/src/services/repository.js
@@ -41,6 +41,10 @@ async function getLabel(labelId) {
return await getEntity("SELECT * FROM labels WHERE labelId = ?", [labelId]);
}
+async function getRelation(relationId) {
+ return await getEntity("SELECT * FROM relations WHERE relationId = ?", [relationId]);
+}
+
async function getOption(name) {
return await getEntity("SELECT * FROM options WHERE name = ?", [name]);
}
@@ -72,6 +76,7 @@ module.exports = {
getBranch,
getImage,
getLabel,
+ getRelation,
getOption,
updateEntity,
setEntityConstructor
diff --git a/src/services/sync.js b/src/services/sync.js
index 25799cd89..51b42c98e 100644
--- a/src/services/sync.js
+++ b/src/services/sync.js
@@ -236,6 +236,7 @@ const primaryKeys = {
"images": "imageId",
"note_images": "noteImageId",
"labels": "labelId",
+ "relations": "relationId",
"api_tokens": "apiTokenId",
"options": "name"
};
diff --git a/src/services/sync_table.js b/src/services/sync_table.js
index 9b4e6ec16..c425fb514 100644
--- a/src/services/sync_table.js
+++ b/src/services/sync_table.js
@@ -42,6 +42,10 @@ async function addLabelSync(labelId, sourceId) {
await addEntitySync("labels", labelId, sourceId);
}
+async function addRelationSync(relationId, sourceId) {
+ await addEntitySync("relations", relationId, sourceId);
+}
+
async function addApiTokenSync(apiTokenId, sourceId) {
await addEntitySync("api_tokens", apiTokenId, sourceId);
}
@@ -101,6 +105,7 @@ async function fillAllSyncRows() {
await fillSyncRows("images", "imageId");
await fillSyncRows("note_images", "noteImageId");
await fillSyncRows("labels", "labelId");
+ await fillSyncRows("relations", "relationId");
await fillSyncRows("api_tokens", "apiTokenId");
await fillSyncRows("options", "name", 'isSynced = 1');
}
@@ -115,6 +120,7 @@ module.exports = {
addImageSync,
addNoteImageSync,
addLabelSync,
+ addRelationSync,
addApiTokenSync,
addEntitySync,
cleanupSyncRowsForMissingEntities,
diff --git a/src/services/sync_update.js b/src/services/sync_update.js
index f966cc458..d342849f0 100644
--- a/src/services/sync_update.js
+++ b/src/services/sync_update.js
@@ -33,6 +33,9 @@ async function updateEntity(sync, entity, sourceId) {
else if (entityName === 'labels') {
await updateLabel(entity, sourceId);
}
+ else if (entityName === 'relations') {
+ await updateRelation(entity, sourceId);
+ }
else if (entityName === 'api_tokens') {
await updateApiToken(entity, sourceId);
}
@@ -185,6 +188,20 @@ async function updateLabel(entity, sourceId) {
}
}
+async function updateRelation(entity, sourceId) {
+ const origRelation = await sql.getRow("SELECT * FROM relations WHERE relationId = ?", [entity.relationId]);
+
+ if (!origRelation || origRelation.dateModified <= entity.dateModified) {
+ await sql.transactional(async () => {
+ await sql.replace("relations", entity);
+
+ await syncTableService.addRelationSync(entity.relationId, sourceId);
+ });
+
+ log.info("Update/sync relation " + entity.relationId);
+ }
+}
+
async function updateApiToken(entity, sourceId) {
const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]);
diff --git a/src/views/index.ejs b/src/views/index.ejs
index bc8c413c3..f4285df07 100644
--- a/src/views/index.ejs
+++ b/src/views/index.ejs
@@ -170,6 +170,7 @@
@@ -587,6 +588,50 @@
+
+