|
|
|
@ -16,7 +16,7 @@ import { |
|
|
|
import { DatasourcePlus } from "./base/datasourcePlus" |
|
|
|
|
|
|
|
module MySQLModule { |
|
|
|
const mysql = require("mysql2") |
|
|
|
const mysql = require("mysql2/promise") |
|
|
|
const Sql = require("./base/sql") |
|
|
|
|
|
|
|
interface MySQLConfig { |
|
|
|
@ -29,7 +29,7 @@ module MySQLModule { |
|
|
|
} |
|
|
|
|
|
|
|
const SCHEMA: Integration = { |
|
|
|
docs: "https://github.com/mysqljs/mysql", |
|
|
|
docs: "https://github.com/sidorares/node-mysql2", |
|
|
|
plus: true, |
|
|
|
friendlyName: "MySQL", |
|
|
|
description: |
|
|
|
@ -80,36 +80,9 @@ module MySQLModule { |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
function internalQuery( |
|
|
|
client: any, |
|
|
|
query: SqlQuery, |
|
|
|
connect: boolean = true |
|
|
|
): Promise<any[] | any> { |
|
|
|
// Node MySQL is callback based, so we must wrap our call in a promise
|
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
if (connect) { |
|
|
|
client.connect() |
|
|
|
} |
|
|
|
return client.query( |
|
|
|
query.sql, |
|
|
|
query.bindings || {}, |
|
|
|
(error: any, results: object[]) => { |
|
|
|
if (error) { |
|
|
|
reject(error) |
|
|
|
} else { |
|
|
|
resolve(results) |
|
|
|
} |
|
|
|
if (connect) { |
|
|
|
client.end() |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
class MySQLIntegration extends Sql implements DatasourcePlus { |
|
|
|
private config: MySQLConfig |
|
|
|
private readonly client: any |
|
|
|
private client: any |
|
|
|
public tables: Record<string, Table> = {} |
|
|
|
public schemaErrors: Record<string, string> = {} |
|
|
|
|
|
|
|
@ -119,93 +92,127 @@ module MySQLModule { |
|
|
|
if (config.ssl && Object.keys(config.ssl).length === 0) { |
|
|
|
delete config.ssl |
|
|
|
} |
|
|
|
this.client = mysql.createConnection(config) |
|
|
|
this.config = config |
|
|
|
} |
|
|
|
|
|
|
|
async connect() { |
|
|
|
this.client = await mysql.createConnection(this.config) |
|
|
|
} |
|
|
|
|
|
|
|
async disconnect() { |
|
|
|
await this.client.end() |
|
|
|
} |
|
|
|
|
|
|
|
async internalQuery( |
|
|
|
query: SqlQuery, |
|
|
|
connect: boolean = true |
|
|
|
): Promise<any[] | any> { |
|
|
|
try { |
|
|
|
if (connect) { |
|
|
|
await this.connect() |
|
|
|
} |
|
|
|
// Node MySQL is callback based, so we must wrap our call in a promise
|
|
|
|
const response = await this.client.query( |
|
|
|
query.sql, |
|
|
|
query.bindings || [] |
|
|
|
) |
|
|
|
return response[0] |
|
|
|
} finally { |
|
|
|
if (connect) { |
|
|
|
await this.disconnect() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async buildSchema(datasourceId: string, entities: Record<string, Table>) { |
|
|
|
const tables: { [key: string]: Table } = {} |
|
|
|
const database = this.config.database |
|
|
|
this.client.connect() |
|
|
|
|
|
|
|
// get the tables first
|
|
|
|
const tablesResp = await internalQuery( |
|
|
|
this.client, |
|
|
|
{ sql: "SHOW TABLES;" }, |
|
|
|
false |
|
|
|
) |
|
|
|
const tableNames = tablesResp.map( |
|
|
|
(obj: any) => |
|
|
|
obj[`Tables_in_${database}`] || |
|
|
|
obj[`Tables_in_${database.toLowerCase()}`] |
|
|
|
) |
|
|
|
for (let tableName of tableNames) { |
|
|
|
const primaryKeys = [] |
|
|
|
const schema: TableSchema = {} |
|
|
|
const descResp = await internalQuery( |
|
|
|
this.client, |
|
|
|
{ sql: `DESCRIBE \`${tableName}\`;` }, |
|
|
|
await this.connect() |
|
|
|
|
|
|
|
try { |
|
|
|
// get the tables first
|
|
|
|
const tablesResp = await this.internalQuery( |
|
|
|
{ sql: "SHOW TABLES;" }, |
|
|
|
false |
|
|
|
) |
|
|
|
for (let column of descResp) { |
|
|
|
const columnName = column.Field |
|
|
|
if (column.Key === "PRI" && primaryKeys.indexOf(column.Key) === -1) { |
|
|
|
primaryKeys.push(columnName) |
|
|
|
} |
|
|
|
const constraints = { |
|
|
|
presence: column.Null !== "YES", |
|
|
|
} |
|
|
|
const isAuto: boolean = |
|
|
|
typeof column.Extra === "string" && |
|
|
|
(column.Extra === "auto_increment" || |
|
|
|
column.Extra.toLowerCase().includes("generated")) |
|
|
|
schema[columnName] = { |
|
|
|
name: columnName, |
|
|
|
autocolumn: isAuto, |
|
|
|
type: convertSqlType(column.Type), |
|
|
|
constraints, |
|
|
|
const tableNames = tablesResp.map( |
|
|
|
(obj: any) => |
|
|
|
obj[`Tables_in_${database}`] || |
|
|
|
obj[`Tables_in_${database.toLowerCase()}`] |
|
|
|
) |
|
|
|
for (let tableName of tableNames) { |
|
|
|
const primaryKeys = [] |
|
|
|
const schema: TableSchema = {} |
|
|
|
const descResp = await this.internalQuery( |
|
|
|
{ sql: `DESCRIBE \`${tableName}\`;` }, |
|
|
|
false |
|
|
|
) |
|
|
|
for (let column of descResp) { |
|
|
|
const columnName = column.Field |
|
|
|
if ( |
|
|
|
column.Key === "PRI" && |
|
|
|
primaryKeys.indexOf(column.Key) === -1 |
|
|
|
) { |
|
|
|
primaryKeys.push(columnName) |
|
|
|
} |
|
|
|
const constraints = { |
|
|
|
presence: column.Null !== "YES", |
|
|
|
} |
|
|
|
const isAuto: boolean = |
|
|
|
typeof column.Extra === "string" && |
|
|
|
(column.Extra === "auto_increment" || |
|
|
|
column.Extra.toLowerCase().includes("generated")) |
|
|
|
schema[columnName] = { |
|
|
|
name: columnName, |
|
|
|
autocolumn: isAuto, |
|
|
|
type: convertSqlType(column.Type), |
|
|
|
constraints, |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (!tables[tableName]) { |
|
|
|
tables[tableName] = { |
|
|
|
_id: buildExternalTableId(datasourceId, tableName), |
|
|
|
primary: primaryKeys, |
|
|
|
name: tableName, |
|
|
|
schema, |
|
|
|
if (!tables[tableName]) { |
|
|
|
tables[tableName] = { |
|
|
|
_id: buildExternalTableId(datasourceId, tableName), |
|
|
|
primary: primaryKeys, |
|
|
|
name: tableName, |
|
|
|
schema, |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} finally { |
|
|
|
await this.disconnect() |
|
|
|
} |
|
|
|
|
|
|
|
this.client.end() |
|
|
|
const final = finaliseExternalTables(tables, entities) |
|
|
|
this.tables = final.tables |
|
|
|
this.schemaErrors = final.errors |
|
|
|
} |
|
|
|
|
|
|
|
async create(query: SqlQuery | string) { |
|
|
|
const results = await internalQuery(this.client, getSqlQuery(query)) |
|
|
|
const results = await this.internalQuery(getSqlQuery(query)) |
|
|
|
return results.length ? results : [{ created: true }] |
|
|
|
} |
|
|
|
|
|
|
|
async read(query: SqlQuery | string) { |
|
|
|
return internalQuery(this.client, getSqlQuery(query)) |
|
|
|
return this.internalQuery(getSqlQuery(query)) |
|
|
|
} |
|
|
|
|
|
|
|
async update(query: SqlQuery | string) { |
|
|
|
const results = await internalQuery(this.client, getSqlQuery(query)) |
|
|
|
const results = await this.internalQuery(getSqlQuery(query)) |
|
|
|
return results.length ? results : [{ updated: true }] |
|
|
|
} |
|
|
|
|
|
|
|
async delete(query: SqlQuery | string) { |
|
|
|
const results = await internalQuery(this.client, getSqlQuery(query)) |
|
|
|
const results = await this.internalQuery(getSqlQuery(query)) |
|
|
|
return results.length ? results : [{ deleted: true }] |
|
|
|
} |
|
|
|
|
|
|
|
async query(json: QueryJson) { |
|
|
|
this.client.connect() |
|
|
|
const queryFn = (query: any) => internalQuery(this.client, query, false) |
|
|
|
const output = await this.queryWithReturning(json, queryFn) |
|
|
|
this.client.end() |
|
|
|
return output |
|
|
|
await this.connect() |
|
|
|
try { |
|
|
|
const queryFn = (query: any) => this.internalQuery(query, false) |
|
|
|
return await this.queryWithReturning(json, queryFn) |
|
|
|
} finally { |
|
|
|
await this.disconnect() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|