diff --git a/api/crm/spec.json b/api/crm/spec.json index c6ad59bfc..38ec504e6 100644 --- a/api/crm/spec.json +++ b/api/crm/spec.json @@ -605,5 +605,151 @@ } } ] + }, + { + "title": "Triggers", + "description": "CRM Triggers", + "package": "crm", + "entrypoint": "trigger", + "path": "/trigger", + "authentication": [], + "apis": [ + { + "name": "list", + "method": "GET", + "path": "/", + "title": "List available triggers", + "parameters": { + "get": [ + { + "type": "uint64", + "name": "moduleID", + "required": false, + "title": "Filter triggers by module" + } + ] + } + }, + { + "name": "create", + "method": "POST", + "title": "Create trigger", + "path": "/", + "parameters": { + "post": [ + { + "type": "uint64", + "name": "moduleID", + "required": false, + "title": "Module ID" + }, + { + "type": "string", + "name": "name", + "required": true, + "title": "Name" + }, + { + "type": "[]string", + "name": "actions", + "required": false, + "title": "Actions that trigger this trigger" + }, + { + "type": "bool", + "name": "enabled", + "required": false, + "title": "Enabled" + }, + { + "type": "string", + "name": "source", + "required": false, + "title": "Trigger source code" + } + ] + } + }, + { + "name": "read", + "path": "/{triggerID}", + "method": "GET", + "title": "Get trigger details", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "Trigger ID" + } + ] + } + }, + { + "name": "edit", + "method": "POST", + "title": "Edit trigger", + "path": "/{triggerID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "Trigger ID" + } + ], + "post": [ + { + "type": "uint64", + "name": "moduleID", + "required": false, + "title": "Module ID" + }, + { + "type": "string", + "name": "name", + "required": true, + "title": "Name" + }, + { + "type": "[]string", + "name": "actions", + "required": false, + "title": "Actions that trigger this trigger" + }, + { + "type": "bool", + "name": "enabled", + "required": false, + "title": "Enabled" + }, + { + "type": "string", + "name": "source", + "required": false, + "title": "Trigger source code" + } + ] + } + }, + { + "name": "delete", + "path": "/{triggerID}", + "method": "Delete", + "title": "Delete trigger", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "Trigger ID" + } + ] + } + } + ] } ] diff --git a/api/crm/spec/trigger.json b/api/crm/spec/trigger.json new file mode 100644 index 000000000..af3cf6f01 --- /dev/null +++ b/api/crm/spec/trigger.json @@ -0,0 +1,149 @@ +{ + "Title": "Triggers", + "Description": "CRM Triggers", + "Package": "crm", + "Interface": "Trigger", + "Struct": null, + "Parameters": null, + "Protocol": "", + "Authentication": [], + "Path": "/trigger", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "List available triggers", + "Path": "/", + "Parameters": { + "get": [ + { + "name": "moduleID", + "required": false, + "title": "Filter triggers by module", + "type": "uint64" + } + ] + } + }, + { + "Name": "create", + "Method": "POST", + "Title": "Create trigger", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "moduleID", + "required": false, + "title": "Module ID", + "type": "uint64" + }, + { + "name": "name", + "required": true, + "title": "Name", + "type": "string" + }, + { + "name": "actions", + "required": false, + "title": "Actions that trigger this trigger", + "type": "[]string" + }, + { + "name": "enabled", + "required": false, + "title": "Enabled", + "type": "bool" + }, + { + "name": "source", + "required": false, + "title": "Trigger source code", + "type": "string" + } + ] + } + }, + { + "Name": "read", + "Method": "GET", + "Title": "Get trigger details", + "Path": "/{triggerID}", + "Parameters": { + "path": [ + { + "name": "triggerID", + "required": true, + "title": "Trigger ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "edit", + "Method": "POST", + "Title": "Edit trigger", + "Path": "/{triggerID}", + "Parameters": { + "path": [ + { + "name": "triggerID", + "required": true, + "title": "Trigger ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "moduleID", + "required": false, + "title": "Module ID", + "type": "uint64" + }, + { + "name": "name", + "required": true, + "title": "Name", + "type": "string" + }, + { + "name": "actions", + "required": false, + "title": "Actions that trigger this trigger", + "type": "[]string" + }, + { + "name": "enabled", + "required": false, + "title": "Enabled", + "type": "bool" + }, + { + "name": "source", + "required": false, + "title": "Trigger source code", + "type": "string" + } + ] + } + }, + { + "Name": "delete", + "Method": "Delete", + "Title": "Delete trigger", + "Path": "/{triggerID}", + "Parameters": { + "path": [ + { + "name": "triggerID", + "required": true, + "title": "Trigger ID", + "type": "uint64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/codegen.sh b/codegen.sh index ed793293d..2b52dfa7c 100755 --- a/codegen.sh +++ b/codegen.sh @@ -37,7 +37,7 @@ function types { CGO_ENABLED=0 go build -o ./build/gen-type-set codegen/v2/type-set.go fi - ./build/gen-type-set --types Module,Page,Chart -no-pk-types ModuleField --output crm/types/type.gen.go + ./build/gen-type-set --types Module,Page,Chart,Trigger -no-pk-types ModuleField --output crm/types/type.gen.go ./build/gen-type-set --types MessageAttachment --output sam/types/attachment.gen.go ./build/gen-type-set --types Channel --output sam/types/channel.gen.go diff --git a/crm/db/mysql/statik.go b/crm/db/mysql/statik.go index c4bcf4dc2..43d5f36f0 100644 --- a/crm/db/mysql/statik.go +++ b/crm/db/mysql/statik.go @@ -8,7 +8,7 @@ import ( ) func Data() string { - return "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\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.sqlPK\x07\x08s\xd4N*.\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!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81r\x18\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81/\x1a\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0e\x00\x0e\x00-\x05\x00\x00\x9a\x1a\x00\x00\x00\x00" + return "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\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.sqlPK\x07\x08s\xd4N*.\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!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x87\x1b\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81D\x1d\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0f\x00\x0f\x00\x8a\x05\x00\x00\xaf\x1d\x00\x00\x00\x00" } func init() { diff --git a/crm/db/schema/mysql/20190108100000.add-triggers-tbl.up.sql b/crm/db/schema/mysql/20190108100000.add-triggers-tbl.up.sql new file mode 100644 index 000000000..c04ebbdd3 --- /dev/null +++ b/crm/db/schema/mysql/20190108100000.add-triggers-tbl.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `crm_trigger` ( + `id` BIGINT(20) UNSIGNED NOT NULL, + `name` VARCHAR(64) NOT NULL COMMENT 'The name of the trigger', + `enabled` BOOLEAN NOT NULL COMMENT 'Trigger enabled?', + `actions` TEXT NOT NULL COMMENT 'All actions that trigger it', + `source` TEXT NOT NULL COMMENT 'Trigger source', + `rel_module` BIGINT(20) UNSIGNED NULL COMMENT 'Primary module', + + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME DEFAULT NULL, + `deleted_at` DATETIME DEFAULT NULL, + + PRIMARY KEY (`id`) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/crm/repository/trigger.go b/crm/repository/trigger.go new file mode 100644 index 000000000..c2825952a --- /dev/null +++ b/crm/repository/trigger.go @@ -0,0 +1,94 @@ +package repository + +import ( + "context" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/crm/types" +) + +type ( + TriggerRepository interface { + With(ctx context.Context, db *factory.DB) TriggerRepository + + FindByID(id uint64) (*types.Trigger, error) + Find(filter types.TriggerFilter) (types.TriggerSet, error) + Create(mod *types.Trigger) (*types.Trigger, error) + Update(mod *types.Trigger) (*types.Trigger, error) + DeleteByID(id uint64) error + } + + trigger struct { + *repository + } +) + +func Trigger(ctx context.Context, db *factory.DB) TriggerRepository { + return (&trigger{}).With(ctx, db) +} + +func (r *trigger) With(ctx context.Context, db *factory.DB) TriggerRepository { + return &trigger{ + repository: r.repository.With(ctx, db), + } +} + +func (r *trigger) FindByID(id uint64) (*types.Trigger, error) { + mod := &types.Trigger{} + query := r.query().Where("id = ?", id) + + if sql, args, err := query.ToSql(); err != nil { + return nil, err + } else { + return mod, r.db().Get(mod, sql, args...) + } +} + +func (r *trigger) Find(filter types.TriggerFilter) (mod types.TriggerSet, err error) { + query := r.query() + + if filter.ModuleID > 0 { + query = query.Where("rel_module = ?", filter.ModuleID) + } + spew.Dump(filter) + spew.Dump(query.ToSql()) + + if sql, args, err := query.ToSql(); err != nil { + return nil, err + } else { + return mod, r.db().Select(&mod, sql, args...) + } +} + +func (r trigger) query() (query squirrel.SelectBuilder) { + query = squirrel.Select(). + Columns( + "id", "name", "actions", "enabled", "source", "rel_module", + "created_at", "updated_at", "deleted_at"). + From("crm_trigger"). + OrderBy("id DESC") + + return +} + +func (r *trigger) Create(mod *types.Trigger) (*types.Trigger, error) { + mod.ID = factory.Sonyflake.NextID() + mod.CreatedAt = time.Now() + + return mod, r.db().Insert("crm_trigger", mod) +} + +func (r *trigger) Update(mod *types.Trigger) (*types.Trigger, error) { + now := time.Now() + mod.UpdatedAt = &now + return mod, r.db().Replace("crm_trigger", mod) +} + +func (r *trigger) DeleteByID(id uint64) error { + _, err := r.db().Exec("DELETE FROM crm_trigger WHERE id = ?", id) + return err +} diff --git a/crm/rest/handlers/trigger.go b/crm/rest/handlers/trigger.go new file mode 100644 index 000000000..69f3c1798 --- /dev/null +++ b/crm/rest/handlers/trigger.go @@ -0,0 +1,97 @@ +package handlers + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `trigger.go`, `trigger.util.go` or `trigger_test.go` to + implement your API calls, helper functions and tests. The file `trigger.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + "github.com/go-chi/chi" + "net/http" + + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/crm/rest/request" +) + +// Internal API interface +type TriggerAPI interface { + List(context.Context, *request.TriggerList) (interface{}, error) + Create(context.Context, *request.TriggerCreate) (interface{}, error) + Read(context.Context, *request.TriggerRead) (interface{}, error) + Edit(context.Context, *request.TriggerEdit) (interface{}, error) + Delete(context.Context, *request.TriggerDelete) (interface{}, error) +} + +// HTTP API interface +type Trigger struct { + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Edit func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) +} + +func NewTrigger(th TriggerAPI) *Trigger { + return &Trigger{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewTriggerList() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return th.List(r.Context(), params) + }) + }, + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewTriggerCreate() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return th.Create(r.Context(), params) + }) + }, + Read: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewTriggerRead() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return th.Read(r.Context(), params) + }) + }, + Edit: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewTriggerEdit() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return th.Edit(r.Context(), params) + }) + }, + Delete: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewTriggerDelete() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return th.Delete(r.Context(), params) + }) + }, + } +} + +func (th *Trigger) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Route("/trigger", func(r chi.Router) { + r.Get("/", th.List) + r.Post("/", th.Create) + r.Get("/{triggerID}", th.Read) + r.Post("/{triggerID}", th.Edit) + r.Delete("/{triggerID}", th.Delete) + }) + }) +} diff --git a/crm/rest/request/trigger.go b/crm/rest/request/trigger.go new file mode 100644 index 000000000..0b180539d --- /dev/null +++ b/crm/rest/request/trigger.go @@ -0,0 +1,288 @@ +package request + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `trigger.go`, `trigger.util.go` or `trigger_test.go` to + implement your API calls, helper functions and tests. The file `trigger.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "encoding/json" + "io" + "mime/multipart" + "net/http" + "strings" + + "github.com/go-chi/chi" + "github.com/pkg/errors" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// Trigger list request parameters +type TriggerList struct { + ModuleID uint64 `json:",string"` +} + +func NewTriggerList() *TriggerList { + return &TriggerList{} +} + +func (t *TriggerList) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(t) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["moduleID"]; ok { + + t.ModuleID = parseUInt64(val) + } + + return err +} + +var _ RequestFiller = NewTriggerList() + +// Trigger create request parameters +type TriggerCreate struct { + ModuleID uint64 `json:",string"` + Name string + Actions []string + Enabled bool + Source string +} + +func NewTriggerCreate() *TriggerCreate { + return &TriggerCreate{} +} + +func (t *TriggerCreate) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(t) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["moduleID"]; ok { + + t.ModuleID = parseUInt64(val) + } + if val, ok := post["name"]; ok { + + t.Name = val + } + if val, ok := post["enabled"]; ok { + + t.Enabled = parseBool(val) + } + if val, ok := post["source"]; ok { + + t.Source = val + } + + return err +} + +var _ RequestFiller = NewTriggerCreate() + +// Trigger read request parameters +type TriggerRead struct { + TriggerID uint64 `json:",string"` +} + +func NewTriggerRead() *TriggerRead { + return &TriggerRead{} +} + +func (t *TriggerRead) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(t) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + t.TriggerID = parseUInt64(chi.URLParam(r, "triggerID")) + + return err +} + +var _ RequestFiller = NewTriggerRead() + +// Trigger edit request parameters +type TriggerEdit struct { + TriggerID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + Name string + Actions []string + Enabled bool + Source string +} + +func NewTriggerEdit() *TriggerEdit { + return &TriggerEdit{} +} + +func (t *TriggerEdit) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(t) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + t.TriggerID = parseUInt64(chi.URLParam(r, "triggerID")) + if val, ok := post["moduleID"]; ok { + + t.ModuleID = parseUInt64(val) + } + if val, ok := post["name"]; ok { + + t.Name = val + } + if val, ok := post["enabled"]; ok { + + t.Enabled = parseBool(val) + } + if val, ok := post["source"]; ok { + + t.Source = val + } + + return err +} + +var _ RequestFiller = NewTriggerEdit() + +// Trigger delete request parameters +type TriggerDelete struct { + TriggerID uint64 `json:",string"` +} + +func NewTriggerDelete() *TriggerDelete { + return &TriggerDelete{} +} + +func (t *TriggerDelete) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(t) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + t.TriggerID = parseUInt64(chi.URLParam(r, "triggerID")) + + return err +} + +var _ RequestFiller = NewTriggerDelete() diff --git a/crm/rest/router.go b/crm/rest/router.go index 9410c7304..e8e705b04 100644 --- a/crm/rest/router.go +++ b/crm/rest/router.go @@ -9,11 +9,10 @@ import ( func MountRoutes(jwtAuth auth.TokenEncoder) func(chi.Router) { var ( - module = Module{}.New() - page = Page{}.New() - chart = Chart{}.New() - workflow = Workflow{}.New() - job = Job{}.New() + module = Module{}.New() + page = Page{}.New() + chart = Chart{}.New() + trigger = Trigger{}.New() ) // Initialize handers & controllers. @@ -25,8 +24,7 @@ func MountRoutes(jwtAuth auth.TokenEncoder) func(chi.Router) { handlers.NewPage(page).MountRoutes(r) handlers.NewModule(module).MountRoutes(r) handlers.NewChart(chart).MountRoutes(r) - handlers.NewWorkflow(workflow).MountRoutes(r) - handlers.NewJob(job).MountRoutes(r) + handlers.NewTrigger(trigger).MountRoutes(r) }) } } diff --git a/crm/rest/trigger.go b/crm/rest/trigger.go new file mode 100644 index 000000000..1faca0c12 --- /dev/null +++ b/crm/rest/trigger.go @@ -0,0 +1,75 @@ +package rest + +import ( + "context" + + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/crm/service" + "github.com/crusttech/crust/crm/types" + + "github.com/pkg/errors" +) + +var _ = errors.Wrap + +type Trigger struct { + trigger service.TriggerService +} + +func (Trigger) New() *Trigger { + return &Trigger{ + trigger: service.DefaultTrigger, + } +} + +func (ctrl *Trigger) List(ctx context.Context, r *request.TriggerList) (interface{}, error) { + filter := types.TriggerFilter{} + + if r.ModuleID > 0 { + filter.ModuleID = r.ModuleID + } + + return ctrl.trigger.With(ctx).Find(filter) +} + +func (ctrl *Trigger) Create(ctx context.Context, r *request.TriggerCreate) (interface{}, error) { + trigger := &types.Trigger{ + Name: r.Name, + Actions: r.Actions, + Enabled: r.Enabled, + Source: r.Source, + } + + if r.ModuleID > 0 { + trigger.ModuleID = r.ModuleID + } + + return ctrl.trigger.With(ctx).Create(trigger) +} + +func (ctrl *Trigger) Read(ctx context.Context, r *request.TriggerRead) (interface{}, error) { + return ctrl.trigger.With(ctx).FindByID(r.TriggerID) +} + +func (ctrl *Trigger) Edit(ctx context.Context, r *request.TriggerEdit) (interface{}, error) { + if trigger, err := ctrl.trigger.FindByID(r.TriggerID); err != nil { + return nil, err + } else { + trigger.Name = r.Name + trigger.Actions = r.Actions + trigger.Enabled = r.Enabled + trigger.Source = r.Source + + if r.ModuleID > 0 { + trigger.ModuleID = r.ModuleID + } + + return ctrl.trigger.With(ctx).Update(trigger) + } +} + +func (ctrl *Trigger) Delete(ctx context.Context, r *request.TriggerDelete) (interface{}, error) { + return resputil.OK(), ctrl.trigger.With(ctx).DeleteByID(r.TriggerID) +} diff --git a/crm/service/main_test.go b/crm/service/main_test.go index 25e450e6b..312bd9a1d 100644 --- a/crm/service/main_test.go +++ b/crm/service/main_test.go @@ -50,7 +50,7 @@ func TestMain(m *testing.M) { // clean up tables { - for _, name := range []string{"crm_module", "crm_module_form", "crm_record", "crm_record_column", "crm_page", "sys_user"} { + for _, name := range []string{"crm_chart", "crm_trigger", "crm_module", "crm_module_form", "crm_record", "crm_record_column", "crm_page", "sys_user"} { _, err := db.Exec("truncate " + name) if err != nil { panic("Error when clearing " + name + ": " + err.Error()) diff --git a/crm/service/service.go b/crm/service/service.go index 5ad0d13f7..bb9c9c46e 100644 --- a/crm/service/service.go +++ b/crm/service/service.go @@ -5,22 +5,20 @@ import ( ) var ( - o sync.Once - DefaultRecord RecordService - DefaultModule ModuleService - DefaultChart ChartService - DefaultPage PageService - DefaultWorkflow WorkflowService - DefaultJob JobService + o sync.Once + DefaultRecord RecordService + DefaultModule ModuleService + DefaultTrigger TriggerService + DefaultChart ChartService + DefaultPage PageService ) func Init() { o.Do(func() { DefaultRecord = Record() DefaultModule = Module() + DefaultTrigger = Trigger() DefaultPage = Page() DefaultChart = Chart() - DefaultWorkflow = Workflow() - DefaultJob = Job() }) } diff --git a/crm/service/trigger.go b/crm/service/trigger.go new file mode 100644 index 000000000..4ec455ed6 --- /dev/null +++ b/crm/service/trigger.go @@ -0,0 +1,87 @@ +package service + +import ( + "context" + "errors" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/crm/repository" + "github.com/crusttech/crust/crm/types" +) + +type ( + trigger struct { + db *factory.DB + ctx context.Context + + triggerRepo repository.TriggerRepository + moduleRepo repository.ModuleRepository + } + + TriggerService interface { + With(ctx context.Context) TriggerService + + FindByID(triggerID uint64) (*types.Trigger, error) + Find(filter types.TriggerFilter) (set types.TriggerSet, err error) + + Create(trigger *types.Trigger) (*types.Trigger, error) + Update(trigger *types.Trigger) (*types.Trigger, error) + DeleteByID(triggerID uint64) error + } +) + +func Trigger() TriggerService { + return (&trigger{}).With(context.Background()) +} + +func (s *trigger) With(ctx context.Context) TriggerService { + db := repository.DB(ctx) + return &trigger{ + db: db, + ctx: ctx, + triggerRepo: repository.Trigger(ctx, db), + moduleRepo: repository.Module(ctx, db), + } +} + +func (s *trigger) FindByID(id uint64) (*types.Trigger, error) { + return s.triggerRepo.FindByID(id) +} + +func (s *trigger) Find(filter types.TriggerFilter) (types.TriggerSet, error) { + return s.triggerRepo.Find(filter) +} + +func (s *trigger) Create(trigger *types.Trigger) (p *types.Trigger, err error) { + validate := func() error { + return nil + } + if err := validate(); err != nil { + return nil, err + } + return p, s.db.Transaction(func() (err error) { + p, err = s.triggerRepo.Create(trigger) + return + }) +} + +func (s *trigger) Update(trigger *types.Trigger) (p *types.Trigger, err error) { + validate := func() error { + if trigger.ID == 0 { + return errors.New("could not update trigger, invalid ID") + } + return nil + } + if err := validate(); err != nil { + return nil, err + } + return p, s.db.Transaction(func() (err error) { + p, err = s.triggerRepo.Update(trigger) + return + }) +} + +func (s *trigger) DeleteByID(id uint64) error { + return s.triggerRepo.DeleteByID(id) +} diff --git a/crm/service/trigger_test.go b/crm/service/trigger_test.go new file mode 100644 index 000000000..ec4d78ac4 --- /dev/null +++ b/crm/service/trigger_test.go @@ -0,0 +1,68 @@ +package service + +import ( + "context" + "testing" + + "github.com/pkg/errors" + + "github.com/crusttech/crust/crm/types" +) + +func TestTrigger(t *testing.T) { + repository := Trigger().With(context.Background()) + + // the trigger object we're working with + trigger := &types.Trigger{ + Name: "Test", + ModuleID: 123, + } + + { + { + m, err := repository.Update(trigger) + assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) + assert(t, err != nil, "Expected error when updating invalid content") + } + + // create trigger + m, err := repository.Create(trigger) + assert(t, err == nil, "Error when creating trigger: %+v", err) + assert(t, m.ID > 0, "Expected auto generated ID") + + { + _, err := repository.Create(trigger) + assert(t, err == nil, "Unexpected error when creating trigger, %+v", err) + } + + // fetch created trigger + { + ms, err := repository.FindByID(m.ID) + assert(t, err == nil, "Error when retrieving trigger by id: %+v", err) + assert(t, ms.ID == m.ID, "Expected ID from database to match, %+v", errors.Errorf("%d != %d", m.ID, ms.ID)) + assert(t, ms.Name == m.Name, "Expected Name from database to match, %+v", errors.Errorf("%s != %s", m.Name, ms.Name)) + } + + // update created trigger + { + m.Name = "Updated test" + _, err := repository.Update(m) + assert(t, err == nil, "Error when updating trigger, %+v", err) + } + + // re-fetch trigger + { + ms, err := repository.FindByID(m.ID) + assert(t, err == nil, "Error when retrieving trigger by id: %+v", err) + assert(t, ms.ID == m.ID, "re-fetch: Expected ID from database to match, %d != %d", m.ID, ms.ID) + assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name) + } + + // delete trigger + { + err := repository.DeleteByID(m.ID) + assert(t, err == nil, "Error when deleting trigger by id: %+v", err) + } + } + +} diff --git a/crm/types/trigger.go b/crm/types/trigger.go new file mode 100644 index 000000000..809080574 --- /dev/null +++ b/crm/types/trigger.go @@ -0,0 +1,45 @@ +package types + +import ( + "database/sql/driver" + "strings" + "time" +) + +type ( + ActionSet []string + Trigger struct { + ID uint64 `json:"triggerID,string" db:"id"` + ModuleID uint64 `json:"moduleID,string,omitempty" db:"rel_module"` + Name string `json:"name" db:"name"` + Actions ActionSet `json:"actions" db:"actions"` + Enabled bool `json:"enabled" db:"enabled"` + Source string `json:"source" db:"source"` + + CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"` + UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"` + DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"` + } + + TriggerFilter struct { + ModuleID uint64 `json:"moduleID,string"` + } +) + +func (set *ActionSet) Scan(src interface{}) error { + if ser, ok := src.([]uint8); ok { + var tmp = make([]string, 0) + for _, a := range strings.Split(string(ser), ",") { + if a = strings.TrimSpace(a); len(a) > 0 { + tmp = append(tmp, a) + } + } + + *set = ActionSet(tmp) + } + return nil +} + +func (set ActionSet) Value() (driver.Value, error) { + return strings.Trim(strings.Join(set, ","), " ,"), nil +} diff --git a/crm/types/type.gen.go b/crm/types/type.gen.go index b4e790f61..a2b918991 100644 --- a/crm/types/type.gen.go +++ b/crm/types/type.gen.go @@ -19,6 +19,11 @@ type ( // This type is auto-generated. ChartSet []*Chart + // TriggerSet slice of Trigger + // + // This type is auto-generated. + TriggerSet []*Trigger + // ModuleFieldSet slice of ModuleField // // This type is auto-generated. @@ -193,6 +198,62 @@ func (set ChartSet) IDs() (IDs []uint64) { return } +// Walk iterates through every slice item and calls w(Trigger) err +// +// This function is auto-generated. +func (set TriggerSet) Walk(w func(*Trigger) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// Filter iterates through every slice item, calls f(Trigger) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set TriggerSet) Filter(f func(*Trigger) (bool, error)) (out TriggerSet, err error) { + var ok bool + out = TriggerSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} + +// FindByID finds items from slice by its ID property +// +// This function is auto-generated. +func (set TriggerSet) FindByID(ID uint64) *Trigger { + for i := range set { + if set[i].ID == ID { + return set[i] + } + } + + return nil +} + +// IDs returns a slice of uint64s from all items in the set +// +// This function is auto-generated. +func (set TriggerSet) IDs() (IDs []uint64) { + IDs = make([]uint64, len(set)) + + for i := range set { + IDs[i] = set[i].ID + } + + return +} + // Walk iterates through every slice item and calls w(ModuleField) err // // This function is auto-generated. diff --git a/docs/crm/README.md b/docs/crm/README.md index 7efbbd7dd..e40f7e399 100644 --- a/docs/crm/README.md +++ b/docs/crm/README.md @@ -369,82 +369,85 @@ CRM module pages -# Workflows +# Triggers -CRM workflow definitions +CRM Triggers -## List available workflows +## List available triggers #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/workflow/` | HTTP/S | GET | | +| `/trigger/` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | +| moduleID | uint64 | GET | Filter triggers by module | N/A | NO | -## Create new workflow +## Create trigger #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/workflow/` | HTTP/S | POST | | +| `/trigger/` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | -| name | string | POST | Workflow name | N/A | YES | -| tasks | types.WorkflowTaskSet | POST | Type ID | N/A | NO | -| onError | types.WorkflowTaskSet | POST | Type ID | N/A | NO | -| timeout | int | POST | Timeout in seconds | N/A | NO | +| moduleID | uint64 | POST | Module ID | N/A | NO | +| name | string | POST | Name | N/A | YES | +| actions | []string | POST | Actions that trigger this trigger | N/A | NO | +| enabled | bool | POST | Enabled | N/A | NO | +| source | string | POST | Trigger source code | N/A | NO | -## Get workflow details +## Get trigger details #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/workflow/{workflowID}` | HTTP/S | GET | | +| `/trigger/{triggerID}` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | -| workflowID | string | PATH | Workflow ID | N/A | YES | +| triggerID | uint64 | PATH | Trigger ID | N/A | YES | -## Update workflow details +## Edit trigger #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/workflow/{workflowID}` | HTTP/S | POST | | +| `/trigger/{triggerID}` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | -| workflowID | string | PATH | Workflow ID | N/A | YES | -| name | string | POST | Workflow name | N/A | YES | -| tasks | types.WorkflowTaskSet | POST | Type ID | N/A | NO | -| onError | types.WorkflowTaskSet | POST | Type ID | N/A | NO | -| timeout | int | POST | Timeout in seconds | N/A | NO | +| triggerID | uint64 | PATH | Trigger ID | N/A | YES | +| moduleID | uint64 | POST | Module ID | N/A | NO | +| name | string | POST | Name | N/A | YES | +| actions | []string | POST | Actions that trigger this trigger | N/A | NO | +| enabled | bool | POST | Enabled | N/A | NO | +| source | string | POST | Trigger source code | N/A | NO | -## Delete workflow +## Delete trigger #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/workflow/{workflowID}` | HTTP/S | DELETE | | +| `/trigger/{triggerID}` | HTTP/S | Delete | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | -| workflowID | string | PATH | Workflow ID | N/A | YES | \ No newline at end of file +| triggerID | uint64 | PATH | Trigger ID | N/A | YES | \ No newline at end of file