anote-server-libs 0.0.5 → 0.1.2
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.js +37 -0
- package/models/ApiCall.ts +16 -4
- package/models/Migration.js +35 -0
- package/models/Migration.ts +15 -4
- package/models/repository/BaseModelRepository.js +156 -0
- package/models/repository/BaseModelRepository.ts +37 -20
- package/models/repository/MemoryCache.js +92 -0
- package/models/repository/ModelDao.js +238 -0
- package/models/repository/ModelDao.ts +192 -36
- package/package.json +13 -8
- package/services/WithBody.js +60 -0
- package/services/WithTransaction.js +137 -0
- package/services/WithTransaction.ts +17 -29
- package/services/utils.js +188 -0
- package/services/utils.ts +18 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ModelDao = exports.promiseAllStepN = void 0;
|
|
4
|
+
function promiseAllStepN(n, list) {
|
|
5
|
+
if (!list || !list.length)
|
|
6
|
+
return Promise.resolve([]);
|
|
7
|
+
const tail = list.splice(n);
|
|
8
|
+
const head = list;
|
|
9
|
+
const resolved = [];
|
|
10
|
+
let processed = 0;
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
head.forEach(x => {
|
|
13
|
+
const res = x();
|
|
14
|
+
resolved.push(res);
|
|
15
|
+
res.then((y) => {
|
|
16
|
+
runNext();
|
|
17
|
+
return y;
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
function runNext() {
|
|
21
|
+
if (processed === tail.length) {
|
|
22
|
+
resolve(Promise.all(resolved));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
resolved.push(tail[processed]().then((x) => {
|
|
26
|
+
runNext();
|
|
27
|
+
return x;
|
|
28
|
+
}));
|
|
29
|
+
processed++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
exports.promiseAllStepN = promiseAllStepN;
|
|
35
|
+
Promise.allConcurrent = (n) => (list) => promiseAllStepN(n, list);
|
|
36
|
+
class Dao {
|
|
37
|
+
constructor(pool, poolMssql, logger, table, nFields, updateDefinition) {
|
|
38
|
+
this.pool = pool;
|
|
39
|
+
this.poolMssql = poolMssql;
|
|
40
|
+
this.logger = logger;
|
|
41
|
+
this.table = table;
|
|
42
|
+
this.nFields = nFields;
|
|
43
|
+
this.updateDefinition = updateDefinition;
|
|
44
|
+
}
|
|
45
|
+
groupResultSet(q, key) {
|
|
46
|
+
const storage = {};
|
|
47
|
+
for (let i = 0; i < q.length; i++) {
|
|
48
|
+
storage[q[i][key]] = storage[q[i][key]] || [];
|
|
49
|
+
storage[q[i][key]].push(q[i]);
|
|
50
|
+
}
|
|
51
|
+
return Object.getOwnPropertyNames(storage).map(k => storage[k]);
|
|
52
|
+
}
|
|
53
|
+
groupResultSetBy(q, key) {
|
|
54
|
+
const storage = {};
|
|
55
|
+
for (let i = 0; i < q.length; i++) {
|
|
56
|
+
storage[key(q[i])] = storage[key(q[i])] || [];
|
|
57
|
+
storage[key(q[i])].push(q[i]);
|
|
58
|
+
}
|
|
59
|
+
return Object.getOwnPropertyNames(storage).map(k => storage[k]);
|
|
60
|
+
}
|
|
61
|
+
update(instance, client) {
|
|
62
|
+
if (instance.archivedOn)
|
|
63
|
+
return Promise.reject('Record archived!');
|
|
64
|
+
instance.updatedOn = new Date();
|
|
65
|
+
if (this.pool) {
|
|
66
|
+
const props = this.serialize(instance);
|
|
67
|
+
props.push(instance.id);
|
|
68
|
+
return client.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=$' + (this.nFields + 1), props).then(() => instance.id);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const request = client.request();
|
|
72
|
+
this.serialize(instance, request);
|
|
73
|
+
request.input(String(this.nFields + 1), instance.id);
|
|
74
|
+
return request.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=@' + (this.nFields + 1)).then(() => instance.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
replace(instance, client) {
|
|
78
|
+
if (instance.archivedOn)
|
|
79
|
+
return Promise.reject('Record archived!');
|
|
80
|
+
return this.create({ ...instance, replaced_id: instance.id }, client).then(id => {
|
|
81
|
+
return this.archive(instance.id, id, client).then(() => id);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
create(instance, client) {
|
|
85
|
+
instance.createdOn = instance.updatedOn = new Date();
|
|
86
|
+
if (this.pool) {
|
|
87
|
+
const props = this.serialize(instance);
|
|
88
|
+
return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '') + ')'
|
|
89
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i) => '$' + (i + 1)).join(',') + ') RETURNING id', props).then(q => {
|
|
90
|
+
const idNum = parseInt(q.rows[0].id, 10);
|
|
91
|
+
if (String(idNum) !== q.rows[0].id)
|
|
92
|
+
return q.rows[0].id;
|
|
93
|
+
return idNum;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const request = (client || this.poolMssql).request();
|
|
98
|
+
this.serialize(instance, request);
|
|
99
|
+
return request.query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=@\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
100
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i) => '@' + (i + 1)).join(',') + '); SELECT SCOPE_IDENTITY() AS id').then(q => {
|
|
101
|
+
return q.recordsets[0][0].id || instance.id;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
createSeveral(instances, client) {
|
|
106
|
+
if (!instances.length)
|
|
107
|
+
return Promise.resolve([]);
|
|
108
|
+
const now = new Date();
|
|
109
|
+
instances.forEach(instance => instance.updatedOn = now);
|
|
110
|
+
const props = [].concat.apply([], instances.map(instance => this.serialize(instance)));
|
|
111
|
+
return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '') + ')'
|
|
112
|
+
+ ' VALUES' + instances.map((_, j) => ('(' + new Array(this.nFields).fill(undefined).map((__, i) => '$' + (j * this.nFields + i + 1)).join(', ') + ')')).join(',') + ' RETURNING id', props).then(q => q.rows.map(r => {
|
|
113
|
+
const idNum = parseInt(r.id, 10);
|
|
114
|
+
if (String(idNum) !== q.rows[0].id)
|
|
115
|
+
return r.id;
|
|
116
|
+
return idNum;
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
class ModelDao extends Dao {
|
|
121
|
+
get(id, client, lock = true) {
|
|
122
|
+
if (this.pool) {
|
|
123
|
+
return (client || this.pool).query('SELECT * FROM ' + this.table + ' WHERE id=$1' + ((client && lock) ? ' FOR UPDATE' : ''), [id]).then(q => this.buildObject(q.rows[0]));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const request = (client || this.poolMssql).request();
|
|
127
|
+
request.input('1', id);
|
|
128
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + ' WHERE id=@1').then(q => this.buildObject(q.recordsets[0][0]));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
count(where, inputs = [], client) {
|
|
132
|
+
if (this.pool) {
|
|
133
|
+
return (client || this.pool).query('SELECT count(*) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''), inputs).then(q => parseInt(q.rows[0].cnt, 10));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const request = (client || this.poolMssql).request();
|
|
137
|
+
if (where)
|
|
138
|
+
where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
139
|
+
return request.query('SELECT count(*) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '')).then(q => q.recordsets[0][0].cnt);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
getList(ids, client, lock = true) {
|
|
143
|
+
if (this.pool) {
|
|
144
|
+
return (client || this.pool).query('SELECT * FROM ' + this.table + ' WHERE id=ANY($1)' + ((client && lock) ? ' FOR UPDATE' : ''), [ids])
|
|
145
|
+
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const request = (client || this.poolMssql).request();
|
|
149
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + ' WHERE id IN ('
|
|
150
|
+
+ (ids.length > 0 ? (typeof ids[0] === 'string' ? '\'' + ids.join('\',\'') + '\'' : ids.join(',')) : '') + ')')
|
|
151
|
+
.then(q => q.recordsets[0].map(r => this.buildObject(r)));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
getAllBy(order, offset, limit, where, inputs = [], client, lock = true) {
|
|
155
|
+
if (this.pool) {
|
|
156
|
+
return (client || this.pool).query('SELECT * FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
157
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
158
|
+
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const request = (client || this.poolMssql).request();
|
|
162
|
+
if (where)
|
|
163
|
+
where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
164
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
165
|
+
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : ''))
|
|
166
|
+
.then(q => q.recordsets[0].map(r => this.buildObject(r)));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
getViewCountBy(order, offset, limit, where, inputs = [], client, lock = true) {
|
|
170
|
+
if (this.pool) {
|
|
171
|
+
return (client || this.pool).query('SELECT *, COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
172
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
173
|
+
.then(q => ({
|
|
174
|
+
views: q.rows.map(r => this.buildObject(r)),
|
|
175
|
+
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const request = (client || this.poolMssql).request();
|
|
180
|
+
if (where)
|
|
181
|
+
where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
182
|
+
return Promise.allConcurrent(1)([
|
|
183
|
+
() => request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
184
|
+
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
185
|
+
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
186
|
+
]).then(([q1, q2]) => ({
|
|
187
|
+
views: q1.recordsets[0].map((r) => this.buildObject(r)),
|
|
188
|
+
count: q2.recordsets.length ? q2.recordsets[0].reduce((p, n) => p + n.cnt, 0) : 0
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
getRowsViewCountBy(rows, order, offset, limit, where, inputs = [], client, lock = true) {
|
|
193
|
+
if (this.pool) {
|
|
194
|
+
return (client || this.pool).query('SELECT ' + rows.map(r => '"' + r + '"').join(',') + ', COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
195
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
196
|
+
.then(q => ({
|
|
197
|
+
views: q.rows.map(r => this.buildObject(r)),
|
|
198
|
+
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const request = (client || this.poolMssql).request();
|
|
203
|
+
if (where)
|
|
204
|
+
where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
205
|
+
return Promise.allConcurrent(1)([
|
|
206
|
+
() => request.query('SELECT ' + rows.map(r => '"' + r + '"').join(',') + ' FROM ' + this.table
|
|
207
|
+
+ ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
208
|
+
+ (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
209
|
+
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
210
|
+
]).then(([q1, q2]) => ({
|
|
211
|
+
views: q1.recordsets[0].map((r) => this.buildObject(r)),
|
|
212
|
+
count: q2.recordsets.length ? q2.recordsets[0].reduce((p, n) => p + n.cnt, 0) : 0
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
archive(id, replacedById, client) {
|
|
217
|
+
if (this.pool) {
|
|
218
|
+
return client.query('UPDATE ' + this.table + ' SET "archivedOn"=now(),replaced_by_id=$1 WHERE id=$2', [replacedById, id]);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const request = client.request();
|
|
222
|
+
request.input('1', replacedById);
|
|
223
|
+
request.input('2', id);
|
|
224
|
+
return request.query('UPDATE ' + this.table + ' SET "archivedOn"=now(),replaced_by_id=@1 WHERE id=@2');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
delete(id, client) {
|
|
228
|
+
if (this.pool) {
|
|
229
|
+
return client.query('DELETE FROM ' + this.table + ' WHERE id=$1', [id]);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
const request = client.request();
|
|
233
|
+
request.input('1', id);
|
|
234
|
+
return request.query('DELETE FROM ' + this.table + ' WHERE id=@1');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
exports.ModelDao = ModelDao;
|
|
@@ -1,11 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
import { ConnectionPool, Request, Transaction } from 'mssql';
|
|
1
3
|
import {ClientBase, Pool} from 'pg';
|
|
2
4
|
import {Logger} from 'winston';
|
|
3
5
|
|
|
6
|
+
declare const Promise: PromiseConstructor & {
|
|
7
|
+
allConcurrent: <T>(n: number) => ((promiseProxies: (() => Promise<T>)[]) => Promise<T[]>);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function promiseAllStepN<T>(n: number, list: (() => Promise<T>)[]): Promise<T[]> {
|
|
11
|
+
if(!list || !list.length) return Promise.resolve([]);
|
|
12
|
+
const tail = list.splice(n);
|
|
13
|
+
const head = list;
|
|
14
|
+
const resolved: any[] = [];
|
|
15
|
+
let processed = 0;
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
head.forEach(x => {
|
|
18
|
+
const res = x();
|
|
19
|
+
resolved.push(res);
|
|
20
|
+
res.then((y: any) => {
|
|
21
|
+
runNext();
|
|
22
|
+
return y;
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
function runNext() {
|
|
26
|
+
if(processed === tail.length) {
|
|
27
|
+
resolve(Promise.all(resolved));
|
|
28
|
+
} else {
|
|
29
|
+
resolved.push(tail[processed]().then((x: any) => {
|
|
30
|
+
runNext();
|
|
31
|
+
return x;
|
|
32
|
+
}));
|
|
33
|
+
processed++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
Promise.allConcurrent = <T>(n: number) => (list: (() => Promise<T>)[]) => promiseAllStepN(n, list);
|
|
39
|
+
|
|
4
40
|
export interface Model<T> {
|
|
5
41
|
id?: T;
|
|
6
42
|
updatedOn?: Date;
|
|
7
43
|
}
|
|
8
44
|
|
|
45
|
+
export interface ArchivedModel<T> extends Model<T> {
|
|
46
|
+
createdOn?: Date;
|
|
47
|
+
archivedOn?: Date;
|
|
48
|
+
replaced_id?: T;
|
|
49
|
+
replaced_by_id?: T;
|
|
50
|
+
}
|
|
51
|
+
|
|
9
52
|
export interface ModelRepr {
|
|
10
53
|
table: string;
|
|
11
54
|
}
|
|
@@ -16,11 +59,20 @@ export interface ViewCount<T> {
|
|
|
16
59
|
}
|
|
17
60
|
|
|
18
61
|
abstract class Dao<R, T extends Model<R>> implements ModelRepr {
|
|
19
|
-
constructor(protected pool: Pool, protected logger: Logger, public table: string, protected nFields: number,
|
|
62
|
+
constructor(protected pool: Pool, protected poolMssql: ConnectionPool, protected logger: Logger, public table: string, protected nFields: number,
|
|
20
63
|
protected updateDefinition: string) {
|
|
21
64
|
}
|
|
22
65
|
|
|
23
|
-
groupResultSet(q: any[], key:
|
|
66
|
+
groupResultSet(q: any[], key: string): any[][] {
|
|
67
|
+
const storage = {};
|
|
68
|
+
for(let i = 0; i < q.length; i++) {
|
|
69
|
+
storage[q[i][key]] = storage[q[i][key]] || [];
|
|
70
|
+
storage[q[i][key]].push(q[i]);
|
|
71
|
+
}
|
|
72
|
+
return Object.getOwnPropertyNames(storage).map(k => storage[k]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
groupResultSetBy(q: any[], key: (qs: any) => string): any[][] {
|
|
24
76
|
const storage = {};
|
|
25
77
|
for(let i = 0; i < q.length; i++) {
|
|
26
78
|
storage[key(q[i])] = storage[key(q[i])] || [];
|
|
@@ -29,23 +81,47 @@ abstract class Dao<R, T extends Model<R>> implements ModelRepr {
|
|
|
29
81
|
return Object.getOwnPropertyNames(storage).map(k => storage[k]);
|
|
30
82
|
}
|
|
31
83
|
|
|
32
|
-
update(instance: T, client: ClientBase): Promise<R> {
|
|
84
|
+
update(instance: T, client: ClientBase | ConnectionPool | Transaction): Promise<R> {
|
|
85
|
+
if((<any>instance).archivedOn) return Promise.reject('Record archived!');
|
|
33
86
|
instance.updatedOn = new Date();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
if(this.pool) {
|
|
88
|
+
const props = this.serialize(instance);
|
|
89
|
+
props.push(instance.id);
|
|
90
|
+
return (<ClientBase>client).query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=$' + (this.nFields + 1), props).then(() => instance.id);
|
|
91
|
+
} else {
|
|
92
|
+
const request = (<ConnectionPool | Transaction>client).request();
|
|
93
|
+
this.serialize(instance, request);
|
|
94
|
+
request.input(String(this.nFields + 1), instance.id);
|
|
95
|
+
return request.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=@' + (this.nFields + 1)).then(() => instance.id);
|
|
96
|
+
}
|
|
37
97
|
}
|
|
38
98
|
|
|
39
|
-
|
|
40
|
-
instance.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
99
|
+
replace(this: ModelDao<R, T>, instance: T, client: ClientBase | Transaction): Promise<R> {
|
|
100
|
+
if((<any>instance).archivedOn) return Promise.reject('Record archived!');
|
|
101
|
+
return this.create({...instance, replaced_id: instance.id}, client).then(id => {
|
|
102
|
+
return this.archive(instance.id, id, client).then(() => id);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
create(instance: T, client?: ClientBase | Transaction): Promise<R> {
|
|
107
|
+
(<any>instance).createdOn = instance.updatedOn = new Date();
|
|
108
|
+
if(this.pool) {
|
|
109
|
+
const props = this.serialize(instance);
|
|
110
|
+
return (<ClientBase | Pool>(client || this.pool)).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '') + ')'
|
|
111
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '$' + (i + 1)).join(',') + ') RETURNING id',
|
|
112
|
+
props).then(q => {
|
|
113
|
+
const idNum = parseInt(q.rows[0].id, 10);
|
|
114
|
+
if(String(idNum) !== q.rows[0].id) return q.rows[0].id;
|
|
115
|
+
return idNum;
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
119
|
+
this.serialize(instance, request);
|
|
120
|
+
return request.query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=@\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
121
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '@' + (i + 1)).join(',') + '); SELECT SCOPE_IDENTITY() AS id').then(q => {
|
|
122
|
+
return q.recordsets[0][0].id || instance.id;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
49
125
|
}
|
|
50
126
|
|
|
51
127
|
createSeveral(instances: T[], client?: ClientBase): Promise<R[]> {
|
|
@@ -65,39 +141,119 @@ abstract class Dao<R, T extends Model<R>> implements ModelRepr {
|
|
|
65
141
|
|
|
66
142
|
protected abstract buildObject(q: any): T;
|
|
67
143
|
|
|
68
|
-
protected abstract serialize(instance: T): any[];
|
|
144
|
+
protected abstract serialize(instance: T, request?: Request): any[];
|
|
69
145
|
}
|
|
70
146
|
|
|
71
147
|
export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
72
|
-
get(id: R, client?: ClientBase, lock = true): Promise<T> {
|
|
73
|
-
|
|
148
|
+
get(id: R, client?: ClientBase | Transaction, lock = true): Promise<T> {
|
|
149
|
+
if(this.pool) {
|
|
150
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT * FROM ' + this.table + ' WHERE id=$1' + ((client && lock) ? ' FOR UPDATE' : ''), [id]).then(q => this.buildObject(q.rows[0]));
|
|
151
|
+
} else {
|
|
152
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
153
|
+
request.input('1', id);
|
|
154
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + ' WHERE id=@1').then(q => this.buildObject(q.recordsets[0][0]));
|
|
155
|
+
}
|
|
74
156
|
}
|
|
75
157
|
|
|
76
|
-
count(where?: string, inputs: any[] = [], client?: ClientBase): Promise<number> {
|
|
77
|
-
|
|
158
|
+
count(where?: string, inputs: any[] = [], client?: ClientBase | Transaction): Promise<number> {
|
|
159
|
+
if(this.pool) {
|
|
160
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT count(*) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''), inputs).then(q => parseInt(q.rows[0].cnt, 10));
|
|
161
|
+
} else {
|
|
162
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
163
|
+
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
164
|
+
return request.query('SELECT count(*) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '')).then(q => q.recordsets[0][0].cnt);
|
|
165
|
+
}
|
|
78
166
|
}
|
|
79
167
|
|
|
80
|
-
getList(ids: R[], client?: ClientBase, lock = true): Promise<T[]> {
|
|
81
|
-
|
|
82
|
-
|
|
168
|
+
getList(ids: R[], client?: ClientBase | Transaction, lock = true): Promise<T[]> {
|
|
169
|
+
if(this.pool) {
|
|
170
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT * FROM ' + this.table + ' WHERE id=ANY($1)' + ((client && lock) ? ' FOR UPDATE' : ''), [ids])
|
|
171
|
+
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
172
|
+
} else {
|
|
173
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
174
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + ' WHERE id IN ('
|
|
175
|
+
+ (ids.length > 0 ? (typeof ids[0] === 'string' ? '\'' + ids.join('\',\'') + '\'' : ids.join(',')) : '') + ')')
|
|
176
|
+
.then(q => q.recordsets[0].map(r => this.buildObject(r)));
|
|
177
|
+
}
|
|
83
178
|
}
|
|
84
179
|
|
|
85
|
-
getAllBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase, lock = true): Promise<T[]> {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
180
|
+
getAllBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<T[]> {
|
|
181
|
+
if(this.pool) {
|
|
182
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT * FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
183
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
184
|
+
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
185
|
+
} else {
|
|
186
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
187
|
+
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
188
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
189
|
+
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : ''))
|
|
190
|
+
.then(q => q.recordsets[0].map(r => this.buildObject(r)));
|
|
191
|
+
}
|
|
89
192
|
}
|
|
90
193
|
|
|
91
|
-
getViewCountBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase, lock = true): Promise<ViewCount<T>> {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
194
|
+
getViewCountBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<ViewCount<T>> {
|
|
195
|
+
if(this.pool) {
|
|
196
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT *, COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
197
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
198
|
+
.then(q => ({
|
|
199
|
+
views: q.rows.map(r => this.buildObject(r)),
|
|
200
|
+
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
201
|
+
}));
|
|
202
|
+
} else {
|
|
203
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
204
|
+
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
205
|
+
return Promise.allConcurrent(1)([
|
|
206
|
+
() => request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
207
|
+
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
208
|
+
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
209
|
+
]).then(([q1, q2]: [any, any]) => ({
|
|
210
|
+
views: q1.recordsets[0].map((r: any) => this.buildObject(r)),
|
|
211
|
+
count: q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0
|
|
97
212
|
}));
|
|
213
|
+
}
|
|
98
214
|
}
|
|
99
215
|
|
|
100
|
-
|
|
101
|
-
|
|
216
|
+
getRowsViewCountBy(rows: string[], order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<ViewCount<any>> {
|
|
217
|
+
if(this.pool) {
|
|
218
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT ' + rows.map(r => '"' + r + '"').join(',') + ', COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
219
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
220
|
+
.then(q => ({
|
|
221
|
+
views: q.rows.map(r => this.buildObject(r)),
|
|
222
|
+
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
223
|
+
}));
|
|
224
|
+
} else {
|
|
225
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
226
|
+
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
227
|
+
return Promise.allConcurrent(1)([
|
|
228
|
+
() => request.query('SELECT ' + rows.map(r => '"' + r + '"').join(',') + ' FROM ' + this.table
|
|
229
|
+
+ ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
230
|
+
+ (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
231
|
+
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
232
|
+
]).then(([q1, q2]: [any, any]) => ({
|
|
233
|
+
views: q1.recordsets[0].map((r: any) => this.buildObject(r)),
|
|
234
|
+
count: q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
archive(id: R, replacedById: R, client: ClientBase | Transaction): Promise<any> {
|
|
240
|
+
if(this.pool) {
|
|
241
|
+
return (<ClientBase>client).query('UPDATE ' + this.table + ' SET "archivedOn"=now(),replaced_by_id=$1 WHERE id=$2', [replacedById, id]);
|
|
242
|
+
} else {
|
|
243
|
+
const request = (<Transaction>client).request();
|
|
244
|
+
request.input('1', replacedById);
|
|
245
|
+
request.input('2', id);
|
|
246
|
+
return request.query('UPDATE ' + this.table + ' SET "archivedOn"=now(),replaced_by_id=@1 WHERE id=@2');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
delete(id: R, client: ClientBase | Transaction): Promise<any> {
|
|
251
|
+
if(this.pool) {
|
|
252
|
+
return (<ClientBase>client).query('DELETE FROM ' + this.table + ' WHERE id=$1', [id]);
|
|
253
|
+
} else {
|
|
254
|
+
const request = (<Transaction>client).request();
|
|
255
|
+
request.input('1', id);
|
|
256
|
+
return request.query('DELETE FROM ' + this.table + ' WHERE id=@1');
|
|
257
|
+
}
|
|
102
258
|
}
|
|
103
259
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anote-server-libs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Helpers for express-TS servers",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
7
|
+
"build": "tsc -p tsconfig.json"
|
|
7
8
|
},
|
|
8
9
|
"repository": {
|
|
9
10
|
"type": "git",
|
|
@@ -15,19 +16,23 @@
|
|
|
15
16
|
"author": "Mathonet Gregoire",
|
|
16
17
|
"license": "MIT",
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"@types/pg": "8.6.4",
|
|
19
|
+
"mssql": "8.0.2",
|
|
20
|
+
"pg": "8.7.3"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
23
|
"express": "4.17.2",
|
|
24
24
|
"jsonschema": "1.4.0",
|
|
25
25
|
"memcached": "2.2.2",
|
|
26
26
|
"node-forge": "1.2.1",
|
|
27
|
-
"pg": "8.7.3",
|
|
28
27
|
"winston": "3.5.1"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
30
|
+
"@types/express": "4.17.13",
|
|
31
|
+
"@types/memcached": "2.2.7",
|
|
32
|
+
"@types/mssql": "7.1.4",
|
|
33
|
+
"@types/node": "14.18.2",
|
|
34
|
+
"@types/node-forge": "1.0.0",
|
|
35
|
+
"@types/pg": "8.6.4",
|
|
31
36
|
"typescript": "4.5.5"
|
|
32
37
|
}
|
|
33
38
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WithBody = void 0;
|
|
4
|
+
const jsonschema_1 = require("jsonschema");
|
|
5
|
+
function WithBody(schema) {
|
|
6
|
+
const validator = new jsonschema_1.Validator();
|
|
7
|
+
validator.attributes.maxDigits = (instance, sc) => {
|
|
8
|
+
if (typeof instance !== 'number')
|
|
9
|
+
return undefined;
|
|
10
|
+
if (typeof sc.maxDigits !== 'number' || Math.floor(sc.maxDigits) !== sc.maxDigits) {
|
|
11
|
+
throw new jsonschema_1.SchemaError('"maxDigits" expects an integer', sc);
|
|
12
|
+
}
|
|
13
|
+
if (Math.round(instance * 10 ** sc.maxDigits) / (10 ** sc.maxDigits) !== instance) {
|
|
14
|
+
return 'has more precision than ' + sc.maxDigits + ' digits';
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
};
|
|
18
|
+
return function (_, __, descriptor) {
|
|
19
|
+
if (typeof descriptor.value === 'function') {
|
|
20
|
+
const previousMethod = descriptor.value;
|
|
21
|
+
descriptor.value = function (req, res) {
|
|
22
|
+
const keys = Object.getOwnPropertyNames(schema.properties);
|
|
23
|
+
keys.forEach(key => {
|
|
24
|
+
if (typeof schema.properties[key] === 'string') {
|
|
25
|
+
schema.properties[key] = this.config.app.endpointsSchemas[schema.properties[key]];
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
if (req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'PUT') {
|
|
29
|
+
return previousMethod.call(this, req, res);
|
|
30
|
+
}
|
|
31
|
+
if (!req.body) {
|
|
32
|
+
res.status(400).json({
|
|
33
|
+
error: {
|
|
34
|
+
errorKey: 'client.body.missing',
|
|
35
|
+
additionalInfo: 'client.extended.badPayload'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const result = validator.validate(req.body, schema);
|
|
41
|
+
if (result.valid) {
|
|
42
|
+
return previousMethod.call(this, req, res);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
res.status(400).json({
|
|
46
|
+
error: {
|
|
47
|
+
errorKey: 'client.body.missing',
|
|
48
|
+
additionalInfo: 'client.extended.badPayload',
|
|
49
|
+
detailedInfo: result.errors
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return descriptor;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
exports.WithBody = WithBody;
|