Merge branch 'compose'
This commit is contained in:
+14
-8
@@ -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:
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/coverage.txt
|
||||
/*.iml
|
||||
/.env*
|
||||
/.dev*
|
||||
/.cover*
|
||||
/public_html
|
||||
/build
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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}",
|
||||
@@ -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",
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": [
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -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))
|
||||
|
||||
|
||||
+9
-8
@@ -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
|
||||
|
||||
@@ -101,6 +101,8 @@ $parsers = array(
|
||||
"int" => "parseInt",
|
||||
"uint" => "parseUint",
|
||||
"bool" => "parseBool",
|
||||
"time.Time" => "parseISODateWithErr",
|
||||
"*time.Time" => "parseISODatePtrWithErr",
|
||||
"sqlxTypes.JSONText" => "parseJSONTextWithErr",
|
||||
);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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`);
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+4
-4
@@ -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)
|
||||
}
|
||||
+4
-4
@@ -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"
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user