anote-server-libs 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/models/ApiCall.ts +28 -0
- package/models/Migration.ts +29 -0
- package/models/repository/BaseModelRepository.ts +135 -0
- package/models/repository/MemoryCache.ts +87 -0
- package/models/repository/ModelDao.ts +103 -0
- package/package.json +28 -0
- package/services/WithBody.ts +56 -0
- package/services/WithTransaction.ts +146 -0
- package/services/utils.ts +158 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {Pool} from 'pg';
|
|
2
|
+
import {Logger} from 'winston';
|
|
3
|
+
import {Model, ModelDao} from './repository/ModelDao';
|
|
4
|
+
|
|
5
|
+
export interface ApiCall extends Model<string> {
|
|
6
|
+
responseCode: number;
|
|
7
|
+
responseJson: string;
|
|
8
|
+
expiresAt: Date;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ApiCallRepository extends ModelDao<string, ApiCall> {
|
|
12
|
+
constructor(protected pool: Pool, protected logger: Logger) {
|
|
13
|
+
super(pool, logger, 'api_call', 5, 'id=$1,"updatedOn"=$2,"responseCode"=$3,"responseJson"=$4,"expiresAt"=$5');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
buildObject(q: any): ApiCall {
|
|
17
|
+
if(!q) return undefined;
|
|
18
|
+
q.id = q.id.trim();
|
|
19
|
+
q.updatedOn = q.updatedOn && new Date(q.updatedOn);
|
|
20
|
+
q.responseCode = parseInt(q.responseCode, 10);
|
|
21
|
+
q.expiresAt = q.expiresAt && new Date(q.expiresAt);
|
|
22
|
+
return q;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
serialize(instance: ApiCall): any[] {
|
|
26
|
+
return [instance.id, instance.updatedOn, instance.responseCode, instance.responseJson, instance.expiresAt];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {Pool} from 'pg';
|
|
2
|
+
import {Logger} from 'winston';
|
|
3
|
+
import {ModelDao} from './repository/ModelDao';
|
|
4
|
+
|
|
5
|
+
export interface Migration {
|
|
6
|
+
id: number;
|
|
7
|
+
hash: string;
|
|
8
|
+
sqlUp: string;
|
|
9
|
+
sqlDown: string;
|
|
10
|
+
state: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class MigrationRepository extends ModelDao<number, Migration> {
|
|
14
|
+
constructor(protected pool: Pool, protected logger: Logger) {
|
|
15
|
+
super(pool, logger, 'migration', 5, 'id=$1,hash=$2,"sqlUp"=$3,"sqlDown"=$4,state=$5');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
buildObject(q: any): Migration {
|
|
19
|
+
if(!q) return undefined;
|
|
20
|
+
q.id = parseInt(q.id, 10);
|
|
21
|
+
q.hash = q.hash.trim();
|
|
22
|
+
q.state = parseInt(q.state, 10);
|
|
23
|
+
return q;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
serialize(instance: Migration): any[] {
|
|
27
|
+
return [instance.id, instance.hash, instance.sqlUp, instance.sqlDown, instance.state];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as Memcached from 'memcached';
|
|
3
|
+
import {md} from 'node-forge';
|
|
4
|
+
import {ClientBase, Pool} from 'pg';
|
|
5
|
+
import {Logger} from 'winston';
|
|
6
|
+
import { ApiCallRepository } from '../ApiCall';
|
|
7
|
+
import { Migration, MigrationRepository } from '../Migration';
|
|
8
|
+
import {ModelRepr} from './ModelDao';
|
|
9
|
+
|
|
10
|
+
export class BaseModelRepository {
|
|
11
|
+
Migration: MigrationRepository;
|
|
12
|
+
ApiCall: ApiCallRepository;
|
|
13
|
+
|
|
14
|
+
constructor(public db: Pool, public dbSpare: Pool, public logger: Logger, public cache: Memcached) {
|
|
15
|
+
const dbQuery = db.query.bind(db);
|
|
16
|
+
db.query = (function(text: any, values: any, cb: any) {
|
|
17
|
+
if((this.idleCount + this.waitingCount) >= this.totalCount && this.totalCount === this.options.max)
|
|
18
|
+
return dbSpare.query(text, values, cb);
|
|
19
|
+
return dbQuery(text, values, cb);
|
|
20
|
+
}).bind(db);
|
|
21
|
+
this.Migration = new MigrationRepository(db, logger);
|
|
22
|
+
this.ApiCall = new ApiCallRepository(db, logger);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
migrate(migrationsPath: string, callback: (() => void)) {
|
|
26
|
+
this.db.query('CREATE TABLE IF NOT EXISTS migration (' +
|
|
27
|
+
'id integer PRIMARY KEY,' +
|
|
28
|
+
'hash text NOT NULL,' +
|
|
29
|
+
'"sqlUp" text NOT NULL,' +
|
|
30
|
+
'"sqlDown" text NOT NULL,' +
|
|
31
|
+
'state integer NOT NULL' +
|
|
32
|
+
')').then(() => {
|
|
33
|
+
this.Migration.getAllBy('id').then((migrations: Migration[]) => {
|
|
34
|
+
if(migrations.find(migration => migration.state !== 0)) process.exit(4); // Have to fix manually
|
|
35
|
+
// Read the new ones
|
|
36
|
+
fs.readdir(migrationsPath, (_, files) => {
|
|
37
|
+
const migrationsAvailable = files
|
|
38
|
+
.filter(file => /[0-9]+\.sql/.test(file))
|
|
39
|
+
.map(file => parseInt(file.split('.sql')[0], 10))
|
|
40
|
+
.filter(file => file > 0)
|
|
41
|
+
.sort((a, b) => a - b)
|
|
42
|
+
.map(file => {
|
|
43
|
+
const content = fs.readFileSync(migrationsPath + file + '.sql', 'utf-8');
|
|
44
|
+
return {
|
|
45
|
+
id: file,
|
|
46
|
+
content: content,
|
|
47
|
+
hash: md.sha256.create().update(content).digest().toHex()
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
if(migrationsAvailable.length === 0
|
|
51
|
+
|| migrationsAvailable.length
|
|
52
|
+
!== migrationsAvailable[migrationsAvailable.length - 1].id) process.exit(5); // Did not use OK files
|
|
53
|
+
let highestCommon = 0;
|
|
54
|
+
while(highestCommon < migrations.length && highestCommon < migrationsAvailable.length
|
|
55
|
+
&& migrations[highestCommon].hash === migrationsAvailable[highestCommon].hash)
|
|
56
|
+
highestCommon++;
|
|
57
|
+
this.applyDownUntil(migrations, migrations.length, highestCommon).then(() => {
|
|
58
|
+
this.applyUpUntil(migrationsAvailable, highestCommon, migrationsAvailable.length).then(callback, process.exit);
|
|
59
|
+
}, process.exit);
|
|
60
|
+
});
|
|
61
|
+
}, () => process.exit(3));
|
|
62
|
+
}, () => process.exit(2));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
lockTables(tables: ModelRepr[], client: ClientBase): Promise<any> {
|
|
66
|
+
return Promise.all(tables.map(t => client.query('LOCK TABLE ' + t.table + ' IN EXCLUSIVE MODE')));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private applyUpUntil(migrations: {id: number, content: string, hash: string}[], current: number, until: number): Promise<void> {
|
|
70
|
+
if(current < until)
|
|
71
|
+
return this.applyUp(migrations[current]).then(() => this.applyUpUntil(migrations, current + 1, until));
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private applyUp(migration: {id: number, content: string, hash: string}): Promise<void> {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const sqlParts = migration.content.split('----');
|
|
78
|
+
this.Migration.create({
|
|
79
|
+
id: migration.id,
|
|
80
|
+
hash: migration.hash,
|
|
81
|
+
sqlUp: sqlParts[0],
|
|
82
|
+
sqlDown: sqlParts[1],
|
|
83
|
+
state: 2
|
|
84
|
+
}).then(() => {
|
|
85
|
+
this.db.query(sqlParts[0], (err: any) => {
|
|
86
|
+
if(err) {
|
|
87
|
+
console.error(err);
|
|
88
|
+
reject(10);
|
|
89
|
+
} else {
|
|
90
|
+
this.db.query('UPDATE "migration" SET "state"=0 WHERE "id"=' + migration.id, (err2: any) => {
|
|
91
|
+
if(err2) reject(11); // No cleanup
|
|
92
|
+
else resolve();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}, () => process.exit(9));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private applyDownUntil(migrations: Migration[], current: number, until: number): Promise<void> {
|
|
101
|
+
if(current > until) {
|
|
102
|
+
current--;
|
|
103
|
+
return this.applyDown(migrations[current]).then(() => this.applyDownUntil(migrations, current, until));
|
|
104
|
+
}
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private applyDown(migration: Migration): Promise<void> {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
this.db.query('UPDATE "migration" SET "state"=1 WHERE "id"=' + migration.id, (err: any) => {
|
|
111
|
+
if(err) reject(6); // No required change
|
|
112
|
+
else this.db.query(migration.sqlDown, (err2: any) => {
|
|
113
|
+
if(err2) {
|
|
114
|
+
console.error(err2);
|
|
115
|
+
reject(7);
|
|
116
|
+
} // No apply down
|
|
117
|
+
else this.db.query('DELETE FROM "migration" WHERE "id"=' + migration.id, (err3: any) => {
|
|
118
|
+
if(err3 && migration.id !== 1) reject(8); // No cleanup for not base migration
|
|
119
|
+
else {
|
|
120
|
+
if(migration.id === 1) {
|
|
121
|
+
this.db.query('CREATE TABLE IF NOT EXISTS migration (' +
|
|
122
|
+
'id integer PRIMARY KEY,' +
|
|
123
|
+
'hash text NOT NULL,' +
|
|
124
|
+
'"sqlUp" text NOT NULL,' +
|
|
125
|
+
'"sqlDown" text NOT NULL,' +
|
|
126
|
+
'state integer NOT NULL' +
|
|
127
|
+
')').then(() => resolve(), reject);
|
|
128
|
+
} else resolve();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as Memcached from 'memcached';
|
|
2
|
+
|
|
3
|
+
export class MemoryCache<T> {
|
|
4
|
+
|
|
5
|
+
private localCache = {};
|
|
6
|
+
|
|
7
|
+
constructor(private localKey: string, private cache?: Memcached) {
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
list(): Promise<T[]> {
|
|
11
|
+
if(this.cache) return new Promise(resolve => {
|
|
12
|
+
let pending = true;
|
|
13
|
+
setTimeout(() => {
|
|
14
|
+
if(pending) {
|
|
15
|
+
pending = false;
|
|
16
|
+
resolve([]);
|
|
17
|
+
}
|
|
18
|
+
}, 250);
|
|
19
|
+
this.cache.items((_, data) => {
|
|
20
|
+
if(pending) {
|
|
21
|
+
pending = false;
|
|
22
|
+
resolve(data.map(s => {
|
|
23
|
+
const value = <string>s[Object.getOwnPropertyNames(s).find(sname => sname.startsWith(this.localKey))];
|
|
24
|
+
return value && JSON.parse(value);
|
|
25
|
+
}).filter(x => x));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
else return Promise.resolve(Object.getOwnPropertyNames(this.localCache).map(key => this.localCache[key]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(key: string | number): Promise<T> {
|
|
33
|
+
if(this.cache) return new Promise(resolve => {
|
|
34
|
+
let pending = true;
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
if(pending) {
|
|
37
|
+
pending = false;
|
|
38
|
+
resolve(undefined);
|
|
39
|
+
}
|
|
40
|
+
}, 250);
|
|
41
|
+
this.cache.get(this.localKey + key, (_, data) => {
|
|
42
|
+
if(pending) {
|
|
43
|
+
pending = false;
|
|
44
|
+
resolve(data && JSON.parse(data));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
else return Promise.resolve(this.localCache[key]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
set(key: string | number, val: T): Promise<void> {
|
|
52
|
+
if(this.cache) return new Promise(resolve => this.cache.add(this.localKey + key, JSON.stringify(val), 15 * 60, resolve));
|
|
53
|
+
else {
|
|
54
|
+
this.localCache[key] = val;
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
delete(key: string | number): Promise<void> {
|
|
60
|
+
if(this.cache) {
|
|
61
|
+
return new Promise(resolve => {
|
|
62
|
+
let fired = false;
|
|
63
|
+
const handler = setTimeout(() => {
|
|
64
|
+
fired = true;
|
|
65
|
+
resolve();
|
|
66
|
+
}, 50);
|
|
67
|
+
this.cache.del(this.localKey + key, () => {
|
|
68
|
+
if(!fired) {
|
|
69
|
+
clearTimeout(handler);
|
|
70
|
+
resolve();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
this.localCache[key] = undefined;
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
clear() {
|
|
81
|
+
if(this.cache) {
|
|
82
|
+
this.localKey = this.localKey + '_';
|
|
83
|
+
if(this.localKey.length > 40)
|
|
84
|
+
this.localKey = this.localKey.replace(/_+$/, '');
|
|
85
|
+
} else this.localCache = {};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {ClientBase, Pool} from 'pg';
|
|
2
|
+
import {Logger} from 'winston';
|
|
3
|
+
|
|
4
|
+
export interface Model<T> {
|
|
5
|
+
id?: T;
|
|
6
|
+
updatedOn?: Date;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ModelRepr {
|
|
10
|
+
table: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ViewCount<T> {
|
|
14
|
+
views: T[];
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
abstract class Dao<R, T extends Model<R>> implements ModelRepr {
|
|
19
|
+
constructor(protected pool: Pool, protected logger: Logger, public table: string, protected nFields: number,
|
|
20
|
+
protected updateDefinition: string) {
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
groupResultSet(q: any[], key: (qs: any) => string): any[][] {
|
|
24
|
+
const storage = {};
|
|
25
|
+
for(let i = 0; i < q.length; i++) {
|
|
26
|
+
storage[key(q[i])] = storage[key(q[i])] || [];
|
|
27
|
+
storage[key(q[i])].push(q[i]);
|
|
28
|
+
}
|
|
29
|
+
return Object.getOwnPropertyNames(storage).map(k => storage[k]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
update(instance: T, client: ClientBase): Promise<R> {
|
|
33
|
+
instance.updatedOn = new Date();
|
|
34
|
+
const props = this.serialize(instance);
|
|
35
|
+
props.push(instance.id);
|
|
36
|
+
return client.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=$' + (this.nFields + 1), props).then(() => instance.id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
create(instance: T, client?: ClientBase): Promise<R> {
|
|
40
|
+
instance.updatedOn = new Date();
|
|
41
|
+
const props = this.serialize(instance);
|
|
42
|
+
return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '') + ')'
|
|
43
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '$' + (i + 1)).join(',') + ') RETURNING id',
|
|
44
|
+
props).then(q => {
|
|
45
|
+
const idNum = parseInt(q.rows[0].id, 10);
|
|
46
|
+
if(String(idNum) !== q.rows[0].id) return q.rows[0].id;
|
|
47
|
+
return idNum;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
createSeveral(instances: T[], client?: ClientBase): Promise<R[]> {
|
|
52
|
+
if(!instances.length) return Promise.resolve([]);
|
|
53
|
+
const now = new Date();
|
|
54
|
+
instances.forEach(instance => instance.updatedOn = now);
|
|
55
|
+
const props = [].concat.apply([], instances.map(instance => this.serialize(instance)));
|
|
56
|
+
return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '') + ')'
|
|
57
|
+
+ ' VALUES' + instances.map((_, j) =>
|
|
58
|
+
('(' + new Array(this.nFields).fill(undefined).map((__, i: number) => '$' + (j * this.nFields + i + 1)).join(', ') + ')')).join(',') + ' RETURNING id',
|
|
59
|
+
props).then(q => q.rows.map(r => {
|
|
60
|
+
const idNum = parseInt(r.id, 10);
|
|
61
|
+
if(String(idNum) !== q.rows[0].id) return r.id;
|
|
62
|
+
return idNum;
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected abstract buildObject(q: any): T;
|
|
67
|
+
|
|
68
|
+
protected abstract serialize(instance: T): any[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
72
|
+
get(id: R, client?: ClientBase, lock = true): Promise<T> {
|
|
73
|
+
return (client || this.pool).query('SELECT * FROM ' + this.table + ' WHERE id=$1' + ((client && lock) ? ' FOR UPDATE' : ''), [id]).then(q => this.buildObject(q.rows[0]));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
count(where?: string, inputs: any[] = [], client?: ClientBase): Promise<number> {
|
|
77
|
+
return (client || this.pool).query('SELECT count(*) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''), inputs).then(q => parseInt(q.rows[0].cnt, 10));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getList(ids: R[], client?: ClientBase, lock = true): Promise<T[]> {
|
|
81
|
+
return (client || this.pool).query('SELECT * FROM ' + this.table + ' WHERE id=ANY($1)' + ((client && lock) ? ' FOR UPDATE' : ''), [ids])
|
|
82
|
+
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getAllBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase, lock = true): Promise<T[]> {
|
|
86
|
+
return (client || this.pool).query('SELECT * FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
87
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
88
|
+
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getViewCountBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase, lock = true): Promise<ViewCount<T>> {
|
|
92
|
+
return (client || this.pool).query('SELECT *, COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
93
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
94
|
+
.then(q => ({
|
|
95
|
+
views: q.rows.map(r => this.buildObject(r)),
|
|
96
|
+
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
delete(id: R, client: ClientBase): Promise<any> {
|
|
101
|
+
return client.query('DELETE FROM ' + this.table + ' WHERE id=$1', [id]);
|
|
102
|
+
}
|
|
103
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "anote-server-libs",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Helpers for express-TS servers",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://gitlab.anotemusic.com/anote/anote-server-libs.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"Express"
|
|
14
|
+
],
|
|
15
|
+
"author": "Mathonet Gregoire",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"express": "4.17.2",
|
|
19
|
+
"jsonschema": "1.4.0",
|
|
20
|
+
"memcached": "2.2.2",
|
|
21
|
+
"node-forge": "1.2.1",
|
|
22
|
+
"pg": "8.7.3",
|
|
23
|
+
"winston": "3.5.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "14.18.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {Request, Response} from 'express';
|
|
2
|
+
import {SchemaError, Validator} from 'jsonschema';
|
|
3
|
+
|
|
4
|
+
export function WithBody(schema: any) {
|
|
5
|
+
const validator = new Validator();
|
|
6
|
+
validator.attributes.maxDigits = (instance, sc: any) => {
|
|
7
|
+
if(typeof instance !== 'number') return undefined;
|
|
8
|
+
if(typeof sc.maxDigits !== 'number' || Math.floor(sc.maxDigits) !== sc.maxDigits) {
|
|
9
|
+
throw new SchemaError('"maxDigits" expects an integer', sc);
|
|
10
|
+
}
|
|
11
|
+
if(Math.round(instance * 10 ** sc.maxDigits) / (10 ** sc.maxDigits) !== instance) {
|
|
12
|
+
return 'has more precision than ' + sc.maxDigits + ' digits';
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return function(_: any, __: string, descriptor: PropertyDescriptor) {
|
|
18
|
+
if(typeof descriptor.value === 'function') {
|
|
19
|
+
const previousMethod = descriptor.value;
|
|
20
|
+
descriptor.value = function(this: any, req: Request, res: Response) {
|
|
21
|
+
const keys = Object.getOwnPropertyNames(schema.properties);
|
|
22
|
+
keys.forEach(key => {
|
|
23
|
+
if(typeof schema.properties[key] === 'string') {
|
|
24
|
+
schema.properties[key] = this.config.app.endpointsSchemas[schema.properties[key]];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
if(req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'PUT') {
|
|
28
|
+
return previousMethod.call(this, req, res);
|
|
29
|
+
}
|
|
30
|
+
if(!req.body) {
|
|
31
|
+
res.status(400).json({
|
|
32
|
+
error: {
|
|
33
|
+
errorKey: 'client.body.missing',
|
|
34
|
+
additionalInfo: 'client.extended.badPayload'
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
const result = validator.validate(req.body, schema);
|
|
39
|
+
if(result.valid) {
|
|
40
|
+
return previousMethod.call(this, req, res);
|
|
41
|
+
} else {
|
|
42
|
+
res.status(400).json({
|
|
43
|
+
error: {
|
|
44
|
+
errorKey: 'client.body.missing',
|
|
45
|
+
additionalInfo: 'client.extended.badPayload',
|
|
46
|
+
detailedInfo: result.errors
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return descriptor;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {NextFunction, Request, Response} from 'express';
|
|
2
|
+
import {Logger} from 'winston';
|
|
3
|
+
import {BaseModelRepository} from '../models/repository/BaseModelRepository';
|
|
4
|
+
import {utils} from './utils';
|
|
5
|
+
|
|
6
|
+
export const enum SystemLock {
|
|
7
|
+
CHECK_CROSSING = 1,
|
|
8
|
+
FLUSH_CALLS = 2
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function jsonStringify(obj: any): string {
|
|
12
|
+
const cache: any = {};
|
|
13
|
+
return JSON.stringify(obj, function(_, value) {
|
|
14
|
+
if(typeof value === 'object' && value !== null) {
|
|
15
|
+
if(cache[value] !== -1) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(JSON.stringify(value));
|
|
18
|
+
} catch(error) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
cache[value] = true;
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function withTransaction(repo: BaseModelRepository, logger: Logger, previousMethod: (req: Request, res: Response, next: NextFunction) => void, lock?: SystemLock) {
|
|
29
|
+
return function(req: Request, res: Response, next: NextFunction) {
|
|
30
|
+
const endTerminator = res.end.bind(res);
|
|
31
|
+
const jsonTerminator = (obj: any) => {
|
|
32
|
+
res.write(jsonStringify(obj) || '{}');
|
|
33
|
+
endTerminator();
|
|
34
|
+
};
|
|
35
|
+
const connectTimeoutHandler = setTimeout(() => {
|
|
36
|
+
// Timed out getting a client, restart worker or process...
|
|
37
|
+
logger.error('Error timed out getting a client, exiting...');
|
|
38
|
+
process.exit(22);
|
|
39
|
+
}, 3000);
|
|
40
|
+
repo.db.connect().then(dbClient => {
|
|
41
|
+
clearTimeout(connectTimeoutHandler);
|
|
42
|
+
// On error, will rollback...
|
|
43
|
+
utils.logger = logger;
|
|
44
|
+
dbClient.removeListener('error', utils.clientErrorHandler);
|
|
45
|
+
dbClient.on('error', utils.clientErrorHandler);
|
|
46
|
+
|
|
47
|
+
res.locals.dbClient = dbClient;
|
|
48
|
+
res.locals.dbClientCommited = false;
|
|
49
|
+
res.locals.dbClientCommit = (cb: (err: any) => any) => {
|
|
50
|
+
if(!res.locals.dbClientCommited) {
|
|
51
|
+
res.locals.dbClientCommited = true;
|
|
52
|
+
dbClient.query('COMMIT').catch(err => err).then((err: any) => {
|
|
53
|
+
dbClient.release();
|
|
54
|
+
if(!(err instanceof Error)) {
|
|
55
|
+
for(let i = 0; i < res.locals.dbClientOnCommit.length; i++) {
|
|
56
|
+
res.locals.dbClientOnCommit[i]();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
cb(err instanceof Error ? err : undefined);
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
cb(undefined);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
res.locals.dbClientOnCommit = [];
|
|
66
|
+
return dbClient.query('BEGIN').then(() => {
|
|
67
|
+
const finish = () => {
|
|
68
|
+
res.json = (obj: any) => {
|
|
69
|
+
if(res.statusCode > 303 && res.statusCode !== 412) {
|
|
70
|
+
if(logger && res.statusCode > 499) {
|
|
71
|
+
logger.error('Uncaught 500: %j', obj.error.additionalInfo);
|
|
72
|
+
}
|
|
73
|
+
dbClient.query('ROLLBACK').catch(err => obj.error.additionalInfo2 = {message: err.message}).then(() => {
|
|
74
|
+
dbClient.release();
|
|
75
|
+
jsonTerminator(obj);
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
res.locals.dbClientCommit((err: any) => {
|
|
79
|
+
if(err) {
|
|
80
|
+
res.status(500);
|
|
81
|
+
jsonTerminator({
|
|
82
|
+
error: {
|
|
83
|
+
errorKey: 'internal.db',
|
|
84
|
+
additionalInfo: {message: err.message}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
} else jsonTerminator(obj);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return res;
|
|
91
|
+
};
|
|
92
|
+
res.end = () => {
|
|
93
|
+
if(res.statusCode > 303 && res.statusCode !== 412) {
|
|
94
|
+
if(logger && res.statusCode > 499) {
|
|
95
|
+
logger.error('Uncaught 500 with no details...');
|
|
96
|
+
}
|
|
97
|
+
dbClient.query('ROLLBACK').catch((): any => undefined).then(() => {
|
|
98
|
+
dbClient.release();
|
|
99
|
+
endTerminator();
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
res.locals.dbClientCommit((err: any) => {
|
|
103
|
+
if(err) {
|
|
104
|
+
res.status(500);
|
|
105
|
+
jsonTerminator({
|
|
106
|
+
error: {
|
|
107
|
+
errorKey: 'internal.db',
|
|
108
|
+
additionalInfo: {message: err.message}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
} else endTerminator();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return res;
|
|
115
|
+
};
|
|
116
|
+
return previousMethod.call(this, req, res, next);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if(lock) {
|
|
120
|
+
dbClient.query('SELECT pg_advisory_xact_lock(' + lock + ')').then(() => finish()).catch(err => {
|
|
121
|
+
res.status(500).json({
|
|
122
|
+
error: {
|
|
123
|
+
errorKey: 'internal.db',
|
|
124
|
+
additionalInfo: {message: err.message}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
dbClient.release();
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
finish();
|
|
131
|
+
}
|
|
132
|
+
}).catch(err => {
|
|
133
|
+
dbClient.release();
|
|
134
|
+
throw err;
|
|
135
|
+
});
|
|
136
|
+
}).catch(err => {
|
|
137
|
+
// Error connecting to database, restarting worker after the timeout as well...
|
|
138
|
+
res.status(500).json({
|
|
139
|
+
error: {
|
|
140
|
+
errorKey: 'internal.db',
|
|
141
|
+
additionalInfo: {message: err.message}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {NextFunction, Request, Response} from 'express';
|
|
2
|
+
import {md} from 'node-forge';
|
|
3
|
+
import {Logger} from 'winston';
|
|
4
|
+
import {BaseModelRepository} from '../models/repository/BaseModelRepository';
|
|
5
|
+
|
|
6
|
+
export function atob(str: string): string {
|
|
7
|
+
return Buffer.from(str, 'base64').toString('binary');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function btoa(str: string): string {
|
|
11
|
+
return Buffer.from(str).toString('base64');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function clientErrorHandle(this: Logger, err: any) {
|
|
15
|
+
this.error('Error on DB client: %j', err);
|
|
16
|
+
}
|
|
17
|
+
export const utils: {[id: string]: any} = {
|
|
18
|
+
clientErrorHandler: undefined
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function gcdTwo(a: number, b: number): number {
|
|
22
|
+
if(a === 0)
|
|
23
|
+
return b;
|
|
24
|
+
return gcdTwo(b % a, a);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function gcd(values: number[]): number {
|
|
28
|
+
let result = values[0];
|
|
29
|
+
for(let i = 1; i < values.length; i++)
|
|
30
|
+
result = gcdTwo(values[i], result);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function lcm(values: number[]): number {
|
|
35
|
+
let l = 1, divisor = 2;
|
|
36
|
+
while(true) {
|
|
37
|
+
let counter = 0;
|
|
38
|
+
let divisible = false;
|
|
39
|
+
for(let i = 0; i < values.length; i++) {
|
|
40
|
+
if(values[i] === 0) {
|
|
41
|
+
return 0;
|
|
42
|
+
} else if(values[i] < 0) {
|
|
43
|
+
values[i] = values[i] * (-1);
|
|
44
|
+
}
|
|
45
|
+
if(values[i] === 1) {
|
|
46
|
+
counter++;
|
|
47
|
+
}
|
|
48
|
+
if(values[i] % divisor === 0) {
|
|
49
|
+
divisible = true;
|
|
50
|
+
values[i] = values[i] / divisor;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if(divisible) {
|
|
54
|
+
l = l * divisor;
|
|
55
|
+
} else {
|
|
56
|
+
divisor++;
|
|
57
|
+
}
|
|
58
|
+
if(counter === values.length) {
|
|
59
|
+
return l;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function idempotent(repo: BaseModelRepository, debug: boolean, logger: Logger) {
|
|
65
|
+
return function(req: Request, res: Response, next: NextFunction) {
|
|
66
|
+
let idempotenceKey = req.header('x-idempotent-key');
|
|
67
|
+
if(idempotenceKey) {
|
|
68
|
+
idempotenceKey = idempotenceKey.substring(0, 40);
|
|
69
|
+
repo.ApiCall.get(idempotenceKey).then(call => {
|
|
70
|
+
if(!call) {
|
|
71
|
+
const jsonTerminator = res.json;
|
|
72
|
+
const endTerminator = res.end;
|
|
73
|
+
res.json = (function(obj: any) {
|
|
74
|
+
repo.ApiCall.create({
|
|
75
|
+
id: idempotenceKey,
|
|
76
|
+
responseCode: res.statusCode,
|
|
77
|
+
responseJson: JSON.stringify(obj),
|
|
78
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
79
|
+
}).then(() => jsonTerminator(obj), err => {
|
|
80
|
+
if(err) logger.warn('Cannot save idempotent key: %j', err);
|
|
81
|
+
jsonTerminator(obj);
|
|
82
|
+
});
|
|
83
|
+
}).bind(res);
|
|
84
|
+
res.end = (function() {
|
|
85
|
+
repo.ApiCall.create({
|
|
86
|
+
id: idempotenceKey,
|
|
87
|
+
responseCode: res.statusCode,
|
|
88
|
+
responseJson: undefined,
|
|
89
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
90
|
+
}).then(() => endTerminator(), err => {
|
|
91
|
+
if(err) logger.warn('Cannot save idempotent key: %j', err);
|
|
92
|
+
endTerminator();
|
|
93
|
+
});
|
|
94
|
+
}).bind(res);
|
|
95
|
+
next();
|
|
96
|
+
} else res.status(417).json({
|
|
97
|
+
responseCode: call.responseCode,
|
|
98
|
+
responseBody: JSON.parse(call.responseJson)
|
|
99
|
+
});
|
|
100
|
+
}, err => res.status(500).json({
|
|
101
|
+
error: {
|
|
102
|
+
errorKey: 'internal.db',
|
|
103
|
+
additionalInfo: {message: err.message, stack: debug && err.stack}
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
106
|
+
} else next();
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function sendSelfPostableMessage(res: Response, code: number, messageType: string, err?: any) {
|
|
111
|
+
res.type('text/html').status(code).write(`
|
|
112
|
+
<!DOCTYPE HTML>
|
|
113
|
+
<html>
|
|
114
|
+
<head>
|
|
115
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
116
|
+
</head>
|
|
117
|
+
<body>
|
|
118
|
+
<script type="text/javascript">
|
|
119
|
+
window.parent.postMessage({
|
|
120
|
+
type: '${messageType}',
|
|
121
|
+
confirm: ${!err},
|
|
122
|
+
error: JSON.parse('${JSON.stringify(err) || 'null'}')
|
|
123
|
+
}, '*');
|
|
124
|
+
</script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
127
|
+
`);
|
|
128
|
+
res.end();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function fpEuros(n: number) {
|
|
132
|
+
return Math.round(n * 100) / 100;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function digitize(value: number | string, opts?: {[key: string]: any}): string {
|
|
136
|
+
if(value === undefined || value === null) return 'undefined';
|
|
137
|
+
if(typeof value === 'number') {
|
|
138
|
+
if(isNaN(value)) return '-';
|
|
139
|
+
if(!isFinite(value)) return 'Infinite';
|
|
140
|
+
value = String(value);
|
|
141
|
+
} else if(typeof value === 'string') return value;
|
|
142
|
+
|
|
143
|
+
const parts = value.split('.');
|
|
144
|
+
const initialLength = parts[0].length;
|
|
145
|
+
for(let i = initialLength - 3; i > 0; i -= 3) {
|
|
146
|
+
parts[0] = parts[0].slice(0, i) + ',' + parts[0].slice(i);
|
|
147
|
+
}
|
|
148
|
+
if(parts[0].startsWith('-,'))
|
|
149
|
+
parts[0] = '-' + parts[0].slice(2);
|
|
150
|
+
if(parts[1]) {
|
|
151
|
+
const expDecimals = parts[1].split(/[eE]-/);
|
|
152
|
+
if (expDecimals.length > 1 && parseInt(expDecimals[1], 10) > 2) return '0.00';
|
|
153
|
+
let decimals = fpEuros(parseFloat('0.' + parts[1])).toString().substr(2, 2);
|
|
154
|
+
if (decimals.length === 1) decimals += '0';
|
|
155
|
+
return parts[0] + '.' + decimals;
|
|
156
|
+
}
|
|
157
|
+
return (opts && opts.currency)? (parts[0] + '.00') : parts[0];
|
|
158
|
+
}
|