@naturalcycles/mysql-lib 3.8.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.
- package/CHANGELOG.md +198 -0
- package/dist/dbAdapter.d.ts +2 -0
- package/dist/dbAdapter.js +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/dist/mysql.db.d.ts +58 -0
- package/dist/mysql.db.js +214 -0
- package/dist/query.util.d.ts +11 -0
- package/dist/query.util.js +177 -0
- package/dist/schema/mysql.schema.util.d.ts +24 -0
- package/dist/schema/mysql.schema.util.js +109 -0
- package/package.json +52 -0
- package/readme.md +14 -0
- package/src/dbAdapter.ts +6 -0
- package/src/index.ts +7 -0
- package/src/mysql.db.ts +312 -0
- package/src/query.util.ts +216 -0
- package/src/schema/mysql.schema.util.ts +131 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { JsonSchemaObject, JsonSchemaRootObject } from '@naturalcycles/js-lib';
|
|
2
|
+
export interface MySQLTableStats {
|
|
3
|
+
Field: string;
|
|
4
|
+
Type: string;
|
|
5
|
+
Null: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MySQLSchemaOptions {
|
|
8
|
+
/**
|
|
9
|
+
* @default 'InnoDB'
|
|
10
|
+
*/
|
|
11
|
+
engine?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* It currently skips nullability and declares everything as "DEFAULT NULL".
|
|
15
|
+
*/
|
|
16
|
+
export declare function jsonSchemaToMySQLDDL(table: string, schema: JsonSchemaObject<any>, opt?: MySQLSchemaOptions): string;
|
|
17
|
+
export declare function mysqlTableStatsToJsonSchemaField<T = any>(table: string, stats: MySQLTableStats[]): JsonSchemaRootObject<T>;
|
|
18
|
+
/**
|
|
19
|
+
* Because MySQL doesn't support `.` in field names and escapes them as tableName + fieldName.
|
|
20
|
+
*
|
|
21
|
+
* @param name
|
|
22
|
+
*/
|
|
23
|
+
export declare function mapNameToMySQL(name: string): string;
|
|
24
|
+
export declare function mapNameFromMySQL(name: string): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapNameFromMySQL = exports.mapNameToMySQL = exports.mysqlTableStatsToJsonSchemaField = exports.jsonSchemaToMySQLDDL = void 0;
|
|
4
|
+
const mysql = require("mysql");
|
|
5
|
+
/**
|
|
6
|
+
* It currently skips nullability and declares everything as "DEFAULT NULL".
|
|
7
|
+
*/
|
|
8
|
+
function jsonSchemaToMySQLDDL(table, schema, opt = {}) {
|
|
9
|
+
const { engine = 'InnoDB' } = opt;
|
|
10
|
+
const lines = [`CREATE TABLE ${mysql.escapeId(table)} (`];
|
|
11
|
+
const innerLines = Object.entries(schema.properties).map(([k, s]) => {
|
|
12
|
+
if (k === 'id') {
|
|
13
|
+
return `id VARCHAR(255) NOT NULL`;
|
|
14
|
+
}
|
|
15
|
+
let type;
|
|
16
|
+
if (s.type === 'string') {
|
|
17
|
+
// can specify isoDate later
|
|
18
|
+
type = 'LONGTEXT';
|
|
19
|
+
}
|
|
20
|
+
else if (s.type === 'integer') {
|
|
21
|
+
type = 'INT(11)';
|
|
22
|
+
}
|
|
23
|
+
else if (s.type === 'number') {
|
|
24
|
+
if (['unixTimestamp', 'int32'].includes(s.format)) {
|
|
25
|
+
type = 'INT(11)';
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
type = 'FLOAT(11)';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (s.type === 'boolean') {
|
|
32
|
+
type = 'TINYINT(1)';
|
|
33
|
+
}
|
|
34
|
+
else if (s.instanceof === 'Buffer') {
|
|
35
|
+
type = 'LONGBLOB';
|
|
36
|
+
}
|
|
37
|
+
else if (s.type === 'null') {
|
|
38
|
+
type = 'VARCHAR(255)';
|
|
39
|
+
}
|
|
40
|
+
else if (s.type === 'array') {
|
|
41
|
+
type = 'LONGTEXT'; // to be JSON.stringified?
|
|
42
|
+
}
|
|
43
|
+
else if (s.type === 'object') {
|
|
44
|
+
type = 'LONGTEXT'; // to be JSON.stringified?
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// default
|
|
48
|
+
type = 'LONGTEXT';
|
|
49
|
+
}
|
|
50
|
+
const tokens = [mysql.escapeId(mapNameToMySQL(k)), type, `DEFAULT NULL`];
|
|
51
|
+
return tokens.join(' ');
|
|
52
|
+
});
|
|
53
|
+
innerLines.push(`PRIMARY KEY (id)`);
|
|
54
|
+
lines.push(innerLines.join(',\n'));
|
|
55
|
+
lines.push(`) ENGINE=${engine}`);
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
exports.jsonSchemaToMySQLDDL = jsonSchemaToMySQLDDL;
|
|
59
|
+
function mysqlTableStatsToJsonSchemaField(table, stats) {
|
|
60
|
+
const s = {
|
|
61
|
+
$id: `${table}.schema.json`,
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {},
|
|
64
|
+
required: [],
|
|
65
|
+
additionalProperties: true,
|
|
66
|
+
};
|
|
67
|
+
stats.forEach(stat => {
|
|
68
|
+
const name = stat.Field;
|
|
69
|
+
const t = stat.Type.toLowerCase();
|
|
70
|
+
const notNull = stat.Null?.toUpperCase() !== 'YES';
|
|
71
|
+
if (notNull) {
|
|
72
|
+
s.required.push(name);
|
|
73
|
+
}
|
|
74
|
+
if (t.includes('text') || t.includes('char')) {
|
|
75
|
+
s.properties[name] = { type: 'string' };
|
|
76
|
+
}
|
|
77
|
+
else if (t.includes('lob')) {
|
|
78
|
+
s.properties[name] = { instanceof: 'Buffer' };
|
|
79
|
+
}
|
|
80
|
+
else if (t.startsWith('tinyint') || t.includes('(1)')) {
|
|
81
|
+
s.properties[name] = { type: 'boolean' };
|
|
82
|
+
}
|
|
83
|
+
else if (t.startsWith('int(')) {
|
|
84
|
+
s.properties[name] = { type: 'integer' };
|
|
85
|
+
}
|
|
86
|
+
else if (t.startsWith('float')) {
|
|
87
|
+
s.properties[name] = { type: 'number' };
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(s);
|
|
91
|
+
throw new Error(`Unknown mysql field type ${name} ${stat.Type}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return s;
|
|
95
|
+
}
|
|
96
|
+
exports.mysqlTableStatsToJsonSchemaField = mysqlTableStatsToJsonSchemaField;
|
|
97
|
+
/**
|
|
98
|
+
* Because MySQL doesn't support `.` in field names and escapes them as tableName + fieldName.
|
|
99
|
+
*
|
|
100
|
+
* @param name
|
|
101
|
+
*/
|
|
102
|
+
function mapNameToMySQL(name) {
|
|
103
|
+
return name.replace(/\./g, '_dot_');
|
|
104
|
+
}
|
|
105
|
+
exports.mapNameToMySQL = mapNameToMySQL;
|
|
106
|
+
function mapNameFromMySQL(name) {
|
|
107
|
+
return name.replace(/_dot_/g, '.');
|
|
108
|
+
}
|
|
109
|
+
exports.mapNameFromMySQL = mapNameFromMySQL;
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@naturalcycles/mysql-lib",
|
|
3
|
+
"scripts": {
|
|
4
|
+
"prepare": "husky install"
|
|
5
|
+
},
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"@naturalcycles/db-lib": ">=7.0.0"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@naturalcycles/db-lib": "^8.16.2",
|
|
11
|
+
"@naturalcycles/js-lib": "^14.47.0",
|
|
12
|
+
"@naturalcycles/nodejs-lib": "^12.37.0",
|
|
13
|
+
"@types/mysql": "^2.15.19",
|
|
14
|
+
"mysql": "^2.17.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@naturalcycles/dev-lib": "^12.7.5",
|
|
18
|
+
"@types/node": "^16.10.2",
|
|
19
|
+
"dotenv": "^10.0.0",
|
|
20
|
+
"jest": "^27.2.4"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"!src/test",
|
|
26
|
+
"!src/**/*.test.ts",
|
|
27
|
+
"!src/**/__snapshots__",
|
|
28
|
+
"!src/**/__exclude"
|
|
29
|
+
],
|
|
30
|
+
"main": "dist/index.js",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/NaturalCycles/mysql-lib"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=14.15.0"
|
|
41
|
+
},
|
|
42
|
+
"version": "3.8.0",
|
|
43
|
+
"description": "MySQL client implementing CommonDB interface",
|
|
44
|
+
"keywords": [
|
|
45
|
+
"mysql",
|
|
46
|
+
"lib",
|
|
47
|
+
"db-lib",
|
|
48
|
+
"commondb"
|
|
49
|
+
],
|
|
50
|
+
"author": "Natural Cycles Team",
|
|
51
|
+
"license": "MIT"
|
|
52
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## @naturalcycles/mysql-lib
|
|
2
|
+
|
|
3
|
+
> MySQL client implementing CommonDB interface
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@naturalcycles/mysql-lib)
|
|
6
|
+
[](https://circleci.com/gh/NaturalCycles/mysql-lib)
|
|
7
|
+
[](https://github.com/prettier/prettier)
|
|
8
|
+
|
|
9
|
+
# Packaging
|
|
10
|
+
|
|
11
|
+
- `engines.node >= 10.13`: Latest Node.js LTS
|
|
12
|
+
- `main: dist/index.js`: commonjs, es2018
|
|
13
|
+
- `types: dist/index.d.ts`: typescript types
|
|
14
|
+
- `/src` folder with source `*.ts` files included
|
package/src/dbAdapter.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getDBAdapter } from './dbAdapter'
|
|
2
|
+
import { MysqlDB, MysqlDBCfg, MysqlDBOptions, MysqlDBSaveOptions } from './mysql.db'
|
|
3
|
+
import { jsonSchemaToMySQLDDL } from './schema/mysql.schema.util'
|
|
4
|
+
|
|
5
|
+
export type { MysqlDBCfg, MysqlDBOptions, MysqlDBSaveOptions }
|
|
6
|
+
|
|
7
|
+
export { MysqlDB, jsonSchemaToMySQLDDL, getDBAdapter }
|
package/src/mysql.db.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { Transform } from 'stream'
|
|
2
|
+
import { promisify } from 'util'
|
|
3
|
+
import {
|
|
4
|
+
BaseCommonDB,
|
|
5
|
+
BaseDBEntity,
|
|
6
|
+
CommonDB,
|
|
7
|
+
CommonDBCreateOptions,
|
|
8
|
+
CommonDBOptions,
|
|
9
|
+
CommonDBSaveOptions,
|
|
10
|
+
DBQuery,
|
|
11
|
+
ObjectWithId,
|
|
12
|
+
RunQueryResult,
|
|
13
|
+
} from '@naturalcycles/db-lib'
|
|
14
|
+
import {
|
|
15
|
+
_filterNullishValues,
|
|
16
|
+
_mapKeys,
|
|
17
|
+
_mapValues,
|
|
18
|
+
_Memo,
|
|
19
|
+
JsonSchemaObject,
|
|
20
|
+
JsonSchemaRootObject,
|
|
21
|
+
} from '@naturalcycles/js-lib'
|
|
22
|
+
import { Debug, ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
23
|
+
import { white } from '@naturalcycles/nodejs-lib/dist/colors'
|
|
24
|
+
import { Connection, Pool, PoolConfig, PoolConnection, QueryOptions, TypeCast } from 'mysql'
|
|
25
|
+
import * as mysql from 'mysql'
|
|
26
|
+
import { dbQueryToSQLDelete, dbQueryToSQLSelect, insertSQL } from './query.util'
|
|
27
|
+
import {
|
|
28
|
+
jsonSchemaToMySQLDDL,
|
|
29
|
+
mapNameFromMySQL,
|
|
30
|
+
MySQLTableStats,
|
|
31
|
+
mysqlTableStatsToJsonSchemaField,
|
|
32
|
+
} from './schema/mysql.schema.util'
|
|
33
|
+
|
|
34
|
+
export interface MysqlDBOptions extends CommonDBOptions {}
|
|
35
|
+
export interface MysqlDBSaveOptions extends CommonDBSaveOptions {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @default false / undefined
|
|
39
|
+
*/
|
|
40
|
+
export interface MysqlDBCfg extends PoolConfig {
|
|
41
|
+
/**
|
|
42
|
+
* If true - will log all produced SQL queries.
|
|
43
|
+
*/
|
|
44
|
+
logSQL?: boolean
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* If true - will emit logs of connection events.
|
|
48
|
+
*/
|
|
49
|
+
debugConnections?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const log = Debug('nc:mysql-lib')
|
|
53
|
+
|
|
54
|
+
const BOOLEAN_TYPES = new Set(['TINY', 'TINYINT', 'INT'])
|
|
55
|
+
const BOOLEAN_BIT_TYPES = new Set(['BIT'])
|
|
56
|
+
|
|
57
|
+
const typeCast: TypeCast = (field, next) => {
|
|
58
|
+
// cast to boolean
|
|
59
|
+
if (field.length === 1) {
|
|
60
|
+
if (BOOLEAN_BIT_TYPES.has(field.type)) {
|
|
61
|
+
const b = field.buffer()
|
|
62
|
+
if (b === null || b === undefined) return undefined // null = undefined
|
|
63
|
+
return b[0] === 1 // 1 = true, 0 = false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (BOOLEAN_TYPES.has(field.type)) {
|
|
67
|
+
const s = field.string()
|
|
68
|
+
// console.log(field.name, field.type, s, s?.charCodeAt(0))
|
|
69
|
+
if (s === null || s === undefined) return undefined // null = undefined
|
|
70
|
+
return s === '1' // 1 = true, 0 = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return next()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class MysqlDB extends BaseCommonDB implements CommonDB {
|
|
78
|
+
constructor(cfg: MysqlDBCfg = {}) {
|
|
79
|
+
super()
|
|
80
|
+
this.cfg = {
|
|
81
|
+
typeCast,
|
|
82
|
+
// charset: 'utf8mb4', // for emoji support
|
|
83
|
+
// host: 'localhost',
|
|
84
|
+
// user: MYSQL_USER,
|
|
85
|
+
// password: MYSQL_PW,
|
|
86
|
+
// database: MYSQL_DB,
|
|
87
|
+
...cfg,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cfg!: MysqlDBCfg
|
|
92
|
+
|
|
93
|
+
override async ping(): Promise<void> {
|
|
94
|
+
const con = await this.getConnection()
|
|
95
|
+
con.release()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@_Memo()
|
|
99
|
+
pool(): Pool {
|
|
100
|
+
const pool = mysql.createPool(this.cfg)
|
|
101
|
+
const { host, database } = this.cfg
|
|
102
|
+
log(`connected to ${white(host + '/' + database)}`)
|
|
103
|
+
|
|
104
|
+
if (this.cfg.debugConnections) {
|
|
105
|
+
pool.on('acquire', con => {
|
|
106
|
+
log(`acquire(${con.threadId})`)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
pool.on('connection', con => {
|
|
110
|
+
log(`connection(${con.threadId})`)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
pool.on('enqueue', () => {
|
|
114
|
+
log(`enqueue`)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
pool.on('release', con => {
|
|
118
|
+
log(`release(${con.threadId})`)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pool.on('error', err => {
|
|
123
|
+
log.error(`error`, err)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
return pool
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async close(): Promise<void> {
|
|
130
|
+
const pool = this.pool()
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
132
|
+
await promisify(pool.end.bind(pool))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Be careful to always call `con.release()` when you get connection with this method.
|
|
137
|
+
*/
|
|
138
|
+
async getConnection(): Promise<PoolConnection> {
|
|
139
|
+
const pool = this.pool()
|
|
140
|
+
return await promisify(pool.getConnection.bind(pool))()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Manually create a single (not pool) connection.
|
|
145
|
+
* Be careful to manage this connection yourself (open, release, etc).
|
|
146
|
+
*/
|
|
147
|
+
async createSingleConnection(): Promise<Connection> {
|
|
148
|
+
const con = mysql.createConnection(this.cfg)
|
|
149
|
+
await promisify(con.connect.bind(con))()
|
|
150
|
+
const { threadId } = con
|
|
151
|
+
|
|
152
|
+
if (this.cfg.debugConnections) {
|
|
153
|
+
log(`createSingleConnection(${threadId})`)
|
|
154
|
+
|
|
155
|
+
con.on('connect', () => log(`createSingleConnection(${threadId}).connect`))
|
|
156
|
+
con.on('drain', () => log(`createSingleConnection(${threadId}).drain`))
|
|
157
|
+
con.on('enqueue', () => log(`createSingleConnection(${threadId}).enqueue`))
|
|
158
|
+
con.on('end', () => log(`createSingleConnection(${threadId}).end`))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
con.on('error', err => {
|
|
162
|
+
log.error(`createSingleConnection(${threadId}).error`, err)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return con
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// GET
|
|
169
|
+
override async getByIds<ROW extends ObjectWithId>(
|
|
170
|
+
table: string,
|
|
171
|
+
ids: string[],
|
|
172
|
+
opt: MysqlDBOptions = {},
|
|
173
|
+
): Promise<ROW[]> {
|
|
174
|
+
if (!ids.length) return []
|
|
175
|
+
const q = new DBQuery<ROW>(table).filterEq('id', ids)
|
|
176
|
+
const { rows } = await this.runQuery(q, opt)
|
|
177
|
+
return rows.map(r => _mapKeys(r, k => mapNameFromMySQL(k)) as any)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// QUERY
|
|
181
|
+
override async runQuery<ROW extends ObjectWithId, OUT = ROW>(
|
|
182
|
+
q: DBQuery<ROW>,
|
|
183
|
+
_opt: MysqlDBOptions = {},
|
|
184
|
+
): Promise<RunQueryResult<OUT>> {
|
|
185
|
+
const sql = dbQueryToSQLSelect(q)
|
|
186
|
+
const rows = await this.runSQL<ROW[]>({ sql })
|
|
187
|
+
return {
|
|
188
|
+
rows: rows.map(r => _mapKeys(_filterNullishValues(r, true), k => mapNameFromMySQL(k)) as any),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async runSQL<RESULT>(q: QueryOptions): Promise<RESULT> {
|
|
193
|
+
if (this.cfg.logSQL) log(q.sql, q.values)
|
|
194
|
+
|
|
195
|
+
return await new Promise<RESULT>((resolve, reject) => {
|
|
196
|
+
this.pool().query(q, (err, res) => {
|
|
197
|
+
if (err) return reject(err)
|
|
198
|
+
resolve(res)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override async runQueryCount<ROW extends ObjectWithId>(
|
|
204
|
+
q: DBQuery<ROW>,
|
|
205
|
+
_opt?: CommonDBOptions,
|
|
206
|
+
): Promise<number> {
|
|
207
|
+
const { rows } = await this.runQuery(q.select(['count(*) as _count']))
|
|
208
|
+
return (rows[0] as any)._count
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
override streamQuery<ROW extends ObjectWithId, OUT = ROW>(
|
|
212
|
+
q: DBQuery<ROW>,
|
|
213
|
+
_opt: MysqlDBOptions = {},
|
|
214
|
+
): ReadableTyped<OUT> {
|
|
215
|
+
const sql = dbQueryToSQLSelect(q)
|
|
216
|
+
|
|
217
|
+
if (this.cfg.logSQL) log(`stream: ${sql}`)
|
|
218
|
+
|
|
219
|
+
// return this.streamSQL(sql, opt)
|
|
220
|
+
return this.pool()
|
|
221
|
+
.query(sql)
|
|
222
|
+
.stream()
|
|
223
|
+
.pipe(
|
|
224
|
+
new Transform({
|
|
225
|
+
objectMode: true,
|
|
226
|
+
transform(row, _encoding, cb) {
|
|
227
|
+
cb(null, _filterNullishValues(row, true))
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// SAVE
|
|
234
|
+
override async saveBatch<ROW extends BaseDBEntity>(
|
|
235
|
+
table: string,
|
|
236
|
+
_rows: ROW[],
|
|
237
|
+
_opt?: MysqlDBSaveOptions,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
if (!_rows.length) return
|
|
240
|
+
|
|
241
|
+
// Stringify object values
|
|
242
|
+
const rows = _rows.map(row =>
|
|
243
|
+
_mapValues(row, v => {
|
|
244
|
+
return v && typeof v === 'object' && !Buffer.isBuffer(v) ? JSON.stringify(v) : v
|
|
245
|
+
}),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// inserts are split into multiple sentenses to respect the max_packet_size (1Mb usually)
|
|
249
|
+
const sqls = insertSQL(table, rows)
|
|
250
|
+
|
|
251
|
+
for await (const sql of sqls) {
|
|
252
|
+
await this.runSQL({ sql })
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// DELETE
|
|
257
|
+
/**
|
|
258
|
+
* Limitation: always returns [], regardless of which rows are actually deleted
|
|
259
|
+
*/
|
|
260
|
+
override async deleteByIds(table: string, ids: string[], _opt?: MysqlDBOptions): Promise<number> {
|
|
261
|
+
if (!ids.length) return 0
|
|
262
|
+
const sql = dbQueryToSQLDelete(new DBQuery(table).filterEq('id', ids))
|
|
263
|
+
const res = await this.runSQL<any>({ sql })
|
|
264
|
+
return res.affectedRows
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
override async deleteByQuery<ROW extends ObjectWithId>(
|
|
268
|
+
q: DBQuery<ROW>,
|
|
269
|
+
_opt?: CommonDBOptions,
|
|
270
|
+
): Promise<number> {
|
|
271
|
+
const sql = dbQueryToSQLDelete(q)
|
|
272
|
+
const res = await this.runSQL<any>({ sql })
|
|
273
|
+
return res.affectedRows
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Use with caution!
|
|
278
|
+
*/
|
|
279
|
+
async dropTable(table: string): Promise<void> {
|
|
280
|
+
await this.runSQL({ sql: `DROP TABLE IF EXISTS ${table}` })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* dropIfExists=true needed as a safety check
|
|
285
|
+
*/
|
|
286
|
+
override async createTable(
|
|
287
|
+
table: string,
|
|
288
|
+
schema: JsonSchemaObject,
|
|
289
|
+
opt: CommonDBCreateOptions = {},
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
if (opt.dropIfExists) await this.dropTable(table)
|
|
292
|
+
|
|
293
|
+
const sql = jsonSchemaToMySQLDDL(table, schema)
|
|
294
|
+
await this.runSQL({ sql })
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
override async getTables(): Promise<string[]> {
|
|
298
|
+
return (await this.runSQL<Record<any, any>[]>({ sql: `show tables` }))
|
|
299
|
+
.map(r => Object.values(r)[0])
|
|
300
|
+
.filter(Boolean)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
override async getTableSchema<ROW extends ObjectWithId>(
|
|
304
|
+
table: string,
|
|
305
|
+
): Promise<JsonSchemaRootObject<ROW>> {
|
|
306
|
+
const stats = await this.runSQL<MySQLTableStats[]>({
|
|
307
|
+
sql: `describe ${mysql.escapeId(table)}`,
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
return mysqlTableStatsToJsonSchemaField<ROW>(table, stats)
|
|
311
|
+
}
|
|
312
|
+
}
|