1
0
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:
Laurent Cozic
2021-08-30 18:53:24 +01:00
parent ce89ee5bab
commit b88b747ba6
12 changed files with 267 additions and 21 deletions

View File

@@ -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);

View 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'));
});
});

View 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;
}
}
}

View File

@@ -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();
}