mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Link entity migrated to Attribute, WIP
This commit is contained in:
		
							
								
								
									
										10
									
								
								db/migrations/0137__links_to_attributes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								db/migrations/0137__links_to_attributes.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | UPDATE links SET type = 'internal-link' WHERE type = 'hyper'; | ||||||
|  | UPDATE links SET type = 'image-link' WHERE type = 'image'; | ||||||
|  | UPDATE links SET type = 'relation-map-link' WHERE type = 'relation-map'; | ||||||
|  |  | ||||||
|  | INSERT INTO attributes (attributeId, noteId, type, name, value, position, utcDateCreated, utcDateModified, isDeleted, hash, isInheritable) | ||||||
|  | SELECT linkId, noteId, 'relation', type, targetNoteId, 0, utcDateCreated, utcDateModified, isDeleted, hash, 0 FROM links; | ||||||
|  |  | ||||||
|  | UPDATE sync SET entityName = 'attributes' WHERE entityName = 'links'; | ||||||
|  |  | ||||||
|  | DROP TABLE links; | ||||||
| @@ -1,6 +1,5 @@ | |||||||
| const Note = require('../entities/note'); | const Note = require('../entities/note'); | ||||||
| const NoteRevision = require('../entities/note_revision'); | const NoteRevision = require('../entities/note_revision'); | ||||||
| const Link = require('../entities/link'); |  | ||||||
| const Branch = require('../entities/branch'); | const Branch = require('../entities/branch'); | ||||||
| const Attribute = require('../entities/attribute'); | const Attribute = require('../entities/attribute'); | ||||||
| const RecentNote = require('../entities/recent_note'); | const RecentNote = require('../entities/recent_note'); | ||||||
| @@ -16,7 +15,6 @@ const ENTITY_NAME_TO_ENTITY = { | |||||||
|     "recent_notes": RecentNote, |     "recent_notes": RecentNote, | ||||||
|     "options": Option, |     "options": Option, | ||||||
|     "api_tokens": ApiToken, |     "api_tokens": ApiToken, | ||||||
|     "links": Link |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| function getEntityFromEntityName(entityName) { | function getEntityFromEntityName(entityName) { | ||||||
| @@ -36,9 +34,6 @@ function createEntityFromRow(row) { | |||||||
|     else if (row.noteRevisionId) { |     else if (row.noteRevisionId) { | ||||||
|         entity = new NoteRevision(row); |         entity = new NoteRevision(row); | ||||||
|     } |     } | ||||||
|     else if (row.linkId) { |  | ||||||
|         entity = new Link(row); |  | ||||||
|     } |  | ||||||
|     else if (row.branchId && row.notePath) { |     else if (row.branchId && row.notePath) { | ||||||
|         entity = new RecentNote(row); |         entity = new RecentNote(row); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,51 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const Entity = require('./entity'); |  | ||||||
| const repository = require('../services/repository'); |  | ||||||
| const dateUtils = require('../services/date_utils'); |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * This class represents link from one note to another in the form of hyperlink or image reference. Note that |  | ||||||
|  * this is different concept than attribute/relation. |  | ||||||
|  * |  | ||||||
|  * @param {string} linkId |  | ||||||
|  * @param {string} noteId |  | ||||||
|  * @param {string} targetNoteId |  | ||||||
|  * @param {string} type |  | ||||||
|  * @param {boolean} isDeleted |  | ||||||
|  * @param {string} utcDateModified |  | ||||||
|  * @param {string} utcDateCreated |  | ||||||
|  * |  | ||||||
|  * @extends Entity |  | ||||||
|  */ |  | ||||||
| class Link extends Entity { |  | ||||||
|     static get entityName() { return "links"; } |  | ||||||
|     static get primaryKeyName() { return "linkId"; } |  | ||||||
|     static get hashedProperties() { return ["linkId", "noteId", "targetNoteId", "type", "isDeleted", "utcDateCreated", "utcDateModified"]; } |  | ||||||
|  |  | ||||||
|     async getNote() { |  | ||||||
|         return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async getTargetNote() { |  | ||||||
|         return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.targetNoteId]); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     beforeSaving() { |  | ||||||
|         if (!this.isDeleted) { |  | ||||||
|             this.isDeleted = false; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!this.utcDateCreated) { |  | ||||||
|             this.utcDateCreated = dateUtils.utcNowDateTime(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         super.beforeSaving(); |  | ||||||
|  |  | ||||||
|         if (this.isChanged) { |  | ||||||
|             this.utcDateModified = dateUtils.utcNowDateTime(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = Link; |  | ||||||
| @@ -628,10 +628,17 @@ class Note extends Entity { | |||||||
|     /** |     /** | ||||||
|      * Get list of links coming out of this note. |      * Get list of links coming out of this note. | ||||||
|      * |      * | ||||||
|      * @returns {Promise<Link[]>} |      * @deprecated - not intended for general use | ||||||
|  |      * @returns {Promise<Attribute[]>} | ||||||
|      */ |      */ | ||||||
|     async getLinks() { |     async getLinks() { | ||||||
|         return await repository.getEntities("SELECT * FROM links WHERE noteId = ? AND isDeleted = 0", [this.noteId]); |         return await repository.getEntities(` | ||||||
|  |             SELECT *  | ||||||
|  |             FROM attributes  | ||||||
|  |             WHERE noteId = ? AND  | ||||||
|  |                   isDeleted = 0 AND  | ||||||
|  |                   type = 'relation' AND  | ||||||
|  |                   name IN ('internal-link', 'image-link', 'relation-map-link')`, [this.noteId]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								src/public/javascripts/entities/link.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/public/javascripts/entities/link.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | class Link { | ||||||
|  |     constructor(treeCache, row) { | ||||||
|  |         this.treeCache = treeCache; | ||||||
|  |         /** @param {string} linkId */ | ||||||
|  |         this.linkId = row.linkId; | ||||||
|  |         /** @param {string} noteId */ | ||||||
|  |         this.noteId = row.noteId; | ||||||
|  |         /** @param {string} type */ | ||||||
|  |         this.type = row.type; | ||||||
|  |         /** @param {string} targetNoteId */ | ||||||
|  |         this.targetNoteId = row.targetNoteId; | ||||||
|  |         /** @param {string} utcDateCreated */ | ||||||
|  |         this.utcDateCreated = row.utcDateCreated; | ||||||
|  |         /** @param {string} utcDateModified */ | ||||||
|  |         this.utcDateModified = row.utcDateModified; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @returns {NoteShort} */ | ||||||
|  |     async getNote() { | ||||||
|  |         return await this.treeCache.getNote(this.noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @returns {NoteShort} */ | ||||||
|  |     async getTargetNote() { | ||||||
|  |         return await this.treeCache.getNote(this.targetNoteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get toString() { | ||||||
|  |         return `Link(linkId=${this.linkId}, type=${this.type}, note=${this.noteId}, targetNoteId=${this.targetNoteId})`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Link; | ||||||
| @@ -1,4 +1,6 @@ | |||||||
| import server from '../services/server.js'; | import server from '../services/server.js'; | ||||||
|  | import Attribute from './attribute.js'; | ||||||
|  | import Link from './link.js'; | ||||||
|  |  | ||||||
| const LABEL = 'label'; | const LABEL = 'label'; | ||||||
| const LABEL_DEFINITION = 'label-definition'; | const LABEL_DEFINITION = 'label-definition'; | ||||||
| @@ -84,7 +86,8 @@ class NoteShort { | |||||||
|      */ |      */ | ||||||
|     async getAttributes(name) { |     async getAttributes(name) { | ||||||
|         if (!this.attributeCache) { |         if (!this.attributeCache) { | ||||||
|             this.attributeCache = await server.get('notes/' + this.noteId + '/attributes'); |             this.attributeCache = (await server.get('notes/' + this.noteId + '/attributes')) | ||||||
|  |                 .map(attrRow => new Attribute(this.treeCache, attrRow)); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (name) { |         if (name) { | ||||||
| @@ -227,6 +230,14 @@ class NoteShort { | |||||||
|         this.attributeCache = null; |         this.attributeCache = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @return {Promise<Link[]>} | ||||||
|  |      */ | ||||||
|  |     async getLinks() { | ||||||
|  |         return (await server.get('notes/' + this.noteId + '/links')) | ||||||
|  |             .map(linkRow => new Link(this.treeCache, linkRow)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     get toString() { |     get toString() { | ||||||
|         return `Note(noteId=${this.noteId}, title=${this.title})`; |         return `Note(noteId=${this.noteId}, title=${this.title})`; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -58,9 +58,9 @@ class LinkMapWidget extends StandardWidget { | |||||||
|         const linkTypes = [ "hyper", "image", "relation", "relation-map" ]; |         const linkTypes = [ "hyper", "image", "relation", "relation-map" ]; | ||||||
|         const maxNotes = 50; |         const maxNotes = 50; | ||||||
|  |  | ||||||
|         const noteId = this.ctx.note.noteId; |         const currentNoteId = this.ctx.note.noteId; | ||||||
|  |  | ||||||
|         const links = await server.post(`notes/${noteId}/link-map`, { |         const links = await server.post(`notes/${currentNoteId}/link-map`, { | ||||||
|             linkTypes, |             linkTypes, | ||||||
|             maxNotes, |             maxNotes, | ||||||
|             maxDepth: 1 |             maxDepth: 1 | ||||||
| @@ -69,7 +69,7 @@ class LinkMapWidget extends StandardWidget { | |||||||
|         const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId))); |         const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId))); | ||||||
|  |  | ||||||
|         if (noteIds.size === 0) { |         if (noteIds.size === 0) { | ||||||
|             noteIds.add(noteId); |             noteIds.add(currentNoteId); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // preload all notes |         // preload all notes | ||||||
| @@ -104,7 +104,7 @@ class LinkMapWidget extends StandardWidget { | |||||||
|                 $noteBox.append($("<span>").addClass("title").append($link)); |                 $noteBox.append($("<span>").addClass("title").append($link)); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             if (noteId === noteId) { |             if (noteId === currentNoteId) { | ||||||
|                 $noteBox.addClass("link-map-active-note"); |                 $noteBox.addClass("link-map-active-note"); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								src/public/javascripts/widgets/what_links_here.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/public/javascripts/widgets/what_links_here.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import StandardWidget from "./standard_widget.js"; | ||||||
|  |  | ||||||
|  | class WhatLinksHereWidget extends StandardWidget { | ||||||
|  |     getWidgetTitle() { return "What links here"; } | ||||||
|  |  | ||||||
|  |     async doRenderBody() { | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         const $noteId = this.$body.find(".note-info-note-id"); | ||||||
|  |         const $dateCreated = this.$body.find(".note-info-date-created"); | ||||||
|  |         const $dateModified = this.$body.find(".note-info-date-modified"); | ||||||
|  |         const $type = this.$body.find(".note-info-type"); | ||||||
|  |         const $mime = this.$body.find(".note-info-mime"); | ||||||
|  |  | ||||||
|  |         const note = this.ctx.note; | ||||||
|  |  | ||||||
|  |         $noteId.text(note.noteId); | ||||||
|  |         $dateCreated.text(note.dateCreated); | ||||||
|  |         $dateModified.text(note.dateModified); | ||||||
|  |         $type.text(note.type); | ||||||
|  |         $mime.text(note.mime); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     syncDataReceived(syncData) { | ||||||
|  |         if (syncData.find(sd => sd.entityName === 'notes' && sd.entityId === this.ctx.note.noteId)) { | ||||||
|  |             this.doRenderBody(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default WhatLinksHereWidget; | ||||||
| @@ -39,5 +39,4 @@ | |||||||
|  |  | ||||||
| .link-map-active-note { | .link-map-active-note { | ||||||
|     background-color: var(--more-accented-background-color) !important; |     background-color: var(--more-accented-background-color) !important; | ||||||
|     border-width: 3px !important; |  | ||||||
| } | } | ||||||
| @@ -9,7 +9,7 @@ const messagingService = require('../../services/messaging'); | |||||||
| const log = require('../../services/log'); | const log = require('../../services/log'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
| const Link = require('../../entities/link'); | const Attribute = require('../../entities/attribute'); | ||||||
|  |  | ||||||
| async function findClippingNote(todayNote, pageUrl) { | async function findClippingNote(todayNote, pageUrl) { | ||||||
|     const notes = await todayNote.getDescendantNotesWithLabel('pageUrl', pageUrl); |     const notes = await todayNote.getDescendantNotesWithLabel('pageUrl', pageUrl); | ||||||
| @@ -84,10 +84,11 @@ async function addImagesToNote(images, note, content) { | |||||||
|  |  | ||||||
|             const {note: imageNote, url} = await imageService.saveImage(buffer, filename, note.noteId, true); |             const {note: imageNote, url} = await imageService.saveImage(buffer, filename, note.noteId, true); | ||||||
|  |  | ||||||
|             await new Link({ |             await new Attribute({ | ||||||
|                 noteId: note.noteId, |                 noteId: note.noteId, | ||||||
|                 targetNoteId: imageNote.noteId, |                 type: 'relation', | ||||||
|                 type: 'image' |                 value: imageNote.noteId, | ||||||
|  |                 name: 'image-link' | ||||||
|             }).save(); |             }).save(); | ||||||
|  |  | ||||||
|             console.log(`Replacing ${imageId} with ${url}`); |             console.log(`Replacing ${imageId} with ${url}`); | ||||||
|   | |||||||
| @@ -2,38 +2,34 @@ | |||||||
|  |  | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
|  |  | ||||||
| async function getLinks(noteIds, linkTypes) { | async function getRelations(noteIds, relationNames) { | ||||||
|     return (await sql.getManyRows(` |     return (await sql.getManyRows(` | ||||||
|         SELECT noteId, targetNoteId, type |         SELECT noteId, name, value AS targetNoteId | ||||||
|         FROM links |  | ||||||
|         WHERE (noteId IN (???) OR targetNoteId IN (???)) |  | ||||||
|           AND isDeleted = 0 |  | ||||||
|         UNION |  | ||||||
|         SELECT noteId, value, 'relation' |  | ||||||
|         FROM attributes |         FROM attributes | ||||||
|         WHERE (noteId IN (???) OR value IN (???)) |         WHERE (noteId IN (???) OR value IN (???)) | ||||||
|           AND type = 'relation' |           AND type = 'relation' | ||||||
|           AND isDeleted = 0 |           AND isDeleted = 0 | ||||||
|     `, Array.from(noteIds))).filter(l => linkTypes.includes(l.type)); |     `, Array.from(noteIds))).filter(l => relationNames.includes(l.name)); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getLinkMap(req) { | async function getLinkMap(req) { | ||||||
|     const {noteId} = req.params; |     const {noteId} = req.params; | ||||||
|     const {linkTypes, maxNotes, maxDepth} = req.body; |     const {relationNames, maxNotes, maxDepth} = req.body; | ||||||
|  |  | ||||||
|     let noteIds = new Set([noteId]); |     let noteIds = new Set([noteId]); | ||||||
|     let links = []; |     let relations; | ||||||
|  |  | ||||||
|     let depth = 0; |     let depth = 0; | ||||||
|  |  | ||||||
|     while (true) { |     while (true) { | ||||||
|         links = await getLinks(noteIds, linkTypes); |         relations = await getRelations(noteIds, relationNames); | ||||||
|  |  | ||||||
|         if (depth === maxDepth) { |         if (depth === maxDepth) { | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const newNoteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId))); |         const newNoteIds = new Set(relations.map(rel => rel.noteId) | ||||||
|  |                                             .concat(relations.map(rel => rel.targetNoteId))); | ||||||
|  |  | ||||||
|         if (newNoteIds.size === noteIds.size) { |         if (newNoteIds.size === noteIds.size) { | ||||||
|             // no new note discovered, no need to search any further |             // no new note discovered, no need to search any further | ||||||
| @@ -51,9 +47,9 @@ async function getLinkMap(req) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // keep only links coming from and targetting some note in the noteIds set |     // keep only links coming from and targetting some note in the noteIds set | ||||||
|     links = links.filter(l => noteIds.has(l.noteId) && noteIds.has(l.targetNoteId)); |     relations = relations.filter(rel => noteIds.has(rel.noteId) && noteIds.has(rel.targetNoteId)); | ||||||
|  |  | ||||||
|     return links; |     return relations; | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								src/routes/api/links.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/routes/api/links.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const repository = require('../../services/repository'); | ||||||
|  |  | ||||||
|  | async function getLinks(req) { | ||||||
|  |     const note = await repository.getNote(req.params.noteId); | ||||||
|  |  | ||||||
|  |     if (!note) { | ||||||
|  |         return [404, `Note ${req.params.noteId} not found`]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return await note.getLinks(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getIncomingLinks(req) { | ||||||
|  |     const note = await repository.getNote(req.params.noteId); | ||||||
|  |  | ||||||
|  |     if (!note) { | ||||||
|  |         return [404, `Note ${req.params.noteId} not found`]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     note.getTargetRelations() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     getLinks, | ||||||
|  |     getIncomingLinks | ||||||
|  | }; | ||||||
| @@ -114,8 +114,7 @@ async function getRelationMap(req) { | |||||||
|         noteTitles: {}, |         noteTitles: {}, | ||||||
|         relations: [], |         relations: [], | ||||||
|         // relation name => inverse relation name |         // relation name => inverse relation name | ||||||
|         inverseRelations: {}, |         inverseRelations: {} | ||||||
|         links: [] |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if (noteIds.length === 0) { |     if (noteIds.length === 0) { | ||||||
| @@ -145,16 +144,6 @@ async function getRelationMap(req) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     resp.links = (await repository.getEntities(`SELECT * FROM links WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds)) |  | ||||||
|         .filter(link => noteIds.includes(link.targetNoteId)) |  | ||||||
|         .map(link => { |  | ||||||
|             return { |  | ||||||
|                 linkId: link.linkId, |  | ||||||
|                 sourceNoteId: link.noteId, |  | ||||||
|                 targetNoteId: link.targetNoteId |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|     return resp; |     return resp; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ const filesRoute = require('./api/file_upload'); | |||||||
| const searchRoute = require('./api/search'); | const searchRoute = require('./api/search'); | ||||||
| const dateNotesRoute = require('./api/date_notes'); | const dateNotesRoute = require('./api/date_notes'); | ||||||
| const linkMapRoute = require('./api/link_map'); | const linkMapRoute = require('./api/link_map'); | ||||||
|  | const linksRoute = require('./api/links'); | ||||||
| const clipperRoute = require('./api/clipper'); | const clipperRoute = require('./api/clipper'); | ||||||
|  |  | ||||||
| const log = require('../services/log'); | const log = require('../services/log'); | ||||||
| @@ -158,6 +159,8 @@ function register(app) { | |||||||
|     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); |     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); | ||||||
|  |  | ||||||
|     apiRoute(POST, '/api/notes/:noteId/link-map', linkMapRoute.getLinkMap); |     apiRoute(POST, '/api/notes/:noteId/link-map', linkMapRoute.getLinkMap); | ||||||
|  |     apiRoute(GET, '/api/notes/:noteId/links', linksRoute.getLinks); | ||||||
|  |     apiRoute(GET, '/api/notes/:noteId/incoming-links', linksRoute.getIncomingLinks); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/date-notes/date/:date', dateNotesRoute.getDateNote); |     apiRoute(GET, '/api/date-notes/date/:date', dateNotesRoute.getDateNote); | ||||||
|     apiRoute(GET, '/api/date-notes/month/:month', dateNotesRoute.getMonthNote); |     apiRoute(GET, '/api/date-notes/month/:month', dateNotesRoute.getMonthNote); | ||||||
|   | |||||||
| @@ -4,8 +4,8 @@ const build = require('./build'); | |||||||
| const packageJson = require('../../package'); | const packageJson = require('../../package'); | ||||||
| const {TRILIUM_DATA_DIR} = require('./data_dir'); | const {TRILIUM_DATA_DIR} = require('./data_dir'); | ||||||
|  |  | ||||||
| const APP_DB_VERSION = 136; | const APP_DB_VERSION = 137; | ||||||
| const SYNC_VERSION = 9; | const SYNC_VERSION = 10; | ||||||
| const CLIPPER_PROTOCOL_VERSION = "1.0"; | const CLIPPER_PROTOCOL_VERSION = "1.0"; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
| @@ -12,7 +12,6 @@ const Attribute = require('../entities/attribute'); | |||||||
| const NoteRevision = require('../entities/note_revision'); | const NoteRevision = require('../entities/note_revision'); | ||||||
| const RecentNote = require('../entities/recent_note'); | const RecentNote = require('../entities/recent_note'); | ||||||
| const Option = require('../entities/option'); | const Option = require('../entities/option'); | ||||||
| const Link = require('../entities/link'); |  | ||||||
|  |  | ||||||
| async function getHash(tableName, primaryKeyName, whereBranch) { | async function getHash(tableName, primaryKeyName, whereBranch) { | ||||||
|     // subselect is necessary to have correct ordering in GROUP_CONCAT |     // subselect is necessary to have correct ordering in GROUP_CONCAT | ||||||
| @@ -40,7 +39,6 @@ async function getHashes() { | |||||||
|         options: await getHash(Option.entityName, Option.primaryKeyName, "isSynced = 1"), |         options: await getHash(Option.entityName, Option.primaryKeyName, "isSynced = 1"), | ||||||
|         attributes: await getHash(Attribute.entityName, Attribute.primaryKeyName), |         attributes: await getHash(Attribute.entityName, Attribute.primaryKeyName), | ||||||
|         api_tokens: await getHash(ApiToken.entityName, ApiToken.primaryKeyName), |         api_tokens: await getHash(ApiToken.entityName, ApiToken.primaryKeyName), | ||||||
|         links: await getHash(Link.entityName, Link.primaryKeyName) |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     const elapseTimeMs = Date.now() - startTime.getTime(); |     const elapseTimeMs = Date.now() - startTime.getTime(); | ||||||
|   | |||||||
| @@ -115,12 +115,6 @@ async function exportToTar(exportContext, branch, format, res) { | |||||||
|                     isInheritable: attribute.isInheritable, |                     isInheritable: attribute.isInheritable, | ||||||
|                     position: attribute.position |                     position: attribute.position | ||||||
|                 }; |                 }; | ||||||
|             }), |  | ||||||
|             links: (await note.getLinks()).map(link => { |  | ||||||
|                 return { |  | ||||||
|                     type: link.type, |  | ||||||
|                     targetNoteId: link.targetNoteId |  | ||||||
|                 } |  | ||||||
|             }) |             }) | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
| @@ -220,9 +214,8 @@ async function exportToTar(exportContext, branch, format, res) { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     for (const noteMeta of Object.values(noteIdToMeta)) { |     for (const noteMeta of Object.values(noteIdToMeta)) { | ||||||
|         // filter out relations and links which are not inside this export |         // filter out relations which are not inside this export | ||||||
|         noteMeta.attributes = noteMeta.attributes.filter(attr => attr.type !== 'relation' || attr.value in noteIdToMeta); |         noteMeta.attributes = noteMeta.attributes.filter(attr => attr.type !== 'relation' || attr.value in noteIdToMeta); | ||||||
|         noteMeta.links = noteMeta.links.filter(link => link.targetNoteId in noteIdToMeta); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!metaFile.files[0]) { // corner case of disabled export for exported note |     if (!metaFile.files[0]) { // corner case of disabled export for exported note | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const Attribute = require('../../entities/attribute'); | const Attribute = require('../../entities/attribute'); | ||||||
| const Link = require('../../entities/link'); |  | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
| const log = require('../../services/log'); | const log = require('../../services/log'); | ||||||
| const repository = require('../../services/repository'); | const repository = require('../../services/repository'); | ||||||
| @@ -26,7 +25,6 @@ async function importTar(importContext, fileBuffer, importRootNote) { | |||||||
|     // maps from original noteId (in tar file) to newly generated noteId |     // maps from original noteId (in tar file) to newly generated noteId | ||||||
|     const noteIdMap = {}; |     const noteIdMap = {}; | ||||||
|     const attributes = []; |     const attributes = []; | ||||||
|     const links = []; |  | ||||||
|     // path => noteId |     // path => noteId | ||||||
|     const createdPaths = { '/': importRootNote.noteId, '\\': importRootNote.noteId }; |     const createdPaths = { '/': importRootNote.noteId, '\\': importRootNote.noteId }; | ||||||
|     const mdReader = new commonmark.Parser(); |     const mdReader = new commonmark.Parser(); | ||||||
| @@ -146,7 +144,7 @@ async function importTar(importContext, fileBuffer, importRootNote) { | |||||||
|         return { type, mime }; |         return { type, mime }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function saveAttributesAndLinks(note, noteMeta) { |     async function saveAttributes(note, noteMeta) { | ||||||
|         if (!noteMeta) { |         if (!noteMeta) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -169,13 +167,6 @@ async function importTar(importContext, fileBuffer, importRootNote) { | |||||||
|  |  | ||||||
|             attributes.push(attr); |             attributes.push(attr); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         for (const link of noteMeta.links) { |  | ||||||
|             link.noteId = note.noteId; |  | ||||||
|             link.targetNoteId = getNewNoteId(link.targetNoteId); |  | ||||||
|  |  | ||||||
|             links.push(link); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function saveDirectory(filePath) { |     async function saveDirectory(filePath) { | ||||||
| @@ -200,7 +191,7 @@ async function importTar(importContext, fileBuffer, importRootNote) { | |||||||
|             isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), |             isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), | ||||||
|         })); |         })); | ||||||
|  |  | ||||||
|         await saveAttributesAndLinks(note, noteMeta); |         await saveAttributes(note, noteMeta); | ||||||
|  |  | ||||||
|         if (!firstNote) { |         if (!firstNote) { | ||||||
|             firstNote = note; |             firstNote = note; | ||||||
| @@ -246,9 +237,11 @@ async function importTar(importContext, fileBuffer, importRootNote) { | |||||||
|             content = content.toString("UTF-8"); |             content = content.toString("UTF-8"); | ||||||
|  |  | ||||||
|             if (noteMeta) { |             if (noteMeta) { | ||||||
|  |                 const internalLinks = (noteMeta.attributes || []).find(attr => attr.type === 'relation' && attr.name === 'internal-link'); | ||||||
|  |  | ||||||
|                 // this will replace all internal links (<a> and <img>) inside the body |                 // this will replace all internal links (<a> and <img>) inside the body | ||||||
|                 // links pointing outside the export will be broken and changed (ctx.getNewNoteId() will still assign new noteId) |                 // links pointing outside the export will be broken and changed (ctx.getNewNoteId() will still assign new noteId) | ||||||
|                 for (const link of noteMeta.links || []) { |                 for (const link of internalLinks) { | ||||||
|                     // no need to escape the regexp find string since it's a noteId which doesn't contain any special characters |                     // no need to escape the regexp find string since it's a noteId which doesn't contain any special characters | ||||||
|                     content = content.replace(new RegExp(link.targetNoteId, "g"), getNewNoteId(link.targetNoteId)); |                     content = content.replace(new RegExp(link.targetNoteId, "g"), getNewNoteId(link.targetNoteId)); | ||||||
|                 } |                 } | ||||||
| @@ -278,7 +271,7 @@ async function importTar(importContext, fileBuffer, importRootNote) { | |||||||
|                 isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), |                 isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), | ||||||
|             })); |             })); | ||||||
|  |  | ||||||
|             await saveAttributesAndLinks(note, noteMeta); |             await saveAttributes(note, noteMeta); | ||||||
|  |  | ||||||
|             if (!noteMeta && (type === 'file' || type === 'image')) { |             if (!noteMeta && (type === 'file' || type === 'image')) { | ||||||
|                 attributes.push({ |                 attributes.push({ | ||||||
| @@ -379,15 +372,6 @@ async function importTar(importContext, fileBuffer, importRootNote) { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             for (const link of links) { |  | ||||||
|                 if (link.targetNoteId in createdNoteIds) { |  | ||||||
|                     await new Link(link).save(); |  | ||||||
|                 } |  | ||||||
|                 else { |  | ||||||
|                     log.info("Link not imported since target note doesn't exist: " + JSON.stringify(link)); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             resolve(firstNote); |             resolve(firstNote); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ const eventService = require('./events'); | |||||||
| const repository = require('./repository'); | const repository = require('./repository'); | ||||||
| const cls = require('../services/cls'); | const cls = require('../services/cls'); | ||||||
| const Note = require('../entities/note'); | const Note = require('../entities/note'); | ||||||
| const Link = require('../entities/link'); |  | ||||||
| const NoteRevision = require('../entities/note_revision'); | const NoteRevision = require('../entities/note_revision'); | ||||||
| const Branch = require('../entities/branch'); | const Branch = require('../entities/branch'); | ||||||
| const Attribute = require('../entities/attribute'); | const Attribute = require('../entities/attribute'); | ||||||
| @@ -215,8 +214,8 @@ function findImageLinks(content, foundLinks) { | |||||||
|  |  | ||||||
|     while (match = re.exec(content)) { |     while (match = re.exec(content)) { | ||||||
|         foundLinks.push({ |         foundLinks.push({ | ||||||
|             type: 'image', |             type: 'image-link', | ||||||
|             targetNoteId: match[1] |             value: match[1] | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -225,14 +224,14 @@ function findImageLinks(content, foundLinks) { | |||||||
|     return content.replace(/src="[^"]*\/api\/images\//g, 'src="api/images/'); |     return content.replace(/src="[^"]*\/api\/images\//g, 'src="api/images/'); | ||||||
| } | } | ||||||
|  |  | ||||||
| function findHyperLinks(content, foundLinks) { | function findInternalLinks(content, foundLinks) { | ||||||
|     const re = /href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g; |     const re = /href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g; | ||||||
|     let match; |     let match; | ||||||
|  |  | ||||||
|     while (match = re.exec(content)) { |     while (match = re.exec(content)) { | ||||||
|         foundLinks.push({ |         foundLinks.push({ | ||||||
|             type: 'hyper', |             name: 'internal-link', | ||||||
|             targetNoteId: match[1] |             value: match[1] | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -245,8 +244,8 @@ function findRelationMapLinks(content, foundLinks) { | |||||||
|  |  | ||||||
|     for (const note of obj.notes) { |     for (const note of obj.notes) { | ||||||
|         foundLinks.push({ |         foundLinks.push({ | ||||||
|             type: 'relation-map', |             type: 'relation-map-link', | ||||||
|             targetNoteId: note.noteId |             value: note.noteId | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -260,7 +259,7 @@ async function saveLinks(note, content) { | |||||||
|  |  | ||||||
|     if (note.type === 'text') { |     if (note.type === 'text') { | ||||||
|         content = findImageLinks(content, foundLinks); |         content = findImageLinks(content, foundLinks); | ||||||
|         content = findHyperLinks(content, foundLinks); |         content = findInternalLinks(content, foundLinks); | ||||||
|     } |     } | ||||||
|     else if (note.type === 'relation-map') { |     else if (note.type === 'relation-map') { | ||||||
|         findRelationMapLinks(content, foundLinks); |         findRelationMapLinks(content, foundLinks); | ||||||
| @@ -273,14 +272,15 @@ async function saveLinks(note, content) { | |||||||
|  |  | ||||||
|     for (const foundLink of foundLinks) { |     for (const foundLink of foundLinks) { | ||||||
|         const existingLink = existingLinks.find(existingLink => |         const existingLink = existingLinks.find(existingLink => | ||||||
|             existingLink.targetNoteId === foundLink.targetNoteId |             existingLink.value === foundLink.value | ||||||
|             && existingLink.type === foundLink.type); |             && existingLink.name === foundLink.name); | ||||||
|  |  | ||||||
|         if (!existingLink) { |         if (!existingLink) { | ||||||
|             await new Link({ |             await new Attribute({ | ||||||
|                 noteId: note.noteId, |                 noteId: note.noteId, | ||||||
|                 targetNoteId: foundLink.targetNoteId, |                 type: 'relation', | ||||||
|                 type: foundLink.type |                 name: foundLink.name, | ||||||
|  |                 value: foundLink.targetNoteId, | ||||||
|             }).save(); |             }).save(); | ||||||
|         } |         } | ||||||
|         else if (existingLink.isDeleted) { |         else if (existingLink.isDeleted) { | ||||||
| @@ -292,8 +292,8 @@ async function saveLinks(note, content) { | |||||||
|  |  | ||||||
|     // marking links as deleted if they are not present on the page anymore |     // marking links as deleted if they are not present on the page anymore | ||||||
|     const unusedLinks = existingLinks.filter(existingLink => !foundLinks.some(foundLink => |     const unusedLinks = existingLinks.filter(existingLink => !foundLinks.some(foundLink => | ||||||
|                                     existingLink.targetNoteId === foundLink.targetNoteId |                                     existingLink.value === foundLink.value | ||||||
|                                     && existingLink.type === foundLink.type)); |                                     && existingLink.name === foundLink.name)); | ||||||
|  |  | ||||||
|     for (const unusedLink of unusedLinks) { |     for (const unusedLink of unusedLinks) { | ||||||
|         unusedLink.isDeleted = true; |         unusedLink.isDeleted = true; | ||||||
| @@ -415,11 +415,6 @@ async function deleteNote(branch) { | |||||||
|             await relation.save(); |             await relation.save(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         for (const link of await note.getLinks()) { |  | ||||||
|             link.isDeleted = true; |  | ||||||
|             await link.save(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         for (const link of await note.getTargetLinks()) { |         for (const link of await note.getTargetLinks()) { | ||||||
|             link.isDeleted = true; |             link.isDeleted = true; | ||||||
|             await link.save(); |             await link.save(); | ||||||
|   | |||||||
| @@ -249,8 +249,7 @@ const primaryKeys = { | |||||||
|     "recent_notes": "noteId", |     "recent_notes": "noteId", | ||||||
|     "api_tokens": "apiTokenId", |     "api_tokens": "apiTokenId", | ||||||
|     "options": "name", |     "options": "name", | ||||||
|     "attributes": "attributeId", |     "attributes": "attributeId" | ||||||
|     "links": "linkId" |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| async function getEntityRow(entityName, entityId) { | async function getEntityRow(entityName, entityId) { | ||||||
|   | |||||||
| @@ -32,10 +32,6 @@ async function addRecentNoteSync(noteId, sourceId) { | |||||||
|     await addEntitySync("recent_notes", noteId, sourceId); |     await addEntitySync("recent_notes", noteId, sourceId); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function addLinkSync(linkId, sourceId) { |  | ||||||
|     await addEntitySync("links", linkId, sourceId); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function addAttributeSync(attributeId, sourceId) { | async function addAttributeSync(attributeId, sourceId) { | ||||||
|     await addEntitySync("attributes", attributeId, sourceId); |     await addEntitySync("attributes", attributeId, sourceId); | ||||||
| } | } | ||||||
| @@ -101,7 +97,6 @@ async function fillAllSyncRows() { | |||||||
|     await fillSyncRows("recent_notes", "noteId"); |     await fillSyncRows("recent_notes", "noteId"); | ||||||
|     await fillSyncRows("attributes", "attributeId"); |     await fillSyncRows("attributes", "attributeId"); | ||||||
|     await fillSyncRows("api_tokens", "apiTokenId"); |     await fillSyncRows("api_tokens", "apiTokenId"); | ||||||
|     await fillSyncRows("links", "linkId"); |  | ||||||
|     await fillSyncRows("options", "name", 'isSynced = 1'); |     await fillSyncRows("options", "name", 'isSynced = 1'); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -115,7 +110,6 @@ module.exports = { | |||||||
|     addRecentNoteSync, |     addRecentNoteSync, | ||||||
|     addAttributeSync, |     addAttributeSync, | ||||||
|     addApiTokenSync, |     addApiTokenSync, | ||||||
|     addLinkSync, |  | ||||||
|     addEntitySync, |     addEntitySync, | ||||||
|     fillAllSyncRows |     fillAllSyncRows | ||||||
| }; | }; | ||||||
| @@ -28,9 +28,6 @@ async function updateEntity(sync, entity, sourceId) { | |||||||
|     else if (entityName === 'recent_notes') { |     else if (entityName === 'recent_notes') { | ||||||
|         await updateRecentNotes(entity, sourceId); |         await updateRecentNotes(entity, sourceId); | ||||||
|     } |     } | ||||||
|     else if (entityName === 'links') { |  | ||||||
|         await updateLink(entity, sourceId); |  | ||||||
|     } |  | ||||||
|     else if (entityName === 'attributes') { |     else if (entityName === 'attributes') { | ||||||
|         await updateAttribute(entity, sourceId); |         await updateAttribute(entity, sourceId); | ||||||
|     } |     } | ||||||
| @@ -159,20 +156,6 @@ async function updateRecentNotes(entity, sourceId) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| async function updateLink(entity, sourceId) { |  | ||||||
|     const origLink = await sql.getRow("SELECT * FROM links WHERE linkId = ?", [entity.linkId]); |  | ||||||
|  |  | ||||||
|     if (!origLink || origLink.utcDateModified <= entity.utcDateModified) { |  | ||||||
|         await sql.transactional(async () => { |  | ||||||
|             await sql.replace("links", entity); |  | ||||||
|  |  | ||||||
|             await syncTableService.addLinkSync(entity.linkId, sourceId); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         log.info("Update/sync link " + entity.linkId); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function updateAttribute(entity, sourceId) { | async function updateAttribute(entity, sourceId) { | ||||||
|     const origAttribute = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [entity.attributeId]); |     const origAttribute = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [entity.attributeId]); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user