@evidence-dev/mssql
Version:
Microsoft Server SQL driver for Evidence projects
359 lines (337 loc) • 9.04 kB
JavaScript
const {
EvidenceType,
TypeFidelity,
asyncIterableToBatchedAsyncGenerator,
cleanQuery,
exhaustStream
} = require('@evidence-dev/db-commons');
const mssql = require('mssql');
/**
*
* @param {(() => mssql.ISqlType) | mssql.ISqlType} data_type
* @param {undefined} defaultType
* @returns {EvidenceType | undefined}
*/
function nativeTypeToEvidenceType(data_type, defaultType = undefined) {
switch (data_type) {
case mssql.TYPES.Int:
case mssql.TYPES.TinyInt:
case mssql.TYPES.BigInt:
case mssql.TYPES.SmallInt:
case mssql.TYPES.Float:
case mssql.TYPES.Real:
case mssql.TYPES.Decimal:
case mssql.TYPES.Numeric:
case mssql.TYPES.SmallMoney:
case mssql.TYPES.Money:
return EvidenceType.NUMBER;
case mssql.TYPES.DateTime:
case mssql.TYPES.SmallDateTime:
case mssql.TYPES.DateTimeOffset:
case mssql.TYPES.Date:
case mssql.TYPES.DateTime2:
return EvidenceType.DATE;
case mssql.TYPES.VarChar:
case mssql.TYPES.NVarChar:
case mssql.TYPES.Char:
case mssql.TYPES.NChar:
case mssql.TYPES.Xml:
case mssql.TYPES.Text:
case mssql.TYPES.NText:
return EvidenceType.STRING;
case mssql.TYPES.Bit:
return EvidenceType.BOOLEAN;
case mssql.TYPES.Time:
case mssql.TYPES.UniqueIdentifier:
case mssql.TYPES.Binary:
case mssql.TYPES.VarBinary:
case mssql.TYPES.Image:
case mssql.TYPES.TVP:
case mssql.TYPES.UDT:
case mssql.TYPES.Geography:
case mssql.TYPES.Geometry:
case mssql.TYPES.Variant:
default:
return defaultType;
}
}
/**
*
* @param {mssql.IColumnMetadata} fields
* @returns
*/
const mapResultsToEvidenceColumnTypes = function (fields) {
return Object.values(fields).map((field) => {
/** @type {TypeFidelity} */
let typeFidelity = TypeFidelity.PRECISE;
let evidenceType = nativeTypeToEvidenceType(field.type);
if (!evidenceType) {
typeFidelity = TypeFidelity.INFERRED;
evidenceType = EvidenceType.STRING;
}
return {
name: field.name,
evidenceType: evidenceType,
typeFidelity: typeFidelity
};
});
};
const buildConfig = function (database) {
const trust_server_certificate = database.trust_server_certificate ?? 'false';
const encrypt = database.encrypt ?? 'true';
const connection_timeout = database.connection_timeout ?? 15000;
const request_timeout = database.request_timeout ?? 15000;
const credentials = {
user: database.user,
server: database.server,
database: database.database,
password: database.password,
port: parseInt(database.port ?? 1433),
connectionTimeout: parseInt(connection_timeout),
requestTimeout: parseInt(request_timeout),
options: {
trustServerCertificate:
trust_server_certificate === 'true' || trust_server_certificate === true,
encrypt: encrypt === 'true' || encrypt === true
}
};
if (database.authenticationType === 'default') {
return credentials;
} else if (database.authenticationType === 'azure-active-directory-default') {
return credentials;
} else if (database.authenticationType === 'azure-active-directory-access-token') {
credentials.options.token = database.attoken;
return credentials;
} else if (database.authenticationType === 'azure-active-directory-password') {
credentials.options = {
userName: database.pwuname,
password: database.pwpword,
clientId: database.pwclientid,
tenantId: database.pwtenantid
};
return credentials;
} else if (database.authenticationType === 'azure-active-directory-service-principal-secret') {
credentials.options = {
clientId: database.spclientid,
clientSecret: database.spclientsecret,
tenantId: database.sptenantid
};
return credentials;
}
};
/** @type {import("@evidence-dev/db-commons").RunQuery<MsSQLOptions>} */
const runQuery = async (queryString, database = {}, batchSize = 100000) => {
try {
const config = buildConfig(database);
const pool = await mssql.connect(config);
const cleaned_string = cleanQuery(queryString);
const expected_count = await pool
.request()
.query(`SELECT COUNT(*) as expected_row_count FROM (${cleaned_string}) as subquery`)
.catch(() => null);
const expected_row_count = expected_count?.recordset[0].expected_row_count;
const request = new mssql.Request();
request.stream = true;
request.query(queryString);
const columns = await new Promise((res) => request.once('recordset', res));
const stream = request.toReadableStream();
const results = await asyncIterableToBatchedAsyncGenerator(stream, batchSize, {
closeConnection: () => pool.close()
});
results.columnTypes = mapResultsToEvidenceColumnTypes(columns);
results.expectedRowCount = expected_row_count;
return results;
} catch (err) {
if (err.message) {
throw err.message.replace(/\n|\r/g, ' ');
} else {
throw err.replace(/\n|\r/g, ' ');
}
}
};
module.exports = runQuery;
/**
* @typedef {Object} MsSQLOptions
* @property {string} user
* @property {string} host
* @property {string} database
* @property {string} password
* @property {`${number}`} port
* @property {`${boolean}`} trust_server_certificate
* @property {`${boolean}`} encrypt
* @property {`${number}`} connection_timeout
*/
/** @type {import('@evidence-dev/db-commons').GetRunner<MsSQLOptions>} */
module.exports.getRunner = async (opts) => {
return async (queryContent, queryPath, batchSize) => {
// Filter out non-sql files
if (!queryPath.endsWith('.sql')) return null;
return runQuery(queryContent, opts, batchSize);
};
};
/** @type {import('@evidence-dev/db-commons').ConnectionTester<MsSQLOptions>} */
module.exports.testConnection = async (opts) => {
return await runQuery('SELECT 1 AS TEST;', opts) //
.then(exhaustStream)
.then(() => true)
.catch((e) => ({ reason: e.message ?? (e.toString() || 'Invalid Credentials') }));
};
module.exports.options = {
authenticationType: {
title: 'Authentication type',
type: 'select',
secret: false,
nest: false,
required: true,
default: 'sqlauth',
options: [
{
value: 'default',
label: 'SQL Login'
},
{
value: 'azure-active-directory-default',
label: 'DefaultAzureCredential'
},
{
value: 'azure-active-directory-access-token',
label: 'Access token'
},
{
value: 'azure-active-directory-password',
label: 'Entra ID User/Password'
},
{
value: 'azure-active-directory-service-principal-secret',
label: 'Service Principal Secret'
}
],
children: {
default: {
user: {
title: 'Username',
secret: false,
type: 'string',
required: true
},
password: {
title: 'Password',
secret: true,
type: 'string',
required: true
}
},
'azure-active-directory-default': {},
'azure-active-directory-access-token': {
attoken: {
title: 'Access Token',
type: 'string',
secret: true,
required: true
}
},
'azure-active-directory-password': {
pwuname: {
title: 'User',
type: 'string',
secret: false,
required: true
},
pwpword: {
title: 'Pstring',
type: 'string',
secret: true,
required: true
},
pwclientid: {
title: 'Client ID',
type: 'string',
secret: true,
required: true
},
pwtenantid: {
title: 'Tenant ID',
type: 'string',
secret: true,
required: true
}
},
'azure-active-directory-service-principal-secret': {
spclientid: {
title: 'Client ID',
type: 'string',
secret: true,
required: true
},
spclientsecret: {
title: 'Client Secret',
type: 'string',
secret: true,
required: true
},
sptenantid: {
title: 'Tenant ID',
type: 'string',
secret: true,
required: true
}
}
// TODO: authentication types that are not supported yet:
// - tediousjs.github.io/tedious/api-connection.html
// - [ ] ntlm
// - [ ] azure-active-directory-msi-vm
// - [ ] azure-active-directory-msi-app-service
// - [x] default
// - [x] azure-active-directory-default
// - [x] azure-active-directory-password
// - [x] azure-active-directory-access-token
// - [x] azure-active-directory-service-principal-secret
}
},
server: {
title: 'Host',
secret: false,
type: 'string',
required: true
},
database: {
title: 'Database',
secret: false,
type: 'string',
required: true
},
port: {
title: 'Port',
secret: false,
type: 'number',
required: false
},
trust_server_certificate: {
title: 'Trust Server Certificate',
secret: false,
type: 'boolean',
description: 'Should be true for local dev / self-signed certificates',
default: false
},
encrypt: {
title: 'Encrypt',
secret: false,
type: 'boolean',
default: false,
description: 'Should be true when using azure'
},
connection_timeout: {
title: 'Connection Timeout',
secret: false,
type: 'number',
required: false,
description: 'Connection timeout in ms'
},
request_timeout: {
title: 'Request Timeout',
secret: false,
type: 'number',
required: false,
description: 'Request timeout in ms'
}
};