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
- const props = this.serialize(instance);
78
- props.push(instance.id);
79
- return client.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=$' + (this.nFields + 1), props).then(() => instance.id);
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.serialize(instance, request);
84
- request.input(String(this.nFields + 1), instance.id);
85
- return request.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=@' + (this.nFields + 1)).then(() => instance.id);
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
- const props = this.serialize(instance);
92
- return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
93
- + ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i) => '$' + (i + 1)).join(',') + ') RETURNING id', props).then(q => {
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.serialize(instance, request);
103
- return request.query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=@\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
104
- + ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i) => '@' + (i + 1)).join(',') + '); SELECT SCOPE_IDENTITY() AS id').then((q) => {
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 = [].concat.apply([], instances.map(instance => this.serialize(instance)));
115
- return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
116
- + ' 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 => {
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.buildObject(q.rows[0]));
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.buildObject(q.recordsets[0][0]));
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.buildObject(r)));
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.buildObject(r)));
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.buildObject(r)));
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.buildObject(r)));
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
- views: q.rows.map(r => this.buildObject(r)),
180
- count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
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
- ]).then(([q1, q2]) => ({
192
- views: q1.recordsets[0].map((r) => this.buildObject(r)),
193
- count: q2.recordsets.length ? q2.recordsets[0].reduce((p, n) => p + n.cnt, 0) : 0
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
- views: q.rows.map(r => this.buildObject(r)),
203
- count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
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
- ]).then(([q1, q2]) => ({
216
- views: q1.recordsets[0].map((r) => this.buildObject(r)),
217
- count: q2.recordsets.length ? q2.recordsets[0].reduce((p, n) => p + n.cnt, 0) : 0
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
- const props = this.serialize(instance);
98
- props.push(instance.id);
99
- return (<ClientBase>client).query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=$' + (this.nFields + 1), props).then(() => instance.id);
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.serialize(instance, request);
103
- request.input(String(this.nFields + 1), instance.id);
104
- return request.query('UPDATE ' + this.table + ' SET ' + this.updateDefinition + ' WHERE id=@' + (this.nFields + 1)).then(() => instance.id);
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
- const props = this.serialize(instance);
112
- return (<ClientBase | Pool>(client || this.pool)).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
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
- props).then(q => {
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.serialize(instance, request);
122
- return request.query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=@\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
123
- + ' VALUES(' + new Array(this.nFields).fill(undefined).map((_, i: number) => '@' + (i + 1)).join(',') + '); SELECT SCOPE_IDENTITY() AS id').then((q: any) => {
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 = [].concat.apply([], instances.map(instance => this.serialize(instance)));
134
- return (client || this.pool).query('INSERT INTO ' + this.table + '(' + this.updateDefinition.replace(/=\$\d+/g, '').replace(/=[^)]+\)/g, '') + ')'
135
- + ' VALUES' + instances.map((_, j) =>
136
- ('(' + new Array(this.nFields).fill(undefined).map((__, i: number) => '$' + (j * this.nFields + i + 1)).join(', ') + ')')).join(',') + ' RETURNING id',
137
- props).then(q => q.rows.map(r => {
138
- const idNum = parseInt(r.id, 10);
139
- if(String(idNum) !== q.rows[0].id) return r.id;
140
- return idNum;
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.buildObject(q.rows[0]));
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.buildObject(q.recordsets[0][0]));
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.buildObject(r)));
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.buildObject(r)));
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.buildObject(r)));
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.buildObject(r)));
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
- views: q.rows.map(r => this.buildObject(r)),
202
- count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
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
- ]).then(([q1, q2]: [any, any]) => ({
212
- views: q1.recordsets[0].map((r: any) => this.buildObject(r)),
213
- count: q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0
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
- views: q.rows.map(r => this.buildObject(r)),
224
- count: q.rows.length ? parseInt(q.rows[0].cnt, 10) : 0
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
- ]).then(([q1, q2]: [any, any]) => ({
235
- views: q1.recordsets[0].map((r: any) => this.buildObject(r)),
236
- count: q2.recordsets.length ? q2.recordsets[0].reduce((p: number, n: any) => p + n.cnt, 0) : 0
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anote-server-libs",
3
- "version": "0.9.6",
3
+ "version": "0.10.0",
4
4
  "description": "Helpers for express-TS servers",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",