Compare commits

...

35 Commits

Author SHA1 Message Date
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
50 changed files with 1247 additions and 1112 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,449 @@ 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>
</column>
<column id="91" parent="13" name="content">
<column id="92" parent="13" name="content">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</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

@@ -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

@@ -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) {

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.10.1-beta",
"version": "0.11.1",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {

View File

@@ -12,13 +12,21 @@ class Note extends Entity {
constructor(row) {
super(row);
if (this.isProtected) {
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
if (this.isProtected && this.noteId) {
protected_session.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,7 +141,7 @@ class Note extends Entity {
beforeSaving() {
super.beforeSaving();
if (this.isJson()) {
if (this.isJson() && this.jsonContent) {
this.content = JSON.stringify(this.jsonContent, null, '\t');
}

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

@@ -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

@@ -24,6 +24,10 @@ class NoteShort {
}
async getChildBranches() {
if (!this.treeCache.children[this.noteId]) {
return [];
}
const branches = [];
for (const child of this.treeCache.children[this.noteId]) {
@@ -44,6 +48,14 @@ class NoteShort {
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

@@ -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,
@@ -38,7 +41,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 +70,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

@@ -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

@@ -293,7 +293,7 @@ function initFancyTree(branch) {
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"],
source: branch,
scrollParent: $("#tree"),
scrollParent: $tree,
click: (event, data) => {
const targetType = data.targetType;
const node = data.node;

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,9 @@
display: grid;
grid-template-areas: "header header"
"tree-actions title"
"search note-content"
"tree note-content"
"parent-list note-content"
"search note-detail"
"tree note-detail"
"parent-list note-detail"
"parent-list label-list";
grid-template-columns: 2fr 5fr;
grid-template-rows: auto
@@ -36,7 +36,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;
}
@@ -249,7 +249,7 @@ div.ui-tooltip {
}
#note-detail-code {
height: 100%;
min-height: 200px;
}
.CodeMirror {
@@ -288,4 +288,36 @@ 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;
}

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

@@ -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.entityName, 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

@@ -70,7 +70,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);
});
});
@@ -147,26 +147,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 = 88;
module.exports = {
appVersion: packageJson.version,

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-01-17T23:59:03-05:00", buildRevision: "651a9fb3272c85d287c16d5a4978464fb7d2490d" };
module.exports = { buildDate:"2018-04-11T00:10:33-04:00", buildRevision: "a4eafb934ff3cdb46dbc138b1b02850872948699" };

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

@@ -1,6 +1,10 @@
"use strict";
const sql = require('./sql');
const utils = require('./utils');
const log = require('./log');
const eventLogService = require('./event_log');
const messagingService = require('./messaging');
function getHash(rows) {
let hash = '';
@@ -121,6 +125,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,112 @@ 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() {
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, rootNote = null) {
if (!rootNote) {
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
};

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

@@ -50,7 +50,7 @@ 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];

View File

@@ -27,7 +27,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, ''));
}
}

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.entityName, 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,80 @@ 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"
};
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 +303,5 @@ sqlInit.dbReady.then(() => {
module.exports = {
sync,
serializeNoteContentBuffer
getSyncRecords
};

View File

@@ -91,6 +91,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,42 @@ const log = require('./log');
const eventLogService = require('./event_log');
const syncTableService = require('./sync_table');
async function updateEntity(entityName, entity, sourceId) {
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(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 ${entityName}`);
}
}
function deserializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary');
@@ -15,7 +51,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 +65,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 +81,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) {
@@ -59,7 +95,7 @@ async function updateNoteRevision(entity, sourceId) {
}
async function updateNoteReordering(entity, sourceId) {
await sql.doInTransaction(async () => {
await sql.transactional(async () => {
Object.keys(entity.ordering).forEach(async key => {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity.ordering[key], key]);
});
@@ -75,7 +111,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 +126,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 +142,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 +156,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 +170,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 +184,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 +195,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

@@ -132,76 +132,80 @@
</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 style="position: relative; overflow: hidden; grid-area: note-detail; padding-left: 10px; padding-top: 10px; display: flex; flex-direction: column;" id="note-detail-wrapper">
<div style="flex-grow: 1; position: relative; overflow: auto; flex-basis: content;">
<div id="note-detail-text" style="height: 100%;" class="note-detail-component"></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="note-detail-render" class="note-detail-component"></div>
<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 id="children-overview"></div>
</div>
<div id="label-list">

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" />