anote-server-libs 0.2.7 → 0.2.8
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 +40 -40
- package/models/Migration.ts +40 -40
- package/models/repository/BaseModelRepository.ts +152 -152
- package/models/repository/MemoryCache.ts +87 -87
- package/models/repository/ModelDao.ts +259 -259
- package/package.json +38 -38
- package/services/WithBody.ts +56 -56
- package/services/WithTransaction.ts +130 -134
- package/services/utils.ts +180 -180
- package/tsconfig.json +30 -30
- package/models/ApiCall.js +0 -37
- package/models/Migration.js +0 -35
- package/models/repository/BaseModelRepository.js +0 -156
- package/models/repository/MemoryCache.js +0 -92
- package/models/repository/ModelDao.js +0 -239
- package/services/WithBody.js +0 -60
- package/services/WithTransaction.js +0 -137
- package/services/utils.js +0 -197
|
@@ -1,259 +1,259 @@
|
|
|
1
|
-
|
|
2
|
-
import { ConnectionPool, Request, Transaction } from 'mssql';
|
|
3
|
-
import {ClientBase, Pool} from 'pg';
|
|
4
|
-
import {Logger} from 'winston';
|
|
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
|
-
|
|
40
|
-
export interface Model<T> {
|
|
41
|
-
id?: T;
|
|
42
|
-
updatedOn?: Date;
|
|
43
|
-
}
|
|
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
|
-
|
|
52
|
-
export interface ModelRepr {
|
|
53
|
-
table: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface ViewCount<T> {
|
|
57
|
-
views: T[];
|
|
58
|
-
count: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export abstract class Dao<R, T extends Model<R>> implements ModelRepr {
|
|
62
|
-
constructor(protected pool: Pool, protected poolMssql: ConnectionPool, protected logger: Logger, public table: string, protected nFields: number,
|
|
63
|
-
protected updateDefinition: string) {
|
|
64
|
-
}
|
|
65
|
-
|
|
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[][] {
|
|
76
|
-
const storage = {};
|
|
77
|
-
for(let i = 0; i < q.length; i++) {
|
|
78
|
-
storage[key(q[i])] = storage[key(q[i])] || [];
|
|
79
|
-
storage[key(q[i])].push(q[i]);
|
|
80
|
-
}
|
|
81
|
-
return Object.getOwnPropertyNames(storage).map(k => storage[k]);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
update(instance: T, client: ClientBase | ConnectionPool | Transaction): Promise<R> {
|
|
85
|
-
if((<any>instance).archivedOn) return Promise.reject('Record archived!');
|
|
86
|
-
instance.updatedOn = new Date();
|
|
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
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
create(instance: T, client?: ClientBase | Transaction): Promise<R> {
|
|
100
|
-
(<any>instance).createdOn = instance.updatedOn = new Date();
|
|
101
|
-
if(this.pool) {
|
|
102
|
-
const props = this.serialize(instance);
|
|
103
|
-
return (<ClientBase | Pool>(client || this.pool)).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
104
|
-
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '$' + (i + 1)).join(',') + ') RETURNING id',
|
|
105
|
-
props).then(q => {
|
|
106
|
-
const idNum = parseInt(q.rows[0].id, 10);
|
|
107
|
-
if(String(idNum) !== q.rows[0].id) return q.rows[0].id;
|
|
108
|
-
return idNum;
|
|
109
|
-
});
|
|
110
|
-
} else {
|
|
111
|
-
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
112
|
-
this.serialize(instance, request);
|
|
113
|
-
return request.query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=@\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
114
|
-
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '@' + (i + 1)).join(',') + '); SELECT SCOPE_IDENTITY() AS id').then(q => {
|
|
115
|
-
return q.recordsets[0][0].id || instance.id;
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
createSeveral(instances: T[], client?: ClientBase): Promise<R[]> {
|
|
121
|
-
if(!instances.length) return Promise.resolve([]);
|
|
122
|
-
const now = new Date();
|
|
123
|
-
instances.forEach(instance => instance.updatedOn = now);
|
|
124
|
-
const props = [].concat.apply([], instances.map(instance => this.serialize(instance)));
|
|
125
|
-
return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
126
|
-
+ ' VALUES' + instances.map((_, j) =>
|
|
127
|
-
('(' + new Array(this.nFields).fill(undefined).map((__, i: number) => '$' + (j * this.nFields + i + 1)).join(', ') + ')')).join(',') + ' RETURNING id',
|
|
128
|
-
props).then(q => q.rows.map(r => {
|
|
129
|
-
const idNum = parseInt(r.id, 10);
|
|
130
|
-
if(String(idNum) !== q.rows[0].id) return r.id;
|
|
131
|
-
return idNum;
|
|
132
|
-
}));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
protected abstract buildObject(q: any): T;
|
|
136
|
-
|
|
137
|
-
protected abstract serialize(instance: T, request?: Request): any[];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
141
|
-
get(id: R, client?: ClientBase | Transaction, lock = true): Promise<T> {
|
|
142
|
-
if(this.pool) {
|
|
143
|
-
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]));
|
|
144
|
-
} else {
|
|
145
|
-
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
146
|
-
request.input('1', id);
|
|
147
|
-
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + ' WHERE id=@1').then(q => this.buildObject(q.recordsets[0][0]));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
count(where?: string, inputs: any[] = [], client?: ClientBase | Transaction): Promise<number> {
|
|
152
|
-
if(this.pool) {
|
|
153
|
-
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));
|
|
154
|
-
} else {
|
|
155
|
-
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
156
|
-
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
157
|
-
return request.query('SELECT count(*) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '')).then(q => q.recordsets[0][0].cnt);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
getList(ids: R[], client?: ClientBase | Transaction, lock = true): Promise<T[]> {
|
|
162
|
-
if(this.pool) {
|
|
163
|
-
return (<ClientBase | Pool>(client || this.pool)).query('SELECT * FROM ' + this.table + ' WHERE id=ANY($1)' + ((client && lock) ? ' FOR UPDATE' : ''), [ids])
|
|
164
|
-
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
165
|
-
} else {
|
|
166
|
-
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
167
|
-
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + ' WHERE id IN ('
|
|
168
|
-
+ (ids.length > 0 ? (typeof ids[0] === 'string' ? '\'' + ids.join('\',\'') + '\'' : ids.join(',')) : '') + ')')
|
|
169
|
-
.then(q => q.recordsets[0].map(r => this.buildObject(r)));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
getAllBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<T[]> {
|
|
174
|
-
if(this.pool) {
|
|
175
|
-
return (<ClientBase | Pool>(client || this.pool)).query('SELECT * FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
176
|
-
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
177
|
-
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
178
|
-
} else {
|
|
179
|
-
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
180
|
-
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
181
|
-
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
182
|
-
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : ''))
|
|
183
|
-
.then(q => q.recordsets[0].map(r => this.buildObject(r)));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
getViewCountBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<ViewCount<T>> {
|
|
188
|
-
if(this.pool) {
|
|
189
|
-
return (<ClientBase | Pool>(client || this.pool)).query('SELECT *, COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
190
|
-
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
191
|
-
.then(q => ({
|
|
192
|
-
views: q.rows.map(r => this.buildObject(r)),
|
|
193
|
-
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
194
|
-
}));
|
|
195
|
-
} else {
|
|
196
|
-
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
197
|
-
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
198
|
-
return Promise.allConcurrent(1)([
|
|
199
|
-
() => request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
200
|
-
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
201
|
-
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
202
|
-
]).then(([q1, q2]: [any, any]) => ({
|
|
203
|
-
views: q1.recordsets[0].map((r: any) => this.buildObject(r)),
|
|
204
|
-
count: q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0
|
|
205
|
-
}));
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
getRowsViewCountBy(rows: string[], order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<ViewCount<any>> {
|
|
210
|
-
if(this.pool) {
|
|
211
|
-
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) : '')
|
|
212
|
-
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
213
|
-
.then(q => ({
|
|
214
|
-
views: q.rows.map(r => this.buildObject(r)),
|
|
215
|
-
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
216
|
-
}));
|
|
217
|
-
} else {
|
|
218
|
-
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
219
|
-
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
220
|
-
return Promise.allConcurrent(1)([
|
|
221
|
-
() => request.query('SELECT ' + rows.map(r => '"' + r + '"').join(',') + ' FROM ' + this.table
|
|
222
|
-
+ ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
223
|
-
+ (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
224
|
-
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
225
|
-
]).then(([q1, q2]: [any, any]) => ({
|
|
226
|
-
views: q1.recordsets[0].map((r: any) => this.buildObject(r)),
|
|
227
|
-
count: q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0
|
|
228
|
-
}));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
archive(id: R, replacedById: R, client: ClientBase | Transaction): Promise<any> {
|
|
233
|
-
if(this.pool) {
|
|
234
|
-
return (<ClientBase>client).query('UPDATE ' + this.table + ' SET "archivedOn"=now(),replaced_by_id=$1 WHERE id=$2', [replacedById, id]);
|
|
235
|
-
} else {
|
|
236
|
-
const request = (<Transaction>client).request();
|
|
237
|
-
request.input('1', replacedById);
|
|
238
|
-
request.input('2', id);
|
|
239
|
-
return request.query('UPDATE ' + this.table + ' SET "archivedOn"=now(),replaced_by_id=@1 WHERE id=@2');
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
replace(instance: T, client: ClientBase | Transaction): Promise<R> {
|
|
244
|
-
if((<any>instance).archivedOn) return Promise.reject('Record archived!');
|
|
245
|
-
return this.create({...instance, replaced_id: instance.id}, client).then(id => {
|
|
246
|
-
return this.archive(instance.id, id, client).then(() => id);
|
|
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
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
1
|
+
|
|
2
|
+
import { ConnectionPool, Request, Transaction } from 'mssql';
|
|
3
|
+
import {ClientBase, Pool} from 'pg';
|
|
4
|
+
import {Logger} from 'winston';
|
|
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
|
+
|
|
40
|
+
export interface Model<T> {
|
|
41
|
+
id?: T;
|
|
42
|
+
updatedOn?: Date;
|
|
43
|
+
}
|
|
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
|
+
|
|
52
|
+
export interface ModelRepr {
|
|
53
|
+
table: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ViewCount<T> {
|
|
57
|
+
views: T[];
|
|
58
|
+
count: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export abstract class Dao<R, T extends Model<R>> implements ModelRepr {
|
|
62
|
+
constructor(protected pool: Pool, protected poolMssql: ConnectionPool, protected logger: Logger, public table: string, protected nFields: number,
|
|
63
|
+
protected updateDefinition: string) {
|
|
64
|
+
}
|
|
65
|
+
|
|
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[][] {
|
|
76
|
+
const storage = {};
|
|
77
|
+
for(let i = 0; i < q.length; i++) {
|
|
78
|
+
storage[key(q[i])] = storage[key(q[i])] || [];
|
|
79
|
+
storage[key(q[i])].push(q[i]);
|
|
80
|
+
}
|
|
81
|
+
return Object.getOwnPropertyNames(storage).map(k => storage[k]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
update(instance: T, client: ClientBase | ConnectionPool | Transaction): Promise<R> {
|
|
85
|
+
if((<any>instance).archivedOn) return Promise.reject('Record archived!');
|
|
86
|
+
instance.updatedOn = new Date();
|
|
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
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
create(instance: T, client?: ClientBase | Transaction): Promise<R> {
|
|
100
|
+
(<any>instance).createdOn = instance.updatedOn = new Date();
|
|
101
|
+
if(this.pool) {
|
|
102
|
+
const props = this.serialize(instance);
|
|
103
|
+
return (<ClientBase | Pool>(client || this.pool)).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
104
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '$' + (i + 1)).join(',') + ') RETURNING id',
|
|
105
|
+
props).then(q => {
|
|
106
|
+
const idNum = parseInt(q.rows[0].id, 10);
|
|
107
|
+
if(String(idNum) !== q.rows[0].id) return q.rows[0].id;
|
|
108
|
+
return idNum;
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
112
|
+
this.serialize(instance, request);
|
|
113
|
+
return request.query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=@\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
114
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '@' + (i + 1)).join(',') + '); SELECT SCOPE_IDENTITY() AS id').then(q => {
|
|
115
|
+
return q.recordsets[0][0].id || instance.id;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
createSeveral(instances: T[], client?: ClientBase): Promise<R[]> {
|
|
121
|
+
if(!instances.length) return Promise.resolve([]);
|
|
122
|
+
const now = new Date();
|
|
123
|
+
instances.forEach(instance => instance.updatedOn = now);
|
|
124
|
+
const props = [].concat.apply([], instances.map(instance => this.serialize(instance)));
|
|
125
|
+
return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
126
|
+
+ ' VALUES' + instances.map((_, j) =>
|
|
127
|
+
('(' + new Array(this.nFields).fill(undefined).map((__, i: number) => '$' + (j * this.nFields + i + 1)).join(', ') + ')')).join(',') + ' RETURNING id',
|
|
128
|
+
props).then(q => q.rows.map(r => {
|
|
129
|
+
const idNum = parseInt(r.id, 10);
|
|
130
|
+
if(String(idNum) !== q.rows[0].id) return r.id;
|
|
131
|
+
return idNum;
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
protected abstract buildObject(q: any): T;
|
|
136
|
+
|
|
137
|
+
protected abstract serialize(instance: T, request?: Request): any[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
141
|
+
get(id: R, client?: ClientBase | Transaction, lock = true): Promise<T> {
|
|
142
|
+
if(this.pool) {
|
|
143
|
+
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]));
|
|
144
|
+
} else {
|
|
145
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
146
|
+
request.input('1', id);
|
|
147
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + ' WHERE id=@1').then(q => this.buildObject(q.recordsets[0][0]));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
count(where?: string, inputs: any[] = [], client?: ClientBase | Transaction): Promise<number> {
|
|
152
|
+
if(this.pool) {
|
|
153
|
+
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));
|
|
154
|
+
} else {
|
|
155
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
156
|
+
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
157
|
+
return request.query('SELECT count(*) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '')).then(q => q.recordsets[0][0].cnt);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getList(ids: R[], client?: ClientBase | Transaction, lock = true): Promise<T[]> {
|
|
162
|
+
if(this.pool) {
|
|
163
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT * FROM ' + this.table + ' WHERE id=ANY($1)' + ((client && lock) ? ' FOR UPDATE' : ''), [ids])
|
|
164
|
+
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
165
|
+
} else {
|
|
166
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
167
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + ' WHERE id IN ('
|
|
168
|
+
+ (ids.length > 0 ? (typeof ids[0] === 'string' ? '\'' + ids.join('\',\'') + '\'' : ids.join(',')) : '') + ')')
|
|
169
|
+
.then(q => q.recordsets[0].map(r => this.buildObject(r)));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getAllBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<T[]> {
|
|
174
|
+
if(this.pool) {
|
|
175
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT * FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
176
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
177
|
+
.then(q => q.rows.map(r => this.buildObject(r)));
|
|
178
|
+
} else {
|
|
179
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
180
|
+
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
181
|
+
return request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
182
|
+
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : ''))
|
|
183
|
+
.then(q => q.recordsets[0].map(r => this.buildObject(r)));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getViewCountBy(order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<ViewCount<T>> {
|
|
188
|
+
if(this.pool) {
|
|
189
|
+
return (<ClientBase | Pool>(client || this.pool)).query('SELECT *, COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
190
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
191
|
+
.then(q => ({
|
|
192
|
+
views: q.rows.map(r => this.buildObject(r)),
|
|
193
|
+
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
194
|
+
}));
|
|
195
|
+
} else {
|
|
196
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
197
|
+
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
198
|
+
return Promise.allConcurrent(1)([
|
|
199
|
+
() => request.query('SELECT * FROM ' + this.table + ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
200
|
+
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
201
|
+
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
202
|
+
]).then(([q1, q2]: [any, any]) => ({
|
|
203
|
+
views: q1.recordsets[0].map((r: any) => this.buildObject(r)),
|
|
204
|
+
count: q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getRowsViewCountBy(rows: string[], order?: string, offset?: number, limit?: number, where?: string, inputs: any[] = [], client?: ClientBase | Transaction, lock = true): Promise<ViewCount<any>> {
|
|
210
|
+
if(this.pool) {
|
|
211
|
+
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) : '')
|
|
212
|
+
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
213
|
+
.then(q => ({
|
|
214
|
+
views: q.rows.map(r => this.buildObject(r)),
|
|
215
|
+
count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
|
|
216
|
+
}));
|
|
217
|
+
} else {
|
|
218
|
+
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
219
|
+
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
220
|
+
return Promise.allConcurrent(1)([
|
|
221
|
+
() => request.query('SELECT ' + rows.map(r => '"' + r + '"').join(',') + ' FROM ' + this.table
|
|
222
|
+
+ ((client && lock) ? ' WITH (ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
223
|
+
+ (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
224
|
+
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
225
|
+
]).then(([q1, q2]: [any, any]) => ({
|
|
226
|
+
views: q1.recordsets[0].map((r: any) => this.buildObject(r)),
|
|
227
|
+
count: q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
archive(id: R, replacedById: R, client: ClientBase | Transaction): Promise<any> {
|
|
233
|
+
if(this.pool) {
|
|
234
|
+
return (<ClientBase>client).query('UPDATE ' + this.table + ' SET "archivedOn"=now(),replaced_by_id=$1 WHERE id=$2', [replacedById, id]);
|
|
235
|
+
} else {
|
|
236
|
+
const request = (<Transaction>client).request();
|
|
237
|
+
request.input('1', replacedById);
|
|
238
|
+
request.input('2', id);
|
|
239
|
+
return request.query('UPDATE ' + this.table + ' SET "archivedOn"=now(),replaced_by_id=@1 WHERE id=@2');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
replace(instance: T, client: ClientBase | Transaction): Promise<R> {
|
|
244
|
+
if((<any>instance).archivedOn) return Promise.reject('Record archived!');
|
|
245
|
+
return this.create({...instance, replaced_id: instance.id}, client).then(id => {
|
|
246
|
+
return this.archive(instance.id, id, client).then(() => id);
|
|
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
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
package/package.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "anote-server-libs",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Helpers for express-TS servers",
|
|
5
|
-
"scripts": {
|
|
6
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
7
|
-
"build": "tsc -p tsconfig.json"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "https://gitlab.anotemusic.com/anote/anote-server-libs.git"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [
|
|
14
|
-
"Express"
|
|
15
|
-
],
|
|
16
|
-
"author": "Mathonet Gregoire",
|
|
17
|
-
"license": "MIT",
|
|
18
|
-
"dependencies": {
|
|
19
|
-
"@types/memcached": "2.2.7",
|
|
20
|
-
"@types/mssql": "7.1.4",
|
|
21
|
-
"@types/pg": "8.6.4",
|
|
22
|
-
"memcached": "2.2.2",
|
|
23
|
-
"mssql": "8.0.2",
|
|
24
|
-
"pg": "8.7.3"
|
|
25
|
-
},
|
|
26
|
-
"peerDependencies": {
|
|
27
|
-
"express": "^4.17.3",
|
|
28
|
-
"jsonschema": "^1.4.0",
|
|
29
|
-
"node-forge": "^1.2.1",
|
|
30
|
-
"winston": "3.6.0"
|
|
31
|
-
},
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"@types/express": "4.17.13",
|
|
34
|
-
"@types/node": "14.18.2",
|
|
35
|
-
"@types/node-forge": "1.0.0",
|
|
36
|
-
"typescript": "4.5.5"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "anote-server-libs",
|
|
3
|
+
"version": "0.2.8",
|
|
4
|
+
"description": "Helpers for express-TS servers",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
7
|
+
"build": "tsc -p tsconfig.json"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://gitlab.anotemusic.com/anote/anote-server-libs.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"Express"
|
|
15
|
+
],
|
|
16
|
+
"author": "Mathonet Gregoire",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@types/memcached": "2.2.7",
|
|
20
|
+
"@types/mssql": "7.1.4",
|
|
21
|
+
"@types/pg": "8.6.4",
|
|
22
|
+
"memcached": "2.2.2",
|
|
23
|
+
"mssql": "8.0.2",
|
|
24
|
+
"pg": "8.7.3"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"express": "^4.17.3",
|
|
28
|
+
"jsonschema": "^1.4.0",
|
|
29
|
+
"node-forge": "^1.2.1",
|
|
30
|
+
"winston": "3.6.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/express": "4.17.13",
|
|
34
|
+
"@types/node": "14.18.2",
|
|
35
|
+
"@types/node-forge": "1.0.0",
|
|
36
|
+
"typescript": "4.5.5"
|
|
37
|
+
}
|
|
38
|
+
}
|