Initial implementation
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import type { JoplinAdapter, NoteQuery } from '../src/adapter/joplin-adapter.js';
|
||||
import type { ChangePage, Note, Notebook, Pagination, RevisionRecord, Tag } from '../src/domain/types.js';
|
||||
|
||||
const id = () => randomBytes(16).toString('hex');
|
||||
|
||||
export class FakeAdapter implements JoplinAdapter {
|
||||
public notebooks: Notebook[] = [];
|
||||
public notes: Note[] = [];
|
||||
public tags: Tag[] = [];
|
||||
public revisions: RevisionRecord[] = [];
|
||||
public syncCalls = 0;
|
||||
public dataCalls = 0;
|
||||
public failSyncCalls = new Set<number>();
|
||||
public delayMs = 0;
|
||||
public concurrentCalls = 0;
|
||||
public maxConcurrentCalls = 0;
|
||||
|
||||
public async prepare(): Promise<void> {}
|
||||
public async close(): Promise<void> {}
|
||||
|
||||
public async sync(): Promise<void> {
|
||||
await this.track(async () => {
|
||||
this.syncCalls += 1;
|
||||
if (this.failSyncCalls.has(this.syncCalls)) throw new Error('sync offline');
|
||||
});
|
||||
}
|
||||
|
||||
public async listNotebooks(): Promise<Notebook[]> {
|
||||
return this.data(() => structuredClone(this.notebooks));
|
||||
}
|
||||
|
||||
public async getNotebook(notebookId: string): Promise<Notebook | null> {
|
||||
return this.data(() => structuredClone(this.notebooks.find(item => item.id === notebookId) ?? null));
|
||||
}
|
||||
|
||||
public async createNotebook(input: { title: string; parent_id?: string }): Promise<Notebook> {
|
||||
return this.data(() => {
|
||||
const notebook: Notebook = { id: id(), parent_id: input.parent_id ?? '', title: input.title, created_time: Date.now(), updated_time: Date.now() };
|
||||
this.notebooks.push(notebook);
|
||||
return structuredClone(notebook);
|
||||
});
|
||||
}
|
||||
|
||||
public async listNotes(query: NoteQuery): Promise<Note[]> {
|
||||
return this.data(() => structuredClone(this.notes.filter(note =>
|
||||
(query.parent_id === undefined || note.parent_id === query.parent_id) && (query.include_deleted || !note.deleted_time))));
|
||||
}
|
||||
|
||||
public async getNote(noteId: string, includeDeleted = false): Promise<Note | null> {
|
||||
return this.data(() => {
|
||||
const note = this.notes.find(item => item.id === noteId && (includeDeleted || !item.deleted_time));
|
||||
return structuredClone(note ?? null);
|
||||
});
|
||||
}
|
||||
|
||||
public async createNote(input: { parent_id: string; title: string; body: string }): Promise<Note> {
|
||||
return this.data(() => {
|
||||
const now = Date.now();
|
||||
const note: Note = { id: id(), ...input, created_time: now, updated_time: now, deleted_time: 0, is_todo: 0, is_conflict: 0 };
|
||||
this.notes.push(note);
|
||||
return structuredClone(note);
|
||||
});
|
||||
}
|
||||
|
||||
public async updateNote(noteId: string, input: Partial<Pick<Note, 'parent_id' | 'title' | 'body' | 'deleted_time'>>): Promise<Note> {
|
||||
return this.data(() => {
|
||||
const note = this.notes.find(item => item.id === noteId);
|
||||
if (!note) throw new Error('not found');
|
||||
Object.assign(note, input, { updated_time: Date.now() });
|
||||
return structuredClone(note);
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteNote(noteId: string): Promise<void> {
|
||||
await this.data(() => {
|
||||
const note = this.notes.find(item => item.id === noteId);
|
||||
if (note) note.deleted_time = Date.now();
|
||||
});
|
||||
}
|
||||
|
||||
public async listTags(): Promise<Tag[]> { return this.data(() => structuredClone(this.tags)); }
|
||||
public async getTag(tagId: string): Promise<Tag | null> { return this.data(() => structuredClone(this.tags.find(tag => tag.id === tagId) ?? null)); }
|
||||
public async createTag(input: { title: string }): Promise<Tag> {
|
||||
return this.data(() => {
|
||||
const tag = { id: id(), title: input.title };
|
||||
this.tags.push(tag);
|
||||
return structuredClone(tag);
|
||||
});
|
||||
}
|
||||
public async listNoteTags(): Promise<Tag[]> { return this.data(() => []); }
|
||||
public async listTagNotes(): Promise<Note[]> { return this.data(() => []); }
|
||||
public async addTagToNote(): Promise<void> { await this.data(() => undefined); }
|
||||
public async removeTagFromNote(): Promise<void> { await this.data(() => undefined); }
|
||||
public async search(_query: string, _pagination: Pagination): Promise<Note[]> { return this.data(() => structuredClone(this.notes)); }
|
||||
public async listRevisions(noteId: string): Promise<RevisionRecord[]> {
|
||||
return this.data(() => structuredClone(this.revisions.filter(revision => revision.item_id === noteId)));
|
||||
}
|
||||
public async listChanges(cursor: string | undefined): Promise<ChangePage> {
|
||||
return this.data(() => ({ items: [], cursor: cursor ?? '0', has_more: false }));
|
||||
}
|
||||
|
||||
private async data<T>(operation: () => T): Promise<T> {
|
||||
this.dataCalls += 1;
|
||||
return this.track(operation);
|
||||
}
|
||||
|
||||
private async track<T>(operation: () => T): Promise<T> {
|
||||
this.concurrentCalls += 1;
|
||||
this.maxConcurrentCalls = Math.max(this.maxConcurrentCalls, this.concurrentCalls);
|
||||
try {
|
||||
if (this.delayMs) await new Promise(resolve => setTimeout(resolve, this.delayMs));
|
||||
return operation();
|
||||
} finally {
|
||||
this.concurrentCalls -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user