mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Merge branch 'beta'
# Conflicts: # docs/backend_api/BAttachment.html # docs/backend_api/BRevision.html # docs/backend_api/BackendScriptApi.html # docs/backend_api/becca_entities_battachment.js.html # docs/backend_api/becca_entities_bblob.js.html # docs/backend_api/becca_entities_brevision.js.html # docs/frontend_api/FNote.html # docs/frontend_api/FrontendScriptApi.html # docs/frontend_api/entities_fattachment.js.html # docs/frontend_api/entities_fblob.js.html # docs/frontend_api/services_frontend_script_api.js.html # package-lock.json # src/public/app/services/frontend_script_api.js
This commit is contained in:
		| @@ -1,11 +1,13 @@ | ||||
| //https://prettier.io/docs/en/options.html | ||||
| module.exports = { | ||||
| 	semi: true, | ||||
| 	trailingComma: 'es5', | ||||
| 	trailingComma: 'none', | ||||
| 	singleQuote: true, | ||||
| 	printWidth: 120, | ||||
| 	printWidth: 100, | ||||
| 	tabWidth: 4, | ||||
| 	// useTabs: false, | ||||
| 	// bracketSpacing: true, | ||||
| 	useTabs: false, | ||||
| 	quoteProps: "as-needed", | ||||
| 	bracketSpacing: true, | ||||
| 	arrowParens: "avoid" | ||||
| 	// htmlWhitespaceSensitivity: 'ignore', | ||||
| }; | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| module.exports = () => console.log("NOOP, moved to migration 0189"); | ||||
| @@ -1,4 +0,0 @@ | ||||
| -- black theme has been removed, dark is closest replacement | ||||
| UPDATE options SET value = 'dark' WHERE name = 'theme' AND value = 'black'; | ||||
|  | ||||
| UPDATE options SET value = 'light' WHERE name = 'theme' AND value = 'white'; | ||||
| @@ -1,2 +0,0 @@ | ||||
| ALTER TABLE branches DROP COLUMN utcDateCreated; | ||||
| ALTER TABLE options DROP COLUMN utcDateCreated; | ||||
| @@ -1,33 +0,0 @@ | ||||
| CREATE TABLE IF NOT EXISTS "mig_entity_changes" ( | ||||
|                                                 `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||
|                                                 `entityName`	TEXT NOT NULL, | ||||
|                                                 `entityId`	TEXT NOT NULL, | ||||
|                                                 `hash`	TEXT NOT NULL, | ||||
|                                                 `isErased` INT NOT NULL, | ||||
|                                                 `changeId` TEXT NOT NULL, | ||||
|                                                 `sourceId` TEXT NOT NULL, | ||||
|                                                 `isSynced` INTEGER NOT NULL, | ||||
|                                                 `utcDateChanged` TEXT NOT NULL | ||||
| ); | ||||
|  | ||||
| INSERT INTO mig_entity_changes (id, entityName, entityId, hash, isErased, changeId, sourceId, isSynced, utcDateChanged) | ||||
|     SELECT id, entityName, entityId, hash, isErased, '', sourceId, isSynced, utcDateChanged FROM entity_changes; | ||||
|  | ||||
| -- delete duplicates https://github.com/zadam/trilium/issues/2534 | ||||
| DELETE FROM mig_entity_changes WHERE isErased = 0 AND id IN ( | ||||
|     SELECT id FROM mig_entity_changes ec | ||||
|     WHERE ( | ||||
|               SELECT COUNT(*) FROM mig_entity_changes | ||||
|               WHERE ec.entityName = mig_entity_changes.entityName | ||||
|                 AND ec.entityId = mig_entity_changes.entityId | ||||
|           ) > 1 | ||||
| ); | ||||
|  | ||||
| DROP TABLE entity_changes; | ||||
|  | ||||
| ALTER TABLE mig_entity_changes RENAME TO entity_changes; | ||||
|  | ||||
| CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" ( | ||||
|                                                                                  `entityName`, | ||||
|                                                                                  `entityId` | ||||
|     ); | ||||
| @@ -1,8 +0,0 @@ | ||||
| UPDATE branches SET branchId = 'hidden' where branchId = ( | ||||
|     SELECT branchId FROM branches | ||||
|     WHERE parentNoteId = 'root' | ||||
|       AND noteId = 'hidden' | ||||
|       AND isDeleted = 0 | ||||
|     ORDER BY utcDateModified | ||||
|     LIMIT 1 | ||||
| ); | ||||
| @@ -1 +0,0 @@ | ||||
| DELETE FROM options WHERE name = 'username'; | ||||
| @@ -1,15 +0,0 @@ | ||||
| CREATE TABLE IF NOT EXISTS "etapi_tokens" | ||||
| ( | ||||
|     etapiTokenId TEXT PRIMARY KEY NOT NULL, | ||||
|     name TEXT NOT NULL, | ||||
|     tokenHash TEXT NOT NULL, | ||||
|     utcDateCreated TEXT NOT NULL, | ||||
|     utcDateModified TEXT NOT NULL, | ||||
|     isDeleted INT NOT NULL DEFAULT 0); | ||||
|  | ||||
| INSERT INTO etapi_tokens (etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified, isDeleted) | ||||
| SELECT apiTokenId, 'Trilium Sender', token, utcDateCreated, utcDateCreated, isDeleted FROM api_tokens; | ||||
|  | ||||
| DROP TABLE api_tokens; | ||||
|  | ||||
| UPDATE entity_changes SET entityName = 'etapi_tokens' WHERE entityName = 'api_tokens'; | ||||
| @@ -1,10 +0,0 @@ | ||||
| module.exports = () => { | ||||
|     const sql = require('../../src/services/sql'); | ||||
|     const crypto = require('crypto'); | ||||
|  | ||||
|     for (const {etapiTokenId, token} of sql.getRows("SELECT etapiTokenId, tokenHash AS token FROM etapi_tokens")) { | ||||
|         const tokenHash = crypto.createHash('sha256').update(token).digest('base64'); | ||||
|          | ||||
|         sql.execute(`UPDATE etapi_tokens SET tokenHash = ? WHERE etapiTokenId = ?`, [tokenHash, etapiTokenId]); | ||||
|     } | ||||
| }; | ||||
| @@ -1,20 +0,0 @@ | ||||
| DROP TABLE entity_changes; | ||||
| -- not preserving the data because of https://github.com/zadam/trilium/issues/3447 | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "entity_changes" ( | ||||
|                                                     `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||
|                                                     `entityName`	TEXT NOT NULL, | ||||
|                                                     `entityId`	TEXT NOT NULL, | ||||
|                                                     `hash`	TEXT NOT NULL, | ||||
|                                                     `isErased` INT NOT NULL, | ||||
|                                                     `changeId` TEXT NOT NULL, | ||||
|                                                     `componentId` TEXT NOT NULL, | ||||
|                                                     `instanceId` TEXT NOT NULL, | ||||
|                                                     `isSynced` INTEGER NOT NULL, | ||||
|                                                     `utcDateChanged` TEXT NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE UNIQUE INDEX `IDX_entityChanges_entityName_entityId` ON "entity_changes" ( | ||||
|                                                                                  `entityName`, | ||||
|                                                                                  `entityId` | ||||
|     ); | ||||
| @@ -1 +0,0 @@ | ||||
| CREATE INDEX `IDX_entity_changes_changeId` ON `entity_changes` (`changeId`); | ||||
| @@ -1,15 +0,0 @@ | ||||
| const becca = require('../../src/becca/becca'); | ||||
| const beccaLoader = require('../../src/becca/becca_loader'); | ||||
| const cls = require('../../src/services/cls'); | ||||
|  | ||||
| module.exports = () => { | ||||
|     cls.init(() => { | ||||
|         beccaLoader.load(); | ||||
|  | ||||
|         for (const note of Object.values(becca.notes)) { | ||||
|             if (note.hasLabel('calendarRoot')) { | ||||
|                 note.addLabel('excludeFromNoteMap', "", true); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -1,2 +0,0 @@ | ||||
| -- removing potential remnants of recent notes in entity changes, see https://github.com/zadam/trilium/issues/2842 | ||||
| DELETE FROM entity_changes WHERE entityName = 'recent_notes'; | ||||
| @@ -1,2 +0,0 @@ | ||||
| UPDATE attributes SET value = replace(value, 'setLabelValue', 'updateLabelValue') WHERE name = 'action' AND type = 'label'; | ||||
| UPDATE attributes SET value = replace(value, 'setRelationTarget', 'updateRelationTarget') WHERE name = 'action' AND type = 'label'; | ||||
| @@ -1 +0,0 @@ | ||||
| module.exports = () => console.log("NOOP, increased because of protected notes IV change"); | ||||
| @@ -1,6 +0,0 @@ | ||||
| UPDATE branches SET branchId = '_hidden__search' WHERE parentNoteId = 'hidden' AND noteId = 'search' AND isDeleted = 0; | ||||
| UPDATE branches SET branchId = 'root__globalNoteMap' WHERE parentNoteId = 'singles' AND noteId = 'globalnotemap' AND isDeleted = 0; | ||||
| UPDATE branches SET branchId = '_hidden__sqlConsole' WHERE parentNoteId = 'hidden' AND noteId = 'sqlconsole' AND isDeleted = 0; | ||||
| UPDATE branches SET branchId = 'root__hidden' WHERE parentNoteId = 'root' AND noteId = 'hidden' AND isDeleted = 0; | ||||
| UPDATE branches SET branchId = '_hidden__bulkAction' WHERE parentNoteId = 'hidden' AND noteId = 'bulkaction' AND isDeleted = 0; | ||||
| UPDATE branches SET branchId = '_hidden__share' WHERE parentNoteId = 'root' AND noteId = 'share' AND isDeleted = 0; | ||||
| @@ -1,53 +0,0 @@ | ||||
| UPDATE notes SET noteId = '_globalNoteMap', title = 'Note Map' WHERE noteId = 'globalnotemap'; | ||||
| UPDATE note_contents SET noteId = '_globalNoteMap' WHERE noteId = 'globalnotemap'; | ||||
| UPDATE note_revisions SET noteId = '_globalNoteMap' WHERE noteId = 'globalnotemap'; | ||||
| UPDATE branches SET noteId = '_globalNoteMap' WHERE noteId = 'globalnotemap'; | ||||
| UPDATE branches SET parentNoteId = '_globalNoteMap' WHERE parentNoteId = 'globalnotemap'; | ||||
| UPDATE attributes SET noteId = '_globalNoteMap' WHERE noteId = 'globalnotemap'; | ||||
| UPDATE attributes SET value = '_globalNoteMap' WHERE type = 'relation' AND value = 'globalnotemap'; | ||||
| UPDATE entity_changes SET entityId = '_globalNoteMap' WHERE entityId = 'globalnotemap'; | ||||
|  | ||||
| UPDATE notes SET noteId = '_bulkAction', title = 'Bulk Action' WHERE noteId = 'bulkaction'; | ||||
| UPDATE note_contents SET noteId = '_bulkAction' WHERE noteId = 'bulkaction'; | ||||
| UPDATE note_revisions SET noteId = '_bulkAction' WHERE noteId = 'bulkaction'; | ||||
| UPDATE branches SET parentNoteId = '_bulkAction' WHERE parentNoteId = 'bulkaction'; | ||||
| UPDATE branches SET noteId = '_bulkAction' WHERE noteId = 'bulkaction'; | ||||
| UPDATE attributes SET noteId = '_bulkAction' WHERE noteId = 'bulkaction'; | ||||
| UPDATE attributes SET value = '_bulkAction' WHERE type = 'relation' AND value = 'bulkaction'; | ||||
| UPDATE entity_changes SET entityId = '_bulkAction' WHERE entityId = 'bulkaction'; | ||||
|  | ||||
| UPDATE notes SET noteId = '_sqlConsole', title = 'SQL Console History' WHERE noteId = 'sqlconsole'; | ||||
| UPDATE note_contents SET noteId = '_sqlConsole' WHERE noteId = 'sqlconsole'; | ||||
| UPDATE note_revisions SET noteId = '_sqlConsole' WHERE noteId = 'sqlconsole'; | ||||
| UPDATE branches SET noteId = '_sqlConsole' WHERE noteId = 'sqlconsole'; | ||||
| UPDATE branches SET parentNoteId = '_sqlConsole' WHERE parentNoteId = 'sqlconsole'; | ||||
| UPDATE attributes SET noteId = '_sqlConsole' WHERE noteId = 'sqlconsole'; | ||||
| UPDATE attributes SET value = '_sqlConsole' WHERE type = 'relation' AND value = 'sqlconsole'; | ||||
| UPDATE entity_changes SET entityId = '_sqlConsole' WHERE entityId = 'sqlconsole'; | ||||
|  | ||||
| UPDATE notes SET noteId = '_hidden', title = 'Hidden Notes' WHERE noteId = 'hidden'; | ||||
| UPDATE note_contents SET noteId = '_hidden' WHERE noteId = 'hidden'; | ||||
| UPDATE note_revisions SET noteId = '_hidden' WHERE noteId = 'hidden'; | ||||
| UPDATE branches SET noteId = '_hidden', prefix = NULL WHERE noteId = 'hidden'; | ||||
| UPDATE branches SET parentNoteId = '_hidden' WHERE parentNoteId = 'hidden'; | ||||
| UPDATE attributes SET noteId = '_hidden' WHERE noteId = 'hidden'; | ||||
| UPDATE attributes SET value = '_hidden' WHERE type = 'relation' AND value = 'hidden'; | ||||
| UPDATE entity_changes SET entityId = '_hidden' WHERE entityId = 'hidden'; | ||||
|  | ||||
| UPDATE notes SET noteId = '_search', title = 'Search History' WHERE noteId = 'search'; | ||||
| UPDATE note_contents SET noteId = '_search' WHERE noteId = 'search'; | ||||
| UPDATE note_revisions SET noteId = '_search' WHERE noteId = 'search'; | ||||
| UPDATE branches SET noteId = '_search' WHERE noteId = 'search'; | ||||
| UPDATE branches SET parentNoteId = '_search' WHERE parentNoteId = 'search'; | ||||
| UPDATE attributes SET noteId = '_search' WHERE noteId = 'search'; | ||||
| UPDATE attributes SET value = '_search' WHERE type = 'relation' AND value = 'search'; | ||||
| UPDATE entity_changes SET entityId = '_search' WHERE entityId = 'search'; | ||||
|  | ||||
| UPDATE notes SET noteId = '_share', title = 'Shared Notes' WHERE noteId = 'share'; | ||||
| UPDATE note_contents SET noteId = '_share' WHERE noteId = 'share'; | ||||
| UPDATE note_revisions SET noteId = '_share' WHERE noteId = 'share'; | ||||
| UPDATE branches SET noteId = '_share' WHERE noteId = 'share'; | ||||
| UPDATE branches SET parentNoteId = '_share' WHERE parentNoteId = 'share'; | ||||
| UPDATE attributes SET noteId = '_share' WHERE noteId = 'share'; | ||||
| UPDATE attributes SET value = '_share' WHERE type = 'relation' AND value = 'share'; | ||||
| UPDATE entity_changes SET entityId = '_share' WHERE entityId = 'share'; | ||||
| @@ -1,12 +0,0 @@ | ||||
| module.exports = () => { | ||||
|     const hiddenSubtreeService = require('../../src/services/hidden_subtree'); | ||||
|     const cls = require("../../src/services/cls"); | ||||
|     const beccaLoader = require("../../src/becca/becca_loader"); | ||||
|  | ||||
|     cls.init(() => { | ||||
|         beccaLoader.load(); | ||||
|         // make sure the hidden subtree exists since the subsequent migrations we will move some existing notes into it (share...) | ||||
|         // in previous releases hidden subtree was created lazily | ||||
|         hiddenSubtreeService.checkHiddenSubtree(true); | ||||
|     }); | ||||
| }; | ||||
| @@ -1,2 +0,0 @@ | ||||
| DELETE FROM branches WHERE noteId = '_share' AND parentNoteId != 'root' AND parentNoteId != '_hidden'; -- delete all other branches of _share if any | ||||
| UPDATE branches SET parentNoteId = '_hidden' WHERE noteId = '_share'; | ||||
| @@ -1,2 +0,0 @@ | ||||
| DELETE FROM branches WHERE noteId = '_globalNoteMap' AND parentNoteId != 'singles' AND parentNoteId != '_hidden'; -- make sure there are no clones which would fail at the next line | ||||
| UPDATE branches SET parentNoteId = '_hidden' WHERE noteId = '_globalNoteMap'; | ||||
| @@ -1,6 +0,0 @@ | ||||
| DELETE FROM branches WHERE noteId = 'singles'; | ||||
| DELETE FROM notes WHERE noteId = 'singles'; | ||||
| DELETE FROM note_contents WHERE noteId = 'singles'; | ||||
| DELETE FROM note_revisions WHERE noteId = 'singles'; | ||||
| DELETE FROM attributes WHERE noteId = 'singles'; | ||||
| DELETE FROM entity_changes WHERE entityId = 'singles'; | ||||
| @@ -1,21 +0,0 @@ | ||||
| module.exports = () => { | ||||
|     const cls = require("../../src/services/cls"); | ||||
|     const cloningService = require("../../src/services/cloning"); | ||||
|     const beccaLoader = require("../../src/becca/becca_loader"); | ||||
|     const becca = require("../../src/becca/becca"); | ||||
|  | ||||
|     cls.init(() => { | ||||
|         beccaLoader.load(); | ||||
|  | ||||
|         for (const attr of becca.findAttributes('label','bookmarked')) { | ||||
|             cloningService.toggleNoteInParent(true, attr.noteId, '_lbBookmarks'); | ||||
|  | ||||
|             attr.markAsDeleted("0204__migrate_bookmarks_to_clones"); | ||||
|         } | ||||
|  | ||||
|         // bookmarkFolder used to work in 0.57 without the bookmarked label | ||||
|         for (const attr of becca.findAttributes('label','bookmarkFolder')) { | ||||
|             cloningService.toggleNoteInParent(true, attr.noteId, '_lbBookmarks'); | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -1,3 +0,0 @@ | ||||
| UPDATE notes SET type = 'relationMap' WHERE type = 'relation-map'; | ||||
| UPDATE notes SET type = 'noteMap' WHERE type = 'note-map'; | ||||
| UPDATE notes SET type = 'webView' WHERE type = 'web-view'; | ||||
| @@ -1,33 +0,0 @@ | ||||
| // the history was previously not exposed and the fact they were not cleaned up is rather a side-effect than an intention | ||||
|  | ||||
| module.exports = () => { | ||||
|     const cls = require("../../src/services/cls"); | ||||
|     const beccaLoader = require("../../src/becca/becca_loader"); | ||||
|     const becca = require("../../src/becca/becca"); | ||||
|  | ||||
|     cls.init(() => { | ||||
|         beccaLoader.load(); | ||||
|  | ||||
|         // deleting just branches because they might be cloned (and therefore saved) also outside of the hidden subtree | ||||
|  | ||||
|         const searchRoot = becca.getNote('_search'); | ||||
|  | ||||
|         for (const searchBranch of searchRoot.getChildBranches()) { | ||||
|             const searchNote = searchBranch.getNote(); | ||||
|  | ||||
|             if (searchNote.type === 'search') { | ||||
|                 searchBranch.deleteBranch('0206__delete_search_and_sql_console_history'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const sqlConsoleRoot = becca.getNote('_sqlConsole'); | ||||
|  | ||||
|         for (const sqlConsoleBranch of sqlConsoleRoot.getChildBranches()) { | ||||
|             const sqlConsoleNote = sqlConsoleBranch.getNote(); | ||||
|  | ||||
|             if (sqlConsoleNote.type === 'code' && sqlConsoleNote.mime === 'text/x-sqlite;schema=trilium') { | ||||
|                 sqlConsoleBranch.deleteBranch('0206__delete_search_and_sql_console_history'); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -1,2 +0,0 @@ | ||||
| UPDATE notes SET title = 'SQL Console History' WHERE noteId = '_sqlConsole'; | ||||
| UPDATE notes SET title = 'Search History' WHERE noteId = '_search'; | ||||
| @@ -1,13 +0,0 @@ | ||||
| module.exports = () => { | ||||
|     const cls = require("../../src/services/cls"); | ||||
|     const beccaLoader = require("../../src/becca/becca_loader"); | ||||
|     const becca = require("../../src/becca/becca"); | ||||
|  | ||||
|     cls.init(() => { | ||||
|         beccaLoader.load(); | ||||
|  | ||||
|         for (const label of becca.getNote('_hidden').getLabels('archived')) { | ||||
|             label.markAsDeleted('0208__remove_archived_from_hidden'); | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -1,5 +0,0 @@ | ||||
| UPDATE attributes SET name = 'workspaceInbox' WHERE type = 'label' AND name = 'hoistedInbox'; | ||||
| UPDATE entity_changes SET entityId = 'workspaceInbox' WHERE entityName = 'attributes' AND entityId = 'hoistedInbox'; | ||||
|  | ||||
| UPDATE attributes SET name = 'workspaceSearchHome' WHERE type = 'label' AND name = 'hoistedSearchHome'; | ||||
| UPDATE entity_changes SET entityId = 'workspaceSearchHome' WHERE entityName = 'attributes' AND entityId = 'hoistedSearchHome'; | ||||
| @@ -1,24 +0,0 @@ | ||||
| module.exports = async () => { | ||||
|     const cls = require("../../src/services/cls"); | ||||
|     const beccaLoader = require("../../src/becca/becca_loader"); | ||||
|     const log = require("../../src/services/log"); | ||||
|     const consistencyChecks = require("../../src/services/consistency_checks"); | ||||
|     const eraseService = require("../../src/services/erase"); | ||||
|  | ||||
|     await cls.init(async () => { | ||||
|         // precaution for the 0211 migration | ||||
|         eraseService.eraseDeletedNotesNow(); | ||||
|  | ||||
|         beccaLoader.load(); | ||||
|  | ||||
|         try { | ||||
|             // precaution before running 211 which might produce unique constraint problems if the DB was not consistent | ||||
|             consistencyChecks.runOnDemandChecksWithoutExclusiveLock(true); | ||||
|         } | ||||
|         catch (e) { | ||||
|             // consistency checks might start failing in the future if there's some incompatible migration down the road | ||||
|             // we can optimistically assume the DB is consistent and still continue | ||||
|             log.error(`Consistency checks failed in migration 0210: ${e.message} ${e.stack}`); | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -1,12 +0,0 @@ | ||||
| -- case based on isDeleted is needed, otherwise 2 branches (1 deleted, 1 not) might get the same ID | ||||
| UPDATE entity_changes SET entityId = COALESCE(( | ||||
|     SELECT | ||||
|         CASE isDeleted | ||||
|             WHEN 0 THEN parentNoteId || '_' || noteId | ||||
|             WHEN 1 THEN branchId | ||||
|         END | ||||
|     FROM branches WHERE branchId = entityId | ||||
| ), entityId) | ||||
| WHERE entityName = 'branches' AND isErased = 0; | ||||
|  | ||||
| UPDATE branches SET branchId = parentNoteId || '_' || noteId WHERE isDeleted = 0; | ||||
| @@ -1,27 +0,0 @@ | ||||
| module.exports = () => { | ||||
|     const cls = require("../../src/services/cls"); | ||||
|     const beccaLoader = require("../../src/becca/becca_loader"); | ||||
|     const becca = require("../../src/becca/becca"); | ||||
|     const log = require("../../src/services/log"); | ||||
|  | ||||
|     cls.init(() => { | ||||
|         beccaLoader.load(); | ||||
|  | ||||
|         const hidden = becca.getNote("_hidden"); | ||||
|  | ||||
|         if (!hidden) { | ||||
|             log.info("MIGRATION 212: no _hidden note, skipping."); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         for (const noteId of hidden.getSubtreeNoteIds({includeHidden: true})) { | ||||
|             if (noteId.startsWith("_")) { // is "named" note | ||||
|                 const note = becca.getNote(noteId); | ||||
|  | ||||
|                 for (const attr of note.getOwnedAttributes().slice()) { | ||||
|                     attr.markAsDeleted("0212__delete_all_attributes_of_named_notes"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -1,48 +0,0 @@ | ||||
| module.exports = () => { | ||||
|     const beccaLoader = require("../../src/becca/becca_loader"); | ||||
|     const becca = require("../../src/becca/becca"); | ||||
|     const cls = require("../../src/services/cls"); | ||||
|     const log = require("../../src/services/log"); | ||||
|  | ||||
|     cls.init(() => { | ||||
|         beccaLoader.load(); | ||||
|  | ||||
|         for (const note of Object.values(becca.notes)) { | ||||
|             try { | ||||
|                 if (!note.isJavaScript()) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 if (!note.mime?.endsWith('env=frontend') && !note.mime?.endsWith('env=backend')) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 const origContent = note.getContent().toString(); | ||||
|                 const fixedContent = origContent | ||||
|                     .replaceAll("runOnServer", "runOnBackend") | ||||
|                     .replaceAll("api.refreshTree()", "") | ||||
|                     .replaceAll("addTextToActiveTabEditor", "addTextToActiveContextEditor") | ||||
|                     .replaceAll("getActiveTabNote", "getActiveContextNote") | ||||
|                     .replaceAll("getActiveTabTextEditor", "getActiveContextTextEditor") | ||||
|                     .replaceAll("getActiveTabNotePath", "getActiveContextNotePath") | ||||
|                     .replaceAll("getDateNote", "getDayNote") | ||||
|                     .replaceAll("utils.unescapeHtml", "unescapeHtml") | ||||
|                     .replaceAll("sortNotesByTitle", "sortNotes") | ||||
|                     .replaceAll("CollapsibleWidget", "RightPanelWidget") | ||||
|                     .replaceAll("TabAwareWidget", "NoteContextAwareWidget") | ||||
|                     .replaceAll("TabCachingWidget", "NoteContextAwareWidget") | ||||
|                     .replaceAll("NoteContextCachingWidget", "NoteContextAwareWidget"); | ||||
|  | ||||
|                 if (origContent !== fixedContent) { | ||||
|                     log.info(`Replacing legacy API calls for note '${note.noteId}'`); | ||||
|  | ||||
|                     note.saveNoteRevision(); | ||||
|                     note.setContent(fixedContent); | ||||
|                 } | ||||
|             } | ||||
|             catch (e) { | ||||
|                 log.error(`Error during migration to 213 for note '${note.noteId}': ${e.message} ${e.stack}`); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -1 +0,0 @@ | ||||
| UPDATE branches SET notePosition = notePosition - 999899999 WHERE parentNoteId = 'root' AND notePosition > 999999999; | ||||
| @@ -586,6 +586,48 @@ function BackendScriptApi(currentNote, apiParams) { | ||||
|      */ | ||||
|     this.exportSubtreeToZipFile = async (noteId, format, zipFilePath) => await exportService.exportToZipFile(noteId, format, zipFilePath); | ||||
|  | ||||
|     /** | ||||
|      * Executes given anonymous function on the frontend(s). | ||||
|      * Internally this serializes the anonymous function into string and sends it to frontend(s) via WebSocket. | ||||
|      * Note that there can be multiple connected frontend instances (e.g. in different tabs). In such case, all | ||||
|      * instances execute the given function. | ||||
|      * | ||||
|      * @method | ||||
|      * @param {string} script - script to be executed on the frontend | ||||
|      * @param {Array.<?>} params - list of parameters to the anonymous function to be sent to frontend | ||||
|      * @returns {undefined} - no return value is provided. | ||||
|      */ | ||||
|     this.runOnFrontend = async (script, params = []) => { | ||||
|         if (typeof script === "function") { | ||||
|             script = script.toString(); | ||||
|         } | ||||
|  | ||||
|         ws.sendMessageToAllClients({ | ||||
|             type: 'execute-script', | ||||
|             script: script, | ||||
|             params: prepareParams(params), | ||||
|             startNoteId: this.startNote.noteId, | ||||
|             currentNoteId: this.currentNote.noteId, | ||||
|             originEntityName: "notes", // currently there's no other entity on the frontend which can trigger event | ||||
|             originEntityId: this.originEntity?.noteId || null | ||||
|         }); | ||||
|  | ||||
|         function prepareParams(params) { | ||||
|             if (!params) { | ||||
|                 return params; | ||||
|             } | ||||
|  | ||||
|             return params.map(p => { | ||||
|                 if (typeof p === "function") { | ||||
|                     return `!@#Function: ${p.toString()}`; | ||||
|                 } | ||||
|                 else { | ||||
|                     return p; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * This object contains "at your risk" and "no BC guarantees" objects for advanced use cases. | ||||
|      * | ||||
|   | ||||
| @@ -167,10 +167,9 @@ class FNote { | ||||
|     } | ||||
|  | ||||
|     async getContent() { | ||||
|         // we're not caching content since these objects are in froca and as such pretty long-lived | ||||
|         const note = await server.get(`notes/${this.noteId}`); | ||||
|         const blob = await this.getBlob(); | ||||
|  | ||||
|         return note.content; | ||||
|         return blob?.content; | ||||
|     } | ||||
|  | ||||
|     async getJsonContent() { | ||||
|   | ||||
| @@ -5,8 +5,8 @@ | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * CKEditor 5 (v38.1.1) content styles. | ||||
|  * Generated on Thu, 27 Jul 2023 08:16:09 GMT. | ||||
|  * CKEditor 5 (v39.0.1) content styles. | ||||
|  * Generated on Thu, 10 Aug 2023 09:21:07 GMT. | ||||
|  * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html | ||||
|  */ | ||||
|  | ||||
| @@ -15,8 +15,8 @@ | ||||
|     --ck-color-image-caption-text: hsl(0, 0%, 20%); | ||||
|     --ck-color-mention-background: hsla(341, 100%, 30%, 0.1); | ||||
|     --ck-color-mention-text: hsl(341, 100%, 30%); | ||||
|     --ck-color-table-caption-background: hsl(0, 0%, 97%); | ||||
|     --ck-color-table-caption-text: hsl(0, 0%, 20%); | ||||
|     --ck-color-selector-caption-background: hsl(0, 0%, 97%); | ||||
|     --ck-color-selector-caption-text: hsl(0, 0%, 20%); | ||||
|     --ck-highlight-marker-blue: hsl(201, 97%, 72%); | ||||
|     --ck-highlight-marker-green: hsl(120, 93%, 68%); | ||||
|     --ck-highlight-marker-pink: hsl(345, 96%, 73%); | ||||
| @@ -42,18 +42,6 @@ | ||||
|     overflow-wrap: break-word; | ||||
|     position: relative; | ||||
| } | ||||
| /* @ckeditor/ckeditor5-table/theme/tablecaption.css */ | ||||
| .ck-content .table > figcaption { | ||||
|     display: table-caption; | ||||
|     caption-side: top; | ||||
|     word-break: break-word; | ||||
|     text-align: center; | ||||
|     color: var(--ck-color-table-caption-text); | ||||
|     background-color: var(--ck-color-table-caption-background); | ||||
|     padding: .6em; | ||||
|     font-size: .75em; | ||||
|     outline-offset: -1px; | ||||
| } | ||||
| /* @ckeditor/ckeditor5-table/theme/table.css */ | ||||
| .ck-content .table { | ||||
|     margin: 0.9em auto; | ||||
| @@ -87,6 +75,18 @@ | ||||
| .ck-content[dir="ltr"] .table th { | ||||
|     text-align: left; | ||||
| } | ||||
| /* @ckeditor/ckeditor5-table/theme/tablecaption.css */ | ||||
| .ck-content .table > figcaption { | ||||
|     display: table-caption; | ||||
|     caption-side: top; | ||||
|     word-break: break-word; | ||||
|     text-align: center; | ||||
|     color: var(--ck-color-selector-caption-text); | ||||
|     background-color: var(--ck-color-selector-caption-background); | ||||
|     padding: .6em; | ||||
|     font-size: .75em; | ||||
|     outline-offset: -1px; | ||||
| } | ||||
| /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ | ||||
| .ck-content .page-break { | ||||
|     position: relative; | ||||
| @@ -382,12 +382,6 @@ | ||||
| .ck-content .image-inline.image-style-align-right { | ||||
|     margin-left: var(--ck-inline-image-style-spacing); | ||||
| } | ||||
| /* @ckeditor/ckeditor5-basic-styles/theme/code.css */ | ||||
| .ck-content code { | ||||
|     background-color: hsla(0, 0%, 78%, 0.3); | ||||
|     padding: .15em; | ||||
|     border-radius: 2px; | ||||
| } | ||||
| /* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ | ||||
| .ck-content blockquote { | ||||
|     overflow: hidden; | ||||
| @@ -403,6 +397,12 @@ | ||||
|     border-left: 0; | ||||
|     border-right: solid 5px hsl(0, 0%, 80%); | ||||
| } | ||||
| /* @ckeditor/ckeditor5-basic-styles/theme/code.css */ | ||||
| .ck-content code { | ||||
|     background-color: hsla(0, 0%, 78%, 0.3); | ||||
|     padding: .15em; | ||||
|     border-radius: 2px; | ||||
| } | ||||
| /* @ckeditor/ckeditor5-font/theme/fontsize.css */ | ||||
| .ck-content .text-tiny { | ||||
|     font-size: .7em; | ||||
|   | ||||
							
								
								
									
										2
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										379
									
								
								libraries/mermaid.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										379
									
								
								libraries/mermaid.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										32
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								package.json
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ | ||||
|   "name": "trilium", | ||||
|   "productName": "Trilium Notes", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.61.4-beta", | ||||
|   "version": "0.61.5-beta", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "bin": { | ||||
| @@ -32,11 +32,11 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@braintree/sanitize-url": "6.0.4", | ||||
|     "@electron/remote": "2.0.10", | ||||
|     "@excalidraw/excalidraw": "0.15.2", | ||||
|     "@electron/remote": "2.0.11", | ||||
|     "@excalidraw/excalidraw": "0.15.3", | ||||
|     "archiver": "5.3.1", | ||||
|     "async-mutex": "0.4.0", | ||||
|     "axios": "1.4.0", | ||||
|     "axios": "1.5.0", | ||||
|     "better-sqlite3": "8.4.0", | ||||
|     "chokidar": "3.5.3", | ||||
|     "cls-hooked": "4.2.2", | ||||
| @@ -53,14 +53,14 @@ | ||||
|     "escape-html": "1.0.3", | ||||
|     "express": "4.18.2", | ||||
|     "express-partial-content": "1.0.2", | ||||
|     "express-rate-limit": "6.9.0", | ||||
|     "express-rate-limit": "6.10.0", | ||||
|     "express-session": "1.17.3", | ||||
|     "fs-extra": "11.1.1", | ||||
|     "helmet": "7.0.0", | ||||
|     "html": "1.0.0", | ||||
|     "html2plaintext": "2.1.4", | ||||
|     "http-proxy-agent": "7.0.0", | ||||
|     "https-proxy-agent": "7.0.1", | ||||
|     "https-proxy-agent": "7.0.2", | ||||
|     "image-type": "4.1.0", | ||||
|     "ini": "3.0.1", | ||||
|     "is-animated": "2.0.2", | ||||
| @@ -68,10 +68,10 @@ | ||||
|     "jimp": "0.22.10", | ||||
|     "joplin-turndown-plugin-gfm": "1.0.12", | ||||
|     "jsdom": "22.1.0", | ||||
|     "marked": "7.0.2", | ||||
|     "marked": "7.0.5", | ||||
|     "mime-types": "2.1.35", | ||||
|     "multer": "1.4.5-lts.1", | ||||
|     "node-abi": "3.45.0", | ||||
|     "node-abi": "3.47.0", | ||||
|     "normalize-strings": "1.1.1", | ||||
|     "open": "8.4.1", | ||||
|     "rand-token": "1.0.1", | ||||
| @@ -97,14 +97,14 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "cross-env": "7.0.3", | ||||
|     "electron": "25.5.0", | ||||
|     "electron-builder": "24.6.3", | ||||
|     "electron-packager": "17.1.1", | ||||
|     "electron": "25.8.0", | ||||
|     "electron-builder": "24.6.4", | ||||
|     "electron-packager": "17.1.2", | ||||
|     "electron-rebuild": "3.2.9", | ||||
|     "eslint": "8.46.0", | ||||
|     "eslint": "8.48.0", | ||||
|     "eslint-config-airbnb-base": "15.0.0", | ||||
|     "eslint-config-prettier": "9.0.0", | ||||
|     "eslint-plugin-import": "2.28.0", | ||||
|     "eslint-plugin-import": "2.28.1", | ||||
|     "eslint-plugin-jsonc": "2.9.0", | ||||
|     "eslint-plugin-prettier": "5.0.0", | ||||
|     "esm": "3.2.25", | ||||
| @@ -112,16 +112,16 @@ | ||||
|     "jasmine": "5.1.0", | ||||
|     "jsdoc": "4.0.2", | ||||
|     "jsonc-eslint-parser": "2.3.0", | ||||
|     "lint-staged": "13.2.3", | ||||
|     "lint-staged": "14.0.1", | ||||
|     "lorem-ipsum": "2.0.8", | ||||
|     "nodemon": "3.0.1", | ||||
|     "prettier": "3.0.1", | ||||
|     "prettier": "3.0.3", | ||||
|     "rcedit": "3.1.0", | ||||
|     "webpack": "5.88.2", | ||||
|     "webpack-cli": "5.1.4" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|     "electron-installer-debian": "3.1.0" | ||||
|     "electron-installer-debian": "3.2.0" | ||||
|   }, | ||||
|   "lint-staged": { | ||||
|     "*.js": "eslint --cache --fix" | ||||
|   | ||||
| @@ -4,6 +4,9 @@ describe("Lexer fulltext", () => { | ||||
|     it("simple lexing", () => { | ||||
|         expect(lex("hello world").fulltextTokens.map(t => t.token)) | ||||
|             .toEqual(["hello", "world"]); | ||||
|  | ||||
|         expect(lex("hello, world").fulltextTokens.map(t => t.token)) | ||||
|             .toEqual(["hello", "world"]); | ||||
|     }); | ||||
|  | ||||
|     it("use quotes to keep words together", () => { | ||||
| @@ -147,6 +150,11 @@ describe("Lexer expression", () => { | ||||
|         expect(lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map(t => t.token)) | ||||
|             .toEqual(["#", "not", "(", "#capital", ")", "and", "note", ".", "noteid", "!=", "root"]); | ||||
|     }); | ||||
|  | ||||
|     it("order by multiple labels", () => { | ||||
|         expect(lex(`# orderby #a,#b`).expressionTokens.map(t => t.token)) | ||||
|             .toEqual(["#", "orderby", "#a", ",", "#b"]); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| describe("Lexer invalid queries and edge cases", () => { | ||||
|   | ||||
| @@ -36,9 +36,9 @@ describe("Parser", () => { | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpressions[0].tokens).toEqual(["hello", "hi"]); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(["hello", "hi"]); | ||||
|     }); | ||||
|  | ||||
|     it("fulltext parser with content", () => { | ||||
| @@ -51,9 +51,9 @@ describe("Parser", () => { | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); | ||||
|  | ||||
|         const subs = rootExp.subExpressions[1].subExpressions; | ||||
|         const subs = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(subs[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(subs[0].tokens).toEqual(["hello", "hi"]); | ||||
| @@ -71,10 +71,10 @@ describe("Parser", () => { | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.subExpressions[1].attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[1].attributeName).toEqual("mylabel"); | ||||
|         expect(rootExp.subExpressions[1].comparator).toBeTruthy(); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.subExpressions[2].attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[2].attributeName).toEqual("mylabel"); | ||||
|         expect(rootExp.subExpressions[2].comparator).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("simple attribute negation", () => { | ||||
| @@ -86,10 +86,10 @@ describe("Parser", () => { | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.attributeName).toEqual("mylabel"); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("mylabel"); | ||||
|  | ||||
|         rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
| @@ -99,10 +99,10 @@ describe("Parser", () => { | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.attributeType).toEqual("relation"); | ||||
|         expect(rootExp.subExpressions[1].subExpression.attributeName).toEqual("myrelation"); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("relation"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("myrelation"); | ||||
|     }); | ||||
|  | ||||
|     it("simple label AND", () => { | ||||
| @@ -115,8 +115,8 @@ describe("Parser", () => { | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -135,8 +135,8 @@ describe("Parser", () => { | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -155,8 +155,8 @@ describe("Parser", () => { | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -173,17 +173,17 @@ describe("Parser", () => { | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub, thirdSub] = rootExp.subExpressions; | ||||
|         const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); | ||||
|         expect(firstSub.propertyName).toEqual('isArchived'); | ||||
|  | ||||
|         expect(secondSub.constructor.name).toEqual("OrExp"); | ||||
|         expect(secondSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(secondSub.subExpressions[0].tokens).toEqual(["hello"]); | ||||
|         expect(thirdSub.constructor.name).toEqual("OrExp"); | ||||
|         expect(thirdSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(thirdSub.subExpressions[0].tokens).toEqual(["hello"]); | ||||
|  | ||||
|         expect(thirdSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(thirdSub.attributeName).toEqual("mylabel"); | ||||
|         expect(fourth.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(fourth.attributeName).toEqual("mylabel"); | ||||
|     }); | ||||
|  | ||||
|     it("label sub-expression", () => { | ||||
| @@ -196,8 +196,8 @@ describe("Parser", () => { | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[1].subExpressions; | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -222,8 +222,8 @@ describe("Parser", () => { | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[1].subExpressions; | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
| @@ -290,10 +290,11 @@ describe("Invalid expressions", () => { | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[1].constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.subExpressions[1].attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[1].attributeName).toEqual("first"); | ||||
|         expect(rootExp.subExpressions[1].comparator).toBeTruthy(); | ||||
|  | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.subExpressions[2].attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[2].attributeName).toEqual("first"); | ||||
|         expect(rootExp.subExpressions[2].comparator).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("searching by relation without note property", () => { | ||||
|   | ||||
| @@ -802,6 +802,12 @@ components: | ||||
|         branchId: | ||||
|           $ref: '#/components/schemas/EntityId' | ||||
|           description: DON'T specify unless you want to force a specific branchId | ||||
|         dateCreated: | ||||
|           $ref: '#/components/schemas/LocalDateTime' | ||||
|           description: Local timestap of the note creation. Specify only if you want to override the default (current datetime in the current timezone/offset). | ||||
|         utcDateCreated: | ||||
|           $ref: '#/components/schemas/UtcDateTime' | ||||
|           description: UTC timestap of the note creation. Specify only if you want to override the default (current datetime). | ||||
|     Note: | ||||
|       type: object | ||||
|       properties: | ||||
| @@ -838,13 +844,11 @@ components: | ||||
|           readOnly: true | ||||
|         dateCreated: | ||||
|           $ref: '#/components/schemas/LocalDateTime' | ||||
|           readOnly: true | ||||
|         dateModified: | ||||
|           $ref: '#/components/schemas/LocalDateTime' | ||||
|           readOnly: true | ||||
|         utcDateCreated: | ||||
|           $ref: '#/components/schemas/UtcDateTime' | ||||
|           readOnly: true | ||||
|         utcDateModified: | ||||
|           $ref: '#/components/schemas/UtcDateTime' | ||||
|           readOnly: true | ||||
| @@ -937,11 +941,11 @@ components: | ||||
|     LocalDateTime: | ||||
|       type: string | ||||
|       pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}[\+\-][0-9]{4}' | ||||
|       example: 2021-12-31 20:18:11.939+0100 | ||||
|       example: 2021-12-31 20:18:11.930+0100 | ||||
|     UtcDateTime: | ||||
|       type: string | ||||
|       pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z' | ||||
|       example: 2021-12-31 19:18:11.939Z | ||||
|       example: 2021-12-31 19:18:11.930Z | ||||
|     AppInfo: | ||||
|       type: object | ||||
|       properties: | ||||
|   | ||||
| @@ -50,7 +50,9 @@ function register(router) { | ||||
|         'notePosition': [v.notNull, v.isInteger], | ||||
|         'prefix': [v.notNull, v.isString], | ||||
|         'isExpanded': [v.notNull, v.isBoolean], | ||||
|         'noteId': [v.notNull, v.isValidEntityId] | ||||
|         'noteId': [v.notNull, v.isValidEntityId], | ||||
|         'dateCreated': [v.notNull, v.isString, v.isLocalDateTime], | ||||
|         'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime] | ||||
|     }; | ||||
|  | ||||
|     eu.route(router, 'post' ,'/etapi/create-note', (req, res, next) => { | ||||
| @@ -74,7 +76,9 @@ function register(router) { | ||||
|     const ALLOWED_PROPERTIES_FOR_PATCH = { | ||||
|         'title': [v.notNull, v.isString], | ||||
|         'type': [v.notNull, v.isString], | ||||
|         'mime': [v.notNull, v.isString] | ||||
|         'mime': [v.notNull, v.isString], | ||||
|         'dateCreated': [v.notNull, v.isString, v.isLocalDateTime], | ||||
|         'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime] | ||||
|     }; | ||||
|  | ||||
|     eu.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| const noteTypeService = require("../services/note_types"); | ||||
| const dateUtils = require("../services/date_utils"); | ||||
|  | ||||
| function mandatory(obj) { | ||||
|     if (obj === undefined ) { | ||||
| @@ -22,6 +23,22 @@ function isString(obj) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function isLocalDateTime(obj) { | ||||
|     if (obj === undefined || obj === null) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     return dateUtils.validateLocalDateTime(obj); | ||||
| } | ||||
|  | ||||
| function isUtcDateTime(obj) { | ||||
|     if (obj === undefined || obj === null) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     return dateUtils.validateUtcDateTime(obj); | ||||
| } | ||||
|  | ||||
| function isBoolean(obj) { | ||||
|     if (obj === undefined || obj === null) { | ||||
|         return; | ||||
| @@ -99,5 +116,7 @@ module.exports = { | ||||
|     isNoteId, | ||||
|     isNoteType, | ||||
|     isAttributeType, | ||||
|     isValidEntityId | ||||
|     isValidEntityId, | ||||
|     isLocalDateTime, | ||||
|     isUtcDateTime | ||||
| }; | ||||
|   | ||||
| @@ -139,10 +139,9 @@ class FNote { | ||||
|     } | ||||
|  | ||||
|     async getContent() { | ||||
|         // we're not caching content since these objects are in froca and as such pretty long-lived | ||||
|         const note = await server.get(`notes/${this.noteId}`); | ||||
|         const blob = await this.getBlob(); | ||||
|  | ||||
|         return note.content; | ||||
|         return blob?.content; | ||||
|     } | ||||
|  | ||||
|     async getJsonContent() { | ||||
|   | ||||
| @@ -4,8 +4,11 @@ import toastService from "./toast.js"; | ||||
| import froca from "./froca.js"; | ||||
| import utils from "./utils.js"; | ||||
|  | ||||
| async function getAndExecuteBundle(noteId, originEntity = null) { | ||||
|     const bundle = await server.get(`script/bundle/${noteId}`); | ||||
| async function getAndExecuteBundle(noteId, originEntity = null, script = null, params = null) { | ||||
|     const bundle = await server.post(`script/bundle/${noteId}`, { | ||||
|         script, | ||||
|         params | ||||
|     }); | ||||
|  | ||||
|     return await executeBundle(bundle, originEntity); | ||||
| } | ||||
|   | ||||
| @@ -331,6 +331,8 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
|      * @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link | ||||
|      * @param {boolean} [params.showNoteIcon=false] - show also note icon before the title | ||||
|      * @param {string} [params.title] - custom link tile with note's title as default | ||||
|      * @param {string} [params.title=] - custom link tile with note's title as default | ||||
|      * @returns {jQuery} - jQuery element with the link (wrapped in <span>) | ||||
|      */ | ||||
|     this.createLink = linkService.createLink; | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ async function render(note, $el) { | ||||
|     $el.empty().toggle(renderNoteIds.length > 0); | ||||
|  | ||||
|     for (const renderNoteId of renderNoteIds) { | ||||
|         const bundle = await server.get(`script/bundle/${renderNoteId}`); | ||||
|         const bundle = await server.post(`script/bundle/${renderNoteId}`); | ||||
|  | ||||
|         const $scriptContainer = $('<div>'); | ||||
|         $el.append($scriptContainer); | ||||
|   | ||||
| @@ -125,6 +125,13 @@ async function handleMessage(event) { | ||||
|     else if (message.type === 'toast') { | ||||
|         toastService.showMessage(message.message); | ||||
|     } | ||||
|     else if (message.type === 'execute-script') { | ||||
|         const bundleService = (await import("../services/bundle.js")).default; | ||||
|         const froca = (await import("../services/froca.js")).default; | ||||
|         const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null; | ||||
|  | ||||
|         bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params); | ||||
|     } | ||||
| } | ||||
|  | ||||
| let entityChangeIdReachedListeners = []; | ||||
|   | ||||
| @@ -115,7 +115,7 @@ export default class FindInCode { | ||||
|  | ||||
|         return { | ||||
|             totalFound, | ||||
|             currentFound: currentFound + 1 | ||||
|             currentFound: Math.min(currentFound + 1, totalFound) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -39,7 +39,7 @@ export default class FindInHtml { | ||||
|  | ||||
|                             res({ | ||||
|                                 totalFound: this.$results.length, | ||||
|                                 currentFound: 1 | ||||
|                                 currentFound: Math.min(1, this.$results.length) | ||||
|                             }); | ||||
|                         } | ||||
|                     }); | ||||
|   | ||||
| @@ -59,7 +59,7 @@ export default class FindInText { | ||||
|  | ||||
|         return { | ||||
|             totalFound, | ||||
|             currentFound: currentFound + 1 | ||||
|             currentFound: Math.min(currentFound + 1, totalFound) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -37,7 +37,9 @@ export default class NoteListWidget extends NoteContextAwareWidget { | ||||
|             threshold: 0.1 | ||||
|         }); | ||||
|  | ||||
|         observer.observe(this.$widget[0]); | ||||
|         // there seems to be a race condition on Firefox which triggers the observer only before the widget is visible | ||||
|         // (intersection is false). https://github.com/zadam/trilium/issues/4165 | ||||
|         setTimeout(() => observer.observe(this.$widget[0]), 10); | ||||
|     } | ||||
|  | ||||
|     checkRenderStatus() { | ||||
|   | ||||
| @@ -94,13 +94,12 @@ function createNote(req) { | ||||
|     clipType = htmlSanitizer.sanitize(clipType); | ||||
|  | ||||
|     const clipperInbox = getClipperInboxNote(); | ||||
|     const dailyNote = dateNoteService.getDayNote(dateUtils.localNowDate()); | ||||
|     pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); | ||||
|     let note = findClippingNote(clipperInbox, pageUrl, clipType); | ||||
|  | ||||
|     if (!note) { | ||||
|         note = noteService.createNewNote({ | ||||
|             parentNoteId: dailyNote.noteId, | ||||
|             parentNoteId: clipperInbox.noteId, | ||||
|             title, | ||||
|             content: '', | ||||
|             type: 'text' | ||||
|   | ||||
| @@ -107,8 +107,9 @@ function getRelationBundles(req) { | ||||
|  | ||||
| function getBundle(req) { | ||||
|     const note = becca.getNote(req.params.noteId); | ||||
|     const {script, params} = req.body; | ||||
|  | ||||
|     return scriptService.getScriptBundleForFrontend(note); | ||||
|     return scriptService.getScriptBundleForFrontend(note, script, params); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -302,7 +302,7 @@ function register(app) { | ||||
|     apiRoute(PST, '/api/script/run/:noteId', scriptRoute.run); | ||||
|     apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles); | ||||
|     apiRoute(GET, '/api/script/widgets', scriptRoute.getWidgetBundles); | ||||
|     apiRoute(GET, '/api/script/bundle/:noteId', scriptRoute.getBundle); | ||||
|     apiRoute(PST, '/api/script/bundle/:noteId', scriptRoute.getBundle); | ||||
|     apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles); | ||||
|  | ||||
|     // no CSRF since this is called from android app | ||||
|   | ||||
| @@ -558,6 +558,48 @@ function BackendScriptApi(currentNote, apiParams) { | ||||
|      */ | ||||
|     this.exportSubtreeToZipFile = async (noteId, format, zipFilePath) => await exportService.exportToZipFile(noteId, format, zipFilePath); | ||||
|  | ||||
|     /** | ||||
|      * Executes given anonymous function on the frontend(s). | ||||
|      * Internally this serializes the anonymous function into string and sends it to frontend(s) via WebSocket. | ||||
|      * Note that there can be multiple connected frontend instances (e.g. in different tabs). In such case, all | ||||
|      * instances execute the given function. | ||||
|      * | ||||
|      * @method | ||||
|      * @param {string} script - script to be executed on the frontend | ||||
|      * @param {Array.<?>} params - list of parameters to the anonymous function to be sent to frontend | ||||
|      * @returns {undefined} - no return value is provided. | ||||
|      */ | ||||
|     this.runOnFrontend = async (script, params = []) => { | ||||
|         if (typeof script === "function") { | ||||
|             script = script.toString(); | ||||
|         } | ||||
|  | ||||
|         ws.sendMessageToAllClients({ | ||||
|             type: 'execute-script', | ||||
|             script: script, | ||||
|             params: prepareParams(params), | ||||
|             startNoteId: this.startNote.noteId, | ||||
|             currentNoteId: this.currentNote.noteId, | ||||
|             originEntityName: "notes", // currently there's no other entity on the frontend which can trigger event | ||||
|             originEntityId: this.originEntity?.noteId || null | ||||
|         }); | ||||
|  | ||||
|         function prepareParams(params) { | ||||
|             if (!params) { | ||||
|                 return params; | ||||
|             } | ||||
|  | ||||
|             return params.map(p => { | ||||
|                 if (typeof p === "function") { | ||||
|                     return `!@#Function: ${p.toString()}`; | ||||
|                 } | ||||
|                 else { | ||||
|                     return p; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * This object contains "at your risk" and "no BC guarantees" objects for advanced use cases. | ||||
|      * | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| module.exports = { buildDate:"2023-08-10T23:49:37+02:00", buildRevision: "e741c2826c3b2ca5f3d6c7505f45a684e5231dba" }; | ||||
| module.exports = { buildDate:"2023-08-16T23:02:15+02:00", buildRevision: "3f7a5504c77263a7118cede5c0d9b450ba37f424" }; | ||||
|   | ||||
| @@ -758,7 +758,7 @@ class ConsistencyChecks { | ||||
|             return `${tableName}: ${count}`; | ||||
|         } | ||||
|  | ||||
|         const tables = [ "notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens" ]; | ||||
|         const tables = [ "notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs" ]; | ||||
|  | ||||
|         log.info(`Table counts: ${tables.map(tableName => getTableRowCount(tableName)).join(", ")}`); | ||||
|     } | ||||
| @@ -767,7 +767,13 @@ class ConsistencyChecks { | ||||
|         let elapsedTimeMs; | ||||
|  | ||||
|         await syncMutexService.doExclusively(() => { | ||||
|             elapsedTimeMs = this.runChecksInner(); | ||||
|             const startTimeMs = Date.now(); | ||||
|  | ||||
|             this.runDbDiagnostics(); | ||||
|  | ||||
|             this.runAllChecksAndFixers(); | ||||
|  | ||||
|             elapsedTimeMs = Date.now() - startTimeMs; | ||||
|         }); | ||||
|  | ||||
|         if (this.unrecoveredConsistencyErrors) { | ||||
| @@ -781,16 +787,6 @@ class ConsistencyChecks { | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     runChecksInner() { | ||||
|         const startTimeMs = Date.now(); | ||||
|  | ||||
|         this.runDbDiagnostics(); | ||||
|  | ||||
|         this.runAllChecksAndFixers(); | ||||
|  | ||||
|         return Date.now() - startTimeMs; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function getBlankContent(isProtected, type, mime) { | ||||
| @@ -825,11 +821,6 @@ async function runOnDemandChecks(autoFix) { | ||||
|     await consistencyChecks.runChecks(); | ||||
| } | ||||
|  | ||||
| function runOnDemandChecksWithoutExclusiveLock(autoFix) { | ||||
|     const consistencyChecks = new ConsistencyChecks(autoFix); | ||||
|     consistencyChecks.runChecksInner(); | ||||
| } | ||||
|  | ||||
| function runEntityChangesChecks() { | ||||
|     const consistencyChecks = new ConsistencyChecks(true); | ||||
|     consistencyChecks.findEntityChangeIssues(); | ||||
| @@ -844,6 +835,5 @@ sqlInit.dbReady.then(() => { | ||||
|  | ||||
| module.exports = { | ||||
|     runOnDemandChecks, | ||||
|     runOnDemandChecksWithoutExclusiveLock, | ||||
|     runEntityChangesChecks | ||||
| }; | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| const dayjs = require('dayjs'); | ||||
| const cls = require('./cls'); | ||||
|  | ||||
| const LOCAL_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSSZZ'; | ||||
| const UTC_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ssZ'; | ||||
|  | ||||
| function utcNowDateTime() { | ||||
|     return utcDateTimeStr(new Date()); | ||||
| } | ||||
| @@ -10,7 +13,7 @@ function utcNowDateTime() { | ||||
| // "trilium-local-now-datetime" header which is then stored in CLS | ||||
| function localNowDateTime() { | ||||
|     return cls.getLocalNowDateTime() | ||||
|         || dayjs().format('YYYY-MM-DD HH:mm:ss.SSSZZ') | ||||
|         || dayjs().format(LOCAL_DATETIME_FORMAT) | ||||
| } | ||||
|  | ||||
| function localNowDate() { | ||||
| @@ -62,6 +65,36 @@ function getDateTimeForFile() { | ||||
|     return new Date().toISOString().substr(0, 19).replace(/:/g, ''); | ||||
| } | ||||
|  | ||||
| function validateLocalDateTime(str) { | ||||
|     if (!str) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}[+-][0-9]{4}/.test(str)) { | ||||
|         return `Invalid local date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110+0200'`; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     if (!dayjs(str, LOCAL_DATETIME_FORMAT)) { | ||||
|         return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function validateUtcDateTime(str) { | ||||
|     if (!str) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (!/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z/.test(str)) { | ||||
|         return `Invalid UTC date time format in '${str}'. Correct format shoud follow example: '2023-08-21 23:38:51.110Z'`; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     if (!dayjs(str, UTC_DATETIME_FORMAT)) { | ||||
|         return `Date '${str}' appears to be in the correct format, but cannot be parsed. It likely represents an invalid date.`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     utcNowDateTime, | ||||
|     localNowDateTime, | ||||
| @@ -70,5 +103,7 @@ module.exports = { | ||||
|     utcDateTimeStr, | ||||
|     parseDateTime, | ||||
|     parseLocalDate, | ||||
|     getDateTimeForFile | ||||
|     getDateTimeForFile, | ||||
|     validateLocalDateTime, | ||||
|     validateUtcDateTime | ||||
| }; | ||||
|   | ||||
| @@ -15,6 +15,12 @@ function putEntityChangeWithInstanceId(origEntityChange, instanceId) { | ||||
|     putEntityChange(ec); | ||||
| } | ||||
|  | ||||
| function putEntityChangeWithForcedChange(origEntityChange) { | ||||
|     const ec = {...origEntityChange, changeId: null}; | ||||
|  | ||||
|     putEntityChange(ec); | ||||
| } | ||||
|  | ||||
| function putEntityChange(origEntityChange) { | ||||
|     const ec = {...origEntityChange}; | ||||
|  | ||||
| @@ -66,13 +72,37 @@ function putEntityChangeForOtherInstances(ec) { | ||||
| function addEntityChangesForSector(entityName, sector) { | ||||
|     const entityChanges = sql.getRows(`SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ?`, [entityName, sector]); | ||||
|  | ||||
|     let entitiesInserted = entityChanges.length; | ||||
|  | ||||
|     sql.transactional(() => { | ||||
|         if (entityName === 'blobs') { | ||||
|             entitiesInserted += addEntityChangesForDependingEntity(sector, 'notes', 'noteId'); | ||||
|             entitiesInserted += addEntityChangesForDependingEntity(sector, 'attachments', 'attachmentId'); | ||||
|             entitiesInserted += addEntityChangesForDependingEntity(sector, 'revisions', 'revisionId'); | ||||
|         } | ||||
|  | ||||
|         for (const ec of entityChanges) { | ||||
|             putEntityChange(ec); | ||||
|             putEntityChangeWithForcedChange(ec); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     log.info(`Added sector ${sector} of '${entityName}' (${entityChanges.length} entities) to the sync queue.`); | ||||
|     log.info(`Added sector ${sector} of '${entityName}' (${entitiesInserted} entities) to the sync queue.`); | ||||
| } | ||||
|  | ||||
| function addEntityChangesForDependingEntity(sector, tableName, primaryKeyColumn) { | ||||
|     // problem in blobs might be caused by problem in entity referencing the blob | ||||
|     const dependingEntityChanges = sql.getRows(` | ||||
|                 SELECT dep_change.*  | ||||
|                 FROM entity_changes orig_sector | ||||
|                 JOIN ${tableName} ON ${tableName}.blobId = orig_sector.entityId | ||||
|                 JOIN entity_changes dep_change ON dep_change.entityName = '${tableName}' AND dep_change.entityId = ${tableName}.${primaryKeyColumn} | ||||
|                 WHERE orig_sector.entityName = 'blobs' AND SUBSTR(orig_sector.entityId, 1, 1) = ?`, [sector]); | ||||
|  | ||||
|     for (const ec of dependingEntityChanges) { | ||||
|         putEntityChangeWithForcedChange(ec); | ||||
|     } | ||||
|  | ||||
|     return dependingEntityChanges.length; | ||||
| } | ||||
|  | ||||
| function cleanupEntityChangesForMissingEntities(entityName, entityPrimaryKey) { | ||||
| @@ -161,6 +191,7 @@ function recalculateMaxEntityChangeId() { | ||||
| module.exports = { | ||||
|     putNoteReorderingEntityChange, | ||||
|     putEntityChangeForOtherInstances, | ||||
|     putEntityChangeWithForcedChange, | ||||
|     putEntityChange, | ||||
|     putEntityChangeWithInstanceId, | ||||
|     fillAllEntityChanges, | ||||
|   | ||||
| @@ -39,7 +39,7 @@ function setEntityChangesAsErased(entityChanges) { | ||||
|         ec.isErased = true; | ||||
|         ec.utcDateChanged = dateUtils.utcNowDateTime(); | ||||
|  | ||||
|         entityChangesService.putEntityChange(ec); | ||||
|         entityChangesService.putEntityChangeWithForcedChange(ec); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -301,16 +301,10 @@ function importEnex(taskContext, file, parentNote) { | ||||
|                         ? resource.title | ||||
|                         : `image.${resource.mime.substr(6)}`; // default if real name is not present | ||||
|  | ||||
|                     const {url, note: imageNote} = imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages); | ||||
|  | ||||
|                     for (const attr of resource.attributes) { | ||||
|                         if (attr.name !== 'originalFileName') { // this one is already saved in imageService | ||||
|                             imageNote.addAttribute(attr.type, attr.name, attr.value); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     updateDates(imageNote, utcDateCreated, utcDateModified); | ||||
|                     const attachment = imageService.saveImageToAttachment(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages); | ||||
|  | ||||
|                     const sanitizedTitle = attachment.title.replace(/[^a-z0-9-.]/gi, ""); | ||||
|                     const url = `api/attachments/${attachment.attachmentId}/image/${sanitizedTitle}`; | ||||
|                     const imageLink = `<img src="${url}">`; | ||||
|  | ||||
|                     content = content.replace(mediaRegex, imageLink); | ||||
|   | ||||
| @@ -9,8 +9,8 @@ const appInfo = require('./app_info'); | ||||
| async function migrate() { | ||||
|     const currentDbVersion = getDbVersion(); | ||||
|  | ||||
|     if (currentDbVersion < 183) { | ||||
|         log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.47.X first and only then to this version."); | ||||
|     if (currentDbVersion < 214) { | ||||
|         log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.X first and only then to this version."); | ||||
|  | ||||
|         utils.crash(); | ||||
|         return; | ||||
| @@ -18,9 +18,9 @@ async function migrate() { | ||||
|  | ||||
|     // backup before attempting migration | ||||
|     await backupService.backupNow( | ||||
|         // creating a special backup for versions 0.60.X and older, the changes in 0.61 are major. | ||||
|         currentDbVersion < 214 | ||||
|             ? `before-migration-v${currentDbVersion}` | ||||
|         // creating a special backup for versions 0.60.X, the changes in 0.61 are major. | ||||
|         currentDbVersion === 214 | ||||
|             ? `before-migration-v060` | ||||
|             : 'before-migration' | ||||
|     ); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| const sql = require('./sql'); | ||||
| const sqlInit = require('./sql_init'); | ||||
| const optionService = require('./options'); | ||||
| const dateUtils = require('./date_utils'); | ||||
| const entityChangesService = require('./entity_changes'); | ||||
| @@ -169,6 +168,15 @@ function createNewNote(params) { | ||||
|         throw new Error(`Note content must be set`); | ||||
|     } | ||||
|  | ||||
|     let error; | ||||
|     if (error = dateUtils.validateLocalDateTime(params.dateCreated)) { | ||||
|         throw new Error(error); | ||||
|     } | ||||
|  | ||||
|     if (error = dateUtils.validateUtcDateTime(params.utcDateCreated)) { | ||||
|         throw new Error(error); | ||||
|     } | ||||
|  | ||||
|     return sql.transactional(() => { | ||||
|         let note, branch, isEntityEventsDisabled; | ||||
|  | ||||
| @@ -189,7 +197,9 @@ function createNewNote(params) { | ||||
|                 title: params.title, | ||||
|                 isProtected: !!params.isProtected, | ||||
|                 type: params.type, | ||||
|                 mime: deriveMime(params.type, params.mime) | ||||
|                 mime: deriveMime(params.type, params.mime), | ||||
|                 dateCreated: params.dateCreated, | ||||
|                 utcDateCreated: params.utcDateCreated | ||||
|             }).save(); | ||||
|  | ||||
|             note.setContent(params.content); | ||||
|   | ||||
| @@ -9,7 +9,7 @@ function getOptionOrNull(name) { | ||||
|         option = becca.getOption(name); | ||||
|     } else { | ||||
|         // e.g. in initial sync becca is not loaded because DB is not initialized | ||||
|         option = sql.getRow("SELECT * FROM options WHERE name = ?", name); | ||||
|         option = sql.getRow("SELECT * FROM options WHERE name = ?", [name]); | ||||
|     } | ||||
|  | ||||
|     return option ? option.value : null; | ||||
|   | ||||
| @@ -10,7 +10,7 @@ function executeNote(note, apiParams) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const bundle = getScriptBundle(note); | ||||
|     const bundle = getScriptBundle(note, true, 'backend'); | ||||
|  | ||||
|     return executeBundle(bundle, apiParams); | ||||
| } | ||||
| @@ -68,9 +68,9 @@ function executeScript(script, params, startNoteId, currentNoteId, originEntityN | ||||
|  | ||||
|     // we're just executing an excerpt of the original frontend script in the backend context, so we must | ||||
|     // override normal note's content, and it's mime type / script environment | ||||
|     const backendOverrideContent = `return (${script}\r\n)(${getParams(params)})`; | ||||
|     const overrideContent = `return (${script}\r\n)(${getParams(params)})`; | ||||
|  | ||||
|     const bundle = getScriptBundle(currentNote, true, null, [], backendOverrideContent); | ||||
|     const bundle = getScriptBundle(currentNote, true, 'backend', [], overrideContent); | ||||
|  | ||||
|     return executeBundle(bundle, { startNote, originEntity }); | ||||
| } | ||||
| @@ -96,9 +96,17 @@ function getParams(params) { | ||||
|  | ||||
| /** | ||||
|  * @param {BNote} note | ||||
|  * @param {string} [script] | ||||
|  * @param {Array} [params] | ||||
|  */ | ||||
| function getScriptBundleForFrontend(note) { | ||||
|     const bundle = getScriptBundle(note); | ||||
| function getScriptBundleForFrontend(note, script, params) { | ||||
|     let overrideContent = null; | ||||
|  | ||||
|     if (script) { | ||||
|         overrideContent = `return (${script}\r\n)(${getParams(params)})`; | ||||
|     } | ||||
|  | ||||
|     const bundle = getScriptBundle(note, true, 'frontend', [], overrideContent); | ||||
|  | ||||
|     if (!bundle) { | ||||
|         return; | ||||
| @@ -119,9 +127,9 @@ function getScriptBundleForFrontend(note) { | ||||
|  * @param {boolean} [root=true] | ||||
|  * @param {string|null} [scriptEnv] | ||||
|  * @param {string[]} [includedNoteIds] | ||||
|  * @param {string|null} [backendOverrideContent] | ||||
|  * @param {string|null} [overrideContent] | ||||
|  */ | ||||
| function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = [], backendOverrideContent = null) { | ||||
| function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = [], overrideContent = null) { | ||||
|     if (!note.isContentAvailable()) { | ||||
|         return; | ||||
|     } | ||||
| @@ -134,12 +142,6 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (root) { | ||||
|         scriptEnv = backendOverrideContent | ||||
|             ? 'backend' | ||||
|             : note.getScriptEnv(); | ||||
|     } | ||||
|  | ||||
|     if (note.type !== 'file' && !root && scriptEnv !== note.getScriptEnv()) { | ||||
|         return; | ||||
|     } | ||||
| @@ -180,7 +182,7 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = | ||||
| apiContext.modules['${note.noteId}'] = { exports: {} }; | ||||
| ${root ? 'return ' : ''}${isFrontend ? 'await' : ''} ((${isFrontend ? 'async' : ''} function(exports, module, require, api${modules.length > 0 ? ', ' : ''}${modules.map(child => sanitizeVariableName(child.title)).join(', ')}) { | ||||
| try { | ||||
| ${backendOverrideContent || note.getContent()}; | ||||
| ${overrideContent || note.getContent()}; | ||||
| } catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); } | ||||
| for (const exportKey in exports) module.exports[exportKey] = exports[exportKey]; | ||||
| return module.exports; | ||||
|   | ||||
| @@ -11,7 +11,7 @@ function lex(str) { | ||||
|     let currentWord = ''; | ||||
|  | ||||
|     function isSymbolAnOperator(chr) { | ||||
|         return ['=', '*', '>', '<', '!', "-", "+", '%'].includes(chr); | ||||
|         return ['=', '*', '>', '<', '!', "-", "+", '%', ','].includes(chr); | ||||
|     } | ||||
|  | ||||
|     function isPreviousSymbolAnOperator() { | ||||
| @@ -128,6 +128,10 @@ function lex(str) { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (chr === ',') { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         currentWord += chr; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -40,13 +40,12 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId) { | ||||
|         // on this side, we can't unerase the entity, so force the entity to be erased on the other side. | ||||
|         entityChangesService.putEntityChangeForOtherInstances(localEC); | ||||
|  | ||||
|         return false; | ||||
|     } else if (localEC?.isErased && remoteEC.isErased) { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     if (!localEC | ||||
|         || localEC.utcDateChanged < remoteEC.utcDateChanged | ||||
|         || (localEC.utcDateChanged === remoteEC.utcDateChanged && localEC.hash !== remoteEC.hash) // sync error, we should still update | ||||
|     ) { | ||||
|     if (!localEC || localEC.utcDateChanged <= remoteEC.utcDateChanged) { | ||||
|         if (remoteEC.entityName === 'blobs' && remoteEntityRow.content !== null) { | ||||
|             // we always use a Buffer object which is different from normal saving - there we use a simple string type for | ||||
|             // "string notes". The problem is that in general, it's not possible to detect whether a blob content | ||||
| @@ -62,7 +61,9 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId) { | ||||
|  | ||||
|         sql.replace(remoteEC.entityName, remoteEntityRow); | ||||
|  | ||||
|         if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged) { | ||||
|             entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } else if (localEC.hash !== remoteEC.hash && localEC.utcDateChanged > remoteEC.utcDateChanged) { | ||||
|   | ||||
| @@ -184,10 +184,8 @@ function sortNotesIfNeeded(parentNoteId) { | ||||
|     } | ||||
|  | ||||
|     const sortReversed = parentNote.getLabelValue('sortDirection')?.toLowerCase() === "desc"; | ||||
|     const sortFoldersFirstLabel = parentNote.getLabel('sortFoldersFirst'); | ||||
|     const sortFoldersFirst = sortFoldersFirstLabel && sortFoldersFirstLabel.value.toLowerCase() !== "false"; | ||||
|     const sortNaturalLabel = parentNote.getLabel('sortNatural'); | ||||
|     const sortNatural = sortNaturalLabel && sortNaturalLabel.value.toLowerCase() !== "false"; | ||||
|     const sortFoldersFirst = parentNote.isLabelTruthy('sortFoldersFirst'); | ||||
|     const sortNatural = parentNote.isLabelTruthy('sortNatural'); | ||||
|     const sortLocale = parentNote.getLabelValue('sortLocale'); | ||||
|  | ||||
|     sortNotes(parentNoteId, sortedLabel.value, sortReversed, sortFoldersFirst, sortNatural, sortLocale); | ||||
|   | ||||
| @@ -7,13 +7,17 @@ Content-Type: application/json | ||||
|   "parentNoteId": "root", | ||||
|   "title": "Hello", | ||||
|   "type": "text", | ||||
|   "content": "Hi there!" | ||||
|   "content": "Hi there!", | ||||
|   "dateCreated": "2023-08-21 23:38:51.123+0200", | ||||
|   "utcDateCreated": "2023-08-21 23:38:51.123Z" | ||||
| } | ||||
|  | ||||
| > {% | ||||
|     client.assert(response.status === 201); | ||||
|     client.assert(response.body.note.noteId.startsWith("forcedId")); | ||||
|     client.assert(response.body.note.title == "Hello"); | ||||
|     client.assert(response.body.note.dateCreated == "2023-08-21 23:38:51.123+0200"); | ||||
|     client.assert(response.body.note.utcDateCreated == "2023-08-21 23:38:51.123Z"); | ||||
|     client.assert(response.body.branch.parentNoteId == "root"); | ||||
|  | ||||
|     client.log(`Created note ` + response.body.note.noteId + ` and branch ` + response.body.branch.branchId); | ||||
|   | ||||
| @@ -33,7 +33,9 @@ Content-Type: application/json | ||||
| { | ||||
|   "title": "Wassup", | ||||
|   "type": "html", | ||||
|   "mime": "text/html" | ||||
|   "mime": "text/html", | ||||
|   "dateCreated": "2023-08-21 23:38:51.123+0200", | ||||
|   "utcDateCreated": "2023-08-21 23:38:51.123Z" | ||||
| } | ||||
|  | ||||
| ### | ||||
| @@ -46,6 +48,8 @@ client.assert(response.status === 200); | ||||
| client.assert(response.body.title === 'Wassup'); | ||||
| client.assert(response.body.type === 'html'); | ||||
| client.assert(response.body.mime === 'text/html'); | ||||
| client.assert(response.body.dateCreated == "2023-08-21 23:38:51.123+0200"); | ||||
| client.assert(response.body.utcDateCreated == "2023-08-21 23:38:51.123Z"); | ||||
| %} | ||||
|  | ||||
| ### | ||||
|   | ||||
		Reference in New Issue
	
	Block a user