anote-server-libs 0.9.6 → 0.10.0
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.
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CryptModelDao = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const util_1 = require("util");
|
|
6
|
+
const ModelDao_1 = require("./ModelDao");
|
|
7
|
+
const randomFillAsync = (0, util_1.promisify)(crypto_1.randomFill);
|
|
8
|
+
class CryptModelDao extends ModelDao_1.ModelDao {
|
|
9
|
+
constructor(keyBase64, encryptedColumns, ...args) {
|
|
10
|
+
super(...args);
|
|
11
|
+
this.key = Buffer.from(keyBase64, 'base64');
|
|
12
|
+
this.encryptedColumns = encryptedColumns;
|
|
13
|
+
}
|
|
14
|
+
serializeWrapper(instance, request) {
|
|
15
|
+
const props = this.serialize(instance, request);
|
|
16
|
+
const encryptPromises = [];
|
|
17
|
+
this.encryptedColumns.forEach(col => {
|
|
18
|
+
const idx = this.updateDefinition.split(',').findIndex(def => def.trim().startsWith('"' + col + '"') || def.trim().startsWith(col + '='));
|
|
19
|
+
if (idx >= 0) {
|
|
20
|
+
const val = request ? request.parameters[idx] : props[idx];
|
|
21
|
+
if (val !== null && val !== undefined) {
|
|
22
|
+
const encryptPromise = this.encrypt(String(val)).then(encrypted => {
|
|
23
|
+
if (request)
|
|
24
|
+
request.replaceInput(String(idx + 1), encrypted);
|
|
25
|
+
else
|
|
26
|
+
props[idx] = encrypted;
|
|
27
|
+
});
|
|
28
|
+
encryptPromises.push(encryptPromise);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (encryptPromises.length > 0) {
|
|
33
|
+
return Promise.all(encryptPromises).then(() => props);
|
|
34
|
+
}
|
|
35
|
+
return props;
|
|
36
|
+
}
|
|
37
|
+
buildObjectWrapper(row) {
|
|
38
|
+
const decryptPromises = [];
|
|
39
|
+
this.encryptedColumns.forEach(col => {
|
|
40
|
+
if (row[col] !== null && row[col] !== undefined) {
|
|
41
|
+
const decryptPromise = this.decrypt(String(row[col])).then(decrypted => {
|
|
42
|
+
row[col] = decrypted;
|
|
43
|
+
});
|
|
44
|
+
decryptPromises.push(decryptPromise);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (decryptPromises.length > 0) {
|
|
48
|
+
return Promise.all(decryptPromises).then(() => this.buildObject(row));
|
|
49
|
+
}
|
|
50
|
+
return this.buildObject(row);
|
|
51
|
+
}
|
|
52
|
+
async encrypt(decrypted) {
|
|
53
|
+
const iv = new Uint8Array(16);
|
|
54
|
+
await randomFillAsync(iv);
|
|
55
|
+
const cipher = (0, crypto_1.createCipheriv)(CryptModelDao.ALGORITHM, this.key, iv);
|
|
56
|
+
let encrypted = '';
|
|
57
|
+
cipher.setEncoding('base64');
|
|
58
|
+
cipher.on('data', (chunk) => encrypted += chunk);
|
|
59
|
+
cipher.write(decrypted);
|
|
60
|
+
cipher.end();
|
|
61
|
+
return `$$enc$$:${Buffer.from(iv).toString('base64')}${encrypted}`;
|
|
62
|
+
}
|
|
63
|
+
async decrypt(encrypted) {
|
|
64
|
+
if (!encrypted.startsWith('$$enc$$:')) {
|
|
65
|
+
return encrypted;
|
|
66
|
+
}
|
|
67
|
+
encrypted = encrypted.slice(8);
|
|
68
|
+
const iv = new Uint8Array(Buffer.from(encrypted.slice(0, 24), 'base64'));
|
|
69
|
+
const decipher = (0, crypto_1.createDecipheriv)(CryptModelDao.ALGORITHM, this.key, iv);
|
|
70
|
+
let decrypted = '';
|
|
71
|
+
decipher.on('readable', () => {
|
|
72
|
+
for (let chunk = decipher.read(); chunk !== null; chunk = decipher.read()) {
|
|
73
|
+
decrypted += chunk.toString('base64');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
decipher.write(encrypted.slice(24), 'base64');
|
|
77
|
+
decipher.end();
|
|
78
|
+
return Buffer.from(decrypted, 'base64').toString('utf8');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.CryptModelDao = CryptModelDao;
|
|
82
|
+
CryptModelDao.ALGORITHM = 'aes-256-cbc';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {createCipheriv, createDecipheriv, randomFill} from 'crypto';
|
|
2
|
+
import {promisify} from 'util';
|
|
3
|
+
import {Model, ModelDao} from './ModelDao';
|
|
4
|
+
import {Request} from 'mssql';
|
|
5
|
+
|
|
6
|
+
const randomFillAsync = promisify(randomFill);
|
|
7
|
+
|
|
8
|
+
export abstract class CryptModelDao<R, T extends Model<R>> extends ModelDao<R, T> {
|
|
9
|
+
private static readonly ALGORITHM = 'aes-256-cbc';
|
|
10
|
+
|
|
11
|
+
private key: Buffer;
|
|
12
|
+
private encryptedColumns: string[];
|
|
13
|
+
|
|
14
|
+
constructor(keyBase64: string, encryptedColumns: string[], ...args: ConstructorParameters<typeof ModelDao>) {
|
|
15
|
+
super(...args);
|
|
16
|
+
this.key = Buffer.from(keyBase64, 'base64');
|
|
17
|
+
this.encryptedColumns = encryptedColumns;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected serializeWrapper(instance: T, request?: Request): Promise<any[]> | any[] {
|
|
21
|
+
const props = this.serialize(instance, request) as any[];
|
|
22
|
+
const encryptPromises: Promise<void>[] = [];
|
|
23
|
+
this.encryptedColumns.forEach(col => {
|
|
24
|
+
// Parse "protected updateDefinition: string" to find index
|
|
25
|
+
// e.g. updateDefinition = '"name"=$1,secret=$2,age=$3' -> secret
|
|
26
|
+
const idx = this.updateDefinition.split(',').findIndex(def => def.trim().startsWith('"' + col + '"') || def.trim().startsWith(col + '='));
|
|
27
|
+
if(idx >= 0) {
|
|
28
|
+
const val = request ? request.parameters[idx] : props[idx];
|
|
29
|
+
if(val !== null && val !== undefined) {
|
|
30
|
+
const encryptPromise = this.encrypt(String(val)).then(encrypted => {
|
|
31
|
+
if(request) request.replaceInput(String(idx + 1), encrypted);
|
|
32
|
+
else props[idx] = encrypted;
|
|
33
|
+
});
|
|
34
|
+
encryptPromises.push(encryptPromise);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if(encryptPromises.length > 0) {
|
|
39
|
+
return Promise.all(encryptPromises).then(() => props);
|
|
40
|
+
}
|
|
41
|
+
return props;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected buildObjectWrapper(row: any): T | Promise<T> {
|
|
45
|
+
const decryptPromises: Promise<void>[] = [];
|
|
46
|
+
this.encryptedColumns.forEach(col => {
|
|
47
|
+
if(row[col] !== null && row[col] !== undefined) {
|
|
48
|
+
const decryptPromise = this.decrypt(String(row[col])).then(decrypted => {
|
|
49
|
+
row[col] = decrypted;
|
|
50
|
+
});
|
|
51
|
+
decryptPromises.push(decryptPromise);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if(decryptPromises.length > 0) {
|
|
55
|
+
return Promise.all(decryptPromises).then(() => this.buildObject(row));
|
|
56
|
+
}
|
|
57
|
+
return this.buildObject(row);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async encrypt(decrypted: string): Promise<string> {
|
|
61
|
+
const iv = new Uint8Array(16);
|
|
62
|
+
await randomFillAsync(iv);
|
|
63
|
+
const cipher = createCipheriv(CryptModelDao.ALGORITHM, this.key, iv);
|
|
64
|
+
let encrypted = '';
|
|
65
|
+
cipher.setEncoding('base64');
|
|
66
|
+
cipher.on('data', (chunk) => encrypted += chunk);
|
|
67
|
+
cipher.write(decrypted);
|
|
68
|
+
cipher.end(); // Blocking
|
|
69
|
+
return `$$enc$$:${Buffer.from(iv).toString('base64')}${encrypted}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async decrypt(encrypted: string): Promise<string> {
|
|
73
|
+
if(!encrypted.startsWith('$$enc$$:')) {
|
|
74
|
+
return encrypted;
|
|
75
|
+
}
|
|
76
|
+
encrypted = encrypted.slice(8);
|
|
77
|
+
const iv = new Uint8Array(Buffer.from(encrypted.slice(0, 24), 'base64'));
|
|
78
|
+
const decipher = createDecipheriv(CryptModelDao.ALGORITHM, this.key, iv);
|
|
79
|
+
let decrypted = '';
|
|
80
|
+
decipher.on('readable', () => {
|
|
81
|
+
for(let chunk = decipher.read(); chunk !== null; chunk = decipher.read()) {
|
|
82
|
+
decrypted += chunk.toString('base64');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
decipher.write(encrypted.slice(24), 'base64');
|
|
86
|
+
decipher.end(); // Blocking
|
|
87
|
+
return Buffer.from(decrypted, 'base64').toString('utf8');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -74,23 +74,33 @@ class Dao {
|
|
|
74
74
|
return Promise.reject('Record archived!');
|
|
75
75
|
instance.updatedOn = on || new Date();
|
|
76
76
|
if (this.pool) {
|
|
77
|
-
|
|
78
|
-
props
|
|
79
|
-
|
|
77
|
+
let props = this.serializeWrapper(instance);
|
|
78
|
+
if (!(props instanceof Promise))
|
|
79
|
+
props = Promise.resolve(props);
|
|
80
|
+
return props.then(resolvedProps => {
|
|
81
|
+
resolvedProps.push(instance.id);
|
|
82
|
+
return client.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=$' + (this.nFields + 1), resolvedProps).then(() => instance.id);
|
|
83
|
+
});
|
|
80
84
|
}
|
|
81
85
|
else {
|
|
82
86
|
const request = client.request();
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
let props = this.serializeWrapper(instance, request);
|
|
88
|
+
if (!(props instanceof Promise))
|
|
89
|
+
props = Promise.resolve(props);
|
|
90
|
+
return props.then(() => {
|
|
91
|
+
request.input(String(this.nFields + 1), instance.id);
|
|
92
|
+
return request.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=@' + (this.nFields + 1)).then(() => instance.id);
|
|
93
|
+
});
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
96
|
create(instance, client, on) {
|
|
89
97
|
instance.createdOn = instance.updatedOn = on || new Date();
|
|
90
98
|
if (this.pool) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
let props = this.serializeWrapper(instance);
|
|
100
|
+
if (!(props instanceof Promise))
|
|
101
|
+
props = Promise.resolve(props);
|
|
102
|
+
return props.then(resolvedProps => (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
103
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i) => '$' + (i + 1)).join(',') + ') RETURNING id', resolvedProps)).then(q => {
|
|
94
104
|
const idNum = parseInt(q.rows[0].id, 10);
|
|
95
105
|
if (String(idNum) !== q.rows[0].id)
|
|
96
106
|
return q.rows[0].id;
|
|
@@ -99,9 +109,11 @@ class Dao {
|
|
|
99
109
|
}
|
|
100
110
|
else {
|
|
101
111
|
const request = (client || this.poolMssql).request();
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
let props = this.serializeWrapper(instance, request);
|
|
113
|
+
if (!(props instanceof Promise))
|
|
114
|
+
props = Promise.resolve(props);
|
|
115
|
+
return props.then(() => request.query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=@\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
116
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i) => '@' + (i + 1)).join(',') + '); SELECT SCOPE_IDENTITY() AS id')).then((q) => {
|
|
105
117
|
return q.recordsets[0][0].id || instance.id;
|
|
106
118
|
});
|
|
107
119
|
}
|
|
@@ -111,26 +123,40 @@ class Dao {
|
|
|
111
123
|
return Promise.resolve([]);
|
|
112
124
|
const now = on || new Date();
|
|
113
125
|
instances.forEach(instance => instance.updatedOn = now);
|
|
114
|
-
const props =
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
const props = instances.map(instance => {
|
|
127
|
+
let props = this.serializeWrapper(instance);
|
|
128
|
+
if (!(props instanceof Promise))
|
|
129
|
+
props = Promise.resolve(props);
|
|
130
|
+
return props;
|
|
131
|
+
});
|
|
132
|
+
return Promise.all(props).then(resolvedProps => {
|
|
133
|
+
resolvedProps = resolvedProps.reduce((p, n) => p.concat(n), []);
|
|
134
|
+
return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
135
|
+
+ ' VALUES' + instances.map((_, j) => ('(' + new Array(this.nFields).fill(undefined).map((__, i) => '$' + (j * this.nFields + i + 1)).join(', ') + ')')).join(',') + ' RETURNING id', resolvedProps);
|
|
136
|
+
}).then(q => q.rows.map(r => {
|
|
117
137
|
const idNum = parseInt(r.id, 10);
|
|
118
138
|
if (String(idNum) !== q.rows[0].id)
|
|
119
139
|
return r.id;
|
|
120
140
|
return idNum;
|
|
121
141
|
}));
|
|
122
142
|
}
|
|
143
|
+
buildObjectWrapper(q) {
|
|
144
|
+
return this.buildObject(q);
|
|
145
|
+
}
|
|
146
|
+
serializeWrapper(instance, request) {
|
|
147
|
+
return this.serialize(instance, request);
|
|
148
|
+
}
|
|
123
149
|
}
|
|
124
150
|
exports.Dao = Dao;
|
|
125
151
|
class ModelDao extends Dao {
|
|
126
152
|
get(id, client, lock = true) {
|
|
127
153
|
if (this.pool) {
|
|
128
|
-
return (client || this.pool).query(this.selectDefinition + ' FROM ' + this.table + ' WHERE id=$1' + ((client && lock) ? ' FOR UPDATE' : ''), [id]).then(q => this.
|
|
154
|
+
return (client || this.pool).query(this.selectDefinition + ' FROM ' + this.table + ' WHERE id=$1' + ((client && lock) ? ' FOR UPDATE' : ''), [id]).then(q => this.buildObjectWrapper(q.rows[0]));
|
|
129
155
|
}
|
|
130
156
|
else {
|
|
131
157
|
const request = (client || this.poolMssql).request();
|
|
132
158
|
request.input('1', id);
|
|
133
|
-
return request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + ' WHERE id=@1').then((q) => this.
|
|
159
|
+
return request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + ' WHERE id=@1').then((q) => this.buildObjectWrapper(q.recordsets[0][0]));
|
|
134
160
|
}
|
|
135
161
|
}
|
|
136
162
|
count(where, inputs = [], client) {
|
|
@@ -147,20 +173,20 @@ class ModelDao extends Dao {
|
|
|
147
173
|
getList(ids, client, lock = true) {
|
|
148
174
|
if (this.pool) {
|
|
149
175
|
return (client || this.pool).query(this.selectDefinition + ' FROM ' + this.table + ' WHERE id=ANY($1)' + ((client && lock) ? ' FOR UPDATE' : ''), [ids])
|
|
150
|
-
.then(q => q.rows.map(r => this.
|
|
176
|
+
.then(q => Promise.all(q.rows.map(r => this.buildObjectWrapper(r))));
|
|
151
177
|
}
|
|
152
178
|
else {
|
|
153
179
|
const request = (client || this.poolMssql).request();
|
|
154
180
|
return request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + ' WHERE id IN ('
|
|
155
181
|
+ (ids.length > 0 ? (typeof ids[0] === 'string' ? '\'' + ids.join('\',\'') + '\'' : ids.join(',')) : '') + ')')
|
|
156
|
-
.then((q) => q.recordsets[0].map((r) => this.
|
|
182
|
+
.then((q) => Promise.all(q.recordsets[0].map((r) => this.buildObjectWrapper(r))));
|
|
157
183
|
}
|
|
158
184
|
}
|
|
159
185
|
getAllBy(order, offset, limit, where, inputs = [], client, lock = true) {
|
|
160
186
|
if (this.pool) {
|
|
161
187
|
return (client || this.pool).query(this.selectDefinition + ' FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
162
188
|
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
163
|
-
.then(q => q.rows.map(r => this.
|
|
189
|
+
.then(q => Promise.all(q.rows.map(r => this.buildObjectWrapper(r))));
|
|
164
190
|
}
|
|
165
191
|
else {
|
|
166
192
|
const request = (client || this.poolMssql).request();
|
|
@@ -168,16 +194,17 @@ class ModelDao extends Dao {
|
|
|
168
194
|
where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
169
195
|
return request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
170
196
|
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : ''))
|
|
171
|
-
.then((q) => q.recordsets[0].map((r) => this.
|
|
197
|
+
.then((q) => Promise.all(q.recordsets[0].map((r) => this.buildObjectWrapper(r))));
|
|
172
198
|
}
|
|
173
199
|
}
|
|
174
200
|
getViewCountBy(order, offset, limit, where, inputs = [], client, lock = true) {
|
|
175
201
|
if (this.pool) {
|
|
176
202
|
return (client || this.pool).query(this.selectDefinition + ', COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
177
203
|
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
178
|
-
.then(q => (
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
.then(q => Promise.all([q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0, Promise.all(q.rows.map(r => this.buildObjectWrapper(r)))]))
|
|
205
|
+
.then(([count, views]) => ({
|
|
206
|
+
views,
|
|
207
|
+
count
|
|
181
208
|
}));
|
|
182
209
|
}
|
|
183
210
|
else {
|
|
@@ -188,9 +215,11 @@ class ModelDao extends Dao {
|
|
|
188
215
|
() => request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
189
216
|
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
190
217
|
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
191
|
-
])
|
|
192
|
-
|
|
193
|
-
|
|
218
|
+
])
|
|
219
|
+
.then(([q1, q2]) => Promise.all([Promise.all(q1.recordsets[0].map((r) => this.buildObjectWrapper(r))), q2.recordsets.length ? q2.recordsets[0].reduce((p, n) => p + n.cnt, 0) : 0]))
|
|
220
|
+
.then(([views, count]) => ({
|
|
221
|
+
views,
|
|
222
|
+
count
|
|
194
223
|
}));
|
|
195
224
|
}
|
|
196
225
|
}
|
|
@@ -198,9 +227,10 @@ class ModelDao extends Dao {
|
|
|
198
227
|
if (this.pool) {
|
|
199
228
|
return (client || this.pool).query('SELECT ' + cols.map(r => r.indexOf(' ') > -1 ? r : ('"' + r + '"')).join(',') + ', COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
200
229
|
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
201
|
-
.then(q => (
|
|
202
|
-
|
|
203
|
-
|
|
230
|
+
.then(q => Promise.all([q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0, Promise.all(q.rows.map(r => this.buildObjectWrapper(r)))]))
|
|
231
|
+
.then(([count, views]) => ({
|
|
232
|
+
views,
|
|
233
|
+
count
|
|
204
234
|
}));
|
|
205
235
|
}
|
|
206
236
|
else {
|
|
@@ -212,9 +242,11 @@ class ModelDao extends Dao {
|
|
|
212
242
|
+ ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
213
243
|
+ (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
214
244
|
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
215
|
-
])
|
|
216
|
-
|
|
217
|
-
|
|
245
|
+
])
|
|
246
|
+
.then(([q1, q2]) => Promise.all([Promise.all(q1.recordsets[0].map((r) => this.buildObjectWrapper(r))), q2.recordsets.length ? q2.recordsets[0].reduce((p, n) => p + n.cnt, 0) : 0]))
|
|
247
|
+
.then(([views, count]) => ({
|
|
248
|
+
views,
|
|
249
|
+
count
|
|
218
250
|
}));
|
|
219
251
|
}
|
|
220
252
|
}
|
|
@@ -94,33 +94,41 @@ export abstract class Dao<R, T extends Model<R>> implements ModelRepr {
|
|
|
94
94
|
if((<any>instance).archivedOn) return Promise.reject('Record archived!');
|
|
95
95
|
instance.updatedOn = on || new Date();
|
|
96
96
|
if(this.pool) {
|
|
97
|
-
|
|
98
|
-
props.
|
|
99
|
-
return
|
|
97
|
+
let props = this.serializeWrapper(instance);
|
|
98
|
+
if(!(props instanceof Promise)) props = Promise.resolve(props);
|
|
99
|
+
return props.then(resolvedProps => {
|
|
100
|
+
resolvedProps.push(instance.id);
|
|
101
|
+
return (<ClientBase>client).query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=$' + (this.nFields + 1), resolvedProps).then(() => instance.id);
|
|
102
|
+
});
|
|
100
103
|
} else {
|
|
101
104
|
const request = (<ConnectionPool | Transaction>client).request();
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
+
let props = this.serializeWrapper(instance, request);
|
|
106
|
+
if(!(props instanceof Promise)) props = Promise.resolve(props);
|
|
107
|
+
return props.then(() => {
|
|
108
|
+
request.input(String(this.nFields + 1), instance.id);
|
|
109
|
+
return request.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=@' + (this.nFields + 1)).then(() => instance.id);
|
|
110
|
+
});
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
create(instance: T, client?: ClientBase | Transaction, on?: Date): Promise<R> {
|
|
109
115
|
(<any>instance).createdOn = instance.updatedOn = on || new Date();
|
|
110
116
|
if(this.pool) {
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
let props = this.serializeWrapper(instance);
|
|
118
|
+
if(!(props instanceof Promise)) props = Promise.resolve(props);
|
|
119
|
+
return props.then(resolvedProps => (<ClientBase | Pool>(client || this.pool)).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
113
120
|
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '$' + (i + 1)).join(',') + ') RETURNING id',
|
|
114
|
-
|
|
121
|
+
resolvedProps)).then(q => {
|
|
115
122
|
const idNum = parseInt(q.rows[0].id, 10);
|
|
116
123
|
if(String(idNum) !== q.rows[0].id) return q.rows[0].id;
|
|
117
124
|
return idNum;
|
|
118
125
|
});
|
|
119
126
|
} else {
|
|
120
127
|
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
let props = this.serializeWrapper(instance, request);
|
|
129
|
+
if(!(props instanceof Promise)) props = Promise.resolve(props);
|
|
130
|
+
return props.then(() => request.query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=@\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
131
|
+
+ ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '@' + (i + 1)).join(',') + '); SELECT SCOPE_IDENTITY() AS id')).then((q: any) => {
|
|
124
132
|
return q.recordsets[0][0].id || instance.id;
|
|
125
133
|
});
|
|
126
134
|
}
|
|
@@ -130,30 +138,45 @@ export abstract class Dao<R, T extends Model<R>> implements ModelRepr {
|
|
|
130
138
|
if(!instances.length) return Promise.resolve([]);
|
|
131
139
|
const now = on || new Date();
|
|
132
140
|
instances.forEach(instance => instance.updatedOn = now);
|
|
133
|
-
const props =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
const props = instances.map(instance => {
|
|
142
|
+
let props = this.serializeWrapper(instance);
|
|
143
|
+
if(!(props instanceof Promise)) props = Promise.resolve(props);
|
|
144
|
+
return props;
|
|
145
|
+
});
|
|
146
|
+
return Promise.all(props).then(resolvedProps => {
|
|
147
|
+
resolvedProps = resolvedProps.reduce((p, n) => p.concat(n), []);
|
|
148
|
+
return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
|
|
149
|
+
+ ' VALUES' + instances.map((_, j) =>
|
|
150
|
+
('(' + new Array(this.nFields).fill(undefined).map((__, i: number) => '$' + (j * this.nFields + i + 1)).join(', ') + ')')).join(',') + ' RETURNING id',
|
|
151
|
+
resolvedProps);
|
|
152
|
+
}).then(q => q.rows.map(r => {
|
|
153
|
+
const idNum = parseInt(r.id, 10);
|
|
154
|
+
if(String(idNum) !== q.rows[0].id) return r.id;
|
|
155
|
+
return idNum;
|
|
156
|
+
}));
|
|
142
157
|
}
|
|
143
158
|
|
|
144
|
-
protected abstract buildObject(q: any): T
|
|
159
|
+
protected abstract buildObject(q: any): T | Promise<T>;
|
|
145
160
|
|
|
146
|
-
protected abstract serialize(instance: T, request?: Request): any[]
|
|
161
|
+
protected abstract serialize(instance: T, request?: Request): any[] | Promise<any[]>;
|
|
162
|
+
|
|
163
|
+
protected buildObjectWrapper(q: any): T | Promise<T> {
|
|
164
|
+
return this.buildObject(q);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
protected serializeWrapper(instance: T, request?: Request): any[] | Promise<any[]> {
|
|
168
|
+
return this.serialize(instance, request);
|
|
169
|
+
}
|
|
147
170
|
}
|
|
148
171
|
|
|
149
172
|
export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
150
173
|
get(id: R, client?: ClientBase | Transaction, lock = true): Promise<T> {
|
|
151
174
|
if(this.pool) {
|
|
152
|
-
return (<ClientBase | Pool>(client || this.pool)).query(this.selectDefinition + ' FROM ' + this.table + ' WHERE id=$1' + ((client && lock) ? ' FOR UPDATE' : ''), [id]).then(q => this.
|
|
175
|
+
return (<ClientBase | Pool>(client || this.pool)).query(this.selectDefinition + ' FROM ' + this.table + ' WHERE id=$1' + ((client && lock) ? ' FOR UPDATE' : ''), [id]).then(q => this.buildObjectWrapper(q.rows[0]));
|
|
153
176
|
} else {
|
|
154
177
|
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
155
178
|
request.input('1', id);
|
|
156
|
-
return request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + ' WHERE id=@1').then((q: any) => this.
|
|
179
|
+
return request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + ' WHERE id=@1').then((q: any) => this.buildObjectWrapper(q.recordsets[0][0]));
|
|
157
180
|
}
|
|
158
181
|
}
|
|
159
182
|
|
|
@@ -170,12 +193,12 @@ export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
|
170
193
|
getList(ids: R[], client?: ClientBase | Transaction, lock = true): Promise<T[]> {
|
|
171
194
|
if(this.pool) {
|
|
172
195
|
return (<ClientBase | Pool>(client || this.pool)).query(this.selectDefinition + ' FROM ' + this.table + ' WHERE id=ANY($1)' + ((client && lock) ? ' FOR UPDATE' : ''), [ids])
|
|
173
|
-
.then(q => q.rows.map(r => this.
|
|
196
|
+
.then(q => Promise.all(q.rows.map(r => this.buildObjectWrapper(r))));
|
|
174
197
|
} else {
|
|
175
198
|
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
176
199
|
return request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + ' WHERE id IN ('
|
|
177
200
|
+ (ids.length > 0 ? (typeof ids[0] === 'string' ? '\'' + ids.join('\',\'') + '\'' : ids.join(',')) : '') + ')')
|
|
178
|
-
.then((q: any) => q.recordsets[0].map((r: any) => this.
|
|
201
|
+
.then((q: any) => Promise.all(q.recordsets[0].map((r: any) => this.buildObjectWrapper(r))));
|
|
179
202
|
}
|
|
180
203
|
}
|
|
181
204
|
|
|
@@ -183,13 +206,13 @@ export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
|
183
206
|
if(this.pool) {
|
|
184
207
|
return (<ClientBase | Pool>(client || this.pool)).query(this.selectDefinition + ' FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
185
208
|
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
186
|
-
.then(q => q.rows.map(r => this.
|
|
209
|
+
.then(q => Promise.all(q.rows.map(r => this.buildObjectWrapper(r))));
|
|
187
210
|
} else {
|
|
188
211
|
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
189
212
|
if(where) where.match(/(@\d+)/g).forEach((match, i) => request.input(match.substr(1), inputs[i]));
|
|
190
213
|
return request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
191
214
|
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : ''))
|
|
192
|
-
.then((q: any) => q.recordsets[0].map((r: any) => this.
|
|
215
|
+
.then((q: any) => Promise.all(q.recordsets[0].map((r: any) => this.buildObjectWrapper(r))));
|
|
193
216
|
}
|
|
194
217
|
}
|
|
195
218
|
|
|
@@ -197,9 +220,10 @@ export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
|
197
220
|
if(this.pool) {
|
|
198
221
|
return (<ClientBase | Pool>(client || this.pool)).query(this.selectDefinition + ', COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
199
222
|
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
200
|
-
.then(q => (
|
|
201
|
-
|
|
202
|
-
|
|
223
|
+
.then(q => Promise.all([q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0, Promise.all(q.rows.map(r => this.buildObjectWrapper(r)))]))
|
|
224
|
+
.then(([count, views]) => ({
|
|
225
|
+
views,
|
|
226
|
+
count
|
|
203
227
|
}));
|
|
204
228
|
} else {
|
|
205
229
|
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
@@ -208,9 +232,11 @@ export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
|
208
232
|
() => request.query(this.selectDefinition + ' FROM ' + this.table + ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '')
|
|
209
233
|
+ (order ? (' ORDER BY ' + order) : '') + (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
210
234
|
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
211
|
-
])
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
])
|
|
236
|
+
.then(([q1, q2]: [any, any]) => Promise.all([Promise.all(q1.recordsets[0].map((r: any) => this.buildObjectWrapper(r))), q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0]))
|
|
237
|
+
.then(([views, count]) => ({
|
|
238
|
+
views,
|
|
239
|
+
count
|
|
214
240
|
}));
|
|
215
241
|
}
|
|
216
242
|
}
|
|
@@ -219,9 +245,10 @@ export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
|
219
245
|
if(this.pool) {
|
|
220
246
|
return (<ClientBase | Pool>(client || this.pool)).query('SELECT ' + cols.map(r => r.indexOf(' ') > -1 ? r : ('"' + r + '"')).join(',') + ', COUNT(*) OVER() AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
221
247
|
+ (offset ? (' OFFSET ' + offset) : '') + (limit !== undefined ? (' LIMIT ' + limit) : '') + ((client && lock) ? ' FOR UPDATE' : ''), inputs)
|
|
222
|
-
.then(q => (
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
.then(q => Promise.all([q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0, Promise.all(q.rows.map(r => this.buildObjectWrapper(r)))]))
|
|
249
|
+
.then(([count, views]) => ({
|
|
250
|
+
views,
|
|
251
|
+
count
|
|
225
252
|
}));
|
|
226
253
|
} else {
|
|
227
254
|
const request = (<Transaction | ConnectionPool>(client || this.poolMssql)).request();
|
|
@@ -231,9 +258,11 @@ export abstract class ModelDao<R, T extends Model<R>> extends Dao<R, T> {
|
|
|
231
258
|
+ ((client && lock) ? ' WITH (UPDLOCK, ROWLOCK)' : '') + (where ? (' WHERE ' + where) : '') + (order ? (' ORDER BY ' + order) : '')
|
|
232
259
|
+ (offset !== undefined ? (' OFFSET ' + offset + ' ROWS') : '') + (limit !== undefined ? (' FETCH NEXT ' + limit + ' ROWS ONLY') : '')),
|
|
233
260
|
() => request.query('SELECT COUNT(DISTINCT id) AS cnt FROM ' + this.table + (where ? (' WHERE ' + where) : ''))
|
|
234
|
-
])
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
])
|
|
262
|
+
.then(([q1, q2]: [any, any]) => Promise.all([Promise.all(q1.recordsets[0].map((r: any) => this.buildObjectWrapper(r))), q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0]))
|
|
263
|
+
.then(([views, count]) => ({
|
|
264
|
+
views,
|
|
265
|
+
count
|
|
237
266
|
}));
|
|
238
267
|
}
|
|
239
268
|
}
|