feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
175
packages/lib/models/ItemChange.ts
普通文件
175
packages/lib/models/ItemChange.ts
普通文件
@@ -0,0 +1,175 @@
|
||||
import BaseModel, { ModelType } from '../BaseModel';
|
||||
import shim from '../shim';
|
||||
import eventManager, { EventName } from '../eventManager';
|
||||
import { ItemChangeEntity, SqlQuery } from '../services/database/types';
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
export interface ChangeSinceIdOptions {
|
||||
limit?: number;
|
||||
fields?: string[];
|
||||
}
|
||||
|
||||
interface BaseAddOptions {
|
||||
changeSource?: number|null;
|
||||
changeId?: string|null;
|
||||
}
|
||||
|
||||
interface AddOneOptions extends BaseAddOptions {
|
||||
beforeChangeItemJson?: string|null;
|
||||
}
|
||||
|
||||
interface AddMultiOptions extends BaseAddOptions {
|
||||
beforeChangeItemJsons?: string[]|null;
|
||||
}
|
||||
|
||||
export default class ItemChange extends BaseModel {
|
||||
|
||||
private static addChangeMutex_: Mutex = new Mutex();
|
||||
private static saveCalls_: boolean[] = [];
|
||||
|
||||
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!
|
||||
public static SOURCE_SHARE_SERVICE = 4;
|
||||
|
||||
public static tableName() {
|
||||
return 'item_changes';
|
||||
}
|
||||
|
||||
public static modelType() {
|
||||
return BaseModel.TYPE_ITEM_CHANGE;
|
||||
}
|
||||
|
||||
public static async addMulti(
|
||||
itemType: ModelType,
|
||||
itemIds: string[],
|
||||
type: number,
|
||||
{ changeSource = null, changeId = null, beforeChangeItemJsons = null }: AddMultiOptions = {},
|
||||
) {
|
||||
if (!itemIds.length) return;
|
||||
|
||||
if (changeSource === null) changeSource = ItemChange.SOURCE_UNSPECIFIED;
|
||||
if (beforeChangeItemJsons && beforeChangeItemJsons.length !== itemIds.length) throw new Error('beforeChangeItemJsons does not match itemIds');
|
||||
|
||||
ItemChange.saveCalls_.push(true);
|
||||
|
||||
// Using a mutex so that records can be added to the database in the
|
||||
// background, without making the UI wait.
|
||||
const release = await ItemChange.addChangeMutex_.acquire();
|
||||
|
||||
try {
|
||||
const queries: SqlQuery[] = [];
|
||||
|
||||
for (let i = 0; i < itemIds.length; i++) {
|
||||
const itemId = itemIds[i];
|
||||
const beforeChangeItemJson = beforeChangeItemJsons ? beforeChangeItemJsons[i] : '';
|
||||
|
||||
queries.push({
|
||||
sql: 'DELETE FROM item_changes WHERE item_id = ?',
|
||||
params: [itemId],
|
||||
});
|
||||
|
||||
queries.push({
|
||||
sql: 'INSERT INTO item_changes (item_type, item_id, type, source, created_time, before_change_item) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
params: [itemType, itemId, type, changeSource, Date.now(), beforeChangeItemJson],
|
||||
});
|
||||
}
|
||||
|
||||
await this.db().transactionExecBatch(queries);
|
||||
} finally {
|
||||
release();
|
||||
ItemChange.saveCalls_.pop();
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
eventManager.emit(EventName.ItemChange, {
|
||||
itemType: itemType,
|
||||
itemId,
|
||||
changeId,
|
||||
eventType: type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async add(itemType: ModelType, itemId: string, type: number, options: AddOneOptions = {}) {
|
||||
await this.addMulti(itemType, [itemId], type, {
|
||||
...options,
|
||||
beforeChangeItemJsons: options?.beforeChangeItemJson ? [options.beforeChangeItemJson] : null,
|
||||
});
|
||||
}
|
||||
|
||||
public static async lastChangeId() {
|
||||
const row = await this.db().selectOne('SELECT max(id) as max_id FROM item_changes');
|
||||
return row && row.max_id ? row.max_id : 0;
|
||||
}
|
||||
|
||||
// Because item changes are recorded in the background, this function
|
||||
// can be used for synchronous code, in particular when unit testing.
|
||||
public static async waitForAllSaved() {
|
||||
return new Promise((resolve) => {
|
||||
const iid = shim.setInterval(() => {
|
||||
if (!ItemChange.saveCalls_.length) {
|
||||
shim.clearInterval(iid);
|
||||
resolve(null);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
public static async deleteOldChanges(lowestChangeId: number, itemMinTtl: number) {
|
||||
if (!lowestChangeId) return;
|
||||
|
||||
const cutOffDate = Date.now() - itemMinTtl;
|
||||
|
||||
return this.db().exec(`
|
||||
DELETE FROM item_changes
|
||||
WHERE id <= ?
|
||||
AND created_time <= ?
|
||||
`, [lowestChangeId, cutOffDate]);
|
||||
}
|
||||
|
||||
public static async changesSinceId(changeId: number, options: ChangeSinceIdOptions = null): Promise<ItemChangeEntity[]> {
|
||||
options = {
|
||||
limit: 100,
|
||||
fields: ['id', 'item_type', 'item_id', 'type', 'created_time'],
|
||||
...options,
|
||||
};
|
||||
|
||||
return this.db().selectAll(`
|
||||
SELECT ${this.db().escapeFieldsToString(options.fields)}
|
||||
FROM item_changes
|
||||
WHERE id > ?
|
||||
ORDER BY id
|
||||
LIMIT ?
|
||||
`, [changeId, options.limit]);
|
||||
}
|
||||
|
||||
public static async itemChange(itemType: ModelType, itemId: string): Promise<ItemChangeEntity> {
|
||||
return await this.db().selectOne(`
|
||||
SELECT item_type, item_id, type, source, created_time, before_change_item
|
||||
FROM item_changes
|
||||
WHERE item_type = ? AND item_id = ?
|
||||
ORDER BY created_time DESC
|
||||
LIMIT 1
|
||||
`, [itemType, itemId]);
|
||||
}
|
||||
|
||||
public static async oldNoteContent(noteId: string): Promise<string> {
|
||||
const itemChange = await this.itemChange(ModelType.Note, noteId);
|
||||
return itemChange && itemChange.before_change_item ? itemChange.before_change_item : null;
|
||||
}
|
||||
|
||||
public static async resetOldNoteContent(noteId: string) {
|
||||
const itemChange = await this.itemChange(ModelType.Note, noteId);
|
||||
|
||||
// Only create a new item change if there is an existing item change and it has the before_change_item populated on it
|
||||
if (itemChange && itemChange.before_change_item) {
|
||||
await this.add(itemChange.item_type, itemChange.item_id, itemChange.type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户