diff --git a/api/compose/spec.json b/api/compose/spec.json index 0647e7564..10e3e8079 100644 --- a/api/compose/spec.json +++ b/api/compose/spec.json @@ -763,6 +763,28 @@ ] } }, + { + "name": "runScript", + "method": "POST", + "title": "Trigger a specific script on record", + "path": "/run-script", + "parameters": { + "post": [ + { + "type": "uint64", + "name": "recordID", + "required": false, + "title": "Record ID" + }, + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "Script ID" + } + ] + } + }, { "name": "upload", "path": "/attachment", @@ -937,192 +959,6 @@ } ] }, - { - "title": "Triggers", - "description": "Compose Triggers", - "entrypoint": "trigger", - "path": "/namespace/{namespaceID}/trigger", - "authentication": [], - "parameters": { - "path": [ - { - "type": "uint64", - "name": "namespaceID", - "required": true, - "title": "Namespace ID" - } - ] - }, - "struct": [ - { - "imports": [ - "time" - ] - } - ], - "apis": [ - { - "name": "list", - "method": "GET", - "path": "/", - "title": "List available triggers", - "parameters": { - "get": [ - { - "type": "uint64", - "name": "moduleID", - "required": false, - "title": "Filter triggers by module" - }, - { - "type": "string", - "name": "query", - "required": false, - "title": "Search query" - }, - { - "name": "page", - "type": "uint", - "required": false, - "title": "Page number (0 based)" - }, - { - "name": "perPage", - "type": "uint", - "required": false, - "title": "Returned items per page (default 50)" - } - ] - } - }, - { - "name": "create", - "method": "POST", - "title": "Create trigger", - "path": "/", - "parameters": { - "post": [ - { - "type": "uint64", - "name": "moduleID", - "required": false, - "title": "Module ID" - }, - { - "type": "string", - "name": "name", - "required": true, - "title": "Name" - }, - { - "type": "[]string", - "name": "actions", - "required": false, - "title": "Actions that trigger this trigger" - }, - { - "type": "bool", - "name": "enabled", - "required": false, - "title": "Enabled" - }, - { - "type": "string", - "name": "source", - "required": false, - "title": "Trigger source code" - }, - { - "type": "*time.Time", - "name": "updatedAt", - "required": false, - "title": "Last update (or creation) date" - } - ] - } - }, - { - "name": "read", - "path": "/{triggerID}", - "method": "GET", - "title": "Get trigger details", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "triggerID", - "required": true, - "title": "Trigger ID" - } - ] - } - }, - { - "name": "update", - "method": "POST", - "title": "Update trigger", - "path": "/{triggerID}", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "triggerID", - "required": true, - "title": "Trigger ID" - } - ], - "post": [ - { - "type": "uint64", - "name": "moduleID", - "required": false, - "title": "Module ID" - }, - { - "type": "string", - "name": "name", - "required": true, - "title": "Name" - }, - { - "type": "[]string", - "name": "actions", - "required": false, - "title": "Actions that trigger this trigger" - }, - { - "type": "bool", - "name": "enabled", - "required": false, - "title": "Enabled" - }, - { - "type": "string", - "name": "source", - "required": false, - "title": "Trigger source code" - } - ] - } - }, - { - "name": "delete", - "path": "/{triggerID}", - "method": "Delete", - "title": "Delete trigger", - "parameters": { - "path": [ - { - "type": "uint64", - "name": "triggerID", - "required": true, - "title": "Trigger ID" - } - ] - } - } - ] - }, { "title": "Notifications", "description": "Compose Notifications", @@ -1447,5 +1283,385 @@ } } ] + }, + { + "title": "Automation scripts", + "entrypoint": "automation_script", + "path": "/namespace/{namespaceID}/automation/script", + "authentication": [ + "Client ID", + "Session ID" + ], + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "Namespace ID" + } + ] + }, + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List/read automation script", + "path": "/", + "parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query to match against automation script", + "type": "string" + }, + { + "name": "resource", + "required": false, + "title": "Limit by resource (via trigger)", + "type": "string" + }, + { + "name": "incDeleted", + "required": false, + "title": "Include deleted scripts", + "type": "bool" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" + } + ] + } + }, + { + "name": "create", + "method": "POST", + "title": "Add new automation script ", + "path": "/", + "parameters": { + "post": [ + { + "name": "name", + "title": "automation name", + "type": "string", + "required": true + }, + { + "name": "sourceRef", + "title": "Source URL", + "type": "string" + }, + { + "name": "source", + "title": "Source code", + "type": "string" + }, + { + "name": "runAs", + "title": "Run as specific user", + "type": "uint64" + }, + { + "name": "runInUA", + "title": "Run script in user-agent (browser)", + "type": "bool" + }, + { + "name": "timeout", + "title": "Script timeout (in milliseconds)", + "type": "uint" + }, + { + "name": "critical", + "title": "Is it critical to run this script successfully", + "type": "bool" + }, + { + "name": "async", + "title": "Will this script be ran asynchronously", + "type": "bool" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read automation script by ID", + "path": "/{scriptID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "automation script ID" + } + ] + } + }, + { + "name": "update", + "method": "POST", + "title": "Update automation script", + "path": "/{scriptID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "Automation script ID" + } + ], + "post": [ + { + "name": "name", + "title": "Script name", + "type": "string", + "required": true + }, + { + "name": "sourceRef", + "title": "Source URL", + "type": "string" + }, + { + "name": "source", + "title": "Source code", + "type": "string" + }, + { + "name": "runAs", + "title": "Run script as specific user", + "type": "uint64" + }, + { + "name": "runInUA", + "title": "Run script in user-agent (browser)", + "type": "bool" + }, + { + "name": "timeout", + "title": "Run script in user-agent (browser)", + "type": "uint" + }, + { + "name": "critical", + "title": "Is it critical to run this script successfully", + "type": "bool" + }, + { + "name": "async", + "title": "Will this script be ran asynchronously", + "type": "bool" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "name": "delete", + "method": "DELETE", + "title": "Delete script", + "path": "/{scriptID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "automation ID" + } + ] + } + } + ] + }, + { + "title": "Automation script triggers", + "entrypoint": "automation_trigger", + "path": "/namespace/{namespaceID}/automation/script/{scriptID}/trigger", + "authentication": [ + "Client ID", + "Session ID" + ], + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "Namespace ID" + }, + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "Script ID" + } + ] + }, + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List/read automation script triggers", + "path": "/", + "parameters": { + "get": [ + { + "name": "resource", + "required": false, + "title": "Only triggers of a specific resource", + "type": "string" + }, + { + "name": "event", + "required": false, + "title": "Only triggers of a specific event", + "type": "string" + }, + { + "name": "incDeleted", + "required": false, + "title": "Include deleted scripts", + "type": "bool" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" + } + ] + } + }, + { + "name": "create", + "method": "POST", + "title": "Add new automation script trigger", + "path": "/", + "parameters": { + "post": [ + { + "name": "resource", + "title": "Resource", + "type": "string", + "required": true + }, + { + "name": "event", + "title": "Event", + "type": "string", + "required": true + }, + { + "name": "condition", + "title": "Event", + "type": "string" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read automation script trigger by ID", + "path": "/{triggerID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "Automation script trigger ID" + } + ] + } + }, + { + "name": "update", + "method": "POST", + "title": "Update automation script trigger", + "path": "/{triggerID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "Automation script trigger ID" + } + ], + "post": [ + { + "name": "resource", + "title": "Resource", + "type": "string", + "required": true + }, + { + "name": "event", + "title": "Event", + "type": "string", + "required": true + }, + { + "name": "condition", + "title": "Event", + "type": "string" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "name": "delete", + "method": "DELETE", + "title": "Delete script", + "path": "/{triggerID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "automation script trigger ID" + } + ] + } + } + ] } ] diff --git a/api/compose/spec/automation_script.json b/api/compose/spec/automation_script.json new file mode 100644 index 000000000..0a4f7f21e --- /dev/null +++ b/api/compose/spec/automation_script.json @@ -0,0 +1,213 @@ +{ + "Title": "Automation scripts", + "Interface": "Automation_script", + "Struct": null, + "Parameters": { + "path": [ + { + "name": "namespaceID", + "required": true, + "title": "Namespace ID", + "type": "uint64" + } + ] + }, + "Protocol": "", + "Authentication": [ + "Client ID", + "Session ID" + ], + "Path": "/namespace/{namespaceID}/automation/script", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "List/read automation script", + "Path": "/", + "Parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query to match against automation script", + "type": "string" + }, + { + "name": "resource", + "required": false, + "title": "Limit by resource (via trigger)", + "type": "string" + }, + { + "name": "incDeleted", + "required": false, + "title": "Include deleted scripts", + "type": "bool" + }, + { + "name": "page", + "required": false, + "title": "Page number (0 based)", + "type": "uint" + }, + { + "name": "perPage", + "required": false, + "title": "Returned items per page (default 50)", + "type": "uint" + } + ] + } + }, + { + "Name": "create", + "Method": "POST", + "Title": "Add new automation script ", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "name", + "required": true, + "title": "automation name", + "type": "string" + }, + { + "name": "sourceRef", + "title": "Source URL", + "type": "string" + }, + { + "name": "source", + "title": "Source code", + "type": "string" + }, + { + "name": "runAs", + "title": "Run as specific user", + "type": "uint64" + }, + { + "name": "runInUA", + "title": "Run script in user-agent (browser)", + "type": "bool" + }, + { + "name": "timeout", + "title": "Script timeout (in milliseconds)", + "type": "uint" + }, + { + "name": "critical", + "title": "Is it critical to run this script successfully", + "type": "bool" + }, + { + "name": "async", + "title": "Will this script be ran asynchronously", + "type": "bool" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "Name": "read", + "Method": "GET", + "Title": "Read automation script by ID", + "Path": "/{scriptID}", + "Parameters": { + "path": [ + { + "name": "scriptID", + "required": true, + "title": "automation script ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "update", + "Method": "POST", + "Title": "Update automation script", + "Path": "/{scriptID}", + "Parameters": { + "path": [ + { + "name": "scriptID", + "required": true, + "title": "Automation script ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "name", + "required": true, + "title": "Script name", + "type": "string" + }, + { + "name": "sourceRef", + "title": "Source URL", + "type": "string" + }, + { + "name": "source", + "title": "Source code", + "type": "string" + }, + { + "name": "runAs", + "title": "Run script as specific user", + "type": "uint64" + }, + { + "name": "runInUA", + "title": "Run script in user-agent (browser)", + "type": "bool" + }, + { + "name": "timeout", + "title": "Run script in user-agent (browser)", + "type": "uint" + }, + { + "name": "critical", + "title": "Is it critical to run this script successfully", + "type": "bool" + }, + { + "name": "async", + "title": "Will this script be ran asynchronously", + "type": "bool" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "Name": "delete", + "Method": "DELETE", + "Title": "Delete script", + "Path": "/{scriptID}", + "Parameters": { + "path": [ + { + "name": "scriptID", + "required": true, + "title": "automation ID", + "type": "uint64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/api/compose/spec/trigger.json b/api/compose/spec/automation_trigger.json similarity index 57% rename from api/compose/spec/trigger.json rename to api/compose/spec/automation_trigger.json index dad412d7a..b81791c1c 100644 --- a/api/compose/spec/trigger.json +++ b/api/compose/spec/automation_trigger.json @@ -1,14 +1,7 @@ { - "Title": "Triggers", - "Description": "Compose Triggers", - "Interface": "Trigger", - "Struct": [ - { - "imports": [ - "time" - ] - } - ], + "Title": "Automation script triggers", + "Interface": "Automation_trigger", + "Struct": null, "Parameters": { "path": [ { @@ -16,32 +9,47 @@ "required": true, "title": "Namespace ID", "type": "uint64" + }, + { + "name": "scriptID", + "required": true, + "title": "Script ID", + "type": "uint64" } ] }, "Protocol": "", - "Authentication": [], - "Path": "/namespace/{namespaceID}/trigger", + "Authentication": [ + "Client ID", + "Session ID" + ], + "Path": "/namespace/{namespaceID}/automation/script/{scriptID}/trigger", "APIs": [ { "Name": "list", "Method": "GET", - "Title": "List available triggers", + "Title": "List/read automation script triggers", "Path": "/", "Parameters": { "get": [ { - "name": "moduleID", + "name": "resource", "required": false, - "title": "Filter triggers by module", - "type": "uint64" + "title": "Only triggers of a specific resource", + "type": "string" }, { - "name": "query", + "name": "event", "required": false, - "title": "Search query", + "title": "Only triggers of a specific event", "type": "string" }, + { + "name": "incDeleted", + "required": false, + "title": "Include deleted scripts", + "type": "bool" + }, { "name": "page", "required": false, @@ -60,45 +68,34 @@ { "Name": "create", "Method": "POST", - "Title": "Create trigger", + "Title": "Add new automation script trigger", "Path": "/", "Parameters": { "post": [ - { - "name": "moduleID", - "required": false, - "title": "Module ID", - "type": "uint64" - }, { "name": "name", "required": true, - "title": "Name", + "title": "automation name", "type": "string" }, { - "name": "actions", - "required": false, - "title": "Actions that trigger this trigger", - "type": "[]string" + "name": "resource", + "title": "Resource", + "type": "string" + }, + { + "name": "event", + "title": "Event", + "type": "string" + }, + { + "name": "condition", + "title": "Event", + "type": "string" }, { "name": "enabled", - "required": false, - "title": "Enabled", "type": "bool" - }, - { - "name": "source", - "required": false, - "title": "Trigger source code", - "type": "string" - }, - { - "name": "updatedAt", - "required": false, - "title": "Last update (or creation) date", - "type": "*time.Time" } ] } @@ -106,14 +103,14 @@ { "Name": "read", "Method": "GET", - "Title": "Get trigger details", + "Title": "Read automation script trigger by ID", "Path": "/{triggerID}", "Parameters": { "path": [ { "name": "triggerID", "required": true, - "title": "Trigger ID", + "title": "Automation script trigger ID", "type": "uint64" } ] @@ -122,62 +119,57 @@ { "Name": "update", "Method": "POST", - "Title": "Update trigger", + "Title": "Update automation script trigger", "Path": "/{triggerID}", "Parameters": { "path": [ { "name": "triggerID", "required": true, - "title": "Trigger ID", + "title": "Automation script trigger ID", "type": "uint64" } ], "post": [ - { - "name": "moduleID", - "required": false, - "title": "Module ID", - "type": "uint64" - }, { "name": "name", "required": true, - "title": "Name", + "title": "automation name", "type": "string" }, { - "name": "actions", - "required": false, - "title": "Actions that trigger this trigger", - "type": "[]string" + "name": "resource", + "title": "Resource", + "type": "string" + }, + { + "name": "event", + "title": "Event", + "type": "string" + }, + { + "name": "condition", + "title": "Event", + "type": "string" }, { "name": "enabled", - "required": false, - "title": "Enabled", "type": "bool" - }, - { - "name": "source", - "required": false, - "title": "Trigger source code", - "type": "string" } ] } }, { "Name": "delete", - "Method": "Delete", - "Title": "Delete trigger", + "Method": "DELETE", + "Title": "Delete script", "Path": "/{triggerID}", "Parameters": { "path": [ { "name": "triggerID", "required": true, - "title": "Trigger ID", + "title": "automation script trigger ID", "type": "uint64" } ] diff --git a/api/compose/spec/record.json b/api/compose/spec/record.json index f6aacf563..c588a7758 100644 --- a/api/compose/spec/record.json +++ b/api/compose/spec/record.json @@ -199,6 +199,28 @@ ] } }, + { + "Name": "runScript", + "Method": "POST", + "Title": "Trigger a specific script on record", + "Path": "/run-script", + "Parameters": { + "post": [ + { + "name": "recordID", + "required": false, + "title": "Record ID", + "type": "uint64" + }, + { + "name": "scriptID", + "required": true, + "title": "Script ID", + "type": "uint64" + } + ] + } + }, { "Name": "upload", "Method": "POST", diff --git a/codegen.sh b/codegen.sh index 333d362c5..bdc89efa2 100755 --- a/codegen.sh +++ b/codegen.sh @@ -38,7 +38,6 @@ function types { ./build/gen-type-set --types Module --output compose/types/module.gen.go ./build/gen-type-set --types Page --output compose/types/page.gen.go ./build/gen-type-set --types Chart --output compose/types/chart.gen.go - ./build/gen-type-set --types Trigger --output compose/types/trigger.gen.go ./build/gen-type-set --types Record --output compose/types/record.gen.go ./build/gen-type-set --types ModuleField --output compose/types/module_field.gen.go @@ -47,7 +46,6 @@ function types { ./build/gen-type-set-test --types Module --output compose/types/module.gen_test.go ./build/gen-type-set-test --types Page --output compose/types/page.gen_test.go ./build/gen-type-set-test --types Chart --output compose/types/chart.gen_test.go - ./build/gen-type-set-test --types Trigger --output compose/types/trigger.gen_test.go ./build/gen-type-set-test --types Record --output compose/types/record.gen_test.go ./build/gen-type-set-test --types ModuleField --output compose/types/module_field.gen_test.go @@ -99,6 +97,12 @@ function types { ./build/gen-type-set-test --types Rule --output internal/permissions/rule.gen_test.go --with-primary-key=false --package permissions ./build/gen-type-set-test --types Resource --output internal/permissions/resource.gen_test.go --with-primary-key=false --package permissions + ./build/gen-type-set --types Script --output pkg/automation/script.gen.go --package automation + ./build/gen-type-set-test --types Script --output pkg/automation/script.gen_test.go --package automation + ./build/gen-type-set --types Trigger --output pkg/automation/trigger.gen.go --package automation + ./build/gen-type-set-test --types Trigger --output pkg/automation/trigger.gen_test.go --package automation + + green "OK" } diff --git a/compose/db/mysql/static.go b/compose/db/mysql/static.go index 37454d344..fa99822c8 100644 --- a/compose/db/mysql/static.go +++ b/compose/db/mysql/static.go @@ -3,4 +3,4 @@ // Package contains static assets. package mysql -var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content` (\n `id` bigint(20) unsigned NOT NULL,\n `module_id` bigint(20) unsigned NOT NULL,\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`,`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_content_column` (\n `content_id` bigint(20) NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `column_value` text NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_field` (\n `field_type` varchar(16) NOT NULL COMMENT 'Short field type (string, boolean,...)',\n `field_name` varchar(255) NOT NULL COMMENT 'Description of field contents',\n `field_template` varchar(255) NOT NULL COMMENT 'HTML template file for field',\n PRIMARY KEY (`field_type`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module` (\n `id` bigint(20) unsigned NOT NULL,\n `name` varchar(64) NOT NULL COMMENT 'The name of the module',\n `json` json NOT NULL COMMENT 'List of field definitions for the module',\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module_form` (\n `module_id` bigint(20) unsigned NOT NULL,\n `place` tinyint(3) unsigned NOT NULL,\n `kind` varchar(64) NOT NULL COMMENT 'The type of the form input field',\n `name` varchar(64) NOT NULL COMMENT 'The name of the field in the form',\n `label` varchar(255) NOT NULL COMMENT 'The label of the form input',\n `help_text` text NOT NULL COMMENT 'Help text',\n `default_value` text NOT NULL COMMENT 'Default value',\n `max_length` int(10) unsigned NOT NULL COMMENT 'Maximum input length',\n `is_private` tinyint(1) NOT NULL COMMENT 'Contains personal/sensitive data?',\n PRIMARY KEY (`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_page` (\n `id` bigint(20) unsigned NOT NULL COMMENT 'Page ID',\n `self_id` bigint(20) unsigned NOT NULL COMMENT 'Parent Page ID',\n `module_id` bigint(20) unsigned NOT NULL COMMENT 'Module ID (optional)',\n `title` varchar(255) NOT NULL COMMENT 'Title (required)',\n `description` text NOT NULL COMMENT 'Description',\n `blocks` json NOT NULL COMMENT 'JSON array of blocks for the page',\n `visible` tinyint(4) NOT NULL COMMENT 'Is page visible in navigation?',\n `weight` int(11) NOT NULL COMMENT 'Order for navigation',\n PRIMARY KEY (`id`) USING BTREE,\n KEY `module_id` (`module_id`),\n KEY `self_id` (`self_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` VALUES ('bool','Boolean value (yes / no)','');\nINSERT INTO `crm_field` VALUES ('email','E-mail input','');\nINSERT INTO `crm_field` VALUES ('enum','Single option picker','');\nINSERT INTO `crm_field` VALUES ('hidden','Hidden value','');\nINSERT INTO `crm_field` VALUES ('stamp','Date/time input','');\nINSERT INTO `crm_field` VALUES ('text','Text input','');\nINSERT INTO `crm_field` VALUES ('textarea','Text input (multi-line)','');\nPK\x07\x08f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00 \x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `user_id` BIGINT UNSIGNED NOT NULL AFTER `module_id`, ADD INDEX (`user_id`);\nPK\x07\x08\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` (`field_type`, `field_name`, `field_template`) VALUES ('related', 'Related content', ''), ('related_multi', 'Related content (multiple)', '');PK\x07\x08:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content_links` (\n `content_id` bigint(20) unsigned NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `rel_content_id` bigint(20) unsigned NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`,`rel_content_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;PK\x07\x08\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00D\x00 \x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `is_required` TINYINT(1) NOT NULL AFTER `is_private`, ADD `is_visible` TINYINT(1) NOT NULL AFTER `is_required`;PK\x07\x08\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x005\x00 \x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` DROP PRIMARY KEY, ADD PRIMARY KEY(`module_id`, `place`);\nPK\x07\x08\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `json` json DEFAULT NULL COMMENT 'Content in JSON format.' AFTER `user_id`;\nPK\x07\x08\"\x96\xd6pj\x00\x00\x00j\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\x00 \x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `json` JSON NOT NULL COMMENT 'Options in JSON format.' AFTER `kind`;PK\x07\x08\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00 \x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` RENAME TO `crm_record`;\nALTER TABLE `crm_record` MODIFY COLUMN `json` json DEFAULT NULL COMMENT 'Records in JSON format.';\n\nALTER TABLE `crm_content_column` RENAME TO `crm_record_column`;\nALTER TABLE `crm_record_column` CHANGE COLUMN `content_id` `record_id` bigint(20);\n\nALTER TABLE `crm_content_links` RENAME TO `crm_record_links`;\nALTER TABLE `crm_record_links` CHANGE COLUMN `content_id` `record_id` bigint(20) unsigned;\nALTER TABLE `crm_record_links` CHANGE COLUMN `rel_content_id` `rel_record_id` bigint(20) unsigned;\nPK\x07\x08mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_chart` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the chart',\n `config` JSON NOT NULL COMMENT 'Chart & reporting configuration',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `crm_field`;\nPK\x07\x08\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x00 \x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_trigger` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the trigger',\n `enabled` BOOLEAN NOT NULL COMMENT 'Trigger enabled?',\n `actions` TEXT NOT NULL COMMENT 'All actions that trigger it',\n `source` TEXT NOT NULL COMMENT 'Trigger source',\n `rel_module` BIGINT(20) UNSIGNED NULL COMMENT 'Primary module',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\x00 \x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` DROP COLUMN `json`;\nPK\x07\x08\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008\x00 \x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8-- No more links, we'll handle this through ref field on crm_record_value tbl\nDROP TABLE IF EXISTS `crm_record_links`;\n\n-- Not columns, values\nALTER TABLE `crm_record_column` RENAME TO `crm_record_value`;\n\n-- Simplify names\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_name` `name` VARCHAR(64);\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_value` `value` TEXT;\n\n-- Add reference\nALTER TABLE `crm_record_value` ADD COLUMN `ref` BIGINT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `deleted_at` datetime DEFAULT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `place` INT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` DROP PRIMARY KEY, ADD PRIMARY KEY(`record_id`, `name`, `place`);\nCREATE INDEX crm_record_value_ref ON crm_record_value (ref);\n\n\n-- We want this as a real field\nALTER TABLE `crm_module_form` ADD COLUMN `is_multi` TINYINT(1) NOT NULL;\n\n-- This will be handled through meta(json) fieldd\nALTER TABLE `crm_module_form` DROP COLUMN `help_text`;\nALTER TABLE `crm_module_form` DROP COLUMN `max_length`;\nALTER TABLE `crm_module_form` DROP COLUMN `default_Value`;\nPK\x07\x08\x04]{\x1fo\x04\x00\x00o\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\x00 \x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` CHANGE COLUMN `user_id` `owned_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `deleted_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nUPDATE crm_record SET created_by = owned_by;\nUPDATE crm_record SET updated_by = owned_by WHERE updated_at IS NOT NULL;\nUPDATE crm_record SET deleted_by = owned_by WHERE deleted_at IS NOT NULL;\nPK\x07\x08h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE crm_attachment (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL,\n\n kind VARCHAR(32) NOT NULL,\n\n url VARCHAR(512),\n preview_url VARCHAR(512),\n\n size INT UNSIGNED,\n mimetype VARCHAR(255),\n name TEXT,\n\n meta JSON,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- page attachments will be referenced via page-block meta data\n-- module/record attachment will be referenced via crm_record_value\nPK\x07\x08\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\x00 \x0020190427180922.change-tbl-prefix.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS crm_field;\nDROP TABLE IF EXISTS crm_fields;\nDROP TABLE IF EXISTS crm_content;\nDROP TABLE IF EXISTS crm_content_links;\nDROP TABLE IF EXISTS crm_content_column;\nDROP TABLE IF EXISTS crm_module_content;\n\nALTER TABLE crm_attachment\n RENAME TO compose_attachment;\n\nALTER TABLE crm_chart\n RENAME TO compose_chart;\n\nALTER TABLE crm_module\n RENAME TO compose_module;\n\nALTER TABLE crm_module_form\n RENAME TO compose_module_form;\n\nALTER TABLE crm_page\n RENAME TO compose_page;\n\nALTER TABLE crm_record\n RENAME TO compose_record;\n\nALTER TABLE crm_record_value\n RENAME TO compose_record_value;\n\nALTER TABLE crm_trigger\n RENAME TO compose_trigger;\nPK\x07\x08\xf2\x1a)|\x97\x02\x00\x00\x97\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190427210922.namespace-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `compose_namespace` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'Name',\n `slug` VARCHAR(64) NOT NULL COMMENT 'URL slug',\n `enabled` BOOLEAN NOT NULL COMMENT 'Is namespace enabled?',\n `meta` JSON NOT NULL COMMENT 'Meta data',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08m\xeb\xed~R\x02\x00\x00R\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8INSERT IGNORE INTO compose_namespace (id, name, slug, enabled, meta)\n VALUES (88714882739863655, 'Crust CRM', 'crm', true, '{}');\n\nALTER TABLE `compose_attachment`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_chart`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_module`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_page`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_record`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_trigger`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nUPDATE `compose_attachment` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_chart` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_module` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_page` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_record` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_trigger` SET `rel_namespace` = 88714882739863655;\n\n\nALTER TABLE `compose_attachment`\n ADD CONSTRAINT `compose_attachment_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_chart`\n ADD CONSTRAINT `compose_chart_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_module`\n ADD CONSTRAINT `compose_module_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_page`\n ADD CONSTRAINT `compose_page_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_record`\n ADD CONSTRAINT `compose_record_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_trigger`\n ADD CONSTRAINT `compose_trigger_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\nPK\x07\x08\xfc\xdb\xfd\xdde \x00\x00e \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `compose_page`\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL;\n\nALTER TABLE `compose_page` CHANGE COLUMN `module_id` `rel_module` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nPK\x07\x08\x82\x01Rn1\x01\x00\x001\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190514090000.module_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE compose_module_form\n RENAME TO compose_module_field;\n\n-- Remove orphaned and invalid fields\nDELETE FROM `compose_module_field` WHERE `module_id` NOT IN (SELECT `id` FROM `compose_module`) OR `name` = '';\n\n-- Order and consistency.\nALTER TABLE `compose_module_field`\n ADD COLUMN `id` BIGINT UNSIGNED NOT NULL FIRST,\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL,\n RENAME COLUMN `module_id` TO `rel_module`,\n RENAME COLUMN `json` TO `options`;\n\n-- Generate IDs for the new field, use module, offset by one (just to start with a different ID)\n-- and use place (0 based, +1 for every field, expecting to be unique per module because of the existing pkey)\nUPDATE `compose_module_field` SET id = rel_module + 1 + place;\n\n-- Drop old primary key (module_id, place)\nALTER TABLE `compose_module_field` DROP PRIMARY KEY, ADD PRIMARY KEY(`id`);\n\n-- Foreign key\nALTER TABLE `compose_module_field`\n ADD CONSTRAINT `compose_module`\n FOREIGN KEY (`rel_module`)\n REFERENCES `compose_module` (`id`);\n\n-- And unique indexes for module+place/name combos.\nCREATE UNIQUE INDEX uid_compose_module_field_place ON compose_module_field (`rel_module`, `place`);\nCREATE UNIQUE INDEX uid_compose_module_field_name ON compose_module_field (`rel_module`, `name`);\nPK\x07\x08\xb1(\xbb\xf0\x8d\x05\x00\x00\x8d\x05\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS compose_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\nPK\x07\x08\"\xd8\xe5H\x12\x01\x00\x00\x12\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` text NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x089S\x05%x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sql\nPK\x07\x08\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81c\n\x00\x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x84\x0c\x00\x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Q\x0d\x00\x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\\\x0e\x00\x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00D\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc8\x0f\x00\x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x005\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd4\x10\x00\x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\x96\xd6pj\x00\x00\x00j\x00\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x97\x11\x00\x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x004\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81h\x12\x00\x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x13\x00\x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc1\x15\x00\x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x18\x00\x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00&\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81r\x18\x00\x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00/\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x87\x1b\x00\x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x04]{\x1fo\x04\x00\x00o\x04\x00\x008\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x1a\x1c\x00\x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xf8 \x00\x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81w#\x00\x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\x1a)|\x97\x02\x00\x00\x97\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x81&\x00\x0020190427180922.change-tbl-prefix.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(m\xeb\xed~R\x02\x00\x00R\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81v)\x00\x0020190427210922.namespace-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xfc\xdb\xfd\xdde \x00\x00e \x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\",\x00\x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x82\x01Rn1\x01\x00\x001\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xe25\x00\x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb1(\xbb\xf0\x8d\x05\x00\x00\x8d\x05\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81o7\x00\x0020190514090000.module_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\xd8\xe5H\x12\x01\x00\x00\x12\x01\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81V=\x00\x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc0>\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81}@\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x19\x00\x19\x005 \x00\x00\xe9@\x00\x00\x00\x00" +var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content` (\n `id` bigint(20) unsigned NOT NULL,\n `module_id` bigint(20) unsigned NOT NULL,\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`,`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_content_column` (\n `content_id` bigint(20) NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `column_value` text NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_field` (\n `field_type` varchar(16) NOT NULL COMMENT 'Short field type (string, boolean,...)',\n `field_name` varchar(255) NOT NULL COMMENT 'Description of field contents',\n `field_template` varchar(255) NOT NULL COMMENT 'HTML template file for field',\n PRIMARY KEY (`field_type`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module` (\n `id` bigint(20) unsigned NOT NULL,\n `name` varchar(64) NOT NULL COMMENT 'The name of the module',\n `json` json NOT NULL COMMENT 'List of field definitions for the module',\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module_form` (\n `module_id` bigint(20) unsigned NOT NULL,\n `place` tinyint(3) unsigned NOT NULL,\n `kind` varchar(64) NOT NULL COMMENT 'The type of the form input field',\n `name` varchar(64) NOT NULL COMMENT 'The name of the field in the form',\n `label` varchar(255) NOT NULL COMMENT 'The label of the form input',\n `help_text` text NOT NULL COMMENT 'Help text',\n `default_value` text NOT NULL COMMENT 'Default value',\n `max_length` int(10) unsigned NOT NULL COMMENT 'Maximum input length',\n `is_private` tinyint(1) NOT NULL COMMENT 'Contains personal/sensitive data?',\n PRIMARY KEY (`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_page` (\n `id` bigint(20) unsigned NOT NULL COMMENT 'Page ID',\n `self_id` bigint(20) unsigned NOT NULL COMMENT 'Parent Page ID',\n `module_id` bigint(20) unsigned NOT NULL COMMENT 'Module ID (optional)',\n `title` varchar(255) NOT NULL COMMENT 'Title (required)',\n `description` text NOT NULL COMMENT 'Description',\n `blocks` json NOT NULL COMMENT 'JSON array of blocks for the page',\n `visible` tinyint(4) NOT NULL COMMENT 'Is page visible in navigation?',\n `weight` int(11) NOT NULL COMMENT 'Order for navigation',\n PRIMARY KEY (`id`) USING BTREE,\n KEY `module_id` (`module_id`),\n KEY `self_id` (`self_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` VALUES ('bool','Boolean value (yes / no)','');\nINSERT INTO `crm_field` VALUES ('email','E-mail input','');\nINSERT INTO `crm_field` VALUES ('enum','Single option picker','');\nINSERT INTO `crm_field` VALUES ('hidden','Hidden value','');\nINSERT INTO `crm_field` VALUES ('stamp','Date/time input','');\nINSERT INTO `crm_field` VALUES ('text','Text input','');\nINSERT INTO `crm_field` VALUES ('textarea','Text input (multi-line)','');\nPK\x07\x08f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00 \x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `user_id` BIGINT UNSIGNED NOT NULL AFTER `module_id`, ADD INDEX (`user_id`);\nPK\x07\x08\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` (`field_type`, `field_name`, `field_template`) VALUES ('related', 'Related content', ''), ('related_multi', 'Related content (multiple)', '');PK\x07\x08:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content_links` (\n `content_id` bigint(20) unsigned NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `rel_content_id` bigint(20) unsigned NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`,`rel_content_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;PK\x07\x08\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00D\x00 \x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `is_required` TINYINT(1) NOT NULL AFTER `is_private`, ADD `is_visible` TINYINT(1) NOT NULL AFTER `is_required`;PK\x07\x08\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x005\x00 \x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` DROP PRIMARY KEY, ADD PRIMARY KEY(`module_id`, `place`);\nPK\x07\x08\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `json` json DEFAULT NULL COMMENT 'Content in JSON format.' AFTER `user_id`;\nPK\x07\x08\"\x96\xd6pj\x00\x00\x00j\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\x00 \x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `json` JSON NOT NULL COMMENT 'Options in JSON format.' AFTER `kind`;PK\x07\x08\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00 \x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` RENAME TO `crm_record`;\nALTER TABLE `crm_record` MODIFY COLUMN `json` json DEFAULT NULL COMMENT 'Records in JSON format.';\n\nALTER TABLE `crm_content_column` RENAME TO `crm_record_column`;\nALTER TABLE `crm_record_column` CHANGE COLUMN `content_id` `record_id` bigint(20);\n\nALTER TABLE `crm_content_links` RENAME TO `crm_record_links`;\nALTER TABLE `crm_record_links` CHANGE COLUMN `content_id` `record_id` bigint(20) unsigned;\nALTER TABLE `crm_record_links` CHANGE COLUMN `rel_content_id` `rel_record_id` bigint(20) unsigned;\nPK\x07\x08mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_chart` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the chart',\n `config` JSON NOT NULL COMMENT 'Chart & reporting configuration',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `crm_field`;\nPK\x07\x08\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x00 \x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_trigger` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the trigger',\n `enabled` BOOLEAN NOT NULL COMMENT 'Trigger enabled?',\n `actions` TEXT NOT NULL COMMENT 'All actions that trigger it',\n `source` TEXT NOT NULL COMMENT 'Trigger source',\n `rel_module` BIGINT(20) UNSIGNED NULL COMMENT 'Primary module',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\x00 \x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` DROP COLUMN `json`;\nPK\x07\x08\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008\x00 \x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8-- No more links, we'll handle this through ref field on crm_record_value tbl\nDROP TABLE IF EXISTS `crm_record_links`;\n\n-- Not columns, values\nALTER TABLE `crm_record_column` RENAME TO `crm_record_value`;\n\n-- Simplify names\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_name` `name` VARCHAR(64);\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_value` `value` TEXT;\n\n-- Add reference\nALTER TABLE `crm_record_value` ADD COLUMN `ref` BIGINT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `deleted_at` datetime DEFAULT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `place` INT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` DROP PRIMARY KEY, ADD PRIMARY KEY(`record_id`, `name`, `place`);\nCREATE INDEX crm_record_value_ref ON crm_record_value (ref);\n\n\n-- We want this as a real field\nALTER TABLE `crm_module_form` ADD COLUMN `is_multi` TINYINT(1) NOT NULL;\n\n-- This will be handled through meta(json) fieldd\nALTER TABLE `crm_module_form` DROP COLUMN `help_text`;\nALTER TABLE `crm_module_form` DROP COLUMN `max_length`;\nALTER TABLE `crm_module_form` DROP COLUMN `default_Value`;\nPK\x07\x08\x04]{\x1fo\x04\x00\x00o\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\x00 \x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` CHANGE COLUMN `user_id` `owned_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `deleted_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nUPDATE crm_record SET created_by = owned_by;\nUPDATE crm_record SET updated_by = owned_by WHERE updated_at IS NOT NULL;\nUPDATE crm_record SET deleted_by = owned_by WHERE deleted_at IS NOT NULL;\nPK\x07\x08h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE crm_attachment (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL,\n\n kind VARCHAR(32) NOT NULL,\n\n url VARCHAR(512),\n preview_url VARCHAR(512),\n\n size INT UNSIGNED,\n mimetype VARCHAR(255),\n name TEXT,\n\n meta JSON,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- page attachments will be referenced via page-block meta data\n-- module/record attachment will be referenced via crm_record_value\nPK\x07\x08\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\x00 \x0020190427180922.change-tbl-prefix.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS crm_field;\nDROP TABLE IF EXISTS crm_fields;\nDROP TABLE IF EXISTS crm_content;\nDROP TABLE IF EXISTS crm_content_links;\nDROP TABLE IF EXISTS crm_content_column;\nDROP TABLE IF EXISTS crm_module_content;\n\nALTER TABLE crm_attachment\n RENAME TO compose_attachment;\n\nALTER TABLE crm_chart\n RENAME TO compose_chart;\n\nALTER TABLE crm_module\n RENAME TO compose_module;\n\nALTER TABLE crm_module_form\n RENAME TO compose_module_form;\n\nALTER TABLE crm_page\n RENAME TO compose_page;\n\nALTER TABLE crm_record\n RENAME TO compose_record;\n\nALTER TABLE crm_record_value\n RENAME TO compose_record_value;\n\nALTER TABLE crm_trigger\n RENAME TO compose_trigger;\nPK\x07\x08\xf2\x1a)|\x97\x02\x00\x00\x97\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190427210922.namespace-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `compose_namespace` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'Name',\n `slug` VARCHAR(64) NOT NULL COMMENT 'URL slug',\n `enabled` BOOLEAN NOT NULL COMMENT 'Is namespace enabled?',\n `meta` JSON NOT NULL COMMENT 'Meta data',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08m\xeb\xed~R\x02\x00\x00R\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8INSERT IGNORE INTO compose_namespace (id, name, slug, enabled, meta)\n VALUES (88714882739863655, 'Crust CRM', 'crm', true, '{}');\n\nALTER TABLE `compose_attachment`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_chart`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_module`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_page`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_record`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_trigger`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nUPDATE `compose_attachment` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_chart` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_module` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_page` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_record` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_trigger` SET `rel_namespace` = 88714882739863655;\n\n\nALTER TABLE `compose_attachment`\n ADD CONSTRAINT `compose_attachment_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_chart`\n ADD CONSTRAINT `compose_chart_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_module`\n ADD CONSTRAINT `compose_module_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_page`\n ADD CONSTRAINT `compose_page_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_record`\n ADD CONSTRAINT `compose_record_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_trigger`\n ADD CONSTRAINT `compose_trigger_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\nPK\x07\x08\xfc\xdb\xfd\xdde \x00\x00e \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `compose_page`\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL;\n\nALTER TABLE `compose_page` CHANGE COLUMN `module_id` `rel_module` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nPK\x07\x08\x82\x01Rn1\x01\x00\x001\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190514090000.module_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE compose_module_form\n RENAME TO compose_module_field;\n\n-- Remove orphaned and invalid fields\nDELETE FROM `compose_module_field` WHERE `module_id` NOT IN (SELECT `id` FROM `compose_module`) OR `name` = '';\n\n-- Order and consistency.\nALTER TABLE `compose_module_field`\n ADD COLUMN `id` BIGINT UNSIGNED NOT NULL FIRST,\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL,\n RENAME COLUMN `module_id` TO `rel_module`,\n RENAME COLUMN `json` TO `options`;\n\n-- Generate IDs for the new field, use module, offset by one (just to start with a different ID)\n-- and use place (0 based, +1 for every field, expecting to be unique per module because of the existing pkey)\nUPDATE `compose_module_field` SET id = rel_module + 1 + place;\n\n-- Drop old primary key (module_id, place)\nALTER TABLE `compose_module_field` DROP PRIMARY KEY, ADD PRIMARY KEY(`id`);\n\n-- Foreign key\nALTER TABLE `compose_module_field`\n ADD CONSTRAINT `compose_module`\n FOREIGN KEY (`rel_module`)\n REFERENCES `compose_module` (`id`);\n\n-- And unique indexes for module+place/name combos.\nCREATE UNIQUE INDEX uid_compose_module_field_place ON compose_module_field (`rel_module`, `place`);\nCREATE UNIQUE INDEX uid_compose_module_field_name ON compose_module_field (`rel_module`, `name`);\nPK\x07\x08\xb1(\xbb\xf0\x8d\x05\x00\x00\x8d\x05\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS compose_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\nPK\x07\x08\"\xd8\xe5H\x12\x01\x00\x00\x12\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x0020190701090000.automation.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS compose_automation_trigger;\nDROP TABLE IF EXISTS compose_automation_script;\n\nCREATE TABLE IF NOT EXISTS compose_automation_script (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL DEFAULT 'unnamed' COMMENT 'The name of the script',\n `source` TEXT NOT NULL COMMENT 'Source code for the script',\n `source_ref` VARCHAR(200) NOT NULL COMMENT 'Where is the script located (if remote)',\n `async` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Do we run this script asynchronously?',\n `rel_runner` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who is running the script? 0 for invoker',\n `run_in_ua` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Run this script inside user-agent environment',\n `timeout` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Any explicit timeout set for this script (milliseconds)?',\n `critical` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is it critical that this script is executed successfully',\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is this script enabled?',\n\n `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `updated_at` DATETIME NULL DEFAULT NULL,\n `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `deleted_at` DATETIME NULL DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE IF NOT EXISTS compose_automation_trigger (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is trigger',\n\n `resource` VARCHAR(128) NOT NULL COMMENT 'Resource triggering the event',\n `event` VARCHAR(128) NOT NULL COMMENT 'Event triggered',\n `condition` TEXT NOT NULL COMMENT 'Trigger condition',\n\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?',\n\n `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `updated_at` DATETIME NULL DEFAULT NULL,\n `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `deleted_at` DATETIME NULL DEFAULT NULL,\n\n CONSTRAINT `fk_script` FOREIGN KEY (`rel_script`) REFERENCES `compose_automation_script` (`id`),\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\n\n# Insert all into\nINSERT INTO compose_automation_script (id, name, source, source_ref, run_in_ua, critical, enabled, created_at, updated_at, deleted_at)\nSELECT id, name, source, '', true, false, enabled, created_at, updated_at, deleted_at from compose_trigger;\n\nINSERT INTO compose_automation_trigger (id, event, resource, `condition`, rel_script, enabled, created_at, updated_at, deleted_at)\nSELECT id+seq, events.event, 'compose:record', rel_module, id, enabled, created_at, updated_at, deleted_at from compose_trigger AS t INNER JOIN\n ( SELECT 0 as seq, '' AS event\n UNION SELECT 1 as seq, 'manual' AS event\n UNION SELECT 2 as seq, 'beforeCreate' AS event\n UNION SELECT 3 as seq, 'afterCreate' AS event\n UNION SELECT 4 as seq, 'beforeUpdate' AS event\n UNION SELECT 5 as seq, 'afterUpdate' AS event\n UNION SELECT 6 as seq, 'beforeDelete' AS event\n UNION SELECT 7 as seq, 'afterDelete' AS event) AS events ON ((event = '' AND t.actions = '')\n OR (event <> '' AND t.actions LIKE concat('%',event,'%') ));\n\n-- DROP TABLE IF EXISTS compose_trigger;\nPK\x07\x08\xb0\xe6U\x96\xc2\x0f\x00\x00\xc2\x0f\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` text NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x089S\x05%x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sql\nPK\x07\x08\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81c\n\x00\x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x84\x0c\x00\x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Q\x0d\x00\x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\\\x0e\x00\x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00D\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc8\x0f\x00\x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x005\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd4\x10\x00\x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\x96\xd6pj\x00\x00\x00j\x00\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x97\x11\x00\x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x004\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81h\x12\x00\x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x13\x00\x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc1\x15\x00\x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x18\x00\x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00&\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81r\x18\x00\x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00/\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x87\x1b\x00\x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x04]{\x1fo\x04\x00\x00o\x04\x00\x008\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x1a\x1c\x00\x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xf8 \x00\x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81w#\x00\x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\x1a)|\x97\x02\x00\x00\x97\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x81&\x00\x0020190427180922.change-tbl-prefix.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(m\xeb\xed~R\x02\x00\x00R\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81v)\x00\x0020190427210922.namespace-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xfc\xdb\xfd\xdde \x00\x00e \x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\",\x00\x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x82\x01Rn1\x01\x00\x001\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xe25\x00\x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb1(\xbb\xf0\x8d\x05\x00\x00\x8d\x05\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81o7\x00\x0020190514090000.module_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\xd8\xe5H\x12\x01\x00\x00\x12\x01\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81V=\x00\x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb0\xe6U\x96\xc2\x0f\x00\x00\xc2\x0f\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc0>\x00\x0020190701090000.automation.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd9N\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\x96P\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x1a\x00\x1a\x00\x8c \x00\x00\x02Q\x00\x00\x00\x00" diff --git a/compose/db/schema/mysql/20190701090000.automation.up.sql b/compose/db/schema/mysql/20190701090000.automation.up.sql new file mode 100644 index 000000000..24753e412 --- /dev/null +++ b/compose/db/schema/mysql/20190701090000.automation.up.sql @@ -0,0 +1,68 @@ +DROP TABLE IF EXISTS compose_automation_trigger; +DROP TABLE IF EXISTS compose_automation_script; + +CREATE TABLE IF NOT EXISTS compose_automation_script ( + `id` BIGINT(20) UNSIGNED NOT NULL, + `name` VARCHAR(64) NOT NULL DEFAULT 'unnamed' COMMENT 'The name of the script', + `source` TEXT NOT NULL COMMENT 'Source code for the script', + `source_ref` VARCHAR(200) NOT NULL COMMENT 'Where is the script located (if remote)', + `async` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Do we run this script asynchronously?', + `rel_runner` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who is running the script? 0 for invoker', + `run_in_ua` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Run this script inside user-agent environment', + `timeout` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Any explicit timeout set for this script (milliseconds)?', + `critical` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is it critical that this script is executed successfully', + `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is this script enabled?', + + `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `updated_at` DATETIME NULL DEFAULT NULL, + `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `deleted_at` DATETIME NULL DEFAULT NULL, + + PRIMARY KEY (`id`) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS compose_automation_trigger ( + `id` BIGINT(20) UNSIGNED NOT NULL, + `rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is trigger', + + `resource` VARCHAR(128) NOT NULL COMMENT 'Resource triggering the event', + `event` VARCHAR(128) NOT NULL COMMENT 'Event triggered', + `condition` TEXT NOT NULL COMMENT 'Trigger condition', + + `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?', + + `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `updated_at` DATETIME NULL DEFAULT NULL, + `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `deleted_at` DATETIME NULL DEFAULT NULL, + + CONSTRAINT `fk_script` FOREIGN KEY (`rel_script`) REFERENCES `compose_automation_script` (`id`), + + PRIMARY KEY (`id`) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + + +# Insert all into +INSERT INTO compose_automation_script (id, name, source, source_ref, run_in_ua, critical, enabled, created_at, updated_at, deleted_at) +SELECT id, name, source, '', true, false, enabled, created_at, updated_at, deleted_at from compose_trigger; + +INSERT INTO compose_automation_trigger (id, event, resource, `condition`, rel_script, enabled, created_at, updated_at, deleted_at) +SELECT id+seq, events.event, 'compose:record', rel_module, id, enabled, created_at, updated_at, deleted_at from compose_trigger AS t INNER JOIN + ( SELECT 0 as seq, '' AS event + UNION SELECT 1 as seq, 'manual' AS event + UNION SELECT 2 as seq, 'beforeCreate' AS event + UNION SELECT 3 as seq, 'afterCreate' AS event + UNION SELECT 4 as seq, 'beforeUpdate' AS event + UNION SELECT 5 as seq, 'afterUpdate' AS event + UNION SELECT 6 as seq, 'beforeDelete' AS event + UNION SELECT 7 as seq, 'afterDelete' AS event) AS events ON ((event = '' AND t.actions = '') + OR (event <> '' AND t.actions LIKE concat('%',event,'%') )); + +-- DROP TABLE IF EXISTS compose_trigger; diff --git a/compose/internal/service/automation_runner.go b/compose/internal/service/automation_runner.go new file mode 100644 index 000000000..6d5d7b51d --- /dev/null +++ b/compose/internal/service/automation_runner.go @@ -0,0 +1,214 @@ +package service + +import ( + "context" + "strconv" + "time" + + "go.uber.org/zap" + "google.golang.org/grpc" + + "github.com/cortezaproject/corteza-server/compose/proto" + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/pkg/automation" + "github.com/cortezaproject/corteza-server/pkg/sentry" +) + +type ( + automationRunner struct { + logger *zap.Logger + runner proto.ScriptRunnerClient + scriptFinder automationScriptsFinder + jwtEncoder auth.TokenEncoder + } + + automationScriptsFinder interface { + Watch(ctx context.Context) + FindRunnableScripts(event, resource string, cc ...automation.TriggerConditionChecker) automation.ScriptSet + } +) + +func AutomationRunner(f automationScriptsFinder, r proto.ScriptRunnerClient) automationRunner { + var svc = automationRunner{ + scriptFinder: f, + runner: r, + + logger: DefaultLogger.Named("automationRunner"), + jwtEncoder: auth.DefaultJwtHandler, + } + + return svc +} + +func (svc automationRunner) findRecordScripts(event string, moduleID uint64) (ss automation.ScriptSet) { + const resource = "compose:record" + + // We'll be comparing strings, not uint64! + var moduleIDs = strconv.FormatUint(moduleID, 10) + + return svc.scriptFinder.FindRunnableScripts(event, resource, + // ModuleID MUST match + func(cModuleID string) bool { + return moduleIDs == cModuleID + }, + ) +} + +func (svc automationRunner) Watch(ctx context.Context) { + svc.scriptFinder.Watch(ctx) +} + +// ManualRecordRun - Manual trigger run +// +// This is explicitly called, extra security check is needed +func (svc automationRunner) ManualRecordRun(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { + // @todo security check (can user run this script (scriptID) manually) + + runner := svc.makeRecordScriptRunner(ctx, ns, m, r, true) + + return svc.findRecordScripts("manual", m.ID).Walk(func(script *automation.Script) error { + // Interested in a specific script, so skip everything else + if script.ID != scriptID { + return nil + } + + return runner(script) + }) +} + +// BeforeRecordCreate - run scripts before record is created +// +// This is implicitly called, no extra security check is needed +func (svc automationRunner) BeforeRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { + return svc.findRecordScripts("beforeCreate", m.ID).Walk( + svc.makeRecordScriptRunner(ctx, ns, m, r, true), + ) +} + +// AfterRecordCreate - run scripts before record is created +// +// This is implicitly called, no extra security check is needed +func (svc automationRunner) AfterRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { + return svc.findRecordScripts("afterCreate", m.ID).Walk( + svc.makeRecordScriptRunner(ctx, ns, m, r, true), + ) +} + +// BeforeRecordUpdate - run scripts before record is updated +// +// This is implicitly called, no extra security check is needed +func (svc automationRunner) BeforeRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { + return svc.findRecordScripts("beforeUpdate", m.ID).Walk( + svc.makeRecordScriptRunner(ctx, ns, m, r, true), + ) +} + +// AfterRecordUpdate - run scripts before record is updated +// +// This is implicitly called, no extra security check is needed +func (svc automationRunner) AfterRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { + return svc.findRecordScripts("afterUpdate", m.ID).Walk( + svc.makeRecordScriptRunner(ctx, ns, m, r, true), + ) +} + +// BeforeRecordDelete - run scripts before record is deleted +// +// This is implicitly called, no extra security check is needed +func (svc automationRunner) BeforeRecordDelete(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { + return svc.findRecordScripts("beforeDelete", m.ID).Walk( + svc.makeRecordScriptRunner(ctx, ns, m, r, true), + ) +} + +// AfterRecordDelete - run scripts after record is deleted +// +// This is implicitly called, no extra security check is needed +func (svc automationRunner) AfterRecordDelete(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { + return svc.findRecordScripts("afterDelete", m.ID).Walk( + svc.makeRecordScriptRunner(ctx, ns, m, r, true), + ) +} + +// Runs record script +// +// We set-up script-running environment: security (definer / invoker), async, critical +// and copying values from the run to the given Record +// +func (svc automationRunner) makeRecordScriptRunner(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record, discard bool) func(script *automation.Script) error { + // Static request params (record gets updated + var req = &proto.RunRecordRequest{ + Namespace: proto.FromNamespace(ns), + Module: proto.FromModule(m), + Record: proto.FromRecord(r), + } + + svc.logger.Debug("executing script", zap.Any("record", r)) + + return func(script *automation.Script) error { + // This could be executed in a goroutine (by *after triggers, + // so we need ot rewire the sentry panic recoverty + defer sentry.Recover() + + ctx, cancelFn := context.WithTimeout(ctx, time.Second*5) + defer cancelFn() + + // Add invoker's or defined credentials/jwt + req.JWT = svc.getJWT(ctx, script.RunAs) + + // Add script info + req.Script = proto.FromAutomationScript(script) + + rsp, err := svc.runner.Record(ctx, req, grpc.WaitForReady(script.Critical)) + + svc.logger.Debug("call sent") + + if err != nil { + // @todo aborted? + svc.logger.Debug("script executed, did not return record", zap.Error(err)) + if !script.Critical { + // This was not a critical call and we do not care about + // errors from script running service. + return nil + } + + return err + } + + if script.Async || discard { + // Discard returned values (in case of async call or when forced) + // + // Backend is still returning, so we do not + // need to handle multiple gRPC endpoints + svc.logger.Debug("script executed / async") + return nil + } + + if rsp.Record == nil { + // Script did not return any results + // This means we should stop with the execution + // @todo aborted + return nil + } + + svc.logger.Debug("script executed", zap.Any("record", rsp.Record)) + + // Convert from proto and copy record owner & values from the result + result := proto.ToRecord(rsp.Record) + r.OwnedBy, r.Values = result.OwnedBy, result.Values + + return nil + } +} + +// Creates a new JWT for +func (svc automationRunner) getJWT(ctx context.Context, userID uint64) string { + if userID > 0 { + // @todo implement this + // at the moment we do not he the ability fetch user info from non-system service + // extend/implement this feature when our services will know how to communicate with each-other + } + + return svc.jwtEncoder.Encode(auth.GetIdentityFromContext(ctx)) +} diff --git a/compose/internal/service/automation_script.go b/compose/internal/service/automation_script.go new file mode 100644 index 000000000..18d772ff5 --- /dev/null +++ b/compose/internal/service/automation_script.go @@ -0,0 +1,60 @@ +package service + +import ( + "context" + + "go.uber.org/zap" + + "github.com/cortezaproject/corteza-server/pkg/automation" +) + +type ( + automationScript struct { + logger *zap.Logger + scriptManager automationScriptManager + } + + automationScriptManager interface { + FindScriptByID(context.Context, uint64) (*automation.Script, error) + FindScripts(context.Context, automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) + CreateScript(context.Context, *automation.Script) error + UpdateScript(context.Context, *automation.Script) error + DeleteScript(context.Context, *automation.Script) error + } +) + +func AutomationScript(sm automationScriptManager) automationScript { + var svc = automationScript{ + scriptManager: sm, + logger: DefaultLogger.Named("automation-script"), + } + + return svc +} + +func (svc automationScript) FindByID(ctx context.Context, scriptID uint64) (*automation.Script, error) { + // @todo security check - can user read this script? + return svc.scriptManager.FindScriptByID(ctx, scriptID) +} + +func (svc automationScript) Find(ctx context.Context, f automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) { + // @todo security check - can user read these scripts? + return svc.scriptManager.FindScripts(ctx, f) +} + +func (svc automationScript) Create(ctx context.Context, s *automation.Script) (err error) { + // @todo security check - can user create scripts? + // @todo security check - can make scripts with security-definer? + return svc.scriptManager.CreateScript(ctx, s) +} + +func (svc automationScript) Update(ctx context.Context, s *automation.Script) (err error) { + // @todo security check - can user update this script? + // @todo security check - can make scripts with security-definer? + return svc.scriptManager.UpdateScript(ctx, s) +} + +func (svc automationScript) Delete(ctx context.Context, s *automation.Script) (err error) { + // @todo security check - can user delete this script? + return svc.scriptManager.DeleteScript(ctx, s) +} diff --git a/compose/internal/service/automation_trigger.go b/compose/internal/service/automation_trigger.go new file mode 100644 index 000000000..a81b5bfd9 --- /dev/null +++ b/compose/internal/service/automation_trigger.go @@ -0,0 +1,59 @@ +package service + +import ( + "context" + + "go.uber.org/zap" + + "github.com/cortezaproject/corteza-server/pkg/automation" +) + +type ( + // Handles automation triggers storing and loading + automationTrigger struct { + logger *zap.Logger + triggerManager automationTriggerManager + } + + automationTriggerManager interface { + FindTriggerByID(context.Context, uint64) (*automation.Trigger, error) + FindTriggers(context.Context, automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error) + CreateTrigger(context.Context, *automation.Script, *automation.Trigger) error + UpdateTrigger(context.Context, *automation.Script, *automation.Trigger) error + DeleteTrigger(context.Context, *automation.Trigger) error + } +) + +func AutomationTrigger(tm automationTriggerManager) automationTrigger { + var svc = automationTrigger{ + triggerManager: tm, + logger: DefaultLogger.Named("automation-trigger"), + } + + return svc +} + +func (svc automationTrigger) FindByID(ctx context.Context, triggerID uint64) (*automation.Trigger, error) { + // @todo security check - can user read this trigger? + return svc.triggerManager.FindTriggerByID(ctx, triggerID) +} + +func (svc automationTrigger) Find(ctx context.Context, f automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error) { + // @todo security check - can user read these triggers? + return svc.triggerManager.FindTriggers(ctx, f) +} + +func (svc automationTrigger) Create(ctx context.Context, s *automation.Script, t *automation.Trigger) (err error) { + // @todo security check - can user create trigger on this specific resource + return svc.triggerManager.CreateTrigger(ctx, s, t) +} + +func (svc automationTrigger) Update(ctx context.Context, s *automation.Script, t *automation.Trigger) (err error) { + // @todo security check - can user create update triggers on this specific resource + return svc.triggerManager.UpdateTrigger(ctx, s, t) +} + +func (svc automationTrigger) Delete(ctx context.Context, t *automation.Trigger) (err error) { + // @todo security check - can user create delete triggers on this specific resource + return svc.triggerManager.DeleteTrigger(ctx, t) +} diff --git a/compose/internal/service/record.go b/compose/internal/service/record.go index b1bbe7f78..84124655e 100644 --- a/compose/internal/service/record.go +++ b/compose/internal/service/record.go @@ -22,7 +22,7 @@ type ( logger *zap.Logger ac recordAccessController - sr RecordScriptRunner + sr RecordScriptsRunner recordRepo repository.RecordRepository moduleRepo repository.ModuleRepository @@ -41,8 +41,14 @@ type ( CanUpdateRecordValue(context.Context, *types.ModuleField) bool } - RecordScriptRunner interface { - Record(context.Context, Runnable, *types.Namespace, *types.Module, *types.Record) (*types.Record, error) + RecordScriptsRunner interface { + ManualRecordRun(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error) + BeforeRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) + AfterRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) + BeforeRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) + AfterRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) + BeforeRecordDelete(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) + AfterRecordDelete(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) } RecordService interface { @@ -59,7 +65,7 @@ type ( DeleteByID(namespaceID, recordID uint64) error - // Fields(module *types.Module, record *types.Record) ([]*types.RecordValue, error) + RunScript(namespaceID, moduleID, recordID, scriptID uint64) error } Encoder interface { @@ -71,7 +77,7 @@ func Record() RecordService { return (&record{ logger: DefaultLogger.Named("record"), ac: DefaultAccessControl, - sr: DefaultScriptRunner, + sr: DefaultAutomationRunner, }).With(context.Background()) } @@ -188,6 +194,7 @@ func (svc record) Find(filter types.RecordFilter) (set types.RecordSet, f types. // @todo better value handling func (svc record) Export(filter types.RecordFilter, enc Encoder) error { m, err := svc.loadModule(filter.NamespaceID, filter.ModuleID) + if err != nil { return err } @@ -205,7 +212,7 @@ func (svc record) Export(filter types.RecordFilter, enc Encoder) error { } func (svc record) Create(mod *types.Record) (r *types.Record, err error) { - ns, m, r, tt, err := svc.loadCombo(mod.NamespaceID, mod.ModuleID, 0) + ns, m, r, err := svc.loadCombo(mod.NamespaceID, mod.ModuleID, 0) if err != nil { return } @@ -231,12 +238,14 @@ func (svc record) Create(mod *types.Record) (r *types.Record, err error) { mod = nil // make sure we do not use it anymore - if err = tt.WalkByAction("beforeCreate", svc.runTrigger(svc.ctx, ns, m, r)); err != nil { + if err = svc.sr.BeforeRecordCreate(svc.ctx, ns, m, r); err != nil { + // Calling return } defer func() { - _ = tt.WalkByAction("afterCreate", svc.runTrigger(svc.ctx, ns, m, r)) + // Run this at the end and discard the error + _ = svc.sr.AfterRecordCreate(svc.ctx, ns, m, r) }() return r, svc.db.Transaction(func() (err error) { @@ -257,7 +266,7 @@ func (svc record) Update(mod *types.Record) (r *types.Record, err error) { return nil, ErrInvalidID.withStack() } - ns, m, r, tt, err := svc.loadCombo(mod.NamespaceID, mod.ModuleID, mod.ID) + ns, m, r, err := svc.loadCombo(mod.NamespaceID, mod.ModuleID, mod.ID) if err != nil { return } @@ -281,12 +290,14 @@ func (svc record) Update(mod *types.Record) (r *types.Record, err error) { mod = nil // make sure we do not use it anymore - if err = tt.WalkByAction("beforeUpdate", svc.runTrigger(svc.ctx, ns, m, r)); err != nil { + if err = svc.sr.BeforeRecordUpdate(svc.ctx, ns, m, r); err != nil { + // Calling return } defer func() { - _ = tt.WalkByAction("afterUpdate", svc.runTrigger(svc.ctx, ns, m, r)) + // Run this at the end and discard the error + _ = svc.sr.AfterRecordUpdate(svc.ctx, ns, m, r) }() return r, svc.db.Transaction(func() (err error) { @@ -307,17 +318,19 @@ func (svc record) DeleteByID(namespaceID, recordID uint64) (err error) { return ErrInvalidID.withStack() } - ns, m, r, tt, err := svc.loadCombo(namespaceID, 0, recordID) + ns, m, r, err := svc.loadCombo(namespaceID, 0, recordID) if err != nil { return } - if err = tt.WalkByAction("beforeDelete", svc.runTrigger(svc.ctx, ns, m, r)); err != nil { + if err = svc.sr.BeforeRecordCreate(svc.ctx, ns, m, r); err != nil { + // Calling return } defer func() { - _ = tt.WalkByAction("afterDelete", svc.runTrigger(svc.ctx, ns, m, r)) + // Run this at the end and discard the error + _ = svc.sr.AfterRecordDelete(svc.ctx, ns, m, r) }() err = svc.db.Transaction(func() (err error) { @@ -342,7 +355,7 @@ func (svc record) DeleteByID(namespaceID, recordID uint64) (err error) { // loadCombo Loads everything we need for record manipulation // // Loads namespace, module, record and set of triggers. -func (svc record) loadCombo(namespaceID, moduleID, recordID uint64) (ns *types.Namespace, m *types.Module, r *types.Record, tt types.TriggerSet, err error) { +func (svc record) loadCombo(namespaceID, moduleID, recordID uint64) (ns *types.Namespace, m *types.Module, r *types.Record, err error) { if namespaceID == 0 { err = ErrNamespaceRequired return @@ -363,54 +376,12 @@ func (svc record) loadCombo(namespaceID, moduleID, recordID uint64) (ns *types.N return } - tt, _, err = svc.tRepo.Find(types.TriggerFilter{ - // Make sure we stay in the same namespace - NamespaceID: ns.ID, - - // Triggered scripts are always module-bound - ModuleID: m.ID, - - // We are only interested in enabled scripts - EnabledOnly: true, - }) - return } -func (svc record) runTrigger(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) func(t *types.Trigger) error { - svc.logger.Debug("initializing trigger runner") - return func(t *types.Trigger) error { - svc.logger.Debug("running trigger", zap.Uint64("triggerID", t.ID)) - if svc.sr == nil { - // No script runner set - svc.logger.Debug("script runner not set") - return nil - } - - // pr == processed record - pr, err := svc.sr.Record(svc.ctx, t, ns, m, r) - if err != nil { - svc.logger.Debug("failed to run record script", zap.Error(err)) - return err - } - - if pr == nil { - // Did not get any processed record, - // consider canceled - return errors.New("aborted by automation") - } - - return svc.copyChanges(m, pr, r) - } -} - // Copies changes from mod to r(ecord) func (svc record) copyChanges(m *types.Module, mod, r *types.Record) (err error) { - // Automation scripts are allowed to modify record owner & values. - if mod.OwnedBy > 0 { - r.OwnedBy = mod.OwnedBy - } - + r.OwnedBy = mod.OwnedBy r.Values, err = svc.sanitizeValues(m, mod.Values) return err } diff --git a/compose/internal/service/script_runner.go b/compose/internal/service/script_runner.go deleted file mode 100644 index 3ada45998..000000000 --- a/compose/internal/service/script_runner.go +++ /dev/null @@ -1,190 +0,0 @@ -package service - -import ( - "context" - "errors" - "time" - - "go.uber.org/zap" - "go.uber.org/zap/zapgrpc" - "google.golang.org/grpc" - "google.golang.org/grpc/grpclog" - - "github.com/cortezaproject/corteza-server/compose/proto" - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/internal/auth" - "github.com/cortezaproject/corteza-server/pkg/cli/options" -) - -// Script runner provides an interface to corteza-corredor (Spanish for runner) service -// that helps us with execution of JavaScript code -- compose's triggers & automation code -// -// corteza-server communicates with corteza-corredor via gRPC protocol. -// -// This service accepts ns/trigger/module/record (combinations), makes a call via gRPC protocol and -// returns record/module/ns or just tests trigger's script - -type ( - scriptRunner struct { - c options.ScriptRunnerOpt - logger *zap.Logger - conn *grpc.ClientConn - client proto.ScriptRunnerClient - jwtEncoder auth.TokenEncoder - } - - Runnable interface { - proto.Runnable - - IsCritical() bool - GetRunnerID() uint64 - } - - ScriptRunnerService interface { - Close() error - Namespace(context.Context, Runnable, *types.Namespace) (*types.Namespace, error) - Module(context.Context, Runnable, *types.Namespace, *types.Module) (*types.Module, error) - Record(context.Context, Runnable, *types.Namespace, *types.Module, *types.Record) (*types.Record, error) - } -) - -// @todo move to opt so all services can use it -func ScriptRunner(c options.ScriptRunnerOpt) (svc *scriptRunner, err error) { - svc = &scriptRunner{ - c: c, - logger: DefaultLogger.Named("script-runner"), - jwtEncoder: auth.DefaultJwtHandler, - } - - if !c.Enabled { - // Do not connect when script runner is not enabled - return - } - - return svc, svc.connect() -} - -func (svc *scriptRunner) connect() (err error) { - if svc.c.Log { - // Send logs to zap - // - // waiting for https://github.com/uber-go/zap/pull/538 - grpclog.SetLogger(zapgrpc.NewLogger(svc.logger.Named("grpc"))) - } - - var dopts = []grpc.DialOption{ - // @todo insecure? - grpc.WithInsecure(), - } - - if svc.c.MaxBackoffDelay > 0 { - dopts = append(dopts, grpc.WithBackoffMaxDelay(svc.c.MaxBackoffDelay)) - } - - svc.conn, err = grpc.Dial(svc.c.Addr, dopts...) - - if err != nil { - return - } - - svc.client = proto.NewScriptRunnerClient(svc.conn) - return -} - -func (svc scriptRunner) Close() error { - return svc.conn.Close() -} - -func (svc scriptRunner) callOptions() []grpc.CallOption { - return []grpc.CallOption{ - grpc.WaitForReady(true), - } -} - -// Creates a new JWT for -func (svc scriptRunner) getJWT(ctx context.Context, r Runnable) string { - if r.GetRunnerID() > 0 { - // @todo implement this - // at the moment we do not he the ability fetch user info from non-system service - // extend/implement this feature when our services will know how to communicate with each-other - } - - return svc.jwtEncoder.Encode(auth.GetIdentityFromContext(ctx)) -} - -func (svc scriptRunner) Namespace(ctx context.Context, s Runnable, ns *types.Namespace) (*types.Namespace, error) { - panic("scriptRunner.Namespace() not implemented") -} - -func (svc scriptRunner) Module(ctx context.Context, s Runnable, ns *types.Namespace, m *types.Module) (*types.Module, error) { - panic("scriptRunner.Module() not implemented") -} - -func (svc scriptRunner) Record(ctx context.Context, s Runnable, ns *types.Namespace, m *types.Module, r *types.Record) (*types.Record, error) { - if s == nil { - return nil, errors.New("script not provided") - } - - if ns == nil { - return nil, errors.New("namespace not provided") - } - - if m == nil { - return nil, errors.New("module not provided") - } - - if !svc.c.Enabled { - if s.IsCritical() { - // Oh dear, we are in quite a pickle: - // Script runner is disabled but we have critical script to run - return nil, errors.New("script runner disabled") - } - - // Log this - svc.logger.Debug("executing script", zap.Any("record", r)) - - // and pretend like nothing happened - return r, nil - } - - svc.logger.Debug("executing script", zap.Any("record", r)) - - ctx, cancelFn := context.WithTimeout(ctx, time.Second*5) - defer cancelFn() - - rsp, err := svc.client.Record( - ctx, - &proto.RunRecordRequest{ - JWT: svc.getJWT(ctx, s), - Script: proto.ScriptFromRunnable(s), - Namespace: proto.FromNamespace(ns), - Module: proto.FromModule(m), - Record: proto.FromRecord(r), - }, - svc.callOptions()..., - ) - - svc.logger.Debug("call sent") - - if err != nil { - svc.logger.Debug("script executed, did not return record", zap.Error(err)) - if !s.IsCritical() { - // This was not a critical call and we do not care about - // errors from script running service. - return r, nil - } - - return nil, err - } - - if s.IsAsync() { - svc.logger.Debug("script executed / async") - // Async call, we do not care about what we get back - return r, nil - } - - svc.logger.Debug("script executed", zap.Any("record", rsp.Record)) - - // Result from the automation script - return proto.ToRecord(rsp.Record), nil -} diff --git a/compose/internal/service/service.go b/compose/internal/service/service.go index 15f001a72..fa1260346 100644 --- a/compose/internal/service/service.go +++ b/compose/internal/service/service.go @@ -7,8 +7,10 @@ import ( "go.uber.org/zap" "github.com/cortezaproject/corteza-server/compose/internal/repository" + "github.com/cortezaproject/corteza-server/compose/proto" "github.com/cortezaproject/corteza-server/internal/permissions" "github.com/cortezaproject/corteza-server/internal/store" + "github.com/cortezaproject/corteza-server/pkg/automation" "github.com/cortezaproject/corteza-server/pkg/cli/options" ) @@ -18,6 +20,13 @@ type ( Watch(ctx context.Context) } + // automationManager interface { + // automationScriptsFinder + // automationScriptManager + // automationTriggerManager + // Watch(ctx context.Context) + // } + Config struct { Storage options.StorageOpt ScriptRunner options.ScriptRunnerOpt @@ -25,25 +34,36 @@ type ( ) var ( - DefaultPermissions permissionServicer - DefaultLogger *zap.Logger + // DefaultPermissions Retrives & stores permissions + DefaultPermissions permissionServicer + + // DefaultAccessControl Access control checking DefaultAccessControl *accessControl - DefaultRecord RecordService - DefaultModule ModuleService - DefaultTrigger TriggerService - DefaultChart ChartService - DefaultPage PageService - DefaultNotification NotificationService - DefaultAttachment AttachmentService - DefaultNamespace NamespaceService + // DefaultAutomationScriptManager manages scripts + DefaultAutomationScriptManager automationScript - DefaultScriptRunner ScriptRunnerService + // DefaultAutomationTriggerManager manages triggerManager + DefaultAutomationTriggerManager automationTrigger + + // DefaultAutomationRunner runs automation scripts by listening to triggerManager and invoking Corredor service + DefaultAutomationRunner automationRunner + + DefaultNamespace NamespaceService + DefaultRecord RecordService + DefaultModule ModuleService + DefaultChart ChartService + DefaultPage PageService + + DefaultAttachment AttachmentService + DefaultNotification NotificationService ) func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { + var db = repository.DB(ctx) + DefaultLogger = log.Named("service") fs, err := store.New(c.Storage.Path) @@ -52,31 +72,47 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { return err } + // Permissions, access control DefaultPermissions = permissions.Service( ctx, DefaultLogger, - permissions.Repository(repository.DB(ctx), "compose_permission_rules")) + permissions.Repository(db, "compose_permission_rules")) DefaultAccessControl = AccessControl(DefaultPermissions) - DefaultScriptRunner, err = ScriptRunner(c.ScriptRunner) + // ias Internal Automatinon Service + // handles script & trigger management & keeping runnables cripts in internal cache + ias := automation.Service(ctx, DefaultLogger, automation.AutomationServiceConfig{DB: db, DbTablePrefix: "compose"}) + + // Pass automation manager to + DefaultAutomationScriptManager = AutomationScript(ias) + DefaultAutomationTriggerManager = AutomationTrigger(ias) + + corredor, err := automation.Corredor(ctx, c.ScriptRunner, DefaultLogger) + log.Info("initializing corredor connection", zap.String("addr", c.ScriptRunner.Addr), zap.Error(err)) if err != nil { - return + return err } + DefaultAutomationRunner = AutomationRunner(ias, proto.NewScriptRunnerClient(corredor)) + + // Compose internals: + DefaultNamespace = Namespace() DefaultRecord = Record() DefaultModule = Module() - DefaultTrigger = Trigger() DefaultPage = Page() DefaultChart = Chart() DefaultNotification = Notification() DefaultAttachment = Attachment(fs) - DefaultNamespace = Namespace() return nil } func Watchers(ctx context.Context) { + // Reloading automation scripts on change + DefaultAutomationRunner.Watch(ctx) + + // Reloading permissions on change DefaultPermissions.Watch(ctx) } diff --git a/compose/internal/service/trigger.go b/compose/internal/service/trigger.go deleted file mode 100644 index fe5152f56..000000000 --- a/compose/internal/service/trigger.go +++ /dev/null @@ -1,170 +0,0 @@ -package service - -import ( - "context" - - "github.com/titpetric/factory" - "go.uber.org/zap" - - "github.com/cortezaproject/corteza-server/compose/internal/repository" - "github.com/cortezaproject/corteza-server/compose/types" -) - -type ( - trigger struct { - db *factory.DB - ctx context.Context - logger *zap.Logger - - ac triggerAccessController - - triggerRepo repository.TriggerRepository - nsRepo repository.NamespaceRepository - } - - triggerAccessController interface { - CanReadNamespace(context.Context, *types.Namespace) bool - CanCreateTrigger(context.Context, *types.Namespace) bool - CanReadTrigger(context.Context, *types.Trigger) bool - CanUpdateTrigger(context.Context, *types.Trigger) bool - CanDeleteTrigger(context.Context, *types.Trigger) bool - } - - TriggerService interface { - With(ctx context.Context) TriggerService - - FindByID(namespaceID, triggerID uint64) (*types.Trigger, error) - Find(filter types.TriggerFilter) (set types.TriggerSet, f types.TriggerFilter, err error) - - Create(trigger *types.Trigger) (*types.Trigger, error) - Update(trigger *types.Trigger) (*types.Trigger, error) - DeleteByID(namespaceID, triggerID uint64) error - } -) - -func Trigger() TriggerService { - return (&trigger{ - logger: DefaultLogger.Named("trigger"), - ac: DefaultAccessControl, - }).With(context.Background()) -} - -func (svc trigger) With(ctx context.Context) TriggerService { - db := repository.DB(ctx) - return &trigger{ - db: db, - ctx: ctx, - logger: svc.logger, - - ac: svc.ac, - - triggerRepo: repository.Trigger(ctx, db), - nsRepo: repository.Namespace(ctx, db), - } -} - -// log() returns zap's logger with requestID from current context and fields. -// func (svc trigger) log(fields ...zapcore.Field) *zap.Logger { -// return logger.AddRequestID(svc.ctx, svc.logger).With(fields...) -// } - -func (svc trigger) FindByID(namespaceID, triggerID uint64) (t *types.Trigger, err error) { - if namespaceID == 0 { - return nil, ErrNamespaceRequired - } - - if t, err = svc.triggerRepo.FindByID(namespaceID, triggerID); err != nil { - return - } else if !svc.ac.CanReadTrigger(svc.ctx, t) { - return nil, ErrNoReadPermissions.withStack() - } - - return -} - -func (svc trigger) Find(filter types.TriggerFilter) (set types.TriggerSet, f types.TriggerFilter, err error) { - set, f, err = svc.triggerRepo.Find(filter) - if err != nil { - return - } - - set, _ = set.Filter(func(m *types.Trigger) (bool, error) { - return svc.ac.CanReadTrigger(svc.ctx, m), nil - }) - - return -} - -func (svc trigger) Create(mod *types.Trigger) (t *types.Trigger, err error) { - if mod.NamespaceID == 0 { - return nil, ErrNamespaceRequired.withStack() - } - - if ns, err := svc.loadNamespace(mod.NamespaceID); err != nil { - return nil, err - } else if !svc.ac.CanCreateTrigger(svc.ctx, ns) { - return nil, ErrNoCreatePermissions.withStack() - } - - return svc.triggerRepo.Create(mod) -} - -func (svc trigger) Update(mod *types.Trigger) (c *types.Trigger, err error) { - if mod.ID == 0 { - return nil, ErrInvalidID.withStack() - } - - if c, err = svc.triggerRepo.FindByID(mod.NamespaceID, mod.ID); err != nil { - return - } - - if isStale(mod.UpdatedAt, c.UpdatedAt, c.CreatedAt) { - return nil, ErrStaleData.withStack() - } - - if !svc.ac.CanUpdateTrigger(svc.ctx, c) { - return nil, ErrNoUpdatePermissions.withStack() - } - - c.Name = mod.Name - c.ModuleID = mod.ModuleID - c.Source = mod.Source - c.Actions = mod.Actions - c.Enabled = mod.Enabled - - return svc.triggerRepo.Update(c) -} - -func (svc trigger) DeleteByID(namespaceID, triggerID uint64) error { - if triggerID == 0 { - return ErrInvalidID.withStack() - } - - if namespaceID == 0 { - return ErrNamespaceRequired.withStack() - } - - if c, err := svc.triggerRepo.FindByID(namespaceID, triggerID); err != nil { - return err - } else if !svc.ac.CanDeleteTrigger(svc.ctx, c) { - return ErrNoDeletePermissions.withStack() - } - - return svc.triggerRepo.DeleteByID(namespaceID, triggerID) -} - -func (svc trigger) loadNamespace(namespaceID uint64) (ns *types.Namespace, err error) { - if namespaceID == 0 { - return nil, ErrNamespaceRequired.withStack() - } - - if ns, err = svc.nsRepo.FindByID(namespaceID); err != nil { - return - } - - if !svc.ac.CanReadNamespace(svc.ctx, ns) { - return nil, ErrNoReadPermissions.withStack() - } - - return -} diff --git a/compose/internal/service/trigger_test.go b/compose/internal/service/trigger_test.go deleted file mode 100644 index e2add8ecf..000000000 --- a/compose/internal/service/trigger_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// +build integration - -package service - -import ( - "context" - "testing" - - "github.com/pkg/errors" - - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/internal/auth" - "github.com/cortezaproject/corteza-server/internal/test" -) - -func TestTrigger(t *testing.T) { - ctx := context.WithValue(context.Background(), "testing", true) - - // Set Identity (required for permission checks). - ctx = auth.SetIdentityToContext(ctx, auth.NewIdentity(1337)) - - ns1, _ := createTestNamespaces(ctx, t) - - svc := Trigger().With(ctx) - - // the trigger object we're working with - trigger := &types.Trigger{ - NamespaceID: ns1.ID, - Name: "Test", - ModuleID: 123, - } - - { - { - m, err := svc.Update(trigger) - test.Assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) - test.Assert(t, err != nil, "Expected error when updating invalid content") - } - - // create trigger - m, err := svc.Create(trigger) - test.Assert(t, err == nil, "Error when creating trigger: %+v", err) - test.Assert(t, m.ID > 0, "Expected auto generated ID") - - { - _, err := svc.Create(trigger) - test.Assert(t, err == nil, "Unexpected error when creating trigger, %+v", err) - } - - // fetch created trigger - { - ms, err := svc.FindByID(m.NamespaceID, m.ID) - test.Assert(t, err == nil, "Error when retrieving trigger by id: %+v", err) - test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %+v", errors.Errorf("%d != %d", m.ID, ms.ID)) - test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %+v", errors.Errorf("%s != %s", m.Name, ms.Name)) - } - - // update created trigger - { - m.UpdatedAt = nil - m.Name = "Updated test" - _, err := svc.Update(m) - test.Assert(t, err == nil, "Error when updating trigger, %+v", err) - } - - // re-fetch trigger - { - ms, err := svc.FindByID(m.NamespaceID, m.ID) - test.Assert(t, err == nil, "Error when retrieving trigger by id: %+v", err) - test.Assert(t, ms.ID == m.ID, "re-fetch: Expected ID from database to match, %d != %d", m.ID, ms.ID) - test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name) - } - - // delete trigger - { - err := svc.DeleteByID(m.NamespaceID, m.ID) - test.Assert(t, err == nil, "Error when deleting trigger by id: %+v", err) - } - } - -} diff --git a/compose/proto/to_proto.go b/compose/proto/to_proto.go index 7cb7a3b93..3c3c64f2d 100644 --- a/compose/proto/to_proto.go +++ b/compose/proto/to_proto.go @@ -6,6 +6,7 @@ import ( "github.com/golang/protobuf/ptypes/timestamp" "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/automation" ) type ( @@ -90,16 +91,12 @@ func FromNamespace(i *types.Namespace) *Namespace { return p } -func ScriptFromRunnable(s Runnable) *Script { - if s == nil { - return nil - } - +func FromAutomationScript(s *automation.Script) *Script { return &Script{ - Source: s.GetSource(), - Name: s.GetName(), - Timeout: s.GetTimeout(), - Async: s.IsAsync(), + Source: s.Source, + Name: s.Name, + Timeout: uint32(s.Timeout), + Async: s.Async, } } diff --git a/compose/rest/automation_script.go b/compose/rest/automation_script.go new file mode 100644 index 000000000..29e8a2591 --- /dev/null +++ b/compose/rest/automation_script.go @@ -0,0 +1,142 @@ +package rest + +import ( + "context" + + "github.com/pkg/errors" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/compose/internal/service" + "github.com/cortezaproject/corteza-server/compose/rest/request" + "github.com/cortezaproject/corteza-server/pkg/automation" + "github.com/cortezaproject/corteza-server/pkg/rh" +) + +var _ = errors.Wrap + +type ( + automationScriptPayload struct { + *automation.Script + } + + automationScriptSetPayload struct { + Filter automation.ScriptFilter `json:"filter"` + Set []*automationScriptPayload `json:"set"` + } + + AutomationScript struct { + scripts automationScriptService + } + + automationScriptService interface { + FindByID(context.Context, uint64) (*automation.Script, error) + Find(context.Context, automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) + Create(context.Context, *automation.Script) error + Update(context.Context, *automation.Script) error + Delete(context.Context, *automation.Script) error + } +) + +func (AutomationScript) New() *AutomationScript { + return &AutomationScript{ + scripts: service.DefaultAutomationScriptManager, + } +} + +func (ctrl AutomationScript) List(ctx context.Context, r *request.AutomationScriptList) (interface{}, error) { + set, filter, err := ctrl.scripts.Find(ctx, automation.ScriptFilter{ + // @todo namespace filtering + // Might be a bit tricky as scripts themselves not know about namespaces + // Namespace: r.NamespaceID + + Query: r.Query, + Resource: r.Resource, + + IncDeleted: false, + PageFilter: rh.Paging(r.Page, r.PerPage), + }) + + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl AutomationScript) Create(ctx context.Context, r *request.AutomationScriptCreate) (interface{}, error) { + var ( + script = &automation.Script{ + Name: r.Name, + SourceRef: r.SourceRef, + Source: r.Source, + Async: r.Async, + RunAs: r.RunAs, + RunInUA: r.RunInUA, + Timeout: r.Timeout, + Critical: r.Critical, + Enabled: r.Enabled, + } + ) + + return ctrl.makePayload(ctx, script, ctrl.scripts.Create(ctx, script)) +} + +func (ctrl AutomationScript) Read(ctx context.Context, r *request.AutomationScriptRead) (interface{}, error) { + script, err := ctrl.scripts.FindByID(ctx, r.ScriptID) + return ctrl.makePayload(ctx, script, err) +} + +func (ctrl AutomationScript) Update(ctx context.Context, r *request.AutomationScriptUpdate) (interface{}, error) { + script, err := ctrl.scripts.FindByID(ctx, r.ScriptID) + if err != nil { + return nil, errors.Wrap(err, "can not update script") + } + + script.Name = r.Name + script.SourceRef = r.SourceRef + script.Source = r.Source + script.Async = r.Async + script.RunAs = r.RunAs + script.RunInUA = r.RunInUA + script.Timeout = r.Timeout + script.Critical = r.Critical + script.Enabled = r.Enabled + + return ctrl.makePayload(ctx, script, ctrl.scripts.Update(ctx, script)) +} + +func (ctrl AutomationScript) Delete(ctx context.Context, r *request.AutomationScriptDelete) (interface{}, error) { + script, err := ctrl.scripts.FindByID(ctx, r.ScriptID) + if err != nil { + return nil, errors.Wrap(err, "can not delete script") + } + + return resputil.OK(), ctrl.scripts.Delete(ctx, script) +} + +func (ctrl AutomationScript) makePayload(ctx context.Context, s *automation.Script, err error) (*automationScriptPayload, error) { + if err != nil || s == nil { + return nil, err + } + + return &automationScriptPayload{ + Script: s, + + // CanUpdateModule: ctrl.ac.CanUpdateModule(ctx, s), + // CanDeleteModule: ctrl.ac.CanDeleteModule(ctx, s), + // CanCreateRecord: ctrl.ac.CanCreateRecord(ctx, s), + // CanReadRecord: ctrl.ac.CanReadRecord(ctx, s), + // CanUpdateRecord: ctrl.ac.CanUpdateRecord(ctx, s), + // CanDeleteRecord: ctrl.ac.CanDeleteRecord(ctx, s), + }, nil +} + +func (ctrl AutomationScript) makeFilterPayload(ctx context.Context, nn automation.ScriptSet, f automation.ScriptFilter, err error) (*automationScriptSetPayload, error) { + if err != nil { + return nil, err + } + + modp := &automationScriptSetPayload{Filter: f, Set: make([]*automationScriptPayload, len(nn))} + + for i := range nn { + modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return modp, nil +} diff --git a/compose/rest/automation_trigger.go b/compose/rest/automation_trigger.go new file mode 100644 index 000000000..8bcdf959a --- /dev/null +++ b/compose/rest/automation_trigger.go @@ -0,0 +1,164 @@ +package rest + +import ( + "context" + + "github.com/pkg/errors" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/compose/internal/service" + "github.com/cortezaproject/corteza-server/compose/rest/request" + "github.com/cortezaproject/corteza-server/pkg/automation" + "github.com/cortezaproject/corteza-server/pkg/rh" +) + +var _ = errors.Wrap + +type ( + automationTriggerPayload struct { + *automation.Trigger + } + + automationTriggerSetPayload struct { + Filter automation.TriggerFilter `json:"filter"` + Set []*automationTriggerPayload `json:"set"` + } + + AutomationTrigger struct { + triggers automationTriggerService + scripts automationScriptFinderService + } + + automationTriggerService interface { + FindByID(context.Context, uint64) (*automation.Trigger, error) + Find(context.Context, automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error) + Create(context.Context, *automation.Script, *automation.Trigger) error + Update(context.Context, *automation.Script, *automation.Trigger) error + Delete(context.Context, *automation.Trigger) error + } + + automationScriptFinderService interface { + FindByID(context.Context, uint64) (*automation.Script, error) + } +) + +func (AutomationTrigger) New() *AutomationTrigger { + return &AutomationTrigger{ + scripts: service.DefaultAutomationScriptManager, + triggers: service.DefaultAutomationTriggerManager, + } +} + +func (ctrl AutomationTrigger) List(ctx context.Context, r *request.AutomationTriggerList) (interface{}, error) { + set, filter, err := ctrl.triggers.Find(ctx, automation.TriggerFilter{ + // @todo namespace filtering + // Might be a bit tricky as triggers themselves not know about namespaces + // Namespace: r.NamespaceID + + Resource: r.Resource, + Event: r.Event, + ScriptID: r.ScriptID, + + IncDeleted: false, + PageFilter: rh.Paging(r.Page, r.PerPage), + }) + + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl AutomationTrigger) Create(ctx context.Context, r *request.AutomationTriggerCreate) (interface{}, error) { + s, _, err := ctrl.loadCombo(ctx, r.ScriptID, 0) + if err != nil { + return nil, errors.Wrap(err, "can not create trigger") + } + + var ( + t = &automation.Trigger{ + Event: r.Event, + Resource: r.Resource, + Condition: r.Condition, + ScriptID: s.ID, + Enabled: r.Enabled, + } + ) + + return ctrl.makePayload(ctx, t, ctrl.triggers.Create(ctx, s, t)) +} + +func (ctrl AutomationTrigger) Read(ctx context.Context, r *request.AutomationTriggerRead) (interface{}, error) { + _, t, err := ctrl.loadCombo(ctx, r.ScriptID, 0) + if err != nil { + return nil, errors.Wrap(err, "can not read trigger") + } + + return ctrl.makePayload(ctx, t, err) +} + +func (ctrl AutomationTrigger) Update(ctx context.Context, r *request.AutomationTriggerUpdate) (interface{}, error) { + s, t, err := ctrl.loadCombo(ctx, r.ScriptID, r.TriggerID) + if err != nil { + return nil, errors.Wrap(err, "can not update trigger") + } + + t.Event = r.Event + t.Resource = r.Resource + t.Condition = r.Condition + t.ScriptID = r.ScriptID + t.Enabled = r.Enabled + + return ctrl.makePayload(ctx, t, ctrl.triggers.Update(ctx, s, t)) +} + +func (ctrl AutomationTrigger) Delete(ctx context.Context, r *request.AutomationTriggerDelete) (interface{}, error) { + trigger, err := ctrl.triggers.FindByID(ctx, r.TriggerID) + if err != nil { + return nil, errors.Wrap(err, "can not delete trigger") + } + + return resputil.OK(), ctrl.triggers.Delete(ctx, trigger) +} + +func (ctrl AutomationTrigger) loadCombo(ctx context.Context, scriptID, triggerID uint64) (s *automation.Script, t *automation.Trigger, err error) { + if triggerID > 0 { + t, err = ctrl.triggers.FindByID(ctx, triggerID) + return + } + + if scriptID > 0 { + s, err = ctrl.scripts.FindByID(ctx, scriptID) + return + } + + return +} + +func (ctrl AutomationTrigger) makePayload(ctx context.Context, s *automation.Trigger, err error) (*automationTriggerPayload, error) { + if err != nil || s == nil { + return nil, err + } + + return &automationTriggerPayload{ + Trigger: s, + + // CanUpdateModule: ctrl.ac.CanUpdateModule(ctx, s), + // CanDeleteModule: ctrl.ac.CanDeleteModule(ctx, s), + // CanCreateRecord: ctrl.ac.CanCreateRecord(ctx, s), + // CanReadRecord: ctrl.ac.CanReadRecord(ctx, s), + // CanUpdateRecord: ctrl.ac.CanUpdateRecord(ctx, s), + // CanDeleteRecord: ctrl.ac.CanDeleteRecord(ctx, s), + }, nil +} + +func (ctrl AutomationTrigger) makeFilterPayload(ctx context.Context, nn automation.TriggerSet, f automation.TriggerFilter, err error) (*automationTriggerSetPayload, error) { + if err != nil { + return nil, err + } + + modp := &automationTriggerSetPayload{Filter: f, Set: make([]*automationTriggerPayload, len(nn))} + + for i := range nn { + modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return modp, nil +} diff --git a/compose/rest/handlers/trigger.go b/compose/rest/handlers/automation_script.go similarity index 53% rename from compose/rest/handlers/trigger.go rename to compose/rest/handlers/automation_script.go index d56fbe2bd..7085b7101 100644 --- a/compose/rest/handlers/trigger.go +++ b/compose/rest/handlers/automation_script.go @@ -10,8 +10,8 @@ package handlers 1. run [spec](https://github.com/titpetric/spec) in the same folder, 2. run `./_gen.php` in this folder. - You may edit `trigger.go`, `trigger.util.go` or `trigger_test.go` to - implement your API calls, helper functions and tests. The file `trigger.go` + You may edit `automation_script.go`, `automation_script.util.go` or `automation_script_test.go` to + implement your API calls, helper functions and tests. The file `automation_script.go` is only generated the first time, and will not be overwritten if it exists. */ @@ -28,16 +28,16 @@ import ( ) // Internal API interface -type TriggerAPI interface { - List(context.Context, *request.TriggerList) (interface{}, error) - Create(context.Context, *request.TriggerCreate) (interface{}, error) - Read(context.Context, *request.TriggerRead) (interface{}, error) - Update(context.Context, *request.TriggerUpdate) (interface{}, error) - Delete(context.Context, *request.TriggerDelete) (interface{}, error) +type AutomationScriptAPI interface { + List(context.Context, *request.AutomationScriptList) (interface{}, error) + Create(context.Context, *request.AutomationScriptCreate) (interface{}, error) + Read(context.Context, *request.AutomationScriptRead) (interface{}, error) + Update(context.Context, *request.AutomationScriptUpdate) (interface{}, error) + Delete(context.Context, *request.AutomationScriptDelete) (interface{}, error) } // HTTP API interface -type Trigger struct { +type AutomationScript struct { List func(http.ResponseWriter, *http.Request) Create func(http.ResponseWriter, *http.Request) Read func(http.ResponseWriter, *http.Request) @@ -45,104 +45,104 @@ type Trigger struct { Delete func(http.ResponseWriter, *http.Request) } -func NewTrigger(h TriggerAPI) *Trigger { - return &Trigger{ +func NewAutomationScript(h AutomationScriptAPI) *AutomationScript { + return &AutomationScript{ List: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewTriggerList() + params := request.NewAutomationScriptList() if err := params.Fill(r); err != nil { - logger.LogParamError("Trigger.List", r, err) + logger.LogParamError("AutomationScript.List", r, err) resputil.JSON(w, err) return } value, err := h.List(r.Context(), params) if err != nil { - logger.LogControllerError("Trigger.List", r, err, params.Auditable()) + logger.LogControllerError("AutomationScript.List", r, err, params.Auditable()) resputil.JSON(w, err) return } - logger.LogControllerCall("Trigger.List", r, params.Auditable()) + logger.LogControllerCall("AutomationScript.List", r, params.Auditable()) if !serveHTTP(value, w, r) { resputil.JSON(w, value) } }, Create: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewTriggerCreate() + params := request.NewAutomationScriptCreate() if err := params.Fill(r); err != nil { - logger.LogParamError("Trigger.Create", r, err) + logger.LogParamError("AutomationScript.Create", r, err) resputil.JSON(w, err) return } value, err := h.Create(r.Context(), params) if err != nil { - logger.LogControllerError("Trigger.Create", r, err, params.Auditable()) + logger.LogControllerError("AutomationScript.Create", r, err, params.Auditable()) resputil.JSON(w, err) return } - logger.LogControllerCall("Trigger.Create", r, params.Auditable()) + logger.LogControllerCall("AutomationScript.Create", r, params.Auditable()) if !serveHTTP(value, w, r) { resputil.JSON(w, value) } }, Read: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewTriggerRead() + params := request.NewAutomationScriptRead() if err := params.Fill(r); err != nil { - logger.LogParamError("Trigger.Read", r, err) + logger.LogParamError("AutomationScript.Read", r, err) resputil.JSON(w, err) return } value, err := h.Read(r.Context(), params) if err != nil { - logger.LogControllerError("Trigger.Read", r, err, params.Auditable()) + logger.LogControllerError("AutomationScript.Read", r, err, params.Auditable()) resputil.JSON(w, err) return } - logger.LogControllerCall("Trigger.Read", r, params.Auditable()) + logger.LogControllerCall("AutomationScript.Read", r, params.Auditable()) if !serveHTTP(value, w, r) { resputil.JSON(w, value) } }, Update: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewTriggerUpdate() + params := request.NewAutomationScriptUpdate() if err := params.Fill(r); err != nil { - logger.LogParamError("Trigger.Update", r, err) + logger.LogParamError("AutomationScript.Update", r, err) resputil.JSON(w, err) return } value, err := h.Update(r.Context(), params) if err != nil { - logger.LogControllerError("Trigger.Update", r, err, params.Auditable()) + logger.LogControllerError("AutomationScript.Update", r, err, params.Auditable()) resputil.JSON(w, err) return } - logger.LogControllerCall("Trigger.Update", r, params.Auditable()) + logger.LogControllerCall("AutomationScript.Update", r, params.Auditable()) if !serveHTTP(value, w, r) { resputil.JSON(w, value) } }, Delete: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewTriggerDelete() + params := request.NewAutomationScriptDelete() if err := params.Fill(r); err != nil { - logger.LogParamError("Trigger.Delete", r, err) + logger.LogParamError("AutomationScript.Delete", r, err) resputil.JSON(w, err) return } value, err := h.Delete(r.Context(), params) if err != nil { - logger.LogControllerError("Trigger.Delete", r, err, params.Auditable()) + logger.LogControllerError("AutomationScript.Delete", r, err, params.Auditable()) resputil.JSON(w, err) return } - logger.LogControllerCall("Trigger.Delete", r, params.Auditable()) + logger.LogControllerCall("AutomationScript.Delete", r, params.Auditable()) if !serveHTTP(value, w, r) { resputil.JSON(w, value) } @@ -150,13 +150,13 @@ func NewTrigger(h TriggerAPI) *Trigger { } } -func (h Trigger) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { +func (h AutomationScript) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { r.Group(func(r chi.Router) { r.Use(middlewares...) - r.Get("/namespace/{namespaceID}/trigger/", h.List) - r.Post("/namespace/{namespaceID}/trigger/", h.Create) - r.Get("/namespace/{namespaceID}/trigger/{triggerID}", h.Read) - r.Post("/namespace/{namespaceID}/trigger/{triggerID}", h.Update) - r.Delete("/namespace/{namespaceID}/trigger/{triggerID}", h.Delete) + r.Get("/namespace/{namespaceID}/automation/script/", h.List) + r.Post("/namespace/{namespaceID}/automation/script/", h.Create) + r.Get("/namespace/{namespaceID}/automation/script/{scriptID}", h.Read) + r.Post("/namespace/{namespaceID}/automation/script/{scriptID}", h.Update) + r.Delete("/namespace/{namespaceID}/automation/script/{scriptID}", h.Delete) }) } diff --git a/compose/rest/handlers/automation_trigger.go b/compose/rest/handlers/automation_trigger.go new file mode 100644 index 000000000..a6956d99f --- /dev/null +++ b/compose/rest/handlers/automation_trigger.go @@ -0,0 +1,162 @@ +package handlers + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `automation_trigger.go`, `automation_trigger.util.go` or `automation_trigger_test.go` to + implement your API calls, helper functions and tests. The file `automation_trigger.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + + "net/http" + + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/compose/rest/request" + "github.com/cortezaproject/corteza-server/pkg/logger" +) + +// Internal API interface +type AutomationTriggerAPI interface { + List(context.Context, *request.AutomationTriggerList) (interface{}, error) + Create(context.Context, *request.AutomationTriggerCreate) (interface{}, error) + Read(context.Context, *request.AutomationTriggerRead) (interface{}, error) + Update(context.Context, *request.AutomationTriggerUpdate) (interface{}, error) + Delete(context.Context, *request.AutomationTriggerDelete) (interface{}, error) +} + +// HTTP API interface +type AutomationTrigger struct { + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) +} + +func NewAutomationTrigger(h AutomationTriggerAPI) *AutomationTrigger { + return &AutomationTrigger{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerList() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.List", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.List(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.List", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.List", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerCreate() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.Create", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Create(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.Create", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.Create", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Read: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerRead() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.Read", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Read(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.Read", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.Read", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Update: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerUpdate() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.Update", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Update(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.Update", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.Update", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Delete: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerDelete() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.Delete", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Delete(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.Delete", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.Delete", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h AutomationTrigger) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/", h.List) + r.Post("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/", h.Create) + r.Get("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}", h.Read) + r.Post("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}", h.Update) + r.Delete("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}", h.Delete) + }) +} diff --git a/compose/rest/handlers/record.go b/compose/rest/handlers/record.go index 46a2a6e13..e311ca9ad 100644 --- a/compose/rest/handlers/record.go +++ b/compose/rest/handlers/record.go @@ -36,19 +36,21 @@ type RecordAPI interface { Read(context.Context, *request.RecordRead) (interface{}, error) Update(context.Context, *request.RecordUpdate) (interface{}, error) Delete(context.Context, *request.RecordDelete) (interface{}, error) + RunScript(context.Context, *request.RecordRunScript) (interface{}, error) Upload(context.Context, *request.RecordUpload) (interface{}, error) } // HTTP API interface type Record struct { - Report func(http.ResponseWriter, *http.Request) - List func(http.ResponseWriter, *http.Request) - Export func(http.ResponseWriter, *http.Request) - Create func(http.ResponseWriter, *http.Request) - Read func(http.ResponseWriter, *http.Request) - Update func(http.ResponseWriter, *http.Request) - Delete func(http.ResponseWriter, *http.Request) - Upload func(http.ResponseWriter, *http.Request) + Report func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Export func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) + RunScript func(http.ResponseWriter, *http.Request) + Upload func(http.ResponseWriter, *http.Request) } func NewRecord(h RecordAPI) *Record { @@ -193,6 +195,26 @@ func NewRecord(h RecordAPI) *Record { resputil.JSON(w, value) } }, + RunScript: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewRecordRunScript() + if err := params.Fill(r); err != nil { + logger.LogParamError("Record.RunScript", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.RunScript(r.Context(), params) + if err != nil { + logger.LogControllerError("Record.RunScript", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Record.RunScript", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, Upload: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewRecordUpload() @@ -226,6 +248,7 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Read) r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Update) r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Delete) + r.Post("/namespace/{namespaceID}/module/{moduleID}/record/run-script", h.RunScript) r.Post("/namespace/{namespaceID}/module/{moduleID}/record/attachment", h.Upload) }) } diff --git a/compose/rest/record.go b/compose/rest/record.go index b4a59c588..c892b8f1e 100644 --- a/compose/rest/record.go +++ b/compose/rest/record.go @@ -229,6 +229,15 @@ func (ctrl *Record) Export(ctx context.Context, r *request.RecordExport) (interf }, nil } +func (ctrl *Record) RunScript(ctx context.Context, r *request.RecordRunScript) (interface{}, error) { + return resputil.OK(), ctrl.record.RunScript( + r.NamespaceID, + r.ModuleID, + r.RecordID, + r.ScriptID, + ) +} + func (ctrl Record) makePayload(ctx context.Context, m *types.Module, r *types.Record, err error) (*recordPayload, error) { if err != nil || r == nil { return nil, err diff --git a/compose/rest/request/automation_script.go b/compose/rest/request/automation_script.go new file mode 100644 index 000000000..bea3e7059 --- /dev/null +++ b/compose/rest/request/automation_script.go @@ -0,0 +1,410 @@ +package request + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `automation_script.go`, `automation_script.util.go` or `automation_script_test.go` to + implement your API calls, helper functions and tests. The file `automation_script.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "io" + "strings" + + "encoding/json" + "mime/multipart" + "net/http" + + "github.com/go-chi/chi" + "github.com/pkg/errors" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// AutomationScript list request parameters +type AutomationScriptList struct { + Query string + Resource string + IncDeleted bool + Page uint + PerPage uint + NamespaceID uint64 `json:",string"` +} + +func NewAutomationScriptList() *AutomationScriptList { + return &AutomationScriptList{} +} + +func (r AutomationScriptList) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["query"] = r.Query + out["resource"] = r.Resource + out["incDeleted"] = r.IncDeleted + out["page"] = r.Page + out["perPage"] = r.PerPage + out["namespaceID"] = r.NamespaceID + + return out +} + +func (r *AutomationScriptList) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["query"]; ok { + r.Query = val + } + if val, ok := get["resource"]; ok { + r.Resource = val + } + if val, ok := get["incDeleted"]; ok { + r.IncDeleted = parseBool(val) + } + if val, ok := get["page"]; ok { + r.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + r.PerPage = parseUint(val) + } + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + + return err +} + +var _ RequestFiller = NewAutomationScriptList() + +// AutomationScript create request parameters +type AutomationScriptCreate struct { + Name string + SourceRef string + Source string + RunAs uint64 `json:",string"` + RunInUA bool + Timeout uint + Critical bool + Async bool + Enabled bool + NamespaceID uint64 `json:",string"` +} + +func NewAutomationScriptCreate() *AutomationScriptCreate { + return &AutomationScriptCreate{} +} + +func (r AutomationScriptCreate) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["name"] = r.Name + out["sourceRef"] = r.SourceRef + out["source"] = r.Source + out["runAs"] = r.RunAs + out["runInUA"] = r.RunInUA + out["timeout"] = r.Timeout + out["critical"] = r.Critical + out["async"] = r.Async + out["enabled"] = r.Enabled + out["namespaceID"] = r.NamespaceID + + return out +} + +func (r *AutomationScriptCreate) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["name"]; ok { + r.Name = val + } + if val, ok := post["sourceRef"]; ok { + r.SourceRef = val + } + if val, ok := post["source"]; ok { + r.Source = val + } + if val, ok := post["runAs"]; ok { + r.RunAs = parseUInt64(val) + } + if val, ok := post["runInUA"]; ok { + r.RunInUA = parseBool(val) + } + if val, ok := post["timeout"]; ok { + r.Timeout = parseUint(val) + } + if val, ok := post["critical"]; ok { + r.Critical = parseBool(val) + } + if val, ok := post["async"]; ok { + r.Async = parseBool(val) + } + if val, ok := post["enabled"]; ok { + r.Enabled = parseBool(val) + } + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + + return err +} + +var _ RequestFiller = NewAutomationScriptCreate() + +// AutomationScript read request parameters +type AutomationScriptRead struct { + ScriptID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` +} + +func NewAutomationScriptRead() *AutomationScriptRead { + return &AutomationScriptRead{} +} + +func (r AutomationScriptRead) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["scriptID"] = r.ScriptID + out["namespaceID"] = r.NamespaceID + + return out +} + +func (r *AutomationScriptRead) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + + return err +} + +var _ RequestFiller = NewAutomationScriptRead() + +// AutomationScript update request parameters +type AutomationScriptUpdate struct { + ScriptID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + Name string + SourceRef string + Source string + RunAs uint64 `json:",string"` + RunInUA bool + Timeout uint + Critical bool + Async bool + Enabled bool +} + +func NewAutomationScriptUpdate() *AutomationScriptUpdate { + return &AutomationScriptUpdate{} +} + +func (r AutomationScriptUpdate) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["scriptID"] = r.ScriptID + out["namespaceID"] = r.NamespaceID + out["name"] = r.Name + out["sourceRef"] = r.SourceRef + out["source"] = r.Source + out["runAs"] = r.RunAs + out["runInUA"] = r.RunInUA + out["timeout"] = r.Timeout + out["critical"] = r.Critical + out["async"] = r.Async + out["enabled"] = r.Enabled + + return out +} + +func (r *AutomationScriptUpdate) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + if val, ok := post["name"]; ok { + r.Name = val + } + if val, ok := post["sourceRef"]; ok { + r.SourceRef = val + } + if val, ok := post["source"]; ok { + r.Source = val + } + if val, ok := post["runAs"]; ok { + r.RunAs = parseUInt64(val) + } + if val, ok := post["runInUA"]; ok { + r.RunInUA = parseBool(val) + } + if val, ok := post["timeout"]; ok { + r.Timeout = parseUint(val) + } + if val, ok := post["critical"]; ok { + r.Critical = parseBool(val) + } + if val, ok := post["async"]; ok { + r.Async = parseBool(val) + } + if val, ok := post["enabled"]; ok { + r.Enabled = parseBool(val) + } + + return err +} + +var _ RequestFiller = NewAutomationScriptUpdate() + +// AutomationScript delete request parameters +type AutomationScriptDelete struct { + ScriptID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` +} + +func NewAutomationScriptDelete() *AutomationScriptDelete { + return &AutomationScriptDelete{} +} + +func (r AutomationScriptDelete) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["scriptID"] = r.ScriptID + out["namespaceID"] = r.NamespaceID + + return out +} + +func (r *AutomationScriptDelete) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + + return err +} + +var _ RequestFiller = NewAutomationScriptDelete() diff --git a/compose/rest/request/trigger.go b/compose/rest/request/automation_trigger.go similarity index 61% rename from compose/rest/request/trigger.go rename to compose/rest/request/automation_trigger.go index b1c2e87b3..9d27d904f 100644 --- a/compose/rest/request/trigger.go +++ b/compose/rest/request/automation_trigger.go @@ -10,8 +10,8 @@ package request 1. run [spec](https://github.com/titpetric/spec) in the same folder, 2. run `./_gen.php` in this folder. - You may edit `trigger.go`, `trigger.util.go` or `trigger_test.go` to - implement your API calls, helper functions and tests. The file `trigger.go` + You may edit `automation_trigger.go`, `automation_trigger.util.go` or `automation_trigger_test.go` to + implement your API calls, helper functions and tests. The file `automation_trigger.go` is only generated the first time, and will not be overwritten if it exists. */ @@ -25,39 +25,41 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" - - "time" ) var _ = chi.URLParam var _ = multipart.FileHeader{} -// Trigger list request parameters -type TriggerList struct { - ModuleID uint64 `json:",string"` - Query string +// AutomationTrigger list request parameters +type AutomationTriggerList struct { + Resource string + Event string + IncDeleted bool Page uint PerPage uint NamespaceID uint64 `json:",string"` + ScriptID uint64 `json:",string"` } -func NewTriggerList() *TriggerList { - return &TriggerList{} +func NewAutomationTriggerList() *AutomationTriggerList { + return &AutomationTriggerList{} } -func (r TriggerList) Auditable() map[string]interface{} { +func (r AutomationTriggerList) Auditable() map[string]interface{} { var out = map[string]interface{}{} - out["moduleID"] = r.ModuleID - out["query"] = r.Query + out["resource"] = r.Resource + out["event"] = r.Event + out["incDeleted"] = r.IncDeleted out["page"] = r.Page out["perPage"] = r.PerPage out["namespaceID"] = r.NamespaceID + out["scriptID"] = r.ScriptID return out } -func (r *TriggerList) Fill(req *http.Request) (err error) { +func (r *AutomationTriggerList) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { err = json.NewDecoder(req.Body).Decode(r) @@ -84,11 +86,14 @@ func (r *TriggerList) Fill(req *http.Request) (err error) { post[name] = string(param[0]) } - if val, ok := get["moduleID"]; ok { - r.ModuleID = parseUInt64(val) + if val, ok := get["resource"]; ok { + r.Resource = val } - if val, ok := get["query"]; ok { - r.Query = val + if val, ok := get["event"]; ok { + r.Event = val + } + if val, ok := get["incDeleted"]; ok { + r.IncDeleted = parseBool(val) } if val, ok := get["page"]; ok { r.Page = parseUint(val) @@ -97,42 +102,43 @@ func (r *TriggerList) Fill(req *http.Request) (err error) { r.PerPage = parseUint(val) } r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) return err } -var _ RequestFiller = NewTriggerList() +var _ RequestFiller = NewAutomationTriggerList() -// Trigger create request parameters -type TriggerCreate struct { - ModuleID uint64 `json:",string"` +// AutomationTrigger create request parameters +type AutomationTriggerCreate struct { Name string - Actions []string + Resource string + Event string + Condition string Enabled bool - Source string - UpdatedAt *time.Time NamespaceID uint64 `json:",string"` + ScriptID uint64 `json:",string"` } -func NewTriggerCreate() *TriggerCreate { - return &TriggerCreate{} +func NewAutomationTriggerCreate() *AutomationTriggerCreate { + return &AutomationTriggerCreate{} } -func (r TriggerCreate) Auditable() map[string]interface{} { +func (r AutomationTriggerCreate) Auditable() map[string]interface{} { var out = map[string]interface{}{} - out["moduleID"] = r.ModuleID out["name"] = r.Name - out["actions"] = r.Actions + out["resource"] = r.Resource + out["event"] = r.Event + out["condition"] = r.Condition out["enabled"] = r.Enabled - out["source"] = r.Source - out["updatedAt"] = r.UpdatedAt out["namespaceID"] = r.NamespaceID + out["scriptID"] = r.ScriptID return out } -func (r *TriggerCreate) Fill(req *http.Request) (err error) { +func (r *AutomationTriggerCreate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { err = json.NewDecoder(req.Body).Decode(r) @@ -159,56 +165,51 @@ func (r *TriggerCreate) Fill(req *http.Request) (err error) { post[name] = string(param[0]) } - if val, ok := post["moduleID"]; ok { - r.ModuleID = parseUInt64(val) - } if val, ok := post["name"]; ok { r.Name = val } - - if val, ok := req.Form["actions"]; ok { - r.Actions = parseStrings(val) + if val, ok := post["resource"]; ok { + r.Resource = val + } + if val, ok := post["event"]; ok { + r.Event = val + } + if val, ok := post["condition"]; ok { + r.Condition = val } - if val, ok := post["enabled"]; ok { r.Enabled = parseBool(val) } - if val, ok := post["source"]; ok { - r.Source = val - } - if val, ok := post["updatedAt"]; ok { - - if r.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil { - return err - } - } r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) return err } -var _ RequestFiller = NewTriggerCreate() +var _ RequestFiller = NewAutomationTriggerCreate() -// Trigger read request parameters -type TriggerRead struct { +// AutomationTrigger read request parameters +type AutomationTriggerRead struct { TriggerID uint64 `json:",string"` NamespaceID uint64 `json:",string"` + ScriptID uint64 `json:",string"` } -func NewTriggerRead() *TriggerRead { - return &TriggerRead{} +func NewAutomationTriggerRead() *AutomationTriggerRead { + return &AutomationTriggerRead{} } -func (r TriggerRead) Auditable() map[string]interface{} { +func (r AutomationTriggerRead) Auditable() map[string]interface{} { var out = map[string]interface{}{} out["triggerID"] = r.TriggerID out["namespaceID"] = r.NamespaceID + out["scriptID"] = r.ScriptID return out } -func (r *TriggerRead) Fill(req *http.Request) (err error) { +func (r *AutomationTriggerRead) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { err = json.NewDecoder(req.Body).Decode(r) @@ -237,42 +238,45 @@ func (r *TriggerRead) Fill(req *http.Request) (err error) { r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID")) r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) return err } -var _ RequestFiller = NewTriggerRead() +var _ RequestFiller = NewAutomationTriggerRead() -// Trigger update request parameters -type TriggerUpdate struct { +// AutomationTrigger update request parameters +type AutomationTriggerUpdate struct { TriggerID uint64 `json:",string"` NamespaceID uint64 `json:",string"` - ModuleID uint64 `json:",string"` + ScriptID uint64 `json:",string"` Name string - Actions []string + Resource string + Event string + Condition string Enabled bool - Source string } -func NewTriggerUpdate() *TriggerUpdate { - return &TriggerUpdate{} +func NewAutomationTriggerUpdate() *AutomationTriggerUpdate { + return &AutomationTriggerUpdate{} } -func (r TriggerUpdate) Auditable() map[string]interface{} { +func (r AutomationTriggerUpdate) Auditable() map[string]interface{} { var out = map[string]interface{}{} out["triggerID"] = r.TriggerID out["namespaceID"] = r.NamespaceID - out["moduleID"] = r.ModuleID + out["scriptID"] = r.ScriptID out["name"] = r.Name - out["actions"] = r.Actions + out["resource"] = r.Resource + out["event"] = r.Event + out["condition"] = r.Condition out["enabled"] = r.Enabled - out["source"] = r.Source return out } -func (r *TriggerUpdate) Fill(req *http.Request) (err error) { +func (r *AutomationTriggerUpdate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { err = json.NewDecoder(req.Body).Decode(r) @@ -301,49 +305,50 @@ func (r *TriggerUpdate) Fill(req *http.Request) (err error) { r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID")) r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) - if val, ok := post["moduleID"]; ok { - r.ModuleID = parseUInt64(val) - } + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) if val, ok := post["name"]; ok { r.Name = val } - - if val, ok := req.Form["actions"]; ok { - r.Actions = parseStrings(val) + if val, ok := post["resource"]; ok { + r.Resource = val + } + if val, ok := post["event"]; ok { + r.Event = val + } + if val, ok := post["condition"]; ok { + r.Condition = val } - if val, ok := post["enabled"]; ok { r.Enabled = parseBool(val) } - if val, ok := post["source"]; ok { - r.Source = val - } return err } -var _ RequestFiller = NewTriggerUpdate() +var _ RequestFiller = NewAutomationTriggerUpdate() -// Trigger delete request parameters -type TriggerDelete struct { +// AutomationTrigger delete request parameters +type AutomationTriggerDelete struct { TriggerID uint64 `json:",string"` NamespaceID uint64 `json:",string"` + ScriptID uint64 `json:",string"` } -func NewTriggerDelete() *TriggerDelete { - return &TriggerDelete{} +func NewAutomationTriggerDelete() *AutomationTriggerDelete { + return &AutomationTriggerDelete{} } -func (r TriggerDelete) Auditable() map[string]interface{} { +func (r AutomationTriggerDelete) Auditable() map[string]interface{} { var out = map[string]interface{}{} out["triggerID"] = r.TriggerID out["namespaceID"] = r.NamespaceID + out["scriptID"] = r.ScriptID return out } -func (r *TriggerDelete) Fill(req *http.Request) (err error) { +func (r *AutomationTriggerDelete) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { err = json.NewDecoder(req.Body).Decode(r) @@ -372,8 +377,9 @@ func (r *TriggerDelete) Fill(req *http.Request) (err error) { r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID")) r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) return err } -var _ RequestFiller = NewTriggerDelete() +var _ RequestFiller = NewAutomationTriggerDelete() diff --git a/compose/rest/request/record.go b/compose/rest/request/record.go index 8cc5b9d77..81e91ec0b 100644 --- a/compose/rest/request/record.go +++ b/compose/rest/request/record.go @@ -478,6 +478,70 @@ func (r *RecordDelete) Fill(req *http.Request) (err error) { var _ RequestFiller = NewRecordDelete() +// Record runScript request parameters +type RecordRunScript struct { + RecordID uint64 `json:",string"` + ScriptID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` +} + +func NewRecordRunScript() *RecordRunScript { + return &RecordRunScript{} +} + +func (r RecordRunScript) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["recordID"] = r.RecordID + out["scriptID"] = r.ScriptID + out["namespaceID"] = r.NamespaceID + out["moduleID"] = r.ModuleID + + return out +} + +func (r *RecordRunScript) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["recordID"]; ok { + r.RecordID = parseUInt64(val) + } + if val, ok := post["scriptID"]; ok { + r.ScriptID = parseUInt64(val) + } + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID")) + + return err +} + +var _ RequestFiller = NewRecordRunScript() + // Record upload request parameters type RecordUpload struct { RecordID uint64 `json:",string"` diff --git a/compose/rest/router.go b/compose/rest/router.go index 58acef364..440e8154b 100644 --- a/compose/rest/router.go +++ b/compose/rest/router.go @@ -14,9 +14,11 @@ func MountRoutes(r chi.Router) { record = Record{}.New() page = Page{}.New() chart = Chart{}.New() - trigger = Trigger{}.New() notification = Notification{}.New() attachment = Attachment{}.New() + + automationScript = AutomationScript{}.New() + automationTrgger = AutomationTrigger{}.New() ) // Initialize handlers & controllers. @@ -34,8 +36,10 @@ func MountRoutes(r chi.Router) { handlers.NewModule(module).MountRoutes(r) handlers.NewRecord(record).MountRoutes(r) handlers.NewChart(chart).MountRoutes(r) - handlers.NewTrigger(trigger).MountRoutes(r) handlers.NewNotification(notification).MountRoutes(r) + + handlers.NewAutomationScript(automationScript).MountRoutes(r) + handlers.NewAutomationTrigger(automationTrgger).MountRoutes(r) }) // Use alternative handlers that support file serving diff --git a/compose/rest/trigger.go b/compose/rest/trigger.go deleted file mode 100644 index c1e37395c..000000000 --- a/compose/rest/trigger.go +++ /dev/null @@ -1,135 +0,0 @@ -package rest - -import ( - "context" - - "github.com/titpetric/factory/resputil" - - "github.com/cortezaproject/corteza-server/compose/internal/service" - "github.com/cortezaproject/corteza-server/compose/rest/request" - "github.com/cortezaproject/corteza-server/compose/types" -) - -type ( - triggerPayload struct { - *types.Trigger - - CanGrant bool `json:"canGrant"` - CanUpdateTrigger bool `json:"canUpdateTrigger"` - CanDeleteTrigger bool `json:"canDeleteTrigger"` - } - - triggerSetPayload struct { - Filter types.TriggerFilter `json:"filter"` - Set []*triggerPayload `json:"set"` - } - - Trigger struct { - trigger service.TriggerService - ac triggerAccessController - } - - triggerAccessController interface { - CanGrant(context.Context) bool - - CanUpdateTrigger(context.Context, *types.Trigger) bool - CanDeleteTrigger(context.Context, *types.Trigger) bool - } -) - -func (Trigger) New() *Trigger { - return &Trigger{ - trigger: service.DefaultTrigger, - ac: service.DefaultAccessControl, - } -} - -func (ctrl Trigger) List(ctx context.Context, r *request.TriggerList) (interface{}, error) { - f := types.TriggerFilter{ - NamespaceID: r.NamespaceID, - Query: r.Query, - PerPage: r.PerPage, - Page: r.Page, - } - - set, filter, err := ctrl.trigger.With(ctx).Find(f) - return ctrl.makeFilterPayload(ctx, set, filter, err) -} - -func (ctrl Trigger) Create(ctx context.Context, r *request.TriggerCreate) (interface{}, error) { - var ( - err error - ns = &types.Trigger{ - NamespaceID: r.NamespaceID, - ModuleID: r.ModuleID, - Name: r.Name, - Actions: r.Actions, - Enabled: r.Enabled, - Source: r.Source, - } - ) - - ns, err = ctrl.trigger.With(ctx).Create(ns) - return ctrl.makePayload(ctx, ns, err) -} - -func (ctrl Trigger) Read(ctx context.Context, r *request.TriggerRead) (interface{}, error) { - mod, err := ctrl.trigger.With(ctx).FindByID(r.NamespaceID, r.TriggerID) - return ctrl.makePayload(ctx, mod, err) -} - -func (ctrl Trigger) Update(ctx context.Context, r *request.TriggerUpdate) (interface{}, error) { - var ( - mod = &types.Trigger{} - err error - ) - - mod.ID = r.TriggerID - mod.NamespaceID = r.NamespaceID - mod.ModuleID = r.ModuleID - mod.Name = r.Name - mod.Actions = r.Actions - mod.Enabled = r.Enabled - mod.Source = r.Source - - mod, err = ctrl.trigger.With(ctx).Update(mod) - return ctrl.makePayload(ctx, mod, err) -} - -func (ctrl Trigger) Delete(ctx context.Context, r *request.TriggerDelete) (interface{}, error) { - _, err := ctrl.trigger.With(ctx).FindByID(r.NamespaceID, r.TriggerID) - if err != nil { - return nil, err - } - - return resputil.OK(), ctrl.trigger.With(ctx).DeleteByID(r.NamespaceID, r.TriggerID) -} - -func (ctrl Trigger) makePayload(ctx context.Context, t *types.Trigger, err error) (*triggerPayload, error) { - if err != nil || t == nil { - return nil, err - } - - return &triggerPayload{ - Trigger: t, - - CanGrant: ctrl.ac.CanGrant(ctx), - - CanUpdateTrigger: ctrl.ac.CanUpdateTrigger(ctx, t), - CanDeleteTrigger: ctrl.ac.CanDeleteTrigger(ctx, t), - }, nil -} - -func (ctrl Trigger) makeFilterPayload(ctx context.Context, nn types.TriggerSet, f types.TriggerFilter, err error) (*triggerSetPayload, error) { - if err != nil { - return nil, err - } - - nsp := &triggerSetPayload{Filter: f, Set: make([]*triggerPayload, len(nn))} - - for i := range nn { - nsp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) - } - - return nsp, nil -} diff --git a/compose/types/trigger.go b/compose/types/trigger.go index 2794c5163..c375d4bdf 100644 --- a/compose/types/trigger.go +++ b/compose/types/trigger.go @@ -11,30 +11,43 @@ import ( type ( ActionSet []string - Trigger struct { + + Trigger struct { ID uint64 `json:"triggerID,string" db:"id"` NamespaceID uint64 `json:"namespaceID,string" db:"rel_namespace"` ModuleID uint64 `json:"moduleID,string,omitempty" db:"rel_module"` Name string `json:"name" db:"name"` Actions ActionSet `json:"actions" db:"actions"` - Enabled bool `json:"enabled" db:"enabled"` - Source string `json:"source" db:"source"` - // Weight int `json:"weight" db:"weight"` + + Enabled bool `json:"enabled" db:"enabled"` + + // What is running this? browser? corredor? + Engine string `json:"engine" db:"engine"` + + Source string `json:"source" db:"source"` + + // Is execution of this script critical? + Critical bool `json:"critical" db:"critical"` + + // No need to wait for script to return the value + Async bool `json:"async" db:"async"` + + // Order in which script(s) will be executed + Weight int `json:"weight" db:"weight"` + + // Who is running this script? + // Leave it at 0 for the current user + RunAs uint64 `json:"runAs", db:"rel_runner"` + + // Are you doing something that can take more time? + // specify timeout (in secods) + Timeout uint32 `json:"timeout" db:"timeout"` CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"` UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"` DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"` } - Script struct { - Source string `json:"source"` - Language string `json:"language"` - Critical bool `json:"critical"` - Async bool `json:"async"` - Timeout uint32 `json:"timeout"` - RunAs uint64 `json:"runAs,string"` - } - TriggerFilter struct { NamespaceID uint64 `json:"namespaceID,string"` Query string `json:"query"` @@ -48,19 +61,19 @@ type ( ) func (t Trigger) IsCritical() bool { - return true + return t.Critical } func (t Trigger) IsAsync() bool { - return false + return t.Async } func (t Trigger) GetRunnerID() uint64 { - return 0 + return t.RunAs } func (t Trigger) GetTimeout() uint32 { - return 0 + return t.Timeout } func (t Trigger) GetName() string { diff --git a/docs/compose/README.md b/docs/compose/README.md index 7fdcbe9a5..56791f6b3 100644 --- a/docs/compose/README.md +++ b/docs/compose/README.md @@ -111,6 +111,225 @@ +# Automation scripts + +| Method | Endpoint | Purpose | +| ------ | -------- | ------- | +| `GET` | `/namespace/{namespaceID}/automation/script/` | List/read automation script | +| `POST` | `/namespace/{namespaceID}/automation/script/` | Add new automation script | +| `GET` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Read automation script by ID | +| `POST` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Update automation script | +| `DELETE` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Delete script | + +## List/read automation script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| query | string | GET | Search query to match against automation script | N/A | NO | +| resource | string | GET | Limit by resource (via trigger) | N/A | NO | +| incDeleted | bool | GET | Include deleted scripts | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | + +## Add new automation script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| name | string | POST | automation name | N/A | YES | +| sourceRef | string | POST | Source URL | N/A | NO | +| source | string | POST | Source code | N/A | NO | +| runAs | uint64 | POST | Run as specific user | N/A | NO | +| runInUA | bool | POST | Run script in user-agent (browser) | N/A | NO | +| timeout | uint | POST | Script timeout (in milliseconds) | N/A | NO | +| critical | bool | POST | Is it critical to run this script successfully | N/A | NO | +| async | bool | POST | Will this script be ran asynchronously | N/A | NO | +| enabled | bool | POST | | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | + +## Read automation script by ID + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/{scriptID}` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| scriptID | uint64 | PATH | automation script ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | + +## Update automation script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/{scriptID}` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| scriptID | uint64 | PATH | Automation script ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| name | string | POST | Script name | N/A | YES | +| sourceRef | string | POST | Source URL | N/A | NO | +| source | string | POST | Source code | N/A | NO | +| runAs | uint64 | POST | Run script as specific user | N/A | NO | +| runInUA | bool | POST | Run script in user-agent (browser) | N/A | NO | +| timeout | uint | POST | Run script in user-agent (browser) | N/A | NO | +| critical | bool | POST | Is it critical to run this script successfully | N/A | NO | +| async | bool | POST | Will this script be ran asynchronously | N/A | NO | +| enabled | bool | POST | | N/A | NO | + +## Delete script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/{scriptID}` | HTTP/S | DELETE | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| scriptID | uint64 | PATH | automation ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | + +--- + + + + +# Automation script triggers + +| Method | Endpoint | Purpose | +| ------ | -------- | ------- | +| `GET` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/` | List/read automation script triggers | +| `POST` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/` | Add new automation script trigger | +| `GET` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | Read automation script trigger by ID | +| `POST` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | Update automation script trigger | +| `DELETE` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | Delete script | + +## List/read automation script triggers + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| resource | string | GET | Only triggers of a specific resource | N/A | NO | +| event | string | GET | Only triggers of a specific event | N/A | NO | +| incDeleted | bool | GET | Include deleted scripts | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +## Add new automation script trigger + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| name | string | POST | automation name | N/A | YES | +| resource | string | POST | Resource | N/A | NO | +| event | string | POST | Event | N/A | NO | +| condition | string | POST | Event | N/A | NO | +| enabled | bool | POST | | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +## Read automation script trigger by ID + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| triggerID | uint64 | PATH | Automation script trigger ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +## Update automation script trigger + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| triggerID | uint64 | PATH | Automation script trigger ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | +| name | string | POST | automation name | N/A | YES | +| resource | string | POST | Resource | N/A | NO | +| event | string | POST | Event | N/A | NO | +| condition | string | POST | Event | N/A | NO | +| enabled | bool | POST | | N/A | NO | + +## Delete script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | DELETE | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| triggerID | uint64 | PATH | automation script trigger ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +--- + + + + # Charts | Method | Endpoint | Purpose | @@ -686,6 +905,7 @@ Compose records | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Read records by ID from module section | | `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Update records in module section | | `DELETE` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Delete record row from module section | +| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/run-script` | Trigger a specific script on record | | `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/attachment` | Uploads attachment and validates it against record field requirements | ## Generates report from module records @@ -809,6 +1029,23 @@ Compose records | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | +## Trigger a specific script on record + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/module/{moduleID}/record/run-script` | HTTP/S | POST | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| recordID | uint64 | POST | Record ID | N/A | NO | +| scriptID | uint64 | POST | Script ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| moduleID | uint64 | PATH | Module ID | N/A | YES | + ## Uploads attachment and validates it against record field requirements #### Method @@ -827,109 +1064,4 @@ Compose records | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | ---- - - - - -# Triggers - -Compose Triggers - -| Method | Endpoint | Purpose | -| ------ | -------- | ------- | -| `GET` | `/namespace/{namespaceID}/trigger/` | List available triggers | -| `POST` | `/namespace/{namespaceID}/trigger/` | Create trigger | -| `GET` | `/namespace/{namespaceID}/trigger/{triggerID}` | Get trigger details | -| `POST` | `/namespace/{namespaceID}/trigger/{triggerID}` | Update trigger | -| `Delete` | `/namespace/{namespaceID}/trigger/{triggerID}` | Delete trigger | - -## List available triggers - -#### Method - -| URI | Protocol | Method | Authentication | -| --- | -------- | ------ | -------------- | -| `/namespace/{namespaceID}/trigger/` | HTTP/S | GET | | - -#### Request parameters - -| Parameter | Type | Method | Description | Default | Required? | -| --------- | ---- | ------ | ----------- | ------- | --------- | -| moduleID | uint64 | GET | Filter triggers by module | N/A | NO | -| query | string | GET | Search query | N/A | NO | -| page | uint | GET | Page number (0 based) | N/A | NO | -| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | -| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | - -## Create trigger - -#### Method - -| URI | Protocol | Method | Authentication | -| --- | -------- | ------ | -------------- | -| `/namespace/{namespaceID}/trigger/` | HTTP/S | POST | | - -#### Request parameters - -| Parameter | Type | Method | Description | Default | Required? | -| --------- | ---- | ------ | ----------- | ------- | --------- | -| moduleID | uint64 | POST | Module ID | N/A | NO | -| name | string | POST | Name | N/A | YES | -| actions | []string | POST | Actions that trigger this trigger | N/A | NO | -| enabled | bool | POST | Enabled | N/A | NO | -| source | string | POST | Trigger source code | N/A | NO | -| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO | -| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | - -## Get trigger details - -#### Method - -| URI | Protocol | Method | Authentication | -| --- | -------- | ------ | -------------- | -| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | GET | | - -#### Request parameters - -| Parameter | Type | Method | Description | Default | Required? | -| --------- | ---- | ------ | ----------- | ------- | --------- | -| triggerID | uint64 | PATH | Trigger ID | N/A | YES | -| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | - -## Update trigger - -#### Method - -| URI | Protocol | Method | Authentication | -| --- | -------- | ------ | -------------- | -| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | POST | | - -#### Request parameters - -| Parameter | Type | Method | Description | Default | Required? | -| --------- | ---- | ------ | ----------- | ------- | --------- | -| triggerID | uint64 | PATH | Trigger ID | N/A | YES | -| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | -| moduleID | uint64 | POST | Module ID | N/A | NO | -| name | string | POST | Name | N/A | YES | -| actions | []string | POST | Actions that trigger this trigger | N/A | NO | -| enabled | bool | POST | Enabled | N/A | NO | -| source | string | POST | Trigger source code | N/A | NO | - -## Delete trigger - -#### Method - -| URI | Protocol | Method | Authentication | -| --- | -------- | ------ | -------------- | -| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | Delete | | - -#### Request parameters - -| Parameter | Type | Method | Description | Default | Required? | -| --------- | ---- | ------ | ----------- | ------- | --------- | -| triggerID | uint64 | PATH | Trigger ID | N/A | YES | -| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | - --- \ No newline at end of file diff --git a/pkg/automation/corredor.go b/pkg/automation/corredor.go new file mode 100644 index 000000000..66a9e0d36 --- /dev/null +++ b/pkg/automation/corredor.go @@ -0,0 +1,38 @@ +package automation + +import ( + "context" + + "go.uber.org/zap" + "go.uber.org/zap/zapgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/grpclog" + + "github.com/cortezaproject/corteza-server/pkg/cli/options" +) + +// Corredor standard connector to Corredor service via gRPC +func Corredor(ctx context.Context, opt options.ScriptRunnerOpt, logger *zap.Logger) (c *grpc.ClientConn, err error) { + if !opt.Enabled { + // Do not connect when script runner is not enabled + return + } + + if opt.Log { + // Send logs to zap + // + // waiting for https://github.com/uber-go/zap/pull/538 + grpclog.SetLogger(zapgrpc.NewLogger(logger.Named("grpc"))) + } + + var dopts = []grpc.DialOption{ + // @todo insecure? + grpc.WithInsecure(), + } + + if opt.MaxBackoffDelay > 0 { + dopts = append(dopts, grpc.WithBackoffMaxDelay(opt.MaxBackoffDelay)) + } + + return grpc.DialContext(ctx, opt.Addr, dopts...) +} diff --git a/pkg/automation/script.gen.go b/pkg/automation/script.gen.go new file mode 100644 index 000000000..f6b2fee65 --- /dev/null +++ b/pkg/automation/script.gen.go @@ -0,0 +1,67 @@ +package automation + +// Hello! This file is auto-generated. + +type ( + + // ScriptSet slice of Script + // + // This type is auto-generated. + ScriptSet []*Script +) + +// Walk iterates through every slice item and calls w(Script) err +// +// This function is auto-generated. +func (set ScriptSet) Walk(w func(*Script) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// Filter iterates through every slice item, calls f(Script) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set ScriptSet) Filter(f func(*Script) (bool, error)) (out ScriptSet, err error) { + var ok bool + out = ScriptSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} + +// FindByID finds items from slice by its ID property +// +// This function is auto-generated. +func (set ScriptSet) FindByID(ID uint64) *Script { + for i := range set { + if set[i].ID == ID { + return set[i] + } + } + + return nil +} + +// IDs returns a slice of uint64s from all items in the set +// +// This function is auto-generated. +func (set ScriptSet) IDs() (IDs []uint64) { + IDs = make([]uint64, len(set)) + + for i := range set { + IDs[i] = set[i].ID + } + + return +} diff --git a/pkg/automation/script.gen_test.go b/pkg/automation/script.gen_test.go new file mode 100644 index 000000000..4255fd086 --- /dev/null +++ b/pkg/automation/script.gen_test.go @@ -0,0 +1,91 @@ +package automation + +import ( + "testing" + + "errors" + + "github.com/cortezaproject/corteza-server/internal/test" +) + +// Hello! This file is auto-generated. + +func TestScriptSetWalk(t *testing.T) { + value := make(ScriptSet, 3) + + // check walk with no errors + { + err := value.Walk(func(*Script) error { + return nil + }) + test.NoError(t, err, "Expected no returned error from Walk, got %+v", err) + } + + // check walk with error + test.Error(t, value.Walk(func(*Script) error { return errors.New("Walk error") }), "Expected error from walk, got nil") +} + +func TestScriptSetFilter(t *testing.T) { + value := make(ScriptSet, 3) + + // filter nothing + { + set, err := value.Filter(func(*Script) (bool, error) { + return true, nil + }) + test.NoError(t, err, "Didn't expect error when filtering set: %+v", err) + test.Assert(t, len(set) == len(value), "Expected equal length filter: %d != %d", len(value), len(set)) + } + + // filter one item + { + found := false + set, err := value.Filter(func(*Script) (bool, error) { + if !found { + found = true + return found, nil + } + return false, nil + }) + test.NoError(t, err, "Didn't expect error when filtering set: %+v", err) + test.Assert(t, len(set) == 1, "Expected single item, got %d", len(value)) + } + + // filter error + { + _, err := value.Filter(func(*Script) (bool, error) { + return false, errors.New("Filter error") + }) + test.Error(t, err, "Expected error, got %#v", err) + } +} + +func TestScriptSetIDs(t *testing.T) { + value := make(ScriptSet, 3) + // construct objects + value[0] = new(Script) + value[1] = new(Script) + value[2] = new(Script) + // set ids + value[0].ID = 1 + value[1].ID = 2 + value[2].ID = 3 + + // Find existing + { + val := value.FindByID(2) + test.Assert(t, val.ID == 2, "Expected ID 2, got %d", val.ID) + } + + // Find non-existing + { + val := value.FindByID(4) + test.Assert(t, val == nil, "Expected no value, got %#v", val) + } + + // List IDs from set + { + val := value.IDs() + test.Assert(t, len(val) == len(value), "Expected ID count mismatch, %d != %d", len(val), len(value)) + } +} diff --git a/pkg/automation/script.go b/pkg/automation/script.go new file mode 100644 index 000000000..87ee50e17 --- /dev/null +++ b/pkg/automation/script.go @@ -0,0 +1,122 @@ +package automation + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/cortezaproject/corteza-server/pkg/rh" +) + +type ( + Script struct { + ID uint64 `json:"scriptID,string" db:"id"` + + Name string `json:"name" db:"name"` + + // (URL) Where did we get the source from? + SourceRef string `json:"sourceRef" db:"source_ref"` + + // Code + Source string `json:"source" db:"source"` + + // No need to wait for script to return the value + Async bool `json:"async" db:"async"` + + // Who is running this script? + // Leave it at 0 for the current user (security invoker) or + // set ID of specific user (security definer) + RunAs uint64 `json:"runAs,string" db:"rel_runner"` + + // Where can we run this script? user-agent? corredor service? + RunInUA bool `json:"runInUA" db:"run_in_ua"` + + // Are you doing something that can take more time? + // specify timeout (in milliseconds) + Timeout uint `json:"timeout" db:"timeout"` + + // Is it critical to run this script successfully? + Critical bool `json:"critical" db:"critical"` + + Enabled bool `json:"enabled" db:"enabled"` + + CreatedAt time.Time `db:"created_at" json:"createdAt"` + CreatedBy uint64 `db:"created_by" json:"createdBy,string" ` + UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"` + UpdatedBy uint64 `db:"updated_by" json:"updatedBy,string,omitempty" ` + DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"` + DeletedBy uint64 `db:"deleted_by" json:"deletedBy,string,omitempty" ` + + triggers TriggerSet + } + + ScriptFilter struct { + Query string + Resource string + IncDeleted bool `json:"incDeleted"` + + // Standard paging fields & helpers + rh.PageFilter + } +) + +// IsValid - enabled, deleted? +func (s *Script) IsValid() bool { + return s != nil && s.Enabled && s.DeletedAt == nil +} + +// Verify - sanity check of script's properties +func (s Script) Verify() error { + if s.RunAsDefined() && s.RunInUA { + return errors.New("user-agent engine does not support run-as-defined scripts") + } + + if s.Critical && s.RunInUA { + return errors.New("user-agent engine scripts can not be critical") + } + + return nil +} + +// IsCompatible verifies if trigger can be added to a script +func (s *Script) CheckCompatibility(t *Trigger) error { + if s == nil { + return errors.New("not compatible with nil script") + } + if s == nil || t == nil { + return errors.New("not compatible with nil trigger") + } + + if t.IsDeferred() { + if s.RunInUA { + return errors.New("deferred triggers are not compatible with user-agent scripts") + } + + if s.RunAsInvoker() { + return errors.New("deferred triggers are not compatible with run-as-invoker scripts") + } + } + + return nil +} + +// FilterByEvent +// +// we will use the Trigger struct as a holder for conditions +func (set ScriptSet) FilterByEvent(event, resource string, cc ...TriggerConditionChecker) (out ScriptSet) { + out, _ = set.Filter(func(s *Script) (bool, error) { + return s.triggers.HasMatch(Trigger{Event: event, Resource: resource}, cc...), nil + }) + + return +} + +// RunAsDefined - script should be run with pre-defined privileges (user) +func (s Script) RunAsDefined() bool { + return s.RunAs > 0 +} + +// RunAsInvoker - this script should run with invoker's privileges (user) +func (s Script) RunAsInvoker() bool { + return s.RunAs == 0 +} diff --git a/pkg/automation/script_repository.go b/pkg/automation/script_repository.go new file mode 100644 index 000000000..6eb5f58b9 --- /dev/null +++ b/pkg/automation/script_repository.go @@ -0,0 +1,140 @@ +package automation + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/cortezaproject/corteza-server/pkg/rh" +) + +type ( + // repository servs as a db storage layer for permission rules + scriptRepository struct { + dbh *factory.DB + + // sql table reference + dbTablePrefix string + } +) + +func ScriptRepository(db *factory.DB, dbTablePrefix string) *scriptRepository { + return &scriptRepository{ + dbTablePrefix: dbTablePrefix, + dbh: db, + } +} + +func (r *scriptRepository) With(ctx context.Context) *scriptRepository { + return &scriptRepository{ + dbTablePrefix: r.dbTablePrefix, + dbh: r.db().With(ctx), + } +} + +func (r *scriptRepository) db() *factory.DB { + return r.dbh +} + +func (r scriptRepository) table() string { + return r.dbTablePrefix + "_automation_script" +} + +func (r scriptRepository) columns() []string { + return []string{ + "id", + "name", + "source_ref", + "source", + "async", + "rel_runner", + "run_in_ua", + "timeout", + "critical", + "enabled", + "created_at", + "created_by", + "updated_at", + "updated_by", + "deleted_at", + "deleted_by", + } +} + +func (r *scriptRepository) query() squirrel.SelectBuilder { + return squirrel. + Select(r.columns()...). + From(r.table()) +} + +// FindByID finds specific script +func (r *scriptRepository) FindByID(ctx context.Context, scriptID uint64) (*Script, error) { + var ( + rval = &Script{} + + query = r.query(). + Columns(r.columns()...). + Where("id = ?", scriptID) + ) + + return rval, rh.IsFound(rh.FetchOne(r.db(), query, rval), rval.ID > 0, errors.New("script not found")) +} + +// Find - finds scripts using given filter +func (r *scriptRepository) Find(ctx context.Context, filter ScriptFilter) (set ScriptSet, f ScriptFilter, err error) { + f = filter + + query := r.query() + + if !filter.IncDeleted { + query = query.Where("deleted_at IS NULL") + } + + if f.Query != "" { + q := "%" + f.Query + "%" + query = query.Where("name like ?", q) + } + + if f.Resource != "" { + // Making partial trigger repo struct on the fly to help us calculate the name of the triggers table + ttable := (triggerRepository{dbTablePrefix: r.dbTablePrefix}).table() + query = query.Where( + fmt.Sprintf("id IN (SELECT rel_script FROM `%s` WHERE resource = ?", ttable), + f.Resource, + ) + } + + if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 { + return + } + + query = query.OrderBy("id ASC") + + return set, f, rh.FetchPaged(r.db(), query, f.Page, f.PerPage, &set) +} + +// FindAllRunnable - loads and returns all runnable scripts +func (r *scriptRepository) FindAllRunnable() (ScriptSet, error) { + rr := make([]*Script, 0) + + return rr, errors.Wrap(rh.FetchAll( + r.db(), + r.query().Where("enabled AND deleted_at IS NULL"), + &rr, + ), "could not load runnable scripts") +} + +func (r *scriptRepository) Create(s *Script) (err error) { + return r.dbh.Transaction(func() error { + return r.dbh.Insert(r.table(), s) + }) +} + +func (r *scriptRepository) Update(s *Script) (err error) { + return r.dbh.Transaction(func() error { + return r.dbh.Update(r.table(), s, "id") + }) +} diff --git a/pkg/automation/script_test.go b/pkg/automation/script_test.go new file mode 100644 index 000000000..6decef6ed --- /dev/null +++ b/pkg/automation/script_test.go @@ -0,0 +1,48 @@ +package automation + +import ( + "testing" +) + +func TestScript_CheckCompatibility(t *testing.T) { + + tests := []struct { + name string + s *Script + t *Trigger + wantErr bool + }{ + {name: "both nil", + s: nil, + t: nil, + wantErr: true, + }, + {name: "both vanilla", + s: &Script{}, + t: &Trigger{}, + wantErr: false, + }, + {name: "deferred trigger with UA script", + s: &Script{RunInUA: true}, + t: &Trigger{Event: EVENT_TYPE_INTERVAL}, + wantErr: true, + }, + {name: "deferred trigger with invoker security", + s: &Script{RunAs: 0}, + t: &Trigger{Event: EVENT_TYPE_INTERVAL}, + wantErr: true, + }, + {name: "deferred trigger with invoker security", + s: &Script{RunAs: 1, RunInUA: false}, + t: &Trigger{Event: EVENT_TYPE_INTERVAL}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.s.CheckCompatibility(tt.t); (err != nil) != tt.wantErr { + t.Errorf("CheckCompatibility() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/automation/service.go b/pkg/automation/service.go new file mode 100644 index 000000000..8c7f21042 --- /dev/null +++ b/pkg/automation/service.go @@ -0,0 +1,322 @@ +package automation + +import ( + "context" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + "go.uber.org/zap" + + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/pkg/sentry" +) + +type ( + service struct { + l sync.Mutex + logger *zap.Logger + + c AutomationServiceConfig + + // service will flush values on TRUE or just reload on FALSE + f chan bool + + // internal list of runnable scripts (and their accompanying triggers) + runnables ScriptSet + + srepo *scriptRepository + trepo *triggerRepository + } + + ScriptsProvider interface { + FilterByEvent(event, resource string, cc ...TriggerConditionChecker) ScriptSet + } + + WatcherService interface { + Watch(ctx context.Context) + } + + AutomationServiceConfig struct { + DB *factory.DB + DbTablePrefix string + } +) + +const ( + watchInterval = time.Hour +) + +// Service initializes service{} struct +// +// service{} struct handles scripts & triggers. It acts as a caching layer and +// proxy to repository where it verifies and enriches payloads +// +func Service(ctx context.Context, logger *zap.Logger, c AutomationServiceConfig) (svc *service) { + svc = &service{ + logger: logger.Named("automation"), + + c: c, + + f: make(chan bool), + } + + if c.DB != nil { + svc.srepo = ScriptRepository(c.DB, c.DbTablePrefix) + svc.trepo = TriggerRepository(c.DB, c.DbTablePrefix) + } + + svc.Reload(ctx) + return +} + +// Watch() Watches for changes +func (svc service) Watch(ctx context.Context) { + go func() { + defer sentry.Recover() + + var ticker = time.NewTicker(watchInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + svc.Reload(ctx) + case <-svc.f: + svc.Reload(ctx) + } + } + }() + + svc.logger.Debug("watcher initialized") +} + +func (svc *service) Reload(ctx context.Context) { + svc.l.Lock() + defer svc.l.Unlock() + + if svc.c.DB == nil { + return + } + + var ( + err error + ss ScriptSet + tt TriggerSet + ) + + ss, err = svc.srepo.With(ctx).FindAllRunnable() + svc.logger.Info("scripts loaded", zap.Error(err), zap.Int("count", len(tt))) + if err != nil { + return + } + + // Only interested in valid scritps + ss, _ = ss.Filter(func(s *Script) (b bool, e error) { + return s.IsValid(), nil + }) + + tt, err = svc.trepo.With(ctx).FindAllRunnable() + svc.logger.Info("triggers loaded", zap.Error(err), zap.Int("count", len(tt))) + if err != nil { + return + } + + _ = tt.Walk(func(t *Trigger) error { + s := ss.FindByID(t.ScriptID) + if t.IsValid() && s.CheckCompatibility(t) != nil { + // Add only compatible triggers + s.triggers = append(s.triggers, t) + } + + return nil + }) +} + +// FindRunnableScripts scans internal list of runnable scripts and filters them by (trigger's) event and origin +func (svc service) FindRunnableScripts(event, origin string, cc ...TriggerConditionChecker) ScriptSet { + return svc.runnables.FilterByEvent(event, origin, cc...) +} + +// updateRunnableScripts - updates script set (internal runnable scripts list) +func (svc service) updateRunnableScripts(n *Script) { + svc.l.Lock() + defer svc.l.Unlock() + + ss := svc.runnables + + for i := range svc.runnables { + if ss[i].ID != n.ID { + continue + } + + if n.IsValid() { + // Valid, replace + ss[i] = n + } + + // Invalid, remove + ss = append(ss[:i], ss[i+1:]...) + return + } + + if n.IsValid() { + ss = append(ss, n) + } + + return +} + +// updateScriptsWithTrigger - finds the referenced script and updates its trigger set +func (svc service) updateScriptWithTrigger(n *Trigger) { + svc.l.Lock() + defer svc.l.Unlock() + + ss := svc.runnables + + for i := range ss { + if ss[i].ID != n.ScriptID { + continue + } + + tt := ss[i].triggers + + for i = range tt { + if n.IsValid() { + // Valid, replace + tt[i] = n + } + + // Invalid, remove + tt = append(tt[:i], tt[i+i:]...) + return + } + + if n.IsValid() { + tt = append(tt, n) + } + + return + } +} + +func (svc service) FindScriptByID(ctx context.Context, scriptID uint64) (*Script, error) { + return svc.srepo.FindByID(ctx, scriptID) +} + +func (svc service) FindScripts(ctx context.Context, f ScriptFilter) (ScriptSet, ScriptFilter, error) { + return svc.srepo.Find(ctx, f) +} + +// CreateScript - modifies script's props, pushes to repo & updates scripts cache +func (svc service) CreateScript(ctx context.Context, s *Script) (err error) { + s.ID = factory.Sonyflake.NextID() + s.CreatedAt = time.Now() + s.CreatedBy = auth.GetIdentityFromContext(ctx).Identity() + + if err = svc.srepo.Create(s); err != nil { + return err + } + + svc.updateRunnableScripts(s) + return +} + +// UpdateScript - modifies script's props, pushes to repo & updates scripts cache +func (svc service) UpdateScript(ctx context.Context, s *Script) (err error) { + s.UpdatedAt = &time.Time{} + *s.UpdatedAt = time.Now() + s.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity() + + // Ensure sanity + s.UpdatedAt, s.UpdatedBy = nil, 0 + s.DeletedAt, s.DeletedBy = nil, 0 + + if err = svc.srepo.Update(s); err != nil { + return err + } + + svc.updateRunnableScripts(s) + return +} + +// DeleteScript - modifies script's props, pushes to repo & updates scripts cache +func (svc service) DeleteScript(ctx context.Context, s *Script) (err error) { + s.DeletedAt = &time.Time{} + *s.DeletedAt = time.Now() + s.DeletedBy = auth.GetIdentityFromContext(ctx).Identity() + + // We're doing soft delete in the repo + if err = svc.srepo.Update(s); err != nil { + return err + } + + svc.updateRunnableScripts(s) + return +} + +func (svc service) FindTriggerByID(ctx context.Context, scriptID uint64) (*Trigger, error) { + return svc.trepo.FindByID(ctx, scriptID) +} + +func (svc service) FindTriggers(ctx context.Context, f TriggerFilter) (TriggerSet, TriggerFilter, error) { + return svc.trepo.Find(ctx, f) +} + +// CreateScript - modifies script's props, pushes to repo & updates scripts cache +func (svc service) CreateTrigger(ctx context.Context, s *Script, t *Trigger) (err error) { + if err = s.CheckCompatibility(t); err != nil { + return err + } + + t.ID = factory.Sonyflake.NextID() + t.CreatedAt = time.Now() + t.CreatedBy = auth.GetIdentityFromContext(ctx).Identity() + t.ScriptID = s.ID + + if err = svc.trepo.Create(t); err != nil { + return err + } + + svc.updateScriptWithTrigger(t) + return +} + +// UpdateTrigger - modifies script's props, pushes to repo & updates scripts cache +func (svc service) UpdateTrigger(ctx context.Context, s *Script, t *Trigger) (err error) { + if s.ID != t.ScriptID { + return errors.New("invalid script-trigger reference") + } + + if err = s.CheckCompatibility(t); err != nil { + return err + } + + t.UpdatedAt = &time.Time{} + *t.UpdatedAt = time.Now() + t.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity() + + if err = svc.trepo.Update(t); err != nil { + return err + } + + svc.updateScriptWithTrigger(t) + return +} + +// DeleteTrigger - modifies script's props, pushes to repo & updates scripts cache +func (svc service) DeleteTrigger(ctx context.Context, t *Trigger) (err error) { + t.DeletedAt = &time.Time{} + *t.DeletedAt = time.Now() + t.DeletedBy = auth.GetIdentityFromContext(ctx).Identity() + + // We're doing soft delete in the repo + if err = svc.trepo.Update(t); err != nil { + return err + } + + svc.updateScriptWithTrigger(t) + return +} diff --git a/pkg/automation/trigger.gen.go b/pkg/automation/trigger.gen.go new file mode 100644 index 000000000..262fbdf56 --- /dev/null +++ b/pkg/automation/trigger.gen.go @@ -0,0 +1,67 @@ +package automation + +// Hello! This file is auto-generated. + +type ( + + // TriggerSet slice of Trigger + // + // This type is auto-generated. + TriggerSet []*Trigger +) + +// Walk iterates through every slice item and calls w(Trigger) err +// +// This function is auto-generated. +func (set TriggerSet) Walk(w func(*Trigger) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// Filter iterates through every slice item, calls f(Trigger) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set TriggerSet) Filter(f func(*Trigger) (bool, error)) (out TriggerSet, err error) { + var ok bool + out = TriggerSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} + +// FindByID finds items from slice by its ID property +// +// This function is auto-generated. +func (set TriggerSet) FindByID(ID uint64) *Trigger { + for i := range set { + if set[i].ID == ID { + return set[i] + } + } + + return nil +} + +// IDs returns a slice of uint64s from all items in the set +// +// This function is auto-generated. +func (set TriggerSet) IDs() (IDs []uint64) { + IDs = make([]uint64, len(set)) + + for i := range set { + IDs[i] = set[i].ID + } + + return +} diff --git a/pkg/automation/trigger.gen_test.go b/pkg/automation/trigger.gen_test.go new file mode 100644 index 000000000..169c8e40f --- /dev/null +++ b/pkg/automation/trigger.gen_test.go @@ -0,0 +1,91 @@ +package automation + +import ( + "testing" + + "errors" + + "github.com/cortezaproject/corteza-server/internal/test" +) + +// Hello! This file is auto-generated. + +func TestTriggerSetWalk(t *testing.T) { + value := make(TriggerSet, 3) + + // check walk with no errors + { + err := value.Walk(func(*Trigger) error { + return nil + }) + test.NoError(t, err, "Expected no returned error from Walk, got %+v", err) + } + + // check walk with error + test.Error(t, value.Walk(func(*Trigger) error { return errors.New("Walk error") }), "Expected error from walk, got nil") +} + +func TestTriggerSetFilter(t *testing.T) { + value := make(TriggerSet, 3) + + // filter nothing + { + set, err := value.Filter(func(*Trigger) (bool, error) { + return true, nil + }) + test.NoError(t, err, "Didn't expect error when filtering set: %+v", err) + test.Assert(t, len(set) == len(value), "Expected equal length filter: %d != %d", len(value), len(set)) + } + + // filter one item + { + found := false + set, err := value.Filter(func(*Trigger) (bool, error) { + if !found { + found = true + return found, nil + } + return false, nil + }) + test.NoError(t, err, "Didn't expect error when filtering set: %+v", err) + test.Assert(t, len(set) == 1, "Expected single item, got %d", len(value)) + } + + // filter error + { + _, err := value.Filter(func(*Trigger) (bool, error) { + return false, errors.New("Filter error") + }) + test.Error(t, err, "Expected error, got %#v", err) + } +} + +func TestTriggerSetIDs(t *testing.T) { + value := make(TriggerSet, 3) + // construct objects + value[0] = new(Trigger) + value[1] = new(Trigger) + value[2] = new(Trigger) + // set ids + value[0].ID = 1 + value[1].ID = 2 + value[2].ID = 3 + + // Find existing + { + val := value.FindByID(2) + test.Assert(t, val.ID == 2, "Expected ID 2, got %d", val.ID) + } + + // Find non-existing + { + val := value.FindByID(4) + test.Assert(t, val == nil, "Expected no value, got %#v", val) + } + + // List IDs from set + { + val := value.IDs() + test.Assert(t, len(val) == len(value), "Expected ID count mismatch, %d != %d", len(val), len(value)) + } +} diff --git a/pkg/automation/trigger.go b/pkg/automation/trigger.go new file mode 100644 index 000000000..27d97f88d --- /dev/null +++ b/pkg/automation/trigger.go @@ -0,0 +1,111 @@ +package automation + +import ( + "time" + + "github.com/cortezaproject/corteza-server/pkg/rh" +) + +type ( + Event string + + Trigger struct { + ID uint64 `json:"triggerID,string" db:"id"` + + // Resource that triggered the event + // - "compose:" (unspec, general) + // - "compose:record" + // - "compose:namespace" + Resource string `json:"resource" db:"resource"` + + // Event name, arbitrary string + // - "before" + // - "after" + // - "on" + // - "at" + Event string `json:"event" db:"event"` + + // Arbitrary data for trigger condition + // + // It is caller's responsibility to encode, decode and verify conditions + Condition string `json:"condition" db:"condition"` + + ScriptID uint64 `json:"scriptID,string" db:"rel_script"` + + // Is trigger enabled or disabled? + Enabled bool `json:"enabled" db:"enabled"` + + CreatedAt time.Time `db:"created_at" json:"createdAt"` + CreatedBy uint64 `db:"created_by" json:"createdBy,string" ` + UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"` + UpdatedBy uint64 `db:"updated_by" json:"updatedBy,string,omitempty" ` + DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"` + DeletedBy uint64 `db:"deleted_by" json:"deletedBy,string,omitempty" ` + } + + TriggerFilter struct { + Resource string + Event string + ScriptID uint64 + + IncDeleted bool + + // Standard paging fields & helpers + rh.PageFilter + } + + TriggerConditionChecker func(string) bool +) + +const ( + EVENT_TYPE_INTERVAL = "interval" + EVENT_TYPE_TIMESTAMP = "at" +) + +// IsValid checks if trigger is enabled and not deleted +func (t *Trigger) IsValid() bool { + return t != nil && t.Enabled && t.DeletedAt == nil +} + +// IsDeferred - not called as consequence of a user's action (create, delete, update) +func (t Trigger) IsDeferred() bool { + return t.Event == EVENT_TYPE_INTERVAL || t.Event == EVENT_TYPE_TIMESTAMP +} + +// HasMatch checks if any og the triggers in a set matches the given parameters +func (set TriggerSet) HasMatch(m Trigger, ff ...TriggerConditionChecker) bool { +withTriggers: + for _, t := range set { + if !t.IsValid() { + // only valid can match + continue withTriggers + } + + if m.ID > 0 && m.ID != t.ID { + // Are we looking for a particular trigger? + continue withTriggers + } + + if m.Resource != t.Resource { + // event should match + continue withTriggers + } + + if m.Event != t.Event { + // event should match + continue withTriggers + } + + // Go through all condition checking functions + // All of them should return true for trigger to match + for _, fn := range ff { + if !fn(t.Condition) { + continue withTriggers + } + } + + return true + } + + return false +} diff --git a/pkg/automation/trigger_repository.go b/pkg/automation/trigger_repository.go new file mode 100644 index 000000000..d9690465e --- /dev/null +++ b/pkg/automation/trigger_repository.go @@ -0,0 +1,149 @@ +package automation + +import ( + "context" + "time" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/cortezaproject/corteza-server/pkg/rh" +) + +type ( + // repository servs as a db storage layer for permission rules + triggerRepository struct { + dbh *factory.DB + + // sql table reference + dbTablePrefix string + } +) + +func TriggerRepository(db *factory.DB, dbTablePrefix string) *triggerRepository { + return &triggerRepository{ + dbTablePrefix: dbTablePrefix, + dbh: db, + } +} + +func (r *triggerRepository) With(ctx context.Context) *triggerRepository { + return &triggerRepository{ + dbTablePrefix: r.dbTablePrefix, + dbh: r.db().With(ctx), + } +} + +func (r *triggerRepository) db() *factory.DB { + return r.dbh +} + +func (r triggerRepository) table() string { + return r.dbTablePrefix + "_automation_trigger" +} + +func (r triggerRepository) columns() []string { + return []string{ + "id", + "event", + "resource", + "`condition`", + "rel_script", + "enabled", + "created_at", + "created_by", + "updated_at", + "updated_by", + "deleted_at", + "deleted_by", + } +} + +func (r *triggerRepository) query() squirrel.SelectBuilder { + return squirrel. + Select(r.columns()...). + From(r.table()) +} + +// FindByID finds specific trigger +func (r *triggerRepository) FindByID(ctx context.Context, triggerID uint64) (*Trigger, error) { + var ( + rval = &Trigger{} + + query = r.query(). + Columns(r.columns()...). + Where("id = ?", triggerID) + ) + + return rval, rh.IsFound(rh.FetchOne(r.db(), query, rval), rval.ID > 0, errors.New("trigger not found")) +} + +// Find - finds triggers using given filter +func (r *triggerRepository) Find(ctx context.Context, filter TriggerFilter) (set TriggerSet, f TriggerFilter, err error) { + f = filter + + query := r.query() + + if f.ScriptID > 0 { + query = query.Where("rel_script = ?", f.ScriptID) + } + + if f.Event != "" { + query = query.Where("resource = ?", f.Event) + } + + if f.Resource != "" { + query = query.Where("resource = ?", f.Resource) + } + + if !filter.IncDeleted { + query = query.Where("deleted_at IS NULL") + } + + if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 { + return + } + + query = query.OrderBy("id ASC") + + return set, f, rh.FetchPaged(r.db(), query, f.Page, f.PerPage, &set) +} + +// FindAllRunnable - loads and returns all runnable triggers +func (r *triggerRepository) FindAllRunnable() (TriggerSet, error) { + rr := make([]*Trigger, 0) + + return rr, errors.Wrap(rh.FetchAll( + r.db(), + r.query().Where("enabled AND deleted_at IS NULL"), + &rr, + ), "could not load runnable triggers") +} + +func (r *triggerRepository) Create(s *Trigger) (err error) { + return r.dbh.Transaction(func() error { + // Generate ID + s.ID = factory.Sonyflake.NextID() + + if s.CreatedAt.IsZero() { + // Make sure time of creation is set + s.CreatedAt = time.Now() + } + + // Ensure sanity + s.UpdatedAt, s.UpdatedBy = nil, 0 + s.DeletedAt, s.DeletedBy = nil, 0 + + return r.dbh.Insert(r.table(), s) + }) +} + +func (r *triggerRepository) Update(s *Trigger) (err error) { + return r.dbh.Transaction(func() error { + s.UpdatedAt = &time.Time{} + *s.UpdatedAt = time.Now() + + return r.dbh.Update(r.table(), s, "id") + }) +} diff --git a/pkg/automation/trigger_test.go b/pkg/automation/trigger_test.go new file mode 100644 index 000000000..15f732f78 --- /dev/null +++ b/pkg/automation/trigger_test.go @@ -0,0 +1,46 @@ +package automation + +import ( + "testing" +) + +func TestTriggerSet_HasMatch(t *testing.T) { + type args struct { + m Trigger + } + tests := []struct { + name string + set TriggerSet + args args + want bool + }{ + { + name: "simple match", + set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: true}, nil, &Trigger{}}, + args: args{m: Trigger{Event: "e"}}, + want: true, + }, { + name: "simple miss", + set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: true}, nil, &Trigger{}}, + args: args{m: Trigger{}}, + want: false, + }, { + name: "specific", + set: TriggerSet{nil, &Trigger{}, &Trigger{ID: 2, Enabled: true}, nil, &Trigger{}}, + args: args{m: Trigger{ID: 2}}, + want: true, + }, { + name: "invalid", + set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: false}, nil, &Trigger{}}, + args: args{m: Trigger{Event: "e"}}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.set.HasMatch(tt.args.m); got != tt.want { + t.Errorf("HasMatch() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/rh/paging.go b/pkg/rh/paging.go index 3886171f2..ae6703960 100644 --- a/pkg/rh/paging.go +++ b/pkg/rh/paging.go @@ -32,3 +32,7 @@ func (pf *PageFilter) NormalizePerPage(min, max, def uint) { func (pf *PageFilter) NormalizePerPageWithDefaults() { pf.PerPage = NormalizePerPage(pf.PerPage, PER_PAGE_MIN, PER_PAGE_MAX, PER_PAGE_DEFAULT) } + +func (pf *PageFilter) NormalizePerPageNoMax() { + pf.PerPage = NormalizePerPage(pf.PerPage, PER_PAGE_MIN, 0, PER_PAGE_DEFAULT) +}