You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-06 09:19:22 +02:00
Api: Resolves #5199: Add support for "events" end point to retrieve info about latest note changes
This commit is contained in:
@@ -9,6 +9,7 @@ import route_master_keys from './routes/master_keys';
|
||||
import route_search from './routes/search';
|
||||
import route_ping from './routes/ping';
|
||||
import route_auth from './routes/auth';
|
||||
import route_events from './routes/events';
|
||||
|
||||
const { ltrimSlashes } = require('../../path-utils');
|
||||
const md5 = require('md5');
|
||||
@@ -43,6 +44,9 @@ interface RequestQuery {
|
||||
|
||||
// Auth token
|
||||
auth_token?: string;
|
||||
|
||||
// Event cursor
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export interface Request {
|
||||
@@ -104,6 +108,7 @@ export default class Api {
|
||||
search: route_search,
|
||||
services: this.action_services.bind(this),
|
||||
auth: route_auth,
|
||||
events: route_events,
|
||||
};
|
||||
|
||||
this.dispatch = this.dispatch.bind(this);
|
||||
|
||||
94
packages/lib/services/rest/routes/events.test.ts
Normal file
94
packages/lib/services/rest/routes/events.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { ModelType } from '../../../BaseModel';
|
||||
import ItemChange from '../../../models/ItemChange';
|
||||
import Note from '../../../models/Note';
|
||||
import { expectThrow, setupDatabaseAndSynchronizer, switchClient } from '../../../testing/test-utils';
|
||||
import { ItemChangeEntity } from '../../database/types';
|
||||
import Api, { RequestMethod } from '../Api';
|
||||
|
||||
let api: Api = null;
|
||||
|
||||
describe('routes/events', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
api = new Api();
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should retrieve the latest events', async () => {
|
||||
let cursor = '0';
|
||||
|
||||
{
|
||||
const response = await api.route(RequestMethod.GET, 'events', { cursor });
|
||||
expect(response.cursor).toBe('0');
|
||||
}
|
||||
|
||||
const note1 = await Note.save({ title: 'toto' });
|
||||
await Note.save({ id: note1.id, title: 'tutu' });
|
||||
const note2 = await Note.save({ title: 'tata' });
|
||||
await ItemChange.waitForAllSaved();
|
||||
|
||||
{
|
||||
const response = await api.route(RequestMethod.GET, 'events', { cursor });
|
||||
expect(response.cursor).toBe('3');
|
||||
expect(response.items.length).toBe(2);
|
||||
expect(response.has_more).toBe(false);
|
||||
expect(response.items.map((it: ItemChangeEntity) => it.item_id).sort()).toEqual([note1.id, note2.id].sort());
|
||||
|
||||
cursor = response.cursor;
|
||||
}
|
||||
|
||||
{
|
||||
const response = await api.route(RequestMethod.GET, 'events', { cursor });
|
||||
expect(response.cursor).toBe(cursor);
|
||||
expect(response.items.length).toBe(0);
|
||||
expect(response.has_more).toBe(false);
|
||||
}
|
||||
|
||||
await Note.save({ id: note2.id, title: 'titi' });
|
||||
await ItemChange.waitForAllSaved();
|
||||
|
||||
{
|
||||
const response = await api.route(RequestMethod.GET, 'events', { cursor });
|
||||
expect(response.cursor).toBe('4');
|
||||
expect(response.items.length).toBe(1);
|
||||
expect(response.items[0].item_id).toBe(note2.id);
|
||||
}
|
||||
});
|
||||
|
||||
it('should limit the number of response items', async () => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < 101; i++) {
|
||||
promises.push(Note.save({ title: 'toto' }));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
await ItemChange.waitForAllSaved();
|
||||
|
||||
const response1 = await api.route(RequestMethod.GET, 'events', { cursor: '0' });
|
||||
expect(response1.items.length).toBe(100);
|
||||
expect(response1.has_more).toBe(true);
|
||||
|
||||
const response2 = await api.route(RequestMethod.GET, 'events', { cursor: response1.cursor });
|
||||
expect(response2.items.length).toBe(1);
|
||||
expect(response2.has_more).toBe(false);
|
||||
});
|
||||
|
||||
it('should retrieve a single item', async () => {
|
||||
const beforeTime = Date.now();
|
||||
|
||||
const note = await Note.save({ title: 'toto' });
|
||||
await ItemChange.waitForAllSaved();
|
||||
|
||||
const response = await api.route(RequestMethod.GET, 'events/1');
|
||||
|
||||
expect(response.item_type).toBe(ModelType.Note);
|
||||
expect(response.type).toBe(1);
|
||||
expect(response.item_id).toBe(note.id);
|
||||
expect(response.created_time).toBeGreaterThanOrEqual(beforeTime);
|
||||
|
||||
await expectThrow(async () => api.route(RequestMethod.GET, 'events/1234'));
|
||||
});
|
||||
|
||||
});
|
||||
39
packages/lib/services/rest/routes/events.ts
Normal file
39
packages/lib/services/rest/routes/events.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ModelType } from '../../../BaseModel';
|
||||
import { Request, RequestMethod } from '../Api';
|
||||
import { ErrorBadRequest, ErrorNotFound } from '../utils/errors';
|
||||
import ItemChange, { ChangeSinceIdOptions } from '../../../models/ItemChange';
|
||||
import requestFields from '../utils/requestFields';
|
||||
|
||||
export default async function(request: Request, id: string = null, _link: string = null) {
|
||||
if (request.method === RequestMethod.GET) {
|
||||
const options: ChangeSinceIdOptions = {
|
||||
limit: 100,
|
||||
fields: requestFields(request, ModelType.ItemChange, ['id', 'item_type', 'item_id', 'type', 'created_time']),
|
||||
};
|
||||
|
||||
if (!id) {
|
||||
if (!('cursor' in request.query)) {
|
||||
return {
|
||||
items: [],
|
||||
has_more: false,
|
||||
cursor: (await ItemChange.lastChangeId()).toString(),
|
||||
};
|
||||
} else {
|
||||
const cursor = Number(request.query.cursor);
|
||||
if (isNaN(cursor)) throw new ErrorBadRequest(`Invalid cursor: ${request.query.cursor}`);
|
||||
|
||||
const changes = await ItemChange.changesSinceId(cursor, options);
|
||||
|
||||
return {
|
||||
items: changes,
|
||||
has_more: changes.length >= options.limit,
|
||||
cursor: (changes.length ? changes[changes.length - 1].id : cursor).toString(),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const change = await ItemChange.load(id, { fields: options.fields });
|
||||
if (!change) throw new ErrorNotFound();
|
||||
return change;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,18 @@ function defaultFieldsByModelType(modelType: number): string[] {
|
||||
return output;
|
||||
}
|
||||
|
||||
export default function(request: Request, modelType: number) {
|
||||
export default function(request: Request, modelType: number, defaultFields: string[] = null) {
|
||||
const getDefaults = () => {
|
||||
if (defaultFields) return defaultFields;
|
||||
return defaultFieldsByModelType(modelType);
|
||||
};
|
||||
|
||||
const query = request.query;
|
||||
if (!query || !query.fields) return defaultFieldsByModelType(modelType);
|
||||
if (!query || !query.fields) return getDefaults();
|
||||
if (Array.isArray(query.fields)) return query.fields.slice();
|
||||
const fields = query.fields
|
||||
.split(',')
|
||||
.map((f: string) => f.trim())
|
||||
.filter((f: string) => !!f);
|
||||
return fields.length ? fields : defaultFieldsByModelType(modelType);
|
||||
return fields.length ? fields : getDefaults();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user