Change page security.

Added special script to handle parcel and node require.
Added new tagging control.
Adding new HTML Sanitizer.
Packages so I could be sqlite with the right electron version.
Some style changes.
Interface changes.
Changes to the sqlite/client. Interfaces cuased errors.
Fixed bugs in electron ipc handling.
Fixes to the sqlite libs.
Create a new input control that can handle pasting.
pasting HTML works with cycle through HTML,Text, Sanatized HTML.
New UI controls.
Time stamp control has more functionality.
This commit is contained in:
Jason Tudisco 2022-02-03 03:27:51 -06:00
parent 02ecda067f
commit c617c63510
15 changed files with 3078 additions and 379 deletions

View File

@ -2,14 +2,15 @@
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<script type="module" src="r.js"></script>
<title>Time Chain</title>
<link rel="stylesheet" href="src/css/main.css">
<link rel="stylesheet" type="text/css" href="node_modules/@yaireo/tagify/dist/tagify.css">
</head>
<body>
<div class="loading" >
<div id="timechain"></div>
<div class="loading" id="main-loading">
<h1>Time Chain!</h1>
<p>Time traveling your data! Comming soon...</p>
<p id="timestamp"></p>

16
main.js
View File

@ -1,18 +1,28 @@
const { app, BrowserWindow } = require('electron')
require('./src/data/sqlite-electron-ipc');
let mainWin;
function createWindow () {
const win = new BrowserWindow({
mainWin = new BrowserWindow({
autoHideMenuBar: true,
width: 800,
height: 600,
icon: __dirname + '/src/img/chains.png'
icon: __dirname + '/src/img/chains.png',
webPreferences: {
nodeIntegration: true,
contextIsolation: false
},
nodeIntegration: true
})
//win.autoHideMenuBar = true;
win.loadFile('index.html')
mainWin.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})

2670
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,11 @@
},
"scripts": {
"start": "electron .",
"build": "parcel build --dist-dir ./src/dist --target \"jason\" start.js"
"build": "parcel build --dist-dir ./src/dist --target \"jason\" start.js",
"watch": "parcel --dist-dir ./src/dist --target \"jason\" start.js",
"build-main": "parcel build --dist-dir ./src/dist --target \"jason\" main.js",
"rebuild": "electron-rebuild -f -w better-sqlite3",
"postinstall": "electron-builder install-app-deps"
},
"browserslist": "> 0.5%, last 2 versions, not dead",
"targets": {
@ -29,11 +33,16 @@
"parcel": "^2.2.1"
},
"dependencies": {
"@yaireo/tagify": "^4.9.5",
"better-sqlite3": "^7.5.0",
"conf": "^10.1.1",
"dayjs": "^1.10.7",
"electron-rebuild": "^3.2.7",
"empty-lite": "^1.2.0",
"es6-interface": "^3.2.1",
"nanoid": "^3.2.0",
"pubsub-js": "^1.9.4",
"riot": "^6.1.2"
"riot": "^6.1.2",
"sanitize-html": "^2.6.1"
}
}

View File

@ -29,3 +29,27 @@ body {
transform: rotate(359deg);
}
}
.timestamp {
text-align: center;
font-size: 1.3em;
margin-top: 1em;
}
#pasteme {
border: 3px dashed #ccc;
border-radius: 0.5em;
padding: 1em;
margin: 0.5em 2em;
color: whitesmoke;
text-align: center;
font-size: 2em;
}
.paste-html {
font-size: 1em !important;
}
.timestamp-puase {
color: yellow !important;
}

View File

@ -17,14 +17,18 @@ const InterfaceFile = {
const InterfaceTag = {
add(tag){},
delete(tag){}
delete(tag){},
get(tag){},
has(tag){}
}
const InterfaceTagLink = {
add(uuid,tag){},
delete(uuid,tag){},
deleteTag(tag){},
deleteRecord(uuid){}
deleteRecord(uuid){},
getRecords(tag){},
getTags(uuid){}
}
module.exports = {

View File

@ -0,0 +1,157 @@
/*const Interface = window.require("es6-interface");
const {InterfaceRecord,InterfaceFile,InterfaceTag,InterfaceTagLink} = require("./interfaces");*/
const { ipcRenderer } = window.require('electron')
///const { ipcRenderer } = window.require('electron')
class TimeChainDataSqliteTag //extends Interface(InterfaceTag)
{
constructor(){
this.cmd = "timechain-tag";
}
send(func, data){
data.func = func;
return ipcRenderer.invoke(this.cmd,data);
}
add(tag){
return this.send('add',{tag:tag})
}
delete(tag){
return this.send('delete',{tag:tag})
}
get(tag){
return this.send('get',{tag:tag})
}
has(tag){
return this.send('has',{tag:tag})
}
}
class TimeChainDataSqliteTagLink //extends Interface(InterfaceTagLink)
{
constructor(){
this.cmd = 'timechain-taglink';
}
send(func, data){
data.func = func;
return ipcRenderer.invoke(this.cmd,data);
}
add(uuid,tag){
return this.send('add',{uuid:uuid,tag:tag});
}
delete(uuid,tag){
return this.send('delete',{uuid:uuid,tag:tag});
}
deleteTag(tag){
return this.send('delete-tag',{tag:tag});
}
deleteRecord(uuid){
return this.send('delete-record',{uuid:uuid});
}
getRecords(tag){
return this.send('get-records',{tag:tag});
}
getTags(uuid){
return this.send('get-tags',{uuid:uuid});
}
}
class TimeChainDataSqliteFile //extends Interface(InterfaceFile)
{
constructor(){
this.cmd = 'timechain-file';
}
send(func, data){
data.func = func;
return ipcRenderer.invoke(this.cmd,data);
}
add(uuid_record,uuid,timestamp,content,mime,hash){
return this.send('add',{uuid_record:uuid_record,uuid:uuid,timestamp:timestamp,content:content,mime:mime,hash:hash});
}
getByRecord(uuid_record){
return this.send('get-record',{uuid_record:uuid_record});
}
get(uuid){
return this.send('get',{uuid:uuid});
}
delete(uuid){
return this.send('delete',{uuid:uuid});
}
deleteRecord(uuid_record){
return this.send('delete-record',{uuid_record:uuid_record});
}
update(uuid,timestamp,content,mime,hash){
return this.send('update',{uuid:uuid,timestamp:timestamp,content:content,mime:mime,hash:hash});
}
}
class TimeChainDataSqliteRecord //extends Interface(InterfaceRecord)
{
cmd = 'timechain-record'
send(func, data){
data.func = func;
return ipcRenderer.invoke(this.cmd,data);
}
add(uuid,timestamp,content,mime,hash){
return this.send('add',{uuid:uuid,timestamp:timestamp,content:content,mime:mime,hash:hash});
}
get(uuid){
return this.send('get',{uuid:uuid});
}
find(search,sort=null,limit=undefined,offset=0){
return this.send('find',{search:search,sort:sort,limit:limit,offset:offset});
}
delete(uuid){
return this.sedn('delete',{uuid:uuid});
}
update(uuid,content,mime,hash){
return this.send('update',{uuid:uuid,content:content,mime:mime,hash:hash});
}
}
class TimeChainDataSqlite {
}
module.exports = {
TimeChainDataSqlite,
TimeChainDataSqliteRecord,
TimeChainDataSqliteFile,
TimeChainDataSqliteTagLink,
TimeChainDataSqliteTag
}

View File

@ -1,76 +1,146 @@
const { ipcMain } = require('electron');
const {app} = require('electron');
const config = app.getPath('userData');
//const config = app.getPath('userData');
const {TimeChainDataSqliteRecord,ConnectToDatabase, TimeChainDataSqliteFile, TimeChainDataSqliteTag, TimeChainDataSqliteTagLink} = require('./sqlite');
const appPath = app.getPath('userData');
const DBPath = appPath+"/timechain.db";
console.log(DBPath);
const DB = ConnectToDatabase(DBPath);
const DbRecord = new TimeChainDataSqliteRecord();
const DbFile = new TimeChainDataSqliteFile();
const dbTag = new TimeChainDataSqliteTag();
const dbTagLink = new TimeChainDataSqliteTagLink();
//const configDir = app.getPath('userData');
//const appPath = app.getPath('exe');
const Conf = require('conf');
const config = new Conf();
// ** Extra Data
ipcMain.on('timechain-config-dir', (event,arg) => {
const configDir = app.getPath('userData');
event.reply('timechain-config-dir-reply', configDir);
ipcMain.handle('timechain-config-dir', (event,arg) => {
return false;
});
// ** RECORD **
console.log("Register timechain-record");
ipcMain.handle('timechain-record', async (event,arg) => {
ipcMain.on('timechain-record-add', (event, arg) => {
event.reply('timechain-record-add-reply', 'pong')
});
let res = null;
ipcMain.on('timechain-record-delete', (event, arg) => {
event.reply('timechain-record-delete-reply', 'pong')
});
switch(arg.func){
case 'add':
res = await DbRecord.add(arg.uuid, arg.timestamp, arg.content, arg.mime, arg.hash);
break;
case 'delete':
res = await DbRecord.delete(arg.uuid);
break;
case 'update':
res = await DbRecord.update(arg.uuid,arg.content,arg.mime,arg.hash);
break;
case 'find':
res = await DbRecord.find(arg.search,arg.sort,arg.limit,arg.offset);
break;
case 'get':
res = await DbRecord.get(arg.uuid);
break;
default:
res = new Error('unknown command');
}
ipcMain.on('timechain-record-update', (event, arg) => {
event.reply('timechain-record-update-reply', 'pong')
});
return res;
ipcMain.on('timechain-record-find', (event, arg) => {
event.reply('timechain-record-find-reply', 'pong')
});
})
// ** FILE **
ipcMain.on('timechain-file-find', (event, arg) => {
event.reply('timechain-file-find-reply', 'pong')
});
ipcMain.handle('timechain-file', async (event, arg) => {
let res = null;
ipcMain.on('timechain-file-add', (event, arg) => {
event.reply('timechain-file-add-reply', 'pong')
});
switch(arg.func){
case 'add':
res = await DbFile.add(arg.uuid_record,arg.uuid,arg.timestamp,arg.content,arg.mime,arg.hash);
break;
case 'update':
res = await DbFile.update(arg.uuid,arg.timestamp,arg.content,arg.mime,arg.hash);
break;
case 'get-record':
res = await DbFile.getByRecord(arg.uuid_record);
break;
case 'delete-record':
res = await DbFile.deleteRecord(arg.uuid_record);
break;
case 'get':
res = await DbFile.get(arg.uuid);
break;
case 'delete':
res = await DbFile.delete(arg.uuid);
break;
default:
res = new Error('Unknow a command');
}
ipcMain.on('timechain-file-update', (event, arg) => {
event.reply('timechain-file-update-reply', 'pong')
});
ipcMain.on('timechain-file-delete', (event, arg) => {
event.reply('timechain-file-delete-reply', 'pong')
return res;
});
// ** TAG **
ipcMain.on('timechain-tag-add', (event, arg) => {
event.reply('timechain-tag-add-reply', 'pong')
ipcMain.handle('timechain-tag', async (event, arg) => {
let res = null;
switch(arg.func){
case 'add':
res = await DbTag.add(arg.tag);
break;
case 'delete':
res = await DbTag.delete(arg.tag);
break;
case 'has':
res = await DbTag.delete(arg.tag);
break;
default:
res = new Error('Command Unknown');
}
return res;
});
ipcMain.on('timechain-tag-delete', (event, arg) => {
event.reply('timechain-tag-delete-reply', 'pong')
});
// TAG LINK
// ** TAG LINK **
ipcMain.on('timechain-taglink-add', (event, arg) => {
event.reply('timechain-taglink-add-reply', 'pong')
});
ipcMain.handle('timechain-taglink', async (event, arg) => {
ipcMain.on('timechain-taglink-delete', (event, arg) => {
event.reply('timechain-taglink-delete-reply', 'pong')
});
let res = null;
ipcMain.on('timechain-taglink-deleteTag', (event, arg) => {
event.reply('timechain-taglink-deleteTag-reply', 'pong')
});
switch(arg.func){
case 'add':
res = await DbTagLink.add(arg.uuid,arg.tag)
break;
case 'delete':
res = await DbTagLink.delete(arg.uuid,arg.tag);
break;
case 'delete-tag':
res = await DbTagLink.deleteTag(arg.tag);
break;
case 'delete-record':
res = await DbTagLink.deleteRecord(arg.uuid);
break;
case 'get-records':
res = await DbTagLink.getRecords(atg.tag);
break;
case 'get-tags':
res = await DbTagLink.getTags(atg.uuid);
break;
default:
res = Error("Commande not known");
}
ipcMain.on('timechain-taglink-deleteRecord', (event, arg) => {
event.reply('timechain-taglink-deleteRecord-reply', 'pong')
return res;
});

View File

@ -34,7 +34,7 @@ class TimeChainDataSqliteTag extends Interface(InterfaceTag) {
if(!this._table_add){
this._table_add = db.prepare("INSERT INTO tags (tag,created_at,updated_at) VALUES (?,?,?)");
}
return this.table_add;
return this._table_add;
}
get table_delete(){
@ -44,6 +44,20 @@ class TimeChainDataSqliteTag extends Interface(InterfaceTag) {
return this._table_delete;
}
get table_get(){
if(!this._table_get){
this._table_get = db.prepare("SELECT * FROM tags WHERE tag = ?");
}
return this._table_get;
}
get table_count(){
if(!this._table_count){
this._table_count = db.prepare("SELECT count(tag) as cnt FROM tags WHERE tag = ?");
}
return this._table_count;
}
add(tag){
return new Promise(resolve=>{
const dt = Math.floor(Date.now());
@ -60,6 +74,20 @@ class TimeChainDataSqliteTag extends Interface(InterfaceTag) {
});
}
get(tag){
return new Promise(resolve=>{
const res = this.table_get.get(tag);
return resolve(res);
});
}
has(tag){
return new Promise(resolve=>{
const res = this.table_count.get(tag);
return resolve(res && res.cnt>0);
});
}
}
class TimeChainDataSqliteTagLink extends Interface(InterfaceTagLink) {
@ -86,7 +114,7 @@ class TimeChainDataSqliteTagLink extends Interface(InterfaceTagLink) {
}
get table_add(){
if(!_table_add){
if(!this._table_add){
this._table_add = db.prepare('INSERT INTO taglink (uuid,tag,created_at) VALUES (?,?,?)');
}
return this._table_add;
@ -110,6 +138,21 @@ class TimeChainDataSqliteTagLink extends Interface(InterfaceTagLink) {
if(!this._table_delete_record){
this._table_delete_record = db.prepare('DELETE FROM taglink WHERE uuid=?');
}
return this._table_delete_record;
}
get table_get_records(){
if(!this._table_get_records){
this._table_get_records = db.prepare("SELECT * FROM taglink WHERE tag=?");
}
return this._table_get_records;
}
get table_get_tags(){
if(!this._table_get_tags){
this._table_get_tags = db.prepare("SELECT * FROM taglink WHERE uuid=?");
}
return this._table_get_tags;
}
add(uuid,tag){
@ -144,6 +187,22 @@ class TimeChainDataSqliteTagLink extends Interface(InterfaceTagLink) {
return resolve(res?.changes);
});
}
getRecords(tag){
return new Promise(resolve=>{
const prepare = this.table_get_records;
const res = prepare.all(tag);
return resolve(res);
})
}
getTags(uuid){
return new Promise(resolve=>{
const prepare = this.table_get_tags;
const res = prepare.all(uuid);
return resolve(res);
})
}
}
class TimeChainDataSqliteFile extends Interface(InterfaceFile) {
@ -238,7 +297,7 @@ class TimeChainDataSqliteFile extends Interface(InterfaceFile) {
get(uuid){
return new Promise(resolve => {
const res = this.table_fine_one.get(uuid);
const res = this.table_find_one.get(uuid);
return resolve(res);
});
}
@ -246,7 +305,7 @@ class TimeChainDataSqliteFile extends Interface(InterfaceFile) {
delete(uuid){
return new Promise(resolve=>{
const prepare = this.table_delete;
const res = prepare.exec(uuid);
const res = prepare.run(uuid);
return resolve(res?.changes);
})
}

View File

@ -1,3 +1,28 @@
<app>
<timestamp />
<timechain-input />
<app-divider label="LIFO" />
<timechain-list />
<script>
import Timestamp from './timestamp.riot'
import TimechainInput from './timechain-input.riot'
import TimechainList from './timechain-list.riot'
import AppDivider from './app-divider.riot'
export default {
components: {
Timestamp,
TimechainInput,
TimechainList,
AppDivider
}
}
</script>
</app>

View File

@ -0,0 +1,136 @@
<timechain-input>
<div class="timechain-input-tempate" if={state.show_tagging}>
<timechain-tag if={state.show_tagging} onchange="{onTags}"/>
<timechain-input-buttons if={state.show_tagging} onsave="{onSave}" />
</div>
<div id="pasteme" ondblclick="{onSwap}">
<p>{state.message}</p>
</div>
<style>
.paste-html {
background: white !important;
color: black !important;
text-align: left !important;
}
.timechain-input-tempate {
display: flex;
flex-direction: row;
padding: 0 3em;
}
.timechain-input-tempate timechain-tag {
flex-grow: 1;
}
.timechain-input-tempate button {
height: 3em;
line-height: 3em;
}
</style>
<script>
const pubsub = require('pubsub-js');
const empty = require('empty-lite');
import sanitizeHtml from 'sanitize-html';
import TimechainTag from './timechain-tag.riot'
import TimechainInputButtons from './timechain-input-buttons.riot';
const {TimeChainDataSqliteRecord} = require('../data/sqlite-client');
export default {
state: {
message:"Paste something in this window.",
show_tagging: false
},
components: {
TimechainTag,
TimechainInputButtons
},
onMounted(){
document.addEventListener('paste', this.pasteEvent.bind(this));
},
pasteEvent(e){
e.preventDefault();
console.log("Bla");
console.log(e.clipboardData.items[0]);
console.log(e.clipboardData.items[1]);
if (e.clipboardData.types.indexOf('text/html') > -1) {
const newData = e.clipboardData.getData('text/html');
const newDataText = e.clipboardData.getData('text/plain');
const el = this.$('#pasteme');
pubsub.publish('timestamp-puase',true);
this.timestamp = Math.floor(new Date().getTime());
pubsub.publish('timestamp-settime',this.timestamp);
el.innerHTML = this.content = newData;
this.content_text = e.clipboardData.getData('text/plain');
this.content_mime = 'text/html';
el.classList.add('paste-html');
this.content_type = "html";
this.content_swap = 0;
this.content_orig = this.content;
}
if(!empty(this.content)){
this.update({show_tagging:true});
}
},
onSwap(){
console.log("Here we can swap to text");
if(this.content_type=='html'){
const el = this.$('#pasteme');
if(this.content_swap==0){
this.content_mime = "text/plain";
this.content = this.content_text;
el.innerText = this.content;
this.content_swap = 1;
}else if(this.content_swap==1){
this.content_mime = "text/html";
this.content = sanitizeHtml(this.content_orig);
el.innerHTML = this.content;
this.content_swap = 2;
}else{
this.content_mime = "text/html";
const temp = this.content;
this.content = this.content_orig;
this.content_swap = 0;
el.innerHTML = this.content;
}
}
if (window.getSelection().empty) { // Chrome
window.getSelection().empty();
} else if (window.getSelection().removeAllRanges) { // Firefox
window.getSelection().removeAllRanges();
}
},
onTags(tags){
console.log(tags);
this.tags = tags;
},
onSave(){
console.log("Save button pressed");
const TR = new TimeChainDataSqliteRecord();
TR.add("iueyriweusdfsd8w",Math.floor(new Date().getTime()),this.content,this.content_mime,"Comming SOon").then(res=>{
console.log(res);
})
}
}
</script>
</timechain-input>

View File

@ -1,8 +1,10 @@
<timestamp>
<div class="timestamp">
<div class="timestamp-icon">
<div class="timestamp-text">{ state.time_text }</div>
<div class="timestamp-text">
<img src="src/img/clock1-white.svg" align="absmiddle" style="width:1em;margin-right:0.3em;">
{ state.time_text }
</div>
</div>
<script>
@ -12,14 +14,23 @@
export default {
state: {
time_text: "",
time_text: "Comming Soon",
format: "long"
},
onMounted(){
this.start();
pubsub.subscribe("timestamp-puase", (event)=>{
clearTimeout(this.event_handle);
this.$('.timestamp-text').classList.add('timestamp-puase');
});
pubsub.subscribe("timestamp-settime",(event,arg)=>{
this.update({time_text:dayjs(new Date(arg)).format("MMMM D, YYYY h:mm:ss A")});
})
},
start(){
setTimeout(()=>{
this.event_handle = setTimeout(()=>{
this.makeString();
this.start();
},200);

View File

@ -1,7 +1,11 @@
import Timestamp from './src/ui/timestamp.riot'
import App from './src/ui/app.riot'
import { component } from 'riot'
component(Timestamp)(document.getElementById('timestamp'))
setTimeout(()=>{
document.getElementById('main-loading').remove();
component(App)(document.getElementById('timechain'));
},0)
console.log("yes!");

View File

@ -74,4 +74,141 @@ test("Should create a record in the database and then remove it",async ()=>{
return rec.delete(id);
})
})
});
test("Should create a record, attach a file, and then remove it",async ()=>{
const rec = new TimeChainDataSqliteRecord();
const filerec = new TimeChainDataSqliteFile();
const record = {
id: nanoid(),
hash: "fakehash",
content: "This is a test",
mime: "text/plain",
ts: Math.floor(Date.now())
};
const attached = {
id: nanoid(),
hash: "faskhash",
ts: Math.floor(Date.now()),
content: "this is fake data that would normally be binary file data",
mime: "application/octet-stream"
}
return rec.add(record.id,record.ts,record.content,record.mime,record.hash).then(res=>{
expect(res).toEqual(1);
return rec.get(record.id).then(res=>{
expect(res.uuid).toEqual(record.id);
expect(res.timestamp).toEqual(record.ts);
expect(res.mime).toEqual(record.mime);
expect(res.content).toEqual(record.content);
expect(res.hash).toEqual(record.hash);
return filerec.add(record.id,attached.id,attached.ts,attached.content,attached.mime,attached.hash).then(res=>{
expect(res).toEqual(1);
return filerec.get(attached.id).then(afile=>{
expect(afile.uuid).toEqual(attached.id);
expect(afile.hash).toEqual(attached.hash);
expect(afile.timestamp).toEqual(attached.ts);
expect(afile.content).toEqual(attached.content);
expect(afile.mime).toEqual(attached.mime);
});
});
});
}).then(()=>{
return rec.delete(record.id).then(res=>{
expect(res).toEqual(1);
})
}).then(()=>{
return filerec.delete(attached.id).then(res=>{
expect(res).toEqual(1);
});
})
});
test('Should add, check that it exist, remove tags to tag table', async ()=>{
const tags = new TimeChainDataSqliteTag();
const tag = "test";
return tags.add(tag).then(res=>{
expect(res).toEqual(1);
return tags.get(tag).then(res=>{
expect(res).not.toBeNull();
expect(res.create_at).not.toBeNull();
expect(res.tag).toEqual(tag);
})
})
.then(()=>{
return tags.has(tag).then(res=>{
expect(res).toBe(true);
})
})
.then(()=>{
return tags.delete(tag).then(res=>{
expect(res).toEqual(1);
})
});
});
test("Should create a record and tag the record", async ()=>{
const records = new TimeChainDataSqliteRecord();
const tags = new TimeChainDataSqliteTag();
const links = new TimeChainDataSqliteTagLink();
const rec = {
id: nanoid(),
hash: "fakehash",
content: "This is a test",
mime: "text/plain",
ts: Math.floor(Date.now())
};
const tag = "testing";
return records.add(rec.id,rec.ts,rec.content,rec.mime,rec.hash).then(res=>{
expect(res).toBe(1);
}).then(()=>{
return tags.add(tag).then(res=>{
expect(res).toBe(1);
});
}).then(()=>{
return links.add(rec.id,tag).then(res=>{
expect(res).toBe(1);
});
}).then(()=>{
return records.get(rec.id).then(res=>{
expect(res).not.toBeNull();
expect(res.uuid).toBe(rec.id);
});
}).then(()=>{
return links.getRecords(tag).then(res=>{
expect(res).not.toBeNull();
expect(res.length).toBeTruthy();
expect(res[0].uuid).toBe(rec.id);
});
}).then(()=>{
return links.getTags(rec.id).then(res=>{
expect(res).not.toBeNull();
expect(res.length).toBeTruthy();
expect(res[0].tag).toBe(tag);
});
}).then(()=>{
return links.deleteRecord(rec.id).then(res=>{
expect(res).toBe(1);
})
}).then(()=>{
return tags.delete(tag).then(res=>{
expect(res).toBe(1);
})
}).then(()=>{
return records.delete(rec.id).then(res=>{
expect(res).toBe(1);
})
});
})

Binary file not shown.