From 6463df9af169d9404026b7eb5cc161a866e22034 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Thu, 8 Aug 2019 08:14:05 +0200 Subject: [PATCH] Improvements of automation trigger management --- api/compose/spec.json | 22 +- api/compose/spec/automation_script.json | 18 +- api/compose/spec/automation_trigger.json | 16 +- compose/db/mysql/static.go | 2 +- .../mysql/20190701090000.automation.up.sql | 14 +- compose/internal/service/access_control.go | 74 ++++-- compose/internal/service/automation_script.go | 94 ++++++- compose/internal/service/service.go | 6 +- compose/rest/automation_script.go | 53 ++-- compose/rest/automation_trigger.go | 36 +-- compose/rest/module.go | 6 + compose/rest/namespace.go | 26 +- compose/rest/request/automation_script.go | 6 + compose/rest/request/automation_trigger.go | 10 - compose/types/permission_resources.go | 3 +- docs/compose/README.md | 16 +- internal/permissions/filter.go | 4 + pkg/automation/script.go | 52 +++- pkg/automation/script_repository.go | 56 ++-- pkg/automation/script_repository_test.go | 53 ++++ pkg/automation/service.go | 239 +++++++++--------- pkg/automation/trigger.go | 29 ++- pkg/automation/trigger_repository.go | 125 +++++---- pkg/automation/trigger_repository_test.go | 81 ++++++ 24 files changed, 699 insertions(+), 342 deletions(-) create mode 100644 pkg/automation/script_repository_test.go create mode 100644 pkg/automation/trigger_repository_test.go diff --git a/api/compose/spec.json b/api/compose/spec.json index 10e3e8079..2778ac936 100644 --- a/api/compose/spec.json +++ b/api/compose/spec.json @@ -1292,6 +1292,14 @@ "Client ID", "Session ID" ], + + "struct": [ + { + "imports": [ + "github.com/cortezaproject/corteza-server/pkg/automation" + ] + } + ], "parameters": { "path": [ { @@ -1353,8 +1361,7 @@ { "name": "name", "title": "automation name", - "type": "string", - "required": true + "type": "string" }, { "name": "sourceRef", @@ -1394,6 +1401,10 @@ { "name": "enabled", "type": "bool" + }, + { + "name": "triggers", + "type": "automation.TriggerSet" } ] } @@ -1432,8 +1443,7 @@ { "name": "name", "title": "Script name", - "type": "string", - "required": true + "type": "string" }, { "name": "sourceRef", @@ -1473,6 +1483,10 @@ { "name": "enabled", "type": "bool" + }, + { + "name": "triggers", + "type": "automation.TriggerSet" } ] } diff --git a/api/compose/spec/automation_script.json b/api/compose/spec/automation_script.json index 0a4f7f21e..b701037a7 100644 --- a/api/compose/spec/automation_script.json +++ b/api/compose/spec/automation_script.json @@ -1,7 +1,13 @@ { "Title": "Automation scripts", "Interface": "Automation_script", - "Struct": null, + "Struct": [ + { + "imports": [ + "github.com/cortezaproject/corteza-server/pkg/automation" + ] + } + ], "Parameters": { "path": [ { @@ -68,7 +74,6 @@ "post": [ { "name": "name", - "required": true, "title": "automation name", "type": "string" }, @@ -110,6 +115,10 @@ { "name": "enabled", "type": "bool" + }, + { + "name": "triggers", + "type": "automation.TriggerSet" } ] } @@ -147,7 +156,6 @@ "post": [ { "name": "name", - "required": true, "title": "Script name", "type": "string" }, @@ -189,6 +197,10 @@ { "name": "enabled", "type": "bool" + }, + { + "name": "triggers", + "type": "automation.TriggerSet" } ] } diff --git a/api/compose/spec/automation_trigger.json b/api/compose/spec/automation_trigger.json index b81791c1c..a9f9a8991 100644 --- a/api/compose/spec/automation_trigger.json +++ b/api/compose/spec/automation_trigger.json @@ -72,19 +72,15 @@ "Path": "/", "Parameters": { "post": [ - { - "name": "name", - "required": true, - "title": "automation name", - "type": "string" - }, { "name": "resource", + "required": true, "title": "Resource", "type": "string" }, { "name": "event", + "required": true, "title": "Event", "type": "string" }, @@ -131,19 +127,15 @@ } ], "post": [ - { - "name": "name", - "required": true, - "title": "automation name", - "type": "string" - }, { "name": "resource", + "required": true, "title": "Resource", "type": "string" }, { "name": "event", + "required": true, "title": "Event", "type": "string" }, diff --git a/compose/db/mysql/static.go b/compose/db/mysql/static.go index fa99822c8..aa20e1647 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 \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" +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 `when` TEXT NOT NULL COMMENT 'Trigger condition',\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?',\n\n `weight` INT NOT NULL DEFAULT 0,\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\x17\x16\xa2*\xfd\x0f\x00\x00\xfd\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!(\x17\x16\xa2*\xfd\x0f\x00\x00\xfd\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\x14O\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\xd1P\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x1a\x00\x1a\x00\x8c \x00\x00=Q\x00\x00\x00\x00" diff --git a/compose/db/schema/mysql/20190701090000.automation.up.sql b/compose/db/schema/mysql/20190701090000.automation.up.sql index 24753e412..03aa07930 100644 --- a/compose/db/schema/mysql/20190701090000.automation.up.sql +++ b/compose/db/schema/mysql/20190701090000.automation.up.sql @@ -30,9 +30,11 @@ CREATE TABLE IF NOT EXISTS compose_automation_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', + `event_condition` + TEXT NOT NULL COMMENT 'Trigger condition', + `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?', - `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?', + `weight` INT NOT NULL DEFAULT 0, `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -49,11 +51,12 @@ CREATE TABLE IF NOT EXISTS compose_automation_trigger ( -# Insert all into +# Migrate old triggers into scripts 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) +# Migrate old triggers into new triggers +INSERT INTO compose_automation_trigger (id, event, resource, event_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 @@ -64,5 +67,8 @@ SELECT id+seq, events.event, 'compose:record', rel_module, id, enabled, created_ 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,'%') )); +# Normalize and cleanup +UPDATE compose_automation_trigger SET event = 'manual' WHERE event = ''; +DELETE FROM compose_automation_trigger WHERE event_condition IN ('', '0') AND event <> 'manual'; -- DROP TABLE IF EXISTS compose_trigger; diff --git a/compose/internal/service/access_control.go b/compose/internal/service/access_control.go index 75b35255b..abff338eb 100644 --- a/compose/internal/service/access_control.go +++ b/compose/internal/service/access_control.go @@ -5,6 +5,7 @@ import ( "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/internal/permissions" + "github.com/cortezaproject/corteza-server/pkg/automation" ) type ( @@ -72,6 +73,10 @@ func (svc accessControl) CanCreateModule(ctx context.Context, r *types.Namespace return svc.can(ctx, r, "module.create") } +func (svc accessControl) CanCreateAutomationScript(ctx context.Context, r *types.Namespace) bool { + return svc.can(ctx, r, "automation-script.create") +} + func (svc accessControl) CanReadModule(ctx context.Context, r *types.Module) bool { return svc.can(ctx, r, "read") } @@ -108,6 +113,10 @@ func (svc accessControl) CanDeleteRecord(ctx context.Context, r *types.Module) b return svc.can(ctx, r, "record.delete") } +func (svc accessControl) CanManageAutomationTriggersOnModule(ctx context.Context, r *types.Module) bool { + return svc.can(ctx, r, "automation-trigger.manage", permissions.Allowed) +} + func (svc accessControl) CanCreateChart(ctx context.Context, r *types.Namespace) bool { return svc.can(ctx, r, "chart.create") } @@ -124,22 +133,6 @@ func (svc accessControl) CanDeleteChart(ctx context.Context, r *types.Chart) boo return svc.can(ctx, r, "delete") } -func (svc accessControl) CanCreateTrigger(ctx context.Context, r *types.Namespace) bool { - return svc.can(ctx, r, "trigger.create") -} - -func (svc accessControl) CanReadTrigger(ctx context.Context, r *types.Trigger) bool { - return svc.can(ctx, r, "read") -} - -func (svc accessControl) CanUpdateTrigger(ctx context.Context, r *types.Trigger) bool { - return svc.can(ctx, r, "update") -} - -func (svc accessControl) CanDeleteTrigger(ctx context.Context, r *types.Trigger) bool { - return svc.can(ctx, r, "delete") -} - func (svc accessControl) CanCreatePage(ctx context.Context, r *types.Namespace) bool { // @todo move to func args when namespaces are implemented return svc.can(ctx, r, "page.create") @@ -157,6 +150,26 @@ func (svc accessControl) CanDeletePage(ctx context.Context, r *types.Page) bool return svc.can(ctx, r, "delete") } +func (svc accessControl) CanReadAnyAutomationScript(ctx context.Context) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendWildcard(), "read") +} + +func (svc accessControl) CanReadAutomationScript(ctx context.Context, r *automation.Script) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendID(r.ID), "read") +} + +func (svc accessControl) CanUpdateAutomationScript(ctx context.Context, r *automation.Script) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendID(r.ID), "update") +} + +func (svc accessControl) CanDeleteAutomationScript(ctx context.Context, r *automation.Script) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendID(r.ID), "delete") +} + +func (svc accessControl) CanRunAutomationTrigger(ctx context.Context, r *automation.Trigger) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendID(r.ID), "run", permissions.Allowed) +} + func (svc accessControl) can(ctx context.Context, res permissionResource, op permissions.Operation, ff ...permissions.CheckAccessFunc) bool { return svc.permissions.Can(ctx, res.PermissionResource(), op, ff...) } @@ -181,6 +194,7 @@ func (svc accessControl) DefaultRules() permissions.RuleSet { modules = types.ModulePermissionResource.AppendWildcard() charts = types.ChartPermissionResource.AppendWildcard() pages = types.PagePermissionResource.AppendWildcard() + ascripts = types.AutomationScriptPermissionResource.AppendWildcard() allowAdm = func(res permissions.Resource, op permissions.Operation) *permissions.Rule { return permissions.AllowRule(permissions.AdminRoleID, res, op) @@ -201,7 +215,7 @@ func (svc accessControl) DefaultRules() permissions.RuleSet { allowAdm(namespaces, "page.create"), allowAdm(namespaces, "module.create"), allowAdm(namespaces, "chart.create"), - allowAdm(namespaces, "trigger.create"), + allowAdm(namespaces, "automation-script.create"), allowAdm(modules, "read"), allowAdm(modules, "update"), @@ -210,6 +224,7 @@ func (svc accessControl) DefaultRules() permissions.RuleSet { allowAdm(modules, "record.read"), allowAdm(modules, "record.update"), allowAdm(modules, "record.delete"), + allowAdm(modules, "automation-trigger.manage"), allowAdm(charts, "read"), allowAdm(charts, "update"), @@ -218,6 +233,10 @@ func (svc accessControl) DefaultRules() permissions.RuleSet { allowAdm(pages, "read"), allowAdm(pages, "update"), allowAdm(pages, "delete"), + + allowAdm(ascripts, "read"), + allowAdm(ascripts, "update"), + allowAdm(ascripts, "delete"), } } @@ -240,6 +259,7 @@ func (svc accessControl) Whitelist() permissions.Whitelist { "module.create", "chart.create", "page.create", + "automation-script.create", ) wl.Set( @@ -251,6 +271,7 @@ func (svc accessControl) Whitelist() permissions.Whitelist { "record.read", "record.update", "record.delete", + "automation-trigger.manage", ) wl.Set( @@ -266,13 +287,6 @@ func (svc accessControl) Whitelist() permissions.Whitelist { "delete", ) - wl.Set( - types.TriggerPermissionResource, - "read", - "update", - "delete", - ) - wl.Set( types.PagePermissionResource, "read", @@ -280,5 +294,17 @@ func (svc accessControl) Whitelist() permissions.Whitelist { "delete", ) + wl.Set( + types.AutomationScriptPermissionResource, + "read", + "update", + "delete", + ) + + wl.Set( + types.AutomationTriggerPermissionResource, + "run", + ) + return wl } diff --git a/compose/internal/service/automation_script.go b/compose/internal/service/automation_script.go index 18d772ff5..fd90441f3 100644 --- a/compose/internal/service/automation_script.go +++ b/compose/internal/service/automation_script.go @@ -5,6 +5,9 @@ import ( "go.uber.org/zap" + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/internal/permissions" "github.com/cortezaproject/corteza-server/pkg/automation" ) @@ -12,6 +15,8 @@ type ( automationScript struct { logger *zap.Logger scriptManager automationScriptManager + ns NamespaceService + ac automationScriptAccessController } automationScriptManager interface { @@ -21,40 +26,103 @@ type ( UpdateScript(context.Context, *automation.Script) error DeleteScript(context.Context, *automation.Script) error } + + automationScriptAccessController interface { + CanReadNamespace(context.Context, *types.Namespace) bool + + CanCreateAutomationScript(context.Context, *types.Namespace) bool + CanReadAnyAutomationScript(context.Context) bool + CanReadAutomationScript(context.Context, *automation.Script) bool + CanUpdateAutomationScript(context.Context, *automation.Script) bool + CanDeleteAutomationScript(context.Context, *automation.Script) bool + } + + automationScriptNamespaceFinder interface { + FindByID(uint64) (*types.Namespace, error) + } ) func AutomationScript(sm automationScriptManager) automationScript { var svc = automationScript{ scriptManager: sm, logger: DefaultLogger.Named("automation-script"), + ac: DefaultAccessControl, + ns: Namespace(), } 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) FindByID(ctx context.Context, namespaceID, scriptID uint64) (*automation.Script, error) { + if _, err := svc.loadNamespace(ctx, namespaceID); err != nil { + return nil, err + } + + if script, err := svc.scriptManager.FindScriptByID(ctx, scriptID); err != nil { + return nil, err + } else if !svc.ac.CanReadAutomationScript(ctx, script) { + return nil, ErrNoCreatePermissions.withStack() + } else { + return script, nil + } } -func (svc automationScript) Find(ctx context.Context, f automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) { - // @todo security check - can user read these scripts? +func (svc automationScript) Find(ctx context.Context, namespaceID uint64, f automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) { + if _, err := svc.loadNamespace(ctx, namespaceID); err != nil { + return nil, f, err + } + + f.AccessCheck = permissions.InitAccessCheckFilter( + "read", + auth.GetIdentityFromContext(ctx).Roles(), + svc.ac.CanReadAnyAutomationScript(ctx), + ) + 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? +func (svc automationScript) Create(ctx context.Context, namespaceID uint64, s *automation.Script) (err error) { + if ns, err := svc.loadNamespace(ctx, namespaceID); err != nil { + return err + } else if !svc.ac.CanCreateAutomationScript(ctx, ns) { + return ErrNoCreatePermissions.withStack() + } + 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? +func (svc automationScript) Update(ctx context.Context, namespaceID uint64, s *automation.Script) (err error) { + if _, err := svc.loadNamespace(ctx, namespaceID); err != nil { + return err + } else if !svc.ac.CanUpdateAutomationScript(ctx, s) { + return ErrNoCreatePermissions.withStack() + } + 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? +func (svc automationScript) Delete(ctx context.Context, namespaceID uint64, s *automation.Script) (err error) { + if _, err := svc.loadNamespace(ctx, namespaceID); err != nil { + return err + } else if !svc.ac.CanDeleteAutomationScript(ctx, s) { + return ErrNoCreatePermissions.withStack() + } + return svc.scriptManager.DeleteScript(ctx, s) } + +func (svc automationScript) loadNamespace(ctx context.Context, namespaceID uint64) (ns *types.Namespace, err error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired.withStack() + } + + if ns, err = svc.ns.With(ctx).FindByID(namespaceID); err != nil { + return + } + + if !svc.ac.CanReadNamespace(ctx, ns) { + return nil, ErrNoReadPermissions.withStack() + } + + return +} diff --git a/compose/internal/service/service.go b/compose/internal/service/service.go index fa1260346..27e57d98e 100644 --- a/compose/internal/service/service.go +++ b/compose/internal/service/service.go @@ -82,7 +82,11 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { // 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"}) + ias := automation.Service(automation.AutomationServiceConfig{ + Logger: DefaultLogger, + DbTablePrefix: "compose", + DB: db, + }) // Pass automation manager to DefaultAutomationScriptManager = AutomationScript(ias) diff --git a/compose/rest/automation_script.go b/compose/rest/automation_script.go index 29e8a2591..f80569f48 100644 --- a/compose/rest/automation_script.go +++ b/compose/rest/automation_script.go @@ -17,6 +17,13 @@ var _ = errors.Wrap type ( automationScriptPayload struct { *automation.Script + + CanGrant bool `json:"canGrant"` + CanUpdate bool `json:"canUpdate"` + CanDelete bool `json:"canDelete"` + CanSetRunner bool `json:"canSetRunner"` + CanSetAsAsync bool `json:"canSetAsAsync"` + CanSetAsCritical bool `json:"canAsCritical"` } automationScriptSetPayload struct { @@ -26,25 +33,34 @@ type ( AutomationScript struct { scripts automationScriptService + ac automationScriptAccessController } 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 + FindByID(context.Context, uint64, uint64) (*automation.Script, error) + Find(context.Context, uint64, automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) + Create(context.Context, uint64, *automation.Script) error + Update(context.Context, uint64, *automation.Script) error + Delete(context.Context, uint64, *automation.Script) error + } + + automationScriptAccessController interface { + CanGrant(context.Context) bool + + CanUpdateAutomationScript(context.Context, *automation.Script) bool + CanDeleteAutomationScript(context.Context, *automation.Script) bool } ) func (AutomationScript) New() *AutomationScript { return &AutomationScript{ scripts: service.DefaultAutomationScriptManager, + ac: service.DefaultAccessControl, } } func (ctrl AutomationScript) List(ctx context.Context, r *request.AutomationScriptList) (interface{}, error) { - set, filter, err := ctrl.scripts.Find(ctx, automation.ScriptFilter{ + set, filter, err := ctrl.scripts.Find(ctx, r.NamespaceID, automation.ScriptFilter{ // @todo namespace filtering // Might be a bit tricky as scripts themselves not know about namespaces // Namespace: r.NamespaceID @@ -74,16 +90,18 @@ func (ctrl AutomationScript) Create(ctx context.Context, r *request.AutomationSc } ) - return ctrl.makePayload(ctx, script, ctrl.scripts.Create(ctx, script)) + script.AddTrigger(automation.STMS_FRESH, r.Triggers...) + + return ctrl.makePayload(ctx, script, ctrl.scripts.Create(ctx, r.NamespaceID, script)) } func (ctrl AutomationScript) Read(ctx context.Context, r *request.AutomationScriptRead) (interface{}, error) { - script, err := ctrl.scripts.FindByID(ctx, r.ScriptID) + script, err := ctrl.scripts.FindByID(ctx, r.NamespaceID, 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) + script, err := ctrl.scripts.FindByID(ctx, r.NamespaceID, r.ScriptID) if err != nil { return nil, errors.Wrap(err, "can not update script") } @@ -98,16 +116,18 @@ func (ctrl AutomationScript) Update(ctx context.Context, r *request.AutomationSc script.Critical = r.Critical script.Enabled = r.Enabled - return ctrl.makePayload(ctx, script, ctrl.scripts.Update(ctx, script)) + script.AddTrigger(automation.STMS_UPDATE, r.Triggers...) + + return ctrl.makePayload(ctx, script, ctrl.scripts.Update(ctx, r.NamespaceID, script)) } func (ctrl AutomationScript) Delete(ctx context.Context, r *request.AutomationScriptDelete) (interface{}, error) { - script, err := ctrl.scripts.FindByID(ctx, r.ScriptID) + script, err := ctrl.scripts.FindByID(ctx, r.NamespaceID, r.ScriptID) if err != nil { return nil, errors.Wrap(err, "can not delete script") } - return resputil.OK(), ctrl.scripts.Delete(ctx, script) + return resputil.OK(), ctrl.scripts.Delete(ctx, r.NamespaceID, script) } func (ctrl AutomationScript) makePayload(ctx context.Context, s *automation.Script, err error) (*automationScriptPayload, error) { @@ -118,12 +138,9 @@ func (ctrl AutomationScript) makePayload(ctx context.Context, s *automation.Scri 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), + CanGrant: ctrl.ac.CanGrant(ctx), + CanUpdate: ctrl.ac.CanUpdateAutomationScript(ctx, s), + CanDelete: ctrl.ac.CanDeleteAutomationScript(ctx, s), }, nil } diff --git a/compose/rest/automation_trigger.go b/compose/rest/automation_trigger.go index 8bcdf959a..76d91368c 100644 --- a/compose/rest/automation_trigger.go +++ b/compose/rest/automation_trigger.go @@ -17,6 +17,8 @@ var _ = errors.Wrap type ( automationTriggerPayload struct { *automation.Trigger + + CanRun bool `json:"canRun"` } automationTriggerSetPayload struct { @@ -27,6 +29,8 @@ type ( AutomationTrigger struct { triggers automationTriggerService scripts automationScriptFinderService + + ac automationTriggerAccessController } automationTriggerService interface { @@ -38,7 +42,13 @@ type ( } automationScriptFinderService interface { - FindByID(context.Context, uint64) (*automation.Script, error) + FindByID(context.Context, uint64, uint64) (*automation.Script, error) + } + + automationTriggerAccessController interface { + CanGrant(context.Context) bool + + CanRunAutomationTrigger(context.Context, *automation.Trigger) bool } ) @@ -46,6 +56,7 @@ func (AutomationTrigger) New() *AutomationTrigger { return &AutomationTrigger{ scripts: service.DefaultAutomationScriptManager, triggers: service.DefaultAutomationTriggerManager, + ac: service.DefaultAccessControl, } } @@ -67,7 +78,7 @@ func (ctrl AutomationTrigger) List(ctx context.Context, r *request.AutomationTri } func (ctrl AutomationTrigger) Create(ctx context.Context, r *request.AutomationTriggerCreate) (interface{}, error) { - s, _, err := ctrl.loadCombo(ctx, r.ScriptID, 0) + s, _, err := ctrl.loadCombo(ctx, r.NamespaceID, r.ScriptID, 0) if err != nil { return nil, errors.Wrap(err, "can not create trigger") } @@ -86,7 +97,7 @@ func (ctrl AutomationTrigger) Create(ctx context.Context, r *request.AutomationT } func (ctrl AutomationTrigger) Read(ctx context.Context, r *request.AutomationTriggerRead) (interface{}, error) { - _, t, err := ctrl.loadCombo(ctx, r.ScriptID, 0) + _, t, err := ctrl.loadCombo(ctx, r.NamespaceID, r.ScriptID, 0) if err != nil { return nil, errors.Wrap(err, "can not read trigger") } @@ -95,7 +106,7 @@ func (ctrl AutomationTrigger) Read(ctx context.Context, r *request.AutomationTri } func (ctrl AutomationTrigger) Update(ctx context.Context, r *request.AutomationTriggerUpdate) (interface{}, error) { - s, t, err := ctrl.loadCombo(ctx, r.ScriptID, r.TriggerID) + s, t, err := ctrl.loadCombo(ctx, r.NamespaceID, r.ScriptID, r.TriggerID) if err != nil { return nil, errors.Wrap(err, "can not update trigger") } @@ -118,34 +129,29 @@ func (ctrl AutomationTrigger) Delete(ctx context.Context, r *request.AutomationT 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) { +func (ctrl AutomationTrigger) loadCombo(ctx context.Context, namespaceID, 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) + s, err = ctrl.scripts.FindByID(ctx, namespaceID, scriptID) return } return } -func (ctrl AutomationTrigger) makePayload(ctx context.Context, s *automation.Trigger, err error) (*automationTriggerPayload, error) { - if err != nil || s == nil { +func (ctrl AutomationTrigger) makePayload(ctx context.Context, t *automation.Trigger, err error) (*automationTriggerPayload, error) { + if err != nil || t == nil { return nil, err } return &automationTriggerPayload{ - Trigger: s, + Trigger: t, - // 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), + CanRun: ctrl.ac.CanRunAutomationTrigger(ctx, t), }, nil } diff --git a/compose/rest/module.go b/compose/rest/module.go index c92d4037e..d362010e0 100644 --- a/compose/rest/module.go +++ b/compose/rest/module.go @@ -23,6 +23,8 @@ type ( CanReadRecord bool `json:"canReadRecord"` CanUpdateRecord bool `json:"canUpdateRecord"` CanDeleteRecord bool `json:"canDeleteRecord"` + + CanManageAutomationTriggers bool `json:"canManageAutomationTriggers"` } moduleFieldPayload struct { @@ -55,6 +57,8 @@ type ( CanReadRecordValue(context.Context, *types.ModuleField) bool CanUpdateRecordValue(context.Context, *types.ModuleField) bool + + CanManageAutomationTriggersOnModule(context.Context, *types.Module) bool } ) @@ -147,6 +151,8 @@ func (ctrl Module) makePayload(ctx context.Context, m *types.Module, err error) CanReadRecord: ctrl.ac.CanReadRecord(ctx, m), CanUpdateRecord: ctrl.ac.CanUpdateRecord(ctx, m), CanDeleteRecord: ctrl.ac.CanDeleteRecord(ctx, m), + + CanManageAutomationTriggers: ctrl.ac.CanManageAutomationTriggersOnModule(ctx, m), }, nil } diff --git a/compose/rest/namespace.go b/compose/rest/namespace.go index 3af1b260a..79a0a7a8f 100644 --- a/compose/rest/namespace.go +++ b/compose/rest/namespace.go @@ -14,14 +14,14 @@ type ( namespacePayload struct { *types.Namespace - CanGrant bool `json:"canGrant"` - CanUpdateNamespace bool `json:"canUpdateNamespace"` - CanDeleteNamespace bool `json:"canDeleteNamespace"` - CanManageNamespace bool `json:"canManageNamespace"` - CanCreateModule bool `json:"canCreateModule"` - CanCreateChart bool `json:"canCreateChart"` - CanCreateTrigger bool `json:"canCreateTrigger"` - CanCreatePage bool `json:"canCreatePage"` + CanGrant bool `json:"canGrant"` + CanUpdateNamespace bool `json:"canUpdateNamespace"` + CanDeleteNamespace bool `json:"canDeleteNamespace"` + CanManageNamespace bool `json:"canManageNamespace"` + CanCreateModule bool `json:"canCreateModule"` + CanCreateChart bool `json:"canCreateChart"` + CanCreateAutomationScript bool `json:"canCreateAutomationScript"` + CanCreatePage bool `json:"canCreatePage"` } namespaceSetPayload struct { @@ -43,7 +43,7 @@ type ( CanCreateModule(context.Context, *types.Namespace) bool CanCreateChart(context.Context, *types.Namespace) bool - CanCreateTrigger(context.Context, *types.Namespace) bool + CanCreateAutomationScript(context.Context, *types.Namespace) bool CanCreatePage(context.Context, *types.Namespace) bool } ) @@ -123,10 +123,10 @@ func (ctrl Namespace) makePayload(ctx context.Context, ns *types.Namespace, err CanDeleteNamespace: ctrl.ac.CanDeleteNamespace(ctx, ns), CanManageNamespace: ctrl.ac.CanManageNamespace(ctx, ns), - CanCreateModule: ctrl.ac.CanCreateModule(ctx, ns), - CanCreateChart: ctrl.ac.CanCreateChart(ctx, ns), - CanCreateTrigger: ctrl.ac.CanCreateTrigger(ctx, ns), - CanCreatePage: ctrl.ac.CanCreatePage(ctx, ns), + CanCreateModule: ctrl.ac.CanCreateModule(ctx, ns), + CanCreateChart: ctrl.ac.CanCreateChart(ctx, ns), + CanCreateAutomationScript: ctrl.ac.CanCreateAutomationScript(ctx, ns), + CanCreatePage: ctrl.ac.CanCreatePage(ctx, ns), }, nil } diff --git a/compose/rest/request/automation_script.go b/compose/rest/request/automation_script.go index bea3e7059..dc2e352e4 100644 --- a/compose/rest/request/automation_script.go +++ b/compose/rest/request/automation_script.go @@ -25,6 +25,8 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" + + "github.com/cortezaproject/corteza-server/pkg/automation" ) var _ = chi.URLParam @@ -117,6 +119,7 @@ type AutomationScriptCreate struct { Critical bool Async bool Enabled bool + Triggers automation.TriggerSet NamespaceID uint64 `json:",string"` } @@ -136,6 +139,7 @@ func (r AutomationScriptCreate) Auditable() map[string]interface{} { out["critical"] = r.Critical out["async"] = r.Async out["enabled"] = r.Enabled + out["triggers"] = r.Triggers out["namespaceID"] = r.NamespaceID return out @@ -269,6 +273,7 @@ type AutomationScriptUpdate struct { Critical bool Async bool Enabled bool + Triggers automation.TriggerSet } func NewAutomationScriptUpdate() *AutomationScriptUpdate { @@ -289,6 +294,7 @@ func (r AutomationScriptUpdate) Auditable() map[string]interface{} { out["critical"] = r.Critical out["async"] = r.Async out["enabled"] = r.Enabled + out["triggers"] = r.Triggers return out } diff --git a/compose/rest/request/automation_trigger.go b/compose/rest/request/automation_trigger.go index 9d27d904f..b7ed22de5 100644 --- a/compose/rest/request/automation_trigger.go +++ b/compose/rest/request/automation_trigger.go @@ -111,7 +111,6 @@ var _ RequestFiller = NewAutomationTriggerList() // AutomationTrigger create request parameters type AutomationTriggerCreate struct { - Name string Resource string Event string Condition string @@ -127,7 +126,6 @@ func NewAutomationTriggerCreate() *AutomationTriggerCreate { func (r AutomationTriggerCreate) Auditable() map[string]interface{} { var out = map[string]interface{}{} - out["name"] = r.Name out["resource"] = r.Resource out["event"] = r.Event out["condition"] = r.Condition @@ -165,9 +163,6 @@ func (r *AutomationTriggerCreate) Fill(req *http.Request) (err error) { post[name] = string(param[0]) } - if val, ok := post["name"]; ok { - r.Name = val - } if val, ok := post["resource"]; ok { r.Resource = val } @@ -250,7 +245,6 @@ type AutomationTriggerUpdate struct { TriggerID uint64 `json:",string"` NamespaceID uint64 `json:",string"` ScriptID uint64 `json:",string"` - Name string Resource string Event string Condition string @@ -267,7 +261,6 @@ func (r AutomationTriggerUpdate) Auditable() map[string]interface{} { out["triggerID"] = r.TriggerID out["namespaceID"] = r.NamespaceID out["scriptID"] = r.ScriptID - out["name"] = r.Name out["resource"] = r.Resource out["event"] = r.Event out["condition"] = r.Condition @@ -306,9 +299,6 @@ func (r *AutomationTriggerUpdate) 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")) - if val, ok := post["name"]; ok { - r.Name = val - } if val, ok := post["resource"]; ok { r.Resource = val } diff --git a/compose/types/permission_resources.go b/compose/types/permission_resources.go index 060005326..37b07e711 100644 --- a/compose/types/permission_resources.go +++ b/compose/types/permission_resources.go @@ -10,4 +10,5 @@ const ChartPermissionResource = permissions.Resource("compose:chart:") const ModulePermissionResource = permissions.Resource("compose:module:") const ModuleFieldPermissionResource = permissions.Resource("compose:module-field:") const PagePermissionResource = permissions.Resource("compose:page:") -const TriggerPermissionResource = permissions.Resource("compose:trigger:") +const AutomationScriptPermissionResource = permissions.Resource("compose:automation-script:") +const AutomationTriggerPermissionResource = permissions.Resource("compose:automation-trigger:") diff --git a/docs/compose/README.md b/docs/compose/README.md index 56791f6b3..326d84c92 100644 --- a/docs/compose/README.md +++ b/docs/compose/README.md @@ -152,7 +152,7 @@ | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | -| name | string | POST | automation name | N/A | YES | +| name | string | POST | automation name | N/A | NO | | 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 | @@ -161,6 +161,7 @@ | 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 | +| triggers | automation.TriggerSet | POST | | N/A | NO | | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Read automation script by ID @@ -192,7 +193,7 @@ | --------- | ---- | ------ | ----------- | ------- | --------- | | 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 | +| name | string | POST | Script name | N/A | NO | | 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 | @@ -201,6 +202,7 @@ | 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 | +| triggers | automation.TriggerSet | POST | | N/A | NO | ## Delete script @@ -264,9 +266,8 @@ | 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 | +| resource | string | POST | Resource | N/A | YES | +| event | string | POST | Event | N/A | YES | | condition | string | POST | Event | N/A | NO | | enabled | bool | POST | | N/A | NO | | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | @@ -303,9 +304,8 @@ | 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 | +| resource | string | POST | Resource | N/A | YES | +| event | string | POST | Event | N/A | YES | | condition | string | POST | Event | N/A | NO | | enabled | bool | POST | | N/A | NO | diff --git a/internal/permissions/filter.go b/internal/permissions/filter.go index 1a25924c8..0dc701eb9 100644 --- a/internal/permissions/filter.go +++ b/internal/permissions/filter.go @@ -39,6 +39,10 @@ func (ac *AccessCheck) SetPrimaryKeyName(col string) *AccessCheck { return ac } +func (ac AccessCheck) HasOperation() bool { + return ac.operation != "" +} + // ToSql converts access check to SQL (with args) that will help with filtering // // Satisfies squirrel.Sqlizer interface diff --git a/pkg/automation/script.go b/pkg/automation/script.go index 87ee50e17..850ec8384 100644 --- a/pkg/automation/script.go +++ b/pkg/automation/script.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" + "github.com/cortezaproject/corteza-server/internal/permissions" "github.com/cortezaproject/corteza-server/pkg/rh" ) @@ -47,7 +48,15 @@ type ( DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"` DeletedBy uint64 `db:"deleted_by" json:"deletedBy,string,omitempty" ` + // Serves as container for valid triggers for runnable scripts internal cache + // and for as a transport on create/update operations + // + // Node: on c/u op., we currently just merge current state with the given list, + // w/o updating the rest of the script's triggers triggers TriggerSet + + // How are we merging? + tms triggersMergeStrategy } ScriptFilter struct { @@ -55,9 +64,27 @@ type ( Resource string IncDeleted bool `json:"incDeleted"` + permissions.AccessCheck `json:"-"` + // Standard paging fields & helpers rh.PageFilter } + + triggersMergeStrategy int +) + +const ( + // Ignore the given triggers + STMS_IGNORE triggersMergeStrategy = iota + + // Create triggers, no pre-checks + STMS_FRESH + + // Update existing with new + STMS_UPDATE + + // Replace existing with new + STMS_REPLACE ) // IsValid - enabled, deleted? @@ -87,7 +114,7 @@ func (s *Script) CheckCompatibility(t *Trigger) error { return errors.New("not compatible with nil trigger") } - if t.IsDeferred() { + if t.IsDeferred() || t.IsInterval() { if s.RunInUA { return errors.New("deferred triggers are not compatible with user-agent scripts") } @@ -120,3 +147,26 @@ func (s Script) RunAsDefined() bool { func (s Script) RunAsInvoker() bool { return s.RunAs == 0 } + +// AddTrigger appends one or more triggers to internal list of triggers on script struct +// +// We do not do any compatibility check (See Script.CheckCompatibility()); +// this is only an utility func that helps us pass data along +func (s *Script) AddTrigger(strategy triggersMergeStrategy, tt ...*Trigger) { + s.tms = strategy + + if s.tms == STMS_IGNORE { + return + } + + if s.tms == STMS_REPLACE { + s.triggers = TriggerSet{} + } + + // Make sure all your trigger belong to us (ref same script or no ref): + for _, t := range tt { + if t.ScriptID == 0 || t.ScriptID == s.ID { + s.triggers = append(s.triggers, t) + } + } +} diff --git a/pkg/automation/script_repository.go b/pkg/automation/script_repository.go index 6eb5f58b9..09781b347 100644 --- a/pkg/automation/script_repository.go +++ b/pkg/automation/script_repository.go @@ -1,44 +1,30 @@ package automation import ( - "context" "fmt" "github.com/pkg/errors" "github.com/titpetric/factory" "gopkg.in/Masterminds/squirrel.v1" + "github.com/cortezaproject/corteza-server/compose/types" "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 { +func ScriptRepository(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" } @@ -71,20 +57,19 @@ func (r *scriptRepository) query() squirrel.SelectBuilder { } // FindByID finds specific script -func (r *scriptRepository) FindByID(ctx context.Context, scriptID uint64) (*Script, error) { +func (r *scriptRepository) findByID(db *factory.DB, 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")) + return rval, rh.IsFound(rh.FetchOne(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) { +func (r *scriptRepository) find(db *factory.DB, filter ScriptFilter) (set ScriptSet, f ScriptFilter, err error) { f = filter query := r.query() @@ -100,41 +85,44 @@ func (r *scriptRepository) Find(ctx context.Context, filter ScriptFilter) (set S 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), + fmt.Sprintf("id IN (SELECT rel_script FROM `%s` WHERE resource = ?", r.table()), f.Resource, ) } - if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 { + // Limit by access + if f.AccessCheck.HasOperation() { + query = query.Where(f.AccessCheck.BindToEnv( + types.AutomationScriptPermissionResource, + r.dbTablePrefix, + )) + } + + if f.Count, err = rh.Count(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) + return set, f, rh.FetchPaged(db, query, f.Page, f.PerPage, &set) } // FindAllRunnable - loads and returns all runnable scripts -func (r *scriptRepository) FindAllRunnable() (ScriptSet, error) { +func (r *scriptRepository) findRunnable(db *factory.DB) (ScriptSet, error) { rr := make([]*Script, 0) return rr, errors.Wrap(rh.FetchAll( - r.db(), + 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) create(db *factory.DB, s *Script) (err error) { + return db.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") - }) +func (r *scriptRepository) update(db *factory.DB, s *Script) (err error) { + return db.Update(r.table(), s, "id") } diff --git a/pkg/automation/script_repository_test.go b/pkg/automation/script_repository_test.go new file mode 100644 index 000000000..8d3bad367 --- /dev/null +++ b/pkg/automation/script_repository_test.go @@ -0,0 +1,53 @@ +// +build integration + +package automation + +import ( + "os" + "testing" + "time" + + "github.com/titpetric/factory" + + "github.com/cortezaproject/corteza-server/internal/test" +) + +func TestScriptRepository_findByID(t *testing.T) { + factory.Database.Add("compose", os.Getenv("COMPOSE_DB_DSN")) + + var ( + scriptID = factory.Sonyflake.NextID() + + err error + ss ScriptSet + script *Script + + db = factory.Database.MustGet("compose") + srepo = ScriptRepository("compose") + ) + + db.Begin() + defer db.Rollback() + + _, err = srepo.findByID(db, scriptID) + test.Error(t, err, "expecting error") + + // runnable script + script = &Script{ID: scriptID, CreatedAt: time.Now(), Enabled: true} + err = srepo.create(db, script) + test.NoError(t, err, "unexpected error: %v") + + // find this script now + script, err = srepo.findByID(db, scriptID) + test.NoError(t, err, "script not found") + test.Assert(t, script != nil && script.ID == scriptID, "script not found (in runnable scripts)") + + // find the script through find func + ss, _, err = srepo.find(db, ScriptFilter{}) + test.NoError(t, err, "unexpected error: %v") + test.Assert(t, len(ss) > 0, "could not find the script") + test.Assert(t, ss.FindByID(scriptID) != nil, "could not find the script") + + err = srepo.update(db, script) + test.NoError(t, err, "unexpected error when updating the script: %v") +} diff --git a/pkg/automation/service.go b/pkg/automation/service.go index 8c7f21042..d664c0d10 100644 --- a/pkg/automation/service.go +++ b/pkg/automation/service.go @@ -28,6 +28,8 @@ type ( srepo *scriptRepository trepo *triggerRepository + + db *factory.DB } ScriptsProvider interface { @@ -39,6 +41,7 @@ type ( } AutomationServiceConfig struct { + Logger *zap.Logger DB *factory.DB DbTablePrefix string } @@ -53,28 +56,29 @@ const ( // 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) { +func Service(c AutomationServiceConfig) (svc *service) { svc = &service{ - logger: logger.Named("automation"), + logger: c.Logger.Named("automation"), c: c, - f: make(chan bool), + srepo: ScriptRepository(c.DbTablePrefix), + trepo: TriggerRepository(c.DbTablePrefix), + + db: c.DB, } - if c.DB != nil { - svc.srepo = ScriptRepository(c.DB, c.DbTablePrefix) - svc.trepo = TriggerRepository(c.DB, c.DbTablePrefix) - } - - svc.Reload(ctx) + // Reload ASAP + svc.Reload() return } -// Watch() Watches for changes +// Watch watches for changes func (svc service) Watch(ctx context.Context) { + svc.f = make(chan bool) go func() { defer sentry.Recover() + defer close(svc.f) var ticker = time.NewTicker(watchInterval) defer ticker.Stop() @@ -83,9 +87,13 @@ func (svc service) Watch(ctx context.Context) { case <-ctx.Done(): return case <-ticker.C: - svc.Reload(ctx) + svc.reload(ctx) case <-svc.f: - svc.Reload(ctx) + for len(svc.f) > 0 { + // Drain just before we reload + <-svc.f + } + svc.reload(ctx) } } }() @@ -93,7 +101,16 @@ func (svc service) Watch(ctx context.Context) { svc.logger.Debug("watcher initialized") } -func (svc *service) Reload(ctx context.Context) { +func (svc *service) Reload() { + select { + case svc.f <- true: + return + default: + // that's ok too.. + } +} + +func (svc *service) reload(ctx context.Context) { svc.l.Lock() defer svc.l.Unlock() @@ -107,7 +124,7 @@ func (svc *service) Reload(ctx context.Context) { tt TriggerSet ) - ss, err = svc.srepo.With(ctx).FindAllRunnable() + ss, err = svc.srepo.findRunnable(svc.db) svc.logger.Info("scripts loaded", zap.Error(err), zap.Int("count", len(tt))) if err != nil { return @@ -118,7 +135,7 @@ func (svc *service) Reload(ctx context.Context) { return s.IsValid(), nil }) - tt, err = svc.trepo.With(ctx).FindAllRunnable() + tt, err = svc.trepo.findRunnable(svc.db) svc.logger.Info("triggers loaded", zap.Error(err), zap.Int("count", len(tt))) if err != nil { return @@ -126,7 +143,7 @@ func (svc *service) Reload(ctx context.Context) { _ = tt.Walk(func(t *Trigger) error { s := ss.FindByID(t.ScriptID) - if t.IsValid() && s.CheckCompatibility(t) != nil { + if s != nil && t.IsValid() && s.CheckCompatibility(t) != nil { // Add only compatible triggers s.triggers = append(s.triggers, t) } @@ -140,106 +157,74 @@ func (svc service) FindRunnableScripts(event, origin string, cc ...TriggerCondit 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) + return svc.srepo.findByID(svc.db, scriptID) } func (svc service) FindScripts(ctx context.Context, f ScriptFilter) (ScriptSet, ScriptFilter, error) { - return svc.srepo.Find(ctx, f) + return svc.srepo.find(svc.db, f) } // CreateScript - modifies script's props, pushes to repo & updates scripts cache -func (svc service) CreateScript(ctx context.Context, s *Script) (err error) { +func (svc service) CreateScript(ctx context.Context, s *Script) 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 - } + return svc.db.Transaction(func() (err error) { + if err = svc.srepo.create(svc.db, s); err != nil { + return + } - svc.updateRunnableScripts(s) - return + err = s.triggers.Walk(func(t *Trigger) error { + return svc.setNewTriggerInfo(ctx, s, t) + }) + + if err != nil { + return + } + + // Force no-pre-check + if err = svc.trepo.mergeSet(svc.db, STMS_FRESH, s.ID, s.triggers); err != nil { + return + } + + svc.Reload() + 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() - +func (svc service) UpdateScript(ctx context.Context, s *Script) error { // Ensure sanity - s.UpdatedAt, s.UpdatedBy = nil, 0 + s.UpdatedAt, s.UpdatedBy = &time.Time{}, auth.GetIdentityFromContext(ctx).Identity() + *s.UpdatedAt = time.Now() s.DeletedAt, s.DeletedBy = nil, 0 - if err = svc.srepo.Update(s); err != nil { - return err - } + return svc.db.Transaction(func() (err error) { + if err = svc.srepo.update(svc.db, s); err != nil { + return + } - svc.updateRunnableScripts(s) - return + err = s.triggers.Walk(func(t *Trigger) error { + if t.ID == 0 { + return svc.setNewTriggerInfo(ctx, s, t) + } else { + return svc.setUpdatedTriggerInfo(ctx, s, t) + } + }) + + if err != nil { + return + } + + if err = svc.trepo.mergeSet(svc.db, s.tms, s.ID, s.triggers); err != nil { + return + } + + svc.Reload() + return + }) } // DeleteScript - modifies script's props, pushes to repo & updates scripts cache @@ -249,24 +234,41 @@ func (svc service) DeleteScript(ctx context.Context, s *Script) (err error) { s.DeletedBy = auth.GetIdentityFromContext(ctx).Identity() // We're doing soft delete in the repo - if err = svc.srepo.Update(s); err != nil { + if err = svc.srepo.update(svc.db, s); err != nil { return err } - svc.updateRunnableScripts(s) + if err = svc.trepo.deleteByScriptID(svc.db, s.ID); err != nil { + return err + } + + svc.Reload() return } func (svc service) FindTriggerByID(ctx context.Context, scriptID uint64) (*Trigger, error) { - return svc.trepo.FindByID(ctx, scriptID) + return svc.trepo.findByID(svc.db, scriptID) } func (svc service) FindTriggers(ctx context.Context, f TriggerFilter) (TriggerSet, TriggerFilter, error) { - return svc.trepo.Find(ctx, f) + return svc.trepo.find(svc.db, f) } -// CreateScript - modifies script's props, pushes to repo & updates scripts cache +// CreateTrigger - 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 = svc.setNewTriggerInfo(ctx, s, t); err != nil { + return err + } + + if err = svc.trepo.replace(svc.db, t); err != nil { + return err + } + + svc.Reload() + return +} + +func (svc service) setNewTriggerInfo(ctx context.Context, s *Script, t *Trigger) (err error) { if err = s.CheckCompatibility(t); err != nil { return err } @@ -275,17 +277,24 @@ func (svc service) CreateTrigger(ctx context.Context, s *Script, t *Trigger) (er 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 + return nil } // 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 err = svc.setUpdatedTriggerInfo(ctx, s, t); err != nil { + return err + } + + if err = svc.trepo.replace(svc.db, t); err != nil { + return err + } + + svc.Reload() + return +} + +func (svc service) setUpdatedTriggerInfo(ctx context.Context, s *Script, t *Trigger) (err error) { if s.ID != t.ScriptID { return errors.New("invalid script-trigger reference") } @@ -297,13 +306,7 @@ func (svc service) UpdateTrigger(ctx context.Context, s *Script, t *Trigger) (er 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 + return nil } // DeleteTrigger - modifies script's props, pushes to repo & updates scripts cache @@ -313,10 +316,10 @@ func (svc service) DeleteTrigger(ctx context.Context, t *Trigger) (err error) { t.DeletedBy = auth.GetIdentityFromContext(ctx).Identity() // We're doing soft delete in the repo - if err = svc.trepo.Update(t); err != nil { + if err = svc.trepo.replace(svc.db, t); err != nil { return err } - svc.updateScriptWithTrigger(t) + svc.Reload() return } diff --git a/pkg/automation/trigger.go b/pkg/automation/trigger.go index 27d97f88d..4febf07b7 100644 --- a/pkg/automation/trigger.go +++ b/pkg/automation/trigger.go @@ -19,22 +19,25 @@ type ( Resource string `json:"resource" db:"resource"` // Event name, arbitrary string + // - "manual" + // - "interval" + // - "deferred" // - "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"` + Condition string `json:"condition" db:"event_condition"` ScriptID uint64 `json:"scriptID,string" db:"rel_script"` // Is trigger enabled or disabled? Enabled bool `json:"enabled" db:"enabled"` + Weight int `json:"weight" db:"weight"` + 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"` @@ -44,11 +47,12 @@ type ( } TriggerFilter struct { - Resource string - Event string - ScriptID uint64 + Resource string `json:"resource"` + Event string `json:"event"` + Condition string `json:"condition"` + ScriptID uint64 `json:"scriptID"` - IncDeleted bool + IncDeleted bool `json:"incDeleted"` // Standard paging fields & helpers rh.PageFilter @@ -58,8 +62,8 @@ type ( ) const ( - EVENT_TYPE_INTERVAL = "interval" - EVENT_TYPE_TIMESTAMP = "at" + EVENT_TYPE_INTERVAL = "interval" + EVENT_TYPE_DEFERRED = "deferred" ) // IsValid checks if trigger is enabled and not deleted @@ -67,9 +71,14 @@ 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) IsInterval() bool { + return t.Event == EVENT_TYPE_INTERVAL +} + // 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 + return t.Event == EVENT_TYPE_DEFERRED } // HasMatch checks if any og the triggers in a set matches the given parameters diff --git a/pkg/automation/trigger_repository.go b/pkg/automation/trigger_repository.go index d9690465e..42727a910 100644 --- a/pkg/automation/trigger_repository.go +++ b/pkg/automation/trigger_repository.go @@ -1,9 +1,6 @@ package automation import ( - "context" - "time" - "github.com/pkg/errors" "github.com/titpetric/factory" "gopkg.in/Masterminds/squirrel.v1" @@ -14,31 +11,17 @@ import ( 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 { +func TriggerRepository(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" } @@ -46,9 +29,9 @@ func (r triggerRepository) table() string { func (r triggerRepository) columns() []string { return []string{ "id", - "event", "resource", - "`condition`", + "event", + "event_condition", "rel_script", "enabled", "created_at", @@ -67,20 +50,18 @@ func (r *triggerRepository) query() squirrel.SelectBuilder { } // FindByID finds specific trigger -func (r *triggerRepository) FindByID(ctx context.Context, triggerID uint64) (*Trigger, error) { +func (r *triggerRepository) findByID(db *factory.DB, triggerID uint64) (*Trigger, error) { var ( rval = &Trigger{} - query = r.query(). - Columns(r.columns()...). - Where("id = ?", triggerID) + query = r.query().Where("id = ?", triggerID) ) - return rval, rh.IsFound(rh.FetchOne(r.db(), query, rval), rval.ID > 0, errors.New("trigger not found")) + return rval, rh.IsFound(rh.FetchOne(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) { +func (r *triggerRepository) find(db *factory.DB, filter TriggerFilter) (set TriggerSet, f TriggerFilter, err error) { f = filter query := r.query() @@ -90,60 +71,100 @@ func (r *triggerRepository) Find(ctx context.Context, filter TriggerFilter) (set } if f.Event != "" { - query = query.Where("resource = ?", f.Event) + query = query.Where("event = ?", f.Event) } if f.Resource != "" { query = query.Where("resource = ?", f.Resource) } + if f.Condition != "" { + query = query.Where("event_condition = ?", f.Condition) + } + if !filter.IncDeleted { query = query.Where("deleted_at IS NULL") } - if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 { + if f.Count, err = rh.Count(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) + return set, f, rh.FetchPaged(db, query, f.Page, f.PerPage, &set) } // FindAllRunnable - loads and returns all runnable triggers -func (r *triggerRepository) FindAllRunnable() (TriggerSet, error) { +func (r *triggerRepository) findRunnable(db *factory.DB) (TriggerSet, error) { rr := make([]*Trigger, 0) return rr, errors.Wrap(rh.FetchAll( - r.db(), + 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() +func (r *triggerRepository) replace(db *factory.DB, t *Trigger) (err error) { + if dup, err := r.checkDuplicate(db, t); err != nil { + return err + } else if dup != nil { + t.ID = dup.ID + } - if s.CreatedAt.IsZero() { - // Make sure time of creation is set - s.CreatedAt = time.Now() + return db.Replace(r.table(), t) +} + +func (r *triggerRepository) deleteByScriptID(db *factory.DB, scriptID uint64) (err error) { + return db.Delete(r.table(), Trigger{ScriptID: scriptID}, "rel_script") +} + +// Check for existing events +func (r *triggerRepository) checkDuplicate(db *factory.DB, t *Trigger) (*Trigger, error) { + if t.IsDeferred() && t.IsInterval() { + // deferred & interval triggers + // can have duplicates + return nil, nil + } + + tt, _, err := r.find(db, TriggerFilter{ + Resource: t.Resource, + Event: t.Event, + ScriptID: t.ScriptID, + Condition: t.Condition, + IncDeleted: false, + }) + + if err != nil || len(tt) == 0 { + return nil, err + } + + return tt[0], nil +} + +func (r *triggerRepository) mergeSet(db *factory.DB, tms triggersMergeStrategy, scriptID uint64, tt TriggerSet) error { + if tms == STMS_IGNORE { + // do nothing + return nil + } + + if tms == STMS_REPLACE { + // Mark all existing as deleted + // + // here, we're assuming we have the entire + // trigger list present (in s.triggers) + if err := r.deleteByScriptID(db, scriptID); err != nil { + return err + } + } + + return tt.Walk(func(t *Trigger) error { + if t.ScriptID != scriptID { + return nil } - // 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") + // Replace (upsert) all triggers we have + return r.replace(db, t) }) } diff --git a/pkg/automation/trigger_repository_test.go b/pkg/automation/trigger_repository_test.go new file mode 100644 index 000000000..16f541ae2 --- /dev/null +++ b/pkg/automation/trigger_repository_test.go @@ -0,0 +1,81 @@ +// +build integration + +package automation + +import ( + "os" + "testing" + "time" + + "github.com/titpetric/factory" + + "github.com/cortezaproject/corteza-server/internal/test" +) + +func TestTriggerRepository_findByID(t *testing.T) { + factory.Database.Add("compose", os.Getenv("COMPOSE_DB_DSN")) + + var ( + scriptID = factory.Sonyflake.NextID() + triggerID = factory.Sonyflake.NextID() + + err error + trigger *Trigger + tt TriggerSet + script *Script + + db = factory.Database.MustGet("compose") + trepo = TriggerRepository("compose") + srepo = ScriptRepository("compose") + ) + + db.Begin() + defer db.Rollback() + + _, err = trepo.findByID(db, triggerID) + test.Error(t, err, "expecting error") + + // should have script present + script = &Script{ID: scriptID, CreatedAt: time.Now()} + err = srepo.create(db, script) + test.NoError(t, err, "unexpected error: %v") + + // runnable trigger + trigger = &Trigger{ID: triggerID, Resource: "test", Event: "test", CreatedAt: time.Now(), Enabled: true, ScriptID: scriptID} + err = trepo.create(db, trigger) + test.NoError(t, err, "unexpected error: %v") + + // find this trigger now + trigger, err = trepo.findByID(db, triggerID) + test.NoError(t, err, "trigger not found") + test.Assert(t, trigger != nil && trigger.ID == triggerID, "trigger not found") + + tt, err = trepo.findRunnable(db) + test.NoError(t, err, "unexpected error: %v") + test.Assert(t, tt.FindByID(triggerID) != nil, "trigger not found (in runnable)") + + // find the trigger through find func + tt, _, err = trepo.find(db, TriggerFilter{ + ScriptID: script.ID, + Resource: "test", + Event: "test", + }) + + test.NoError(t, err, "unexpected error: %v") + test.Assert(t, len(tt) == 1, "could not find the trigger") + test.Assert(t, tt[0].ID == triggerID, "unexpected trigger") + + err = trepo.replace(db, trigger) + test.NoError(t, err, "unexpected error when replacing the trigger: %v") + + err = trepo.update(db, trigger) + test.NoError(t, err, "unexpected error when updating the trigger: %v") + + err = trepo.deleteByScriptID(db, scriptID) + test.NoError(t, err, "unexpected error when updating the trigger: %v") + + // find this trigger now + trigger, err = trepo.findByID(db, triggerID) + test.Error(t, err, "expecting error") + +}