Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions reverse_engineering/databaseService/databaseService.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const sql = require('mssql');
const { getObjectsFromDatabase, getNewConnectionClientByDb } = require('./helpers');
const getSampleDocSize = require('../helpers/getSampleDocSize');
const { getConnectionClient } = require('./helpers/connection');
const { parseProcedure } = require('../helpers/parsers/parseProcedure');

const PERMISSION_DENIED_CODE = 297;

Expand Down Expand Up @@ -929,6 +930,36 @@ const getWhereClauseForUniqueSchemasAndTables = ({
`OBJECT_SCHEMA_NAME(${schemaAlias || tableAlias}.object_id) IN (${[...schemas].join(', ')})
AND OBJECT_NAME(${tableAlias}.object_id) IN (${[...tables].join(', ')})`;

const getDatabaseProcedures = async ({ client, dbName, logger }) => {
const currentDbConnectionClient = await getClient({
client,
dbName,
meta: {
action: 'getting procedures query',
objects: ['sys.procedures', 'sys.schemas', 'sys.sql_modules'],
skip: true,
},
logger,
});

logger.log('info', { message: `Get '${dbName}' database procedures.` }, 'Reverse Engineering');

const response = await currentDbConnectionClient.query(`
SELECT
s.name AS schema_name,
p.name AS procedure_name,
sm.definition AS procedure_body
FROM sys.procedures p
JOIN sys.schemas s ON p.schema_id = s.schema_id
LEFT JOIN sys.sql_modules sm ON p.object_id = sm.object_id
ORDER BY s.name, p.name;
`);

const rawProcedures = await mapResponse(response);

return rawProcedures.map(parseProcedure);
};

module.exports = {
getConnectionClient,
getObjectsFromDatabase,
Expand All @@ -955,4 +986,5 @@ module.exports = {
getTableSystemTime,
getVersionInfo,
getDescriptionComments,
getDatabaseProcedures,
};
80 changes: 80 additions & 0 deletions reverse_engineering/helpers/parsers/parseProcedure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @typedef {object} RawProcedure
* @property {string} schema_name
* @property {string} procedure_name
* @property {string|null} procedure_body
*
* @typedef {object} Procedure
* @property {string} name
* @property {string} schemaName
* @property {boolean} orReplace
* @property {string} [inputArgs]
* @property {string} [body]
* @property {string} [encryption]
* @property {string} [recompile]
* @property {string} [forReplication]
* @property {string} [executeAs]
*/

const createProcedureRegexp =
/CREATE(?<orReplace>\s*OR\s*ALTER)?\s*(?:PROC|PROCEDURE)\s*(?:[^\s(]+)\s*(?<inputArgs>\((?:[^()']+|'[^']*'|\([^()]*\))*\)|(?:\s*@\w+[^@]*?)*?)?\s*(?<parameters>(?:WITH\s*(?:ENCRYPTION|RECOMPILE|,\s*|EXECUTE\s*AS\s*(?:OWNER|CALLER|SELF|'[^']+'))+)(?:\s*FOR\s*REPLICATION)?)?\s*AS\s*(?<body>[\s\S]*)/im;

Check warning on line 20 in reverse_engineering/helpers/parsers/parseProcedure.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its complexity from 103 to the 20 allowed.

See more on https://sonarcloud.io/project/issues?id=hackolade_SQLServer&issues=AZ0qSI2uEJp05_z-mar1&open=AZ0qSI2uEJp05_z-mar1&pullRequest=188

const encryptionRegexp = /WITH[\s\S]*\b(?<value>ENCRYPTION)/i;
const recompileRegexp = /WITH[\s\S]*\b(?<value>RECOMPILE)/i;
const executeAsRegexp = /WITH[\s\S]*(?:EXECUTE AS\s*(?<value>(?:CALLER|SELF|OWNER|'[^']+')))/i;
const forReplicationRegexp = /\b(?<value>FOR\sREPLICATION)/i;

/**
*
* @param {string} parametersStatement
*/
const parseParameters = parametersStatement => {
if (!parametersStatement) {
return {};
}

const hasEncryption = encryptionRegexp.test(parametersStatement);
const hasRecompile = recompileRegexp.test(parametersStatement);
const hasForReplication = forReplicationRegexp.test(parametersStatement);
const hasExecuteAs = executeAsRegexp.test(parametersStatement);
const executeAs = hasExecuteAs && executeAsRegexp.exec(parametersStatement)[1];

return {
...(hasEncryption && { encryption: 'ENCRYPTION' }),
...(hasRecompile && { recompile: 'RECOMPILE' }),
...(hasForReplication && { forReplication: 'FOR REPLICATION' }),
...(executeAs && { executeAs }),
};
};
/**
*
* @param {RawProcedure} rawProcedure
* @returns {Procedure}
*/
const parseProcedure = rawProcedure => {
const { schema_name, procedure_name, procedure_body } = rawProcedure;

if (!procedure_body) {
return {
name: procedure_name,
schemaName: schema_name,
encryption: 'ENCRYPTION',
};
}
const result = createProcedureRegexp.exec(procedure_body);
const { orReplace, inputArgs, parameters, body } = result.groups;
const procedureParameters = parseParameters(parameters);

return {
name: procedure_name,
schemaName: schema_name,
orReplace: !!orReplace,
inputArgs,
body,
...procedureParameters,
};
};

module.exports = {
parseProcedure,
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
getSpatialIndexes,
getIndexesBucketCount,
getVersionInfo,
getDatabaseProcedures,
} = require('../databaseService/databaseService');
const {
transformDatabaseTableInfoToJSON,
Expand Down Expand Up @@ -296,6 +297,7 @@ const fetchDatabaseMetadata = async ({ client, dbName, tablesInfo, logger }) =>
viewsIndexes,
fullTextIndexes,
spatialIndexes,
procedures,
] = await Promise.all([
getDatabaseIndexes({ client, dbName, tablesInfo, logger }),
getDatabaseMemoryOptimizedTables({ client, dbName, logger }),
Expand All @@ -305,6 +307,7 @@ const fetchDatabaseMetadata = async ({ client, dbName, tablesInfo, logger }) =>
getViewsIndexes({ client, dbName, logger }),
getFullTextIndexes({ client, dbName, allUniqueSchemasAndTables, logger }),
getSpatialIndexes({ client, dbName, allUniqueSchemasAndTables, logger }),
getDatabaseProcedures({ client, dbName, logger }),
]);

const indexesBucketCount = await getIndexesBucketCount({
Expand All @@ -329,6 +332,7 @@ const fetchDatabaseMetadata = async ({ client, dbName, tablesInfo, logger }) =>
viewsIndexes,
fullTextIndexes,
spatialIndexes,
procedures,
};
};

Expand Down Expand Up @@ -490,13 +494,16 @@ const createTableResult = ({
spatialIndexes,
databaseCheckConstraints,
databaseMemoryOptimizedTables,
procedures,
}) => {
const tableIndexes = [...databaseIndexes, ...fullTextIndexes, ...spatialIndexes].filter(
index => index.TableName === tableName && index.schemaName === schemaName,
);

const tableCheckConstraints = databaseCheckConstraints.filter(cc => cc.table === tableName);

const schemaProcedures = procedures.filter(procedure => procedure.schemaName === schemaName);

return {
collectionName: tableName,
dbName: schemaName,
Expand All @@ -511,7 +518,7 @@ const createTableResult = ({
documentTemplate: standardDoc,
collectionDocs: reorderedTableRows,
documents: cleanDocuments(reorderedTableRows),
bucketInfo: { databaseName: dbName },
bucketInfo: { databaseName: dbName, Procedures: schemaProcedures },
modelDefinitions: { definitions: getUserDefinedTypes(tableInfo, databaseUDT) },
emptyBucket: false,
validation: { jsonSchema },
Expand Down Expand Up @@ -549,6 +556,7 @@ const reverseCollectionsToJSON = async ({ client, tablesInfo, reverseEngineering
viewsIndexes,
fullTextIndexes,
spatialIndexes,
procedures,
} = await fetchDatabaseMetadata({ client, dbName, tablesInfo, logger });

return processSchemas({
Expand All @@ -565,6 +573,7 @@ const reverseCollectionsToJSON = async ({ client, tablesInfo, reverseEngineering
viewsIndexes,
fullTextIndexes,
spatialIndexes,
procedures,
});
};

Expand Down