Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: auto crud #51

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
11 changes: 6 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
docs
node_modules/**/*
.idea*
*.log
.vscode
docs
node_modules/**/*
.idea*
*.log
.vscode
package-lock.json
45 changes: 45 additions & 0 deletions crud/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module.exports = (binding) => {
let columns = binding.fields.filter(f => !f.identity).map(f => `[${f.column}]`).join(', ');
return `
CREATE PROCEDURE ${binding.spName}
@data ${binding.tt} READONLY
AS
SET NOCOUNT ON
DECLARE @result ${binding.tt}
BEGIN TRY
INSERT INTO ${binding.name} (${columns})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also have a generic logic for checking for already existing records and raising standard errors?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I just wrote some generic SQL code which I didn't mean to be final ... just to test the concept.
We can change the bodies and the names of the procedures however we like. My main idea was to align with you whether the changes in index.js are fine. Maybe @mlessevsg can help with the SQL.

OUTPUT INSERTED.* INTO @result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is @result really needed here? Is it needed because of history triggers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No praticular reason. Just thought that storing the data in a table type would be easier to return all the data as a named resultset at the end.

SELECT ${columns}
FROM @data

SELECT 'data' AS resultSetName
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we discuss if 'data' is good as name? If we call multiple procedures as part of a bigger one, it is better each to return different name for the resultset.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Instead of 'data' it can be the table name. e.g. 'customer.customer', 'customer.organization', etc.

SELECT * from @result
END TRY
BEGIN CATCH
IF @@TRANCOUNT != 0
ROLLBACK TRANSACTION
DECLARE
@errmsg NVARCHAR(2048),
@severity TINYINT,
@state TINYINT,
@errno INT,
@proc sysname,
@lineno INT
SELECT
@errmsg = error_message(),
@severity = error_severity(),
@state = error_state(),
@errno = error_number(),
@proc = error_procedure(),
@lineno = error_line()
IF @errmsg NOT LIKE '***%'
BEGIN
SELECT @errmsg = '*** ' + COALESCE(QUOTENAME(@proc), '<dynamic SQL>') +
', Line ' + LTRIM(STR(@lineno)) + '. Errno ' +
LTRIM(STR(@errno)) + ': ' + @errmsg
END
RAISERROR('%s', @severity, @state, @errmsg)
RETURN 55555
END CATCH
`;
};
49 changes: 49 additions & 0 deletions crud/delete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module.exports = (binding) => {
const pkField = binding.fields.find(f => !!f.identity);
if (!pkField) {
return '';
}
const pk = `[${pkField.column}]`;
return `
CREATE PROCEDURE ${binding.spName}
@data ${binding.tt} READONLY
AS
SET NOCOUNT ON
DECLARE @result ${binding.tt}
BEGIN TRY
DELETE target
OUTPUT DELETED.* INTO @result
FROM ${binding.name} target
JOIN @data source ON target.${pk} = source.${pk}

SELECT 'data' AS resultSetName
SELECT * from @result
END TRY
BEGIN CATCH
IF @@TRANCOUNT != 0
ROLLBACK TRANSACTION
DECLARE
@errmsg NVARCHAR(2048),
@severity TINYINT,
@state TINYINT,
@errno INT,
@proc sysname,
@lineno INT
SELECT
@errmsg = error_message(),
@severity = error_severity(),
@state = error_state(),
@errno = error_number(),
@proc = error_procedure(),
@lineno = error_line()
IF @errmsg NOT LIKE '***%'
BEGIN
SELECT @errmsg = '*** ' + COALESCE(QUOTENAME(@proc), '<dynamic SQL>') +
', Line ' + LTRIM(STR(@lineno)) + '. Errno ' +
LTRIM(STR(@errno)) + ': ' + @errmsg
END
RAISERROR('%s', @severity, @state, @errmsg)
RETURN 55555
END CATCH
`;
};
25 changes: 25 additions & 0 deletions crud/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const crud = {
create: require('./create'),
read: require('./read'),
update: require('./update'),
delete: require('./delete')
};

module.exports = {
actions: Object.keys(crud),
generate: (binding, action) => {
let name;
let suffix;
if (binding.name.match(/]$/)) {
name = binding.name.slice(0, -1);
suffix = ']';
} else {
name = binding.name;
suffix = '';
}
binding.spName = `${name}.${action}${suffix}`;
binding.tt = `${name}TT${suffix}`;
binding.ttu = `${name}TTU${suffix}`;
return crud[action](binding);
}
};
3 changes: 3 additions & 0 deletions crud/read.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = () => {
return ``;
};
3 changes: 3 additions & 0 deletions crud/update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = () => {
return ``;
};
73 changes: 63 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const fs = require('fs');
const fsplus = require('fs-plus');
const crypto = require('./crypto');
const mssqlQueries = require('./sql');
const crud = require('./crud');
const parserSP = require('./parsers/mssqlSP');
const parserDefault = require('./parsers/mssqlDefault');
const xml2js = require('xml2js');
const uuid = require('uuid');
const path = require('path');
Expand Down Expand Up @@ -78,10 +81,13 @@ module.exports = function({parent}) {
type: 'sql',
cache: false,
createTT: false,
allowQuery: false,
retry: 10000,
tableToType: {},
skipTableType: [],
createCRUD: false,
tableToCRUD: {},
skipCRUD: {},
allowQuery: false,
retry: 10000,
paramsOutName: 'out',
doc: false,
db: {
Expand Down Expand Up @@ -165,6 +171,18 @@ module.exports = function({parent}) {
return obj;
}, {}));
}

if (typeof this.config.createCRUD === 'object') {
// e.g {'namespace.entity': ['create', 'read', 'update', 'delete']}
// or {'namespace.entity': true}
let crudTables = Object.keys(this.config.createCRUD);
if (crudTables.length) {
Object.assign(this.config.tableToCRUD, crudTables.reduce((obj, tableName) => {
obj[tableName.toLowerCase()] = this.config.createCRUD[tableName];
return obj;
}, {}));
}
}
return Promise.resolve()
.then(() => parent && parent.prototype.start.apply(this, Array.prototype.slice.call(arguments)))
.then(this.connect.bind(this))
Expand Down Expand Up @@ -381,13 +399,11 @@ module.exports = function({parent}) {
let busConfig = flatten(this.bus.config);

function replaceAuditLog(statement) {
let parserSP = require('./parsers/mssqlSP');
let binding = parserSP.parse(statement);
return statement.trim().replace(AUDIT_LOG, mssqlQueries.auditLog(binding));
}

function replaceCallParams(statement) {
let parserSP = require('./parsers/mssqlSP');
let binding = parserSP.parse(statement);
return statement.trim().replace(CALL_PARAMS, mssqlQueries.callParams(binding));
}
Expand Down Expand Up @@ -430,7 +446,6 @@ module.exports = function({parent}) {

function tableToType(statement) {
if (statement.match(/^CREATE\s+TABLE/i)) {
let parserSP = require('./parsers/mssqlSP');
let binding = parserSP.parse(statement);
if (binding.type === 'table') {
let name = binding.name.match(/]$/) ? binding.name.slice(0, -1) + 'TT]' : binding.name + 'TT';
Expand All @@ -448,10 +463,19 @@ module.exports = function({parent}) {
return '';
}

function tableToCRUD(statement, action) {
if (statement.match(/^CREATE\s+TABLE/i)) {
let binding = parserSP.parse(statement);
if (binding.type === 'table') {
return crud.generate(binding, action);
}
}
return '';
}

function tableToTTU(statement) {
let result = '';
if (statement.match(/^CREATE\s+TABLE/i)) {
let parserSP = require('./parsers/mssqlSP');
let binding = parserSP.parse(statement);
if (binding.type === 'table') {
let name = binding.name.match(/]$/) ? binding.name.slice(0, -1) + 'TTU]' : binding.name + 'TTU';
Expand All @@ -477,7 +501,6 @@ module.exports = function({parent}) {
function getSource(statement, fileName, objectName) {
statement = preProcess(statement, fileName, objectName);
if (statement.trim().match(/^CREATE\s+TYPE/i)) {
let parserSP = require('./parsers/mssqlSP');
let binding = parserSP.parse(statement);
if (binding && binding.type === 'table type') {
return binding.fields.map(fieldSource).join('\r\n');
Expand Down Expand Up @@ -522,6 +545,20 @@ module.exports = function({parent}) {
return (self.config.createTT === true || self.includesConfig('tableToType', tableName, false)) && !self.includesConfig('skipTableType', tableName, false);
}

function shouldCreateCRUD(tableName, action) {
let includesConfig = (name) => {
let config = self.config[name] && self.config[name][tableName];
if (typeof config === 'boolean') {
return config;
}
if (Array.isArray(config)) {
return config.indexOf(action) !== -1;
}
return false;
};
return (self.config.createCRUD === true || includesConfig('tableToCRUD')) && !includesConfig('skipCRUD');
}

function retrySchemaUpdate(failedQueue) {
let newFailedQueue = [];
let request = self.getRequest();
Expand Down Expand Up @@ -630,6 +667,25 @@ module.exports = function({parent}) {
});
}
}
crud.actions.forEach((action) => {
if (!objectIds[`${objectId}.${action}`] && shouldCreateCRUD(objectId, action)) {
let sp = tableToCRUD(fileContent.trim().replace(/^ALTER /i, 'CREATE '), action);
if (sp) {
let context = {
objectName: `${objectName}.${action}`,
objectId: `${objectId}.${action}`
};
schemaConfig.linkSP && (objectList[context.objectId] = fileName);
addQuery(queries, {
fileName: fileName,
objectName: context.objectName,
objectId: context.objectId,
fileContent: sp,
createStatement: sp
});
}
}
});
});

let request = self.getRequest();
Expand Down Expand Up @@ -1067,7 +1123,6 @@ module.exports = function({parent}) {

SqlPort.prototype.linkSP = function(schema) {
if (schema.parseList.length) {
let parserSP = require('./parsers/mssqlSP');
schema.parseList.forEach(function(procedure) {
let binding = parserSP.parse(procedure.source);
let flatName = binding.name.replace(/[[\]]/g, '');
Expand Down Expand Up @@ -1186,7 +1241,6 @@ module.exports = function({parent}) {
return prev;
}, schema);
result.recordsets[1].reduce(function(prev, cur) { // extract columns of user defined table types
let parserDefault = require('./parsers/mssqlDefault');
changeRowVersionType(cur);
if (!(mssql[cur.type.toUpperCase()] instanceof Function)) {
throw errors.unexpectedColumnType({
Expand Down Expand Up @@ -1267,7 +1321,6 @@ module.exports = function({parent}) {
this.checkConnection();
let self = this;
let schemas = this.getSchema();
let parserSP = require('./parsers/mssqlSP');
return new Promise(function(resolve, reject) {
let docList = [];
let promise = Promise.resolve();
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
"fs-plus": "3.0.2",
"json-stringify-deterministic": "1.0.1",
"lodash.merge": "4.6.0",
"through2": "2.0.3",
"mssql": "4.1.0",
"through2": "2.0.3",
"uuid": "3.2.1",
"xml2js": "0.4.19"
},
"peerDependencies": {
"tedious": "^2.0.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already a peer dependency of mssql?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a dependency of mssql. And I saw it is directly required here so I had to add it to the package.json. If we end up with 2 different versions of tedious that patch won't work.

},
"devDependencies": {
"ut-tools": "^5.32.5",
"pegjs": "0.10.0"
Expand Down
Loading