2021-06-24 19:17:26 +02:00
|
|
|
import {
|
2022-08-11 14:50:05 +02:00
|
|
|
DatasourceFieldType,
|
2021-11-05 19:55:36 +01:00
|
|
|
Integration,
|
|
|
|
Operation,
|
2023-06-20 18:07:12 +02:00
|
|
|
ExternalTable,
|
2022-08-11 12:48:58 +02:00
|
|
|
TableSchema,
|
2021-06-24 19:17:26 +02:00
|
|
|
QueryJson,
|
2022-08-11 14:50:05 +02:00
|
|
|
QueryType,
|
2021-06-25 14:46:02 +02:00
|
|
|
SqlQuery,
|
2022-08-11 12:48:58 +02:00
|
|
|
DatasourcePlus,
|
2023-05-05 16:47:55 +02:00
|
|
|
DatasourceFeature,
|
2023-05-15 18:36:16 +02:00
|
|
|
ConnectionInfo,
|
2022-08-11 12:48:58 +02:00
|
|
|
} from "@budibase/types"
|
2021-11-08 19:12:40 +01:00
|
|
|
import {
|
|
|
|
getSqlQuery,
|
|
|
|
buildExternalTableId,
|
|
|
|
convertSqlType,
|
|
|
|
finaliseExternalTables,
|
2022-08-11 14:50:05 +02:00
|
|
|
SqlClient,
|
2021-11-08 19:12:40 +01:00
|
|
|
} from "./utils"
|
2022-08-03 19:20:07 +02:00
|
|
|
import Sql from "./base/sql"
|
2022-12-02 18:28:37 +01:00
|
|
|
import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"
|
2022-08-12 18:03:06 +02:00
|
|
|
const sqlServer = require("mssql")
|
|
|
|
const DEFAULT_SCHEMA = "dbo"
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
interface MSSQLConfig {
|
|
|
|
user: string
|
|
|
|
password: string
|
|
|
|
server: string
|
2023-02-10 12:54:16 +01:00
|
|
|
port: number | string
|
2022-08-12 18:03:06 +02:00
|
|
|
database: string
|
|
|
|
schema: string
|
|
|
|
encrypt?: boolean
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
const SCHEMA: Integration = {
|
|
|
|
docs: "https://github.com/tediousjs/node-mssql",
|
|
|
|
plus: true,
|
|
|
|
description:
|
|
|
|
"Microsoft SQL Server is a relational database management system developed by Microsoft. ",
|
|
|
|
friendlyName: "MS SQL Server",
|
|
|
|
type: "Relational",
|
2023-05-24 10:50:51 +02:00
|
|
|
features: {
|
|
|
|
[DatasourceFeature.CONNECTION_CHECKING]: true,
|
|
|
|
[DatasourceFeature.FETCH_TABLE_NAMES]: true,
|
2023-06-19 17:11:02 +02:00
|
|
|
[DatasourceFeature.EXPORT_SCHEMA]: true,
|
2023-05-24 10:50:51 +02:00
|
|
|
},
|
2022-08-12 18:03:06 +02:00
|
|
|
datasource: {
|
|
|
|
user: {
|
|
|
|
type: DatasourceFieldType.STRING,
|
|
|
|
required: true,
|
|
|
|
default: "localhost",
|
2021-06-24 19:16:48 +02:00
|
|
|
},
|
2022-08-12 18:03:06 +02:00
|
|
|
password: {
|
|
|
|
type: DatasourceFieldType.PASSWORD,
|
|
|
|
required: true,
|
2021-06-24 19:16:48 +02:00
|
|
|
},
|
2022-08-12 18:03:06 +02:00
|
|
|
server: {
|
|
|
|
type: DatasourceFieldType.STRING,
|
|
|
|
default: "localhost",
|
|
|
|
},
|
|
|
|
port: {
|
|
|
|
type: DatasourceFieldType.NUMBER,
|
|
|
|
required: false,
|
|
|
|
default: 1433,
|
|
|
|
},
|
|
|
|
database: {
|
|
|
|
type: DatasourceFieldType.STRING,
|
|
|
|
default: "root",
|
|
|
|
},
|
|
|
|
schema: {
|
|
|
|
type: DatasourceFieldType.STRING,
|
|
|
|
default: DEFAULT_SCHEMA,
|
|
|
|
},
|
|
|
|
encrypt: {
|
|
|
|
type: DatasourceFieldType.BOOLEAN,
|
|
|
|
default: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
query: {
|
|
|
|
create: {
|
|
|
|
type: QueryType.SQL,
|
|
|
|
},
|
|
|
|
read: {
|
|
|
|
type: QueryType.SQL,
|
|
|
|
},
|
|
|
|
update: {
|
|
|
|
type: QueryType.SQL,
|
|
|
|
},
|
|
|
|
delete: {
|
|
|
|
type: QueryType.SQL,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|
|
|
private readonly config: MSSQLConfig
|
|
|
|
private index: number = 0
|
|
|
|
private readonly pool: any
|
|
|
|
private client: any
|
2023-06-20 18:07:12 +02:00
|
|
|
public tables: Record<string, ExternalTable> = {}
|
2022-08-12 18:03:06 +02:00
|
|
|
public schemaErrors: Record<string, string> = {}
|
2021-10-27 19:36:27 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
MASTER_TABLES = [
|
|
|
|
"spt_fallback_db",
|
|
|
|
"spt_fallback_dev",
|
|
|
|
"spt_fallback_usg",
|
|
|
|
"spt_monitor",
|
|
|
|
"MSreplication_options",
|
|
|
|
]
|
|
|
|
TABLES_SQL =
|
|
|
|
"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'"
|
2021-10-27 19:36:27 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
constructor(config: MSSQLConfig) {
|
|
|
|
super(SqlClient.MS_SQL)
|
|
|
|
this.config = config
|
|
|
|
const clientCfg = {
|
|
|
|
...this.config,
|
|
|
|
options: {
|
|
|
|
encrypt: this.config.encrypt,
|
|
|
|
enableArithAbort: true,
|
|
|
|
},
|
2022-03-04 16:12:07 +01:00
|
|
|
}
|
2022-08-12 18:03:06 +02:00
|
|
|
delete clientCfg.encrypt
|
|
|
|
if (!this.pool) {
|
|
|
|
this.pool = new sqlServer.ConnectionPool(clientCfg)
|
2022-03-04 16:12:07 +01:00
|
|
|
}
|
2022-08-12 18:03:06 +02:00
|
|
|
}
|
2022-03-04 16:12:07 +01:00
|
|
|
|
2023-05-12 12:49:08 +02:00
|
|
|
async testConnection() {
|
2023-05-15 18:36:16 +02:00
|
|
|
const response: ConnectionInfo = {
|
|
|
|
connected: false,
|
|
|
|
}
|
2023-05-12 12:49:08 +02:00
|
|
|
try {
|
|
|
|
await this.connect()
|
2023-05-15 18:36:16 +02:00
|
|
|
response.connected = true
|
2023-05-12 12:49:08 +02:00
|
|
|
} catch (e: any) {
|
2023-05-15 18:36:16 +02:00
|
|
|
response.error = e.message as string
|
2023-05-12 12:49:08 +02:00
|
|
|
}
|
2023-05-15 18:36:16 +02:00
|
|
|
return response
|
2023-05-12 12:49:08 +02:00
|
|
|
}
|
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
getBindingIdentifier(): string {
|
|
|
|
return `@p${this.index++}`
|
|
|
|
}
|
2022-03-11 01:19:26 +01:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
getStringConcat(parts: string[]): string {
|
|
|
|
return `concat(${parts.join(", ")})`
|
|
|
|
}
|
|
|
|
|
|
|
|
async connect() {
|
|
|
|
try {
|
|
|
|
this.client = await this.pool.connect()
|
|
|
|
} catch (err) {
|
|
|
|
// @ts-ignore
|
|
|
|
throw new Error(err)
|
2022-03-04 16:12:07 +01:00
|
|
|
}
|
2022-08-12 18:03:06 +02:00
|
|
|
}
|
2022-03-04 16:12:07 +01:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
async internalQuery(
|
|
|
|
query: SqlQuery,
|
|
|
|
operation: string | undefined = undefined
|
|
|
|
) {
|
|
|
|
const client = this.client
|
|
|
|
const request = client.request()
|
|
|
|
this.index = 0
|
|
|
|
try {
|
|
|
|
if (Array.isArray(query.bindings)) {
|
|
|
|
let count = 0
|
|
|
|
for (let binding of query.bindings) {
|
|
|
|
request.input(`p${count++}`, binding)
|
2022-03-02 23:45:10 +01:00
|
|
|
}
|
|
|
|
}
|
2022-08-12 18:03:06 +02: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)
|
|
|
|
} catch (err) {
|
|
|
|
// @ts-ignore
|
|
|
|
throw new Error(err)
|
2022-03-02 23:45:10 +01:00
|
|
|
}
|
2022-08-12 18:03:06 +02:00
|
|
|
}
|
2022-03-02 23:45:10 +01:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
getDefinitionSQL(tableName: string) {
|
|
|
|
return `select *
|
|
|
|
from INFORMATION_SCHEMA.COLUMNS
|
|
|
|
where TABLE_NAME='${tableName}'`
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02: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;`
|
|
|
|
}
|
2021-11-05 13:33:48 +01:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
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-11-05 13:33:48 +01:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
async runSQL(sql: string) {
|
|
|
|
return (await this.internalQuery(getSqlQuery(sql))).recordset
|
|
|
|
}
|
2021-11-05 13:41:26 +01:00
|
|
|
|
2022-08-12 18:03:06 +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
|
|
|
|
*/
|
2023-06-20 18:07:12 +02:00
|
|
|
async buildSchema(
|
|
|
|
datasourceId: string,
|
|
|
|
entities: Record<string, ExternalTable>
|
|
|
|
) {
|
2022-08-12 18:03:06 +02:00
|
|
|
await this.connect()
|
2022-12-02 18:28:37 +01:00
|
|
|
let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL)
|
2022-08-12 18:03:06 +02:00
|
|
|
if (tableInfo == null || !Array.isArray(tableInfo)) {
|
|
|
|
throw "Unable to get list of tables in database"
|
|
|
|
}
|
2022-03-04 16:12:07 +01:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
const schema = this.config.schema || DEFAULT_SCHEMA
|
|
|
|
const tableNames = tableInfo
|
|
|
|
.filter((record: any) => record.TABLE_SCHEMA === schema)
|
|
|
|
.map((record: any) => record.TABLE_NAME)
|
|
|
|
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
|
2021-11-05 13:41:26 +01:00
|
|
|
|
2023-06-20 18:07:12 +02:00
|
|
|
const tables: Record<string, ExternalTable> = {}
|
2022-08-12 18:03:06 +02:00
|
|
|
for (let tableName of tableNames) {
|
|
|
|
// 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)
|
2022-12-02 17:17:56 +01:00
|
|
|
const columns: MSSQLColumn[] = await this.runSQL(
|
|
|
|
this.getAutoColumnsSQL(tableName)
|
|
|
|
)
|
2022-08-12 18:03:06 +02:00
|
|
|
const primaryKeys = constraints
|
|
|
|
.filter(
|
|
|
|
(constraint: any) => constraint.CONSTRAINT_TYPE === "PRIMARY KEY"
|
|
|
|
)
|
|
|
|
.map((constraint: any) => constraint.COLUMN_NAME)
|
|
|
|
const autoColumns = columns
|
2022-12-02 17:17:56 +01:00
|
|
|
.filter(col => col.IS_COMPUTED || col.IS_IDENTITY)
|
|
|
|
.map(col => col.COLUMN_NAME)
|
|
|
|
const requiredColumns = columns
|
|
|
|
.filter(col => col.IS_NULLABLE === "NO")
|
|
|
|
.map(col => col.COLUMN_NAME)
|
2021-11-05 13:41:26 +01:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
let schema: TableSchema = {}
|
|
|
|
for (let def of definition) {
|
|
|
|
const name = def.COLUMN_NAME
|
|
|
|
if (typeof name !== "string") {
|
|
|
|
continue
|
2021-10-27 19:36:27 +02:00
|
|
|
}
|
2023-04-14 19:10:10 +02:00
|
|
|
const hasDefault = def.COLUMN_DEFAULT
|
2023-04-14 18:56:17 +02:00
|
|
|
const isAuto = !!autoColumns.find(col => col === name)
|
|
|
|
const required = !!requiredColumns.find(col => col === name)
|
2022-08-12 18:03:06 +02:00
|
|
|
schema[name] = {
|
2023-04-14 18:56:17 +02:00
|
|
|
autocolumn: isAuto,
|
2022-08-12 18:03:06 +02:00
|
|
|
name: name,
|
2022-12-02 17:17:56 +01:00
|
|
|
constraints: {
|
2023-04-14 19:10:10 +02:00
|
|
|
presence: required && !isAuto && !hasDefault,
|
2022-12-02 17:17:56 +01:00
|
|
|
},
|
2022-08-12 18:03:06 +02:00
|
|
|
...convertSqlType(def.DATA_TYPE),
|
|
|
|
externalType: def.DATA_TYPE,
|
2021-10-27 19:36:27 +02:00
|
|
|
}
|
|
|
|
}
|
2022-08-12 18:03:06 +02:00
|
|
|
tables[tableName] = {
|
|
|
|
_id: buildExternalTableId(datasourceId, tableName),
|
2023-06-20 18:07:12 +02:00
|
|
|
sourceId: datasourceId,
|
2022-08-12 18:03:06 +02:00
|
|
|
primary: primaryKeys,
|
|
|
|
name: tableName,
|
|
|
|
schema,
|
|
|
|
}
|
2021-10-27 19:36:27 +02:00
|
|
|
}
|
2022-08-12 18:03:06 +02:00
|
|
|
const final = finaliseExternalTables(tables, entities)
|
|
|
|
this.tables = final.tables
|
|
|
|
this.schemaErrors = final.errors
|
|
|
|
}
|
2021-10-27 19:36:27 +02:00
|
|
|
|
2023-05-18 23:09:44 +02:00
|
|
|
async queryTableNames() {
|
|
|
|
let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL)
|
|
|
|
const schema = this.config.schema || DEFAULT_SCHEMA
|
|
|
|
return tableInfo
|
|
|
|
.filter((record: any) => record.TABLE_SCHEMA === schema)
|
|
|
|
.map((record: any) => record.TABLE_NAME)
|
|
|
|
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
|
|
|
|
}
|
|
|
|
|
|
|
|
async getTableNames() {
|
|
|
|
await this.connect()
|
|
|
|
return this.queryTableNames()
|
|
|
|
}
|
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
async read(query: SqlQuery | string) {
|
|
|
|
await this.connect()
|
|
|
|
const response = await this.internalQuery(getSqlQuery(query))
|
|
|
|
return response.recordset
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
async create(query: SqlQuery | string) {
|
|
|
|
await this.connect()
|
|
|
|
const response = await this.internalQuery(getSqlQuery(query))
|
|
|
|
return response.recordset || [{ created: true }]
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
async update(query: SqlQuery | string) {
|
|
|
|
await this.connect()
|
|
|
|
const response = await this.internalQuery(getSqlQuery(query))
|
|
|
|
return response.recordset || [{ updated: true }]
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
async delete(query: SqlQuery | string) {
|
|
|
|
await this.connect()
|
|
|
|
const response = await this.internalQuery(getSqlQuery(query))
|
|
|
|
return response.recordset || [{ deleted: true }]
|
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
async query(json: QueryJson) {
|
|
|
|
const schema = this.config.schema
|
|
|
|
await this.connect()
|
|
|
|
if (schema && schema !== DEFAULT_SCHEMA && json?.endpoint) {
|
|
|
|
json.endpoint.schema = schema
|
2021-06-24 19:16:48 +02:00
|
|
|
}
|
2022-08-12 18:03:06 +02:00
|
|
|
const operation = this._operation(json)
|
|
|
|
const queryFn = (query: any, op: string) => this.internalQuery(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
|
|
|
}
|
2023-06-19 17:11:02 +02:00
|
|
|
|
|
|
|
async getExternalSchema() {
|
|
|
|
// Query to retrieve table schema
|
|
|
|
const query = `
|
|
|
|
SELECT
|
|
|
|
t.name AS TableName,
|
|
|
|
c.name AS ColumnName,
|
|
|
|
ty.name AS DataType,
|
|
|
|
c.max_length AS MaxLength,
|
|
|
|
c.is_nullable AS IsNullable,
|
|
|
|
c.is_identity AS IsIdentity
|
|
|
|
FROM
|
|
|
|
sys.tables t
|
|
|
|
INNER JOIN sys.columns c ON t.object_id = c.object_id
|
|
|
|
INNER JOIN sys.types ty ON c.system_type_id = ty.system_type_id
|
|
|
|
WHERE
|
|
|
|
t.is_ms_shipped = 0
|
|
|
|
ORDER BY
|
|
|
|
t.name, c.column_id
|
|
|
|
`
|
|
|
|
|
|
|
|
await this.connect()
|
|
|
|
|
|
|
|
const result = await this.internalQuery({
|
|
|
|
sql: query,
|
|
|
|
})
|
|
|
|
|
|
|
|
const scriptParts = []
|
|
|
|
const tables: any = {}
|
|
|
|
for (const row of result.recordset) {
|
|
|
|
const {
|
|
|
|
TableName,
|
|
|
|
ColumnName,
|
|
|
|
DataType,
|
|
|
|
MaxLength,
|
|
|
|
IsNullable,
|
|
|
|
IsIdentity,
|
|
|
|
} = row
|
|
|
|
|
|
|
|
if (!tables[TableName]) {
|
|
|
|
tables[TableName] = {
|
|
|
|
columns: [],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const columnDefinition = `${ColumnName} ${DataType}${
|
|
|
|
MaxLength ? `(${MaxLength})` : ""
|
|
|
|
}${IsNullable ? " NULL" : " NOT NULL"}`
|
|
|
|
|
|
|
|
tables[TableName].columns.push(columnDefinition)
|
|
|
|
|
|
|
|
if (IsIdentity) {
|
|
|
|
tables[TableName].identityColumn = ColumnName
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate SQL statements for table creation
|
|
|
|
for (const tableName in tables) {
|
|
|
|
const { columns, identityColumn } = tables[tableName]
|
|
|
|
|
|
|
|
let createTableStatement = `CREATE TABLE [${tableName}] (\n`
|
|
|
|
createTableStatement += columns.join(",\n")
|
|
|
|
|
|
|
|
if (identityColumn) {
|
|
|
|
createTableStatement += `,\n CONSTRAINT [PK_${tableName}] PRIMARY KEY (${identityColumn})`
|
|
|
|
}
|
|
|
|
|
|
|
|
createTableStatement += "\n);"
|
|
|
|
|
|
|
|
scriptParts.push(createTableStatement)
|
|
|
|
}
|
|
|
|
|
|
|
|
const schema = scriptParts.join("\n")
|
|
|
|
return schema
|
|
|
|
}
|
2022-08-12 18:03:06 +02:00
|
|
|
}
|
2021-06-24 19:16:48 +02:00
|
|
|
|
2022-08-12 18:03:06 +02:00
|
|
|
export default {
|
|
|
|
schema: SCHEMA,
|
|
|
|
integration: SqlServerIntegration,
|
2021-06-24 19:16:48 +02:00
|
|
|
}
|