Commit 57dde446 authored by Benjamin Bouvier's avatar Benjamin Bouvier

Set up Typescript on the server and fix models;

parent 863b08d0
Pipeline #201451 passed with stages
in 7 minutes and 58 seconds
---
settings:
import/resolver:
node:
extensions:
- ".js"
- ".ts"
parser: 'babel-eslint'
parserOptions:
ecmaFeatures:
{ legacyDecorators: true }
env:
node: true
browser: true
es6: true
plugins:
- import
- prettier
overrides:
- files: '**/*.ts'
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2018
sourceType: "module"
extends:
- 'prettier/@typescript-eslint'
- 'plugin:@typescript-eslint/eslint-recommended'
- 'plugin:@typescript-eslint/recommended'
- files: 'server/models/entities/*.ts'
rules:
'@typescript-eslint/no-use-before-define':
- 0
rules:
prettier/prettier:
- 2
......
......@@ -48,6 +48,11 @@
"@babel/preset-env": "7.6.3",
"@babel/preset-react": "7.6.3",
"@babel/register": "7.6.2",
"@types/mocha": "5.2.7",
"@types/node": "13.1.4",
"@typescript-eslint/eslint-plugin": "2.14.0",
"@typescript-eslint/parser": "2.14.0",
"awesome-typescript-loader": "5.2.1",
"babel-eslint": "10.0.3",
"babel-loader": "8.0.6",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
......@@ -59,6 +64,7 @@
"cssnano": "4.1.10",
"dygraphs": "2.1.0",
"eslint": "6.5.1",
"eslint-config-prettier": "6.9.0",
"eslint-plugin-dependencies": "2.4.0",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-prettier": "3.1.1",
......@@ -99,6 +105,8 @@
"sqlite3": "4.1.1",
"stylelint": "11.1.1",
"stylelint-config-standard": "19.0.0",
"ts-node": "8.5.4",
"typescript": "3.7.4",
"updeep": "1.2.0",
"url-loader": "2.2.0",
"webpack": "4.41.2",
......@@ -111,6 +119,7 @@
"scripts": {
"build:ci": "scripty",
"build:server": "scripty",
"build:server-common": "scripty",
"build:prod": "scripty",
"build:dev": "scripty",
"check:lint": "scripty",
......
#!/bin/bash
set -e
# Default to development
if [ -z "$NODE_ENV" ]
then
NODE_ENV="development"
fi
echo "Building in $NODE_ENV mode..."
rm -rf ./build/server
echo "Copying shared files..."
mkdir -p ./build/server/shared
cp -r ./shared/*.json ./build/server/shared
echo "Copying locale files..."
mkdir -p ./build/server/shared/locales
cp -r ./shared/locales/*.json ./build/server/shared/locales
echo "Copying Weboob endpoint..."
cp -r ./server/weboob ./build/server/weboob && chmod +x ./build/server/weboob/main.py
echo "Done!"
......@@ -8,21 +8,13 @@ then
fi
echo "Building in $NODE_ENV mode..."
rm -rf ./build/server
echo "Copying shared files..."
mkdir -p ./build/server/shared
cp -r ./shared/*.json ./build/server/shared
echo "Copying locale files..."
mkdir -p ./build/server/shared/locales
cp -r ./shared/locales/*.json ./build/server/shared/locales
yarn build:server-common
echo "Building server JS..."
mkdir -p ./build/server
yarn run -- babel ./server/ -d ./build/server
echo "Copying Weboob endpoint..."
cp -r ./server/weboob ./build/server/weboob && chmod +x ./build/server/weboob/main.py
# tsc returns an error code equal to 2 when it emitted files, but with some
# type errors. During the migration to typescript, it's expected to have those.
yarn run -- tsc || [ $? -eq 2 ]
echo "Done!"
......@@ -29,5 +29,5 @@ fi
concurrently \
"yarn run check:package-json" \
"yarn run -- eslint --cache $QUIET $FIX $TARGET"\
"yarn run -- eslint --cache $QUIET --ext .js,.ts $FIX $TARGET"\
"yarn run check:lint-css $QUIET $FIX"
......@@ -16,9 +16,13 @@ then
fi
# Run proper tests
BABEL_ENV=tests NODE_ENV=test yarn mocha \
BABEL_ENV=tests \
NODE_ENV=test \
TS_NODE_TRANSPILE_ONLY=true \
yarn mocha \
--require @babel/polyfill \
--require @babel/register \
--require ts-node/register \
--file ./tests/database/config.js \
--recursive $TARGET \
--ignore ./tests/fixtures \
......
#!/bin/bash
set -e
yarn run build:server
yarn run build:server-common
concurrently -k \
"yarn run webpack-dev-server" \
......
#!/bin/bash
set -e
yarn run build:server
yarn run build:server-common
concurrently -k \
"./scripts/watch/client.sh" \
......
......@@ -5,4 +5,4 @@ concurrently -k \
"yarn run -- onchange './shared/*.json' -- cp '{{changed}}' ./build/server/shared" \
"yarn run -- onchange './shared/locales/*.json' -- cp '{{changed}}' ./build/server/shared/locales" \
"yarn run -- onchange './server/weboob/**/*.py' -- cp './{{changed}}' './build/{{changed}}'" \
"yarn run babel --skip-initial-build ./server/ -d ./build/server -w"
"yarn run tsc -w"
import {
getRepository,
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn
} from 'typeorm';
import User from './users';
import Access from './accesses';
import { assert } from '../../helpers';
@Entity()
export default class AccessFields {
@PrimaryGeneratedColumn()
id;
@ManyToOne(
// eslint-disable-next-line no-unused-vars
type => User,
{ cascade: true, onDelete: 'CASCADE', nullable: false }
)
@JoinColumn()
user;
@Column('integer')
userId;
// The access unique identifier of the access the field is attached to.
@ManyToOne(
// eslint-disable-next-line no-unused-vars
type => Access,
{ cascade: true, onDelete: 'CASCADE', nullable: false },
access => access.fields
)
@JoinColumn()
access;
@Column('integer')
accessId;
// The name of the field.
@Column('varchar')
name;
// The value of the field.
@Column('varchar')
value;
}
let REPO = null;
function repo() {
if (REPO === null) {
REPO = getRepository(AccessFields);
}
return REPO;
}
AccessFields.create = async function(userId, attributes) {
let { accessId } = attributes;
assert(
typeof accessId === 'number',
'AccessFields.create second arg should have "accessId" id property'
);
let entity = repo().create(Object.assign({}, attributes, { userId }));
return await repo().save(entity);
};
AccessFields.find = async function(userId, fieldId) {
return await repo().findOne({ where: { id: fieldId, userId } });
};
AccessFields.all = async function(userId) {
return await repo().find({ userId });
};
AccessFields.exists = async function(userId, fieldId) {
let found = await repo().findOne({ where: { userId, id: fieldId } });
return !!found;
};
AccessFields.destroy = async function(userId, fieldId) {
return await repo().delete({ userId, id: fieldId });
};
AccessFields.update = async function(userId, fieldId, attributes) {
return await repo().update({ userId, id: fieldId }, attributes);
};
// TODO optimize with SQL?
AccessFields.batchCreate = async function batchCreate(userId, accessId, fields) {
assert(
typeof accessId === 'number',
'AccessFields.batchCreate second arg should be an id "accessId"'
);
assert(fields instanceof Array, 'AccessFields.batchCreate third arg should be an array.');
let fieldsFromDb = [];
for (let field of fields) {
fieldsFromDb.push(await AccessFields.create(userId, { ...field, accessId }));
}
return fieldsFromDb;
};
AccessFields.allByAccessId = async function allByAccessId(userId, accessId) {
assert(
typeof accessId === 'number',
'AccessFields.allByAccessId second arg should be an id "accessId".'
);
return await repo().find({ accessId, userId });
};
AccessFields.updateOrCreateByAccessIdAndName = async function(userId, accessId, name, value) {
if (value === null) {
return await repo().delete({ accessId, name });
}
// TODO optimize with upsert() if available?
let field = await repo().find({ accessId, name });
if (field instanceof Array && field.length) {
assert(field.length === 1, 'more than one value set for a given custom field');
field = field[0];
return await AccessFields.update(userId, field.id, { value });
}
return await AccessFields.create(userId, { name, value, accessId });
};
AccessFields.batchUpdateOrCreate = async function batchUpdateOrCreate(
userId,
accessId,
fields = []
) {
assert(fields instanceof Array, 'AccessFields.batchUpdateOrCreate third arg must be an array.');
for (let field of fields) {
await AccessFields.updateOrCreateByAccessIdAndName(
userId,
accessId,
field.name,
field.value
);
}
};
import {
getRepository,
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
Repository
} from 'typeorm';
import User from './users';
import Access from './accesses';
import { assert } from '../../helpers';
@Entity()
export default class AccessFields {
@PrimaryGeneratedColumn()
id;
@ManyToOne(
// eslint-disable-next-line no-unused-vars
type => User,
{ cascade: true, onDelete: 'CASCADE', nullable: false }
)
@JoinColumn()
user;
@Column('integer')
userId;
// The access unique identifier of the access the field is attached to.
@ManyToOne(
// eslint-disable-next-line no-unused-vars
type => Access,
access => access.fields,
{ cascade: true, onDelete: 'CASCADE', nullable: false }
)
@JoinColumn()
access;
@Column('integer')
accessId;
// The name of the field.
@Column('varchar')
name;
// The value of the field.
@Column('varchar')
value;
static async create(userId, attributes) {
const { accessId } = attributes;
assert(
typeof accessId === 'number',
'AccessFields.create second arg should have "accessId" id property'
);
const entity = repo().create(Object.assign({}, attributes, { userId }));
return await repo().save(entity);
}
static async find(userId, fieldId) {
return await repo().findOne({ where: { id: fieldId, userId } });
}
static async all(userId) {
return await repo().find({ userId });
}
static async exists(userId, fieldId) {
const found = await repo().findOne({ where: { userId, id: fieldId } });
return !!found;
}
static async destroy(userId, fieldId) {
return await repo().delete({ userId, id: fieldId });
}
static async update(userId, fieldId, attributes) {
return await repo().update({ userId, id: fieldId }, attributes);
}
// TODO optimize with SQL?
static async batchCreate(userId, accessId, fields) {
assert(
typeof accessId === 'number',
'AccessFields.batchCreate second arg should be an id "accessId"'
);
assert(fields instanceof Array, 'AccessFields.batchCreate third arg should be an array.');
const fieldsFromDb: AccessFields[] = [];
for (const field of fields) {
fieldsFromDb.push(await AccessFields.create(userId, { ...field, accessId }));
}
return fieldsFromDb;
}
static async allByAccessId(userId, accessId) {
assert(
typeof accessId === 'number',
'AccessFields.allByAccessId second arg should be an id "accessId".'
);
return await repo().find({ accessId, userId });
}
static async updateOrCreateByAccessIdAndName(userId, accessId, name, value) {
if (value === null) {
return await repo().delete({ userId, accessId, name });
}
// TODO optimize with upsert() if available?
const field = await repo().find({ userId, accessId, name });
if (field instanceof Array && field.length) {
assert(field.length === 1, 'more than one value set for a given custom field');
return await AccessFields.update(userId, field[0].id, { value });
}
return await AccessFields.create(userId, { name, value, accessId });
}
static async batchUpdateOrCreate(userId, accessId, fields = []) {
assert(
fields instanceof Array,
'AccessFields.batchUpdateOrCreate third arg must be an array.'
);
for (const field of fields) {
await AccessFields.updateOrCreateByAccessIdAndName(
userId,
accessId,
field.name,
field.value
);
}
}
}
let REPO: Repository<AccessFields> | null = null;
function repo(): Repository<AccessFields> {
if (REPO === null) {
REPO = getRepository(AccessFields);
}
return REPO;
}
......@@ -5,7 +5,8 @@ import {
Column,
JoinColumn,
ManyToOne,
OneToMany
OneToMany,
Repository
} from 'typeorm';
import User from './users';
......@@ -14,7 +15,7 @@ import AccessFields from './access-fields';
import { FETCH_STATUS_SUCCESS, makeLogger } from '../../helpers';
import { bankVendorByUuid } from '../../lib/bank-vendors';
let log = makeLogger('models/entities/accesses');
const log = makeLogger('models/entities/accesses');
@Entity()
export default class Access {
......@@ -83,61 +84,63 @@ export default class Access {
this.fetchStatus !== 'AUTH_METHOD_NYI'
);
}
}
let REPO = null;
function repo() {
if (REPO === null) {
REPO = getRepository(Access);
// Static attributes.
static renamings = {
bank: 'vendorId'
};
// Doesn't insert anything in db, only creates a new instance and normalizes its fields.
static cast(...args) {
return repo().create(...args);
}
return REPO;
}
Access.renamings = {
bank: 'vendorId'
};
static async create(userId, { fields = null, ...other }) {
const entity = repo().create({ userId, ...other });
const access = await repo().save(entity);
if (fields !== null) {
await AccessFields.batchCreate(userId, access.id, fields);
}
access.fields = await AccessFields.allByAccessId(userId, access.id);
return access;
}
static async find(userId, accessId) {
return await repo().findOne({ where: { userId, id: accessId }, relations: ['fields'] });
}
// Doesn't insert anything in db, only creates a new instance and normalizes its fields.
Access.cast = function(...args) {
return repo().create(...args);
};
static async all(userId) {
return await repo().find({ where: { userId }, relations: ['fields'] });
}
Access.create = async function(userId, { fields = null, ...other }) {
let entity = repo().create({ userId, ...other });
let access = await repo().save(entity);
if (fields !== null) {
await AccessFields.batchCreate(userId, access.id, fields);
static async exists(userId, accessId) {
const found = await repo().findOne({ where: { userId, id: accessId } });
return !!found;
}
access.fields = await AccessFields.allByAccessId(userId, access.id);
return access;
};
Access.find = async function(userId, accessId) {
return await repo().findOne({ where: { userId, id: accessId }, relations: ['fields'] });
};
Access.all = async function(userId) {
return await repo().find({ userId, relations: ['fields'] });
};
Access.exists = async function(userId, accessId) {
let found = await repo().findOne({ where: { userId, id: accessId } });
return !!found;
};
Access.destroy = async function(userId, accessId) {
return await repo().delete({ userId, id: accessId });
};
Access.update = async function(userId, accessId, { fields = [], ...other }) {
await AccessFields.batchUpdateOrCreate(userId, accessId, fields);
await repo().update({ userId, id: accessId }, other);
return await Access.find(userId, accessId);
};
Access.byVendorId = async function byVendorId(userId, bank) {
if (typeof bank !== 'object' || typeof bank.uuid !== 'string') {
log.warn('Access.byVendorId misuse: bank must be a Bank instance.');
static async destroy(userId, accessId) {
return await repo().delete({ userId, id: accessId });
}
return await repo().find({ userId, vendorId: bank.uuid, relations: ['fields'] });
};
static async update(userId, accessId, { fields = [], ...other }) {
await AccessFields.batchUpdateOrCreate(userId, accessId, fields);
await repo().update({ userId, id: accessId }, other);
return await Access.find(userId, accessId);
}
static async byVendorId(userId, bank) {
if (typeof bank !== 'object' || typeof bank.uuid !== 'string') {
log.warn('Access.byVendorId misuse: bank must be a Bank instance.');
}
return await repo().find({ where: { userId, vendorId: bank.uuid }, relations: ['fields'] });
}
}
let REPO: Repository<Access> | null = null;
function repo(): Repository<Access> {
if (REPO === null) {
REPO = getRepository(Access);
}
return REPO;
}
......@@ -5,7 +5,8 @@ import {
PrimaryGeneratedColumn,
JoinColumn,
Column,
ManyToOne
ManyToOne,
Repository
} from 'typeorm';
import moment from 'moment';
......@@ -24,7 +25,7 @@ import {
} from '../../helpers';
import { ForceNumericColumn, DatetimeType } from '../helpers';
let log = makeLogger('models/entities/accounts');
const log = makeLogger('models/entities/accounts');
@Entity()
export default class Account {
......@@ -102,101 +103,103 @@ export default class Account {
excludeFromBalance;
// Methods.
computeBalance = async () => {
let ops = await Transaction.byAccount(this.userId, this);
let today = moment();
let s = ops
const ops = await Transaction.byAccount(this.userId, this);
const today = moment();
const s = ops
.filter(op => shouldIncludeInBalance(op, today, this.type))
.reduce((sum, op) => sum + op.amount, this.initialBalance);
return Math.round(s * 100) / 100;
};
computeOutstandingSum = async () => {
let ops = await Transaction.byAccount(this.userId, this);
let s = ops
const ops = await Transaction.byAccount(this.userId, this);
const s = ops
.filter(op => shouldIncludeInOutstandingSum(op))
.reduce((sum, op) => sum + op.amount, 0);
return Math.round(s * 100) / 100;
};
getCurrencyFormatter = async () => {
let curr = currency.isKnown(this.currency)
const curr = currency.isKnown(this.currency)
? this.currency
: (await Setting.findOrCreateDefault(await this.userId, 'default-currency')).value;
return currency.makeFormat(curr);
};
}
let REPO = null;
function repo() {
if (REPO === null) {
REPO = getRepository(Account);
// Static methods
static renamings = {
initialAmount: 'initialBalance',
bank: 'vendorId',
lastChecked: 'lastCheckDate',
bankAccess: 'accessId',
accountNumber: 'vendorAccountId',
title: 'label'
};
static async byVendorId(userId, bank) {
if (typeof bank !== 'object' || typeof bank.uuid !== 'string') {
log.warn('Account.byVendorId misuse: bank must be a Bank instance');
}
return await repo().find({ userId, vendorId: bank.uuid });
}
static async findMany(userId, accountIds) {
if (!(accountIds instanceof Array)) {
log.warn('Account.findMany misuse: accountIds must be an Array');
}
if (accountIds.length && typeof accountIds[0] !== 'number') {
log.warn('Account.findMany misuse: accountIds must be a [Number]');
}
return await repo().find({ userId, id: In(accountIds) });
}
static async byAccess(userId, access) {
if (typeof access !== 'object' || typeof access.id !== 'number') {
log.warn('Account.byAccess misuse: access must be an Access instance');
}
return await repo().find({ userId, accessId: access.id });
}
// Doesn't insert anything in db, only creates a new instance and normalizes its fields.
static cast(...args) {
return repo().create(...args);
}
static async create(userId, attributes) {
const entity = repo().create({ userId, ...attributes });
return await repo().save(entity);
}
return REPO;
}
Account.renamings = {