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
Changes from 4 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
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
`;
};
3 changes: 3 additions & 0 deletions crud/delete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = () => {
return ``;
};
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
@@ -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 through2 = require('through2');
@@ -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: {
@@ -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))
@@ -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));
}
@@ -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';
@@ -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';
@@ -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');
@@ -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();
@@ -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();
@@ -1108,7 +1164,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, '');
@@ -1227,7 +1282,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({
@@ -1308,7 +1362,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();
803 changes: 0 additions & 803 deletions sql.js

This file was deleted.

16 changes: 16 additions & 0 deletions sql/auditLog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = function(statement) {
if (!statement.params) {
return;
}
var sql = ' DECLARE @callParams XML = ( SELECT ';
statement.params.map(function(param) {
if (param.def.type === 'table') {
sql += `(SELECT * from @${param.name} rows FOR XML AUTO, TYPE) [${param.name}], `;
} else {
sql += `@${param.name} [${param.name}], `;
}
});
sql = sql.replace(/,\s$/, ' ');
sql += 'FOR XML RAW(\'params\'),TYPE) EXEC core.auditCall @procid = @@PROCID, @params=@callParams';
return sql;
};
16 changes: 16 additions & 0 deletions sql/callParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = function(statement) {
if (!statement.params) {
return;
}
var sql = 'DECLARE @callParams XML = ( SELECT ';
statement.params.map(function(param) {
if (param.def.type === 'table') {
sql += `(SELECT * from @${param.name} rows FOR XML AUTO, TYPE) [${param.name}], `;
} else {
sql += `@${param.name} [${param.name}], `;
}
});
sql = sql.replace(/,\s$/, ' ');
sql += 'FOR XML RAW(\'params\'),TYPE)';
return sql;
};
9 changes: 9 additions & 0 deletions sql/createDatabase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = function(name, user) {
return `
IF NOT EXISTS (SELECT name FROM master.dbo.sysdatabases WHERE name = '${name}')
BEGIN
CREATE DATABASE [${name}]
ALTER DATABASE [${name}] SET READ_COMMITTED_SNAPSHOT ON
ALTER DATABASE [${name}] SET AUTO_SHRINK OFF
END`;
};
24 changes: 24 additions & 0 deletions sql/createUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = function(name, user, password) {
return `
IF NOT EXISTS (SELECT name FROM master.sys.server_principals WHERE name = '${user}')
BEGIN
CREATE LOGIN [${user}] WITH PASSWORD = N'${password}', CHECK_POLICY = OFF
END
USE [${name}]
IF NOT EXISTS (SELECT name FROM sys.database_principals WHERE name = '${user}')
BEGIN
CREATE USER [${user}] FOR LOGIN [${user}]
END
IF (is_rolemember('db_owner', '${user}') IS NULL OR is_rolemember('db_owner', '${user}') = 0)
BEGIN
EXEC sp_addrolemember 'db_owner', '${user}'
END
IF NOT EXISTS (SELECT 1 FROM sys.server_principals AS pr
INNER JOIN sys.server_permissions AS pe ON pe.grantee_principal_id = pr.principal_id
WHERE permission_name = N'VIEW SERVER STATE' AND state = N'G' AND pr.name = N'${user}')
BEGIN
USE [master]
GRANT VIEW SERVER STATE to [${user}]
END
`;
};
31 changes: 31 additions & 0 deletions sql/disableDatabaseDiagrams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = function(name) {
return `
USE [${name}]
IF OBJECT_ID(N'dbo.fn_diagramobjects') IS NULL and IS_MEMBER('db_owner') = 1
DROP FUNCTION dbo.fn_diagramobjects
IF OBJECT_ID(N'dbo.sp_dropdiagram') IS NULL and IS_MEMBER('db_owner') = 1
DROP PROCEDURE dbo.sp_dropdiagram
IF OBJECT_ID(N'dbo.sp_alterdiagram') IS NULL and IS_MEMBER('db_owner') = 1
DROP PROCEDURE dbo.sp_alterdiagram
IF OBJECT_ID(N'dbo.sp_renamediagram') IS NULL and IS_MEMBER('db_owner') = 1
DROP PROCEDURE dbo.sp_renamediagram
IF OBJECT_ID(N'dbo.sp_creatediagram') IS NULL and IS_MEMBER('db_owner') = 1
DROP PROCEDURE dbo.sp_creatediagram
IF OBJECT_ID(N'dbo.sp_helpdiagramdefinition') IS NULL and IS_MEMBER('db_owner') = 1
DROP PROCEDURE dbo.sp_helpdiagramdefinition
IF OBJECT_ID(N'dbo.sp_helpdiagrams') IS NULL and IS_MEMBER('db_owner') = 1
DROP PROCEDURE dbo.sp_helpdiagrams
IF OBJECT_ID(N'dbo.sysdiagrams') IS NOT NULL and IS_MEMBER('db_owner') = 1
DROP TABLE dbo.sysdiagrams
IF OBJECT_ID(N'dbo.sp_upgraddiagrams') IS NULL and IS_MEMBER('db_owner') = 1
DROP PROCEDURE dbo.sp_upgraddiagrams`;
};
3 changes: 3 additions & 0 deletions sql/dropHash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function() {
return `IF OBJECT_ID('dbo.utSchemaHash') IS NOT NULL DROP FUNCTION dbo.utSchemaHash`;
};
526 changes: 526 additions & 0 deletions sql/enableDatabaseDiagrams.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions sql/getHash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function() {
return `IF OBJECT_ID('dbo.utSchemaHash') IS NOT NULL SELECT dbo.utSchemaHash() hash`;
};
13 changes: 13 additions & 0 deletions sql/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
auditLog: require('./auditLog'),
callParams: require('./callParams'),
createDatabase: require('./createDatabase'),
createUser: require('./createUser'),
disableDatabaseDiagrams: require('./disableDatabaseDiagrams'),
dropHash: require('./dropHash'),
enableDatabaseDiagrams: require('./enableDatabaseDiagrams'),
getHash: require('./getHash'),
loadSchema: require('./loadSchema'),
refreshView: require('./refreshView'),
setHash: require('./setHash')
};
99 changes: 99 additions & 0 deletions sql/loadSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
module.exports = function(partial) {
return `
SELECT
o.create_date,
c.id,
c.colid,
RTRIM(o.[type]) [type],
SCHEMA_NAME(o.schema_id) [namespace],
o.Name AS [name],
SCHEMA_NAME(o.schema_id) + '.' + o.Name AS [full],
CASE o.[type]
WHEN 'SN' THEN 'DROP SYNONYM [' + SCHEMA_NAME(o.schema_id) + '].[' + o.Name +
'] CREATE SYNONYM [' + SCHEMA_NAME(o.schema_id) + '].[' + o.Name + '] FOR ' + s.base_object_name
ELSE ${partial ? `LEFT(c.text, CASE CHARINDEX(CHAR(10)+'AS'+CHAR(13), c.text) WHEN 0 THEN 2500 ELSE CHARINDEX(CHAR(10)+'AS'+CHAR(13), c.text) + 10 END)` : 'c.text'}
END AS [source]
FROM
sys.objects o
LEFT JOIN
dbo.syscomments c on o.object_id = c.id
LEFT JOIN
sys.synonyms s on s.object_id = o.object_id
WHERE
o.type IN (${partial ? `'P'` : `'V', 'P', 'FN','F','IF','SN','TF','TR','U'`}) AND
user_name(objectproperty(o.object_id, 'OwnerId')) in (USER_NAME(),'dbo') AND
objectproperty(o.object_id, 'IsMSShipped') = 0 AND
SCHEMA_NAME(o.schema_id) != 'dbo'
${partial ? 'AND ISNULL(c.colid, 1)=1' : ''}
UNION ALL
SELECT 0,0,0,'S',name,NULL,NULL,NULL FROM sys.schemas WHERE principal_id = USER_ID()
UNION ALL
SELECT
0,0,0,'T',SCHEMA_NAME(t.schema_id)+'.'+t.name,NULL,NULL,NULL
FROM
sys.types t
JOIN
sys.schemas s ON s.principal_id = USER_ID() AND s.schema_id=t.schema_id
WHERE
t.is_user_defined=1
ORDER BY
1, 2, 3
SELECT
SCHEMA_NAME(types.schema_id) + '.' + types.name name,
c.name [column],
st.name type,
CASE
WHEN st.name in ('decimal','numeric') then CAST(c.[precision] AS VARCHAR)
WHEN st.name in ('datetime2','time','datetimeoffset') then CAST(c.[scale] AS VARCHAR)
WHEN st.name in ('varchar','varbinary','char','binary') AND c.max_length>=0 THEN CAST(c.max_length as VARCHAR)
WHEN st.name in ('nvarchar','nchar') AND c.max_length>=0 THEN CAST(c.max_length/2 as VARCHAR)
WHEN st.name in ('varchar','varbinary','char','binary','nvarchar','nchar') AND c.max_length<0 THEN 'max'
END [length],
CASE
WHEN st.name in ('decimal','numeric') then c.scale
END scale,
object_definition(c.default_object_id) [default]
FROM
sys.table_types types
JOIN
sys.columns c ON types.type_table_object_id = c.object_id
JOIN
sys.systypes AS st ON st.xtype = c.system_type_id
WHERE
types.is_user_defined = 1 AND st.name <> 'sysname'
ORDER BY
1,c.column_id
SELECT
1 sort,
s.name + '.' + o.name [name],
'IF (OBJECT_ID(''[' + s.name + '].[' + o.name + ']'') IS NOT NULL) DROP ' + CASE o.type WHEN 'FN' THEN 'FUNCTION' ELSE 'PROCEDURE' END + ' [' + s.name + '].[' + o.name + ']' [drop],
p.name [param],
SCHEMA_NAME(t.schema_id) + '.' + t.name [type]
FROM
sys.schemas s
JOIN
sys.objects o ON o.schema_id = s.schema_id
JOIN
sys.parameters p ON p.object_id = o.object_id
JOIN
sys.types t ON p.user_type_id = t.user_type_id AND t.is_user_defined=1
WHERE
user_name(objectproperty(o.object_id, 'OwnerId')) in (USER_NAME(),'dbo')
UNION
SELECT
2,
s.name + '.' + t.name [name],
'DROP TYPE [' + s.name + '].[' + t.name + ']' [drop],
NULL [param],
SCHEMA_NAME(t.schema_id) + '.' + t.name [type]
FROM
sys.schemas s
JOIN
sys.types t ON t.schema_id=s.schema_id and t.is_user_defined=1
WHERE
user_name(s.principal_id) in (USER_NAME(),'dbo')
ORDER BY 1`;
};
70 changes: 70 additions & 0 deletions sql/refreshView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module.exports = function(drop) {
return `
SET NOCOUNT ON;
DECLARE @ViewName VARCHAR(255);
DECLARE @error_table TABLE
(
view_name VARCHAR(255) ,
error_msg VARCHAR(MAX)
);
DECLARE view_cursor CURSOR FAST_FORWARD
FOR
--- Get all the user defined views with no schema binding on them
SELECT DISTINCT
'[' + ss.name + '].[' + av.name +']' AS ViewName
FROM sys.all_views av
JOIN sys.schemas ss ON av.schema_id = ss.schema_id
WHERE OBJECTPROPERTY(av.[object_id], 'IsSchemaBound') <> 1
AND av.Is_Ms_Shipped = 0
OPEN view_cursor
FETCH NEXT FROM view_cursor
INTO @ViewName
WHILE @@FETCH_STATUS = 0
BEGIN
BEGIN TRY
-- Refresh the view
EXEC sp_refreshview @ViewName;
-- RAISERROR('%s', 10, 1, @ViewName) WITH NOWAIT;
END TRY
BEGIN CATCH
IF @@trancount > 0 ROLLBACK TRANSACTION
--- Insert all the errors
IF (1=${drop ? 1 : 0})
BEGIN
EXEC ('DROP VIEW ' + @ViewName)
END ELSE
BEGIN
INSERT INTO
@error_table(view_name, error_msg)
SELECT @ViewName, ERROR_MESSAGE();
END
END CATCH
FETCH NEXT FROM view_cursor INTO @ViewName;
END
--- Check if there was an error
IF EXISTS (SELECT TOP 1 1 FROM @error_table)
BEGIN
SELECT view_name ,
error_msg
FROM @error_table;
END
CLOSE view_cursor
DEALLOCATE view_cursor
IF OBJECT_ID('dbo.utSchemaHash') IS NOT NULL SELECT dbo.utSchemaHash() hash
SET NOCOUNT OFF;`;
};
3 changes: 3 additions & 0 deletions sql/setHash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function(hash) {
return `CREATE FUNCTION dbo.utSchemaHash() RETURNS VARCHAR(64) AS BEGIN RETURN '${hash}' END`;
};