Skip to content
Snippets Groups Projects
Commit dd541e5f authored by James Long's avatar James Long
Browse files

initial (open-source)

parents
Branches
Tags
No related merge requests found
BEGIN TRANSACTION;
ALTER TABLE accounts ADD COLUMN sort_order REAL;
COMMIT;
BEGIN TRANSACTION;
CREATE TABLE schedules
(id TEXT PRIMARY KEY,
rule TEXT,
active INTEGER DEFAULT 0,
completed INTEGER DEFAULT 0,
posts_transaction INTEGER DEFAULT 0,
tombstone INTEGER DEFAULT 0);
CREATE TABLE schedules_next_date
(id TEXT PRIMARY KEY,
schedule_id TEXT,
local_next_date INTEGER,
local_next_date_ts INTEGER,
base_next_date INTEGER,
base_next_date_ts INTEGER);
CREATE TABLE schedules_json_paths
(schedule_id TEXT PRIMARY KEY,
payee TEXT,
account TEXT,
amount TEXT,
date TEXT);
ALTER TABLE transactions ADD COLUMN schedule TEXT;
COMMIT;
export default async function runMigration(db, uuid) {
function getValue(node) {
return node.expr != null ? node.expr : node.cachedValue;
}
db.execQuery(`
CREATE TABLE zero_budget_months
(id TEXT PRIMARY KEY,
buffered INTEGER DEFAULT 0);
CREATE TABLE zero_budgets
(id TEXT PRIMARY KEY,
month INTEGER,
category TEXT,
amount INTEGER DEFAULT 0,
carryover INTEGER DEFAULT 0);
CREATE TABLE reflect_budgets
(id TEXT PRIMARY KEY,
month INTEGER,
category TEXT,
amount INTEGER DEFAULT 0,
carryover INTEGER DEFAULT 0);
CREATE TABLE notes
(id TEXT PRIMARY KEY,
note TEXT);
CREATE TABLE kvcache (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE kvcache_key (id INTEGER PRIMARY KEY, key REAL);
`);
// Migrate budget amounts and carryover
let budget = db.runQuery(
`SELECT * FROM spreadsheet_cells WHERE name LIKE 'budget%!budget-%'`,
[],
true
);
db.transaction(() => {
budget.map(monthBudget => {
let match = monthBudget.name.match(
/^(budget-report|budget)(\d+)!budget-(.+)$/
);
if (match == null) {
console.log('Warning: invalid budget month name', monthBudget.name);
return;
}
let type = match[1];
let month = match[2].slice(0, 4) + '-' + match[2].slice(4);
let dbmonth = parseInt(match[2]);
let cat = match[3];
let amount = parseInt(getValue(monthBudget));
if (isNaN(amount)) {
amount = 0;
}
let sheetName = monthBudget.name.split('!')[0];
let carryover = db.runQuery(
'SELECT * FROM spreadsheet_cells WHERE name = ?',
[`${sheetName}!carryover-${cat}`],
true
);
let table = type === 'budget-report' ? 'reflect_budgets' : 'zero_budgets';
db.runQuery(
`INSERT INTO ${table} (id, month, category, amount, carryover) VALUES (?, ?, ?, ?, ?)`,
[
`${month}-${cat}`,
dbmonth,
cat,
amount,
carryover.length > 0 && getValue(carryover[0]) === 'true' ? 1 : 0
]
);
});
});
// Migrate buffers
let buffers = db.runQuery(
`SELECT * FROM spreadsheet_cells WHERE name LIKE 'budget%!buffered'`,
[],
true
);
db.transaction(() => {
buffers.map(buffer => {
let match = buffer.name.match(/^budget(\d+)!buffered$/);
if (match) {
let month = match[1].slice(0, 4) + '-' + match[1].slice(4);
let amount = parseInt(getValue(buffer));
if (isNaN(amount)) {
amount = 0;
}
db.runQuery(
`INSERT INTO zero_budget_months (id, buffered) VALUES (?, ?)`,
[month, amount]
);
}
});
});
// Migrate notes
let notes = db.runQuery(
`SELECT * FROM spreadsheet_cells WHERE name LIKE 'notes!%'`,
[],
true
);
let parseNote = str => {
try {
let value = JSON.parse(str);
return value && value !== '' ? value : null;
} catch (e) {
return null;
}
};
db.transaction(() => {
notes.forEach(note => {
let parsed = parseNote(getValue(note));
if (parsed) {
let [, id] = note.name.split('!');
db.runQuery(`INSERT INTO notes (id, note) VALUES (?, ?)`, [id, parsed]);
}
});
});
db.execQuery(`
DROP TABLE spreadsheet_cells;
ANALYZE;
VACUUM;
`);
}
{
"name": "actual-sync",
"version": "1.0.0",
"description": "actual syncing server",
"main": "index.js",
"scripts": {
"start": "node app",
"lint": "eslint --ignore-pattern '**/node_modules/*' --ignore-pattern '**/log/*' --ignore-pattern 'supervise' --ignore-pattern '**/shared/*' ."
},
"dependencies": {
"@actual-app/api": "^2.0.1",
"adm-zip": "^0.5.9",
"bcrypt": "^5.0.1",
"better-sqlite3": "^7.5.0",
"body-parser": "^1.18.3",
"cors": "^2.8.5",
"express": "^4.16.3",
"express-response-size": "^0.0.3",
"node-fetch": "^2.2.0",
"source-map-support": "^0.5.21",
"uuid": "^3.3.2"
},
"eslintConfig": {
"extends": "react-app"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"eslint": "^5.12.1",
"eslint-config-react-app": "^3.0.6",
"eslint-plugin-flowtype": "^3.2.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-react": "^7.12.4"
},
"prettier": {
"singleQuote": true,
"trailingComma": "none"
}
}
run 0 → 100755
#!/bin/sh
NODE="/home/ec2-user/.nvm/versions/node/v10.15.1/bin/node"
export VERSION=`"$NODE" ./node_modules/.bin/sentry-cli releases propose-version`
exec chpst -uec2-user "$NODE" app.js 2>&1
CREATE TABLE auth
(password TEXT PRIMARY KEY);
CREATE TABLE sessions
(token TEXT PRIMARY KEY);
CREATE TABLE files
(id TEXT PRIMARY KEY,
group_id TEXT,
sync_version SMALLINT,
encrypt_meta TEXT,
encrypt_keyid TEXT,
encrypt_salt TEXT,
encrypt_test TEXT,
deleted BOOLEAN DEFAULT FALSE,
name TEXT);
CREATE TABLE messages_binary
(timestamp TEXT,
is_encrypted BOOLEAN,
content bytea,
PRIMARY KEY(timestamp, group_id));
CREATE TABLE messages_merkles
(id TEXT PRIMAREY KEY,
merkle TEXT);
let { sequential } = require('./util/async');
let actual = require('@actual-app/api');
let SyncPb = actual.internal.SyncProtoBuf;
// This method must be sequential (TODO: document why, because Actual
// is global etc)
const sync = sequential(async function syncAPI(messages, since, fileId) {
let prefs = await actual.internal.send('load-prefs');
if (prefs == null || prefs.id !== fileId) {
if (prefs != null) {
await actual.internal.send('close-budget');
}
await actual.internal.send('load-budget', { id: fileId });
}
messages = messages.map(envPb => {
let timestamp = envPb.getTimestamp();
let msg = SyncPb.Message.deserializeBinary(envPb.getContent());
return {
timestamp: timestamp,
dataset: msg.getDataset(),
row: msg.getRow(),
column: msg.getColumn(),
value: msg.getValue()
};
});
let newMessages = actual.internal.syncAndReceiveMessages(messages, since);
return {
trie: actual.internal.timestamp.getClock().merkle,
newMessages: newMessages
};
});
module.exports = { sync };
let { existsSync, readFileSync } = require('fs');
let { join } = require('path');
let { openDatabase } = require('./db');
let actual = require('@actual-app/api');
let merkle = actual.internal.merkle;
let Timestamp = actual.internal.timestamp.Timestamp;
function getGroupDb(groupId) {
let path = join(__dirname, `user-files/${groupId}.sqlite`);
let needsInit = !existsSync(path);
let db = openDatabase(path);
if (needsInit) {
let sql = readFileSync(join(__dirname, 'sql/messages.sql'), 'utf8');
db.exec(sql);
}
return db;
}
function addMessages(db, messages) {
let returnValue;
db.transaction(() => {
let trie = getMerkle(db);
if (messages.length > 0) {
for (let msg of messages) {
let info = db.mutate(
`INSERT OR IGNORE INTO messages_binary (timestamp, is_encrypted, content)
VALUES (?, ?, ?)`,
[
msg.getTimestamp(),
msg.getIsencrypted() ? 1 : 0,
Buffer.from(msg.getContent())
]
);
if (info.changes > 0) {
trie = merkle.insert(trie, Timestamp.parse(msg.getTimestamp()));
}
}
}
trie = merkle.prune(trie);
db.mutate(
'INSERT INTO messages_merkles (id, merkle) VALUES (1, ?) ON CONFLICT (id) DO UPDATE SET merkle = ?',
[JSON.stringify(trie), JSON.stringify(trie)]
);
returnValue = trie;
});
return returnValue;
}
function getMerkle(db, group_id) {
let rows = db.all('SELECT * FROM messages_merkles', [group_id]);
if (rows.length > 0) {
return JSON.parse(rows[0].merkle);
} else {
// No merkle trie exists yet (first sync of the app), so create a
// default one.
return {};
}
}
function sync(messages, since, fileId) {
let db = getGroupDb(fileId);
let newMessages = db.all(
`SELECT * FROM messages_binary
WHERE timestamp > ?
ORDER BY timestamp`,
[since],
true
);
let trie = addMessages(db, messages);
return { trie, newMessages };
}
module.exports = { sync };
james@james.local.427
\ No newline at end of file
function sequential(fn) {
let sequenceState = {
running: null,
queue: []
};
function pump() {
if (sequenceState.queue.length > 0) {
const next = sequenceState.queue.shift();
run(next.args, next.resolve, next.reject);
} else {
sequenceState.running = null;
}
}
function run(args, resolve, reject) {
sequenceState.running = fn(...args);
sequenceState.running.then(
val => {
pump();
resolve(val);
},
err => {
pump();
reject(err);
}
);
}
return (...args) => {
if (!sequenceState.running) {
return new Promise((resolve, reject) => {
return run(args, resolve, reject);
});
} else {
return new Promise((resolve, reject) => {
sequenceState.queue.push({ resolve, reject, args });
});
}
};
}
module.exports = { sequential };
async function middleware(err, req, res, next) {
console.log('ERROR', err);
res.status(500).send({ status: 'error', reason: 'internal-error' });
}
module.exports = middleware;
function handleError(func) {
return (req, res) => {
func(req, res).catch(err => {
console.log('Error', req.originalUrl, err);
res.status(500);
res.send({ status: 'error', reason: 'internal-error' });
});
};
};
module.exports = { handleError }
let { getAccountDb } = require('../account-db');
function validateUser(req, res) {
let { token } = req.body || {};
if (!token) {
token = req.headers['x-actual-token'];
}
let db = getAccountDb();
let rows = db.all('SELECT * FROM sessions WHERE token = ?', [token]);
console.log(req.url, rows, token);
if (rows.length === 0) {
res.status(401);
res.send({
status: 'error',
reason: 'unauthorized',
details: 'token-not-found'
});
return null;
}
return rows[0];
}
module.exports = { validateUser };
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment