diff --git a/.drone.yml b/.drone.yml index 8913a10c1..3ea1411a9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,7 +15,7 @@ steps: # GOARCH: amd64 # commands: # - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/crust-$GOOS-$GOARCH cmd/crust/*.go -# - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/crm-$GOOS-$GOARCH cmd/crm/*.go +# - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/compose-$GOOS-$GOARCH cmd/compose/*.go # - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/messaging-$GOOS-$GOARCH cmd/messaging/*.go # - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/system-$GOOS-$GOARCH cmd/system/*.go # - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/system-cli-$GOOS-$GOARCH cmd/system-cli/*.go @@ -32,12 +32,12 @@ steps: from_secret: docker_hub_password -- name: api-crm +- name: api-compose image: plugins/docker settings: - repo: crusttech/api-crm + repo: crusttech/api-compose auto_tag: true # generate tag names automatically based on git branch and git tag - dockerfile: Dockerfile.api-crm + dockerfile: Dockerfile.api-compose username: from_secret: docker_hub_username password: @@ -93,21 +93,27 @@ steps: CI: circleci AUTH_OIDC_ENABLED: 0 AUTH_JWT_SECRET: bRxJ37sJ6Qu4 + DB_DSN: crust:crust@tcp(crust-db:3306)/crust?collation=utf8mb4_general_ci + SYSTEM_DB_DSN: crust:crust@tcp(crust-db:3306)/crust?collation=utf8mb4_general_ci + COMPOSE_DB_DSN: crust:crust@tcp(crust-db:3306)/crust?collation=utf8mb4_general_ci + MESSAGING_DB_DSN: crust:crust@tcp(crust-db:3306)/crust?collation=utf8mb4_general_ci commands: - make mocks - make codegen - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/crust-$GOOS-$GOARCH cmd/crust/*.go - - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/crm-$GOOS-$GOARCH cmd/crm/*.go + - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/compose-$GOOS-$GOARCH cmd/compose/*.go - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/messaging-$GOOS-$GOARCH cmd/messaging/*.go - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/system-$GOOS-$GOARCH cmd/system/*.go - go build -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime=`date +%FT%T%z` -X github.com/crusttech/crust/internal/version.Version=`git describe --always --tags`" -o build/system-cli-$GOOS-$GOARCH cmd/system-cli/*.go # check all tests are able to build - wait-for-it.sh -t 60 --strict crust-db:3306 -- echo "Crust DB1 is up" + - ./build/system-cli-$GOOS-$GOARCH roles reset - gotest -v --tags="migrations" ./system/db/... - - gotest -v --tags="migrations" ./crm/db/... + - gotest -v --tags="migrations" ./compose/db/... - gotest -v --tags="migrations" ./messaging/db/... - - go test -run=^$ --tags="unit integration external" ./cmd/... ./internal/... ./crm/... ./messaging/... ./system/... - - gotest -failfast --coverprofile=coverage.txt -v --tags="unit integration" ./internal/... ./system/... ./crm/... ./messaging/... + - go test -run=^$ --tags="unit integration external" ./cmd/... ./internal/... ./compose/... ./messaging/... ./system/... + - gotest -failfast --coverprofile=coverage.txt -v --tags="unit integration" ./internal/... ./system/... ./compose/... ./messaging/... + - name: coverage image: plugins/codecov settings: diff --git a/.env.sample b/.env.sample index 540c702f5..d3a7367f2 100644 --- a/.env.sample +++ b/.env.sample @@ -4,11 +4,11 @@ MESSAGING_HTTP_ERROR_TRACING=1 MESSAGING_DB_DSN=crust:crust@tcp(localhost:3306)/crust?collation=utf8mb4_general_ci MESSAGING_DB_PROFILER=stdout -CRM_HTTP_ADDR=:3001 -CRM_HTTP_PRETTY_JSON=1 -CRM_HTTP_ERROR_TRACING=1 -CRM_DB_DSN=crust:crust@tcp(localhost:3306)/crust?collation=utf8mb4_general_ci -CRM_DB_PROFILER=stdout +COMPOSE_HTTP_ADDR=:3001 +COMPOSE_HTTP_PRETTY_JSON=1 +COMPOSE_HTTP_ERROR_TRACING=1 +COMPOSE_DB_DSN=crust:crust@tcp(localhost:3306)/crust?collation=utf8mb4_general_ci +COMPOSE_DB_PROFILER=stdout SYSTEM_HTTP_ADDR=:3002 SYSTEM_HTTP_PRETTY_JSON=1 diff --git a/.gitignore b/.gitignore index 857e64f86..8618e5b13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /coverage.txt /*.iml /.env* +/.dev* /.cover* /public_html /build diff --git a/.realize.yaml b/.realize.yaml index 8540df6cc..0c160ebc8 100755 --- a/.realize.yaml +++ b/.realize.yaml @@ -66,8 +66,8 @@ schema: - ../../.git # ###################################################################################################################### -- name: crm - path: cmd/crm +- name: cmp + path: cmd/compose commands: vet: status: false @@ -80,7 +80,7 @@ schema: watcher: paths: - . - - ../../crm + - ../../compose - ../../internal extensions: - go diff --git a/Dockerfile.api-crm b/Dockerfile.api-compose similarity index 87% rename from Dockerfile.api-crm rename to Dockerfile.api-compose index 30fb999df..cfc5bea92 100644 --- a/Dockerfile.api-crm +++ b/Dockerfile.api-compose @@ -8,7 +8,7 @@ COPY . . RUN mkdir /build; \ go build \ -ldflags "-X github.com/crusttech/crust/internal/version.BuildTime="`date +%FT%T%z`" -X github.com/crusttech/crust/internal/version.Version="`git describe --always --tags` \ - -o /build/crm cmd/crm/*.go + -o /build/compose cmd/compose/*.go ## == target image == @@ -23,4 +23,4 @@ WORKDIR /crust EXPOSE 80 -ENTRYPOINT /crust/crm +ENTRYPOINT /crust/compose diff --git a/Makefile b/Makefile index 40ce13dac..efc1596d9 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ -.PHONY: help docker docker-push realize dep dep.update test test.messaging test.crm qa critic vet codegen integration +.PHONY: help docker docker-push realize dep dep.update test test.messaging test.compose qa critic vet codegen integration PKG = "github.com/$(shell cat .project)" GO = go GOGET = $(GO) get -u -BASEPKGS = system crm messaging -IMAGES = system crm messaging +BASEPKGS = system compose messaging +IMAGES = system compose messaging ######################################################################################################################## # Tool bins @@ -25,7 +25,7 @@ help: @echo @echo - vet - run go vet on all code @echo - critic - run go critic on all code - @echo - test.crm - individual package unit tests + @echo - test.compose - individual package unit tests @echo - test.messaging - individual package unit tests @echo - test - run all available unit tests @echo - qa - run vet, critic and test on code @@ -86,13 +86,13 @@ test.events: $(GOTEST) perl -pi -e 's/command-line-arguments/.\/messaging\/repository/g' .cover.out $(GO) tool cover -func=.cover.out | grep --color "^\|[^0-9]0.0%" -test.crm: $(GOTEST) - $(GOTEST) -covermode count -coverprofile .cover.out -v ./crm/service/... +test.compose: $(GOTEST) + $(GOTEST) -covermode count -coverprofile .cover.out -v ./compose/service/... $(GO) tool cover -func=.cover.out | grep --color "^\|[^0-9]0.0%" -test.crm.db: $(GOTEST) - cd crm/db && $(GO) generate && cd ../.. - $(GOTEST) -covermode count -coverprofile .cover.out -v ./crm/db/... +test.compose.db: $(GOTEST) + cd compose/db && $(GO) generate && cd ../.. + $(GOTEST) -covermode count -coverprofile .cover.out -v ./compose/db/... $(GO) tool cover -func=.cover.out | grep --color "^\|[^0-9]0.0%" test.system: $(GOTEST) diff --git a/README.md b/README.md index db270a9f7..7a174aa86 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Crust brings your user ecosystem and essential applications together on one plat **Crust CRM** is the highly flexible, scalable and open source Salesforce alternative, that enables you to sell faster and interact with leads, clients and team members easier then ever before. Seamless integration with Crust Messaging and Crust Identity and Access Management make it the most complete and flexible self-hosted CRM platform on the market. -**Crust Unify** manages user experience for Crust applications, such as CRM and Messaging, as well as providing an integrated interface for third party or other bespoke applications. 100% responsive and with an intuitive design, Crust Unify increases productivity and ease of access to all IT resources. +**Crust Unify** manages user experience for Crust applications, such as Compose and Messaging, as well as providing an integrated interface for third party or other bespoke applications. 100% responsive and with an intuitive design, Crust Unify increases productivity and ease of access to all IT resources. ## Contributing @@ -18,9 +18,7 @@ Crust brings your user ecosystem and essential applications together on one plat Copy `.env.example` to `.env` and make proper modifications for your local environment. An access to a (local) instance of MySQL must be available. -Configure access to your database with `SAM_DB_DSN` and `CRM_DB_DSN`. - -Please check the options available with `./app -h`. +Configure access to your database with `SYSTEM_DB_DSN`, `MESSAGING_DB_DSN` and `COMPOSE_DB_DSN`. The database will be populated with migrations at the start of each service. You don't need to pre-populate the database, just make sure that your permissions include CREATE and ALTER capabilities. diff --git a/api/crm/spec.json b/api/compose/spec.json similarity index 72% rename from api/crm/spec.json rename to api/compose/spec.json index f773d02b4..7bdc718ab 100644 --- a/api/crm/spec.json +++ b/api/compose/spec.json @@ -24,11 +24,170 @@ } ] }, + { + "title": "Namespaces", + "parameters": {}, + "entrypoint": "namespace", + "path": "/namespace", + "authentication": [], + "struct": [ + { + "imports": [ + "sqlxTypes github.com/jmoiron/sqlx/types", + "time" + ] + } + ], + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List namespaces", + "path": "/", + "parameters": { + "get": [ + { + "type": "string", + "name": "query", + "required": false, + "title": "Search query" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" + } + ] + } + }, + { + "name": "create", + "method": "POST", + "title": "Create namespace", + "path": "/", + "parameters": { + "post": [ + { + "type": "string", + "name": "name", + "required": true, + "title": "Name" + }, + { + "type": "string", + "name": "slug", + "required": true, + "title": "Slug (url path part)" + }, + { + "type": "bool", + "name": "enabled", + "required": true, + "title": "Enabled" + }, + { + "type": "sqlxTypes.JSONText", + "name": "meta", + "required": true, + "title": "Meta data" + } + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read namespace", + "path": "/{namespaceID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "ID" + } + ] + } + }, + { + "name": "update", + "method": "POST", + "title": "Update namespace", + "path": "/{namespaceID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "ID" + } + ], + "post": [ + { + "type": "string", + "name": "name", + "required": true, + "title": "Name" + }, + { + "type": "string", + "name": "slug", + "required": true, + "title": "Slug (url path part)" + }, + { + "type": "bool", + "name": "enabled", + "required": true, + "title": "Enabled" + }, + { + "type": "sqlxTypes.JSONText", + "name": "meta", + "required": true, + "title": "Meta data" + }, + { + "type": "*time.Time", + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date" + } + ] + } + }, + { + "name": "delete", + "method": "DELETE", + "title": "Delete namespace", + "path": "/{namespaceID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "ID" + } + ] + } + } + ] + }, { "title": "Pages", - "description": "CRM module pages", + "description": "Compose pages", "entrypoint": "page", - "path": "/page", + "path": "/namespace/{namespaceID}/page", "authentication": [], "struct": [ { @@ -37,6 +196,16 @@ ] } ], + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "Namespace ID" + } + ] + }, "apis": [ { "name": "list", @@ -50,6 +219,24 @@ "name": "selfID", "required": false, "title": "Parent page ID" + }, + { + "type": "string", + "name": "query", + "required": false, + "title": "Search query" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" } ] } @@ -245,15 +432,26 @@ }, { "title": "Modules", - "description": "CRM module definitions", + "description": "Compose module definitions", "entrypoint": "module", - "path": "/module", + "path": "/namespace/{namespaceID}/module", "authentication": [], + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "Namespace ID" + } + ] + }, "struct": [ { "imports": [ "sqlxTypes github.com/jmoiron/sqlx/types", - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types", + "time" ] } ], @@ -270,6 +468,18 @@ "name": "query", "required": false, "title": "Search query" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" } ] } @@ -350,6 +560,12 @@ "name": "meta", "required": true, "title": "Module meta data" + }, + { + "type": "*time.Time", + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date" } ] } @@ -374,19 +590,25 @@ }, { "title": "Records", - "description": "CRM records ", + "description": "Compose records", "entrypoint": "record", - "path": "/module/{moduleID}/record", + "path": "/namespace/{namespaceID}/module/{moduleID}/record", "authentication": [], "struct": [ { "imports": [ - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" ] } ], "parameters": { "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "Namespace ID" + }, { "type": "uint64", "name": "moduleID", @@ -439,13 +661,13 @@ }, { "name": "page", - "type": "int", + "type": "uint", "required": false, "title": "Page number (0 based)" }, { "name": "perPage", - "type": "int", + "type": "uint", "required": false, "title": "Returned items per page (default 50)" }, @@ -564,27 +786,60 @@ }, { "title": "Charts", - "path": "/chart", + "path": "/namespace/{namespaceID}/chart", "entrypoint": "chart", "authentication": [], "struct": [ { "imports": [ - "sqlxTypes github.com/jmoiron/sqlx/types" + "sqlxTypes github.com/jmoiron/sqlx/types", + "time" ] } ], + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "Namespace ID" + } + ] + }, "apis": [ { "name": "list", "method": "GET", - "title": "List/read charts from module section", - "path": "/" + "title": "List/read charts", + "path": "/", + "parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query to match against charts", + "type": "string" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" + } + ] + } }, { "name": "create", "method": "POST", - "title": "List/read charts from module section", + "title": "List/read charts ", "path": "/", "parameters": { "post": [ @@ -606,7 +861,7 @@ { "name": "read", "method": "GET", - "title": "Read charts by ID from module section", + "title": "Read charts by ID", "path": "/{chartID}", "parameters": { "path": [ @@ -622,7 +877,7 @@ { "name": "update", "method": "POST", - "title": "Add/update charts in module section", + "title": "Add/update charts", "path": "/{chartID}", "parameters": { "path": [ @@ -645,6 +900,12 @@ "title": "Chart name", "type": "string", "required": true + }, + { + "type": "*time.Time", + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date" } ] } @@ -669,10 +930,27 @@ }, { "title": "Triggers", - "description": "CRM Triggers", + "description": "Compose Triggers", "entrypoint": "trigger", - "path": "/trigger", + "path": "/namespace/{namespaceID}/trigger", "authentication": [], + "parameters": { + "path": [ + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "Namespace ID" + } + ] + }, + "struct": [ + { + "imports": [ + "time" + ] + } + ], "apis": [ { "name": "list", @@ -686,6 +964,24 @@ "name": "moduleID", "required": false, "title": "Filter triggers by module" + }, + { + "type": "string", + "name": "query", + "required": false, + "title": "Search query" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" } ] } @@ -726,6 +1022,12 @@ "name": "source", "required": false, "title": "Trigger source code" + }, + { + "type": "*time.Time", + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date" } ] } @@ -814,7 +1116,7 @@ }, { "title": "Notifications", - "description": "CRM Notifications", + "description": "Compose Notifications", "entrypoint": "notification", "path": "/notification", "authentication": [], @@ -829,7 +1131,7 @@ { "name": "email/send", "method": "POST", - "title": "Send email from the CRM", + "title": "Send email from the Compose", "path": "/email", "parameters": { "post": [ @@ -870,7 +1172,7 @@ }, { "title": "Attachments", - "path": "/attachment/{kind}", + "path": "/namespace/{namespaceID}/attachment/{kind}", "entrypoint": "attachment", "authentication": [ "Client ID", @@ -883,19 +1185,25 @@ "type": "string", "required": true, "title": "Attachment kind" + }, + { + "type": "uint64", + "name": "namespaceID", + "required": true, + "title": "Namespace ID" } ], "get": [ { "type": "string", "name": "sign", - "required": true, + "required": false, "title": "Signature" }, { "type": "uint64", "name": "userID", - "required": true, + "required": false, "title": "User ID" } ] @@ -918,7 +1226,7 @@ "type": "uint64", "name": "moduleID", "required": false, - "title": "Filter attachments by mnodule ID" + "title": "Filter attachments by module ID" }, { "name": "recordID", @@ -948,7 +1256,7 @@ } }, { - "name": "details", + "name": "read", "path": "/{attachmentID}", "method": "GET", "title": "Attachment details", @@ -963,6 +1271,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/crm/spec/attachment.json b/api/compose/spec/attachment.json similarity index 83% rename from api/crm/spec/attachment.json rename to api/compose/spec/attachment.json index 5679850d8..6b4f62a6b 100644 --- a/api/crm/spec/attachment.json +++ b/api/compose/spec/attachment.json @@ -6,13 +6,13 @@ "get": [ { "name": "sign", - "required": true, + "required": false, "title": "Signature", "type": "string" }, { "name": "userID", - "required": true, + "required": false, "title": "User ID", "type": "uint64" } @@ -23,6 +23,12 @@ "required": true, "title": "Attachment kind", "type": "string" + }, + { + "name": "namespaceID", + "required": true, + "title": "Namespace ID", + "type": "uint64" } ] }, @@ -31,7 +37,7 @@ "Client ID", "Session ID" ], - "Path": "/attachment/{kind}", + "Path": "/namespace/{namespaceID}/attachment/{kind}", "APIs": [ { "Name": "list", @@ -49,7 +55,7 @@ { "name": "moduleID", "required": false, - "title": "Filter attachments by mnodule ID", + "title": "Filter attachments by module ID", "type": "uint64" }, { @@ -80,7 +86,7 @@ } }, { - "Name": "details", + "Name": "read", "Method": "GET", "Title": "Attachment details", "Path": "/{attachmentID}", @@ -95,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/api/crm/spec/chart.json b/api/compose/spec/chart.json similarity index 62% rename from api/crm/spec/chart.json rename to api/compose/spec/chart.json index bf1cb21fb..d2e5dc902 100644 --- a/api/crm/spec/chart.json +++ b/api/compose/spec/chart.json @@ -4,26 +4,57 @@ "Struct": [ { "imports": [ - "sqlxTypes github.com/jmoiron/sqlx/types" + "sqlxTypes github.com/jmoiron/sqlx/types", + "time" ] } ], - "Parameters": null, + "Parameters": { + "path": [ + { + "name": "namespaceID", + "required": true, + "title": "Namespace ID", + "type": "uint64" + } + ] + }, "Protocol": "", "Authentication": [], - "Path": "/chart", + "Path": "/namespace/{namespaceID}/chart", "APIs": [ { "Name": "list", "Method": "GET", - "Title": "List/read charts from module section", + "Title": "List/read charts", "Path": "/", - "Parameters": null + "Parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query to match against charts", + "type": "string" + }, + { + "name": "page", + "required": false, + "title": "Page number (0 based)", + "type": "uint" + }, + { + "name": "perPage", + "required": false, + "title": "Returned items per page (default 50)", + "type": "uint" + } + ] + } }, { "Name": "create", "Method": "POST", - "Title": "List/read charts from module section", + "Title": "List/read charts ", "Path": "/", "Parameters": { "post": [ @@ -45,7 +76,7 @@ { "Name": "read", "Method": "GET", - "Title": "Read charts by ID from module section", + "Title": "Read charts by ID", "Path": "/{chartID}", "Parameters": { "path": [ @@ -61,7 +92,7 @@ { "Name": "update", "Method": "POST", - "Title": "Add/update charts in module section", + "Title": "Add/update charts", "Path": "/{chartID}", "Parameters": { "path": [ @@ -84,6 +115,12 @@ "required": true, "title": "Chart name", "type": "string" + }, + { + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date", + "type": "*time.Time" } ] } diff --git a/api/crm/spec/module.json b/api/compose/spec/module.json similarity index 75% rename from api/crm/spec/module.json rename to api/compose/spec/module.json index 4d55ad861..a123fa355 100644 --- a/api/crm/spec/module.json +++ b/api/compose/spec/module.json @@ -1,19 +1,29 @@ { "Title": "Modules", - "Description": "CRM module definitions", + "Description": "Compose module definitions", "Interface": "Module", "Struct": [ { "imports": [ "sqlxTypes github.com/jmoiron/sqlx/types", - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types", + "time" ] } ], - "Parameters": null, + "Parameters": { + "path": [ + { + "name": "namespaceID", + "required": true, + "title": "Namespace ID", + "type": "uint64" + } + ] + }, "Protocol": "", "Authentication": [], - "Path": "/module", + "Path": "/namespace/{namespaceID}/module", "APIs": [ { "Name": "list", @@ -27,6 +37,18 @@ "required": false, "title": "Search query", "type": "string" + }, + { + "name": "page", + "required": false, + "title": "Page number (0 based)", + "type": "uint" + }, + { + "name": "perPage", + "required": false, + "title": "Returned items per page (default 50)", + "type": "uint" } ] } @@ -107,6 +129,12 @@ "required": true, "title": "Module meta data", "type": "sqlxTypes.JSONText" + }, + { + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date", + "type": "*time.Time" } ] } diff --git a/api/compose/spec/namespace.json b/api/compose/spec/namespace.json new file mode 100644 index 000000000..bf994f511 --- /dev/null +++ b/api/compose/spec/namespace.json @@ -0,0 +1,160 @@ +{ + "Title": "Namespaces", + "Interface": "Namespace", + "Struct": [ + { + "imports": [ + "sqlxTypes github.com/jmoiron/sqlx/types", + "time" + ] + } + ], + "Parameters": {}, + "Protocol": "", + "Authentication": [], + "Path": "/namespace", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "List namespaces", + "Path": "/", + "Parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query", + "type": "string" + }, + { + "name": "page", + "required": false, + "title": "Page number (0 based)", + "type": "uint" + }, + { + "name": "perPage", + "required": false, + "title": "Returned items per page (default 50)", + "type": "uint" + } + ] + } + }, + { + "Name": "create", + "Method": "POST", + "Title": "Create namespace", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "name", + "required": true, + "title": "Name", + "type": "string" + }, + { + "name": "slug", + "required": true, + "title": "Slug (url path part)", + "type": "string" + }, + { + "name": "enabled", + "required": true, + "title": "Enabled", + "type": "bool" + }, + { + "name": "meta", + "required": true, + "title": "Meta data", + "type": "sqlxTypes.JSONText" + } + ] + } + }, + { + "Name": "read", + "Method": "GET", + "Title": "Read namespace", + "Path": "/{namespaceID}", + "Parameters": { + "path": [ + { + "name": "namespaceID", + "required": true, + "title": "ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "update", + "Method": "POST", + "Title": "Update namespace", + "Path": "/{namespaceID}", + "Parameters": { + "path": [ + { + "name": "namespaceID", + "required": true, + "title": "ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "name", + "required": true, + "title": "Name", + "type": "string" + }, + { + "name": "slug", + "required": true, + "title": "Slug (url path part)", + "type": "string" + }, + { + "name": "enabled", + "required": true, + "title": "Enabled", + "type": "bool" + }, + { + "name": "meta", + "required": true, + "title": "Meta data", + "type": "sqlxTypes.JSONText" + }, + { + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date", + "type": "*time.Time" + } + ] + } + }, + { + "Name": "delete", + "Method": "DELETE", + "Title": "Delete namespace", + "Path": "/{namespaceID}", + "Parameters": { + "path": [ + { + "name": "namespaceID", + "required": true, + "title": "ID", + "type": "uint64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/api/crm/spec/notification.json b/api/compose/spec/notification.json similarity index 93% rename from api/crm/spec/notification.json rename to api/compose/spec/notification.json index c1eed0a98..121e7c78b 100644 --- a/api/crm/spec/notification.json +++ b/api/compose/spec/notification.json @@ -1,6 +1,6 @@ { "Title": "Notifications", - "Description": "CRM Notifications", + "Description": "Compose Notifications", "Interface": "Notification", "Struct": [ { @@ -17,7 +17,7 @@ { "Name": "email/send", "Method": "POST", - "Title": "Send email from the CRM", + "Title": "Send email from the Compose", "Path": "/email", "Parameters": { "post": [ diff --git a/api/crm/spec/page.json b/api/compose/spec/page.json similarity index 86% rename from api/crm/spec/page.json rename to api/compose/spec/page.json index 0d50d05c9..ab63344c9 100644 --- a/api/crm/spec/page.json +++ b/api/compose/spec/page.json @@ -1,6 +1,6 @@ { "Title": "Pages", - "Description": "CRM module pages", + "Description": "Compose pages", "Interface": "Page", "Struct": [ { @@ -9,10 +9,19 @@ ] } ], - "Parameters": null, + "Parameters": { + "path": [ + { + "name": "namespaceID", + "required": true, + "title": "Namespace ID", + "type": "uint64" + } + ] + }, "Protocol": "", "Authentication": [], - "Path": "/page", + "Path": "/namespace/{namespaceID}/page", "APIs": [ { "Name": "list", @@ -26,6 +35,24 @@ "required": false, "title": "Parent page ID", "type": "uint64" + }, + { + "name": "query", + "required": false, + "title": "Search query", + "type": "string" + }, + { + "name": "page", + "required": false, + "title": "Page number (0 based)", + "type": "uint" + }, + { + "name": "perPage", + "required": false, + "title": "Returned items per page (default 50)", + "type": "uint" } ] } diff --git a/api/crm/spec/permissions.json b/api/compose/spec/permissions.json similarity index 100% rename from api/crm/spec/permissions.json rename to api/compose/spec/permissions.json diff --git a/api/crm/spec/record.json b/api/compose/spec/record.json similarity index 92% rename from api/crm/spec/record.json rename to api/compose/spec/record.json index 9a3f244ec..ebc06d2de 100644 --- a/api/crm/spec/record.json +++ b/api/compose/spec/record.json @@ -1,16 +1,22 @@ { "Title": "Records", - "Description": "CRM records ", + "Description": "Compose records", "Interface": "Record", "Struct": [ { "imports": [ - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" ] } ], "Parameters": { "path": [ + { + "name": "namespaceID", + "required": true, + "title": "Namespace ID", + "type": "uint64" + }, { "name": "moduleID", "required": true, @@ -21,7 +27,7 @@ }, "Protocol": "", "Authentication": [], - "Path": "/module/{moduleID}/record", + "Path": "/namespace/{namespaceID}/module/{moduleID}/record", "APIs": [ { "Name": "report", @@ -68,13 +74,13 @@ "name": "page", "required": false, "title": "Page number (0 based)", - "type": "int" + "type": "uint" }, { "name": "perPage", "required": false, "title": "Returned items per page (default 50)", - "type": "int" + "type": "uint" }, { "name": "sort", diff --git a/api/crm/spec/trigger.json b/api/compose/spec/trigger.json similarity index 76% rename from api/crm/spec/trigger.json rename to api/compose/spec/trigger.json index 5d497cc60..dad412d7a 100644 --- a/api/crm/spec/trigger.json +++ b/api/compose/spec/trigger.json @@ -1,12 +1,27 @@ { "Title": "Triggers", - "Description": "CRM Triggers", + "Description": "Compose Triggers", "Interface": "Trigger", - "Struct": null, - "Parameters": null, + "Struct": [ + { + "imports": [ + "time" + ] + } + ], + "Parameters": { + "path": [ + { + "name": "namespaceID", + "required": true, + "title": "Namespace ID", + "type": "uint64" + } + ] + }, "Protocol": "", "Authentication": [], - "Path": "/trigger", + "Path": "/namespace/{namespaceID}/trigger", "APIs": [ { "Name": "list", @@ -20,6 +35,24 @@ "required": false, "title": "Filter triggers by module", "type": "uint64" + }, + { + "name": "query", + "required": false, + "title": "Search query", + "type": "string" + }, + { + "name": "page", + "required": false, + "title": "Page number (0 based)", + "type": "uint" + }, + { + "name": "perPage", + "required": false, + "title": "Returned items per page (default 50)", + "type": "uint" } ] } @@ -60,6 +93,12 @@ "required": false, "title": "Trigger source code", "type": "string" + }, + { + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date", + "type": "*time.Time" } ] } diff --git a/cmd/crm/main.go b/cmd/compose/main.go similarity index 87% rename from cmd/crm/main.go rename to cmd/compose/main.go index ecd9e0d88..6663ae52a 100644 --- a/cmd/crm/main.go +++ b/cmd/compose/main.go @@ -12,7 +12,7 @@ import ( "github.com/crusttech/crust/internal/subscription" "github.com/crusttech/crust/internal/version" - crm "github.com/crusttech/crust/crm" + compose "github.com/crusttech/crust/compose" system "github.com/crusttech/crust/system" ) @@ -24,7 +24,7 @@ func main() { ctx := context.AsContext(sigctx.New()) - crm.Flags("crm") + compose.Flags("compose") system.Flags("system") subscription.Flags() @@ -34,7 +34,7 @@ func main() { if err := system.Init(ctx); err != nil { log.Fatalf("Error initializing: %+v", err) } - if err := crm.Init(ctx); err != nil { + if err := compose.Init(ctx); err != nil { log.Fatalf("Error initializing: %+v", err) } @@ -51,7 +51,7 @@ func main() { // Disabled for now, system service is the only one that validates subscription // ctx = subscription.Monitor(ctx) - if err := crm.StartRestAPI(ctx); err != nil { + if err := compose.StartRestAPI(ctx); err != nil { log.Fatalf("Error starting/running: %+v", err) } } diff --git a/cmd/crust/main.go b/cmd/crust/main.go index 5b5ea1c70..aeb3f61e2 100644 --- a/cmd/crust/main.go +++ b/cmd/crust/main.go @@ -13,7 +13,7 @@ import ( _ "github.com/joho/godotenv/autoload" "github.com/namsral/flag" - crm "github.com/crusttech/crust/crm" + compose "github.com/crusttech/crust/compose" "github.com/crusttech/crust/internal/auth" messaging "github.com/crusttech/crust/messaging" system "github.com/crusttech/crust/system" @@ -55,7 +55,7 @@ func main() { flags.http = new(config.HTTP).Init() flags.monitor = new(config.Monitor).Init() - crm.Flags("crm") + compose.Flags("compose") messaging.Flags("messaging") system.Flags("system") @@ -78,8 +78,8 @@ func main() { if err := system.Init(ctx); err != nil { log.Fatalf("Error initializing system: %+v", err) } - if err := crm.Init(ctx); err != nil { - log.Fatalf("Error initializing crm: %+v", err) + if err := compose.Init(ctx); err != nil { + log.Fatalf("Error initializing compose: %+v", err) } if err := messaging.Init(ctx); err != nil { log.Fatalf("Error initializing messaging: %+v", err) @@ -103,16 +103,16 @@ func main() { // logging, cors and such middleware.Mount(ctx, r, flags.http) - // Use JWT secret for hmac signer for now - auth.DefaultSigner = auth.HmacSigner(authJwtFlags.Secret) - auth.DefaultJwtHandler, err = auth.JWT(authJwtFlags.Secret, authJwtFlags.Expiry) + // Use JWT secret for hmac signer for now + auth.DefaultSigner = auth.HmacSigner(authJwtFlags.Secret) + auth.DefaultJwtHandler, err = auth.JWT(authJwtFlags.Secret, authJwtFlags.Expiry) if err != nil { log.Fatalf("Error creating JWT Auth: %v", err) } r.Route("/api", func(r chi.Router) { - r.Route("/crm", func(r chi.Router) { - crm.MountRoutes(ctx, r) + r.Route("/compose", func(r chi.Router) { + compose.MountRoutes(ctx, r) }) r.Route("/messaging", func(r chi.Router) { messaging.MountRoutes(ctx, r) @@ -125,8 +125,8 @@ func main() { fileserver := http.FileServer(http.Dir("webapp")) - for _, service := range []string{"admin", "system", "messaging", "crm"} { - r.HandleFunc("/"+service+"*", serveIndex("webapp", "crm/index.html", fileserver)) + for _, service := range []string{"admin", "system", "messaging", "compose"} { + r.HandleFunc("/"+service+"*", serveIndex("webapp", "compose/index.html", fileserver)) } r.HandleFunc("/*", serveIndex("webapp", "index.html", fileserver)) diff --git a/codegen.sh b/codegen.sh index 7bb557a5a..59a1ffab3 100755 --- a/codegen.sh +++ b/codegen.sh @@ -22,15 +22,16 @@ function types { CGO_ENABLED=0 go build -o ./build/gen-type-set codegen/v2/type-set.go fi - ./build/gen-type-set --types Attachment --output crm/types/attachment.gen.go - ./build/gen-type-set --types Module --output crm/types/module.gen.go - ./build/gen-type-set --types Page --output crm/types/page.gen.go - ./build/gen-type-set --types Chart --output crm/types/chart.gen.go - ./build/gen-type-set --types Trigger --output crm/types/trigger.gen.go - ./build/gen-type-set --types Record --output crm/types/record.gen.go + ./build/gen-type-set --types Namespace --output compose/types/namespace.gen.go + ./build/gen-type-set --types Attachment --output compose/types/attachment.gen.go + ./build/gen-type-set --types Module --output compose/types/module.gen.go + ./build/gen-type-set --types Page --output compose/types/page.gen.go + ./build/gen-type-set --types Chart --output compose/types/chart.gen.go + ./build/gen-type-set --types Trigger --output compose/types/trigger.gen.go + ./build/gen-type-set --types Record --output compose/types/record.gen.go - ./build/gen-type-set --with-primary-key=false --types ModuleField --output crm/types/module_field.gen.go - ./build/gen-type-set --with-primary-key=false --types RecordValue --output crm/types/record_value.gen.go + ./build/gen-type-set --with-primary-key=false --types ModuleField --output compose/types/module_field.gen.go + ./build/gen-type-set --with-primary-key=false --types RecordValue --output compose/types/record_value.gen.go ./build/gen-type-set --types MessageAttachment --output messaging/types/attachment.gen.go ./build/gen-type-set --types Mention --output messaging/types/mention.gen.go diff --git a/codegen/codegen.php b/codegen/codegen.php index 9a25eb96e..e0e7c3acb 100755 --- a/codegen/codegen.php +++ b/codegen/codegen.php @@ -101,6 +101,8 @@ $parsers = array( "int" => "parseInt", "uint" => "parseUint", "bool" => "parseBool", + "time.Time" => "parseISODateWithErr", + "*time.Time" => "parseISODatePtrWithErr", "sqlxTypes.JSONText" => "parseJSONTextWithErr", ); diff --git a/codegen/crm/docs/index.php b/codegen/compose/docs/index.php similarity index 100% rename from codegen/crm/docs/index.php rename to codegen/compose/docs/index.php diff --git a/codegen/crm/rest/handlers/index.php b/codegen/compose/rest/handlers/index.php similarity index 100% rename from codegen/crm/rest/handlers/index.php rename to codegen/compose/rest/handlers/index.php diff --git a/codegen/crm/rest/index.php b/codegen/compose/rest/index.php similarity index 100% rename from codegen/crm/rest/index.php rename to codegen/compose/rest/index.php diff --git a/codegen/crm/rest/request/index.php b/codegen/compose/rest/request/index.php similarity index 100% rename from codegen/crm/rest/request/index.php rename to codegen/compose/rest/request/index.php diff --git a/codegen/crm/types/index.php b/codegen/compose/types/index.php similarity index 100% rename from codegen/crm/types/index.php rename to codegen/compose/types/index.php diff --git a/crm/db/migrate.go b/compose/db/migrate.go similarity index 98% rename from crm/db/migrate.go rename to compose/db/migrate.go index 3c8258589..57eb8b12d 100644 --- a/crm/db/migrate.go +++ b/compose/db/migrate.go @@ -13,7 +13,7 @@ import ( "github.com/pkg/errors" "github.com/titpetric/factory" - "github.com/crusttech/crust/crm/db/mysql" + "github.com/crusttech/crust/compose/db/mysql" ) func statements(contents []byte, err error) ([]string, error) { diff --git a/crm/db/migrate_test.go b/compose/db/migrate_test.go similarity index 100% rename from crm/db/migrate_test.go rename to compose/db/migrate_test.go diff --git a/crm/db/mysql/static.go b/compose/db/mysql/static.go similarity index 53% rename from crm/db/mysql/static.go rename to compose/db/mysql/static.go index a7827c564..5d3c5d094 100644 --- a/crm/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\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!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x81&\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81>(\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x13\x00\x13\x00\x14\x07\x00\x00\xaa(\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\nUPDATE `compose_attachment` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_chart` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_module` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_page` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_record` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_trigger` SET `rel_namespace` = 88714882739863655;\n\n\nALTER TABLE `compose_attachment`\n ADD CONSTRAINT `compose_attachment_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_chart`\n ADD CONSTRAINT `compose_chart_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_module`\n ADD CONSTRAINT `compose_module_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_page`\n ADD CONSTRAINT `compose_page_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_record`\n ADD CONSTRAINT `compose_record_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_trigger`\n ADD CONSTRAINT `compose_trigger_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\nPK\x07\x08\xb2\x05Q\x14R \x00\x00R \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `compose_page`\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL;\n\nALTER TABLE `compose_page` CHANGE COLUMN `module_id` `rel_module` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nPK\x07\x08\x82\x01Rn1\x01\x00\x001\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\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!(\xb2\x05Q\x14R \x00\x00R \x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\",\x00\x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x82\x01Rn1\x01\x00\x001\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xcf5\x00\x0020190428080000.page-timestamps.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\x199\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x17\x00\x17\x00\x83\x08\x00\x00\x859\x00\x00\x00\x00" diff --git a/crm/db/schema/mysql/20180704080000.base.up.sql b/compose/db/schema/mysql/20180704080000.base.up.sql similarity index 100% rename from crm/db/schema/mysql/20180704080000.base.up.sql rename to compose/db/schema/mysql/20180704080000.base.up.sql diff --git a/crm/db/schema/mysql/20180704080001.crm_fields-data.up.sql b/compose/db/schema/mysql/20180704080001.crm_fields-data.up.sql similarity index 100% rename from crm/db/schema/mysql/20180704080001.crm_fields-data.up.sql rename to compose/db/schema/mysql/20180704080001.crm_fields-data.up.sql diff --git a/crm/db/schema/mysql/20181109133134.crm_content-ownership.up.sql b/compose/db/schema/mysql/20181109133134.crm_content-ownership.up.sql similarity index 100% rename from crm/db/schema/mysql/20181109133134.crm_content-ownership.up.sql rename to compose/db/schema/mysql/20181109133134.crm_content-ownership.up.sql diff --git a/crm/db/schema/mysql/20181109193047.crm_fields-related_types.up.sql b/compose/db/schema/mysql/20181109193047.crm_fields-related_types.up.sql similarity index 100% rename from crm/db/schema/mysql/20181109193047.crm_fields-related_types.up.sql rename to compose/db/schema/mysql/20181109193047.crm_fields-related_types.up.sql diff --git a/crm/db/schema/mysql/20181125122152.add_multiple_relationships.up.sql b/compose/db/schema/mysql/20181125122152.add_multiple_relationships.up.sql similarity index 100% rename from crm/db/schema/mysql/20181125122152.add_multiple_relationships.up.sql rename to compose/db/schema/mysql/20181125122152.add_multiple_relationships.up.sql diff --git a/crm/db/schema/mysql/20181125132142.add_required_and_visible_to_module_form_fields.up.sql b/compose/db/schema/mysql/20181125132142.add_required_and_visible_to_module_form_fields.up.sql similarity index 100% rename from crm/db/schema/mysql/20181125132142.add_required_and_visible_to_module_form_fields.up.sql rename to compose/db/schema/mysql/20181125132142.add_required_and_visible_to_module_form_fields.up.sql diff --git a/crm/db/schema/mysql/20181202163130.fix-crm-module-form-primary-key.up.sql b/compose/db/schema/mysql/20181202163130.fix-crm-module-form-primary-key.up.sql similarity index 100% rename from crm/db/schema/mysql/20181202163130.fix-crm-module-form-primary-key.up.sql rename to compose/db/schema/mysql/20181202163130.fix-crm-module-form-primary-key.up.sql diff --git a/crm/db/schema/mysql/20181204123650.add-crm-content-json-field.up.sql b/compose/db/schema/mysql/20181204123650.add-crm-content-json-field.up.sql similarity index 100% rename from crm/db/schema/mysql/20181204123650.add-crm-content-json-field.up.sql rename to compose/db/schema/mysql/20181204123650.add-crm-content-json-field.up.sql diff --git a/crm/db/schema/mysql/20181204155326.add-crm-module-form-json-field.up.sql b/compose/db/schema/mysql/20181204155326.add-crm-module-form-json-field.up.sql similarity index 100% rename from crm/db/schema/mysql/20181204155326.add-crm-module-form-json-field.up.sql rename to compose/db/schema/mysql/20181204155326.add-crm-module-form-json-field.up.sql diff --git a/crm/db/schema/mysql/20181216214630.crm-content-to-record.up.sql b/compose/db/schema/mysql/20181216214630.crm-content-to-record.up.sql similarity index 100% rename from crm/db/schema/mysql/20181216214630.crm-content-to-record.up.sql rename to compose/db/schema/mysql/20181216214630.crm-content-to-record.up.sql diff --git a/crm/db/schema/mysql/20181217100000.add-charts-tbl.up.sql b/compose/db/schema/mysql/20181217100000.add-charts-tbl.up.sql similarity index 100% rename from crm/db/schema/mysql/20181217100000.add-charts-tbl.up.sql rename to compose/db/schema/mysql/20181217100000.add-charts-tbl.up.sql diff --git a/crm/db/schema/mysql/20181224122301.rem-crm_field.up.sql b/compose/db/schema/mysql/20181224122301.rem-crm_field.up.sql similarity index 100% rename from crm/db/schema/mysql/20181224122301.rem-crm_field.up.sql rename to compose/db/schema/mysql/20181224122301.rem-crm_field.up.sql diff --git a/crm/db/schema/mysql/20190108100000.add-triggers-tbl.up.sql b/compose/db/schema/mysql/20190108100000.add-triggers-tbl.up.sql similarity index 100% rename from crm/db/schema/mysql/20190108100000.add-triggers-tbl.up.sql rename to compose/db/schema/mysql/20190108100000.add-triggers-tbl.up.sql diff --git a/crm/db/schema/mysql/20190110175924.rem-crm-record-json-field.up.sql b/compose/db/schema/mysql/20190110175924.rem-crm-record-json-field.up.sql similarity index 100% rename from crm/db/schema/mysql/20190110175924.rem-crm-record-json-field.up.sql rename to compose/db/schema/mysql/20190110175924.rem-crm-record-json-field.up.sql diff --git a/crm/db/schema/mysql/20190114072000.cleanup-record-tables-and-multival.up.sql b/compose/db/schema/mysql/20190114072000.cleanup-record-tables-and-multival.up.sql similarity index 100% rename from crm/db/schema/mysql/20190114072000.cleanup-record-tables-and-multival.up.sql rename to compose/db/schema/mysql/20190114072000.cleanup-record-tables-and-multival.up.sql diff --git a/crm/db/schema/mysql/20190121132408.record-updated-by.up.sql b/compose/db/schema/mysql/20190121132408.record-updated-by.up.sql similarity index 100% rename from crm/db/schema/mysql/20190121132408.record-updated-by.up.sql rename to compose/db/schema/mysql/20190121132408.record-updated-by.up.sql diff --git a/crm/db/schema/mysql/20190227090642.attachment.up.sql b/compose/db/schema/mysql/20190227090642.attachment.up.sql similarity index 100% rename from crm/db/schema/mysql/20190227090642.attachment.up.sql rename to compose/db/schema/mysql/20190227090642.attachment.up.sql diff --git a/compose/db/schema/mysql/20190427180922.change-tbl-prefix.up.sql b/compose/db/schema/mysql/20190427180922.change-tbl-prefix.up.sql new file mode 100644 index 000000000..f8cb60e27 --- /dev/null +++ b/compose/db/schema/mysql/20190427180922.change-tbl-prefix.up.sql @@ -0,0 +1,30 @@ +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; + +ALTER TABLE crm_attachment + RENAME TO compose_attachment; + +ALTER TABLE crm_chart + RENAME TO compose_chart; + +ALTER TABLE crm_module + RENAME TO compose_module; + +ALTER TABLE crm_module_form + RENAME TO compose_module_form; + +ALTER TABLE crm_page + RENAME TO compose_page; + +ALTER TABLE crm_record + RENAME TO compose_record; + +ALTER TABLE crm_record_value + RENAME TO compose_record_value; + +ALTER TABLE crm_trigger + RENAME TO compose_trigger; diff --git a/compose/db/schema/mysql/20190427210922.namespace-tbl.up.sql b/compose/db/schema/mysql/20190427210922.namespace-tbl.up.sql new file mode 100644 index 000000000..3be6620b5 --- /dev/null +++ b/compose/db/schema/mysql/20190427210922.namespace-tbl.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE `compose_namespace` ( + `id` BIGINT(20) UNSIGNED NOT NULL, + `name` VARCHAR(64) NOT NULL COMMENT 'Name', + `slug` VARCHAR(64) NOT NULL COMMENT 'URL slug', + `enabled` BOOLEAN NOT NULL COMMENT 'Is namespace enabled?', + `meta` JSON NOT NULL COMMENT 'Meta data', + + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME DEFAULT NULL, + `deleted_at` DATETIME DEFAULT NULL, + + PRIMARY KEY (`id`) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/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..229483efb --- /dev/null +++ b/compose/db/schema/mysql/20190428080000.namespace-refs.up.sql @@ -0,0 +1,63 @@ +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`); + +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; + + +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`); diff --git a/compose/db/schema/mysql/20190428080000.page-timestamps.up.sql b/compose/db/schema/mysql/20190428080000.page-timestamps.up.sql new file mode 100644 index 000000000..e4e43432f --- /dev/null +++ b/compose/db/schema/mysql/20190428080000.page-timestamps.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE `compose_page` + ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ADD COLUMN `updated_at` DATETIME DEFAULT NULL, + ADD COLUMN `deleted_at` DATETIME DEFAULT NULL; + +ALTER TABLE `compose_page` CHANGE COLUMN `module_id` `rel_module` BIGINT UNSIGNED NOT NULL DEFAULT 0; diff --git a/crm/db/schema/mysql/migrations.sql b/compose/db/schema/mysql/migrations.sql similarity index 100% rename from crm/db/schema/mysql/migrations.sql rename to compose/db/schema/mysql/migrations.sql diff --git a/crm/db/schema/mysql/new.sh b/compose/db/schema/mysql/new.sh similarity index 100% rename from crm/db/schema/mysql/new.sh rename to compose/db/schema/mysql/new.sh diff --git a/crm/db/types.go b/compose/db/types.go similarity index 100% rename from crm/db/types.go rename to compose/db/types.go diff --git a/crm/flags.go b/compose/flags.go similarity index 95% rename from crm/flags.go rename to compose/flags.go index 1621055eb..4c616d35f 100644 --- a/crm/flags.go +++ b/compose/flags.go @@ -3,7 +3,7 @@ package service import ( "github.com/pkg/errors" - "github.com/crusttech/crust/crm/internal/repository" + "github.com/crusttech/crust/compose/internal/repository" "github.com/crusttech/crust/internal/config" ) diff --git a/compose/internal/repository/attachment.go b/compose/internal/repository/attachment.go new file mode 100644 index 000000000..c12182829 --- /dev/null +++ b/compose/internal/repository/attachment.go @@ -0,0 +1,154 @@ +package repository + +import ( + "context" + "time" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/compose/types" +) + +type ( + AttachmentRepository interface { + With(ctx context.Context, db *factory.DB) AttachmentRepository + + Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) + FindByID(namespaceID, attachmentID uint64) (*types.Attachment, error) + Create(mod *types.Attachment) (*types.Attachment, error) + DeleteByID(namespaceID, attachmentID uint64) error + } + + attachment struct { + *repository + } +) + +const ( + 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 { + return &attachment{ + repository: r.repository.With(ctx, db), + } +} + +func (r attachment) table() string { + return "compose_attachment" +} + +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) 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 + 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) + } + + switch f.Kind { + case types.PageAttachment: + // @todo implement filtering by page + if f.PageID > 0 { + err = errors.New("filtering by pageID not implemented") + return + } + case types.RecordAttachment: + query = query. + Join("compose_record_value AS v ON (v.ref = a.id)") + + if f.ModuleID > 0 { + query = query. + Join("compose_record AS r ON (r.id = v.record_id)"). + Where(squirrel.Eq{"r.module_id": f.ModuleID}) + } + + if f.RecordID > 0 { + query = query.Where(squirrel.Eq{"v.record_id": f.RecordID}) + } + + if f.FieldName != "" { + query = query.Where(squirrel.Eq{"v.name": f.FieldName}) + } + default: + err = errors.New("unsupported kind value") + } + + if f.Filter != "" { + err = errors.New("filtering by filter not implemented") + return + } + + if f.Count, err = r.count(query); err != nil || f.Count == 0 { + return + } + + query = query. + Columns(r.columns()...). + OrderBy("id ASC") + + return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage) +} + +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(r.table(), mod) +} + +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/chart.go b/compose/internal/repository/chart.go new file mode 100644 index 000000000..e068426a0 --- /dev/null +++ b/compose/internal/repository/chart.go @@ -0,0 +1,124 @@ +package repository + +import ( + "context" + "time" + + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/compose/types" +) + +type ( + ChartRepository interface { + With(ctx context.Context, db *factory.DB) ChartRepository + + FindByID(namespaceID, chartID uint64) (*types.Chart, error) + Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error) + Create(mod *types.Chart) (*types.Chart, error) + Update(mod *types.Chart) (*types.Chart, error) + DeleteByID(namespaceID, chartID uint64) error + } + + chart struct { + *repository + } +) + +const ( + ErrChartNotFound = repositoryError("ChartNotFound") +) + +func Chart(ctx context.Context, db *factory.DB) ChartRepository { + return (&chart{}).With(ctx, db) +} + +func (r chart) With(ctx context.Context, db *factory.DB) ChartRepository { + return &chart{ + repository: r.repository.With(ctx, db), + } +} + +func (r chart) table() string { + return "compose_chart" +} + +func (r chart) columns() []string { + return []string{ + "id", "rel_namespace", "name", "config", + "created_at", "updated_at", "deleted_at", + } +} + +func (r chart) query() squirrel.SelectBuilder { + return squirrel. + Select(). + From(r.table()). + Where("deleted_at IS NULL") +} + +func (r chart) FindByID(namespaceID, chartID uint64) (*types.Chart, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("id = ?", chartID) + + c = &types.Chart{} + ) + + if namespaceID > 0 { + query = query.Where("rel_namespace = ?", namespaceID) + } + + return c, isFound(r.fetchOne(c, query), c.ID > 0, ErrChartNotFound) +} + +func (r chart) Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error) { + f = filter + f.PerPage = normalizePerPage(f.PerPage, 5, 100, 50) + + query := r.query() + + if filter.NamespaceID > 0 { + query = query.Where("rel_namespace = ?", filter.NamespaceID) + } + + if f.Query != "" { + q := "%" + f.Query + "%" + query = query.Where("name like ?", q) + } + + if f.Count, err = r.count(query); err != nil || f.Count == 0 { + return + } + + query = query. + Columns(r.columns()...). + OrderBy("id ASC") + + return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage) +} + +func (r chart) Create(mod *types.Chart) (*types.Chart, error) { + mod.ID = factory.Sonyflake.NextID() + mod.CreatedAt = time.Now() + + return mod, r.db().Insert(r.table(), mod) +} + +func (r chart) Update(mod *types.Chart) (*types.Chart, error) { + now := time.Now() + mod.UpdatedAt = &now + return mod, r.db().Replace(r.table(), mod) +} + +func (r chart) DeleteByID(namespaceID, chartID uint64) error { + _, err := r.db().Exec( + "UPDATE "+r.table()+" SET deleted_at = NOW() WHERE rel_namespace = ? AND id = ?", + namespaceID, + chartID, + ) + + return err +} diff --git a/compose/internal/repository/error.go b/compose/internal/repository/error.go new file mode 100644 index 000000000..f1d4dd37f --- /dev/null +++ b/compose/internal/repository/error.go @@ -0,0 +1,25 @@ +package repository + +import ( + "github.com/pkg/errors" +) + +type ( + repositoryError string +) + +const ( + ErrNotImplemented = repositoryError("NotImplemented") +) + +func (e repositoryError) Error() string { + return e.String() +} + +func (e repositoryError) String() string { + return "crust.compose.repository." + string(e) +} + +func (e repositoryError) new() error { + return errors.WithStack(e) +} diff --git a/crm/internal/repository/flags.go b/compose/internal/repository/flags.go similarity index 100% rename from crm/internal/repository/flags.go rename to compose/internal/repository/flags.go diff --git a/compose/internal/repository/module.go b/compose/internal/repository/module.go new file mode 100644 index 000000000..117d4b2f1 --- /dev/null +++ b/compose/internal/repository/module.go @@ -0,0 +1,166 @@ +package repository + +import ( + "context" + "time" + + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/compose/types" +) + +type ( + ModuleRepository interface { + With(ctx context.Context, db *factory.DB) ModuleRepository + + FindByID(namespaceID, moduleID uint64) (*types.Module, error) + Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) + FindFields(moduleIDs ...uint64) (ff types.ModuleFieldSet, err error) + Create(mod *types.Module) (*types.Module, error) + Update(mod *types.Module) (*types.Module, error) + DeleteByID(namespaceID, moduleID uint64) error + } + + module struct { + *repository + } +) + +const ( + ErrModuleNotFound = repositoryError("ModuleNotFound") +) + +func Module(ctx context.Context, db *factory.DB) ModuleRepository { + return (&module{}).With(ctx, db) +} + +func (r module) With(ctx context.Context, db *factory.DB) ModuleRepository { + return &module{ + repository: r.repository.With(ctx, db), + } +} + +func (r module) table() string { + return "compose_module" +} + +func (r module) columns() []string { + return []string{ + "id", "rel_namespace", "name", "json", + "created_at", "updated_at", "deleted_at", + } +} + +func (r module) query() squirrel.SelectBuilder { + return squirrel. + Select(). + From(r.table()). + Where("deleted_at IS NULL") + +} + +func (r module) FindByID(namespaceID, moduleID uint64) (*types.Module, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("id = ?", moduleID) + + c = &types.Module{} + ) + + if namespaceID > 0 { + query = query.Where("rel_namespace = ?", namespaceID) + } + + return c, isFound(r.fetchOne(c, query), c.ID > 0, ErrModuleNotFound) +} + +func (r module) Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) { + f = filter + f.PerPage = normalizePerPage(f.PerPage, 5, 100, 50) + + query := r.query() + + if filter.NamespaceID > 0 { + query = query.Where("rel_namespace = ?", filter.NamespaceID) + } + + if f.Query != "" { + q := "%" + f.Query + "%" + query = query.Where("name like ?", q) + } + + if f.Count, err = r.count(query); err != nil || f.Count == 0 { + return + } + + query = query. + Columns(r.columns()...). + OrderBy("id ASC") + + return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage) +} + +func (r module) Create(mod *types.Module) (*types.Module, error) { + mod.ID = factory.Sonyflake.NextID() + mod.CreatedAt = time.Now() + + if err := r.updateFields(mod.ID, mod.Fields); err != nil { + return nil, err + } + + return mod, r.db().Insert(r.table(), mod) +} + +func (r module) Update(mod *types.Module) (*types.Module, error) { + now := time.Now() + mod.UpdatedAt = &now + + if err := r.updateFields(mod.ID, mod.Fields); err != nil { + return nil, err + } + + return mod, r.db().Replace(r.table(), mod) +} + +func (r module) updateFields(moduleID uint64, ff types.ModuleFieldSet) error { + // @todo be more selective when deleting + if _, err := r.db().Exec("DELETE FROM compose_module_form WHERE module_id = ?", moduleID); err != nil { + return errors.Wrap(err, "Error updating module fields") + } + + for idx, v := range ff { + v.ModuleID = moduleID + v.Place = idx + if err := r.db().Replace("compose_module_form", v); err != nil { + return errors.Wrap(err, "Error updating module fields") + } + } + + return nil +} + +func (r module) DeleteByID(namespaceID, moduleID uint64) error { + _, err := r.db().Exec( + "UPDATE "+r.table()+" SET deleted_at = NOW() WHERE rel_namespace = ? AND id = ?", + namespaceID, + moduleID, + ) + + return err +} + +func (r module) FindFields(moduleIDs ...uint64) (ff types.ModuleFieldSet, err error) { + if len(moduleIDs) == 0 { + return + } + + if sql, args, err := sqlx.In("SELECT * FROM compose_module_form WHERE module_id IN (?) ORDER BY module_id AND place", moduleIDs); err != nil { + return nil, err + } else { + return ff, r.db().Select(&ff, sql, args...) + } +} diff --git a/compose/internal/repository/namespace.go b/compose/internal/repository/namespace.go new file mode 100644 index 000000000..a8cfae8e9 --- /dev/null +++ b/compose/internal/repository/namespace.go @@ -0,0 +1,122 @@ +package repository + +import ( + "context" + "time" + + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/compose/types" +) + +type ( + NamespaceRepository interface { + With(ctx context.Context, db *factory.DB) NamespaceRepository + + FindByID(id uint64) (*types.Namespace, error) + Find(filter types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error) + Create(mod *types.Namespace) (*types.Namespace, error) + Update(mod *types.Namespace) (*types.Namespace, error) + DeleteByID(id uint64) error + } + + namespace struct { + *repository + } +) + +const ( + ErrNamespaceNotFound = repositoryError("NamespaceNotFound") +) + +func Namespace(ctx context.Context, db *factory.DB) NamespaceRepository { + return (&namespace{}).With(ctx, db) +} + +func (r namespace) table() string { + return "compose_namespace" +} + +func (r namespace) columns() []string { + return []string{ + "id", + "name", + "slug", + "enabled", + "meta", + "created_at", + "updated_at", + "deleted_at", + } +} + +func (r namespace) query() squirrel.SelectBuilder { + return squirrel. + Select(). + From(r.table()). + Where("deleted_at IS NULL") + +} + +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() + + return mod, r.db().Insert(r.table(), mod) +} + +func (r *namespace) Update(mod *types.Namespace) (*types.Namespace, error) { + now := time.Now() + mod.UpdatedAt = &now + + return mod, r.db().Replace(r.table(), mod) +} + +func (r *namespace) DeleteByID(namespaceID uint64) error { + _, err := r.db().Exec("UPDATE "+r.table()+" SET deleted_at = NOW() WHERE id = ?", namespaceID) + return err +} diff --git a/compose/internal/repository/page.go b/compose/internal/repository/page.go new file mode 100644 index 000000000..02d8b0bde --- /dev/null +++ b/compose/internal/repository/page.go @@ -0,0 +1,188 @@ +package repository + +import ( + "context" + "time" + + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/compose/types" +) + +type ( + PageRepository interface { + With(ctx context.Context, db *factory.DB) PageRepository + + FindByID(namespaceID, pageID uint64) (*types.Page, error) + FindByModuleID(namespaceID, moduleID uint64) (*types.Page, error) + Find(filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error) + + Create(mod *types.Page) (*types.Page, error) + Update(mod *types.Page) (*types.Page, error) + DeleteByID(namespaceID, pageID uint64) error + + Reorder(namespaceID, selfID uint64, pageIDs []uint64) error + } + + page struct { + *repository + } +) + +const ( + ErrPageNotFound = repositoryError("PageNotFound") +) + +func Page(ctx context.Context, db *factory.DB) PageRepository { + return (&page{}).With(ctx, db) +} + +func (r page) With(ctx context.Context, db *factory.DB) PageRepository { + return &page{ + repository: r.repository.With(ctx, db), + } +} + +func (r page) table() string { + return "compose_page" +} + +func (r page) columns() []string { + return []string{ + "id", "rel_namespace", "self_id", "rel_module", "title", + "blocks", "description", "visible", "weight", + "created_at", "updated_at", "deleted_at", + } +} + +func (r page) query() squirrel.SelectBuilder { + return squirrel. + Select(). + From(r.table()). + Where("deleted_at IS NULL") +} + +func (r page) FindByID(namespaceID, pageID uint64) (*types.Page, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("id = ?", pageID) + + c = &types.Page{} + ) + + if namespaceID > 0 { + query = query.Where("rel_namespace = ?", namespaceID) + } + + return c, isFound(r.fetchOne(c, query), c.ID > 0, ErrPageNotFound) +} + +func (r page) FindByModuleID(namespaceID, moduleID uint64) (*types.Page, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("rel_module = ?", moduleID) + + c = &types.Page{} + ) + + if namespaceID > 0 { + query = query.Where("rel_namespace = ?", namespaceID) + } + + return c, isFound(r.fetchOne(c, query), c.ID > 0, ErrPageNotFound) +} + +func (r page) Find(filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error) { + f = filter + f.PerPage = normalizePerPage(f.PerPage, 5, 100, 50) + + query := r.query() + + if filter.NamespaceID > 0 { + query = query.Where("rel_namespace = ?", filter.NamespaceID) + } + + if filter.ParentID > 0 { + query = query.Where("self_id = ?", filter.ParentID) + } else if filter.Root { + query = query.Where("self_id = 0") + } + + if f.Query != "" { + q := "%" + f.Query + "%" + query = query.Where("title LIKE ? OR description LIKE ?", q, q) + } + + if f.Count, err = r.count(query); err != nil || f.Count == 0 { + return + } + + query = query. + Columns(r.columns()...). + OrderBy("weight ASC") + + return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage) +} + +func (r page) Reorder(namespaceID, parentID uint64, pageIDs []uint64) error { + var ( + pageMap = map[uint64]bool{} + filter = types.PageFilter{NamespaceID: namespaceID, ParentID: parentID} + ) + + if pages, _, err := r.Find(filter); err != nil { + return nil + } else { + for _, page := range pages { + pageMap[page.ID] = true + } + } + weight := 1 + db := r.db() + // honor parameter first + for _, pageID := range pageIDs { + if pageMap[pageID] { + pageMap[pageID] = false + if _, err := db.Exec("UPDATE compose_page SET weight = ? WHERE id = ? AND self_id = ?", weight, pageID, parentID); err != nil { + return err + } + weight++ + } + } + for pageID, update := range pageMap { + if update { + if _, err := db.Exec("UPDATE compose_page SET weight = ? WHERE id = ? AND self_id = ?", weight, pageID, parentID); err != nil { + return err + } + weight++ + } + } + return nil +} + +func (r page) Create(mod *types.Page) (*types.Page, error) { + mod.ID = factory.Sonyflake.NextID() + mod.CreatedAt = time.Now() + + return mod, r.db().Insert(r.table(), mod) +} + +func (r page) Update(mod *types.Page) (*types.Page, error) { + now := time.Now() + mod.UpdatedAt = &now + + return mod, r.db().Replace(r.table(), mod) +} + +func (r page) DeleteByID(namespaceID, pageID uint64) error { + _, err := r.db().Exec( + "UPDATE "+r.table()+" SET deleted_at = NOW() WHERE rel_namespace = ? AND id = ?", + namespaceID, + pageID, + ) + + return err +} diff --git a/crm/internal/repository/ql/README.md b/compose/internal/repository/ql/README.md similarity index 100% rename from crm/internal/repository/ql/README.md rename to compose/internal/repository/ql/README.md diff --git a/crm/internal/repository/ql/ast_nodes.go b/compose/internal/repository/ql/ast_nodes.go similarity index 100% rename from crm/internal/repository/ql/ast_nodes.go rename to compose/internal/repository/ql/ast_nodes.go diff --git a/crm/internal/repository/ql/ast_parser.go b/compose/internal/repository/ql/ast_parser.go similarity index 100% rename from crm/internal/repository/ql/ast_parser.go rename to compose/internal/repository/ql/ast_parser.go diff --git a/crm/internal/repository/ql/ast_parser_test.go b/compose/internal/repository/ql/ast_parser_test.go similarity index 100% rename from crm/internal/repository/ql/ast_parser_test.go rename to compose/internal/repository/ql/ast_parser_test.go diff --git a/crm/internal/repository/ql/handlers.go b/compose/internal/repository/ql/handlers.go similarity index 100% rename from crm/internal/repository/ql/handlers.go rename to compose/internal/repository/ql/handlers.go diff --git a/crm/internal/repository/ql/lexer.go b/compose/internal/repository/ql/lexer.go similarity index 100% rename from crm/internal/repository/ql/lexer.go rename to compose/internal/repository/ql/lexer.go diff --git a/crm/internal/repository/ql/lexer_test.go b/compose/internal/repository/ql/lexer_test.go similarity index 100% rename from crm/internal/repository/ql/lexer_test.go rename to compose/internal/repository/ql/lexer_test.go diff --git a/crm/internal/repository/ql/squirrel.go b/compose/internal/repository/ql/squirrel.go similarity index 100% rename from crm/internal/repository/ql/squirrel.go rename to compose/internal/repository/ql/squirrel.go diff --git a/crm/internal/repository/ql/token_codes.go b/compose/internal/repository/ql/token_codes.go similarity index 100% rename from crm/internal/repository/ql/token_codes.go rename to compose/internal/repository/ql/token_codes.go diff --git a/crm/internal/repository/ql/token_consumers.go b/compose/internal/repository/ql/token_consumers.go similarity index 100% rename from crm/internal/repository/ql/token_consumers.go rename to compose/internal/repository/ql/token_consumers.go diff --git a/compose/internal/repository/record.go b/compose/internal/repository/record.go new file mode 100644 index 000000000..b80315e58 --- /dev/null +++ b/compose/internal/repository/record.go @@ -0,0 +1,343 @@ +package repository + +import ( + "context" + "fmt" + "strings" + + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/compose/internal/repository/ql" + + "github.com/crusttech/crust/compose/types" +) + +type ( + RecordRepository interface { + With(ctx context.Context, db *factory.DB) RecordRepository + + FindByID(namespaceID, recordID uint64) (*types.Record, error) + + Report(module *types.Module, metrics, dimensions, filter string) (results interface{}, err error) + Find(module *types.Module, filter types.RecordFilter) (set types.RecordSet, f types.RecordFilter, err error) + + Create(record *types.Record) (*types.Record, error) + Update(record *types.Record) (*types.Record, error) + Delete(record *types.Record) error + + LoadValues(IDs ...uint64) (rvs types.RecordValueSet, err error) + DeleteValues(record *types.Record) error + UpdateValues(recordID uint64, rvs types.RecordValueSet) (err error) + } + + record struct { + *repository + } +) + +const ( + ErrRecordNotFound = repositoryError("RecordNotFound") +) + +func Record(ctx context.Context, db *factory.DB) RecordRepository { + return (&record{}).With(ctx, db) +} + +func (r record) With(ctx context.Context, db *factory.DB) RecordRepository { + return &record{ + repository: r.repository.With(ctx, db), + } +} + +func (r record) table() string { + return "compose_record" +} + +func (r record) columns() []string { + return []string{ + "r.id", + "r.module_id", + "r.rel_namespace", + "r.owned_by", + "r.created_at", + "r.created_by", + "r.updated_at", + "r.updated_by", + "r.deleted_at", + "r.deleted_by", + } +} + +func (r record) query() squirrel.SelectBuilder { + return squirrel. + Select(). + From(r.table() + " AS r"). + Where("r.deleted_at IS NULL") +} + +// @todo: update to accepted DeletedAt column semantics from Messaging + +func (r record) FindByID(namespaceID, recordID uint64) (*types.Record, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("id = ?", recordID) + + c = &types.Record{} + ) + + if namespaceID > 0 { + query = query.Where("rel_namespace = ?", namespaceID) + } + + return c, isFound(r.fetchOne(c, query), c.ID > 0, ErrRecordNotFound) +} + +func (r record) Report(module *types.Module, metrics, dimensions, filter string) (results interface{}, err error) { + crb := NewRecordReportBuilder(module) + + var result = make([]map[string]interface{}, 0) + + if query, args, err := crb.Build(metrics, dimensions, filter); err != nil { + return nil, errors.Wrap(err, "can not generate report query") + } else if rows, err := r.db().Query(query, args...); err != nil { + return nil, errors.Wrapf(err, "can not execute report query (%s)", query) + } else { + for rows.Next() { + result = append(result, crb.Cast(rows)) + } + + return result, nil + } +} + +func (r record) Find(module *types.Module, filter types.RecordFilter) (set types.RecordSet, f types.RecordFilter, err error) { + var query squirrel.SelectBuilder + f = filter + f.PerPage = normalizePerPage(f.PerPage, 5, 100, 50) + + query, err = r.buildQuery(module, filter) + if err != nil { + return + } + + if f.Count, err = r.count(query); err != nil || f.Count == 0 { + return + } + + // Assemble SQL for fetching record (where + sorting + paging)... + query = query. + Columns(r.columns()...) + return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage) +} + +func (r record) buildQuery(module *types.Module, f types.RecordFilter) (query squirrel.SelectBuilder, err error) { + // Create query for fetching and counting records. + query = r.query(). + Where("r.module_id = ?", module.ID). + Where("r.rel_namespace = ?", module.NamespaceID) + + var joinedFields = []string{} + var alreadyJoined = func(f string) bool { + for _, a := range joinedFields { + if a == f { + return true + } + } + + joinedFields = append(joinedFields, f) + return false + } + + // Parse filters. + if f.Filter != "" { + var ( + // Filter parser + fp = ql.NewParser() + + // Filter node + fn ql.ASTNode + ) + + // Make a nice wrapper that will translate module fields to subqueries + fp.OnIdent = func(i ql.Ident) (ql.Ident, error) { + var is bool + if i.Value, is = isRealRecordCol(i.Value); is { + return i, nil + } + + if !module.Fields.HasName(i.Value) { + return i, errors.Errorf("unknown field %q", i.Value) + } + + if !alreadyJoined(i.Value) { + query = query.LeftJoin(fmt.Sprintf( + "compose_record_value AS rv_%s ON (rv_%s.record_id = r.id AND rv_%s.name = ? AND rv_%s.deleted_at IS NULL)", + i.Value, i.Value, i.Value, i.Value, + ), i.Value) + } + + // @todo switch value for ref when doing Record/Owner lookup + i.Value = fmt.Sprintf("rv_%s.value", i.Value) + + return i, nil + } + + if fn, err = fp.ParseExpression(f.Filter); err != nil { + return + } else if filterSql, filterArgs, err := fn.ToSql(); err != nil { + return query, err + } else { + query = query.Where("("+filterSql+")", filterArgs...) + } + } + + if f.Sort != "" { + var ( + // Sort parser + sp = ql.NewParser() + + // Sort columns + sc ql.Columns + ) + + sp.OnIdent = func(i ql.Ident) (ql.Ident, error) { + var is bool + if i.Value, is = isRealRecordCol(i.Value); is { + i.Value += " " + return i, nil + } + + if !module.Fields.HasName(i.Value) { + return i, errors.Errorf("unknown field %q", i.Value) + } + + if !alreadyJoined(i.Value) { + query = query.LeftJoin(fmt.Sprintf( + "compose_record_value AS rv_%s ON (rv_%s.record_id = r.id AND rv_%s.name = ? AND rv_%s.deleted_at IS NULL)", + i.Value, i.Value, i.Value, i.Value, + ), i.Value) + } + + // @todo switch value for ref when doing Record/Owner lookup + i.Value = fmt.Sprintf("rv_%s.value ", i.Value) + + return i, nil + } + + if sc, err = sp.ParseColumns(f.Sort); err != nil { + return + + } + + query = query.OrderBy(sc.Strings()...) + } + + return +} + +func (r record) Create(record *types.Record) (*types.Record, error) { + record.ID = factory.Sonyflake.NextID() + + if err := r.db().Replace("compose_record", record); err != nil { + return nil, errors.Wrap(err, "could not update record") + } + + return record, nil +} + +func (r record) Update(record *types.Record) (*types.Record, error) { + if err := r.db().Replace("compose_record", record); err != nil { + return nil, errors.Wrap(err, "could not update record") + } + + return record, nil +} + +func (r record) Delete(record *types.Record) error { + _, err := r.db().Exec( + "UPDATE compose_record SET deleted_at = ?, deleted_by = ? WHERE rel_namespace = ? AND id = ?", + record.DeletedAt, + record.DeletedBy, + record.NamespaceID, + record.ID, + ) + + return err +} + +func (r record) DeleteValues(record *types.Record) error { + _, err := r.db().Exec( + "UPDATE compose_record_value SET deleted_at = ? WHERE record_id = ?", + record.DeletedAt, + record.ID) + + return err +} + +func (r record) UpdateValues(recordID uint64, rvs types.RecordValueSet) (err error) { + // Remove all records and prepare to be updated + // @todo be more selective and delete only removed values + if _, err = r.db().Exec("DELETE FROM compose_record_value WHERE record_id = ?", recordID); err != nil { + return errors.Wrap(err, "could not remove record values") + } + + err = rvs.Walk(func(value *types.RecordValue) error { + value.RecordID = recordID + return r.db().Replace("compose_record_value", value) + }) + + return errors.Wrap(err, "could not replace record values") + +} + +func (r record) LoadValues(IDs ...uint64) (rvs types.RecordValueSet, err error) { + if len(IDs) == 0 { + return + } + + var sql = "SELECT record_id, name, value, ref, place, deleted_at " + + " FROM compose_record_value " + + " WHERE record_id IN (?) " + + " AND deleted_at IS NULL " + + " ORDER BY record_id, place" + + if sql, args, err := sqlx.In(sql, IDs); err != nil { + return nil, err + } else { + return rvs, r.db().Select(&rvs, sql, args...) + } +} + +// Checks if field name is "real column", reformats it and returns +func isRealRecordCol(name string) (string, bool) { + switch name { + case + "id", + "module_id", + "owned_by", + "created_by", + "created_at", + "updated_by", + "updated_at", + "deleted_by", + "deleted_at": + return name, true + + case + "moduleID", + "ownedBy", + "createdBy", + "createdAt", + "updatedBy", + "updatedAt", + "deletedBy", + "deletedAt": + return name[0:len(name)-2] + "_" + strings.ToLower(name[len(name)-2:]), true + } + + return name, false +} diff --git a/crm/internal/repository/record_report_builder.go b/compose/internal/repository/record_report_builder.go similarity index 94% rename from crm/internal/repository/record_report_builder.go rename to compose/internal/repository/record_report_builder.go index 1c31ad9ce..d3b1e036a 100644 --- a/crm/internal/repository/record_report_builder.go +++ b/compose/internal/repository/record_report_builder.go @@ -9,8 +9,8 @@ import ( "github.com/pkg/errors" "gopkg.in/Masterminds/squirrel.v1" - "github.com/crusttech/crust/crm/internal/repository/ql" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/internal/repository/ql" + "github.com/crusttech/crust/compose/types" ) type ( @@ -53,7 +53,7 @@ func NewRecordReportBuilder(module *types.Module) *recordReportBuilder { var report = squirrel. Select(). Column(squirrel.Alias(squirrel.Expr("COUNT(*)"), "count")). - From("crm_record"). + From("compose_record"). Where("module_id = ?", module.ID) return &recordReportBuilder{ @@ -88,7 +88,7 @@ func (b *recordReportBuilder) Build(metrics, dimensions, filters string) (sql st if !alreadyJoined(i.Value) { b.report = b.report.LeftJoin(fmt.Sprintf( - "crm_record_value AS rv_%s ON (rv_%s.record_id = crm_record.id AND rv_%s.name = ? AND rv_%s.deleted_at IS NULL)", + "compose_record_value AS rv_%s ON (rv_%s.record_id = compose_record.id AND rv_%s.name = ? AND rv_%s.deleted_at IS NULL)", i.Value, i.Value, i.Value, i.Value, ), i.Value) } diff --git a/crm/internal/repository/record_report_builder_test.go b/compose/internal/repository/record_report_builder_test.go similarity index 71% rename from crm/internal/repository/record_report_builder_test.go rename to compose/internal/repository/record_report_builder_test.go index 58493925b..0c3008aad 100644 --- a/crm/internal/repository/record_report_builder_test.go +++ b/compose/internal/repository/record_report_builder_test.go @@ -5,7 +5,7 @@ package repository import ( "testing" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" "github.com/crusttech/crust/internal/test" ) @@ -22,9 +22,9 @@ func TestRecordReportBuilder2(t *testing.T) { expected := "SELECT (COUNT(*)) AS count, (CAST(max(rv_single1.value) AS DECIMAL(14,2))) AS metric_0, " + "(QUARTER(rv_ref1.value)) AS dimension_0 " + - "FROM crm_record " + - "LEFT JOIN crm_record_value AS rv_single1 ON (rv_single1.record_id = crm_record.id AND rv_single1.name = ? AND rv_single1.deleted_at IS NULL) " + - "LEFT JOIN crm_record_value AS rv_ref1 ON (rv_ref1.record_id = crm_record.id AND rv_ref1.name = ? AND rv_ref1.deleted_at IS NULL) " + + "FROM compose_record " + + "LEFT JOIN compose_record_value AS rv_single1 ON (rv_single1.record_id = compose_record.id AND rv_single1.name = ? AND rv_single1.deleted_at IS NULL) " + + "LEFT JOIN compose_record_value AS rv_ref1 ON (rv_ref1.record_id = compose_record.id AND rv_ref1.name = ? AND rv_ref1.deleted_at IS NULL) " + "WHERE module_id = ? AND rv_ref1.value = 2 " + "GROUP BY dimension_0 " + "ORDER BY dimension_0" diff --git a/crm/internal/repository/record_test.go b/compose/internal/repository/record_test.go similarity index 97% rename from crm/internal/repository/record_test.go rename to compose/internal/repository/record_test.go index a60e70eb8..12107aa01 100644 --- a/crm/internal/repository/record_test.go +++ b/compose/internal/repository/record_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" "github.com/crusttech/crust/internal/test" ) diff --git a/compose/internal/repository/repository.go b/compose/internal/repository/repository.go new file mode 100644 index 000000000..547d6d687 --- /dev/null +++ b/compose/internal/repository/repository.go @@ -0,0 +1,124 @@ +package repository + +import ( + "context" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/internal/auth" +) + +type ( + repository struct { + ctx context.Context + dbh *factory.DB + } +) + +// DB produces a contextual DB handle +func DB(ctx context.Context) *factory.DB { + return factory.Database.MustGet("compose").With(ctx) +} + +// Identity returns the User ID from context +func Identity(ctx context.Context) uint64 { + return auth.GetIdentityFromContext(ctx).Identity() +} + +// With updates repository and database contexts +func (r *repository) With(ctx context.Context, db *factory.DB) *repository { + return &repository{ + ctx: ctx, + dbh: db, + } +} + +// Context returns current active repository context +func (r *repository) Context() context.Context { + return r.ctx +} + +// db returns context-aware db handle +func (r *repository) db() *factory.DB { + if r.dbh != nil { + return r.dbh + } + 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/crm/internal/repository/repository_test.go b/compose/internal/repository/repository_test.go similarity index 100% rename from crm/internal/repository/repository_test.go rename to compose/internal/repository/repository_test.go diff --git a/crm/internal/repository/sqirrel.go b/compose/internal/repository/sqirrel.go similarity index 100% rename from crm/internal/repository/sqirrel.go rename to compose/internal/repository/sqirrel.go diff --git a/compose/internal/repository/trigger.go b/compose/internal/repository/trigger.go new file mode 100644 index 000000000..045094d5d --- /dev/null +++ b/compose/internal/repository/trigger.go @@ -0,0 +1,125 @@ +package repository + +import ( + "context" + "time" + + "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" + + "github.com/crusttech/crust/compose/types" +) + +type ( + TriggerRepository interface { + With(ctx context.Context, db *factory.DB) TriggerRepository + + FindByID(namespaceID, triggerID uint64) (*types.Trigger, error) + Find(filter types.TriggerFilter) (set types.TriggerSet, f types.TriggerFilter, err error) + Create(mod *types.Trigger) (*types.Trigger, error) + Update(mod *types.Trigger) (*types.Trigger, error) + DeleteByID(namespaceID, triggerID uint64) error + } + + trigger struct { + *repository + } +) + +const ( + ErrTriggerNotFound = repositoryError("TriggerNotFound") +) + +func Trigger(ctx context.Context, db *factory.DB) TriggerRepository { + return (&trigger{}).With(ctx, db) +} + +func (r trigger) With(ctx context.Context, db *factory.DB) TriggerRepository { + return &trigger{ + repository: r.repository.With(ctx, db), + } +} + +func (r trigger) table() string { + return "compose_trigger" +} + +func (r trigger) columns() []string { + return []string{ + "id", "rel_namespace", "name", + "actions", "enabled", "source", "rel_module", + "created_at", "updated_at", "deleted_at", + } +} + +func (r trigger) query() squirrel.SelectBuilder { + return squirrel. + Select(). + From(r.table()). + Where("deleted_at IS NULL") +} + +func (r trigger) FindByID(namespaceID, triggerID uint64) (*types.Trigger, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("id = ?", triggerID) + + c = &types.Trigger{} + ) + + if namespaceID > 0 { + query = query.Where("rel_namespace = ?", namespaceID) + } + + return c, isFound(r.fetchOne(c, query), c.ID > 0, ErrTriggerNotFound) +} + +func (r trigger) Find(filter types.TriggerFilter) (set types.TriggerSet, f types.TriggerFilter, err error) { + f = filter + f.PerPage = normalizePerPage(f.PerPage, 5, 100, 50) + + query := r.query() + + if filter.NamespaceID > 0 { + query = query.Where("rel_namespace = ?", filter.NamespaceID) + } + + if f.Query != "" { + q := "%" + f.Query + "%" + query = query.Where("name like ?", q) + } + + if f.Count, err = r.count(query); err != nil || f.Count == 0 { + return + } + + query = query. + Columns(r.columns()...). + OrderBy("id ASC") + + return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage) +} + +func (r trigger) Create(mod *types.Trigger) (*types.Trigger, error) { + mod.ID = factory.Sonyflake.NextID() + mod.CreatedAt = time.Now() + + return mod, r.db().Insert(r.table(), mod) +} + +func (r trigger) Update(mod *types.Trigger) (*types.Trigger, error) { + now := time.Now() + mod.UpdatedAt = &now + return mod, r.db().Replace(r.table(), mod) +} + +func (r trigger) DeleteByID(namespaceID, triggerID uint64) error { + _, err := r.db().Exec( + "UPDATE "+r.table()+" SET deleted_at = NOW() WHERE rel_namespace = ? AND id = ?", + namespaceID, + triggerID, + ) + + return err +} diff --git a/crm/internal/service/attachment.go b/compose/internal/service/attachment.go similarity index 70% rename from crm/internal/service/attachment.go rename to compose/internal/service/attachment.go index 482b61732..0405e7c66 100644 --- a/crm/internal/service/attachment.go +++ b/compose/internal/service/attachment.go @@ -16,8 +16,8 @@ import ( "github.com/pkg/errors" "github.com/titpetric/factory" - "github.com/crusttech/crust/crm/internal/repository" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/internal/repository" + "github.com/crusttech/crust/compose/types" "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/internal/store" @@ -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,25 +86,41 @@ 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 { + if _, err := svc.pageSvc.FindByID(filter.NamespaceID, filter.PageID); err != nil { return nil, filter, err } } if filter.ModuleID > 0 { - if _, err := svc.moduleSvc.FindByID(filter.ModuleID); err != nil { + if _, err := svc.moduleSvc.FindByID(filter.NamespaceID, filter.ModuleID); err != nil { return nil, filter, err } } if filter.RecordID > 0 { - if _, err := svc.recordSvc.FindByID(filter.RecordID); err != nil { + if _, err := svc.recordSvc.FindByID(filter.NamespaceID, filter.RecordID); 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,46 +144,56 @@ 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 { + if p, err := svc.pageSvc.FindByID(namespaceID, pageID); err != nil { return nil, err } else if !svc.prmSvc.CanUpdatePage(p) { return nil, errors.New("not allowed to add attachments to this page") } 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 { + if _, err := svc.moduleSvc.FindByID(namespaceID, moduleID); err != nil { return nil, err - } else if r, err := svc.recordSvc.FindByID(recordID); err != nil { + } else if r, err := svc.recordSvc.FindByID(namespaceID, recordID); err != nil { return nil, err } else if !svc.prmSvc.CanUpdateRecord(r) { return nil, errors.New("not allowed to add attachments to this record") } 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/chart.go b/compose/internal/service/chart.go new file mode 100644 index 000000000..9c812cbb3 --- /dev/null +++ b/compose/internal/service/chart.go @@ -0,0 +1,122 @@ +package service + +import ( + "context" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/compose/internal/repository" + "github.com/crusttech/crust/compose/types" +) + +type ( + chart struct { + db *factory.DB + ctx context.Context + + prmSvc PermissionsService + + chartRepo repository.ChartRepository + } + + ChartService interface { + With(ctx context.Context) ChartService + + FindByID(namespaceID, chartID uint64) (*types.Chart, error) + Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error) + + Create(chart *types.Chart) (*types.Chart, error) + Update(chart *types.Chart) (*types.Chart, error) + DeleteByID(namespaceID, chartID uint64) error + } +) + +func Chart() ChartService { + return (&chart{ + prmSvc: DefaultPermissions, + }).With(context.Background()) +} + +func (svc chart) With(ctx context.Context) ChartService { + db := repository.DB(ctx) + return &chart{ + db: db, + ctx: ctx, + + prmSvc: svc.prmSvc.With(ctx), + + chartRepo: repository.Chart(ctx, db), + } +} + +func (svc chart) FindByID(namespaceID, chartID uint64) (c *types.Chart, err error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired + } + + if c, err = svc.chartRepo.FindByID(namespaceID, chartID); err != nil { + return + } else if !svc.prmSvc.CanReadChart(c) { + return nil, ErrNoReadPermissions.withStack() + } + + return +} + +func (svc chart) Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error) { + set, f, err = svc.chartRepo.Find(filter) + if err != nil { + return + } + + set, _ = set.Filter(func(m *types.Chart) (bool, error) { + return svc.prmSvc.CanReadChart(m), nil + }) + + return +} + +func (svc chart) Create(mod *types.Chart) (c *types.Chart, err error) { + if !svc.prmSvc.CanCreateChart(crmNamespace()) { + return nil, ErrNoCreatePermissions.withStack() + } + + return svc.chartRepo.Create(mod) +} + +func (svc chart) Update(mod *types.Chart) (c *types.Chart, err error) { + if mod.ID == 0 { + return nil, ErrInvalidID.withStack() + } + + if c, err = svc.chartRepo.FindByID(mod.NamespaceID, mod.ID); err != nil { + return + } + + if isStale(mod.UpdatedAt, c.UpdatedAt, c.CreatedAt) { + return nil, ErrStaleData.withStack() + } + + if !svc.prmSvc.CanUpdateChart(c) { + return nil, ErrNoUpdatePermissions.withStack() + } + + c.Config = mod.Config + c.Name = mod.Name + + return svc.chartRepo.Update(c) +} + +func (svc chart) DeleteByID(namespaceID, chartID uint64) error { + if namespaceID == 0 { + return ErrNamespaceRequired.withStack() + } + + if c, err := svc.chartRepo.FindByID(namespaceID, chartID); err != nil { + return err + } else if !svc.prmSvc.CanDeleteChart(c) { + return ErrNoDeletePermissions.withStack() + } + + return svc.chartRepo.DeleteByID(namespaceID, chartID) +} diff --git a/compose/internal/service/chart_test.go b/compose/internal/service/chart_test.go new file mode 100644 index 000000000..ddd93eee4 --- /dev/null +++ b/compose/internal/service/chart_test.go @@ -0,0 +1,87 @@ +// +build integration + +package service + +import ( + "context" + "testing" + + "github.com/pkg/errors" + + "github.com/crusttech/crust/compose/types" + "github.com/crusttech/crust/internal/auth" + "github.com/crusttech/crust/internal/test" + systemTypes "github.com/crusttech/crust/system/types" +) + +func TestChart(t *testing.T) { + ctx := context.WithValue(context.Background(), "testing", true) + + user := &systemTypes.User{ + ID: 1337, + Name: "John Crm Doe", + Username: "johndoe", + } + + // Set Identity (required for permission checks). + ctx = auth.SetIdentityToContext(ctx, user) + + ns1, _ := createTestNamespaces(ctx, t) + + svc := Chart().With(ctx) + + // the chart object we're working with + chart := &types.Chart{ + NamespaceID: ns1.ID, + Name: "Test", + } + + { + { + m, err := svc.Update(chart) + test.Assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) + test.Assert(t, err != nil, "Expected error when updating invalid content") + } + + // create chart + m, err := svc.Create(chart) + test.Assert(t, err == nil, "Error when creating chart: %+v", err) + test.Assert(t, m.ID > 0, "Expected auto generated ID") + + { + _, err := svc.Create(chart) + test.Assert(t, err == nil, "Unexpected error when creating chart, %+v", err) + } + + // fetch created chart + { + ms, err := svc.FindByID(m.NamespaceID, m.ID) + test.Assert(t, err == nil, "Error when retrieving chart by id: %+v", err) + test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %+v", errors.Errorf("%d != %d", m.ID, ms.ID)) + test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %+v", errors.Errorf("%s != %s", m.Name, ms.Name)) + } + + // update created chart + { + m.UpdatedAt = nil + m.Name = "Updated test" + _, err := svc.Update(m) + test.Assert(t, err == nil, "Error when updating chart, %+v", err) + } + + // re-fetch chart + { + ms, err := svc.FindByID(m.NamespaceID, m.ID) + test.Assert(t, err == nil, "Error when retrieving chart by id: %+v", err) + test.Assert(t, ms.ID == m.ID, "re-fetch: Expected ID from database to match, %d != %d", m.ID, ms.ID) + test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name) + } + + // delete chart + { + err := svc.DeleteByID(m.NamespaceID, m.ID) + test.Assert(t, err == nil, "Error when deleting chart by id: %+v", err) + } + } + +} diff --git a/compose/internal/service/error.go b/compose/internal/service/error.go new file mode 100644 index 000000000..f453ca23f --- /dev/null +++ b/compose/internal/service/error.go @@ -0,0 +1,32 @@ +package service + +import ( + "github.com/pkg/errors" +) + +type ( + serviceError string +) + +const ( + ErrInvalidID serviceError = "InvalidID" + ErrStaleData serviceError = "StaleData" + ErrNoCreatePermissions serviceError = "NoCreatePermissions" + ErrNoReadPermissions serviceError = "NoReadPermissions" + ErrNoUpdatePermissions serviceError = "NoUpdatePermissions" + ErrNoDeletePermissions serviceError = "NoDeletePermissions" + ErrNamespaceRequired serviceError = "NamespaceRequired" + ErrNotImplemented serviceError = "NotImplemented" +) + +func (e serviceError) Error() string { + return e.String() +} + +func (e serviceError) String() string { + return "crust.compose.service." + string(e) +} + +func (e serviceError) withStack() error { + return errors.WithStack(e) +} diff --git a/crm/internal/service/main_test.go b/compose/internal/service/main_test.go similarity index 55% rename from crm/internal/service/main_test.go rename to compose/internal/service/main_test.go index d6e964b23..ebecd84d9 100644 --- a/crm/internal/service/main_test.go +++ b/compose/internal/service/main_test.go @@ -3,6 +3,7 @@ package service import ( + "context" "log" "os" "testing" @@ -10,7 +11,9 @@ import ( "github.com/namsral/flag" "github.com/titpetric/factory" - crmMigrate "github.com/crusttech/crust/crm/db" + composeMigrate "github.com/crusttech/crust/compose/db" + "github.com/crusttech/crust/compose/types" + "github.com/crusttech/crust/internal/test" systemMigrate "github.com/crusttech/crust/system/db" systemService "github.com/crusttech/crust/system/service" ) @@ -25,7 +28,7 @@ func TestMain(m *testing.M) { flag.Parse() factory.Database.Add("default", dsn) - factory.Database.Add("crm", dsn) + factory.Database.Add("compose", dsn) factory.Database.Add("system", dsn) db := factory.Database.MustGet() @@ -36,14 +39,14 @@ func TestMain(m *testing.M) { log.Printf("Error running migrations: %+v\n", err) return } - if err := crmMigrate.Migrate(db); err != nil { + if err := composeMigrate.Migrate(db); err != nil { log.Printf("Error running migrations: %+v\n", err) return } // clean up tables { - for _, name := range []string{"crm_chart", "crm_trigger", "crm_module", "crm_module_form", "crm_record", "crm_record_value", "crm_page", "sys_user"} { + for _, name := range []string{"compose_chart", "compose_trigger", "compose_module", "compose_module_form", "compose_record", "compose_record_value", "compose_page", "sys_user"} { _, err := db.Exec("truncate " + name) if err != nil { panic("Error when clearing " + name + ": " + err.Error()) @@ -56,3 +59,15 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } + +func createTestNamespaces(ctx context.Context, t *testing.T) (ns1 *types.Namespace, ns2 *types.Namespace) { + var err error + + ns1, err = Namespace().With(ctx).Create(&types.Namespace{Enabled: true, Name: "TestNamespace"}) + test.Assert(t, err == nil, "Error when creating namespace: %+v", err) + + ns2, err = Namespace().With(ctx).Create(&types.Namespace{Enabled: true, Name: "TestNamespace"}) + test.Assert(t, err == nil, "Error when creating namespace: %+v", err) + + return ns1, ns2 +} diff --git a/compose/internal/service/module.go b/compose/internal/service/module.go new file mode 100644 index 000000000..c45d30bce --- /dev/null +++ b/compose/internal/service/module.go @@ -0,0 +1,151 @@ +package service + +import ( + "context" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/compose/internal/repository" + "github.com/crusttech/crust/compose/types" +) + +type ( + module struct { + db *factory.DB + ctx context.Context + + prmSvc PermissionsService + + moduleRepo repository.ModuleRepository + pageRepo repository.PageRepository + } + + ModuleService interface { + With(ctx context.Context) ModuleService + + FindByID(namespaceID, moduleID uint64) (*types.Module, error) + Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) + + Create(module *types.Module) (*types.Module, error) + Update(module *types.Module) (*types.Module, error) + DeleteByID(namespaceID, moduleID uint64) error + } +) + +func Module() ModuleService { + return (&module{ + prmSvc: DefaultPermissions, + }).With(context.Background()) +} + +func (svc *module) With(ctx context.Context) ModuleService { + db := repository.DB(ctx) + return &module{ + db: db, + ctx: ctx, + + prmSvc: svc.prmSvc.With(ctx), + + moduleRepo: repository.Module(ctx, db), + pageRepo: repository.Page(ctx, db), + } +} + +func (svc *module) FindByID(namespaceID, moduleID uint64) (m *types.Module, err error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired + } + + if m, err = svc.moduleRepo.FindByID(namespaceID, moduleID); err != nil { + return + } else if !svc.prmSvc.CanReadModule(m) { + return nil, ErrNoReadPermissions.withStack() + } + + var ff types.ModuleFieldSet + if ff, err = svc.moduleRepo.FindFields(m.ID); err != nil { + return + } else { + _ = ff.Walk(func(f *types.ModuleField) error { + m.Fields = append(m.Fields, f) + return nil + }) + } + + return +} + +func (svc *module) Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) { + set, f, err = svc.moduleRepo.Find(filter) + if err != nil { + return + } + + set, _ = set.Filter(func(m *types.Module) (bool, error) { + return svc.prmSvc.CanReadModule(m), nil + }) + + // Preload all fields and update all modules + var ff types.ModuleFieldSet + if ff, err = svc.moduleRepo.FindFields(set.IDs()...); err != nil { + return + } else { + _ = ff.Walk(func(f *types.ModuleField) error { + set.FindByID(f.ModuleID).Fields = append(set.FindByID(f.ModuleID).Fields, f) + return nil + }) + } + + return +} + +func (svc *module) Create(mod *types.Module) (*types.Module, error) { + + if mod.NamespaceID == 0 { + return nil, ErrNamespaceRequired.withStack() + } + + if !svc.prmSvc.CanCreateModule(crmNamespace()) { + return nil, ErrNoCreatePermissions.withStack() + } + + return svc.moduleRepo.Create(mod) +} + +func (svc *module) Update(mod *types.Module) (m *types.Module, err error) { + if mod.ID == 0 { + return nil, ErrInvalidID.withStack() + } + + if m, err = svc.moduleRepo.FindByID(mod.NamespaceID, mod.ID); err != nil { + return + } + + if isStale(mod.UpdatedAt, m.UpdatedAt, m.CreatedAt) { + return nil, ErrStaleData.withStack() + } + + if !svc.prmSvc.CanUpdateModule(m) { + return nil, ErrNoUpdatePermissions.withStack() + } + + m.Name = mod.Name + m.Meta = mod.Meta + m.Fields = mod.Fields + + return svc.moduleRepo.Update(m) +} + +func (svc *module) DeleteByID(namespaceID, moduleID uint64) error { + if namespaceID == 0 { + return ErrNamespaceRequired.withStack() + } + + if c, err := svc.moduleRepo.FindByID(namespaceID, moduleID); err != nil { + return err + } else if !svc.prmSvc.CanDeleteModule(c) { + return ErrNoDeletePermissions.withStack() + } + + return svc.moduleRepo.DeleteByID(namespaceID, moduleID) +} diff --git a/crm/internal/service/module_test.go b/compose/internal/service/module_test.go similarity index 88% rename from crm/internal/service/module_test.go rename to compose/internal/service/module_test.go index 678bf8698..d8fd5eaf3 100644 --- a/crm/internal/service/module_test.go +++ b/compose/internal/service/module_test.go @@ -6,7 +6,7 @@ import ( "context" "testing" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/internal/test" systemTypes "github.com/crusttech/crust/system/types" @@ -24,11 +24,14 @@ func TestModule(t *testing.T) { // Set Identity (required for permission checks). ctx = auth.SetIdentityToContext(ctx, user) + ns1, _ := createTestNamespaces(ctx, t) + svc := Module().With(ctx) // the module object we're working with module := &types.Module{ - Name: "Test", + NamespaceID: ns1.ID, + Name: "Test", Fields: types.ModuleFieldSet{ &types.ModuleField{ Name: "name", @@ -65,7 +68,7 @@ func TestModule(t *testing.T) { // fetch created module { - ms, err := svc.FindByID(m.ID) + ms, err := svc.FindByID(m.NamespaceID, m.ID) test.Assert(t, err == nil, "Error when retrieving module by id: %+v", err) test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %d != %d", m.ID, ms.ID) test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name) @@ -75,6 +78,7 @@ func TestModule(t *testing.T) { // update created module { m.Name = "Updated test" + m.UpdatedAt = nil _, err := svc.Update(m) test.Assert(t, err == nil, "Error when updating module, %+v", err) } @@ -88,7 +92,7 @@ func TestModule(t *testing.T) { // re-fetch module { - ms, err := svc.FindByID(m.ID) + ms, err := svc.FindByID(m.NamespaceID, m.ID) test.Assert(t, err == nil, "Error when retrieving module by id: %+v", err) test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %d != %d", m.ID, ms.ID) test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name) @@ -96,7 +100,7 @@ func TestModule(t *testing.T) { // fetch all modules { - ms, err := svc.Find() + ms, _, err := svc.Find(types.ModuleFilter{}) test.Assert(t, err == nil, "Error when retrieving modules: %+v", err) test.Assert(t, len(ms) >= 1, "Expected at least one module, got %d", len(ms)) prevModuleCount = len(ms) @@ -104,13 +108,13 @@ func TestModule(t *testing.T) { // re-fetch module { - err := svc.DeleteByID(m.ID) + err := svc.DeleteByID(m.NamespaceID, m.ID) test.Assert(t, err == nil, "Error when deleting module by id: %+v", err) } // fetch all modules { - ms, err := svc.Find() + ms, _, err := svc.Find(types.ModuleFilter{}) test.Assert(t, err == nil, "Error when retrieving modules: %+v", err) test.Assert(t, len(ms) < prevModuleCount, "Expected modules count to decrease after deletion, %d < %d", len(ms), prevModuleCount) } diff --git a/compose/internal/service/namespace.go b/compose/internal/service/namespace.go new file mode 100644 index 000000000..13a36c0f6 --- /dev/null +++ b/compose/internal/service/namespace.go @@ -0,0 +1,121 @@ +package service + +import ( + "context" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/compose/internal/repository" + "github.com/crusttech/crust/compose/types" +) + +type ( + namespace struct { + db *factory.DB + ctx context.Context + + prmSvc PermissionsService + + namespaceRepo repository.NamespaceRepository + } + + NamespaceService interface { + With(ctx context.Context) NamespaceService + + FindByID(namespaceID uint64) (*types.Namespace, error) + Find(types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error) + + Create(namespace *types.Namespace) (*types.Namespace, error) + Update(namespace *types.Namespace) (*types.Namespace, error) + DeleteByID(namespaceID uint64) error + } +) + +func Namespace() NamespaceService { + return (&namespace{ + prmSvc: DefaultPermissions, + }).With(context.Background()) +} + +func (svc *namespace) With(ctx context.Context) NamespaceService { + db := repository.DB(ctx) + return &namespace{ + db: db, + ctx: ctx, + + prmSvc: svc.prmSvc.With(ctx), + + namespaceRepo: repository.Namespace(ctx, db), + } +} + +func (svc *namespace) FindByID(ID uint64) (n *types.Namespace, err error) { + if ID == 0 { + return nil, ErrInvalidID.withStack() + } + + if n, err = svc.namespaceRepo.FindByID(ID); err != nil { + return + } else if !svc.prmSvc.CanReadNamespace(n) { + return nil, ErrNoReadPermissions.withStack() + } + + return +} + +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 + } + + set, _ = set.Filter(func(m *types.Namespace) (bool, error) { + return svc.prmSvc.CanReadNamespace(m), nil + }) + + return +} + +func (svc *namespace) Create(mod *types.Namespace) (*types.Namespace, error) { + if !svc.prmSvc.CanCreateNamespace() { + return nil, ErrNoCreatePermissions.withStack() + } + + return svc.namespaceRepo.Create(mod) +} + +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(mod.UpdatedAt, m.UpdatedAt, m.CreatedAt) { + return nil, ErrStaleData.withStack() + } + + if !svc.prmSvc.CanUpdateNamespace(m) { + return nil, ErrNoUpdatePermissions.withStack() + } + + m.Name = mod.Name + m.Slug = mod.Slug + m.Meta = mod.Meta + m.Enabled = mod.Enabled + + return svc.namespaceRepo.Update(m) +} + +func (svc *namespace) DeleteByID(ID uint64) error { + if m, err := svc.namespaceRepo.FindByID(ID); err != nil { + return err + } else if !svc.prmSvc.CanDeleteNamespace(m) { + return ErrNoDeletePermissions.withStack() + } + + return svc.namespaceRepo.DeleteByID(ID) +} diff --git a/compose/internal/service/namespace_test.go b/compose/internal/service/namespace_test.go new file mode 100644 index 000000000..ab5075747 --- /dev/null +++ b/compose/internal/service/namespace_test.go @@ -0,0 +1,93 @@ +// +build integration + +package service + +import ( + "context" + "testing" + + "github.com/crusttech/crust/compose/types" + "github.com/crusttech/crust/internal/auth" + "github.com/crusttech/crust/internal/test" + systemTypes "github.com/crusttech/crust/system/types" +) + +func TestNamespace(t *testing.T) { + ctx := context.WithValue(context.Background(), "testing", true) + + user := &systemTypes.User{ + ID: 1337, + Name: "John Crm Doe", + Username: "johndoe", + } + + // Set Identity (required for permission checks). + ctx = auth.SetIdentityToContext(ctx, user) + + svc := Namespace().With(ctx) + + // the namespace object we're working with + namespace := &types.Namespace{ + Name: "Test", + } + + prevNamespaceCount := uint(0) + + { + { + m, err := svc.Update(namespace) + test.Assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) + test.Assert(t, err != nil, "Expected error when updating invalid content") + } + + // create namespace + m, err := svc.Create(namespace) + test.Assert(t, err == nil, "Error when creating namespace: %+v", err) + test.Assert(t, m.ID > 0, "Expected auto generated ID") + + // fetch created namespace + { + ms, err := svc.FindByID(m.ID) + test.Assert(t, err == nil, "Error when retrieving namespace by id: %+v", err) + test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %d != %d", m.ID, ms.ID) + test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name) + } + + // update created namespace + { + m.Name = "Updated test" + m.UpdatedAt = nil + _, err := svc.Update(m) + test.Assert(t, err == nil, "Error when updating namespace, %+v", err) + } + + // re-fetch namespace + { + ms, err := svc.FindByID(m.ID) + test.Assert(t, err == nil, "Error when retrieving namespace by id: %+v", err) + test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %d != %d", m.ID, ms.ID) + test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name) + } + + // fetch all namespaces + { + _, f, err := svc.Find(types.NamespaceFilter{}) + test.Assert(t, err == nil, "Error when retrieving namespaces: %+v", err) + test.Assert(t, f.Count > 0, "Expected at least one namespace, got %d", f.Count) + prevNamespaceCount = f.Count + } + + // re-fetch namespace + { + err := svc.DeleteByID(m.ID) + test.Assert(t, err == nil, "Error when deleting namespace by id: %+v", err) + } + + // fetch all namespaces + { + _, f, err := svc.Find(types.NamespaceFilter{}) + test.Assert(t, err == nil, "Error when retrieving namespaces: %+v", err) + test.Assert(t, f.Count < prevNamespaceCount, "Expected namespaces count to decrease after deletion, %d < %d", f.Count, prevNamespaceCount) + } + } +} diff --git a/crm/internal/service/notification.go b/compose/internal/service/notification.go similarity index 100% rename from crm/internal/service/notification.go rename to compose/internal/service/notification.go diff --git a/compose/internal/service/page.go b/compose/internal/service/page.go new file mode 100644 index 000000000..d13d021e3 --- /dev/null +++ b/compose/internal/service/page.go @@ -0,0 +1,236 @@ +package service + +import ( + "context" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + + "github.com/crusttech/crust/compose/internal/repository" + "github.com/crusttech/crust/compose/types" +) + +type ( + page struct { + db *factory.DB + ctx context.Context + + prmSvc PermissionsService + + pageRepo repository.PageRepository + moduleRepo repository.ModuleRepository + } + + PageService interface { + With(ctx context.Context) PageService + + FindByID(namespaceID, pageID uint64) (*types.Page, error) + FindByModuleID(namespaceID, moduleID uint64) (*types.Page, error) + FindBySelfID(namespaceID, selfID uint64) (pages types.PageSet, f types.PageFilter, err error) + Find(filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error) + Tree(namespaceID uint64) (pages types.PageSet, err error) + + Create(page *types.Page) (*types.Page, error) + Update(page *types.Page) (*types.Page, error) + DeleteByID(namespaceID, pageID uint64) error + + Reorder(namespaceID, selfID uint64, pageIDs []uint64) error + } +) + +const ( + ErrModulePageExists serviceError = "ModulePageExists" +) + +func Page() PageService { + return (&page{ + prmSvc: DefaultPermissions, + }).With(context.Background()) +} + +func (svc *page) With(ctx context.Context) PageService { + db := repository.DB(ctx) + return &page{ + db: db, + ctx: ctx, + + prmSvc: svc.prmSvc.With(ctx), + + pageRepo: repository.Page(ctx, db), + moduleRepo: repository.Module(ctx, db), + } +} + +func (svc *page) FindByID(namespaceID, pageID uint64) (p *types.Page, err error) { + return svc.checkPermissions(svc.pageRepo.FindByID(namespaceID, pageID)) +} + +func (svc *page) FindByModuleID(namespaceID, moduleID uint64) (p *types.Page, err error) { + return svc.checkPermissions(svc.pageRepo.FindByModuleID(namespaceID, moduleID)) +} + +func (svc *page) checkPermissions(p *types.Page, err error) (*types.Page, error) { + if err != nil { + return nil, err + } else if !svc.prmSvc.CanReadPage(p) { + return nil, errors.New("not allowed to access this page") + } + + return p, err +} + +func (svc *page) FindBySelfID(namespaceID, parentID uint64) (pp types.PageSet, f types.PageFilter, err error) { + if namespaceID == 0 { + return nil, f, ErrNamespaceRequired.withStack() + } + + return svc.filterPageSetByPermission(svc.pageRepo.Find(types.PageFilter{ + NamespaceID: namespaceID, + ParentID: parentID, + + // This will enable parentID=0 query + Root: true, + })) +} + +func (svc *page) Find(filter types.PageFilter) (set types.PageSet, f types.PageFilter, err error) { + if filter.NamespaceID == 0 { + return nil, f, ErrNamespaceRequired.withStack() + } + + return svc.filterPageSetByPermission(svc.pageRepo.Find(filter)) +} + +func (svc *page) Tree(namespaceID uint64) (pages types.PageSet, err error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired.withStack() + } + + var ( + tree types.PageSet + filter = types.PageFilter{ + NamespaceID: namespaceID, + } + ) + + return tree, svc.db.Transaction(func() (err error) { + if pages, _, err = svc.filterPageSetByPermission(svc.pageRepo.Find(filter)); err != nil { + return + } + + // No preloading - we do not need (or should have) any modules + // associated with us + _ = pages.Walk(func(p *types.Page) error { + if p.SelfID == 0 { + tree = append(tree, p) + } else if c := pages.FindByID(p.SelfID); c != nil { + if c.Children == nil { + c.Children = types.PageSet{} + } + + c.Children = append(c.Children, p) + } else { + // Move orphans to root + p.SelfID = 0 + tree = append(tree, p) + } + + return nil + }) + + return nil + }) +} + +func (svc *page) filterPageSetByPermission(pp types.PageSet, f types.PageFilter, err error) (types.PageSet, types.PageFilter, error) { + if err != nil { + return nil, f, err + } + + // @todo Filter-by-permission can/will mess up filter's count & paging... + pp, err = pp.Filter(func(m *types.Page) (bool, error) { + return svc.prmSvc.CanReadPage(m), nil + }) + + return pp, f, err +} + +func (svc *page) Reorder(namespaceID, selfID uint64, pageIDs []uint64) error { + return svc.pageRepo.Reorder(namespaceID, selfID, pageIDs) +} + +func (svc *page) Create(mod *types.Page) (p *types.Page, err error) { + mod.ID = 0 + + if mod.NamespaceID == 0 { + return nil, ErrNamespaceRequired.withStack() + } + + if !svc.prmSvc.CanCreatePage(crmNamespace()) { + return nil, ErrNoCreatePermissions.withStack() + } + + if err = svc.checkModulePage(mod); err != nil { + return + } + + p, err = svc.pageRepo.Create(mod) + return +} + +func (svc *page) Update(mod *types.Page) (p *types.Page, err error) { + if mod.ID == 0 { + return nil, ErrInvalidID.withStack() + } + + if p, err = svc.pageRepo.FindByID(mod.NamespaceID, mod.ID); err != nil { + return + } + + if isStale(mod.UpdatedAt, p.UpdatedAt, p.CreatedAt) { + return nil, ErrStaleData.withStack() + } + + if !svc.prmSvc.CanUpdatePage(p) { + return nil, ErrNoUpdatePermissions.withStack() + } + + if err = svc.checkModulePage(mod); err != nil { + return + } + + p.ModuleID = mod.ModuleID + p.SelfID = mod.SelfID + p.Blocks = mod.Blocks + p.Title = mod.Title + p.Description = mod.Description + p.Visible = mod.Visible + p.Weight = mod.Weight + + p, err = svc.pageRepo.Update(p) + return +} + +func (svc page) checkModulePage(mod *types.Page) error { + if mod.ModuleID > 0 { + if p, err := svc.pageRepo.FindByModuleID(mod.NamespaceID, mod.ModuleID); err != nil { + if err.Error() != repository.ErrPageNotFound.Error() { + return err + } + } else if p.ID > 0 && mod.ID != p.ID { + return ErrModulePageExists + } + } + + return nil +} + +func (svc *page) DeleteByID(namespaceID, pageID uint64) error { + if p, err := svc.pageRepo.FindByID(namespaceID, pageID); err != nil { + return errors.Wrap(err, "could not delete page") + } else if !svc.prmSvc.CanDeletePage(p) { + return errors.New("not allowed to delete this page") + } + + return svc.pageRepo.DeleteByID(namespaceID, pageID) +} diff --git a/compose/internal/service/page_test.go b/compose/internal/service/page_test.go new file mode 100644 index 000000000..d3f2b66d8 --- /dev/null +++ b/compose/internal/service/page_test.go @@ -0,0 +1,143 @@ +// +build integration + +package service + +import ( + "context" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/pkg/errors" + + "github.com/crusttech/crust/compose/types" + "github.com/crusttech/crust/internal/auth" + "github.com/crusttech/crust/internal/test" + systemTypes "github.com/crusttech/crust/system/types" +) + +func TestPage(t *testing.T) { + ctx := context.WithValue(context.Background(), "testing", true) + + user := &systemTypes.User{ + ID: 1337, + Name: "John Crm Doe", + Username: "johndoe", + } + + // Set Identity (required for permission checks). + ctx = auth.SetIdentityToContext(ctx, user) + + ns1, _ := createTestNamespaces(ctx, t) + + svc := Page().With(ctx) + + // the page object we're working with + var err error + page := &types.Page{ + NamespaceID: ns1.ID, + Title: "Test", + ModuleID: 123, + } + (&page.Blocks).Scan([]byte("[]")) + + prevPageCount := 0 + + { + { + m, err := svc.Update(page) + test.Assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) + test.Assert(t, err != nil, "Expected error when updating invalid content") + } + + // create page + page, err = svc.Create(page) + test.Assert(t, err == nil, "Error when creating page: %+v", err) + test.Assert(t, page.ID > 0, "Expected auto generated ID") + + var firstPageID = page.ID + + page.SelfID = page.ID + + { + _, err = svc.Create(page) + test.Assert(t, err != nil, "%+v", errors.Errorf("Expected error when creating duplicate moduleID page")) + } + + page.ModuleID = 0 + + { + _, err = svc.Create(page) + test.Assert(t, err == nil, "Unexpected error when creating page, %+v", err) + } + { + _, err = svc.Create(page) + test.Assert(t, err == nil, "Unexpected error when creating page, %+v", err) + } + + // fetch created page + { + p, err := svc.FindByID(page.NamespaceID, page.ID) + test.Assert(t, err == nil, "Error when retrieving page by id: %+v", err) + test.Assert(t, p.ID == page.ID, "Expected ID from database to match, %+v", errors.Errorf("%d != %d", page.ID, p.ID)) + test.Assert(t, p.Title == page.Title, "Expected Title from database to match, %+v", errors.Errorf("%s != %s", page.Title, p.Title)) + } + + // update created page + { + page.Title = "Updated test" + page.UpdatedAt = nil + _, err := svc.Update(page) + test.Assert(t, err == nil, "Error when updating page, %+v", err) + } + + // re-fetch page + { + p, err := svc.FindByID(page.NamespaceID, page.ID) + test.Assert(t, err == nil, "Error when retrieving page by id: %+v", err) + test.Assert(t, p.ID == page.ID, "re-fetch: Expected ID from database to match, %d != %d", page.ID, p.ID) + test.Assert(t, p.Title == page.Title, "Expected Title from database to match, %s != %s", page.Title, p.Title) + } + + // fetch all pages + { + p, _, err := svc.FindBySelfID(page.NamespaceID, 0) + test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) + test.Assert(t, len(p) >= 1, "Expected at least one page, got %d", len(p)) + prevPageCount = len(p) + } + + // fetch all pages + { + p, _, err := svc.FindBySelfID(page.NamespaceID, firstPageID) + test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) + test.Assert(t, len(p) == 2, "Expected 2 pages with selfID=%d, got %v", page.ID, spew.Sdump(p)) + prevPageCount = len(p) + + ids := []uint64{p[0].ID, p[1].ID} + + { + err := svc.Reorder(page.NamespaceID, firstPageID, ids) + test.Assert(t, err == nil, "Error when reordering pages: %+v", err) + + p, _, err = svc.FindBySelfID(page.NamespaceID, firstPageID) + test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) + test.Assert(t, len(p) == 2, "Expected 2 pages with selfID=%d, got %v", page.ID, spew.Sdump(p)) + test.Assert(t, p[0].Weight < p[1].Weight, "Expected ascending order, %+v", errors.Errorf("%d < %d", p[0].Weight, p[1].Weight)) + } + } + + // re-fetch page + { + err := svc.DeleteByID(page.NamespaceID, page.ID) + test.Assert(t, err == nil, "Error when deleting page by id: %+v", err) + } + + // fetch all pages + { + p, _, err := svc.FindBySelfID(page.NamespaceID, 0) + test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) + test.Assert(t, len(p) < prevPageCount, "Expected pages count to decrease after deletion, %d < %d", len(p), prevPageCount) + } + } + +} diff --git a/crm/internal/service/permissions.go b/compose/internal/service/permissions.go similarity index 85% rename from crm/internal/service/permissions.go rename to compose/internal/service/permissions.go index e4bf725a9..c5c8ea560 100644 --- a/crm/internal/service/permissions.go +++ b/compose/internal/service/permissions.go @@ -3,8 +3,8 @@ package service import ( "context" - "github.com/crusttech/crust/crm/internal/repository" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/internal/repository" + "github.com/crusttech/crust/compose/types" internalRules "github.com/crusttech/crust/internal/rules" systemService "github.com/crusttech/crust/system/service" ) @@ -27,7 +27,11 @@ type ( Effective() (ee []effectivePermission, err error) CanAccess() bool - CanCreateNamspace() bool + CanGrant() bool + CanCreateNamespace() bool + CanReadNamespace(r permissionResource) bool + CanUpdateNamespace(r permissionResource) bool + CanDeleteNamespace(r permissionResource) bool CanCreateModule(r permissionResource) bool CanReadModule(r permissionResource) bool CanUpdateModule(r permissionResource) bool @@ -95,7 +99,7 @@ func (p *permissions) Effective() (ee []effectivePermission, err error) { ee = append(ee, ep("compose", "access", p.CanAccess())) ee = append(ee, ep("compose", "grant", p.CanGrant())) - ee = append(ee, ep("compose", "namespace.create", p.CanCreateNamspace())) + ee = append(ee, ep("compose", "namespace.create", p.CanCreateNamespace())) ee = append(ee, ep("compose:namespace:crm", "module.create", p.CanCreateModule(crmNamespace()))) ee = append(ee, ep("compose:namespace:crm", "chart.create", p.CanCreateChart(crmNamespace()))) @@ -113,13 +117,24 @@ func (p *permissions) CanGrant() bool { return p.checkAccess(types.PermissionResource, "grant") } -func (p *permissions) CanCreateNamspace() bool { +func (p *permissions) CanCreateNamespace() bool { return p.checkAccess(types.PermissionResource, "namespace.create") } -func (p *permissions) CanCreateModule(ns permissionResource) bool { - // @todo move to func args when namespaces are implemented - return p.checkAccess(ns, "module.create") +func (p *permissions) CanReadNamespace(r permissionResource) bool { + return p.checkAccess(r, "read", p.allow()) +} + +func (p *permissions) CanUpdateNamespace(r permissionResource) bool { + return p.checkAccess(r, "update") +} + +func (p *permissions) CanDeleteNamespace(r permissionResource) bool { + return p.checkAccess(r, "delete") +} + +func (p *permissions) CanCreateModule(r permissionResource) bool { + return p.checkAccess(r, "module.create") } func (p *permissions) CanReadModule(r permissionResource) bool { @@ -206,3 +221,9 @@ func (p *permissions) checkAccess(resource permissionResource, operation string, } return false } + +func (p permissions) allow() func() internalRules.Access { + return func() internalRules.Access { + return internalRules.Allow + } +} diff --git a/compose/internal/service/record.go b/compose/internal/service/record.go new file mode 100644 index 000000000..2df3694aa --- /dev/null +++ b/compose/internal/service/record.go @@ -0,0 +1,295 @@ +package service + +import ( + "context" + "strconv" + "time" + + "github.com/pkg/errors" + "github.com/titpetric/factory" + + "github.com/crusttech/crust/compose/internal/repository" + "github.com/crusttech/crust/compose/types" + "github.com/crusttech/crust/internal/auth" + + systemService "github.com/crusttech/crust/system/service" +) + +type ( + record struct { + db *factory.DB + ctx context.Context + + prmSvc PermissionsService + userSvc systemService.UserService + + recordRepo repository.RecordRepository + moduleRepo repository.ModuleRepository + } + + RecordService interface { + With(ctx context.Context) RecordService + + FindByID(namespaceID, recordID uint64) (*types.Record, error) + + Report(namespaceID, moduleID uint64, metrics, dimensions, filter string) (interface{}, error) + Find(filter types.RecordFilter) (set types.RecordSet, f types.RecordFilter, err error) + + Create(record *types.Record) (*types.Record, error) + Update(record *types.Record) (*types.Record, error) + + DeleteByID(namespaceID, recordID uint64) error + + // Fields(module *types.Module, record *types.Record) ([]*types.RecordValue, error) + } +) + +func Record() RecordService { + return (&record{ + prmSvc: DefaultPermissions, + userSvc: systemService.DefaultUser, + }).With(context.Background()) +} + +func (svc *record) With(ctx context.Context) RecordService { + db := repository.DB(ctx) + return &record{ + db: db, + ctx: ctx, + + prmSvc: svc.prmSvc.With(ctx), + userSvc: systemService.User(ctx), + + recordRepo: repository.Record(ctx, db), + moduleRepo: repository.Module(ctx, db), + } +} + +func (svc *record) FindByID(namespaceID, recordID uint64) (r *types.Record, err error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired + } + + if r, err = svc.recordRepo.FindByID(namespaceID, recordID); err != nil { + return + } + + if !svc.prmSvc.CanReadRecord(r) { + return nil, ErrNoReadPermissions.withStack() + } + + if err = svc.preloadValues(r); err != nil { + return + } + + return +} + +func (svc record) loadModule(namespaceID, moduleID uint64) (m *types.Module, err error) { + if m, err = svc.moduleRepo.FindByID(namespaceID, moduleID); err != nil { + return + } + + if m.Fields, err = svc.moduleRepo.FindFields(m.ID); err != nil { + return + } + + if !svc.prmSvc.CanReadRecord(m) { + return nil, ErrNoReadPermissions.withStack() + } + + return +} + +func (svc *record) Report(namespaceID, moduleID uint64, metrics, dimensions, filter string) (out interface{}, err error) { + var m *types.Module + if m, err = svc.loadModule(namespaceID, moduleID); err != nil { + return + } + + if !svc.prmSvc.CanReadRecord(m) { + return nil, ErrNoReadPermissions.withStack() + } + + return svc.recordRepo.Report(m, metrics, dimensions, filter) +} + +func (svc *record) Find(filter types.RecordFilter) (set types.RecordSet, f types.RecordFilter, err error) { + var m *types.Module + if m, err = svc.loadModule(filter.NamespaceID, filter.ModuleID); err != nil { + return + } + + if !svc.prmSvc.CanReadRecord(m) { + return nil, filter, ErrNoReadPermissions.withStack() + } + + set, f, err = svc.recordRepo.Find(m, filter) + if err != nil { + return + } + + if err = svc.preloadValues(set...); err != nil { + return + } + + return +} + +func (svc *record) Create(mod *types.Record) (r *types.Record, err error) { + if mod.NamespaceID == 0 { + return nil, ErrNamespaceRequired + } + + var m *types.Module + if m, err = svc.loadModule(mod.NamespaceID, mod.ModuleID); err != nil { + return + } + + if !svc.prmSvc.CanCreateRecord(m) { + return nil, ErrNoCreatePermissions.withStack() + } + + if err = svc.sanitizeValues(m, mod.Values); err != nil { + return + } + + mod.OwnedBy = auth.GetIdentityFromContext(svc.ctx).Identity() + mod.CreatedBy = mod.OwnedBy + mod.CreatedAt = time.Now() + + return r, svc.db.Transaction(func() (err error) { + if r, err = svc.recordRepo.Create(mod); err != nil { + return + } + + if err = svc.recordRepo.UpdateValues(r.ID, mod.Values); err != nil { + return + } + + if err = svc.preloadValues(r); err != nil { + return + } + + return + }) +} + +func (svc *record) Update(mod *types.Record) (r *types.Record, err error) { + if mod.ID == 0 { + return nil, ErrInvalidID.withStack() + } + + if mod.NamespaceID == 0 { + return nil, ErrNamespaceRequired + } + + var m *types.Module + if m, err = svc.loadModule(mod.NamespaceID, mod.ModuleID); err != nil { + return + } + + if r, err = svc.recordRepo.FindByID(mod.NamespaceID, mod.ID); err != nil { + return + } + + if !svc.prmSvc.CanUpdateRecord(r) { + return nil, ErrNoUpdatePermissions.withStack() + } + + if isStale(mod.UpdatedAt, r.UpdatedAt, r.CreatedAt) { + return nil, ErrStaleData.withStack() + } + + if err = svc.sanitizeValues(m, mod.Values); err != nil { + return + } + + now := time.Now() + r.UpdatedAt = &now + r.UpdatedBy = auth.GetIdentityFromContext(svc.ctx).Identity() + + return r, svc.db.Transaction(func() (err error) { + if r, err = svc.recordRepo.Update(r); err != nil { + return + } + + if err = svc.recordRepo.UpdateValues(r.ID, mod.Values); err != nil { + return + } + + return + }) +} + +func (svc *record) DeleteByID(namespaceID, recordID uint64) (err error) { + err = svc.db.Transaction(func() (err error) { + var record *types.Record + + if record, err = svc.recordRepo.FindByID(namespaceID, recordID); err != nil { + return errors.Wrap(err, "nonexistent record") + } + + now := time.Now() + record.DeletedAt = &now + record.DeletedBy = auth.GetIdentityFromContext(svc.ctx).Identity() + + if err = svc.recordRepo.Delete(record); err != nil { + return + } + + if err = svc.recordRepo.DeleteValues(record); err != nil { + return + } + + return + }) + + return errors.Wrap(err, "unable to delete record") +} + +// Validates and filters record values +func (svc *record) sanitizeValues(module *types.Module, values types.RecordValueSet) (err error) { + // Make sure there are no multi values in a non-multi value fields + err = module.Fields.Walk(func(field *types.ModuleField) error { + if !field.Multi && len(values.FilterByName(field.Name)) > 1 { + return errors.Errorf("more than one value for a single-value field %q", field.Name) + } + return nil + }) + + if err != nil { + return + } + + var places = map[string]uint{} + + return values.Walk(func(value *types.RecordValue) (err error) { + var field = module.Fields.FindByName(value.Name) + if field == nil { + return errors.Errorf("no such field %q", value.Name) + } + + if field.IsRef() { + if value.Ref, err = strconv.ParseUint(value.Value, 10, 64); err != nil { + return err + } + } + + value.Place = places[field.Name] + places[field.Name]++ + + return nil + }) +} + +func (svc *record) preloadValues(rr ...*types.Record) error { + if rvs, err := svc.recordRepo.LoadValues(types.RecordSet(rr).IDs()...); err != nil { + return err + } else { + return types.RecordSet(rr).Walk(func(r *types.Record) error { + r.Values = rvs.FilterByRecordID(r.ID) + return nil + }) + } +} diff --git a/crm/internal/service/record_test.go b/compose/internal/service/record_test.go similarity index 64% rename from crm/internal/service/record_test.go rename to compose/internal/service/record_test.go index f393ac293..090539900 100644 --- a/crm/internal/service/record_test.go +++ b/compose/internal/service/record_test.go @@ -6,7 +6,7 @@ import ( "context" "testing" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/internal/test" systemService "github.com/crusttech/crust/system/service" @@ -23,6 +23,9 @@ func TestRecord(t *testing.T) { ctx = auth.SetIdentityToContext(ctx, auth.NewIdentity(user.Identity())) + var err error + ns1, ns2 := createTestNamespaces(ctx, t) + { userSvc := systemService.TestUser(t, ctx) _, err := userSvc.Create(user) @@ -31,8 +34,9 @@ func TestRecord(t *testing.T) { svc := Record().With(ctx) - module := &types.Module{ - Name: "Test", + module1 := &types.Module{ + NamespaceID: ns1.ID, + Name: "Test", Fields: types.ModuleFieldSet{ &types.ModuleField{ Name: "name", @@ -54,18 +58,27 @@ func TestRecord(t *testing.T) { }, } - // set up a module - var err error - module, err = Module().With(ctx).Create(module) - test.Assert(t, err == nil, "Error when creating module: %+v", err) - test.Assert(t, module.ID > 0, "Expected auto generated ID") + // set up a module1 + module1, err = Module().With(ctx).Create(module1) + test.Assert(t, err == nil, "Error when creating module1: %+v", err) + test.Assert(t, module1.ID > 0, "Expected auto generated ID") + + module2 := &types.Module{ + NamespaceID: ns2.ID, + Name: "Test Dummy", + Fields: module1.Fields, + } + module2, err = Module().With(ctx).Create(module2) + test.Assert(t, err == nil, "Error when creating module1 in another namespace: %+v", err) record1 := &types.Record{ - ModuleID: module.ID, + NamespaceID: ns1.ID, + ModuleID: module1.ID, } record2 := &types.Record{ - ModuleID: module.ID, + NamespaceID: ns1.ID, + ModuleID: module1.ID, Values: types.RecordValueSet{ &types.RecordValue{ Name: "name", @@ -98,9 +111,19 @@ func TestRecord(t *testing.T) { }, } + { + // Let's put something to another namespace... + _, err := svc.Create(&types.Record{ + NamespaceID: ns2.ID, + ModuleID: module2.ID, + }) + test.Assert(t, err == nil, "Error when creating record: %+v", err) + } + // now work with records { { + record1.UpdatedAt = nil m, err := svc.Update(record1) test.Assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) test.Assert(t, err != nil, "Expected error when updating invalid record") @@ -118,7 +141,7 @@ func TestRecord(t *testing.T) { // fetch created record { - ms, err := svc.FindByID(m1.ID) + ms, err := svc.FindByID(m1.NamespaceID, m1.ID) test.Assert(t, err == nil, "Error when retrieving record by id: %+v", err) test.Assert(t, ms.ID == m1.ID, "Expected ID from database to match, %d != %d", m1.ID, ms.ID) test.Assert(t, ms.ModuleID == m1.ModuleID, "Expected Module ID from database to match, %d != %d", m1.ModuleID, ms.ModuleID) @@ -126,13 +149,14 @@ func TestRecord(t *testing.T) { // update created record { + m1.UpdatedAt = nil _, err := svc.Update(m1) test.Assert(t, err == nil, "Error when updating record, %+v", err) } // re-fetch record { - ms, err := svc.FindByID(m1.ID) + ms, err := svc.FindByID(m1.NamespaceID, m1.ID) test.Assert(t, err == nil, "Error when retrieving record by id: %+v", err) test.Assert(t, ms.ID == m1.ID, "Expected ID from database to match, %d != %d", m1.ID, ms.ID) test.Assert(t, ms.ModuleID == m1.ModuleID, "Expected ID from database to match, %d != %d", m1.ModuleID, ms.ModuleID) @@ -140,39 +164,40 @@ func TestRecord(t *testing.T) { // fetch all records { - mr, err := svc.Find(module.ID, "", "id desc", 0, 20) + mr, f, err := svc.Find(types.RecordFilter{ModuleID: module1.ID, Sort: "id desc"}) test.Assert(t, err == nil, "Error when retrieving records: %+v", err) - test.Assert(t, len(mr.Records) == 2, "Expected two record, got %d", len(mr.Records)) - test.Assert(t, mr.Meta.Count == 2, "Expected Meta.Count == 2, got %d", mr.Meta.Count) - test.Assert(t, mr.Meta.Sort == "id desc", "Expected Meta.Sort == id desc, got '%s'", mr.Meta.Sort) - test.Assert(t, mr.Records[0].ModuleID == m1.ModuleID, "Expected record module to match, %d != %d", m1.ModuleID, mr.Records[0].ModuleID) - test.Assert(t, mr.Records[0].ID > mr.Records[1].ID, "Expected order to be descending") + test.Assert(t, len(mr) == 2, "Expected two record, got %d", len(mr)) + test.Assert(t, f.Count == 2, "Expected Meta.Count == 2, got %d", f.Count) + test.Assert(t, f.Sort == "id desc", "Expected Meta.Sort == id desc, got '%s'", f.Sort) + test.Assert(t, mr[0].ModuleID == m1.ModuleID, "Expected record module1 to match, %d != %d", m1.ModuleID, mr[0].ModuleID) + test.Assert(t, mr[0].ID > mr[1].ID, "Expected order to be descending") } // fetch all records { - mr, err := svc.Find(module.ID, "", "name asc, email desc", 0, 20) + + mr, f, err := svc.Find(types.RecordFilter{ModuleID: module1.ID, Sort: "name asc, email desc"}) test.Assert(t, err == nil, "Error when retrieving records: %+v", err) - test.Assert(t, len(mr.Records) == 2, "Expected two record, got %d", len(mr.Records)) - test.Assert(t, mr.Meta.Count == 2, "Expected Meta.Count == 2, got %d", mr.Meta.Count) - test.Assert(t, mr.Meta.Sort == "name asc, email desc", "Expected Meta.Sort == 'name asc, email desc' '%s'", mr.Meta.Sort) - test.Assert(t, mr.Records[0].ModuleID == m1.ModuleID, "Expected record module to match, %d != %d", m1.ModuleID, mr.Records[0].ModuleID) + test.Assert(t, len(mr) == 2, "Expected two record, got %d", len(mr)) + test.Assert(t, f.Count == 2, "Expected Meta.Count == 2, got %d", f.Count) + test.Assert(t, f.Sort == "name asc, email desc", "Expected Meta.Sort == 'name asc, email desc' '%s'", f.Sort) + test.Assert(t, mr[0].ModuleID == m1.ModuleID, "Expected record module1 to match, %d != %d", m1.ModuleID, mr[0].ModuleID) // @todo sort is not stable - // test.Assert(t, mr.Records[0].ID > mr.Records[1].ID, "Expected order to be ascending") + // test.Assert(t, mr[0].ID > mr[1].ID, "Expected order to be ascending") } // fetch all records { - mr, err := svc.Find(module.ID, "", "created_at desc", 0, 20) + mr, f, err := svc.Find(types.RecordFilter{ModuleID: module1.ID, Sort: "created_at desc"}) test.Assert(t, err == nil, "Error when retrieving records: %+v", err) - test.Assert(t, len(mr.Records) == 2, "Expected two record, got %d", len(mr.Records)) - test.Assert(t, mr.Meta.Count == 2, "Expected Meta.Count == 2, got %d", mr.Meta.Count) - test.Assert(t, mr.Meta.Sort == "created_at desc", "Expected Meta.Sort == created_at desc, got '%s'", mr.Meta.Sort) - test.Assert(t, mr.Records[0].ModuleID == m1.ModuleID, "Expected record module to match, %d != %d", m1.ModuleID, mr.Records[0].ModuleID) + test.Assert(t, len(mr) == 2, "Expected two record, got %d", len(mr)) + test.Assert(t, f.Count == 2, "Expected Meta.Count == 2, got %d", f.Count) + test.Assert(t, f.Sort == "created_at desc", "Expected Meta.Sort == created_at desc, got '%s'", f.Sort) + test.Assert(t, mr[0].ModuleID == m1.ModuleID, "Expected record module1 to match, %d != %d", m1.ModuleID, mr[0].ModuleID) // @todo sort is not stable - // test.Assert(t, mr.Records[0].ID > mr.Records[1].ID, "Expected order to be ascending") + // test.Assert(t, mr[0].ID > mr[1].ID, "Expected order to be ascending") } // fetch all records by query @@ -180,37 +205,37 @@ func TestRecord(t *testing.T) { filter := "name='John Doe' AND email='john.doe@example.com'" sort := "id desc" - mr, err := svc.Find(module.ID, filter, sort, 0, 20) + mr, f, err := svc.Find(types.RecordFilter{ModuleID: module1.ID, Sort: sort, Filter: filter}) test.Assert(t, err == nil, "Error when retrieving records: %+v", err) - test.Assert(t, len(mr.Records) == 1, "Expected one record, got %d", len(mr.Records)) - test.Assert(t, mr.Meta.Count == 1, "Expected Meta.Count == 1, got %d", mr.Meta.Count) - test.Assert(t, mr.Meta.Page == 0, "Expected Meta.Page == 0, got %d", mr.Meta.Page) - test.Assert(t, mr.Meta.PerPage == 20, "Expected Meta.PerPage == 20, got %d", mr.Meta.PerPage) - test.Assert(t, mr.Meta.Filter == filter, "Expected Meta.Filter == %q, got %q", filter, mr.Meta.Filter) - test.Assert(t, mr.Meta.Sort == sort, "Expected Meta.Sort == %q, got %q", sort, mr.Meta.Sort) + test.Assert(t, len(mr) == 1, "Expected one record, got %d", len(mr)) + test.Assert(t, f.Count == 1, "Expected Meta.Count == 1, got %d", f.Count) + test.Assert(t, f.Page == 0, "Expected Meta.Page == 0, got %d", f.Page) + test.Assert(t, f.PerPage == 50, "Expected Meta.PerPage == 50, got %d", f.PerPage) + test.Assert(t, f.Filter == filter, "Expected Meta.Filter == %q, got %q", filter, f.Filter) + test.Assert(t, f.Sort == sort, "Expected Meta.Sort == %q, got %q", sort, f.Sort) } // fetch all records by query { - mr, err := svc.Find(module.ID, "name='niall'", "id asc", 0, 20) + mr, _, err := svc.Find(types.RecordFilter{ModuleID: module1.ID, Sort: "id asc", Filter: "name='niall'"}) test.Assert(t, err == nil, "Error when retrieving records: %+v", err) - test.Assert(t, len(mr.Records) == 0, "Expected no records, got %d", len(mr.Records)) + test.Assert(t, len(mr) == 0, "Expected no records, got %d", len(mr)) } // delete record { - err := svc.DeleteByID(m1.ID) + err := svc.DeleteByID(m1.NamespaceID, m1.ID) test.Assert(t, err == nil, "Error when retrieving record by id: %+v", err) - err = svc.DeleteByID(m2.ID) + err = svc.DeleteByID(m2.NamespaceID, m2.ID) test.Assert(t, err == nil, "Error when retrieving record by id: %+v", err) } // fetch all records { - mr, err := svc.Find(module.ID, "", "", 0, 20) + mr, _, err := svc.Find(types.RecordFilter{ModuleID: module1.ID}) test.Assert(t, err == nil, "Error when retrieving records: %+v", err) - test.Assert(t, len(mr.Records) == 0, "Expected no record, got %d", len(mr.Records)) + test.Assert(t, len(mr) == 0, "Expected no record, got %d", len(mr)) } } } diff --git a/crm/internal/service/service.go b/compose/internal/service/service.go similarity index 65% rename from crm/internal/service/service.go rename to compose/internal/service/service.go index 5cbb01218..95d25417e 100644 --- a/crm/internal/service/service.go +++ b/compose/internal/service/service.go @@ -1,6 +1,8 @@ package service import ( + "time" + "github.com/crusttech/crust/internal/store" ) @@ -19,6 +21,7 @@ var ( DefaultNotification NotificationService DefaultPermissions PermissionsService DefaultAttachment AttachmentService + DefaultNamespace NamespaceService ) func Init() error { @@ -35,6 +38,21 @@ func Init() error { DefaultChart = Chart() DefaultNotification = Notification() DefaultAttachment = Attachment(fs) + DefaultNamespace = Namespace() 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 for stale-data-check + return false + } + + if updatedAt != nil { + return !new.Equal(*updatedAt) + } + + return new.Equal(createdAt) +} diff --git a/compose/internal/service/trigger.go b/compose/internal/service/trigger.go new file mode 100644 index 000000000..322de20d5 --- /dev/null +++ b/compose/internal/service/trigger.go @@ -0,0 +1,129 @@ +package service + +import ( + "context" + + "github.com/titpetric/factory" + + "github.com/crusttech/crust/compose/internal/repository" + "github.com/crusttech/crust/compose/types" +) + +type ( + trigger struct { + db *factory.DB + ctx context.Context + + prmSvc PermissionsService + + triggerRepo repository.TriggerRepository + } + + TriggerService interface { + With(ctx context.Context) TriggerService + + FindByID(namespaceID, triggerID uint64) (*types.Trigger, error) + Find(filter types.TriggerFilter) (set types.TriggerSet, f types.TriggerFilter, err error) + + Create(trigger *types.Trigger) (*types.Trigger, error) + Update(trigger *types.Trigger) (*types.Trigger, error) + DeleteByID(namespaceID, triggerID uint64) error + } +) + +func Trigger() TriggerService { + return (&trigger{ + prmSvc: DefaultPermissions, + }).With(context.Background()) +} + +func (svc trigger) With(ctx context.Context) TriggerService { + db := repository.DB(ctx) + return &trigger{ + db: db, + ctx: ctx, + + prmSvc: svc.prmSvc.With(ctx), + + triggerRepo: repository.Trigger(ctx, db), + } +} + +func (svc trigger) FindByID(namespaceID, triggerID uint64) (c *types.Trigger, err error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired + } + + if c, err = svc.triggerRepo.FindByID(namespaceID, triggerID); err != nil { + return + } else if !svc.prmSvc.CanReadTrigger(c) { + return nil, ErrNoReadPermissions.withStack() + } + + return +} + +func (svc trigger) Find(filter types.TriggerFilter) (set types.TriggerSet, f types.TriggerFilter, err error) { + set, f, err = svc.triggerRepo.Find(filter) + if err != nil { + return + } + + set, _ = set.Filter(func(m *types.Trigger) (bool, error) { + return svc.prmSvc.CanReadTrigger(m), nil + }) + + return +} + +func (svc trigger) Create(mod *types.Trigger) (c *types.Trigger, err error) { + if mod.NamespaceID == 0 { + return nil, ErrNamespaceRequired.withStack() + } + + if !svc.prmSvc.CanCreateTrigger(crmNamespace()) { + return nil, ErrNoCreatePermissions.withStack() + } + + return svc.triggerRepo.Create(mod) +} + +func (svc trigger) Update(mod *types.Trigger) (c *types.Trigger, err error) { + if mod.ID == 0 { + return nil, ErrInvalidID.withStack() + } + + if c, err = svc.triggerRepo.FindByID(mod.NamespaceID, mod.ID); err != nil { + return + } + + if isStale(mod.UpdatedAt, c.UpdatedAt, c.CreatedAt) { + return nil, ErrStaleData.withStack() + } + + if !svc.prmSvc.CanUpdateTrigger(c) { + return nil, ErrNoUpdatePermissions.withStack() + } + + c.Name = mod.Name + c.ModuleID = mod.ModuleID + c.Source = mod.Source + c.Actions = mod.Actions + c.Enabled = mod.Enabled + + return svc.triggerRepo.Update(c) +} + +func (svc trigger) DeleteByID(namespaceID, triggerID uint64) error { + if namespaceID == 0 { + return ErrNamespaceRequired.withStack() + } + + if c, err := svc.triggerRepo.FindByID(namespaceID, triggerID); err != nil { + return err + } else if !svc.prmSvc.CanDeleteTrigger(c) { + return ErrNoDeletePermissions.withStack() + } + + return svc.triggerRepo.DeleteByID(namespaceID, triggerID) +} diff --git a/crm/internal/service/trigger_test.go b/compose/internal/service/trigger_test.go similarity index 87% rename from crm/internal/service/trigger_test.go rename to compose/internal/service/trigger_test.go index d9b9c51aa..267a62aa0 100644 --- a/crm/internal/service/trigger_test.go +++ b/compose/internal/service/trigger_test.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/internal/test" systemTypes "github.com/crusttech/crust/system/types" @@ -26,12 +26,15 @@ func TestTrigger(t *testing.T) { // Set Identity (required for permission checks). ctx = auth.SetIdentityToContext(ctx, user) + ns1, _ := createTestNamespaces(ctx, t) + svc := Trigger().With(ctx) // the trigger object we're working with trigger := &types.Trigger{ - Name: "Test", - ModuleID: 123, + NamespaceID: ns1.ID, + Name: "Test", + ModuleID: 123, } { @@ -53,7 +56,7 @@ func TestTrigger(t *testing.T) { // fetch created trigger { - ms, err := svc.FindByID(m.ID) + ms, err := svc.FindByID(m.NamespaceID, m.ID) test.Assert(t, err == nil, "Error when retrieving trigger by id: %+v", err) test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %+v", errors.Errorf("%d != %d", m.ID, ms.ID)) test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %+v", errors.Errorf("%s != %s", m.Name, ms.Name)) @@ -61,6 +64,7 @@ func TestTrigger(t *testing.T) { // update created trigger { + m.UpdatedAt = nil m.Name = "Updated test" _, err := svc.Update(m) test.Assert(t, err == nil, "Error when updating trigger, %+v", err) @@ -68,7 +72,7 @@ func TestTrigger(t *testing.T) { // re-fetch trigger { - ms, err := svc.FindByID(m.ID) + ms, err := svc.FindByID(m.NamespaceID, m.ID) test.Assert(t, err == nil, "Error when retrieving trigger by id: %+v", err) test.Assert(t, ms.ID == m.ID, "re-fetch: Expected ID from database to match, %d != %d", m.ID, ms.ID) test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name) @@ -76,7 +80,7 @@ func TestTrigger(t *testing.T) { // delete trigger { - err := svc.DeleteByID(m.ID) + err := svc.DeleteByID(m.NamespaceID, m.ID) test.Assert(t, err == nil, "Error when deleting trigger by id: %+v", err) } } diff --git a/compose/rest/attachment.go b/compose/rest/attachment.go new file mode 100644 index 000000000..0eee17bcb --- /dev/null +++ b/compose/rest/attachment.go @@ -0,0 +1,183 @@ +package rest + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/titpetric/factory/resputil" + + "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 + +type ( + attachmentPayload struct { + *types.Attachment + } + + attachmentSetPayload struct { + Filter types.AttachmentFilter `json:"filter"` + Set []*attachmentPayload `json:"set"` + } + + Attachment struct { + attachment service.AttachmentService + } +) + +func (Attachment) New() *Attachment { + 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) { + f := types.AttachmentFilter{ + NamespaceID: r.NamespaceID, + Kind: r.Kind, + ModuleID: r.ModuleID, + RecordID: r.RecordID, + FieldName: r.FieldName, + // Filter: r.Filter, + PerPage: r.PerPage, + Page: r.Page, + // Sort: r.Sort, + } + + 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 + } + + 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.NamespaceID, r.AttachmentID, r.UserID, r.Sign); err != nil { + return nil, err + } + + 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.NamespaceID, r.AttachmentID, r.UserID, r.Sign); err != nil { + return nil, err + } + + return ctrl.serve(ctx, r.NamespaceID, r.AttachmentID, true, false) +} + +func (ctrl Attachment) isAccessible(namespaceID, attachmentID, userID uint64, signature string) error { + if userID == 0 { + return errors.New("missing or invalid user ID") + } + + if attachmentID == 0 { + return errors.New("missing or invalid attachment ID") + } + + if auth.DefaultSigner.Verify(signature, userID, namespaceID, attachmentID) { + return errors.New("missing or invalid signature") + } + + return nil +} + +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(namespaceID, attachmentID) + if err != nil { + // Simplify error handling for now + w.WriteHeader(http.StatusNotFound) + return + } + + var fh io.ReadSeeker + + if preview { + fh, err = ctrl.attachment.OpenPreview(att) + } else { + fh, err = ctrl.attachment.OpenOriginal(att) + } + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + name := url.QueryEscape(att.Name) + + if download { + w.Header().Add("Content-Disposition", "attachment; filename="+name) + } else { + w.Header().Add("Content-Disposition", "inline; filename="+name) + } + + http.ServeContent(w, req, name, att.CreatedAt, fh) + }, 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 ( + 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("/namespace/%d/attachment/%s/%d/", a.NamespaceID, a.Kind, a.ID) + ) + + if a.Meta.Preview != nil { + var ext = a.Meta.Preview.Extension + if ext == "" { + ext = "jpg" + } + + preview = baseURL + fmt.Sprintf("preview.%s", ext) + } + + 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/chart.go b/compose/rest/chart.go new file mode 100644 index 000000000..e08552114 --- /dev/null +++ b/compose/rest/chart.go @@ -0,0 +1,126 @@ +package rest + +import ( + "context" + + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/compose/internal/service" + "github.com/crusttech/crust/compose/rest/request" + "github.com/crusttech/crust/compose/types" + + "github.com/pkg/errors" +) + +var _ = errors.Wrap + +type ( + chartPayload struct { + *types.Chart + + CanUpdateChart bool `json:"canUpdateChart"` + CanDeleteChart bool `json:"canDeleteChart"` + } + + chartSetPayload struct { + Filter types.ChartFilter `json:"filter"` + Set []*chartPayload `json:"set"` + } + + Chart struct { + chart service.ChartService + permissions service.PermissionsService + } +) + +func (Chart) New() *Chart { + return &Chart{ + chart: service.DefaultChart, + permissions: service.DefaultPermissions, + } +} + +func (ctrl Chart) List(ctx context.Context, r *request.ChartList) (interface{}, error) { + f := types.ChartFilter{ + NamespaceID: r.NamespaceID, + + Query: r.Query, + PerPage: r.PerPage, + Page: r.Page, + } + + set, filter, err := ctrl.chart.With(ctx).Find(f) + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl Chart) Create(ctx context.Context, r *request.ChartCreate) (interface{}, error) { + var err error + mod := &types.Chart{ + NamespaceID: r.NamespaceID, + Name: r.Name, + Config: r.Config, + } + + mod, err = ctrl.chart.With(ctx).Create(mod) + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl Chart) Read(ctx context.Context, r *request.ChartRead) (interface{}, error) { + mod, err := ctrl.chart.With(ctx).FindByID(r.NamespaceID, r.ChartID) + return ctrl.makePayload(ctx, mod, err) + +} + +func (ctrl Chart) Update(ctx context.Context, r *request.ChartUpdate) (interface{}, error) { + var ( + mod = &types.Chart{} + err error + ) + + mod.ID = r.ChartID + mod.Name = r.Name + mod.Config = r.Config + mod.NamespaceID = r.NamespaceID + mod.UpdatedAt = r.UpdatedAt + + mod, err = ctrl.chart.With(ctx).Update(mod) + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl Chart) Delete(ctx context.Context, r *request.ChartDelete) (interface{}, error) { + _, err := ctrl.chart.With(ctx).FindByID(r.NamespaceID, r.ChartID) + if err != nil { + return nil, err + } + + return resputil.OK(), ctrl.chart.With(ctx).DeleteByID(r.NamespaceID, r.ChartID) +} + +func (ctrl Chart) makePayload(ctx context.Context, c *types.Chart, err error) (*chartPayload, error) { + if err != nil || c == nil { + return nil, err + } + + perm := ctrl.permissions.With(ctx) + + return &chartPayload{ + Chart: c, + + CanUpdateChart: perm.CanUpdateChart(c), + CanDeleteChart: perm.CanDeleteChart(c), + }, nil +} + +func (ctrl Chart) makeFilterPayload(ctx context.Context, nn types.ChartSet, f types.ChartFilter, err error) (*chartSetPayload, error) { + if err != nil { + return nil, err + } + + modp := &chartSetPayload{Filter: f, Set: make([]*chartPayload, len(nn))} + + for i := range nn { + modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return modp, nil +} diff --git a/crm/rest/handlers/attachment.go b/compose/rest/handlers/attachment.go similarity index 70% rename from crm/rest/handlers/attachment.go rename to compose/rest/handlers/attachment.go index 9a1225521..453e4106e 100644 --- a/crm/rest/handlers/attachment.go +++ b/compose/rest/handlers/attachment.go @@ -23,13 +23,14 @@ import ( "github.com/go-chi/chi" "github.com/titpetric/factory/resputil" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/rest/request" ) // 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 { @@ -130,9 +152,10 @@ func NewAttachment(ah AttachmentAPI) *Attachment { func (ah *Attachment) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { r.Group(func(r chi.Router) { r.Use(middlewares...) - r.Get("/attachment/{kind}/", ah.List) - r.Get("/attachment/{kind}/{attachmentID}", ah.Details) - r.Get("/attachment/{kind}/{attachmentID}/original/{name}", ah.Original) - r.Get("/attachment/{kind}/{attachmentID}/preview.{ext}", ah.Preview) + r.Get("/namespace/{namespaceID}/attachment/{kind}/", ah.List) + 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/crm/rest/handlers/chart.go b/compose/rest/handlers/chart.go similarity index 91% rename from crm/rest/handlers/chart.go rename to compose/rest/handlers/chart.go index 7dc5880e8..2b5b01d3f 100644 --- a/crm/rest/handlers/chart.go +++ b/compose/rest/handlers/chart.go @@ -23,7 +23,7 @@ import ( "github.com/go-chi/chi" "github.com/titpetric/factory/resputil" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/rest/request" ) // Internal API interface @@ -152,10 +152,10 @@ func NewChart(ch ChartAPI) *Chart { func (ch *Chart) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { r.Group(func(r chi.Router) { r.Use(middlewares...) - r.Get("/chart/", ch.List) - r.Post("/chart/", ch.Create) - r.Get("/chart/{chartID}", ch.Read) - r.Post("/chart/{chartID}", ch.Update) - r.Delete("/chart/{chartID}", ch.Delete) + r.Get("/namespace/{namespaceID}/chart/", ch.List) + r.Post("/namespace/{namespaceID}/chart/", ch.Create) + r.Get("/namespace/{namespaceID}/chart/{chartID}", ch.Read) + r.Post("/namespace/{namespaceID}/chart/{chartID}", ch.Update) + r.Delete("/namespace/{namespaceID}/chart/{chartID}", ch.Delete) }) } diff --git a/crm/rest/handlers/module.go b/compose/rest/handlers/module.go similarity index 91% rename from crm/rest/handlers/module.go rename to compose/rest/handlers/module.go index ad8f4bad8..b42c7153d 100644 --- a/crm/rest/handlers/module.go +++ b/compose/rest/handlers/module.go @@ -23,7 +23,7 @@ import ( "github.com/go-chi/chi" "github.com/titpetric/factory/resputil" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/rest/request" ) // Internal API interface @@ -152,10 +152,10 @@ func NewModule(mh ModuleAPI) *Module { func (mh *Module) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { r.Group(func(r chi.Router) { r.Use(middlewares...) - r.Get("/module/", mh.List) - r.Post("/module/", mh.Create) - r.Get("/module/{moduleID}", mh.Read) - r.Post("/module/{moduleID}", mh.Update) - r.Delete("/module/{moduleID}", mh.Delete) + r.Get("/namespace/{namespaceID}/module/", mh.List) + r.Post("/namespace/{namespaceID}/module/", mh.Create) + r.Get("/namespace/{namespaceID}/module/{moduleID}", mh.Read) + r.Post("/namespace/{namespaceID}/module/{moduleID}", mh.Update) + r.Delete("/namespace/{namespaceID}/module/{moduleID}", mh.Delete) }) } diff --git a/compose/rest/handlers/namespace.go b/compose/rest/handlers/namespace.go new file mode 100644 index 000000000..4594d6a7b --- /dev/null +++ b/compose/rest/handlers/namespace.go @@ -0,0 +1,161 @@ +package handlers + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `namespace.go`, `namespace.util.go` or `namespace_test.go` to + implement your API calls, helper functions and tests. The file `namespace.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + + "net/http" + + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/compose/rest/request" +) + +// Internal API interface +type NamespaceAPI interface { + List(context.Context, *request.NamespaceList) (interface{}, error) + Create(context.Context, *request.NamespaceCreate) (interface{}, error) + Read(context.Context, *request.NamespaceRead) (interface{}, error) + Update(context.Context, *request.NamespaceUpdate) (interface{}, error) + Delete(context.Context, *request.NamespaceDelete) (interface{}, error) +} + +// HTTP API interface +type Namespace struct { + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) +} + +func NewNamespace(nh NamespaceAPI) *Namespace { + return &Namespace{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNamespaceList() + if err := params.Fill(r); err != nil { + resputil.JSON(w, err) + return + } + if value, err := nh.List(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 + } + }, + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNamespaceCreate() + if err := params.Fill(r); err != nil { + resputil.JSON(w, err) + return + } + if value, err := nh.Create(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 + } + }, + Read: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNamespaceRead() + if err := params.Fill(r); err != nil { + resputil.JSON(w, err) + return + } + if value, err := nh.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 + } + }, + Update: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewNamespaceUpdate() + if err := params.Fill(r); err != nil { + resputil.JSON(w, err) + return + } + if value, err := nh.Update(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.NewNamespaceDelete() + if err := params.Fill(r); err != nil { + resputil.JSON(w, err) + return + } + if value, err := nh.Delete(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 + } + }, + } +} + +func (nh *Namespace) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/namespace/", nh.List) + r.Post("/namespace/", nh.Create) + r.Get("/namespace/{namespaceID}", nh.Read) + r.Post("/namespace/{namespaceID}", nh.Update) + r.Delete("/namespace/{namespaceID}", nh.Delete) + }) +} diff --git a/crm/rest/handlers/notification.go b/compose/rest/handlers/notification.go similarity index 97% rename from crm/rest/handlers/notification.go rename to compose/rest/handlers/notification.go index 802382de4..ce176b119 100644 --- a/crm/rest/handlers/notification.go +++ b/compose/rest/handlers/notification.go @@ -23,7 +23,7 @@ import ( "github.com/go-chi/chi" "github.com/titpetric/factory/resputil" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/rest/request" ) // Internal API interface diff --git a/crm/rest/handlers/page.go b/compose/rest/handlers/page.go similarity index 91% rename from crm/rest/handlers/page.go rename to compose/rest/handlers/page.go index 2e5b2b3ef..f8437b910 100644 --- a/crm/rest/handlers/page.go +++ b/compose/rest/handlers/page.go @@ -23,7 +23,7 @@ import ( "github.com/go-chi/chi" "github.com/titpetric/factory/resputil" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/rest/request" ) // Internal API interface @@ -218,13 +218,13 @@ func NewPage(ph PageAPI) *Page { func (ph *Page) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { r.Group(func(r chi.Router) { r.Use(middlewares...) - r.Get("/page/", ph.List) - r.Post("/page/", ph.Create) - r.Get("/page/{pageID}", ph.Read) - r.Get("/page/tree", ph.Tree) - r.Post("/page/{pageID}", ph.Update) - r.Post("/page/{selfID}/reorder", ph.Reorder) - r.Delete("/page/{pageID}", ph.Delete) - r.Post("/page/{pageID}/attachment", ph.Upload) + r.Get("/namespace/{namespaceID}/page/", ph.List) + r.Post("/namespace/{namespaceID}/page/", ph.Create) + r.Get("/namespace/{namespaceID}/page/{pageID}", ph.Read) + r.Get("/namespace/{namespaceID}/page/tree", ph.Tree) + r.Post("/namespace/{namespaceID}/page/{pageID}", ph.Update) + r.Post("/namespace/{namespaceID}/page/{selfID}/reorder", ph.Reorder) + r.Delete("/namespace/{namespaceID}/page/{pageID}", ph.Delete) + r.Post("/namespace/{namespaceID}/page/{pageID}/attachment", ph.Upload) }) } diff --git a/crm/rest/handlers/permissions.go b/compose/rest/handlers/permissions.go similarity index 97% rename from crm/rest/handlers/permissions.go rename to compose/rest/handlers/permissions.go index 09fa705f4..b6fd05a53 100644 --- a/crm/rest/handlers/permissions.go +++ b/compose/rest/handlers/permissions.go @@ -23,7 +23,7 @@ import ( "github.com/go-chi/chi" "github.com/titpetric/factory/resputil" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/rest/request" ) // Internal API interface diff --git a/crm/rest/handlers/record.go b/compose/rest/handlers/record.go similarity index 89% rename from crm/rest/handlers/record.go rename to compose/rest/handlers/record.go index e80b7b067..f63bbc621 100644 --- a/crm/rest/handlers/record.go +++ b/compose/rest/handlers/record.go @@ -23,7 +23,7 @@ import ( "github.com/go-chi/chi" "github.com/titpetric/factory/resputil" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/rest/request" ) // Internal API interface @@ -196,12 +196,12 @@ func NewRecord(rh RecordAPI) *Record { func (rh *Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { r.Group(func(r chi.Router) { r.Use(middlewares...) - r.Get("/module/{moduleID}/record/report", rh.Report) - r.Get("/module/{moduleID}/record/", rh.List) - r.Post("/module/{moduleID}/record/", rh.Create) - r.Get("/module/{moduleID}/record/{recordID}", rh.Read) - r.Post("/module/{moduleID}/record/{recordID}", rh.Update) - r.Delete("/module/{moduleID}/record/{recordID}", rh.Delete) - r.Post("/module/{moduleID}/record/{recordID}/{fieldName}/attachment", rh.Upload) + r.Get("/namespace/{namespaceID}/module/{moduleID}/record/report", rh.Report) + r.Get("/namespace/{namespaceID}/module/{moduleID}/record/", rh.List) + r.Post("/namespace/{namespaceID}/module/{moduleID}/record/", rh.Create) + r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", rh.Read) + r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", rh.Update) + r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", rh.Delete) + r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/{fieldName}/attachment", rh.Upload) }) } diff --git a/crm/rest/handlers/trigger.go b/compose/rest/handlers/trigger.go similarity index 91% rename from crm/rest/handlers/trigger.go rename to compose/rest/handlers/trigger.go index d5e6108ae..277565b54 100644 --- a/crm/rest/handlers/trigger.go +++ b/compose/rest/handlers/trigger.go @@ -23,7 +23,7 @@ import ( "github.com/go-chi/chi" "github.com/titpetric/factory/resputil" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/rest/request" ) // Internal API interface @@ -152,10 +152,10 @@ func NewTrigger(th TriggerAPI) *Trigger { func (th *Trigger) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { r.Group(func(r chi.Router) { r.Use(middlewares...) - r.Get("/trigger/", th.List) - r.Post("/trigger/", th.Create) - r.Get("/trigger/{triggerID}", th.Read) - r.Post("/trigger/{triggerID}", th.Update) - r.Delete("/trigger/{triggerID}", th.Delete) + r.Get("/namespace/{namespaceID}/trigger/", th.List) + r.Post("/namespace/{namespaceID}/trigger/", th.Create) + r.Get("/namespace/{namespaceID}/trigger/{triggerID}", th.Read) + r.Post("/namespace/{namespaceID}/trigger/{triggerID}", th.Update) + r.Delete("/namespace/{namespaceID}/trigger/{triggerID}", th.Delete) }) } diff --git a/crm/rest/middleware.go b/compose/rest/middleware.go similarity index 86% rename from crm/rest/middleware.go rename to compose/rest/middleware.go index 0277d619e..4ed310faa 100644 --- a/crm/rest/middleware.go +++ b/compose/rest/middleware.go @@ -3,7 +3,7 @@ package rest import ( "net/http" - "github.com/crusttech/crust/crm/internal/service" + "github.com/crusttech/crust/compose/internal/service" ) func middlewareAllowedAccess(next http.Handler) http.Handler { diff --git a/compose/rest/module.go b/compose/rest/module.go new file mode 100644 index 000000000..1d625a73c --- /dev/null +++ b/compose/rest/module.go @@ -0,0 +1,133 @@ +package rest + +import ( + "context" + + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/compose/internal/service" + "github.com/crusttech/crust/compose/rest/request" + "github.com/crusttech/crust/compose/types" +) + +type ( + modulePayload struct { + *types.Module + + CanUpdateModule bool `json:"canUpdateModule"` + CanDeleteModule bool `json:"canDeleteModule"` + CanCreateRecord bool `json:"canCreateRecord"` + CanReadRecord bool `json:"canReadRecord"` + CanUpdateRecord bool `json:"canUpdateRecord"` + CanDeleteRecord bool `json:"canDeleteRecord"` + } + + moduleSetPayload struct { + Filter types.ModuleFilter `json:"filter"` + Set []*modulePayload `json:"set"` + } + + Module struct { + module service.ModuleService + record service.RecordService + permissions service.PermissionsService + } +) + +func (Module) New() *Module { + return &Module{ + module: service.DefaultModule, + record: service.DefaultRecord, + permissions: service.DefaultPermissions, + } +} + +func (ctrl *Module) List(ctx context.Context, r *request.ModuleList) (interface{}, error) { + f := types.ModuleFilter{ + NamespaceID: r.NamespaceID, + Query: r.Query, + PerPage: r.PerPage, + Page: r.Page, + } + + set, filter, err := ctrl.module.With(ctx).Find(f) + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl *Module) Read(ctx context.Context, r *request.ModuleRead) (interface{}, error) { + mod, err := ctrl.module.With(ctx).FindByID(r.NamespaceID, r.ModuleID) + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl *Module) Create(ctx context.Context, r *request.ModuleCreate) (interface{}, error) { + var ( + err error + mod = &types.Module{ + NamespaceID: r.NamespaceID, + Name: r.Name, + Fields: r.Fields, + Meta: r.Meta, + } + ) + + mod, err = ctrl.module.With(ctx).Create(mod) + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl *Module) Update(ctx context.Context, r *request.ModuleUpdate) (interface{}, error) { + var ( + m = &types.Module{} + err error + ) + + m.ID = r.ModuleID + m.Name = r.Name + m.Meta = r.Meta + m.NamespaceID = r.NamespaceID + m.UpdatedAt = r.UpdatedAt + + m, err = ctrl.module.With(ctx).Update(m) + return ctrl.makePayload(ctx, m, err) +} + +func (ctrl *Module) Delete(ctx context.Context, r *request.ModuleDelete) (interface{}, error) { + _, err := ctrl.module.With(ctx).FindByID(r.NamespaceID, r.ModuleID) + if err != nil { + return nil, err + } + + return resputil.OK(), ctrl.module.With(ctx).DeleteByID(r.NamespaceID, r.ModuleID) +} + +func (ctrl Module) makePayload(ctx context.Context, m *types.Module, err error) (*modulePayload, error) { + if err != nil || m == nil { + return nil, err + } + + perm := ctrl.permissions.With(ctx) + + return &modulePayload{ + Module: m, + + CanUpdateModule: perm.CanUpdateModule(m), + CanDeleteModule: perm.CanDeleteModule(m), + CanCreateRecord: perm.CanCreateRecord(m), + CanReadRecord: perm.CanReadRecord(m), + CanUpdateRecord: perm.CanUpdateRecord(m), + CanDeleteRecord: perm.CanDeleteRecord(m), + }, nil +} + +func (ctrl Module) makeFilterPayload(ctx context.Context, nn types.ModuleSet, f types.ModuleFilter, err error) (*moduleSetPayload, error) { + if err != nil { + return nil, err + } + + msp := &moduleSetPayload{Filter: f, Set: make([]*modulePayload, len(nn))} + + for i := range nn { + msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return msp, nil +} diff --git a/compose/rest/namespace.go b/compose/rest/namespace.go new file mode 100644 index 000000000..b1767638d --- /dev/null +++ b/compose/rest/namespace.go @@ -0,0 +1,129 @@ +package rest + +import ( + "context" + + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/compose/internal/service" + "github.com/crusttech/crust/compose/rest/request" + "github.com/crusttech/crust/compose/types" +) + +type ( + namespacePayload struct { + *types.Namespace + + CanUpdateNamespace bool `json:"canUpdateNamespace"` + CanDeleteNamespace bool `json:"canDeleteNamespace"` + CanCreateModule bool `json:"canCreateModule"` + CanCreateChart bool `json:"canCreateChart"` + CanCreateTrigger bool `json:"canCreateTrigger"` + CanCreatePage bool `json:"canCreatePage"` + } + + namespaceSetPayload struct { + Filter types.NamespaceFilter `json:"filter"` + Set []*namespacePayload `json:"set"` + } +) + +type Namespace struct { + namespace service.NamespaceService + permissions service.PermissionsService +} + +func (Namespace) New() *Namespace { + return &Namespace{ + namespace: service.DefaultNamespace, + permissions: service.DefaultPermissions, + } +} + +func (ctrl Namespace) List(ctx context.Context, r *request.NamespaceList) (interface{}, error) { + f := types.NamespaceFilter{ + Query: r.Query, + PerPage: r.PerPage, + Page: r.Page, + } + + 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) { + var err error + ns := &types.Namespace{ + Name: r.Name, + Slug: r.Slug, + Meta: r.Meta, + Enabled: r.Enabled, + } + + ns, err = ctrl.namespace.With(ctx).Create(ns) + return ctrl.makePayload(ctx, ns, err) +} + +func (ctrl Namespace) Read(ctx context.Context, r *request.NamespaceRead) (interface{}, error) { + ns, err := ctrl.namespace.With(ctx).FindByID(r.NamespaceID) + return ctrl.makePayload(ctx, ns, err) +} + +func (ctrl Namespace) Update(ctx context.Context, r *request.NamespaceUpdate) (interface{}, error) { + var ( + ns = &types.Namespace{} + err error + ) + + ns.ID = r.NamespaceID + ns.Name = r.Name + ns.Slug = r.Slug + ns.Meta = r.Meta + ns.Enabled = r.Enabled + ns.UpdatedAt = r.UpdatedAt + + ns, err = ctrl.namespace.With(ctx).Update(ns) + return ctrl.makePayload(ctx, ns, err) +} + +func (ctrl Namespace) Delete(ctx context.Context, r *request.NamespaceDelete) (interface{}, error) { + _, err := ctrl.namespace.With(ctx).FindByID(r.NamespaceID) + if err != nil { + return nil, err + } + + 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 || ns == nil { + return nil, err + } + + perm := ctrl.permissions.With(ctx) + + return &namespacePayload{ + Namespace: ns, + + CanUpdateNamespace: perm.CanUpdateNamespace(ns), + CanDeleteNamespace: perm.CanDeleteNamespace(ns), + CanCreateModule: perm.CanCreateModule(ns), + CanCreateChart: perm.CanCreateChart(ns), + CanCreateTrigger: perm.CanCreateTrigger(ns), + CanCreatePage: perm.CanCreatePage(ns), + }, nil +} + +func (ctrl Namespace) makeFilterPayload(ctx context.Context, nn types.NamespaceSet, f types.NamespaceFilter, err error) (*namespaceSetPayload, error) { + if err != nil { + return nil, err + } + + nsp := &namespaceSetPayload{Filter: f, Set: make([]*namespacePayload, len(nn))} + + for i := range nn { + nsp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return nsp, nil +} diff --git a/crm/rest/notification.go b/compose/rest/notification.go similarity index 92% rename from crm/rest/notification.go rename to compose/rest/notification.go index 7d316e5e5..52a9f706c 100644 --- a/crm/rest/notification.go +++ b/compose/rest/notification.go @@ -3,8 +3,8 @@ package rest import ( "context" - "github.com/crusttech/crust/crm/internal/service" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/internal/service" + "github.com/crusttech/crust/compose/rest/request" "github.com/crusttech/crust/internal/mail" "github.com/pkg/errors" diff --git a/compose/rest/page.go b/compose/rest/page.go new file mode 100644 index 000000000..746646352 --- /dev/null +++ b/compose/rest/page.go @@ -0,0 +1,156 @@ +package rest + +import ( + "context" + + "github.com/titpetric/factory/resputil" + + "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/payload" +) + +type ( + pagePayload struct { + *types.Page + + CanUpdatePage bool `json:"canUpdatePage"` + CanDeletePage bool `json:"canDeletePage"` + } + + pageSetPayload struct { + Filter types.PageFilter `json:"filter"` + Set []*pagePayload `json:"set"` + } + + Page struct { + page service.PageService + attachment service.AttachmentService + permissions service.PermissionsService + } +) + +func (Page) New() *Page { + return &Page{ + page: service.DefaultPage, + attachment: service.DefaultAttachment, + permissions: service.DefaultPermissions, + } +} + +func (ctrl *Page) List(ctx context.Context, r *request.PageList) (interface{}, error) { + f := types.PageFilter{ + NamespaceID: r.NamespaceID, + ParentID: r.SelfID, + + Query: r.Query, + PerPage: r.PerPage, + Page: r.Page, + } + + set, filter, err := ctrl.page.With(ctx).Find(f) + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl *Page) Tree(ctx context.Context, r *request.PageTree) (interface{}, error) { + return ctrl.page.With(ctx).Tree(r.NamespaceID) +} + +func (ctrl *Page) Create(ctx context.Context, r *request.PageCreate) (interface{}, error) { + var ( + err error + mod = &types.Page{ + NamespaceID: r.NamespaceID, + SelfID: r.SelfID, + ModuleID: r.ModuleID, + Title: r.Title, + Description: r.Description, + Blocks: r.Blocks, + Visible: r.Visible, + } + ) + + mod, err = ctrl.page.With(ctx).Create(mod) + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl *Page) Read(ctx context.Context, r *request.PageRead) (interface{}, error) { + mod, err := ctrl.page.With(ctx).FindByID(r.NamespaceID, r.PageID) + return ctrl.makePayload(ctx, mod, err) + +} + +func (ctrl *Page) Reorder(ctx context.Context, r *request.PageReorder) (interface{}, error) { + return resputil.OK(), ctrl.page.With(ctx).Reorder(r.NamespaceID, r.SelfID, payload.ParseUInt64s(r.PageIDs)) +} + +func (ctrl *Page) Update(ctx context.Context, r *request.PageUpdate) (interface{}, error) { + var ( + err error + mod = &types.Page{ + ID: r.PageID, + SelfID: r.SelfID, + ModuleID: r.ModuleID, + Title: r.Title, + Description: r.Description, + Blocks: r.Blocks, + Visible: r.Visible, + } + ) + + mod, err = ctrl.page.With(ctx).Update(mod) + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl *Page) Delete(ctx context.Context, r *request.PageDelete) (interface{}, error) { + return resputil.OK(), ctrl.page.With(ctx).DeleteByID(r.NamespaceID, r.PageID) +} + +func (ctrl *Page) Upload(ctx context.Context, r *request.PageUpload) (interface{}, error) { + file, err := r.Upload.Open() + if err != nil { + return nil, err + } + + defer file.Close() + + a, err := ctrl.attachment.With(ctx).CreatePageAttachment( + r.NamespaceID, + r.Upload.Filename, + r.Upload.Size, + file, + r.PageID, + ) + + return makeAttachmentPayload(ctx, a, err) +} + +func (ctrl Page) makePayload(ctx context.Context, c *types.Page, err error) (*pagePayload, error) { + if err != nil || c == nil { + return nil, err + } + + perm := ctrl.permissions.With(ctx) + + return &pagePayload{ + Page: c, + + CanUpdatePage: perm.CanUpdatePage(c), + CanDeletePage: perm.CanDeletePage(c), + }, nil +} + +func (ctrl Page) makeFilterPayload(ctx context.Context, nn types.PageSet, f types.PageFilter, err error) (*pageSetPayload, error) { + if err != nil { + return nil, err + } + + modp := &pageSetPayload{Filter: f, Set: make([]*pagePayload, len(nn))} + + for i := range nn { + modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return modp, nil +} diff --git a/crm/rest/permissions.go b/compose/rest/permissions.go similarity index 80% rename from crm/rest/permissions.go rename to compose/rest/permissions.go index a7f8680b4..7d5b3556d 100644 --- a/crm/rest/permissions.go +++ b/compose/rest/permissions.go @@ -3,8 +3,8 @@ package rest import ( "context" - "github.com/crusttech/crust/crm/internal/service" - "github.com/crusttech/crust/crm/rest/request" + "github.com/crusttech/crust/compose/internal/service" + "github.com/crusttech/crust/compose/rest/request" "github.com/pkg/errors" ) diff --git a/compose/rest/record.go b/compose/rest/record.go new file mode 100644 index 000000000..3d0598367 --- /dev/null +++ b/compose/rest/record.go @@ -0,0 +1,140 @@ +package rest + +import ( + "context" + + "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" +) + +var _ = errors.Wrap + +type ( + recordPayload struct { + *types.Record + + CanUpdateRecord bool `json:"canUpdateRecord"` + CanDeleteRecord bool `json:"canDeleteRecord"` + } + + recordSetPayload struct { + Filter types.RecordFilter `json:"filter"` + Set []*recordPayload `json:"set"` + } + + Record struct { + record service.RecordService + attachment service.AttachmentService + permissions service.PermissionsService + } +) + +func (Record) New() *Record { + return &Record{ + record: service.DefaultRecord, + attachment: service.DefaultAttachment, + permissions: service.DefaultPermissions, + } +} + +func (ctrl *Record) Report(ctx context.Context, r *request.RecordReport) (interface{}, error) { + return ctrl.record.With(ctx).Report(r.NamespaceID, r.ModuleID, r.Metrics, r.Dimensions, r.Filter) +} + +func (ctrl *Record) List(ctx context.Context, r *request.RecordList) (interface{}, error) { + set, filter, err := ctrl.record.With(ctx).Find(types.RecordFilter{ + NamespaceID: r.NamespaceID, + ModuleID: r.ModuleID, + Filter: r.Filter, + Sort: r.Sort, + PerPage: r.PerPage, + Page: r.Page, + }) + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl *Record) Read(ctx context.Context, r *request.RecordRead) (interface{}, error) { + mod, err := ctrl.record.With(ctx).FindByID(r.NamespaceID, r.RecordID) + + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl *Record) Create(ctx context.Context, r *request.RecordCreate) (interface{}, error) { + mod, err := ctrl.record.With(ctx).Create(&types.Record{ + NamespaceID: r.NamespaceID, + ModuleID: r.ModuleID, + Values: r.Values, + }) + + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl *Record) Update(ctx context.Context, r *request.RecordUpdate) (interface{}, error) { + mod, err := ctrl.record.With(ctx).Update(&types.Record{ + ID: r.RecordID, + NamespaceID: r.NamespaceID, + ModuleID: r.ModuleID, + Values: r.Values, + }) + + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl *Record) Delete(ctx context.Context, r *request.RecordDelete) (interface{}, error) { + return resputil.OK(), ctrl.record.With(ctx).DeleteByID(r.NamespaceID, r.RecordID) +} + +func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interface{}, error) { + file, err := r.Upload.Open() + if err != nil { + return nil, err + } + + defer file.Close() + + a, err := ctrl.attachment.With(ctx).CreateRecordAttachment( + r.NamespaceID, + r.Upload.Filename, + r.Upload.Size, + file, + r.ModuleID, + r.RecordID, + r.FieldName, + ) + + return makeAttachmentPayload(ctx, a, err) +} + +func (ctrl Record) makePayload(ctx context.Context, c *types.Record, err error) (*recordPayload, error) { + if err != nil || c == nil { + return nil, err + } + + perm := ctrl.permissions.With(ctx) + + return &recordPayload{ + Record: c, + + CanUpdateRecord: perm.CanUpdateRecord(c), + CanDeleteRecord: perm.CanDeleteRecord(c), + }, nil +} + +func (ctrl Record) makeFilterPayload(ctx context.Context, nn types.RecordSet, f types.RecordFilter, err error) (*recordSetPayload, error) { + if err != nil { + return nil, err + } + + modp := &recordSetPayload{Filter: f, Set: make([]*recordPayload, len(nn))} + + for i := range nn { + modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return modp, nil +} diff --git a/crm/rest/request/attachment.go b/compose/rest/request/attachment.go similarity index 72% rename from crm/rest/request/attachment.go rename to compose/rest/request/attachment.go index a10c63174..fe361837e 100644 --- a/crm/rest/request/attachment.go +++ b/compose/rest/request/attachment.go @@ -32,15 +32,16 @@ var _ = multipart.FileHeader{} // Attachment list request parameters type AttachmentList struct { - PageID uint64 `json:",string"` - ModuleID uint64 `json:",string"` - RecordID uint64 `json:",string"` - FieldName string - Page uint - PerPage uint - Sign string - UserID uint64 `json:",string"` - Kind string + PageID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + RecordID uint64 `json:",string"` + FieldName string + Page uint + PerPage uint + Sign string + UserID uint64 `json:",string"` + Kind string + NamespaceID uint64 `json:",string"` } func NewAttachmentList() *AttachmentList { @@ -107,25 +108,27 @@ func (aReq *AttachmentList) Fill(r *http.Request) (err error) { aReq.UserID = parseUInt64(val) } aReq.Kind = chi.URLParam(r, "kind") + aReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } 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"` Sign string 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) @@ -154,6 +157,7 @@ func (aReq *AttachmentDetails) Fill(r *http.Request) (err error) { 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 @@ -166,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 { @@ -176,6 +237,7 @@ type AttachmentOriginal struct { AttachmentID uint64 `json:",string"` Name string Kind string + NamespaceID uint64 `json:",string"` } func NewAttachmentOriginal() *AttachmentOriginal { @@ -224,6 +286,7 @@ func (aReq *AttachmentOriginal) Fill(r *http.Request) (err error) { aReq.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID")) aReq.Name = chi.URLParam(r, "name") aReq.Kind = chi.URLParam(r, "kind") + aReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -235,6 +298,7 @@ type AttachmentPreview struct { AttachmentID uint64 `json:",string"` Ext string Kind string + NamespaceID uint64 `json:",string"` Sign string UserID uint64 `json:",string"` } @@ -273,6 +337,7 @@ func (aReq *AttachmentPreview) Fill(r *http.Request) (err error) { aReq.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID")) aReq.Ext = chi.URLParam(r, "ext") aReq.Kind = chi.URLParam(r, "kind") + aReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) if val, ok := get["sign"]; ok { aReq.Sign = val diff --git a/crm/rest/request/chart.go b/compose/rest/request/chart.go similarity index 82% rename from crm/rest/request/chart.go rename to compose/rest/request/chart.go index 1437af312..6002c4dcf 100644 --- a/crm/rest/request/chart.go +++ b/compose/rest/request/chart.go @@ -27,6 +27,7 @@ import ( "github.com/pkg/errors" sqlxTypes "github.com/jmoiron/sqlx/types" + "time" ) var _ = chi.URLParam @@ -34,6 +35,10 @@ var _ = multipart.FileHeader{} // Chart list request parameters type ChartList struct { + Query string + Page uint + PerPage uint + NamespaceID uint64 `json:",string"` } func NewChartList() *ChartList { @@ -67,6 +72,20 @@ func (cReq *ChartList) Fill(r *http.Request) (err error) { post[name] = string(param[0]) } + if val, ok := get["query"]; ok { + + cReq.Query = val + } + if val, ok := get["page"]; ok { + + cReq.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + + cReq.PerPage = parseUint(val) + } + cReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) + return err } @@ -74,8 +93,9 @@ var _ RequestFiller = NewChartList() // Chart create request parameters type ChartCreate struct { - Config sqlxTypes.JSONText - Name string + Config sqlxTypes.JSONText + Name string + NamespaceID uint64 `json:",string"` } func NewChartCreate() *ChartCreate { @@ -119,6 +139,7 @@ func (cReq *ChartCreate) Fill(r *http.Request) (err error) { cReq.Name = val } + cReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -127,7 +148,8 @@ var _ RequestFiller = NewChartCreate() // Chart read request parameters type ChartRead struct { - ChartID uint64 `json:",string"` + ChartID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` } func NewChartRead() *ChartRead { @@ -162,6 +184,7 @@ func (cReq *ChartRead) Fill(r *http.Request) (err error) { } cReq.ChartID = parseUInt64(chi.URLParam(r, "chartID")) + cReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -170,9 +193,11 @@ var _ RequestFiller = NewChartRead() // Chart update request parameters type ChartUpdate struct { - ChartID uint64 `json:",string"` - Config sqlxTypes.JSONText - Name string + ChartID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + Config sqlxTypes.JSONText + Name string + UpdatedAt *time.Time } func NewChartUpdate() *ChartUpdate { @@ -207,6 +232,7 @@ func (cReq *ChartUpdate) Fill(r *http.Request) (err error) { } cReq.ChartID = parseUInt64(chi.URLParam(r, "chartID")) + cReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) if val, ok := post["config"]; ok { if cReq.Config, err = parseJSONTextWithErr(val); err != nil { @@ -217,6 +243,12 @@ func (cReq *ChartUpdate) Fill(r *http.Request) (err error) { cReq.Name = val } + if val, ok := post["updatedAt"]; ok { + + if cReq.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil { + return err + } + } return err } @@ -225,7 +257,8 @@ var _ RequestFiller = NewChartUpdate() // Chart delete request parameters type ChartDelete struct { - ChartID uint64 `json:",string"` + ChartID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` } func NewChartDelete() *ChartDelete { @@ -260,6 +293,7 @@ func (cReq *ChartDelete) Fill(r *http.Request) (err error) { } cReq.ChartID = parseUInt64(chi.URLParam(r, "chartID")) + cReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } diff --git a/crm/rest/request/misc.go b/compose/rest/request/misc.go similarity index 100% rename from crm/rest/request/misc.go rename to compose/rest/request/misc.go diff --git a/crm/rest/request/module.go b/compose/rest/request/module.go similarity index 81% rename from crm/rest/request/module.go rename to compose/rest/request/module.go index 28cdd1e3c..fe51fa52b 100644 --- a/crm/rest/request/module.go +++ b/compose/rest/request/module.go @@ -26,8 +26,9 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" sqlxTypes "github.com/jmoiron/sqlx/types" + "time" ) var _ = chi.URLParam @@ -35,7 +36,10 @@ var _ = multipart.FileHeader{} // Module list request parameters type ModuleList struct { - Query string + Query string + Page uint + PerPage uint + NamespaceID uint64 `json:",string"` } func NewModuleList() *ModuleList { @@ -73,6 +77,15 @@ func (mReq *ModuleList) Fill(r *http.Request) (err error) { mReq.Query = val } + if val, ok := get["page"]; ok { + + mReq.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + + mReq.PerPage = parseUint(val) + } + mReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -81,9 +94,10 @@ var _ RequestFiller = NewModuleList() // Module create request parameters type ModuleCreate struct { - Name string - Fields types.ModuleFieldSet - Meta sqlxTypes.JSONText + Name string + Fields types.ModuleFieldSet + Meta sqlxTypes.JSONText + NamespaceID uint64 `json:",string"` } func NewModuleCreate() *ModuleCreate { @@ -127,6 +141,7 @@ func (mReq *ModuleCreate) Fill(r *http.Request) (err error) { return err } } + mReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -135,7 +150,8 @@ var _ RequestFiller = NewModuleCreate() // Module read request parameters type ModuleRead struct { - ModuleID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` } func NewModuleRead() *ModuleRead { @@ -170,6 +186,7 @@ func (mReq *ModuleRead) Fill(r *http.Request) (err error) { } mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) + mReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -178,10 +195,12 @@ var _ RequestFiller = NewModuleRead() // Module update request parameters type ModuleUpdate struct { - ModuleID uint64 `json:",string"` - Name string - Fields types.ModuleFieldSet - Meta sqlxTypes.JSONText + ModuleID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + Name string + Fields types.ModuleFieldSet + Meta sqlxTypes.JSONText + UpdatedAt *time.Time } func NewModuleUpdate() *ModuleUpdate { @@ -216,6 +235,7 @@ func (mReq *ModuleUpdate) Fill(r *http.Request) (err error) { } mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) + mReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) if val, ok := post["name"]; ok { mReq.Name = val @@ -226,6 +246,12 @@ func (mReq *ModuleUpdate) Fill(r *http.Request) (err error) { return err } } + if val, ok := post["updatedAt"]; ok { + + if mReq.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil { + return err + } + } return err } @@ -234,7 +260,8 @@ var _ RequestFiller = NewModuleUpdate() // Module delete request parameters type ModuleDelete struct { - ModuleID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` } func NewModuleDelete() *ModuleDelete { @@ -269,6 +296,7 @@ func (mReq *ModuleDelete) Fill(r *http.Request) (err error) { } mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) + mReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } diff --git a/compose/rest/request/namespace.go b/compose/rest/request/namespace.go new file mode 100644 index 000000000..125f6d337 --- /dev/null +++ b/compose/rest/request/namespace.go @@ -0,0 +1,311 @@ +package request + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `namespace.go`, `namespace.util.go` or `namespace_test.go` to + implement your API calls, helper functions and tests. The file `namespace.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "io" + "strings" + + "encoding/json" + "mime/multipart" + "net/http" + + "github.com/go-chi/chi" + "github.com/pkg/errors" + + sqlxTypes "github.com/jmoiron/sqlx/types" + "time" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// Namespace list request parameters +type NamespaceList struct { + Query string + Page uint + PerPage uint +} + +func NewNamespaceList() *NamespaceList { + return &NamespaceList{} +} + +func (nReq *NamespaceList) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(nReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["query"]; ok { + + nReq.Query = val + } + if val, ok := get["page"]; ok { + + nReq.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + + nReq.PerPage = parseUint(val) + } + + return err +} + +var _ RequestFiller = NewNamespaceList() + +// Namespace create request parameters +type NamespaceCreate struct { + Name string + Slug string + Enabled bool + Meta sqlxTypes.JSONText +} + +func NewNamespaceCreate() *NamespaceCreate { + return &NamespaceCreate{} +} + +func (nReq *NamespaceCreate) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(nReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["name"]; ok { + + nReq.Name = val + } + if val, ok := post["slug"]; ok { + + nReq.Slug = val + } + if val, ok := post["enabled"]; ok { + + nReq.Enabled = parseBool(val) + } + if val, ok := post["meta"]; ok { + + if nReq.Meta, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + + return err +} + +var _ RequestFiller = NewNamespaceCreate() + +// Namespace read request parameters +type NamespaceRead struct { + NamespaceID uint64 `json:",string"` +} + +func NewNamespaceRead() *NamespaceRead { + return &NamespaceRead{} +} + +func (nReq *NamespaceRead) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(nReq) + + 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]) + } + + nReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) + + return err +} + +var _ RequestFiller = NewNamespaceRead() + +// Namespace update request parameters +type NamespaceUpdate struct { + NamespaceID uint64 `json:",string"` + Name string + Slug string + Enabled bool + Meta sqlxTypes.JSONText + UpdatedAt *time.Time +} + +func NewNamespaceUpdate() *NamespaceUpdate { + return &NamespaceUpdate{} +} + +func (nReq *NamespaceUpdate) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(nReq) + + 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]) + } + + nReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) + if val, ok := post["name"]; ok { + + nReq.Name = val + } + if val, ok := post["slug"]; ok { + + nReq.Slug = val + } + if val, ok := post["enabled"]; ok { + + nReq.Enabled = parseBool(val) + } + if val, ok := post["meta"]; ok { + + if nReq.Meta, err = parseJSONTextWithErr(val); err != nil { + return err + } + } + if val, ok := post["updatedAt"]; ok { + + if nReq.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil { + return err + } + } + + return err +} + +var _ RequestFiller = NewNamespaceUpdate() + +// Namespace delete request parameters +type NamespaceDelete struct { + NamespaceID uint64 `json:",string"` +} + +func NewNamespaceDelete() *NamespaceDelete { + return &NamespaceDelete{} +} + +func (nReq *NamespaceDelete) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(nReq) + + 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]) + } + + nReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) + + return err +} + +var _ RequestFiller = NewNamespaceDelete() diff --git a/crm/rest/request/notification.go b/compose/rest/request/notification.go similarity index 100% rename from crm/rest/request/notification.go rename to compose/rest/request/notification.go diff --git a/crm/rest/request/page.go b/compose/rest/request/page.go similarity index 87% rename from crm/rest/request/page.go rename to compose/rest/request/page.go index 7d0727fb0..b9b859e28 100644 --- a/crm/rest/request/page.go +++ b/compose/rest/request/page.go @@ -34,7 +34,11 @@ var _ = multipart.FileHeader{} // Page list request parameters type PageList struct { - SelfID uint64 `json:",string"` + SelfID uint64 `json:",string"` + Query string + Page uint + PerPage uint + NamespaceID uint64 `json:",string"` } func NewPageList() *PageList { @@ -72,6 +76,19 @@ func (pReq *PageList) Fill(r *http.Request) (err error) { pReq.SelfID = parseUInt64(val) } + if val, ok := get["query"]; ok { + + pReq.Query = val + } + if val, ok := get["page"]; ok { + + pReq.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + + pReq.PerPage = parseUint(val) + } + pReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -86,6 +103,7 @@ type PageCreate struct { Description string Visible bool Blocks sqlxTypes.JSONText + NamespaceID uint64 `json:",string"` } func NewPageCreate() *PageCreate { @@ -145,6 +163,7 @@ func (pReq *PageCreate) Fill(r *http.Request) (err error) { return err } } + pReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -153,7 +172,8 @@ var _ RequestFiller = NewPageCreate() // Page read request parameters type PageRead struct { - PageID uint64 `json:",string"` + PageID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` } func NewPageRead() *PageRead { @@ -188,6 +208,7 @@ func (pReq *PageRead) Fill(r *http.Request) (err error) { } pReq.PageID = parseUInt64(chi.URLParam(r, "pageID")) + pReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -196,6 +217,7 @@ var _ RequestFiller = NewPageRead() // Page tree request parameters type PageTree struct { + NamespaceID uint64 `json:",string"` } func NewPageTree() *PageTree { @@ -229,6 +251,8 @@ func (pReq *PageTree) Fill(r *http.Request) (err error) { post[name] = string(param[0]) } + pReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) + return err } @@ -237,6 +261,7 @@ var _ RequestFiller = NewPageTree() // Page update request parameters type PageUpdate struct { PageID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` SelfID uint64 `json:",string"` ModuleID uint64 `json:",string"` Title string @@ -277,6 +302,7 @@ func (pReq *PageUpdate) Fill(r *http.Request) (err error) { } pReq.PageID = parseUInt64(chi.URLParam(r, "pageID")) + pReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) if val, ok := post["selfID"]; ok { pReq.SelfID = parseUInt64(val) @@ -311,8 +337,9 @@ var _ RequestFiller = NewPageUpdate() // Page reorder request parameters type PageReorder struct { - SelfID uint64 `json:",string"` - PageIDs []string + SelfID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + PageIDs []string } func NewPageReorder() *PageReorder { @@ -347,6 +374,7 @@ func (pReq *PageReorder) Fill(r *http.Request) (err error) { } pReq.SelfID = parseUInt64(chi.URLParam(r, "selfID")) + pReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -355,7 +383,8 @@ var _ RequestFiller = NewPageReorder() // Page delete request parameters type PageDelete struct { - PageID uint64 `json:",string"` + PageID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` } func NewPageDelete() *PageDelete { @@ -390,6 +419,7 @@ func (pReq *PageDelete) Fill(r *http.Request) (err error) { } pReq.PageID = parseUInt64(chi.URLParam(r, "pageID")) + pReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -398,8 +428,9 @@ var _ RequestFiller = NewPageDelete() // Page upload request parameters type PageUpload struct { - PageID uint64 `json:",string"` - Upload *multipart.FileHeader + PageID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + Upload *multipart.FileHeader } func NewPageUpload() *PageUpload { @@ -434,6 +465,7 @@ func (pReq *PageUpload) Fill(r *http.Request) (err error) { } pReq.PageID = parseUInt64(chi.URLParam(r, "pageID")) + pReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) if _, pReq.Upload, err = r.FormFile("upload"); err != nil { return errors.Wrap(err, "error procesing uploaded file") } diff --git a/crm/rest/request/permissions.go b/compose/rest/request/permissions.go similarity index 100% rename from crm/rest/request/permissions.go rename to compose/rest/request/permissions.go diff --git a/crm/rest/request/record.go b/compose/rest/request/record.go similarity index 83% rename from crm/rest/request/record.go rename to compose/rest/request/record.go index 1cf3164db..be2ec3b9e 100644 --- a/crm/rest/request/record.go +++ b/compose/rest/request/record.go @@ -26,7 +26,7 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" - "github.com/crusttech/crust/crm/types" + "github.com/crusttech/crust/compose/types" ) var _ = chi.URLParam @@ -34,10 +34,11 @@ var _ = multipart.FileHeader{} // Record report request parameters type RecordReport struct { - Metrics string - Dimensions string - Filter string - ModuleID uint64 `json:",string"` + Metrics string + Dimensions string + Filter string + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` } func NewRecordReport() *RecordReport { @@ -83,6 +84,7 @@ func (rReq *RecordReport) Fill(r *http.Request) (err error) { rReq.Filter = val } + rReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) return err @@ -92,11 +94,12 @@ var _ RequestFiller = NewRecordReport() // Record list request parameters type RecordList struct { - Filter string - Page int - PerPage int - Sort string - ModuleID uint64 `json:",string"` + Filter string + Page uint + PerPage uint + Sort string + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` } func NewRecordList() *RecordList { @@ -136,16 +139,17 @@ func (rReq *RecordList) Fill(r *http.Request) (err error) { } if val, ok := get["page"]; ok { - rReq.Page = parseInt(val) + rReq.Page = parseUint(val) } if val, ok := get["perPage"]; ok { - rReq.PerPage = parseInt(val) + rReq.PerPage = parseUint(val) } if val, ok := get["sort"]; ok { rReq.Sort = val } + rReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) return err @@ -155,8 +159,9 @@ var _ RequestFiller = NewRecordList() // Record create request parameters type RecordCreate struct { - Values types.RecordValueSet - ModuleID uint64 `json:",string"` + Values types.RecordValueSet + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` } func NewRecordCreate() *RecordCreate { @@ -190,6 +195,7 @@ func (rReq *RecordCreate) Fill(r *http.Request) (err error) { post[name] = string(param[0]) } + rReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) return err @@ -199,8 +205,9 @@ var _ RequestFiller = NewRecordCreate() // Record read request parameters type RecordRead struct { - RecordID uint64 `json:",string"` - ModuleID uint64 `json:",string"` + RecordID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` } func NewRecordRead() *RecordRead { @@ -235,6 +242,7 @@ func (rReq *RecordRead) Fill(r *http.Request) (err error) { } rReq.RecordID = parseUInt64(chi.URLParam(r, "recordID")) + rReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) return err @@ -244,9 +252,10 @@ var _ RequestFiller = NewRecordRead() // Record update request parameters type RecordUpdate struct { - RecordID uint64 `json:",string"` - ModuleID uint64 `json:",string"` - Values types.RecordValueSet + RecordID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + Values types.RecordValueSet } func NewRecordUpdate() *RecordUpdate { @@ -281,6 +290,7 @@ func (rReq *RecordUpdate) Fill(r *http.Request) (err error) { } rReq.RecordID = parseUInt64(chi.URLParam(r, "recordID")) + rReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) return err @@ -290,8 +300,9 @@ var _ RequestFiller = NewRecordUpdate() // Record delete request parameters type RecordDelete struct { - RecordID uint64 `json:",string"` - ModuleID uint64 `json:",string"` + RecordID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` } func NewRecordDelete() *RecordDelete { @@ -326,6 +337,7 @@ func (rReq *RecordDelete) Fill(r *http.Request) (err error) { } rReq.RecordID = parseUInt64(chi.URLParam(r, "recordID")) + rReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) return err @@ -335,10 +347,11 @@ var _ RequestFiller = NewRecordDelete() // Record upload request parameters type RecordUpload struct { - RecordID uint64 `json:",string"` - FieldName string - ModuleID uint64 `json:",string"` - Upload *multipart.FileHeader + RecordID uint64 `json:",string"` + FieldName string + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + Upload *multipart.FileHeader } func NewRecordUpload() *RecordUpload { @@ -374,6 +387,7 @@ func (rReq *RecordUpload) Fill(r *http.Request) (err error) { rReq.RecordID = parseUInt64(chi.URLParam(r, "recordID")) rReq.FieldName = chi.URLParam(r, "fieldName") + rReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) if _, rReq.Upload, err = r.FormFile("upload"); err != nil { return errors.Wrap(err, "error procesing uploaded file") diff --git a/crm/rest/request/trigger.go b/compose/rest/request/trigger.go similarity index 81% rename from crm/rest/request/trigger.go rename to compose/rest/request/trigger.go index eec77f593..49fc43252 100644 --- a/crm/rest/request/trigger.go +++ b/compose/rest/request/trigger.go @@ -25,6 +25,8 @@ import ( "github.com/go-chi/chi" "github.com/pkg/errors" + + "time" ) var _ = chi.URLParam @@ -32,7 +34,11 @@ var _ = multipart.FileHeader{} // Trigger list request parameters type TriggerList struct { - ModuleID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + Query string + Page uint + PerPage uint + NamespaceID uint64 `json:",string"` } func NewTriggerList() *TriggerList { @@ -70,6 +76,19 @@ func (tReq *TriggerList) Fill(r *http.Request) (err error) { tReq.ModuleID = parseUInt64(val) } + if val, ok := get["query"]; ok { + + tReq.Query = val + } + if val, ok := get["page"]; ok { + + tReq.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + + tReq.PerPage = parseUint(val) + } + tReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -78,11 +97,13 @@ var _ RequestFiller = NewTriggerList() // Trigger create request parameters type TriggerCreate struct { - ModuleID uint64 `json:",string"` - Name string - Actions []string - Enabled bool - Source string + ModuleID uint64 `json:",string"` + Name string + Actions []string + Enabled bool + Source string + UpdatedAt *time.Time + NamespaceID uint64 `json:",string"` } func NewTriggerCreate() *TriggerCreate { @@ -132,6 +153,13 @@ func (tReq *TriggerCreate) Fill(r *http.Request) (err error) { tReq.Source = val } + if val, ok := post["updatedAt"]; ok { + + if tReq.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil { + return err + } + } + tReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -140,7 +168,8 @@ var _ RequestFiller = NewTriggerCreate() // Trigger read request parameters type TriggerRead struct { - TriggerID uint64 `json:",string"` + TriggerID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` } func NewTriggerRead() *TriggerRead { @@ -175,6 +204,7 @@ func (tReq *TriggerRead) Fill(r *http.Request) (err error) { } tReq.TriggerID = parseUInt64(chi.URLParam(r, "triggerID")) + tReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } @@ -183,12 +213,13 @@ var _ RequestFiller = NewTriggerRead() // Trigger update request parameters type TriggerUpdate struct { - TriggerID uint64 `json:",string"` - ModuleID uint64 `json:",string"` - Name string - Actions []string - Enabled bool - Source string + TriggerID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` + ModuleID uint64 `json:",string"` + Name string + Actions []string + Enabled bool + Source string } func NewTriggerUpdate() *TriggerUpdate { @@ -223,6 +254,7 @@ func (tReq *TriggerUpdate) Fill(r *http.Request) (err error) { } tReq.TriggerID = parseUInt64(chi.URLParam(r, "triggerID")) + tReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) if val, ok := post["moduleID"]; ok { tReq.ModuleID = parseUInt64(val) @@ -247,7 +279,8 @@ var _ RequestFiller = NewTriggerUpdate() // Trigger delete request parameters type TriggerDelete struct { - TriggerID uint64 `json:",string"` + TriggerID uint64 `json:",string"` + NamespaceID uint64 `json:",string"` } func NewTriggerDelete() *TriggerDelete { @@ -282,6 +315,7 @@ func (tReq *TriggerDelete) Fill(r *http.Request) (err error) { } tReq.TriggerID = parseUInt64(chi.URLParam(r, "triggerID")) + tReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err } diff --git a/crm/rest/request/util.go b/compose/rest/request/util.go similarity index 85% rename from crm/rest/request/util.go rename to compose/rest/request/util.go index 4a1d7096e..fc9a011d5 100644 --- a/crm/rest/request/util.go +++ b/compose/rest/request/util.go @@ -4,6 +4,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/jmoiron/sqlx/types" "github.com/pkg/errors" @@ -17,6 +18,19 @@ func parseJSONTextWithErr(s string) (types.JSONText, error) { return *result, err } +func parseISODateWithErr(s string) (time.Time, error) { + return time.Parse(time.RFC3339, s) +} + +func parseISODatePtrWithErr(s string) (*time.Time, error) { + t, err := parseISODateWithErr(s) + if err != nil { + return nil, err + } + + return &t, nil +} + // parseInt parses a string to int func parseInt(s string) int { if s == "" { diff --git a/crm/rest/request/util_test.go b/compose/rest/request/util_test.go similarity index 100% rename from crm/rest/request/util_test.go rename to compose/rest/request/util_test.go diff --git a/crm/rest/router.go b/compose/rest/router.go similarity index 88% rename from crm/rest/router.go rename to compose/rest/router.go index b8dde6b61..ebbbcad76 100644 --- a/crm/rest/router.go +++ b/compose/rest/router.go @@ -3,12 +3,13 @@ package rest import ( "github.com/go-chi/chi" - "github.com/crusttech/crust/crm/rest/handlers" + "github.com/crusttech/crust/compose/rest/handlers" "github.com/crusttech/crust/internal/auth" ) func MountRoutes() func(chi.Router) { var ( + namespace = Namespace{}.New() module = Module{}.New() record = Record{}.New() page = Page{}.New() @@ -16,8 +17,6 @@ func MountRoutes() func(chi.Router) { trigger = Trigger{}.New() notification = Notification{}.New() attachment = Attachment{}.New() - // pageAttachment = PageAttachment{}.New() - // recordAttachment = RecordAttachment{}.New() ) // Initialize handlers & controllers. @@ -32,6 +31,7 @@ func MountRoutes() func(chi.Router) { r.Use(auth.MiddlewareValidOnly) r.Use(middlewareAllowedAccess) + handlers.NewNamespace(namespace).MountRoutes(r) handlers.NewPage(page).MountRoutes(r) handlers.NewModule(module).MountRoutes(r) handlers.NewRecord(record).MountRoutes(r) diff --git a/compose/rest/trigger.go b/compose/rest/trigger.go new file mode 100644 index 000000000..b069ca345 --- /dev/null +++ b/compose/rest/trigger.go @@ -0,0 +1,127 @@ +package rest + +import ( + "context" + + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/compose/internal/service" + "github.com/crusttech/crust/compose/rest/request" + "github.com/crusttech/crust/compose/types" +) + +type ( + triggerPayload struct { + *types.Trigger + + CanUpdateTrigger bool `json:"canUpdateTrigger"` + CanDeleteTrigger bool `json:"canDeleteTrigger"` + } + + triggerSetPayload struct { + Filter types.TriggerFilter `json:"filter"` + Set []*triggerPayload `json:"set"` + } + + Trigger struct { + trigger service.TriggerService + permissions service.PermissionsService + } +) + +func (Trigger) New() *Trigger { + return &Trigger{ + trigger: service.DefaultTrigger, + permissions: service.DefaultPermissions, + } +} + +func (ctrl Trigger) List(ctx context.Context, r *request.TriggerList) (interface{}, error) { + f := types.TriggerFilter{ + NamespaceID: r.NamespaceID, + Query: r.Query, + PerPage: r.PerPage, + Page: r.Page, + } + + set, filter, err := ctrl.trigger.With(ctx).Find(f) + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl Trigger) Create(ctx context.Context, r *request.TriggerCreate) (interface{}, error) { + var ( + err error + ns = &types.Trigger{ + NamespaceID: r.NamespaceID, + ModuleID: r.ModuleID, + Name: r.Name, + Actions: r.Actions, + Enabled: r.Enabled, + Source: r.Source, + } + ) + + ns, err = ctrl.trigger.With(ctx).Create(ns) + return ctrl.makePayload(ctx, ns, err) +} + +func (ctrl Trigger) Read(ctx context.Context, r *request.TriggerRead) (interface{}, error) { + mod, err := ctrl.trigger.With(ctx).FindByID(r.NamespaceID, r.TriggerID) + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl Trigger) Update(ctx context.Context, r *request.TriggerUpdate) (interface{}, error) { + var ( + mod = &types.Trigger{} + err error + ) + + mod.ID = r.TriggerID + mod.NamespaceID = r.NamespaceID + mod.ModuleID = r.ModuleID + mod.Name = r.Name + mod.Actions = r.Actions + mod.Enabled = r.Enabled + mod.Source = r.Source + + mod, err = ctrl.trigger.With(ctx).Update(mod) + return ctrl.makePayload(ctx, mod, err) +} + +func (ctrl Trigger) Delete(ctx context.Context, r *request.TriggerDelete) (interface{}, error) { + _, err := ctrl.trigger.With(ctx).FindByID(r.NamespaceID, r.TriggerID) + if err != nil { + return nil, err + } + + return resputil.OK(), ctrl.trigger.With(ctx).DeleteByID(r.NamespaceID, r.TriggerID) +} + +func (ctrl Trigger) makePayload(ctx context.Context, t *types.Trigger, err error) (*triggerPayload, error) { + if err != nil || t == nil { + return nil, err + } + + perm := ctrl.permissions.With(ctx) + + return &triggerPayload{ + Trigger: t, + + CanUpdateTrigger: perm.CanUpdateTrigger(t), + CanDeleteTrigger: perm.CanDeleteTrigger(t), + }, nil +} + +func (ctrl Trigger) makeFilterPayload(ctx context.Context, nn types.TriggerSet, f types.TriggerFilter, err error) (*triggerSetPayload, error) { + if err != nil { + return nil, err + } + + nsp := &triggerSetPayload{Filter: f, Set: make([]*triggerPayload, len(nn))} + + for i := range nn { + nsp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return nsp, nil +} diff --git a/crm/routes.go b/compose/routes.go similarity index 95% rename from crm/routes.go rename to compose/routes.go index 4d5e30f6d..f71f5ce21 100644 --- a/crm/routes.go +++ b/compose/routes.go @@ -5,7 +5,7 @@ import ( "github.com/go-chi/chi" - "github.com/crusttech/crust/crm/rest" + "github.com/crusttech/crust/compose/rest" "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/internal/config" "github.com/crusttech/crust/internal/middleware" diff --git a/crm/start.go b/compose/start.go similarity index 90% rename from crm/start.go rename to compose/start.go index decbce70f..073d59dd3 100644 --- a/crm/start.go +++ b/compose/start.go @@ -10,8 +10,8 @@ import ( "github.com/pkg/errors" "github.com/titpetric/factory/resputil" - migrate "github.com/crusttech/crust/crm/db" - "github.com/crusttech/crust/crm/internal/service" + migrate "github.com/crusttech/crust/compose/db" + "github.com/crusttech/crust/compose/internal/service" "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/internal/db" "github.com/crusttech/crust/internal/mail" @@ -52,7 +52,7 @@ func Init(ctx context.Context) (err error) { func InitDatabase(ctx context.Context) error { // start/configure database connection - db, err := db.TryToConnect(ctx, "crm", flags.db.DSN, flags.db.Profiler) + db, err := db.TryToConnect(ctx, "compose", flags.db.DSN, flags.db.Profiler) if err != nil { return errors.Wrap(err, "could not connect to database") } diff --git a/crm/types/attachment.gen.go b/compose/types/attachment.gen.go similarity index 100% rename from crm/types/attachment.gen.go rename to compose/types/attachment.gen.go diff --git a/crm/types/attachment.go b/compose/types/attachment.go similarity index 61% rename from crm/types/attachment.go rename to compose/types/attachment.go index 61982617d..f23028e75 100644 --- a/crm/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/crm/types/chart.gen.go b/compose/types/chart.gen.go similarity index 100% rename from crm/types/chart.gen.go rename to compose/types/chart.gen.go diff --git a/crm/types/chart.go b/compose/types/chart.go similarity index 67% rename from crm/types/chart.go rename to compose/types/chart.go index d4bdcd3f3..3930aaaf9 100644 --- a/crm/types/chart.go +++ b/compose/types/chart.go @@ -9,16 +9,26 @@ import ( ) type ( - // Modules - CRM module definitions Chart struct { ID uint64 `json:"chartID,string" db:"id"` 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 { + NamespaceID uint64 `json:"namespaceID,string"` + 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/crm/types/module.gen.go b/compose/types/module.gen.go similarity index 100% rename from crm/types/module.gen.go rename to compose/types/module.gen.go diff --git a/crm/types/module.go b/compose/types/module.go similarity index 70% rename from crm/types/module.go rename to compose/types/module.go index 638be5d49..a6f119595 100644 --- a/crm/types/module.go +++ b/compose/types/module.go @@ -9,7 +9,6 @@ import ( ) type ( - // Modules - CRM module definitions Module struct { ID uint64 `json:"moduleID,string" db:"id"` Name string `json:"name" db:"name"` @@ -17,10 +16,21 @@ 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"` } + + ModuleFilter struct { + NamespaceID uint64 `json:"namespaceID,string"` + 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/crm/types/module_field.gen.go b/compose/types/module_field.gen.go similarity index 100% rename from crm/types/module_field.gen.go rename to compose/types/module_field.gen.go diff --git a/crm/types/module_field.go b/compose/types/module_field.go similarity index 100% rename from crm/types/module_field.go rename to compose/types/module_field.go diff --git a/compose/types/namespace.gen.go b/compose/types/namespace.gen.go new file mode 100644 index 000000000..55f87f097 --- /dev/null +++ b/compose/types/namespace.gen.go @@ -0,0 +1,67 @@ +package types + +// Hello! This file is auto-generated. + +type ( + + // NamespaceSet slice of Namespace + // + // This type is auto-generated. + NamespaceSet []*Namespace +) + +// Walk iterates through every slice item and calls w(Namespace) err +// +// This function is auto-generated. +func (set NamespaceSet) Walk(w func(*Namespace) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// Filter iterates through every slice item, calls f(Namespace) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set NamespaceSet) Filter(f func(*Namespace) (bool, error)) (out NamespaceSet, err error) { + var ok bool + out = NamespaceSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} + +// FindByID finds items from slice by its ID property +// +// This function is auto-generated. +func (set NamespaceSet) FindByID(ID uint64) *Namespace { + for i := range set { + if set[i].ID == ID { + return set[i] + } + } + + return nil +} + +// IDs returns a slice of uint64s from all items in the set +// +// This function is auto-generated. +func (set NamespaceSet) IDs() (IDs []uint64) { + IDs = make([]uint64, len(set)) + + for i := range set { + IDs[i] = set[i].ID + } + + return +} diff --git a/compose/types/namespace.go b/compose/types/namespace.go new file mode 100644 index 000000000..70439e26e --- /dev/null +++ b/compose/types/namespace.go @@ -0,0 +1,40 @@ +package types + +import ( + "time" + + "github.com/jmoiron/sqlx/types" + + "github.com/crusttech/crust/internal/rules" +) + +type ( + Namespace struct { + 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 `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 { + Query string `json:"query"` + Page uint `json:"page"` + PerPage uint `json:"perPage"` + Sort string `json:"sort"` + Count uint `json:"count"` + } +) + +const ( + NamespaceCRM uint64 = 88714882739863655 +) + +// Resource returns a system resource ID for this type +func (n Namespace) PermissionResource() rules.Resource { + return NamespacePermissionResource.AppendID(n.ID) +} diff --git a/crm/types/page.gen.go b/compose/types/page.gen.go similarity index 100% rename from crm/types/page.gen.go rename to compose/types/page.gen.go diff --git a/crm/types/page.go b/compose/types/page.go similarity index 63% rename from crm/types/page.go rename to compose/types/page.go index 1a20ac8e7..b65d407c9 100644 --- a/crm/types/page.go +++ b/compose/types/page.go @@ -1,6 +1,8 @@ package types import ( + "time" + "github.com/jmoiron/sqlx/types" "github.com/crusttech/crust/internal/rules" @@ -12,8 +14,9 @@ type ( ID uint64 `json:"pageID,string" db:"id"` SelfID uint64 `json:"selfID,string" db:"self_id"` - ModuleID uint64 `json:"moduleID,string" db:"module_id"` - Module *Module `json:"module,omitempty" db:"-"` + NamespaceID uint64 `json:"namespaceID,string" db:"rel_namespace"` + + ModuleID uint64 `json:"moduleID,string" db:"rel_module"` Title string `json:"title" db:"title"` Description string `json:"description" db:"description"` @@ -24,6 +27,10 @@ type ( Visible bool `json:"visible" db:"visible"` Weight int `json:"-" db:"weight"` + + 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"` } // Block - value of Page.Blocks ([]Block) @@ -37,6 +44,16 @@ type ( Width int `json:"width"` Height int `json:"height"` } + + PageFilter struct { + NamespaceID uint64 `json:"namespaceID,string"` + ParentID uint64 `json:"parentID,string,omitempty"` + Root bool `json:"root,omitempty"` + Query string `json:"query"` + Page uint `json:"page"` + PerPage uint `json:"perPage"` + Count uint `json:"count"` + } ) // Resource returns a system resource ID for this type diff --git a/crm/types/permission_resources.go b/compose/types/permission_resources.go similarity index 100% rename from crm/types/permission_resources.go rename to compose/types/permission_resources.go diff --git a/crm/types/record.gen.go b/compose/types/record.gen.go similarity index 100% rename from crm/types/record.gen.go rename to compose/types/record.gen.go diff --git a/crm/types/record.go b/compose/types/record.go similarity index 77% rename from crm/types/record.go rename to compose/types/record.go index e542bc3c3..9ea96fd99 100644 --- a/crm/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" ` @@ -22,6 +24,16 @@ type ( DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"` DeletedBy uint64 `db:"deleted_by" json:"deletedBy,string,omitempty" ` } + + RecordFilter struct { + ModuleID uint64 `json:"moduleID,string"` + NamespaceID uint64 `json:"namespaceID,string"` + Filter string `json:"query"` + Page uint `json:"page"` + PerPage uint `json:"perPage"` + Sort string `json:"sort"` + Count uint `json:"count"` + } ) // UserIDs returns a slice of user IDs from all items in the set diff --git a/crm/types/record_value.gen.go b/compose/types/record_value.gen.go similarity index 100% rename from crm/types/record_value.gen.go rename to compose/types/record_value.gen.go diff --git a/crm/types/record_value.go b/compose/types/record_value.go similarity index 100% rename from crm/types/record_value.go rename to compose/types/record_value.go diff --git a/crm/types/trigger.gen.go b/compose/types/trigger.gen.go similarity index 100% rename from crm/types/trigger.gen.go rename to compose/types/trigger.gen.go diff --git a/crm/types/trigger.go b/compose/types/trigger.go similarity index 81% rename from crm/types/trigger.go rename to compose/types/trigger.go index 3baf2916c..358077741 100644 --- a/crm/types/trigger.go +++ b/compose/types/trigger.go @@ -18,12 +18,20 @@ 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"` } TriggerFilter struct { + NamespaceID uint64 `json:"namespaceID,string"` + Query string `json:"query"` + Page uint `json:"page"` + PerPage uint `json:"perPage"` + // Sort string `json:"sort"` + Count uint `json:"count"` ModuleID uint64 `json:"moduleID,string"` } ) diff --git a/crm/internal/repository/attachment.go b/crm/internal/repository/attachment.go deleted file mode 100644 index 1443ef500..000000000 --- a/crm/internal/repository/attachment.go +++ /dev/null @@ -1,165 +0,0 @@ -package repository - -import ( - "context" - "time" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - "github.com/titpetric/factory" - sq "gopkg.in/Masterminds/squirrel.v1" - - "github.com/crusttech/crust/crm/types" -) - -type ( - AttachmentRepository interface { - 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) - Create(mod *types.Attachment) (*types.Attachment, error) - DeleteByID(id uint64) error - } - - attachment struct { - *repository - } -) - -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 crm_attachment AS a WHERE id = ? AND ` + sqlAttachmentScope - - sqlAttachmentsByIDs = `SELECT ` + sqlAttachmentColumns + - ` FROM crm_attachment AS a WHERE id IN (?) AND ` + sqlAttachmentScope -) - -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 { - 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) 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) 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 - } - - set = types.AttachmentSet{} - - query := sq.Select().From("crm_attachment AS a").Where(sq.Eq{"a.kind": f.Kind}) - - switch f.Kind { - case types.PageAttachment: - // @todo implement filtering by page - if f.PageID > 0 { - err = errors.New("filtering by pageID not implemented") - return - } - case types.RecordAttachment: - query = query. - Join("crm_record_value AS v ON (v.ref = a.id)") - - if f.ModuleID > 0 { - query = query. - Join("crm_record AS r ON (r.id = v.record_id)"). - Where(sq.Eq{"r.module_id": f.ModuleID}) - } - - if f.RecordID > 0 { - query = query.Where(sq.Eq{"v.record_id": f.RecordID}) - } - - if f.FieldName != "" { - query = query.Where(sq.Eq{"v.name": f.FieldName}) - } - default: - err = errors.New("unsupported kind value") - } - - if f.Filter != "" { - err = errors.New("filtering by filter not implemented") - 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 - } - } - - // Assemble SQL for fetching attachments (where + sorting + paging)... - query = query. - Column(sqlAttachmentColumns). - Limit(uint64(f.PerPage)). - Offset(uint64(f.Page * f.PerPage)) - - if sqlSelect, argsSelect, err := query.ToSql(); err != nil { - return set, f, err - } else { - return set, f, r.db().Select(&set, sqlSelect, argsSelect...) - } -} - -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("crm_attachment", mod) -} - -func (r *attachment) DeleteByID(id uint64) error { - _, err := r.db().Exec("UPDATE crm_attachment SET deleted_at = NOW() WHERE id = ?", id) - return err -} diff --git a/crm/internal/repository/chart.go b/crm/internal/repository/chart.go deleted file mode 100644 index b650900b6..000000000 --- a/crm/internal/repository/chart.go +++ /dev/null @@ -1,72 +0,0 @@ -package repository - -import ( - "context" - "time" - - "github.com/titpetric/factory" - - "github.com/crusttech/crust/crm/types" -) - -type ( - ChartRepository interface { - With(ctx context.Context, db *factory.DB) ChartRepository - - FindByID(id uint64) (*types.Chart, error) - Find() (types.ChartSet, error) - Create(mod *types.Chart) (*types.Chart, error) - Update(mod *types.Chart) (*types.Chart, error) - DeleteByID(id uint64) error - } - - chart struct { - *repository - } -) - -const sqlChartColumns = "id, name, config, " + - "created_at, updated_at, deleted_at" - -const sqlChartSelect = "SELECT " + sqlChartColumns + " FROM crm_chart" - -func Chart(ctx context.Context, db *factory.DB) ChartRepository { - return (&chart{}).With(ctx, db) -} - -func (r *chart) With(ctx context.Context, db *factory.DB) ChartRepository { - return &chart{ - repository: r.repository.With(ctx, db), - } -} - -func (r *chart) FindByID(id uint64) (*types.Chart, error) { - mod := &types.Chart{} - if err := r.db().Get(mod, sqlChartSelect+" WHERE id = ?", id); err != nil { - return nil, err - } - return mod, nil -} - -func (r *chart) Find() (types.ChartSet, error) { - mod := types.ChartSet{} - return mod, r.db().Select(&mod, sqlChartSelect+" ORDER BY id ASC") -} - -func (r *chart) Create(mod *types.Chart) (*types.Chart, error) { - mod.ID = factory.Sonyflake.NextID() - mod.CreatedAt = time.Now() - - return mod, r.db().Insert("crm_chart", mod) -} - -func (r *chart) Update(mod *types.Chart) (*types.Chart, error) { - now := time.Now() - mod.UpdatedAt = &now - return mod, r.db().Replace("crm_chart", mod) -} - -func (r *chart) DeleteByID(id uint64) error { - _, err := r.db().Exec("DELETE FROM crm_chart WHERE id = ?", id) - return err -} diff --git a/crm/internal/repository/module.go b/crm/internal/repository/module.go deleted file mode 100644 index 007832f09..000000000 --- a/crm/internal/repository/module.go +++ /dev/null @@ -1,151 +0,0 @@ -package repository - -import ( - "context" - "time" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - "github.com/titpetric/factory" - - "github.com/crusttech/crust/crm/types" -) - -type ( - ModuleRepository interface { - With(ctx context.Context, db *factory.DB) ModuleRepository - - FindByID(id uint64) (*types.Module, error) - Find() (types.ModuleSet, error) - Create(mod *types.Module) (*types.Module, error) - Update(mod *types.Module) (*types.Module, error) - DeleteByID(id uint64) error - } - - module struct { - *repository - } -) - -const ( - sqlModuleColumns = ` - id, name, json, - created_at, updated_at, deleted_at - ` - sqlModuleSelect = ` - SELECT ` + sqlModuleColumns + ` FROM crm_module WHERE deleted_at IS NULL - ` -) - -func Module(ctx context.Context, db *factory.DB) ModuleRepository { - return (&module{}).With(ctx, db) -} - -func (r *module) With(ctx context.Context, db *factory.DB) ModuleRepository { - return &module{ - repository: r.repository.With(ctx, db), - } -} - -// @todo: update to accepted DeletedAt column semantics from Messaging - -func (r *module) FindByID(id uint64) (mod *types.Module, err error) { - mod = &types.Module{} - - if err = r.db().Get(mod, sqlModuleSelect+" AND id = ? ", id); err != nil { - return - } - - if mod.Fields, err = r.fields(id); err != nil { - return - } - - return -} - -func (r *module) Find() (mm types.ModuleSet, err error) { - if err = r.db().Select(&mm, sqlModuleSelect+" ORDER BY id ASC"); err != nil { - return - } - - var ff types.ModuleFieldSet - if ff, err = r.fields(mm.IDs()...); err != nil { - return - } else { - _ = ff.Walk(func(f *types.ModuleField) error { - mm.FindByID(f.ModuleID).Fields = append(mm.FindByID(f.ModuleID).Fields, f) - return nil - }) - } - - return mm, nil -} - -func (r *module) Create(mod *types.Module) (*types.Module, error) { - mod.ID = factory.Sonyflake.NextID() - mod.CreatedAt = time.Now() - - if err := r.updateFields(mod.ID, mod.Fields); err != nil { - return nil, err - } - - return mod, r.db().Insert("crm_module", mod) -} - -func (r *module) Update(mod *types.Module) (*types.Module, error) { - now := time.Now() - mod.UpdatedAt = &now - - if err := r.updateFields(mod.ID, mod.Fields); err != nil { - return nil, err - } - - return mod, r.db().Replace("crm_module", mod) -} - -func (r *module) updateFields(moduleID uint64, ff types.ModuleFieldSet) error { - // @todo be more selective when deleting - if _, err := r.db().Exec("DELETE FROM crm_module_form WHERE module_id = ?", moduleID); err != nil { - return errors.Wrap(err, "Error updating module fields") - } - - for idx, v := range ff { - v.ModuleID = moduleID - v.Place = idx - if err := r.db().Replace("crm_module_form", v); err != nil { - return errors.Wrap(err, "Error updating module fields") - } - } - - return nil -} - -func (r *module) DeleteByID(id uint64) error { - _, err := r.db().Exec("DELETE FROM crm_module WHERE id=?", id) - return err -} - -func (r *module) fields(IDs ...uint64) (ff types.ModuleFieldSet, err error) { - if len(IDs) == 0 { - return - } - - if sql, args, err := sqlx.In("SELECT * FROM crm_module_form WHERE module_id IN (?) ORDER BY module_id AND place", IDs); err != nil { - return nil, err - } else { - return ff, r.db().Select(&ff, sql, args...) - } -} - -// // FieldNames returns a slice of field names, used for ordering record row columns -// func (r *module) FieldNames(mod *types.Module) ([]string, error) { -// if fields, err := r.Fields(mod.ID); err != nil { -// return []string{}, err -// } else { -// result := make([]string, len(fields)) -// for k, v := range fields { -// result[k] = v.Name -// } -// return result, nil -// } -// } diff --git a/crm/internal/repository/page.go b/crm/internal/repository/page.go deleted file mode 100644 index 18881a1a1..000000000 --- a/crm/internal/repository/page.go +++ /dev/null @@ -1,122 +0,0 @@ -package repository - -import ( - "context" - - "github.com/titpetric/factory" - - "github.com/crusttech/crust/crm/types" -) - -type ( - PageRepository interface { - With(ctx context.Context, db *factory.DB) PageRepository - - FindByID(id uint64) (*types.Page, error) - FindByModuleID(id uint64) (*types.Page, error) - FindBySelfID(selfID uint64) (types.PageSet, error) - Find() (types.PageSet, error) - FindRecordPages() (types.PageSet, error) - - Create(mod *types.Page) (*types.Page, error) - Update(mod *types.Page) (*types.Page, error) - DeleteByID(id uint64) error - - Reorder(selfID uint64, pageIDs []uint64) error - } - - page struct { - *repository - } -) - -func Page(ctx context.Context, db *factory.DB) PageRepository { - return (&page{}).With(ctx, db) -} - -func (r *page) With(ctx context.Context, db *factory.DB) PageRepository { - return &page{ - repository: r.repository.With(ctx, db), - } -} - -func (r *page) FindByID(id uint64) (*types.Page, error) { - page := &types.Page{} - if err := r.db().Get(page, "SELECT * FROM crm_page WHERE id=?", id); err != nil { - return page, err - } - return page, nil -} - -func (r *page) FindByModuleID(id uint64) (*types.Page, error) { - page := &types.Page{} - if err := r.db().Get(page, "SELECT * FROM crm_page WHERE module_id=?", id); err != nil { - return nil, err - } - return page, nil -} - -func (r *page) FindRecordPages() (set types.PageSet, err error) { - return set, r.db().Select(&set, "SELECT * FROM crm_page WHERE module_id > 0") -} - -func (r *page) FindBySelfID(selfID uint64) (types.PageSet, error) { - pages := types.PageSet{} - if err := r.db().Select(&pages, "SELECT * FROM crm_page WHERE self_id = ? ORDER BY weight ASC", selfID); err != nil { - return pages, err - } - return pages, nil -} - -func (r *page) Find() (set types.PageSet, err error) { - return set, r.db().Select(&set, "SELECT * FROM crm_page ORDER BY self_id, weight ASC") -} - -func (r *page) Reorder(selfID uint64, pageIDs []uint64) error { - pageMap := map[uint64]bool{} - if pages, err := r.FindBySelfID(selfID); err != nil { - return nil - } else { - for _, page := range pages { - pageMap[page.ID] = true - } - } - weight := 1 - db := r.db() - // honor parameter first - for _, pageID := range pageIDs { - if pageMap[pageID] { - pageMap[pageID] = false - if _, err := db.Exec("UPDATE crm_page set weight=? where id=? and self_id=?", weight, pageID, selfID); err != nil { - return err - } - weight++ - } - } - for pageID, update := range pageMap { - if update { - if _, err := db.Exec("UPDATE crm_page set weight=? where id=? and self_id=?", weight, pageID, selfID); err != nil { - return err - } - weight++ - } - } - return nil -} - -func (r *page) Create(item *types.Page) (*types.Page, error) { - page := &types.Page{} - *page = *item - - page.ID = factory.Sonyflake.NextID() - return page, r.db().Insert("crm_page", page) -} - -func (r *page) Update(page *types.Page) (*types.Page, error) { - return page, r.db().Replace("crm_page", page) -} - -func (r *page) DeleteByID(id uint64) error { - _, err := r.db().Exec("DELETE FROM crm_page WHERE id=?", id) - return err -} diff --git a/crm/internal/repository/record.go b/crm/internal/repository/record.go deleted file mode 100644 index 8664b332f..000000000 --- a/crm/internal/repository/record.go +++ /dev/null @@ -1,401 +0,0 @@ -package repository - -import ( - "context" - "fmt" - "strings" - - "github.com/jmoiron/sqlx" - "github.com/lann/builder" - "github.com/pkg/errors" - "github.com/titpetric/factory" - sq "gopkg.in/Masterminds/squirrel.v1" - - "github.com/crusttech/crust/crm/internal/repository/ql" - - "github.com/crusttech/crust/crm/types" -) - -type ( - RecordRepository interface { - With(ctx context.Context, db *factory.DB) RecordRepository - - FindByID(id uint64) (*types.Record, error) - - Report(module *types.Module, metrics, dimensions, filter string) (results interface{}, err error) - Find(module *types.Module, filter string, sort string, page int, perPage int) (*FindResponse, error) - - Create(record *types.Record) (*types.Record, error) - Update(record *types.Record) (*types.Record, error) - Delete(record *types.Record) error - - LoadValues(IDs ...uint64) (rvs types.RecordValueSet, err error) - DeleteValues(record *types.Record) error - UpdateValues(recordID uint64, rvs types.RecordValueSet) (err error) - - CountAuthored(userID uint64) (c int, err error) - ChangeAuthor(userID, target uint64) error - CountReferenced(userID uint64) (c int, err error) - ChangeReferences(userID, target uint64) error - } - - FindResponseMeta struct { - Filter string `json:"filter,omitempty"` - Sort string `json:"sort,omitempty"` - - Page int `json:"page"` - PerPage int `json:"perPage"` - Count int `json:"count"` - } - - FindResponse struct { - Meta FindResponseMeta `json:"meta"` - Records types.RecordSet `json:"records"` - } - - record struct { - *repository - } -) - -const ( - sortWrap = `sort` -) - -func Record(ctx context.Context, db *factory.DB) RecordRepository { - return (&record{}).With(ctx, db) -} - -func (r *record) With(ctx context.Context, db *factory.DB) RecordRepository { - return &record{ - repository: r.repository.With(ctx, db), - } -} - -// @todo: update to accepted DeletedAt column semantics from Messaging - -func (r *record) FindByID(id uint64) (*types.Record, error) { - mod := &types.Record{} - if err := r.db().Get(mod, "SELECT * FROM crm_record WHERE id=? and deleted_at IS NULL", id); err != nil { - return nil, err - } - return mod, nil -} - -func (r *record) Report(module *types.Module, metrics, dimensions, filter string) (results interface{}, err error) { - crb := NewRecordReportBuilder(module) - - var result = make([]map[string]interface{}, 0) - - if query, args, err := crb.Build(metrics, dimensions, filter); err != nil { - return nil, errors.Wrap(err, "can not generate report query") - } else if rows, err := r.db().Query(query, args...); err != nil { - return nil, errors.Wrapf(err, "can not execute report query (%s)", query) - } else { - for rows.Next() { - result = append(result, crb.Cast(rows)) - } - - return result, nil - } -} - -func (r *record) Find(module *types.Module, filter string, sort string, page int, perPage int) (*FindResponse, error) { - if page < 0 { - page = 0 - } - if perPage <= 0 { - perPage = 50 - } - if perPage > 100 { - perPage = 100 - } - - response := &FindResponse{ - Meta: FindResponseMeta{ - Filter: filter, - Page: page, - PerPage: perPage, - Sort: sort, - }, - Records: make([]*types.Record, 0), - } - - var query, err = r.buildQuery(module, filter, sort) - if err != nil { - return nil, err - } - - // Assemble SQL for counting (includes only where) - count := query.Column("COUNT(*)") - count = builder.Delete(count, "OrderBys").(sq.SelectBuilder) - sqlSelect, argsSelect, err := count.ToSql() - if err != nil { - return nil, err - } - - // Execute count query. - if err := r.db().Get(&response.Meta.Count, sqlSelect, argsSelect...); err != nil { - return nil, err - } - - // Return empty response if count of records is zero. - if response.Meta.Count == 0 { - return response, nil - } - - // Assemble SQL for fetching record (where + sorting + paging)... - query = query. - Column("r.*"). - Limit(uint64(perPage)). - Offset(uint64(page * perPage)) - - // Create actual fetch SQL sentences. - sqlSelect, argsSelect, err = query.ToSql() - if err != nil { - return nil, err - } - - // append order args to select args and execute actual query. - if err := r.db().Select(&response.Records, sqlSelect, argsSelect...); err != nil { - return nil, err - } - - return response, nil -} - -func (r *record) buildQuery(module *types.Module, filter string, sort string) (query sq.SelectBuilder, err error) { - // Create query for fetching and counting records. - query = sq.Select(). - From("crm_record AS r"). - Where(sq.Eq{"r.module_id": module.ID}). - Where(sq.Eq{"r.deleted_at": nil}) - - var joinedFields = []string{} - var alreadyJoined = func(f string) bool { - for _, a := range joinedFields { - if a == f { - return true - } - } - - joinedFields = append(joinedFields, f) - return false - } - - // Parse filters. - if filter != "" { - var ( - // Filter parser - fp = ql.NewParser() - - // Filter node - fn ql.ASTNode - ) - - // Make a nice wrapper that will translate module fields to subqueries - fp.OnIdent = func(i ql.Ident) (ql.Ident, error) { - var is bool - if i.Value, is = isRealRecordCol(i.Value); is { - return i, nil - } - - if !module.Fields.HasName(i.Value) { - return i, errors.Errorf("unknown field %q", i.Value) - } - - if !alreadyJoined(i.Value) { - query = query.LeftJoin(fmt.Sprintf( - "crm_record_value AS rv_%s ON (rv_%s.record_id = r.id AND rv_%s.name = ? AND rv_%s.deleted_at IS NULL)", - i.Value, i.Value, i.Value, i.Value, - ), i.Value) - } - - // @todo switch value for ref when doing Record/Owner lookup - i.Value = fmt.Sprintf("rv_%s.value", i.Value) - - return i, nil - } - - if fn, err = fp.ParseExpression(filter); err != nil { - return - } else if filterSql, filterArgs, err := fn.ToSql(); err != nil { - return query, err - } else { - query = query.Where("("+filterSql+")", filterArgs...) - } - } - - if sort != "" { - var ( - // Sort parser - sp = ql.NewParser() - - // Sort columns - sc ql.Columns - ) - - sp.OnIdent = func(i ql.Ident) (ql.Ident, error) { - var is bool - if i.Value, is = isRealRecordCol(i.Value); is { - i.Value += " " - return i, nil - } - - if !module.Fields.HasName(i.Value) { - return i, errors.Errorf("unknown field %q", i.Value) - } - - if !alreadyJoined(i.Value) { - query = query.LeftJoin(fmt.Sprintf( - "crm_record_value AS rv_%s ON (rv_%s.record_id = r.id AND rv_%s.name = ? AND rv_%s.deleted_at IS NULL)", - i.Value, i.Value, i.Value, i.Value, - ), i.Value) - } - - // @todo switch value for ref when doing Record/Owner lookup - i.Value = fmt.Sprintf("rv_%s.value ", i.Value) - - return i, nil - } - - if sc, err = sp.ParseColumns(sort); err != nil { - return - - } - - query = query.OrderBy(sc.Strings()...) - } - - return -} - -func (r *record) Create(record *types.Record) (*types.Record, error) { - record.ID = factory.Sonyflake.NextID() - - if err := r.db().Replace("crm_record", record); err != nil { - return nil, errors.Wrap(err, "could not update record") - } - - return record, nil -} - -func (r *record) Update(record *types.Record) (*types.Record, error) { - if err := r.db().Replace("crm_record", record); err != nil { - return nil, errors.Wrap(err, "could not update record") - } - - return record, nil -} - -func (r *record) Delete(record *types.Record) error { - _, err := r.db().Exec( - "UPDATE crm_record SET deleted_at = ?, deleted_by = ? WHERE id = ?", - record.DeletedAt, - record.DeletedBy, - record.ID, - ) - - return err -} - -func (r *record) DeleteValues(record *types.Record) error { - _, err := r.db().Exec( - "UPDATE crm_record_value SET deleted_at = ? WHERE record_id = ?", - record.DeletedAt, - record.ID) - - return err -} - -func (r *record) UpdateValues(recordID uint64, rvs types.RecordValueSet) (err error) { - // Remove all records and prepare to be updated - // @todo be more selective and delete only removed values - if _, err = r.db().Exec("DELETE FROM crm_record_value WHERE record_id = ?", recordID); err != nil { - return errors.Wrap(err, "could not remove record values") - } - - err = rvs.Walk(func(value *types.RecordValue) error { - value.RecordID = recordID - return r.db().Replace("crm_record_value", value) - }) - - return errors.Wrap(err, "could not replace record values") - -} - -func (r *record) LoadValues(IDs ...uint64) (rvs types.RecordValueSet, err error) { - if len(IDs) == 0 { - return - } - - var sql = "SELECT * FROM crm_record_value WHERE record_id IN (?) AND deleted_at IS NULL ORDER BY record_id, place" - - if sql, args, err := sqlx.In(sql, IDs); err != nil { - return nil, err - } else { - return rvs, r.db().Select(&rvs, sql, args...) - } -} - -// Checks if field name is "real column", reformats it and returns -func isRealRecordCol(name string) (string, bool) { - switch name { - case - "id", - "module_id", - "owned_by", - "created_by", - "created_at", - "updated_by", - "updated_at", - "deleted_by", - "deleted_at": - return name, true - - case - "moduleID", - "ownedBy", - "createdBy", - "createdAt", - "updatedBy", - "updatedAt", - "deletedBy", - "deletedAt": - return name[0:len(name)-2] + "_" + strings.ToLower(name[len(name)-2:]), true - } - - return name, false -} - -func (r *record) CountAuthored(userID uint64) (c int, err error) { - return c, r.db().Get(&c, - "SELECT COUNT(*) FROM crm_record WHERE created_by = ? OR updated_by = ? OR deleted_by = ?", - userID, userID, userID) -} - -func (r *record) ChangeAuthor(userID, target uint64) error { - if _, err := r.db().Exec("UPDATE crm_record SET created_by = ? WHERE created_by = ?", target, userID); err != nil { - return err - } - if _, err := r.db().Exec("UPDATE crm_record SET updated_by = ? WHERE updated_by = ?", target, userID); err != nil { - return err - } - if _, err := r.db().Exec("UPDATE crm_record SET deleted_by = ? WHERE deleted_by = ?", target, userID); err != nil { - return err - } - - return nil -} - -func (r *record) CountReferenced(userID uint64) (c int, err error) { - // @todo add field type (User) check - return c, r.db().Get(&c, - "SELECT COUNT(*) FROM crm_record_value WHERE ref = ?", - userID) -} - -func (r *record) ChangeReferences(userID, target uint64) error { - _, err := r.db().Exec("UPDATE crm_record_value SET ref = ? WHERE ref = ?", target, userID) - return err -} diff --git a/crm/internal/repository/repository.go b/crm/internal/repository/repository.go deleted file mode 100644 index 61eb878b8..000000000 --- a/crm/internal/repository/repository.go +++ /dev/null @@ -1,47 +0,0 @@ -package repository - -import ( - "context" - - "github.com/titpetric/factory" - - "github.com/crusttech/crust/internal/auth" -) - -type ( - repository struct { - ctx context.Context - dbh *factory.DB - } -) - -// DB produces a contextual DB handle -func DB(ctx context.Context) *factory.DB { - return factory.Database.MustGet("crm").With(ctx) -} - -// Identity returns the User ID from context -func Identity(ctx context.Context) uint64 { - return auth.GetIdentityFromContext(ctx).Identity() -} - -// With updates repository and database contexts -func (r *repository) With(ctx context.Context, db *factory.DB) *repository { - return &repository{ - ctx: ctx, - dbh: db, - } -} - -// Context returns current active repository context -func (r *repository) Context() context.Context { - return r.ctx -} - -// db returns context-aware db handle -func (r *repository) db() *factory.DB { - if r.dbh != nil { - return r.dbh - } - return DB(r.ctx) -} diff --git a/crm/internal/repository/trigger.go b/crm/internal/repository/trigger.go deleted file mode 100644 index ea38bda57..000000000 --- a/crm/internal/repository/trigger.go +++ /dev/null @@ -1,91 +0,0 @@ -package repository - -import ( - "context" - "time" - - "github.com/titpetric/factory" - "gopkg.in/Masterminds/squirrel.v1" - - "github.com/crusttech/crust/crm/types" -) - -type ( - TriggerRepository interface { - With(ctx context.Context, db *factory.DB) TriggerRepository - - FindByID(id uint64) (*types.Trigger, error) - Find(filter types.TriggerFilter) (types.TriggerSet, error) - Create(mod *types.Trigger) (*types.Trigger, error) - Update(mod *types.Trigger) (*types.Trigger, error) - DeleteByID(id uint64) error - } - - trigger struct { - *repository - } -) - -func Trigger(ctx context.Context, db *factory.DB) TriggerRepository { - return (&trigger{}).With(ctx, db) -} - -func (r *trigger) With(ctx context.Context, db *factory.DB) TriggerRepository { - return &trigger{ - repository: r.repository.With(ctx, db), - } -} - -func (r *trigger) FindByID(id uint64) (*types.Trigger, error) { - mod := &types.Trigger{} - query := r.query().Where("id = ?", id) - - if sql, args, err := query.ToSql(); err != nil { - return nil, err - } else { - return mod, r.db().Get(mod, sql, args...) - } -} - -func (r *trigger) Find(filter types.TriggerFilter) (mod types.TriggerSet, err error) { - query := r.query() - - if filter.ModuleID > 0 { - query = query.Where("rel_module = ?", filter.ModuleID) - } - - if sql, args, err := query.ToSql(); err != nil { - return nil, err - } else { - return mod, r.db().Select(&mod, sql, args...) - } -} - -func (r trigger) query() (query squirrel.SelectBuilder) { - query = squirrel.Select(). - Columns( - "id", "name", "actions", "enabled", "source", "rel_module", - "created_at", "updated_at", "deleted_at"). - From("crm_trigger"). - OrderBy("id DESC") - - return -} - -func (r *trigger) Create(mod *types.Trigger) (*types.Trigger, error) { - mod.ID = factory.Sonyflake.NextID() - mod.CreatedAt = time.Now() - - return mod, r.db().Insert("crm_trigger", mod) -} - -func (r *trigger) Update(mod *types.Trigger) (*types.Trigger, error) { - now := time.Now() - mod.UpdatedAt = &now - return mod, r.db().Replace("crm_trigger", mod) -} - -func (r *trigger) DeleteByID(id uint64) error { - _, err := r.db().Exec("DELETE FROM crm_trigger WHERE id = ?", id) - return err -} diff --git a/crm/internal/service/chart.go b/crm/internal/service/chart.go deleted file mode 100644 index 2335a9bd0..000000000 --- a/crm/internal/service/chart.go +++ /dev/null @@ -1,122 +0,0 @@ -package service - -import ( - "context" - - "github.com/pkg/errors" - "github.com/titpetric/factory" - - "github.com/crusttech/crust/crm/internal/repository" - "github.com/crusttech/crust/crm/types" -) - -type ( - chart struct { - db *factory.DB - ctx context.Context - - prmSvc PermissionsService - - chartRepo repository.ChartRepository - } - - ChartService interface { - With(ctx context.Context) ChartService - - FindByID(chartID uint64) (*types.Chart, error) - Find() (types.ChartSet, error) - - Create(chart *types.Chart) (*types.Chart, error) - Update(chart *types.Chart) (*types.Chart, error) - DeleteByID(chartID uint64) error - } -) - -func Chart() ChartService { - return (&chart{ - prmSvc: DefaultPermissions, - }).With(context.Background()) -} - -func (svc *chart) With(ctx context.Context) ChartService { - db := repository.DB(ctx) - return &chart{ - db: db, - ctx: ctx, - - prmSvc: svc.prmSvc.With(ctx), - - chartRepo: repository.Chart(ctx, db), - } -} - -func (svc *chart) FindByID(chartID uint64) (c *types.Chart, err error) { - if c, err = svc.chartRepo.FindByID(chartID); err != nil { - return - } else if !svc.prmSvc.CanReadChart(c) { - return nil, errors.New("not allowed to access this chart") - } - - return -} - -func (svc *chart) Find() (cc types.ChartSet, err error) { - if cc, err = svc.chartRepo.Find(); err != nil { - return nil, err - } else { - return cc.Filter(func(m *types.Chart) (bool, error) { - return svc.prmSvc.CanReadChart(m), nil - }) - } -} - -func (svc *chart) Create(mod *types.Chart) (c *types.Chart, err error) { - if !svc.prmSvc.CanCreateChart(crmNamespace()) { - return nil, errors.New("not allowed to create this chart") - } - - return c, svc.db.Transaction(func() error { - c, err = svc.chartRepo.Create(mod) - return err - }) -} - -func (svc *chart) Update(mod *types.Chart) (c *types.Chart, err error) { - validate := func() error { - if mod.ID == 0 { - return errors.New("Error updating chart: invalid ID") - } else if c, err = svc.chartRepo.FindByID(mod.ID); err != nil { - return errors.Wrap(err, "Error while loading chart for update") - } else { - if !svc.prmSvc.CanUpdateChart(c) { - return errors.New("not allowed to update this chart") - } - - mod.CreatedAt = c.CreatedAt - } - - return nil - } - - if err = validate(); err != nil { - return nil, err - } - - c.Config = mod.Config - c.Name = mod.Name - - return c, svc.db.Transaction(func() error { - c, err = svc.chartRepo.Update(c) - return err - }) -} - -func (svc *chart) DeleteByID(ID uint64) error { - if c, err := svc.chartRepo.FindByID(ID); err != nil { - return errors.Wrap(err, "could not delete chart") - } else if !svc.prmSvc.CanDeleteChart(c) { - return errors.New("not allowed to delete this chart") - } - - return svc.chartRepo.DeleteByID(ID) -} diff --git a/crm/internal/service/chart_test.go b/crm/internal/service/chart_test.go deleted file mode 100644 index 66bec541a..000000000 --- a/crm/internal/service/chart_test.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build unit - -package service - -import ( - "testing" -) - -func TestModuleGraph(t *testing.T) { - // repository := Module().With(context.Background()) -} diff --git a/crm/internal/service/module.go b/crm/internal/service/module.go deleted file mode 100644 index 791556b03..000000000 --- a/crm/internal/service/module.go +++ /dev/null @@ -1,126 +0,0 @@ -package service - -import ( - "context" - - "github.com/pkg/errors" - "github.com/titpetric/factory" - - "github.com/crusttech/crust/crm/internal/repository" - "github.com/crusttech/crust/crm/types" -) - -type ( - module struct { - db *factory.DB - ctx context.Context - - prmSvc PermissionsService - - moduleRepo repository.ModuleRepository - pageRepo repository.PageRepository - } - - ModuleService interface { - With(ctx context.Context) ModuleService - - FindByID(moduleID uint64) (*types.Module, error) - Find() (types.ModuleSet, error) - - Create(module *types.Module) (*types.Module, error) - Update(module *types.Module) (*types.Module, error) - DeleteByID(moduleID uint64) error - } -) - -func Module() ModuleService { - return (&module{ - prmSvc: DefaultPermissions, - }).With(context.Background()) -} - -func (svc *module) With(ctx context.Context) ModuleService { - db := repository.DB(ctx) - return &module{ - db: db, - ctx: ctx, - - prmSvc: svc.prmSvc.With(ctx), - - moduleRepo: repository.Module(ctx, db), - pageRepo: repository.Page(ctx, db), - } -} - -func (svc *module) FindByID(id uint64) (m *types.Module, err error) { - if m, err = svc.moduleRepo.FindByID(id); err != nil { - return - } else if !svc.prmSvc.CanReadModule(m) { - return nil, errors.New("not allowed to access this module") - } - - return -} - -func (svc *module) Find() (mm types.ModuleSet, err error) { - if mm, err = svc.moduleRepo.Find(); err != nil { - return nil, err - } else { - return mm.Filter(func(m *types.Module) (bool, error) { - return svc.prmSvc.CanReadModule(m), nil - }) - } -} - -func (svc *module) Create(mod *types.Module) (*types.Module, error) { - if !svc.prmSvc.CanCreateModule(crmNamespace()) { - return nil, errors.New("not allowed to create this module") - } - - if len(mod.Fields) == 0 { - return nil, errors.New("Error creating module: no fields") - } - - return svc.moduleRepo.Create(mod) -} - -func (svc *module) Update(module *types.Module) (m *types.Module, err error) { - validate := func() error { - if module.ID == 0 { - return errors.New("Error updating module: invalid ID") - } else if m, err = svc.moduleRepo.FindByID(module.ID); err != nil { - return errors.Wrap(err, "Error while loading module for update") - } else { - if !svc.prmSvc.CanUpdateModule(m) { - return errors.New("not allowed to update this module") - } - - module.CreatedAt = m.CreatedAt - } - - if len(module.Fields) == 0 { - return errors.New("Error updating module: no fields") - } - - return nil - } - - if err = validate(); err != nil { - return nil, err - } - - return m, svc.db.Transaction(func() (err error) { - m, err = svc.moduleRepo.Update(module) - return - }) -} - -func (svc *module) DeleteByID(ID uint64) error { - if m, err := svc.moduleRepo.FindByID(ID); err != nil { - return errors.Wrap(err, "could not delete module") - } else if !svc.prmSvc.CanDeleteModule(m) { - return errors.New("not allowed to delete this module") - } - - return svc.moduleRepo.DeleteByID(ID) -} diff --git a/crm/internal/service/page.go b/crm/internal/service/page.go deleted file mode 100644 index ca5ac998d..000000000 --- a/crm/internal/service/page.go +++ /dev/null @@ -1,199 +0,0 @@ -package service - -import ( - "context" - - "github.com/pkg/errors" - "github.com/titpetric/factory" - - "github.com/crusttech/crust/crm/internal/repository" - "github.com/crusttech/crust/crm/types" -) - -type ( - page struct { - db *factory.DB - ctx context.Context - - prmSvc PermissionsService - - pageRepo repository.PageRepository - moduleRepo repository.ModuleRepository - } - - PageService interface { - With(ctx context.Context) PageService - - FindByID(pageID uint64) (*types.Page, error) - FindByModuleID(moduleID uint64) (*types.Page, error) - FindBySelfID(selfID uint64) (pages types.PageSet, err error) - Find() (pages types.PageSet, err error) - Tree() (pages types.PageSet, err error) - FindRecordPages() (pages types.PageSet, err error) - - Create(page *types.Page) (*types.Page, error) - Update(page *types.Page) (*types.Page, error) - DeleteByID(pageID uint64) error - - Reorder(selfID uint64, pageIDs []uint64) error - } -) - -func Page() PageService { - return (&page{ - prmSvc: DefaultPermissions, - }).With(context.Background()) -} - -func (svc *page) With(ctx context.Context) PageService { - db := repository.DB(ctx) - return &page{ - db: db, - ctx: ctx, - - prmSvc: svc.prmSvc.With(ctx), - - pageRepo: repository.Page(ctx, db), - moduleRepo: repository.Module(ctx, db), - } -} - -func (svc *page) FindByID(id uint64) (p *types.Page, err error) { - return svc.checkPermissions(svc.pageRepo.FindByID(id)) -} - -func (svc *page) FindByModuleID(moduleID uint64) (p *types.Page, err error) { - return svc.checkPermissions(svc.pageRepo.FindByModuleID(moduleID)) -} - -func (svc *page) checkPermissions(p *types.Page, err error) (*types.Page, error) { - if err != nil { - return nil, err - } else if !svc.prmSvc.CanReadPage(p) { - return nil, errors.New("not allowed to access this page") - } - - return p, err -} - -func (svc *page) FindBySelfID(selfID uint64) (pp types.PageSet, err error) { - return svc.filterPageSet(svc.pageRepo.FindBySelfID(selfID)) -} - -func (svc *page) Find() (pages types.PageSet, err error) { - return svc.filterPageSet(svc.pageRepo.Find()) -} - -func (svc *page) Tree() (pages types.PageSet, err error) { - var tree types.PageSet - - return tree, svc.db.Transaction(func() (err error) { - if pages, err = svc.filterPageSet(svc.pageRepo.Find()); err != nil { - return - } - - // No preloading - we do not need (or should have) any modules - // associated with us - _ = pages.Walk(func(p *types.Page) error { - if p.SelfID == 0 { - tree = append(tree, p) - } else if c := pages.FindByID(p.SelfID); c != nil { - if c.Children == nil { - c.Children = types.PageSet{} - } - - c.Children = append(c.Children, p) - } else { - // Move orphans to root - p.SelfID = 0 - tree = append(tree, p) - } - - return nil - }) - - return nil - }) -} - -func (svc *page) FindRecordPages() (pages types.PageSet, err error) { - return svc.pageRepo.FindRecordPages() -} - -func (svc *page) filterPageSet(pp types.PageSet, err error) (types.PageSet, error) { - if err != nil { - return nil, err - } - - return pp.Filter(func(m *types.Page) (bool, error) { - return svc.prmSvc.CanReadPage(m), nil - }) -} - -func (svc *page) Reorder(selfID uint64, pageIDs []uint64) error { - return svc.pageRepo.Reorder(selfID, pageIDs) -} - -func (svc *page) Create(page *types.Page) (p *types.Page, err error) { - validate := func() error { - if !svc.prmSvc.CanCreatePage(crmNamespace()) { - return errors.New("not allowed to create this page") - } - - if page.ModuleID > 0 { - if p, err = svc.pageRepo.FindByModuleID(page.ModuleID); err != nil { - return err - } else if p.ID > 0 { - return errors.New("Page for module already exists") - } - } - return nil - } - if err := validate(); err != nil { - return nil, err - } - return p, svc.db.Transaction(func() (err error) { - p, err = svc.pageRepo.Create(page) - return - }) -} - -func (svc *page) Update(page *types.Page) (p *types.Page, err error) { - validate := func() error { - if page.ID == 0 { - return errors.New("Error when saving page, invalid ID") - } else if p, err = svc.pageRepo.FindByID(page.ID); err != nil { - return errors.Wrap(err, "Error while loading page for update") - } else { - if !svc.prmSvc.CanUpdatePage(p) { - return errors.New("not allowed to update this page") - } - } - - if page.ModuleID > 0 { - if p, err = svc.pageRepo.FindByModuleID(page.ModuleID); err != nil { - return err - } else if p.ID > 0 && page.ID != p.ID { - return errors.New("Page for module already exists") - } - } - return nil - } - if err := validate(); err != nil { - return nil, err - } - return p, svc.db.Transaction(func() (err error) { - p, err = svc.pageRepo.Update(page) - return - }) -} - -func (svc *page) DeleteByID(ID uint64) error { - if p, err := svc.pageRepo.FindByID(ID); err != nil { - return errors.Wrap(err, "could not delete page") - } else if !svc.prmSvc.CanDeletePage(p) { - return errors.New("not allowed to delete this page") - } - - return svc.pageRepo.DeleteByID(ID) -} diff --git a/crm/internal/service/page_test.go b/crm/internal/service/page_test.go deleted file mode 100644 index d0a221447..000000000 --- a/crm/internal/service/page_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// +build integration - -package service - -import ( - "context" - "testing" - - "github.com/davecgh/go-spew/spew" - "github.com/pkg/errors" - - "github.com/crusttech/crust/crm/types" - "github.com/crusttech/crust/internal/auth" - "github.com/crusttech/crust/internal/test" - systemTypes "github.com/crusttech/crust/system/types" -) - -func TestPage(t *testing.T) { - ctx := context.WithValue(context.Background(), "testing", true) - - user := &systemTypes.User{ - ID: 1337, - Name: "John Crm Doe", - Username: "johndoe", - } - - // Set Identity (required for permission checks). - ctx = auth.SetIdentityToContext(ctx, user) - - repository := Page().With(ctx) - - // the page object we're working with - page := &types.Page{ - Title: "Test", - ModuleID: 123, - } - (&page.Blocks).Scan([]byte("[]")) - - prevPageCount := 0 - - { - { - m, err := repository.Update(page) - test.Assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) - test.Assert(t, err != nil, "Expected error when updating invalid content") - } - - // create page - m, err := repository.Create(page) - test.Assert(t, err == nil, "Error when creating page: %+v", err) - test.Assert(t, m.ID > 0, "Expected auto generated ID") - - page.SelfID = m.ID - - { - _, err := repository.Create(page) - test.Assert(t, err != nil, "%+v", errors.Errorf("Expected error when creating duplicate moduleID page")) - } - - { - page.ModuleID = 0 - _, err := repository.Create(page) - test.Assert(t, err == nil, "Unexpected error when creating page, %+v", err) - } - { - _, err := repository.Create(page) - test.Assert(t, err == nil, "Unexpected error when creating page, %+v", err) - } - - // fetch created page - { - ms, err := repository.FindByID(m.ID) - test.Assert(t, err == nil, "Error when retrieving page by id: %+v", err) - test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %+v", errors.Errorf("%d != %d", m.ID, ms.ID)) - test.Assert(t, ms.Title == m.Title, "Expected Title from database to match, %+v", errors.Errorf("%s != %s", m.Title, ms.Title)) - } - - // update created page - { - m.Title = "Updated test" - _, err := repository.Update(m) - test.Assert(t, err == nil, "Error when updating page, %+v", err) - } - - // re-fetch page - { - ms, err := repository.FindByID(m.ID) - test.Assert(t, err == nil, "Error when retrieving page by id: %+v", err) - test.Assert(t, ms.ID == m.ID, "re-fetch: Expected ID from database to match, %d != %d", m.ID, ms.ID) - test.Assert(t, ms.Title == m.Title, "Expected Title from database to match, %s != %s", m.Title, ms.Title) - } - - // re-fetch page with moduleID - { - ms, err := repository.FindByModuleID(m.ModuleID) - test.Assert(t, err == nil, "Error when retrieving page by id: %+v", err) - test.Assert(t, ms.ID == m.ID, "fetch-module: Expected ID from database to match, %d != %d", m.ID, ms.ID) - test.Assert(t, ms.Title == m.Title, "Expected Title from database to match, %s != %s", m.Title, ms.Title) - } - - // fetch all pages - { - ms, err := repository.FindBySelfID(0) - test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) - test.Assert(t, len(ms) >= 1, "Expected at least one page, got %d", len(ms)) - prevPageCount = len(ms) - } - - // fetch all record pages - { - ms, err := repository.FindRecordPages() - test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) - test.Assert(t, len(ms) >= 1, "Expected at least one page, got %d", len(ms)) - prevPageCount = len(ms) - } - - // fetch all pages - { - ms, err := repository.FindBySelfID(m.ID) - test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) - test.Assert(t, len(ms) == 2, "Expected two pages with selfID=%d, got %v", m.ID, spew.Sdump(ms)) - prevPageCount = len(ms) - - parent := m.ID - ids := []uint64{ms[0].ID, ms[1].ID} - - { - err := repository.Reorder(parent, ids) - test.Assert(t, err == nil, "Error when reordering pages: %+v", err) - - ms, err = repository.FindBySelfID(m.ID) - test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) - test.Assert(t, len(ms) == 2, "Expected two pages with selfID=%d, got %v", m.ID, spew.Sdump(ms)) - test.Assert(t, ms[0].Weight < ms[1].Weight, "Expected ascending order, %+v", errors.Errorf("%d < %d", ms[0].Weight, ms[1].Weight)) - } - } - - // re-fetch page - { - err := repository.DeleteByID(m.ID) - test.Assert(t, err == nil, "Error when deleting page by id: %+v", err) - } - - // fetch all pages - { - ms, err := repository.FindBySelfID(0) - test.Assert(t, err == nil, "Error when retrieving pages: %+v", err) - test.Assert(t, len(ms) < prevPageCount, "Expected pages count to decrease after deletion, %d < %d", len(ms), prevPageCount) - } - } - -} diff --git a/crm/internal/service/permissions_test.go b/crm/internal/service/permissions_test.go deleted file mode 100644 index add7de9f0..000000000 --- a/crm/internal/service/permissions_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// +build integration - -package service - -import ( - "context" - "testing" - - "github.com/crusttech/crust/crm/internal/repository" - "github.com/crusttech/crust/crm/types" - "github.com/crusttech/crust/internal/auth" - "github.com/crusttech/crust/internal/rules" - . "github.com/crusttech/crust/internal/test" - systemService "github.com/crusttech/crust/system/service" - systemTypes "github.com/crusttech/crust/system/types" -) - -func TestPermissions(t *testing.T) { - var err error - ctx := context.WithValue(context.Background(), "testing", true) - { - user := &systemTypes.User{ID: 1337} - ctx = auth.SetIdentityToContext(ctx, auth.NewIdentity(user.Identity())) - } - - // Create user with role and add it to context. - userSvc := systemService.TestUser(t, ctx) - user := &systemTypes.User{ - Name: "John Crm Doe", - Username: "johndoe", - } - - _, err = userSvc.Create(user) - NoError(t, err, "expected no error creating user, got %+v", err) - - ctx = auth.SetIdentityToContext(ctx, user) - - roleSvc := systemService.TestRole(t, ctx) - role := &systemTypes.Role{ - Name: "Test role v1", - } - role, err = roleSvc.Create(role) - NoError(t, err, "expected no error creating role, got %+v", err) - - err = roleSvc.MemberAdd(role.ID, user.ID) - NoError(t, err, "expected no error adding user to role, got %+v", err) - - // Insert `grant` permission for `compose`. - { - db := repository.DB(ctx) - resources := rules.NewResources(ctx, db) - - list := []rules.Rule{ - rules.Rule{Resource: types.PermissionResource, Operation: "grant", Value: rules.Allow}, - } - - err := resources.Grant(role.ID, list) - NoError(t, err, "expected no error, got %+v", err) - } - - // Generate services. - cleanContext := auth.SetIdentityToContext(context.Background(), user) - permissionsSvc := Permissions().With(cleanContext) - systemRulesSvc := systemService.TestRules(t, ctx) - - // Test `access` to compose service. - ret := permissionsSvc.CanAccess() - Assert(t, ret == false, "expected CanAccess == false, got %+v", ret) - - // Add `access` to compose service. - list := []rules.Rule{ - rules.Rule{Resource: types.PermissionResource, Operation: "access", Value: rules.Allow}, - } - _, err = systemRulesSvc.Update(role.ID, list) - NoError(t, err, "expected no error, got %+v", err) - - // Test `access` to compose service. - ret = permissionsSvc.CanAccess() - Assert(t, ret == true, "expected CanAccess == true, got %+v", ret) -} diff --git a/crm/internal/service/record.go b/crm/internal/service/record.go deleted file mode 100644 index 249d5e6a1..000000000 --- a/crm/internal/service/record.go +++ /dev/null @@ -1,278 +0,0 @@ -package service - -import ( - "context" - "strconv" - "time" - - "github.com/pkg/errors" - "github.com/titpetric/factory" - - "github.com/crusttech/crust/crm/internal/repository" - "github.com/crusttech/crust/crm/types" - "github.com/crusttech/crust/internal/auth" - - systemService "github.com/crusttech/crust/system/service" -) - -type ( - record struct { - db *factory.DB - ctx context.Context - - prmSvc PermissionsService - userSvc systemService.UserService - - repository repository.RecordRepository - moduleRepo repository.ModuleRepository - } - - RecordService interface { - With(ctx context.Context) RecordService - - FindByID(recordID uint64) (*types.Record, error) - - Report(moduleID uint64, metrics, dimensions, filter string) (interface{}, error) - Find(moduleID uint64, filter string, sort string, page int, perPage int) (*repository.FindResponse, error) - - Create(record *types.Record) (*types.Record, error) - Update(record *types.Record) (*types.Record, error) - - DeleteByID(recordID uint64) error - - // Fields(module *types.Module, record *types.Record) ([]*types.RecordValue, error) - } -) - -func Record() RecordService { - return (&record{ - prmSvc: DefaultPermissions, - userSvc: systemService.DefaultUser, - }).With(context.Background()) -} - -func (svc *record) With(ctx context.Context) RecordService { - db := repository.DB(ctx) - return &record{ - db: db, - ctx: ctx, - - prmSvc: svc.prmSvc.With(ctx), - userSvc: systemService.User(ctx), - - repository: repository.Record(ctx, db), - moduleRepo: repository.Module(ctx, db), - } -} - -func (svc *record) FindByID(recordID uint64) (r *types.Record, err error) { - err = svc.db.Transaction(func() (err error) { - if r, err = svc.repository.FindByID(recordID); err != nil { - return - } else if !svc.prmSvc.CanReadRecord(r) { - return errors.New("not allowed to access this record") - } - - if err = svc.preloadValues(r); err != nil { - return - } - - return - }) - - return r, errors.Wrap(err, "unable to find record") -} - -func (svc *record) Report(moduleID uint64, metrics, dimensions, filter string) (out interface{}, err error) { - var module *types.Module - - err = svc.db.Transaction(func() (err error) { - if module, err = svc.moduleRepo.FindByID(moduleID); err != nil { - return - } else if !svc.prmSvc.CanReadRecord(module) { - return errors.New("not allowed to access this record") - } - - out, err = svc.repository.Report(module, metrics, dimensions, filter) - return - }) - - return out, errors.Wrap(err, "unable to build a report") -} - -func (svc *record) Find(moduleID uint64, filter, sort string, page, perPage int) (rsp *repository.FindResponse, err error) { - var module *types.Module - - err = svc.db.Transaction(func() (err error) { - if module, err = svc.moduleRepo.FindByID(moduleID); err != nil { - return - } else if !svc.prmSvc.CanReadRecord(module) { - return errors.New("not allowed to access this record") - } - - if rsp, err = svc.repository.Find(module, filter, sort, page, perPage); err != nil { - return - } - - if err = svc.preloadValues(rsp.Records...); err != nil { - return - } - - return - }) - - return rsp, errors.Wrap(err, "unable to find records") - -} - -func (svc *record) Create(in *types.Record) (record *types.Record, err error) { - var module *types.Module - - err = svc.db.Transaction(func() (err error) { - if module, err = svc.moduleRepo.FindByID(in.ModuleID); err != nil { - return - } else if !svc.prmSvc.CanCreateRecord(module) { - return errors.New("not allowed to create records for this module") - } - - if err = svc.sanitizeValues(module, in.Values); err != nil { - return - } - - in.OwnedBy = auth.GetIdentityFromContext(svc.ctx).Identity() - in.CreatedBy = in.OwnedBy - in.CreatedAt = time.Now() - if record, err = svc.repository.Create(in); err != nil { - return - } - - if err = svc.repository.UpdateValues(record.ID, in.Values); err != nil { - return - } - - if err = svc.preloadValues(record); err != nil { - return - } - - return - }) - - return record, errors.Wrap(err, "unable to create record") -} - -func (svc *record) Update(updated *types.Record) (record *types.Record, err error) { - var module *types.Module - - err = svc.db.Transaction(func() (err error) { - if updated.ID == 0 { - return errors.New("invalid record ID") - } - - if record, err = svc.repository.FindByID(updated.ID); err != nil { - return errors.Wrap(err, "nonexistent record") - } else if !svc.prmSvc.CanUpdateRecord(record) { - return errors.New("not allowed to update this record") - } - - if module, err = svc.moduleRepo.FindByID(updated.ModuleID); err != nil { - return - } - - if err = svc.sanitizeValues(module, updated.Values); err != nil { - return - } - - now := time.Now() - record.UpdatedAt = &now - record.UpdatedBy = auth.GetIdentityFromContext(svc.ctx).Identity() - - if record, err = svc.repository.Update(record); err != nil { - return - } - - if err = svc.repository.UpdateValues(record.ID, updated.Values); err != nil { - return - } - - return - }) - - return record, errors.Wrap(err, "unable to update record") -} - -// func (s *record) Fields(module *types.Module, record *types.Record) ([]*types.RecordValue, error) { -// return s.repository.Fields(module, record) -// } - -func (svc *record) DeleteByID(ID uint64) (err error) { - err = svc.db.Transaction(func() (err error) { - var record *types.Record - - if record, err = svc.repository.FindByID(ID); err != nil { - return errors.Wrap(err, "nonexistent record") - } - - now := time.Now() - record.DeletedAt = &now - record.DeletedBy = auth.GetIdentityFromContext(svc.ctx).Identity() - - if err = svc.repository.Delete(record); err != nil { - return - } - - if err = svc.repository.DeleteValues(record); err != nil { - return - } - - return - }) - - return errors.Wrap(err, "unable to delete record") -} - -// Validates and filters record values -func (svc *record) sanitizeValues(module *types.Module, values types.RecordValueSet) (err error) { - // Make sure there are no multi values in a non-multi value fields - err = module.Fields.Walk(func(field *types.ModuleField) error { - if !field.Multi && len(values.FilterByName(field.Name)) > 1 { - return errors.Errorf("more than one value for a single-value field %q", field.Name) - } - return nil - }) - - if err != nil { - return - } - - var places = map[string]uint{} - // var has bool - - return values.Walk(func(value *types.RecordValue) (err error) { - var field = module.Fields.FindByName(value.Name) - if field == nil { - return errors.Errorf("no such field %q", value.Name) - } - - if field.IsRef() { - if value.Ref, err = strconv.ParseUint(value.Value, 10, 64); err != nil { - return err - } - } - - value.Place = places[field.Name] - places[field.Name]++ - - return nil - }) -} - -func (svc *record) preloadValues(rr ...*types.Record) error { - if rvs, err := svc.repository.LoadValues(types.RecordSet(rr).IDs()...); err != nil { - return err - } else { - return types.RecordSet(rr).Walk(func(r *types.Record) error { - r.Values = rvs.FilterByRecordID(r.ID) - return nil - }) - } -} diff --git a/crm/internal/service/trigger.go b/crm/internal/service/trigger.go deleted file mode 100644 index ecd2cc54d..000000000 --- a/crm/internal/service/trigger.go +++ /dev/null @@ -1,120 +0,0 @@ -package service - -import ( - "context" - - "github.com/pkg/errors" - "github.com/titpetric/factory" - - "github.com/crusttech/crust/crm/internal/repository" - "github.com/crusttech/crust/crm/types" -) - -type ( - trigger struct { - db *factory.DB - ctx context.Context - - prmSvc PermissionsService - - triggerRepo repository.TriggerRepository - moduleRepo repository.ModuleRepository - } - - TriggerService interface { - With(ctx context.Context) TriggerService - - FindByID(triggerID uint64) (*types.Trigger, error) - Find(filter types.TriggerFilter) (set types.TriggerSet, err error) - - Create(trigger *types.Trigger) (*types.Trigger, error) - Update(trigger *types.Trigger) (*types.Trigger, error) - DeleteByID(triggerID uint64) error - } -) - -func Trigger() TriggerService { - return (&trigger{ - prmSvc: DefaultPermissions}).With(context.Background()) -} - -func (svc *trigger) With(ctx context.Context) TriggerService { - db := repository.DB(ctx) - return &trigger{ - db: db, - ctx: ctx, - - prmSvc: svc.prmSvc.With(ctx), - - triggerRepo: repository.Trigger(ctx, db), - moduleRepo: repository.Module(ctx, db), - } -} - -func (svc *trigger) FindByID(id uint64) (t *types.Trigger, err error) { - if t, err = svc.triggerRepo.FindByID(id); err != nil { - return - } else if !svc.prmSvc.CanReadTrigger(t) { - return nil, errors.New("not allowed to access this trigger") - } - - return -} - -func (svc *trigger) Find(filter types.TriggerFilter) (tt types.TriggerSet, err error) { - if tt, err = svc.triggerRepo.Find(filter); err != nil { - return nil, err - } else { - return tt.Filter(func(m *types.Trigger) (bool, error) { - return svc.prmSvc.CanReadTrigger(m), nil - }) - } -} - -func (svc *trigger) Create(trigger *types.Trigger) (p *types.Trigger, err error) { - if !svc.prmSvc.CanCreateTrigger(crmNamespace()) { - return nil, errors.New("not allowed to create this trigger") - } - - return p, svc.db.Transaction(func() (err error) { - p, err = svc.triggerRepo.Create(trigger) - return - }) -} - -func (svc *trigger) Update(trigger *types.Trigger) (t *types.Trigger, err error) { - validate := func() error { - if trigger.ID == 0 { - return errors.New("Error updating trigger: invalid ID") - } else if t, err = svc.triggerRepo.FindByID(trigger.ID); err != nil { - return errors.Wrap(err, "Error while loading trigger for update") - } else { - if !svc.prmSvc.CanUpdateModule(t) { - return errors.New("not allowed to update this trigger") - } - - trigger.CreatedAt = t.CreatedAt - } - - return nil - } - - if err := validate(); err != nil { - return nil, err - } - - return t, svc.db.Transaction(func() (err error) { - t, err = svc.triggerRepo.Update(trigger) - return - }) -} - -func (svc *trigger) DeleteByID(ID uint64) error { - if t, err := svc.triggerRepo.FindByID(ID); err != nil { - return errors.Wrap(err, "could not delete trigger") - } else if !svc.prmSvc.CanDeleteTrigger(t) { - return errors.New("not allowed to delete this trigger") - } - - return svc.triggerRepo.DeleteByID(ID) -} diff --git a/crm/rest/attachment.go b/crm/rest/attachment.go deleted file mode 100644 index 5b4bb1b37..000000000 --- a/crm/rest/attachment.go +++ /dev/null @@ -1,198 +0,0 @@ -package rest - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "time" - - "github.com/crusttech/crust/crm/internal/service" - "github.com/crusttech/crust/crm/rest/request" - "github.com/crusttech/crust/crm/types" - "github.com/crusttech/crust/internal/auth" - - "github.com/pkg/errors" -) - -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"` - } - - file struct { - *types.Attachment - content io.ReadSeeker - download bool - } - - Attachment struct { - attachment service.AttachmentService - } -) - -func (Attachment) New() *Attachment { - 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) { - f := types.AttachmentFilter{ - Kind: r.Kind, - ModuleID: r.ModuleID, - RecordID: r.RecordID, - FieldName: r.FieldName, - // Filter: r.Filter, - PerPage: r.PerPage, - Page: r.Page, - // Sort: r.Sort, - } - - aa, meta, err := ctrl.attachment.Find(f) - 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 - } -} - -func (ctrl Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) { - if err := ctrl.isAccessible(r.AttachmentID, r.UserID, r.Sign); err != nil { - return nil, err - } - - return ctrl.serve(ctx, 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 { - return nil, err - } - - return ctrl.serve(ctx, r.AttachmentID, true, false) -} - -func (ctrl Attachment) isAccessible(attachmentID, userID uint64, signature string) error { - if userID == 0 { - return errors.New("missing or invalid user ID") - } - - if attachmentID == 0 { - return errors.New("missing or invalid attachment ID") - } - - if auth.DefaultSigner.Verify(signature, userID, attachmentID) { - return errors.New("missing or invalid signature") - } - - return nil -} - -func (ctrl Attachment) serve(ctx context.Context, ID uint64, preview, download bool) (interface{}, error) { - return func(w http.ResponseWriter, req *http.Request) { - att, err := ctrl.attachment.With(ctx).FindByID(ID) - if err != nil { - // Simplify error handling for now - w.WriteHeader(http.StatusNotFound) - return - } - - var fh io.ReadSeeker - - if preview { - fh, err = ctrl.attachment.OpenPreview(att) - } else { - fh, err = ctrl.attachment.OpenOriginal(att) - } - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - name := url.QueryEscape(att.Name) - - if download { - w.Header().Add("Content-Disposition", "attachment; filename="+name) - } else { - w.Header().Add("Content-Disposition", "inline; filename="+name) - } - - http.ServeContent(w, req, name, att.CreatedAt, fh) - }, nil -} - -func makeAttachmentPayload(a *types.Attachment, userID uint64) *attachmentPayload { - if a == nil { - return nil - } - - var ( - signParams = fmt.Sprintf("?sign=%s&userID=%d", auth.DefaultSigner.Sign(userID, a.ID), userID) - - preview string - baseURL = fmt.Sprintf("/attachment/%s/%d/", a.Kind, a.ID) - ) - if a.Meta.Preview != nil { - var ext = a.Meta.Preview.Extension - if ext == "" { - ext = "jpg" - } - - 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, - } -} - -func (f *file) Download() bool { - return f.download -} - -func (f *file) Name() string { - return f.Attachment.Name -} - -func (f *file) ModTime() time.Time { - return f.Attachment.CreatedAt -} - -func (f *file) Content() io.ReadSeeker { - return f.content -} - -func (f *file) Valid() bool { - return f.content != nil -} diff --git a/crm/rest/chart.go b/crm/rest/chart.go deleted file mode 100644 index 4dc54af80..000000000 --- a/crm/rest/chart.go +++ /dev/null @@ -1,58 +0,0 @@ -package rest - -import ( - "context" - - "github.com/titpetric/factory/resputil" - - "github.com/crusttech/crust/crm/internal/service" - "github.com/crusttech/crust/crm/rest/request" - "github.com/crusttech/crust/crm/types" - - "github.com/pkg/errors" -) - -var _ = errors.Wrap - -type Chart struct { - module service.ModuleService - chart service.ChartService -} - -func (Chart) New() *Chart { - return &Chart{ - module: service.DefaultModule, - chart: service.DefaultChart, - } -} - -func (ctrl *Chart) List(ctx context.Context, r *request.ChartList) (interface{}, error) { - return ctrl.chart.With(ctx).Find() -} - -func (ctrl *Chart) Create(ctx context.Context, r *request.ChartCreate) (interface{}, error) { - chart := &types.Chart{ - Name: r.Name, - Config: r.Config, - } - - return ctrl.chart.With(ctx).Create(chart) -} - -func (ctrl *Chart) Read(ctx context.Context, r *request.ChartRead) (interface{}, error) { - return ctrl.chart.With(ctx).FindByID(r.ChartID) -} - -func (ctrl *Chart) Update(ctx context.Context, r *request.ChartUpdate) (interface{}, error) { - chart := &types.Chart{ - ID: r.ChartID, - Name: r.Name, - Config: r.Config, - } - - return ctrl.chart.With(ctx).Update(chart) -} - -func (ctrl *Chart) Delete(ctx context.Context, r *request.ChartDelete) (interface{}, error) { - return resputil.OK(), ctrl.chart.With(ctx).DeleteByID(r.ChartID) -} diff --git a/crm/rest/module.go b/crm/rest/module.go deleted file mode 100644 index 591b15f3a..000000000 --- a/crm/rest/module.go +++ /dev/null @@ -1,56 +0,0 @@ -package rest - -import ( - "context" - - "github.com/titpetric/factory/resputil" - - "github.com/crusttech/crust/crm/internal/service" - "github.com/crusttech/crust/crm/rest/request" - "github.com/crusttech/crust/crm/types" -) - -type ( - Module struct { - module service.ModuleService - record service.RecordService - } -) - -func (Module) New() *Module { - return &Module{ - module: service.DefaultModule, - record: service.DefaultRecord, - } -} - -func (ctrl *Module) List(ctx context.Context, r *request.ModuleList) (interface{}, error) { - return ctrl.module.With(ctx).Find() -} - -func (ctrl *Module) Read(ctx context.Context, r *request.ModuleRead) (interface{}, error) { - return ctrl.module.With(ctx).FindByID(r.ModuleID) -} - -func (ctrl *Module) Delete(ctx context.Context, r *request.ModuleDelete) (interface{}, error) { - return resputil.OK(), ctrl.module.With(ctx).DeleteByID(r.ModuleID) -} - -func (ctrl *Module) Create(ctx context.Context, r *request.ModuleCreate) (interface{}, error) { - item := &types.Module{ - Name: r.Name, - Fields: r.Fields, - Meta: r.Meta, - } - return ctrl.module.With(ctx).Create(item) -} - -func (ctrl *Module) Update(ctx context.Context, r *request.ModuleUpdate) (interface{}, error) { - item := &types.Module{ - ID: r.ModuleID, - Name: r.Name, - Fields: r.Fields, - Meta: r.Meta, - } - return ctrl.module.With(ctx).Update(item) -} diff --git a/crm/rest/page.go b/crm/rest/page.go deleted file mode 100644 index b0b83ba32..000000000 --- a/crm/rest/page.go +++ /dev/null @@ -1,99 +0,0 @@ -package rest - -import ( - "context" - - "github.com/titpetric/factory/resputil" - - "github.com/crusttech/crust/crm/internal/service" - "github.com/crusttech/crust/crm/rest/request" - "github.com/crusttech/crust/crm/types" - "github.com/crusttech/crust/internal/auth" - "github.com/crusttech/crust/internal/payload" -) - -type ( - Page struct { - page service.PageService - attachment service.AttachmentService - } -) - -func (Page) New() *Page { - return &Page{ - page: service.DefaultPage, - attachment: service.DefaultAttachment, - } -} - -func (ctrl *Page) List(ctx context.Context, r *request.PageList) (interface{}, error) { - if r.SelfID > 0 { - return ctrl.page.With(ctx).FindBySelfID(r.SelfID) - } else { - return ctrl.page.With(ctx).Find() - } -} - -func (ctrl *Page) Tree(ctx context.Context, r *request.PageTree) (interface{}, error) { - return ctrl.page.With(ctx).Tree() -} - -func (ctrl *Page) Create(ctx context.Context, r *request.PageCreate) (interface{}, error) { - p := &types.Page{ - SelfID: r.SelfID, - ModuleID: r.ModuleID, - Title: r.Title, - Description: r.Description, - Blocks: r.Blocks, - Visible: r.Visible, - } - return ctrl.page.With(ctx).Create(p) -} - -func (ctrl *Page) Read(ctx context.Context, r *request.PageRead) (interface{}, error) { - return ctrl.page.With(ctx).FindByID(r.PageID) -} - -func (ctrl *Page) Reorder(ctx context.Context, r *request.PageReorder) (interface{}, error) { - return resputil.OK(), ctrl.page.With(ctx).Reorder(r.SelfID, payload.ParseUInt64s(r.PageIDs)) -} - -func (ctrl *Page) Update(ctx context.Context, r *request.PageUpdate) (interface{}, error) { - p := &types.Page{ - ID: r.PageID, - SelfID: r.SelfID, - ModuleID: r.ModuleID, - Title: r.Title, - Description: r.Description, - Blocks: r.Blocks, - Visible: r.Visible, - } - return ctrl.page.With(ctx).Update(p) -} - -func (ctrl *Page) Delete(ctx context.Context, r *request.PageDelete) (interface{}, error) { - return resputil.OK(), ctrl.page.With(ctx).DeleteByID(r.PageID) -} - -func (ctrl *Page) Upload(ctx context.Context, r *request.PageUpload) (interface{}, error) { - // @todo [SECURITY] check if attachments can be added to this page - file, err := r.Upload.Open() - if err != nil { - return nil, err - } - - defer file.Close() - - a, err := ctrl.attachment.With(ctx).CreatePageAttachment( - r.Upload.Filename, - r.Upload.Size, - file, - r.PageID, - ) - - if err != nil { - return nil, err - } - - return makeAttachmentPayload(a, auth.GetIdentityFromContext(ctx).Identity()), nil -} diff --git a/crm/rest/record.go b/crm/rest/record.go deleted file mode 100644 index f8c80b9ad..000000000 --- a/crm/rest/record.go +++ /dev/null @@ -1,80 +0,0 @@ -package rest - -import ( - "context" - - "github.com/titpetric/factory/resputil" - - "github.com/crusttech/crust/crm/internal/service" - "github.com/crusttech/crust/crm/rest/request" - "github.com/crusttech/crust/crm/types" - "github.com/crusttech/crust/internal/auth" - - "github.com/pkg/errors" -) - -var _ = errors.Wrap - -type Record struct { - record service.RecordService - attachment service.AttachmentService -} - -func (Record) New() *Record { - return &Record{ - record: service.DefaultRecord, - attachment: service.DefaultAttachment, - } -} - -func (ctrl *Record) Report(ctx context.Context, r *request.RecordReport) (interface{}, error) { - return ctrl.record.With(ctx).Report(r.ModuleID, r.Metrics, r.Dimensions, r.Filter) -} - -func (ctrl *Record) List(ctx context.Context, r *request.RecordList) (interface{}, error) { - return ctrl.record.With(ctx).Find(r.ModuleID, r.Filter, r.Sort, r.Page, r.PerPage) -} - -func (ctrl *Record) Read(ctx context.Context, r *request.RecordRead) (interface{}, error) { - return ctrl.record.With(ctx).FindByID(r.RecordID) -} - -func (ctrl *Record) Create(ctx context.Context, r *request.RecordCreate) (interface{}, error) { - return ctrl.record.With(ctx).Create(&types.Record{ModuleID: r.ModuleID, Values: r.Values}) -} - -func (ctrl *Record) Update(ctx context.Context, r *request.RecordUpdate) (interface{}, error) { - return ctrl.record.With(ctx).Update(&types.Record{ - ID: r.RecordID, - ModuleID: r.ModuleID, - Values: r.Values}) -} - -func (ctrl *Record) Delete(ctx context.Context, r *request.RecordDelete) (interface{}, error) { - return resputil.OK(), ctrl.record.With(ctx).DeleteByID(r.RecordID) -} - -func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interface{}, error) { - // @todo [SECURITY] check if attachments can be added to this page - file, err := r.Upload.Open() - if err != nil { - return nil, err - } - - defer file.Close() - - a, err := ctrl.attachment.With(ctx).CreateRecordAttachment( - r.Upload.Filename, - r.Upload.Size, - file, - r.ModuleID, - r.RecordID, - r.FieldName, - ) - - if err != nil { - return nil, err - } - - return makeAttachmentPayload(a, auth.GetIdentityFromContext(ctx).Identity()), nil -} diff --git a/crm/rest/trigger.go b/crm/rest/trigger.go deleted file mode 100644 index d91a55833..000000000 --- a/crm/rest/trigger.go +++ /dev/null @@ -1,74 +0,0 @@ -package rest - -import ( - "context" - - "github.com/titpetric/factory/resputil" - - "github.com/crusttech/crust/crm/internal/service" - "github.com/crusttech/crust/crm/rest/request" - "github.com/crusttech/crust/crm/types" - - "github.com/pkg/errors" -) - -var _ = errors.Wrap - -type Trigger struct { - trigger service.TriggerService -} - -func (Trigger) New() *Trigger { - return &Trigger{ - trigger: service.DefaultTrigger, - } -} - -func (ctrl *Trigger) List(ctx context.Context, r *request.TriggerList) (interface{}, error) { - filter := types.TriggerFilter{} - - if r.ModuleID > 0 { - filter.ModuleID = r.ModuleID - } - - return ctrl.trigger.With(ctx).Find(filter) -} - -func (ctrl *Trigger) Create(ctx context.Context, r *request.TriggerCreate) (interface{}, error) { - trigger := &types.Trigger{ - Name: r.Name, - Actions: r.Actions, - Enabled: r.Enabled, - Source: r.Source, - } - - if r.ModuleID > 0 { - trigger.ModuleID = r.ModuleID - } - - return ctrl.trigger.With(ctx).Create(trigger) -} - -func (ctrl *Trigger) Read(ctx context.Context, r *request.TriggerRead) (interface{}, error) { - return ctrl.trigger.With(ctx).FindByID(r.TriggerID) -} - -func (ctrl *Trigger) Update(ctx context.Context, r *request.TriggerUpdate) (interface{}, error) { - svc := ctrl.trigger.With(ctx) - - if trigger, err := svc.FindByID(r.TriggerID); err != nil { - return nil, err - } else { - trigger.Name = r.Name - trigger.Actions = r.Actions - trigger.Enabled = r.Enabled - trigger.Source = r.Source - trigger.ModuleID = r.ModuleID - - return svc.Update(trigger) - } -} - -func (ctrl *Trigger) Delete(ctx context.Context, r *request.TriggerDelete) (interface{}, error) { - return resputil.OK(), ctrl.trigger.With(ctx).DeleteByID(r.TriggerID) -} diff --git a/crm/types/namespace.go b/crm/types/namespace.go deleted file mode 100644 index 3a60ecd3f..000000000 --- a/crm/types/namespace.go +++ /dev/null @@ -1,20 +0,0 @@ -package types - -import ( - "github.com/crusttech/crust/internal/rules" -) - -type ( - Namespace struct { - ID uint64 `json:"id,string" db:"id"` - } -) - -const ( - NamespaceCRM uint64 = 10000000 -) - -// Resource returns a system resource ID for this type -func (n Namespace) PermissionResource() rules.Resource { - return NamespacePermissionResource.AppendID(n.ID) -} diff --git a/docs/crm/README.md b/docs/compose/README.md similarity index 56% rename from docs/crm/README.md rename to docs/compose/README.md index 7d2224c2f..9da4d4e57 100644 --- a/docs/crm/README.md +++ b/docs/compose/README.md @@ -2,10 +2,11 @@ | Method | Endpoint | Purpose | | ------ | -------- | ------- | -| `GET` | `/attachment/{kind}/` | List, filter all page attachments | -| `GET` | `/attachment/{kind}/{attachmentID}` | Attachment details | -| `GET` | `/attachment/{kind}/{attachmentID}/original/{name}` | Serves attached file | -| `GET` | `/attachment/{kind}/{attachmentID}/preview.{ext}` | Serves preview of an attached file | +| `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 | ## List, filter all page attachments @@ -13,21 +14,22 @@ | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/attachment/{kind}/` | HTTP/S | GET | Client ID, Session ID | +| `/namespace/{namespaceID}/attachment/{kind}/` | HTTP/S | GET | Client ID, Session ID | #### Request parameters | 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 | | perPage | uint | GET | Returned items per page (default 50) | N/A | NO | -| sign | string | GET | Signature | N/A | YES | -| userID | uint64 | GET | User ID | N/A | YES | +| sign | string | GET | Signature | N/A | NO | +| userID | uint64 | GET | User ID | N/A | NO | | kind | string | PATH | Attachment kind | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Attachment details @@ -35,7 +37,7 @@ | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/attachment/{kind}/{attachmentID}` | HTTP/S | GET | Client ID, Session ID | +| `/namespace/{namespaceID}/attachment/{kind}/{attachmentID}` | HTTP/S | GET | Client ID, Session ID | #### Request parameters @@ -43,8 +45,27 @@ | --------- | ---- | ------ | ----------- | ------- | --------- | | attachmentID | uint64 | PATH | Attachment ID | N/A | YES | | kind | string | PATH | Attachment kind | N/A | YES | -| sign | string | GET | Signature | N/A | YES | -| userID | uint64 | GET | User ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| sign | string | GET | Signature | N/A | NO | +| userID | uint64 | GET | User ID | N/A | NO | + +## 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 | NO | +| userID | uint64 | GET | User ID | N/A | NO | ## Serves attached file @@ -52,18 +73,19 @@ | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/attachment/{kind}/{attachmentID}/original/{name}` | HTTP/S | GET | Client ID, Session ID | +| `/namespace/{namespaceID}/attachment/{kind}/{attachmentID}/original/{name}` | HTTP/S | GET | Client ID, Session ID | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | download | bool | GET | Force file download | N/A | NO | -| sign | string | GET | Signature | N/A | YES | -| userID | uint64 | GET | User ID | N/A | YES | +| sign | string | GET | Signature | N/A | NO | +| userID | uint64 | GET | User ID | N/A | NO | | attachmentID | uint64 | PATH | Attachment ID | N/A | YES | | name | string | PATH | File name | N/A | YES | | kind | string | PATH | Attachment kind | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Serves preview of an attached file @@ -71,7 +93,7 @@ | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/attachment/{kind}/{attachmentID}/preview.{ext}` | HTTP/S | GET | Client ID, Session ID | +| `/namespace/{namespaceID}/attachment/{kind}/{attachmentID}/preview.{ext}` | HTTP/S | GET | Client ID, Session ID | #### Request parameters @@ -80,8 +102,9 @@ | attachmentID | uint64 | PATH | Attachment ID | N/A | YES | | ext | string | PATH | Preview extension/format | N/A | YES | | kind | string | PATH | Attachment kind | N/A | YES | -| sign | string | GET | Signature | N/A | YES | -| userID | uint64 | GET | User ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | +| sign | string | GET | Signature | N/A | NO | +| userID | uint64 | GET | User ID | N/A | NO | --- @@ -92,32 +115,36 @@ | Method | Endpoint | Purpose | | ------ | -------- | ------- | -| `GET` | `/chart/` | List/read charts from module section | -| `POST` | `/chart/` | List/read charts from module section | -| `GET` | `/chart/{chartID}` | Read charts by ID from module section | -| `POST` | `/chart/{chartID}` | Add/update charts in module section | -| `DELETE` | `/chart/{chartID}` | Delete chart | +| `GET` | `/namespace/{namespaceID}/chart/` | List/read charts | +| `POST` | `/namespace/{namespaceID}/chart/` | List/read charts | +| `GET` | `/namespace/{namespaceID}/chart/{chartID}` | Read charts by ID | +| `POST` | `/namespace/{namespaceID}/chart/{chartID}` | Add/update charts | +| `DELETE` | `/namespace/{namespaceID}/chart/{chartID}` | Delete chart | -## List/read charts from module section +## List/read charts #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/chart/` | HTTP/S | GET | | +| `/namespace/{namespaceID}/chart/` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | +| query | string | GET | Search query to match against charts | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | -## List/read charts from module section +## List/read charts #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/chart/` | HTTP/S | POST | | +| `/namespace/{namespaceID}/chart/` | HTTP/S | POST | | #### Request parameters @@ -125,36 +152,40 @@ | --------- | ---- | ------ | ----------- | ------- | --------- | | config | sqlxTypes.JSONText | POST | Chart JSON | N/A | YES | | name | string | POST | Chart name | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | -## Read charts by ID from module section +## Read charts by ID #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/chart/{chartID}` | HTTP/S | GET | | +| `/namespace/{namespaceID}/chart/{chartID}` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | chartID | uint64 | PATH | Chart ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | -## Add/update charts in module section +## Add/update charts #### Method | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/chart/{chartID}` | HTTP/S | POST | | +| `/namespace/{namespaceID}/chart/{chartID}` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | chartID | uint64 | PATH | Chart ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | config | sqlxTypes.JSONText | POST | Chart JSON | N/A | YES | | name | string | POST | Chart name | N/A | YES | +| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO | ## Delete chart @@ -162,13 +193,14 @@ | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/chart/{chartID}` | HTTP/S | DELETE | | +| `/namespace/{namespaceID}/chart/{chartID}` | HTTP/S | DELETE | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | chartID | uint64 | PATH | Chart ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | --- @@ -177,15 +209,15 @@ # Modules -CRM module definitions +Compose module definitions | Method | Endpoint | Purpose | | ------ | -------- | ------- | -| `GET` | `/module/` | List modules | -| `POST` | `/module/` | Create module | -| `GET` | `/module/{moduleID}` | Read module | -| `POST` | `/module/{moduleID}` | Update module | -| `DELETE` | `/module/{moduleID}` | Delete module | +| `GET` | `/namespace/{namespaceID}/module/` | List modules | +| `POST` | `/namespace/{namespaceID}/module/` | Create module | +| `GET` | `/namespace/{namespaceID}/module/{moduleID}` | Read module | +| `POST` | `/namespace/{namespaceID}/module/{moduleID}` | Update module | +| `DELETE` | `/namespace/{namespaceID}/module/{moduleID}` | Delete module | ## List modules @@ -193,13 +225,16 @@ CRM module definitions | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/` | HTTP/S | GET | | +| `/namespace/{namespaceID}/module/` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | query | string | GET | Search query | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Create module @@ -207,7 +242,7 @@ CRM module definitions | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/` | HTTP/S | POST | | +| `/namespace/{namespaceID}/module/` | HTTP/S | POST | | #### Request parameters @@ -216,6 +251,7 @@ CRM module definitions | name | string | POST | Module Name | N/A | YES | | fields | types.ModuleFieldSet | POST | Fields JSON | N/A | YES | | meta | sqlxTypes.JSONText | POST | Module meta data | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Read module @@ -223,13 +259,14 @@ CRM module definitions | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}` | HTTP/S | GET | | +| `/namespace/{namespaceID}/module/{moduleID}` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | moduleID | uint64 | PATH | Module ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Update module @@ -237,16 +274,18 @@ CRM module definitions | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}` | HTTP/S | POST | | +| `/namespace/{namespaceID}/module/{moduleID}` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | moduleID | uint64 | PATH | Module ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | name | string | POST | Module Name | N/A | YES | | fields | types.ModuleFieldSet | POST | Fields JSON | N/A | YES | | meta | sqlxTypes.JSONText | POST | Module meta data | N/A | YES | +| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO | ## Delete module @@ -254,13 +293,109 @@ CRM module definitions | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}` | HTTP/S | DELETE | | +| `/namespace/{namespaceID}/module/{moduleID}` | HTTP/S | DELETE | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | moduleID | uint64 | PATH | Module ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | + +--- + + + + +# Namespaces + +| Method | Endpoint | Purpose | +| ------ | -------- | ------- | +| `GET` | `/namespace/` | List namespaces | +| `POST` | `/namespace/` | Create namespace | +| `GET` | `/namespace/{namespaceID}` | Read namespace | +| `POST` | `/namespace/{namespaceID}` | Update namespace | +| `DELETE` | `/namespace/{namespaceID}` | Delete namespace | + +## List namespaces + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| query | string | GET | Search query | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | + +## Create namespace + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/` | HTTP/S | POST | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| name | string | POST | Name | N/A | YES | +| slug | string | POST | Slug (url path part) | N/A | YES | +| enabled | bool | POST | Enabled | N/A | YES | +| meta | sqlxTypes.JSONText | POST | Meta data | N/A | YES | + +## Read namespace + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}` | HTTP/S | GET | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| namespaceID | uint64 | PATH | ID | N/A | YES | + +## Update namespace + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}` | HTTP/S | POST | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| namespaceID | uint64 | PATH | ID | N/A | YES | +| name | string | POST | Name | N/A | YES | +| slug | string | POST | Slug (url path part) | N/A | YES | +| enabled | bool | POST | Enabled | N/A | YES | +| meta | sqlxTypes.JSONText | POST | Meta data | N/A | YES | +| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO | + +## Delete namespace + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/namespace/{namespaceID}` | HTTP/S | DELETE | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| namespaceID | uint64 | PATH | ID | N/A | YES | --- @@ -269,13 +404,13 @@ CRM module definitions # Notifications -CRM Notifications +Compose Notifications | Method | Endpoint | Purpose | | ------ | -------- | ------- | -| `POST` | `/notification/email` | Send email from the CRM | +| `POST` | `/notification/email` | Send email from the Compose | -## Send email from the CRM +## Send email from the Compose #### Method @@ -300,18 +435,18 @@ CRM Notifications # Pages -CRM module pages +Compose pages | Method | Endpoint | Purpose | | ------ | -------- | ------- | -| `GET` | `/page/` | List available pages | -| `POST` | `/page/` | Create page | -| `GET` | `/page/{pageID}` | Get page details | -| `GET` | `/page/tree` | Get page all (non-record) pages, hierarchically | -| `POST` | `/page/{pageID}` | Update page | -| `POST` | `/page/{selfID}/reorder` | Reorder pages | -| `Delete` | `/page/{pageID}` | Delete page | -| `POST` | `/page/{pageID}/attachment` | Uploads attachment to page | +| `GET` | `/namespace/{namespaceID}/page/` | List available pages | +| `POST` | `/namespace/{namespaceID}/page/` | Create page | +| `GET` | `/namespace/{namespaceID}/page/{pageID}` | Get page details | +| `GET` | `/namespace/{namespaceID}/page/tree` | Get page all (non-record) pages, hierarchically | +| `POST` | `/namespace/{namespaceID}/page/{pageID}` | Update page | +| `POST` | `/namespace/{namespaceID}/page/{selfID}/reorder` | Reorder pages | +| `Delete` | `/namespace/{namespaceID}/page/{pageID}` | Delete page | +| `POST` | `/namespace/{namespaceID}/page/{pageID}/attachment` | Uploads attachment to page | ## List available pages @@ -319,13 +454,17 @@ CRM module pages | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/page/` | HTTP/S | GET | | +| `/namespace/{namespaceID}/page/` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | selfID | uint64 | GET | Parent page ID | N/A | NO | +| query | string | GET | Search query | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Create page @@ -333,7 +472,7 @@ CRM module pages | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/page/` | HTTP/S | POST | | +| `/namespace/{namespaceID}/page/` | HTTP/S | POST | | #### Request parameters @@ -345,6 +484,7 @@ CRM module pages | description | string | POST | Description | N/A | NO | | visible | bool | POST | Visible in navigation | N/A | NO | | blocks | sqlxTypes.JSONText | POST | Blocks JSON | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Get page details @@ -352,13 +492,14 @@ CRM module pages | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/page/{pageID}` | HTTP/S | GET | | +| `/namespace/{namespaceID}/page/{pageID}` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | pageID | uint64 | PATH | Page ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Get page all (non-record) pages, hierarchically @@ -366,12 +507,13 @@ CRM module pages | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/page/tree` | HTTP/S | GET | | +| `/namespace/{namespaceID}/page/tree` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Update page @@ -379,13 +521,14 @@ CRM module pages | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/page/{pageID}` | HTTP/S | POST | | +| `/namespace/{namespaceID}/page/{pageID}` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | pageID | uint64 | PATH | Page ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | selfID | uint64 | POST | Parent Page ID | N/A | NO | | moduleID | uint64 | POST | Module ID (optional) | N/A | NO | | title | string | POST | Title | N/A | YES | @@ -399,13 +542,14 @@ CRM module pages | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/page/{selfID}/reorder` | HTTP/S | POST | | +| `/namespace/{namespaceID}/page/{selfID}/reorder` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | selfID | uint64 | PATH | Parent page ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | pageIDs | []string | POST | Page ID order | N/A | YES | ## Delete page @@ -414,13 +558,14 @@ CRM module pages | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/page/{pageID}` | HTTP/S | Delete | | +| `/namespace/{namespaceID}/page/{pageID}` | HTTP/S | Delete | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | pageID | uint64 | PATH | Page ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Uploads attachment to page @@ -428,13 +573,14 @@ CRM module pages | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/page/{pageID}/attachment` | HTTP/S | POST | | +| `/namespace/{namespaceID}/page/{pageID}/attachment` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | pageID | uint64 | PATH | Page ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | upload | *multipart.FileHeader | POST | File to upload | N/A | YES | --- @@ -469,17 +615,17 @@ CRM module pages # Records -CRM records +Compose records | Method | Endpoint | Purpose | | ------ | -------- | ------- | -| `GET` | `/module/{moduleID}/record/report` | Generates report from module records | -| `GET` | `/module/{moduleID}/record/` | List/read records from module section | -| `POST` | `/module/{moduleID}/record/` | Create record in module section | -| `GET` | `/module/{moduleID}/record/{recordID}` | Read records by ID from module section | -| `POST` | `/module/{moduleID}/record/{recordID}` | Update records in module section | -| `DELETE` | `/module/{moduleID}/record/{recordID}` | Delete record row from module section | -| `POST` | `/module/{moduleID}/record/{recordID}/{fieldName}/attachment` | Uploads attachment and validates it against record field requirements | +| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/report` | Generates report from module records | +| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/` | List/read records from module section | +| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/` | Create record in module section | +| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Read records by ID from module section | +| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Update records in module section | +| `DELETE` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Delete record row from module section | +| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/{fieldName}/attachment` | Uploads attachment and validates it against record field requirements | ## Generates report from module records @@ -487,7 +633,7 @@ CRM records | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}/record/report` | HTTP/S | GET | | +| `/namespace/{namespaceID}/module/{moduleID}/record/report` | HTTP/S | GET | | #### Request parameters @@ -496,6 +642,7 @@ CRM records | metrics | string | GET | Metrics (eg: 'SUM(money), MAX(calls)') | N/A | NO | | dimensions | string | GET | Dimensions (eg: 'DATE(foo), status') | N/A | YES | | filter | string | GET | Filter (eg: 'DATE(foo) > 2010') | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | ## List/read records from module section @@ -504,16 +651,17 @@ CRM records | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}/record/` | HTTP/S | GET | | +| `/namespace/{namespaceID}/module/{moduleID}/record/` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | filter | string | GET | Filtering condition | N/A | NO | -| page | int | GET | Page number (0 based) | N/A | NO | -| perPage | int | GET | Returned items per page (default 50) | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | | sort | string | GET | Sort field (default id desc) | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | ## Create record in module section @@ -522,13 +670,14 @@ CRM records | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}/record/` | HTTP/S | POST | | +| `/namespace/{namespaceID}/module/{moduleID}/record/` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | values | types.RecordValueSet | POST | Record values | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | ## Read records by ID from module section @@ -537,13 +686,14 @@ CRM records | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}/record/{recordID}` | HTTP/S | GET | | +| `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | recordID | uint64 | PATH | Record ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | ## Update records in module section @@ -552,13 +702,14 @@ CRM records | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}/record/{recordID}` | HTTP/S | POST | | +| `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | recordID | uint64 | PATH | Record ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | | values | types.RecordValueSet | POST | Record values | N/A | YES | @@ -568,13 +719,14 @@ CRM records | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}/record/{recordID}` | HTTP/S | DELETE | | +| `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | HTTP/S | DELETE | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | recordID | uint64 | PATH | Record ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | ## Uploads attachment and validates it against record field requirements @@ -583,7 +735,7 @@ CRM records | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/module/{moduleID}/record/{recordID}/{fieldName}/attachment` | HTTP/S | POST | | +| `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}/{fieldName}/attachment` | HTTP/S | POST | | #### Request parameters @@ -591,6 +743,7 @@ CRM records | --------- | ---- | ------ | ----------- | ------- | --------- | | recordID | uint64 | PATH | Record ID | N/A | YES | | fieldName | string | PATH | Field name | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES | | upload | *multipart.FileHeader | POST | File to upload | N/A | YES | @@ -601,15 +754,15 @@ CRM records # Triggers -CRM Triggers +Compose Triggers | Method | Endpoint | Purpose | | ------ | -------- | ------- | -| `GET` | `/trigger/` | List available triggers | -| `POST` | `/trigger/` | Create trigger | -| `GET` | `/trigger/{triggerID}` | Get trigger details | -| `POST` | `/trigger/{triggerID}` | Update trigger | -| `Delete` | `/trigger/{triggerID}` | Delete trigger | +| `GET` | `/namespace/{namespaceID}/trigger/` | List available triggers | +| `POST` | `/namespace/{namespaceID}/trigger/` | Create trigger | +| `GET` | `/namespace/{namespaceID}/trigger/{triggerID}` | Get trigger details | +| `POST` | `/namespace/{namespaceID}/trigger/{triggerID}` | Update trigger | +| `Delete` | `/namespace/{namespaceID}/trigger/{triggerID}` | Delete trigger | ## List available triggers @@ -617,13 +770,17 @@ CRM Triggers | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/trigger/` | HTTP/S | GET | | +| `/namespace/{namespaceID}/trigger/` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | moduleID | uint64 | GET | Filter triggers by module | N/A | NO | +| query | string | GET | Search query | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Create trigger @@ -631,7 +788,7 @@ CRM Triggers | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/trigger/` | HTTP/S | POST | | +| `/namespace/{namespaceID}/trigger/` | HTTP/S | POST | | #### Request parameters @@ -642,6 +799,8 @@ CRM Triggers | actions | []string | POST | Actions that trigger this trigger | N/A | NO | | enabled | bool | POST | Enabled | N/A | NO | | source | string | POST | Trigger source code | N/A | NO | +| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Get trigger details @@ -649,13 +808,14 @@ CRM Triggers | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/trigger/{triggerID}` | HTTP/S | GET | | +| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | GET | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | triggerID | uint64 | PATH | Trigger ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | ## Update trigger @@ -663,13 +823,14 @@ CRM Triggers | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/trigger/{triggerID}` | HTTP/S | POST | | +| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | POST | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | triggerID | uint64 | PATH | Trigger ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | moduleID | uint64 | POST | Module ID | N/A | NO | | name | string | POST | Name | N/A | YES | | actions | []string | POST | Actions that trigger this trigger | N/A | NO | @@ -682,12 +843,13 @@ CRM Triggers | URI | Protocol | Method | Authentication | | --- | -------- | ------ | -------------- | -| `/trigger/{triggerID}` | HTTP/S | Delete | | +| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | Delete | | #### Request parameters | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | triggerID | uint64 | PATH | Trigger ID | N/A | YES | +| namespaceID | uint64 | PATH | Namespace ID | N/A | YES | --- \ No newline at end of file diff --git a/docs/messaging/README.md b/docs/messaging/README.md index 966cb8dea..5ccaac3c9 100644 --- a/docs/messaging/README.md +++ b/docs/messaging/README.md @@ -304,26 +304,6 @@ A channel is a representation of a sequence of messages. It has meta data like c # Messages -Messages represent individual messages in the chat system. Messages are typed, indicating the event which triggered the message. - -Currently expected message types are: - -| Name | Description | -| ---- | ----------- | -| CREATE | The first message when the channel is created | -| TOPIC | A member changed the topic of the channel | -| RENAME | A member renamed the channel | -| MESSAGE | A member posted a message to the channel | -| FILE | A member uploaded a file to the channel | - -The following event types may be sent with a message event: - -| Name | Description | -| ---- | ----------- | -| CREATED | A message has been created on a channel | -| EDITED | A message has been edited by the sender | -| REMOVED | A message has been removed by the sender | - | Method | Endpoint | Purpose | | ------ | -------- | ------- | | `POST` | `/channels/{channelID}/messages/` | Post new message to the channel | diff --git a/system/cli/roles.go b/system/cli/roles.go index 3e2db0b24..a1e550525 100644 --- a/system/cli/roles.go +++ b/system/cli/roles.go @@ -70,10 +70,10 @@ func rolesResetCmd(ctx context.Context, db *factory.DB) func(cmd *cobra.Command, {2, "compose", "namespace.create", 2}, {2, "compose", "access", 2}, {2, "compose", "grant", 2}, - {2, "compose:namespace:*", "page.create", 2}, {2, "compose:namespace:*", "read", 2}, {2, "compose:namespace:*", "update", 2}, {2, "compose:namespace:*", "delete", 2}, + {2, "compose:namespace:*", "page.create", 2}, {2, "compose:namespace:*", "module.create", 2}, {2, "compose:namespace:*", "chart.create", 2}, {2, "compose:namespace:*", "trigger.create", 2}, diff --git a/system/db/mysql/static.go b/system/db/mysql/static.go index f6b7ec230..a439cda58 100644 --- a/system/db/mysql/static.go +++ b/system/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\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \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 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\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 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\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 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\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 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\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 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\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\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\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\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\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 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose:*', 'access', 2),\n (1, 'messaging:*', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace:*', 'page.create', 2),\n (2, 'compose:namespace:*', 'read', 2),\n (2, 'compose:namespace:*', 'update', 2),\n (2, 'compose:namespace:*', 'delete', 2),\n (2, 'compose:namespace:*', 'module.create', 2),\n (2, 'compose:namespace:*', 'chart.create', 2),\n (2, 'compose:namespace:*', 'trigger.create', 2),\n (2, 'compose:chart:*', 'read', 2),\n (2, 'compose:chart:*', 'update', 2),\n (2, 'compose:chart:*', 'delete', 2),\n (2, 'compose:trigger:*', 'read', 2),\n (2, 'compose:trigger:*', 'update', 2),\n (2, 'compose:trigger:*', 'delete', 2),\n (2, 'compose:page:*', 'read', 2),\n (2, 'compose:page:*', 'update', 2),\n (2, 'compose:page:*', 'delete', 2),\n (2, 'system', 'access', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation:*', 'access', 2),\n (2, 'system:role:*', 'read', 2),\n (2, 'system:role:*', 'update', 2),\n (2, 'system:role:*', 'delete', 2),\n (2, 'system:role:*', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 2),\n (2, 'messaging:channel:*', 'message.react', 2),\n (2, 'compose:module:*', 'read', 2),\n (2, 'compose:module:*', 'update', 2),\n (2, 'compose:module:*', 'delete', 2),\n (2, 'compose:module:*', 'record.create', 2),\n (2, 'compose:module:*', 'record.read', 2),\n (2, 'compose:module:*', 'record.update', 2),\n (2, 'compose:module:*', 'record.delete', 2);\nPK\x07\x08z\",\xe8t\x0b\x00\x00t\x0b\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 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `settings`;\n\nCREATE TABLE `sys_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xba\xa4\xab\xaf\\\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 \x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP `password`;\nALTER TABLE `sys_user` DROP `satosa_id`;\nALTER TABLE `sys_credentials` ADD `last_used_at` DATETIME NULL;\nPK\x07\x088\x92\x0fs\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\x00#\x00 \x0020190405090000.internal-auth.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` ADD `email_confirmed` BOOLEAN NOT NULL DEFAULT FALSE;\nPK\x07\x08\x8fQs\x8cM\x00\x00\x00M\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \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!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(z\",\xe8t\x0b\x00\x00t\x0b\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(8\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81{*\x00\x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x8fQs\x8cM\x00\x00\x00M\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81f+\x00\x0020190405090000.internal-auth.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x0d,\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\xca-\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0f\x00\x0f\x00/\x05\x00\x005.\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\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \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 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\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 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\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 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\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 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\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 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\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\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\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\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\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 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose:*', 'access', 2),\n (1, 'messaging:*', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace:*', 'page.create', 2),\n (2, 'compose:namespace:*', 'read', 2),\n (2, 'compose:namespace:*', 'update', 2),\n (2, 'compose:namespace:*', 'delete', 2),\n (2, 'compose:namespace:*', 'module.create', 2),\n (2, 'compose:namespace:*', 'chart.create', 2),\n (2, 'compose:namespace:*', 'trigger.create', 2),\n (2, 'compose:chart:*', 'read', 2),\n (2, 'compose:chart:*', 'update', 2),\n (2, 'compose:chart:*', 'delete', 2),\n (2, 'compose:trigger:*', 'read', 2),\n (2, 'compose:trigger:*', 'update', 2),\n (2, 'compose:trigger:*', 'delete', 2),\n (2, 'compose:page:*', 'read', 2),\n (2, 'compose:page:*', 'update', 2),\n (2, 'compose:page:*', 'delete', 2),\n (2, 'system', 'access', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation:*', 'access', 2),\n (2, 'system:role:*', 'read', 2),\n (2, 'system:role:*', 'update', 2),\n (2, 'system:role:*', 'delete', 2),\n (2, 'system:role:*', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 2),\n (2, 'messaging:channel:*', 'message.react', 2),\n (2, 'compose:module:*', 'read', 2),\n (2, 'compose:module:*', 'update', 2),\n (2, 'compose:module:*', 'delete', 2),\n (2, 'compose:module:*', 'record.create', 2),\n (2, 'compose:module:*', 'record.read', 2),\n (2, 'compose:module:*', 'record.update', 2),\n (2, 'compose:module:*', 'record.delete', 2);\nPK\x07\x08z\",\xe8t\x0b\x00\x00t\x0b\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 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS `settings`;\n\nCREATE TABLE IF NOT EXISTS `sys_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08`\xcb\x1b\x81t\x02\x00\x00t\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 \x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP `password`;\nALTER TABLE `sys_user` DROP `satosa_id`;\nALTER TABLE `sys_credentials` ADD `last_used_at` DATETIME NULL;\nPK\x07\x088\x92\x0fs\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\x00#\x00 \x0020190405090000.internal-auth.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` ADD `email_confirmed` BOOLEAN NOT NULL DEFAULT FALSE;\nPK\x07\x08\x8fQs\x8cM\x00\x00\x00M\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \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!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(z\",\xe8t\x0b\x00\x00t\x0b\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(`\xcb\x1b\x81t\x02\x00\x00t\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(8\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x93*\x00\x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x8fQs\x8cM\x00\x00\x00M\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81~+\x00\x0020190405090000.internal-auth.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81%,\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\xe2-\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0f\x00\x0f\x00/\x05\x00\x00M.\x00\x00\x00\x00" diff --git a/system/db/schema/mysql/20190326122000.settings.up.sql b/system/db/schema/mysql/20190326122000.settings.up.sql index f98ab175c..9e06d8b05 100644 --- a/system/db/schema/mysql/20190326122000.settings.up.sql +++ b/system/db/schema/mysql/20190326122000.settings.up.sql @@ -1,6 +1,6 @@ -DROP TABLE `settings`; +DROP TABLE IF EXISTS `settings`; -CREATE TABLE `sys_settings` ( +CREATE TABLE IF NOT EXISTS `sys_settings` ( rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings', name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys', value JSON COMMENT 'Setting value', diff --git a/system/internal/repository/error.go b/system/internal/repository/error.go index 309b60aee..2cc825428 100644 --- a/system/internal/repository/error.go +++ b/system/internal/repository/error.go @@ -14,5 +14,5 @@ func (e repositoryError) Error() string { } func (e repositoryError) String() string { - return "crust.auth.repository." + string(e) + return "crust.system.repository." + string(e) }