Skip to content
Commits on Source (2)
......@@ -62,6 +62,7 @@ import entitiesStorage from './src/common/services/sql/entities.storage';
import feedsStorage from './src/common/services/sql/feeds.storage';
import connectivityService from './src/common/services/connectivity.service';
import sqliteStorageProviderService from './src/common/services/sqlite-storage-provider.service';
import commentStorageService from './src/comments/CommentStorageService';
let deepLinkUrl = '';
......@@ -124,6 +125,7 @@ sessionService.onLogin(async () => {
if (!connectivityService.isConnected) return;
entitiesStorage.removeOlderThan(30);
feedsStorage.removeOlderThan(30);
commentStorageService.removeOlderThan(30);
}, 30000);
} catch (err) {
......
import { CommentStorageService } from '../../src/comments/CommentStorageService';
import sqliteService from '../../src/common/services/sqlite.service';
import sqliteStorageProviderService from '../../src/common/services/sqlite-storage-provider.service';
jest.mock('../../src/common/services/sqlite.service');
jest.mock('../../src/common/services/log.service');
/**
* Tests
*/
describe('Comment storage service', () => {
const storage = new CommentStorageService();
let sql;
const response = [{
"data":'{"comments":[{"type":"comment","entity_guid":"988128928149561344","parent_guid_l1":"0","parent_guid_l2":"0","guid":"eyJfdHlwZSI6ImNvbW1lbnQiLCJjaGlsZF9wYXRoIjoiOTg5Njg4NDA0MTEyMDI3NjQ4OjA6MCIsImVudGl0eV9ndWlkIjoiOTg4MTI4OTI4MTQ5NTYxMzQ0IiwiZ3VpZCI6Ijk4OTY4ODQwNDExMjAyNzY0OCIsInBhcmVudF9wYXRoIjoiMDowOjAiLCJwYXJ0aXRpb25fcGF0aCI6IjA6MDowIn0=","replies_count":0,"owner_guid":"781640694769917958","time_created":1561336103,"time_updated":1561336103,"attachments":[],"mature":false,"edited":false,"spam":false,"deleted":false,"_guid":"989688404112027648","partition_path":"0:0:0","parent_path":"0:0:0","child_path":"989688404112027648:0:0","description":"Test","thumbs:up:user_guids":[],"thumbs:up:count":0,"thumbs:down:user_guids":[],"thumbs:down:count":0,"can_reply":true}],"load-previous":"","load-next":"","socketRoomName":"comments:988128928149561344:0:0:0"}'
}];
beforeEach(async () => {
sql = await sqliteStorageProviderService.get();
});
it('should return null on error', () => {
sql.executeSql.mockRejectedValue(Error('Boom'));
return expect(storage.read('0001', '0:0:0', true, '', '')).resolves.toBe(null);
});
it('should return null on empty result', () => {
const result = {rows: {raw: jest.fn()}};
result.rows.raw.mockReturnValue(null);
sql.executeSql.mockResolvedValue([result]);
return expect(storage.read('0001', '0:0:0', true, '', '')).resolves.toBe(null);
});
it('should return null on no data', () => {
const result = {rows: {raw: jest.fn()}};
result.rows.raw.mockReturnValue([{}]);
sql.executeSql.mockResolvedValue([result]);
return expect(storage.read('0001', '0:0:0', true, '', '')).resolves.toBe(null);
});
it('should return stored comments', () => {
const result = {rows: {raw: jest.fn()}};
result.rows.raw.mockReturnValue(response);
sql.executeSql.mockResolvedValue([result]);
return expect(storage.read('0001', '0:0:0', true, '', '')).resolves.toEqual(JSON.parse(response[0].data));
});
it('should insert into comments_feeds', async (done) => {
sql.init.mockResolvedValue();
sql.executeSql.mockResolvedValue([true]);
const result = {comments: []};
try {
await storage.write('1234', '0:0:0', true, '1', '0002', result);
// TODO: check parameters, for some reason jest is transforming the paramters array.
expect(sql.executeSql).toBeCalled();
done();
} catch(err) {
done.fail();
}
});
});
\ No newline at end of file
import sqliteStorageProviderService from '../common/services/sqlite-storage-provider.service';
import logService from '../common/services/log.service';
import moment from 'moment';
/**
* Comment storage service
*/
export class CommentStorageService {
/**
* @var {SqliteService}
*/
db;
/**
* Get the sqlite service
*/
async getDb() {
if (!this.db) {
this.db = await sqliteStorageProviderService.get();
}
return this.db;
}
/**
* Read a page from the storage
*
* @param {string} entityGuid
* @param {string} parentPath
* @param {boolean} descending
* @param {string} offset
* @param {string} focusedUrn
*/
async read(entityGuid: string, parentPath: string, descending: boolean, offset: string, focusedUrn: string) {
try {
await this.getDb();
const params = [
entityGuid,
parentPath,
descending,
offset || '',
focusedUrn || ''
];
const [result] = await this.db.executeSql('SELECT data FROM comments_feeds WHERE parent = ? AND parent_path = ? AND descending = ? AND offset = ? AND focused_urn = ?;', params);
const rows = result.rows.raw();
if (!rows[0] || !rows[0].data) return null;
return JSON.parse(rows[0].data);
} catch (err) {
logService.exception('[CommentStorageService]', err);
return null;
}
}
/**
* Write a page to the storage
*
* @param {string} entityGuid
* @param {string} parentPath
* @param {boolean} descending
* @param {string} offset
* @param {string} focusedUrn
* @param {Object} data
*/
async write(entityGuid: string, parentPath: string, descending: boolean, offset: string, focusedUrn: string, data: Object) {
try {
await this.getDb();
await this.db.executeSql('REPLACE INTO comments_feeds (parent, parent_path, descending, offset, focused_urn, data, updated) values (?,?,?,?,?,?,?)',
[
entityGuid,
parentPath,
descending,
offset || '',
focusedUrn || '',
JSON.stringify(this.clean(data)),
Math.floor(Date.now() / 1000)
]
);
} catch (err) {
logService.exception('[CommentStorageService]', err);
console.log(err)
}
}
/**
* Remove all comments feeds
*/
async removeAll() {
await this.getDb();
return this.db.executeSql('DELETE FROM comments_feeds');
}
/**
* Remove comments feeds older than given days
*
* @param {integer} days
*/
async removeOlderThan(days) {
const when = moment().subtract(days, 'days');
await this.getDb();
this.db.executeSql('DELETE FROM comments_feeds WHERE updated < ?', [when.format("X")]);
}
/**
* Clean repeated data to reduce the stored size
*
* @param {Object} data
*/
clean(data: Object){
data.comments.forEach(comment => {
delete(comment.luid);
delete(comment.body);
});
delete(data.status);
return data;
}
}
export default new CommentStorageService();
\ No newline at end of file
import api from './../common/services/api.service';
import commentStorageService from './CommentStorageService';
const decodeUrn = (urn) => {
let parts = urn.split(':');
......@@ -52,7 +53,17 @@ export async function getComments(focusedUrn, entity_guid, parent_path, level, l
let uri = `api/v2/comments/${opts.entity_guid}/0/${opts.parent_path}`;
let response = await api.get(uri, opts);
let response;
try {
response = await api.get(uri, opts);
commentStorageService.write(entity_guid, parent_path, descending, loadNext || loadPrevious, focusedUrn, response);
} catch(err) {
response = await commentStorageService.read(entity_guid, parent_path, descending, loadNext || loadPrevious, focusedUrn);
// if there is no local data we throw the exception again
if (!response) throw err;
}
if (focusedUrn && focusedUrnObject) {
for (let comment of response.comments) {
......
class SqliteServiceMock {
init = jest.fn();
runMigrations = jest.fn();
rebuildDB = jest.fn();
executeSql = jest.fn();
transaction = jest.fn();
}
export default SqliteServiceMock
\ No newline at end of file
......@@ -7,7 +7,8 @@ const migrations = [
"CREATE TABLE IF NOT EXISTS entities ( urn VARCHAR(255) NOT NULL PRIMARY KEY, data TEXT, updated INTEGER )",
"CREATE TABLE IF NOT EXISTS feeds ( key VARCHAR(255) NOT NULL, offset INTEGER NOT NULL, data TEXT, updated INTEGER, PRIMARY KEY(key, offset))",
"CREATE INDEX feeds_updated ON feeds(updated);",
"CREATE INDEX entities_updated ON entities(updated);"
"CREATE INDEX entities_updated ON entities(updated);",
"CREATE TABLE IF NOT EXISTS comments_feeds ( parent VARCHAR(128) NOT NULL, parent_path VARCHAR(128) NOT NULL, descending BOOLEAN, offset VARCHAR(64) NOT NULL, focused_urn VARCHAR(255) NOT NULL, data TEXT, updated INTEGER, PRIMARY KEY(parent, parent_path, descending, offset, focused_urn))",
"CREATE INDEX comments_feeds_updated ON comments_feeds(updated);",
];
export default migrations;
\ No newline at end of file