1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-23 22:36:32 +02:00

Converted models and services to TypeScript

This commit is contained in:
Laurent Cozic
2021-01-22 17:41:11 +00:00
parent c895f7cd4f
commit 86610e7561
237 changed files with 1568 additions and 1443 deletions

View File

@@ -1,5 +1,5 @@
const BaseModel = require('../BaseModel').default;
const Note = require('./Note.js');
import BaseModel from '../BaseModel';
import Note from './Note';
export interface Notification {
id: number;

View File

@@ -1,15 +1,45 @@
const BaseModel = require('../BaseModel').default;
const { Database } = require('../database.js');
const Setting = require('./Setting').default;
const ItemChange = require('./ItemChange.js');
const JoplinError = require('../JoplinError.js');
const time = require('../time').default;
const { sprintf } = require('sprintf-js');
const { _ } = require('../locale');
const moment = require('moment');
const markdownUtils = require('../markdownUtils').default;
import { ModelType } from '../BaseModel';
import { NoteEntity } from '../services/database/types';
import Setting from './Setting';
import BaseModel from '../BaseModel';
import time from '../time';
import markdownUtils from '../markdownUtils';
import { _ } from '../locale';
const { Database } = require('../database.js');
import ItemChange from './ItemChange';
const JoplinError = require('../JoplinError.js');
const { sprintf } = require('sprintf-js');
const moment = require('moment');
export interface ItemsThatNeedDecryptionResult {
hasMore: boolean;
items: any[];
}
export default class BaseItem extends BaseModel {
public static encryptionService_: any = null;
public static revisionService_: any = null;
// Also update:
// - itemsThatNeedSync()
// - syncedItems()
public static syncItemDefinitions_: any[] = [
{ type: BaseModel.TYPE_NOTE, className: 'Note' },
{ type: BaseModel.TYPE_FOLDER, className: 'Folder' },
{ type: BaseModel.TYPE_RESOURCE, className: 'Resource' },
{ type: BaseModel.TYPE_TAG, className: 'Tag' },
{ type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' },
{ type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' },
{ type: BaseModel.TYPE_REVISION, className: 'Revision' },
];
public static SYNC_ITEM_LOCATION_LOCAL = 1;
public static SYNC_ITEM_LOCATION_REMOTE = 2;
class BaseItem extends BaseModel {
static useUuid() {
return true;
}
@@ -18,7 +48,7 @@ class BaseItem extends BaseModel {
return true;
}
static loadClass(className, classRef) {
static loadClass(className: string, classRef: any) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].className == className) {
BaseItem.syncItemDefinitions_[i].classRef = classRef;
@@ -29,7 +59,7 @@ class BaseItem extends BaseModel {
throw new Error(`Invalid class name: ${className}`);
}
static async findUniqueItemTitle(title, parentId = null) {
static async findUniqueItemTitle(title: string, parentId: string = null) {
let counter = 1;
let titleToTry = title;
while (true) {
@@ -53,7 +83,7 @@ class BaseItem extends BaseModel {
}
// Need to dynamically load the classes like this to avoid circular dependencies
static getClass(name) {
static getClass(name: string) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].className == name) {
const classRef = BaseItem.syncItemDefinitions_[i].classRef;
@@ -65,7 +95,7 @@ class BaseItem extends BaseModel {
throw new Error(`Invalid class name: ${name}`);
}
static getClassByItemType(itemType) {
static getClassByItemType(itemType: ModelType) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].type == itemType) {
return BaseItem.syncItemDefinitions_[i].classRef;
@@ -75,7 +105,7 @@ class BaseItem extends BaseModel {
throw new Error(`Invalid item type: ${itemType}`);
}
static async syncedCount(syncTarget) {
static async syncedCount(syncTarget: number) {
const ItemClass = this.itemClass(this.modelType());
const itemType = ItemClass.modelType();
// The fact that we don't check if the item_id still exist in the corresponding item table, means
@@ -85,24 +115,24 @@ class BaseItem extends BaseModel {
return r.total;
}
static systemPath(itemOrId, extension = null) {
static systemPath(itemOrId: any, extension: string = null) {
if (extension === null) extension = 'md';
if (typeof itemOrId === 'string') return `${itemOrId}.${extension}`;
else return `${itemOrId.id}.${extension}`;
}
static isSystemPath(path) {
static isSystemPath(path: string) {
// 1b175bb38bba47baac22b0b47f778113.md
if (!path || !path.length) return false;
let p = path.split('/');
let p: any = path.split('/');
p = p[p.length - 1];
p = p.split('.');
if (p.length != 2) return false;
return p[0].length == 32 && p[1] == 'md';
}
static itemClass(item) {
static itemClass(item: any): any {
if (!item) throw new Error('Item cannot be null');
if (typeof item === 'object') {
@@ -118,7 +148,7 @@ class BaseItem extends BaseModel {
}
// Returns the IDs of the items that have been synced at least once
static async syncedItemIds(syncTarget) {
static async syncedItemIds(syncTarget: number) {
if (!syncTarget) throw new Error('No syncTarget specified');
const temp = await this.db().selectAll('SELECT item_id FROM sync_items WHERE sync_time > 0 AND sync_target = ?', [syncTarget]);
const output = [];
@@ -128,25 +158,25 @@ class BaseItem extends BaseModel {
return output;
}
static async allSyncItems(syncTarget) {
static async allSyncItems(syncTarget: number) {
const output = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_target = ?', [syncTarget]);
return output;
}
static pathToId(path) {
static pathToId(path: string) {
const p = path.split('/');
const s = p[p.length - 1].split('.');
let name = s[0];
let name: any = s[0];
if (!name) return name;
name = name.split('-');
return name[name.length - 1];
}
static loadItemByPath(path) {
static loadItemByPath(path: string) {
return this.loadItemById(this.pathToId(path));
}
static async loadItemById(id) {
static async loadItemById(id: string) {
const classes = this.syncItemClassNames();
for (let i = 0; i < classes.length; i++) {
const item = await this.getClass(classes[i]).load(id);
@@ -155,11 +185,11 @@ class BaseItem extends BaseModel {
return null;
}
static async loadItemsByIds(ids) {
static async loadItemsByIds(ids: string[]) {
if (!ids.length) return [];
const classes = this.syncItemClassNames();
let output = [];
let output: any[] = [];
for (let i = 0; i < classes.length; i++) {
const ItemClass = this.getClass(classes[i]);
const sql = `SELECT * FROM ${ItemClass.tableName()} WHERE id IN ("${ids.join('","')}")`;
@@ -169,26 +199,26 @@ class BaseItem extends BaseModel {
return output;
}
static loadItemByField(itemType, field, value) {
static loadItemByField(itemType: number, field: string, value: any) {
const ItemClass = this.itemClass(itemType);
return ItemClass.loadByField(field, value);
}
static loadItem(itemType, id) {
static loadItem(itemType: ModelType, id: string) {
const ItemClass = this.itemClass(itemType);
return ItemClass.load(id);
}
static deleteItem(itemType, id) {
static deleteItem(itemType: ModelType, id: string) {
const ItemClass = this.itemClass(itemType);
return ItemClass.delete(id);
}
static async delete(id, options = null) {
static async delete(id: string, options: any = null) {
return this.batchDelete([id], options);
}
static async batchDelete(ids, options = null) {
static async batchDelete(ids: string[], options: any = null) {
if (!options) options = {};
let trackDeleted = true;
if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
@@ -198,7 +228,7 @@ class BaseItem extends BaseModel {
let conflictNoteIds = [];
if (this.modelType() == BaseModel.TYPE_NOTE) {
const conflictNotes = await this.db().selectAll(`SELECT id FROM notes WHERE id IN ("${ids.join('","')}") AND is_conflict = 1`);
conflictNoteIds = conflictNotes.map(n => {
conflictNoteIds = conflictNotes.map((n: NoteEntity) => {
return n.id;
});
}
@@ -234,20 +264,20 @@ class BaseItem extends BaseModel {
// - Client 1 syncs with target 2 only => the note is *not* deleted from target 2 because no information
// that it was previously deleted exist (deleted_items entry has been deleted).
// The solution would be to permanently store the list of deleted items on each client.
static deletedItems(syncTarget) {
static deletedItems(syncTarget: number) {
return this.db().selectAll('SELECT * FROM deleted_items WHERE sync_target = ?', [syncTarget]);
}
static async deletedItemCount(syncTarget) {
static async deletedItemCount(syncTarget: number) {
const r = await this.db().selectOne('SELECT count(*) as total FROM deleted_items WHERE sync_target = ?', [syncTarget]);
return r['total'];
}
static remoteDeletedItem(syncTarget, itemId) {
static remoteDeletedItem(syncTarget: number, itemId: string) {
return this.db().exec('DELETE FROM deleted_items WHERE item_id = ? AND sync_target = ?', [itemId, syncTarget]);
}
static serialize_format(propName, propValue) {
static serialize_format(propName: string, propValue: any) {
if (['created_time', 'updated_time', 'sync_time', 'user_updated_time', 'user_created_time'].indexOf(propName) >= 0) {
if (!propValue) return '';
propValue = `${moment.unix(propValue / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS')}Z`;
@@ -269,7 +299,7 @@ class BaseItem extends BaseModel {
.replace(/\r/g, '\\r');
}
static unserialize_format(type, propName, propValue) {
static unserialize_format(type: ModelType, propName: string, propValue: any) {
if (propName[propName.length - 1] == '_') return propValue; // Private property
const ItemClass = this.itemClass(type);
@@ -297,7 +327,7 @@ class BaseItem extends BaseModel {
: propValue;
}
static async serialize(item, shownKeys = null) {
static async serialize(item: any, shownKeys: any[] = null) {
if (shownKeys === null) {
shownKeys = this.itemClass(item).fieldNames();
shownKeys.push('type_');
@@ -305,7 +335,7 @@ class BaseItem extends BaseModel {
item = this.filter(item);
const output = {};
const output: any = {};
if ('title' in item && shownKeys.indexOf('title') >= 0) {
output.title = item.title;
@@ -352,7 +382,7 @@ class BaseItem extends BaseModel {
return this.revisionService_;
}
static async serializeForSync(item) {
static async serializeForSync(item: any) {
const ItemClass = this.itemClass(item);
const shownKeys = ItemClass.fieldNames();
shownKeys.push('type_');
@@ -366,7 +396,7 @@ class BaseItem extends BaseModel {
}
if (item.encryption_applied) {
const e = new Error('Trying to encrypt item that is already encrypted');
const e: any = new Error('Trying to encrypt item that is already encrypted');
e.code = 'cannotEncryptEncrypted';
throw e;
}
@@ -386,7 +416,7 @@ class BaseItem extends BaseModel {
// List of keys that won't be encrypted - mostly foreign keys required to link items
// with each others and timestamp required for synchronisation.
const keepKeys = ['id', 'note_id', 'tag_id', 'parent_id', 'updated_time', 'type_'];
const reducedItem = {};
const reducedItem: any = {};
for (let i = 0; i < keepKeys.length; i++) {
const n = keepKeys[i];
@@ -399,7 +429,7 @@ class BaseItem extends BaseModel {
return ItemClass.serialize(reducedItem);
}
static async decrypt(item) {
static async decrypt(item: any) {
if (!item.encryption_cipher_text) throw new Error(`Item is not encrypted: ${item.id}`);
const ItemClass = this.itemClass(item);
@@ -413,11 +443,11 @@ class BaseItem extends BaseModel {
return ItemClass.save(plainItem, { autoTimestamp: false, changeSource: ItemChange.SOURCE_DECRYPTION });
}
static async unserialize(content) {
static async unserialize(content: string) {
const lines = content.split('\n');
let output = {};
let output: any = {};
let state = 'readingProps';
const body = [];
const body: string[] = [];
for (let i = lines.length - 1; i >= 0; i--) {
let line = lines[i];
@@ -506,7 +536,7 @@ class BaseItem extends BaseModel {
return false;
}
static async itemsThatNeedDecryption(exclusions = [], limit = 100) {
static async itemsThatNeedDecryption(exclusions: string[] = [], limit = 100): Promise<ItemsThatNeedDecryptionResult> {
const classNames = this.encryptableItemClassNames();
for (let i = 0; i < classNames.length; i++) {
@@ -546,7 +576,7 @@ class BaseItem extends BaseModel {
throw new Error('Unreachable');
}
static async itemsThatNeedSync(syncTarget, limit = 100) {
static async itemsThatNeedSync(syncTarget: number, limit = 100) {
const classNames = this.syncItemClassNames();
for (let i = 0; i < classNames.length; i++) {
@@ -560,7 +590,7 @@ class BaseItem extends BaseModel {
// // CHANGED:
// 'SELECT * FROM [ITEMS] items JOIN sync_items s ON s.item_id = items.id WHERE sync_target = ? AND'
let extraWhere = [];
let extraWhere: any = [];
if (className == 'Note') extraWhere.push('is_conflict = 0');
if (className == 'Resource') extraWhere.push('encryption_blob_encrypted = 0');
if (ItemClass.encryptionSupported()) extraWhere.push('encryption_applied = 0');
@@ -636,7 +666,7 @@ class BaseItem extends BaseModel {
}
static syncItemClassNames() {
return BaseItem.syncItemDefinitions_.map(def => {
return BaseItem.syncItemDefinitions_.map((def: any) => {
return def.className;
});
}
@@ -652,19 +682,19 @@ class BaseItem extends BaseModel {
}
static syncItemTypes() {
return BaseItem.syncItemDefinitions_.map(def => {
return BaseItem.syncItemDefinitions_.map((def: any) => {
return def.type;
});
}
static modelTypeToClassName(type) {
static modelTypeToClassName(type: number) {
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
if (BaseItem.syncItemDefinitions_[i].type == type) return BaseItem.syncItemDefinitions_[i].className;
}
throw new Error(`Invalid type: ${type}`);
}
static async syncDisabledItems(syncTargetId) {
static async syncDisabledItems(syncTargetId: number) {
const rows = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_disabled = 1 AND sync_target = ?', [syncTargetId]);
const output = [];
for (let i = 0; i < rows.length; i++) {
@@ -681,7 +711,7 @@ class BaseItem extends BaseModel {
return output;
}
static updateSyncTimeQueries(syncTarget, item, syncTime, syncDisabled = false, syncDisabledReason = '', itemLocation = null) {
static updateSyncTimeQueries(syncTarget: number, item: any, syncTime: number, syncDisabled = false, syncDisabledReason = '', itemLocation: number = null) {
const itemType = item.type_;
const itemId = item.id;
if (!itemType || !itemId || syncTime === undefined) throw new Error(sprintf('Invalid parameters in updateSyncTimeQueries(): %d, %s, %d', syncTarget, JSON.stringify(item), syncTime));
@@ -700,12 +730,12 @@ class BaseItem extends BaseModel {
];
}
static async saveSyncTime(syncTarget, item, syncTime) {
static async saveSyncTime(syncTarget: number, item: any, syncTime: number) {
const queries = this.updateSyncTimeQueries(syncTarget, item, syncTime);
return this.db().transactionExecBatch(queries);
}
static async saveSyncDisabled(syncTargetId, item, syncDisabledReason, itemLocation = null) {
static async saveSyncDisabled(syncTargetId: number, item: any, syncDisabledReason: string, itemLocation: number = null) {
const syncTime = 'sync_time' in item ? item.sync_time : 0;
const queries = this.updateSyncTimeQueries(syncTargetId, item, syncTime, true, syncDisabledReason, itemLocation);
return this.db().transactionExecBatch(queries);
@@ -731,7 +761,7 @@ class BaseItem extends BaseModel {
await this.db().transactionExecBatch(queries);
}
static displayTitle(item) {
static displayTitle(item: any) {
if (!item) return '';
if (item.encryption_applied) return `🔑 ${_('Encrypted')}`;
return item.title ? item.title : _('Untitled');
@@ -753,7 +783,7 @@ class BaseItem extends BaseModel {
);
const items = await ItemClass.modelSelectAll(sql);
const ids = items.map(item => {
const ids = items.map((item: any) => {
return item.id;
});
if (!ids.length) continue;
@@ -762,7 +792,7 @@ class BaseItem extends BaseModel {
}
}
static async updateShareStatus(item, isShared) {
static async updateShareStatus(item: any, isShared: boolean) {
if (!item.id || !item.type_) throw new Error('Item must have an ID and a type');
if (!!item.is_shared === !!isShared) return false;
const ItemClass = this.getClassByItemType(item.type_);
@@ -781,7 +811,7 @@ class BaseItem extends BaseModel {
return true;
}
static async forceSync(itemId) {
static async forceSync(itemId: string) {
await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id = ?', [itemId]);
}
@@ -789,7 +819,7 @@ class BaseItem extends BaseModel {
await this.db().exec('UPDATE sync_items SET force_sync = 1');
}
static async save(o, options = null) {
static async save(o: any, options: any = null) {
if (!options) options = {};
if (options.userSideValidation === true) {
@@ -799,7 +829,7 @@ class BaseItem extends BaseModel {
return super.save(o, options);
}
static markdownTag(itemOrId) {
static markdownTag(itemOrId: any) {
const item = typeof itemOrId === 'object' ? itemOrId : {
id: itemOrId,
title: '',
@@ -813,23 +843,9 @@ class BaseItem extends BaseModel {
return output.join('');
}
static isMarkdownTag(md) {
static isMarkdownTag(md: any) {
if (!md) return false;
return !!md.match(/^\[.*?\]\(:\/[0-9a-zA-Z]{32}\)$/);
}
}
BaseItem.encryptionService_ = null;
BaseItem.revisionService_ = null;
// Also update:
// - itemsThatNeedSync()
// - syncedItems()
BaseItem.syncItemDefinitions_ = [{ type: BaseModel.TYPE_NOTE, className: 'Note' }, { type: BaseModel.TYPE_FOLDER, className: 'Folder' }, { type: BaseModel.TYPE_RESOURCE, className: 'Resource' }, { type: BaseModel.TYPE_TAG, className: 'Tag' }, { type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' }, { type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' }, { type: BaseModel.TYPE_REVISION, className: 'Revision' }];
BaseItem.SYNC_ITEM_LOCATION_LOCAL = 1;
BaseItem.SYNC_ITEM_LOCATION_REMOTE = 2;
module.exports = BaseItem;

View File

@@ -1,12 +1,18 @@
const BaseModel = require('../BaseModel').default;
const time = require('../time').default;
const Note = require('./Note.js');
import { FolderEntity } from '../services/database/types';
import BaseModel from '../BaseModel';
import time from '../time';
import { _ } from '../locale';
import Note from './Note';
const { Database } = require('../database.js');
const { _ } = require('../locale');
const BaseItem = require('./BaseItem.js');
import BaseItem from './BaseItem';
const { substrWithEllipsis } = require('../string-utils.js');
class Folder extends BaseItem {
interface FolderEntityWithChildren extends FolderEntity {
children?: FolderEntity[];
}
export default class Folder extends BaseItem {
static tableName() {
return 'folders';
}
@@ -15,15 +21,15 @@ class Folder extends BaseItem {
return BaseModel.TYPE_FOLDER;
}
static newFolder() {
static newFolder(): FolderEntity {
return {
id: null,
title: '',
};
}
static fieldToLabel(field) {
const fieldsToLabels = {
static fieldToLabel(field: string) {
const fieldsToLabels: any = {
title: _('title'),
last_note_user_updated_time: _('updated date'),
};
@@ -31,7 +37,7 @@ class Folder extends BaseItem {
return field in fieldsToLabels ? fieldsToLabels[field] : field;
}
static noteIds(parentId, options = null) {
static noteIds(parentId: string, options: any = null) {
options = Object.assign({}, {
includeConflicts: false,
}, options);
@@ -43,7 +49,7 @@ class Folder extends BaseItem {
return this.db()
.selectAll(`SELECT id FROM notes WHERE ${where.join(' AND ')}`, [parentId])
.then(rows => {
.then((rows: any[]) => {
const output = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
@@ -53,22 +59,22 @@ class Folder extends BaseItem {
});
}
static async subFolderIds(parentId) {
static async subFolderIds(parentId: string) {
const rows = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [parentId]);
return rows.map(r => r.id);
return rows.map((r: FolderEntity) => r.id);
}
static async noteCount(parentId) {
static async noteCount(parentId: string) {
const r = await this.db().selectOne('SELECT count(*) as total FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]);
return r ? r.total : 0;
}
static markNotesAsConflict(parentId) {
static markNotesAsConflict(parentId: string) {
const query = Database.updateQuery('notes', { is_conflict: 1 }, { parent_id: parentId });
return this.db().exec(query);
}
static async delete(folderId, options = null) {
static async delete(folderId: string, options: any = null) {
if (!options) options = {};
if (!('deleteChildren' in options)) options.deleteChildren = true;
@@ -114,8 +120,8 @@ class Folder extends BaseItem {
// Calculates note counts for all folders and adds the note_count attribute to each folder
// Note: this only calculates the overall number of nodes for this folder and all its descendants
static async addNoteCounts(folders, includeCompletedTodos = true) {
const foldersById = {};
static async addNoteCounts(folders: any[], includeCompletedTodos = true) {
const foldersById: any = {};
for (const f of folders) {
foldersById[f.id] = f;
@@ -137,7 +143,7 @@ class Folder extends BaseItem {
`;
const noteCounts = await this.db().selectAll(sql);
noteCounts.forEach((noteCount) => {
noteCounts.forEach((noteCount: any) => {
let parentId = noteCount.folder_id;
do {
const folder = foldersById[parentId];
@@ -155,18 +161,18 @@ class Folder extends BaseItem {
// Folders that contain notes that have been modified recently go on top.
// The remaining folders, that don't contain any notes are sorted by their own user_updated_time
static async orderByLastModified(folders, dir = 'DESC') {
static async orderByLastModified(folders: FolderEntity[], dir = 'DESC') {
dir = dir.toUpperCase();
const sql = 'select parent_id, max(user_updated_time) content_updated_time from notes where parent_id != "" group by parent_id';
const rows = await this.db().selectAll(sql);
const folderIdToTime = {};
const folderIdToTime: Record<string, number> = {};
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
folderIdToTime[row.parent_id] = row.content_updated_time;
}
const findFolderParent = folderId => {
const findFolderParent = (folderId: string) => {
const folder = BaseModel.byId(folders, folderId);
if (!folder) return null; // For the rare case of notes that are associated with a no longer existing folder
if (!folder.parent_id) return null;
@@ -180,7 +186,7 @@ class Folder extends BaseItem {
return null;
};
const applyChildTimeToParent = folderId => {
const applyChildTimeToParent = (folderId: string) => {
const parent = findFolderParent(folderId);
if (!parent) return;
@@ -213,7 +219,7 @@ class Folder extends BaseItem {
return output;
}
static async all(options = null) {
static async all(options: any = null) {
const output = await super.all(options);
if (options && options.includeConflictFolder) {
const conflictCount = await Note.conflictedCount();
@@ -222,24 +228,22 @@ class Folder extends BaseItem {
return output;
}
static async childrenIds(folderId, recursive) {
if (recursive === false) throw new Error('Not implemented');
static async childrenIds(folderId: string) {
const folders = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [folderId]);
let output = [];
let output: string[] = [];
for (let i = 0; i < folders.length; i++) {
const f = folders[i];
output.push(f.id);
const subChildrenIds = await this.childrenIds(f.id, true);
const subChildrenIds = await this.childrenIds(f.id);
output = output.concat(subChildrenIds);
}
return output;
}
static async expandTree(folders, parentId) {
static async expandTree(folders: FolderEntity[], parentId: string) {
const folderPath = await this.folderPath(folders, parentId);
folderPath.pop(); // We don't expand the leaft notebook
@@ -252,11 +256,11 @@ class Folder extends BaseItem {
}
}
static async allAsTree(folders = null, options = null) {
static async allAsTree(folders: FolderEntity[] = null, options: any = null) {
const all = folders ? folders : await this.all(options);
// https://stackoverflow.com/a/49387427/561309
function getNestedChildren(models, parentId) {
function getNestedChildren(models: FolderEntityWithChildren[], parentId: string) {
const nestedTreeStructure = [];
const length = models.length;
@@ -280,8 +284,8 @@ class Folder extends BaseItem {
return getNestedChildren(all, '');
}
static folderPath(folders, folderId) {
const idToFolders = {};
static folderPath(folders: FolderEntity[], folderId: string) {
const idToFolders: Record<string, FolderEntity> = {};
for (let i = 0; i < folders.length; i++) {
idToFolders[folders[i].id] = folders[i];
}
@@ -299,7 +303,7 @@ class Folder extends BaseItem {
return path;
}
static folderPathString(folders, folderId, maxTotalLength = 80) {
static folderPathString(folders: FolderEntity[], folderId: string, maxTotalLength = 80) {
const path = this.folderPath(folders, folderId);
let currentTotalLength = 0;
@@ -320,8 +324,8 @@ class Folder extends BaseItem {
return output.join(' / ');
}
static buildTree(folders) {
const idToFolders = {};
static buildTree(folders: FolderEntity[]) {
const idToFolders: Record<string, any> = {};
for (let i = 0; i < folders.length; i++) {
idToFolders[folders[i].id] = Object.assign({}, folders[i]);
idToFolders[folders[i].id].children = [];
@@ -348,19 +352,20 @@ class Folder extends BaseItem {
return rootFolders;
}
static async sortFolderTree(folders) {
static async sortFolderTree(folders: FolderEntityWithChildren[] = null) {
const output = folders ? folders : await this.allAsTree();
const sortFoldersAlphabetically = (folders) => {
folders.sort((a, b) => {
if (a.parentId === b.parentId) {
const sortFoldersAlphabetically = (folders: FolderEntityWithChildren[]) => {
folders.sort((a: FolderEntityWithChildren, b: FolderEntityWithChildren) => {
if (a.parent_id === b.parent_id) {
return a.title.localeCompare(b.title, undefined, { sensitivity: 'accent' });
}
return 0;
});
return folders;
};
const sortFolders = (folders) => {
const sortFolders = (folders: FolderEntityWithChildren[]) => {
for (let i = 0; i < folders.length; i++) {
const folder = folders[i];
if (folder.children) {
@@ -375,7 +380,7 @@ class Folder extends BaseItem {
return output;
}
static load(id) {
static load(id: string) {
if (id == this.conflictFolderId()) return this.conflictFolder();
return super.load(id);
}
@@ -384,7 +389,7 @@ class Folder extends BaseItem {
return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1');
}
static async canNestUnder(folderId, targetFolderId) {
static async canNestUnder(folderId: string, targetFolderId: string) {
if (folderId === targetFolderId) return false;
const conflictFolderId = Folder.conflictFolderId();
@@ -402,7 +407,7 @@ class Folder extends BaseItem {
return true;
}
static async moveToFolder(folderId, targetFolderId) {
static async moveToFolder(folderId: string, targetFolderId: string) {
if (!(await this.canNestUnder(folderId, targetFolderId))) throw new Error(_('Cannot move notebook to this location'));
// When moving a note to a different folder, the user timestamp is not updated.
@@ -421,7 +426,7 @@ class Folder extends BaseItem {
// manually creating a folder. They shouldn't be done for example when the folders
// are being synced to avoid any strange side-effects. Technically it's possible to
// have folders and notes with duplicate titles (or no title), or with reserved words.
static async save(o, options = null) {
static async save(o: FolderEntity, options: any = null) {
if (!options) options = {};
if (options.userSideValidation === true) {
@@ -458,7 +463,7 @@ class Folder extends BaseItem {
if (o.title == Folder.conflictFolderTitle()) throw new Error(_('Notebooks cannot be named "%s", which is a reserved title.', o.title));
}
return super.save(o, options).then(folder => {
return super.save(o, options).then((folder: FolderEntity) => {
this.dispatch({
type: 'FOLDER_UPDATE_ONE',
item: folder,
@@ -467,5 +472,3 @@ class Folder extends BaseItem {
});
}
}
module.exports = Folder;

View File

@@ -1,9 +1,21 @@
const BaseModel = require('../BaseModel').default;
import BaseModel, { ModelType } from '../BaseModel';
import shim from '../shim';
import eventManager from '../eventManager';
const Mutex = require('async-mutex').Mutex;
const shim = require('../shim').default;
const eventManager = require('../eventManager').default;
class ItemChange extends BaseModel {
export default class ItemChange extends BaseModel {
private static addChangeMutex_: any = new Mutex();
private static saveCalls_: any[] = [];
public static TYPE_CREATE = 1;
public static TYPE_UPDATE = 2;
public static TYPE_DELETE = 3;
public static SOURCE_UNSPECIFIED = 1;
public static SOURCE_SYNC = 2;
public static SOURCE_DECRYPTION = 2; // CAREFUL - SAME ID AS SOURCE_SYNC!
static tableName() {
return 'item_changes';
}
@@ -12,7 +24,7 @@ class ItemChange extends BaseModel {
return BaseModel.TYPE_ITEM_CHANGE;
}
static async add(itemType, itemId, type, changeSource = null, beforeChangeItemJson = null) {
static async add(itemType: ModelType, itemId: string, type: number, changeSource: any = null, beforeChangeItemJson: string = null) {
if (changeSource === null) changeSource = ItemChange.SOURCE_UNSPECIFIED;
if (!beforeChangeItemJson) beforeChangeItemJson = '';
@@ -57,27 +69,14 @@ class ItemChange extends BaseModel {
const iid = shim.setInterval(() => {
if (!ItemChange.saveCalls_.length) {
shim.clearInterval(iid);
resolve();
resolve(null);
}
}, 100);
});
}
static async deleteOldChanges(lowestChangeId) {
static async deleteOldChanges(lowestChangeId: number) {
if (!lowestChangeId) return;
return this.db().exec('DELETE FROM item_changes WHERE id <= ?', [lowestChangeId]);
}
}
ItemChange.addChangeMutex_ = new Mutex();
ItemChange.saveCalls_ = [];
ItemChange.TYPE_CREATE = 1;
ItemChange.TYPE_UPDATE = 2;
ItemChange.TYPE_DELETE = 3;
ItemChange.SOURCE_UNSPECIFIED = 1;
ItemChange.SOURCE_SYNC = 2;
ItemChange.SOURCE_DECRYPTION = 2; // CAREFUL - SAME ID AS SOURCE_SYNC!
module.exports = ItemChange;

View File

@@ -1,7 +1,8 @@
const BaseModel = require('../BaseModel').default;
const BaseItem = require('./BaseItem.js');
import BaseModel from '../BaseModel';
import { MasterKeyEntity } from '../services/database/types';
import BaseItem from './BaseItem';
class MasterKey extends BaseItem {
export default class MasterKey extends BaseItem {
static tableName() {
return 'master_keys';
}
@@ -18,11 +19,11 @@ class MasterKey extends BaseItem {
return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)');
}
static allWithoutEncryptionMethod(masterKeys, method) {
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) {
return masterKeys.filter(m => m.encryption_method !== method);
}
static async save(o, options = null) {
static async save(o: MasterKeyEntity, options: any = null) {
return super.save(o, options).then(item => {
this.dispatch({
type: 'MASTERKEY_UPDATE_ONE',
@@ -32,5 +33,3 @@ class MasterKey extends BaseItem {
});
}
}
module.exports = MasterKey;

View File

@@ -1,12 +1,12 @@
const BaseModel = require('../BaseModel').default;
import BaseModel from '../BaseModel';
const migrationScripts = {
const migrationScripts: Record<number, any> = {
20: require('../migrations/20.js'),
27: require('../migrations/27.js'),
33: require('../migrations/33.js'),
};
class Migration extends BaseModel {
export default class Migration extends BaseModel {
static tableName() {
return 'migrations';
}
@@ -19,10 +19,8 @@ class Migration extends BaseModel {
return this.modelSelectAll('SELECT * FROM migrations ORDER BY number ASC');
}
static script(number) {
static script(number: number) {
if (!migrationScripts[number]) throw new Error('Migration script has not been added to "migrationScripts" array');
return migrationScripts[number];
}
}
module.exports = Migration;

View File

@@ -1,28 +1,36 @@
const BaseModel = require('../BaseModel').default;
import BaseModel, { ModelType } from '../BaseModel';
import BaseItem from './BaseItem';
import ItemChange from './ItemChange';
import Setting from './Setting';
import shim from '../shim';
import time from '../time';
import markdownUtils from '../markdownUtils';
import { NoteEntity } from '../services/database/types';
const { sprintf } = require('sprintf-js');
const BaseItem = require('./BaseItem.js');
const ItemChange = require('./ItemChange.js');
const Resource = require('./Resource.js');
const Setting = require('./Setting').default;
const shim = require('../shim').default;
import Resource from './Resource';
const { pregQuote } = require('../string-utils.js');
const time = require('../time').default;
const { _ } = require('../locale');
const ArrayUtils = require('../ArrayUtils.js');
const lodash = require('lodash');
const urlUtils = require('../urlUtils.js');
const markdownUtils = require('../markdownUtils').default;
const { isImageMimeType } = require('../resourceUtils');
const { MarkupToHtml } = require('@joplin/renderer');
const { ALL_NOTES_FILTER_ID } = require('../reserved-ids');
class Note extends BaseItem {
export default class Note extends BaseItem {
public static updateGeolocationEnabled_ = true;
private static geolocationUpdating_ = false;
private static geolocationCache_: any;
private static dueDateObjects_: any;
static tableName() {
return 'notes';
}
static fieldToLabel(field) {
const fieldsToLabels = {
static fieldToLabel(field: string) {
const fieldsToLabels: Record<string, string> = {
title: _('title'),
user_updated_time: _('updated date'),
user_created_time: _('created date'),
@@ -32,11 +40,11 @@ class Note extends BaseItem {
return field in fieldsToLabels ? fieldsToLabels[field] : field;
}
static async serializeForEdit(note) {
static async serializeForEdit(note: NoteEntity) {
return this.replaceResourceInternalToExternalLinks(await super.serialize(note, ['title', 'body']));
}
static async unserializeForEdit(content) {
static async unserializeForEdit(content: string) {
content += `\n\ntype_: ${BaseModel.TYPE_NOTE}`;
const output = await super.unserialize(content);
if (!output.title) output.title = '';
@@ -45,14 +53,14 @@ class Note extends BaseItem {
return output;
}
static async serializeAllProps(note) {
static async serializeAllProps(note: NoteEntity) {
const fieldNames = this.fieldNames();
fieldNames.push('type_');
lodash.pull(fieldNames, 'title', 'body');
return super.serialize(note, fieldNames);
}
static minimalSerializeForDisplay(note) {
static minimalSerializeForDisplay(note: NoteEntity) {
const n = Object.assign({}, note);
const fieldNames = this.fieldNames();
@@ -80,21 +88,21 @@ class Note extends BaseItem {
return super.serialize(n, fieldNames);
}
static defaultTitle(noteBody) {
static defaultTitle(noteBody: string) {
return this.defaultTitleFromBody(noteBody);
}
static defaultTitleFromBody(body) {
static defaultTitleFromBody(body: string) {
return markdownUtils.titleFromBody(body);
}
static geolocationUrl(note) {
static geolocationUrl(note: NoteEntity) {
if (!('latitude' in note) || !('longitude' in note)) throw new Error('Latitude or longitude is missing');
if (!Number(note.latitude) && !Number(note.longitude)) throw new Error(_('This note does not have geolocation information.'));
return this.geoLocationUrlFromLatLong(note.latitude, note.longitude);
}
static geoLocationUrlFromLatLong(lat, long) {
static geoLocationUrlFromLatLong(lat: number, long: number) {
return sprintf('https://www.openstreetmap.org/?lat=%s&lon=%s&zoom=20', lat, long);
}
@@ -102,21 +110,21 @@ class Note extends BaseItem {
return BaseModel.TYPE_NOTE;
}
static linkedItemIds(body) {
static linkedItemIds(body: string) {
if (!body || body.length <= 32) return [];
const links = urlUtils.extractResourceUrls(body);
const itemIds = links.map(l => l.itemId);
const itemIds = links.map((l: any) => l.itemId);
return ArrayUtils.unique(itemIds);
}
static async linkedItems(body) {
static async linkedItems(body: string) {
const itemIds = this.linkedItemIds(body);
const r = await BaseItem.loadItemsByIds(itemIds);
return r;
}
static async linkedItemIdsByType(type, body) {
static async linkedItemIdsByType(type: ModelType, body: string) {
const items = await this.linkedItems(body);
const output = [];
@@ -128,15 +136,15 @@ class Note extends BaseItem {
return output;
}
static async linkedResourceIds(body) {
static async linkedResourceIds(body: string) {
return this.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, body);
}
static async linkedNoteIds(body) {
static async linkedNoteIds(body: string) {
return this.linkedItemIdsByType(BaseModel.TYPE_NOTE, body);
}
static async replaceResourceInternalToExternalLinks(body, options = null) {
static async replaceResourceInternalToExternalLinks(body: string, options: any = null) {
options = Object.assign({}, {
useAbsolutePaths: false,
}, options);
@@ -166,7 +174,7 @@ class Note extends BaseItem {
return body;
}
static async replaceResourceExternalToInternalLinks(body, options = null) {
static async replaceResourceExternalToInternalLinks(body: string, options: any = null) {
options = Object.assign({}, {
useAbsolutePaths: false,
}, options);
@@ -233,19 +241,19 @@ class Note extends BaseItem {
}
// Note: sort logic must be duplicated in previews();
static sortNotes(notes, orders, uncompletedTodosOnTop) {
const noteOnTop = note => {
static sortNotes(notes: NoteEntity[], orders: any[], uncompletedTodosOnTop: boolean) {
const noteOnTop = (note: NoteEntity) => {
return uncompletedTodosOnTop && note.is_todo && !note.todo_completed;
};
const noteFieldComp = (f1, f2) => {
const noteFieldComp = (f1: any, f2: any) => {
if (f1 === f2) return 0;
return f1 < f2 ? -1 : +1;
};
// Makes the sort deterministic, so that if, for example, a and b have the
// same updated_time, they aren't swapped every time a list is refreshed.
const sortIdenticalNotes = (a, b) => {
const sortIdenticalNotes = (a: NoteEntity, b: NoteEntity) => {
let r = null;
r = noteFieldComp(a.user_updated_time, b.user_updated_time);
if (r) return r;
@@ -262,7 +270,7 @@ class Note extends BaseItem {
const collator = this.getNaturalSortingCollator();
return notes.sort((a, b) => {
return notes.sort((a: NoteEntity, b: NoteEntity) => {
if (noteOnTop(a) && !noteOnTop(b)) return -1;
if (!noteOnTop(a) && noteOnTop(b)) return +1;
@@ -270,8 +278,8 @@ class Note extends BaseItem {
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
let aProp = a[order.by];
let bProp = b[order.by];
let aProp = (a as any)[order.by];
let bProp = (b as any)[order.by];
if (typeof aProp === 'string') aProp = aProp.toLowerCase();
if (typeof bProp === 'string') bProp = bProp.toLowerCase();
@@ -289,11 +297,11 @@ class Note extends BaseItem {
});
}
static previewFieldsWithDefaultValues(options = null) {
static previewFieldsWithDefaultValues(options: any = null) {
return Note.defaultValues(this.previewFields(options));
}
static previewFields(options = null) {
static previewFields(options: any = null) {
options = Object.assign({
includeTimestamps: true,
}, options);
@@ -309,12 +317,12 @@ class Note extends BaseItem {
return output;
}
static previewFieldsSql(fields = null) {
static previewFieldsSql(fields: string[] = null) {
if (fields === null) fields = this.previewFields();
return this.db().escapeFields(fields).join(',');
}
static async loadFolderNoteByField(folderId, field, value) {
static async loadFolderNoteByField(folderId: string, field: string, value: any) {
if (!folderId) throw new Error('folderId is undefined');
const options = {
@@ -327,7 +335,7 @@ class Note extends BaseItem {
return results.length ? results[0] : null;
}
static async previews(parentId, options = null) {
static async previews(parentId: string, options: any = null) {
// Note: ordering logic must be duplicated in sortNotes(), which is used
// to sort already loaded notes.
@@ -416,12 +424,12 @@ class Note extends BaseItem {
return results;
}
static preview(noteId, options = null) {
static preview(noteId: string, options: any = null) {
if (!options) options = { fields: null };
return this.modelSelectOne(`SELECT ${this.previewFieldsSql(options.fields)} FROM notes WHERE is_conflict = 0 AND id = ?`, [noteId]);
}
static async search(options = null) {
static async search(options: any = null) {
if (!options) options = {};
if (!options.conditions) options.conditions = [];
if (!options.conditionsParams) options.conditionsParams = [];
@@ -448,7 +456,7 @@ class Note extends BaseItem {
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
}
static async updateGeolocation(noteId) {
static async updateGeolocation(noteId: string) {
if (!Setting.value('trackLocation')) return;
if (!Note.updateGeolocationEnabled_) return;
@@ -496,7 +504,7 @@ class Note extends BaseItem {
return Note.save(note);
}
static filter(note) {
static filter(note: NoteEntity) {
if (!note) return note;
const output = super.filter(note);
@@ -506,7 +514,7 @@ class Note extends BaseItem {
return output;
}
static async copyToFolder(noteId, folderId) {
static async copyToFolder(noteId: string, folderId: string) {
if (folderId == this.getClass('Folder').conflictFolderId()) throw new Error(_('Cannot copy note to "%s" notebook', this.getClass('Folder').conflictFolderTitle()));
return Note.duplicate(noteId, {
@@ -517,7 +525,7 @@ class Note extends BaseItem {
});
}
static async moveToFolder(noteId, folderId) {
static async moveToFolder(noteId: string, folderId: string) {
if (folderId == this.getClass('Folder').conflictFolderId()) throw new Error(_('Cannot move note to "%s" notebook', this.getClass('Folder').conflictFolderTitle()));
// When moving a note to a different folder, the user timestamp is not updated.
@@ -533,7 +541,7 @@ class Note extends BaseItem {
return Note.save(modifiedNote, { autoTimestamp: false });
}
static changeNoteType(note, type) {
static changeNoteType(note: NoteEntity, type: string) {
if (!('is_todo' in note)) throw new Error('Missing "is_todo" property');
const newIsTodo = type === 'todo' ? 1 : 0;
@@ -548,11 +556,11 @@ class Note extends BaseItem {
return output;
}
static toggleIsTodo(note) {
static toggleIsTodo(note: NoteEntity) {
return this.changeNoteType(note, note.is_todo ? 'note' : 'todo');
}
static toggleTodoCompleted(note) {
static toggleTodoCompleted(note: NoteEntity) {
if (!('todo_completed' in note)) throw new Error('Missing "todo_completed" property');
note = Object.assign({}, note);
@@ -565,12 +573,12 @@ class Note extends BaseItem {
return note;
}
static async duplicateMultipleNotes(noteIds, options = null) {
static async duplicateMultipleNotes(noteIds: string[], options: any = null) {
// if options.uniqueTitle is true, a unique title for the duplicated file will be assigned.
const ensureUniqueTitle = options && options.ensureUniqueTitle;
for (const noteId of noteIds) {
const noteOptions = {};
const noteOptions: any = {};
// If ensureUniqueTitle is truthy, set the original note's name as root for the unique title.
if (ensureUniqueTitle) {
@@ -582,7 +590,7 @@ class Note extends BaseItem {
}
}
static async duplicate(noteId, options = null) {
static async duplicate(noteId: string, options: any = null) {
const changes = options && options.changes;
const uniqueTitle = options && options.uniqueTitle;
@@ -609,13 +617,13 @@ class Note extends BaseItem {
return this.save(newNote);
}
static async noteIsOlderThan(noteId, date) {
static async noteIsOlderThan(noteId: string, date: number) {
const n = await this.db().selectOne('SELECT updated_time FROM notes WHERE id = ?', [noteId]);
if (!n) throw new Error(`No such note: ${noteId}`);
return n.updated_time < date;
}
static async save(o, options = null) {
static async save(o: NoteEntity, options: any = null) {
const isNew = this.isNew(o, options);
const isProvisional = options && !!options.provisional;
const dispatchUpdateAction = options ? options.dispatchUpdateAction !== false : true;
@@ -647,7 +655,7 @@ class Note extends BaseItem {
if (oldNote) {
for (const field in o) {
if (!o.hasOwnProperty(field)) continue;
if (o[field] !== oldNote[field]) {
if ((o as any)[field] !== oldNote[field]) {
changedFields.push(field);
}
}
@@ -656,7 +664,7 @@ class Note extends BaseItem {
const note = await super.save(o, options);
const changeSource = options && options.changeSource ? options.changeSource : null;
ItemChange.add(BaseModel.TYPE_NOTE, note.id, isNew ? ItemChange.TYPE_CREATE : ItemChange.TYPE_UPDATE, changeSource, beforeNoteJson);
void ItemChange.add(BaseModel.TYPE_NOTE, note.id, isNew ? ItemChange.TYPE_CREATE : ItemChange.TYPE_UPDATE, changeSource, beforeNoteJson);
if (dispatchUpdateAction) {
this.dispatch({
@@ -677,14 +685,14 @@ class Note extends BaseItem {
return note;
}
static async batchDelete(ids, options = null) {
static async batchDelete(ids: string[], options: any = null) {
ids = ids.slice();
while (ids.length) {
const processIds = ids.splice(0, 50);
const notes = await Note.byIds(processIds);
const beforeChangeItems = {};
const beforeChangeItems: any = {};
for (const note of notes) {
beforeChangeItems[note.id] = JSON.stringify(note);
}
@@ -693,7 +701,7 @@ class Note extends BaseItem {
const changeSource = options && options.changeSource ? options.changeSource : null;
for (let i = 0; i < processIds.length; i++) {
const id = processIds[i];
ItemChange.add(BaseModel.TYPE_NOTE, id, ItemChange.TYPE_DELETE, changeSource, beforeChangeItems[id]);
void ItemChange.add(BaseModel.TYPE_NOTE, id, ItemChange.TYPE_DELETE, changeSource, beforeChangeItems[id]);
this.dispatch({
type: 'NOTE_DELETE',
@@ -707,11 +715,11 @@ class Note extends BaseItem {
return this.modelSelectAll('SELECT id, title, body, is_todo, todo_due, todo_completed, is_conflict FROM notes WHERE is_conflict = 0 AND is_todo = 1 AND todo_completed = 0 AND todo_due > ?', [time.unixMs()]);
}
static needAlarm(note) {
static needAlarm(note: NoteEntity) {
return note.is_todo && !note.todo_completed && note.todo_due >= time.unixMs() && !note.is_conflict;
}
static dueDateObject(note) {
static dueDateObject(note: NoteEntity) {
if (!!note.is_todo && note.todo_due) {
if (!this.dueDateObjects_) this.dueDateObjects_ = {};
if (this.dueDateObjects_[note.todo_due]) return this.dueDateObjects_[note.todo_due];
@@ -723,7 +731,7 @@ class Note extends BaseItem {
}
// Tells whether the conflict between the local and remote note can be ignored.
static mustHandleConflict(localNote, remoteNote) {
static mustHandleConflict(localNote: NoteEntity, remoteNote: NoteEntity) {
// That shouldn't happen so throw an exception
if (localNote.id !== remoteNote.id) throw new Error('Cannot handle conflict for two different notes');
@@ -737,7 +745,7 @@ class Note extends BaseItem {
return false;
}
static markupLanguageToLabel(markupLanguageId) {
static markupLanguageToLabel(markupLanguageId: number) {
if (markupLanguageId === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN) return 'Markdown';
if (markupLanguageId === MarkupToHtml.MARKUP_LANGUAGE_HTML) return 'HTML';
throw new Error(`Invalid markup language ID: ${markupLanguageId}`);
@@ -745,7 +753,7 @@ class Note extends BaseItem {
// When notes are sorted in "custom order", they are sorted by the "order" field first and,
// in those cases, where the order field is the same for some notes, by created time.
static customOrderByColumns(type = null) {
static customOrderByColumns(type: string = null) {
if (!type) type = 'object';
if (type === 'object') return [{ by: 'order', dir: 'DESC' }, { by: 'user_created_time', dir: 'DESC' }];
if (type === 'string') return 'ORDER BY `order` DESC, user_created_time DESC';
@@ -754,7 +762,7 @@ class Note extends BaseItem {
// Update the note "order" field without changing the user timestamps,
// which is generally what we want.
static async updateNoteOrder_(note, order) {
static async updateNoteOrder_(note: NoteEntity, order: any) {
return Note.save(Object.assign({}, note, {
order: order,
user_updated_time: note.user_updated_time,
@@ -765,7 +773,7 @@ class Note extends BaseItem {
// of unecessary updates, so it's the caller's responsability to update
// the UI once the call is finished. This is done by listening to the
// NOTE_IS_INSERTING_NOTES action in the application middleware.
static async insertNotesAt(folderId, noteIds, index) {
static async insertNotesAt(folderId: string, noteIds: string[], index: number) {
if (!noteIds.length) return;
const defer = () => {
@@ -874,7 +882,7 @@ class Note extends BaseItem {
}
}
static handleTitleNaturalSorting(items, options) {
static handleTitleNaturalSorting(items: NoteEntity[], options: any) {
if (options.order.length > 0 && options.order[0].by === 'title') {
const collator = this.getNaturalSortingCollator();
items.sort((a, b) => ((options.order[0].dir === 'ASC') ? 1 : -1) * collator.compare(a.title, b.title));
@@ -886,8 +894,3 @@ class Note extends BaseItem {
}
}
Note.updateGeolocationEnabled_ = true;
Note.geolocationUpdating_ = false;
module.exports = Note;

View File

@@ -1,7 +1,7 @@
const BaseItem = require('./BaseItem.js');
const BaseModel = require('../BaseModel').default;
import BaseItem from './BaseItem';
import BaseModel from '../BaseModel';
class NoteTag extends BaseItem {
export default class NoteTag extends BaseItem {
static tableName() {
return 'note_tags';
}
@@ -10,12 +10,12 @@ class NoteTag extends BaseItem {
return BaseModel.TYPE_NOTE_TAG;
}
static async byNoteIds(noteIds) {
static async byNoteIds(noteIds: string[]) {
if (!noteIds.length) return [];
return this.modelSelectAll(`SELECT * FROM note_tags WHERE note_id IN ("${noteIds.join('","')}")`);
}
static async tagIdsByNoteId(noteId) {
static async tagIdsByNoteId(noteId: string) {
const rows = await this.db().selectAll('SELECT tag_id FROM note_tags WHERE note_id = ?', [noteId]);
const output = [];
for (let i = 0; i < rows.length; i++) {
@@ -24,5 +24,3 @@ class NoteTag extends BaseItem {
return output;
}
}
module.exports = NoteTag;

View File

@@ -1,18 +1,29 @@
const BaseModel = require('../BaseModel').default;
const BaseItem = require('./BaseItem.js');
const ItemChange = require('./ItemChange.js');
const NoteResource = require('./NoteResource').default;
const ResourceLocalState = require('./ResourceLocalState.js');
const Setting = require('./Setting').default;
import BaseModel from '../BaseModel';
import BaseItem from './BaseItem';
import ItemChange from './ItemChange';
import NoteResource from './NoteResource';
import Setting from './Setting';
import markdownUtils from '../markdownUtils';
import { _ } from '../locale';
import { ResourceEntity, ResourceLocalStateEntity } from '../services/database/types';
import ResourceLocalState from './ResourceLocalState';
const pathUtils = require('../path-utils');
const { mime } = require('../mime-utils.js');
const { filename, safeFilename } = require('../path-utils');
const { FsDriverDummy } = require('../fs-driver-dummy.js');
const markdownUtils = require('../markdownUtils').default;
const JoplinError = require('../JoplinError');
const { _ } = require('../locale');
class Resource extends BaseItem {
export default class Resource extends BaseItem {
public static IMAGE_MAX_DIMENSION = 1920;
public static FETCH_STATUS_IDLE = 0;
public static FETCH_STATUS_STARTED = 1;
public static FETCH_STATUS_DONE = 2;
public static FETCH_STATUS_ERROR = 3;
public static fsDriver_: any;
static tableName() {
return 'resources';
}
@@ -26,12 +37,12 @@ class Resource extends BaseItem {
return this.encryptionService_;
}
static isSupportedImageMimeType(type) {
static isSupportedImageMimeType(type: string) {
const imageMimeTypes = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'];
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
}
static fetchStatuses(resourceIds) {
static fetchStatuses(resourceIds: string[]) {
if (!resourceIds.length) return [];
return this.db().selectAll(`SELECT resource_id, fetch_status FROM resource_local_states WHERE resource_id IN ("${resourceIds.join('","')}")`);
}
@@ -45,7 +56,7 @@ class Resource extends BaseItem {
`, [Resource.FETCH_STATUS_ERROR]);
}
static needToBeFetched(resourceDownloadMode = null, limit = null) {
static needToBeFetched(resourceDownloadMode: string = null, limit: number = null) {
const sql = ['SELECT * FROM resources WHERE encryption_applied = 0 AND id IN (SELECT resource_id FROM resource_local_states WHERE fetch_status = ?)'];
if (resourceDownloadMode !== 'always') {
sql.push('AND resources.id IN (SELECT resource_id FROM resources_to_download)');
@@ -59,7 +70,7 @@ class Resource extends BaseItem {
return await this.db().exec('UPDATE resource_local_states SET fetch_status = ? WHERE fetch_status = ?', [Resource.FETCH_STATUS_IDLE, Resource.FETCH_STATUS_STARTED]);
}
static resetErrorStatus(resourceId) {
static resetErrorStatus(resourceId: string) {
return this.db().exec('UPDATE resource_local_states SET fetch_status = ?, fetch_error = "" WHERE resource_id = ?', [Resource.FETCH_STATUS_IDLE, resourceId]);
}
@@ -69,7 +80,7 @@ class Resource extends BaseItem {
}
// DEPRECATED IN FAVOUR OF friendlySafeFilename()
static friendlyFilename(resource) {
static friendlyFilename(resource: ResourceEntity) {
let output = safeFilename(resource.title); // Make sure not to allow spaces or any special characters as it's not supported in HTTP headers
if (!output) output = resource.id;
let extension = resource.file_extension;
@@ -86,14 +97,14 @@ class Resource extends BaseItem {
return Setting.value('resourceDirName');
}
static filename(resource, encryptedBlob = false) {
static filename(resource: ResourceEntity, encryptedBlob = false) {
let extension = encryptedBlob ? 'crypted' : resource.file_extension;
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
extension = extension ? `.${extension}` : '';
return resource.id + extension;
}
static friendlySafeFilename(resource) {
static friendlySafeFilename(resource: ResourceEntity) {
let ext = resource.file_extension;
if (!ext) ext = resource.mime ? mime.toFileExtension(resource.mime) : '';
const safeExt = ext ? pathUtils.safeFileExtension(ext).toLowerCase() : '';
@@ -102,20 +113,20 @@ class Resource extends BaseItem {
return pathUtils.friendlySafeFilename(title) + (safeExt ? `.${safeExt}` : '');
}
static relativePath(resource, encryptedBlob = false) {
static relativePath(resource: ResourceEntity, encryptedBlob = false) {
return `${Setting.value('resourceDirName')}/${this.filename(resource, encryptedBlob)}`;
}
static fullPath(resource, encryptedBlob = false) {
static fullPath(resource: ResourceEntity, encryptedBlob = false) {
return `${Setting.value('resourceDir')}/${this.filename(resource, encryptedBlob)}`;
}
static async isReady(resource) {
static async isReady(resource: ResourceEntity) {
const r = await this.readyStatus(resource);
return r === 'ok';
}
static async readyStatus(resource) {
static async readyStatus(resource: ResourceEntity) {
const ls = await this.localState(resource);
if (!resource) return 'notFound';
if (ls.fetch_status !== Resource.FETCH_STATUS_DONE) return 'notDownloaded';
@@ -123,13 +134,13 @@ class Resource extends BaseItem {
return 'ok';
}
static async requireIsReady(resource) {
static async requireIsReady(resource: ResourceEntity) {
const readyStatus = await Resource.readyStatus(resource);
if (readyStatus !== 'ok') throw new Error(`Resource is not ready. Status: ${readyStatus}`);
}
// For resources, we need to decrypt the item (metadata) and the resource binary blob.
static async decrypt(item) {
static async decrypt(item: ResourceEntity) {
// The item might already be decrypted but not the blob (for instance if it crashes while
// decrypting the blob or was otherwise interrupted).
const decryptedItem = item.encryption_cipher_text ? await super.decrypt(item) : Object.assign({}, item);
@@ -177,7 +188,7 @@ class Resource extends BaseItem {
// as it should be uploaded to the sync target. Note that this may be different from what is stored
// in the database. In particular, the flag encryption_blob_encrypted might be 1 on the sync target
// if the resource is encrypted, but will be 0 locally because the device has the decrypted resource.
static async fullPathForSyncUpload(resource) {
static async fullPathForSyncUpload(resource: ResourceEntity) {
const plainTextPath = this.fullPath(resource);
if (!Setting.value('encryption.enabled')) {
@@ -201,7 +212,7 @@ class Resource extends BaseItem {
return { path: encryptedPath, resource: resourceCopy };
}
static markdownTag(resource) {
static markdownTag(resource: any) {
let tagAlt = resource.alt ? resource.alt : resource.title;
if (!tagAlt) tagAlt = '';
const lines = [];
@@ -217,36 +228,36 @@ class Resource extends BaseItem {
return lines.join('');
}
static internalUrl(resource) {
static internalUrl(resource: ResourceEntity) {
return `:/${resource.id}`;
}
static pathToId(path) {
static pathToId(path: string) {
return filename(path);
}
static async content(resource) {
static async content(resource: ResourceEntity) {
return this.fsDriver().readFile(this.fullPath(resource), 'Buffer');
}
static setContent(resource, content) {
static setContent(resource: ResourceEntity, content: any) {
return this.fsDriver().writeBinaryFile(this.fullPath(resource), content);
}
static isResourceUrl(url) {
static isResourceUrl(url: string) {
return url && url.length === 34 && url[0] === ':' && url[1] === '/';
}
static urlToId(url) {
static urlToId(url: string) {
if (!this.isResourceUrl(url)) throw new Error(`Not a valid resource URL: ${url}`);
return url.substr(2);
}
static async localState(resourceOrId) {
static async localState(resourceOrId: any) {
return ResourceLocalState.byResourceId(typeof resourceOrId === 'object' ? resourceOrId.id : resourceOrId);
}
static async setLocalState(resourceOrId, state) {
static async setLocalState(resourceOrId: any, state: ResourceLocalStateEntity) {
const id = typeof resourceOrId === 'object' ? resourceOrId.id : resourceOrId;
await ResourceLocalState.save(Object.assign({}, state, { resource_id: id }));
}
@@ -258,11 +269,11 @@ class Resource extends BaseItem {
// Only set the `size` field and nothing else, not even the update_time
// This is because it's only necessary to do it once after migration 20
// and each client does it so there's no need to sync the resource.
static async setFileSizeOnly(resourceId, fileSize) {
static async setFileSizeOnly(resourceId: string, fileSize: number) {
return this.db().exec('UPDATE resources set `size` = ? WHERE id = ?', [fileSize, resourceId]);
}
static async batchDelete(ids, options = null) {
static async batchDelete(ids: string[], options: any = null) {
// For resources, there's not really batch deleting since there's the file data to delete
// too, so each is processed one by one with the item being deleted last (since the db
// call is the less likely to fail).
@@ -280,13 +291,13 @@ class Resource extends BaseItem {
await ResourceLocalState.batchDelete(ids);
}
static async markForDownload(resourceId) {
static async markForDownload(resourceId: string) {
// Insert the row only if it's not already there
const t = Date.now();
await this.db().exec('INSERT INTO resources_to_download (resource_id, updated_time, created_time) SELECT ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM resources_to_download WHERE resource_id = ?)', [resourceId, t, t, resourceId]);
}
static async downloadedButEncryptedBlobCount(excludedIds = null) {
static async downloadedButEncryptedBlobCount(excludedIds: string[] = null) {
let excludedSql = '';
if (excludedIds && excludedIds.length) {
excludedSql = `AND resource_id NOT IN ("${excludedIds.join('","')}")`;
@@ -303,7 +314,7 @@ class Resource extends BaseItem {
return r ? r.total : 0;
}
static async downloadStatusCounts(status) {
static async downloadStatusCounts(status: number) {
const r = await this.db().selectOne(`
SELECT count(*) as total
FROM resource_local_states
@@ -324,7 +335,7 @@ class Resource extends BaseItem {
return r ? r.total : 0;
}
static fetchStatusToLabel(status) {
static fetchStatusToLabel(status: number) {
if (status === Resource.FETCH_STATUS_IDLE) return _('Not downloaded');
if (status === Resource.FETCH_STATUS_STARTED) return _('Downloading');
if (status === Resource.FETCH_STATUS_DONE) return _('Downloaded');
@@ -332,7 +343,7 @@ class Resource extends BaseItem {
throw new Error(`Invalid status: ${status}`);
}
static async updateResourceBlobContent(resourceId, newBlobFilePath) {
static async updateResourceBlobContent(resourceId: string, newBlobFilePath: string) {
const resource = await Resource.load(resourceId);
await this.requireIsReady(resource);
@@ -345,13 +356,13 @@ class Resource extends BaseItem {
});
}
static async resourceBlobContent(resourceId, encoding = 'Buffer') {
static async resourceBlobContent(resourceId: string, encoding = 'Buffer') {
const resource = await Resource.load(resourceId);
await this.requireIsReady(resource);
return await this.fsDriver().readFile(Resource.fullPath(resource), encoding);
}
static async duplicateResource(resourceId) {
static async duplicateResource(resourceId: string) {
const resource = await Resource.load(resourceId);
const localState = await Resource.localState(resource);
@@ -373,7 +384,7 @@ class Resource extends BaseItem {
return newResource;
}
static async createConflictResourceNote(resource) {
static async createConflictResourceNote(resource: ResourceEntity) {
const Note = this.getClass('Note');
const conflictResource = await Resource.duplicateResource(resource.id);
@@ -386,12 +397,3 @@ class Resource extends BaseItem {
}
}
Resource.IMAGE_MAX_DIMENSION = 1920;
Resource.FETCH_STATUS_IDLE = 0;
Resource.FETCH_STATUS_STARTED = 1;
Resource.FETCH_STATUS_DONE = 2;
Resource.FETCH_STATUS_ERROR = 3;
module.exports = Resource;

View File

@@ -1,7 +1,8 @@
const BaseModel = require('../BaseModel').default;
import BaseModel from '../BaseModel';
import { ResourceLocalStateEntity } from '../services/database/types';
const { Database } = require('../database.js');
class ResourceLocalState extends BaseModel {
export default class ResourceLocalState extends BaseModel {
static tableName() {
return 'resource_local_states';
}
@@ -10,7 +11,7 @@ class ResourceLocalState extends BaseModel {
return BaseModel.TYPE_RESOURCE_LOCAL_STATE;
}
static async byResourceId(resourceId) {
static async byResourceId(resourceId: string) {
if (!resourceId) throw new Error('Resource ID not provided'); // Sanity check
const result = await this.modelSelectOne('SELECT * FROM resource_local_states WHERE resource_id = ?', [resourceId]);
@@ -25,17 +26,15 @@ class ResourceLocalState extends BaseModel {
return result;
}
static async save(o) {
static async save(o: ResourceLocalStateEntity) {
const queries = [{ sql: 'DELETE FROM resource_local_states WHERE resource_id = ?', params: [o.resource_id] }, Database.insertQuery(this.tableName(), o)];
return this.db().transactionExecBatch(queries);
}
static batchDelete(ids, options = null) {
static batchDelete(ids: string[], options: any = null) {
options = options ? Object.assign({}, options) : {};
options.idFieldName = 'resource_id';
return super.batchDelete(ids, options);
}
}
module.exports = ResourceLocalState;

View File

@@ -1,5 +1,6 @@
const BaseModel = require('../BaseModel').default;
const BaseItem = require('./BaseItem.js');
import BaseModel, { ModelType } from '../BaseModel';
import { RevisionEntity } from '../services/database/types';
import BaseItem from './BaseItem';
const DiffMatchPatch = require('diff-match-patch');
const ArrayUtils = require('../ArrayUtils.js');
const JoplinError = require('../JoplinError');
@@ -7,7 +8,7 @@ const { sprintf } = require('sprintf-js');
const dmp = new DiffMatchPatch();
class Revision extends BaseItem {
export default class Revision extends BaseItem {
static tableName() {
return 'revisions';
}
@@ -16,21 +17,21 @@ class Revision extends BaseItem {
return BaseModel.TYPE_REVISION;
}
static createTextPatch(oldText, newText) {
static createTextPatch(oldText: string, newText: string) {
return dmp.patch_toText(dmp.patch_make(oldText, newText));
}
static applyTextPatch(text, patch) {
static applyTextPatch(text: string, patch: string) {
patch = dmp.patch_fromText(patch);
const result = dmp.patch_apply(patch, text);
if (!result || !result.length) throw new Error('Could not apply patch');
return result[0];
}
static createObjectPatch(oldObject, newObject) {
static createObjectPatch(oldObject: any, newObject: any) {
if (!oldObject) oldObject = {};
const output = {
const output: any = {
new: {},
deleted: [],
};
@@ -49,7 +50,7 @@ class Revision extends BaseItem {
return JSON.stringify(output);
}
static applyObjectPatch(object, patch) {
static applyObjectPatch(object: any, patch: any) {
patch = JSON.parse(patch);
const output = Object.assign({}, object);
@@ -64,10 +65,10 @@ class Revision extends BaseItem {
return output;
}
static patchStats(patch) {
static patchStats(patch: string) {
if (typeof patch === 'object') throw new Error('Not implemented');
const countChars = diffLine => {
const countChars = (diffLine: string) => {
return unescape(diffLine).length - 1;
};
@@ -93,7 +94,7 @@ class Revision extends BaseItem {
};
}
static revisionPatchStatsText(rev) {
static revisionPatchStatsText(rev: RevisionEntity) {
const titleStats = this.patchStats(rev.title_diff);
const bodyStats = this.patchStats(rev.body_diff);
const total = {
@@ -107,28 +108,28 @@ class Revision extends BaseItem {
return output.join(', ');
}
static async countRevisions(itemType, itemId) {
static async countRevisions(itemType: ModelType, itemId: string) {
const r = await this.db().selectOne('SELECT count(*) as total FROM revisions WHERE item_type = ? AND item_id = ?', [itemType, itemId]);
return r ? r.total : 0;
}
static latestRevision(itemType, itemId) {
static latestRevision(itemType: ModelType, itemId: string) {
return this.modelSelectOne('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time DESC LIMIT 1', [itemType, itemId]);
}
static allByType(itemType, itemId) {
static allByType(itemType: ModelType, itemId: string) {
return this.modelSelectAll('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time ASC', [itemType, itemId]);
}
static async itemsWithRevisions(itemType, itemIds) {
static async itemsWithRevisions(itemType: ModelType, itemIds: string[]) {
if (!itemIds.length) return [];
const rows = await this.db().selectAll(`SELECT distinct item_id FROM revisions WHERE item_type = ? AND item_id IN ("${itemIds.join('","')}")`, [itemType]);
return rows.map(r => r.item_id);
return rows.map((r: RevisionEntity) => r.item_id);
}
static async itemsWithNoRevisions(itemType, itemIds) {
static async itemsWithNoRevisions(itemType: ModelType, itemIds: string[]) {
const withRevs = await this.itemsWithRevisions(itemType, itemIds);
const output = [];
for (let i = 0; i < itemIds.length; i++) {
@@ -137,7 +138,7 @@ class Revision extends BaseItem {
return ArrayUtils.unique(output);
}
static moveRevisionToTop(revision, revs) {
static moveRevisionToTop(revision: RevisionEntity, revs: RevisionEntity[]) {
let targetIndex = -1;
for (let i = revs.length - 1; i >= 0; i--) {
const rev = revs[i];
@@ -160,7 +161,7 @@ class Revision extends BaseItem {
}
// Note: revs must be sorted by update_time ASC (as returned by allByType)
static async mergeDiffs(revision, revs = null) {
static async mergeDiffs(revision: RevisionEntity, revs: RevisionEntity[] = null) {
if (!('encryption_applied' in revision) || !!revision.encryption_applied) throw new JoplinError('Target revision is encrypted', 'revision_encrypted');
if (!revs) {
@@ -202,7 +203,7 @@ class Revision extends BaseItem {
return output;
}
static async deleteOldRevisions(ttl) {
static async deleteOldRevisions(ttl: number) {
// When deleting old revisions, we need to make sure that the oldest surviving revision
// is a "merged" one (as opposed to a diff from a now deleted revision). So every time
// we deleted a revision, we need to find if there's a corresponding surviving revision
@@ -210,7 +211,7 @@ class Revision extends BaseItem {
const cutOffDate = Date.now() - ttl;
const revisions = await this.modelSelectAll('SELECT * FROM revisions WHERE item_updated_time < ? ORDER BY item_updated_time DESC', [cutOffDate]);
const doneItems = {};
const doneItems: Record<string, boolean> = {};
for (const rev of revisions) {
const doneKey = `${rev.item_type}_${rev.item_id}`;
@@ -249,10 +250,8 @@ class Revision extends BaseItem {
}
}
static async revisionExists(itemType, itemId, updatedTime) {
static async revisionExists(itemType: ModelType, itemId: string, updatedTime: number) {
const existingRev = await Revision.latestRevision(itemType, itemId);
return existingRev && existingRev.item_updated_time === updatedTime;
}
}
module.exports = Revision;

View File

@@ -1,20 +0,0 @@
const BaseModel = require('../BaseModel').default;
class Search extends BaseModel {
static tableName() {
throw new Error('Not using database');
}
static modelType() {
return BaseModel.TYPE_SEARCH;
}
static keywords(query) {
let output = query.trim();
output = output.split(/[\s\t\n]+/);
output = output.filter(o => !!o);
return output;
}
}
module.exports = Search;

View File

@@ -0,0 +1,20 @@
// This class doesn't appear to be used at all
import BaseModel from '../BaseModel';
export default class Search extends BaseModel {
static tableName(): string {
throw new Error('Not using database');
}
static modelType() {
return BaseModel.TYPE_SEARCH;
}
static keywords(query: string) {
let output: any = query.trim();
output = output.split(/[\s\t\n]+/);
output = output.filter((o: any) => !!o);
return output;
}
}

View File

@@ -2,10 +2,10 @@ import shim from '../shim';
import { _, supportedLocalesToLanguages, defaultLocale } from '../locale';
import { ltrimSlashes } from '../path-utils';
import eventManager from '../eventManager';
const BaseModel = require('../BaseModel').default;
import BaseModel from '../BaseModel';
const { Database } = require('../database.js');
const SyncTargetRegistry = require('../SyncTargetRegistry.js');
const time = require('../time').default;
import time from '../time';
const { sprintf } = require('sprintf-js');
const ObjectUtils = require('../ObjectUtils');
const { toTitleCase } = require('../string-utils.js');
@@ -81,6 +81,81 @@ interface SettingSections {
class Setting extends BaseModel {
// For backward compatibility
public static TYPE_INT = SettingItemType.Int;
public static TYPE_STRING = SettingItemType.String;
public static TYPE_BOOL = SettingItemType.Bool;
public static TYPE_ARRAY = SettingItemType.Array;
public static TYPE_OBJECT = SettingItemType.Object;
public static TYPE_BUTTON = SettingItemType.Button;
public static THEME_LIGHT = 1;
public static THEME_DARK = 2;
public static THEME_OLED_DARK = 22;
public static THEME_SOLARIZED_LIGHT = 3;
public static THEME_SOLARIZED_DARK = 4;
public static THEME_DRACULA = 5;
public static THEME_NORD = 6;
public static THEME_ARITIM_DARK = 7;
public static FONT_DEFAULT = 0;
public static FONT_MENLO = 1;
public static FONT_COURIER_NEW = 2;
public static FONT_AVENIR = 3;
public static FONT_MONOSPACE = 4;
public static LAYOUT_ALL = 0;
public static LAYOUT_EDITOR_VIEWER = 1;
public static LAYOUT_EDITOR_SPLIT = 2;
public static LAYOUT_VIEWER_SPLIT = 3;
public static DATE_FORMAT_1 = 'DD/MM/YYYY';
public static DATE_FORMAT_2 = 'DD/MM/YY';
public static DATE_FORMAT_3 = 'MM/DD/YYYY';
public static DATE_FORMAT_4 = 'MM/DD/YY';
public static DATE_FORMAT_5 = 'YYYY-MM-DD';
public static DATE_FORMAT_6 = 'DD.MM.YYYY';
public static DATE_FORMAT_7 = 'YYYY.MM.DD';
public static TIME_FORMAT_1 = 'HH:mm';
public static TIME_FORMAT_2 = 'h:mm A';
public static SHOULD_REENCRYPT_NO = 0; // Data doesn't need to be re-encrypted
public static SHOULD_REENCRYPT_YES = 1; // Data should be re-encrypted
public static SHOULD_REENCRYPT_NOTIFIED = 2; // Data should be re-encrypted, and user has been notified
public static SYNC_UPGRADE_STATE_IDLE = 0; // Doesn't need to be upgraded
public static SYNC_UPGRADE_STATE_SHOULD_DO = 1; // Should be upgraded, but waiting for user to confirm
public static SYNC_UPGRADE_STATE_MUST_DO = 2; // Must be upgraded - on next restart, the upgrade will start
public static custom_css_files = {
JOPLIN_APP: 'userchrome.css',
RENDERED_MARKDOWN: 'userstyle.css',
};
// Contains constants that are set by the application and
// cannot be modified by the user:
public static constants_: any = {
env: 'SET_ME',
isDemo: false,
appName: 'joplin',
appId: 'SET_ME', // Each app should set this identifier
appType: 'SET_ME', // 'cli' or 'mobile'
resourceDirName: '',
resourceDir: '',
profileDir: '',
templateDir: '',
tempDir: '',
cacheDir: '',
pluginDir: '',
flagOpenDevTools: false,
syncVersion: 2,
startupDevPlugins: [],
};
public static autoSaveEnabled = true;
private static metadata_: SettingItems = null;
private static keychainService_: any = null;
private static keys_: string[] = null;
@@ -540,7 +615,7 @@ class Setting extends BaseModel {
appTypes: ['cli'],
label: () => _('Sort notes by'),
options: () => {
const Note = require('./Note');
const Note = require('./Note').default;
const noteSortFields = ['user_updated_time', 'user_created_time', 'title', 'order'];
const options: any = {};
for (let i = 0; i < noteSortFields.length; i++) {
@@ -566,7 +641,7 @@ class Setting extends BaseModel {
appTypes: ['cli'],
label: () => _('Sort notebooks by'),
options: () => {
const Folder = require('./Folder');
const Folder = require('./Folder').default;
const folderSortFields = ['title', 'last_note_user_updated_time'];
const options: any = {};
for (let i = 0; i < folderSortFields.length; i++) {
@@ -1609,79 +1684,4 @@ class Setting extends BaseModel {
}
}
// For backward compatibility
Setting.TYPE_INT = SettingItemType.Int;
Setting.TYPE_STRING = SettingItemType.String;
Setting.TYPE_BOOL = SettingItemType.Bool;
Setting.TYPE_ARRAY = SettingItemType.Array;
Setting.TYPE_OBJECT = SettingItemType.Object;
Setting.TYPE_BUTTON = SettingItemType.Button;
Setting.THEME_LIGHT = 1;
Setting.THEME_DARK = 2;
Setting.THEME_OLED_DARK = 22;
Setting.THEME_SOLARIZED_LIGHT = 3;
Setting.THEME_SOLARIZED_DARK = 4;
Setting.THEME_DRACULA = 5;
Setting.THEME_NORD = 6;
Setting.THEME_ARITIM_DARK = 7;
Setting.FONT_DEFAULT = 0;
Setting.FONT_MENLO = 1;
Setting.FONT_COURIER_NEW = 2;
Setting.FONT_AVENIR = 3;
Setting.FONT_MONOSPACE = 4;
Setting.LAYOUT_ALL = 0;
Setting.LAYOUT_EDITOR_VIEWER = 1;
Setting.LAYOUT_EDITOR_SPLIT = 2;
Setting.LAYOUT_VIEWER_SPLIT = 3;
Setting.DATE_FORMAT_1 = 'DD/MM/YYYY';
Setting.DATE_FORMAT_2 = 'DD/MM/YY';
Setting.DATE_FORMAT_3 = 'MM/DD/YYYY';
Setting.DATE_FORMAT_4 = 'MM/DD/YY';
Setting.DATE_FORMAT_5 = 'YYYY-MM-DD';
Setting.DATE_FORMAT_6 = 'DD.MM.YYYY';
Setting.DATE_FORMAT_7 = 'YYYY.MM.DD';
Setting.TIME_FORMAT_1 = 'HH:mm';
Setting.TIME_FORMAT_2 = 'h:mm A';
Setting.SHOULD_REENCRYPT_NO = 0; // Data doesn't need to be re-encrypted
Setting.SHOULD_REENCRYPT_YES = 1; // Data should be re-encrypted
Setting.SHOULD_REENCRYPT_NOTIFIED = 2; // Data should be re-encrypted, and user has been notified
Setting.SYNC_UPGRADE_STATE_IDLE = 0; // Doesn't need to be upgraded
Setting.SYNC_UPGRADE_STATE_SHOULD_DO = 1; // Should be upgraded, but waiting for user to confirm
Setting.SYNC_UPGRADE_STATE_MUST_DO = 2; // Must be upgraded - on next restart, the upgrade will start
Setting.custom_css_files = {
JOPLIN_APP: 'userchrome.css',
RENDERED_MARKDOWN: 'userstyle.css',
};
// Contains constants that are set by the application and
// cannot be modified by the user:
Setting.constants_ = {
env: 'SET_ME',
isDemo: false,
appName: 'joplin',
appId: 'SET_ME', // Each app should set this identifier
appType: 'SET_ME', // 'cli' or 'mobile'
resourceDirName: '',
resourceDir: '',
profileDir: '',
templateDir: '',
tempDir: '',
cacheDir: '',
pluginDir: '',
flagOpenDevTools: false,
syncVersion: 2,
startupDevPlugins: [],
};
Setting.autoSaveEnabled = true;
export default Setting;

View File

@@ -1,13 +0,0 @@
const BaseModel = require('../BaseModel').default;
class SmartFilter extends BaseModel {
static tableName() {
throw new Error('Not using database');
}
static modelType() {
return BaseModel.TYPE_SMART_FILTER;
}
}
module.exports = SmartFilter;

View File

@@ -0,0 +1,13 @@
// Not used??
import BaseModel from '../BaseModel';
export default class SmartFilter extends BaseModel {
static tableName(): string {
throw new Error('Not using database');
}
static modelType() {
return BaseModel.TYPE_SMART_FILTER;
}
}

View File

@@ -1,10 +1,12 @@
const BaseModel = require('../BaseModel').default;
const BaseItem = require('./BaseItem.js');
const NoteTag = require('./NoteTag.js');
const Note = require('./Note.js');
const { _ } = require('../locale');
import { TagEntity } from '../services/database/types';
class Tag extends BaseItem {
import BaseModel from '../BaseModel';
import BaseItem from './BaseItem';
import NoteTag from './NoteTag';
import Note from './Note';
import { _ } from '../locale';
export default class Tag extends BaseItem {
static tableName() {
return 'tags';
}
@@ -13,7 +15,7 @@ class Tag extends BaseItem {
return BaseModel.TYPE_TAG;
}
static async noteIds(tagId) {
static async noteIds(tagId: string) {
const rows = await this.db().selectAll('SELECT note_id FROM note_tags WHERE tag_id = ?', [tagId]);
const output = [];
for (let i = 0; i < rows.length; i++) {
@@ -22,7 +24,7 @@ class Tag extends BaseItem {
return output;
}
static async notes(tagId, options = null) {
static async notes(tagId: string, options: any = null) {
if (options === null) options = {};
const noteIds = await this.noteIds(tagId);
@@ -37,7 +39,7 @@ class Tag extends BaseItem {
}
// Untag all the notes and delete tag
static async untagAll(tagId) {
static async untagAll(tagId: string) {
const noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ?', [tagId]);
for (let i = 0; i < noteTags.length; i++) {
await NoteTag.delete(noteTags[i].id);
@@ -46,7 +48,7 @@ class Tag extends BaseItem {
await Tag.delete(tagId);
}
static async delete(id, options = null) {
static async delete(id: string, options: any = null) {
if (!options) options = {};
await super.delete(id, options);
@@ -57,7 +59,7 @@ class Tag extends BaseItem {
});
}
static async addNote(tagId, noteId) {
static async addNote(tagId: string, noteId: string) {
const hasIt = await this.hasNote(tagId, noteId);
if (hasIt) return;
@@ -87,7 +89,7 @@ class Tag extends BaseItem {
return output;
}
static async removeNote(tagId, noteId) {
static async removeNote(tagId: string, noteId: string) {
const noteTags = await NoteTag.modelSelectAll('SELECT id FROM note_tags WHERE tag_id = ? and note_id = ?', [tagId, noteId]);
for (let i = 0; i < noteTags.length; i++) {
await NoteTag.delete(noteTags[i].id);
@@ -99,12 +101,12 @@ class Tag extends BaseItem {
});
}
static loadWithCount(tagId) {
static loadWithCount(tagId: string) {
const sql = 'SELECT * FROM tags_with_note_count WHERE id = ?';
return this.modelSelectOne(sql, [tagId]);
}
static async hasNote(tagId, noteId) {
static async hasNote(tagId: string, noteId: string) {
const r = await this.db().selectOne('SELECT note_id FROM note_tags WHERE tag_id = ? AND note_id = ? LIMIT 1', [tagId, noteId]);
return !!r;
}
@@ -113,24 +115,24 @@ class Tag extends BaseItem {
return await Tag.modelSelectAll('SELECT * FROM tags_with_note_count');
}
static async searchAllWithNotes(options) {
static async searchAllWithNotes(options: any) {
if (!options) options = {};
if (!options.conditions) options.conditions = [];
options.conditions.push('id IN (SELECT distinct id FROM tags_with_note_count)');
return this.search(options);
}
static async tagsByNoteId(noteId) {
static async tagsByNoteId(noteId: string) {
const tagIds = await NoteTag.tagIdsByNoteId(noteId);
if (!tagIds.length) return [];
return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${tagIds.join('","')}")`);
}
static async commonTagsByNoteIds(noteIds) {
static async commonTagsByNoteIds(noteIds: string[]) {
if (!noteIds || noteIds.length === 0) {
return [];
}
let commonTagIds = await NoteTag.tagIdsByNoteId(noteIds[0]);
let commonTagIds: string[] = await NoteTag.tagIdsByNoteId(noteIds[0]);
for (let i = 1; i < noteIds.length; i++) {
const tagIds = await NoteTag.tagIdsByNoteId(noteIds[i]);
commonTagIds = commonTagIds.filter(value => tagIds.includes(value));
@@ -141,17 +143,17 @@ class Tag extends BaseItem {
return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${commonTagIds.join('","')}")`);
}
static async loadByTitle(title) {
static async loadByTitle(title: string) {
return this.loadByField('title', title, { caseInsensitive: true });
}
static async addNoteTagByTitle(noteId, tagTitle) {
static async addNoteTagByTitle(noteId: string, tagTitle: string) {
let tag = await this.loadByTitle(tagTitle);
if (!tag) tag = await Tag.save({ title: tagTitle }, { userSideValidation: true });
return await this.addNote(tag.id, noteId);
}
static async setNoteTagsByTitles(noteId, tagTitles) {
static async setNoteTagsByTitles(noteId: string, tagTitles: string[]) {
const previousTags = await this.tagsByNoteId(noteId);
const addedTitles = [];
@@ -171,7 +173,7 @@ class Tag extends BaseItem {
}
}
static async setNoteTagsByIds(noteId, tagIds) {
static async setNoteTagsByIds(noteId: string, tagIds: string[]) {
const previousTags = await this.tagsByNoteId(noteId);
const addedIds = [];
@@ -188,7 +190,7 @@ class Tag extends BaseItem {
}
}
static async save(o, options = null) {
static async save(o: TagEntity, options: any = null) {
options = Object.assign({}, {
dispatchUpdateAction: true,
userSideValidation: false,
@@ -203,7 +205,7 @@ class Tag extends BaseItem {
}
}
return super.save(o, options).then(tag => {
return super.save(o, options).then((tag: TagEntity) => {
if (options.dispatchUpdateAction) {
this.dispatch({
type: 'TAG_UPDATE_ONE',
@@ -215,5 +217,3 @@ class Tag extends BaseItem {
});
}
}
module.exports = Tag;