2021-06-24 19:17:26 +02:00
|
|
|
import {
|
|
|
|
DatasourceFieldTypes,
|
2021-11-05 19:55:36 +01:00
|
|
|
Integration,
|
|
|
|
Operation,
|
2021-06-24 19:17:26 +02:00
|
|
|
QueryJson,
|
2021-11-05 19:55:36 +01:00
|
|
|
QueryTypes,
|
2021-06-25 14:46:02 +02:00
|
|
|
SqlQuery,
|
2021-06-27 00:09:46 +02:00
|
|
|
} from "../definitions/datasource"
|
2021-11-08 19:12:40 +01:00
|
|
|
import {
|
|
|
|
getSqlQuery,
|
|
|
|
buildExternalTableId,
|
|
|
|
convertSqlType,
|
|
|
|
finaliseExternalTables,
|
|
|
|
SqlClients,
|
|
|
|
} from "./utils"
|
2021-10-27 19:36:27 +02:00
|
|
|
import { DatasourcePlus } from "./base/datasourcePlus"
|
2021-11-05 13:33:48 +01:00
|
|
|
import { Table, TableSchema } from "../definitions/common"
|
2021-06-24 19:16:48 +02:00
|
|
|
|
|
|
|
module MSSQLModule {
|
|
|
|
const sqlServer = require("mssql")
|
|
|
|
const Sql = require("./base/sql")
|
|
|
|
|
|
|
|
interface MSSQLConfig {
|
|
|
|
user: string
|
|
|
|
password: string
|
|
|
|
server: string
|
|
|
|
port: number
|
|
|
|
database: string
|
|
|
|
encrypt?: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
const SCHEMA: Integration = {
|
|
|
|
docs: "https://github.com/tediousjs/node-mssql",
|
2021-10-27 19:36:27 +02:00
|
|
|
plus: true,
|
2021-06-24 19:16:48 +02:00
|
|
|
description:
|
|
|
|
"Microsoft SQL Server is a relational database management system developed by Microsoft. ",
|
|
|
|
friendlyName: "MS SQL Server",
|
|
|
|
datasource: {
|
|
|
|
user: {
|
|
|
|
type: DatasourceFieldTypes.STRING,
|
|
|
|
required: true,
|
|
|
|
default: "localhost",
|
|
|
|
},
|
|
|
|
password: {
|
|
|
|
type: DatasourceFieldTypes.PASSWORD,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
server: {
|
|
|
|
type: DatasourceFieldTypes.STRING,
|
|
|
|
default: "localhost",
|
|
|
|
},
|
|
|
|
port: {
|
|
|
|
type: DatasourceFieldTypes.NUMBER,
|
|
|
|
required: false,
|
|
|
|
default: 1433,
|
|
|
|
},
|
|
|
|
database: {
|
|
|
|
type: DatasourceFieldTypes.STRING,
|
|
|
|
default: "root",
|
|
|
|
},
|
|
|
|
encrypt: {
|
|
|
|
type: DatasourceFieldTypes.BOOLEAN,
|
|
|
|
default: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
query: {
|
|
|
|
create: {
|
|
|
|
type: QueryTypes.SQL,
|
|
|
|
},
|
|
|
|
read: {
|
|
|
|
type: QueryTypes.SQL,
|
|
|
|
},
|
|
|
|
update: {
|
|
|
|
type: QueryTypes.SQL,
|
|
|
|
},
|
|
|
|
delete: {
|
|
|
|
type: QueryTypes.SQL,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-11-05 19:55:36 +01:00
|
|
|
async function internalQuery(
|
|
|
|
client: any,
|
|
|
|
query: SqlQuery,
|
|
|
|
operation: string | undefined = undefined
|
|
|
|
) {
|
|
|
|
const request = client.request()
|
2021-06-24 19:16:48 +02:00
|
|
|
try {
|
2021-10-27 19:36:27 +02:00
|
|
|
if (Array.isArray(query.bindings)) {
|
|
|
|
let count = 0
|
|
|
|
for (let binding of query.bindings) {
|
2021-11-05 19:55:36 +01:00
|
|
|
request.input(`p${count++}`, binding)
|
2021-10-27 19:36:27 +02:00
|
|
|
}
|
|
|
|
}
|
2021-11-05 19:55:36 +01:00
|
|
|
// this is a hack to get the inserted ID back,
|
|
|
|
// no way to do this with Knex nicely
|
|
|
|
const sql =
|
|
|
|
operation === Operation.CREATE
|
|
|
|
? `${query.sql}; SELECT SCOPE_IDENTITY() AS id;`
|
|
|
|
: query.sql
|
|
|
|
return await request.query(sql)
|
2021-06-24 19:16:48 +02:00
|
|
|
} catch (err) {
|
2021-09-02 19:33:41 +02:00
|
|
|
// @ts-ignore
|
2021-06-24 19:16:48 +02:00
|
|
|
throw new Error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-27 19:36:27 +02:00
|
|
|
class SqlServerIntegration extends Sql implements DatasourcePlus {
|
2021-06-24 19:16:48 +02:00
|
|
|
private readonly config: MSSQLConfig
|
|
|
|
static pool: any
|
2021-10-27 19:36:27 +02:00
|
|
|
public tables: Record<string, Table> = {}
|
|
|
|
public schemaErrors: Record<string, string> = {}
|
|
|
|
|
|
|
|
MASTER_TABLES = [
|
|
|
|
"spt_fallback_db",
|
|
|
|
"spt_fallback_dev",
|
|
|
|
"spt_fallback_usg",
|
|
|
|
"spt_monitor",
|
2021-11-05 13:33:48 +01:00
|
|
|
"MSreplication_options",
|
2021-10-27 19:36:27 +02:00
|
|
|
]
|
2021-11-05 13:33:48 +01:00
|
|
|
TABLES_SQL =
|
|
|
|
"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'"
|
2021-10-27 19:36:27 +02:00
|
|
|
|
|
|
|
getDefinitionSQL(tableName: string) {
|
|
|
|
return `select *
|
|
|
|
from INFORMATION_SCHEMA.COLUMNS
|
|
|
|
where TABLE_NAME='${tableName}'`
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2021-11-05 13:33:48 +01:00
|
|
|
getConstraintsSQL(tableName: string) {
|
|
|
|
return `SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
|
|
|
|
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KU
|
|
|
|
ON TC.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
|
|
AND TC.CONSTRAINT_NAME = KU.CONSTRAINT_NAME
|
|
|
|
AND KU.table_name='${tableName}'
|
|
|
|
ORDER BY
|
|
|
|
KU.TABLE_NAME,
|
|
|
|
KU.ORDINAL_POSITION;`
|
|
|
|
}
|
|
|
|
|
|
|
|
getAutoColumnsSQL(tableName: string) {
|
|
|
|
return `SELECT
|
|
|
|
COLUMNPROPERTY(OBJECT_ID(TABLE_SCHEMA+'.'+TABLE_NAME),COLUMN_NAME,'IsComputed')
|
|
|
|
AS IS_COMPUTED,
|
|
|
|
COLUMNPROPERTY(object_id(TABLE_SCHEMA+'.'+TABLE_NAME), COLUMN_NAME, 'IsIdentity')
|
|
|
|
AS IS_IDENTITY,
|
|
|
|
*
|
|
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
|
|
WHERE TABLE_NAME='${tableName}'`
|
|
|
|
}
|
|
|
|
|
2021-06-24 19:16:48 +02:00
|
|
|
constructor(config: MSSQLConfig) {
|
2021-11-08 19:12:40 +01:00
|
|
|
super(SqlClients.MS_SQL)
|
2021-06-24 19:16:48 +02:00
|
|
|
this.config = config
|
|
|
|
const clientCfg = {
|
|
|
|
...this.config,
|
|
|
|
options: {
|
|
|
|
encrypt: this.config.encrypt,
|
2021-10-27 19:36:27 +02:00
|
|
|
enableArithAbort: true,
|
2021-06-24 19:16:48 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
delete clientCfg.encrypt
|
|
|
|
if (!this.pool) {
|
|
|
|
this.pool = new sqlServer.ConnectionPool(clientCfg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async connect() {
|
|
|
|
try {
|
2021-11-05 19:55:36 +01:00
|
|
|
this.client = await this.pool.connect()
|
2021-06-24 19:16:48 +02:00
|
|
|
} catch (err) {
|
2021-09-02 19:33:41 +02:00
|
|
|
// @ts-ignore
|
2021-06-24 19:16:48 +02:00
|
|
|
throw new Error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-05 13:41:26 +01:00
|
|
|
async runSQL(sql: string) {
|
|
|
|
return (await internalQuery(this.client, getSqlQuery(sql))).recordset
|
|
|
|
}
|
|
|
|
|
2021-10-27 19:36:27 +02:00
|
|
|
/**
|
|
|
|
* Fetches the tables from the sql server database and assigns them to the datasource.
|
|
|
|
* @param {*} datasourceId - datasourceId to fetch
|
|
|
|
* @param entities - the tables that are to be built
|
|
|
|
*/
|
|
|
|
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
|
|
|
|
await this.connect()
|
2021-11-05 13:41:26 +01:00
|
|
|
let tableNames = await this.runSQL(this.TABLES_SQL)
|
2021-11-08 18:25:05 +01:00
|
|
|
if (tableNames == null || !Array.isArray(tableNames)) {
|
2021-10-27 19:36:27 +02:00
|
|
|
throw "Unable to get list of tables in database"
|
|
|
|
}
|
2021-11-08 18:25:05 +01:00
|
|
|
tableNames = tableNames
|
2021-11-05 13:33:48 +01:00
|
|
|
.map((record: any) => record.TABLE_NAME)
|
|
|
|
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
|
2021-11-05 13:41:26 +01:00
|
|
|
|
2021-10-27 19:36:27 +02:00
|
|
|
const tables: Record<string, Table> = {}
|
|
|
|
for (let tableName of tableNames) {
|
2021-11-05 13:41:26 +01:00
|
|
|
// get the column definition (type)
|
|
|
|
const definition = await this.runSQL(this.getDefinitionSQL(tableName))
|
|
|
|
// find primary key constraints
|
|
|
|
const constraints = await this.runSQL(this.getConstraintsSQL(tableName))
|
|
|
|
// find the computed and identity columns (auto columns)
|
|
|
|
const columns = await this.runSQL(this.getAutoColumnsSQL(tableName))
|
|
|
|
const primaryKeys = constraints
|
2021-11-05 13:33:48 +01:00
|
|
|
.filter(
|
|
|
|
(constraint: any) => constraint.CONSTRAINT_TYPE === "PRIMARY KEY"
|
|
|
|
)
|
|
|
|
.map((constraint: any) => constraint.COLUMN_NAME)
|
2021-11-05 13:41:26 +01:00
|
|
|
const autoColumns = columns
|
|
|
|
.filter((col: any) => col.IS_COMPUTED || col.IS_IDENTITY)
|
|
|
|
.map((col: any) => col.COLUMN_NAME)
|
|
|
|
|
2021-10-27 19:36:27 +02:00
|
|
|
let schema: TableSchema = {}
|
2021-11-05 13:41:26 +01:00
|
|
|
for (let def of definition) {
|
2021-10-27 19:36:27 +02:00
|
|
|
const name = def.COLUMN_NAME
|
|
|
|
if (typeof name !== "string") {
|
|
|
|
continue
|
|
|
|
}
|
2021-11-08 19:12:40 +01:00
|
|
|
const type: string = convertSqlType(def.DATA_TYPE)
|
2021-11-05 13:33:48 +01:00
|
|
|
|
2021-10-27 19:36:27 +02:00
|
|
|
schema[name] = {
|
2021-11-05 13:33:48 +01:00
|
|
|
autocolumn: !!autoColumns.find((col: string) => col === name),
|
2021-10-27 19:36:27 +02:00
|
|
|
name: name,
|
|
|
|
type,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
tables[tableName] = {
|
|
|
|
_id: buildExternalTableId(datasourceId, tableName),
|
2021-11-05 13:33:48 +01:00
|
|
|
primary: primaryKeys,
|
2021-10-27 19:36:27 +02:00
|
|
|
name: tableName,
|
|
|
|
schema,
|
|
|
|
}
|
|
|
|
}
|
2021-11-08 19:12:40 +01:00
|
|
|
const final = finaliseExternalTables(tables, entities)
|
2021-11-05 14:48:13 +01:00
|
|
|
this.tables = final.tables
|
|
|
|
this.schemaErrors = final.errors
|
2021-10-27 19:36:27 +02:00
|
|
|
}
|
|
|
|
|
2021-06-25 14:46:02 +02:00
|
|
|
async read(query: SqlQuery | string) {
|
2021-06-24 19:16:48 +02:00
|
|
|
await this.connect()
|
2021-06-25 14:46:02 +02:00
|
|
|
const response = await internalQuery(this.client, getSqlQuery(query))
|
2021-06-24 19:16:48 +02:00
|
|
|
return response.recordset
|
|
|
|
}
|
|
|
|
|
2021-06-25 14:46:02 +02:00
|
|
|
async create(query: SqlQuery | string) {
|
2021-06-24 19:16:48 +02:00
|
|
|
await this.connect()
|
2021-06-25 14:46:02 +02:00
|
|
|
const response = await internalQuery(this.client, getSqlQuery(query))
|
2021-06-24 19:16:48 +02:00
|
|
|
return response.recordset || [{ created: true }]
|
|
|
|
}
|
|
|
|
|
2021-06-25 14:46:02 +02:00
|
|
|
async update(query: SqlQuery | string) {
|
2021-06-24 19:16:48 +02:00
|
|
|
await this.connect()
|
2021-06-25 14:46:02 +02:00
|
|
|
const response = await internalQuery(this.client, getSqlQuery(query))
|
2021-06-24 19:16:48 +02:00
|
|
|
return response.recordset || [{ updated: true }]
|
|
|
|
}
|
|
|
|
|
2021-06-25 14:46:02 +02:00
|
|
|
async delete(query: SqlQuery | string) {
|
2021-06-24 19:16:48 +02:00
|
|
|
await this.connect()
|
2021-06-25 14:46:02 +02:00
|
|
|
const response = await internalQuery(this.client, getSqlQuery(query))
|
2021-06-24 19:16:48 +02:00
|
|
|
return response.recordset || [{ deleted: true }]
|
|
|
|
}
|
|
|
|
|
|
|
|
async query(json: QueryJson) {
|
2021-10-27 19:36:27 +02:00
|
|
|
await this.connect()
|
2021-11-05 19:55:36 +01:00
|
|
|
const operation = this._operation(json)
|
|
|
|
const queryFn = (query: any, op: string) =>
|
|
|
|
internalQuery(this.client, query, op)
|
|
|
|
const processFn = (result: any) =>
|
|
|
|
result.recordset ? result.recordset : [{ [operation]: true }]
|
|
|
|
return this.queryWithReturning(json, queryFn, processFn)
|
2021-06-24 19:16:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
schema: SCHEMA,
|
|
|
|
integration: SqlServerIntegration,
|
|
|
|
}
|
|
|
|
}
|