Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb69914f09 | |||
| a372cbb2df | |||
| 0ce5caefe8 | |||
| 94dabb81f6 | |||
| cd45bcfd03 | |||
| 49a53f7a45 | |||
| 9fa6c0918c | |||
| e8d089e37e | |||
| a931ce25fa | |||
| b507abb4f7 | |||
| 66e7c6de62 | |||
| 4ce5ea9886 | |||
| 8c54b62f07 | |||
| 85eb50ed0f | |||
| 5ffd621e9d | |||
| df93cb09da | |||
| bbf04209f0 | |||
| 834bfa39c7 | |||
| 52b445f70b | |||
| 7b9b4fbb0c | |||
| 5af0ba1fcb | |||
| 85a9748291 | |||
| b4005a7ffe | |||
| 82de1c88d4 | |||
| 1687ed7e0b | |||
| c8b9c7d936 | |||
| d57057ba28 | |||
| 66cee8daa4 | |||
| afd7df0942 | |||
| bd6ae33d32 | |||
| 70660a0d68 | |||
| cdad18551a | |||
| 592c51d1a5 | |||
| 6a57b8a7e7 | |||
| 7a94e21c54 | |||
| 5b43f321e2 | |||
| a4eafb934f | |||
| 7b59a665dd | |||
| 3d15450ffc | |||
| b0c6d52461 | |||
| 2dc16dd29f | |||
| d8924c536b | |||
| 3ebbf2cc46 | |||
| f4079604c9 | |||
| 1f96a6beab | |||
| b277a250e5 | |||
| 5b0e1a644d | |||
| 6bb3cfa9a3 | |||
| 9720868f5a | |||
| 8d8ee2a87a | |||
| 542e82ee5d | |||
| 0104b19502 | |||
| 120888b53e | |||
| d2e2caed62 | |||
| 63066802a8 | |||
| 6128bb4ff3 | |||
| 982796255d | |||
| 36b15f474d | |||
| 13f71f8967 | |||
| 64336ffbee | |||
| b09463d1b2 | |||
| b5e6f46b9c | |||
| 08af4a0465 | |||
| 8c5df6321f | |||
| d19f044961 | |||
| e378d9f645 | |||
| 39dc0f71b4 | |||
| 0cef5c6b8c | |||
| 9b5a44cef4 | |||
| 29769ed91d |
+112
-103
@@ -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>''</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>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="84" parent="12" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</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>"unnamed"</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>""</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>'text'</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>'text/html'</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>
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
@@ -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`
|
||||
);
|
||||
@@ -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');
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
create index IDX_notes_type
|
||||
on notes (type);
|
||||
@@ -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;
|
||||
+23
-23
@@ -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`
|
||||
);
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
Generated
+5098
-3405
File diff suppressed because it is too large
Load Diff
+22
-21
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
+15
-6
@@ -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) {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>')
|
||||
|
||||
@@ -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})`;
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+15
-29
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 |
@@ -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
Vendored
+455
-344
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
+62
-46
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
+12
-139
@@ -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
|
||||
};
|
||||
+64
-43
@@ -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
|
||||
};
|
||||
|
||||
+6
-21
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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" };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
+52
-102
@@ -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
|
||||
};
|
||||
+43
-48
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
+2
-17
@@ -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
|
||||
};
|
||||
+11
-3
@@ -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" ];
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
+12
-20
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
+7
-5
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
+113
-166
@@ -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
|
||||
};
|
||||
@@ -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");
|
||||
|
||||
+53
-24
@@ -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
|
||||
};
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+90
-87
@@ -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: </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: </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<=5</code> - numerical comparison (also >, >=, <).</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<=5</code> - numerical comparison (also >, >=, <).</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user