1
0
Fork 0
mirror of synced 2024-07-04 14:01:27 +12:00
budibase/packages/server/src/integrations/mysql.ts
2021-10-29 18:43:50 +01:00

295 lines
7.9 KiB
TypeScript

import {
Integration,
DatasourceFieldTypes,
QueryTypes,
Operation,
QueryJson,
SqlQuery,
} from "../definitions/datasource"
import { Table, TableSchema } from "../definitions/common"
import { getSqlQuery } from "./utils"
import { DatasourcePlus } from "./base/datasourcePlus"
module MySQLModule {
const mysql = require("mysql2")
const Sql = require("./base/sql")
const {
buildExternalTableId,
convertType,
finaliseExternalTables,
} = require("./utils")
const { FieldTypes } = require("../constants")
interface MySQLConfig {
host: string
port: number
user: string
password: string
database: string
ssl?: object
}
const TYPE_MAP = {
text: FieldTypes.LONGFORM,
blob: FieldTypes.LONGFORM,
enum: FieldTypes.STRING,
varchar: FieldTypes.STRING,
float: FieldTypes.NUMBER,
int: FieldTypes.NUMBER,
numeric: FieldTypes.NUMBER,
bigint: FieldTypes.NUMBER,
mediumint: FieldTypes.NUMBER,
decimal: FieldTypes.NUMBER,
dec: FieldTypes.NUMBER,
double: FieldTypes.NUMBER,
real: FieldTypes.NUMBER,
fixed: FieldTypes.NUMBER,
smallint: FieldTypes.NUMBER,
timestamp: FieldTypes.DATETIME,
date: FieldTypes.DATETIME,
datetime: FieldTypes.DATETIME,
time: FieldTypes.DATETIME,
tinyint: FieldTypes.BOOLEAN,
json: DatasourceFieldTypes.JSON,
}
const SCHEMA: Integration = {
docs: "https://github.com/mysqljs/mysql",
plus: true,
friendlyName: "MySQL",
description:
"MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
datasource: {
host: {
type: DatasourceFieldTypes.STRING,
default: "localhost",
required: true,
},
port: {
type: DatasourceFieldTypes.NUMBER,
default: 3306,
required: false,
},
user: {
type: DatasourceFieldTypes.STRING,
default: "root",
required: true,
},
password: {
type: DatasourceFieldTypes.PASSWORD,
default: "root",
required: true,
},
database: {
type: DatasourceFieldTypes.STRING,
required: true,
},
ssl: {
type: DatasourceFieldTypes.OBJECT,
required: false,
},
},
query: {
create: {
type: QueryTypes.SQL,
},
read: {
type: QueryTypes.SQL,
},
update: {
type: QueryTypes.SQL,
},
delete: {
type: QueryTypes.SQL,
},
},
}
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
public tables: Record<string, Table> = {}
public schemaErrors: Record<string, string> = {}
constructor(config: MySQLConfig) {
super("mysql")
this.config = config
if (config.ssl && Object.keys(config.ssl).length === 0) {
delete config.ssl
}
this.client = mysql.createConnection(config)
}
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.toLowerCase()}`]
)
for (let tableName of tableNames) {
const primaryKeys = []
const schema: TableSchema = {}
const descResp = await internalQuery(
this.client,
{ 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: convertType(column.Type, TYPE_MAP),
constraints,
}
}
if (!tables[tableName]) {
tables[tableName] = {
_id: buildExternalTableId(datasourceId, tableName),
primary: primaryKeys,
name: tableName,
schema,
}
}
}
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))
return results.length ? results : [{ created: true }]
}
read(query: SqlQuery | string) {
return internalQuery(this.client, getSqlQuery(query))
}
async update(query: SqlQuery | string) {
const results = await internalQuery(this.client, getSqlQuery(query))
return results.length ? results : [{ updated: true }]
}
async delete(query: SqlQuery | string) {
const results = await internalQuery(this.client, getSqlQuery(query))
return results.length ? results : [{ deleted: true }]
}
async getReturningRow(json: QueryJson) {
if (!json.extra || !json.extra.idFilter) {
return {}
}
const input = this._query({
endpoint: {
...json.endpoint,
operation: Operation.READ,
},
fields: [],
filters: json.extra.idFilter,
paginate: {
limit: 1,
},
})
return internalQuery(this.client, input, false)
}
// when creating if an ID has been inserted need to make sure
// the id filter is enriched with it before trying to retrieve the row
checkLookupKeys(results: any, json: QueryJson) {
if (!results?.insertId || !json.meta?.table || !json.meta.table.primary) {
return json
}
const primaryKey = json.meta.table.primary?.[0]
json.extra = {
idFilter: {
equal: {
[primaryKey]: results.insertId,
},
},
}
return json
}
async query(json: QueryJson) {
const operation = this._operation(json)
this.client.connect()
const input = this._query(json, { disableReturning: true })
if (Array.isArray(input)) {
const responses = []
for (let query of input) {
responses.push(await internalQuery(this.client, query))
}
return responses
}
let row
// need to manage returning, a feature mySQL can't do
if (operation === operation.DELETE) {
row = this.getReturningRow(json)
}
const results = await internalQuery(this.client, input, false)
// same as delete, manage returning
if (operation === Operation.CREATE || operation === Operation.UPDATE) {
row = this.getReturningRow(this.checkLookupKeys(results, json))
}
this.client.end()
if (operation !== Operation.READ) {
return row
}
return results.length ? results : [{ [operation.toLowerCase()]: true }]
}
}
module.exports = {
schema: SCHEMA,
integration: MySQLIntegration,
}
}