anote-server-libs 0.9.5 → 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.
@@ -23,34 +23,34 @@ class BaseModelRepository {
23
23
  this.Migration = new Migration_1.MigrationRepository(db, dbMssql, logger);
24
24
  this.ApiCall = new ApiCall_1.ApiCallRepository(db, dbMssql, logger);
25
25
  }
26
- migrate(migrationsPath, callback, onlyAboveOrEquals = 0) {
26
+ migrate(migrationsPath, callback, onlyAboveOrEquals = 0, onlyBelow) {
27
27
  (this.db ? Promise.all([
28
- this.db.query(`CREATE TABLE IF NOT EXISTS migration (
29
- id integer PRIMARY KEY,
30
- hash text NOT NULL,
31
- "sqlUp" text NOT NULL,
32
- "sqlDown" text NOT NULL,
33
- state integer NOT NULL,
34
- skip boolean NOT NULL
28
+ this.db.query(`CREATE TABLE IF NOT EXISTS migration (
29
+ id integer PRIMARY KEY,
30
+ hash text NOT NULL,
31
+ "sqlUp" text NOT NULL,
32
+ "sqlDown" text NOT NULL,
33
+ state integer NOT NULL,
34
+ skip boolean NOT NULL
35
35
  )`),
36
36
  this.db.query(`ALTER TABLE migration ADD COLUMN IF NOT EXISTS skip BOOLEAN NOT NULL DEFAULT FALSE`)
37
37
  ]) : Promise.all([
38
- this.dbMssql.query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=\'migration\' AND xtype=\'U\') CREATE TABLE migration (
39
- id int PRIMARY KEY,
40
- hash text NOT NULL,
41
- "sqlUp" text NOT NULL,
42
- "sqlDown" text NOT NULL,
43
- state int NOT NULL,
44
- skip bit NOT NULL
38
+ this.dbMssql.query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=\'migration\' AND xtype=\'U\') CREATE TABLE migration (
39
+ id int PRIMARY KEY,
40
+ hash text NOT NULL,
41
+ "sqlUp" text NOT NULL,
42
+ "sqlDown" text NOT NULL,
43
+ state int NOT NULL,
44
+ skip bit NOT NULL
45
45
  )`),
46
- this.dbMssql.query(`IF NOT EXISTS (
47
- SELECT *
48
- FROM INFORMATION_SCHEMA.COLUMNS
49
- WHERE TABLE_NAME = 'migration'
50
- AND COLUMN_NAME = 'skip'
51
- )
52
- BEGIN
53
- ALTER TABLE migration ADD skip BIT NOT NULL DEFAULT 0;
46
+ this.dbMssql.query(`IF NOT EXISTS (
47
+ SELECT *
48
+ FROM INFORMATION_SCHEMA.COLUMNS
49
+ WHERE TABLE_NAME = 'migration'
50
+ AND COLUMN_NAME = 'skip'
51
+ )
52
+ BEGIN
53
+ ALTER TABLE migration ADD skip BIT NOT NULL DEFAULT 0;
54
54
  END`)
55
55
  ])).then(() => {
56
56
  this.Migration.getAllBy('id').then((migrations) => {
@@ -70,16 +70,22 @@ END`)
70
70
  hash: crypto.createHash('sha256').update(content).digest('hex')
71
71
  };
72
72
  });
73
- if (migrationsAvailable.length === 0
74
- || migrationsAvailable.length
75
- !== migrationsAvailable[migrationsAvailable.length - 1].id)
73
+ migrations = migrations.filter(m => m.id >= onlyAboveOrEquals);
74
+ if (onlyBelow)
75
+ migrations = migrations.filter(m => m.id < onlyBelow);
76
+ if (migrationsAvailable.length === 0 && migrations.length === 0)
76
77
  process.exit(5);
77
- let highestCommon = onlyAboveOrEquals;
78
- while (highestCommon < migrations.length && highestCommon < migrationsAvailable.length
78
+ if (migrationsAvailable.length && migrationsAvailable.length !== (migrationsAvailable[migrationsAvailable.length - 1].id - migrationsAvailable[0].id + 1))
79
+ process.exit(5);
80
+ let highestCommon = 0;
81
+ while (highestCommon < migrations.length
82
+ && highestCommon < migrationsAvailable.length
83
+ && migrations[highestCommon]
84
+ && migrationsAvailable[highestCommon]
79
85
  && migrations[highestCommon].hash === migrationsAvailable[highestCommon].hash)
80
86
  highestCommon++;
81
- this.applyDownUntil(migrations, migrations.length, highestCommon).then(highestCommon => {
82
- this.applyUpUntil(migrationsAvailable, Math.min(highestCommon, migrations.length), migrationsAvailable.length).then(callback, process.exit);
87
+ this.applyDownUntil(migrations, migrations.length, highestCommon).then(appliedId => {
88
+ this.applyUpUntil(migrationsAvailable, Math.min(appliedId, migrations.length), migrationsAvailable.length).then(callback, process.exit);
83
89
  }, process.exit);
84
90
  });
85
91
  }, () => process.exit(3));
@@ -91,8 +97,10 @@ END`)
91
97
  return Promise.all(tables.map(t => client.request().query('SELECT id FROM ' + t.table + ' WITH (UPDLOCK)')));
92
98
  }
93
99
  applyUpUntil(migrations, current, until) {
94
- if (current < until)
100
+ if (current < until) {
101
+ console.log('Applying migration ' + migrations[current].id);
95
102
  return this.applyUp(migrations[current]).then(() => this.applyUpUntil(migrations, current + 1, until));
103
+ }
96
104
  return Promise.resolve();
97
105
  }
98
106
  applyUp(migration) {
@@ -126,6 +134,7 @@ END`)
126
134
  applyDownUntil(migrations, current, until) {
127
135
  if (current > until && !migrations[current - 1]?.skip) {
128
136
  current--;
137
+ console.log('Reverting migration ' + migrations[current].id);
129
138
  return this.applyDown(migrations[current]).then(() => this.applyDownUntil(migrations, current, until));
130
139
  }
131
140
  return Promise.resolve(current);
@@ -1,168 +1,175 @@
1
- import * as crypto from 'crypto';
2
- import * as fs from 'fs';
3
- import * as Memcached from 'memcached';
4
- import {ConnectionPool, Transaction} from 'mssql';
5
- import {ClientBase, Pool} from 'pg';
6
- import {Logger} from 'winston';
7
- import { ApiCallRepository } from '../ApiCall';
8
- import { Migration, MigrationRepository } from '../Migration';
9
- import {ModelRepr} from './ModelDao';
10
-
11
- export class BaseModelRepository {
12
- Migration: MigrationRepository;
13
- ApiCall: ApiCallRepository;
14
-
15
- constructor(public db: Pool, public dbSpare: Pool, public dbMssql: ConnectionPool, public logger: Logger, public cache: Memcached) {
16
- if(db && dbSpare) {
17
- const dbQuery = db.query.bind(db);
18
- db.query = (function(text: any, values: any, cb: any) {
19
- if((this.idleCount + this.waitingCount) >= this.totalCount && this.totalCount === this.options.max)
20
- return dbSpare.query(text, values, cb);
21
- return dbQuery(text, values, cb);
22
- }).bind(db);
23
- }
24
- this.Migration = new MigrationRepository(db, dbMssql, logger);
25
- this.ApiCall = new ApiCallRepository(db, dbMssql, logger);
26
- }
27
-
28
- // TODO: alter table migration if exists without ok column
29
- migrate(migrationsPath: string, callback: (() => void), onlyAboveOrEquals = 0) {
30
- (this.db ? Promise.all([
31
- this.db.query(`CREATE TABLE IF NOT EXISTS migration (
32
- id integer PRIMARY KEY,
33
- hash text NOT NULL,
34
- "sqlUp" text NOT NULL,
35
- "sqlDown" text NOT NULL,
36
- state integer NOT NULL,
37
- skip boolean NOT NULL
38
- )`),
39
- this.db.query(`ALTER TABLE migration ADD COLUMN IF NOT EXISTS skip BOOLEAN NOT NULL DEFAULT FALSE`)]) : Promise.all([
40
- this.dbMssql.query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=\'migration\' AND xtype=\'U\') CREATE TABLE migration (
41
- id int PRIMARY KEY,
42
- hash text NOT NULL,
43
- "sqlUp" text NOT NULL,
44
- "sqlDown" text NOT NULL,
45
- state int NOT NULL,
46
- skip bit NOT NULL
47
- )`),
48
- this.dbMssql.query(`IF NOT EXISTS (
49
- SELECT *
50
- FROM INFORMATION_SCHEMA.COLUMNS
51
- WHERE TABLE_NAME = 'migration'
52
- AND COLUMN_NAME = 'skip'
53
- )
54
- BEGIN
55
- ALTER TABLE migration ADD skip BIT NOT NULL DEFAULT 0;
56
- END`)])).then(() => {
57
- this.Migration.getAllBy('id').then((migrations: Migration[]) => {
58
- if(migrations.find(migration => migration.state !== 0)) process.exit(4); // Have to fix manually
59
- // Read the new ones
60
- fs.readdir(migrationsPath, (_, files) => {
61
- const migrationsAvailable = files
62
- .filter(file => /[0-9]+\.sql/.test(file))
63
- .map(file => parseInt(file.split('.sql')[0], 10))
64
- .filter(file => file > 0)
65
- .sort((a, b) => a - b)
66
- .map(file => {
67
- const content = fs.readFileSync(migrationsPath + file + '.sql', 'utf-8');
68
- return {
69
- id: file,
70
- content: content,
71
- hash: crypto.createHash('sha256').update(content).digest('hex')
72
- };
73
- });
74
- if(migrationsAvailable.length === 0
75
- || migrationsAvailable.length
76
- !== migrationsAvailable[migrationsAvailable.length - 1].id) process.exit(5); // Did not use OK files
77
- let highestCommon = onlyAboveOrEquals;
78
- while(highestCommon < migrations.length && highestCommon < migrationsAvailable.length
79
- && migrations[highestCommon].hash === migrationsAvailable[highestCommon].hash)
80
- highestCommon++;
81
- this.applyDownUntil(migrations, migrations.length, highestCommon).then(highestCommon => {
82
- this.applyUpUntil(migrationsAvailable, Math.min(highestCommon, migrations.length), migrationsAvailable.length).then(callback, process.exit);
83
- }, process.exit);
84
- });
85
- }, () => process.exit(3));
86
- }, () => process.exit(2));
87
- }
88
-
89
- lockTables(tables: ModelRepr[], client: ClientBase | Transaction): Promise<any> {
90
- if(this.db)
91
- return Promise.all(tables.map(t => (<ClientBase>client).query('LOCK TABLE ' + t.table + ' IN EXCLUSIVE MODE')));
92
- return Promise.all(tables.map(t => (<Transaction>client).request().query('SELECT id FROM ' + t.table + ' WITH (UPDLOCK)')));
93
- }
94
-
95
- private applyUpUntil(migrations: {id: number, content: string, hash: string}[], current: number, until: number): Promise<void> {
96
- if(current < until)
97
- return this.applyUp(migrations[current]).then(() => this.applyUpUntil(migrations, current + 1, until));
98
- return Promise.resolve();
99
- }
100
-
101
- private applyUp(migration: {id: number, content: string, hash: string}): Promise<void> {
102
- return new Promise((resolve, reject) => {
103
- const sqlParts = migration.content.split(/-{4,}/);
104
- this.Migration.create({
105
- id: migration.id,
106
- hash: migration.hash,
107
- sqlUp: sqlParts[0],
108
- sqlDown: sqlParts[1],
109
- state: 2,
110
- skip: false
111
- }).then(() => {
112
- (<any>(this.db|| this.dbMssql)).query(sqlParts[0], (err: any) => {
113
- if(err) {
114
- console.error(err);
115
- reject(10);
116
- } else {
117
- (<any>(this.db|| this.dbMssql)).query('UPDATE "migration" SET "state"=0 WHERE "id"=' + migration.id, (err2: any) => {
118
- if(err2) reject(11); // No cleanup
119
- else resolve();
120
- });
121
- }
122
- });
123
- }, () => process.exit(9));
124
- });
125
- }
126
-
127
- private applyDownUntil(migrations: Migration[], current: number, until: number): Promise<number> {
128
- if(current > until && !migrations[current - 1]?.skip) {
129
- current--;
130
- return this.applyDown(migrations[current]).then(() => this.applyDownUntil(migrations, current, until));
131
- }
132
- return Promise.resolve(current);
133
- }
134
-
135
- private applyDown(migration: Migration): Promise<void> {
136
- return new Promise((resolve, reject) => {
137
- (<any>(this.db|| this.dbMssql)).query('UPDATE "migration" SET "state"=1 WHERE "id"=' + migration.id, (err: any) => {
138
- if(err) reject(6); // No required change
139
- else (<any>(this.db|| this.dbMssql)).query(migration.sqlDown, (err2: any) => {
140
- if(err2) {
141
- console.error(err2);
142
- reject(7);
143
- } // No apply down
144
- else (<any>(this.db|| this.dbMssql)).query('DELETE FROM "migration" WHERE "id"=' + migration.id, (err3: any) => {
145
- if(err3 && migration.id !== 1) reject(8); // No cleanup for not base migration
146
- else {
147
- if(migration.id === 1) {
148
- (this.db ? this.db.query('CREATE TABLE IF NOT EXISTS migration (' +
149
- 'id integer PRIMARY KEY,' +
150
- 'hash text NOT NULL,' +
151
- '"sqlUp" text NOT NULL,' +
152
- '"sqlDown" text NOT NULL,' +
153
- 'state integer NOT NULL' +
154
- ')') : this.dbMssql.query('if not exists (select * from sysobjects where name=\'migration\' and xtype=\'U\') CREATE TABLE migration (' +
155
- 'id int PRIMARY KEY,' +
156
- 'hash text NOT NULL,' +
157
- '"sqlUp" text NOT NULL,' +
158
- '"sqlDown" text NOT NULL,' +
159
- 'state int NOT NULL' +
160
- ')')).then(() => resolve(), reject);
161
- } else resolve();
162
- }
163
- });
164
- });
165
- });
166
- });
167
- }
168
- }
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as Memcached from 'memcached';
4
+ import {ConnectionPool, Transaction} from 'mssql';
5
+ import {ClientBase, Pool} from 'pg';
6
+ import {Logger} from 'winston';
7
+ import { ApiCallRepository } from '../ApiCall';
8
+ import { Migration, MigrationRepository } from '../Migration';
9
+ import {ModelRepr} from './ModelDao';
10
+
11
+ export class BaseModelRepository {
12
+ Migration: MigrationRepository;
13
+ ApiCall: ApiCallRepository;
14
+
15
+ constructor(public db: Pool, public dbSpare: Pool, public dbMssql: ConnectionPool, public logger: Logger, public cache: Memcached) {
16
+ if(db && dbSpare) {
17
+ const dbQuery = db.query.bind(db);
18
+ db.query = (function(text: any, values: any, cb: any) {
19
+ if((this.idleCount + this.waitingCount) >= this.totalCount && this.totalCount === this.options.max)
20
+ return dbSpare.query(text, values, cb);
21
+ return dbQuery(text, values, cb);
22
+ }).bind(db);
23
+ }
24
+ this.Migration = new MigrationRepository(db, dbMssql, logger);
25
+ this.ApiCall = new ApiCallRepository(db, dbMssql, logger);
26
+ }
27
+
28
+ // TODO: alter table migration if exists without ok column
29
+ migrate(migrationsPath: string, callback: (() => void), onlyAboveOrEquals = 0, onlyBelow?: number) {
30
+ (this.db ? Promise.all([
31
+ this.db.query(`CREATE TABLE IF NOT EXISTS migration (
32
+ id integer PRIMARY KEY,
33
+ hash text NOT NULL,
34
+ "sqlUp" text NOT NULL,
35
+ "sqlDown" text NOT NULL,
36
+ state integer NOT NULL,
37
+ skip boolean NOT NULL
38
+ )`),
39
+ this.db.query(`ALTER TABLE migration ADD COLUMN IF NOT EXISTS skip BOOLEAN NOT NULL DEFAULT FALSE`)]) : Promise.all([
40
+ this.dbMssql.query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=\'migration\' AND xtype=\'U\') CREATE TABLE migration (
41
+ id int PRIMARY KEY,
42
+ hash text NOT NULL,
43
+ "sqlUp" text NOT NULL,
44
+ "sqlDown" text NOT NULL,
45
+ state int NOT NULL,
46
+ skip bit NOT NULL
47
+ )`),
48
+ this.dbMssql.query(`IF NOT EXISTS (
49
+ SELECT *
50
+ FROM INFORMATION_SCHEMA.COLUMNS
51
+ WHERE TABLE_NAME = 'migration'
52
+ AND COLUMN_NAME = 'skip'
53
+ )
54
+ BEGIN
55
+ ALTER TABLE migration ADD skip BIT NOT NULL DEFAULT 0;
56
+ END`)])).then(() => {
57
+ this.Migration.getAllBy('id').then((migrations: Migration[]) => {
58
+ if(migrations.find(migration => migration.state !== 0)) process.exit(4); // Have to fix manually
59
+ // Read the new ones
60
+ fs.readdir(migrationsPath, (_, files) => {
61
+ const migrationsAvailable = files
62
+ .filter(file => /[0-9]+\.sql/.test(file))
63
+ .map(file => parseInt(file.split('.sql')[0], 10))
64
+ .filter(file => file > 0)
65
+ .sort((a, b) => a - b)
66
+ .map(file => {
67
+ const content = fs.readFileSync(migrationsPath + file + '.sql', 'utf-8');
68
+ return {
69
+ id: file,
70
+ content: content,
71
+ hash: crypto.createHash('sha256').update(content).digest('hex')
72
+ };
73
+ });
74
+ migrations = migrations.filter(m => m.id >= onlyAboveOrEquals);
75
+ if(onlyBelow) migrations = migrations.filter(m => m.id < onlyBelow);
76
+ if(migrationsAvailable.length === 0 && migrations.length === 0) process.exit(5);
77
+ if(migrationsAvailable.length && migrationsAvailable.length !== (migrationsAvailable[migrationsAvailable.length - 1].id - migrationsAvailable[0].id + 1)) process.exit(5);
78
+ let highestCommon = 0;
79
+ while(highestCommon < migrations.length
80
+ && highestCommon < migrationsAvailable.length
81
+ && migrations[highestCommon]
82
+ && migrationsAvailable[highestCommon]
83
+ && migrations[highestCommon].hash === migrationsAvailable[highestCommon].hash)
84
+ highestCommon++;
85
+ this.applyDownUntil(migrations, migrations.length, highestCommon).then(appliedId => {
86
+ this.applyUpUntil(migrationsAvailable, Math.min(appliedId, migrations.length), migrationsAvailable.length).then(callback, process.exit);
87
+ }, process.exit);
88
+ });
89
+ }, () => process.exit(3));
90
+ }, () => process.exit(2));
91
+ }
92
+
93
+ lockTables(tables: ModelRepr[], client: ClientBase | Transaction): Promise<any> {
94
+ if(this.db)
95
+ return Promise.all(tables.map(t => (<ClientBase>client).query('LOCK TABLE ' + t.table + ' IN EXCLUSIVE MODE')));
96
+ return Promise.all(tables.map(t => (<Transaction>client).request().query('SELECT id FROM ' + t.table + ' WITH (UPDLOCK)')));
97
+ }
98
+
99
+ private applyUpUntil(migrations: {id: number, content: string, hash: string}[], current: number, until: number): Promise<void> {
100
+ if(current < until) {
101
+ console.log('Applying migration ' + migrations[current].id);
102
+ return this.applyUp(migrations[current]).then(() => this.applyUpUntil(migrations, current + 1, until));
103
+ }
104
+ return Promise.resolve();
105
+ }
106
+
107
+ private applyUp(migration: {id: number, content: string, hash: string}): Promise<void> {
108
+ return new Promise((resolve, reject) => {
109
+ const sqlParts = migration.content.split(/-{4,}/);
110
+ this.Migration.create({
111
+ id: migration.id,
112
+ hash: migration.hash,
113
+ sqlUp: sqlParts[0],
114
+ sqlDown: sqlParts[1],
115
+ state: 2,
116
+ skip: false
117
+ }).then(() => {
118
+ (<any>(this.db|| this.dbMssql)).query(sqlParts[0], (err: any) => {
119
+ if(err) {
120
+ console.error(err);
121
+ reject(10);
122
+ } else {
123
+ (<any>(this.db|| this.dbMssql)).query('UPDATE "migration" SET "state"=0 WHERE "id"=' + migration.id, (err2: any) => {
124
+ if(err2) reject(11); // No cleanup
125
+ else resolve();
126
+ });
127
+ }
128
+ });
129
+ }, () => process.exit(9));
130
+ });
131
+ }
132
+
133
+ private applyDownUntil(migrations: Migration[], current: number, until: number): Promise<number> {
134
+ if(current > until && !migrations[current - 1]?.skip) {
135
+ current--;
136
+ console.log('Reverting migration ' + migrations[current].id);
137
+ return this.applyDown(migrations[current]).then(() => this.applyDownUntil(migrations, current, until));
138
+ }
139
+ return Promise.resolve(current);
140
+ }
141
+
142
+ private applyDown(migration: Migration): Promise<void> {
143
+ return new Promise((resolve, reject) => {
144
+ (<any>(this.db|| this.dbMssql)).query('UPDATE "migration" SET "state"=1 WHERE "id"=' + migration.id, (err: any) => {
145
+ if(err) reject(6); // No required change
146
+ else (<any>(this.db|| this.dbMssql)).query(migration.sqlDown, (err2: any) => {
147
+ if(err2) {
148
+ console.error(err2);
149
+ reject(7);
150
+ } // No apply down
151
+ else (<any>(this.db|| this.dbMssql)).query('DELETE FROM "migration" WHERE "id"=' + migration.id, (err3: any) => {
152
+ if(err3 && migration.id !== 1) reject(8); // No cleanup for not base migration
153
+ else {
154
+ if(migration.id === 1) {
155
+ (this.db ? this.db.query('CREATE TABLE IF NOT EXISTS migration (' +
156
+ 'id integer PRIMARY KEY,' +
157
+ 'hash text NOT NULL,' +
158
+ '"sqlUp" text NOT NULL,' +
159
+ '"sqlDown" text NOT NULL,' +
160
+ 'state integer NOT NULL' +
161
+ ')') : this.dbMssql.query('if not exists (select * from sysobjects where name=\'migration\' and xtype=\'U\') CREATE TABLE migration (' +
162
+ 'id int PRIMARY KEY,' +
163
+ 'hash text NOT NULL,' +
164
+ '"sqlUp" text NOT NULL,' +
165
+ '"sqlDown" text NOT NULL,' +
166
+ 'state int NOT NULL' +
167
+ ')')).then(() => resolve(), reject);
168
+ } else resolve();
169
+ }
170
+ });
171
+ });
172
+ });
173
+ });
174
+ }
175
+ }
@@ -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.5",
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",