diff --git a/Makefile b/Makefile index d0c204eb8..e208d9747 100644 --- a/Makefile +++ b/Makefile @@ -137,10 +137,13 @@ mocks: $(GOMOCK) # Cleanup all pre-generated find . -name '*_mock_test.go' -delete rm -rf system/internal/repository/mocks && mkdir -p system/internal/repository/mocks + rm -rf compose/internal/service/mocks && mkdir -p compose/internal/service/mocks $(MOCKGEN) -package repository -source system/internal/repository/user.go -destination system/internal/repository/mocks/user.go $(MOCKGEN) -package repository -source system/internal/repository/credentials.go -destination system/internal/repository/mocks/credentials.go + $(MOCKGEN) -package service_mocks -source compose/internal/service/automation_runner.go -destination compose/internal/service/mocks/automation_runner.go + $(MOCKGEN) -package mail -source internal/mail/mail.go -destination internal/mail/mail_mock_test.go diff --git a/api/compose/spec.json b/api/compose/spec.json index 2778ac936..799e78934 100644 --- a/api/compose/spec.json +++ b/api/compose/spec.json @@ -763,28 +763,6 @@ ] } }, - { - "name": "runScript", - "method": "POST", - "title": "Trigger a specific script on record", - "path": "/run-script", - "parameters": { - "post": [ - { - "type": "uint64", - "name": "recordID", - "required": false, - "title": "Record ID" - }, - { - "type": "uint64", - "name": "scriptID", - "required": true, - "title": "Script ID" - } - ] - } - }, { "name": "upload", "path": "/attachment", @@ -1502,10 +1480,38 @@ "type": "uint64", "name": "scriptID", "required": true, - "title": "automation ID" + "title": "Script ID" } ] } + }, + { + "name": "runnable", + "method": "GET", + "title": "List of runnable (event=manual) scripts (executable on the backend or from user-agent/browser)", + "path": "/runnable", + "parameters": { + "get": [ + {"name": "resource", "type": "string", "title": "Resource"}, + {"name": "condition", "type": "string", "title": "Trigger condition"} + ] + } + }, + { + "name": "run", + "method": "POST", + "title": "Run a specific script or code at the backend. For testing and manual execution", + "path": "/run", + "parameters": { + "post": [ + {"name": "scriptID", "type": "uint64", "title": "Script ID (scripts with manual triggers)"}, + {"name": "source", "type": "string", "title": "Script's source code (overrides scriptID parameter)"}, + {"name": "moduleID", "type": "uint64", "title": "Preload module and pass it to the automation script"}, + {"name": "recordID", "type": "uint64", "title": "Preload record and pass it to the automation script"}, + {"name": "module", "type": "interface{}", "title": "Module to pass to the automation script"}, + {"name": "record", "type": "interface{}", "title": "Record to pass to the automation script"} + ] + } } ] }, diff --git a/api/compose/spec/automation_script.json b/api/compose/spec/automation_script.json index b701037a7..e804b3292 100644 --- a/api/compose/spec/automation_script.json +++ b/api/compose/spec/automation_script.json @@ -215,11 +215,71 @@ { "name": "scriptID", "required": true, - "title": "automation ID", + "title": "Script ID", "type": "uint64" } ] } + }, + { + "Name": "runnable", + "Method": "GET", + "Title": "List of runnable (event=manual) scripts (executable on the backend or from user-agent/browser)", + "Path": "/runnable", + "Parameters": { + "get": [ + { + "name": "resource", + "title": "Resource", + "type": "string" + }, + { + "name": "condition", + "title": "Trigger condition", + "type": "string" + } + ] + } + }, + { + "Name": "run", + "Method": "POST", + "Title": "Run a specific script or code at the backend. For testing and manual execution", + "Path": "/run", + "Parameters": { + "post": [ + { + "name": "scriptID", + "title": "Script ID (scripts with manual triggers)", + "type": "uint64" + }, + { + "name": "source", + "title": "Script's source code (overrides scriptID parameter)", + "type": "string" + }, + { + "name": "moduleID", + "title": "Preload module and pass it to the automation script", + "type": "uint64" + }, + { + "name": "recordID", + "title": "Preload record and pass it to the automation script", + "type": "uint64" + }, + { + "name": "module", + "title": "Module to pass to the automation script", + "type": "interface{}" + }, + { + "name": "record", + "title": "Record to pass to the automation script", + "type": "interface{}" + } + ] + } } ] } \ No newline at end of file diff --git a/api/compose/spec/record.json b/api/compose/spec/record.json index c588a7758..f6aacf563 100644 --- a/api/compose/spec/record.json +++ b/api/compose/spec/record.json @@ -199,28 +199,6 @@ ] } }, - { - "Name": "runScript", - "Method": "POST", - "Title": "Trigger a specific script on record", - "Path": "/run-script", - "Parameters": { - "post": [ - { - "name": "recordID", - "required": false, - "title": "Record ID", - "type": "uint64" - }, - { - "name": "scriptID", - "required": true, - "title": "Script ID", - "type": "uint64" - } - ] - } - }, { "Name": "upload", "Method": "POST", diff --git a/compose/db/mysql/static.go b/compose/db/mysql/static.go index aa20e1647..10b984346 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 `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" +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 `event_condition`\n 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# Migrate old triggers into scripts\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\n# Migrate old triggers into new triggers\nINSERT INTO compose_automation_trigger (id, event, resource, event_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# Normalize and cleanup\nUPDATE compose_automation_trigger SET event = 'manual' WHERE event = '';\nDELETE FROM compose_automation_trigger WHERE event_condition IN ('', '0') AND event <> 'manual';\n\n-- DROP TABLE IF EXISTS compose_trigger;\nPK\x07\x08\xec'\xc5X\x14\x11\x00\x00\x14\x11\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!(\xec'\xc5X\x14\x11\x00\x00\x14\x11\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+P\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\xe8Q\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x1a\x00\x1a\x00\x8c \x00\x00TR\x00\x00\x00\x00" diff --git a/compose/internal/service/automation_runner.go b/compose/internal/service/automation_runner.go index 6d5d7b51d..0a503eedb 100644 --- a/compose/internal/service/automation_runner.go +++ b/compose/internal/service/automation_runner.go @@ -2,9 +2,9 @@ package service import ( "context" - "strconv" "time" + "github.com/pkg/errors" "go.uber.org/zap" "google.golang.org/grpc" @@ -17,6 +17,7 @@ import ( type ( automationRunner struct { + ac automationRunnerAccessControler logger *zap.Logger runner proto.ScriptRunnerClient scriptFinder automationScriptsFinder @@ -25,12 +26,22 @@ type ( automationScriptsFinder interface { Watch(ctx context.Context) - FindRunnableScripts(event, resource string, cc ...automation.TriggerConditionChecker) automation.ScriptSet + FindRunnableScripts(resource, event string, cc ...automation.TriggerConditionChecker) automation.ScriptSet } + + automationRunnerAccessControler interface { + CanRunAutomationTrigger(ctx context.Context, r *automation.Trigger) bool + } +) + +const ( + AutomationResourceRecord = "compose:record" ) func AutomationRunner(f automationScriptsFinder, r proto.ScriptRunnerClient) automationRunner { var svc = automationRunner{ + ac: DefaultAccessControl, + scriptFinder: f, runner: r, @@ -41,42 +52,10 @@ func AutomationRunner(f automationScriptsFinder, r proto.ScriptRunnerClient) aut return svc } -func (svc automationRunner) findRecordScripts(event string, moduleID uint64) (ss automation.ScriptSet) { - const resource = "compose:record" - - // We'll be comparing strings, not uint64! - var moduleIDs = strconv.FormatUint(moduleID, 10) - - return svc.scriptFinder.FindRunnableScripts(event, resource, - // ModuleID MUST match - func(cModuleID string) bool { - return moduleIDs == cModuleID - }, - ) -} - func (svc automationRunner) Watch(ctx context.Context) { svc.scriptFinder.Watch(ctx) } -// ManualRecordRun - Manual trigger run -// -// This is explicitly called, extra security check is needed -func (svc automationRunner) ManualRecordRun(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { - // @todo security check (can user run this script (scriptID) manually) - - runner := svc.makeRecordScriptRunner(ctx, ns, m, r, true) - - return svc.findRecordScripts("manual", m.ID).Walk(func(script *automation.Script) error { - // Interested in a specific script, so skip everything else - if script.ID != scriptID { - return nil - } - - return runner(script) - }) -} - // BeforeRecordCreate - run scripts before record is created // // This is implicitly called, no extra security check is needed @@ -131,6 +110,79 @@ func (svc automationRunner) AfterRecordDelete(ctx context.Context, ns *types.Nam ) } +// Finds all scripts that are implicitly triggered by backend actions before/after +func (svc automationRunner) findRecordScripts(event string, moduleID uint64) automation.ScriptSet { + ss, _ := svc.scriptFinder.FindRunnableScripts(AutomationResourceRecord, event, automation.MakeMatcherIDCondition(moduleID)). + Filter(func(script *automation.Script) (bool, error) { + // Filter out user-agent scripts + return !script.RunInUA, nil + }) + + return ss +} + +// UserScripts - collect all scripts runnable by users, appends compatible triggers +// +// So, either in their browser (RunInUA) or by running backend scripts explicitly (event:manual) +// All triggers are permission-checked for "run" operation. +// +func (svc automationRunner) UserScripts(ctx context.Context) automation.ScriptSet { + var ss = automation.ScriptSet{} + + _ = svc.scriptFinder.FindRunnableScripts("", "").Walk(func(script *automation.Script) error { + var tt = []*automation.Trigger{} + + for _, t := range script.Triggers() { + if (script.RunInUA || t.Event == "manual") && svc.ac.CanRunAutomationTrigger(ctx, t) { + // Making a copy so that we do not corrupt the + tt = append(tt, &(*t)) + } + } + + // Have any triggers left? + if len(tt) > 0 { + var sc = &automation.Script{} + + *sc = *script + + // Replace triggers with a new set + sc.AddTrigger(automation.STMS_REPLACE, tt...) + + // andd append t + ss = append(ss, sc) + } + + return nil + }) + + return ss +} + +// ManualRecordRun - Manual trigger run +// +// This is explicitly called, extra security check is needed +func (svc automationRunner) RecordManual(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error) { + // This scripts are all prechecked & filtered + script := svc.UserScripts(ctx).FindByID(scriptID) + + if script == nil { + return errors.New("can not find compatible script") + } + + // Do not execute UA scripts + if script.RunInUA { + return errors.New("can not execute user-agent scripts") + } + + // Make record script runner and + runner := svc.makeRecordScriptRunner(ctx, ns, m, r, false) + + // Run it with a script + // + // Successfully executed record scripts can have an effect on given record value (r) + return runner(script) +} + // Runs record script // // We set-up script-running environment: security (definer / invoker), async, critical @@ -147,6 +199,18 @@ func (svc automationRunner) makeRecordScriptRunner(ctx context.Context, ns *type svc.logger.Debug("executing script", zap.Any("record", r)) return func(script *automation.Script) error { + if !script.IsValid() { + return errors.New("refusing to run invalid script") + } + + if script.RunInUA { + return errors.New("refusing to run user-agent script") + } + + if svc.runner == nil { + return errors.New("can not run corredor script: not connected") + } + // This could be executed in a goroutine (by *after triggers, // so we need ot rewire the sentry panic recoverty defer sentry.Recover() diff --git a/compose/internal/service/automation_runner_test.go b/compose/internal/service/automation_runner_test.go new file mode 100644 index 000000000..44f7797f8 --- /dev/null +++ b/compose/internal/service/automation_runner_test.go @@ -0,0 +1,108 @@ +package service + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + + service_mocks "github.com/cortezaproject/corteza-server/compose/internal/service/mocks" + "github.com/cortezaproject/corteza-server/internal/test" + "github.com/cortezaproject/corteza-server/pkg/automation" +) + +func Test_automationRunner_findImplicitScripts(t *testing.T) { + var ( + // Should be a part of users-scripts, with only one (manual) trigger + s1 = &automation.Script{ID: 1000, Enabled: true} + s1t1 = &automation.Trigger{ID: 1001, Enabled: true, Resource: "res", Condition: "5555", Event: "manual"} + s1t2 = &automation.Trigger{ID: 1002, Enabled: true, Resource: "res", Condition: "5555", Event: "beforeCreate"} + s1t3 = &automation.Trigger{ID: 1003, Enabled: true, Resource: "res", Condition: "5555", Event: "afterDelete"} + + // Should be a part of user-scripts, with all triggers + s2 = &automation.Script{ID: 2000, Enabled: true, RunInUA: true} + s2t1 = &automation.Trigger{ID: 2001, Enabled: true, Resource: "res", Condition: "5555", Event: "manual"} + s2t2 = &automation.Trigger{ID: 2002, Enabled: true, Resource: "res", Condition: "5555", Event: "beforeCreate"} + s2t3 = &automation.Trigger{ID: 2003, Enabled: true, Resource: "res", Condition: "5555", Event: "afterDelete"} + + // Should not be a part of the user-scripts + s3 = &automation.Script{ID: 3000, Enabled: true} + ) + + s1.AddTrigger(automation.STMS_REPLACE, s1t1, s1t2, s1t3) + s2.AddTrigger(automation.STMS_REPLACE, s2t1, s2t2, s2t3) + + var runnables = automation.ScriptSet{s1, s2, s3} + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + sfMock := service_mocks.NewMockautomationScriptsFinder(mockCtrl) + sfMock.EXPECT(). + FindRunnableScripts(gomock.Eq(AutomationResourceRecord), gomock.Eq("beforeCreate"), gomock.Any()). + Return(runnables) + + runner := automationRunner{ + scriptFinder: sfMock, + } + + ss := runner.findRecordScripts("beforeCreate", 5555) + + test.Assert(t, len(ss) == 2, "Received user scripts do not match") + + test.Assert(t, len(runnables) == 3, "Expected runnable scriptSet to be intact") + test.Assert(t, len(runnables.FindByID(1000).Triggers()) == 3, "Expected runnable scriptSet (triggers from first script) to be intact") +} + +func Test_automationRunner_UserScripts(t *testing.T) { + var ( + // Should be a part of users-scripts, with only one (manual) trigger + s1 = &automation.Script{ID: 1000, Enabled: true} + s1t1 = &automation.Trigger{ID: 1001, Enabled: true, Event: "manual"} + s1t2 = &automation.Trigger{ID: 1002, Enabled: true, Event: "beforeCreate"} + s1t3 = &automation.Trigger{ID: 1003, Enabled: true, Event: "afterDelete"} + + // Should be a part of user-scripts, with all triggers + s2 = &automation.Script{ID: 2000, Enabled: true, RunInUA: true} + s2t1 = &automation.Trigger{ID: 2001, Enabled: true, Event: "manual"} + s2t2 = &automation.Trigger{ID: 2002, Enabled: true, Event: "beforeCreate"} + s2t3 = &automation.Trigger{ID: 2003, Enabled: true, Event: "afterDelete"} + + // Should not be a part of the user-scripts + s3 = &automation.Script{ID: 3000, Enabled: true} + ) + + s1.AddTrigger(automation.STMS_REPLACE, s1t1, s1t2, s1t3) + s2.AddTrigger(automation.STMS_REPLACE, s2t1, s2t2, s2t3) + + var runnables = automation.ScriptSet{s1, s2, s3} + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + acAllowMock := service_mocks.NewMockautomationRunnerAccessControler(mockCtrl) + acAllowMock.EXPECT(). + CanRunAutomationTrigger(gomock.Any(), gomock.Any()). + // Should be equal to number of matching triggers + Times(4). + Return(true) + + sfMock := service_mocks.NewMockautomationScriptsFinder(mockCtrl) + sfMock.EXPECT(). + FindRunnableScripts(gomock.Eq(""), gomock.Eq("")). + Return(runnables) + + runner := automationRunner{ + ac: acAllowMock, + scriptFinder: sfMock, + } + + ss := runner.UserScripts(context.Background()) + + test.Assert(t, len(ss) == 2, "Received user scripts do not match") + test.Assert(t, len(ss.FindByID(1000).Triggers()) == 1, "Received user script triggers do not match") + test.Assert(t, len(ss.FindByID(2000).Triggers()) == 3, "Received user script triggers do not match") + + test.Assert(t, len(runnables) == 3, "Expected runnable scriptSet to be intact") + test.Assert(t, len(runnables.FindByID(1000).Triggers()) == 3, "Expected runnable scriptSet (triggers from first script) to be intact") +} diff --git a/compose/internal/service/mocks/automation_runner.go b/compose/internal/service/mocks/automation_runner.go new file mode 100644 index 000000000..b50833a8c --- /dev/null +++ b/compose/internal/service/mocks/automation_runner.go @@ -0,0 +1,97 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: compose/internal/service/automation_runner.go + +// Package service_mocks is a generated GoMock package. +package service_mocks + +import ( + context "context" + automation "github.com/cortezaproject/corteza-server/pkg/automation" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockautomationScriptsFinder is a mock of automationScriptsFinder interface +type MockautomationScriptsFinder struct { + ctrl *gomock.Controller + recorder *MockautomationScriptsFinderMockRecorder +} + +// MockautomationScriptsFinderMockRecorder is the mock recorder for MockautomationScriptsFinder +type MockautomationScriptsFinderMockRecorder struct { + mock *MockautomationScriptsFinder +} + +// NewMockautomationScriptsFinder creates a new mock instance +func NewMockautomationScriptsFinder(ctrl *gomock.Controller) *MockautomationScriptsFinder { + mock := &MockautomationScriptsFinder{ctrl: ctrl} + mock.recorder = &MockautomationScriptsFinderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockautomationScriptsFinder) EXPECT() *MockautomationScriptsFinderMockRecorder { + return m.recorder +} + +// Watch mocks base method +func (m *MockautomationScriptsFinder) Watch(ctx context.Context) { + m.ctrl.Call(m, "Watch", ctx) +} + +// Watch indicates an expected call of Watch +func (mr *MockautomationScriptsFinderMockRecorder) Watch(ctx interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockautomationScriptsFinder)(nil).Watch), ctx) +} + +// FindRunnableScripts mocks base method +func (m *MockautomationScriptsFinder) FindRunnableScripts(resource, event string, cc ...automation.TriggerConditionChecker) automation.ScriptSet { + varargs := []interface{}{resource, event} + for _, a := range cc { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "FindRunnableScripts", varargs...) + ret0, _ := ret[0].(automation.ScriptSet) + return ret0 +} + +// FindRunnableScripts indicates an expected call of FindRunnableScripts +func (mr *MockautomationScriptsFinderMockRecorder) FindRunnableScripts(resource, event interface{}, cc ...interface{}) *gomock.Call { + varargs := append([]interface{}{resource, event}, cc...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindRunnableScripts", reflect.TypeOf((*MockautomationScriptsFinder)(nil).FindRunnableScripts), varargs...) +} + +// MockautomationRunnerAccessControler is a mock of automationRunnerAccessControler interface +type MockautomationRunnerAccessControler struct { + ctrl *gomock.Controller + recorder *MockautomationRunnerAccessControlerMockRecorder +} + +// MockautomationRunnerAccessControlerMockRecorder is the mock recorder for MockautomationRunnerAccessControler +type MockautomationRunnerAccessControlerMockRecorder struct { + mock *MockautomationRunnerAccessControler +} + +// NewMockautomationRunnerAccessControler creates a new mock instance +func NewMockautomationRunnerAccessControler(ctrl *gomock.Controller) *MockautomationRunnerAccessControler { + mock := &MockautomationRunnerAccessControler{ctrl: ctrl} + mock.recorder = &MockautomationRunnerAccessControlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockautomationRunnerAccessControler) EXPECT() *MockautomationRunnerAccessControlerMockRecorder { + return m.recorder +} + +// CanRunAutomationTrigger mocks base method +func (m *MockautomationRunnerAccessControler) CanRunAutomationTrigger(ctx context.Context, r *automation.Trigger) bool { + ret := m.ctrl.Call(m, "CanRunAutomationTrigger", ctx, r) + ret0, _ := ret[0].(bool) + return ret0 +} + +// CanRunAutomationTrigger indicates an expected call of CanRunAutomationTrigger +func (mr *MockautomationRunnerAccessControlerMockRecorder) CanRunAutomationTrigger(ctx, r interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanRunAutomationTrigger", reflect.TypeOf((*MockautomationRunnerAccessControler)(nil).CanRunAutomationTrigger), ctx, r) +} diff --git a/compose/internal/service/record.go b/compose/internal/service/record.go index 80e39b64f..846d8bf85 100644 --- a/compose/internal/service/record.go +++ b/compose/internal/service/record.go @@ -41,7 +41,6 @@ type ( } RecordScriptsRunner interface { - ManualRecordRun(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error) BeforeRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) AfterRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) BeforeRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) @@ -63,8 +62,6 @@ type ( Update(record *types.Record) (*types.Record, error) DeleteByID(namespaceID, recordID uint64) error - - RunScript(namespaceID, moduleID, recordID, scriptID uint64) error } Encoder interface { diff --git a/compose/internal/service/service.go b/compose/internal/service/service.go index 27e57d98e..bc4f0b3ae 100644 --- a/compose/internal/service/service.go +++ b/compose/internal/service/service.go @@ -92,13 +92,22 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { DefaultAutomationScriptManager = AutomationScript(ias) DefaultAutomationTriggerManager = AutomationTrigger(ias) - corredor, err := automation.Corredor(ctx, c.ScriptRunner, DefaultLogger) - log.Info("initializing corredor connection", zap.String("addr", c.ScriptRunner.Addr), zap.Error(err)) - if err != nil { - return err - } + { + var scriptRunnerClient proto.ScriptRunnerClient - DefaultAutomationRunner = AutomationRunner(ias, proto.NewScriptRunnerClient(corredor)) + if c.ScriptRunner.Enabled { + corredor, err := automation.Corredor(ctx, c.ScriptRunner, DefaultLogger) + + log.Info("initializing corredor connection", zap.String("addr", c.ScriptRunner.Addr), zap.Error(err)) + if err != nil { + return err + } + + scriptRunnerClient = proto.NewScriptRunnerClient(corredor) + } + + DefaultAutomationRunner = AutomationRunner(ias, scriptRunnerClient) + } // Compose internals: DefaultNamespace = Namespace() diff --git a/compose/rest/automation_script.go b/compose/rest/automation_script.go index f80569f48..336011381 100644 --- a/compose/rest/automation_script.go +++ b/compose/rest/automation_script.go @@ -8,6 +8,7 @@ import ( "github.com/cortezaproject/corteza-server/compose/internal/service" "github.com/cortezaproject/corteza-server/compose/rest/request" + "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/automation" "github.com/cortezaproject/corteza-server/pkg/rh" ) @@ -31,9 +32,27 @@ type ( Set []*automationScriptPayload `json:"set"` } + automationScriptRunnablePayload struct { + Set []*automationScriptRunnable `json:"set"` + } + + automationScriptRunnable struct { + ScriptID uint64 `json:"scriptID,string"` + Name string `json:"name"` + Events map[string][]string `json:"events"` + Source string `json:"source,omitempty"` + Async bool `json:"async"` + RunInUA bool `json:"runInUA"` + } + AutomationScript struct { scripts automationScriptService + runner automationScriptRunner ac automationScriptAccessController + + namespace automationScriptNamespaceLoader + module automationScriptModuleLoader + record automationScriptRecordLoader } automationScriptService interface { @@ -44,18 +63,40 @@ type ( Delete(context.Context, uint64, *automation.Script) error } + automationScriptRunner interface { + UserScripts(context.Context) automation.ScriptSet + RecordManual(context.Context, uint64, *types.Namespace, *types.Module, *types.Record) (err error) + } + automationScriptAccessController interface { CanGrant(context.Context) bool CanUpdateAutomationScript(context.Context, *automation.Script) bool CanDeleteAutomationScript(context.Context, *automation.Script) bool } + + automationScriptNamespaceLoader interface { + FindByID(uint64) (*types.Namespace, error) + } + + automationScriptModuleLoader interface { + FindByID(uint64, uint64) (*types.Module, error) + } + + automationScriptRecordLoader interface { + FindByID(uint64, uint64) (*types.Record, error) + } ) func (AutomationScript) New() *AutomationScript { return &AutomationScript{ scripts: service.DefaultAutomationScriptManager, + runner: service.DefaultAutomationRunner, ac: service.DefaultAccessControl, + + namespace: service.DefaultNamespace, + module: service.DefaultModule, + record: service.DefaultRecord, } } @@ -130,6 +171,87 @@ func (ctrl AutomationScript) Delete(ctx context.Context, r *request.AutomationSc return resputil.OK(), ctrl.scripts.Delete(ctx, r.NamespaceID, script) } +func (ctrl AutomationScript) Runnable(ctx context.Context, r *request.AutomationScriptRunnable) (interface{}, error) { + var ( + rval = &automationScriptRunnablePayload{ + Set: make([]*automationScriptRunnable, 0), + } + ) + + return rval, ctrl.runner.UserScripts(ctx).Walk(func(script *automation.Script) error { + // @todo filter out all modules (by t.Condition) we do not have access to + out := &automationScriptRunnable{ + ScriptID: script.ID, + Name: script.Name, + Events: map[string][]string{}, + Async: script.Async, + RunInUA: script.RunInUA, + } + + if script.RunInUA { + out.Source = script.Source + } + + _ = script.Triggers().Walk(func(t *automation.Trigger) error { + if r.Condition != "" && r.Condition != t.Condition { + // When not requesting explicit module and condition does not match (module id or 0) + // ignore + return nil + } + + if _, ok := out.Events[t.Event]; ok { + out.Events[t.Event] = append(out.Events[t.Event], t.Condition) + } else { + out.Events[t.Event] = []string{t.Condition} + } + + return nil + }) + + if len(out.Events) == 0 { + return nil + } + rval.Set = append(rval.Set, out) + return nil + }) +} + +func (ctrl AutomationScript) Run(ctx context.Context, r *request.AutomationScriptRun) (interface{}, error) { + var ( + err error + ns *types.Namespace + module *types.Module + record *types.Record + ) + + if ns, err = ctrl.namespace.FindByID(r.NamespaceID); err != nil { + return nil, err + } + + if module, err = ctrl.module.FindByID(ns.ID, r.ModuleID); err != nil { + return nil, err + } + + if record, err = ctrl.record.FindByID(ns.ID, r.RecordID); err != nil { + return nil, err + } + + if err = ctrl.runner.RecordManual(ctx, r.ScriptID, ns, module, record); err != nil { + return nil, err + } + + // When record was passed return it. + if record != nil { + // (ab)user payload maker from record controller + // @todo find a way how to solve this more elegantly. + return (Record{ + ac: service.DefaultAccessControl, + }).makePayload(ctx, module, record, nil) + } + + return resputil.OK(), nil +} + func (ctrl AutomationScript) makePayload(ctx context.Context, s *automation.Script, err error) (*automationScriptPayload, error) { if err != nil || s == nil { return nil, err diff --git a/compose/rest/handlers/automation_script.go b/compose/rest/handlers/automation_script.go index 7085b7101..6f7c2d414 100644 --- a/compose/rest/handlers/automation_script.go +++ b/compose/rest/handlers/automation_script.go @@ -34,15 +34,19 @@ type AutomationScriptAPI interface { Read(context.Context, *request.AutomationScriptRead) (interface{}, error) Update(context.Context, *request.AutomationScriptUpdate) (interface{}, error) Delete(context.Context, *request.AutomationScriptDelete) (interface{}, error) + Runnable(context.Context, *request.AutomationScriptRunnable) (interface{}, error) + Run(context.Context, *request.AutomationScriptRun) (interface{}, error) } // HTTP API interface type AutomationScript struct { - List func(http.ResponseWriter, *http.Request) - Create func(http.ResponseWriter, *http.Request) - Read func(http.ResponseWriter, *http.Request) - Update func(http.ResponseWriter, *http.Request) - Delete func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) + Runnable func(http.ResponseWriter, *http.Request) + Run func(http.ResponseWriter, *http.Request) } func NewAutomationScript(h AutomationScriptAPI) *AutomationScript { @@ -147,6 +151,46 @@ func NewAutomationScript(h AutomationScriptAPI) *AutomationScript { resputil.JSON(w, value) } }, + Runnable: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationScriptRunnable() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationScript.Runnable", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Runnable(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationScript.Runnable", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationScript.Runnable", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Run: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationScriptRun() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationScript.Run", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Run(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationScript.Run", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationScript.Run", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, } } @@ -158,5 +202,7 @@ func (h AutomationScript) MountRoutes(r chi.Router, middlewares ...func(http.Han r.Get("/namespace/{namespaceID}/automation/script/{scriptID}", h.Read) r.Post("/namespace/{namespaceID}/automation/script/{scriptID}", h.Update) r.Delete("/namespace/{namespaceID}/automation/script/{scriptID}", h.Delete) + r.Get("/namespace/{namespaceID}/automation/script/runnable", h.Runnable) + r.Post("/namespace/{namespaceID}/automation/script/run", h.Run) }) } diff --git a/compose/rest/handlers/record.go b/compose/rest/handlers/record.go index e311ca9ad..46a2a6e13 100644 --- a/compose/rest/handlers/record.go +++ b/compose/rest/handlers/record.go @@ -36,21 +36,19 @@ type RecordAPI interface { Read(context.Context, *request.RecordRead) (interface{}, error) Update(context.Context, *request.RecordUpdate) (interface{}, error) Delete(context.Context, *request.RecordDelete) (interface{}, error) - RunScript(context.Context, *request.RecordRunScript) (interface{}, error) Upload(context.Context, *request.RecordUpload) (interface{}, error) } // HTTP API interface type Record struct { - Report func(http.ResponseWriter, *http.Request) - List func(http.ResponseWriter, *http.Request) - Export func(http.ResponseWriter, *http.Request) - Create func(http.ResponseWriter, *http.Request) - Read func(http.ResponseWriter, *http.Request) - Update func(http.ResponseWriter, *http.Request) - Delete func(http.ResponseWriter, *http.Request) - RunScript func(http.ResponseWriter, *http.Request) - Upload func(http.ResponseWriter, *http.Request) + Report func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Export func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) + Upload func(http.ResponseWriter, *http.Request) } func NewRecord(h RecordAPI) *Record { @@ -195,26 +193,6 @@ func NewRecord(h RecordAPI) *Record { resputil.JSON(w, value) } }, - RunScript: func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - params := request.NewRecordRunScript() - if err := params.Fill(r); err != nil { - logger.LogParamError("Record.RunScript", r, err) - resputil.JSON(w, err) - return - } - - value, err := h.RunScript(r.Context(), params) - if err != nil { - logger.LogControllerError("Record.RunScript", r, err, params.Auditable()) - resputil.JSON(w, err) - return - } - logger.LogControllerCall("Record.RunScript", r, params.Auditable()) - if !serveHTTP(value, w, r) { - resputil.JSON(w, value) - } - }, Upload: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewRecordUpload() @@ -248,7 +226,6 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Read) r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Update) r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Delete) - r.Post("/namespace/{namespaceID}/module/{moduleID}/record/run-script", h.RunScript) r.Post("/namespace/{namespaceID}/module/{moduleID}/record/attachment", h.Upload) }) } diff --git a/compose/rest/module.go b/compose/rest/module.go index d362010e0..876f512d0 100644 --- a/compose/rest/module.go +++ b/compose/rest/module.go @@ -11,6 +11,11 @@ import ( ) type ( + moduleSetPayload struct { + Filter types.ModuleFilter `json:"filter"` + Set []*modulePayload `json:"set"` + } + modulePayload struct { *types.Module @@ -34,14 +39,8 @@ type ( CanUpdateRecordValue bool `json:"canUpdateRecordValue"` } - moduleSetPayload struct { - Filter types.ModuleFilter `json:"filter"` - Set []*modulePayload `json:"set"` - } - Module struct { module service.ModuleService - record service.RecordService ac moduleAccessController } @@ -65,7 +64,6 @@ type ( func (Module) New() *Module { return &Module{ module: service.DefaultModule, - record: service.DefaultRecord, ac: service.DefaultAccessControl, } } diff --git a/compose/rest/record.go b/compose/rest/record.go index c892b8f1e..b4a59c588 100644 --- a/compose/rest/record.go +++ b/compose/rest/record.go @@ -229,15 +229,6 @@ func (ctrl *Record) Export(ctx context.Context, r *request.RecordExport) (interf }, nil } -func (ctrl *Record) RunScript(ctx context.Context, r *request.RecordRunScript) (interface{}, error) { - return resputil.OK(), ctrl.record.RunScript( - r.NamespaceID, - r.ModuleID, - r.RecordID, - r.ScriptID, - ) -} - func (ctrl Record) makePayload(ctx context.Context, m *types.Module, r *types.Record, err error) (*recordPayload, error) { if err != nil || r == nil { return nil, err diff --git a/compose/rest/request/automation_script.go b/compose/rest/request/automation_script.go index dc2e352e4..0e1375500 100644 --- a/compose/rest/request/automation_script.go +++ b/compose/rest/request/automation_script.go @@ -414,3 +414,145 @@ func (r *AutomationScriptDelete) Fill(req *http.Request) (err error) { } var _ RequestFiller = NewAutomationScriptDelete() + +// AutomationScript runnable request parameters +type AutomationScriptRunnable struct { + Resource string + Condition string + NamespaceID uint64 `json:",string"` +} + +func NewAutomationScriptRunnable() *AutomationScriptRunnable { + return &AutomationScriptRunnable{} +} + +func (r AutomationScriptRunnable) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["resource"] = r.Resource + out["condition"] = r.Condition + out["namespaceID"] = r.NamespaceID + + return out +} + +func (r *AutomationScriptRunnable) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["resource"]; ok { + r.Resource = val + } + if val, ok := get["condition"]; ok { + r.Condition = val + } + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + + return err +} + +var _ RequestFiller = NewAutomationScriptRunnable() + +// AutomationScript run request parameters +type AutomationScriptRun struct { + ScriptID uint64 `json:",string"` + Source string + ModuleID uint64 `json:",string"` + RecordID uint64 `json:",string"` + Module interface{} + Record interface{} + NamespaceID uint64 `json:",string"` +} + +func NewAutomationScriptRun() *AutomationScriptRun { + return &AutomationScriptRun{} +} + +func (r AutomationScriptRun) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["scriptID"] = r.ScriptID + out["source"] = r.Source + out["moduleID"] = r.ModuleID + out["recordID"] = r.RecordID + out["module"] = r.Module + out["record"] = r.Record + out["namespaceID"] = r.NamespaceID + + return out +} + +func (r *AutomationScriptRun) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["scriptID"]; ok { + r.ScriptID = parseUInt64(val) + } + if val, ok := post["source"]; ok { + r.Source = val + } + if val, ok := post["moduleID"]; ok { + r.ModuleID = parseUInt64(val) + } + if val, ok := post["recordID"]; ok { + r.RecordID = parseUInt64(val) + } + if val, ok := post["module"]; ok { + r.Module = interface{}(val) + } + if val, ok := post["record"]; ok { + r.Record = interface{}(val) + } + r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) + + return err +} + +var _ RequestFiller = NewAutomationScriptRun() diff --git a/compose/rest/request/record.go b/compose/rest/request/record.go index 81e91ec0b..8cc5b9d77 100644 --- a/compose/rest/request/record.go +++ b/compose/rest/request/record.go @@ -478,70 +478,6 @@ func (r *RecordDelete) Fill(req *http.Request) (err error) { var _ RequestFiller = NewRecordDelete() -// Record runScript request parameters -type RecordRunScript struct { - RecordID uint64 `json:",string"` - ScriptID uint64 `json:",string"` - NamespaceID uint64 `json:",string"` - ModuleID uint64 `json:",string"` -} - -func NewRecordRunScript() *RecordRunScript { - return &RecordRunScript{} -} - -func (r RecordRunScript) Auditable() map[string]interface{} { - var out = map[string]interface{}{} - - out["recordID"] = r.RecordID - out["scriptID"] = r.ScriptID - out["namespaceID"] = r.NamespaceID - out["moduleID"] = r.ModuleID - - return out -} - -func (r *RecordRunScript) Fill(req *http.Request) (err error) { - if strings.ToLower(req.Header.Get("content-type")) == "application/json" { - err = json.NewDecoder(req.Body).Decode(r) - - switch { - case err == io.EOF: - err = nil - case err != nil: - return errors.Wrap(err, "error parsing http request body") - } - } - - if err = req.ParseForm(); err != nil { - return err - } - - get := map[string]string{} - post := map[string]string{} - urlQuery := req.URL.Query() - for name, param := range urlQuery { - get[name] = string(param[0]) - } - postVars := req.Form - for name, param := range postVars { - post[name] = string(param[0]) - } - - if val, ok := post["recordID"]; ok { - r.RecordID = parseUInt64(val) - } - if val, ok := post["scriptID"]; ok { - r.ScriptID = parseUInt64(val) - } - r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID")) - r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID")) - - return err -} - -var _ RequestFiller = NewRecordRunScript() - // Record upload request parameters type RecordUpload struct { RecordID uint64 `json:",string"` diff --git a/docs/compose/README.md b/docs/compose/README.md index 326d84c92..fe9c19dcb 100644 --- a/docs/compose/README.md +++ b/docs/compose/README.md @@ -120,6 +120,8 @@ | `GET` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Read automation script by ID | | `POST` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Update automation script | | `DELETE` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Delete script | +| `GET` | `/namespace/{namespaceID}/automation/script/runnable` | List of runnable (event=manual) scripts (executable on the backend or from user-agent/browser) | +| `POST` | `/namespace/{namespaceID}/automation/script/run` | Run a specific script or code at the backend. For testing and manual execution | ## List/read automation script @@ -216,7 +218,43 @@ | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | -| scriptID | uint64 | PATH | automation ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | + +## List of runnable (event=manual) scripts (executable on the backend or from user-agent/browser) + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/runnable` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| resource | string | GET | Resource | N/A | NO | +| condition | string | GET | Trigger condition | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | + +## Run a specific script or code at the backend. For testing and manual execution + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/automation/script/run` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| scriptID | uint64 | POST | Script ID (scripts with manual triggers) | N/A | NO | +| source | string | POST | Script's source code (overrides scriptID parameter) | N/A | NO | +| moduleID | uint64 | POST | Preload module and pass it to the automation script | N/A | NO | +| recordID | uint64 | POST | Preload record and pass it to the automation script | N/A | NO | +| module | interface{} | POST | Module to pass to the automation script | N/A | NO | +| record | interface{} | POST | Record to pass to the automation script | N/A | NO | | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | --- @@ -905,7 +943,6 @@ Compose records | `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Read records by ID from module section | | `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Update records in module section | | `DELETE` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Delete record row from module section | -| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/run-script` | Trigger a specific script on record | | `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/attachment` | Uploads attachment and validates it against record field requirements | ## Generates report from module records @@ -1029,23 +1066,6 @@ Compose records | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | -## Trigger a specific script on record - -#### Method - -| URI | Protocol | Method | Authentication | -| --- | -------- | ------ | -------------- | -| `/namespace/{namespaceID}/module/{moduleID}/record/run-script` | HTTP/S | POST | | - -#### Request parameters - -| Parameter | Type | Method | Description | Default | Required? | -| --------- | ---- | ------ | ----------- | ------- | --------- | -| recordID | uint64 | POST | Record ID | N/A | NO | -| scriptID | uint64 | POST | Script ID | N/A | YES | -| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | -| moduleID | uint64 | PATH | Module ID | N/A | YES | - ## Uploads attachment and validates it against record field requirements #### Method diff --git a/pkg/automation/script.go b/pkg/automation/script.go index 850ec8384..b0862dd59 100644 --- a/pkg/automation/script.go +++ b/pkg/automation/script.go @@ -1,6 +1,7 @@ package automation import ( + "strconv" "time" "github.com/pkg/errors" @@ -127,12 +128,12 @@ func (s *Script) CheckCompatibility(t *Trigger) error { return nil } -// FilterByEvent +// FilterByTrigger // -// we will use the Trigger struct as a holder for conditions -func (set ScriptSet) FilterByEvent(event, resource string, cc ...TriggerConditionChecker) (out ScriptSet) { +// Filters non-UA scripts that match event and resource + all extra conditions +func (set ScriptSet) FilterByTrigger(event, resource string, cc ...TriggerConditionChecker) (out ScriptSet) { out, _ = set.Filter(func(s *Script) (bool, error) { - return s.triggers.HasMatch(Trigger{Event: event, Resource: resource}, cc...), nil + return s.IsValid() && s.triggers.HasMatch(Trigger{Event: event, Resource: resource}, cc...), nil }) return @@ -170,3 +171,20 @@ func (s *Script) AddTrigger(strategy triggersMergeStrategy, tt ...*Trigger) { } } } + +func (s *Script) Triggers() TriggerSet { + return s.triggers +} + +func (s Script) HasEvent(event string) bool { + return s.triggers.HasMatch(Trigger{Event: event}) +} + +func MakeMatcherIDCondition(id uint64) TriggerConditionChecker { + // We'll be comparing strings, not uint64! + var s = strconv.FormatUint(id, 10) + + return func(c string) bool { + return s == c + } +} diff --git a/pkg/automation/service.go b/pkg/automation/service.go index d664c0d10..0a1e45908 100644 --- a/pkg/automation/service.go +++ b/pkg/automation/service.go @@ -33,7 +33,7 @@ type ( } ScriptsProvider interface { - FilterByEvent(event, resource string, cc ...TriggerConditionChecker) ScriptSet + FilterByTrigger(event, resource string, cc ...TriggerConditionChecker) ScriptSet } WatcherService interface { @@ -66,6 +66,8 @@ func Service(c AutomationServiceConfig) (svc *service) { trepo: TriggerRepository(c.DbTablePrefix), db: c.DB, + + f: make(chan bool, 64), } // Reload ASAP @@ -74,8 +76,7 @@ func Service(c AutomationServiceConfig) (svc *service) { } // Watch watches for changes -func (svc service) Watch(ctx context.Context) { - svc.f = make(chan bool) +func (svc *service) Watch(ctx context.Context) { go func() { defer sentry.Recover() defer close(svc.f) @@ -102,67 +103,77 @@ func (svc service) Watch(ctx context.Context) { } func (svc *service) Reload() { - select { - case svc.f <- true: - return - default: - // that's ok too.. - } + go func() { + 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() - if svc.c.DB == nil { - return - } - var ( - err error - ss ScriptSet - tt TriggerSet + ss ScriptSet + tt TriggerSet + db = svc.db.With(ctx) ) - ss, err = svc.srepo.findRunnable(svc.db) - svc.logger.Info("scripts loaded", zap.Error(err), zap.Int("count", len(tt))) - if err != nil { - return - } - - // Only interested in valid scritps - ss, _ = ss.Filter(func(s *Script) (b bool, e error) { - return s.IsValid(), nil - }) - - tt, err = svc.trepo.findRunnable(svc.db) - svc.logger.Info("triggers loaded", zap.Error(err), zap.Int("count", len(tt))) - if err != nil { - return - } - - _ = tt.Walk(func(t *Trigger) error { - s := ss.FindByID(t.ScriptID) - if s != nil && t.IsValid() && s.CheckCompatibility(t) != nil { - // Add only compatible triggers - s.triggers = append(s.triggers, t) + _ = db.Transaction(func() (err error) { + ss, err = svc.srepo.findRunnable(db) + svc.logger.Info("scripts loaded", zap.Error(err), zap.Int("count", len(ss))) + if err != nil { + return } - return nil + // Remove all invalid scripts + svc.runnables, _ = ss.Filter(func(s *Script) (b bool, e error) { + return s.IsValid(), nil + }) + + tt, err = svc.trepo.findRunnable(db) + svc.logger.Info("triggers loaded", zap.Error(err), zap.Int("count", len(tt))) + if err != nil { + return + } + + return tt.Walk(func(t *Trigger) error { + s := svc.runnables.FindByID(t.ScriptID) + if s != nil && t.IsValid() && s.CheckCompatibility(t) == nil { + // Add only compatible triggers + s.triggers = append(s.triggers, t) + } + + return nil + }) }) } -// FindRunnableScripts scans internal list of runnable scripts and filters them by (trigger's) event and origin -func (svc service) FindRunnableScripts(event, origin string, cc ...TriggerConditionChecker) ScriptSet { - return svc.runnables.FilterByEvent(event, origin, cc...) +// FindRunnableScripts finds runnable scripts in internal list +// +// It uses resource, event and extra condition checkers to filter out all scripts +// that have matching triggers +func (svc service) FindRunnableScripts(resource, event string, cc ...TriggerConditionChecker) ScriptSet { + svc.l.Lock() + defer svc.l.Unlock() + + return svc.runnables.FilterByTrigger( + event, + resource, + cc..., + ) } func (svc service) FindScriptByID(ctx context.Context, scriptID uint64) (*Script, error) { - return svc.srepo.findByID(svc.db, scriptID) + return svc.srepo.findByID(svc.db.With(ctx), scriptID) } func (svc service) FindScripts(ctx context.Context, f ScriptFilter) (ScriptSet, ScriptFilter, error) { - return svc.srepo.find(svc.db, f) + return svc.srepo.find(svc.db.With(ctx), f) } // CreateScript - modifies script's props, pushes to repo & updates scripts cache @@ -171,8 +182,13 @@ func (svc service) CreateScript(ctx context.Context, s *Script) error { s.CreatedAt = time.Now() s.CreatedBy = auth.GetIdentityFromContext(ctx).Identity() - return svc.db.Transaction(func() (err error) { - if err = svc.srepo.create(svc.db, s); err != nil { + db := svc.db.With(ctx) + + // Reloading scripts at the end (after the transaction completes) + defer svc.Reload() + + return db.Transaction(func() (err error) { + if err = svc.srepo.create(db, s); err != nil { return } @@ -185,11 +201,10 @@ func (svc service) CreateScript(ctx context.Context, s *Script) error { } // Force no-pre-check - if err = svc.trepo.mergeSet(svc.db, STMS_FRESH, s.ID, s.triggers); err != nil { + if err = svc.trepo.mergeSet(db, STMS_FRESH, s.ID, s.triggers); err != nil { return } - svc.Reload() return }) } @@ -201,8 +216,13 @@ func (svc service) UpdateScript(ctx context.Context, s *Script) error { *s.UpdatedAt = time.Now() s.DeletedAt, s.DeletedBy = nil, 0 - return svc.db.Transaction(func() (err error) { - if err = svc.srepo.update(svc.db, s); err != nil { + db := svc.db.With(ctx) + + // Reloading scripts at the end (after the transaction completes) + defer svc.Reload() + + return db.Transaction(func() (err error) { + if err = svc.srepo.update(db, s); err != nil { return } @@ -218,11 +238,10 @@ func (svc service) UpdateScript(ctx context.Context, s *Script) error { return } - if err = svc.trepo.mergeSet(svc.db, s.tms, s.ID, s.triggers); err != nil { + if err = svc.trepo.mergeSet(db, s.tms, s.ID, s.triggers); err != nil { return } - svc.Reload() return }) } @@ -233,25 +252,31 @@ func (svc service) DeleteScript(ctx context.Context, s *Script) (err error) { *s.DeletedAt = time.Now() s.DeletedBy = auth.GetIdentityFromContext(ctx).Identity() - // We're doing soft delete in the repo - if err = svc.srepo.update(svc.db, s); err != nil { - return err - } + db := svc.db.With(ctx) - if err = svc.trepo.deleteByScriptID(svc.db, s.ID); err != nil { - return err - } + // Reloading scripts at the end (after the transaction completes) + defer svc.Reload() - svc.Reload() - return + return db.Transaction(func() error { + // We're doing soft delete in the repo + if err = svc.srepo.update(db, s); err != nil { + return err + } + + if err = svc.trepo.deleteByScriptID(db, s.ID); err != nil { + return err + } + + return nil + }) } func (svc service) FindTriggerByID(ctx context.Context, scriptID uint64) (*Trigger, error) { - return svc.trepo.findByID(svc.db, scriptID) + return svc.trepo.findByID(svc.db.With(ctx), scriptID) } func (svc service) FindTriggers(ctx context.Context, f TriggerFilter) (TriggerSet, TriggerFilter, error) { - return svc.trepo.find(svc.db, f) + return svc.trepo.find(svc.db.With(ctx), f) } // CreateTrigger - modifies script's props, pushes to repo & updates scripts cache @@ -260,7 +285,7 @@ func (svc service) CreateTrigger(ctx context.Context, s *Script, t *Trigger) (er return err } - if err = svc.trepo.replace(svc.db, t); err != nil { + if err = svc.trepo.replace(svc.db.With(ctx), t); err != nil { return err } @@ -286,7 +311,7 @@ func (svc service) UpdateTrigger(ctx context.Context, s *Script, t *Trigger) (er return err } - if err = svc.trepo.replace(svc.db, t); err != nil { + if err = svc.trepo.replace(svc.db.With(ctx), t); err != nil { return err } @@ -316,7 +341,7 @@ 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.replace(svc.db, t); err != nil { + if err = svc.trepo.replace(svc.db.With(ctx), t); err != nil { return err } diff --git a/pkg/automation/trigger.go b/pkg/automation/trigger.go index 4febf07b7..223530dd1 100644 --- a/pkg/automation/trigger.go +++ b/pkg/automation/trigger.go @@ -95,12 +95,12 @@ withTriggers: continue withTriggers } - if m.Resource != t.Resource { + if m.Resource != "" && m.Resource != t.Resource { // event should match continue withTriggers } - if m.Event != t.Event { + if m.Event != "" && m.Event != t.Event { // event should match continue withTriggers } diff --git a/pkg/automation/trigger_test.go b/pkg/automation/trigger_test.go index 15f732f78..9f1cf6604 100644 --- a/pkg/automation/trigger_test.go +++ b/pkg/automation/trigger_test.go @@ -22,7 +22,7 @@ func TestTriggerSet_HasMatch(t *testing.T) { }, { name: "simple miss", set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: true}, nil, &Trigger{}}, - args: args{m: Trigger{}}, + args: args{m: Trigger{Event: "E"}}, want: false, }, { name: "specific",