Merge branch 'actualbudget:master' into master
This commit is contained in:
commit
a94def9c55
138 changed files with 25586 additions and 19252 deletions
|
@ -1,121 +0,0 @@
|
||||||
default_config: &default_config
|
|
||||||
environment:
|
|
||||||
SENTRY_ORG: shift-reset-llc
|
|
||||||
SENTRY_PROJECT: actual
|
|
||||||
YARN_CACHE_FOLDER: ~/.cache/yarn
|
|
||||||
CSC_LINK: ~/windows-shift-reset-llc.p12
|
|
||||||
|
|
||||||
cached_files: &cached_files
|
|
||||||
paths:
|
|
||||||
- ~/.cache/yarn
|
|
||||||
- node_modules
|
|
||||||
- ./packages/desktop-electron/node_modules
|
|
||||||
- ./packages/loot-core/node_modules
|
|
||||||
- ./mobile/node_modules
|
|
||||||
- ./import-ynab4/node_modules
|
|
||||||
- ./api/node_modules
|
|
||||||
- ./node-libofx/node_modules
|
|
||||||
- ./loot-design/node_modules
|
|
||||||
- ./desktop-client/node_modules
|
|
||||||
key: v3-dependencies-{{ checksum "yarn.lock" }}
|
|
||||||
|
|
||||||
version_tag_only: &version_tag_only
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
ignore: /.*/
|
|
||||||
tags:
|
|
||||||
only: /^\d+\.\d+\.\d+$/
|
|
||||||
|
|
||||||
version: 2.1
|
|
||||||
|
|
||||||
orbs:
|
|
||||||
win: circleci/windows@2.2.0
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
<<: *default_config
|
|
||||||
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:16.15.0
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v3-dependencies-{{ checksum "yarn.lock" }}
|
|
||||||
|
|
||||||
- run: yarn install --pure-lockfile
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
<<: *cached_files
|
|
||||||
|
|
||||||
- run: yarn test
|
|
||||||
|
|
||||||
build_windows:
|
|
||||||
<<: *default_config
|
|
||||||
|
|
||||||
executor:
|
|
||||||
name: win/default
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
- run:
|
|
||||||
command: npm install -g @sentry/cli --unsafe-perm
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- run:
|
|
||||||
command: echo $WINDOWS_CERT | base64 --decode >> ${HOME}/windows-shift-reset-llc.p12
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- run:
|
|
||||||
command: yarn install --pure-lockfile
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- run:
|
|
||||||
command: ./bin/package --release --version ${CIRCLE_TAG}
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
build_linux:
|
|
||||||
<<: *default_config
|
|
||||||
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:16.15.0
|
|
||||||
|
|
||||||
working_directory: ~/repo
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v3-dependencies-{{ checksum "yarn.lock" }}
|
|
||||||
|
|
||||||
- run: yarn install --pure-lockfile
|
|
||||||
|
|
||||||
- run: sudo npm install -g @sentry/cli --unsafe-perm
|
|
||||||
|
|
||||||
- run: ./bin/package --release --version ${CIRCLE_TAG}
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
test:
|
|
||||||
jobs:
|
|
||||||
- test
|
|
||||||
build_version:
|
|
||||||
jobs:
|
|
||||||
- test:
|
|
||||||
<<: *version_tag_only
|
|
||||||
- build_windows:
|
|
||||||
<<: *version_tag_only
|
|
||||||
requires:
|
|
||||||
- test
|
|
||||||
- build_linux:
|
|
||||||
<<: *version_tag_only
|
|
||||||
requires:
|
|
||||||
- test
|
|
10
.eslintrc.js
Normal file
10
.eslintrc.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: ["prettier"],
|
||||||
|
extends: ["react-app"],
|
||||||
|
rules: {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-loop-func": "off",
|
||||||
|
"no-restricted-globals": "off"
|
||||||
|
}
|
||||||
|
};
|
14
.github/workflows/opened-issues-triage.yml
vendored
Normal file
14
.github/workflows/opened-issues-triage.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name: Mark new issue for triage
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
needs-triage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-ecosystem/action-add-labels@v1
|
||||||
|
with:
|
||||||
|
labels: needs triage
|
18
.github/workflows/test.yml
vendored
Normal file
18
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches: '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Test
|
||||||
|
run: yarn test
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -20,3 +20,12 @@ bundle.mobile.js.map
|
||||||
export-2020-01-10.csv
|
export-2020-01-10.csv
|
||||||
|
|
||||||
**/*.log
|
**/*.log
|
||||||
|
|
||||||
|
# Yarn
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
4
.prettierrc.json
Normal file
4
.prettierrc.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
785
.yarn/releases/yarn-3.2.0.cjs
vendored
Executable file
785
.yarn/releases/yarn-3.2.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
7
.yarnrc.yml
Normal file
7
.yarnrc.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||||
|
spec: "@yarnpkg/plugin-workspace-tools"
|
||||||
|
|
||||||
|
yarnPath: .yarn/releases/yarn-3.2.0.cjs
|
21
bin/package
21
bin/package
|
@ -80,27 +80,14 @@ if [[ $CI != true && "$OSTYPE" == "darwin"* ]]; then
|
||||||
yarn lint
|
yarn lint
|
||||||
fi
|
fi
|
||||||
|
|
||||||
./node_modules/.bin/patch-package
|
yarn patch-package
|
||||||
|
|
||||||
(
|
yarn workspace mobile patch-package
|
||||||
cd packages/loot-design;
|
|
||||||
../../node_modules/.bin/patch-package
|
|
||||||
)
|
|
||||||
|
|
||||||
(
|
yarn workspace loot-core build:node
|
||||||
cd packages/mobile;
|
|
||||||
../../node_modules/.bin/patch-package
|
|
||||||
)
|
|
||||||
|
|
||||||
(
|
yarn workspace @actual-app/web build
|
||||||
cd packages/loot-core;
|
|
||||||
NODE_ENV=production yarn build:node
|
|
||||||
)
|
|
||||||
|
|
||||||
(
|
|
||||||
cd packages/desktop-client;
|
|
||||||
yarn build
|
|
||||||
)
|
|
||||||
|
|
||||||
rm -fr packages/desktop-electron/client-build
|
rm -fr packages/desktop-electron/client-build
|
||||||
cp -r packages/desktop-client/build packages/desktop-electron/client-build
|
cp -r packages/desktop-client/build packages/desktop-electron/client-build
|
||||||
|
|
|
@ -56,19 +56,8 @@ if [ $CI != true ]; then
|
||||||
yarn lint
|
yarn lint
|
||||||
fi
|
fi
|
||||||
|
|
||||||
(
|
ACTUAL_RELEASE_TYPE=$RELEASE yarn workspace loot-core build:browser
|
||||||
cd packages/loot-design;
|
|
||||||
../../node_modules/.bin/patch-package
|
|
||||||
)
|
|
||||||
|
|
||||||
(
|
REACT_APP_RELEASE_TYPE=$RELEASE yarn workspace @actual-app/web build:browser
|
||||||
cd packages/loot-core;
|
|
||||||
ACTUAL_RELEASE_TYPE=$RELEASE yarn build:browser
|
|
||||||
)
|
|
||||||
|
|
||||||
(
|
|
||||||
cd packages/desktop-client;
|
|
||||||
REACT_APP_RELEASE_TYPE=$RELEASE yarn build:browser
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "packages/desktop-client/build"
|
echo "packages/desktop-client/build"
|
||||||
|
|
58
package.json
58
package.json
|
@ -15,46 +15,22 @@
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
|
||||||
"nohoist": [
|
|
||||||
"**/better-sqlite3",
|
|
||||||
"**/better-sqlite3/**",
|
|
||||||
"mobile/react-native",
|
|
||||||
"mobile/react-native-*",
|
|
||||||
"mobile/rn-fetch-blob",
|
|
||||||
"mobile/**/event-target-shim",
|
|
||||||
"mobile/@sentry/react-native",
|
|
||||||
"mobile/nodejs-mobile-react-native",
|
|
||||||
"**/mobile/nodejs-mobile-react-native/**",
|
|
||||||
"**/@react-native-community/**",
|
|
||||||
"**/@react-navigation/**",
|
|
||||||
"mobile/react-navigation",
|
|
||||||
"mobile/react-navigation-tabs",
|
|
||||||
"mobile/rn-snoopy",
|
|
||||||
"mobile/rn-snoopy/**",
|
|
||||||
"mobile/detox",
|
|
||||||
"mobile/detox/**",
|
|
||||||
"mobile/jsc-android",
|
|
||||||
"mobile/jsc-android/**",
|
|
||||||
"**/react-native-web",
|
|
||||||
"**/react-native-web/**",
|
|
||||||
"**/@sentry/cli"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm-run-all --parallel start:desktop-*",
|
"start": "npm-run-all --parallel 'start:desktop-*'",
|
||||||
"start:desktop-node": "cd packages/loot-core && yarn watch:node",
|
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||||
"start:desktop-client": "cd packages/desktop-client && yarn watch",
|
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||||
"start:desktop-electron": "cd packages/desktop-electron && yarn watch",
|
"start:desktop-electron": "yarn workspace Actual watch",
|
||||||
"start:browser": "npm-run-all --parallel start:browser-*",
|
"start:browser": "npm-run-all --parallel 'start:browser-*'",
|
||||||
"start:browser-backend": "cd packages/loot-core && yarn watch:browser",
|
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||||
"start:browser-frontend": "cd packages/desktop-client && yarn start:browser",
|
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||||
"test": "./node_modules/.bin/jest --maxWorkers=4",
|
"test": "./node_modules/.bin/jest --maxWorkers=4",
|
||||||
"test:debug": "node ./node_modules/.bin/jest --runInBand --useStderr",
|
"test:debug": "node ./node_modules/.bin/jest --runInBand --useStderr",
|
||||||
"test:debug-brk": "node --inspect-brk ./node_modules/.bin/jest --runInBand",
|
"test:debug-brk": "node --inspect-brk ./node_modules/.bin/jest --runInBand",
|
||||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
|
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
|
||||||
"rebuild-node": "cd packages/loot-core && npm rebuild",
|
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||||
"lint": "cd packages/loot-core && yarn lint",
|
"lint": "yarn workspace loot-core lint",
|
||||||
"postinstall": "rm -rf ./packages/loot-design/node_modules/react && rm -rf ./packages/mobile/node_modules/react && rm -rf ./node_modules/react-native && patch-package"
|
"postinstall": "rm -rf ./packages/loot-design/node_modules/react && rm -rf ./packages/mobile/node_modules/react && rm -rf ./node_modules/react-native && patch-package"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -62,28 +38,18 @@
|
||||||
"cross-env": "^5.1.5",
|
"cross-env": "^5.1.5",
|
||||||
"husky": "^3.0.4",
|
"husky": "^3.0.4",
|
||||||
"npm-run-all": "^4.1.3",
|
"npm-run-all": "^4.1.3",
|
||||||
|
"patch-package": "^6.1.2",
|
||||||
"prettier": "^1.18.1",
|
"prettier": "^1.18.1",
|
||||||
"pretty-quick": "^1.11.1",
|
"pretty-quick": "^1.11.1",
|
||||||
"shelljs": "^0.8.2",
|
"shelljs": "^0.8.2",
|
||||||
"source-map-support": "^0.5.21"
|
"source-map-support": "^0.5.21"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "react-app",
|
|
||||||
"rules": {
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"no-loop-func": "off",
|
|
||||||
"no-restricted-globals": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"prettier": {
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@babel/preset-env": "^7.15.1",
|
"@babel/preset-env": "^7.15.1",
|
||||||
"@babel/core": "^7.15.1",
|
"@babel/core": "^7.15.1",
|
||||||
"@babel/runtime": "^7.15.1",
|
"@babel/runtime": "^7.15.1",
|
||||||
"@babel/helper-plugin-utils": "^7.14.5",
|
"@babel/helper-plugin-utils": "^7.14.5",
|
||||||
"react-error-overlay": "6.0.9"
|
"react-error-overlay": "6.0.9"
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@3.2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -518,6 +518,10 @@ module.exports = function(webpackEnv) {
|
||||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||||
// You can remove this if you don't use Moment.js:
|
// You can remove this if you don't use Moment.js:
|
||||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||||
|
// Pikaday throws a warning if Moment.js is not installed however it doesn't
|
||||||
|
// actually require it to be installed. As we don't use Moment.js ourselves
|
||||||
|
// then we can just silence this warning.
|
||||||
|
new webpack.IgnorePlugin(/moment$/, /pikaday$/),
|
||||||
!(isEnvDevelopment || process.env.PERF_BUILD) &&
|
!(isEnvDevelopment || process.env.PERF_BUILD) &&
|
||||||
new webpack.IgnorePlugin(/perf-deets\/frontend/),
|
new webpack.IgnorePlugin(/perf-deets\/frontend/),
|
||||||
// Generate a service worker script that will precache, and keep up to date,
|
// Generate a service worker script that will precache, and keep up to date,
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
"@reactions/component": "^2.0.2",
|
"@reactions/component": "^2.0.2",
|
||||||
"@sentry/browser": "6.12.0",
|
"@sentry/browser": "6.12.0",
|
||||||
"@svgr/webpack": "2.4.1",
|
"@svgr/webpack": "2.4.1",
|
||||||
"add": "^2.0.6",
|
|
||||||
"babel-eslint": "9.0.0",
|
"babel-eslint": "9.0.0",
|
||||||
"babel-loader": "8.0.4",
|
"babel-loader": "8.0.4",
|
||||||
"babel-preset-jwl-app": "6.1.12",
|
"babel-preset-jwl-app": "6.1.12",
|
||||||
|
@ -23,6 +22,7 @@
|
||||||
"case-sensitive-paths-webpack-plugin": "2.1.2",
|
"case-sensitive-paths-webpack-plugin": "2.1.2",
|
||||||
"chalk": "2.4.1",
|
"chalk": "2.4.1",
|
||||||
"codemirror": "^5.37.0",
|
"codemirror": "^5.37.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "1.0.0",
|
"css-loader": "1.0.0",
|
||||||
"date-fns": "2.0.0-alpha.27",
|
"date-fns": "2.0.0-alpha.27",
|
||||||
"debounce": "^1.2.0",
|
"debounce": "^1.2.0",
|
||||||
|
@ -41,10 +41,8 @@
|
||||||
"fs-extra": "7.0.0",
|
"fs-extra": "7.0.0",
|
||||||
"glamor": "^2.20.40",
|
"glamor": "^2.20.40",
|
||||||
"html-webpack-plugin": "4.0.0-alpha.2",
|
"html-webpack-plugin": "4.0.0-alpha.2",
|
||||||
"http-client": "^4.3.1",
|
|
||||||
"identity-obj-proxy": "3.0.0",
|
"identity-obj-proxy": "3.0.0",
|
||||||
"load-js": "^3.0.3",
|
"load-js": "^3.0.3",
|
||||||
"lodash.memoize": "^4.1.2",
|
|
||||||
"mini-css-extract-plugin": "0.4.3",
|
"mini-css-extract-plugin": "0.4.3",
|
||||||
"mitt": "^1.1.2",
|
"mitt": "^1.1.2",
|
||||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||||
|
@ -55,7 +53,6 @@
|
||||||
"postcss-preset-env": "6.3.1",
|
"postcss-preset-env": "6.3.1",
|
||||||
"postcss-safe-parser": "4.0.1",
|
"postcss-safe-parser": "4.0.1",
|
||||||
"prop-types": "15.6.0",
|
"prop-types": "15.6.0",
|
||||||
"raven-js": "^3.21.0",
|
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-addons-shallow-compare": "^15.6.0",
|
"react-addons-shallow-compare": "^15.6.0",
|
||||||
"react-app-polyfill": "^0.1.3",
|
"react-app-polyfill": "^0.1.3",
|
||||||
|
@ -75,7 +72,6 @@
|
||||||
"sass-loader": "7.1.0",
|
"sass-loader": "7.1.0",
|
||||||
"style-loader": "0.23.0",
|
"style-loader": "0.23.0",
|
||||||
"terser-webpack-plugin": "1.1.0",
|
"terser-webpack-plugin": "1.1.0",
|
||||||
"url-loader": "1.1.1",
|
|
||||||
"victory": "^0.26.1",
|
"victory": "^0.26.1",
|
||||||
"webpack": "4.19.1",
|
"webpack": "4.19.1",
|
||||||
"webpack-dev-server": "3.11.0",
|
"webpack-dev-server": "3.11.0",
|
||||||
|
|
|
@ -9,14 +9,18 @@ function getFlex(flex) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Box({ flex, children, direction, style }) {
|
function Box({ flex, children, direction, style }) {
|
||||||
return <div
|
return (
|
||||||
|
<div
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
flex: getFlex(flex),
|
flex: getFlex(flex),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: direction || 'column'
|
flexDirection: direction || 'column'
|
||||||
}}
|
}}
|
||||||
>{children}</div>;
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Box;
|
export default Box;
|
||||||
|
|
|
@ -206,7 +206,7 @@ class Debugger extends React.Component {
|
||||||
height: 10,
|
height: 10,
|
||||||
backgroundColor: '#303030',
|
backgroundColor: '#303030',
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
borderRadius: 10,
|
borderRadius: 10
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button onClick={this.toggleRecord} style={{ marginRight: 10 }}>
|
<Button onClick={this.toggleRecord} style={{ marginRight: 10 }}>
|
||||||
|
|
|
@ -99,7 +99,9 @@ class FatalError extends React.Component {
|
||||||
.
|
.
|
||||||
</P>
|
</P>
|
||||||
<P>
|
<P>
|
||||||
<Button onClick={() => window.Actual.relaunch()}>{buttonText}</Button>
|
<Button onClick={() => window.Actual.relaunch()}>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
</P>
|
</P>
|
||||||
<P isLast={true} style={{ fontSize: 11 }}>
|
<P isLast={true} style={{ fontSize: 11 }}>
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -277,7 +277,4 @@ function FinancesAppWithContext(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(null, actions)(FinancesAppWithContext);
|
||||||
null,
|
|
||||||
actions
|
|
||||||
)(FinancesAppWithContext);
|
|
||||||
|
|
|
@ -642,7 +642,7 @@ const AccountHeader = React.memo(
|
||||||
let searchInput = useRef(null);
|
let searchInput = useRef(null);
|
||||||
let splitsExpanded = useSplitsExpanded();
|
let splitsExpanded = useSplitsExpanded();
|
||||||
|
|
||||||
let canSync = syncEnabled && (account && account.account_id);
|
let canSync = syncEnabled && account && account.account_id;
|
||||||
if (!account) {
|
if (!account) {
|
||||||
// All accounts - check for any syncable account
|
// All accounts - check for any syncable account
|
||||||
canSync = !!accounts.find(account => !!account.account_id);
|
canSync = !!accounts.find(account => !!account.account_id);
|
||||||
|
@ -1701,9 +1701,9 @@ class AccountInternal extends React.PureComponent {
|
||||||
}
|
}
|
||||||
showAccount={
|
showAccount={
|
||||||
!accountId ||
|
!accountId ||
|
||||||
(accountId === 'offbudget' ||
|
accountId === 'offbudget' ||
|
||||||
accountId === 'budgeted' ||
|
accountId === 'budgeted' ||
|
||||||
accountId === 'uncategorized')
|
accountId === 'uncategorized'
|
||||||
}
|
}
|
||||||
isAdding={this.state.isAdding}
|
isAdding={this.state.isAdding}
|
||||||
isNew={this.isNew}
|
isNew={this.isNew}
|
||||||
|
|
|
@ -149,7 +149,11 @@ function ConfigureField({ field, op, value, dispatch, onApply }) {
|
||||||
['amount-outflow', 'Amount (outflow)']
|
['amount-outflow', 'Amount (outflow)']
|
||||||
]
|
]
|
||||||
: field === 'date'
|
: field === 'date'
|
||||||
? [['date', 'Date'], ['month', 'Month'], ['year', 'Year']]
|
? [
|
||||||
|
['date', 'Date'],
|
||||||
|
['month', 'Month'],
|
||||||
|
['year', 'Year']
|
||||||
|
]
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
value={subfield}
|
value={subfield}
|
||||||
|
|
|
@ -230,17 +230,14 @@ class ManagementApp extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(state => {
|
||||||
state => {
|
let { modalStack } = state.modals;
|
||||||
let { modalStack } = state.modals;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
files: state.budgets.allFiles,
|
files: state.budgets.allFiles,
|
||||||
userData: state.user.data,
|
userData: state.user.data,
|
||||||
managerHasInitialized: state.app.managerHasInitialized,
|
managerHasInitialized: state.app.managerHasInitialized,
|
||||||
loadingText: state.app.loadingText,
|
loadingText: state.app.loadingText,
|
||||||
currentModals: modalStack.map(modal => modal.name)
|
currentModals: modalStack.map(modal => modal.name)
|
||||||
};
|
};
|
||||||
},
|
}, actions)(ManagementApp);
|
||||||
actions
|
|
||||||
)(ManagementApp);
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ export function ConfirmPasswordForm({ buttons, onSetPassword, onError }) {
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={password1}
|
value={password1}
|
||||||
onChange={e => setPassword1(e.target.value)}
|
onChange={e => setPassword1(e.target.value)}
|
||||||
|
onEnter={onSubmit}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
|
@ -54,6 +55,7 @@ export function ConfirmPasswordForm({ buttons, onSetPassword, onError }) {
|
||||||
value={password2}
|
value={password2}
|
||||||
onChange={e => setPassword2(e.target.value)}
|
onChange={e => setPassword2(e.target.value)}
|
||||||
style={{ marginTop: 10 }}
|
style={{ marginTop: 10 }}
|
||||||
|
onEnter={onSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -69,10 +71,7 @@ export function ConfirmPasswordForm({ buttons, onSetPassword, onError }) {
|
||||||
</label>
|
</label>
|
||||||
<View style={{ flex: 1 }} />
|
<View style={{ flex: 1 }} />
|
||||||
{buttons}
|
{buttons}
|
||||||
<ButtonWithLoading
|
<ButtonWithLoading primary loading={loading}>
|
||||||
primary
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
OK
|
OK
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -70,7 +70,4 @@ function WelcomeScreen({ modalProps, actions }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(null, actions)(WelcomeScreen);
|
||||||
null,
|
|
||||||
actions
|
|
||||||
)(WelcomeScreen);
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { styles } from 'loot-design/src/style';
|
import { styles } from 'loot-design/src/style';
|
||||||
import { integerToCurrency } from 'loot-core/src/shared/util';
|
import { integerToCurrency } from 'loot-core/src/shared/util';
|
||||||
import { Block } from 'loot-design/src/components/common';
|
import { Block } from 'loot-design/src/components/common';
|
||||||
import { colors } from 'loot-design/src/style'
|
import { colors } from 'loot-design/src/style';
|
||||||
|
|
||||||
function Change({ amount, style }) {
|
function Change({ amount, style }) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -110,7 +110,10 @@ function CashFlowCard() {
|
||||||
const end = monthUtils.currentDay();
|
const end = monthUtils.currentDay();
|
||||||
const start = monthUtils.currentMonth() + '-01';
|
const start = monthUtils.currentMonth() + '-01';
|
||||||
|
|
||||||
const data = useReport('cash_flow_simple', useArgsMemo(simpleCashFlow)(start, end));
|
const data = useReport(
|
||||||
|
'cash_flow_simple',
|
||||||
|
useArgsMemo(simpleCashFlow)(start, end)
|
||||||
|
);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -218,9 +221,9 @@ function CashFlowCard() {
|
||||||
function Overview({ accounts }) {
|
function Overview({ accounts }) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.page,
|
styles.page,
|
||||||
{ paddingLeft: 40, paddingRight: 40, minWidth: 700 }
|
{ paddingLeft: 40, paddingRight: 40, minWidth: 700 }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
|
|
@ -15,9 +15,9 @@ function BudgetInitial({ accounts, navigationProps }) {
|
||||||
{!minimized && (
|
{!minimized && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<P>
|
<P>
|
||||||
You should see all of your current accounts' balance available
|
You should see all of your current accounts' balance available to
|
||||||
to budget. Click on the budgeted column for a category create a
|
budget. Click on the budgeted column for a category create a budget.
|
||||||
budget. Keep doing this until your "To Budget" amount is zero.
|
Keep doing this until your "To Budget" amount is zero.
|
||||||
</P>
|
</P>
|
||||||
<P>
|
<P>
|
||||||
Don't worry too much about your initial budget. Just guess. You'll
|
Don't worry too much about your initial budget. Just guess. You'll
|
||||||
|
|
|
@ -61,7 +61,6 @@ function BudgetNextMonth({ stepTwo, navigationProps }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(null, dispatch => bindActionCreators(actions, dispatch))(
|
||||||
null,
|
BudgetNextMonth
|
||||||
dispatch => bindActionCreators(actions, dispatch)
|
);
|
||||||
)(BudgetNextMonth);
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { styles, colors } from 'loot-design/src/style';
|
||||||
import { Standalone, Title, useMinimized } from './common';
|
import { Standalone, Title, useMinimized } from './common';
|
||||||
|
|
||||||
function CategoryBalance({ targetRect, navigationProps }) {
|
function CategoryBalance({ targetRect, navigationProps }) {
|
||||||
let [minimized, toggle] = useMinimized()
|
let [minimized, toggle] = useMinimized();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Standalone>
|
<Standalone>
|
||||||
|
|
|
@ -96,7 +96,6 @@ function Overspending({ navigationProps, stepTwo }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(null, dispatch => bindActionCreators(actions, dispatch))(
|
||||||
null,
|
Overspending
|
||||||
dispatch => bindActionCreators(actions, dispatch)
|
);
|
||||||
)(Overspending);
|
|
||||||
|
|
|
@ -33,8 +33,8 @@ function TransactionAdd({ targetRect, navigationProps }) {
|
||||||
</P>
|
</P>
|
||||||
|
|
||||||
<P isLast={true}>
|
<P isLast={true}>
|
||||||
Try <strong>clicking "Add New"</strong> to see how adding
|
Try <strong>clicking "Add New"</strong> to see how adding transactions
|
||||||
transactions affects your budget.
|
affects your budget.
|
||||||
</P>
|
</P>
|
||||||
|
|
||||||
<Navigation {...navigationProps} showBack={false} />
|
<Navigation {...navigationProps} showBack={false} />
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { P } from 'loot-design/src/components/common';
|
||||||
import { css } from 'glamor';
|
import { css } from 'glamor';
|
||||||
import Navigation from './Navigation';
|
import Navigation from './Navigation';
|
||||||
import * as monthUtils from 'loot-core/src/shared/months';
|
import * as monthUtils from 'loot-core/src/shared/months';
|
||||||
import {Standalone} from './common';
|
import { Standalone } from './common';
|
||||||
|
|
||||||
function TransactionFinalize({ navigationProps }) {
|
function TransactionFinalize({ navigationProps }) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,8 +2,8 @@ const d = require('date-fns');
|
||||||
const normalizePathSep = require('slash');
|
const normalizePathSep = require('slash');
|
||||||
const uuid = require('uuid');
|
const uuid = require('uuid');
|
||||||
const AdmZip = require('adm-zip');
|
const AdmZip = require('adm-zip');
|
||||||
const actual = require('@actual-app/api/methods');
|
const actual = require('@actual-app/api');
|
||||||
const { amountToInteger } = require('@actual-app/api/utils');
|
const amountToInteger = actual.utils.amountToInteger;
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const d = require('date-fns');
|
const d = require('date-fns');
|
||||||
const uuid = require('uuid');
|
const uuid = require('uuid');
|
||||||
const actual = require('@actual-app/api/methods');
|
const actual = require('@actual-app/api');
|
||||||
const { amountToInteger } = require('@actual-app/api/utils');
|
|
||||||
|
|
||||||
function amountFromYnab(amount) {
|
function amountFromYnab(amount) {
|
||||||
// ynabs multiplies amount by 1000 and actual by 100
|
// ynabs multiplies amount by 1000 and actual by 100
|
||||||
|
|
1
packages/loot-core/bin/get-db-schema
Executable file → Normal file
1
packages/loot-core/bin/get-db-schema
Executable file → Normal file
|
@ -3,7 +3,6 @@ process.env.LOOT_DATA_DIR = __dirname + '/../../../../data';
|
||||||
import * as sqlite from '../platform/server/sqlite';
|
import * as sqlite from '../platform/server/sqlite';
|
||||||
import * as db from '../server/db';
|
import * as db from '../server/db';
|
||||||
import { getMessages } from '../server/sync';
|
import { getMessages } from '../server/sync';
|
||||||
import Timestamp from '../server/timestamp';
|
|
||||||
|
|
||||||
let dbPath = process.argv[2];
|
let dbPath = process.argv[2];
|
||||||
let tables = [
|
let tables = [
|
||||||
|
|
|
@ -4,7 +4,6 @@ import os from 'os';
|
||||||
import * as sqlite from '../src/platform/server/sqlite';
|
import * as sqlite from '../src/platform/server/sqlite';
|
||||||
import * as db from '../src/server/db';
|
import * as db from '../src/server/db';
|
||||||
import { batchMessages, setSyncingMode } from '../src/server/sync';
|
import { batchMessages, setSyncingMode } from '../src/server/sync';
|
||||||
import Timestamp from '../src/server/timestamp';
|
|
||||||
import { runQuery } from '../src/server/aql/schema/run-query';
|
import { runQuery } from '../src/server/aql/schema/run-query';
|
||||||
import asyncStorage from '../src/platform/server/asyncStorage';
|
import asyncStorage from '../src/platform/server/asyncStorage';
|
||||||
import { makeChild } from '../src/shared/transactions';
|
import { makeChild } from '../src/shared/transactions';
|
||||||
|
|
|
@ -43,25 +43,29 @@
|
||||||
"babel-jest": "25.2.6",
|
"babel-jest": "25.2.6",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"damerau-levenshtein": "^1.0.4",
|
"damerau-levenshtein": "^1.0.4",
|
||||||
"date-fns": "2.0.0-alpha.27",
|
"date-fns": "2.0.0-alpha.27",
|
||||||
|
"eslint": "5.6.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"esm": "^3.0.82",
|
"esm": "^3.0.82",
|
||||||
"fake-indexeddb": "^3.1.3",
|
"fake-indexeddb": "^3.1.3",
|
||||||
"fast-check": "^2.11.0",
|
"fast-check": "2.13.0",
|
||||||
"fast-glob": "^2.2.0",
|
"fast-glob": "^2.2.0",
|
||||||
"jest": "25.2.7",
|
"jest": "25.2.7",
|
||||||
"jsverify": "^0.8.4",
|
"jsverify": "^0.8.4",
|
||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
"memfs": "3.1.1",
|
"memfs": "3.1.1",
|
||||||
"memoize-one": "^4.0.0",
|
"memoize-one": "^4.0.0",
|
||||||
"mockdate": "^3.0.5",
|
|
||||||
"mock-require": "^3.0.2",
|
"mock-require": "^3.0.2",
|
||||||
|
"mockdate": "^3.0.5",
|
||||||
"murmurhash": "^0.0.2",
|
"murmurhash": "^0.0.2",
|
||||||
"perf-deets": "^1.0.15",
|
"perf-deets": "^1.0.15",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
"sanitize-filename": "^1.6.1",
|
"sanitize-filename": "^1.6.1",
|
||||||
"search-query-parser": "^1.3.0",
|
"search-query-parser": "^1.3.0",
|
||||||
"source-map": "^0.7.3",
|
|
||||||
"snapshot-diff": "^0.2.2",
|
"snapshot-diff": "^0.2.2",
|
||||||
|
"source-map": "^0.7.3",
|
||||||
"throttleit": "^1.0.0",
|
"throttleit": "^1.0.0",
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
"webpack": "^4.41.2",
|
"webpack": "^4.41.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { send } from '../../platform/client/fetch';
|
import { send } from '../../platform/client/fetch';
|
||||||
import constants from '../constants';
|
import constants from '../constants';
|
||||||
import { getPayees, getAccounts, filterTransactions } from './queries';
|
import { getPayees, getAccounts } from './queries';
|
||||||
import { addNotification } from './notifications';
|
import { addNotification } from './notifications';
|
||||||
|
|
||||||
export function setAccountsSyncing(name) {
|
export function setAccountsSyncing(name) {
|
||||||
|
|
|
@ -5,5 +5,5 @@ export function debugCell(sheet, name) {
|
||||||
type: constants.DEBUG_CELL,
|
type: constants.DEBUG_CELL,
|
||||||
sheet,
|
sheet,
|
||||||
name
|
name
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { send } from '../../platform/client/fetch';
|
import { send } from '../../platform/client/fetch';
|
||||||
import constants from '../constants';
|
import constants from '../constants';
|
||||||
import { loadPrefs } from './prefs';
|
import { loadPrefs } from './prefs';
|
||||||
import { createBudget, loadBudget } from './budgets';
|
|
||||||
import { getCategories, getAccounts, getPayees } from './queries';
|
|
||||||
import { syncAccounts } from './account';
|
import { syncAccounts } from './account';
|
||||||
import { setAppState } from './app';
|
|
||||||
import { pushModal } from './modals';
|
import { pushModal } from './modals';
|
||||||
import { getUploadError } from '../../shared/errors';
|
import { getUploadError } from '../../shared/errors';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import constants from '../constants';
|
|
||||||
import { send } from '../../platform/client/fetch';
|
import { send } from '../../platform/client/fetch';
|
||||||
import Platform from '../platform';
|
import Platform from '../platform';
|
||||||
import { pushModal } from './modals';
|
import { pushModal } from './modals';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useMemo, useEffect, useState, useContext } from 'react';
|
import React, { useEffect, useState, useContext } from 'react';
|
||||||
import q, { liveQuery, runQuery } from 'loot-core/src/client/query-helpers';
|
import q, { liveQuery } from 'loot-core/src/client/query-helpers';
|
||||||
import { getAccountsById } from 'loot-core/src/client/reducers/queries';
|
import { getAccountsById } from 'loot-core/src/client/reducers/queries';
|
||||||
|
|
||||||
export function useAccounts() {
|
export function useAccounts() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useMemo, useEffect, useState, useContext } from 'react';
|
import React, { useEffect, useState, useContext } from 'react';
|
||||||
import q, { liveQuery, runQuery } from 'loot-core/src/client/query-helpers';
|
import q, { liveQuery } from 'loot-core/src/client/query-helpers';
|
||||||
import { getPayeesById } from 'loot-core/src/client/reducers/queries';
|
import { getPayeesById } from 'loot-core/src/client/reducers/queries';
|
||||||
|
|
||||||
export function usePayees() {
|
export function usePayees() {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import React, { useMemo, useEffect, useState, useContext } from 'react';
|
import React, { useEffect, useState, useContext } from 'react';
|
||||||
import * as monthUtils from 'loot-core/src/shared/months';
|
|
||||||
import {
|
import {
|
||||||
getStatus,
|
getStatus,
|
||||||
getHasTransactionsQuery
|
getHasTransactionsQuery
|
||||||
} from 'loot-core/src/shared/schedules';
|
} from 'loot-core/src/shared/schedules';
|
||||||
import q, { liveQuery, runQuery } from 'loot-core/src/client/query-helpers';
|
import q, { liveQuery } from 'loot-core/src/client/query-helpers';
|
||||||
import { send } from 'loot-core/src/platform/client/fetch';
|
|
||||||
|
|
||||||
function loadStatuses(schedules, onData) {
|
function loadStatuses(schedules, onData) {
|
||||||
return liveQuery(getHasTransactionsQuery(schedules), onData, {
|
return liveQuery(getHasTransactionsQuery(schedules), onData, {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { listen, send } from '../platform/client/fetch';
|
import { listen, send } from '../platform/client/fetch';
|
||||||
import { once, runAgain } from '../shared/async';
|
import { once } from '../shared/async';
|
||||||
|
|
||||||
import q, { getPrimaryOrderBy } from '../shared/query';
|
import q, { getPrimaryOrderBy } from '../shared/query';
|
||||||
export default q;
|
export default q;
|
||||||
|
|
|
@ -439,15 +439,19 @@ async function fillOther(handlers, account, payees, groups) {
|
||||||
|
|
||||||
async function createBudget(accounts, payees, groups) {
|
async function createBudget(accounts, payees, groups) {
|
||||||
let primaryAccount = accounts.find(a => (a.name = 'Bank of America'));
|
let primaryAccount = accounts.find(a => (a.name = 'Bank of America'));
|
||||||
let earliestDate = (await db.first(
|
let earliestDate = (
|
||||||
`SELECT * FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
|
await db.first(
|
||||||
|
`SELECT * FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
|
||||||
WHERE a.offbudget = 0 AND t.is_child = 0 ORDER BY date ASC LIMIT 1`
|
WHERE a.offbudget = 0 AND t.is_child = 0 ORDER BY date ASC LIMIT 1`
|
||||||
)).date;
|
)
|
||||||
let earliestPrimaryDate = (await db.first(
|
).date;
|
||||||
`SELECT * FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
|
let earliestPrimaryDate = (
|
||||||
|
await db.first(
|
||||||
|
`SELECT * FROM v_transactions t LEFT JOIN accounts a ON t.account = a.id
|
||||||
WHERE a.id = ? AND a.offbudget = 0 AND t.is_child = 0 ORDER BY date ASC LIMIT 1`,
|
WHERE a.id = ? AND a.offbudget = 0 AND t.is_child = 0 ORDER BY date ASC LIMIT 1`,
|
||||||
[primaryAccount.id]
|
[primaryAccount.id]
|
||||||
)).date;
|
)
|
||||||
|
).date;
|
||||||
|
|
||||||
let start = monthUtils.monthFromDate(db.fromDateRepr(earliestDate));
|
let start = monthUtils.monthFromDate(db.fromDateRepr(earliestDate));
|
||||||
let end = monthUtils.currentMonth();
|
let end = monthUtils.currentMonth();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import dateFns from 'date-fns';
|
|
||||||
const uuid = require('../platform/uuid');
|
const uuid = require('../platform/uuid');
|
||||||
|
|
||||||
export function generateAccount(balance) {
|
export function generateAccount(balance) {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import * as sheet from '../server/sheet';
|
||||||
import * as rules from '../server/accounts/transaction-rules';
|
import * as rules from '../server/accounts/transaction-rules';
|
||||||
import * as tracking from '../server/tracking/events';
|
import * as tracking from '../server/tracking/events';
|
||||||
import { setSyncingMode } from '../server/sync';
|
import { setSyncingMode } from '../server/sync';
|
||||||
import * as monthUtils from '../shared/months';
|
|
||||||
import { updateVersion } from '../server/update';
|
import { updateVersion } from '../server/update';
|
||||||
import { resetTracer, tracer } from '../shared/test-helpers';
|
import { resetTracer, tracer } from '../shared/test-helpers';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -172,7 +172,10 @@ module.exports.listen = function listen(name, cb) {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
let arr = listeners.get(name);
|
let arr = listeners.get(name);
|
||||||
listeners.set(name, arr.filter(cb_ => cb_ !== cb));
|
listeners.set(
|
||||||
|
name,
|
||||||
|
arr.filter(cb_ => cb_ !== cb)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,10 @@ function listen(name, cb) {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
let arr = listeners.get(name);
|
let arr = listeners.get(name);
|
||||||
listeners.set(name, arr.filter(cb_ => cb_ !== cb));
|
listeners.set(
|
||||||
|
name,
|
||||||
|
arr.filter(cb_ => cb_ !== cb)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,9 @@ module.exports.listen = function listen(name, cb) {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
let arr = listeners.get(name);
|
let arr = listeners.get(name);
|
||||||
listeners.set(name, arr.filter(cb_ => cb_ !== cb));
|
listeners.set(
|
||||||
|
name,
|
||||||
|
arr.filter(cb_ => cb_ !== cb)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -117,7 +117,10 @@ module.exports.listen = function listen(name, cb) {
|
||||||
return () => {
|
return () => {
|
||||||
let arr = listeners.get(name);
|
let arr = listeners.get(name);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
listeners.set(name, arr.filter(cb_ => cb_ !== cb));
|
listeners.set(
|
||||||
|
name,
|
||||||
|
arr.filter(cb_ => cb_ !== cb)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
export function captureException(exc) {
|
export function captureException(exc) {}
|
||||||
}
|
|
||||||
|
|
||||||
export function captureBreadcrumb(info) {
|
export function captureBreadcrumb(info) {}
|
||||||
}
|
|
||||||
|
|
|
@ -167,9 +167,9 @@ async function _removeFile(filepath) {
|
||||||
|
|
||||||
// Load files from the server that should exist by default
|
// Load files from the server that should exist by default
|
||||||
async function populateDefaultFilesystem() {
|
async function populateDefaultFilesystem() {
|
||||||
let index = await (await fetch(
|
let index = await (
|
||||||
process.env.PUBLIC_URL + 'data-file-index.txt'
|
await fetch(process.env.PUBLIC_URL + 'data-file-index.txt')
|
||||||
)).text();
|
).text();
|
||||||
let files = index
|
let files = index
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(name => name.trim())
|
.map(name => name.trim())
|
||||||
|
|
|
@ -4,16 +4,7 @@ require('fake-indexeddb/auto');
|
||||||
let FDBFactory = require('fake-indexeddb/lib/FDBFactory');
|
let FDBFactory = require('fake-indexeddb/lib/FDBFactory');
|
||||||
|
|
||||||
let idb = require('../indexeddb');
|
let idb = require('../indexeddb');
|
||||||
let {
|
let { init, readFile, writeFile, exists, pathToId, join } = require('./index');
|
||||||
init,
|
|
||||||
populateFileHeirarchy,
|
|
||||||
readFile,
|
|
||||||
writeFile,
|
|
||||||
exists,
|
|
||||||
pathToId,
|
|
||||||
listDir,
|
|
||||||
join
|
|
||||||
} = require('./index');
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
process.env.PUBLIC_URL =
|
process.env.PUBLIC_URL =
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import fs from 'fs';
|
|
||||||
import csvStringify from 'csv-stringify/lib/sync';
|
import csvStringify from 'csv-stringify/lib/sync';
|
||||||
import { runQuery as aqlQuery } from '../aql/schema/run-query';
|
import { runQuery as aqlQuery } from '../aql/schema/run-query';
|
||||||
import { integerToAmount } from '../../shared/util';
|
import { integerToAmount } from '../../shared/util';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import asyncStorage from '../../platform/server/asyncStorage';
|
import asyncStorage from '../../platform/server/asyncStorage';
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
import { getServer } from '../server-config';
|
import { getServer } from '../server-config';
|
||||||
import * as bankSync from './sync';
|
import * as bankSync from './sync';
|
||||||
import { fromPlaidAccountType } from '../../shared/accounts';
|
import { fromPlaidAccountType } from '../../shared/accounts';
|
||||||
import { amountToInteger } from '../../shared/util';
|
import { amountToInteger } from '../../shared/util';
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
isAfter,
|
isAfter,
|
||||||
addDays,
|
addDays,
|
||||||
subDays,
|
subDays,
|
||||||
format as formatDate,
|
|
||||||
parseDate
|
parseDate
|
||||||
} from '../../shared/months';
|
} from '../../shared/months';
|
||||||
import { fastSetMerge } from '../../shared/util';
|
import { fastSetMerge } from '../../shared/util';
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
parseDateString,
|
parseDateString,
|
||||||
parseRecurDate,
|
|
||||||
rankRules,
|
rankRules,
|
||||||
iterateIds,
|
iterateIds,
|
||||||
Condition,
|
Condition,
|
||||||
|
|
|
@ -1,24 +1,15 @@
|
||||||
import title from './title';
|
import title from './title';
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
import {
|
import { hasFieldsChanged, amountToInteger } from '../../shared/util';
|
||||||
mergeObjects,
|
|
||||||
hasFieldsChanged,
|
|
||||||
toRelaxedNumber,
|
|
||||||
amountToInteger,
|
|
||||||
integerToAmount
|
|
||||||
} from '../../shared/util';
|
|
||||||
import {
|
import {
|
||||||
makeChild as makeChildTransaction,
|
makeChild as makeChildTransaction,
|
||||||
recalculateSplit
|
recalculateSplit
|
||||||
} from '../../shared/transactions';
|
} from '../../shared/transactions';
|
||||||
import * as monthUtils from '../../shared/months';
|
import * as monthUtils from '../../shared/months';
|
||||||
import { transactionModel } from '../api-models';
|
|
||||||
import { getServer } from '../server-config';
|
import { getServer } from '../server-config';
|
||||||
import { batchMessages } from '../sync';
|
import { batchMessages } from '../sync';
|
||||||
import { runMutator } from '../mutators';
|
import { runMutator } from '../mutators';
|
||||||
import { getStartingBalancePayee } from './payees';
|
import { getStartingBalancePayee } from './payees';
|
||||||
import * as transfer from './transfer';
|
|
||||||
import { TransactionError } from '../errors';
|
|
||||||
import { runRules } from './transaction-rules';
|
import { runRules } from './transaction-rules';
|
||||||
import { batchUpdateTransactions } from './transactions';
|
import { batchUpdateTransactions } from './transactions';
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
const conjunctions = [
|
const conjunctions = [
|
||||||
'for',
|
'for', //
|
||||||
'and',
|
'and',
|
||||||
'nor',
|
'nor',
|
||||||
'but',
|
'but',
|
||||||
'or',
|
'or',
|
||||||
'yet',
|
'yet',
|
||||||
'so'
|
'so'
|
||||||
]
|
];
|
||||||
|
|
||||||
const articles = [
|
const articles = [
|
||||||
'a',
|
'a', //
|
||||||
'an',
|
'an',
|
||||||
'the'
|
'the'
|
||||||
]
|
];
|
||||||
|
|
||||||
const prepositions = [
|
const prepositions = [
|
||||||
'aboard',
|
'aboard',
|
||||||
|
@ -84,10 +84,6 @@ const prepositions = [
|
||||||
'with',
|
'with',
|
||||||
'within',
|
'within',
|
||||||
'without'
|
'without'
|
||||||
]
|
];
|
||||||
|
|
||||||
module.exports = new Set([
|
module.exports = new Set([...conjunctions, ...articles, ...prepositions]);
|
||||||
...conjunctions,
|
|
||||||
...articles,
|
|
||||||
...prepositions
|
|
||||||
])
|
|
||||||
|
|
|
@ -17,13 +17,11 @@ import {
|
||||||
sortNumbers,
|
sortNumbers,
|
||||||
getApproxNumberThreshold
|
getApproxNumberThreshold
|
||||||
} from '../../shared/rules';
|
} from '../../shared/rules';
|
||||||
import q from '../../shared/query';
|
|
||||||
import { requiredFields, toDateRepr } from '../models';
|
import { requiredFields, toDateRepr } from '../models';
|
||||||
import { currentDay } from '../../shared/months';
|
import { currentDay } from '../../shared/months';
|
||||||
import { partitionByField, fastSetMerge } from '../../shared/util';
|
import { partitionByField, fastSetMerge } from '../../shared/util';
|
||||||
import { setSyncingMode, batchMessages } from '../sync';
|
import { setSyncingMode, batchMessages } from '../sync';
|
||||||
import { schemaConfig } from '../aql/schema';
|
import { schemaConfig } from '../aql/schema';
|
||||||
const uuid = require('../../platform/uuid');
|
|
||||||
|
|
||||||
// TODO: Detect if it looks like the user is creating a rename rule
|
// TODO: Detect if it looks like the user is creating a rename rule
|
||||||
// and prompt to create it in the pre phase instead
|
// and prompt to create it in the pre phase instead
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
import { Rule } from './rules';
|
|
||||||
import {
|
import {
|
||||||
getRules,
|
getRules,
|
||||||
loadRules,
|
loadRules,
|
||||||
|
@ -15,7 +14,6 @@ import {
|
||||||
migrateOldRules
|
migrateOldRules
|
||||||
} from './transaction-rules';
|
} from './transaction-rules';
|
||||||
import { loadMappings } from '../db/mappings';
|
import { loadMappings } from '../db/mappings';
|
||||||
import { applyMigration } from '../migrate/migrations';
|
|
||||||
import { runQuery } from '../aql/schema/run-query';
|
import { runQuery } from '../aql/schema/run-query';
|
||||||
import q from '../../shared/query';
|
import q from '../../shared/query';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import * as sheet from '../sheet';
|
|
||||||
import { batchMessages } from '../sync';
|
import { batchMessages } from '../sync';
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
import { incrFetch, whereIn } from '../db/util';
|
import { incrFetch, whereIn } from '../db/util';
|
||||||
|
@ -117,9 +116,9 @@ export async function batchUpdateTransactions({
|
||||||
await Promise.all(allAdded.map(t => transfer.onInsert(t)));
|
await Promise.all(allAdded.map(t => transfer.onInsert(t)));
|
||||||
|
|
||||||
// Return any updates from here
|
// Return any updates from here
|
||||||
resultUpdated = (await Promise.all(
|
resultUpdated = (
|
||||||
allUpdated.map(t => transfer.onUpdate(t))
|
await Promise.all(allUpdated.map(t => transfer.onUpdate(t)))
|
||||||
)).filter(Boolean);
|
).filter(Boolean);
|
||||||
|
|
||||||
await Promise.all(allDeleted.map(t => transfer.onDelete(t)));
|
await Promise.all(allDeleted.map(t => transfer.onDelete(t)));
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,24 +6,28 @@ async function getPayee(acct) {
|
||||||
|
|
||||||
async function getTransferredAccount(transaction) {
|
async function getTransferredAccount(transaction) {
|
||||||
if (transaction.payee) {
|
if (transaction.payee) {
|
||||||
let { transfer_acct, id } = await db.first(
|
let {
|
||||||
'SELECT id, transfer_acct FROM v_payees WHERE id = ?',
|
transfer_acct,
|
||||||
[transaction.payee]
|
id
|
||||||
);
|
} = await db.first('SELECT id, transfer_acct FROM v_payees WHERE id = ?', [
|
||||||
|
transaction.payee
|
||||||
|
]);
|
||||||
return transfer_acct;
|
return transfer_acct;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearCategory(transaction, transferAcct) {
|
async function clearCategory(transaction, transferAcct) {
|
||||||
const { offbudget: fromOffBudget } = await db.first(
|
const {
|
||||||
'SELECT offbudget FROM accounts WHERE id = ?',
|
offbudget: fromOffBudget
|
||||||
[transaction.account]
|
} = await db.first('SELECT offbudget FROM accounts WHERE id = ?', [
|
||||||
);
|
transaction.account
|
||||||
const { offbudget: toOffBudget } = await db.first(
|
]);
|
||||||
'SELECT offbudget FROM accounts WHERE id = ?',
|
const {
|
||||||
[transferAcct]
|
offbudget: toOffBudget
|
||||||
);
|
} = await db.first('SELECT offbudget FROM accounts WHERE id = ?', [
|
||||||
|
transferAcct
|
||||||
|
]);
|
||||||
|
|
||||||
// We should clear the category to make sure it's not being
|
// We should clear the category to make sure it's not being
|
||||||
// accounted for in the budget, unless it should be in the case of
|
// accounted for in the budget, unless it should be in the case of
|
||||||
|
@ -36,10 +40,11 @@ async function clearCategory(transaction, transferAcct) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addTransfer(transaction, transferredAccount) {
|
export async function addTransfer(transaction, transferredAccount) {
|
||||||
let { id: fromPayee } = await db.first(
|
let {
|
||||||
'SELECT id FROM payees WHERE transfer_acct = ?',
|
id: fromPayee
|
||||||
[transaction.account]
|
} = await db.first('SELECT id FROM payees WHERE transfer_acct = ?', [
|
||||||
);
|
transaction.account
|
||||||
|
]);
|
||||||
|
|
||||||
// We need to enforce certain constraints with child transaction transfers
|
// We need to enforce certain constraints with child transaction transfers
|
||||||
if (transaction.parent_id) {
|
if (transaction.parent_id) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { addTransactions } from './accounts/sync';
|
import { addTransactions } from './accounts/sync';
|
||||||
import {
|
import {
|
||||||
transactionModel,
|
|
||||||
accountModel,
|
accountModel,
|
||||||
categoryModel,
|
categoryModel,
|
||||||
categoryGroupModel,
|
categoryGroupModel,
|
||||||
|
@ -16,17 +15,14 @@ import * as db from './db';
|
||||||
import * as sheet from './sheet';
|
import * as sheet from './sheet';
|
||||||
import * as prefs from './prefs';
|
import * as prefs from './prefs';
|
||||||
import * as monthUtils from '../shared/months';
|
import * as monthUtils from '../shared/months';
|
||||||
import * as update from './update';
|
|
||||||
import * as cloudStorage from './cloud-storage';
|
import * as cloudStorage from './cloud-storage';
|
||||||
import { setSyncingMode, batchMessages } from './sync';
|
import { setSyncingMode, batchMessages } from './sync';
|
||||||
import { groupById, cleanUUID } from '../shared/util';
|
import { getClock } from './crdt';
|
||||||
import { getClock } from './timestamp';
|
|
||||||
import { runMutator } from './mutators';
|
import { runMutator } from './mutators';
|
||||||
import { integerToAmount } from '../shared/util';
|
import { integerToAmount } from '../shared/util';
|
||||||
import { runQuery as aqlQuery } from './aql/schema/run-query';
|
import { runQuery as aqlQuery } from './aql/schema/run-query';
|
||||||
import q from '../shared/query';
|
import q from '../shared/query';
|
||||||
|
|
||||||
const { resolveName } = require('./spreadsheet/util');
|
|
||||||
const connection = require('../platform/server/connection');
|
const connection = require('../platform/server/connection');
|
||||||
|
|
||||||
let IMPORT_MODE = false;
|
let IMPORT_MODE = false;
|
||||||
|
|
|
@ -81,28 +81,34 @@ describe('runQuery', () => {
|
||||||
expect(data[0].date).toBe('2020-01-04');
|
expect(data[0].date).toBe('2020-01-04');
|
||||||
|
|
||||||
// date-month
|
// date-month
|
||||||
data = (await runQuery(
|
data = (
|
||||||
query('transactions')
|
await runQuery(
|
||||||
.select({ month: { $month: '$date' } })
|
query('transactions')
|
||||||
.serialize()
|
.select({ month: { $month: '$date' } })
|
||||||
)).data;
|
.serialize()
|
||||||
|
)
|
||||||
|
).data;
|
||||||
expect(data[0].month).toBe('2020-01');
|
expect(data[0].month).toBe('2020-01');
|
||||||
|
|
||||||
// date-year
|
// date-year
|
||||||
data = (await runQuery(
|
data = (
|
||||||
query('transactions')
|
await runQuery(
|
||||||
.select({ year: { $year: '$date' } })
|
query('transactions')
|
||||||
.serialize()
|
.select({ year: { $year: '$date' } })
|
||||||
)).data;
|
.serialize()
|
||||||
|
)
|
||||||
|
).data;
|
||||||
expect(data[0].year).toBe('2020');
|
expect(data[0].year).toBe('2020');
|
||||||
|
|
||||||
// boolean
|
// boolean
|
||||||
data = (await runQuery(
|
data = (
|
||||||
query('transactions')
|
await runQuery(
|
||||||
.select(['is_child', 'is_parent'])
|
query('transactions')
|
||||||
.raw()
|
.select(['is_child', 'is_parent'])
|
||||||
.serialize()
|
.raw()
|
||||||
)).data;
|
.serialize()
|
||||||
|
)
|
||||||
|
).data;
|
||||||
expect(data[0].is_child).toBe(false);
|
expect(data[0].is_child).toBe(false);
|
||||||
expect(data[0].is_parent).toBe(true);
|
expect(data[0].is_parent).toBe(true);
|
||||||
expect(data[1].is_child).toBe(true);
|
expect(data[1].is_child).toBe(true);
|
||||||
|
@ -128,31 +134,37 @@ describe('runQuery', () => {
|
||||||
);
|
);
|
||||||
expect(data[0].id).toBe(transId);
|
expect(data[0].id).toBe(transId);
|
||||||
|
|
||||||
data = (await runQuery(
|
data = (
|
||||||
query('transactions')
|
await runQuery(
|
||||||
.filter({ date: { $transform: '$month', $eq: { $month: ':month' } } })
|
query('transactions')
|
||||||
.select('date')
|
.filter({ date: { $transform: '$month', $eq: { $month: ':month' } } })
|
||||||
.serialize(),
|
.select('date')
|
||||||
{ params: { month: '2020-01-02' } }
|
.serialize(),
|
||||||
)).data;
|
{ params: { month: '2020-01-02' } }
|
||||||
|
)
|
||||||
|
).data;
|
||||||
expect(data[0].id).toBe(transId);
|
expect(data[0].id).toBe(transId);
|
||||||
|
|
||||||
data = (await runQuery(
|
data = (
|
||||||
query('transactions')
|
await runQuery(
|
||||||
.filter({ date: { $transform: '$year', $eq: { $year: ':month' } } })
|
query('transactions')
|
||||||
.select('date')
|
.filter({ date: { $transform: '$year', $eq: { $year: ':month' } } })
|
||||||
.serialize(),
|
.select('date')
|
||||||
{ params: { month: '2020-01-02' } }
|
.serialize(),
|
||||||
)).data;
|
{ params: { month: '2020-01-02' } }
|
||||||
|
)
|
||||||
|
).data;
|
||||||
expect(data[0].id).toBe(transId);
|
expect(data[0].id).toBe(transId);
|
||||||
|
|
||||||
data = (await runQuery(
|
data = (
|
||||||
query('transactions')
|
await runQuery(
|
||||||
.filter({ cleared: ':cleared' })
|
query('transactions')
|
||||||
.select('date')
|
.filter({ cleared: ':cleared' })
|
||||||
.serialize(),
|
.select('date')
|
||||||
{ params: { cleared: true } }
|
.serialize(),
|
||||||
)).data;
|
{ params: { cleared: true } }
|
||||||
|
)
|
||||||
|
).data;
|
||||||
expect(data[0].id).toBe(transId);
|
expect(data[0].id).toBe(transId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as db from '../../db';
|
import * as db from '../../db';
|
||||||
import { whereIn } from '../../db/util';
|
import { whereIn } from '../../db/util';
|
||||||
import { groupBy } from '../../../shared/util';
|
|
||||||
import { isAggregateQuery } from '../compiler';
|
import { isAggregateQuery } from '../compiler';
|
||||||
import { convertOutputType } from '../schema-helpers';
|
import { convertOutputType } from '../schema-helpers';
|
||||||
import { execQuery } from '../exec';
|
import { execQuery } from '../exec';
|
||||||
|
@ -143,7 +142,10 @@ async function execTransactionsGrouped(
|
||||||
|
|
||||||
rows = await db.all(rowSql, params);
|
rows = await db.all(rowSql, params);
|
||||||
matched = new Set(
|
matched = new Set(
|
||||||
[].concat.apply([], rows.map(row => row.matched.split(',')))
|
[].concat.apply(
|
||||||
|
[],
|
||||||
|
rows.map(row => row.matched.split(','))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import fc from 'fast-check';
|
import fc from 'fast-check';
|
||||||
import * as db from '../../db';
|
import * as db from '../../db';
|
||||||
import query from '../../../shared/query';
|
import query from '../../../shared/query';
|
||||||
import { makeChild } from '../../../shared/transactions';
|
|
||||||
import { batchMessages, setSyncingMode } from '../../sync/index';
|
import { batchMessages, setSyncingMode } from '../../sync/index';
|
||||||
import { setClock } from '../../timestamp';
|
import { setClock } from '../../crdt';
|
||||||
import { groupById } from '../../../shared/util';
|
import { groupById } from '../../../shared/util';
|
||||||
import arbs from '../../../mocks/arbitrary-schema';
|
import arbs from '../../../mocks/arbitrary-schema';
|
||||||
import { isAggregateQuery } from '../compiler';
|
|
||||||
import { runQuery } from './run-query';
|
import { runQuery } from './run-query';
|
||||||
import { toGroup, isHappyPathQuery } from './executors';
|
import { isHappyPathQuery } from './executors';
|
||||||
|
|
||||||
const uuid = require('../../../platform/uuid');
|
|
||||||
|
|
||||||
beforeEach(global.emptyDatabase());
|
beforeEach(global.emptyDatabase());
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ describe('schema', () => {
|
||||||
test('never returns transactions without a date', async () => {
|
test('never returns transactions without a date', async () => {
|
||||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(0);
|
expect((await db.all('SELECT * FROM transactions')).length).toBe(0);
|
||||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||||
await db.runQuery('INSERT INTO transactions (acct) VALUES (?)', ["foo"]);
|
await db.runQuery('INSERT INTO transactions (acct) VALUES (?)', ['foo']);
|
||||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(1);
|
expect((await db.all('SELECT * FROM transactions')).length).toBe(1);
|
||||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||||
});
|
});
|
||||||
|
@ -28,7 +28,7 @@ describe('schema', () => {
|
||||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||||
await db.runQuery(
|
await db.runQuery(
|
||||||
'INSERT INTO transactions (date, acct, isChild) VALUES (?, ?, ?)',
|
'INSERT INTO transactions (date, acct, isChild) VALUES (?, ?, ?)',
|
||||||
[20200101, "foo", 1]
|
[20200101, 'foo', 1]
|
||||||
);
|
);
|
||||||
expect((await db.all('SELECT * FROM transactions')).length).toBe(1);
|
expect((await db.all('SELECT * FROM transactions')).length).toBe(1);
|
||||||
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
expect((await db.all('SELECT * FROM v_transactions')).length).toBe(0);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import * as monthUtils from '../../shared/months';
|
import * as monthUtils from '../../shared/months';
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
import { batchMessages } from '../sync';
|
import { batchMessages } from '../sync';
|
||||||
import { groupBySingle } from '../../shared/util';
|
|
||||||
import * as prefs from '../prefs';
|
import * as prefs from '../prefs';
|
||||||
import * as sheet from '../sheet';
|
import * as sheet from '../sheet';
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { mutator } from '../mutators';
|
||||||
import { undoable } from '../undo';
|
import { undoable } from '../undo';
|
||||||
import * as actions from './actions';
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
|
||||||
let app = createApp();
|
let app = createApp();
|
||||||
|
|
||||||
app.method('budget/budget-amount', mutator(undoable(actions.setBudget)));
|
app.method('budget/budget-amount', mutator(undoable(actions.setBudget)));
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as sheet from '../sheet';
|
import * as sheet from '../sheet';
|
||||||
import * as monthUtils from '../../shared/months';
|
|
||||||
import { number, sumAmounts } from './util';
|
import { number, sumAmounts } from './util';
|
||||||
const { resolveName } = require('../spreadsheet/util');
|
const { resolveName } = require('../spreadsheet/util');
|
||||||
|
|
||||||
|
|
|
@ -19,4 +19,3 @@ export function unflatten2(arr) {
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
packages/loot-core/src/server/crdt/index.js
Normal file
12
packages/loot-core/src/server/crdt/index.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import * as merkle from './merkle';
|
||||||
|
|
||||||
|
export { merkle };
|
||||||
|
export {
|
||||||
|
getClock,
|
||||||
|
setClock,
|
||||||
|
makeClock,
|
||||||
|
makeClientId,
|
||||||
|
serializeClock,
|
||||||
|
deserializeClock,
|
||||||
|
Timestamp
|
||||||
|
} from './timestamp';
|
|
@ -1,5 +1,5 @@
|
||||||
import * as merkle from './merkle';
|
import * as merkle from './merkle';
|
||||||
import Timestamp from './timestamp';
|
import { Timestamp } from './timestamp';
|
||||||
|
|
||||||
function pretty(n) {
|
function pretty(n) {
|
||||||
if (n < 60) {
|
if (n < 60) {
|
|
@ -1,5 +1,5 @@
|
||||||
import murmurhash from 'murmurhash';
|
import murmurhash from 'murmurhash';
|
||||||
const uuid = require('../platform/uuid');
|
const uuid = require('../../platform/uuid');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hybrid Unique Logical Clock (HULC) timestamp generator
|
* Hybrid Unique Logical Clock (HULC) timestamp generator
|
||||||
|
@ -81,7 +81,7 @@ const MAX_NODE_LENGTH = 16;
|
||||||
/**
|
/**
|
||||||
* timestamp instance class
|
* timestamp instance class
|
||||||
*/
|
*/
|
||||||
export default class Timestamp {
|
export class Timestamp {
|
||||||
constructor(millis, counter, node) {
|
constructor(millis, counter, node) {
|
||||||
this._state = {
|
this._state = {
|
||||||
millis: millis,
|
millis: millis,
|
||||||
|
@ -124,7 +124,7 @@ export default class Timestamp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MutableTimestamp extends Timestamp {
|
class MutableTimestamp extends Timestamp {
|
||||||
setMillis(n) {
|
setMillis(n) {
|
||||||
this._state.millis = n;
|
this._state.millis = n;
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import Timestamp, { setClock, makeClock } from './timestamp';
|
import { Timestamp } from './timestamp';
|
||||||
|
|
||||||
describe('Timestamp', function() {
|
describe('Timestamp', function() {
|
||||||
let now = 0;
|
let now = 0;
|
|
@ -11,13 +11,14 @@ import {
|
||||||
payeeRuleModel
|
payeeRuleModel
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { groupById } from '../../shared/util';
|
import { groupById } from '../../shared/util';
|
||||||
import Timestamp, {
|
import {
|
||||||
makeClock,
|
makeClock,
|
||||||
setClock,
|
setClock,
|
||||||
serializeClock,
|
serializeClock,
|
||||||
deserializeClock,
|
deserializeClock,
|
||||||
makeClientId
|
makeClientId,
|
||||||
} from '../timestamp';
|
Timestamp
|
||||||
|
} from '../crdt';
|
||||||
import {
|
import {
|
||||||
convertForInsert,
|
convertForInsert,
|
||||||
convertForUpdate,
|
convertForUpdate,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import './polyfills';
|
import './polyfills';
|
||||||
import { differenceInDays } from 'date-fns';
|
|
||||||
import asyncStorage from '../platform/server/asyncStorage';
|
import asyncStorage from '../platform/server/asyncStorage';
|
||||||
import { captureException, captureBreadcrumb } from '../platform/exceptions';
|
import { captureException, captureBreadcrumb } from '../platform/exceptions';
|
||||||
import * as prefs from './prefs';
|
import * as prefs from './prefs';
|
||||||
|
@ -31,8 +30,16 @@ import * as bankSync from './accounts/sync';
|
||||||
import * as link from './accounts/link';
|
import * as link from './accounts/link';
|
||||||
import { uniqueFileName, idFromFileName } from './util/budget-name';
|
import { uniqueFileName, idFromFileName } from './util/budget-name';
|
||||||
import { mutator, runHandler } from './mutators';
|
import { mutator, runHandler } from './mutators';
|
||||||
import * as timestamp from './timestamp';
|
import {
|
||||||
import * as merkle from './merkle';
|
getClock,
|
||||||
|
setClock,
|
||||||
|
makeClock,
|
||||||
|
makeClientId,
|
||||||
|
serializeClock,
|
||||||
|
deserializeClock,
|
||||||
|
Timestamp,
|
||||||
|
merkle
|
||||||
|
} from './crdt';
|
||||||
import {
|
import {
|
||||||
initialFullSync,
|
initialFullSync,
|
||||||
fullSync,
|
fullSync,
|
||||||
|
@ -416,7 +423,10 @@ handlers['category-group-delete'] = mutator(async function({ id, transferId }) {
|
||||||
|
|
||||||
return batchMessages(async () => {
|
return batchMessages(async () => {
|
||||||
if (transferId) {
|
if (transferId) {
|
||||||
await budget.doTransfer(groupCategories.map(c => c.id), transferId);
|
await budget.doTransfer(
|
||||||
|
groupCategories.map(c => c.id),
|
||||||
|
transferId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await db.deleteCategoryGroup({ id }, transferId);
|
await db.deleteCategoryGroup({ id }, transferId);
|
||||||
});
|
});
|
||||||
|
@ -761,11 +771,15 @@ handlers['accounts-get'] = async function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
handlers['account-properties'] = async function({ id }) {
|
handlers['account-properties'] = async function({ id }) {
|
||||||
const { balance } = await db.first(
|
const {
|
||||||
|
balance
|
||||||
|
} = await db.first(
|
||||||
'SELECT sum(amount) as balance FROM transactions WHERE acct = ? AND isParent = 0 AND tombstone = 0',
|
'SELECT sum(amount) as balance FROM transactions WHERE acct = ? AND isParent = 0 AND tombstone = 0',
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
const { count } = await db.first(
|
const {
|
||||||
|
count
|
||||||
|
} = await db.first(
|
||||||
'SELECT count(id) as count FROM transactions WHERE acct = ? AND tombstone = 0',
|
'SELECT count(id) as count FROM transactions WHERE acct = ? AND tombstone = 0',
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
@ -904,10 +918,9 @@ handlers['account-close'] = mutator(async function({
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
let { id: payeeId } = await db.first(
|
let {
|
||||||
'SELECT id FROM payees WHERE transfer_acct = ?',
|
id: payeeId
|
||||||
[id]
|
} = await db.first('SELECT id FROM payees WHERE transfer_acct = ?', [id]);
|
||||||
);
|
|
||||||
|
|
||||||
await batchMessages(() => {
|
await batchMessages(() => {
|
||||||
// TODO: what this should really do is send a special message that
|
// TODO: what this should really do is send a special message that
|
||||||
|
@ -941,10 +954,11 @@ handlers['account-close'] = mutator(async function({
|
||||||
// If there is a balance we need to transfer it to the specified
|
// If there is a balance we need to transfer it to the specified
|
||||||
// account (and possibly categorize it)
|
// account (and possibly categorize it)
|
||||||
if (balance !== 0) {
|
if (balance !== 0) {
|
||||||
let { id: payeeId } = await db.first(
|
let {
|
||||||
'SELECT id FROM payees WHERE transfer_acct = ?',
|
id: payeeId
|
||||||
[transferAccountId]
|
} = await db.first('SELECT id FROM payees WHERE transfer_acct = ?', [
|
||||||
);
|
transferAccountId
|
||||||
|
]);
|
||||||
|
|
||||||
await handlers['transaction-add']({
|
await handlers['transaction-add']({
|
||||||
id: uuid.v4Sync(),
|
id: uuid.v4Sync(),
|
||||||
|
@ -1084,9 +1098,7 @@ handlers['accounts-sync'] = async function({ id }) {
|
||||||
} else if (err instanceof PostError && err.reason !== 'internal') {
|
} else if (err instanceof PostError && err.reason !== 'internal') {
|
||||||
errors.push({
|
errors.push({
|
||||||
accountId: acct.id,
|
accountId: acct.id,
|
||||||
message: `Account "${
|
message: `Account "${acct.name}" is not linked properly. Please link it again`
|
||||||
acct.name
|
|
||||||
}" is not linked properly. Please link it again`
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
errors.push({
|
errors.push({
|
||||||
|
@ -1136,10 +1148,9 @@ handlers['transactions-import'] = mutator(function({
|
||||||
});
|
});
|
||||||
|
|
||||||
handlers['account-unlink'] = mutator(async function({ id }) {
|
handlers['account-unlink'] = mutator(async function({ id }) {
|
||||||
let { bank: bankId } = await db.first(
|
let {
|
||||||
'SELECT bank FROM accounts WHERE id = ?',
|
bank: bankId
|
||||||
[id]
|
} = await db.first('SELECT bank FROM accounts WHERE id = ?', [id]);
|
||||||
);
|
|
||||||
|
|
||||||
if (!bankId) {
|
if (!bankId) {
|
||||||
return 'ok';
|
return 'ok';
|
||||||
|
@ -1154,10 +1165,11 @@ handlers['account-unlink'] = mutator(async function({ id }) {
|
||||||
balance_limit: null
|
balance_limit: null
|
||||||
});
|
});
|
||||||
|
|
||||||
let { count } = await db.first(
|
let {
|
||||||
'SELECT COUNT(*) as count FROM accounts WHERE bank = ?',
|
count
|
||||||
[bankId]
|
} = await db.first('SELECT COUNT(*) as count FROM accounts WHERE bank = ?', [
|
||||||
);
|
bankId
|
||||||
|
]);
|
||||||
|
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
// No more accounts are associated with this bank. We can remove
|
// No more accounts are associated with this bank. We can remove
|
||||||
|
@ -1547,35 +1559,37 @@ handlers['get-version'] = async function() {
|
||||||
|
|
||||||
handlers['get-budgets'] = async function() {
|
handlers['get-budgets'] = async function() {
|
||||||
const paths = await fs.listDir(fs.getDocumentDir());
|
const paths = await fs.listDir(fs.getDocumentDir());
|
||||||
const budgets = (await Promise.all(
|
const budgets = (
|
||||||
paths.map(async name => {
|
await Promise.all(
|
||||||
const prefsPath = fs.join(fs.getDocumentDir(), name, 'metadata.json');
|
paths.map(async name => {
|
||||||
if (await fs.exists(prefsPath)) {
|
const prefsPath = fs.join(fs.getDocumentDir(), name, 'metadata.json');
|
||||||
let prefs;
|
if (await fs.exists(prefsPath)) {
|
||||||
try {
|
let prefs;
|
||||||
prefs = JSON.parse(await fs.readFile(prefsPath));
|
try {
|
||||||
} catch (e) {
|
prefs = JSON.parse(await fs.readFile(prefsPath));
|
||||||
console.log('Error parsing metadata:', e.stack);
|
} catch (e) {
|
||||||
return;
|
console.log('Error parsing metadata:', e.stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We treat the directory name as the canonical id so that if
|
||||||
|
// the user moves it around/renames/etc, nothing breaks. The
|
||||||
|
// id is stored in prefs just for convenience (and the prefs
|
||||||
|
// will always update to the latest given id)
|
||||||
|
if (name !== DEMO_BUDGET_ID) {
|
||||||
|
return {
|
||||||
|
id: name,
|
||||||
|
cloudFileId: prefs.cloudFileId,
|
||||||
|
groupId: prefs.groupId,
|
||||||
|
name: prefs.budgetName || '(no name)'
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We treat the directory name as the canonical id so that if
|
return null;
|
||||||
// the user moves it around/renames/etc, nothing breaks. The
|
})
|
||||||
// id is stored in prefs just for convenience (and the prefs
|
)
|
||||||
// will always update to the latest given id)
|
).filter(x => x);
|
||||||
if (name !== DEMO_BUDGET_ID) {
|
|
||||||
return {
|
|
||||||
id: name,
|
|
||||||
cloudFileId: prefs.cloudFileId,
|
|
||||||
groupId: prefs.groupId,
|
|
||||||
name: prefs.budgetName || '(no name)'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
)).filter(x => x);
|
|
||||||
|
|
||||||
return budgets;
|
return budgets;
|
||||||
};
|
};
|
||||||
|
@ -1969,10 +1983,10 @@ async function loadBudget(id, appVersion, { showUpdate } = {}) {
|
||||||
//
|
//
|
||||||
// TODO: The client id should be stored elsewhere. It shouldn't
|
// TODO: The client id should be stored elsewhere. It shouldn't
|
||||||
// work this way, but it's fine for now.
|
// work this way, but it's fine for now.
|
||||||
timestamp.getClock().timestamp.setNode(timestamp.makeClientId());
|
getClock().timestamp.setNode(makeClientId());
|
||||||
await db.runQuery(
|
await db.runQuery(
|
||||||
'INSERT OR REPLACE INTO messages_clock (id, clock) VALUES (1, ?)',
|
'INSERT OR REPLACE INTO messages_clock (id, clock) VALUES (1, ?)',
|
||||||
[timestamp.serializeClock(timestamp.getClock())]
|
[serializeClock(getClock())]
|
||||||
);
|
);
|
||||||
|
|
||||||
await prefs.savePrefs({ resetClock: false });
|
await prefs.savePrefs({ resetClock: false });
|
||||||
|
@ -2254,7 +2268,15 @@ export const lib = {
|
||||||
|
|
||||||
// Expose CRDT mechanisms so server can use them
|
// Expose CRDT mechanisms so server can use them
|
||||||
merkle,
|
merkle,
|
||||||
timestamp,
|
timestamp: {
|
||||||
|
getClock,
|
||||||
|
setClock,
|
||||||
|
makeClock,
|
||||||
|
makeClientId,
|
||||||
|
serializeClock,
|
||||||
|
deserializeClock,
|
||||||
|
Timestamp
|
||||||
|
},
|
||||||
SyncProtoBuf: SyncPb
|
SyncProtoBuf: SyncPb
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as prefs from './prefs';
|
||||||
import * as db from './db';
|
import * as db from './db';
|
||||||
import * as budget from './budget/base';
|
import * as budget from './budget/base';
|
||||||
import * as monthUtils from '../shared/months';
|
import * as monthUtils from '../shared/months';
|
||||||
import { getClock, deserializeClock } from './timestamp';
|
import { getClock, deserializeClock } from './crdt';
|
||||||
import {
|
import {
|
||||||
runHandler,
|
runHandler,
|
||||||
runMutator,
|
runMutator,
|
||||||
|
|
|
@ -3,8 +3,7 @@ import {
|
||||||
withMigrationsDir,
|
withMigrationsDir,
|
||||||
getAppliedMigrations,
|
getAppliedMigrations,
|
||||||
getMigrationList,
|
getMigrationList,
|
||||||
getPending,
|
getPending
|
||||||
applyMigration
|
|
||||||
} from './migrations';
|
} from './migrations';
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
// Mobile needs this
|
// Mobile needs this
|
||||||
import 'core-js/modules/es.object.from-entries'
|
import 'core-js/modules/es.object.from-entries';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Platform from './platform';
|
import Platform from './platform';
|
||||||
const { PostError, HTTPError } = require('./errors');
|
const { PostError } = require('./errors');
|
||||||
const { fetch } = require('../platform/server/fetch');
|
const { fetch } = require('../platform/server/fetch');
|
||||||
|
|
||||||
function throwIfNot200(res, text) {
|
function throwIfNot200(res, text) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { sendMessages } from './sync';
|
import { sendMessages } from './sync';
|
||||||
import Timestamp from './timestamp';
|
import { Timestamp } from './crdt';
|
||||||
const fs = require('../platform/server/fs');
|
const fs = require('../platform/server/fs');
|
||||||
|
|
||||||
let prefs = null;
|
let prefs = null;
|
||||||
|
|
|
@ -5,8 +5,6 @@ import * as db from '../db';
|
||||||
import * as prefs from '../prefs';
|
import * as prefs from '../prefs';
|
||||||
import { toDateRepr } from '../models';
|
import { toDateRepr } from '../models';
|
||||||
import { runQuery as aqlQuery } from '../aql/schema/run-query';
|
import { runQuery as aqlQuery } from '../aql/schema/run-query';
|
||||||
import { compileQuery } from '../aql/compiler';
|
|
||||||
import { schema, schemaConfig } from '../aql/schema';
|
|
||||||
import { dayFromDate, currentDay, parseDate } from '../../shared/months';
|
import { dayFromDate, currentDay, parseDate } from '../../shared/months';
|
||||||
import q from '../../shared/query';
|
import q from '../../shared/query';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import * as db from '../db';
|
|
||||||
import { runQuery as aqlQuery } from '../aql/schema/run-query';
|
import { runQuery as aqlQuery } from '../aql/schema/run-query';
|
||||||
import q from '../../shared/query';
|
import q from '../../shared/query';
|
||||||
import { loadRules, updateRule } from '../accounts/transaction-rules';
|
import { loadRules, updateRule } from '../accounts/transaction-rules';
|
||||||
|
@ -6,7 +5,6 @@ import { loadMappings } from '../db/mappings';
|
||||||
import {
|
import {
|
||||||
updateConditions,
|
updateConditions,
|
||||||
getNextDate,
|
getNextDate,
|
||||||
listSchedules,
|
|
||||||
createSchedule,
|
createSchedule,
|
||||||
updateSchedule,
|
updateSchedule,
|
||||||
deleteSchedule,
|
deleteSchedule,
|
||||||
|
@ -73,7 +71,10 @@ describe('schedule app', () => {
|
||||||
value: {
|
value: {
|
||||||
start: '2020-12-20',
|
start: '2020-12-20',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 15 }, { type: 'day', value: 30 }]
|
patterns: [
|
||||||
|
{ type: 'day', value: 15 },
|
||||||
|
{ type: 'day', value: 30 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
).toBe('2021-05-30');
|
).toBe('2021-05-30');
|
||||||
|
@ -90,7 +91,10 @@ describe('schedule app', () => {
|
||||||
value: {
|
value: {
|
||||||
start: '2020-12-20',
|
start: '2020-12-20',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 15 }, { type: 'day', value: 30 }]
|
patterns: [
|
||||||
|
{ type: 'day', value: 15 },
|
||||||
|
{ type: 'day', value: 30 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -125,7 +129,10 @@ describe('schedule app', () => {
|
||||||
value: {
|
value: {
|
||||||
start: '2020-12-20',
|
start: '2020-12-20',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 15 }, { type: 'day', value: 30 }]
|
patterns: [
|
||||||
|
{ type: 'day', value: 15 },
|
||||||
|
{ type: 'day', value: 30 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -152,7 +159,10 @@ describe('schedule app', () => {
|
||||||
value: {
|
value: {
|
||||||
start: '2020-12-20',
|
start: '2020-12-20',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 18 }, { type: 'day', value: 29 }]
|
patterns: [
|
||||||
|
{ type: 'day', value: 18 },
|
||||||
|
{ type: 'day', value: 29 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -179,7 +189,10 @@ describe('schedule app', () => {
|
||||||
value: {
|
value: {
|
||||||
start: '2020-12-20',
|
start: '2020-12-20',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 15 }, { type: 'day', value: 30 }]
|
patterns: [
|
||||||
|
{ type: 'day', value: 15 },
|
||||||
|
{ type: 'day', value: 30 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -202,7 +215,10 @@ describe('schedule app', () => {
|
||||||
value: {
|
value: {
|
||||||
start: '2020-12-20',
|
start: '2020-12-20',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 15 }, { type: 'day', value: 30 }]
|
patterns: [
|
||||||
|
{ type: 'day', value: 15 },
|
||||||
|
{ type: 'day', value: 30 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -224,7 +240,10 @@ describe('schedule app', () => {
|
||||||
value: {
|
value: {
|
||||||
start: '2020-12-20',
|
start: '2020-12-20',
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: 'day', value: 18 }, { type: 'day', value: 28 }]
|
patterns: [
|
||||||
|
{ type: 'day', value: 18 },
|
||||||
|
{ type: 'day', value: 28 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { runQuery as aqlQuery } from '../aql/schema/run-query';
|
||||||
import q from '../../shared/query';
|
import q from '../../shared/query';
|
||||||
import { getApproxNumberThreshold } from '../../shared/rules';
|
import { getApproxNumberThreshold } from '../../shared/rules';
|
||||||
import { recurConfigToRSchedule } from '../../shared/schedules';
|
import { recurConfigToRSchedule } from '../../shared/schedules';
|
||||||
import { dayFromDate, parseDate, subDays } from '../../shared/months';
|
import { dayFromDate, parseDate } from '../../shared/months';
|
||||||
import { conditionsToAQL } from '../accounts/transaction-rules';
|
import { conditionsToAQL } from '../accounts/transaction-rules';
|
||||||
const uuid = require('../../platform/uuid');
|
const uuid = require('../../platform/uuid');
|
||||||
|
|
||||||
|
@ -219,7 +219,10 @@ async function monthly1stor3rd(startDate, accountId) {
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: dayValue, value: 1 }, { type: dayValue, value: 3 }]
|
patterns: [
|
||||||
|
{ type: dayValue, value: 1 },
|
||||||
|
{ type: dayValue, value: 3 }
|
||||||
|
]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
accountId
|
accountId
|
||||||
|
@ -237,7 +240,10 @@ async function monthly2ndor4th(startDate, accountId) {
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
frequency: 'monthly',
|
frequency: 'monthly',
|
||||||
patterns: [{ type: dayValue, value: 2 }, { type: dayValue, value: 4 }]
|
patterns: [
|
||||||
|
{ type: dayValue, value: 2 },
|
||||||
|
{ type: dayValue, value: 4 }
|
||||||
|
]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
accountId
|
accountId
|
||||||
|
|
|
@ -363,7 +363,7 @@ function parsePostfix(state, node) {
|
||||||
while ((tok = nextToken(state))) {
|
while ((tok = nextToken(state))) {
|
||||||
if (tok.type === types.TOKEN_LEFT_PAREN) {
|
if (tok.type === types.TOKEN_LEFT_PAREN) {
|
||||||
pushToken(state, tok);
|
pushToken(state, tok);
|
||||||
let args = parseArgs(state)
|
let args = parseArgs(state);
|
||||||
node = new nodes.FunCall(tok.lineno, tok.colno, node, args);
|
node = new nodes.FunCall(tok.lineno, tok.colno, node, args);
|
||||||
} else if (tok.type === types.TOKEN_DOT) {
|
} else if (tok.type === types.TOKEN_DOT) {
|
||||||
const val = nextToken(state);
|
const val = nextToken(state);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as nodes from './nodes';
|
import * as nodes from './nodes';
|
||||||
const uuid = require('../../../platform/uuid');
|
|
||||||
|
|
||||||
let _uid = 0;
|
let _uid = 0;
|
||||||
function resetUid() {
|
function resetUid() {
|
||||||
|
@ -239,9 +238,7 @@ export default function generate(table, where, groupby, select, deps) {
|
||||||
joins.push(meta.sql(lookup.tableId));
|
joins.push(meta.sql(lookup.tableId));
|
||||||
} else {
|
} else {
|
||||||
joins.push(
|
joins.push(
|
||||||
`LEFT JOIN ${meta.table} ${lookup.tableId} ON ${
|
`LEFT JOIN ${meta.table} ${lookup.tableId} ON ${lookup.tableId}.id = ${currentTable.id}.${lookup.field}`
|
||||||
lookup.tableId
|
|
||||||
}.id = ${currentTable.id}.${lookup.field}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { compile, compileBinding } from './compiler';
|
import { compile } from './compiler';
|
||||||
import { MOV, CALL, QUERY, UOP, BOP, REG1, SP, VAR, JUMPF, JUMPT } from './ops';
|
import { MOV, CALL, QUERY, UOP, BOP, JUMPF, JUMPT } from './ops';
|
||||||
|
|
||||||
export default class VM {
|
export default class VM {
|
||||||
constructor(db, scopes) {
|
constructor(db, scopes) {
|
||||||
|
@ -74,7 +74,10 @@ export default class VM {
|
||||||
|
|
||||||
call(callee, args) {
|
call(callee, args) {
|
||||||
const func = this.get(callee);
|
const func = this.get(callee);
|
||||||
this.reg1 = func.apply(null, args.map(arg => this.get(arg)));
|
this.reg1 = func.apply(
|
||||||
|
null,
|
||||||
|
args.map(arg => this.get(arg))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
query(sql, calculated) {
|
query(sql, calculated) {
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
|
const expect = require('expect');
|
||||||
|
const propagate = require('../data-compute/propagate.js');
|
||||||
|
|
||||||
|
describe('data propagation', () => {
|
||||||
const expect = require("expect");
|
it('should work', () => {
|
||||||
const propagate = require("../data-compute/propagate.js");
|
|
||||||
|
|
||||||
describe("data propagation", () => {
|
|
||||||
it("should work", () => {
|
|
||||||
expect(true).toExist();
|
expect(true).toExist();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,308 +1,304 @@
|
||||||
|
|
||||||
// Unit tests for reactive-property.
|
// Unit tests for reactive-property.
|
||||||
var assert = require("assert");
|
var assert = require('assert');
|
||||||
|
|
||||||
// If using from the NPM package, this line would be
|
// If using from the NPM package, this line would be
|
||||||
// var Graph = require("graph-data-structure");
|
// var Graph = require("graph-data-structure");
|
||||||
var Graph = require("../data-compute/graph-data-structure");
|
var Graph = require('../data-compute/graph-data-structure');
|
||||||
|
|
||||||
describe("Graph", function() {
|
describe('Graph', function() {
|
||||||
describe("Data structure", function() {
|
describe('Data structure', function() {
|
||||||
it("Should add nodes and list them.", function (){
|
it('Should add nodes and list them.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addNode("a");
|
graph.addNode('a');
|
||||||
graph.addNode("b");
|
graph.addNode('b');
|
||||||
assert.equal(graph.nodes().length, 2);
|
assert.equal(graph.nodes().length, 2);
|
||||||
assert(contains(graph.nodes(), "a"));
|
assert(contains(graph.nodes(), 'a'));
|
||||||
assert(contains(graph.nodes(), "b"));
|
assert(contains(graph.nodes(), 'b'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should chain addNode.", function (){
|
it('Should chain addNode.', function() {
|
||||||
var graph = Graph().addNode("a").addNode("b");
|
var graph = Graph()
|
||||||
|
.addNode('a')
|
||||||
|
.addNode('b');
|
||||||
assert.equal(graph.nodes().length, 2);
|
assert.equal(graph.nodes().length, 2);
|
||||||
assert(contains(graph.nodes(), "a"));
|
assert(contains(graph.nodes(), 'a'));
|
||||||
assert(contains(graph.nodes(), "b"));
|
assert(contains(graph.nodes(), 'b'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should remove nodes.", function (){
|
it('Should remove nodes.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addNode("a");
|
graph.addNode('a');
|
||||||
graph.addNode("b");
|
graph.addNode('b');
|
||||||
graph.removeNode("a");
|
graph.removeNode('a');
|
||||||
graph.removeNode("b");
|
graph.removeNode('b');
|
||||||
assert.equal(graph.nodes().length, 0);
|
assert.equal(graph.nodes().length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should chain removeNode.", function (){
|
it('Should chain removeNode.', function() {
|
||||||
var graph = Graph()
|
var graph = Graph()
|
||||||
.addNode("a")
|
.addNode('a')
|
||||||
.addNode("b")
|
.addNode('b')
|
||||||
.removeNode("a")
|
.removeNode('a')
|
||||||
.removeNode("b");
|
.removeNode('b');
|
||||||
assert.equal(graph.nodes().length, 0);
|
assert.equal(graph.nodes().length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should add edges and query for adjacent nodes.", function (){
|
it('Should add edges and query for adjacent nodes.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addNode("a");
|
graph.addNode('a');
|
||||||
graph.addNode("b");
|
graph.addNode('b');
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
assert.equal(graph.adjacent("a").length, 1);
|
assert.equal(graph.adjacent('a').length, 1);
|
||||||
assert.equal(graph.adjacent("a")[0], "b");
|
assert.equal(graph.adjacent('a')[0], 'b');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should implicitly add nodes when edges are added.", function (){
|
it('Should implicitly add nodes when edges are added.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
assert.equal(graph.adjacent("a").length, 1);
|
assert.equal(graph.adjacent('a').length, 1);
|
||||||
assert.equal(graph.adjacent("a")[0], "b");
|
assert.equal(graph.adjacent('a')[0], 'b');
|
||||||
assert.equal(graph.nodes().length, 2);
|
assert.equal(graph.nodes().length, 2);
|
||||||
assert(contains(graph.nodes(), "a"));
|
assert(contains(graph.nodes(), 'a'));
|
||||||
assert(contains(graph.nodes(), "b"));
|
assert(contains(graph.nodes(), 'b'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should chain addEdge.", function (){
|
it('Should chain addEdge.', function() {
|
||||||
var graph = Graph().addEdge("a", "b");
|
var graph = Graph().addEdge('a', 'b');
|
||||||
assert.equal(graph.adjacent("a").length, 1);
|
assert.equal(graph.adjacent('a').length, 1);
|
||||||
assert.equal(graph.adjacent("a")[0], "b");
|
assert.equal(graph.adjacent('a')[0], 'b');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should remove edges.", function (){
|
it('Should remove edges.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
graph.removeEdge("a", "b");
|
graph.removeEdge('a', 'b');
|
||||||
assert.equal(graph.adjacent("a").length, 0);
|
assert.equal(graph.adjacent('a').length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should chain removeEdge.", function (){
|
it('Should chain removeEdge.', function() {
|
||||||
var graph = Graph()
|
var graph = Graph()
|
||||||
.addEdge("a", "b")
|
.addEdge('a', 'b')
|
||||||
.removeEdge("a", "b");
|
.removeEdge('a', 'b');
|
||||||
assert.equal(graph.adjacent("a").length, 0);
|
assert.equal(graph.adjacent('a').length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should not remove nodes when edges are removed.", function (){
|
it('Should not remove nodes when edges are removed.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
graph.removeEdge("a", "b");
|
graph.removeEdge('a', 'b');
|
||||||
assert.equal(graph.nodes().length, 2);
|
assert.equal(graph.nodes().length, 2);
|
||||||
assert(contains(graph.nodes(), "a"));
|
assert(contains(graph.nodes(), 'a'));
|
||||||
assert(contains(graph.nodes(), "b"));
|
assert(contains(graph.nodes(), 'b'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should remove outgoing edges when a node is removed.", function (){
|
it('Should remove outgoing edges when a node is removed.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
graph.removeNode("a");
|
graph.removeNode('a');
|
||||||
assert.equal(graph.adjacent("a").length, 0);
|
assert.equal(graph.adjacent('a').length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should remove incoming edges when a node is removed.", function (){
|
it('Should remove incoming edges when a node is removed.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
graph.removeNode("b");
|
graph.removeNode('b');
|
||||||
assert.equal(graph.adjacent("a").length, 0);
|
assert.equal(graph.adjacent('a').length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should compute indegree.", function (){
|
it('Should compute indegree.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
assert.equal(graph.indegree("a"), 0);
|
assert.equal(graph.indegree('a'), 0);
|
||||||
assert.equal(graph.indegree("b"), 1);
|
assert.equal(graph.indegree('b'), 1);
|
||||||
|
|
||||||
graph.addEdge("c", "b");
|
graph.addEdge('c', 'b');
|
||||||
assert.equal(graph.indegree("b"), 2);
|
assert.equal(graph.indegree('b'), 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should compute outdegree.", function (){
|
it('Should compute outdegree.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
assert.equal(graph.outdegree("a"), 1);
|
assert.equal(graph.outdegree('a'), 1);
|
||||||
assert.equal(graph.outdegree("b"), 0);
|
assert.equal(graph.outdegree('b'), 0);
|
||||||
|
|
||||||
graph.addEdge("a", "c");
|
graph.addEdge('a', 'c');
|
||||||
assert.equal(graph.outdegree("a"), 2);
|
assert.equal(graph.outdegree('a'), 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Algorithms", function() {
|
describe('Algorithms', function() {
|
||||||
|
|
||||||
// This example is from Cormen et al. "Introduction to Algorithms" page 550
|
// This example is from Cormen et al. "Introduction to Algorithms" page 550
|
||||||
it("Should compute topological sort.", function (){
|
it('Should compute topological sort.', function() {
|
||||||
|
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
|
|
||||||
// Shoes depend on socks.
|
// Shoes depend on socks.
|
||||||
// Socks need to be put on before shoes.
|
// Socks need to be put on before shoes.
|
||||||
graph.addEdge("socks", "shoes");
|
graph.addEdge('socks', 'shoes');
|
||||||
|
|
||||||
graph.addEdge("shirt", "belt");
|
graph.addEdge('shirt', 'belt');
|
||||||
graph.addEdge("shirt", "tie");
|
graph.addEdge('shirt', 'tie');
|
||||||
graph.addEdge("tie", "jacket");
|
graph.addEdge('tie', 'jacket');
|
||||||
graph.addEdge("belt", "jacket");
|
graph.addEdge('belt', 'jacket');
|
||||||
graph.addEdge("pants", "shoes");
|
graph.addEdge('pants', 'shoes');
|
||||||
graph.addEdge("underpants", "pants");
|
graph.addEdge('underpants', 'pants');
|
||||||
graph.addEdge("pants", "belt");
|
graph.addEdge('pants', 'belt');
|
||||||
|
|
||||||
var sorted = graph.topologicalSort();
|
var sorted = graph.topologicalSort();
|
||||||
|
|
||||||
assert(comesBefore(sorted, "pants", "shoes"));
|
assert(comesBefore(sorted, 'pants', 'shoes'));
|
||||||
assert(comesBefore(sorted, "underpants", "pants"));
|
assert(comesBefore(sorted, 'underpants', 'pants'));
|
||||||
assert(comesBefore(sorted, "underpants", "shoes"));
|
assert(comesBefore(sorted, 'underpants', 'shoes'));
|
||||||
assert(comesBefore(sorted, "shirt", "jacket"));
|
assert(comesBefore(sorted, 'shirt', 'jacket'));
|
||||||
assert(comesBefore(sorted, "shirt", "belt"));
|
assert(comesBefore(sorted, 'shirt', 'belt'));
|
||||||
assert(comesBefore(sorted, "belt", "jacket"));
|
assert(comesBefore(sorted, 'belt', 'jacket'));
|
||||||
|
|
||||||
assert.equal(sorted.length, 8);
|
assert.equal(sorted.length, 8);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should compute topological sort, excluding source nodes.", function (){
|
it('Should compute topological sort, excluding source nodes.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.addEdge("a", "b");
|
graph.addEdge('a', 'b');
|
||||||
graph.addEdge("b", "c");
|
graph.addEdge('b', 'c');
|
||||||
var sorted = graph.topologicalSort(["a"], false);
|
var sorted = graph.topologicalSort(['a'], false);
|
||||||
assert.equal(sorted.length, 2);
|
assert.equal(sorted.length, 2);
|
||||||
assert.equal(sorted[0], "b");
|
assert.equal(sorted[0], 'b');
|
||||||
assert.equal(sorted[1], "c");
|
assert.equal(sorted[1], 'c');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should compute topological sort tricky case.", function (){
|
it('Should compute topological sort tricky case.', function() {
|
||||||
|
var graph = Graph(); // a
|
||||||
|
// / \
|
||||||
|
graph.addEdge('a', 'b'); // b |
|
||||||
|
graph.addEdge('a', 'd'); // | d
|
||||||
|
graph.addEdge('b', 'c'); // c |
|
||||||
|
graph.addEdge('d', 'e'); // \ /
|
||||||
|
graph.addEdge('c', 'e'); // e
|
||||||
|
|
||||||
var graph = Graph(); // a
|
var sorted = graph.topologicalSort(['a'], false);
|
||||||
// / \
|
|
||||||
graph.addEdge("a", "b"); // b |
|
|
||||||
graph.addEdge("a", "d"); // | d
|
|
||||||
graph.addEdge("b", "c"); // c |
|
|
||||||
graph.addEdge("d", "e"); // \ /
|
|
||||||
graph.addEdge("c", "e"); // e
|
|
||||||
|
|
||||||
var sorted = graph.topologicalSort(["a"], false);
|
|
||||||
assert.equal(sorted.length, 4);
|
assert.equal(sorted.length, 4);
|
||||||
assert(contains(sorted, "b"));
|
assert(contains(sorted, 'b'));
|
||||||
assert(contains(sorted, "c"));
|
assert(contains(sorted, 'c'));
|
||||||
assert(contains(sorted, "d"));
|
assert(contains(sorted, 'd'));
|
||||||
assert.equal(sorted[sorted.length - 1], "e");
|
assert.equal(sorted[sorted.length - 1], 'e');
|
||||||
|
|
||||||
assert(comesBefore(sorted, "b", "c"));
|
|
||||||
assert(comesBefore(sorted, "b", "e"));
|
|
||||||
assert(comesBefore(sorted, "c", "e"));
|
|
||||||
assert(comesBefore(sorted, "d", "e"));
|
|
||||||
|
|
||||||
|
assert(comesBefore(sorted, 'b', 'c'));
|
||||||
|
assert(comesBefore(sorted, 'b', 'e'));
|
||||||
|
assert(comesBefore(sorted, 'c', 'e'));
|
||||||
|
assert(comesBefore(sorted, 'd', 'e'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should exclude source nodes with a cycle.", function (){
|
it('Should exclude source nodes with a cycle.', function() {
|
||||||
var graph = Graph()
|
var graph = Graph()
|
||||||
.addEdge("a", "b")
|
.addEdge('a', 'b')
|
||||||
.addEdge("b", "c")
|
.addEdge('b', 'c')
|
||||||
.addEdge("c", "a");
|
.addEdge('c', 'a');
|
||||||
var sorted = graph.topologicalSort(["a"], false);
|
var sorted = graph.topologicalSort(['a'], false);
|
||||||
assert.equal(sorted.length, 2);
|
assert.equal(sorted.length, 2);
|
||||||
assert.equal(sorted[0], "b");
|
assert.equal(sorted[0], 'b');
|
||||||
assert.equal(sorted[1], "c");
|
assert.equal(sorted[1], 'c');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should exclude source nodes with multiple cycles.", function (){
|
it('Should exclude source nodes with multiple cycles.', function() {
|
||||||
var graph = Graph()
|
var graph = Graph()
|
||||||
|
.addEdge('a', 'b')
|
||||||
|
.addEdge('b', 'a')
|
||||||
|
|
||||||
.addEdge("a", "b")
|
.addEdge('b', 'c')
|
||||||
.addEdge("b", "a")
|
.addEdge('c', 'b')
|
||||||
|
|
||||||
.addEdge("b", "c")
|
.addEdge('a', 'c')
|
||||||
.addEdge("c", "b")
|
.addEdge('c', 'a');
|
||||||
|
|
||||||
.addEdge("a", "c")
|
var sorted = graph.topologicalSort(['a', 'b'], false);
|
||||||
.addEdge("c", "a");
|
assert(!contains(sorted, 'b'));
|
||||||
|
|
||||||
var sorted = graph.topologicalSort(["a", "b"], false);
|
|
||||||
assert(!contains(sorted, "b"));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Edge cases and error handling", function() {
|
describe('Edge cases and error handling', function() {
|
||||||
|
it('Should return empty array of adjacent nodes for unknown nodes.', function() {
|
||||||
it("Should return empty array of adjacent nodes for unknown nodes.", function (){
|
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
assert.equal(graph.adjacent("a").length, 0);
|
assert.equal(graph.adjacent('a').length, 0);
|
||||||
assert.equal(graph.nodes(), 0);
|
assert.equal(graph.nodes(), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should do nothing if removing an edge that does not exist.", function (){
|
it('Should do nothing if removing an edge that does not exist.', function() {
|
||||||
assert.doesNotThrow(function (){
|
assert.doesNotThrow(function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.removeEdge("a", "b");
|
graph.removeEdge('a', 'b');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return indegree of 0 for unknown nodes.", function (){
|
it('Should return indegree of 0 for unknown nodes.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
assert.equal(graph.indegree("z"), 0);
|
assert.equal(graph.indegree('z'), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return outdegree of 0 for unknown nodes.", function (){
|
it('Should return outdegree of 0 for unknown nodes.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
assert.equal(graph.outdegree("z"), 0);
|
assert.equal(graph.outdegree('z'), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Serialization", function() {
|
describe('Serialization', function() {
|
||||||
|
|
||||||
var serialized;
|
var serialized;
|
||||||
|
|
||||||
function checkSerialized(graph){
|
function checkSerialized(graph) {
|
||||||
assert.equal(graph.nodes.length, 3);
|
assert.equal(graph.nodes.length, 3);
|
||||||
assert.equal(graph.links.length, 2);
|
assert.equal(graph.links.length, 2);
|
||||||
|
|
||||||
assert.equal(graph.nodes[0].id, "a");
|
assert.equal(graph.nodes[0].id, 'a');
|
||||||
assert.equal(graph.nodes[1].id, "b");
|
assert.equal(graph.nodes[1].id, 'b');
|
||||||
assert.equal(graph.nodes[2].id, "c");
|
assert.equal(graph.nodes[2].id, 'c');
|
||||||
|
|
||||||
assert.equal(graph.links[0].source, "a");
|
assert.equal(graph.links[0].source, 'a');
|
||||||
assert.equal(graph.links[0].target, "b");
|
assert.equal(graph.links[0].target, 'b');
|
||||||
assert.equal(graph.links[1].source, "b");
|
assert.equal(graph.links[1].source, 'b');
|
||||||
assert.equal(graph.links[1].target, "c");
|
assert.equal(graph.links[1].target, 'c');
|
||||||
}
|
}
|
||||||
|
|
||||||
it("Should serialize a graph.", function (){
|
it('Should serialize a graph.', function() {
|
||||||
var graph = Graph()
|
var graph = Graph()
|
||||||
.addEdge("a", "b")
|
.addEdge('a', 'b')
|
||||||
.addEdge("b", "c");
|
.addEdge('b', 'c');
|
||||||
serialized = graph.serialize();
|
serialized = graph.serialize();
|
||||||
checkSerialized(serialized);
|
checkSerialized(serialized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should deserialize a graph.", function (){
|
it('Should deserialize a graph.', function() {
|
||||||
var graph = Graph();
|
var graph = Graph();
|
||||||
graph.deserialize(serialized);
|
graph.deserialize(serialized);
|
||||||
checkSerialized(graph.serialize());
|
checkSerialized(graph.serialize());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should chain deserialize a graph.", function (){
|
it('Should chain deserialize a graph.', function() {
|
||||||
var graph = Graph().deserialize(serialized);
|
var graph = Graph().deserialize(serialized);
|
||||||
checkSerialized(graph.serialize());
|
checkSerialized(graph.serialize());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should deserialize a graph passed to constructor.", function (){
|
it('Should deserialize a graph passed to constructor.', function() {
|
||||||
var graph = Graph(serialized);
|
var graph = Graph(serialized);
|
||||||
checkSerialized(graph.serialize());
|
checkSerialized(graph.serialize());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function contains(arr, item){
|
function contains(arr, item) {
|
||||||
return arr.filter(function (d){
|
return (
|
||||||
return d === item;
|
arr.filter(function(d) {
|
||||||
}).length > 0;
|
return d === item;
|
||||||
|
}).length > 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function comesBefore(arr, a, b){
|
function comesBefore(arr, a, b) {
|
||||||
var aIndex, bIndex;
|
var aIndex, bIndex;
|
||||||
arr.forEach(function (d, i){
|
arr.forEach(function(d, i) {
|
||||||
if(d === a){ aIndex = i; }
|
if (d === a) {
|
||||||
if(d === b){ bIndex = i; }
|
aIndex = i;
|
||||||
|
}
|
||||||
|
if (d === b) {
|
||||||
|
bIndex = i;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return aIndex < bIndex;
|
return aIndex < bIndex;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const sqlite = require("sqlite3");
|
const sqlite = require('sqlite3');
|
||||||
const escodegen = require("escodegen");
|
const escodegen = require('escodegen');
|
||||||
const sqlgen = require("./sqlgen");
|
const sqlgen = require('./sqlgen');
|
||||||
|
|
||||||
// Example usage:
|
// Example usage:
|
||||||
|
|
||||||
const Spreadsheet = require("./spreadsheet");
|
const Spreadsheet = require('./spreadsheet');
|
||||||
|
|
||||||
const db = new sqlite.Database(__dirname + "/../../db.sqlite");
|
const db = new sqlite.Database(__dirname + '/../../db.sqlite');
|
||||||
const sheet = new Spreadsheet({
|
const sheet = new Spreadsheet({
|
||||||
plugins: {
|
plugins: {
|
||||||
runQuery: {
|
runQuery: {
|
||||||
|
@ -20,22 +20,24 @@ const sheet = new Spreadsheet({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
type: "query",
|
type: 'query',
|
||||||
query: query,
|
query: query,
|
||||||
sql: sql
|
sql: sql
|
||||||
},
|
},
|
||||||
|
|
||||||
ast: {
|
ast: {
|
||||||
type: "CallExpression",
|
type: 'CallExpression',
|
||||||
callee: {
|
callee: {
|
||||||
type: "Identifier",
|
type: 'Identifier',
|
||||||
name: "runQuery"
|
name: 'runQuery'
|
||||||
},
|
},
|
||||||
arguments: [{
|
arguments: [
|
||||||
type: "Literal",
|
{
|
||||||
raw: sql,
|
type: 'Literal',
|
||||||
value: sql
|
raw: sql,
|
||||||
}]
|
value: sql
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -43,7 +45,7 @@ const sheet = new Spreadsheet({
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
db.all(sql, function(err, rows) {
|
db.all(sql, function(err, rows) {
|
||||||
if(err) {
|
if (err) {
|
||||||
throw new Error(err);
|
throw new Error(err);
|
||||||
}
|
}
|
||||||
resolve(rows);
|
resolve(rows);
|
||||||
|
@ -54,11 +56,12 @@ const sheet = new Spreadsheet({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
db.on("preupdate", function(type, dbname, table, old, _new, oldId, newId) {
|
db.on('preupdate', function(type, dbname, table, old, _new, oldId, newId) {
|
||||||
sheet.resolve().then(() => {
|
sheet.resolve().then(() => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
sheet.startTransaction();
|
sheet.startTransaction();
|
||||||
sheet.getNodesOfType("query")
|
sheet
|
||||||
|
.getNodesOfType('query')
|
||||||
.filter(node => node.data.query.table === table)
|
.filter(node => node.data.query.table === table)
|
||||||
.forEach(q => {
|
.forEach(q => {
|
||||||
sheet.signal(q.name);
|
sheet.signal(q.name);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { sequential, once } from '../../shared/async';
|
import { sequential, once } from '../../shared/async';
|
||||||
import * as perf from '../perf';
|
|
||||||
import * as prefs from '../prefs';
|
import * as prefs from '../prefs';
|
||||||
import app from '../main-app';
|
import app from '../main-app';
|
||||||
import asyncStorage from '../../platform/server/asyncStorage';
|
import asyncStorage from '../../platform/server/asyncStorage';
|
||||||
|
@ -12,12 +11,13 @@ import { triggerBudgetChanges, setType as setBudgetType } from '../budget/base';
|
||||||
import * as undo from '../undo';
|
import * as undo from '../undo';
|
||||||
import { runMutator } from '../mutators';
|
import { runMutator } from '../mutators';
|
||||||
import { setIn, getIn } from '../../shared/util';
|
import { setIn, getIn } from '../../shared/util';
|
||||||
import Timestamp, {
|
import {
|
||||||
serializeClock,
|
serializeClock,
|
||||||
deserializeClock,
|
deserializeClock,
|
||||||
getClock
|
getClock,
|
||||||
} from '../timestamp';
|
Timestamp,
|
||||||
import * as merkle from '../merkle';
|
merkle
|
||||||
|
} from '../crdt';
|
||||||
import * as encoder from './encoder';
|
import * as encoder from './encoder';
|
||||||
import { getServer } from '../server-config';
|
import { getServer } from '../server-config';
|
||||||
import { rebuildMerkleHash } from './repair';
|
import { rebuildMerkleHash } from './repair';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { addSyncListener, applyMessages } from './index';
|
import { addSyncListener, applyMessages } from './index';
|
||||||
import Timestamp from '../timestamp';
|
import { Timestamp } from '../crdt';
|
||||||
|
|
||||||
function migrateParentIds(oldValues, newValues) {
|
function migrateParentIds(oldValues, newValues) {
|
||||||
newValues.forEach((items, table) => {
|
newValues.forEach((items, table) => {
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import fc from 'fast-check';
|
import fc from 'fast-check';
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
import { migrateParentIds, listen, unlisten } from './migrate';
|
import { listen, unlisten } from './migrate';
|
||||||
import { addSyncListener, batchMessages, sendMessages } from './index';
|
import { addSyncListener, sendMessages } from './index';
|
||||||
import { execTracer } from '../../shared/test-helpers';
|
import { execTracer } from '../../shared/test-helpers';
|
||||||
import { schema, schemaConfig } from '../aql/schema';
|
import { schema, schemaConfig } from '../aql/schema';
|
||||||
import arbs from '../../mocks/arbitrary-schema';
|
import arbs from '../../mocks/arbitrary-schema';
|
||||||
import { convertInputType } from '../aql/schema-helpers';
|
import { convertInputType } from '../aql/schema-helpers';
|
||||||
import { groupBy } from '../../shared/util';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
listen();
|
listen();
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
import Timestamp, { serializeClock, getClock } from '../timestamp';
|
import { serializeClock, getClock, Timestamp, merkle } from '../crdt';
|
||||||
import * as merkle from '../merkle';
|
|
||||||
|
|
||||||
export function rebuildMerkleHash() {
|
export function rebuildMerkleHash() {
|
||||||
let rows = db.runQuery('SELECT timestamp FROM messages_crdt', [], true);
|
let rows = db.runQuery('SELECT timestamp FROM messages_crdt', [], true);
|
||||||
|
|
|
@ -2,16 +2,13 @@ import * as prefs from '../prefs';
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
import * as sheet from '../sheet';
|
import * as sheet from '../sheet';
|
||||||
import * as sync from './index';
|
import * as sync from './index';
|
||||||
import Timestamp, { deserializeClock, getClock } from '../timestamp';
|
import { merkle, getClock, Timestamp } from '../crdt';
|
||||||
import * as merkle from '../merkle';
|
|
||||||
import * as encoder from './encoder';
|
import * as encoder from './encoder';
|
||||||
const Database = require('better-sqlite3');
|
|
||||||
const fs = require('fs');
|
|
||||||
const jsc = require('jsverify');
|
const jsc = require('jsverify');
|
||||||
const uuid = require('uuid');
|
const uuidGenerator = jsc.integer(97, 122).smap(
|
||||||
const uuidGenerator = jsc
|
x => String.fromCharCode(x),
|
||||||
.integer(97, 122)
|
x => x.charCodeAt(x)
|
||||||
.smap(x => String.fromCharCode(x), x => x.charCodeAt(x));
|
);
|
||||||
|
|
||||||
const mockSyncServer = require('../tests/mockSyncServer');
|
const mockSyncServer = require('../tests/mockSyncServer');
|
||||||
|
|
||||||
|
@ -129,7 +126,10 @@ Object.keys(schema).forEach(table => {
|
||||||
generators.push(
|
generators.push(
|
||||||
makeGen({
|
makeGen({
|
||||||
table,
|
table,
|
||||||
row: jsc.asciinestring.smap(x => 'sheet!' + x, x => x),
|
row: jsc.asciinestring.smap(
|
||||||
|
x => 'sheet!' + x,
|
||||||
|
x => x
|
||||||
|
),
|
||||||
field: 'expr',
|
field: 'expr',
|
||||||
value: jsc.constant(JSON.stringify('fooooo'))
|
value: jsc.constant(JSON.stringify('fooooo'))
|
||||||
})
|
})
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue