Compare commits

...

70 Commits

Author SHA1 Message Date
azivner
cb69914f09 release 0.13.0-beta 2018-05-22 23:51:43 -04:00
azivner
a372cbb2df fix #105 2018-05-22 23:51:13 -04:00
azivner
0ce5caefe8 refactoring 2018-05-22 22:22:15 -04:00
azivner
94dabb81f6 fix sync of unsyncable options 2018-05-22 19:29:18 -04:00
azivner
cd45bcfd03 converted option operations to repository 2018-05-22 00:22:43 -04:00
azivner
49a53f7a45 added hash columns for faster sync check calculation 2018-05-22 00:15:54 -04:00
azivner
9fa6c0918c add index for note's type + some fixes 2018-05-21 20:12:46 -04:00
azivner
e8d089e37e ckeditor 10.0.0 2018-05-21 19:35:49 -04:00
azivner
a931ce25fa attempt to fix the hoek security warning with package upgrade 2018-05-21 16:08:34 -04:00
azivner
b507abb4f7 electron upgrade to 2.0.0 2018-05-08 16:39:01 -04:00
azivner
66e7c6de62 fix ordering 2018-04-21 12:23:35 -04:00
azivner
4ce5ea9886 autocomplete supports encrypted notes now as well 2018-04-20 00:12:01 -04:00
azivner
8c54b62f07 fix protect branch 2018-04-19 22:18:19 -04:00
azivner
85eb50ed0f autocomplete with prefixes 2018-04-19 20:59:44 -04:00
azivner
5ffd621e9d autocomplete respects hideInAutocomplete label 2018-04-19 00:13:55 -04:00
azivner
df93cb09da fix hide-toggle 2018-04-18 23:13:37 -04:00
azivner
bbf04209f0 autocomplete cache gets updated with note update 2018-04-18 23:11:30 -04:00
azivner
834bfa39c7 limit number of results to 200, other tweaks 2018-04-18 20:56:23 -04:00
azivner
52b445f70b Merge branch 'stable' 2018-04-18 20:22:16 -04:00
azivner
7b9b4fbb0c backend autocomplete WIP 2018-04-18 00:26:42 -04:00
azivner
5af0ba1fcb layout fixes 2018-04-17 20:04:27 -04:00
azivner
85a9748291 fix for clones & optimizations 2018-04-16 23:34:56 -04:00
azivner
b4005a7ffe optimizations to the lazy loading - expanding tree now takes only one request 2018-04-16 23:13:33 -04:00
azivner
82de1c88d4 basic lazy loading of tree now works, still WIP 2018-04-16 20:40:18 -04:00
azivner
1687ed7e0b load only expanded tree with the rest being lazy loaded, WIP 2018-04-16 16:26:47 -04:00
azivner
c8b9c7d936 release 0.12.0 2018-04-14 08:28:50 -04:00
azivner
d57057ba28 fix note ordering sync 2018-04-14 08:23:06 -04:00
azivner
66cee8daa4 restructuring CSS grid/flex which fixes jumpy scrolling in tree 2018-04-13 19:58:33 -04:00
azivner
afd7df0942 fix collapse tree button 2018-04-13 19:22:12 -04:00
azivner
bd6ae33d32 fancytree upgrade to 2.28.1 2018-04-12 20:42:12 -04:00
azivner
70660a0d68 Merge branch 'stable' 2018-04-12 20:04:01 -04:00
azivner
cdad18551a upgrade CKEditor to 1.0 beta.2, fixes #93 2018-04-12 20:03:23 -04:00
azivner
592c51d1a5 fix note reordering sync again 2018-04-12 18:31:29 -04:00
azivner
6a57b8a7e7 fix ordering sync 2018-04-12 18:13:48 -04:00
azivner
7a94e21c54 tabindex 2 for text and code editor so that tabbing from title leads to editor focus 2018-04-11 22:44:33 -04:00
azivner
5b43f321e2 release 0.11.1 2018-04-11 00:10:33 -04:00
azivner
a4eafb934f non null note title and content in the DB schema, allow saving non-valid JSON notes, children overview style changes 2018-04-11 00:10:11 -04:00
azivner
7b59a665dd hideChildrenOverview label which can disable children overview for specific notes 2018-04-10 23:15:41 -04:00
azivner
3d15450ffc children overview styling 2018-04-10 21:08:00 -04:00
azivner
b0c6d52461 can't rollback transaction multiple times 2018-04-10 20:28:02 -04:00
azivner
2dc16dd29f release 0.11.0-beta 2018-04-09 22:38:37 -04:00
azivner
d8924c536b Merge branch 'master' into stable 2018-04-09 22:30:50 -04:00
azivner
3ebbf2cc46 fix generating build.js 2018-04-09 22:30:11 -04:00
azivner
f4079604c9 basic implementation of children overview, closes #80 2018-04-08 22:38:52 -04:00
azivner
1f96a6beab export & import work correctly with clones 2018-04-08 13:14:30 -04:00
azivner
b277a250e5 protected notes are not in autocomplete when not in protected session, fixes #46 2018-04-08 12:27:10 -04:00
azivner
5b0e1a644d codemirror now doesn't hijack alt-left/right, fixes #86 2018-04-08 12:17:42 -04:00
azivner
6bb3cfa9a3 note revisions for code is now properly formatted, fixes #97 2018-04-08 12:13:52 -04:00
azivner
9720868f5a added type and mime to note revisions 2018-04-08 11:57:14 -04:00
azivner
8d8ee2a87a small sync refactorings 2018-04-08 10:09:33 -04:00
azivner
542e82ee5d upgraded uncompressed jquery 2018-04-08 09:40:28 -04:00
azivner
0104b19502 naming standards 2018-04-08 09:25:35 -04:00
azivner
120888b53e fix JSON saving bug 2018-04-08 08:31:19 -04:00
azivner
d2e2caed62 refactoring of note saving code & API 2018-04-08 08:21:49 -04:00
azivner
63066802a8 fix showMessage, showError
(cherry picked from commit 6128bb4)
2018-04-08 07:49:21 -04:00
azivner
6128bb4ff3 fix showMessage, showError 2018-04-08 07:48:47 -04:00
azivner
982796255d sync content check refactoring 2018-04-07 22:59:47 -04:00
azivner
36b15f474d sync cleanup 2018-04-07 22:32:46 -04:00
azivner
13f71f8967 bulk push sync 2018-04-07 22:25:28 -04:00
azivner
64336ffbee implemented bulk sync pull for increased performance 2018-04-07 21:53:42 -04:00
azivner
b09463d1b2 async logging of info messages 2018-04-07 21:30:01 -04:00
azivner
b5e6f46b9c release 0.10.2-beta 2018-04-07 16:07:25 -04:00
azivner
08af4a0465 fix code mirror loading 2018-04-07 15:56:46 -04:00
azivner
8c5df6321f fix windows sqlite binary for electron 2.0 2018-04-07 13:18:08 -04:00
azivner
d19f044961 fix bug 2018-04-07 13:14:01 -04:00
azivner
e378d9f645 label service refactoring + rename of doInTransaction to transactional 2018-04-07 13:03:16 -04:00
azivner
39dc0f71b4 fix execute note 2018-04-06 19:41:48 -04:00
azivner
0cef5c6b8c added showMessage/showError to script api as they are being used 2018-04-06 19:08:42 -04:00
azivner
9b5a44cef4 fix bugs 2018-04-06 18:49:37 -04:00
azivner
29769ed91d fix force note sync 2018-04-06 18:46:29 -04:00
92 changed files with 7160 additions and 4892 deletions

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.7">
<root id="1"/>
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.8">
<root id="1">
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
<Visible>1</Visible>
@@ -107,8 +109,7 @@
<index id="35" parent="7" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId
parentNoteId</ColNames>
<ColumnCollations>
</ColumnCollations>
<ColumnCollations></ColumnCollations>
</index>
<index id="36" parent="7" name="IDX_branches_noteId">
<ColNames>noteId</ColNames>
@@ -142,445 +143,453 @@ parentNoteId</ColNames>
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<foreign-key id="43" parent="8">
<ColNames>noteId</ColNames>
<RefTableName>notes</RefTableName>
<RefColNames>noteId</RefColNames>
</foreign-key>
<column id="44" parent="9" name="imageId">
<column id="43" parent="9" name="imageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="45" parent="9" name="format">
<column id="44" parent="9" name="format">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="46" parent="9" name="checksum">
<column id="45" parent="9" name="checksum">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="47" parent="9" name="name">
<column id="46" parent="9" name="name">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="48" parent="9" name="data">
<column id="47" parent="9" name="data">
<Position>5</Position>
<DataType>BLOB|0s</DataType>
</column>
<column id="49" parent="9" name="isDeleted">
<column id="48" parent="9" name="isDeleted">
<Position>6</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="50" parent="9" name="dateModified">
<column id="49" parent="9" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="51" parent="9" name="dateCreated">
<column id="50" parent="9" name="dateCreated">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="52" parent="9" name="sqlite_autoindex_images_1">
<index id="51" parent="9" name="sqlite_autoindex_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="53" parent="9">
<key id="52" parent="9">
<ColNames>imageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
</key>
<column id="54" parent="10" name="labelId">
<column id="53" parent="10" name="labelId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="55" parent="10" name="noteId">
<column id="54" parent="10" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="56" parent="10" name="name">
<column id="55" parent="10" name="name">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="57" parent="10" name="value">
<column id="56" parent="10" name="value">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="58" parent="10" name="position">
<column id="57" parent="10" name="position">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="59" parent="10" name="dateCreated">
<column id="58" parent="10" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="60" parent="10" name="dateModified">
<column id="59" parent="10" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="61" parent="10" name="isDeleted">
<column id="60" parent="10" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="62" parent="10" name="sqlite_autoindex_labels_1">
<index id="61" parent="10" name="sqlite_autoindex_labels_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>labelId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="63" parent="10" name="IDX_labels_noteId">
<index id="62" parent="10" name="IDX_labels_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="64" parent="10" name="IDX_labels_name_value">
<index id="63" parent="10" name="IDX_labels_name_value">
<ColNames>name
value</ColNames>
<ColumnCollations>
</ColumnCollations>
<ColumnCollations></ColumnCollations>
</index>
<key id="65" parent="10">
<key id="64" parent="10">
<ColNames>labelId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
</key>
<column id="66" parent="11" name="noteImageId">
<column id="65" parent="11" name="noteImageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="67" parent="11" name="noteId">
<column id="66" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="68" parent="11" name="imageId">
<column id="67" parent="11" name="imageId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="69" parent="11" name="isDeleted">
<column id="68" parent="11" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="70" parent="11" name="dateModified">
<column id="69" parent="11" name="dateModified">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="71" parent="11" name="dateCreated">
<column id="70" parent="11" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="72" parent="11" name="sqlite_autoindex_note_images_1">
<index id="71" parent="11" name="sqlite_autoindex_note_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteImageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="73" parent="11" name="IDX_note_images_noteId_imageId">
<index id="72" parent="11" name="IDX_note_images_noteId_imageId">
<ColNames>noteId
imageId</ColNames>
<ColumnCollations>
</ColumnCollations>
<ColumnCollations></ColumnCollations>
</index>
<index id="74" parent="11" name="IDX_note_images_noteId">
<index id="73" parent="11" name="IDX_note_images_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="75" parent="11" name="IDX_note_images_imageId">
<index id="74" parent="11" name="IDX_note_images_imageId">
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="76" parent="11">
<key id="75" parent="11">
<ColNames>noteImageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
</key>
<column id="77" parent="12" name="noteRevisionId">
<column id="76" parent="12" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="78" parent="12" name="noteId">
<column id="77" parent="12" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="79" parent="12" name="title">
<column id="78" parent="12" name="title">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="80" parent="12" name="content">
<column id="79" parent="12" name="content">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="81" parent="12" name="isProtected">
<column id="80" parent="12" name="isProtected">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="82" parent="12" name="dateModifiedFrom">
<column id="81" parent="12" name="dateModifiedFrom">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="83" parent="12" name="dateModifiedTo">
<column id="82" parent="12" name="dateModifiedTo">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="84" parent="12" name="sqlite_autoindex_note_revisions_1">
<column id="83" parent="12" name="type">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="84" parent="12" name="mime">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<index id="85" parent="12" name="sqlite_autoindex_note_revisions_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="85" parent="12" name="IDX_note_revisions_noteId">
<index id="86" parent="12" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="86" parent="12" name="IDX_note_revisions_dateModifiedFrom">
<index id="87" parent="12" name="IDX_note_revisions_dateModifiedFrom">
<ColNames>dateModifiedFrom</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="87" parent="12" name="IDX_note_revisions_dateModifiedTo">
<index id="88" parent="12" name="IDX_note_revisions_dateModifiedTo">
<ColNames>dateModifiedTo</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="88" parent="12">
<key id="89" parent="12">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key>
<column id="89" parent="13" name="noteId">
<column id="90" parent="13" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="90" parent="13" name="title">
<column id="91" parent="13" name="title">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;unnamed&quot;</DefaultExpression>
</column>
<column id="91" parent="13" name="content">
<column id="92" parent="13" name="content">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="92" parent="13" name="isProtected">
<column id="93" parent="13" name="isProtected">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="93" parent="13" name="isDeleted">
<column id="94" parent="13" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="94" parent="13" name="dateCreated">
<column id="95" parent="13" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="95" parent="13" name="dateModified">
<column id="96" parent="13" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="96" parent="13" name="type">
<column id="97" parent="13" name="type">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text&apos;</DefaultExpression>
</column>
<column id="97" parent="13" name="mime">
<column id="98" parent="13" name="mime">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text/html&apos;</DefaultExpression>
</column>
<index id="98" parent="13" name="sqlite_autoindex_notes_1">
<index id="99" parent="13" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="99" parent="13" name="IDX_notes_isDeleted">
<index id="100" parent="13" name="IDX_notes_isDeleted">
<ColNames>isDeleted</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="100" parent="13">
<key id="101" parent="13">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key>
<column id="101" parent="14" name="name">
<column id="102" parent="14" name="name">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="102" parent="14" name="value">
<column id="103" parent="14" name="value">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="103" parent="14" name="dateModified">
<column id="104" parent="14" name="dateModified">
<Position>3</Position>
<DataType>INT|0s</DataType>
</column>
<column id="104" parent="14" name="isSynced">
<column id="105" parent="14" name="isSynced">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<index id="105" parent="14" name="sqlite_autoindex_options_1">
<index id="106" parent="14" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="106" parent="14">
<key id="107" parent="14">
<ColNames>name</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
</key>
<column id="107" parent="15" name="branchId">
<column id="108" parent="15" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="108" parent="15" name="notePath">
<column id="109" parent="15" name="notePath">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="109" parent="15" name="dateAccessed">
<column id="110" parent="15" name="dateAccessed">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="110" parent="15" name="isDeleted">
<column id="111" parent="15" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
</column>
<index id="111" parent="15" name="sqlite_autoindex_recent_notes_1">
<index id="112" parent="15" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="112" parent="15">
<key id="113" parent="15">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key>
<column id="113" parent="16" name="sourceId">
<column id="114" parent="16" name="sourceId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="114" parent="16" name="dateCreated">
<column id="115" parent="16" name="dateCreated">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="115" parent="16" name="sqlite_autoindex_source_ids_1">
<index id="116" parent="16" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="116" parent="16">
<key id="117" parent="16">
<ColNames>sourceId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key>
<column id="117" parent="17" name="type">
<column id="118" parent="17" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="118" parent="17" name="name">
<column id="119" parent="17" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="119" parent="17" name="tbl_name">
<column id="120" parent="17" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="120" parent="17" name="rootpage">
<column id="121" parent="17" name="rootpage">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<column id="121" parent="17" name="sql">
<column id="122" parent="17" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="122" parent="18" name="name">
<column id="123" parent="18" name="name">
<Position>1</Position>
</column>
<column id="123" parent="18" name="seq">
<column id="124" parent="18" name="seq">
<Position>2</Position>
</column>
<column id="124" parent="19" name="id">
<column id="125" parent="19" name="id">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="125" parent="19" name="entityName">
<column id="126" parent="19" name="entityName">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="126" parent="19" name="entityId">
<column id="127" parent="19" name="entityId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="127" parent="19" name="sourceId">
<column id="128" parent="19" name="sourceId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="128" parent="19" name="syncDate">
<column id="129" parent="19" name="syncDate">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="129" parent="19" name="IDX_sync_entityName_entityId">
<index id="130" parent="19" name="IDX_sync_entityName_entityId">
<ColNames>entityName
entityId</ColNames>
<ColumnCollations>
</ColumnCollations>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="130" parent="19" name="IDX_sync_syncDate">
<index id="131" parent="19" name="IDX_sync_syncDate">
<ColNames>syncDate</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="131" parent="19">
<key id="132" parent="19">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>

View File

@@ -24,9 +24,9 @@ jq '.version = "'$VERSION'"' package.json|sponge package.json
git add package.json
echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > services/build.js
echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > src/services/build.js
git add services/build.js
git add src/services/build.js
TAG=v$VERSION

View File

@@ -1,3 +1,4 @@
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('root', 'root', 'none', 0, null, 1, 0, '2018-01-01T00:00:00.000Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');

View File

@@ -0,0 +1,5 @@
ALTER TABLE note_revisions ADD type TEXT DEFAULT '' NOT NULL;
ALTER TABLE note_revisions ADD mime TEXT DEFAULT '' NOT NULL;
UPDATE note_revisions SET type = (SELECT type FROM notes WHERE notes.noteId = note_revisions.noteId);
UPDATE note_revisions SET mime = (SELECT mime FROM notes WHERE notes.noteId = note_revisions.noteId);

View File

@@ -0,0 +1,34 @@
CREATE TABLE event_logc027
(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
noteId TEXT,
comment TEXT,
dateAdded TEXT NOT NULL
);
INSERT INTO event_logc027(id, noteId, comment, dateAdded) SELECT id, noteId, comment, dateAdded FROM event_log;
DROP TABLE event_log;
ALTER TABLE event_logc027 RENAME TO event_log;
CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed",
`content` TEXT NOT NULL DEFAULT "",
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
INSERT INTO notes_mig (noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime)
SELECT noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime FROM notes;
DROP TABLE notes;
ALTER TABLE notes_mig RENAME TO notes;
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);

View File

@@ -0,0 +1,5 @@
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, dateModified)
VALUES ('root', 'root', 'none', 0, null, 1, '2018-01-01T00:00:00.000Z');
INSERT INTO sync (entityName, entityId, sourceId, syncDate)
VALUES ('branches' ,'root', 'SYNC_FILL', '2018-01-01T00:00:00.000Z');

View File

@@ -0,0 +1 @@
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);

View File

@@ -0,0 +1,2 @@
-- index confuses planner and is mostly useless anyway since we're mostly used in non-deleted notes (which are presumably majority)
DROP INDEX IDX_notes_isDeleted;

View File

@@ -0,0 +1,2 @@
create index IDX_notes_type
on notes (type);

View File

@@ -0,0 +1,9 @@
ALTER TABLE notes ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE branches ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE note_revisions ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE recent_notes ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE options ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE note_images ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE images ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE labels ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE api_tokens ADD hash TEXT DEFAULT "" NOT NULL;

View File

@@ -21,28 +21,6 @@ CREATE TABLE IF NOT EXISTS "source_ids" (
`dateCreated` TEXT NOT NULL,
PRIMARY KEY(`sourceId`)
);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT,
`content` TEXT,
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);
CREATE TABLE IF NOT EXISTS "event_log" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`noteId` TEXT,
`comment` TEXT,
`dateAdded` TEXT NOT NULL,
FOREIGN KEY(noteId) REFERENCES notes(noteId)
);
CREATE TABLE IF NOT EXISTS "note_revisions" (
`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
@@ -51,7 +29,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
`isProtected` INT NOT NULL DEFAULT 0,
`dateModifiedFrom` TEXT NOT NULL,
`dateModifiedTo` TEXT NOT NULL
);
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId`
);
@@ -130,3 +108,25 @@ CREATE INDEX IDX_labels_name_value
on labels (name, value);
CREATE INDEX IDX_labels_noteId
on labels (noteId);
CREATE TABLE IF NOT EXISTS "event_log"
(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
noteId TEXT,
comment TEXT,
dateAdded TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed",
`content` TEXT NOT NULL DEFAULT "",
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);

View File

@@ -76,12 +76,12 @@ app.on('ready', () => {
const dateNoteService = require('./src/services/date_notes');
const dateUtils = require('./src/services/date_utils');
const parentNoteId = await dateNoteService.getDateNoteId(dateUtils.nowDate());
const parentNote = await dateNoteService.getDateNote(dateUtils.nowDate());
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('create-day-sub-note', parentNoteId);
mainWindow.webContents.send('create-day-sub-note', parentNote.noteId);
});
if (!result) {

8503
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.10.1-beta",
"version": "0.13.0-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -9,11 +9,11 @@
"url": "https://github.com/zadam/trilium.git"
},
"scripts": {
"start": "node ./bin/www",
"start": "node ./src/www",
"test-electron": "xo",
"rebuild-electron": "electron-rebuild",
"start-electron": "electron . --disable-gpu",
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64",
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version=",
"start-forge": "electron-forge start",
"package-forge": "electron-forge package",
"make-forge": "electron-forge make",
@@ -21,22 +21,20 @@
},
"dependencies": {
"async-mutex": "^0.1.3",
"axios": "^0.17.1",
"body-parser": "~1.18.2",
"axios": "^0.18",
"body-parser": "^1.18.3",
"cls-hooked": "^4.2.2",
"cookie-parser": "~1.4.3",
"debug": "~3.1.0",
"devtron": "^1.4.0",
"ejs": "~2.5.7",
"electron": "^2.0.0-beta.5",
"ejs": "~2.6.1",
"electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4",
"electron-rebuild": "^1.7.3",
"electron-dl": "^1.12.0",
"electron-in-page-search": "^1.3.2",
"express": "~4.16.3",
"express-session": "^1.15.6",
"fs-extra": "^4.0.3",
"helmet": "^3.12.0",
"fs-extra": "^6.0.1",
"helmet": "^3.12.1",
"html": "^1.0.0",
"image-type": "^3.0.0",
"imagemin": "^5.3.1",
@@ -45,30 +43,33 @@
"imagemin-pngquant": "^5.1.0",
"ini": "^1.3.5",
"jimp": "^0.2.28",
"moment": "^2.21.0",
"moment": "^2.22.1",
"multer": "^1.3.0",
"open": "0.0.5",
"rand-token": "^0.4.0",
"request": "^2.85.0",
"rcedit": "^1.1.0",
"request": "^2.87.0",
"request-promise": "^4.2.2",
"rimraf": "^2.6.2",
"sanitize-filename": "^1.6.1",
"scrypt": "^6.0.3",
"serve-favicon": "~2.4.5",
"serve-favicon": "~2.5.0",
"session-file-store": "^1.2.0",
"simple-node-logger": "^0.93.37",
"sqlite": "^2.9.1",
"tar-stream": "^1.5.5",
"sqlite": "^2.9.2",
"tar-stream": "^1.6.1",
"unescape": "^1.0.1",
"ws": "^3.3.3"
"ws": "^5.2.0"
},
"devDependencies": {
"electron": "^2.0.1",
"electron-compile": "^6.4.2",
"electron-packager": "^11.1.0",
"electron-prebuilt-compile": "2.0.0-beta.5",
"electron-packager": "^12.1.0",
"electron-prebuilt-compile": "2.0.0",
"electron-rebuild": "^1.7.3",
"lorem-ipsum": "^1.0.4",
"tape": "^4.9.0",
"xo": "^0.18.0"
"xo": "^0.21.1"
},
"config": {
"forge": {

View File

@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
class ApiToken extends Entity {
static get tableName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; }
static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
beforeSaving() {
super.beforeSaving();

View File

@@ -8,6 +8,8 @@ const sql = require('../services/sql');
class Branch extends Entity {
static get tableName() { return "branches"; }
static get primaryKeyName() { return "branchId"; }
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "dateModified", "isDeleted", "prefix"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -14,6 +14,17 @@ class Entity {
if (!this[this.constructor.primaryKeyName]) {
this[this.constructor.primaryKeyName] = utils.newEntityId();
}
let contentToHash = "";
for (const propertyName of this.constructor.hashedProperties) {
contentToHash += "|" + this[propertyName];
}
// this IF is to ease the migration from before hashed options, can be later removed
if (this.constructor.tableName !== 'options' || this.isSynced) {
this["hash"] = utils.hash(contentToHash).substr(0, 10);
}
}
async save() {

View File

@@ -6,6 +6,7 @@ const Branch = require('../entities/branch');
const Label = require('../entities/label');
const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token');
const Option = require('../entities/option');
const repository = require('../services/repository');
function createEntityFromRow(row) {
@@ -35,6 +36,9 @@ function createEntityFromRow(row) {
else if (row.noteId) {
entity = new Note(row);
}
else if (row.name) {
entity = new Option(row);
}
else {
throw new Error('Unknown entity type for row: ' + JSON.stringify(row));
}

View File

@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
class Image extends Entity {
static get tableName() { return "images"; }
static get primaryKeyName() { return "imageId"; }
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
beforeSaving() {
super.beforeSaving();

View File

@@ -8,6 +8,7 @@ const sql = require('../services/sql');
class Label extends Entity {
static get tableName() { return "labels"; }
static get primaryKeyName() { return "labelId"; }
static get hashedProperties() { return ["labelId", "noteId", "name", "value", "dateModified", "dateCreated"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -1,24 +1,33 @@
"use strict";
const Entity = require('./entity');
const protected_session = require('../services/protected_session');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
class Note extends Entity {
static get tableName() { return "notes"; }
static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
constructor(row) {
super(row);
if (this.isProtected) {
protected_session.decryptNote(this);
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
if (this.isProtected && this.noteId) {
protectedSessionService.decryptNote(this);
}
if (this.isJson()) {
this.setContent(this.content);
}
setContent(content) {
this.content = content;
try {
this.jsonContent = JSON.parse(this.content);
}
catch(e) {}
}
isJson() {
@@ -133,12 +142,12 @@ class Note extends Entity {
beforeSaving() {
super.beforeSaving();
if (this.isJson()) {
if (this.isJson() && this.jsonContent) {
this.content = JSON.stringify(this.jsonContent, null, '\t');
}
if (this.isProtected) {
protected_session.encryptNote(this);
protectedSessionService.encryptNote(this);
}
if (!this.isDeleted) {

View File

@@ -7,6 +7,7 @@ const dateUtils = require('../services/date_utils');
class NoteImage extends Entity {
static get tableName() { return "note_images"; }
static get primaryKeyName() { return "noteImageId"; }
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateModified", "dateCreated"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -1,19 +1,19 @@
"use strict";
const Entity = require('./entity');
const protected_session = require('../services/protected_session');
const utils = require('../services/utils');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
class NoteRevision extends Entity {
static get tableName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
constructor(row) {
super(row);
if (this.isProtected) {
protected_session.decryptNoteRevision(this);
protectedSessionService.decryptNoteRevision(this);
}
}
@@ -25,7 +25,7 @@ class NoteRevision extends Entity {
super.beforeSaving();
if (this.isProtected) {
protected_session.encryptNoteRevision(this);
protectedSessionService.encryptNoteRevision(this);
}
}
}

18
src/entities/option.js Normal file
View File

@@ -0,0 +1,18 @@
"use strict";
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
class Option extends Entity {
static get tableName() { return "options"; }
static get primaryKeyName() { return "name"; }
static get hashedProperties() { return ["name", "value"]; }
beforeSaving() {
super.beforeSaving();
this.dateModified = dateUtils.nowDate();
}
}
module.exports = Option;

View File

@@ -5,6 +5,7 @@ const Entity = require('./entity');
class RecentNote extends Entity {
static get tableName() { return "recent_notes"; }
static get primaryKeyName() { return "branchId"; }
static get hashedProperties() { return ["branchId", "notePath", "dateAccessed", "isDeleted"]; }
}
module.exports = RecentNote;

View File

@@ -45,8 +45,8 @@ async function showDialog() {
$clonePrefix.val('');
$linkTitle.val('');
function setDefaultLinkTitle(noteId) {
const noteTitle = treeUtils.getNoteTitle(noteId);
async function setDefaultLinkTitle(noteId) {
const noteTitle = await treeUtils.getNoteTitle(noteId);
$linkTitle.val(noteTitle);
}
@@ -54,7 +54,7 @@ async function showDialog() {
$autoComplete.autocomplete({
source: await autocompleteService.getAutocompleteItems(),
minLength: 0,
change: () => {
change: async () => {
const val = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(val);
if (!notePath) {
@@ -64,16 +64,16 @@ async function showDialog() {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
await setDefaultLinkTitle(noteId);
}
},
// this is called when user goes through autocomplete list with keyboard
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
focus: (event, ui) => {
focus: async (event, ui) => {
const notePath = linkService.getNodePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
setDefaultLinkTitle(noteId);
await setDefaultLinkTitle(noteId);
}
});
}

View File

@@ -25,7 +25,7 @@ async function showDialog() {
$treePrefixInput.val(branch.prefix).focus();
const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId);
const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
$noteTitle.html(noteTitle);
}

View File

@@ -22,7 +22,7 @@ async function showDialog() {
const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
if (event.noteId) {
const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML');
const noteLink = await linkService.createNoteLink(event.noteId).prop('outerHTML');
event.comment = event.comment.replace('<note>', noteLink);
}

View File

@@ -1,7 +1,6 @@
import treeService from '../services/tree.js';
import linkService from '../services/link.js';
import utils from '../services/utils.js';
import autocompleteService from '../services/autocomplete.js';
import server from '../services/server.js';
const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
@@ -18,8 +17,12 @@ async function showDialog() {
});
await $autoComplete.autocomplete({
source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems),
minLength: 1
source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
response(result);
},
minLength: 2
});
}

View File

@@ -54,7 +54,13 @@ $list.on('change', () => {
const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal);
$title.html(revisionItem.title);
$content.html(revisionItem.content);
if (revisionItem.type === 'text') {
$content.html(revisionItem.content);
}
else if (revisionItem.type === 'code') {
$content.html($("<pre>").text(revisionItem.content));
}
});
$(document).on('click', "a[action='note-revision']", event => {

View File

@@ -40,7 +40,7 @@ async function showDialog() {
noteLink = change.current_title;
}
else {
noteLink = linkService.createNoteLink(change.noteId, change.title);
noteLink = await linkService.createNoteLink(change.noteId, change.title);
}
changesListEl.append($('<li>')

View File

@@ -14,6 +14,10 @@ class Branch {
return await this.treeCache.getNote(this.noteId);
}
isTopLevel() {
return this.parentNoteId === 'root';
}
get toString() {
return `Branch(branchId=${this.branchId})`;
}

View File

@@ -6,8 +6,11 @@ class NoteFull extends NoteShort {
this.content = row.content;
if (this.isJson()) {
this.jsonContent = JSON.parse(this.content);
if (this.content !== "" && this.isJson()) {
try {
this.jsonContent = JSON.parse(this.content);
}
catch(e) {}
}
}
}

View File

@@ -14,36 +14,55 @@ class NoteShort {
}
async getBranches() {
const branches = [];
const branchIds = this.treeCache.parents[this.noteId].map(
parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
for (const parent of this.treeCache.parents[this.noteId]) {
branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId));
}
return this.treeCache.getBranches(branchIds);
}
return branches;
hasChildren() {
return this.treeCache.children[this.noteId]
&& this.treeCache.children[this.noteId].length > 0;
}
async getChildBranches() {
const branches = [];
for (const child of this.treeCache.children[this.noteId]) {
branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
if (!this.treeCache.children[this.noteId]) {
return [];
}
return branches;
const branchIds = this.treeCache.children[this.noteId].map(
childNoteId => this.treeCache.getBranchIdByChildParent(childNoteId, this.noteId));
return await this.treeCache.getBranches(branchIds);
}
async getParentNotes() {
getParentNoteIds() {
return this.treeCache.parents[this.noteId] || [];
}
async getChildNotes() {
async getParentNotes() {
return await this.treeCache.getNotes(this.getParentNoteIds());
}
getChildNoteIds() {
return this.treeCache.children[this.noteId] || [];
}
async getChildNotes() {
return await this.treeCache.getNotes(this.getChildNoteIds());
}
get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
get dto() {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.hideInAutocomplete;
return dto;
}
}
export default NoteShort;

View File

@@ -1,5 +1,6 @@
import treeCache from "./tree_cache.js";
import treeUtils from "./tree_utils.js";
import protectedSessionHolder from './protected_session_holder.js';
async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
if (!parentNoteId) {
@@ -21,9 +22,6 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
titlePath = '';
}
// https://github.com/zadam/trilium/issues/46
// unfortunately not easy to implement because we don't have an easy access to note's isProtected property
const autocompleteItems = [];
for (const childNote of childNotes) {
@@ -34,10 +32,12 @@ async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
autocompleteItems.push({
value: childTitlePath + ' (' + childNotePath + ')',
label: childTitlePath
});
if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
autocompleteItems.push({
value: childTitlePath + ' (' + childNotePath + ')',
label: childTitlePath
});
}
const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);

View File

@@ -8,6 +8,7 @@ import treeUtils from './tree_utils.js';
import branchPrefixDialog from '../dialogs/branch_prefix.js';
import infoService from "./info.js";
import treeCache from "./tree_cache.js";
import syncService from "./sync.js";
const $tree = $("#tree");
@@ -103,7 +104,7 @@ const contextMenuOptions = {
],
beforeOpen: async (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
const branch = await treeCache.getBranch(branchId);
const branch = await treeCache.getBranch(node.data.branchId);
const note = await treeCache.getNote(node.data.noteId);
const parentNote = await treeCache.getNote(branch.parentNoteId);

View File

@@ -32,18 +32,19 @@ async function requireLibrary(library) {
}
}
const dynamicallyLoadedScripts = [];
// we save the promises in case of the same script being required concurrently multiple times
const loadedScriptPromises = {};
async function requireScript(url) {
if (!dynamicallyLoadedScripts.includes(url)) {
dynamicallyLoadedScripts.push(url);
return await $.ajax({
if (!loadedScriptPromises[url]) {
loadedScriptPromises[url] = $.ajax({
url: url,
dataType: "script",
cache: true
})
});
}
await loadedScriptPromises[url];
}
async function requireCss(url) {

View File

@@ -23,11 +23,11 @@ function getNodePathFromLabel(label) {
return null;
}
function createNoteLink(notePath, noteTitle) {
async function createNoteLink(notePath, noteTitle) {
if (!noteTitle) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteTitle = treeUtils.getNoteTitle(noteId);
noteTitle = await treeUtils.getNoteTitle(noteId);
}
const noteLink = $("<a>", {
@@ -76,9 +76,11 @@ function goToLink(e) {
function addLinkToEditor(linkTitle, linkHref) {
const editor = noteDetailText.getEditor();
const doc = editor.document;
doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
editor.model.change( writer => {
const insertPosition = editor.model.document.selection.getFirstPosition();
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
});
}
function addTextToEditor(text) {

View File

@@ -1,4 +1,5 @@
import treeService from './tree.js';
import treeUtils from './tree_utils.js';
import noteTypeService from './note_type.js';
import protectedSessionService from './protected_session.js';
import protectedSessionHolder from './protected_session_holder.js';
@@ -24,6 +25,7 @@ const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list");
const $labelListInner = $("#label-list-inner");
const $childrenOverview = $("#children-overview");
let currentNote = null;
@@ -73,50 +75,42 @@ function noteChanged() {
async function reload() {
// no saving here
await loadNoteToEditor(getCurrentNoteId());
await loadNoteDetail(getCurrentNoteId());
}
async function switchToNote(noteId) {
if (getCurrentNoteId() !== noteId) {
await saveNoteIfChanged();
await loadNoteToEditor(noteId);
await loadNoteDetail(noteId);
}
}
async function saveNote() {
const note = getCurrentNote();
note.title = $noteTitle.val();
note.content = getComponent(note.type).getContent();
treeService.setNoteTitle(note.noteId, note.title);
await server.put('notes/' + note.noteId, note.dto);
isNoteChanged = false;
if (note.isProtected) {
protectedSessionHolder.touchProtectedSession();
}
infoService.showMessage("Saved!");
}
async function saveNoteIfChanged() {
if (!isNoteChanged) {
return;
}
const note = getCurrentNote();
updateNoteFromInputs(note);
await saveNoteToServer(note);
if (note.isProtected) {
protectedSessionHolder.touchProtectedSession();
}
}
function updateNoteFromInputs(note) {
note.title = $noteTitle.val();
note.content = getComponent(note.type).getContent();
treeService.setNoteTitle(note.noteId, note.title);
}
async function saveNoteToServer(note) {
const dto = Object.assign({}, note);
delete dto.treeCache;
delete dto.hideInAutocomplete;
await server.put('notes/' + dto.noteId, dto);
isNoteChanged = false;
infoService.showMessage("Saved!");
await saveNote();
}
function setNoteBackgroundIfProtected(note) {
@@ -145,7 +139,7 @@ async function handleProtectedSession() {
protectedSessionService.ensureDialogIsClosed();
}
async function loadNoteToEditor(noteId) {
async function loadNoteDetail(noteId) {
currentNote = await loadNote(noteId);
if (isNewNoteCreated) {
@@ -182,7 +176,35 @@ async function loadNoteToEditor(noteId) {
// after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0);
await loadLabelList();
const labels = await loadLabelList();
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
await showChildrenOverview(hideChildrenOverview);
}
async function showChildrenOverview(hideChildrenOverview) {
if (hideChildrenOverview) {
$childrenOverview.hide();
return;
}
const note = getCurrentNote();
$childrenOverview.empty();
const notePath = treeService.getCurrentNotePath();
for (const childBranch of await note.getChildBranches()) {
const link = $('<a>', {
href: 'javascript:',
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
}).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
const childEl = $('<div class="child-overview">').html(link);
$childrenOverview.append(childEl);
}
$childrenOverview.show();
}
async function loadLabelList() {
@@ -202,6 +224,8 @@ async function loadLabelList() {
else {
$labelList.hide();
}
return labels;
}
async function loadNote(noteId) {
@@ -245,8 +269,6 @@ setInterval(saveNoteIfChanged, 5000);
export default {
reload,
switchToNote,
updateNoteFromInputs,
saveNoteToServer,
setNoteBackgroundIfProtected,
loadNote,
getCurrentNote,
@@ -255,6 +277,7 @@ export default {
newNoteCreated,
focus,
loadLabelList,
saveNote,
saveNoteIfChanged,
noteChanged
};

View File

@@ -1,4 +1,3 @@
import utils from "./utils.js";
import libraryLoader from "./library_loader.js";
import bundleService from "./bundle.js";
import infoService from "./info.js";
@@ -11,15 +10,19 @@ const $noteDetailCode = $('#note-detail-code');
const $executeScriptButton = $("#execute-script-button");
async function show() {
if (!codeEditor) {
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
if (!codeEditor) {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
// these conflict with backward/forward navigation shortcuts
delete CodeMirror.keyMap.default["Alt-Left"];
delete CodeMirror.keyMap.default["Alt-Right"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
codeEditor = CodeMirror($noteDetailCode[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
@@ -28,7 +31,8 @@ async function show() {
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true
lineNumbers: true,
tabindex: 2 // so that tab from title will lead to code editor focus
});
codeEditor.on('change', noteDetailService.noteChanged);
@@ -38,7 +42,7 @@ async function show() {
const currentNote = noteDetailService.getCurrentNote();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
// this needs to happen after the element is shown, otherwise the editor won't be refreshed
codeEditor.setValue(currentNote.content);
const info = CodeMirror.findModeByMIME(currentNote.mime);
@@ -67,13 +71,13 @@ async function executeCurrentNote() {
const currentNote = noteDetailService.getCurrentNote();
if (currentNote.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
bundleService.executeBundle(bundle);
}
if (currentNote.mime.endsWith("env=backend")) {
await server.post('script/run/' + getCurrentNoteId());
await server.post('script/run/' + noteDetailService.getCurrentNoteId());
}
infoService.showMessage("Note executed");

View File

@@ -11,11 +11,10 @@ async function show() {
textEditor = await BalloonEditor.create($noteDetailText[0], {});
textEditor.document.on('change', noteDetailService.noteChanged);
textEditor.model.document.on('change', noteDetailService.noteChanged);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
textEditor.setData(noteDetailService.getCurrentNote().content || "<p></p>");
textEditor.setData(noteDetailService.getCurrentNote().content);
$noteDetailText.show();
}

View File

@@ -1,5 +1,5 @@
import treeService from './tree.js';
import noteDetail from './note_detail.js';
import noteDetailService from './note_detail.js';
import server from './server.js';
import infoService from "./info.js";
@@ -84,13 +84,13 @@ function NoteTypeModel() {
};
async function save() {
const note = noteDetail.getCurrentNote();
const note = noteDetailService.getCurrentNote();
await server.put('notes/' + note.noteId
+ '/type/' + encodeURIComponent(self.type())
+ '/mime/' + encodeURIComponent(self.mime()));
await noteDetail.reload();
await noteDetailService.reload();
// for the note icon to be updated in the tree
await treeService.reload();

View File

@@ -1,5 +1,5 @@
import treeService from './tree.js';
import noteDetail from './note_detail.js';
import noteDetailService from './note_detail.js';
import utils from './utils.js';
import server from './server.js';
import protectedSessionHolder from './protected_session_holder.js';
@@ -57,7 +57,7 @@ async function setupProtectedSession() {
$dialog.dialog("close");
noteDetail.reload();
noteDetailService.reload();
treeService.reload();
if (protectedSessionDeferred !== null) {
@@ -90,33 +90,27 @@ async function enterProtectedSession(password) {
async function protectNoteAndSendToServer() {
await ensureProtectedSession(true, true);
const note = noteDetail.getCurrentNote();
noteDetail.updateNoteFromInputs(note);
const note = noteDetailService.getCurrentNote();
note.isProtected = true;
await noteDetail.saveNoteToServer(note);
await noteDetailService.saveNote(note);
treeService.setProtected(note.noteId, note.isProtected);
noteDetail.setNoteBackgroundIfProtected(note);
noteDetailService.setNoteBackgroundIfProtected(note);
}
async function unprotectNoteAndSendToServer() {
await ensureProtectedSession(true, true);
const note = noteDetail.getCurrentNote();
noteDetail.updateNoteFromInputs(note);
const note = noteDetailService.getCurrentNote();
note.isProtected = false;
await noteDetail.saveNoteToServer(note);
await noteDetailService.saveNote(note);
treeService.setProtected(note.noteId, note.isProtected);
noteDetail.setNoteBackgroundIfProtected(note);
noteDetailService.setNoteBackgroundIfProtected(note);
}
async function protectBranch(noteId, protect) {
@@ -127,7 +121,7 @@ async function protectBranch(noteId, protect) {
infoService.showMessage("Request to un/protect sub tree has finished successfully");
treeService.reload();
noteDetail.reload();
noteDetailService.reload();
}
$passwordForm.submit(() => {

View File

@@ -1,6 +1,7 @@
import treeService from './tree.js';
import server from './server.js';
import utils from './utils.js';
import infoService from './info.js';
function ScriptApi(startNote, currentNote) {
const $pluginButtons = $("#plugin-buttons");
@@ -54,7 +55,11 @@ function ScriptApi(startNote, currentNote) {
activateNote,
getInstanceName: () => window.glob.instanceName,
runOnServer,
formatDateISO: utils.formatDateISO
formatDateISO: utils.formatDateISO,
parseDate: utils.parseDate,
showMessage: infoService.showMessage,
showError: infoService.showError,
reloadTree: treeService.reload
}
}

View File

@@ -1,4 +1,4 @@
import utils from './utils.js';
import server from './server.js';
import infoService from "./info.js";
async function syncNow() {
@@ -19,7 +19,7 @@ async function syncNow() {
$("#sync-now-button").click(syncNow);
async function forceNoteSync(noteId) {
const result = await server.post('sync/force-note-sync/' + noteId);
await server.post('sync/force-note-sync/' + noteId);
infoService.showMessage("Note added to sync queue.");
}

View File

@@ -203,7 +203,7 @@ async function showParentList(noteId, node) {
item = $("<span/>").attr("title", "Current note").append(title);
}
else {
item = linkService.createNoteLink(notePath, title);
item = await linkService.createNoteLink(notePath, title);
}
$parentListList.append($("<li/>").append(item));
@@ -285,15 +285,15 @@ async function treeInitialized() {
}
}
function initFancyTree(branch) {
utils.assertArguments(branch);
function initFancyTree(tree) {
utils.assertArguments(tree);
$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"],
source: branch,
scrollParent: $("#tree"),
source: tree,
scrollParent: $tree,
click: (event, data) => {
const targetType = data.targetType;
const node = data.node;
@@ -375,7 +375,7 @@ async function loadTree() {
startNotePath = getNotePathFromAddress();
}
return await treeBuilder.prepareTree(resp.notes, resp.branches);
return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.relations);
}
function collapseTree(node = null) {
@@ -541,9 +541,9 @@ $(window).bind('hashchange', function() {
});
utils.bindShortcut('alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
$collapseTreeButton.click(() => collapseTree());
$createTopLevelNoteButton.click(createNewTopLevelNote);
$collapseTreeButton.click(collapseTree);
$scrollToCurrentNoteButton.click(scrollToCurrentNote);
export default {

View File

@@ -5,10 +5,10 @@ import server from "./server.js";
import treeCache from "./tree_cache.js";
import messagingService from "./messaging.js";
async function prepareTree(noteRows, branchRows) {
utils.assertArguments(noteRows);
async function prepareTree(noteRows, branchRows, relations) {
utils.assertArguments(noteRows, branchRows, relations);
treeCache.load(noteRows, branchRows);
treeCache.load(noteRows, branchRows, relations);
return await prepareRealBranch(await treeCache.getNote('root'));
}
@@ -49,9 +49,7 @@ async function prepareRealBranch(parentNote) {
expanded: note.type !== 'search' && branch.isExpanded
};
const hasChildren = (await note.getChildNotes()).length > 0;
if (hasChildren || note.type === 'search') {
if (note.hasChildren() || note.type === 'search') {
node.folder = true;
if (node.expanded && note.type !== 'search') {
@@ -96,7 +94,7 @@ async function getExtraClasses(note) {
extraClasses.push("protected");
}
if ((await note.getParentNotes()).length > 1) {
if (note.getParentNoteIds().length > 1) {
extraClasses.push("multiple-parents");
}

View File

@@ -2,45 +2,85 @@ import utils from "./utils.js";
import Branch from "../entities/branch.js";
import NoteShort from "../entities/note_short.js";
import infoService from "./info.js";
import server from "./server.js";
class TreeCache {
load(noteRows, branchRows) {
this.parents = [];
this.children = [];
load(noteRows, branchRows, relations) {
this.parents = {};
this.children = {};
this.childParentToBranch = {};
/** @type {Object.<string, NoteShort>} */
this.notes = {};
/** @type {Object.<string, Branch>} */
this.branches = {};
this.addResp(noteRows, branchRows, relations);
}
addResp(noteRows, branchRows, relations) {
for (const noteRow of noteRows) {
const note = new NoteShort(this, noteRow);
this.notes[note.noteId] = note;
}
/** @type {Object.<string, Branch>} */
this.branches = {};
for (const branchRow of branchRows) {
const branch = new Branch(this, branchRow);
this.addBranch(branch);
}
for (const relation of relations) {
this.addBranchRelationship(relation.branchId, relation.childNoteId, relation.parentNoteId);
}
}
async getNotes(noteIds) {
const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
if (missingNoteIds.length > 0) {
const resp = await server.post('tree/load', { noteIds: missingNoteIds });
this.addResp(resp.notes, resp.branches, resp.relations);
}
return noteIds.map(noteId => {
if (!this.notes[noteId]) {
throw new Error(`Can't find note ${noteId}`);
}
else {
return this.notes[noteId];
}
});
}
/** @return NoteShort */
async getNote(noteId) {
return this.notes[noteId];
return (await this.getNotes([noteId]))[0];
}
addBranch(branch) {
this.branches[branch.branchId] = branch;
this.parents[branch.noteId] = this.parents[branch.noteId] || [];
this.parents[branch.noteId].push(this.notes[branch.parentNoteId]);
this.addBranchRelationship(branch.branchId, branch.noteId, branch.parentNoteId);
}
this.children[branch.parentNoteId] = this.children[branch.parentNoteId] || [];
this.children[branch.parentNoteId].push(this.notes[branch.noteId]);
addBranchRelationship(branchId, childNoteId, parentNoteId) {
this.childParentToBranch[childNoteId + '-' + parentNoteId] = branchId;
this.childParentToBranch[branch.noteId + '-' + branch.parentNoteId] = branch;
this.parents[childNoteId] = this.parents[childNoteId] || [];
if (!this.parents[childNoteId].includes(parentNoteId)) {
this.parents[childNoteId].push(parentNoteId);
}
this.children[parentNoteId] = this.children[parentNoteId] || [];
if (!this.children[parentNoteId].includes(childNoteId)) {
this.children[parentNoteId].push(childNoteId);
}
}
add(note, branch) {
@@ -49,21 +89,46 @@ class TreeCache {
this.addBranch(branch);
}
async getBranches(branchIds) {
const missingBranchIds = branchIds.filter(branchId => this.branches[branchId] === undefined);
if (missingBranchIds.length > 0) {
const resp = await server.post('tree/load', { branchIds: branchIds });
this.addResp(resp.notes, resp.branches, resp.relations);
}
return branchIds.map(branchId => {
if (!this.branches[branchId]) {
throw new Error(`Can't find branch ${branchId}`);
}
else {
return this.branches[branchId];
}
});
}
/** @return Branch */
async getBranch(branchId) {
return this.branches[branchId];
return (await this.getBranches([branchId]))[0];
}
/** @return Branch */
async getBranchByChildParent(childNoteId, parentNoteId) {
const key = (childNoteId + '-' + parentNoteId);
const branch = this.childParentToBranch[key];
const branchId = this.getBranchIdByChildParent(childNoteId, parentNoteId);
if (!branch) {
return await this.getBranch(branchId);
}
getBranchIdByChildParent(childNoteId, parentNoteId) {
const key = childNoteId + '-' + parentNoteId;
const branchId = this.childParentToBranch[key];
if (!branchId) {
infoService.throwError("Cannot find branch for child-parent=" + key);
}
return branch;
return branchId;
}
/* Move note from one parent to another. */
@@ -78,33 +143,14 @@ class TreeCache {
delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId
// remove old associations
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== oldParentNoteId);
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch.noteId !== childNoteId);
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p !== oldParentNoteId);
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch !== childNoteId);
// add new associations
treeCache.parents[childNoteId].push(await treeCache.getNote(newParentNoteId));
treeCache.parents[childNoteId].push(newParentNoteId);
treeCache.children[newParentNoteId] = treeCache.children[newParentNoteId] || []; // this might be first child
treeCache.children[newParentNoteId].push(await treeCache.getNote(childNoteId));
}
removeParentChildRelation(parentNoteId, childNoteId) {
utils.assertArguments(parentNoteId, childNoteId);
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== parentNoteId);
treeCache.children[parentNoteId] = treeCache.children[parentNoteId].filter(ch => ch.noteId !== childNoteId);
delete treeCache.childParentToBranch[childNoteId + '-' + parentNoteId];
}
async setParentChildRelation(branchId, parentNoteId, childNoteId) {
treeCache.parents[childNoteId] = treeCache.parents[childNoteId] || [];
treeCache.parents[childNoteId].push(await treeCache.getNote(parentNoteId));
treeCache.children[parentNoteId] = treeCache.children[parentNoteId] || [];
treeCache.children[parentNoteId].push(await treeCache.getNote(childNoteId));
treeCache.childParentToBranch[childNoteId + '-' + parentNoteId] = await treeCache.getBranch(branchId);
treeCache.children[newParentNoteId].push(childNoteId);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -12,9 +12,40 @@
/*------------------------------------------------------------------------------
* Helpers
*----------------------------------------------------------------------------*/
.ui-helper-hidden {
.fancytree-helper-hidden {
display: none;
}
.fancytree-helper-indeterminate-cb {
color: #777;
}
.fancytree-helper-disabled {
color: #c0c0c0;
}
/* Helper to allow spinning loader icon with glyph-, ligature-, and SVG-icons. */
.fancytree-helper-spin {
-webkit-animation: spin 1000ms infinite linear;
animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
/*------------------------------------------------------------------------------
* Container and UL / LI
*----------------------------------------------------------------------------*/
@@ -338,6 +369,16 @@ span.fancytree-node.fancytree-error span.fancytree-title {
/*------------------------------------------------------------------------------
* Drag'n'drop support
*----------------------------------------------------------------------------*/
/* ext-dnd5: */
span.fancytree-childcounter {
color: #fff;
background: #337ab7;
border: 1px solid gray;
border-radius: 10px;
padding: 2px;
text-align: center;
}
/* ext-dnd: */
div.fancytree-drag-helper span.fancytree-childcounter,
div.fancytree-drag-helper span.fancytree-dnd-modifier {
display: inline-block;
@@ -402,8 +443,7 @@ span.fancytree-drag-source.fancytree-drag-remove {
.fancytree-container.fancytree-rtl span.fancytree-connector,
.fancytree-container.fancytree-rtl span.fancytree-expander,
.fancytree-container.fancytree-rtl span.fancytree-icon,
.fancytree-container.fancytree-rtl span.fancytree-drag-helper-img,
.fancytree-container.fancytree-rtl #fancytree-drop-marker {
.fancytree-container.fancytree-rtl span.fancytree-drag-helper-img {
background-image: url("../skin-win8/icons-rtl.gif");
}
.fancytree-container.fancytree-rtl .fancytree-exp-n span.fancytree-expander,
@@ -425,6 +465,9 @@ ul.fancytree-container.fancytree-rtl li.fancytree-lastsib,
ul.fancytree-container.fancytree-rtl.fancytree-no-connector > li {
background-image: none;
}
#fancytree-drop-marker.fancytree-rtl {
background-image: url("../skin-win8/icons-rtl.gif");
}
/*------------------------------------------------------------------------------
* 'table' extension
*----------------------------------------------------------------------------*/
@@ -482,7 +525,7 @@ table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:h
* 'filter' extension
*----------------------------------------------------------------------------*/
.fancytree-ext-filter-dimm span.fancytree-node span.fancytree-title {
color: silver;
color: #c0c0c0;
font-weight: lighter;
}
.fancytree-ext-filter-dimm tr.fancytree-submatch span.fancytree-title,
@@ -501,7 +544,7 @@ table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:h
}
.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title,
.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title {
color: silver;
color: #c0c0c0;
font-weight: lighter;
}
.fancytree-ext-filter-hide tr.fancytree-match span.fancytree-title,

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,12 @@
display: grid;
grid-template-areas: "header header"
"tree-actions title"
"search note-content"
"tree note-content"
"parent-list note-content"
"parent-list label-list";
grid-template-columns: 2fr 5fr;
"left-pane title"
"left-pane note-detail";
grid-template-columns: 29% 70%;
grid-template-rows: auto
auto
auto
1fr
auto
auto;
1fr;
justify-content: center;
grid-gap: 10px;
@@ -28,6 +22,23 @@
align-items: center;
}
#note-detail-wrapper {
position: relative;
overflow: hidden;
grid-area: note-detail;
padding-left: 10px;
padding-top: 10px;
display: flex;
flex-direction: column;
}
#note-detail-component-wrapper {
flex-grow: 100;
position: relative;
overflow: auto;
flex-basis: content;
}
.note-detail-component {
display: none;
}
@@ -36,7 +47,7 @@
border: 0 !important;
box-shadow: none !important;
/* This is because with empty content height of editor is 0 and it's impossible to click into it */
min-height: 400px;
min-height: 200px;
overflow: auto;
}
@@ -50,8 +61,6 @@
}
ul.fancytree-container {
overflow: auto;
position: relative;
outline: none !important;
}
@@ -150,12 +159,21 @@ div.ui-tooltip {
width: auto;
}
#tree {
overflow: auto;
flex-grow: 100;
flex-shrink: 100;
margin-top: 10px;
}
#parent-list {
display: none;
margin-left: 20px;
border-top: 2px solid #eee;
padding-top: 10px;
grid-area: parent-list;
max-height: 300px;
overflow: auto;
}
#parent-list ul {
@@ -249,11 +267,10 @@ div.ui-tooltip {
}
#note-detail-code {
height: 100%;
min-height: 200px;
}
.CodeMirror {
height: 100%;
font-family: "Liberation Mono", "Lucida Console", monospace;
}
@@ -274,7 +291,6 @@ div.ui-tooltip {
.cm-matchhighlight {background-color: #eeeeee}
#label-list {
grid-area: label-list;
color: #777777;
border-top: 1px solid #eee;
padding: 5px; display: none;
@@ -288,4 +304,47 @@ div.ui-tooltip {
#file-table th, #file-table td {
padding: 10px;
font-size: large;
}
#children-overview {
flex-grow: 1000;
flex-shrink: 1000;
flex-basis: 0px;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
height: 100px;
overflow: auto;
}
.child-overview {
font-weight: bold;
font-size: large;
padding: 10px;
background: #f4f4f4;
width: 150px;
height: 90px;
line-height: 2em;
margin-right: 20px;
border-radius: 15px;
overflow: hidden;
text-align: center;
margin-top: 15px;
text-overflow: ellipsis;
word-wrap: break-word;
}
.child-overview a {
color: #444;
}
#sql-console-query {
height: 150px;
width: 100%;
border: 1px solid #ccc;
margin-bottom: 10px;
}
#sql-console-query .CodeMirror {
height: 150px;
}

View File

@@ -0,0 +1,20 @@
"use strict";
const autocompleteService = require('../../services/autocomplete');
async function getAutocomplete(req) {
const query = req.query.query;
const results = autocompleteService.getResults(query);
return results.map(res => {
return {
value: res.title + ' (' + res.path + ')',
title: res.title
}
});
}
module.exports = {
getAutocomplete
};

View File

@@ -13,7 +13,68 @@ async function exportNote(req, res) {
const pack = tar.pack();
const name = await exportNoteInner(branchId, '', pack);
const exportedNoteIds = [];
const name = await exportNoteInner(branchId, '');
async function exportNoteInner(branchId, directory) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
const childFileName = directory + sanitize(note.title);
if (exportedNoteIds.includes(note.noteId)) {
saveMetadataFile(childFileName, {
version: 1,
clone: true,
noteId: note.noteId,
prefix: branch.prefix
});
return;
}
const metadata = {
version: 1,
clone: false,
noteId: note.noteId,
title: note.title,
prefix: branch.prefix,
type: note.type,
mime: note.mime,
labels: (await note.getLabels()).map(label => {
return {
name: label.name,
value: label.value
};
})
};
if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
return;
}
saveMetadataFile(childFileName, metadata);
saveDataFile(childFileName, note);
exportedNoteIds.push(note.noteId);
for (const child of await note.getChildBranches()) {
await exportNoteInner(child.branchId, childFileName + "/");
}
return childFileName;
}
function saveDataFile(childFileName, note) {
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({name: childFileName + ".dat", size: content.length}, content);
}
function saveMetadataFile(childFileName, metadata) {
const metadataJson = JSON.stringify(metadata, null, '\t');
pack.entry({name: childFileName + ".meta", size: metadataJson.length}, metadataJson);
}
pack.finalize();
@@ -23,51 +84,6 @@ async function exportNote(req, res) {
pack.pipe(res);
}
async function exportNoteInner(branchId, directory, pack) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
if (note.isProtected) {
return;
}
const metadata = await getMetadata(note);
if (metadata.labels.find(label => label.name === 'excludeFromExport')) {
return;
}
const metadataJson = JSON.stringify(metadata, null, '\t');
const childFileName = directory + sanitize(note.title);
pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson);
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({ name: childFileName + ".dat", size: content.length }, content);
for (const child of await note.getChildBranches()) {
await exportNoteInner(child.branchId, childFileName + "/", pack);
}
return childFileName;
}
async function getMetadata(note) {
return {
version: 1,
title: note.title,
type: note.type,
mime: note.mime,
labels: (await note.getLabels()).map(label => {
return {
name: label.name,
value: label.value
};
})
};
}
module.exports = {
exportNote
};

View File

@@ -3,6 +3,7 @@
const repository = require('../../services/repository');
const labelService = require('../../services/labels');
const noteService = require('../../services/notes');
const Branch = require('../../entities/branch');
const tar = require('tar-stream');
const stream = require('stream');
const path = require('path');
@@ -31,7 +32,7 @@ async function parseImportFile(file) {
const extract = tar.extract();
extract.on('entry', function(header, stream, next) {
let {name, key} = getFileName(header.name);
const {name, key} = getFileName(header.name);
let file = fileMap[name];
@@ -97,30 +98,46 @@ async function importTar(req) {
const files = await parseImportFile(file);
await importNotes(files, parentNoteId);
// maps from original noteId (in tar file) to newly generated noteId
const noteIdMap = {};
await importNotes(files, parentNoteId, noteIdMap);
}
async function importNotes(files, parentNoteId) {
async function importNotes(files, parentNoteId, noteIdMap) {
for (const file of files) {
if (file.meta.version !== 1) {
throw new Error("Can't read meta data version " + file.meta.version);
}
if (file.meta.clone) {
await new Branch({
parentNoteId: parentNoteId,
noteId: noteIdMap[file.meta.noteId],
prefix: file.meta.prefix
}).save();
return;
}
if (file.meta.type !== 'file') {
file.data = file.data.toString("UTF-8");
}
const {note} = await noteService.createNote(parentNoteId, file.meta.title, file.data, {
type: file.meta.type,
mime: file.meta.mime
mime: file.meta.mime,
prefix: file.meta.prefix
});
noteIdMap[file.meta.noteId] = note.noteId;
for (const label of file.meta.labels) {
await labelService.createLabel(note.noteId, label.name, label.value);
}
if (file.children.length > 0) {
await importNotes(file.children, note.noteId);
await importNotes(file.children, note.noteId, noteIdMap);
}
}
}

View File

@@ -7,6 +7,8 @@ const sourceIdService = require('../../services/source_id');
const passwordEncryptionService = require('../../services/password_encryption');
const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const cls = require('../../services/cls');
async function loginSync(req) {
const timestampStr = req.body.timestamp;
@@ -53,7 +55,12 @@ async function loginToProtectedSession(req) {
const decryptedDataKey = await passwordEncryptionService.getDataKey(password);
const protectedSessionId = protectedSessionService.setDataKey(req, decryptedDataKey);
const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey);
// this is set here so that event handlers have access to the protected session
cls.namespace.set('protectedSessionId', protectedSessionId);
eventService.emit(eventService.ENTER_PROTECTED_SESSION);
return {
success: true,

View File

@@ -47,7 +47,7 @@ async function sortNotes(req) {
async function protectBranch(req) {
const noteId = req.params.noteId;
const note = repository.getNote(noteId);
const note = await repository.getNote(noteId);
const protect = !!parseInt(req.params.isProtected);
await noteService.protectNoteRecursively(note, protect);

View File

@@ -36,9 +36,9 @@ async function uploadImage(req) {
return [400, "Unknown image type: " + file.mimetype];
}
const parentNoteId = await dateNoteService.getDateNoteId(req.headers['x-local-date']);
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
const {note} = await noteService.createNewNote(parentNoteId, {
const {note} = await noteService.createNewNote(parentNote.noteId, {
title: "Sender image",
content: "",
target: 'into',
@@ -57,9 +57,9 @@ async function uploadImage(req) {
}
async function saveNote(req) {
const parentNoteId = await dateNoteService.getDateNoteId(req.headers['x-local-date']);
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
await noteService.createNewNote(parentNoteId, {
await noteService.createNewNote(parentNote.noteId, {
title: req.body.title,
content: req.body.content,
target: 'into',

View File

@@ -10,8 +10,8 @@ const log = require('../../services/log');
async function checkSync() {
return {
'hashes': await contentHashService.getHashes(),
'max_sync_id': await sql.getValue('SELECT MAX(id) FROM sync')
hashes: await contentHashService.getHashes(),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync')
};
}
@@ -55,129 +55,21 @@ async function forceNoteSync(req) {
syncService.sync();
}
async function getChanged() {
async function getChanged(req) {
const lastSyncId = parseInt(req.query.lastSyncId);
return await sql.getRows("SELECT * FROM sync WHERE id > ?", [lastSyncId]);
const syncs = await sql.getRows("SELECT * FROM sync WHERE id > ? LIMIT 1000", [lastSyncId]);
return await syncService.getSyncRecords(syncs);
}
async function getNote(req) {
const noteId = req.params.noteId;
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
async function update(req) {
const sourceId = req.body.sourceId;
const entities = req.body.entities;
syncService.serializeNoteContentBuffer(entity);
return {
entity: entity
};
}
async function getBranch(req) {
const branchId = req.params.branchId;
return await sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]);
}
async function getNoteRevision(req) {
const noteRevisionId = req.params.noteRevisionId;
return await sql.getRow("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [noteRevisionId]);
}
async function getOption(req) {
const name = req.params.name;
const opt = await sql.getRow("SELECT * FROM options WHERE name = ?", [name]);
if (!opt.isSynced) {
return [400, "This option can't be synced."];
for (const {sync, entity} of entities) {
await syncUpdateService.updateEntity(sync, entity, sourceId);
}
else {
return opt;
}
}
async function getNoteReordering(req) {
const parentNoteId = req.params.parentNoteId;
return {
parentNoteId: parentNoteId,
ordering: await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId])
};
}
async function getRecentNote(req) {
const branchId = req.params.branchId;
return await sql.getRow("SELECT * FROM recent_notes WHERE branchId = ?", [branchId]);
}
async function getImage(req) {
const imageId = req.params.imageId;
const entity = await sql.getRow("SELECT * FROM images WHERE imageId = ?", [imageId]);
if (entity && entity.data !== null) {
entity.data = entity.data.toString('base64');
}
return entity;
}
async function getNoteImage(req) {
const noteImageId = req.params.noteImageId;
return await sql.getRow("SELECT * FROM note_images WHERE noteImageId = ?", [noteImageId]);
}
async function getLabel(req) {
const labelId = req.params.labelId;
return await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [labelId]);
}
async function getApiToken(req) {
const apiTokenId = req.params.apiTokenId;
return await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [apiTokenId]);
}
async function updateNote(req) {
await syncUpdateService.updateNote(req.body.entity, req.body.sourceId);
}
async function updateBranch(req) {
await syncUpdateService.updateBranch(req.body.entity, req.body.sourceId);
}
async function updateNoteRevision(req) {
await syncUpdateService.updateNoteRevision(req.body.entity, req.body.sourceId);
}
async function updateNoteReordering(req) {
await syncUpdateService.updateNoteReordering(req.body.entity, req.body.sourceId);
}
async function updateOption(req) {
await syncUpdateService.updateOptions(req.body.entity, req.body.sourceId);
}
async function updateRecentNote(req) {
await syncUpdateService.updateRecentNotes(req.body.entity, req.body.sourceId);
}
async function updateImage(req) {
await syncUpdateService.updateImage(req.body.entity, req.body.sourceId);
}
async function updateNoteImage(req) {
await syncUpdateService.updateNoteImage(req.body.entity, req.body.sourceId);
}
async function updateLabel(req) {
await syncUpdateService.updateLabel(req.body.entity, req.body.sourceId);
}
async function updateApiToken(req) {
await syncUpdateService.updateApiToken(req.body.entity, req.body.sourceId);
}
module.exports = {
@@ -187,24 +79,5 @@ module.exports = {
forceFullSync,
forceNoteSync,
getChanged,
getNote,
getBranch,
getImage,
getNoteImage,
getNoteReordering,
getNoteRevision,
getRecentNote,
getOption,
getLabel,
getApiToken,
updateNote,
updateBranch,
updateImage,
updateNoteImage,
updateNoteReordering,
updateNoteRevision,
updateRecentNote,
updateOption,
updateLabel,
updateApiToken
update
};

View File

@@ -4,58 +4,79 @@ const sql = require('../../services/sql');
const optionService = require('../../services/options');
const protectedSessionService = require('../../services/protected_session');
async function getTree() {
const branches = await sql.getRows(`
SELECT
branchId,
noteId,
parentNoteId,
notePosition,
prefix,
isExpanded
FROM
branches
WHERE
isDeleted = 0
ORDER BY
notePosition`);
async function getNotes(noteIds) {
const questionMarks = noteIds.map(() => "?").join(",");
const notes = [{
noteId: 'root',
title: 'root',
isProtected: false,
type: 'none',
mime: 'none'
}].concat(await sql.getRows(`
SELECT
notes.noteId,
notes.title,
notes.isProtected,
notes.type,
notes.mime,
hideInAutocomplete.labelId AS 'hideInAutocomplete'
FROM
notes
LEFT JOIN labels AS hideInAutocomplete ON hideInAutocomplete.noteId = notes.noteId
AND hideInAutocomplete.name = 'hideInAutocomplete'
AND hideInAutocomplete.isDeleted = 0
WHERE
notes.isDeleted = 0`));
const notes = await sql.getRows(`
SELECT noteId, title, isProtected, type, mime
FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
protectedSessionService.decryptNotes(notes);
notes.forEach(note => {
note.hideInAutocomplete = !!note.hideInAutocomplete;
note.isProtected = !!note.isProtected;
});
notes.forEach(note => note.isProtected = !!note.isProtected);
return notes;
}
async function getRelations(noteIds) {
const questionMarks = noteIds.map(() => "?").join(",");
const doubledNoteIds = noteIds.concat(noteIds);
return await sql.getRows(`SELECT branchId, noteId AS 'childNoteId', parentNoteId FROM branches WHERE isDeleted = 0
AND (parentNoteId IN (${questionMarks}) OR noteId IN (${questionMarks}))`, doubledNoteIds);
}
async function getTree() {
// we fetch all branches of notes, even if that particular branch isn't visible
// this allows us to e.g. detect and properly display clones
const branches = await sql.getRows(`
WITH RECURSIVE
tree(branchId, noteId, isExpanded) AS (
SELECT branchId, noteId, isExpanded FROM branches WHERE branchId = 'root'
UNION ALL
SELECT branches.branchId, branches.noteId, branches.isExpanded FROM branches
JOIN tree ON branches.parentNoteId = tree.noteId
WHERE tree.isExpanded = 1 AND branches.isDeleted = 0
)
SELECT branches.* FROM tree JOIN branches USING(noteId) ORDER BY branches.notePosition`);
const noteIds = branches.map(b => b.noteId);
const notes = await getNotes(noteIds);
const relations = await getRelations(noteIds);
return {
startNotePath: await optionService.getOption('startNotePath'),
branches: branches,
notes: notes
branches,
notes,
relations
};
}
async function load(req) {
let noteIds = req.body.noteIds;
const branchIds = req.body.branchIds;
if (branchIds && branchIds.length > 0) {
noteIds = await sql.getColumn(`SELECT noteId FROM branches WHERE isDeleted = 0 AND branchId IN(${branchIds.map(() => "?").join(",")})`, branchIds);
}
const questionMarks = noteIds.map(() => "?").join(",");
const branches = await sql.getRows(`SELECT * FROM branches WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
const notes = await getNotes(noteIds);
const relations = await getRelations(noteIds);
return {
branches,
notes,
relations
};
}
module.exports = {
getTree
getTree,
load
};

View File

@@ -8,6 +8,7 @@ const multer = require('multer')();
const treeApiRoute = require('./api/tree');
const notesApiRoute = require('./api/notes');
const branchesApiRoute = require('./api/branches');
const autocompleteApiRoute = require('./api/autocomplete');
const cloningApiRoute = require('./api/cloning');
const noteRevisionsApiRoute = require('./api/note_revisions');
const recentChangesApiRoute = require('./api/recent_changes');
@@ -70,7 +71,7 @@ function route(method, path, middleware, routeHandler, resultHandler) {
cls.namespace.set('sourceId', req.headers.source_id);
protectedSessionService.setProtectedSessionId(req);
return await sql.doInTransaction(async () => {
return await sql.transactional(async () => {
return await routeHandler(req, res, next);
});
});
@@ -99,6 +100,7 @@ function register(app) {
route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
apiRoute(POST, '/api/tree/load', treeApiRoute.load);
apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix);
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentNoteId', branchesApiRoute.moveBranchToParent);
@@ -107,6 +109,8 @@ function register(app) {
apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch);
apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete);
apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote);
apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote);
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
@@ -147,26 +151,7 @@ function register(app) {
apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync);
apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync);
apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged);
apiRoute(GET, '/api/sync/notes/:noteId', syncApiRoute.getNote);
apiRoute(GET, '/api/sync/branches/:branchId', syncApiRoute.getBranch);
apiRoute(GET, '/api/sync/note_revisions/:noteRevisionId', syncApiRoute.getNoteRevision);
apiRoute(GET, '/api/sync/options/:name', syncApiRoute.getOption);
apiRoute(GET, '/api/sync/note_reordering/:parentNoteId', syncApiRoute.getNoteReordering);
apiRoute(GET, '/api/sync/recent_notes/:branchId', syncApiRoute.getRecentNote);
apiRoute(GET, '/api/sync/images/:imageId', syncApiRoute.getImage);
apiRoute(GET, '/api/sync/note_images/:noteImageId', syncApiRoute.getNoteImage);
apiRoute(GET, '/api/sync/labels/:labelId', syncApiRoute.getLabel);
apiRoute(GET, '/api/sync/api_tokens/:apiTokenId', syncApiRoute.getApiToken);
apiRoute(PUT, '/api/sync/notes', syncApiRoute.updateNote);
apiRoute(PUT, '/api/sync/branches', syncApiRoute.updateBranch);
apiRoute(PUT, '/api/sync/note_revisions', syncApiRoute.updateNoteRevision);
apiRoute(PUT, '/api/sync/note_reordering', syncApiRoute.updateNoteReordering);
apiRoute(PUT, '/api/sync/options', syncApiRoute.updateOption);
apiRoute(PUT, '/api/sync/recent_notes', syncApiRoute.updateRecentNote);
apiRoute(PUT, '/api/sync/images', syncApiRoute.updateImage);
apiRoute(PUT, '/api/sync/note_images', syncApiRoute.updateNoteImage);
apiRoute(PUT, '/api/sync/labels', syncApiRoute.updateLabel);
apiRoute(PUT, '/api/sync/api_tokens', syncApiRoute.updateApiToken);
apiRoute(PUT, '/api/sync/update', syncApiRoute.update);
apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog);

View File

@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 86;
const APP_DB_VERSION = 93;
module.exports = {
appVersion: packageJson.version,

View File

@@ -0,0 +1,248 @@
const sql = require('./sql');
const sqlInit = require('./sql_init');
const eventService = require('./events');
const repository = require('./repository');
const protectedSessionService = require('./protected_session');
let noteTitles;
let protectedNoteTitles;
let noteIds;
const childToParent = {};
const hideInAutocomplete = {};
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
let prefixes = {};
async function load() {
noteTitles = await sql.getMap(`SELECT noteId, LOWER(title) FROM notes WHERE isDeleted = 0 AND isProtected = 0`);
noteIds = Object.keys(noteTitles);
prefixes = await sql.getMap(`SELECT noteId || '-' || parentNoteId, LOWER(prefix) FROM branches WHERE prefix IS NOT NULL AND prefix != ''`);
const relations = await sql.getRows(`SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
for (const rel of relations) {
childToParent[rel.noteId] = childToParent[rel.noteId] || [];
childToParent[rel.noteId].push(rel.parentNoteId);
}
const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'hideInAutocomplete'`);
for (const noteId of hiddenLabels) {
hideInAutocomplete[noteId] = true;
}
}
function getResults(query) {
if (!noteTitles || query.length <= 2) {
return [];
}
const tokens = query.toLowerCase().split(" ");
const results = [];
let noteIds = Object.keys(noteTitles);
if (protectedSessionService.isProtectedSessionAvailable()) {
noteIds = noteIds.concat(Object.keys(protectedNoteTitles));
}
for (const noteId of noteIds) {
if (hideInAutocomplete[noteId]) {
continue;
}
const parents = childToParent[noteId];
if (!parents) {
continue;
}
for (const parentNoteId of parents) {
const title = getNoteTitle(noteId, parentNoteId);
const foundTokens = [];
for (const token of tokens) {
if (title.includes(token)) {
foundTokens.push(token);
}
}
if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
search(parentNoteId, remainingTokens, [noteId], results);
}
}
}
results.sort((a, b) => a.title < b.title ? -1 : 1);
return results;
}
function search(noteId, tokens, path, results) {
if (tokens.length === 0) {
const retPath = getSomePath(noteId, path);
if (retPath) {
const noteTitle = getNoteTitleForPath(retPath);
results.push({
title: noteTitle,
path: retPath.join('/')
});
}
return;
}
const parents = childToParent[noteId];
if (!parents) {
return;
}
for (const parentNoteId of parents) {
if (results.length >= 200) {
return;
}
if (parentNoteId === 'root' || hideInAutocomplete[parentNoteId]) {
continue;
}
const title = getNoteTitle(noteId, parentNoteId);
const foundTokens = [];
for (const token of tokens) {
if (title.includes(token)) {
foundTokens.push(token);
}
}
if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
search(parentNoteId, remainingTokens, path.concat([noteId]), results);
}
else {
search(parentNoteId, tokens, path.concat([noteId]), results);
}
}
}
function getNoteTitle(noteId, parentNoteId) {
const prefix = prefixes[noteId + '-' + parentNoteId];
let title = noteTitles[noteId];
if (!title) {
if (protectedSessionService.isProtectedSessionAvailable()) {
title = protectedNoteTitles[noteId];
}
else {
title = '[protected]';
}
}
return (prefix ? (prefix + ' - ') : '') + title;
}
function getNoteTitleForPath(path) {
const titles = [];
let parentNoteId = 'root';
for (const noteId of path) {
const title = getNoteTitle(noteId, parentNoteId);
titles.push(title);
parentNoteId = noteId;
}
return titles.join(' / ');
}
function getSomePath(noteId, path) {
if (noteId === 'root') {
path.reverse();
return path;
}
const parents = childToParent[noteId];
if (!parents || parents.length === 0) {
return false;
}
for (const parentNoteId of parents) {
const retPath = getSomePath(parentNoteId, path.concat([noteId]));
if (retPath) {
return retPath;
}
}
return false;
}
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
if (entityName === 'notes') {
const note = await repository.getNote(entityId);
if (note.isDeleted) {
delete noteTitles[note.noteId];
delete childToParent[note.noteId];
}
else {
noteTitles[note.noteId] = note.title;
}
}
else if (entityName === 'branches') {
const branch = await repository.getBranch(entityId);
if (childToParent[branch.noteId]) {
childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId)
}
if (branch.isDeleted) {
delete prefixes[branch.noteId + '-' + branch.parentNoteId];
}
else {
if (branch.prefix) {
prefixes[branch.noteId + '-' + branch.parentNoteId] = branch.prefix;
}
childToParent[branch.noteId] = childToParent[branch.noteId] || [];
childToParent[branch.noteId].push(branch.parentNoteId);
}
}
else if (entityName === 'labels') {
const label = await repository.getLabel(entityId);
if (label.name === 'hideInAutocomplete') {
// we're not using label object directly, since there might be other non-deleted hideInAutocomplete label
const hideLabel = await repository.getEntity(`SELECT * FROM labels WHERE isDeleted = 0
AND name = 'hideInAutocomplete' AND noteId = ?`, [label.noteId]);
if (hideLabel) {
hideInAutocomplete[label.noteId] = true;
}
else {
delete hideInAutocomplete[label.noteId];
}
}
}
});
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
for (const noteId in protectedNoteTitles) {
protectedNoteTitles[noteId] = protectedSessionService.decryptNoteTitle(noteId, protectedNoteTitles[noteId]);
}
});
sqlInit.dbReady.then(load);
module.exports = {
getResults
};

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-01-17T23:59:03-05:00", buildRevision: "651a9fb3272c85d287c16d5a4978464fb7d2490d" };
module.exports = { buildDate:"2018-05-22T23:51:43-04:00", buildRevision: "a372cbb2dfa918084e2d447a01fca6f076ddf486" };

View File

@@ -17,7 +17,7 @@ async function changePassword(currentPassword, newPassword) {
const newPasswordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(newPassword));
const decryptedDataKey = await passwordEncryptionService.getDataKey(currentPassword);
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await passwordEncryptionService.setDataKey(newPassword, decryptedDataKey);
await optionService.setOption('passwordVerificationHash', newPasswordVerificationKey);

View File

@@ -3,6 +3,7 @@
const sql = require('./sql');
const sqlInit = require('./sql_init');
const log = require('./log');
const utils = require('./utils');
const messagingService = require('./messaging');
const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
@@ -116,7 +117,7 @@ async function runAllChecks() {
WHERE
notes.isDeleted = 1
AND branches.isDeleted = 0`,
"Note tree is not deleted even though main note is deleted for following branch IDs", errorList);
"Branch is not deleted even though main note is deleted for following branch IDs", errorList);
await runCheck(`
SELECT
@@ -125,12 +126,12 @@ async function runAllChecks() {
branches AS child
WHERE
child.isDeleted = 0
AND child.parentNoteId != 'root'
AND child.parentNoteId != 'none'
AND (SELECT COUNT(*) FROM branches AS parent WHERE parent.noteId = child.parentNoteId
AND parent.isDeleted = 0) = 0`,
"All parent branches are deleted but child note tree is not for these child note tree IDs", errorList);
"All parent branches are deleted but child branch is not for these child branch IDs", errorList);
// we do extra JOIN to eliminate orphan notes without note tree (which are reported separately)
// we do extra JOIN to eliminate orphan notes without branches (which are reported separately)
await runCheck(`
SELECT
DISTINCT noteId
@@ -150,7 +151,7 @@ async function runAllChecks() {
LEFT JOIN branches AS parent ON parent.noteId = child.parentNoteId
WHERE
parent.noteId IS NULL
AND child.parentNoteId != 'root'`,
AND child.parentNoteId != 'none'`,
"Not existing parent in the following parent > child relations", errorList);
await runCheck(`
@@ -197,16 +198,6 @@ async function runAllChecks() {
AND images.isDeleted = 1`,
"Note image is not deleted while image is deleted for noteImageId", errorList);
await runCheck(`
SELECT
noteId
FROM
notes
WHERE
isDeleted = 0
AND (title IS NULL OR content IS NULL)`,
"Note has null title or text", errorList);
await runCheck(`
SELECT
noteId

View File

@@ -1,117 +1,44 @@
"use strict";
const sql = require('./sql');
const utils = require('./utils');
const log = require('./log');
const eventLogService = require('./event_log');
const messagingService = require('./messaging');
const ApiToken = require('../entities/api_token');
const Branch = require('../entities/branch');
const Image = require('../entities/image');
const Note = require('../entities/note');
const NoteImage = require('../entities/note_image');
const Label = require('../entities/label');
const NoteRevision = require('../entities/note_revision');
const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option');
function getHash(rows) {
let hash = '';
async function getHash(entityConstructor, whereBranch) {
let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`);
for (const row of rows) {
hash = utils.hash(hash + JSON.stringify(row));
if (!contentToHash) { // might be null in case of no rows
contentToHash = "";
}
return hash;
return utils.hash(contentToHash);
}
async function getHashes() {
const startTime = new Date();
const hashes = {
notes: getHash(await sql.getRows(`
SELECT
noteId,
title,
content,
type,
dateModified,
isProtected,
isDeleted
FROM notes
ORDER BY noteId`)),
branches: getHash(await sql.getRows(`
SELECT
branchId,
noteId,
parentNoteId,
notePosition,
dateModified,
isDeleted,
prefix
FROM branches
ORDER BY branchId`)),
note_revisions: getHash(await sql.getRows(`
SELECT
noteRevisionId,
noteId,
title,
content,
dateModifiedFrom,
dateModifiedTo
FROM note_revisions
ORDER BY noteRevisionId`)),
recent_notes: getHash(await sql.getRows(`
SELECT
branchId,
notePath,
dateAccessed,
isDeleted
FROM recent_notes
ORDER BY notePath`)),
options: getHash(await sql.getRows(`
SELECT
name,
value
FROM options
WHERE isSynced = 1
ORDER BY name`)),
// we don't include image data on purpose because they are quite large, checksum is good enough
// to represent the data anyway
images: getHash(await sql.getRows(`
SELECT
imageId,
format,
checksum,
name,
isDeleted,
dateModified,
dateCreated
FROM images
ORDER BY imageId`)),
note_images: getHash(await sql.getRows(`
SELECT
noteImageId,
noteId,
imageId,
isDeleted,
dateModified,
dateCreated
FROM note_images
ORDER BY noteImageId`)),
labels: getHash(await sql.getRows(`
SELECT
labelId,
noteId,
name,
value,
dateModified,
dateCreated
FROM labels
ORDER BY labelId`)),
api_tokens: getHash(await sql.getRows(`
SELECT
apiTokenId,
token,
dateCreated,
isDeleted
FROM api_tokens
ORDER BY apiTokenId`))
notes: await getHash(Note),
branches: await getHash(Branch),
note_revisions: await getHash(NoteRevision),
recent_notes: await getHash(RecentNote),
options: await getHash(Option, "isSynced = 1"),
images: await getHash(Image),
note_images: await getHash(NoteImage),
labels: await getHash(Label),
api_tokens: await getHash(ApiToken)
};
const elapseTimeMs = new Date().getTime() - startTime.getTime();
@@ -121,6 +48,29 @@ async function getHashes() {
return hashes;
}
async function checkContentHashes(otherHashes) {
const hashes = await getHashes();
let allChecksPassed = true;
for (const key in hashes) {
if (hashes[key] !== otherHashes[key]) {
allChecksPassed = false;
await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${resp.hashes[key]}`);
if (key !== 'recent_notes') {
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
await messagingService.sendMessageToAllClients({type: 'sync-hash-check-failed'});
}
}
}
if (allChecksPassed) {
log.info("Content hash checks PASSED");
}
}
module.exports = {
getHashes
getHashes,
checkContentHashes
};

View File

@@ -4,6 +4,7 @@ const sql = require('./sql');
const noteService = require('./notes');
const labelService = require('./labels');
const dateUtils = require('./date_utils');
const repository = require('./repository');
const CALENDAR_ROOT_LABEL = 'calendarRoot';
const YEAR_LABEL = 'yearNote';
@@ -14,117 +15,111 @@ const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Satur
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
async function createNote(parentNoteId, noteTitle, noteText) {
const {note} = await noteService.createNewNote(parentNoteId, {
return (await noteService.createNewNote(parentNoteId, {
title: noteTitle,
content: noteText,
target: 'into',
isProtected: false
});
return note.noteId;
})).note;
}
async function getNoteStartingWith(parentNoteId, startsWith) {
return await sql.getValue(`SELECT noteId FROM notes JOIN branches USING(noteId)
return await repository.getEntity(`SELECT notes.* FROM notes JOIN branches USING(noteId)
WHERE parentNoteId = ? AND title LIKE '${startsWith}%'
AND notes.isDeleted = 0 AND isProtected = 0
AND branches.isDeleted = 0`, [parentNoteId]);
}
async function getRootCalendarNoteId() {
let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN labels USING(noteId)
WHERE labels.name = '${CALENDAR_ROOT_LABEL}' AND notes.isDeleted = 0`);
async function getRootCalendarNote() {
// some caching here could be useful (e.g. in CLS)
let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
if (!rootNoteId) {
const {rootNote} = await noteService.createNewNote('root', {
if (!rootNote) {
rootNote = (await noteService.createNewNote('root', {
title: 'Calendar',
target: 'into',
isProtected: false
});
})).note;
const rootNoteId = rootNote.noteId;
await labelService.createLabel(rootNoteId, CALENDAR_ROOT_LABEL);
await labelService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
}
return rootNoteId;
return rootNote;
}
async function getYearNoteId(dateTimeStr, rootNoteId) {
async function getYearNote(dateTimeStr, rootNote) {
const yearStr = dateTimeStr.substr(0, 4);
let yearNoteId = await labelService.getNoteIdWithLabel(YEAR_LABEL, yearStr);
let yearNote = await labelService.getNoteWithLabel(YEAR_LABEL, yearStr);
if (!yearNoteId) {
yearNoteId = await getNoteStartingWith(rootNoteId, yearStr);
if (!yearNote) {
yearNote = await getNoteStartingWith(rootNote.noteId, yearStr);
if (!yearNoteId) {
yearNoteId = await createNote(rootNoteId, yearStr);
if (!yearNote) {
yearNote = await createNote(rootNote.noteId, yearStr);
}
await labelService.createLabel(yearNoteId, YEAR_LABEL, yearStr);
await labelService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
}
return yearNoteId;
return yearNote;
}
async function getMonthNoteId(dateTimeStr, rootNoteId) {
async function getMonthNote(dateTimeStr, rootNote) {
const monthStr = dateTimeStr.substr(0, 7);
const monthNumber = dateTimeStr.substr(5, 2);
let monthNoteId = await labelService.getNoteIdWithLabel(MONTH_LABEL, monthStr);
let monthNote = await labelService.getNoteWithLabel(MONTH_LABEL, monthStr);
if (!monthNoteId) {
const yearNoteId = await getYearNoteId(dateTimeStr, rootNoteId);
if (!monthNote) {
const yearNote = await getYearNote(dateTimeStr, rootNote);
monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber);
monthNote = await getNoteStartingWith(yearNote.noteId, monthNumber);
if (!monthNoteId) {
if (!monthNote) {
const dateObj = dateUtils.parseDate(dateTimeStr);
const noteTitle = monthNumber + " - " + MONTHS[dateObj.getMonth()];
monthNoteId = await createNote(yearNoteId, noteTitle);
monthNote = await createNote(yearNote.noteId, noteTitle);
}
await labelService.createLabel(monthNoteId, MONTH_LABEL, monthStr);
await labelService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
}
return monthNoteId;
return monthNote;
}
async function getDateNoteId(dateTimeStr, rootNoteId = null) {
if (!rootNoteId) {
rootNoteId = await getRootCalendarNoteId();
}
async function getDateNote(dateTimeStr) {
const rootNote = await getRootCalendarNote();
const dateStr = dateTimeStr.substr(0, 10);
const dayNumber = dateTimeStr.substr(8, 2);
let dateNoteId = await labelService.getNoteIdWithLabel(DATE_LABEL, dateStr);
let dateNote = await labelService.getNoteWithLabel(DATE_LABEL, dateStr);
if (!dateNoteId) {
const monthNoteId = await getMonthNoteId(dateTimeStr, rootNoteId);
if (!dateNote) {
const monthNote = await getMonthNote(dateTimeStr, rootNote);
dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber);
dateNote = await getNoteStartingWith(monthNote.noteId, dayNumber);
if (!dateNoteId) {
if (!dateNote) {
const dateObj = dateUtils.parseDate(dateTimeStr);
const noteTitle = dayNumber + " - " + DAYS[dateObj.getDay()];
dateNoteId = await createNote(monthNoteId, noteTitle);
dateNote = await createNote(monthNote.noteId, noteTitle);
}
await labelService.createLabel(dateNoteId, DATE_LABEL, dateStr);
await labelService.createLabel(dateNote.noteId, DATE_LABEL, dateStr);
}
return dateNoteId;
return dateNote;
}
module.exports = {
getRootCalendarNoteId,
getYearNoteId,
getMonthNoteId,
getDateNoteId
getRootCalendarNote,
getYearNote,
getMonthNote,
getDateNote
};

28
src/services/events.js Normal file
View File

@@ -0,0 +1,28 @@
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
const ENTITY_CHANGED = "ENTITY_CHANGED";
const eventListeners = {};
function subscribe(eventType, listener) {
eventListeners[eventType] = eventListeners[eventType] || [];
eventListeners[eventType].push(listener);
}
function emit(eventType, data) {
const listeners = eventListeners[eventType];
if (listeners) {
for (const listener of listeners) {
// not awaiting for async processing
listener(data);
}
}
}
module.exports = {
subscribe,
emit,
// event types:
ENTER_PROTECTED_SESSION,
ENTITY_CHANGED
};

View File

@@ -1,6 +1,5 @@
"use strict";
const sql = require('./sql');
const repository = require('./repository');
const Label = require('../entities/label');
@@ -12,17 +11,10 @@ const BUILTIN_LABELS = [
'run',
'manualTransactionHandling',
'disableInclusion',
'appCss'
'appCss',
'hideChildrenOverview'
];
async function getNoteIdWithLabel(name, value) {
return await sql.getValue(`SELECT notes.noteId FROM notes JOIN labels USING(noteId)
WHERE notes.isDeleted = 0
AND labels.isDeleted = 0
AND labels.name = ?
AND labels.value = ?`, [name, value]);
}
async function getNotesWithLabel(name, value) {
let notes;
@@ -44,11 +36,6 @@ async function getNoteWithLabel(name, value) {
return notes.length > 0 ? notes[0] : null;
}
async function getNoteIdsWithLabel(name) {
return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN labels USING(noteId)
WHERE notes.isDeleted = 0 AND labels.isDeleted = 0 AND labels.name = ? AND labels.isDeleted = 0`, [name]);
}
async function createLabel(noteId, name, value = "") {
return await new Label({
noteId: noteId,
@@ -58,10 +45,8 @@ async function createLabel(noteId, name, value = "") {
}
module.exports = {
getNoteIdWithLabel,
getNotesWithLabel,
getNoteWithLabel,
getNoteIdsWithLabel,
createLabel,
BUILTIN_LABELS
};

View File

@@ -15,14 +15,22 @@ const logger = require('simple-node-logger').createRollingFileLogger({
});
function info(message) {
logger.info(message);
// info messages are logged asynchronously
setTimeout(() => {
console.log(message);
console.log(message);
logger.info(message);
}, 0);
}
function error(message) {
message = "ERROR: " + message;
// we're using .info() instead of .error() because simple-node-logger emits weird error for showError()
info("ERROR: " + message);
// errors are logged synchronously to make sure it doesn't get lost in case of crash
logger.info(message);
console.trace(message);
}
const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ];

View File

@@ -45,7 +45,7 @@ async function migrate() {
// needs to happen outside of the transaction (otherwise it's a NO-OP)
await sql.execute("PRAGMA foreign_keys = OFF");
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
if (mig.type === 'sql') {
const migrationSql = fs.readFileSync(resourceDir.MIGRATIONS_DIR + "/" + mig.file).toString('utf8');

View File

@@ -56,6 +56,7 @@ async function createNewNote(parentNoteId, noteData) {
noteId: note.noteId,
parentNoteId: parentNoteId,
notePosition: newNotePos,
prefix: noteData.prefix,
isExpanded: 0
}).save();
@@ -180,6 +181,8 @@ async function saveNoteRevision(note) {
// title and text should be decrypted now
title: note.title,
content: note.content,
type: note.type,
mime: note.mime,
isProtected: 0, // will be fixed in the protectNoteRevisions() call
dateModifiedFrom: note.dateModified,
dateModifiedTo: dateUtils.nowDate()
@@ -198,7 +201,7 @@ async function updateNote(noteId, noteUpdates) {
await saveNoteRevision(note);
note.title = noteUpdates.title;
note.content = noteUpdates.content;
note.setContent(noteUpdates.content);
note.isProtected = noteUpdates.isProtected;
await note.save();

View File

@@ -1,45 +1,37 @@
const sql = require('./sql');
const repository = require('./repository');
const utils = require('./utils');
const dateUtils = require('./date_utils');
const syncTableService = require('./sync_table');
const appInfo = require('./app_info');
const Option = require('../entities/option');
async function getOption(name) {
const row = await await sql.getRowOrNull("SELECT value FROM options WHERE name = ?", [name]);
const option = await repository.getOption(name);
if (!row) {
if (!option) {
throw new Error("Option " + name + " doesn't exist");
}
return row.value;
return option.value;
}
async function setOption(name, value) {
const opt = await sql.getRow("SELECT * FROM options WHERE name = ?", [name]);
const option = await repository.getOption(name);
if (!opt) {
if (!option) {
throw new Error(`Option ${name} doesn't exist`);
}
if (opt.isSynced) {
await syncTableService.addOptionsSync(name);
}
option.value = value;
await sql.execute("UPDATE options SET value = ?, dateModified = ? WHERE name = ?",
[value, dateUtils.nowDate(), name]);
await option.save();
}
async function createOption(name, value, isSynced) {
await sql.insert("options", {
await new Option({
name: name,
value: value,
isSynced: isSynced,
dateModified: dateUtils.nowDate()
});
if (isSynced) {
await syncTableService.addOptionsSync(name);
}
isSynced: isSynced
}).save();
}
async function initOptions(startNotePath) {

View File

@@ -6,7 +6,7 @@ const cls = require('./cls');
const dataKeyMap = {};
function setDataKey(req, decryptedDataKey) {
function setDataKey(decryptedDataKey) {
const protectedSessionId = utils.randomSecureToken(32);
dataKeyMap[protectedSessionId] = Array.from(decryptedDataKey); // can't store buffer in session
@@ -28,12 +28,20 @@ function getDataKey() {
return dataKeyMap[protectedSessionId];
}
function isProtectedSessionAvailable(req) {
const protectedSessionId = getProtectedSessionId(req);
function isProtectedSessionAvailable() {
const protectedSessionId = getProtectedSessionId();
return !!dataKeyMap[protectedSessionId];
}
function decryptNoteTitle(noteId, encryptedTitle) {
const dataKey = getDataKey();
const iv = dataEncryptionService.noteTitleIv(noteId);
return dataEncryptionService.decryptString(dataKey, iv, encryptedTitle);
}
function decryptNote(note) {
const dataKey = getDataKey();
@@ -99,6 +107,7 @@ module.exports = {
setDataKey,
getDataKey,
isProtectedSessionAvailable,
decryptNoteTitle,
decryptNote,
decryptNotes,
decryptNoteRevision,

View File

@@ -41,6 +41,10 @@ async function getLabel(labelId) {
return await getEntity("SELECT * FROM labels WHERE labelId = ?", [labelId]);
}
async function getOption(name) {
return await getEntity("SELECT * FROM options WHERE name = ?", [name]);
}
async function updateEntity(entity) {
if (entity.beforeSaving) {
await entity.beforeSaving();
@@ -50,12 +54,14 @@ async function updateEntity(entity) {
delete clone.jsonContent;
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await sql.replace(entity.constructor.tableName, clone);
const primaryKey = entity[entity.constructor.primaryKeyName];
await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey);
if (entity.constructor.tableName !== 'options' || entity.isSynced) {
await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey);
}
});
}
@@ -66,6 +72,7 @@ module.exports = {
getBranch,
getImage,
getLabel,
getOption,
updateEntity,
setEntityConstructor
};

View File

@@ -1,6 +1,8 @@
const sql = require('./sql');
const ScriptContext = require('./script_context');
const repository = require('./repository');
const cls = require('./cls');
const sourceIdService = require('./source_id');
async function executeNote(note) {
if (!note.isJavaScript()) {
@@ -27,7 +29,7 @@ async function executeBundle(bundle, startNote) {
return await execute(ctx, script, '');
}
else {
return await sql.doInTransaction(async () => execute(ctx, script, ''));
return await sql.transactional(async () => execute(ctx, script, ''));
}
}
@@ -49,6 +51,9 @@ async function executeScript(script, params, startNoteId, currentNoteId) {
}
async function execute(ctx, script, paramsStr) {
// scripts run as "server" sourceId so clients recognize the changes as "foreign" and update themselves
cls.namespace.set('sourceId', sourceIdService.getCurrentSourceId());
return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx));
}

View File

@@ -56,10 +56,10 @@ function ScriptApi(startNote, currentNote) {
this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`);
this.getRootCalendarNoteId = dateNoteService.getRootCalendarNoteId;
this.getDateNoteId = dateNoteService.getDateNoteId;
this.getRootCalendarNote = dateNoteService.getRootCalendarNote;
this.getDateNote = dateNoteService.getDateNote;
this.transactional = sql.doInTransaction;
this.transactional = sql.transactional;
}
module.exports = ScriptContext;

View File

@@ -122,7 +122,7 @@ async function wrap(func) {
let transactionActive = false;
let transactionPromise = null;
async function doInTransaction(func) {
async function transactional(func) {
if (cls.namespace.get('isInTransaction')) {
return await func();
}
@@ -149,11 +149,13 @@ async function doInTransaction(func) {
resolve();
}
catch (e) {
log.error("Error executing transaction, executing rollback. Inner exception: " + e.stack + error.stack);
if (transactionActive) {
log.error("Error executing transaction, executing rollback. Inner exception: " + e.stack + error.stack);
await rollback();
await rollback();
transactionActive = false;
transactionActive = false;
}
reject(e);
}
@@ -181,5 +183,5 @@ module.exports = {
getColumn,
execute,
executeScript,
doInTransaction
transactional
};

View File

@@ -58,7 +58,7 @@ async function createInitialDatabase() {
const imagesSql = fs.readFileSync(resourceDir.DB_INIT_DIR + '/main_images.sql', 'UTF-8');
const notesImageSql = fs.readFileSync(resourceDir.DB_INIT_DIR + '/main_note_images.sql', 'UTF-8');
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await sql.executeScript(schema);
await sql.executeScript(notesSql);
await sql.executeScript(notesTreeSql);

View File

@@ -10,10 +10,8 @@ const sourceIdService = require('./source_id');
const dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update');
const contentHashService = require('./content_hash');
const eventLogService = require('./event_log');
const fs = require('fs');
const appInfo = require('./app_info');
const messagingService = require('./messaging');
const syncSetup = require('./sync_setup');
const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
@@ -91,69 +89,19 @@ async function login() {
return syncContext;
}
async function getLastSyncedPull() {
return parseInt(await optionService.getOption('lastSyncedPull'));
}
async function setLastSyncedPull(syncId) {
await optionService.setOption('lastSyncedPull', syncId);
}
async function pullSync(syncContext) {
const lastSyncedPull = await getLastSyncedPull();
const changesUri = '/api/sync/changed?lastSyncId=' + await getLastSyncedPull();
const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull;
const rows = await syncRequest(syncContext, 'GET', changesUri);
const syncRows = await syncRequest(syncContext, 'GET', changesUri);
log.info("Pulled " + rows.length + " changes from " + changesUri);
log.info("Pulled " + syncRows.length + " changes from " + changesUri);
for (const sync of syncRows) {
for (const {sync, entity} of rows) {
if (sourceIdService.isLocalSourceId(sync.sourceId)) {
log.info(`Skipping pull #${sync.id} ${sync.entityName} ${sync.entityId} because ${sync.sourceId} is a local source id.`);
await setLastSyncedPull(sync.id);
continue;
}
const resp = await syncRequest(syncContext, 'GET', "/api/sync/" + sync.entityName + "/" + encodeURIComponent(sync.entityId));
if (!resp || (sync.entityName === 'notes' && !resp.entity)) {
log.error(`Empty response to pull for sync #${sync.id} ${sync.entityName}, id=${sync.entityId}`);
}
else if (sync.entityName === 'notes') {
await syncUpdateService.updateNote(resp.entity, syncContext.sourceId);
}
else if (sync.entityName === 'branches') {
await syncUpdateService.updateBranch(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_revisions') {
await syncUpdateService.updateNoteRevision(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_reordering') {
await syncUpdateService.updateNoteReordering(resp, syncContext.sourceId);
}
else if (sync.entityName === 'options') {
await syncUpdateService.updateOptions(resp, syncContext.sourceId);
}
else if (sync.entityName === 'recent_notes') {
await syncUpdateService.updateRecentNotes(resp, syncContext.sourceId);
}
else if (sync.entityName === 'images') {
await syncUpdateService.updateImage(resp, syncContext.sourceId);
}
else if (sync.entityName === 'note_images') {
await syncUpdateService.updateNoteImage(resp, syncContext.sourceId);
}
else if (sync.entityName === 'labels') {
await syncUpdateService.updateLabel(resp, syncContext.sourceId);
}
else if (sync.entityName === 'api_tokens') {
await syncUpdateService.updateApiToken(resp, syncContext.sourceId);
}
else {
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
await syncUpdateService.updateEntity(sync, entity, syncContext.sourceId);
}
await setLastSyncedPull(sync.id);
@@ -162,145 +110,69 @@ async function pullSync(syncContext) {
log.info("Finished pull");
}
async function getLastSyncedPush() {
return parseInt(await optionService.getOption('lastSyncedPush'));
}
async function setLastSyncedPush(lastSyncedPush) {
await optionService.setOption('lastSyncedPush', lastSyncedPush);
}
async function pushSync(syncContext) {
let lastSyncedPush = await getLastSyncedPush();
while (true) {
const sync = await sql.getRowOrNull('SELECT * FROM sync WHERE id > ? LIMIT 1', [lastSyncedPush]);
const syncs = await sql.getRows('SELECT * FROM sync WHERE id > ? LIMIT 1000', [lastSyncedPush]);
if (sync === null) {
// nothing to sync
const filteredSyncs = syncs.filter(sync => {
if (sync.sourceId === syncContext.sourceId) {
log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`);
// this may set lastSyncedPush beyond what's actually sent (because of size limit)
// so this is applied to the database only if there's no actual update
// TODO: it would be better to simplify this somehow
lastSyncedPush = sync.id;
return false;
}
else {
return true;
}
});
if (filteredSyncs.length === 0) {
log.info("Nothing to push");
await setLastSyncedPush(lastSyncedPush);
break;
}
if (sync.sourceId === syncContext.sourceId) {
log.info(`Skipping push #${sync.id} ${sync.entityName} ${sync.entityId} because it originates from sync target`);
}
else {
await pushEntity(sync, syncContext);
}
const syncRecords = await getSyncRecords(filteredSyncs);
lastSyncedPush = sync.id;
log.info(`Pushing ${syncRecords.length} syncs.`);
await syncRequest(syncContext, 'PUT', '/api/sync/update', {
sourceId: sourceIdService.getCurrentSourceId(),
entities: syncRecords
});
lastSyncedPush = syncRecords[syncRecords.length - 1].sync.id;
await setLastSyncedPush(lastSyncedPush);
}
}
async function pushEntity(sync, syncContext) {
let entity;
if (sync.entityName === 'notes') {
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
serializeNoteContentBuffer(entity);
}
else if (sync.entityName === 'branches') {
entity = await sql.getRow('SELECT * FROM branches WHERE branchId = ?', [sync.entityId]);
}
else if (sync.entityName === 'note_revisions') {
entity = await sql.getRow('SELECT * FROM note_revisions WHERE noteRevisionId = ?', [sync.entityId]);
}
else if (sync.entityName === 'note_reordering') {
entity = {
parentNoteId: sync.entityId,
ordering: await sql.getMap('SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [sync.entityId])
};
}
else if (sync.entityName === 'options') {
entity = await sql.getRow('SELECT * FROM options WHERE name = ?', [sync.entityId]);
}
else if (sync.entityName === 'recent_notes') {
entity = await sql.getRow('SELECT * FROM recent_notes WHERE branchId = ?', [sync.entityId]);
}
else if (sync.entityName === 'images') {
entity = await sql.getRow('SELECT * FROM images WHERE imageId = ?', [sync.entityId]);
if (entity.data !== null) {
entity.data = entity.data.toString('base64');
}
}
else if (sync.entityName === 'note_images') {
entity = await sql.getRow('SELECT * FROM note_images WHERE noteImageId = ?', [sync.entityId]);
}
else if (sync.entityName === 'labels') {
entity = await sql.getRow('SELECT * FROM labels WHERE labelId = ?', [sync.entityId]);
}
else if (sync.entityName === 'api_tokens') {
entity = await sql.getRow('SELECT * FROM api_tokens WHERE apiTokenId = ?', [sync.entityId]);
}
else {
throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
}
if (!entity) {
log.info(`Sync #${sync.id} entity for ${sync.entityName} ${sync.entityId} doesn't exist. Skipping.`);
return;
}
log.info(`Pushing changes in sync #${sync.id} ${sync.entityName} ${sync.entityId}`);
const payload = {
sourceId: sourceIdService.getCurrentSourceId(),
entity: entity
};
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
}
function serializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = note.content.toString("binary");
}
}
async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
if (await getLastSyncedPull() < resp.max_sync_id) {
if (await getLastSyncedPull() < resp.maxSyncId) {
log.info("There are some outstanding pulls, skipping content check.");
return;
}
const lastSyncedPush = await getLastSyncedPush();
const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
const notPushedSyncs = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [await getLastSyncedPush()]);
if (notPushedSyncs > 0) {
log.info("There's " + notPushedSyncs + " outstanding pushes, skipping content check.");
log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`);
return;
}
const hashes = await contentHashService.getHashes();
let allChecksPassed = true;
for (const key in hashes) {
if (hashes[key] !== resp.hashes[key]) {
allChecksPassed = false;
await eventLogService.addEvent(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${resp.hashes[key]}`);
if (key !== 'recent_notes') {
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
await messagingService.sendMessageToAllClients({type: 'sync-hash-check-failed'});
}
}
}
if (allChecksPassed) {
log.info("Content hash checks PASSED");
}
await contentHashService.checkContentHashes(resp.hashes);
}
async function syncRequest(syncContext, method, uri, body) {
@@ -331,6 +203,81 @@ async function syncRequest(syncContext, method, uri, body) {
}
}
const primaryKeys = {
"notes": "noteId",
"branches": "branchId",
"note_revisions": "noteRevisionId",
"option": "name",
"recent_notes": "branchId",
"images": "imageId",
"note_images": "noteImageId",
"labels": "labelId",
"api_tokens": "apiTokenId",
"options": "name"
};
async function getEntityRow(entityName, entityId) {
if (entityName === 'note_reordering') {
return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]);
}
else {
const primaryKey = primaryKeys[entityName];
if (!primaryKey) {
throw new Error("Unknown entity " + entityName);
}
const entity = await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (entityName === 'notes' && entity.type === 'file') {
entity.content = entity.content.toString("binary");
}
else if (entityName === 'images') {
entity.data = entity.data.toString('base64');
}
return entity;
}
}
async function getSyncRecords(syncs) {
const records = [];
let length = 0;
for (const sync of syncs) {
const record = {
sync: sync,
entity: await getEntityRow(sync.entityName, sync.entityId)
};
records.push(record);
length += JSON.stringify(record).length;
if (length > 1000000) {
break;
}
}
return records;
}
async function getLastSyncedPull() {
return parseInt(await optionService.getOption('lastSyncedPull'));
}
async function setLastSyncedPull(syncId) {
await optionService.setOption('lastSyncedPull', syncId);
}
async function getLastSyncedPush() {
return parseInt(await optionService.getOption('lastSyncedPush'));
}
async function setLastSyncedPush(lastSyncedPush) {
await optionService.setOption('lastSyncedPush', lastSyncedPush);
}
sqlInit.dbReady.then(() => {
if (syncSetup.isSyncSetup) {
log.info("Setting up sync to " + syncSetup.SYNC_SERVER + " with timeout " + syncSetup.SYNC_TIMEOUT);
@@ -357,5 +304,5 @@ sqlInit.dbReady.then(() => {
module.exports = {
sync,
serializeNoteContentBuffer
getSyncRecords
};

View File

@@ -4,6 +4,7 @@ const dateUtils = require('./date_utils');
const syncSetup = require('./sync_setup');
const log = require('./log');
const cls = require('./cls');
const eventService = require('./events');
async function addNoteSync(noteId, sourceId) {
await addEntitySync("notes", noteId, sourceId)
@@ -58,6 +59,11 @@ async function addEntitySync(entityName, entityId, sourceId) {
// useful when you fork the DB for new "client" instance, it won't try to sync the whole DB
await sql.execute("UPDATE options SET value = (SELECT MAX(id) FROM sync) WHERE name IN('lastSyncedPush', 'lastSyncedPull')");
}
eventService.emit(eventService.ENTITY_CHANGED, {
entityName,
entityId
});
}
async function cleanupSyncRowsForMissingEntities(entityName, entityKey) {
@@ -91,6 +97,8 @@ async function fillSyncRows(entityName, entityKey) {
}
async function fillAllSyncRows() {
await sql.execute("DELETE FROM sync");
await fillSyncRows("notes", "noteId");
await fillSyncRows("branches", "branchId");
await fillSyncRows("note_revisions", "noteRevisionId");

View File

@@ -3,6 +3,44 @@ const log = require('./log');
const eventLogService = require('./event_log');
const syncTableService = require('./sync_table');
async function updateEntity(sync, entity, sourceId) {
const {entityName} = sync;
if (entityName === 'notes') {
await updateNote(entity, sourceId);
}
else if (entityName === 'branches') {
await updateBranch(entity, sourceId);
}
else if (entityName === 'note_revisions') {
await updateNoteRevision(entity, sourceId);
}
else if (entityName === 'note_reordering') {
await updateNoteReordering(sync.entityId, entity, sourceId);
}
else if (entityName === 'options') {
await updateOptions(entity, sourceId);
}
else if (entityName === 'recent_notes') {
await updateRecentNotes(entity, sourceId);
}
else if (entityName === 'images') {
await updateImage(entity, sourceId);
}
else if (entityName === 'note_images') {
await updateNoteImage(entity, sourceId);
}
else if (entityName === 'labels') {
await updateLabel(entity, sourceId);
}
else if (entityName === 'api_tokens') {
await updateApiToken(entity, sourceId);
}
else {
throw new Error(`Unrecognized entity type ${sync}`);
}
}
function deserializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary');
@@ -15,7 +53,7 @@ async function updateNote(entity, sourceId) {
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
if (!origNote || origNote.dateModified <= entity.dateModified) {
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await sql.replace("notes", entity);
await syncTableService.addNoteSync(entity.noteId, sourceId);
@@ -29,7 +67,7 @@ async function updateNote(entity, sourceId) {
async function updateBranch(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [entity.branchId]);
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
if (orig === null || orig.dateModified < entity.dateModified) {
delete entity.isExpanded;
@@ -45,7 +83,7 @@ async function updateBranch(entity, sourceId) {
async function updateNoteRevision(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [entity.noteRevisionId]);
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
// we update note revision even if date modified to is the same because the only thing which might have changed
// is the protected status (and correnspondingly title and content) which doesn't affect the dateModifiedTo
if (orig === null || orig.dateModifiedTo <= entity.dateModifiedTo) {
@@ -58,13 +96,13 @@ async function updateNoteRevision(entity, sourceId) {
});
}
async function updateNoteReordering(entity, sourceId) {
await sql.doInTransaction(async () => {
Object.keys(entity.ordering).forEach(async key => {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity.ordering[key], key]);
async function updateNoteReordering(entityId, entity, sourceId) {
await sql.transactional(async () => {
Object.keys(entity).forEach(async key => {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
});
await syncTableService.addNoteReorderingSync(entity.parentNoteId, sourceId);
await syncTableService.addNoteReorderingSync(entityId, sourceId);
});
}
@@ -75,7 +113,7 @@ async function updateOptions(entity, sourceId) {
return;
}
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
if (orig === null || orig.dateModified < entity.dateModified) {
await sql.replace('options', entity);
@@ -90,7 +128,7 @@ async function updateRecentNotes(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE branchId = ?", [entity.branchId]);
if (orig === null || orig.dateAccessed < entity.dateAccessed) {
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await sql.replace('recent_notes', entity);
await syncTableService.addRecentNoteSync(entity.branchId, sourceId);
@@ -106,7 +144,7 @@ async function updateImage(entity, sourceId) {
const origImage = await sql.getRow("SELECT * FROM images WHERE imageId = ?", [entity.imageId]);
if (!origImage || origImage.dateModified <= entity.dateModified) {
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await sql.replace("images", entity);
await syncTableService.addImageSync(entity.imageId, sourceId);
@@ -120,7 +158,7 @@ async function updateNoteImage(entity, sourceId) {
const origNoteImage = await sql.getRow("SELECT * FROM note_images WHERE noteImageId = ?", [entity.noteImageId]);
if (!origNoteImage || origNoteImage.dateModified <= entity.dateModified) {
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await sql.replace("note_images", entity);
await syncTableService.addNoteImageSync(entity.noteImageId, sourceId);
@@ -134,7 +172,7 @@ async function updateLabel(entity, sourceId) {
const origLabel = await sql.getRow("SELECT * FROM labels WHERE labelId = ?", [entity.labelId]);
if (!origLabel || origLabel.dateModified <= entity.dateModified) {
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await sql.replace("labels", entity);
await syncTableService.addLabelSync(entity.labelId, sourceId);
@@ -148,7 +186,7 @@ async function updateApiToken(entity, sourceId) {
const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]);
if (!apiTokenId) {
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
await sql.replace("api_tokens", entity);
await syncTableService.addApiTokenSync(entity.apiTokenId, sourceId);
@@ -159,14 +197,5 @@ async function updateApiToken(entity, sourceId) {
}
module.exports = {
updateNote,
updateBranch,
updateNoteRevision,
updateNoteReordering,
updateOptions,
updateRecentNotes,
updateImage,
updateNoteImage,
updateLabel,
updateApiToken
updateEntity
};

View File

@@ -77,7 +77,7 @@ async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) {
}
async function sortNotesAlphabetically(parentNoteId) {
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
const notes = await sql.getRows(`SELECT branchId, noteId, title, isProtected
FROM notes JOIN branches USING(noteId)
WHERE branches.isDeleted = 0 AND parentNoteId = ?`, [parentNoteId]);

View File

@@ -58,7 +58,7 @@ async function start() {
}
// we'll create clones for 20% of notes
for (let i = 0; i < (noteCount / 5); i++) {
for (let i = 0; i < (noteCount / 50); i++) {
const noteIdToClone = getRandomParentNoteId();
const parentNoteId = getRandomParentNoteId();
const prefix = Math.random() > 0.8 ? "prefix" : null;

View File

@@ -38,7 +38,7 @@
</div>
</div>
<div class="hide-toggle" style="grid-area: tree-actions;">
<div style="grid-area: left-pane; display: flex; flex-direction: column;" class="hide-toggle">
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action"
style="background: url('/images/icons/file-plus.png')"></a>
@@ -54,32 +54,31 @@
</div>
<input type="file" id="import-upload" style="display: none" />
</div>
<div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;">
<div style="display: flex; align-items: center;">
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
<button id="do-search-button" class="btn btn-primary btn-sm" title="Search">Search</button>
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
<div style="display: flex; align-items: center;">
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
<button id="do-search-button" class="btn btn-primary btn-sm" title="Search">Search</button>
</div>
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;">
<button id="reset-search-button" class="btn btn-sm" title="Reset search">Reset search</button>
<button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button>
</div>
</div>
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;">
<button id="reset-search-button" class="btn btn-sm" title="Reset search">Reset search</button>
<div id="tree"></div>
<button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button>
<div id="parent-list">
<p><strong>Note locations:</strong></p>
<ul id="parent-list-inner"></ul>
</div>
</div>
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;">
</div>
<div id="parent-list" class="hide-toggle">
<p><strong>Note locations:</strong></p>
<ul id="parent-list-inner"></ul>
</div>
<div class="hide-toggle" style="grid-area: title;">
<div style="display: flex; align-items: center;">
<div style="grid-area: title;">
<div class="hide-toggle" style="display: flex; align-items: center;">
<a title="Protect the note so that password will be required to view the note"
class="icon-action"
id="protect-button"
@@ -132,82 +131,86 @@
</div>
</div>
<div style="position: relative; overflow: auto; grid-area: note-content; padding-left: 10px; padding-top: 10px;" id="note-detail-wrapper">
<div id="note-detail-text" class="note-detail-component"></div>
<div id="note-detail-wrapper">
<div id="note-detail-component-wrapper">
<div id="note-detail-text" class="note-detail-component" tabindex="2"></div>
<div id="note-detail-search" class="note-detail-component">
<div style="display: flex; align-items: center;">
<strong>Search string: &nbsp; &nbsp;</strong>
<textarea rows="4" cols="50" id="search-string"></textarea>
<div id="note-detail-search" class="note-detail-component">
<div style="display: flex; align-items: center;">
<strong>Search string: &nbsp; &nbsp;</strong>
<textarea rows="4" cols="50" id="search-string"></textarea>
</div>
<br />
<h4>Help</h4>
<p>
<ul>
<li>
<code>@abc</code> - matches notes with label abc</li>
<li>
<code>@!abc</code> - matches notes without abc label (maybe not the best syntax)</li>
<li>
<code>@abc=true</code> - matches notes with label abc having value true</li>
<li><code>@abc!=true</code></li>
<li>
<code>@"weird label"="weird value"</code> - works also with whitespace inside names values</li>
<li>
<code>@abc and @def</code> - matches notes with both abc and def</li>
<li>
<code>@abc @def</code> - AND relation is implicit when specifying multiple labels</li>
<li>
<code>@abc or @def</code> - OR relation</li>
<li>
<code>@abc&lt;=5</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li>
<code>some search string @abc @def</code> - combination of fulltext and label search - both of them need to match (OR not supported)</li>
<li>
<code>@abc @def some search string</code> - same combination</li>
</ul>
<a href="https://github.com/zadam/trilium/wiki/Labels">Complete help on search syntax</a>
</p>
</div>
<br />
<div id="note-detail-code" class="note-detail-component"></div>
<h4>Help</h4>
<p>
<ul>
<li>
<code>@abc</code> - matches notes with label abc</li>
<li>
<code>@!abc</code> - matches notes without abc label (maybe not the best syntax)</li>
<li>
<code>@abc=true</code> - matches notes with label abc having value true</li>
<li><code>@abc!=true</code></li>
<li>
<code>@"weird label"="weird value"</code> - works also with whitespace inside names values</li>
<li>
<code>@abc and @def</code> - matches notes with both abc and def</li>
<li>
<code>@abc @def</code> - AND relation is implicit when specifying multiple labels</li>
<li>
<code>@abc or @def</code> - OR relation</li>
<li>
<code>@abc&lt;=5</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li>
<code>some search string @abc @def</code> - combination of fulltext and label search - both of them need to match (OR not supported)</li>
<li>
<code>@abc @def some search string</code> - same combination</li>
</ul>
<div id="note-detail-render" class="note-detail-component"></div>
<a href="https://github.com/zadam/trilium/wiki/Labels">Complete help on search syntax</a>
</p>
<div id="note-detail-file" class="note-detail-component">
<table id="file-table">
<tr>
<th>File name:</th>
<td id="file-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="file-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="file-filesize"></td>
</tr>
<tr>
<td>
<button id="file-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="file-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="file-upload" style="display: none" />
</div>
<div id="note-detail-code" class="note-detail-component"></div>
<div id="children-overview"></div>
<div id="note-detail-render" class="note-detail-component"></div>
<div id="label-list">
<button class="btn btn-sm show-labels-button">Labels:</button>
<div id="note-detail-file" class="note-detail-component">
<table id="file-table">
<tr>
<th>File name:</th>
<td id="file-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="file-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="file-filesize"></td>
</tr>
<tr>
<td>
<button id="file-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="file-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
<span id="label-list-inner"></span>
</div>
<input type="file" id="file-upload" style="display: none" />
</div>
<div id="label-list">
<button class="btn btn-sm show-labels-button">Labels:</button>
<span id="label-list-inner"></span>
</div>
</div>
@@ -421,7 +424,7 @@
</div>
<div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
<div style="height: 150px; width: 100%; border: 1px solid #ccc; margin-bottom: 10px;" id="sql-console-query"></div>
<div id="sql-console-query"></div>
<div style="text-align: center">
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>

View File

@@ -5,6 +5,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />