From 8ef7b77a770f1eb111124ab7a84e218d642236a0 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Sun, 28 Apr 2019 12:35:08 +0200 Subject: [PATCH] Namespace changes on all types, attachment repository, service --- api/compose/spec.json | 20 ++- api/compose/spec/attachment.json | 20 ++- compose/db/mysql/static.go | 2 +- .../20190427180922.change-tbl-prefix.up.sql | 4 +- .../20190428080000.namespace-refs.up.sql | 73 ++++++++++ compose/internal/repository/attachment.go | 137 ++++++++---------- compose/internal/repository/namespace.go | 134 ++++++----------- compose/internal/repository/repository.go | 77 ++++++++++ compose/internal/service/attachment.go | 71 ++++++--- compose/internal/service/error.go | 1 + compose/internal/service/namespace.go | 45 +++--- compose/internal/service/service.go | 16 ++ compose/rest/attachment.go | 110 +++++++------- compose/rest/handlers/attachment.go | 35 ++++- compose/rest/namespace.go | 13 +- compose/rest/page.go | 8 +- compose/rest/record.go | 12 +- compose/rest/request/attachment.go | 69 ++++++++- compose/types/attachment.go | 44 +++--- compose/types/chart.go | 10 ++ compose/types/module.go | 2 + compose/types/namespace.go | 18 +-- compose/types/page.go | 2 + compose/types/record.go | 2 + compose/types/trigger.go | 2 + docs/compose/README.md | 21 ++- 26 files changed, 611 insertions(+), 337 deletions(-) create mode 100644 compose/db/schema/mysql/20190428080000.namespace-refs.up.sql diff --git a/api/compose/spec.json b/api/compose/spec.json index 35839d4b9..5a3c7f89d 100644 --- a/api/compose/spec.json +++ b/api/compose/spec.json @@ -1129,7 +1129,7 @@ "type": "uint64", "name": "moduleID", "required": false, - "title": "Filter attachments by mnodule ID" + "title": "Filter attachments by module ID" }, { "name": "recordID", @@ -1159,7 +1159,7 @@ } }, { - "name": "details", + "name": "read", "path": "/{attachmentID}", "method": "GET", "title": "Attachment details", @@ -1174,6 +1174,22 @@ ] } }, + { + "name": "delete", + "path": "/{attachmentID}", + "method": "DELETE", + "title": "Delete attachment", + "parameters": { + "path": [ + { + "name": "attachmentID", + "type": "uint64", + "required": true, + "title": "Attachment ID" + } + ] + } + }, { "name": "original", "path": "/{attachmentID}/original/{name}", diff --git a/api/compose/spec/attachment.json b/api/compose/spec/attachment.json index 80cf0d9f0..6f4873af2 100644 --- a/api/compose/spec/attachment.json +++ b/api/compose/spec/attachment.json @@ -55,7 +55,7 @@ { "name": "moduleID", "required": false, - "title": "Filter attachments by mnodule ID", + "title": "Filter attachments by module ID", "type": "uint64" }, { @@ -86,7 +86,7 @@ } }, { - "Name": "details", + "Name": "read", "Method": "GET", "Title": "Attachment details", "Path": "/{attachmentID}", @@ -101,6 +101,22 @@ ] } }, + { + "Name": "delete", + "Method": "DELETE", + "Title": "Delete attachment", + "Path": "/{attachmentID}", + "Parameters": { + "path": [ + { + "name": "attachmentID", + "required": true, + "title": "Attachment ID", + "type": "uint64" + } + ] + } + }, { "Name": "original", "Method": "GET", diff --git a/compose/db/mysql/static.go b/compose/db/mysql/static.go index 988dd5f27..f0543a7b4 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_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_content\n RENAME TO compose_content;\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\x08z\x97\x10\xc8\xab\x02\x00\x00\xab\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\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!(z\x97\x10\xc8\xab\x02\x00\x00\xab\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\x81\x8a)\x00\x0020190427210922.namespace-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\x816,\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\xf3-\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x15\x00\x15\x00\xcc\x07\x00\x00_.\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 INTO compose_namespace (id, name, slug, enabled, meta) 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\nALTER TABLE `compose_record_value`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `record_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;\nUPDATE `compose_record_value` 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`);\n\nALTER TABLE `compose_record_value`\n ADD CONSTRAINT `compose_record_value_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\nPK\x07\x08\xd7\xb5!\xe1\xe3\n\x00\x00\xe3\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\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!(\xd7\xb5!\xe1\xe3\n\x00\x00\xe3\n\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!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81`7\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\x1d9\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x16\x00\x16\x00'\x08\x00\x00\x899\x00\x00\x00\x00" diff --git a/compose/db/schema/mysql/20190427180922.change-tbl-prefix.up.sql b/compose/db/schema/mysql/20190427180922.change-tbl-prefix.up.sql index dfb25b246..f8cb60e27 100644 --- a/compose/db/schema/mysql/20190427180922.change-tbl-prefix.up.sql +++ b/compose/db/schema/mysql/20190427180922.change-tbl-prefix.up.sql @@ -1,5 +1,6 @@ DROP TABLE IF EXISTS crm_field; DROP TABLE IF EXISTS crm_fields; +DROP TABLE IF EXISTS crm_content; DROP TABLE IF EXISTS crm_content_links; DROP TABLE IF EXISTS crm_content_column; DROP TABLE IF EXISTS crm_module_content; @@ -10,9 +11,6 @@ ALTER TABLE crm_attachment ALTER TABLE crm_chart RENAME TO compose_chart; -ALTER TABLE crm_content - RENAME TO compose_content; - ALTER TABLE crm_module RENAME TO compose_module; diff --git a/compose/db/schema/mysql/20190428080000.namespace-refs.up.sql b/compose/db/schema/mysql/20190428080000.namespace-refs.up.sql new file mode 100644 index 000000000..26a9287bd --- /dev/null +++ b/compose/db/schema/mysql/20190428080000.namespace-refs.up.sql @@ -0,0 +1,73 @@ +INSERT INTO compose_namespace (id, name, slug, enabled, meta) VALUES (88714882739863655, 'Crust CRM', 'crm', true, '{}'); + +ALTER TABLE `compose_attachment` + ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`, + ADD INDEX (`rel_namespace`); + +ALTER TABLE `compose_chart` + ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`, + ADD INDEX (`rel_namespace`); + +ALTER TABLE `compose_module` + ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`, + ADD INDEX (`rel_namespace`); + +ALTER TABLE `compose_page` + ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`, + ADD INDEX (`rel_namespace`); + +ALTER TABLE `compose_record` + ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`, + ADD INDEX (`rel_namespace`); + +ALTER TABLE `compose_trigger` + ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`, + ADD INDEX (`rel_namespace`); + +ALTER TABLE `compose_record_value` + ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `record_id`, + ADD INDEX (`rel_namespace`); + +UPDATE `compose_attachment` SET `rel_namespace` = 88714882739863655; +UPDATE `compose_chart` SET `rel_namespace` = 88714882739863655; +UPDATE `compose_module` SET `rel_namespace` = 88714882739863655; +UPDATE `compose_page` SET `rel_namespace` = 88714882739863655; +UPDATE `compose_record` SET `rel_namespace` = 88714882739863655; +UPDATE `compose_trigger` SET `rel_namespace` = 88714882739863655; +UPDATE `compose_record_value` SET `rel_namespace` = 88714882739863655; + + +ALTER TABLE `compose_attachment` + ADD CONSTRAINT `compose_attachment_namespace` + FOREIGN KEY (`rel_namespace`) + REFERENCES `compose_namespace` (`id`); + +ALTER TABLE `compose_chart` + ADD CONSTRAINT `compose_chart_namespace` + FOREIGN KEY (`rel_namespace`) + REFERENCES `compose_namespace` (`id`); + +ALTER TABLE `compose_module` + ADD CONSTRAINT `compose_module_namespace` + FOREIGN KEY (`rel_namespace`) + REFERENCES `compose_namespace` (`id`); + +ALTER TABLE `compose_page` + ADD CONSTRAINT `compose_page_namespace` + FOREIGN KEY (`rel_namespace`) + REFERENCES `compose_namespace` (`id`); + +ALTER TABLE `compose_record` + ADD CONSTRAINT `compose_record_namespace` + FOREIGN KEY (`rel_namespace`) + REFERENCES `compose_namespace` (`id`); + +ALTER TABLE `compose_trigger` + ADD CONSTRAINT `compose_trigger_namespace` + FOREIGN KEY (`rel_namespace`) + REFERENCES `compose_namespace` (`id`); + +ALTER TABLE `compose_record_value` + ADD CONSTRAINT `compose_record_value_namespace` + FOREIGN KEY (`rel_namespace`) + REFERENCES `compose_namespace` (`id`); diff --git a/compose/internal/repository/attachment.go b/compose/internal/repository/attachment.go index 373b20c81..c12182829 100644 --- a/compose/internal/repository/attachment.go +++ b/compose/internal/repository/attachment.go @@ -4,10 +4,9 @@ import ( "context" "time" - "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/titpetric/factory" - sq "gopkg.in/Masterminds/squirrel.v1" + "gopkg.in/Masterminds/squirrel.v1" "github.com/crusttech/crust/compose/types" ) @@ -17,10 +16,9 @@ type ( With(ctx context.Context, db *factory.DB) AttachmentRepository Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) - FindByID(id uint64) (*types.Attachment, error) - FindByIDs(IDs ...uint64) (types.AttachmentSet, error) + FindByID(namespaceID, attachmentID uint64) (*types.Attachment, error) Create(mod *types.Attachment) (*types.Attachment, error) - DeleteByID(id uint64) error + DeleteByID(namespaceID, attachmentID uint64) error } attachment struct { @@ -29,64 +27,68 @@ type ( ) const ( - sqlAttachmentColumns = ` - a.id, a.rel_owner, a.kind, - a.url, a.preview_url, - a.name, - a.meta, - a.created_at, a.updated_at, a.deleted_at - ` - sqlAttachmentScope = "deleted_at IS NULL" - - sqlAttachmentByID = `SELECT ` + sqlAttachmentColumns + - ` FROM compose_attachment AS a WHERE id = ? AND ` + sqlAttachmentScope - - sqlAttachmentsByIDs = `SELECT ` + sqlAttachmentColumns + - ` FROM compose_attachment AS a WHERE id IN (?) AND ` + sqlAttachmentScope + ErrAttachmentNotFound = repositoryError("AttachmentNotFound") ) func Attachment(ctx context.Context, db *factory.DB) AttachmentRepository { return (&attachment{}).With(ctx, db) } -func (r *attachment) With(ctx context.Context, db *factory.DB) AttachmentRepository { +func (r attachment) With(ctx context.Context, db *factory.DB) AttachmentRepository { return &attachment{ repository: r.repository.With(ctx, db), } } -func (r *attachment) FindByID(id uint64) (*types.Attachment, error) { - mod := &types.Attachment{} - - return mod, r.db().Get(mod, sqlAttachmentByID, id) +func (r attachment) table() string { + return "compose_attachment" } -func (r *attachment) FindByIDs(IDs ...uint64) (rval types.AttachmentSet, err error) { - rval = make([]*types.Attachment, 0) - - if len(IDs) == 0 { - return - } - - if sql, args, err := sqlx.In(sqlAttachmentsByIDs, IDs); err != nil { - return nil, err - } else { - return rval, r.db().Select(&rval, sql, args...) +func (r attachment) columns() []string { + return []string{ + "a.id", "a.rel_namespace", "a.rel_owner", "a.kind", + "a.url", "a.preview_url", + "a.name", + "a.meta", + "a.created_at", "a.updated_at", "a.deleted_at", } } -func (r *attachment) Find(filter types.AttachmentFilter) (set types.AttachmentSet, f types.AttachmentFilter, err error) { +func (r attachment) query() squirrel.SelectBuilder { + return squirrel. + Select(). + From(r.table() + " AS a"). + Where("a.deleted_at IS NULL") + +} + +func (r attachment) FindByID(namespaceID, attachmentID uint64) (*types.Attachment, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("a.id = ?", attachmentID) + + a = &types.Attachment{} + ) + + if namespaceID > 0 { + query = query.Where("a.rel_namespace = ?", namespaceID) + } + + return a, isFound(r.fetchOne(a, query), a.ID > 0, ErrAttachmentNotFound) +} + +func (r attachment) Find(filter types.AttachmentFilter) (set types.AttachmentSet, f types.AttachmentFilter, err error) { f = filter - if f.PerPage > 100 { - f.PerPage = 100 - } else if f.PerPage == 0 { - f.PerPage = 50 + f.PerPage = normalizePerPage(f.PerPage, 5, 100, 50) + + query := r.query(). + Where(squirrel.Eq{"a.kind": f.Kind}) + + if filter.NamespaceID > 0 { + query = query.Where("a.rel_namespace = ?", filter.NamespaceID) } - set = types.AttachmentSet{} - - query := sq.Select().From("compose_attachment AS a").Where(sq.Eq{"a.kind": f.Kind}) - switch f.Kind { case types.PageAttachment: // @todo implement filtering by page @@ -101,15 +103,15 @@ func (r *attachment) Find(filter types.AttachmentFilter) (set types.AttachmentSe if f.ModuleID > 0 { query = query. Join("compose_record AS r ON (r.id = v.record_id)"). - Where(sq.Eq{"r.module_id": f.ModuleID}) + Where(squirrel.Eq{"r.module_id": f.ModuleID}) } if f.RecordID > 0 { - query = query.Where(sq.Eq{"v.record_id": f.RecordID}) + query = query.Where(squirrel.Eq{"v.record_id": f.RecordID}) } if f.FieldName != "" { - query = query.Where(sq.Eq{"v.name": f.FieldName}) + query = query.Where(squirrel.Eq{"v.name": f.FieldName}) } default: err = errors.New("unsupported kind value") @@ -120,46 +122,33 @@ func (r *attachment) Find(filter types.AttachmentFilter) (set types.AttachmentSe return } - // Assemble SQL for counting (includes only where) - count := query.Column("COUNT(*)") - if sqlSelect, argsSelect, err := count.ToSql(); err != nil { - return set, f, err - } else { - // Execute count query. - if err := r.db().Get(&f.Count, sqlSelect, argsSelect...); err != nil { - return set, f, err - } - - // Return empty response if count of records is zero. - if f.Count == 0 { - return set, f, nil - } + if f.Count, err = r.count(query); err != nil || f.Count == 0 { + return } - // Assemble SQL for fetching attachments (where + sorting + paging)... query = query. - Column(sqlAttachmentColumns). - Limit(uint64(f.PerPage)). - Offset(uint64(f.Page * f.PerPage)) + Columns(r.columns()...). + OrderBy("id ASC") - if sqlSelect, argsSelect, err := query.ToSql(); err != nil { - return set, f, err - } else { - return set, f, r.db().Select(&set, sqlSelect, argsSelect...) - } + return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage) } -func (r *attachment) Create(mod *types.Attachment) (*types.Attachment, error) { +func (r attachment) Create(mod *types.Attachment) (*types.Attachment, error) { if mod.ID == 0 { mod.ID = factory.Sonyflake.NextID() } mod.CreatedAt = time.Now() - return mod, r.db().Insert("compose_attachment", mod) + return mod, r.db().Insert(r.table(), mod) } -func (r *attachment) DeleteByID(id uint64) error { - _, err := r.db().Exec("UPDATE compose_attachment SET deleted_at = NOW() WHERE id = ?", id) +func (r attachment) DeleteByID(namespaceID, attachmentID uint64) error { + _, err := r.db().Exec( + "UPDATE "+r.table()+" SET deleted_at = NOW() WHERE rel_namespace = ? AND id = ?", + namespaceID, + attachmentID, + ) + return err } diff --git a/compose/internal/repository/namespace.go b/compose/internal/repository/namespace.go index b6a2af4ce..11c5d870c 100644 --- a/compose/internal/repository/namespace.go +++ b/compose/internal/repository/namespace.go @@ -51,95 +51,6 @@ func (r namespace) columns() []string { } } -func (r *namespace) With(ctx context.Context, db *factory.DB) NamespaceRepository { - return &namespace{ - repository: r.repository.With(ctx, db), - } -} - -func (r *namespace) FindByID(id uint64) (n *types.Namespace, err error) { - var ( - sql string - args []interface{} - ) - - n = &types.Namespace{} - - qb := r.query(). - Columns(r.columns()...). - Where("id = ?", id) - - if sql, args, err = qb.ToSql(); err != nil { - return - } - - if err = r.db().Get(n, sql, args...); err != nil { - return - } - - if n == nil || n.ID == 0 { - return nil, ErrNamespaceNotFound - } - - return -} - -func (r *namespace) Find(filter types.NamespaceFilter) (nn types.NamespaceSet, f types.NamespaceFilter, err error) { - var ( - sql string - args []interface{} - ) - - f = filter - - if f.PerPage > 100 { - f.PerPage = 100 - } else if f.PerPage == 0 { - f.PerPage = 50 - } - - qb := r.query() - if f.Query != "" { - q := "%" + f.Query + "%" - qb = qb.Where("name like ? OR slug like ?", q, q) - } - - { - cq := qb.Column(squirrel.Alias(squirrel.Expr("COUNT(*)"), "count")) - if sql, args, err = cq.ToSql(); err != nil { - return - } - - if err = r.db().Get(&f.Count, sql, args...); err != nil { - return - } - - if f.Count == 0 { - // No rows with this filter no need to continue - return - } - } - - if f.Page > 0 { - qb = qb.Offset(uint64(f.PerPage * f.Page)) - } - - qb = qb. - Limit(uint64(f.PerPage)). - Columns(r.columns()...). - OrderBy("id ASC") - - if sql, args, err = qb.ToSql(); err != nil { - return - } - - if err = r.db().Select(&nn, sql, args...); err != nil { - return - } - - return -} - func (r namespace) query() squirrel.SelectBuilder { return squirrel. Select(). @@ -148,6 +59,49 @@ func (r namespace) query() squirrel.SelectBuilder { } +func (r *namespace) With(ctx context.Context, db *factory.DB) NamespaceRepository { + return &namespace{ + repository: r.repository.With(ctx, db), + } +} + +func (r *namespace) FindByID(namespaceID uint64) (*types.Namespace, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("id = ?", namespaceID) + + n = &types.Namespace{} + ) + + return n, isFound(r.fetchOne(n, query), n.ID > 0, ErrNamespaceNotFound) +} + +func (r *namespace) Find(filter types.NamespaceFilter) (set types.NamespaceSet, f types.NamespaceFilter, err error) { + f = filter + f.PerPage = normalizePerPage(f.PerPage, 5, 100, 50) + + query := r.query() + if f.Query != "" { + q := "%" + f.Query + "%" + query = query.Where("name like ? OR slug like ?", q, q) + } + + if f.Count, err = r.count(query); err != nil || f.Count == 0 { + return + } + + if f.Page > 0 { + query = query.Offset(uint64(f.PerPage * f.Page)) + } + + query = query. + Columns(r.columns()...). + OrderBy("id ASC") + + return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage) +} + func (r *namespace) Create(mod *types.Namespace) (*types.Namespace, error) { mod.ID = factory.Sonyflake.NextID() mod.CreatedAt = time.Now() @@ -163,6 +117,6 @@ func (r *namespace) Update(mod *types.Namespace) (*types.Namespace, error) { } func (r *namespace) DeleteByID(id uint64) error { - _, err := r.db().Exec("DELETE FROM compose_namespace WHERE id=?", id) + _, err := r.db().Exec("DELETE FROM "+r.table()+" WHERE id=?", id) return err } diff --git a/compose/internal/repository/repository.go b/compose/internal/repository/repository.go index fd7418b90..547d6d687 100644 --- a/compose/internal/repository/repository.go +++ b/compose/internal/repository/repository.go @@ -3,7 +3,9 @@ package repository import ( "context" + "github.com/pkg/errors" "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" "github.com/crusttech/crust/internal/auth" ) @@ -45,3 +47,78 @@ func (r *repository) db() *factory.DB { } return DB(r.ctx) } + +// Fetches single row from table +func (r repository) fetchOne(one interface{}, q squirrel.SelectBuilder) (err error) { + var ( + sql string + args []interface{} + ) + + if sql, args, err = q.ToSql(); err != nil { + return + } + + if err = r.db().Get(one, sql, args...); err != nil { + return + } + + return +} + +// Counts all rows that match conditions from given query builder +func (r repository) count(q squirrel.SelectBuilder) (uint, error) { + var ( + count uint + cq = q.Column("COUNT(*)") + ) + + if sqlSelect, argsSelect, err := cq.ToSql(); err != nil { + return 0, err + } else { + if err := r.db().Get(&count, sqlSelect, argsSelect...); err != nil { + return 0, err + } + } + + return count, nil +} + +// Fetches paged rows +func (r repository) fetchPaged(set interface{}, q squirrel.SelectBuilder, page, perPage uint) error { + q = q. + Limit(uint64(perPage)). + Offset(uint64(page * perPage)) + + if sqlSelect, argsSelect, err := q.ToSql(); err != nil { + return err + } else { + return r.db().Select(set, sqlSelect, argsSelect...) + } +} + +func normalizePerPage(val, min, max, def uint) uint { + if val == 0 { + return def + } + + if val > max { + return max + } + + if val < min { + return min + } + + return val +} + +func isFound(err error, valid bool, nerr error) error { + if err != nil { + return errors.WithStack(err) + } else if !valid { + return errors.WithStack(nerr) + } + + return nil +} diff --git a/compose/internal/service/attachment.go b/compose/internal/service/attachment.go index 7b7af28a5..c19057b8d 100644 --- a/compose/internal/service/attachment.go +++ b/compose/internal/service/attachment.go @@ -48,12 +48,13 @@ type ( AttachmentService interface { With(ctx context.Context) AttachmentService - FindByID(id uint64) (*types.Attachment, error) + FindByID(namespaceID, attachmentID uint64) (*types.Attachment, error) Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) - CreatePageAttachment(name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error) - CreateRecordAttachment(name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (*types.Attachment, error) + CreatePageAttachment(namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error) + CreateRecordAttachment(namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (*types.Attachment, error) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) + DeleteByID(namespaceID, attachmentID uint64) error } ) @@ -68,7 +69,7 @@ func Attachment(store store.Store) AttachmentService { }).With(context.Background()) } -func (svc *attachment) With(ctx context.Context) AttachmentService { +func (svc attachment) With(ctx context.Context) AttachmentService { db := repository.DB(ctx) return &attachment{ db: db, @@ -85,11 +86,27 @@ func (svc *attachment) With(ctx context.Context) AttachmentService { } } -func (svc *attachment) FindByID(id uint64) (*types.Attachment, error) { - return svc.attachment.FindByID(id) +func (svc attachment) FindByID(namespaceID, attachmentID uint64) (*types.Attachment, error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired + } + + return svc.attachment.FindByID(namespaceID, attachmentID) } -func (svc *attachment) Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) { +func (svc attachment) DeleteByID(namespaceID, attachmentID uint64) error { + if namespaceID == 0 { + return ErrNamespaceRequired + } + + return svc.attachment.DeleteByID(namespaceID, attachmentID) +} + +func (svc attachment) Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) { + if filter.NamespaceID == 0 { + return nil, filter, ErrNamespaceRequired + } + if filter.PageID > 0 { if _, err := svc.pageSvc.FindByID(filter.PageID); err != nil { return nil, filter, err @@ -111,7 +128,7 @@ func (svc *attachment) Find(filter types.AttachmentFilter) (types.AttachmentSet, return svc.attachment.Find(filter) } -func (svc *attachment) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) { +func (svc attachment) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) { if len(att.Url) == 0 { return nil, nil } @@ -119,7 +136,7 @@ func (svc *attachment) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error return svc.store.Open(att.Url) } -func (svc *attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) { +func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) { if len(att.PreviewUrl) == 0 { return nil, nil } @@ -127,7 +144,11 @@ func (svc *attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) return svc.store.Open(att.PreviewUrl) } -func (svc *attachment) CreatePageAttachment(name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error) { +func (svc attachment) CreatePageAttachment(namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired + } + var currentUserID uint64 = auth.GetIdentityFromContext(svc.ctx).Identity() if p, err := svc.pageSvc.FindByID(pageID); err != nil { @@ -137,15 +158,20 @@ func (svc *attachment) CreatePageAttachment(name string, size int64, fh io.ReadS } att := &types.Attachment{ - ID: factory.Sonyflake.NextID(), - OwnerID: currentUserID, - Name: strings.TrimSpace(name), - Kind: types.PageAttachment, + ID: factory.Sonyflake.NextID(), + NamespaceID: namespaceID, + OwnerID: currentUserID, + Name: strings.TrimSpace(name), + Kind: types.PageAttachment, } return att, svc.create(name, size, fh, att) } -func (svc *attachment) CreateRecordAttachment(name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (*types.Attachment, error) { +func (svc attachment) CreateRecordAttachment(namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (*types.Attachment, error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired + } + var currentUserID uint64 = auth.GetIdentityFromContext(svc.ctx).Identity() if _, err := svc.moduleSvc.FindByID(moduleID); err != nil { @@ -157,16 +183,17 @@ func (svc *attachment) CreateRecordAttachment(name string, size int64, fh io.Rea } att := &types.Attachment{ - ID: factory.Sonyflake.NextID(), - OwnerID: currentUserID, - Name: strings.TrimSpace(name), - Kind: types.RecordAttachment, + ID: factory.Sonyflake.NextID(), + NamespaceID: namespaceID, + OwnerID: currentUserID, + Name: strings.TrimSpace(name), + Kind: types.RecordAttachment, } return att, svc.create(name, size, fh, att) } -func (svc *attachment) create(name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) { +func (svc attachment) create(name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) { if svc.store == nil { return errors.New("Can not create attachment: store handler not set") } @@ -205,7 +232,7 @@ func (svc *attachment) create(name string, size int64, fh io.ReadSeeker, att *ty }) } -func (svc *attachment) extractMimetype(file io.ReadSeeker) (mimetype string, err error) { +func (svc attachment) extractMimetype(file io.ReadSeeker) (mimetype string, err error) { if _, err = file.Seek(0, 0); err != nil { return } @@ -222,7 +249,7 @@ func (svc *attachment) extractMimetype(file io.ReadSeeker) (mimetype string, err return http.DetectContentType(buf), nil } -func (svc *attachment) processImage(original io.ReadSeeker, att *types.Attachment) (err error) { +func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment) (err error) { if !strings.HasPrefix(att.Meta.Original.Mimetype, "image/") { // Only supporting previews from images (for now) return diff --git a/compose/internal/service/error.go b/compose/internal/service/error.go index ae7b9751a..f453ca23f 100644 --- a/compose/internal/service/error.go +++ b/compose/internal/service/error.go @@ -15,6 +15,7 @@ const ( ErrNoReadPermissions serviceError = "NoReadPermissions" ErrNoUpdatePermissions serviceError = "NoUpdatePermissions" ErrNoDeletePermissions serviceError = "NoDeletePermissions" + ErrNamespaceRequired serviceError = "NamespaceRequired" ErrNotImplemented serviceError = "NotImplemented" ) diff --git a/compose/internal/service/namespace.go b/compose/internal/service/namespace.go index e544c49fd..13a36c0f6 100644 --- a/compose/internal/service/namespace.go +++ b/compose/internal/service/namespace.go @@ -2,7 +2,6 @@ package service import ( "context" - "time" "github.com/titpetric/factory" @@ -64,17 +63,17 @@ func (svc *namespace) FindByID(ID uint64) (n *types.Namespace, err error) { return } -func (svc *namespace) Find(filter types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error) { - nn, f, err := svc.namespaceRepo.Find(filter) +func (svc *namespace) Find(filter types.NamespaceFilter) (set types.NamespaceSet, f types.NamespaceFilter, err error) { + set, f, err = svc.namespaceRepo.Find(filter) if err != nil { - return nil, f, err + return } - nn, _ = nn.Filter(func(m *types.Namespace) (bool, error) { + set, _ = set.Filter(func(m *types.Namespace) (bool, error) { return svc.prmSvc.CanReadNamespace(m), nil }) - return nn, f, nil + return } func (svc *namespace) Create(mod *types.Namespace) (*types.Namespace, error) { @@ -85,24 +84,28 @@ func (svc *namespace) Create(mod *types.Namespace) (*types.Namespace, error) { return svc.namespaceRepo.Create(mod) } -func (svc *namespace) Update(updated *types.Namespace) (m *types.Namespace, err error) { - m, err = svc.FindByID(updated.ID) +func (svc *namespace) Update(mod *types.Namespace) (m *types.Namespace, err error) { + if mod.ID == 0 { + return nil, ErrInvalidID.withStack() + } + + m, err = svc.FindByID(mod.ID) if err != nil { return nil, err } - if isStale(updated.UpdatedAt, m.UpdatedAt, m.CreatedAt) { - return nil, ErrStaleData + if isStale(mod.UpdatedAt, m.UpdatedAt, m.CreatedAt) { + return nil, ErrStaleData.withStack() } if !svc.prmSvc.CanUpdateNamespace(m) { return nil, ErrNoUpdatePermissions.withStack() } - m.Name = updated.Name - m.Slug = updated.Slug - m.Meta = updated.Meta - m.Enabled = updated.Enabled + m.Name = mod.Name + m.Slug = mod.Slug + m.Meta = mod.Meta + m.Enabled = mod.Enabled return svc.namespaceRepo.Update(m) } @@ -116,17 +119,3 @@ func (svc *namespace) DeleteByID(ID uint64) error { return svc.namespaceRepo.DeleteByID(ID) } - -// Data is stale when new date does not match updatedAt or createdAt (before first update) -func isStale(new *time.Time, updatedAt *time.Time, createdAt time.Time) bool { - - if new == nil { - return false - } - - if updatedAt != nil { - return !new.Equal(*updatedAt) - } - - return new.Equal(createdAt) -} diff --git a/compose/internal/service/service.go b/compose/internal/service/service.go index 7a1416380..a6657f45c 100644 --- a/compose/internal/service/service.go +++ b/compose/internal/service/service.go @@ -1,6 +1,8 @@ package service import ( + "time" + "github.com/crusttech/crust/internal/store" ) @@ -40,3 +42,17 @@ func Init() error { return nil } + +// Data is stale when new date does not match updatedAt or createdAt (before first update) +func isStale(new *time.Time, updatedAt *time.Time, createdAt time.Time) bool { + if new == nil { + // Change to true to require stale-data-check. + return true + } + + if updatedAt != nil { + return !new.Equal(*updatedAt) + } + + return new.Equal(createdAt) +} diff --git a/compose/rest/attachment.go b/compose/rest/attachment.go index b6417e1bd..0eee17bcb 100644 --- a/compose/rest/attachment.go +++ b/compose/rest/attachment.go @@ -6,7 +6,8 @@ import ( "io" "net/http" "net/url" - "time" + + "github.com/titpetric/factory/resputil" "github.com/crusttech/crust/compose/internal/service" "github.com/crusttech/crust/compose/rest/request" @@ -20,14 +21,12 @@ var _ = errors.Wrap type ( attachmentPayload struct { - ID uint64 `json:"attachmentID,string"` - OwnerID uint64 `json:"ownerID,string"` - Url string `json:"url"` - PreviewUrl string `json:"previewUrl,omitempty"` - Meta interface{} `json:"meta"` - Name string `json:"name"` - CreatedAt time.Time `json:"createdAt,omitempty"` - UpdatedAt *time.Time `json:"updatedAt,omitempty"` + *types.Attachment + } + + attachmentSetPayload struct { + Filter types.AttachmentFilter `json:"filter"` + Set []*attachmentPayload `json:"set"` } Attachment struct { @@ -36,11 +35,13 @@ type ( ) func (Attachment) New() *Attachment { - return &Attachment{attachment: service.DefaultAttachment} + return &Attachment{ + attachment: service.DefaultAttachment, + } } // Attachments returns list of all files attached to records -func (ctrl *Attachment) List(ctx context.Context, r *request.AttachmentList) (interface{}, error) { +func (ctrl Attachment) List(ctx context.Context, r *request.AttachmentList) (interface{}, error) { f := types.AttachmentFilter{ NamespaceID: r.NamespaceID, Kind: r.Kind, @@ -53,44 +54,41 @@ func (ctrl *Attachment) List(ctx context.Context, r *request.AttachmentList) (in // Sort: r.Sort, } - aa, meta, err := ctrl.attachment.Find(f) + set, filter, err := ctrl.attachment.With(ctx).Find(f) + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl Attachment) Read(ctx context.Context, r *request.AttachmentRead) (interface{}, error) { + a, err := ctrl.attachment.FindByID(r.NamespaceID, r.AttachmentID) + return makeAttachmentPayload(ctx, a, err) +} + +func (ctrl Attachment) Delete(ctx context.Context, r *request.AttachmentDelete) (interface{}, error) { + _, err := ctrl.attachment.FindByID(r.NamespaceID, r.AttachmentID) if err != nil { return nil, err } - pp := make([]*attachmentPayload, len(aa)) - for i := range aa { - pp[i] = makeAttachmentPayload(aa[i], auth.GetIdentityFromContext(ctx).Identity()) - } - - return map[string]interface{}{"meta": meta, "attachments": pp}, nil -} - -func (ctrl Attachment) Details(ctx context.Context, r *request.AttachmentDetails) (interface{}, error) { - if a, err := ctrl.attachment.FindByID(r.AttachmentID); err != nil { - return nil, err - } else { - return makeAttachmentPayload(a, auth.GetIdentityFromContext(ctx).Identity()), nil - } + return resputil.OK(), ctrl.attachment.With(ctx).DeleteByID(r.NamespaceID, r.AttachmentID) } func (ctrl Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) { - if err := ctrl.isAccessible(r.AttachmentID, r.UserID, r.Sign); err != nil { + if err := ctrl.isAccessible(r.NamespaceID, r.AttachmentID, r.UserID, r.Sign); err != nil { return nil, err } - return ctrl.serve(ctx, r.AttachmentID, false, r.Download) + return ctrl.serve(ctx, r.NamespaceID, r.AttachmentID, false, r.Download) } -func (ctrl *Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) { - if err := ctrl.isAccessible(r.AttachmentID, r.UserID, r.Sign); err != nil { +func (ctrl Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) { + if err := ctrl.isAccessible(r.NamespaceID, r.AttachmentID, r.UserID, r.Sign); err != nil { return nil, err } - return ctrl.serve(ctx, r.AttachmentID, true, false) + return ctrl.serve(ctx, r.NamespaceID, r.AttachmentID, true, false) } -func (ctrl Attachment) isAccessible(attachmentID, userID uint64, signature string) error { +func (ctrl Attachment) isAccessible(namespaceID, attachmentID, userID uint64, signature string) error { if userID == 0 { return errors.New("missing or invalid user ID") } @@ -99,16 +97,16 @@ func (ctrl Attachment) isAccessible(attachmentID, userID uint64, signature strin return errors.New("missing or invalid attachment ID") } - if auth.DefaultSigner.Verify(signature, userID, attachmentID) { + if auth.DefaultSigner.Verify(signature, userID, namespaceID, attachmentID) { return errors.New("missing or invalid signature") } return nil } -func (ctrl Attachment) serve(ctx context.Context, ID uint64, preview, download bool) (interface{}, error) { +func (ctrl Attachment) serve(ctx context.Context, namespaceID, attachmentID uint64, preview, download bool) (interface{}, error) { return func(w http.ResponseWriter, req *http.Request) { - att, err := ctrl.attachment.With(ctx).FindByID(ID) + att, err := ctrl.attachment.With(ctx).FindByID(namespaceID, attachmentID) if err != nil { // Simplify error handling for now w.WriteHeader(http.StatusNotFound) @@ -140,17 +138,33 @@ func (ctrl Attachment) serve(ctx context.Context, ID uint64, preview, download b }, nil } -func makeAttachmentPayload(a *types.Attachment, userID uint64) *attachmentPayload { - if a == nil { - return nil +func (ctrl Attachment) makeFilterPayload(ctx context.Context, aa types.AttachmentSet, f types.AttachmentFilter, err error) (*attachmentSetPayload, error) { + if err != nil { + return nil, err + } + + asp := &attachmentSetPayload{Filter: f, Set: make([]*attachmentPayload, len(aa))} + + for i := range aa { + asp.Set[i], _ = makeAttachmentPayload(ctx, aa[i], nil) + } + + return asp, nil +} + +func makeAttachmentPayload(ctx context.Context, a *types.Attachment, err error) (*attachmentPayload, error) { + if err != nil || a == nil { + return nil, err } var ( - signParams = fmt.Sprintf("?sign=%s&userID=%d", auth.DefaultSigner.Sign(userID, a.ID), userID) + userID = auth.GetIdentityFromContext(ctx).Identity() + signParams = fmt.Sprintf("?sign=%s&userID=%d", auth.DefaultSigner.Sign(userID, a.NamespaceID, a.ID), userID) preview string - baseURL = fmt.Sprintf("/attachment/%s/%d/", a.Kind, a.ID) + baseURL = fmt.Sprintf("/namespace/%d/attachment/%s/%d/", a.NamespaceID, a.Kind, a.ID) ) + if a.Meta.Preview != nil { var ext = a.Meta.Preview.Extension if ext == "" { @@ -160,14 +174,10 @@ func makeAttachmentPayload(a *types.Attachment, userID uint64) *attachmentPayloa preview = baseURL + fmt.Sprintf("preview.%s", ext) } - return &attachmentPayload{ - ID: a.ID, - OwnerID: a.OwnerID, - Url: baseURL + fmt.Sprintf("original/%s", url.PathEscape(a.Name)) + signParams, - PreviewUrl: preview + signParams, - Meta: a.Meta, - Name: a.Name, - CreatedAt: a.CreatedAt, - UpdatedAt: a.UpdatedAt, - } + ap := &attachmentPayload{a} + + ap.Url = baseURL + fmt.Sprintf("original/%s", url.PathEscape(a.Name)) + signParams + ap.PreviewUrl = preview + signParams + + return ap, nil } diff --git a/compose/rest/handlers/attachment.go b/compose/rest/handlers/attachment.go index e2e641e1e..453e4106e 100644 --- a/compose/rest/handlers/attachment.go +++ b/compose/rest/handlers/attachment.go @@ -29,7 +29,8 @@ import ( // Internal API interface type AttachmentAPI interface { List(context.Context, *request.AttachmentList) (interface{}, error) - Details(context.Context, *request.AttachmentDetails) (interface{}, error) + Read(context.Context, *request.AttachmentRead) (interface{}, error) + Delete(context.Context, *request.AttachmentDelete) (interface{}, error) Original(context.Context, *request.AttachmentOriginal) (interface{}, error) Preview(context.Context, *request.AttachmentPreview) (interface{}, error) } @@ -37,7 +38,8 @@ type AttachmentAPI interface { // HTTP API interface type Attachment struct { List func(http.ResponseWriter, *http.Request) - Details func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) Original func(http.ResponseWriter, *http.Request) Preview func(http.ResponseWriter, *http.Request) } @@ -64,14 +66,34 @@ func NewAttachment(ah AttachmentAPI) *Attachment { return } }, - Details: func(w http.ResponseWriter, r *http.Request) { + Read: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - params := request.NewAttachmentDetails() + params := request.NewAttachmentRead() if err := params.Fill(r); err != nil { resputil.JSON(w, err) return } - if value, err := ah.Details(r.Context(), params); err != nil { + if value, err := ah.Read(r.Context(), params); err != nil { + resputil.JSON(w, err) + return + } else { + switch fn := value.(type) { + case func(http.ResponseWriter, *http.Request): + fn(w, r) + return + } + resputil.JSON(w, value) + return + } + }, + Delete: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAttachmentDelete() + if err := params.Fill(r); err != nil { + resputil.JSON(w, err) + return + } + if value, err := ah.Delete(r.Context(), params); err != nil { resputil.JSON(w, err) return } else { @@ -131,7 +153,8 @@ func (ah *Attachment) MountRoutes(r chi.Router, middlewares ...func(http.Handler r.Group(func(r chi.Router) { r.Use(middlewares...) r.Get("/namespace/{namespaceID}/attachment/{kind}/", ah.List) - r.Get("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}", ah.Details) + r.Get("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}", ah.Read) + r.Delete("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}", ah.Delete) r.Get("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}/original/{name}", ah.Original) r.Get("/namespace/{namespaceID}/attachment/{kind}/{attachmentID}/preview.{ext}", ah.Preview) }) diff --git a/compose/rest/namespace.go b/compose/rest/namespace.go index 2c693756d..b1767638d 100644 --- a/compose/rest/namespace.go +++ b/compose/rest/namespace.go @@ -47,8 +47,8 @@ func (ctrl Namespace) List(ctx context.Context, r *request.NamespaceList) (inter Page: r.Page, } - nn, filter, err := ctrl.namespace.With(ctx).Find(f) - return ctrl.makeFilterPayload(ctx, nn, filter, err) + set, filter, err := ctrl.namespace.With(ctx).Find(f) + return ctrl.makeFilterPayload(ctx, set, filter, err) } func (ctrl Namespace) Create(ctx context.Context, r *request.NamespaceCreate) (interface{}, error) { @@ -92,16 +92,11 @@ func (ctrl Namespace) Delete(ctx context.Context, r *request.NamespaceDelete) (i return nil, err } - err = ctrl.namespace.With(ctx).DeleteByID(r.NamespaceID) - if err != nil { - return nil, err - } else { - return resputil.Success("deleted"), nil - } + return resputil.OK(), ctrl.namespace.With(ctx).DeleteByID(r.NamespaceID) } func (ctrl Namespace) makePayload(ctx context.Context, ns *types.Namespace, err error) (*namespacePayload, error) { - if err != nil { + if err != nil || ns == nil { return nil, err } diff --git a/compose/rest/page.go b/compose/rest/page.go index 4d42aed83..8eb346c24 100644 --- a/compose/rest/page.go +++ b/compose/rest/page.go @@ -8,7 +8,6 @@ import ( "github.com/crusttech/crust/compose/internal/service" "github.com/crusttech/crust/compose/rest/request" "github.com/crusttech/crust/compose/types" - "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/internal/payload" ) @@ -85,15 +84,12 @@ func (ctrl *Page) Upload(ctx context.Context, r *request.PageUpload) (interface{ defer file.Close() a, err := ctrl.attachment.With(ctx).CreatePageAttachment( + r.NamespaceID, r.Upload.Filename, r.Upload.Size, file, r.PageID, ) - if err != nil { - return nil, err - } - - return makeAttachmentPayload(a, auth.GetIdentityFromContext(ctx).Identity()), nil + return makeAttachmentPayload(ctx, a, err) } diff --git a/compose/rest/record.go b/compose/rest/record.go index c565b22be..13f40e39a 100644 --- a/compose/rest/record.go +++ b/compose/rest/record.go @@ -5,12 +5,11 @@ import ( "github.com/titpetric/factory/resputil" + "github.com/pkg/errors" + "github.com/crusttech/crust/compose/internal/service" "github.com/crusttech/crust/compose/rest/request" "github.com/crusttech/crust/compose/types" - "github.com/crusttech/crust/internal/auth" - - "github.com/pkg/errors" ) var _ = errors.Wrap @@ -64,6 +63,7 @@ func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interf defer file.Close() a, err := ctrl.attachment.With(ctx).CreateRecordAttachment( + r.NamespaceID, r.Upload.Filename, r.Upload.Size, file, @@ -72,9 +72,5 @@ func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interf r.FieldName, ) - if err != nil { - return nil, err - } - - return makeAttachmentPayload(a, auth.GetIdentityFromContext(ctx).Identity()), nil + return makeAttachmentPayload(ctx, a, err) } diff --git a/compose/rest/request/attachment.go b/compose/rest/request/attachment.go index 7325e683d..fe361837e 100644 --- a/compose/rest/request/attachment.go +++ b/compose/rest/request/attachment.go @@ -115,8 +115,8 @@ func (aReq *AttachmentList) Fill(r *http.Request) (err error) { var _ RequestFiller = NewAttachmentList() -// Attachment details request parameters -type AttachmentDetails struct { +// Attachment read request parameters +type AttachmentRead struct { AttachmentID uint64 `json:",string"` Kind string NamespaceID uint64 `json:",string"` @@ -124,11 +124,11 @@ type AttachmentDetails struct { UserID uint64 `json:",string"` } -func NewAttachmentDetails() *AttachmentDetails { - return &AttachmentDetails{} +func NewAttachmentRead() *AttachmentRead { + return &AttachmentRead{} } -func (aReq *AttachmentDetails) Fill(r *http.Request) (err error) { +func (aReq *AttachmentRead) Fill(r *http.Request) (err error) { if strings.ToLower(r.Header.Get("content-type")) == "application/json" { err = json.NewDecoder(r.Body).Decode(aReq) @@ -170,7 +170,64 @@ func (aReq *AttachmentDetails) Fill(r *http.Request) (err error) { return err } -var _ RequestFiller = NewAttachmentDetails() +var _ RequestFiller = NewAttachmentRead() + +// Attachment delete request parameters +type AttachmentDelete struct { + AttachmentID uint64 `json:",string"` + Kind string + NamespaceID uint64 `json:",string"` + Sign string + UserID uint64 `json:",string"` +} + +func NewAttachmentDelete() *AttachmentDelete { + return &AttachmentDelete{} +} + +func (aReq *AttachmentDelete) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(aReq) + + 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]) + } + + aReq.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID")) + aReq.Kind = chi.URLParam(r, "kind") + aReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) + if val, ok := get["sign"]; ok { + + aReq.Sign = val + } + if val, ok := get["userID"]; ok { + + aReq.UserID = parseUInt64(val) + } + + return err +} + +var _ RequestFiller = NewAttachmentDelete() // Attachment original request parameters type AttachmentOriginal struct { diff --git a/compose/types/attachment.go b/compose/types/attachment.go index 61982617d..f23028e75 100644 --- a/compose/types/attachment.go +++ b/compose/types/attachment.go @@ -10,30 +10,34 @@ import ( type ( Attachment struct { - ID uint64 `db:"id" json:"attachmentID,omitempty"` - OwnerID uint64 `db:"rel_owner" json:"ownerID,omitempty"` - Kind string `db:"kind" json:"-"` - Url string `db:"url" json:"url,omitempty"` - PreviewUrl string `db:"preview_url"json:"previewUrl,omitempty"` - Name string `db:"name" json:"name,omitempty"` - Meta attachmentMeta `db:"meta" json:"meta"` - 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"` + ID uint64 `db:"id" json:"attachmentID,string"` + OwnerID uint64 `db:"rel_owner" json:"ownerID,string"` + Kind string `db:"kind" json:"-"` + Url string `db:"url" json:"url,omitempty"` + PreviewUrl string `db:"preview_url" json:"previewUrl,omitempty"` + Name string `db:"name" json:"name,omitempty"` + Meta attachmentMeta `db:"meta" json:"meta"` + + NamespaceID uint64 `db:"rel_namespace" json:"namespaceID,string"` + + 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"` } // AttachmentFilter is used for filtering and as a return value from Find AttachmentFilter struct { - Kind string `json:"kind,omitempty"` - PageID uint64 `json:"pageID,string,omitempty"` - RecordID uint64 `json:"recordID,string,omitempty"` - ModuleID uint64 `json:"moduleID,string,omitempty"` - FieldName string `json:"fieldName,omitempty"` - Filter string `json:"filter"` - Page uint `json:"page"` - PerPage uint `json:"perPage"` - Sort string `json:"sort"` - Count uint `json:"count"` + NamespaceID uint64 `json:"namespaceID,string"` + Kind string `json:"kind,omitempty"` + PageID uint64 `json:"pageID,string,omitempty"` + RecordID uint64 `json:"recordID,string,omitempty"` + ModuleID uint64 `json:"moduleID,string,omitempty"` + FieldName string `json:"fieldName,omitempty"` + Filter string `json:"filter"` + Page uint `json:"page"` + PerPage uint `json:"perPage"` + Sort string `json:"sort"` + Count uint `json:"count"` } attachmentImageMeta struct { diff --git a/compose/types/chart.go b/compose/types/chart.go index 03a44486f..0832a8e43 100644 --- a/compose/types/chart.go +++ b/compose/types/chart.go @@ -14,10 +14,20 @@ type ( Name string `json:"name" db:"name"` Config types.JSONText `json:"config" db:"config"` + NamespaceID uint64 `json:"namespaceID,string" db:"rel_namespace,string"` + 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"` } + + ChartFilter struct { + Query string `json:"query"` + Page uint `json:"page"` + PerPage uint `json:"perPage"` + Sort string `json:"sort"` + Count uint `json:"count"` + } ) // Resource returns a system resource ID for this type diff --git a/compose/types/module.go b/compose/types/module.go index 1a73f0d94..06590f390 100644 --- a/compose/types/module.go +++ b/compose/types/module.go @@ -16,6 +16,8 @@ type ( Fields ModuleFieldSet `json:"fields" db:"-"` Page *Page `json:"page,omitempty"` + NamespaceID uint64 `json:"namespaceID,string" db:"rel_namespace"` + 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"` diff --git a/compose/types/namespace.go b/compose/types/namespace.go index ef1390ef7..70439e26e 100644 --- a/compose/types/namespace.go +++ b/compose/types/namespace.go @@ -10,15 +10,15 @@ import ( type ( Namespace struct { - ID uint64 `json:"namespaceID,string" db:"id"` - Name string `json:"name" db:"name"` - Slug string `json:"slug" db:"slug"` - Enabled bool `json:"enabled" db:"enabled"` - Meta types.JSONText `json:"meta" db:"meta"` + ID uint64 `db:"id" json:"namespaceID,string"` + Name string `db:"name" json:"name"` + Slug string `db:"slug" json:"slug"` + Enabled bool `db:"enabled" json:"enabled"` + Meta types.JSONText `db:"meta" json:"meta"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` - UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"` - DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"` + 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"` } NamespaceFilter struct { @@ -31,7 +31,7 @@ type ( ) const ( - NamespaceCRM uint64 = 10000000 + NamespaceCRM uint64 = 88714882739863655 ) // Resource returns a system resource ID for this type diff --git a/compose/types/page.go b/compose/types/page.go index 1a20ac8e7..b0fdabbb8 100644 --- a/compose/types/page.go +++ b/compose/types/page.go @@ -12,6 +12,8 @@ type ( ID uint64 `json:"pageID,string" db:"id"` SelfID uint64 `json:"selfID,string" db:"self_id"` + NamespaceID uint64 `json:"namespaceID,string" db:"rel_namespace"` + ModuleID uint64 `json:"moduleID,string" db:"module_id"` Module *Module `json:"module,omitempty" db:"-"` diff --git a/compose/types/record.go b/compose/types/record.go index e542bc3c3..7517d1697 100644 --- a/compose/types/record.go +++ b/compose/types/record.go @@ -14,6 +14,8 @@ type ( Values RecordValueSet `json:"values,omitempty" db:"-"` + NamespaceID uint64 `json:"namespaceID,string", db:"rel_namespace"` + OwnedBy uint64 `db:"owned_by" json:"ownedBy,string"` CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"` CreatedBy uint64 `db:"created_by" json:"createdBy,string" ` diff --git a/compose/types/trigger.go b/compose/types/trigger.go index 3baf2916c..59a389be3 100644 --- a/compose/types/trigger.go +++ b/compose/types/trigger.go @@ -18,6 +18,8 @@ type ( Enabled bool `json:"enabled" db:"enabled"` Source string `json:"source" db:"source"` + NamespaceID uint64 `json:"namespaceID,string" db:"rel_namespace"` + 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"` diff --git a/docs/compose/README.md b/docs/compose/README.md index cde1eb7c8..5e81254c1 100644 --- a/docs/compose/README.md +++ b/docs/compose/README.md @@ -4,6 +4,7 @@ | ------ | -------- | ------- | | `GET` | `/namespace/{namespaceID}/attachment/{kind}/` | List, filter all page attachments | | `GET` | `/namespace/{namespaceID}/attachment/{kind}/{attachmentID}` | Attachment details | +| `DELETE` | `/namespace/{namespaceID}/attachment/{kind}/{attachmentID}` | Delete attachment | | `GET` | `/namespace/{namespaceID}/attachment/{kind}/{attachmentID}/original/{name}` | Serves attached file | | `GET` | `/namespace/{namespaceID}/attachment/{kind}/{attachmentID}/preview.{ext}` | Serves preview of an attached file | @@ -20,7 +21,7 @@ | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | pageID | uint64 | GET | Filter attachments by page ID | N/A | NO | -| moduleID | uint64 | GET | Filter attachments by mnodule ID | N/A | NO | +| moduleID | uint64 | GET | Filter attachments by module ID | N/A | NO | | recordID | uint64 | GET | Filter attachments by record ID | N/A | NO | | fieldName | string | GET | Filter attachments by field name | N/A | NO | | page | uint | GET | Page number (0 based) | N/A | NO | @@ -48,6 +49,24 @@ | sign | string | GET | Signature | N/A | YES | | userID | uint64 | GET | User ID | N/A | YES | +## Delete attachment + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}/attachment/{kind}/{attachmentID}` | HTTP/S | DELETE | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| attachmentID | uint64 | PATH | Attachment ID | N/A | YES | +| kind | string | PATH | Attachment kind | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| sign | string | GET | Signature | N/A | YES | +| userID | uint64 | GET | User ID | N/A | YES | + ## Serves attached file #### Method