mirror of
https://github.com/zadam/trilium.git
synced 2025-10-29 01:06:36 +01:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d185a5178 | ||
|
|
2ff7a890bc | ||
|
|
2eb1a9705f | ||
|
|
ed1381103a | ||
|
|
170d317589 | ||
|
|
ededc063df | ||
|
|
986eace1be | ||
|
|
29086d8dfe | ||
|
|
9b3f3fde05 | ||
|
|
6a50afd952 | ||
|
|
697eee2706 | ||
|
|
8a95afd756 | ||
|
|
4d6eda8fe6 | ||
|
|
e4f459fa2b | ||
|
|
f578e001b0 | ||
|
|
2a08aef885 | ||
|
|
7564bf388c | ||
|
|
7e4d70259f | ||
|
|
5b98c1c0f3 | ||
|
|
02dc7b199b | ||
|
|
d39cdbfada | ||
|
|
50bb4a47ee | ||
|
|
a4627f2ddb | ||
|
|
c8253caae9 | ||
|
|
0ece9bd1be | ||
|
|
b6935abcc9 | ||
|
|
37ab7b4641 | ||
|
|
013714cb5c | ||
|
|
1fe7c62f5a | ||
|
|
a06618d851 | ||
|
|
e7460ca3a9 | ||
|
|
073300bbcd | ||
|
|
a201661ce5 | ||
|
|
6235a3c886 |
@@ -21,533 +21,529 @@
|
||||
<table id="13" parent="2" name="notes"/>
|
||||
<table id="14" parent="2" name="options"/>
|
||||
<table id="15" parent="2" name="recent_notes"/>
|
||||
<table id="16" parent="2" name="source_ids"/>
|
||||
<table id="17" parent="2" name="sqlite_master">
|
||||
<table id="16" parent="2" name="relations"/>
|
||||
<table id="17" parent="2" name="source_ids"/>
|
||||
<table id="18" parent="2" name="sqlite_master">
|
||||
<System>1</System>
|
||||
</table>
|
||||
<table id="18" parent="2" name="sqlite_sequence">
|
||||
<table id="19" parent="2" name="sqlite_sequence">
|
||||
<System>1</System>
|
||||
</table>
|
||||
<table id="19" parent="2" name="sync"/>
|
||||
<column id="20" parent="6" name="apiTokenId">
|
||||
<table id="20" parent="2" name="sync"/>
|
||||
<column id="21" parent="6" name="apiTokenId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="21" parent="6" name="token">
|
||||
<column id="22" parent="6" name="token">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="22" parent="6" name="dateCreated">
|
||||
<column id="23" parent="6" name="dateCreated">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="23" parent="6" name="isDeleted">
|
||||
<column id="24" parent="6" name="isDeleted">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="24" parent="6" name="hash">
|
||||
<column id="25" parent="6" name="hash">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="25" parent="6" name="sqlite_autoindex_api_tokens_1">
|
||||
<index id="26" parent="6" name="sqlite_autoindex_api_tokens_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>apiTokenId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="26" parent="6">
|
||||
<key id="27" parent="6">
|
||||
<ColNames>apiTokenId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="27" parent="7" name="branchId">
|
||||
<column id="28" parent="7" name="branchId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="28" parent="7" name="noteId">
|
||||
<column id="29" parent="7" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="29" parent="7" name="parentNoteId">
|
||||
<column id="30" parent="7" name="parentNoteId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="30" parent="7" name="notePosition">
|
||||
<column id="31" parent="7" name="notePosition">
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="31" parent="7" name="prefix">
|
||||
<column id="32" parent="7" name="prefix">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="32" parent="7" name="isExpanded">
|
||||
<column id="33" parent="7" name="isExpanded">
|
||||
<Position>6</Position>
|
||||
<DataType>BOOLEAN|0s</DataType>
|
||||
</column>
|
||||
<column id="33" parent="7" name="isDeleted">
|
||||
<column id="34" parent="7" name="isDeleted">
|
||||
<Position>7</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="34" parent="7" name="dateModified">
|
||||
<column id="35" parent="7" name="dateModified">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="35" parent="7" name="hash">
|
||||
<column id="36" parent="7" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="36" parent="7" name="dateCreated">
|
||||
<column id="37" parent="7" name="dateCreated">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'1970-01-01T00:00:00.000Z'</DefaultExpression>
|
||||
</column>
|
||||
<index id="37" parent="7" name="sqlite_autoindex_branches_1">
|
||||
<index id="38" parent="7" name="sqlite_autoindex_branches_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>branchId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="38" parent="7" name="IDX_branches_noteId_parentNoteId">
|
||||
<index id="39" parent="7" name="IDX_branches_noteId_parentNoteId">
|
||||
<ColNames>noteId
|
||||
parentNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="39" parent="7" name="IDX_branches_noteId">
|
||||
<index id="40" parent="7" name="IDX_branches_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="40" parent="7" name="IDX_branches_parentNoteId">
|
||||
<index id="41" parent="7" name="IDX_branches_parentNoteId">
|
||||
<ColNames>parentNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="41" parent="7">
|
||||
<key id="42" parent="7">
|
||||
<ColNames>branchId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="42" parent="8" name="eventId">
|
||||
<column id="43" parent="8" name="eventId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="43" parent="8" name="noteId">
|
||||
<column id="44" parent="8" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="44" parent="8" name="comment">
|
||||
<column id="45" parent="8" name="comment">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="45" parent="8" name="dateCreated">
|
||||
<column id="46" parent="8" name="dateCreated">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="46" parent="8" name="sqlite_autoindex_event_log_1">
|
||||
<index id="47" parent="8" name="sqlite_autoindex_event_log_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>eventId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="47" parent="8">
|
||||
<key id="48" parent="8">
|
||||
<ColNames>eventId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_event_log_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="48" parent="9" name="imageId">
|
||||
<column id="49" parent="9" name="imageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="49" parent="9" name="format">
|
||||
<column id="50" parent="9" name="format">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="50" parent="9" name="checksum">
|
||||
<column id="51" parent="9" name="checksum">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="51" parent="9" name="name">
|
||||
<column id="52" parent="9" name="name">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="52" parent="9" name="data">
|
||||
<column id="53" parent="9" name="data">
|
||||
<Position>5</Position>
|
||||
<DataType>BLOB|0s</DataType>
|
||||
</column>
|
||||
<column id="53" parent="9" name="isDeleted">
|
||||
<column id="54" parent="9" name="isDeleted">
|
||||
<Position>6</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="54" parent="9" name="dateModified">
|
||||
<column id="55" parent="9" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="55" parent="9" name="dateCreated">
|
||||
<column id="56" parent="9" name="dateCreated">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="56" parent="9" name="hash">
|
||||
<column id="57" parent="9" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="57" parent="9" name="sqlite_autoindex_images_1">
|
||||
<index id="58" parent="9" name="sqlite_autoindex_images_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="58" parent="9">
|
||||
<key id="59" parent="9">
|
||||
<ColNames>imageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="59" parent="10" name="labelId">
|
||||
<column id="60" parent="10" name="labelId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="60" parent="10" name="noteId">
|
||||
<column id="61" parent="10" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="61" parent="10" name="name">
|
||||
<column id="62" parent="10" name="name">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="62" parent="10" name="value">
|
||||
<column id="63" parent="10" name="value">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="63" parent="10" name="position">
|
||||
<column id="64" parent="10" name="position">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="64" parent="10" name="dateCreated">
|
||||
<column id="65" parent="10" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="65" parent="10" name="dateModified">
|
||||
<column id="66" parent="10" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="66" parent="10" name="isDeleted">
|
||||
<column id="67" parent="10" name="isDeleted">
|
||||
<Position>8</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="67" parent="10" name="hash">
|
||||
<column id="68" parent="10" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="68" parent="10" name="sqlite_autoindex_labels_1">
|
||||
<index id="69" parent="10" name="sqlite_autoindex_labels_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>labelId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="69" parent="10" name="IDX_labels_noteId">
|
||||
<index id="70" parent="10" name="IDX_labels_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="70" parent="10" name="IDX_labels_name_value">
|
||||
<index id="71" parent="10" name="IDX_labels_name_value">
|
||||
<ColNames>name
|
||||
value</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="71" parent="10">
|
||||
<key id="72" parent="10">
|
||||
<ColNames>labelId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="72" parent="11" name="noteImageId">
|
||||
<column id="73" parent="11" name="noteImageId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="73" parent="11" name="noteId">
|
||||
<column id="74" parent="11" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="74" parent="11" name="imageId">
|
||||
<column id="75" parent="11" name="imageId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="75" parent="11" name="isDeleted">
|
||||
<column id="76" parent="11" name="isDeleted">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="76" parent="11" name="dateModified">
|
||||
<column id="77" parent="11" name="dateModified">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="77" parent="11" name="dateCreated">
|
||||
<column id="78" parent="11" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="78" parent="11" name="hash">
|
||||
<column id="79" parent="11" name="hash">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="79" parent="11" name="sqlite_autoindex_note_images_1">
|
||||
<index id="80" parent="11" name="sqlite_autoindex_note_images_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteImageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="80" parent="11" name="IDX_note_images_noteId_imageId">
|
||||
<index id="81" parent="11" name="IDX_note_images_noteId_imageId">
|
||||
<ColNames>noteId
|
||||
imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="81" parent="11" name="IDX_note_images_noteId">
|
||||
<index id="82" parent="11" name="IDX_note_images_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="82" parent="11" name="IDX_note_images_imageId">
|
||||
<index id="83" parent="11" name="IDX_note_images_imageId">
|
||||
<ColNames>imageId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="83" parent="11">
|
||||
<key id="84" parent="11">
|
||||
<ColNames>noteImageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="84" parent="12" name="noteRevisionId">
|
||||
<column id="85" parent="12" name="noteRevisionId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="85" parent="12" name="noteId">
|
||||
<column id="86" parent="12" name="noteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="86" parent="12" name="title">
|
||||
<column id="87" parent="12" name="title">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="87" parent="12" name="content">
|
||||
<column id="88" parent="12" name="content">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="88" parent="12" name="isProtected">
|
||||
<column id="89" parent="12" name="isProtected">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="89" parent="12" name="dateModifiedFrom">
|
||||
<column id="90" parent="12" name="dateModifiedFrom">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="90" parent="12" name="dateModifiedTo">
|
||||
<column id="91" parent="12" name="dateModifiedTo">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="91" parent="12" name="type">
|
||||
<column id="92" parent="12" name="type">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="92" parent="12" name="mime">
|
||||
<column id="93" parent="12" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>''</DefaultExpression>
|
||||
</column>
|
||||
<column id="93" parent="12" name="hash">
|
||||
<column id="94" parent="12" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="94" parent="12" name="sqlite_autoindex_note_revisions_1">
|
||||
<index id="95" parent="12" name="sqlite_autoindex_note_revisions_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteRevisionId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="95" parent="12" name="IDX_note_revisions_noteId">
|
||||
<index id="96" parent="12" name="IDX_note_revisions_noteId">
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="96" parent="12" name="IDX_note_revisions_dateModifiedFrom">
|
||||
<index id="97" parent="12" name="IDX_note_revisions_dateModifiedFrom">
|
||||
<ColNames>dateModifiedFrom</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="97" parent="12" name="IDX_note_revisions_dateModifiedTo">
|
||||
<index id="98" parent="12" name="IDX_note_revisions_dateModifiedTo">
|
||||
<ColNames>dateModifiedTo</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="98" parent="12">
|
||||
<key id="99" parent="12">
|
||||
<ColNames>noteRevisionId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="99" parent="13" name="noteId">
|
||||
<column id="100" parent="13" name="noteId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="100" parent="13" name="title">
|
||||
<column id="101" parent="13" name="title">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>"unnamed"</DefaultExpression>
|
||||
</column>
|
||||
<column id="101" parent="13" name="content">
|
||||
<column id="102" parent="13" name="content">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="102" parent="13" name="isProtected">
|
||||
<column id="103" parent="13" name="isProtected">
|
||||
<Position>4</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="103" parent="13" name="isDeleted">
|
||||
<column id="104" parent="13" name="isDeleted">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="104" parent="13" name="dateCreated">
|
||||
<column id="105" parent="13" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="105" parent="13" name="dateModified">
|
||||
<column id="106" parent="13" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="106" parent="13" name="type">
|
||||
<column id="107" parent="13" name="type">
|
||||
<Position>8</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text'</DefaultExpression>
|
||||
</column>
|
||||
<column id="107" parent="13" name="mime">
|
||||
<column id="108" parent="13" name="mime">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'text/html'</DefaultExpression>
|
||||
</column>
|
||||
<column id="108" parent="13" name="hash">
|
||||
<column id="109" parent="13" name="hash">
|
||||
<Position>10</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="109" parent="13" name="sqlite_autoindex_notes_1">
|
||||
<index id="110" parent="13" name="sqlite_autoindex_notes_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>noteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="110" parent="13" name="IDX_notes_type">
|
||||
<index id="111" parent="13" name="IDX_notes_type">
|
||||
<ColNames>type</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="111" parent="13">
|
||||
<key id="112" parent="13">
|
||||
<ColNames>noteId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="112" parent="14" name="optionId">
|
||||
<column id="113" parent="14" name="name">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="113" parent="14" name="name">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="114" parent="14" name="value">
|
||||
<Position>3</Position>
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
</column>
|
||||
<column id="115" parent="14" name="dateModified">
|
||||
<Position>4</Position>
|
||||
<Position>3</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
</column>
|
||||
<column id="116" parent="14" name="isSynced">
|
||||
<Position>5</Position>
|
||||
<Position>4</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="117" parent="14" name="hash">
|
||||
<Position>6</Position>
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<column id="118" parent="14" name="dateCreated">
|
||||
<Position>7</Position>
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>'1970-01-01T00:00:00.000Z'</DefaultExpression>
|
||||
</column>
|
||||
<index id="119" parent="14" name="sqlite_autoindex_options_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>optionId</ColNames>
|
||||
<ColNames>name</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="120" parent="14">
|
||||
<ColNames>optionId</ColNames>
|
||||
<ColNames>name</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
|
||||
</key>
|
||||
@@ -587,90 +583,156 @@ imageId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="128" parent="16" name="sourceId">
|
||||
<column id="128" parent="16" name="relationId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="129" parent="16" name="dateCreated">
|
||||
<column id="129" parent="16" name="sourceNoteId">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="130" parent="16" name="sqlite_autoindex_source_ids_1">
|
||||
<column id="130" parent="16" name="name">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="131" parent="16" name="targetNoteId">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="132" parent="16" name="position">
|
||||
<Position>5</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>0</DefaultExpression>
|
||||
</column>
|
||||
<column id="133" parent="16" name="dateCreated">
|
||||
<Position>6</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="134" parent="16" name="dateModified">
|
||||
<Position>7</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="135" parent="16" name="isDeleted">
|
||||
<Position>8</Position>
|
||||
<DataType>INT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="136" parent="16" name="hash">
|
||||
<Position>9</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<DefaultExpression>""</DefaultExpression>
|
||||
</column>
|
||||
<index id="137" parent="16" name="sqlite_autoindex_relations_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>relationId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="138" parent="16" name="IDX_relation_sourceNoteId">
|
||||
<ColNames>sourceNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<index id="139" parent="16" name="IDX_relation_targetNoteId">
|
||||
<ColNames>targetNoteId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="140" parent="16">
|
||||
<ColNames>relationId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_relations_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="141" parent="17" name="sourceId">
|
||||
<Position>1</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="142" parent="17" name="dateCreated">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="143" parent="17" name="sqlite_autoindex_source_ids_1">
|
||||
<NameSurrogate>1</NameSurrogate>
|
||||
<ColNames>sourceId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="131" parent="16">
|
||||
<key id="144" parent="17">
|
||||
<ColNames>sourceId</ColNames>
|
||||
<Primary>1</Primary>
|
||||
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
|
||||
</key>
|
||||
<column id="132" parent="17" name="type">
|
||||
<column id="145" parent="18" name="type">
|
||||
<Position>1</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="133" parent="17" name="name">
|
||||
<column id="146" parent="18" name="name">
|
||||
<Position>2</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="134" parent="17" name="tbl_name">
|
||||
<column id="147" parent="18" name="tbl_name">
|
||||
<Position>3</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="135" parent="17" name="rootpage">
|
||||
<column id="148" parent="18" name="rootpage">
|
||||
<Position>4</Position>
|
||||
<DataType>integer|0s</DataType>
|
||||
</column>
|
||||
<column id="136" parent="17" name="sql">
|
||||
<column id="149" parent="18" name="sql">
|
||||
<Position>5</Position>
|
||||
<DataType>text|0s</DataType>
|
||||
</column>
|
||||
<column id="137" parent="18" name="name">
|
||||
<column id="150" parent="19" name="name">
|
||||
<Position>1</Position>
|
||||
</column>
|
||||
<column id="138" parent="18" name="seq">
|
||||
<column id="151" parent="19" name="seq">
|
||||
<Position>2</Position>
|
||||
</column>
|
||||
<column id="139" parent="19" name="id">
|
||||
<column id="152" parent="20" name="id">
|
||||
<Position>1</Position>
|
||||
<DataType>INTEGER|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
<SequenceIdentity>1</SequenceIdentity>
|
||||
</column>
|
||||
<column id="140" parent="19" name="entityName">
|
||||
<column id="153" parent="20" name="entityName">
|
||||
<Position>2</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="141" parent="19" name="entityId">
|
||||
<column id="154" parent="20" name="entityId">
|
||||
<Position>3</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="142" parent="19" name="sourceId">
|
||||
<column id="155" parent="20" name="sourceId">
|
||||
<Position>4</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<column id="143" parent="19" name="syncDate">
|
||||
<column id="156" parent="20" name="syncDate">
|
||||
<Position>5</Position>
|
||||
<DataType>TEXT|0s</DataType>
|
||||
<NotNull>1</NotNull>
|
||||
</column>
|
||||
<index id="144" parent="19" name="IDX_sync_entityName_entityId">
|
||||
<index id="157" parent="20" name="IDX_sync_entityName_entityId">
|
||||
<ColNames>entityName
|
||||
entityId</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<index id="145" parent="19" name="IDX_sync_syncDate">
|
||||
<index id="158" parent="20" name="IDX_sync_syncDate">
|
||||
<ColNames>syncDate</ColNames>
|
||||
<ColumnCollations></ColumnCollations>
|
||||
</index>
|
||||
<key id="146" parent="19">
|
||||
<key id="159" parent="20">
|
||||
<ColNames>id</ColNames>
|
||||
<Primary>1</Primary>
|
||||
</key>
|
||||
|
||||
16
bin/build-pkg.sh
Executable file
16
bin/build-pkg.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PKG_DIR=dist/trilium-linux-x64-server
|
||||
|
||||
mkdir $PKG_DIR
|
||||
|
||||
pkg . --targets node8-linux-x64 --output ${PKG_DIR}/trilium
|
||||
|
||||
chmod +x ${PKG_DIR}/trilium
|
||||
|
||||
cp node_modules/sqlite3/lib/binding/node-v57-linux-x64/node_sqlite3.node ${PKG_DIR}/
|
||||
cp node_modules/scrypt/build/Release/scrypt.node ${PKG_DIR}/
|
||||
|
||||
cd dist
|
||||
|
||||
7z a trilium-linux-x64-server.7z trilium-linux-x64-server
|
||||
@@ -9,9 +9,3 @@ https=false
|
||||
# path to certificate (run "bash generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
|
||||
certPath=
|
||||
keyPath=
|
||||
|
||||
[Sync]
|
||||
syncServerHost=
|
||||
syncServerTimeout=10000
|
||||
syncProxy=
|
||||
syncServerCertificate=
|
||||
8
db/migrations/0101__add_sync_options.sql
Normal file
8
db/migrations/0101__add_sync_options.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('syncServerHost', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('syncServerTimeout', '5000', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('syncProxy', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
2
db/migrations/0102__fix_sync_entityIds.sql
Normal file
2
db/migrations/0102__fix_sync_entityIds.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DELETE FROM sync WHERE entityName = 'note_tree';
|
||||
DELETE FROM sync WHERE entityName = 'attributes';
|
||||
2
db/migrations/0103__add_initialized_option.sql
Normal file
2
db/migrations/0103__add_initialized_option.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('initialized', 'true', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
4
db/migrations/0104__fill_sync_rows_for_options.js
Normal file
4
db/migrations/0104__fill_sync_rows_for_options.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const syncTableService = require('../../src/services/sync_table');
|
||||
|
||||
// options has not been filled so far which caused problems with clean-slate sync.
|
||||
module.exports = async () => await syncTableService.fillAllSyncRows();
|
||||
@@ -0,0 +1,2 @@
|
||||
UPDATE notes SET content = '' WHERE isDeleted = 1;
|
||||
UPDATE note_revisions SET content = '' WHERE (SELECT isDeleted FROM notes WHERE noteId = note_revisions.noteId) = 1;
|
||||
15
db/migrations/0106__add_relations_table.sql
Normal file
15
db/migrations/0106__add_relations_table.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE relations
|
||||
(
|
||||
relationId TEXT not null primary key,
|
||||
sourceNoteId TEXT not null,
|
||||
name TEXT not null,
|
||||
targetNoteId TEXT not null,
|
||||
position INT default 0 not null,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_relation_sourceNoteId
|
||||
on relations (sourceNoteId);
|
||||
CREATE INDEX IDX_relation_targetNoteId
|
||||
on relations (targetNoteId);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE relations ADD isInheritable int DEFAULT 0 NULL;
|
||||
@@ -134,3 +134,19 @@ CREATE TABLE IF NOT EXISTS "options"
|
||||
hash TEXT default "" not null,
|
||||
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
|
||||
);
|
||||
CREATE TABLE relations
|
||||
(
|
||||
relationId TEXT not null primary key,
|
||||
sourceNoteId TEXT not null,
|
||||
name TEXT not null,
|
||||
targetNoteId TEXT not null,
|
||||
isInheritable int DEFAULT 0 NULL,
|
||||
position INT default 0 not null,
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_relation_sourceNoteId
|
||||
on relations (sourceNoteId);
|
||||
CREATE INDEX IDX_relation_targetNoteId
|
||||
on relations (targetNoteId);
|
||||
|
||||
10
package.json
10
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -20,8 +20,7 @@
|
||||
"start-forge": "electron-forge start",
|
||||
"package-forge": "electron-forge package",
|
||||
"make-forge": "electron-forge make",
|
||||
"publish-forge": "electron-forge publish",
|
||||
"build-pkg": "pkg . --targets node8-linux-x64 --output dist/trilium-linux-x64-server.elf"
|
||||
"publish-forge": "electron-forge publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.1.3",
|
||||
@@ -119,7 +118,10 @@
|
||||
"assets": [
|
||||
"./db/**/*",
|
||||
"./src/public/**/*",
|
||||
"./src/views/**/*"
|
||||
"./src/views/**/*",
|
||||
"./node_modules/mozjpeg/vendor/*",
|
||||
"./node_modules/pngquant-bin/vendor/*",
|
||||
"./node_modules/giflossy/vendor/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ const sessionParser = session({
|
||||
cookie: {
|
||||
// path: "/",
|
||||
httpOnly: true,
|
||||
maxAge: 1800000
|
||||
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
|
||||
},
|
||||
store: new FileStore({
|
||||
ttl: 30 * 24 * 3600,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
const utils = require('../services/utils');
|
||||
const repository = require('../services/repository');
|
||||
|
||||
class Entity {
|
||||
constructor(row = {}) {
|
||||
@@ -25,7 +24,7 @@ class Entity {
|
||||
}
|
||||
|
||||
async save() {
|
||||
await repository.updateEntity(this);
|
||||
await require('../services/repository').updateEntity(this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const Image = require('../entities/image');
|
||||
const NoteImage = require('../entities/note_image');
|
||||
const Branch = require('../entities/branch');
|
||||
const Label = require('../entities/label');
|
||||
const Relation = require('../entities/relation');
|
||||
const RecentNote = require('../entities/recent_note');
|
||||
const ApiToken = require('../entities/api_token');
|
||||
const Option = require('../entities/option');
|
||||
@@ -15,6 +16,9 @@ function createEntityFromRow(row) {
|
||||
if (row.labelId) {
|
||||
entity = new Label(row);
|
||||
}
|
||||
else if (row.relationId) {
|
||||
entity = new Relation(row);
|
||||
}
|
||||
else if (row.noteRevisionId) {
|
||||
entity = new NoteRevision(row);
|
||||
}
|
||||
|
||||
44
src/entities/relation.js
Normal file
44
src/entities/relation.js
Normal file
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
const Entity = require('./entity');
|
||||
const repository = require('../services/repository');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
const sql = require('../services/sql');
|
||||
|
||||
class Relation extends Entity {
|
||||
static get tableName() { return "relations"; }
|
||||
static get primaryKeyName() { return "relationId"; }
|
||||
static get hashedProperties() { return ["relationId", "sourceNoteId", "name", "targetNoteId", "isInheritable", "dateModified", "dateCreated"]; }
|
||||
|
||||
async getSourceNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.sourceNoteId]);
|
||||
}
|
||||
|
||||
async getTargetNote() {
|
||||
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.targetNoteId]);
|
||||
}
|
||||
|
||||
async beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
if (this.position === undefined) {
|
||||
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM relations WHERE sourceNoteId = ?`, [this.sourceNoteId]);
|
||||
}
|
||||
|
||||
if (!this.isInheritable) {
|
||||
this.isInheritable = false;
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.nowDate();
|
||||
}
|
||||
|
||||
this.dateModified = dateUtils.nowDate();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Relation;
|
||||
BIN
src/public/images/icons/clock-16.png
Normal file
BIN
src/public/images/icons/clock-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 381 B |
@@ -15,6 +15,7 @@ const $prefixFormGroup = $("#add-link-prefix-form-group");
|
||||
const $linkTypeDiv = $("#add-link-type-div");
|
||||
const $linkTypes = $("input[name='add-link-type']");
|
||||
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
|
||||
const $showRecentNotesButton = $("#add-link-show-recent-notes");
|
||||
|
||||
function setLinkType(linkType) {
|
||||
$linkTypes.each(function () {
|
||||
@@ -53,12 +54,17 @@ async function showDialog() {
|
||||
$linkTitle.val(noteTitle);
|
||||
}
|
||||
|
||||
$autoComplete.autocomplete({
|
||||
await $autoComplete.autocomplete({
|
||||
source: async function(request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result);
|
||||
response(result.map(row => {
|
||||
return {
|
||||
label: row.label,
|
||||
value: row.label + ' (' + row.value + ')'
|
||||
}
|
||||
}));
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
@@ -67,10 +73,14 @@ async function showDialog() {
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 2,
|
||||
change: async () => {
|
||||
const val = $autoComplete.val();
|
||||
const notePath = linkService.getNodePathFromLabel(val);
|
||||
minLength: 0,
|
||||
change: async (event, ui) => {
|
||||
if (!ui.item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notePath = linkService.getNotePathFromLabel(ui.item.value);
|
||||
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
@@ -81,21 +91,30 @@ async function showDialog() {
|
||||
await setDefaultLinkTitle(noteId);
|
||||
}
|
||||
},
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// 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: async (event, ui) => {
|
||||
const notePath = linkService.getNodePathFromLabel(ui.item.value);
|
||||
const notePath = linkService.getNotePathFromLabel(ui.item.value);
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
await setDefaultLinkTitle(noteId);
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
showRecentNotes();
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
const value = $autoComplete.val();
|
||||
|
||||
const notePath = linkService.getNodePathFromLabel(value);
|
||||
const notePath = linkService.getNotePathFromLabel(value);
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
if (notePath) {
|
||||
@@ -153,8 +172,14 @@ function linkTypeChanged() {
|
||||
$linkTypeDiv.toggle(!hasSelection());
|
||||
}
|
||||
|
||||
function showRecentNotes() {
|
||||
$autoComplete.autocomplete("search", "");
|
||||
}
|
||||
|
||||
$linkTypes.change(linkTypeChanged);
|
||||
|
||||
$showRecentNotesButton.click(showRecentNotes);
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
@@ -1,13 +1,11 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import linkService from '../services/link.js';
|
||||
import server from '../services/server.js';
|
||||
import searchNotesService from '../services/search_notes.js';
|
||||
|
||||
const $dialog = $("#jump-to-note-dialog");
|
||||
const $autoComplete = $("#jump-to-note-autocomplete");
|
||||
const $form = $("#jump-to-note-form");
|
||||
const $jumpToNoteButton = $("#jump-to-note-button");
|
||||
const $showInFullTextButton = $("#show-in-full-text-button");
|
||||
const $showRecentNotesButton = $("#jump-to-note-show-recent-notes");
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
@@ -16,7 +14,8 @@ async function showDialog() {
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 800
|
||||
width: 800,
|
||||
position: { my: "center top+100", at: "top", of: window }
|
||||
});
|
||||
|
||||
await $autoComplete.autocomplete({
|
||||
@@ -34,25 +33,22 @@ async function showDialog() {
|
||||
}
|
||||
},
|
||||
focus: function(event, ui) {
|
||||
return $(ui.item).val() !== 'No results';
|
||||
event.preventDefault();
|
||||
},
|
||||
minLength: 2
|
||||
minLength: 0,
|
||||
autoFocus: true,
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
|
||||
treeService.activateNode(ui.item.value);
|
||||
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedNotePath() {
|
||||
const val = $autoComplete.val();
|
||||
return linkService.getNodePathFromLabel(val);
|
||||
}
|
||||
|
||||
function goToNote() {
|
||||
const notePath = getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
treeService.activateNode(notePath);
|
||||
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
showRecentNotes();
|
||||
}
|
||||
|
||||
function showInFullText(e) {
|
||||
@@ -69,16 +65,14 @@ function showInFullText(e) {
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
goToNote();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$jumpToNoteButton.click(goToNote);
|
||||
function showRecentNotes() {
|
||||
$autoComplete.autocomplete("search", "");
|
||||
}
|
||||
|
||||
$showInFullTextButton.click(showInFullText);
|
||||
|
||||
$showRecentNotesButton.click(showRecentNotes);
|
||||
|
||||
$dialog.bind('keydown', 'ctrl+return', showInFullText);
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import utils from '../services/utils.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
|
||||
@@ -168,9 +167,9 @@ async function showDialog() {
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(labelsModel, document.getElementById('labels-dialog'));
|
||||
ko.applyBindings(labelsModel, $dialog[0]);
|
||||
|
||||
$(document).on('focus', '.label-name', function (e) {
|
||||
$dialog.on('focus', '.label-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
@@ -188,7 +187,7 @@ $(document).on('focus', '.label-name', function (e) {
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
$(document).on('focus', '.label-value', async function (e) {
|
||||
$dialog.on('focus', '.label-value', async function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
const labelName = $(this).parent().parent().find('.label-name').val();
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ async function showDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOptions(optionName, optionValue) {
|
||||
await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue));
|
||||
async function saveOptions(options) {
|
||||
await server.put('options', options);
|
||||
|
||||
infoService.showMessage("Options change have been saved.");
|
||||
}
|
||||
@@ -129,16 +129,15 @@ addTabHandler((function() {
|
||||
addTabHandler((function() {
|
||||
const $form = $("#protected-session-timeout-form");
|
||||
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
|
||||
const optionName = 'protectedSessionTimeout';
|
||||
|
||||
function optionsLoaded(options) {
|
||||
$protectedSessionTimeout.val(options[optionName]);
|
||||
$protectedSessionTimeout.val(options['protectedSessionTimeout']);
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
const protectedSessionTimeout = $protectedSessionTimeout.val();
|
||||
|
||||
saveOptions(optionName, protectedSessionTimeout).then(() => {
|
||||
saveOptions({ 'protectedSessionTimeout': protectedSessionTimeout }).then(() => {
|
||||
protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout);
|
||||
});
|
||||
|
||||
@@ -153,14 +152,13 @@ addTabHandler((function() {
|
||||
addTabHandler((function () {
|
||||
const $form = $("#note-revision-snapshot-time-interval-form");
|
||||
const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
|
||||
const optionName = 'noteRevisionSnapshotTimeInterval';
|
||||
|
||||
function optionsLoaded(options) {
|
||||
$timeInterval.val(options[optionName]);
|
||||
$timeInterval.val(options['noteRevisionSnapshotTimeInterval']);
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
saveOptions(optionName, $timeInterval.val());
|
||||
saveOptions({ 'noteRevisionSnapshotTimeInterval': $timeInterval.val() });
|
||||
|
||||
return false;
|
||||
});
|
||||
@@ -189,6 +187,57 @@ addTabHandler((async function () {
|
||||
return {};
|
||||
})());
|
||||
|
||||
addTabHandler((function() {
|
||||
const $form = $("#sync-setup-form");
|
||||
const $syncServerHost = $("#sync-server-host");
|
||||
const $syncServerTimeout = $("#sync-server-timeout");
|
||||
const $syncProxy = $("#sync-proxy");
|
||||
const $testSyncButton = $("#test-sync-button");
|
||||
const $syncToServerButton = $("#sync-to-server-button");
|
||||
|
||||
function optionsLoaded(options) {
|
||||
$syncServerHost.val(options['syncServerHost']);
|
||||
$syncServerTimeout.val(options['syncServerTimeout']);
|
||||
$syncProxy.val(options['syncProxy']);
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
saveOptions({
|
||||
'syncServerHost': $syncServerHost.val(),
|
||||
'syncServerTimeout': $syncServerTimeout.val(),
|
||||
'syncProxy': $syncProxy.val()
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$testSyncButton.click(async () => {
|
||||
const result = await server.post('sync/test');
|
||||
|
||||
if (result.connection === "Success") {
|
||||
infoService.showMessage("Sync server handshake has been successful");
|
||||
}
|
||||
else {
|
||||
infoService.showError("Sync server handshake failed, error: " + result.error);
|
||||
}
|
||||
});
|
||||
|
||||
$syncToServerButton.click(async () => {
|
||||
const resp = await server.post("setup/sync-to-server");
|
||||
|
||||
if (resp.success) {
|
||||
infoService.showMessage("Sync has been established to the server instance. It will take some time to finish.");
|
||||
}
|
||||
else {
|
||||
infoService.showError('Sync setup failed: ' + resp.error);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
optionsLoaded
|
||||
};
|
||||
})());
|
||||
|
||||
addTabHandler((async function () {
|
||||
const $forceFullSyncButton = $("#force-full-sync-button");
|
||||
const $fillSyncRowsButton = $("#fill-sync-rows-button");
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import server from '../services/server.js';
|
||||
|
||||
const $dialog = $("#recent-notes-dialog");
|
||||
const $searchInput = $('#recent-notes-search-input');
|
||||
|
||||
function addRecentNote(branchId, notePath) {
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (notePath && notePath === treeService.getCurrentNotePath()) {
|
||||
const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 100,
|
||||
position: { my: "center top+100", at: "top", of: window }
|
||||
});
|
||||
|
||||
$searchInput.val('');
|
||||
|
||||
const result = await server.get('recent-notes');
|
||||
|
||||
// remove the current note
|
||||
const recNotes = result.filter(note => note.notePath !== treeService.getCurrentNotePath());
|
||||
|
||||
const items = recNotes.map(rn => {
|
||||
return {
|
||||
label: rn.title,
|
||||
value: rn.notePath
|
||||
};
|
||||
});
|
||||
|
||||
$searchInput.autocomplete({
|
||||
source: items,
|
||||
minLength: 0,
|
||||
autoFocus: true,
|
||||
select: function (event, ui) {
|
||||
treeService.activateNode(ui.item.value);
|
||||
|
||||
$searchInput.autocomplete('destroy');
|
||||
$dialog.dialog('close');
|
||||
},
|
||||
focus: function (event, ui) {
|
||||
event.preventDefault();
|
||||
},
|
||||
close: function (event, ui) {
|
||||
if (event.keyCode === 27) { // escape closes dialog
|
||||
$searchInput.autocomplete('destroy');
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
else {
|
||||
// keep autocomplete open
|
||||
// we're kind of abusing autocomplete to work in a way which it's not designed for
|
||||
$searchInput.autocomplete("search", "");
|
||||
}
|
||||
},
|
||||
create: () => $searchInput.autocomplete("search", ""),
|
||||
classes: {
|
||||
"ui-autocomplete": "recent-notes-autocomplete"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
showDialog,
|
||||
addRecentNote
|
||||
};
|
||||
250
src/public/javascripts/dialogs/relations.js
Normal file
250
src/public/javascripts/dialogs/relations.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
import linkService from "../services/link.js";
|
||||
import treeUtils from "../services/tree_utils.js";
|
||||
|
||||
const $dialog = $("#relations-dialog");
|
||||
const $saveRelationsButton = $("#save-relations-button");
|
||||
const $relationsBody = $('#relations-table tbody');
|
||||
|
||||
const relationsModel = new RelationsModel();
|
||||
let relationNames = [];
|
||||
|
||||
function RelationsModel() {
|
||||
const self = this;
|
||||
|
||||
this.relations = ko.observableArray();
|
||||
|
||||
this.updateRelationPositions = function() {
|
||||
let position = 0;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// relations in the viewmodel (self.relations()) stays the same
|
||||
$relationsBody.find('input[name="position"]').each(function() {
|
||||
const relation = self.getTargetRelation(this);
|
||||
|
||||
relation().position = position++;
|
||||
});
|
||||
};
|
||||
|
||||
async function showRelations(relations) {
|
||||
for (const relation of relations) {
|
||||
relation.targetNoteId = await treeUtils.getNoteTitle(relation.targetNoteId) + " (" + relation.targetNoteId + ")";
|
||||
}
|
||||
|
||||
self.relations(relations.map(ko.observable));
|
||||
}
|
||||
|
||||
this.loadRelations = async function() {
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const relations = await server.get('notes/' + noteId + '/relations');
|
||||
|
||||
await showRelations(relations);
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
relationNames = await server.get('relations/names');
|
||||
|
||||
// relation might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".relation-name:last").focus(), 100);
|
||||
|
||||
$relationsBody.sortable({
|
||||
handle: '.handle',
|
||||
containment: $relationsBody,
|
||||
update: this.updateRelationPositions
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteRelation = function(data, event) {
|
||||
const relation = self.getTargetRelation(event.target);
|
||||
const relationData = relation();
|
||||
|
||||
if (relationData) {
|
||||
relationData.isDeleted = 1;
|
||||
|
||||
relation(relationData);
|
||||
|
||||
addLastEmptyRow();
|
||||
}
|
||||
};
|
||||
|
||||
function isValid() {
|
||||
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
|
||||
if (self.isEmptyName(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
$saveRelationsButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.updateRelationPositions();
|
||||
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const relationsToSave = self.relations()
|
||||
.map(relation => relation())
|
||||
.filter(relation => relation.relationId !== "" || relation.name !== "");
|
||||
|
||||
relationsToSave.forEach(relation => relation.targetNoteId = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(relation.targetNoteId)));
|
||||
|
||||
console.log(relationsToSave);
|
||||
|
||||
const relations = await server.put('notes/' + noteId + '/relations', relationsToSave);
|
||||
|
||||
await showRelations(relations);
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
infoService.showMessage("Relations have been saved.");
|
||||
|
||||
noteDetailService.loadRelationList();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
const relations = self.relations().filter(attr => attr().isDeleted === 0);
|
||||
const last = relations.length === 0 ? null : relations[relations.length - 1]();
|
||||
|
||||
if (!last || last.name.trim() !== "" || last.targetNoteId !== "") {
|
||||
self.relations.push(ko.observable({
|
||||
relationId: '',
|
||||
name: '',
|
||||
targetNoteId: '',
|
||||
isInheritable: 0,
|
||||
isDeleted: 0,
|
||||
position: 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.relationChanged = function (data, event) {
|
||||
addLastEmptyRow();
|
||||
|
||||
const relation = self.getTargetRelation(event.target);
|
||||
|
||||
relation.valueHasMutated();
|
||||
};
|
||||
|
||||
this.isNotUnique = function(index) {
|
||||
const cur = self.relations()[index]();
|
||||
|
||||
if (cur.name.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
|
||||
const relation = relations[i]();
|
||||
|
||||
if (index !== i && cur.name === relation.name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.relations()[index]();
|
||||
|
||||
return cur.name.trim() === "" && (cur.relationId !== "" || cur.targetNoteId !== "");
|
||||
};
|
||||
|
||||
this.getTargetRelation = function(target) {
|
||||
const context = ko.contextFor(target);
|
||||
const index = context.$index();
|
||||
|
||||
return self.relations()[index];
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
await relationsModel.loadRelations();
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 900,
|
||||
height: 500
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(relationsModel, document.getElementById('relations-dialog'));
|
||||
|
||||
$dialog.on('focus', '.relation-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: relationNames.map(relation => {
|
||||
return {
|
||||
label: relation,
|
||||
value: relation
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
async function initNoteAutocomplete($el) {
|
||||
if (!$el.hasClass("ui-autocomplete-input")) {
|
||||
await $el.autocomplete({
|
||||
source: async function (request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result.map(row => {
|
||||
return {
|
||||
label: row.label,
|
||||
value: row.label + ' (' + row.value + ')'
|
||||
}
|
||||
}));
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 0,
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$dialog.on('focus', '.relation-target-note-id', async function () {
|
||||
await initNoteAutocomplete($(this));
|
||||
});
|
||||
|
||||
$dialog.on('click', '.relations-show-recent-notes', async function () {
|
||||
const $autocomplete = $(this).parent().find('.relation-target-note-id');
|
||||
|
||||
await initNoteAutocomplete($autocomplete);
|
||||
|
||||
$autocomplete.autocomplete("search", "");
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
1
src/public/javascripts/services/bootstrap.js
vendored
1
src/public/javascripts/services/bootstrap.js
vendored
@@ -4,7 +4,6 @@ import labelsDialog from '../dialogs/labels.js';
|
||||
import noteRevisionsDialog from '../dialogs/note_revisions.js';
|
||||
import noteSourceDialog from '../dialogs/note_source.js';
|
||||
import recentChangesDialog from '../dialogs/recent_changes.js';
|
||||
import recentNotesDialog from '../dialogs/recent_notes.js';
|
||||
import optionsDialog from '../dialogs/options.js';
|
||||
import sqlConsoleDialog from '../dialogs/sql_console.js';
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import ScriptContext from "./script_context.js";
|
||||
import server from "./server.js";
|
||||
|
||||
async function executeBundle(bundle) {
|
||||
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
|
||||
async function getAndExecuteBundle(noteId, targetNote = null) {
|
||||
const bundle = await server.get('script/bundle/' + noteId);
|
||||
|
||||
await executeBundle(bundle, targetNote);
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, targetNote) {
|
||||
const apiContext = ScriptContext(bundle.note, bundle.allNotes, targetNote);
|
||||
|
||||
return await (function () {
|
||||
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
|
||||
@@ -17,7 +23,17 @@ async function executeStartupBundles() {
|
||||
}
|
||||
}
|
||||
|
||||
async function executeRelationBundles(note, relationName) {
|
||||
const bundlesToRun = await server.get("script/relation/" + note.noteId + "/" + relationName);
|
||||
|
||||
for (const bundle of bundlesToRun) {
|
||||
await executeBundle(bundle, note);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
executeBundle,
|
||||
executeStartupBundles
|
||||
getAndExecuteBundle,
|
||||
executeStartupBundles,
|
||||
executeRelationBundles
|
||||
}
|
||||
@@ -121,7 +121,7 @@ const contextMenuOptions = {
|
||||
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "editBranchPrefix", parentNote.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "editBranchPrefix", isNotRoot && parentNote.type !== 'search');
|
||||
|
||||
// Activate node on right-click
|
||||
node.setActive();
|
||||
|
||||
@@ -6,13 +6,13 @@ import zoomService from "./zoom.js";
|
||||
import noteRevisionsDialog from "../dialogs/note_revisions.js";
|
||||
import optionsDialog from "../dialogs/options.js";
|
||||
import addLinkDialog from "../dialogs/add_link.js";
|
||||
import recentNotesDialog from "../dialogs/recent_notes.js";
|
||||
import jumpToNoteDialog from "../dialogs/jump_to_note.js";
|
||||
import noteSourceDialog from "../dialogs/note_source.js";
|
||||
import recentChangesDialog from "../dialogs/recent_changes.js";
|
||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
|
||||
import searchNotesService from "./search_notes.js";
|
||||
import labelsDialog from "../dialogs/labels.js";
|
||||
import relationsDialog from "../dialogs/relations.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
|
||||
function registerEntrypoints() {
|
||||
@@ -35,15 +35,15 @@ function registerEntrypoints() {
|
||||
$("#protected-session-on").click(protectedSessionService.enterProtectedSession);
|
||||
$("#protected-session-off").click(protectedSessionService.leaveProtectedSession);
|
||||
|
||||
$("#recent-notes-button").click(recentNotesDialog.showDialog);
|
||||
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
|
||||
|
||||
$("#toggle-search-button").click(searchNotesService.toggleSearch);
|
||||
utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch);
|
||||
|
||||
$(".show-labels-button").click(labelsDialog.showDialog);
|
||||
utils.bindShortcut('alt+l', labelsDialog.showDialog);
|
||||
|
||||
$(".show-relations-button").click(relationsDialog.showDialog);
|
||||
utils.bindShortcut('alt+r', relationsDialog.showDialog);
|
||||
|
||||
$("#options-button").click(optionsDialog.showDialog);
|
||||
|
||||
utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
|
||||
|
||||
@@ -13,7 +13,7 @@ function getNotePathFromLink(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function getNodePathFromLabel(label) {
|
||||
function getNotePathFromLabel(label) {
|
||||
const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
|
||||
|
||||
if (notePathMatch !== null) {
|
||||
@@ -97,7 +97,7 @@ $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToL
|
||||
$(document).on('dblclick', '#note-detail-text a', goToLink);
|
||||
|
||||
export default {
|
||||
getNodePathFromLabel,
|
||||
getNotePathFromLabel,
|
||||
getNotePathFromLink,
|
||||
createNoteLink,
|
||||
addLinkToEditor,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import utils from './utils.js';
|
||||
import infoService from "./info.js";
|
||||
|
||||
const $changesToPushCount = $("#changes-to-push-count");
|
||||
const $outstandingSyncsCount = $("#outstanding-syncs-count");
|
||||
|
||||
const messageHandlers = [];
|
||||
|
||||
@@ -43,7 +43,7 @@ function handleMessage(event) {
|
||||
messageHandler(syncData);
|
||||
}
|
||||
|
||||
$changesToPushCount.html(message.changesToPushCount);
|
||||
$outstandingSyncsCount.html(message.outstandingSyncs);
|
||||
}
|
||||
else if (message.type === 'sync-hash-check-failed') {
|
||||
infoService.showError("Sync check failed!", 60000);
|
||||
|
||||
@@ -7,6 +7,7 @@ import utils from './utils.js';
|
||||
import server from './server.js';
|
||||
import messagingService from "./messaging.js";
|
||||
import infoService from "./info.js";
|
||||
import linkService from "./link.js";
|
||||
import treeCache from "./tree_cache.js";
|
||||
import NoteFull from "../entities/note_full.js";
|
||||
import noteDetailCode from './note_detail_code.js';
|
||||
@@ -14,6 +15,7 @@ import noteDetailText from './note_detail_text.js';
|
||||
import noteDetailFile from './note_detail_file.js';
|
||||
import noteDetailSearch from './note_detail_search.js';
|
||||
import noteDetailRender from './note_detail_render.js';
|
||||
import bundleService from "./bundle.js";
|
||||
|
||||
const $noteTitle = $("#note-title");
|
||||
|
||||
@@ -26,7 +28,10 @@ const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
|
||||
const $noteIdDisplay = $("#note-id-display");
|
||||
const $labelList = $("#label-list");
|
||||
const $labelListInner = $("#label-list-inner");
|
||||
const $relationList = $("#relation-list");
|
||||
const $relationListInner = $("#relation-list-inner");
|
||||
const $childrenOverview = $("#children-overview");
|
||||
const $scriptArea = $("#note-detail-script-area");
|
||||
|
||||
let currentNote = null;
|
||||
|
||||
@@ -182,6 +187,12 @@ async function loadNoteDetail(noteId) {
|
||||
|
||||
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
|
||||
await showChildrenOverview(hideChildrenOverview);
|
||||
|
||||
await loadRelationList();
|
||||
|
||||
$scriptArea.html('');
|
||||
|
||||
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
|
||||
}
|
||||
|
||||
async function showChildrenOverview(hideChildrenOverview) {
|
||||
@@ -230,6 +241,29 @@ async function loadLabelList() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
async function loadRelationList() {
|
||||
const noteId = getCurrentNoteId();
|
||||
|
||||
const relations = await server.get('notes/' + noteId + '/relations');
|
||||
|
||||
$relationListInner.html('');
|
||||
|
||||
if (relations.length > 0) {
|
||||
for (const relation of relations) {
|
||||
$relationListInner.append(relation.name + " = ");
|
||||
$relationListInner.append(await linkService.createNoteLink(relation.targetNoteId));
|
||||
$relationListInner.append(" ");
|
||||
}
|
||||
|
||||
$relationList.show();
|
||||
}
|
||||
else {
|
||||
$relationList.hide();
|
||||
}
|
||||
|
||||
return relations;
|
||||
}
|
||||
|
||||
async function loadNote(noteId) {
|
||||
const row = await server.get('notes/' + noteId);
|
||||
|
||||
@@ -279,6 +313,7 @@ export default {
|
||||
newNoteCreated,
|
||||
focus,
|
||||
loadLabelList,
|
||||
loadRelationList,
|
||||
saveNote,
|
||||
saveNoteIfChanged,
|
||||
noteChanged
|
||||
|
||||
@@ -75,9 +75,7 @@ async function executeCurrentNote() {
|
||||
const currentNote = noteDetailService.getCurrentNote();
|
||||
|
||||
if (currentNote.mime.endsWith("env=frontend")) {
|
||||
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
|
||||
|
||||
bundleService.executeBundle(bundle);
|
||||
await bundleService.getAndExecuteBundle(noteDetailService.getCurrentNoteId());
|
||||
}
|
||||
|
||||
if (currentNote.mime.endsWith("env=backend")) {
|
||||
|
||||
@@ -2,8 +2,9 @@ import treeService from './tree.js';
|
||||
import server from './server.js';
|
||||
import utils from './utils.js';
|
||||
import infoService from './info.js';
|
||||
import linkService from './link.js';
|
||||
|
||||
function ScriptApi(startNote, currentNote) {
|
||||
function ScriptApi(startNote, currentNote, targetNote = null) {
|
||||
const $pluginButtons = $("#plugin-buttons");
|
||||
|
||||
async function activateNote(notePath) {
|
||||
@@ -42,7 +43,8 @@ function ScriptApi(startNote, currentNote) {
|
||||
script: script,
|
||||
params: prepareParams(params),
|
||||
startNoteId: startNote.noteId,
|
||||
currentNoteId: currentNote.noteId
|
||||
currentNoteId: currentNote.noteId,
|
||||
targetNoteId: targetNote ? targetNote.noteId : null
|
||||
});
|
||||
|
||||
return ret.executionResult;
|
||||
@@ -51,6 +53,7 @@ function ScriptApi(startNote, currentNote) {
|
||||
return {
|
||||
startNote: startNote,
|
||||
currentNote: currentNote,
|
||||
targetNote: targetNote,
|
||||
addButtonToToolbar,
|
||||
activateNote,
|
||||
getInstanceName: () => window.glob.instanceName,
|
||||
@@ -59,7 +62,8 @@ function ScriptApi(startNote, currentNote) {
|
||||
parseDate: utils.parseDate,
|
||||
showMessage: infoService.showMessage,
|
||||
showError: infoService.showError,
|
||||
reloadTree: treeService.reload
|
||||
reloadTree: treeService.reload,
|
||||
createNoteLink: linkService.createNoteLink
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import ScriptApi from './script_api.js';
|
||||
import utils from './utils.js';
|
||||
|
||||
function ScriptContext(startNote, allNotes) {
|
||||
function ScriptContext(startNote, allNotes, targetNote = null) {
|
||||
const modules = {};
|
||||
|
||||
return {
|
||||
modules: modules,
|
||||
notes: utils.toObject(allNotes, note => [note.noteId, note]),
|
||||
apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]),
|
||||
apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note, targetNote)]),
|
||||
require: moduleNoteIds => {
|
||||
return moduleName => {
|
||||
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||
|
||||
@@ -4,11 +4,15 @@ import linkService from "./link.js";
|
||||
|
||||
function setupTooltip() {
|
||||
$(document).tooltip({
|
||||
items: "#note-detail-text a",
|
||||
items: "#note-detail-wrapper a",
|
||||
content: function (callback) {
|
||||
const notePath = linkService.getNotePathFromLink($(this).attr("href"));
|
||||
let notePath = linkService.getNotePathFromLink($(this).attr("href"));
|
||||
|
||||
if (notePath !== null) {
|
||||
if (!notePath) {
|
||||
notePath = $(this).attr("note-path");
|
||||
}
|
||||
|
||||
if (notePath) {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
noteDetailService.loadNote(noteId).then(note => callback(note.content));
|
||||
|
||||
@@ -8,7 +8,6 @@ import treeChangesService from './branches.js';
|
||||
import treeUtils from './tree_utils.js';
|
||||
import utils from './utils.js';
|
||||
import server from './server.js';
|
||||
import recentNotesDialog from '../dialogs/recent_notes.js';
|
||||
import treeCache from './tree_cache.js';
|
||||
import infoService from "./info.js";
|
||||
import treeBuilder from "./tree_builder.js";
|
||||
@@ -239,6 +238,15 @@ async function setExpandedToServer(branchId, isExpanded) {
|
||||
await server.put('branches/' + branchId + '/expanded/' + expandedNum);
|
||||
}
|
||||
|
||||
function addRecentNote(branchId, notePath) {
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (notePath && notePath === getCurrentNotePath()) {
|
||||
await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function setCurrentNotePathToHash(node) {
|
||||
utils.assertArguments(node);
|
||||
|
||||
@@ -247,7 +255,7 @@ function setCurrentNotePathToHash(node) {
|
||||
|
||||
document.location.hash = currentNotePath;
|
||||
|
||||
recentNotesDialog.addRecentNote(currentBranchId, currentNotePath);
|
||||
addRecentNote(currentBranchId, currentNotePath);
|
||||
}
|
||||
|
||||
function getSelectedNodes(stopOnParents = false) {
|
||||
|
||||
@@ -1,36 +1,141 @@
|
||||
import server from './services/server.js';
|
||||
import utils from "./services/utils.js";
|
||||
|
||||
$("#setup-form").submit(() => {
|
||||
const username = $("#username").val();
|
||||
const password1 = $("#password1").val();
|
||||
const password2 = $("#password2").val();
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return false;
|
||||
function SetupModel() {
|
||||
if (syncInProgress) {
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
}
|
||||
|
||||
if (!password1) {
|
||||
showAlert("Password can't be empty");
|
||||
return false;
|
||||
}
|
||||
const serverAddress = location.protocol + '//' + location.host;
|
||||
|
||||
if (password1 !== password2) {
|
||||
showAlert("Both password fields need be identical.");
|
||||
return false;
|
||||
}
|
||||
$("#current-host").html(serverAddress);
|
||||
|
||||
server.post('setup', {
|
||||
username: username,
|
||||
password: password1
|
||||
}).then(() => {
|
||||
this.step = ko.observable(syncInProgress ? "sync-in-progress" : "setup-type");
|
||||
this.setupType = ko.observable();
|
||||
|
||||
this.setupNewDocument = ko.observable(false);
|
||||
this.setupSyncFromDesktop = ko.observable(false);
|
||||
this.setupSyncFromServer = ko.observable(false);
|
||||
|
||||
this.username = ko.observable();
|
||||
this.password1 = ko.observable();
|
||||
this.password2 = ko.observable();
|
||||
|
||||
this.syncServerHost = ko.observable();
|
||||
this.syncProxy = ko.observable();
|
||||
|
||||
this.instanceType = utils.isElectron() ? "desktop" : "server";
|
||||
|
||||
this.setupTypeSelected = this.getSetupType = () =>
|
||||
this.setupNewDocument()
|
||||
|| this.setupSyncFromDesktop()
|
||||
|| this.setupSyncFromServer();
|
||||
|
||||
this.selectSetupType = () => {
|
||||
this.step(this.getSetupType());
|
||||
this.setupType(this.getSetupType());
|
||||
};
|
||||
|
||||
this.back = () => {
|
||||
this.step("setup-type");
|
||||
|
||||
this.setupNewDocument(false);
|
||||
this.setupSyncFromServer(false);
|
||||
this.setupSyncFromDesktop(false);
|
||||
};
|
||||
|
||||
this.finish = async () => {
|
||||
if (this.setupNewDocument()) {
|
||||
const username = this.username();
|
||||
const password1 = this.password1();
|
||||
const password2 = this.password2();
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password1) {
|
||||
showAlert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password1 !== password2) {
|
||||
showAlert("Both password fields need be identical.");
|
||||
return;
|
||||
}
|
||||
|
||||
// not using server.js because it loads too many dependencies
|
||||
$.post('/api/setup/new-document', {
|
||||
username: username,
|
||||
password: password1
|
||||
}).then(() => {
|
||||
window.location.replace("/");
|
||||
});
|
||||
}
|
||||
else if (this.setupSyncFromServer()) {
|
||||
const syncServerHost = this.syncServerHost();
|
||||
const syncProxy = this.syncProxy();
|
||||
const username = this.username();
|
||||
const password = this.password1();
|
||||
|
||||
if (!syncServerHost) {
|
||||
showAlert("Trilium server address can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
showAlert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// not using server.js because it loads too many dependencies
|
||||
const resp = await $.post('/api/setup/sync-from-server', {
|
||||
syncServerHost: syncServerHost,
|
||||
syncProxy: syncProxy,
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (resp.result === 'success') {
|
||||
this.step('sync-in-progress');
|
||||
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
|
||||
hideAlert();
|
||||
}
|
||||
else {
|
||||
showAlert('Sync setup failed: ' + resp.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function checkOutstandingSyncs() {
|
||||
const { stats, initialized } = await $.get('/api/sync/stats');
|
||||
|
||||
if (initialized) {
|
||||
window.location.replace("/");
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
|
||||
|
||||
$("#outstanding-syncs").html(totalOutstandingSyncs);
|
||||
}
|
||||
|
||||
function showAlert(message) {
|
||||
$("#alert").html(message);
|
||||
$("#alert").show();
|
||||
}
|
||||
}
|
||||
|
||||
function hideAlert() {
|
||||
$("#alert").hide();
|
||||
}
|
||||
|
||||
ko.applyBindings(new SetupModel(), document.getElementById('setup-dialog'));
|
||||
|
||||
$("#setup-dialog").show();
|
||||
@@ -308,13 +308,13 @@ div.ui-tooltip {
|
||||
|
||||
.cm-matchhighlight {background-color: #eeeeee}
|
||||
|
||||
#label-list {
|
||||
#label-list, #relation-list {
|
||||
color: #777777;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 5px; display: none;
|
||||
padding: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#label-list button {
|
||||
#label-list button, #relation-list button {
|
||||
padding: 2px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
"use strict";
|
||||
|
||||
const noteCacheService = require('../../services/note_cache');
|
||||
const repository = require('../../services/repository');
|
||||
|
||||
async function getAutocomplete(req) {
|
||||
const query = req.query.query;
|
||||
|
||||
const results = noteCacheService.findNotes(query);
|
||||
let results;
|
||||
|
||||
if (query.trim().length === 0) {
|
||||
results = await getRecentNotes();
|
||||
}
|
||||
else {
|
||||
results = noteCacheService.findNotes(query);
|
||||
}
|
||||
|
||||
return results.map(res => {
|
||||
return {
|
||||
value: res.title + ' (' + res.path + ')',
|
||||
value: res.path,
|
||||
label: res.title
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getRecentNotes() {
|
||||
const recentNotes = await repository.getEntities(`
|
||||
SELECT
|
||||
recent_notes.*
|
||||
FROM
|
||||
recent_notes
|
||||
JOIN branches USING(branchId)
|
||||
WHERE
|
||||
recent_notes.isDeleted = 0
|
||||
AND branches.isDeleted = 0
|
||||
ORDER BY
|
||||
dateCreated DESC
|
||||
LIMIT 200`);
|
||||
|
||||
return recentNotes.map(rn => {
|
||||
return {
|
||||
path: rn.notePath,
|
||||
title: noteCacheService.getNoteTitleForPath(rn.notePath.split('/'))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAutocomplete
|
||||
};
|
||||
@@ -1,46 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const utils = require('../../services/utils');
|
||||
const syncTable = require('../../services/sync_table');
|
||||
const log = require('../../services/log');
|
||||
const repository = require('../../services/repository');
|
||||
|
||||
async function cleanupSoftDeletedItems() {
|
||||
const noteIdsToDelete = await sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1");
|
||||
const noteIdsSql = noteIdsToDelete
|
||||
.map(noteId => "'" + utils.sanitizeSql(noteId) + "'")
|
||||
.join(', ');
|
||||
|
||||
await sql.execute(`DELETE FROM event_log WHERE noteId IN (${noteIdsSql})`);
|
||||
|
||||
await sql.execute(`DELETE FROM note_revisions WHERE noteId IN (${noteIdsSql})`);
|
||||
|
||||
await sql.execute(`DELETE FROM note_images WHERE noteId IN (${noteIdsSql})`);
|
||||
|
||||
await sql.execute(`DELETE FROM labels WHERE noteId IN (${noteIdsSql})`);
|
||||
|
||||
await sql.execute("DELETE FROM branches WHERE isDeleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM note_images WHERE isDeleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM images WHERE isDeleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM notes WHERE isDeleted = 1");
|
||||
|
||||
await sql.execute("DELETE FROM recent_notes");
|
||||
|
||||
await syncTable.cleanupSyncRowsForMissingEntities("notes", "noteId");
|
||||
await syncTable.cleanupSyncRowsForMissingEntities("branches", "branchId");
|
||||
await syncTable.cleanupSyncRowsForMissingEntities("note_revisions", "noteRevisionId");
|
||||
await syncTable.cleanupSyncRowsForMissingEntities("recent_notes", "branchId");
|
||||
await syncTable.cleanupSyncRowsForMissingEntities("images", "imageId");
|
||||
await syncTable.cleanupSyncRowsForMissingEntities("note_images", "noteImageId");
|
||||
await syncTable.cleanupSyncRowsForMissingEntities("labels", "labelId");
|
||||
|
||||
log.info("Following notes has been completely cleaned from database: " + noteIdsSql);
|
||||
}
|
||||
|
||||
async function cleanupUnusedImages() {
|
||||
const unusedImageIds = await sql.getColumn(`
|
||||
SELECT images.imageId
|
||||
@@ -67,7 +30,6 @@ async function vacuumDatabase() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cleanupSoftDeletedItems,
|
||||
cleanupUnusedImages,
|
||||
vacuumDatabase
|
||||
};
|
||||
@@ -9,8 +9,13 @@ const protectedSessionService = require('../../services/protected_session');
|
||||
const appInfo = require('../../services/app_info');
|
||||
const eventService = require('../../services/events');
|
||||
const cls = require('../../services/cls');
|
||||
const sqlInit = require('../../services/sql_init');
|
||||
|
||||
async function loginSync(req) {
|
||||
if (!await sqlInit.schemaExists()) {
|
||||
return [400, { message: "DB schema does not exist, can't sync." }];
|
||||
}
|
||||
|
||||
const timestampStr = req.body.timestamp;
|
||||
|
||||
const timestamp = dateUtils.parseDateTime(timestampStr);
|
||||
|
||||
@@ -5,7 +5,8 @@ const optionService = require('../../services/options');
|
||||
const log = require('../../services/log');
|
||||
|
||||
// options allowed to be updated directly in options dialog
|
||||
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval', 'zoomFactor', 'theme'];
|
||||
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval',
|
||||
'zoomFactor', 'theme', 'syncServerHost', 'syncServerTimeout', 'syncProxy'];
|
||||
|
||||
async function getOptions() {
|
||||
const options = await sql.getMap("SELECT name, value FROM options WHERE name IN ("
|
||||
@@ -17,16 +18,35 @@ async function getOptions() {
|
||||
async function updateOption(req) {
|
||||
const {name, value} = req.params;
|
||||
|
||||
if (!update(name, value)) {
|
||||
return [400, "not allowed option to change"];
|
||||
}
|
||||
}
|
||||
|
||||
async function updateOptions(req) {
|
||||
for (const optionName in req.body) {
|
||||
if (!update(optionName, req.body[optionName])) {
|
||||
// this should be improved
|
||||
// it should return 400 instead of current 500, but at least it now rollbacks transaction
|
||||
throw new Error(`${optionName} is not allowed to change`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function update(name, value) {
|
||||
if (!ALLOWED_OPTIONS.includes(name)) {
|
||||
return [400, "not allowed option to set"];
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info(`Updating option ${name} to ${value}`);
|
||||
|
||||
await optionService.setOption(name, value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOptions,
|
||||
updateOption
|
||||
updateOption,
|
||||
updateOptions
|
||||
};
|
||||
@@ -1,30 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const repository = require('../../services/repository');
|
||||
const optionService = require('../../services/options');
|
||||
const RecentNote = require('../../entities/recent_note');
|
||||
const noteCacheService = require('../../services/note_cache');
|
||||
|
||||
async function getRecentNotes() {
|
||||
const recentNotes = await repository.getEntities(`
|
||||
SELECT
|
||||
recent_notes.*
|
||||
FROM
|
||||
recent_notes
|
||||
JOIN branches USING(branchId)
|
||||
WHERE
|
||||
recent_notes.isDeleted = 0
|
||||
AND branches.isDeleted = 0
|
||||
ORDER BY
|
||||
dateCreated DESC
|
||||
LIMIT 200`);
|
||||
|
||||
for (const rn of recentNotes) {
|
||||
rn.title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/'));
|
||||
}
|
||||
|
||||
return recentNotes;
|
||||
}
|
||||
|
||||
async function addRecentNote(req) {
|
||||
const branchId = req.params.branchId;
|
||||
@@ -36,11 +13,8 @@ async function addRecentNote(req) {
|
||||
}).save();
|
||||
|
||||
await optionService.setOption('startNotePath', notePath);
|
||||
|
||||
return await getRecentNotes();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRecentNotes,
|
||||
addRecentNote
|
||||
};
|
||||
64
src/routes/api/relations.js
Normal file
64
src/routes/api/relations.js
Normal file
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const relationService = require('../../services/relations');
|
||||
const repository = require('../../services/repository');
|
||||
const Relation = require('../../entities/relation');
|
||||
|
||||
async function getNoteRelations(req) {
|
||||
const noteId = req.params.noteId;
|
||||
|
||||
return await repository.getEntities("SELECT * FROM relations WHERE isDeleted = 0 AND sourceNoteId = ? ORDER BY position, dateCreated", [noteId]);
|
||||
}
|
||||
|
||||
async function updateNoteRelations(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const relations = req.body;
|
||||
|
||||
for (const relation of relations) {
|
||||
let relationEntity;
|
||||
|
||||
if (relation.relationId) {
|
||||
relationEntity = await repository.getRelation(relation.relationId);
|
||||
}
|
||||
else {
|
||||
// if it was "created" and then immediatelly deleted, we just don't create it at all
|
||||
if (relation.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
relationEntity = new Relation();
|
||||
relationEntity.sourceNoteId = noteId;
|
||||
}
|
||||
|
||||
relationEntity.name = relation.name;
|
||||
relationEntity.targetNoteId = relation.targetNoteId;
|
||||
relationEntity.isInheritable = relation.isInheritable;
|
||||
relationEntity.position = relation.position;
|
||||
relationEntity.isDeleted = relation.isDeleted;
|
||||
|
||||
await relationEntity.save();
|
||||
}
|
||||
|
||||
return await repository.getEntities("SELECT * FROM relations WHERE isDeleted = 0 AND sourceNoteId = ? ORDER BY position, dateCreated", [noteId]);
|
||||
}
|
||||
|
||||
async function getAllRelationNames() {
|
||||
const names = await sql.getColumn("SELECT DISTINCT name FROM relations WHERE isDeleted = 0");
|
||||
|
||||
for (const relationName of relationService.BUILTIN_RELATIONS) {
|
||||
if (!names.includes(relationName)) {
|
||||
names.push(relationName);
|
||||
}
|
||||
}
|
||||
|
||||
names.sort();
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNoteRelations,
|
||||
updateNoteRelations,
|
||||
getAllRelationNames
|
||||
};
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
const labelService = require('../../services/labels');
|
||||
const scriptService = require('../../services/script');
|
||||
const relationService = require('../../services/relations');
|
||||
const repository = require('../../services/repository');
|
||||
|
||||
async function exec(req) {
|
||||
const result = await scriptService.executeScript(req.body.script, req.body.params, req.body.startNoteId, req.body.currentNoteId);
|
||||
const result = await scriptService.executeScript(req.body.script, req.body.params, req.body.startNoteId,
|
||||
req.body.currentNoteId, req.body.targetNoteId);
|
||||
|
||||
return { executionResult: result };
|
||||
}
|
||||
@@ -34,14 +36,32 @@ async function getStartupBundles() {
|
||||
return bundles;
|
||||
}
|
||||
|
||||
async function getRelationBundles(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const relationName = req.params.relationName;
|
||||
|
||||
const relations = await relationService.getEffectiveRelations(noteId);
|
||||
const filtered = relations.filter(relation => relation.name === relationName);
|
||||
const targetNoteIds = filtered.map(relation => relation.targetNoteId);
|
||||
const uniqueNoteIds = Array.from(new Set(targetNoteIds));
|
||||
|
||||
const bundles = [];
|
||||
|
||||
for (const noteId of uniqueNoteIds) {
|
||||
bundles.push(await scriptService.getScriptBundleForNoteId(noteId));
|
||||
}
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
async function getBundle(req) {
|
||||
const note = await repository.getNote(req.params.noteId);
|
||||
return await scriptService.getScriptBundle(note);
|
||||
return await scriptService.getScriptBundleForNoteId(req.params.noteId);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exec,
|
||||
run,
|
||||
getStartupBundles,
|
||||
getRelationBundles,
|
||||
getBundle
|
||||
};
|
||||
@@ -1,27 +1,76 @@
|
||||
"use strict";
|
||||
|
||||
const optionService = require('../../services/options');
|
||||
const sqlInit = require('../../services/sql_init');
|
||||
const utils = require('../../services/utils');
|
||||
const myScryptService = require('../../services/my_scrypt');
|
||||
const passwordEncryptionService = require('../../services/password_encryption');
|
||||
const setupService = require('../../services/setup');
|
||||
const optionService = require('../../services/options');
|
||||
const syncService = require('../../services/sync');
|
||||
const log = require('../../services/log');
|
||||
const rp = require('request-promise');
|
||||
|
||||
async function setup(req) {
|
||||
async function setupNewDocument(req) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
await optionService.setOption('username', username);
|
||||
await sqlInit.createInitialDatabase(username, password);
|
||||
}
|
||||
|
||||
await optionService.setOption('passwordVerificationSalt', utils.randomSecureToken(32));
|
||||
await optionService.setOption('passwordDerivedKeySalt', utils.randomSecureToken(32));
|
||||
async function setupSyncFromServer(req) {
|
||||
const { syncServerHost, syncProxy, username, password } = req.body;
|
||||
|
||||
const passwordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(password));
|
||||
await optionService.setOption('passwordVerificationHash', passwordVerificationKey);
|
||||
return await setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, username, password);
|
||||
}
|
||||
|
||||
await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16));
|
||||
async function setupSyncToSyncServer() {
|
||||
log.info("Initiating sync to server");
|
||||
|
||||
sqlInit.setDbReadyAsResolved();
|
||||
const syncServerHost = await optionService.getOption('syncServerHost');
|
||||
const syncProxy = await optionService.getOption('syncProxy');
|
||||
|
||||
const rpOpts = {
|
||||
uri: syncServerHost + '/api/setup/sync-seed',
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: {
|
||||
options: await setupService.getSyncSeedOptions()
|
||||
}
|
||||
};
|
||||
|
||||
if (syncProxy) {
|
||||
rpOpts.proxy = syncProxy;
|
||||
}
|
||||
|
||||
try {
|
||||
await rp(rpOpts);
|
||||
}
|
||||
catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
|
||||
// this is completely new sync, need to reset counters. If this would not be new sync,
|
||||
// the previous request would have failed.
|
||||
await optionService.setOption('lastSyncedPush', 0);
|
||||
await optionService.setOption('lastSyncedPull', 0);
|
||||
|
||||
syncService.sync();
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function saveSyncSeed(req) {
|
||||
const options = req.body.options;
|
||||
|
||||
await sqlInit.createDatabaseForSync(options);
|
||||
}
|
||||
|
||||
async function getSyncSeed() {
|
||||
log.info("Serving sync seed.");
|
||||
|
||||
return await setupService.getSyncSeedOptions();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setup
|
||||
setupNewDocument,
|
||||
setupSyncFromServer,
|
||||
setupSyncToSyncServer,
|
||||
getSyncSeed,
|
||||
saveSyncSeed
|
||||
};
|
||||
@@ -4,10 +4,37 @@ const syncService = require('../../services/sync');
|
||||
const syncUpdateService = require('../../services/sync_update');
|
||||
const syncTableService = require('../../services/sync_table');
|
||||
const sql = require('../../services/sql');
|
||||
const sqlInit = require('../../services/sql_init');
|
||||
const optionService = require('../../services/options');
|
||||
const contentHashService = require('../../services/content_hash');
|
||||
const log = require('../../services/log');
|
||||
|
||||
async function testSync() {
|
||||
try {
|
||||
await syncService.login();
|
||||
|
||||
return { connection: "Success" };
|
||||
}
|
||||
catch (e) {
|
||||
return {
|
||||
connection: "Failure",
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getStats() {
|
||||
if (!await sqlInit.schemaExists()) {
|
||||
// fail silently but prevent errors from not existing options table
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
initialized: await optionService.getOption('initialized') === 'true',
|
||||
stats: syncService.stats
|
||||
};
|
||||
}
|
||||
|
||||
async function checkSync() {
|
||||
return {
|
||||
hashes: await contentHashService.getHashes(),
|
||||
@@ -60,7 +87,10 @@ async function getChanged(req) {
|
||||
|
||||
const syncs = await sql.getRows("SELECT * FROM sync WHERE id > ? LIMIT 1000", [lastSyncId]);
|
||||
|
||||
return await syncService.getSyncRecords(syncs);
|
||||
return {
|
||||
syncs: await syncService.getSyncRecords(syncs),
|
||||
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync')
|
||||
};
|
||||
}
|
||||
|
||||
async function update(req) {
|
||||
@@ -72,12 +102,21 @@ async function update(req) {
|
||||
}
|
||||
}
|
||||
|
||||
async function syncFinished() {
|
||||
// after first sync finishes, the application is ready to be used
|
||||
// this is meaningless but at the same time harmless (idempotent) for further syncs
|
||||
await sqlInit.dbInitialized();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testSync,
|
||||
checkSync,
|
||||
syncNow,
|
||||
fillSyncRows,
|
||||
forceFullSync,
|
||||
forceNoteSync,
|
||||
getChanged,
|
||||
update
|
||||
update,
|
||||
getStats,
|
||||
syncFinished
|
||||
};
|
||||
@@ -26,6 +26,7 @@ const anonymizationRoute = require('./api/anonymization');
|
||||
const cleanupRoute = require('./api/cleanup');
|
||||
const imageRoute = require('./api/image');
|
||||
const labelsRoute = require('./api/labels');
|
||||
const relationsRoute = require('./api/relations');
|
||||
const scriptRoute = require('./api/script');
|
||||
const senderRoute = require('./api/sender');
|
||||
const filesRoute = require('./api/file_upload');
|
||||
@@ -62,16 +63,21 @@ function apiRoute(method, path, routeHandler) {
|
||||
route(method, path, [auth.checkApiAuth], routeHandler, apiResultHandler);
|
||||
}
|
||||
|
||||
function route(method, path, middleware, routeHandler, resultHandler) {
|
||||
function route(method, path, middleware, routeHandler, resultHandler, transactional = true) {
|
||||
router[method](path, ...middleware, async (req, res, next) => {
|
||||
try {
|
||||
const result = await cls.init(async () => {
|
||||
cls.namespace.set('sourceId', req.headers.source_id);
|
||||
protectedSessionService.setProtectedSessionId(req);
|
||||
|
||||
return await sql.transactional(async () => {
|
||||
if (transactional) {
|
||||
return await sql.transactional(async () => {
|
||||
return await routeHandler(req, res, next);
|
||||
});
|
||||
}
|
||||
else {
|
||||
return await routeHandler(req, res, next);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (resultHandler) {
|
||||
@@ -91,7 +97,7 @@ const uploadMiddleware = multer.single('upload');
|
||||
|
||||
function register(app) {
|
||||
route(GET, '/', [auth.checkAuth], indexRoute.index);
|
||||
route(GET, '/login', [], loginRoute.loginPage);
|
||||
route(GET, '/login', [auth.checkAppInitialized], loginRoute.loginPage);
|
||||
route(POST, '/login', [], loginRoute.login);
|
||||
route(POST, '/logout', [auth.checkAuth], loginRoute.logout);
|
||||
route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
|
||||
@@ -132,6 +138,10 @@ function register(app) {
|
||||
apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames);
|
||||
apiRoute(GET, '/api/labels/values/:labelName', labelsRoute.getValuesForLabel);
|
||||
|
||||
apiRoute(GET, '/api/notes/:noteId/relations', relationsRoute.getNoteRelations);
|
||||
apiRoute(PUT, '/api/notes/:noteId/relations', relationsRoute.updateNoteRelations);
|
||||
apiRoute(GET, '/api/relations/names', relationsRoute.getAllRelationNames);
|
||||
|
||||
route(GET, '/api/images/:imageId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
|
||||
route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware], imageRoute.uploadImage, apiResultHandler);
|
||||
|
||||
@@ -139,9 +149,11 @@ function register(app) {
|
||||
|
||||
apiRoute(GET, '/api/options', optionsApiRoute.getOptions);
|
||||
apiRoute(PUT, '/api/options/:name/:value', optionsApiRoute.updateOption);
|
||||
apiRoute(PUT, '/api/options', optionsApiRoute.updateOptions);
|
||||
|
||||
apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword);
|
||||
|
||||
apiRoute(POST, '/api/sync/test', syncApiRoute.testSync);
|
||||
apiRoute(GET, '/api/sync/check', syncApiRoute.checkSync);
|
||||
apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow);
|
||||
apiRoute(POST, '/api/sync/fill-sync-rows', syncApiRoute.fillSyncRows);
|
||||
@@ -149,19 +161,23 @@ function register(app) {
|
||||
apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync);
|
||||
apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged);
|
||||
apiRoute(PUT, '/api/sync/update', syncApiRoute.update);
|
||||
route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler);
|
||||
apiRoute(POST, '/api/sync/finished', syncApiRoute.syncFinished);
|
||||
|
||||
apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog);
|
||||
|
||||
apiRoute(GET, '/api/recent-notes', recentNotesRoute.getRecentNotes);
|
||||
apiRoute(PUT, '/api/recent-notes/:branchId/:notePath', recentNotesRoute.addRecentNote);
|
||||
apiRoute(GET, '/api/app-info', appInfoRoute.getAppInfo);
|
||||
|
||||
route(POST, '/api/setup', [auth.checkAppNotInitialized], setupApiRoute.setup, apiResultHandler);
|
||||
route(POST, '/api/setup/new-document', [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
|
||||
route(POST, '/api/setup/sync-from-server', [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler, false);
|
||||
apiRoute(POST, '/api/setup/sync-to-server', setupApiRoute.setupSyncToSyncServer);
|
||||
route(GET, '/api/setup/sync-seed', [auth.checkBasicAuth], setupApiRoute.getSyncSeed, apiResultHandler);
|
||||
route(POST, '/api/setup/sync-seed', [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler, false);
|
||||
|
||||
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
|
||||
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
|
||||
|
||||
apiRoute(POST, '/api/cleanup/cleanup-soft-deleted-items', cleanupRoute.cleanupSoftDeletedItems);
|
||||
apiRoute(POST, '/api/cleanup/cleanup-unused-images', cleanupRoute.cleanupUnusedImages);
|
||||
apiRoute(POST, '/api/cleanup/vacuum-database', cleanupRoute.vacuumDatabase);
|
||||
|
||||
@@ -169,6 +185,7 @@ function register(app) {
|
||||
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
|
||||
apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles);
|
||||
apiRoute(GET, '/api/script/bundle/:noteId', scriptRoute.getBundle);
|
||||
apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles);
|
||||
|
||||
route(POST, '/api/sender/login', [], senderRoute.login, apiResultHandler);
|
||||
route(POST, '/api/sender/image', [auth.checkSenderToken], senderRoute.uploadImage, apiResultHandler);
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
"use strict";
|
||||
|
||||
function setupPage(req, res) {
|
||||
res.render('setup', {});
|
||||
const sqlInit = require('../services/sql_init');
|
||||
const setupService = require('../services/setup');
|
||||
|
||||
async function setupPage(req, res) {
|
||||
if (await sqlInit.isDbInitialized()) {
|
||||
res.redirect('/');
|
||||
}
|
||||
|
||||
// we got here because DB is not completely initialized so if schema exists
|
||||
// it means we're in sync in progress state.
|
||||
const syncInProgress = await sqlInit.schemaExists();
|
||||
|
||||
if (syncInProgress) {
|
||||
// trigger sync if it's not already running
|
||||
setupService.triggerSync();
|
||||
}
|
||||
|
||||
res.render('setup', {
|
||||
syncInProgress: syncInProgress
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
BIN
src/scripts/Edited files on day.tar
Normal file
BIN
src/scripts/Edited files on day.tar
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
|
||||
const APP_DB_VERSION = 100;
|
||||
const APP_DB_VERSION = 107;
|
||||
const SYNC_VERSION = 1;
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use strict";
|
||||
|
||||
const migrationService = require('./migration');
|
||||
const sql = require('./sql');
|
||||
const sqlInit = require('./sql_init');
|
||||
const utils = require('./utils');
|
||||
const passwordEncryptionService = require('./password_encryption');
|
||||
const optionService = require('./options');
|
||||
|
||||
async function checkAuth(req, res, next) {
|
||||
if (!await sqlInit.isUserInitialized()) {
|
||||
if (!await sqlInit.isDbInitialized()) {
|
||||
res.redirect("setup");
|
||||
}
|
||||
else if (!req.session.loggedIn && !utils.isElectron()) {
|
||||
@@ -37,8 +38,17 @@ async function checkApiAuth(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAppInitialized(req, res, next) {
|
||||
if (!await sqlInit.isDbInitialized()) {
|
||||
res.redirect("setup");
|
||||
}
|
||||
else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAppNotInitialized(req, res, next) {
|
||||
if (await sqlInit.isUserInitialized()) {
|
||||
if (await sqlInit.isDbInitialized()) {
|
||||
res.status(400).send("App already initialized.");
|
||||
}
|
||||
else {
|
||||
@@ -57,10 +67,28 @@ async function checkSenderToken(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkBasicAuth(req, res, next) {
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.split(/\s+/).pop() || '';
|
||||
const auth = new Buffer.from(token, 'base64').toString();
|
||||
const [username, password] = auth.split(/:/);
|
||||
|
||||
const dbUsername = await optionService.getOption('username');
|
||||
|
||||
if (dbUsername !== username || !await passwordEncryptionService.verifyPassword(password)) {
|
||||
res.status(401).send("Not authorized");
|
||||
}
|
||||
else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkAuth,
|
||||
checkApiAuth,
|
||||
checkAppInitialized,
|
||||
checkAppNotInitialized,
|
||||
checkApiAuthOrElectron,
|
||||
checkSenderToken
|
||||
checkSenderToken,
|
||||
checkBasicAuth
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2018-07-09T21:22:12+02:00", buildRevision: "14cffbbe625036d7284056f6a37a5e748e397148" };
|
||||
module.exports = { buildDate:"2018-07-30T08:18:25+02:00", buildRevision: "2ff7a890bceec873a082e496afb349b93f787985" };
|
||||
|
||||
@@ -13,9 +13,14 @@ function getSourceId() {
|
||||
return namespace.get('sourceId');
|
||||
}
|
||||
|
||||
function reset() {
|
||||
clsHooked.reset();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
wrap,
|
||||
namespace,
|
||||
getSourceId
|
||||
getSourceId,
|
||||
reset
|
||||
};
|
||||
@@ -19,6 +19,5 @@ async function addNoteEvent(noteId, comment) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addEvent,
|
||||
addNoteEvent
|
||||
addEvent
|
||||
};
|
||||
@@ -2,8 +2,6 @@ const WebSocket = require('ws');
|
||||
const utils = require('./utils');
|
||||
const log = require('./log');
|
||||
const sql = require('./sql');
|
||||
const optionService = require('./options');
|
||||
const syncSetup = require('./sync_setup');
|
||||
|
||||
let webSocketServer;
|
||||
|
||||
@@ -65,15 +63,12 @@ async function sendMessageToAllClients(message) {
|
||||
|
||||
async function sendPing(client, lastSentSyncId) {
|
||||
const syncData = await sql.getRows("SELECT * FROM sync WHERE id > ?", [lastSentSyncId]);
|
||||
|
||||
const lastSyncedPush = await optionService.getOption('lastSyncedPush');
|
||||
|
||||
const changesToPushCount = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
|
||||
const stats = require('./sync').stats;
|
||||
|
||||
await sendMessage(client, {
|
||||
type: 'sync',
|
||||
data: syncData,
|
||||
changesToPushCount: syncSetup.isSyncSetup ? changesToPushCount : 0
|
||||
outstandingSyncs: stats.outstandingPushes + stats.outstandingPulls
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -56,28 +56,23 @@ async function migrate() {
|
||||
else if (mig.type === 'js') {
|
||||
console.log("Migration with JS module");
|
||||
|
||||
const migrationModule = require("../" + resourceDir.MIGRATIONS_DIR + "/" + mig.file);
|
||||
await migrationModule(db);
|
||||
const migrationModule = require(resourceDir.MIGRATIONS_DIR + "/" + mig.file);
|
||||
await migrationModule();
|
||||
}
|
||||
else {
|
||||
throw new Error("Unknown migration type " + mig.type);
|
||||
}
|
||||
|
||||
await optionService.setOption("dbVersion", mig.dbVersion);
|
||||
|
||||
});
|
||||
|
||||
log.info("Migration to version " + mig.dbVersion + " has been successful.");
|
||||
|
||||
mig['success'] = true;
|
||||
}
|
||||
catch (e) {
|
||||
mig['success'] = false;
|
||||
mig['error'] = e.stack;
|
||||
|
||||
log.error("error during migration to version " + mig.dbVersion + ": " + e.stack);
|
||||
log.error("migration failed, crashing hard"); // this is not very user friendly :-/
|
||||
|
||||
break;
|
||||
process.exit(1);
|
||||
}
|
||||
finally {
|
||||
// make sure foreign keys are enabled even if migration script disables them
|
||||
@@ -85,11 +80,9 @@ async function migrate() {
|
||||
}
|
||||
}
|
||||
|
||||
if (sqlInit.isDbUpToDate()) {
|
||||
sqlInit.setDbReadyAsResolved();
|
||||
if (await sqlInit.isDbUpToDate()) {
|
||||
await sqlInit.initDbConnection();
|
||||
}
|
||||
|
||||
return migrations;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -5,6 +5,7 @@ const repository = require('./repository');
|
||||
const protectedSessionService = require('./protected_session');
|
||||
const utils = require('./utils');
|
||||
|
||||
let loaded = false;
|
||||
let noteTitles;
|
||||
let protectedNoteTitles;
|
||||
let noteIds;
|
||||
@@ -34,6 +35,8 @@ async function load() {
|
||||
for (const noteId of hiddenLabels) {
|
||||
archived[noteId] = true;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
function findNotes(query) {
|
||||
@@ -226,6 +229,10 @@ function getNotePath(noteId) {
|
||||
}
|
||||
|
||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entityName === 'notes') {
|
||||
const note = await repository.getNote(entityId);
|
||||
|
||||
@@ -277,6 +284,10 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
|
||||
});
|
||||
|
||||
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
|
||||
|
||||
for (const noteId in protectedNoteTitles) {
|
||||
|
||||
@@ -223,8 +223,14 @@ async function deleteNote(branch) {
|
||||
|
||||
if (notDeletedBranches.length === 0) {
|
||||
note.isDeleted = true;
|
||||
note.content = '';
|
||||
await note.save();
|
||||
|
||||
for (const noteRevision of await note.getRevisions()) {
|
||||
noteRevision.content = '';
|
||||
await noteRevision.save();
|
||||
}
|
||||
|
||||
for (const childBranch of await note.getChildBranches()) {
|
||||
await deleteNote(childBranch);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
const repository = require('./repository');
|
||||
const utils = require('./utils');
|
||||
const dateUtils = require('./date_utils');
|
||||
const appInfo = require('./app_info');
|
||||
|
||||
async function getOption(name) {
|
||||
const option = await repository.getOption(name);
|
||||
const option = await require('./repository').getOption(name);
|
||||
|
||||
if (!option) {
|
||||
throw new Error("Option " + name + " doesn't exist");
|
||||
@@ -14,7 +9,7 @@ async function getOption(name) {
|
||||
}
|
||||
|
||||
async function setOption(name, value) {
|
||||
const option = await repository.getOption(name);
|
||||
const option = await require('./repository').getOption(name);
|
||||
|
||||
if (!option) {
|
||||
throw new Error(`Option ${name} doesn't exist`);
|
||||
@@ -36,32 +31,8 @@ async function createOption(name, value, isSynced) {
|
||||
}).save();
|
||||
}
|
||||
|
||||
async function initOptions(startNotePath) {
|
||||
await createOption('documentId', utils.randomSecureToken(16), false);
|
||||
await createOption('documentSecret', utils.randomSecureToken(16), false);
|
||||
|
||||
await createOption('username', '', true);
|
||||
await createOption('passwordVerificationHash', '', true);
|
||||
await createOption('passwordVerificationSalt', '', true);
|
||||
await createOption('passwordDerivedKeySalt', '', true);
|
||||
await createOption('encryptedDataKey', '', true);
|
||||
await createOption('encryptedDataKeyIv', '', true);
|
||||
|
||||
await createOption('startNotePath', startNotePath, false);
|
||||
await createOption('protectedSessionTimeout', 600, true);
|
||||
await createOption('noteRevisionSnapshotTimeInterval', 600, true);
|
||||
await createOption('lastBackupDate', dateUtils.nowDate(), false);
|
||||
await createOption('dbVersion', appInfo.dbVersion, false);
|
||||
|
||||
await createOption('lastSyncedPull', appInfo.dbVersion, false);
|
||||
await createOption('lastSyncedPush', 0, false);
|
||||
|
||||
await createOption('zoomFactor', 1.0, false);
|
||||
await createOption('theme', 'white', false);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOption,
|
||||
setOption,
|
||||
initOptions
|
||||
createOption
|
||||
};
|
||||
54
src/services/options_init.js
Normal file
54
src/services/options_init.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const optionService = require('./options');
|
||||
const passwordEncryptionService = require('./password_encryption');
|
||||
const myScryptService = require('./my_scrypt');
|
||||
const appInfo = require('./app_info');
|
||||
const utils = require('./utils');
|
||||
const dateUtils = require('./date_utils');
|
||||
|
||||
async function initDocumentOptions() {
|
||||
await optionService.createOption('documentId', utils.randomSecureToken(16), false);
|
||||
await optionService.createOption('documentSecret', utils.randomSecureToken(16), false);
|
||||
}
|
||||
|
||||
async function initSyncedOptions(username, password) {
|
||||
await optionService.createOption('protectedSessionTimeout', 600);
|
||||
await optionService.createOption('noteRevisionSnapshotTimeInterval', 600);
|
||||
|
||||
await optionService.createOption('username', username);
|
||||
|
||||
await optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32));
|
||||
await optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32));
|
||||
|
||||
const passwordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(password));
|
||||
await optionService.createOption('passwordVerificationHash', passwordVerificationKey);
|
||||
|
||||
// passwordEncryptionService expects these options to already exist
|
||||
await optionService.createOption('encryptedDataKey', '');
|
||||
await optionService.createOption('encryptedDataKeyIv', '');
|
||||
|
||||
await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16));
|
||||
}
|
||||
|
||||
async function initNotSyncedOptions(initialized, startNotePath = 'root', syncServerHost = '', syncProxy = '') {
|
||||
await optionService.createOption('startNotePath', startNotePath, false);
|
||||
await optionService.createOption('lastBackupDate', dateUtils.nowDate(), false);
|
||||
await optionService.createOption('dbVersion', appInfo.dbVersion, false);
|
||||
|
||||
await optionService.createOption('lastSyncedPull', 0, false);
|
||||
await optionService.createOption('lastSyncedPush', 0, false);
|
||||
|
||||
await optionService.createOption('zoomFactor', 1.0, false);
|
||||
await optionService.createOption('theme', 'white', false);
|
||||
|
||||
await optionService.createOption('syncServerHost', syncServerHost, false);
|
||||
await optionService.createOption('syncServerTimeout', 5000, false);
|
||||
await optionService.createOption('syncProxy', syncProxy, false);
|
||||
|
||||
await optionService.createOption('initialized', initialized ? 'true' : 'false', false);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initDocumentOptions,
|
||||
initSyncedOptions,
|
||||
initNotSyncedOptions
|
||||
};
|
||||
59
src/services/relations.js
Normal file
59
src/services/relations.js
Normal file
@@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
|
||||
const repository = require('./repository');
|
||||
const Relation = require('../entities/relation');
|
||||
|
||||
const BUILTIN_RELATIONS = [
|
||||
'runOnNoteView'
|
||||
];
|
||||
|
||||
async function getNotesWithRelation(name, targetNoteId) {
|
||||
let notes;
|
||||
|
||||
if (targetNoteId !== undefined) {
|
||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN relations ON notes.noteId = relations.sourceNoteId
|
||||
WHERE notes.isDeleted = 0 AND relations.isDeleted = 0 AND relations.name = ? AND relations.targetNoteId = ?`, [name, targetNoteId]);
|
||||
}
|
||||
else {
|
||||
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN relations ON notes.noteId = relations.sourceNoteId
|
||||
WHERE notes.isDeleted = 0 AND relations.isDeleted = 0 AND relations.name = ?`, [name]);
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function getNoteWithRelation(name, value) {
|
||||
const notes = await getNotesWithRelation(name, value);
|
||||
|
||||
return notes.length > 0 ? notes[0] : null;
|
||||
}
|
||||
|
||||
async function createRelation(sourceNoteId, name, targetNoteId) {
|
||||
return await new Relation({
|
||||
sourceNoteId: sourceNoteId,
|
||||
name: name,
|
||||
targetNoteId: targetNoteId
|
||||
}).save();
|
||||
}
|
||||
|
||||
async function getEffectiveRelations(noteId) {
|
||||
return await repository.getEntities(`
|
||||
WITH RECURSIVE tree(noteId) AS (
|
||||
SELECT ?
|
||||
UNION
|
||||
SELECT branches.parentNoteId FROM branches
|
||||
JOIN tree ON branches.noteId = tree.noteId
|
||||
JOIN notes ON notes.noteId = branches.parentNoteId
|
||||
WHERE notes.isDeleted = 0 AND branches.isDeleted = 0
|
||||
)
|
||||
SELECT relations.* FROM relations JOIN tree ON relations.sourceNoteId = tree.noteId
|
||||
WHERE relations.isDeleted = 0 AND (relations.isInheritable = 1 OR relations.sourceNoteId = ?)`, [noteId, noteId]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BUILTIN_RELATIONS,
|
||||
getNotesWithRelation,
|
||||
getNoteWithRelation,
|
||||
createRelation,
|
||||
getEffectiveRelations
|
||||
};
|
||||
@@ -41,6 +41,10 @@ async function getLabel(labelId) {
|
||||
return await getEntity("SELECT * FROM labels WHERE labelId = ?", [labelId]);
|
||||
}
|
||||
|
||||
async function getRelation(relationId) {
|
||||
return await getEntity("SELECT * FROM relations WHERE relationId = ?", [relationId]);
|
||||
}
|
||||
|
||||
async function getOption(name) {
|
||||
return await getEntity("SELECT * FROM options WHERE name = ?", [name]);
|
||||
}
|
||||
@@ -72,6 +76,7 @@ module.exports = {
|
||||
getBranch,
|
||||
getImage,
|
||||
getLabel,
|
||||
getRelation,
|
||||
getOption,
|
||||
updateEntity,
|
||||
setEntityConstructor
|
||||
|
||||
@@ -14,7 +14,7 @@ async function executeNote(note) {
|
||||
await executeBundle(bundle);
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, startNote) {
|
||||
async function executeBundle(bundle, startNote, targetNote = null) {
|
||||
if (!startNote) {
|
||||
// this is the default case, the only exception is when we want to preserve frontend startNote
|
||||
startNote = bundle.note;
|
||||
@@ -23,7 +23,7 @@ async function executeBundle(bundle, startNote) {
|
||||
// last \r\n is necessary if script contains line comment on its last line
|
||||
const script = "async function() {\r\n" + bundle.script + "\r\n}";
|
||||
|
||||
const ctx = new ScriptContext(startNote, bundle.allNotes);
|
||||
const ctx = new ScriptContext(startNote, bundle.allNotes, targetNote);
|
||||
|
||||
if (await bundle.note.hasLabel('manualTransactionHandling')) {
|
||||
return await execute(ctx, script, '');
|
||||
@@ -37,9 +37,10 @@ async function executeBundle(bundle, startNote) {
|
||||
* This method preserves frontend startNode - that's why we start execution from currentNote and override
|
||||
* bundle's startNote.
|
||||
*/
|
||||
async function executeScript(script, params, startNoteId, currentNoteId) {
|
||||
async function executeScript(script, params, startNoteId, currentNoteId, targetNoteId) {
|
||||
const startNote = await repository.getNote(startNoteId);
|
||||
const currentNote = await repository.getNote(currentNoteId);
|
||||
const targetNote = await repository.getNote(targetNoteId);
|
||||
|
||||
currentNote.content = `return await (${script}\r\n)(${getParams(params)})`;
|
||||
currentNote.type = 'code';
|
||||
@@ -47,7 +48,7 @@ async function executeScript(script, params, startNoteId, currentNoteId) {
|
||||
|
||||
const bundle = await getScriptBundle(currentNote);
|
||||
|
||||
return await executeBundle(bundle, startNote);
|
||||
return await executeBundle(bundle, startNote, targetNote);
|
||||
}
|
||||
|
||||
async function execute(ctx, script, paramsStr) {
|
||||
@@ -138,8 +139,14 @@ function sanitizeVariableName(str) {
|
||||
return str.replace(/[^a-z0-9_]/gim, "");
|
||||
}
|
||||
|
||||
async function getScriptBundleForNoteId(noteId) {
|
||||
const note = await repository.getNote(noteId);
|
||||
return await getScriptBundle(note);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
executeNote,
|
||||
executeScript,
|
||||
getScriptBundle
|
||||
getScriptBundle,
|
||||
getScriptBundleForNoteId
|
||||
};
|
||||
@@ -9,10 +9,10 @@ const config = require('./config');
|
||||
const repository = require('./repository');
|
||||
const axios = require('axios');
|
||||
|
||||
function ScriptContext(startNote, allNotes) {
|
||||
function ScriptContext(startNote, allNotes, targetNote = null) {
|
||||
this.modules = {};
|
||||
this.notes = utils.toObject(allNotes, note => [note.noteId, note]);
|
||||
this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(startNote, note)]);
|
||||
this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(startNote, note, targetNote)]);
|
||||
this.require = moduleNoteIds => {
|
||||
return moduleName => {
|
||||
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||
@@ -27,9 +27,10 @@ function ScriptContext(startNote, allNotes) {
|
||||
};
|
||||
}
|
||||
|
||||
function ScriptApi(startNote, currentNote) {
|
||||
function ScriptApi(startNote, currentNote, targetNote) {
|
||||
this.startNote = startNote;
|
||||
this.currentNote = currentNote;
|
||||
this.targetNote = targetNote;
|
||||
|
||||
this.axios = axios;
|
||||
|
||||
@@ -44,6 +45,7 @@ function ScriptApi(startNote, currentNote) {
|
||||
this.getNote = repository.getNote;
|
||||
this.getBranch = repository.getBranch;
|
||||
this.getLabel = repository.getLabel;
|
||||
this.getRelation = repository.getRelation;
|
||||
this.getImage = repository.getImage;
|
||||
this.getEntity = repository.getEntity;
|
||||
this.getEntities = repository.getEntities;
|
||||
|
||||
70
src/services/setup.js
Normal file
70
src/services/setup.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const rp = require('request-promise');
|
||||
const syncService = require('./sync');
|
||||
const log = require('./log');
|
||||
const sqlInit = require('./sql_init');
|
||||
const repository = require('./repository');
|
||||
|
||||
function triggerSync() {
|
||||
log.info("Triggering sync.");
|
||||
|
||||
// it's ok to not wait for it here
|
||||
syncService.sync().then(async res => {
|
||||
if (res.success) {
|
||||
await sqlInit.dbInitialized();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) {
|
||||
if (await sqlInit.isDbInitialized()) {
|
||||
return {
|
||||
result: 'failure',
|
||||
error: 'DB is already initialized.'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Getting document options from sync server.");
|
||||
|
||||
// response is expected to contain documentId and documentSecret options
|
||||
const options = await rp.get({
|
||||
uri: syncServerHost + '/api/setup/sync-seed',
|
||||
auth: {
|
||||
'user': username,
|
||||
'pass': password
|
||||
},
|
||||
json: true
|
||||
});
|
||||
|
||||
if (syncProxy) {
|
||||
options.proxy = syncProxy;
|
||||
}
|
||||
|
||||
await sqlInit.createDatabaseForSync(options, syncServerHost, syncProxy);
|
||||
|
||||
triggerSync();
|
||||
|
||||
return { result: 'success' };
|
||||
}
|
||||
catch (e) {
|
||||
log.error("Sync failed: " + e.message);
|
||||
|
||||
return {
|
||||
result: 'failure',
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getSyncSeedOptions() {
|
||||
return [
|
||||
await repository.getOption('documentId'),
|
||||
await repository.getOption('documentSecret')
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupSyncFromSyncServer,
|
||||
getSyncSeedOptions,
|
||||
triggerSync
|
||||
};
|
||||
@@ -157,10 +157,10 @@ async function transactional(func) {
|
||||
transactionActive = true;
|
||||
transactionPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
cls.namespace.set('isInTransaction', true);
|
||||
|
||||
await beginTransaction();
|
||||
|
||||
cls.namespace.set('isInTransaction', true);
|
||||
|
||||
ret = await func();
|
||||
|
||||
await commit();
|
||||
|
||||
@@ -6,41 +6,51 @@ const resourceDir = require('./resource_dir');
|
||||
const appInfo = require('./app_info');
|
||||
const sql = require('./sql');
|
||||
const cls = require('./cls');
|
||||
const optionService = require('./options');
|
||||
const Option = require('../entities/option');
|
||||
|
||||
async function createConnection() {
|
||||
return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise});
|
||||
}
|
||||
|
||||
let schemaReadyResolve = null;
|
||||
const schemaReady = new Promise((resolve, reject) => schemaReadyResolve = resolve);
|
||||
|
||||
let dbReadyResolve = null;
|
||||
const dbReady = new Promise((resolve, reject) => {
|
||||
cls.init(async () => {
|
||||
const db = await createConnection();
|
||||
sql.setDbConnection(db);
|
||||
const dbReady = new Promise(async (resolve, reject) => {
|
||||
dbReadyResolve = resolve;
|
||||
|
||||
await sql.execute("PRAGMA foreign_keys = ON");
|
||||
// no need to create new connection now since DB stays the same all the time
|
||||
const db = await createConnection();
|
||||
sql.setDbConnection(db);
|
||||
|
||||
dbReadyResolve = () => {
|
||||
log.info("DB ready.");
|
||||
initDbConnection();
|
||||
});
|
||||
|
||||
resolve(db);
|
||||
};
|
||||
async function schemaExists() {
|
||||
const tableResults = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='options'");
|
||||
|
||||
const tableResults = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'");
|
||||
if (tableResults.length !== 1) {
|
||||
await createInitialDatabase();
|
||||
}
|
||||
return tableResults.length === 1;
|
||||
}
|
||||
|
||||
schemaReadyResolve();
|
||||
async function isDbInitialized() {
|
||||
if (!await schemaExists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await isUserInitialized()) {
|
||||
log.info("Login/password not initialized. DB not ready.");
|
||||
const initialized = await sql.getValue("SELECT value FROM options WHERE name = 'initialized'");
|
||||
|
||||
// !initialized may be removed in the future, required only for migration
|
||||
return !initialized || initialized === 'true';
|
||||
}
|
||||
|
||||
async function initDbConnection() {
|
||||
await cls.init(async () => {
|
||||
if (!await isDbInitialized()) {
|
||||
log.info("DB not initialized, please visit setup page to initialize Trilium.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await sql.execute("PRAGMA foreign_keys = ON");
|
||||
|
||||
if (!await isDbUpToDate()) {
|
||||
// avoiding circular dependency
|
||||
const migrationService = require('./migration');
|
||||
@@ -48,12 +58,17 @@ const dbReady = new Promise((resolve, reject) => {
|
||||
await migrationService.migrate();
|
||||
}
|
||||
|
||||
resolve(db);
|
||||
log.info("DB ready.");
|
||||
dbReadyResolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createInitialDatabase() {
|
||||
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
|
||||
async function createInitialDatabase(username, password) {
|
||||
log.info("Creating initial database ...");
|
||||
|
||||
if (await isDbInitialized()) {
|
||||
throw new Error("DB is already initialized");
|
||||
}
|
||||
|
||||
const schema = fs.readFileSync(resourceDir.DB_INIT_DIR + '/schema.sql', 'UTF-8');
|
||||
const notesSql = fs.readFileSync(resourceDir.DB_INIT_DIR + '/main_notes.sql', 'UTF-8');
|
||||
@@ -70,18 +85,41 @@ async function createInitialDatabase() {
|
||||
|
||||
const startNoteId = await sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
|
||||
|
||||
await require('./options').initOptions(startNoteId);
|
||||
const optionsInitService = require('./options_init');
|
||||
|
||||
await optionsInitService.initDocumentOptions();
|
||||
await optionsInitService.initSyncedOptions(username, password);
|
||||
await optionsInitService.initNotSyncedOptions(true, startNoteId);
|
||||
|
||||
await require('./sync_table').fillAllSyncRows();
|
||||
});
|
||||
|
||||
log.info("Schema and initial content generated. Waiting for user to enter username/password to finish setup.");
|
||||
log.info("Schema and initial content generated.");
|
||||
|
||||
// we don't resolve dbReady promise because user needs to setup the username and password to initialize
|
||||
// the database
|
||||
await initDbConnection();
|
||||
}
|
||||
|
||||
function setDbReadyAsResolved() {
|
||||
dbReadyResolve();
|
||||
async function createDatabaseForSync(options, syncServerHost = '', syncProxy = '') {
|
||||
log.info("Creating database for sync");
|
||||
|
||||
if (await isDbInitialized()) {
|
||||
throw new Error("DB is already initialized");
|
||||
}
|
||||
|
||||
const schema = fs.readFileSync(resourceDir.DB_INIT_DIR + '/schema.sql', 'UTF-8');
|
||||
|
||||
await sql.transactional(async () => {
|
||||
await sql.executeScript(schema);
|
||||
|
||||
await require('./options_init').initNotSyncedOptions(false, '', syncServerHost, syncProxy);
|
||||
|
||||
// document options required for sync to kick off
|
||||
for (const opt of options) {
|
||||
await new Option(opt).save();
|
||||
}
|
||||
});
|
||||
|
||||
log.info("Schema and not synced options generated.");
|
||||
}
|
||||
|
||||
async function isDbUpToDate() {
|
||||
@@ -96,22 +134,19 @@ async function isDbUpToDate() {
|
||||
return upToDate;
|
||||
}
|
||||
|
||||
async function isUserInitialized() {
|
||||
const optionsTable = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='options'");
|
||||
async function dbInitialized() {
|
||||
await optionService.setOption('initialized', 'true');
|
||||
|
||||
if (optionsTable.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const username = await sql.getValue("SELECT value FROM options WHERE name = 'username'");
|
||||
|
||||
return !!username;
|
||||
await initDbConnection();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dbReady,
|
||||
schemaReady,
|
||||
isUserInitialized,
|
||||
setDbReadyAsResolved,
|
||||
isDbUpToDate
|
||||
schemaExists,
|
||||
isDbInitialized,
|
||||
initDbConnection,
|
||||
isDbUpToDate,
|
||||
createInitialDatabase,
|
||||
createDatabaseForSync,
|
||||
dbInitialized
|
||||
};
|
||||
@@ -10,18 +10,25 @@ const sourceIdService = require('./source_id');
|
||||
const dateUtils = require('./date_utils');
|
||||
const syncUpdateService = require('./sync_update');
|
||||
const contentHashService = require('./content_hash');
|
||||
const fs = require('fs');
|
||||
const appInfo = require('./app_info');
|
||||
const syncSetup = require('./sync_setup');
|
||||
const syncOptions = require('./sync_options');
|
||||
const syncMutexService = require('./sync_mutex');
|
||||
const cls = require('./cls');
|
||||
|
||||
let proxyToggle = true;
|
||||
let syncServerCertificate = null;
|
||||
|
||||
const stats = {
|
||||
outstandingPushes: 0,
|
||||
outstandingPulls: 0
|
||||
};
|
||||
|
||||
async function sync() {
|
||||
try {
|
||||
await syncMutexService.doExclusively(async () => {
|
||||
return await syncMutexService.doExclusively(async () => {
|
||||
if (!await syncOptions.isSyncSetup()) {
|
||||
return { success: false, message: 'Sync not configured' };
|
||||
}
|
||||
|
||||
const syncContext = await login();
|
||||
|
||||
await pushSync(syncContext);
|
||||
@@ -30,12 +37,14 @@ async function sync() {
|
||||
|
||||
await pushSync(syncContext);
|
||||
|
||||
await checkContentHash(syncContext);
|
||||
});
|
||||
await syncFinished(syncContext);
|
||||
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
await checkContentHash(syncContext);
|
||||
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
proxyToggle = !proxyToggle;
|
||||
@@ -49,7 +58,7 @@ async function sync() {
|
||||
};
|
||||
}
|
||||
else {
|
||||
log.info("sync failed: " + e.stack);
|
||||
log.info("sync failed: " + e.message);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -83,21 +92,34 @@ async function login() {
|
||||
}
|
||||
|
||||
async function pullSync(syncContext) {
|
||||
const changesUri = '/api/sync/changed?lastSyncId=' + await getLastSyncedPull();
|
||||
while (true) {
|
||||
const lastSyncedPull = await getLastSyncedPull();
|
||||
const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull;
|
||||
|
||||
const rows = await syncRequest(syncContext, 'GET', changesUri);
|
||||
const startDate = new Date();
|
||||
|
||||
log.info("Pulled " + rows.length + " changes from " + changesUri);
|
||||
const resp = await syncRequest(syncContext, 'GET', changesUri);
|
||||
stats.outstandingPulls = resp.maxSyncId - lastSyncedPull;
|
||||
|
||||
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.`);
|
||||
}
|
||||
else {
|
||||
await syncUpdateService.updateEntity(sync, entity, syncContext.sourceId);
|
||||
const rows = resp.syncs;
|
||||
|
||||
if (rows.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
await setLastSyncedPull(sync.id);
|
||||
log.info("Pulled " + rows.length + " changes from " + changesUri + " in "
|
||||
+ (new Date().getTime() - startDate.getTime()) + "ms");
|
||||
|
||||
for (const {sync, entity} of rows) {
|
||||
if (!sourceIdService.isLocalSourceId(sync.sourceId)) {
|
||||
await syncUpdateService.updateEntity(sync, entity, syncContext.sourceId);
|
||||
}
|
||||
|
||||
stats.outstandingPulls = resp.maxSyncId - sync.id;
|
||||
|
||||
}
|
||||
|
||||
await setLastSyncedPull(rows[rows.length - 1].sync.id);
|
||||
}
|
||||
|
||||
log.info("Finished pull");
|
||||
@@ -109,9 +131,16 @@ async function pushSync(syncContext) {
|
||||
while (true) {
|
||||
const syncs = await sql.getRows('SELECT * FROM sync WHERE id > ? LIMIT 1000', [lastSyncedPush]);
|
||||
|
||||
if (syncs.length === 0) {
|
||||
log.info("Nothing to push");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
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`);
|
||||
// too noisy
|
||||
//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
|
||||
@@ -126,28 +155,33 @@ async function pushSync(syncContext) {
|
||||
});
|
||||
|
||||
if (filteredSyncs.length === 0) {
|
||||
log.info("Nothing to push");
|
||||
|
||||
// there still might be more syncs (because of batch limit), just all from current batch
|
||||
// has been filtered out
|
||||
await setLastSyncedPush(lastSyncedPush);
|
||||
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
const syncRecords = await getSyncRecords(filteredSyncs);
|
||||
|
||||
log.info(`Pushing ${syncRecords.length} syncs.`);
|
||||
const startDate = new Date();
|
||||
|
||||
await syncRequest(syncContext, 'PUT', '/api/sync/update', {
|
||||
sourceId: sourceIdService.getCurrentSourceId(),
|
||||
entities: syncRecords
|
||||
});
|
||||
|
||||
log.info(`Pushing ${syncRecords.length} syncs in ` + (new Date().getTime() - startDate.getTime()) + "ms");
|
||||
|
||||
lastSyncedPush = syncRecords[syncRecords.length - 1].sync.id;
|
||||
|
||||
await setLastSyncedPush(lastSyncedPush);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncFinished(syncContext) {
|
||||
await syncRequest(syncContext, 'POST', '/api/sync/finished');
|
||||
}
|
||||
|
||||
async function checkContentHash(syncContext) {
|
||||
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
|
||||
|
||||
@@ -169,7 +203,7 @@ async function checkContentHash(syncContext) {
|
||||
}
|
||||
|
||||
async function syncRequest(syncContext, method, uri, body) {
|
||||
const fullUri = syncSetup.SYNC_SERVER + uri;
|
||||
const fullUri = await syncOptions.getSyncServerHost() + uri;
|
||||
|
||||
try {
|
||||
const options = {
|
||||
@@ -178,21 +212,19 @@ async function syncRequest(syncContext, method, uri, body) {
|
||||
jar: syncContext.cookieJar,
|
||||
json: true,
|
||||
body: body,
|
||||
timeout: syncSetup.SYNC_TIMEOUT
|
||||
timeout: await syncOptions.getSyncTimeout()
|
||||
};
|
||||
|
||||
if (syncServerCertificate) {
|
||||
options.ca = syncServerCertificate;
|
||||
}
|
||||
const syncProxy = await syncOptions.getSyncProxy();
|
||||
|
||||
if (syncSetup.SYNC_PROXY && proxyToggle) {
|
||||
options.proxy = syncSetup.SYNC_PROXY;
|
||||
if (syncProxy && proxyToggle) {
|
||||
options.proxy = syncProxy;
|
||||
}
|
||||
|
||||
return await rp(options);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Request to ${method} ${fullUri} failed, inner exception: ${e.stack}`);
|
||||
throw new Error(`Request to ${method} ${fullUri} failed, error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +236,7 @@ const primaryKeys = {
|
||||
"images": "imageId",
|
||||
"note_images": "noteImageId",
|
||||
"labels": "labelId",
|
||||
"relations": "relationId",
|
||||
"api_tokens": "apiTokenId",
|
||||
"options": "name"
|
||||
};
|
||||
@@ -270,24 +303,28 @@ 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);
|
||||
async function updatePushStats() {
|
||||
const lastSyncedPush = await optionService.getOption('lastSyncedPush');
|
||||
|
||||
if (syncSetup.SYNC_PROXY) {
|
||||
log.info("Sync proxy: " + syncSetup.SYNC_PROXY);
|
||||
}
|
||||
stats.outstandingPushes = await sql.getValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]);
|
||||
}
|
||||
|
||||
if (syncSetup.SYNC_CERT_PATH) {
|
||||
log.info('Sync certificate: ' + syncSetup.SYNC_CERT_PATH);
|
||||
sqlInit.dbReady.then(async () => {
|
||||
if (await syncOptions.isSyncSetup()) {
|
||||
log.info("Setting up sync to " + await syncOptions.getSyncServerHost() + " with timeout " + await syncOptions.getSyncTimeout());
|
||||
|
||||
syncServerCertificate = fs.readFileSync(syncSetup.SYNC_CERT_PATH);
|
||||
const syncProxy = await syncOptions.getSyncProxy();
|
||||
|
||||
if (syncProxy) {
|
||||
log.info("Sync proxy: " + syncProxy);
|
||||
}
|
||||
|
||||
setInterval(cls.wrap(sync), 60000);
|
||||
|
||||
// kickoff initial sync immediately
|
||||
setTimeout(cls.wrap(sync), 1000);
|
||||
|
||||
setInterval(cls.wrap(updatePushStats), 1000);
|
||||
}
|
||||
else {
|
||||
log.info("Sync server not configured, sync timer not running.")
|
||||
@@ -296,5 +333,7 @@ sqlInit.dbReady.then(() => {
|
||||
|
||||
module.exports = {
|
||||
sync,
|
||||
getSyncRecords
|
||||
login,
|
||||
getSyncRecords,
|
||||
stats
|
||||
};
|
||||
@@ -10,12 +10,11 @@ async function doExclusively(func) {
|
||||
const releaseMutex = await instance.acquire();
|
||||
|
||||
try {
|
||||
await func();
|
||||
return await func();
|
||||
}
|
||||
finally {
|
||||
releaseMutex();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
10
src/services/sync_options.js
Normal file
10
src/services/sync_options.js
Normal file
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
|
||||
const optionService = require('./options');
|
||||
|
||||
module.exports = {
|
||||
getSyncServerHost: async () => await optionService.getOption('syncServerHost'),
|
||||
isSyncSetup: async () => !!await optionService.getOption('syncServerHost'),
|
||||
getSyncTimeout: async () => await optionService.getOption('syncServerTimeout'),
|
||||
getSyncProxy: async () => await optionService.getOption('syncProxy')
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = {
|
||||
SYNC_SERVER: config['Sync']['syncServerHost'],
|
||||
isSyncSetup: !!config['Sync']['syncServerHost'],
|
||||
SYNC_TIMEOUT: config['Sync']['syncServerTimeout'] || 5000,
|
||||
SYNC_PROXY: config['Sync']['syncProxy'],
|
||||
SYNC_CERT_PATH: config['Sync']['syncServerCertificate']
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
const sql = require('./sql');
|
||||
const sourceIdService = require('./source_id');
|
||||
const dateUtils = require('./date_utils');
|
||||
const syncSetup = require('./sync_setup');
|
||||
const syncOptions = require('./sync_options');
|
||||
const log = require('./log');
|
||||
const cls = require('./cls');
|
||||
const eventService = require('./events');
|
||||
@@ -42,6 +42,10 @@ async function addLabelSync(labelId, sourceId) {
|
||||
await addEntitySync("labels", labelId, sourceId);
|
||||
}
|
||||
|
||||
async function addRelationSync(relationId, sourceId) {
|
||||
await addEntitySync("relations", relationId, sourceId);
|
||||
}
|
||||
|
||||
async function addApiTokenSync(apiTokenId, sourceId) {
|
||||
await addEntitySync("api_tokens", apiTokenId, sourceId);
|
||||
}
|
||||
@@ -54,12 +58,6 @@ async function addEntitySync(entityName, entityId, sourceId) {
|
||||
sourceId: sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId()
|
||||
});
|
||||
|
||||
if (!syncSetup.isSyncSetup) {
|
||||
// this is because the "server" instances shouldn't have outstanding pushes
|
||||
// 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
|
||||
@@ -74,10 +72,11 @@ async function cleanupSyncRowsForMissingEntities(entityName, entityKey) {
|
||||
AND sync.entityId NOT IN (SELECT ${entityKey} FROM ${entityName})`);
|
||||
}
|
||||
|
||||
async function fillSyncRows(entityName, entityKey) {
|
||||
async function fillSyncRows(entityName, entityKey, condition = '') {
|
||||
await cleanupSyncRowsForMissingEntities(entityName, entityKey);
|
||||
|
||||
const entityIds = await sql.getColumn(`SELECT ${entityKey} FROM ${entityName}`);
|
||||
const entityIds = await sql.getColumn(`SELECT ${entityKey} FROM ${entityName}`
|
||||
+ (condition ? ` WHERE ${condition}` : ''));
|
||||
|
||||
for (const entityId of entityIds) {
|
||||
const existingRows = await sql.getValue("SELECT COUNT(id) FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
|
||||
@@ -106,7 +105,9 @@ async function fillAllSyncRows() {
|
||||
await fillSyncRows("images", "imageId");
|
||||
await fillSyncRows("note_images", "noteImageId");
|
||||
await fillSyncRows("labels", "labelId");
|
||||
await fillSyncRows("relations", "relationId");
|
||||
await fillSyncRows("api_tokens", "apiTokenId");
|
||||
await fillSyncRows("options", "name", 'isSynced = 1');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -119,6 +120,7 @@ module.exports = {
|
||||
addImageSync,
|
||||
addNoteImageSync,
|
||||
addLabelSync,
|
||||
addRelationSync,
|
||||
addApiTokenSync,
|
||||
addEntitySync,
|
||||
cleanupSyncRowsForMissingEntities,
|
||||
|
||||
@@ -33,11 +33,14 @@ async function updateEntity(sync, entity, sourceId) {
|
||||
else if (entityName === 'labels') {
|
||||
await updateLabel(entity, sourceId);
|
||||
}
|
||||
else if (entityName === 'relations') {
|
||||
await updateRelation(entity, sourceId);
|
||||
}
|
||||
else if (entityName === 'api_tokens') {
|
||||
await updateApiToken(entity, sourceId);
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unrecognized entity type ${sync}`);
|
||||
throw new Error(`Unrecognized entity type ${entityName}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +60,6 @@ async function updateNote(entity, sourceId) {
|
||||
await sql.replace("notes", entity);
|
||||
|
||||
await syncTableService.addNoteSync(entity.noteId, sourceId);
|
||||
await eventLogService.addNoteEvent(entity.noteId, "Synced note <note>");
|
||||
});
|
||||
|
||||
log.info("Update/sync note " + entity.noteId);
|
||||
@@ -69,7 +71,11 @@ async function updateBranch(entity, sourceId) {
|
||||
|
||||
await sql.transactional(async () => {
|
||||
if (orig === null || orig.dateModified < entity.dateModified) {
|
||||
delete entity.isExpanded;
|
||||
// isExpanded is not synced unless it's a new branch instance
|
||||
// otherwise in case of full new sync we'll get all branches (even root) collapsed.
|
||||
if (orig) {
|
||||
delete entity.isExpanded;
|
||||
}
|
||||
|
||||
await sql.replace('branches', entity);
|
||||
|
||||
@@ -109,7 +115,7 @@ async function updateNoteReordering(entityId, entity, sourceId) {
|
||||
async function updateOptions(entity, sourceId) {
|
||||
const orig = await sql.getRowOrNull("SELECT * FROM options WHERE name = ?", [entity.name]);
|
||||
|
||||
if (!orig.isSynced) {
|
||||
if (orig && !orig.isSynced) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,6 +188,20 @@ async function updateLabel(entity, sourceId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRelation(entity, sourceId) {
|
||||
const origRelation = await sql.getRow("SELECT * FROM relations WHERE relationId = ?", [entity.relationId]);
|
||||
|
||||
if (!origRelation || origRelation.dateModified <= entity.dateModified) {
|
||||
await sql.transactional(async () => {
|
||||
await sql.replace("relations", entity);
|
||||
|
||||
await syncTableService.addRelationSync(entity.relationId, sourceId);
|
||||
});
|
||||
|
||||
log.info("Update/sync relation " + entity.relationId);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateApiToken(entity, sourceId) {
|
||||
const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ async function setUserNamePassword() {
|
||||
|
||||
await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16));
|
||||
|
||||
sqlInit.setDbReadyAsResolved();
|
||||
await sqlInit.initDbConnection();
|
||||
}
|
||||
|
||||
const noteCount = parseInt(process.argv[2]);
|
||||
@@ -71,4 +71,4 @@ async function start() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
sqlInit.schemaReady.then(cls.wrap(start));
|
||||
sqlInit.dbReady.then(cls.wrap(start));
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
<div style="flex-grow: 100; display: flex;">
|
||||
<button class="btn btn-xs" id="jump-to-note-dialog-button" title="CTRL+J">Jump to note</button>
|
||||
<button class="btn btn-xs" id="recent-notes-button" title="CTRL+E">Recent notes</button>
|
||||
<button class="btn btn-xs" id="recent-changes-button">Recent changes</button>
|
||||
<div>
|
||||
<span style="font-size: smaller">Protected session:</span>
|
||||
@@ -38,7 +37,7 @@
|
||||
<button class="btn btn-xs" id="sync-now-button" title="Number of outstanding changes to be pushed to server">
|
||||
<span class="ui-icon ui-icon-refresh"></span>
|
||||
|
||||
Sync now (<span id="changes-to-push-count">0</span>)
|
||||
Sync now (<span id="outstanding-syncs-count">0</span>)
|
||||
</button>
|
||||
|
||||
<button class="btn btn-xs" id="options-button">
|
||||
@@ -171,6 +170,7 @@
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a id="show-note-revisions-button">Note revisions</a></li>
|
||||
<li><a class="show-labels-button"><kbd>Alt+L</kbd> Labels</a></li>
|
||||
<li><a class="show-relations-button"><kbd>Alt+R</kbd> Relations</a></li>
|
||||
<li><a id="show-source-button">HTML source</a></li>
|
||||
<li><a id="upload-file-button">Upload file</a></li>
|
||||
</ul>
|
||||
@@ -180,6 +180,8 @@
|
||||
</div>
|
||||
|
||||
<div id="note-detail-wrapper">
|
||||
<div id="note-detail-script-area"></div>
|
||||
|
||||
<div id="note-detail-component-wrapper">
|
||||
<div id="note-detail-text" class="note-detail-component" tabindex="2"></div>
|
||||
|
||||
@@ -254,18 +256,22 @@
|
||||
|
||||
<div id="children-overview"></div>
|
||||
|
||||
<div id="label-list">
|
||||
<button class="btn btn-sm show-labels-button">Labels:</button>
|
||||
<div id="labels-and-relations">
|
||||
<span id="label-list">
|
||||
<button class="btn btn-sm show-labels-button">Labels:</button>
|
||||
|
||||
<span id="label-list-inner"></span>
|
||||
<span id="label-list-inner"></span>
|
||||
</span>
|
||||
|
||||
<span id="relation-list">
|
||||
<button class="btn btn-sm show-relations-button">Relations:</button>
|
||||
|
||||
<span id="relation-list-inner"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="recent-notes-dialog" title="Recent notes" style="display: none;">
|
||||
<input id="recent-notes-search-input" class="form-control"/>
|
||||
</div>
|
||||
|
||||
<div id="add-link-dialog" title="Add note link" style="display: none;">
|
||||
<form id="add-link-form">
|
||||
<div id="add-link-type-div" class="radio">
|
||||
@@ -284,36 +290,39 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="note-autocomplete">Note</label>
|
||||
<input id="note-autocomplete" placeholder="search for note by its name" style="width: 100%;">
|
||||
|
||||
<div class="input-group">
|
||||
<input id="note-autocomplete" class="form-control" placeholder="search for note by its name" style="width: 100%;">
|
||||
|
||||
<span class="input-group-addon" id="add-link-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="add-link-title-form-group">
|
||||
<label for="link-title">Link title</label>
|
||||
<input id="link-title" style="width: 100%;">
|
||||
<input id="link-title" class="form-control" style="width: 100%;">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="add-link-prefix-form-group" title="Cloned note will be shown in note tree with given prefix">
|
||||
<label for="clone-prefix">Prefix (optional)</label>
|
||||
<input id="clone-prefix" style="width: 100%;">
|
||||
<input id="clone-prefix" class="form-control" style="width: 100%;">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-sm">Add note link</button>
|
||||
<button class="btn btn-primary">Add note link <kbd>enter</kbd></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="jump-to-note-dialog" title="Jump to note" style="display: none;">
|
||||
<form id="jump-to-note-form">
|
||||
<div class="form-group">
|
||||
<label for="jump-to-note-autocomplete">Note</label>
|
||||
<input id="jump-to-note-autocomplete" placeholder="search for note by its name" style="width: 100%;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="jump-to-note-autocomplete">Note</label>
|
||||
<div class="input-group">
|
||||
<input id="jump-to-note-autocomplete" class="form-control" placeholder="search for note by its name" style="width: 100%;">
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button id="jump-to-note-button" class="btn btn-sm btn-primary">Jump <kbd>enter</kbd></button>
|
||||
|
||||
<button id="show-in-full-text-button" class="btn btn-sm">Search in full text <kbd>ctrl+enter</kbd></button>
|
||||
<span class="input-group-addon" id="jump-to-note-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button id="show-in-full-text-button" class="btn btn-sm">Search in full text <kbd>ctrl+enter</kbd></button>
|
||||
</div>
|
||||
|
||||
<div id="protected-session-password-dialog" title="Protected session" style="display: none;">
|
||||
@@ -334,6 +343,7 @@
|
||||
<li><a href="#change-password">Change password</a></li>
|
||||
<li><a href="#protected-session-timeout">Protected session</a></li>
|
||||
<li><a href="#note-revision-snapshot-time-interval">Note revisions</a></li>
|
||||
<li><a href="#sync-setup">Sync</a></li>
|
||||
<li><a href="#advanced">Advanced</a></li>
|
||||
<li><a href="#about">About Trilium</a></li>
|
||||
</ul>
|
||||
@@ -404,6 +414,40 @@
|
||||
<button class="btn btn-sm">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="sync-setup">
|
||||
<h4 style="margin-top: 0px;">Sync configuration</h4>
|
||||
|
||||
<form id="sync-setup-form">
|
||||
<div class="form-group">
|
||||
<label for="sync-server-host">Server instance address</label>
|
||||
<input class="form-control" id="sync-server-host" placeholder="https://<host>:<port>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-server-timeout">Sync timeout (milliseconds)</label>
|
||||
<input class="form-control" id="sync-server-timeout" min="1" max="10000000" type="number">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-proxy">Sync proxy server (optional)</label>
|
||||
<input class="form-control" id="sync-proxy" placeholder="https://<host>:<port>">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-sm">Save</button>
|
||||
</form>
|
||||
|
||||
<h4>Sync test</h4>
|
||||
|
||||
<p>This will test connection and handshake to the sync server.</p>
|
||||
|
||||
<button id="test-sync-button" class="btn btn-sm">Test sync</button>
|
||||
|
||||
<h4>Sync document to the server instance</h4>
|
||||
|
||||
<p>This is used when you want to sync your local document to the server instance configured above. This is a one time action after which the documents are synced automatically and transparently.</p>
|
||||
|
||||
<button id="sync-to-server-button" class="btn btn-sm">Sync local document to the server instance</button>
|
||||
</div>
|
||||
<div id="advanced">
|
||||
<h4 style="margin-top: 0px;">Sync</h4>
|
||||
<button id="force-full-sync-button" class="btn btn-sm">Force full sync</button>
|
||||
@@ -426,14 +470,7 @@
|
||||
|
||||
This means that some images can disappear from note revisions.</p>
|
||||
|
||||
<button id="cleanup-unused-images-button" class="btn btn-warning btn-sm">Permanently cleanup unused images</button>
|
||||
|
||||
<h4>Soft-delete cleanup</h4>
|
||||
|
||||
<p>This deletes all soft deleted rows from the database. This change isn't synced and should be done manually on all instances.
|
||||
<strong>Use this only if you really know what you're doing.</strong></p>
|
||||
|
||||
<button id="cleanup-soft-deleted-items-button" class="btn btn-danger btn-sm">Permanently cleanup soft-deleted items</button>
|
||||
<button id="cleanup-unused-images-button" class="btn btn-sm">Permanently cleanup unused images</button>
|
||||
|
||||
<h4>Vacuum database</h4>
|
||||
|
||||
@@ -544,14 +581,14 @@
|
||||
<td data-bind="text: labelId" style="width: 150px;"></td>
|
||||
<td>
|
||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||
<input type="text" class="label-name" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.labelChanged }"/>
|
||||
<input type="text" class="label-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.labelChanged }"/>
|
||||
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate label.</div>
|
||||
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Label name can't be empty.</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="label-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.labelChanged }" style="width: 300px"/>
|
||||
<input type="text" class="label-value form-control" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.labelChanged }" style="width: 300px"/>
|
||||
</td>
|
||||
<td title="Delete" style="padding: 13px;">
|
||||
<td title="Delete" style="padding: 13px; cursor: pointer;">
|
||||
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteLabel"></span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -561,6 +598,61 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="relations-dialog" title="Note relations" style="display: none; padding: 20px;">
|
||||
<form data-bind="submit: save">
|
||||
<div style="text-align: center">
|
||||
<button class="btn btn-large" style="width: 200px;" id="save-relations-button" type="submit">Save changes <kbd>enter</kbd></button>
|
||||
</div>
|
||||
|
||||
<div style="height: 97%; overflow: auto">
|
||||
<table id="relations-table" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>ID</th>
|
||||
<th>Relation name</th>
|
||||
<th>Target note</th>
|
||||
<th>Inheritable</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: relations">
|
||||
<tr data-bind="if: isDeleted == 0">
|
||||
<td class="handle">
|
||||
<span class="glyphicon glyphicon-resize-vertical"></span>
|
||||
<input type="hidden" name="position" data-bind="value: position"/>
|
||||
</td>
|
||||
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
|
||||
<td data-bind="text: relationId" style="width: 150px;"></td>
|
||||
<td>
|
||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||
<input type="text" class="relation-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"/>
|
||||
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate relation.</div>
|
||||
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Relation name can't be empty.</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input class="form-control relation-target-note-id"
|
||||
placeholder="search for note by its name"
|
||||
data-bind="value: targetNoteId, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"
|
||||
style="width: 300px;">
|
||||
|
||||
<span class="input-group-addon relations-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td title="Inheritable relations are automatically inherited to the child notes">
|
||||
<input type="checkbox" value="1" data-bind="checked: isInheritable" />
|
||||
</td>
|
||||
<td title="Delete" style="padding: 13px; cursor: pointer;">
|
||||
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteRelation"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="tooltip" style="display: none;"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
@@ -5,31 +5,110 @@
|
||||
<title>Setup</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="width: 500px; margin: auto;">
|
||||
<div id="setup-dialog" style="width: 500px; margin: auto; display:none;">
|
||||
<h1>Trilium Notes setup</h1>
|
||||
|
||||
<div class="alert alert-warning" id="alert" style="display: none;">
|
||||
</div>
|
||||
|
||||
<p>You're almost done with the setup. That last thing is to choose username and password using which you'll login to the application.
|
||||
This password is also used for generating encryption key which encrypts protected notes.</p>
|
||||
<div id="setup-type" data-bind="visible: step() == 'setup-type'">
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupNewDocument">
|
||||
I'm a new user and I want to create new Trilium document for my notes</label>
|
||||
</div>
|
||||
<div class="radio" data-bind="if: instanceType == 'server'">
|
||||
<label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupSyncFromDesktop">
|
||||
I have desktop instance already and I want to setup sync with it</label>
|
||||
</div>
|
||||
<div class="radio" data-bind="if: instanceType == 'desktop'">
|
||||
<label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupSyncFromServer">
|
||||
I have server instance up and I want to setup sync with it</label>
|
||||
</div>
|
||||
|
||||
<button type="button" data-bind="disable: !setupTypeSelected(), click: selectSetupType" class="btn btn-primary">Next</button>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'new-document'">
|
||||
<h2>New document</h2>
|
||||
|
||||
<p>You're almost done with the setup. The last thing is to choose username and password using which you'll login to the application.
|
||||
This password is also used for generating encryption key which encrypts protected notes.</p>
|
||||
|
||||
<form id="setup-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" id="username" placeholder="Arbitrary string">
|
||||
<input type="text" class="form-control" data-bind="value: username" placeholder="Arbitrary string">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password1">Password</label>
|
||||
<input type="password" class="form-control" id="password1" placeholder="Password">
|
||||
<input type="password" class="form-control" data-bind="value: password1" placeholder="Password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password2">Repeat password</label>
|
||||
<input type="password" class="form-control" id="password2" placeholder="Password">
|
||||
<input type="password" class="form-control" data-bind="value: password2" placeholder="Password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-default">Save</button>
|
||||
</form>
|
||||
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
|
||||
|
||||
|
||||
|
||||
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-from-desktop'">
|
||||
<h2>Sync from Desktop</h2>
|
||||
|
||||
<p>This setup needs to be initiated from the desktop instance:</p>
|
||||
|
||||
<ol>
|
||||
<li>please open your desktop instance of Trilium Notes</li>
|
||||
<li>click on Options button in the top right</li>
|
||||
<li>click on Sync tab</li>
|
||||
<li>configure server instance address to the: <span id="current-host"></span> and click save.</li>
|
||||
<li>click on "Sync document to the server instance" button</li>
|
||||
<li>once you've done all this, click <a href="/">here</a></li>
|
||||
</ol>
|
||||
|
||||
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-from-server'">
|
||||
<h2>Sync from Server</h2>
|
||||
|
||||
<p>Please enter Trilium server address and credentials below. This will download the whole Trilium document from server and setup sync to it. Depending on the document size and your connection speed, this may take a while.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-server-host">Trilium server address</label>
|
||||
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="https://<hostname>:<port>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sync-proxy">Proxy server (optional)</label>
|
||||
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="https://<hostname>:<port>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" class="form-control" data-bind="value: username" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password1">Password</label>
|
||||
<input type="password" id="password1" class="form-control" data-bind="value: password1" placeholder="Password">
|
||||
</div>
|
||||
|
||||
<button type="button" data-bind="click: back" class="btn btn-default">Back</button>
|
||||
|
||||
|
||||
|
||||
<button type="button" data-bind="click: finish" class="btn btn-primary">Finish setup</button>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-in-progress'">
|
||||
<h2>Sync in progress</h2>
|
||||
|
||||
<div class="alert alert-success">Sync has been correctly set up. It will take some time for the initial sync to finish. Once it's done, you'll be redirected to the login page.</div>
|
||||
|
||||
<div data-bind="if: instanceType == 'desktop'">
|
||||
Outstanding sync items: <strong id="outstanding-syncs">N/A</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -37,6 +116,7 @@
|
||||
const glob = {
|
||||
sourceId: ''
|
||||
};
|
||||
const syncInProgress = <%= syncInProgress ? 'true' : 'false' %>;
|
||||
</script>
|
||||
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
@@ -47,6 +127,8 @@
|
||||
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
|
||||
<script src="libraries/bootstrap/js/bootstrap.js"></script>
|
||||
|
||||
<script src="/libraries/knockout.min.js"></script>
|
||||
|
||||
<script src="javascripts/setup.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user