3
0

Basic (rest) structure for page & record/module attachments

This commit is contained in:
Denis Arh
2019-02-27 08:37:19 +01:00
parent 79229ef1f1
commit 8d43907bd6
36 changed files with 2849 additions and 860 deletions

View File

@@ -191,6 +191,30 @@
} }
] ]
} }
},
{
"name": "upload",
"path": "/{pageID}/attachment",
"method": "POST",
"title": "Uploads attachment to page",
"parameters": {
"path": [
{
"type": "uint64",
"name": "pageID",
"required": true,
"title": "Page ID"
}
],
"post": [
{
"name": "upload",
"type": "*multipart.FileHeader",
"required": true,
"title": "File to upload"
}
]
}
} }
] ]
}, },
@@ -320,21 +344,39 @@
} }
] ]
} }
}, }
]
},
{
"title": "Records",
"description": "CRM records ",
"entrypoint": "record",
"path": "/module/{moduleID}",
"authentication": [],
"struct": [
{ {
"name": "record/report", "imports": [
"github.com/crusttech/crust/crm/types"
]
}
],
"parameters": {
"path": [
{
"type": "uint64",
"name": "moduleID",
"required": true,
"title": "Module ID"
}
]
},
"apis": [
{
"name": "report",
"method": "GET", "method": "GET",
"title": "Generates report from module records", "title": "Generates report from module records",
"path": "/{moduleID}/report", "path": "/report",
"parameters": { "parameters": {
"path": [
{
"type": "uint64",
"name": "moduleID",
"required": true,
"title": "Module ID"
}
],
"get": [ "get": [
{ {
"type": "string", "type": "string",
@@ -358,19 +400,11 @@
} }
}, },
{ {
"name": "record/list", "name": "list",
"method": "GET", "method": "GET",
"title": "List/read records from module section", "title": "List/read records from module section",
"path": "/{moduleID}/record", "path": "/record",
"parameters": { "parameters": {
"path": [
{
"type": "uint64",
"name": "moduleID",
"required": true,
"title": "Module ID"
}
],
"get": [ "get": [
{ {
"name": "filter", "name": "filter",
@@ -400,19 +434,11 @@
} }
}, },
{ {
"name": "record/create", "name": "create",
"method": "POST", "method": "POST",
"title": "Create record in module section", "title": "Create record in module section",
"path": "/{moduleID}/record", "path": "/record",
"parameters": { "parameters": {
"path": [
{
"type": "uint64",
"name": "moduleID",
"required": true,
"title": "Module ID"
}
],
"post": [ "post": [
{ {
"type": "types.RecordValueSet", "type": "types.RecordValueSet",
@@ -424,18 +450,12 @@
} }
}, },
{ {
"name": "record/read", "name": "read",
"method": "GET", "method": "GET",
"title": "Read records by ID from module section", "title": "Read records by ID from module section",
"path": "/{moduleID}/record/{recordID}", "path": "/record/{recordID}",
"parameters": { "parameters": {
"path": [ "path": [
{
"type": "uint64",
"name": "moduleID",
"required": true,
"title": "Module ID"
},
{ {
"type": "uint64", "type": "uint64",
"name": "recordID", "name": "recordID",
@@ -446,18 +466,12 @@
} }
}, },
{ {
"name": "record/update", "name": "update",
"method": "POST", "method": "POST",
"title": "Update records in module section", "title": "Update records in module section",
"path": "/{moduleID}/record/{recordID}", "path": "/record/{recordID}",
"parameters": { "parameters": {
"path": [ "path": [
{
"type": "uint64",
"name": "moduleID",
"required": true,
"title": "Module ID"
},
{ {
"type": "uint64", "type": "uint64",
"name": "recordID", "name": "recordID",
@@ -476,18 +490,12 @@
} }
}, },
{ {
"name": "record/delete", "name": "delete",
"method": "DELETE", "method": "DELETE",
"title": "Delete record row from module section", "title": "Delete record row from module section",
"path": "/{moduleID}/record/{recordID}", "path": "/record/{recordID}",
"parameters": { "parameters": {
"path": [ "path": [
{
"type": "uint64",
"name": "moduleID",
"required": true,
"title": "Module ID"
},
{ {
"type": "uint64", "type": "uint64",
"name": "recordID", "name": "recordID",
@@ -496,6 +504,36 @@
} }
] ]
} }
},
{
"name": "upload",
"path": "/record/{recordID}/{fieldName}/attachment",
"method": "POST",
"title": "Uploads attachment and validates it against record field requirements",
"parameters": {
"path": [
{
"name": "recordID",
"type": "uint64",
"required": true,
"title": "Record ID"
},
{
"name": "fieldName",
"type": "string",
"required": true,
"title": "Field name"
}
],
"post": [
{
"name": "upload",
"type": "*multipart.FileHeader",
"required": true,
"title": "File to upload"
}
]
}
} }
] ]
}, },
@@ -779,5 +817,140 @@
} }
} }
] ]
},
{
"title": "Attachments",
"path": "/attachment/{kind}",
"entrypoint": "attachment",
"authentication": [
"Client ID",
"Session ID"
],
"parameters": {
"path": [
{
"name": "kind",
"type": "string",
"required": true,
"title": "Attachment kind"
}
]
},
"apis": [
{
"name": "list",
"path": "/",
"method": "GET",
"title": "List, filter all page attachments",
"parameters": {
"get": [
{
"name": "pageID",
"type": "uint64",
"required": false,
"title": "Filter attachments by page ID"
},
{
"type": "uint64",
"name": "moduleID",
"required": false,
"title": "Filter attachments by mnodule ID"
},
{
"name": "recordID",
"type": "uint64",
"required": false,
"title": "Filter attachments by record ID"
},
{
"name": "fieldName",
"type": "string",
"required": false,
"title": "Filter attachments by field name"
},
{
"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": "details",
"path": "/{attachmentID}",
"method": "GET",
"title": "Attachment details",
"parameters": {
"path": [
{
"name": "attachmentID",
"type": "uint64",
"required": true,
"title": "Attachment ID"
}
]
}
},
{
"name": "original",
"path": "/{attachmentID}/original/{name}",
"method": "GET",
"title": "Serves attached file",
"parameters": {
"path": [
{
"name": "attachmentID",
"type": "uint64",
"required": true,
"title": "Attachment ID"
},
{
"name": "name",
"type": "string",
"required": true,
"title": "File name"
}
],
"get": [
{
"type": "bool",
"name": "download",
"required": false,
"title": "Force file download"
}
]
}
},
{
"name": "preview",
"path": "/{attachmentID}/preview.{ext}",
"method": "GET",
"title": "Serves preview of an attached file",
"parameters": {
"path": [
{
"name": "attachmentID",
"type": "uint64",
"required": true,
"title": "Attachment ID"
},
{
"name": "ext",
"type": "string",
"required": true,
"title": "Preview extension/format"
}
]
}
}
]
} }
] ]

View File

@@ -0,0 +1,137 @@
{
"Title": "Attachments",
"Interface": "Attachment",
"Struct": null,
"Parameters": {
"path": [
{
"name": "kind",
"required": true,
"title": "Attachment kind",
"type": "string"
}
]
},
"Protocol": "",
"Authentication": [
"Client ID",
"Session ID"
],
"Path": "/attachment/{kind}",
"APIs": [
{
"Name": "list",
"Method": "GET",
"Title": "List, filter all page attachments",
"Path": "/",
"Parameters": {
"get": [
{
"name": "pageID",
"required": false,
"title": "Filter attachments by page ID",
"type": "uint64"
},
{
"name": "moduleID",
"required": false,
"title": "Filter attachments by mnodule ID",
"type": "uint64"
},
{
"name": "recordID",
"required": false,
"title": "Filter attachments by record ID",
"type": "uint64"
},
{
"name": "fieldName",
"required": false,
"title": "Filter attachments by field name",
"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": "details",
"Method": "GET",
"Title": "Attachment details",
"Path": "/{attachmentID}",
"Parameters": {
"path": [
{
"name": "attachmentID",
"required": true,
"title": "Attachment ID",
"type": "uint64"
}
]
}
},
{
"Name": "original",
"Method": "GET",
"Title": "Serves attached file",
"Path": "/{attachmentID}/original/{name}",
"Parameters": {
"get": [
{
"name": "download",
"required": false,
"title": "Force file download",
"type": "bool"
}
],
"path": [
{
"name": "attachmentID",
"required": true,
"title": "Attachment ID",
"type": "uint64"
},
{
"name": "name",
"required": true,
"title": "File name",
"type": "string"
}
]
}
},
{
"Name": "preview",
"Method": "GET",
"Title": "Serves preview of an attached file",
"Path": "/{attachmentID}/preview.{ext}",
"Parameters": {
"path": [
{
"name": "attachmentID",
"required": true,
"title": "Attachment ID",
"type": "uint64"
},
{
"name": "ext",
"required": true,
"title": "Preview extension/format",
"type": "string"
}
]
}
}
]
}

View File

@@ -126,182 +126,6 @@
} }
] ]
} }
},
{
"Name": "record/report",
"Method": "GET",
"Title": "Generates report from module records",
"Path": "/{moduleID}/report",
"Parameters": {
"get": [
{
"name": "metrics",
"required": false,
"title": "Metrics (eg: 'SUM(money), MAX(calls)')",
"type": "string"
},
{
"name": "dimensions",
"required": true,
"title": "Dimensions (eg: 'DATE(foo), status')",
"type": "string"
},
{
"name": "filter",
"required": false,
"title": "Filter (eg: 'DATE(foo) \u003e 2010')",
"type": "string"
}
],
"path": [
{
"name": "moduleID",
"required": true,
"title": "Module ID",
"type": "uint64"
}
]
}
},
{
"Name": "record/list",
"Method": "GET",
"Title": "List/read records from module section",
"Path": "/{moduleID}/record",
"Parameters": {
"get": [
{
"name": "filter",
"required": false,
"title": "Filtering condition",
"type": "string"
},
{
"name": "page",
"required": false,
"title": "Page number (0 based)",
"type": "int"
},
{
"name": "perPage",
"required": false,
"title": "Returned items per page (default 50)",
"type": "int"
},
{
"name": "sort",
"required": false,
"title": "Sort field (default id desc)",
"type": "string"
}
],
"path": [
{
"name": "moduleID",
"required": true,
"title": "Module ID",
"type": "uint64"
}
]
}
},
{
"Name": "record/create",
"Method": "POST",
"Title": "Create record in module section",
"Path": "/{moduleID}/record",
"Parameters": {
"path": [
{
"name": "moduleID",
"required": true,
"title": "Module ID",
"type": "uint64"
}
],
"post": [
{
"name": "values",
"required": true,
"title": "Record values",
"type": "types.RecordValueSet"
}
]
}
},
{
"Name": "record/read",
"Method": "GET",
"Title": "Read records by ID from module section",
"Path": "/{moduleID}/record/{recordID}",
"Parameters": {
"path": [
{
"name": "moduleID",
"required": true,
"title": "Module ID",
"type": "uint64"
},
{
"name": "recordID",
"required": true,
"title": "Record ID",
"type": "uint64"
}
]
}
},
{
"Name": "record/update",
"Method": "POST",
"Title": "Update records in module section",
"Path": "/{moduleID}/record/{recordID}",
"Parameters": {
"path": [
{
"name": "moduleID",
"required": true,
"title": "Module ID",
"type": "uint64"
},
{
"name": "recordID",
"required": true,
"title": "Record ID",
"type": "uint64"
}
],
"post": [
{
"name": "values",
"required": true,
"title": "Record values",
"type": "types.RecordValueSet"
}
]
}
},
{
"Name": "record/delete",
"Method": "DELETE",
"Title": "Delete record row from module section",
"Path": "/{moduleID}/record/{recordID}",
"Parameters": {
"path": [
{
"name": "moduleID",
"required": true,
"title": "Module ID",
"type": "uint64"
},
{
"name": "recordID",
"required": true,
"title": "Record ID",
"type": "uint64"
}
]
}
} }
] ]
} }

View File

@@ -192,6 +192,30 @@
} }
] ]
} }
},
{
"Name": "upload",
"Method": "POST",
"Title": "Uploads attachment to page",
"Path": "/{pageID}/attachment",
"Parameters": {
"path": [
{
"name": "pageID",
"required": true,
"title": "Page ID",
"type": "uint64"
}
],
"post": [
{
"name": "upload",
"required": true,
"title": "File to upload",
"type": "*multipart.FileHeader"
}
]
}
} }
] ]
} }

191
api/crm/spec/record.json Normal file
View File

@@ -0,0 +1,191 @@
{
"Title": "Records",
"Description": "CRM records ",
"Interface": "Record",
"Struct": [
{
"imports": [
"github.com/crusttech/crust/crm/types"
]
}
],
"Parameters": {
"path": [
{
"name": "moduleID",
"required": true,
"title": "Module ID",
"type": "uint64"
}
]
},
"Protocol": "",
"Authentication": [],
"Path": "/module/{moduleID}",
"APIs": [
{
"Name": "report",
"Method": "GET",
"Title": "Generates report from module records",
"Path": "/report",
"Parameters": {
"get": [
{
"name": "metrics",
"required": false,
"title": "Metrics (eg: 'SUM(money), MAX(calls)')",
"type": "string"
},
{
"name": "dimensions",
"required": true,
"title": "Dimensions (eg: 'DATE(foo), status')",
"type": "string"
},
{
"name": "filter",
"required": false,
"title": "Filter (eg: 'DATE(foo) \u003e 2010')",
"type": "string"
}
]
}
},
{
"Name": "list",
"Method": "GET",
"Title": "List/read records from module section",
"Path": "/record",
"Parameters": {
"get": [
{
"name": "filter",
"required": false,
"title": "Filtering condition",
"type": "string"
},
{
"name": "page",
"required": false,
"title": "Page number (0 based)",
"type": "int"
},
{
"name": "perPage",
"required": false,
"title": "Returned items per page (default 50)",
"type": "int"
},
{
"name": "sort",
"required": false,
"title": "Sort field (default id desc)",
"type": "string"
}
]
}
},
{
"Name": "create",
"Method": "POST",
"Title": "Create record in module section",
"Path": "/record",
"Parameters": {
"post": [
{
"name": "values",
"required": true,
"title": "Record values",
"type": "types.RecordValueSet"
}
]
}
},
{
"Name": "read",
"Method": "GET",
"Title": "Read records by ID from module section",
"Path": "/record/{recordID}",
"Parameters": {
"path": [
{
"name": "recordID",
"required": true,
"title": "Record ID",
"type": "uint64"
}
]
}
},
{
"Name": "update",
"Method": "POST",
"Title": "Update records in module section",
"Path": "/record/{recordID}",
"Parameters": {
"path": [
{
"name": "recordID",
"required": true,
"title": "Record ID",
"type": "uint64"
}
],
"post": [
{
"name": "values",
"required": true,
"title": "Record values",
"type": "types.RecordValueSet"
}
]
}
},
{
"Name": "delete",
"Method": "DELETE",
"Title": "Delete record row from module section",
"Path": "/record/{recordID}",
"Parameters": {
"path": [
{
"name": "recordID",
"required": true,
"title": "Record ID",
"type": "uint64"
}
]
}
},
{
"Name": "upload",
"Method": "POST",
"Title": "Uploads attachment and validates it against record field requirements",
"Path": "/record/{recordID}/{fieldName}/attachment",
"Parameters": {
"path": [
{
"name": "recordID",
"required": true,
"title": "Record ID",
"type": "uint64"
},
{
"name": "fieldName",
"required": true,
"title": "Field name",
"type": "string"
}
],
"post": [
{
"name": "upload",
"required": true,
"title": "File to upload",
"type": "*multipart.FileHeader"
}
]
}
}
]
}

View File

@@ -23,9 +23,11 @@ function types {
fi fi
./build/gen-type-set --types Module,Page,Chart,Trigger,Record \ ./build/gen-type-set --types Module,Page,Chart,Trigger,Record \
--output crm/types/type.primary.gen.go --output crm/types/type.primary.gen.go
./build/gen-type-set --with-primary-key=false --types ModuleField,RecordValue \ ./build/gen-type-set --with-primary-key=false --types ModuleField,RecordValue \
--output crm/types/type.other.gen.go --output crm/types/type.other.gen.go
./build/gen-type-set --types Attachment \
--output crm/types/attachment.gen.go
./build/gen-type-set --types MessageAttachment --output messaging/types/attachment.gen.go ./build/gen-type-set --types MessageAttachment --output messaging/types/attachment.gen.go
./build/gen-type-set --with-resources=true --types Channel --resource-type "rules.Resource" --imports "github.com/crusttech/crust/internal/rules" --output messaging/types/channel.gen.go ./build/gen-type-set --with-resources=true --types Channel --resource-type "rules.Resource" --imports "github.com/crusttech/crust/internal/rules" --output messaging/types/channel.gen.go

View File

@@ -98,6 +98,7 @@ $parsers = array(
"uint64" => "parseUInt64", "uint64" => "parseUInt64",
"[]uint64" => "parseUInt64A", "[]uint64" => "parseUInt64A",
"int" => "parseInt", "int" => "parseInt",
"uint" => "parseUint",
"bool" => "parseBool", "bool" => "parseBool",
"sqlxTypes.JSONText" => "parseJSONTextWithErr", "sqlxTypes.JSONText" => "parseJSONTextWithErr",
); );

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,24 @@
CREATE TABLE crm_attachment (
id BIGINT UNSIGNED NOT NULL,
rel_owner BIGINT UNSIGNED NOT NULL,
kind VARCHAR(32) NOT NULL,
url VARCHAR(512),
preview_url VARCHAR(512),
size INT UNSIGNED,
mimetype VARCHAR(255),
name TEXT,
meta JSON,
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NULL,
deleted_at DATETIME NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- page attachments will be referenced via page-block meta data
-- module/record attachment will be referenced via crm_record_value

View File

@@ -0,0 +1,161 @@
package repository
import (
"context"
"time"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/titpetric/factory"
sq "gopkg.in/Masterminds/squirrel.v1"
"github.com/crusttech/crust/crm/types"
)
type (
AttachmentRepository interface {
With(ctx context.Context, db *factory.DB) AttachmentRepository
Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error)
FindByID(id uint64) (*types.Attachment, error)
FindByIDs(IDs ...uint64) (types.AttachmentSet, error)
Create(mod *types.Attachment) (*types.Attachment, error)
DeleteByID(id uint64) error
}
attachment struct {
*repository
}
)
const (
sqlAttachmentColumns = `
a.id, a.rel_owner, a.kind,
a.url, a.preview_url,
a.name,
a.meta,
a.created_at, a.updated_at, a.deleted_at
`
sqlAttachmentScope = "deleted_at IS NULL"
sqlAttachmentByID = `SELECT ` + sqlAttachmentColumns +
` FROM crm_attachment AS a WHERE id = ? AND ` + sqlAttachmentScope
sqlAttachmentsByIDs = `SELECT ` + sqlAttachmentColumns +
` FROM crm_attachment AS a WHERE id IN (?) AND ` + sqlAttachmentScope
)
func Attachment(ctx context.Context, db *factory.DB) AttachmentRepository {
return (&attachment{}).With(ctx, db)
}
func (r *attachment) With(ctx context.Context, db *factory.DB) AttachmentRepository {
return &attachment{
repository: r.repository.With(ctx, db),
}
}
func (r *attachment) FindByID(id uint64) (*types.Attachment, error) {
mod := &types.Attachment{}
return mod, r.db().Get(mod, sqlAttachmentByID, id)
}
func (r *attachment) FindByIDs(IDs ...uint64) (rval types.AttachmentSet, err error) {
rval = make([]*types.Attachment, 0)
if len(IDs) == 0 {
return
}
if sql, args, err := sqlx.In(sqlAttachmentsByIDs, IDs); err != nil {
return nil, err
} else {
return rval, r.db().Select(&rval, sql, args...)
}
}
func (r *attachment) Find(filter types.AttachmentFilter) (set types.AttachmentSet, f types.AttachmentFilter, err error) {
f = filter
if f.PerPage > 100 {
f.PerPage = 100
} else if f.PerPage == 0 {
f.PerPage = 50
}
set = types.AttachmentSet{}
query := sq.Select().From("crm_attachment AS a").Where(sq.Eq{"a.kind": f.Kind})
switch f.Kind {
case types.PageAttachment:
// @todo implement filtering by page
err = errors.New("filtering by page not implemented")
return
case types.RecordAttachment:
query = query.
Join("crm_record_value AS v ON (v.ref = a.id)")
if f.ModuleID > 0 {
query = query.
Join("crm_record AS r ON (r.id = v.record_id)").
Where(sq.Eq{"r.module_id": f.ModuleID})
}
if f.RecordID > 0 {
query = query.Where(sq.Eq{"v.record_id": f.RecordID})
}
if f.FieldName != "" {
query = query.Where(sq.Eq{"v.name": f.FieldName})
}
}
if f.Filter != "" {
err = errors.New("filtering by filter not implemented")
return
}
// Assemble SQL for counting (includes only where)
count := query.Column("COUNT(*)")
if sqlSelect, argsSelect, err := count.ToSql(); err != nil {
return set, f, err
} else {
// Execute count query.
if err := r.db().Get(&f.Count, sqlSelect, argsSelect...); err != nil {
return set, f, err
}
// Return empty response if count of records is zero.
if f.Count == 0 {
return set, f, nil
}
}
// Assemble SQL for fetching attachments (where + sorting + paging)...
query = query.
Column(sqlAttachmentColumns).
Limit(uint64(f.PerPage)).
Offset(uint64(f.Page * f.PerPage))
if sqlSelect, argsSelect, err := query.ToSql(); err != nil {
return set, f, err
} else {
return set, f, r.db().Select(&set, sqlSelect, argsSelect...)
}
}
func (r *attachment) Create(mod *types.Attachment) (*types.Attachment, error) {
if mod.ID == 0 {
mod.ID = factory.Sonyflake.NextID()
}
mod.CreatedAt = time.Now()
return mod, r.db().Insert("crm_attachment", mod)
}
func (r *attachment) DeleteByID(id uint64) error {
_, err := r.db().Exec("UPDATE crm_attachment SET deleted_at = NOW() WHERE id = ?", id)
return err
}

53
crm/rest/attachment.go Normal file
View File

@@ -0,0 +1,53 @@
package rest
import (
"context"
"github.com/crusttech/crust/crm/rest/request"
"github.com/crusttech/crust/crm/service"
"github.com/crusttech/crust/crm/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
type Attachment struct {
attachment service.AttachmentService
}
func (Attachment) New() *Attachment {
return &Attachment{attachment: service.DefaultAttachment}
}
// Attachments returns list of all files attached to records
func (ctrl *Attachment) List(ctx context.Context, r *request.AttachmentList) (interface{}, error) {
f := types.AttachmentFilter{
Kind: types.RecordAttachment,
ModuleID: r.ModuleID,
RecordID: r.RecordID,
FieldName: r.FieldName,
// Filter: r.Filter,
PerPage: r.PerPage,
Page: r.Page,
// Sort: r.Sort,
}
return makeRecordAttachmentSetPayload(ctrl.attachment.Find(f))
}
func (ctrl *Attachment) Details(ctx context.Context, r *request.AttachmentDetails) (interface{}, error) {
if a, err := ctrl.attachment.FindByID(r.AttachmentID); err != nil {
return nil, err
} else {
return makeAttachmentPayload(a), nil
}
}
func (ctrl *Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) {
return loadAttachedFile(ctrl.attachment, r.AttachmentID, false, r.Download)
}
func (ctrl *Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) {
return loadAttachedFile(ctrl.attachment, r.AttachmentID, true, false)
}

View File

@@ -0,0 +1,119 @@
package rest
import (
"fmt"
"io"
"net/url"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/crusttech/crust/crm/rest/handlers"
"github.com/crusttech/crust/crm/service"
"github.com/crusttech/crust/crm/types"
)
type (
attachmentPayload struct {
ID uint64 `json:"attachmentID,string"`
OwnerID uint64 `json:"ownerID,string"`
Url string `json:"url"`
PreviewUrl string `json:"previewUrl,omitempty"`
Meta interface{} `json:"meta"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
file struct {
*types.Attachment
content io.ReadSeeker
download bool
}
)
func (f *file) Download() bool {
return f.download
}
func (f *file) Name() string {
return f.Attachment.Name
}
func (f *file) ModTime() time.Time {
return f.Attachment.CreatedAt
}
func (f *file) Content() io.ReadSeeker {
return f.content
}
func (f *file) Valid() bool {
return f.content != nil
}
func loadAttachedFile(svc service.AttachmentService, ID uint64, preview, download bool) (handlers.Downloadable, error) {
rval := &file{download: download}
if att, err := svc.FindByID(ID); err != nil {
return nil, err
} else {
rval.Attachment = att
if preview {
spew.Dump(att)
rval.content, err = svc.OpenPreview(att)
} else {
rval.content, err = svc.OpenOriginal(att)
}
if err != nil {
return nil, err
}
}
return rval, nil
}
func makeAttachmentPayload(a *types.Attachment) *attachmentPayload {
if a == nil {
return nil
}
var preview string
var baseURL = fmt.Sprintf("/attachment/%s/%d/", a.Kind, a.ID)
if a.Meta.Preview != nil {
var ext = a.Meta.Preview.Extension
if ext == "" {
ext = "jpg"
}
preview = baseURL + fmt.Sprintf("preview.%s", ext)
}
return &attachmentPayload{
ID: a.ID,
OwnerID: a.OwnerID,
Url: baseURL + fmt.Sprintf("original/%s", url.PathEscape(a.Name)),
PreviewUrl: preview,
Meta: a.Meta,
Name: a.Name,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
}
}
func makeRecordAttachmentSetPayload(aa types.AttachmentSet, meta types.AttachmentFilter, err error) (map[string]interface{}, error) {
if err != nil {
return nil, err
}
pp := make([]*attachmentPayload, len(aa))
for i := range aa {
pp[i] = makeAttachmentPayload(aa[i])
}
rval := map[string]interface{}{"meta": meta, "attachments": pp}
return rval, err
}

View File

@@ -0,0 +1,87 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `attachment.go`, `attachment.util.go` or `attachment_test.go` to
implement your API calls, helper functions and tests. The file `attachment.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"github.com/go-chi/chi"
"net/http"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/crm/rest/request"
)
// Internal API interface
type AttachmentAPI interface {
List(context.Context, *request.AttachmentList) (interface{}, error)
Details(context.Context, *request.AttachmentDetails) (interface{}, error)
Original(context.Context, *request.AttachmentOriginal) (interface{}, error)
Preview(context.Context, *request.AttachmentPreview) (interface{}, error)
}
// HTTP API interface
type Attachment struct {
List func(http.ResponseWriter, *http.Request)
Details func(http.ResponseWriter, *http.Request)
Original func(http.ResponseWriter, *http.Request)
Preview func(http.ResponseWriter, *http.Request)
}
func NewAttachment(ah AttachmentAPI) *Attachment {
return &Attachment{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentList()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ah.List(r.Context(), params)
})
},
Details: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentDetails()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ah.Details(r.Context(), params)
})
},
Original: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentOriginal()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ah.Original(r.Context(), params)
})
},
Preview: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentPreview()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ah.Preview(r.Context(), params)
})
},
}
}
func (ah *Attachment) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Route("/attachment/{kind}", func(r chi.Router) {
r.Get("/", ah.List)
r.Get("/{attachmentID}", ah.Details)
r.Get("/{attachmentID}/original/{name}", ah.Original)
r.Get("/{attachmentID}/preview.{ext}", ah.Preview)
})
})
}

View File

@@ -0,0 +1,61 @@
package handlers
import (
"io"
"net/http"
"net/url"
"time"
"github.com/crusttech/crust/crm/rest/request"
)
type Downloadable interface {
Name() string
Download() bool
ModTime() time.Time
Content() io.ReadSeeker
Valid() bool
}
func NewAttachmentDownloadable(ctrl AttachmentAPI) *Attachment {
h := NewAttachment(ctrl)
h.Original = func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentOriginal()
params.Fill(r)
f, err := ctrl.Original(r.Context(), params)
serveFile(f, err, w, r)
}
h.Preview = func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewAttachmentPreview()
params.Fill(r)
f, err := ctrl.Preview(r.Context(), params)
serveFile(f, err, w, r)
}
return h
}
func serveFile(f interface{}, err error, w http.ResponseWriter, r *http.Request) {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else if dl, ok := f.(Downloadable); ok {
if !dl.Valid() {
w.WriteHeader(http.StatusNotFound)
} else {
if dl.Download() {
w.Header().Add("Content-Disposition", "attachment; filename="+url.QueryEscape(dl.Name()))
} else {
w.Header().Add("Content-Disposition", "inline; filename="+url.QueryEscape(dl.Name()))
}
http.ServeContent(w, r, dl.Name(), dl.ModTime(), dl.Content())
}
} else {
http.Error(w, "Got incompatible type from controller", http.StatusInternalServerError)
}
}

View File

@@ -32,27 +32,15 @@ type ModuleAPI interface {
Read(context.Context, *request.ModuleRead) (interface{}, error) Read(context.Context, *request.ModuleRead) (interface{}, error)
Update(context.Context, *request.ModuleUpdate) (interface{}, error) Update(context.Context, *request.ModuleUpdate) (interface{}, error)
Delete(context.Context, *request.ModuleDelete) (interface{}, error) Delete(context.Context, *request.ModuleDelete) (interface{}, error)
RecordReport(context.Context, *request.ModuleRecordReport) (interface{}, error)
RecordList(context.Context, *request.ModuleRecordList) (interface{}, error)
RecordCreate(context.Context, *request.ModuleRecordCreate) (interface{}, error)
RecordRead(context.Context, *request.ModuleRecordRead) (interface{}, error)
RecordUpdate(context.Context, *request.ModuleRecordUpdate) (interface{}, error)
RecordDelete(context.Context, *request.ModuleRecordDelete) (interface{}, error)
} }
// HTTP API interface // HTTP API interface
type Module struct { type Module struct {
List func(http.ResponseWriter, *http.Request) List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request) Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request) Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request) Update func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request) Delete func(http.ResponseWriter, *http.Request)
RecordReport func(http.ResponseWriter, *http.Request)
RecordList func(http.ResponseWriter, *http.Request)
RecordCreate func(http.ResponseWriter, *http.Request)
RecordRead func(http.ResponseWriter, *http.Request)
RecordUpdate func(http.ResponseWriter, *http.Request)
RecordDelete func(http.ResponseWriter, *http.Request)
} }
func NewModule(mh ModuleAPI) *Module { func NewModule(mh ModuleAPI) *Module {
@@ -92,48 +80,6 @@ func NewModule(mh ModuleAPI) *Module {
return mh.Delete(r.Context(), params) return mh.Delete(r.Context(), params)
}) })
}, },
RecordReport: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleRecordReport()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.RecordReport(r.Context(), params)
})
},
RecordList: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleRecordList()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.RecordList(r.Context(), params)
})
},
RecordCreate: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleRecordCreate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.RecordCreate(r.Context(), params)
})
},
RecordRead: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleRecordRead()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.RecordRead(r.Context(), params)
})
},
RecordUpdate: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleRecordUpdate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.RecordUpdate(r.Context(), params)
})
},
RecordDelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleRecordDelete()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.RecordDelete(r.Context(), params)
})
},
} }
} }
@@ -146,12 +92,6 @@ func (mh *Module) MountRoutes(r chi.Router, middlewares ...func(http.Handler) ht
r.Get("/{moduleID}", mh.Read) r.Get("/{moduleID}", mh.Read)
r.Post("/{moduleID}", mh.Update) r.Post("/{moduleID}", mh.Update)
r.Delete("/{moduleID}", mh.Delete) r.Delete("/{moduleID}", mh.Delete)
r.Get("/{moduleID}/report", mh.RecordReport)
r.Get("/{moduleID}/record", mh.RecordList)
r.Post("/{moduleID}/record", mh.RecordCreate)
r.Get("/{moduleID}/record/{recordID}", mh.RecordRead)
r.Post("/{moduleID}/record/{recordID}", mh.RecordUpdate)
r.Delete("/{moduleID}/record/{recordID}", mh.RecordDelete)
}) })
}) })
} }

View File

@@ -34,6 +34,7 @@ type PageAPI interface {
Update(context.Context, *request.PageUpdate) (interface{}, error) Update(context.Context, *request.PageUpdate) (interface{}, error)
Reorder(context.Context, *request.PageReorder) (interface{}, error) Reorder(context.Context, *request.PageReorder) (interface{}, error)
Delete(context.Context, *request.PageDelete) (interface{}, error) Delete(context.Context, *request.PageDelete) (interface{}, error)
Upload(context.Context, *request.PageUpload) (interface{}, error)
} }
// HTTP API interface // HTTP API interface
@@ -45,6 +46,7 @@ type Page struct {
Update func(http.ResponseWriter, *http.Request) Update func(http.ResponseWriter, *http.Request)
Reorder func(http.ResponseWriter, *http.Request) Reorder func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request) Delete func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
} }
func NewPage(ph PageAPI) *Page { func NewPage(ph PageAPI) *Page {
@@ -98,6 +100,13 @@ func NewPage(ph PageAPI) *Page {
return ph.Delete(r.Context(), params) return ph.Delete(r.Context(), params)
}) })
}, },
Upload: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageUpload()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return ph.Upload(r.Context(), params)
})
},
} }
} }
@@ -112,6 +121,7 @@ func (ph *Page) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
r.Post("/{pageID}", ph.Update) r.Post("/{pageID}", ph.Update)
r.Post("/{selfID}/reorder", ph.Reorder) r.Post("/{selfID}/reorder", ph.Reorder)
r.Delete("/{pageID}", ph.Delete) r.Delete("/{pageID}", ph.Delete)
r.Post("/{pageID}/attachment", ph.Upload)
}) })
}) })
} }

117
crm/rest/handlers/record.go Normal file
View File

@@ -0,0 +1,117 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `record.go`, `record.util.go` or `record_test.go` to
implement your API calls, helper functions and tests. The file `record.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"github.com/go-chi/chi"
"net/http"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/crm/rest/request"
)
// Internal API interface
type RecordAPI interface {
Report(context.Context, *request.RecordReport) (interface{}, error)
List(context.Context, *request.RecordList) (interface{}, error)
Create(context.Context, *request.RecordCreate) (interface{}, error)
Read(context.Context, *request.RecordRead) (interface{}, error)
Update(context.Context, *request.RecordUpdate) (interface{}, error)
Delete(context.Context, *request.RecordDelete) (interface{}, error)
Upload(context.Context, *request.RecordUpload) (interface{}, error)
}
// HTTP API interface
type Record struct {
Report func(http.ResponseWriter, *http.Request)
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
}
func NewRecord(rh RecordAPI) *Record {
return &Record{
Report: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordReport()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return rh.Report(r.Context(), params)
})
},
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordList()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return rh.List(r.Context(), params)
})
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordCreate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return rh.Create(r.Context(), params)
})
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordRead()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return rh.Read(r.Context(), params)
})
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordUpdate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return rh.Update(r.Context(), params)
})
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordDelete()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return rh.Delete(r.Context(), params)
})
},
Upload: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewRecordUpload()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return rh.Upload(r.Context(), params)
})
},
}
}
func (rh *Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Route("/module/{moduleID}", func(r chi.Router) {
r.Get("/report", rh.Report)
r.Get("/record", rh.List)
r.Post("/record", rh.Create)
r.Get("/record/{recordID}", rh.Read)
r.Post("/record/{recordID}", rh.Update)
r.Delete("/record/{recordID}", rh.Delete)
r.Post("/record/{recordID}/{fieldName}/attachment", rh.Upload)
})
})
}

View File

@@ -24,60 +24,33 @@ func (Module) New() *Module {
} }
} }
func (s *Module) List(ctx context.Context, r *request.ModuleList) (interface{}, error) { func (ctrl *Module) List(ctx context.Context, r *request.ModuleList) (interface{}, error) {
return s.module.With(ctx).Find() return ctrl.module.With(ctx).Find()
} }
func (s *Module) Read(ctx context.Context, r *request.ModuleRead) (interface{}, error) { func (ctrl *Module) Read(ctx context.Context, r *request.ModuleRead) (interface{}, error) {
return s.module.With(ctx).FindByID(r.ModuleID) return ctrl.module.With(ctx).FindByID(r.ModuleID)
} }
func (s *Module) Delete(ctx context.Context, r *request.ModuleDelete) (interface{}, error) { func (ctrl *Module) Delete(ctx context.Context, r *request.ModuleDelete) (interface{}, error) {
return resputil.OK(), s.module.With(ctx).DeleteByID(r.ModuleID) return resputil.OK(), ctrl.module.With(ctx).DeleteByID(r.ModuleID)
} }
func (s *Module) Create(ctx context.Context, r *request.ModuleCreate) (interface{}, error) { func (ctrl *Module) Create(ctx context.Context, r *request.ModuleCreate) (interface{}, error) {
item := &types.Module{ item := &types.Module{
Name: r.Name, Name: r.Name,
Fields: r.Fields, Fields: r.Fields,
Meta: r.Meta, Meta: r.Meta,
} }
return s.module.With(ctx).Create(item) return ctrl.module.With(ctx).Create(item)
} }
func (s *Module) Update(ctx context.Context, r *request.ModuleUpdate) (interface{}, error) { func (ctrl *Module) Update(ctx context.Context, r *request.ModuleUpdate) (interface{}, error) {
item := &types.Module{ item := &types.Module{
ID: r.ModuleID, ID: r.ModuleID,
Name: r.Name, Name: r.Name,
Fields: r.Fields, Fields: r.Fields,
Meta: r.Meta, Meta: r.Meta,
} }
return s.module.With(ctx).Update(item) return ctrl.module.With(ctx).Update(item)
}
func (s *Module) RecordReport(ctx context.Context, r *request.ModuleRecordReport) (interface{}, error) {
return s.record.With(ctx).Report(r.ModuleID, r.Metrics, r.Dimensions, r.Filter)
}
func (s *Module) RecordList(ctx context.Context, r *request.ModuleRecordList) (interface{}, error) {
return s.record.With(ctx).Find(r.ModuleID, r.Filter, r.Sort, r.Page, r.PerPage)
}
func (s *Module) RecordRead(ctx context.Context, r *request.ModuleRecordRead) (interface{}, error) {
return s.record.With(ctx).FindByID(r.RecordID)
}
func (s *Module) RecordCreate(ctx context.Context, r *request.ModuleRecordCreate) (interface{}, error) {
return s.record.With(ctx).Create(&types.Record{ModuleID: r.ModuleID, Values: r.Values})
}
func (s *Module) RecordUpdate(ctx context.Context, r *request.ModuleRecordUpdate) (interface{}, error) {
return s.record.With(ctx).Update(&types.Record{
ID: r.RecordID,
ModuleID: r.ModuleID,
Values: r.Values})
}
func (s *Module) RecordDelete(ctx context.Context, r *request.ModuleRecordDelete) (interface{}, error) {
return resputil.OK(), s.record.With(ctx).DeleteByID(r.RecordID)
} }

View File

@@ -13,13 +13,15 @@ import (
type ( type (
Page struct { Page struct {
page service.PageService page service.PageService
attachment service.AttachmentService
} }
) )
func (Page) New() *Page { func (Page) New() *Page {
return &Page{ return &Page{
page: service.DefaultPage, page: service.DefaultPage,
attachment: service.DefaultAttachment,
} }
} }
@@ -71,3 +73,26 @@ func (ctrl *Page) Update(ctx context.Context, r *request.PageUpdate) (interface{
func (ctrl *Page) Delete(ctx context.Context, r *request.PageDelete) (interface{}, error) { func (ctrl *Page) Delete(ctx context.Context, r *request.PageDelete) (interface{}, error) {
return resputil.OK(), ctrl.page.With(ctx).DeleteByID(r.PageID) return resputil.OK(), ctrl.page.With(ctx).DeleteByID(r.PageID)
} }
func (ctrl *Page) Upload(ctx context.Context, r *request.PageUpload) (interface{}, error) {
// @todo [SECURITY] check if attachments can be added to this page
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
a, err := ctrl.attachment.With(ctx).CreatePageAttachment(
r.Upload.Filename,
r.Upload.Size,
file,
r.PageID,
)
if err != nil {
return nil, err
}
return makeAttachmentPayload(a), nil
}

View File

@@ -0,0 +1,67 @@
package rest
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/crusttech/crust/crm/rest/request"
"github.com/crusttech/crust/crm/service"
)
var _ = errors.Wrap
type PageAttachment struct {
att service.AttachmentService
}
func (PageAttachment) New() *PageAttachment {
return &PageAttachment{att: service.DefaultAttachment}
}
func (ctrl *PageAttachment) Upload(ctx context.Context, r *request.PageAttachmentUpload) (interface{}, error) {
// @todo [SECURITY] check if attachments can be added to this page
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
a, err := ctrl.att.With(ctx).CreatePageAttachment(
r.Upload.Filename,
r.Upload.Size,
file,
r.PageID,
)
if err != nil {
return nil, err
}
baseURL := fmt.Sprintf("/page/%d/attachment", r.PageID)
return makeAttachmentPayload(baseURL, a), nil
}
func (ctrl *PageAttachment) Details(ctx context.Context, r *request.PageAttachmentDetails) (interface{}, error) {
// @todo [SECURITY] check if page can be accessed
// @todo [SECURITY] test any of the page blocks contain this attachment, return 404 if not
if a, err := ctrl.att.FindByID(r.AttachmentID); err != nil {
return nil, err
} else {
return makePageAttachmentPayload(r.PageID, a), nil
}
}
func (ctrl *PageAttachment) Original(ctx context.Context, r *request.PageAttachmentOriginal) (interface{}, error) {
// @todo [SECURITY] check if page can be accessed
// @todo [SECURITY] test any of the page blocks contain this attachment, return 404 if not
return loadAttachedFile(ctrl.att, r.AttachmentID, false, r.Download)
}
func (ctrl *PageAttachment) Preview(ctx context.Context, r *request.PageAttachmentPreview) (interface{}, error) {
// @todo [SECURITY] check if page can be accessed
// @todo [SECURITY] test any of the page blocks contain this attachment, return 404 if not
return loadAttachedFile(ctrl.att, r.AttachmentID, true, false)
}

79
crm/rest/record.go Normal file
View File

@@ -0,0 +1,79 @@
package rest
import (
"context"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/crm/rest/request"
"github.com/crusttech/crust/crm/service"
"github.com/crusttech/crust/crm/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
type Record struct {
record service.RecordService
attachment service.AttachmentService
}
func (Record) New() *Record {
return &Record{
record: service.DefaultRecord,
attachment: service.DefaultAttachment,
}
}
func (ctrl *Record) Report(ctx context.Context, r *request.RecordReport) (interface{}, error) {
return ctrl.record.With(ctx).Report(r.ModuleID, r.Metrics, r.Dimensions, r.Filter)
}
func (ctrl *Record) List(ctx context.Context, r *request.RecordList) (interface{}, error) {
return ctrl.record.With(ctx).Find(r.ModuleID, r.Filter, r.Sort, r.Page, r.PerPage)
}
func (ctrl *Record) Read(ctx context.Context, r *request.RecordRead) (interface{}, error) {
return ctrl.record.With(ctx).FindByID(r.RecordID)
}
func (ctrl *Record) Create(ctx context.Context, r *request.RecordCreate) (interface{}, error) {
return ctrl.record.With(ctx).Create(&types.Record{ModuleID: r.ModuleID, Values: r.Values})
}
func (ctrl *Record) Update(ctx context.Context, r *request.RecordUpdate) (interface{}, error) {
return ctrl.record.With(ctx).Update(&types.Record{
ID: r.RecordID,
ModuleID: r.ModuleID,
Values: r.Values})
}
func (ctrl *Record) Delete(ctx context.Context, r *request.RecordDelete) (interface{}, error) {
return resputil.OK(), ctrl.record.With(ctx).DeleteByID(r.RecordID)
}
func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interface{}, error) {
// @todo [SECURITY] check if attachments can be added to this page
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
a, err := ctrl.attachment.With(ctx).CreateRecordAttachment(
r.Upload.Filename,
r.Upload.Size,
file,
r.ModuleID,
r.RecordID,
r.FieldName,
)
if err != nil {
return nil, err
}
return makeAttachmentPayload(a), nil
}

View File

@@ -0,0 +1,69 @@
package rest
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/crusttech/crust/crm/rest/request"
"github.com/crusttech/crust/crm/service"
)
var _ = errors.Wrap
type RecordAttachment struct {
att service.AttachmentService
}
func (RecordAttachment) New() *RecordAttachment {
return &RecordAttachment{att: service.DefaultAttachment}
}
func (ctrl *RecordAttachment) Upload(ctx context.Context, r *request.RecordAttachmentUpload) (interface{}, error) {
// @todo [SECURITY] check if attachments can be added to this page
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
a, err := ctrl.att.With(ctx).CreateRecordAttachment(
r.Upload.Filename,
r.Upload.Size,
file,
r.ModuleID,
r.RecordID,
r.FieldName,
)
if err != nil {
return nil, err
}
baseURL := fmt.Sprintf("/module/%d/record/%d/attachment/%s/", r.ModuleID, r.RecordID, r.FieldName)
return makeAttachmentPayload(baseURL, a), nil
}
func (ctrl *RecordAttachment) Details(ctx context.Context, r *request.RecordAttachmentDetails) (interface{}, error) {
// @todo [security] check if record can be accessed
// @todo [SECURITY] test if module/record/field has this attachment, return 404 if not
if a, err := ctrl.att.FindByID(r.AttachmentID); err != nil {
return nil, err
} else {
return makeRecordAttachmentPayload(r.ModuleID, r.RecordID, r.FieldName, a), nil
}
}
func (ctrl *RecordAttachment) Original(ctx context.Context, r *request.RecordAttachmentOriginal) (interface{}, error) {
// @todo [security] check if record can be accessed
// @todo [SECURITY] test if module/record/field has this attachment, return 404 if not
return loadAttachedFile(ctrl.att, r.AttachmentID, false, r.Download)
}
func (ctrl *RecordAttachment) Preview(ctx context.Context, r *request.RecordAttachmentPreview) (interface{}, error) {
// @todo [security] check if record can be accessed
// @todo [SECURITY] test if module/record/field has this attachment, return 404 if not
return loadAttachedFile(ctrl.att, r.AttachmentID, true, false)
}

View File

@@ -0,0 +1,247 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `attachment.go`, `attachment.util.go` or `attachment_test.go` to
implement your API calls, helper functions and tests. The file `attachment.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"encoding/json"
"io"
"mime/multipart"
"net/http"
"strings"
"github.com/go-chi/chi"
"github.com/pkg/errors"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// Attachment list request parameters
type AttachmentList struct {
PageID uint64 `json:",string"`
ModuleID uint64 `json:",string"`
RecordID uint64 `json:",string"`
FieldName string
Page uint
PerPage uint
Kind string
}
func NewAttachmentList() *AttachmentList {
return &AttachmentList{}
}
func (aReq *AttachmentList) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(aReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["pageID"]; ok {
aReq.PageID = parseUInt64(val)
}
if val, ok := get["moduleID"]; ok {
aReq.ModuleID = parseUInt64(val)
}
if val, ok := get["recordID"]; ok {
aReq.RecordID = parseUInt64(val)
}
if val, ok := get["fieldName"]; ok {
aReq.FieldName = val
}
if val, ok := get["page"]; ok {
aReq.Page = parseUint(val)
}
if val, ok := get["perPage"]; ok {
aReq.PerPage = parseUint(val)
}
aReq.Kind = chi.URLParam(r, "kind")
return err
}
var _ RequestFiller = NewAttachmentList()
// Attachment details request parameters
type AttachmentDetails struct {
AttachmentID uint64 `json:",string"`
Kind string
}
func NewAttachmentDetails() *AttachmentDetails {
return &AttachmentDetails{}
}
func (aReq *AttachmentDetails) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(aReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
aReq.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID"))
aReq.Kind = chi.URLParam(r, "kind")
return err
}
var _ RequestFiller = NewAttachmentDetails()
// Attachment original request parameters
type AttachmentOriginal struct {
Download bool
AttachmentID uint64 `json:",string"`
Name string
Kind string
}
func NewAttachmentOriginal() *AttachmentOriginal {
return &AttachmentOriginal{}
}
func (aReq *AttachmentOriginal) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(aReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["download"]; ok {
aReq.Download = parseBool(val)
}
aReq.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID"))
aReq.Name = chi.URLParam(r, "name")
aReq.Kind = chi.URLParam(r, "kind")
return err
}
var _ RequestFiller = NewAttachmentOriginal()
// Attachment preview request parameters
type AttachmentPreview struct {
AttachmentID uint64 `json:",string"`
Ext string
Kind string
}
func NewAttachmentPreview() *AttachmentPreview {
return &AttachmentPreview{}
}
func (aReq *AttachmentPreview) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(aReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
aReq.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID"))
aReq.Ext = chi.URLParam(r, "ext")
aReq.Kind = chi.URLParam(r, "kind")
return err
}
var _ RequestFiller = NewAttachmentPreview()

View File

@@ -175,67 +175,6 @@ func (mReq *ModuleRead) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewModuleRead() var _ RequestFiller = NewModuleRead()
// Module attachments request parameters
type ModuleAttachments struct {
Filter string
Page int
PerPage int
Sort string
}
func NewModuleAttachments() *ModuleAttachments {
return &ModuleAttachments{}
}
func (mReq *ModuleAttachments) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(mReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["filter"]; ok {
mReq.Filter = val
}
if val, ok := get["page"]; ok {
mReq.Page = parseInt(val)
}
if val, ok := get["perPage"]; ok {
mReq.PerPage = parseInt(val)
}
if val, ok := get["sort"]; ok {
mReq.Sort = val
}
return err
}
var _ RequestFiller = NewModuleAttachments()
// Module update request parameters // Module update request parameters
type ModuleUpdate struct { type ModuleUpdate struct {
ModuleID uint64 `json:",string"` ModuleID uint64 `json:",string"`
@@ -334,304 +273,3 @@ func (mReq *ModuleDelete) Fill(r *http.Request) (err error) {
} }
var _ RequestFiller = NewModuleDelete() var _ RequestFiller = NewModuleDelete()
// Module record/report request parameters
type ModuleRecordReport struct {
Metrics string
Dimensions string
Filter string
ModuleID uint64 `json:",string"`
}
func NewModuleRecordReport() *ModuleRecordReport {
return &ModuleRecordReport{}
}
func (mReq *ModuleRecordReport) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(mReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["metrics"]; ok {
mReq.Metrics = val
}
if val, ok := get["dimensions"]; ok {
mReq.Dimensions = val
}
if val, ok := get["filter"]; ok {
mReq.Filter = val
}
mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewModuleRecordReport()
// Module record/list request parameters
type ModuleRecordList struct {
Filter string
Page int
PerPage int
Sort string
ModuleID uint64 `json:",string"`
}
func NewModuleRecordList() *ModuleRecordList {
return &ModuleRecordList{}
}
func (mReq *ModuleRecordList) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(mReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["filter"]; ok {
mReq.Filter = val
}
if val, ok := get["page"]; ok {
mReq.Page = parseInt(val)
}
if val, ok := get["perPage"]; ok {
mReq.PerPage = parseInt(val)
}
if val, ok := get["sort"]; ok {
mReq.Sort = val
}
mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewModuleRecordList()
// Module record/create request parameters
type ModuleRecordCreate struct {
ModuleID uint64 `json:",string"`
Values types.RecordValueSet
}
func NewModuleRecordCreate() *ModuleRecordCreate {
return &ModuleRecordCreate{}
}
func (mReq *ModuleRecordCreate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(mReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewModuleRecordCreate()
// Module record/read request parameters
type ModuleRecordRead struct {
ModuleID uint64 `json:",string"`
RecordID uint64 `json:",string"`
}
func NewModuleRecordRead() *ModuleRecordRead {
return &ModuleRecordRead{}
}
func (mReq *ModuleRecordRead) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(mReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
mReq.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
return err
}
var _ RequestFiller = NewModuleRecordRead()
// Module record/update request parameters
type ModuleRecordUpdate struct {
ModuleID uint64 `json:",string"`
RecordID uint64 `json:",string"`
Values types.RecordValueSet
}
func NewModuleRecordUpdate() *ModuleRecordUpdate {
return &ModuleRecordUpdate{}
}
func (mReq *ModuleRecordUpdate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(mReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
mReq.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
return err
}
var _ RequestFiller = NewModuleRecordUpdate()
// Module record/delete request parameters
type ModuleRecordDelete struct {
ModuleID uint64 `json:",string"`
RecordID uint64 `json:",string"`
}
func NewModuleRecordDelete() *ModuleRecordDelete {
return &ModuleRecordDelete{}
}
func (mReq *ModuleRecordDelete) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(mReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
mReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
mReq.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
return err
}
var _ RequestFiller = NewModuleRecordDelete()

View File

@@ -193,67 +193,6 @@ func (pReq *PageRead) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewPageRead() var _ RequestFiller = NewPageRead()
// Page attachments request parameters
type PageAttachments struct {
Filter string
Page int
PerPage int
Sort string
}
func NewPageAttachments() *PageAttachments {
return &PageAttachments{}
}
func (pReq *PageAttachments) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(pReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["filter"]; ok {
pReq.Filter = val
}
if val, ok := get["page"]; ok {
pReq.Page = parseInt(val)
}
if val, ok := get["perPage"]; ok {
pReq.PerPage = parseInt(val)
}
if val, ok := get["sort"]; ok {
pReq.Sort = val
}
return err
}
var _ RequestFiller = NewPageAttachments()
// Page tree request parameters // Page tree request parameters
type PageTree struct { type PageTree struct {
} }
@@ -455,3 +394,50 @@ func (pReq *PageDelete) Fill(r *http.Request) (err error) {
} }
var _ RequestFiller = NewPageDelete() var _ RequestFiller = NewPageDelete()
// Page upload request parameters
type PageUpload struct {
PageID uint64 `json:",string"`
Upload *multipart.FileHeader
}
func NewPageUpload() *PageUpload {
return &PageUpload{}
}
func (pReq *PageUpload) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(pReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseMultipartForm(32 << 20); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
pReq.PageID = parseUInt64(chi.URLParam(r, "pageID"))
if _, pReq.Upload, err = r.FormFile("upload"); err != nil {
return errors.Wrap(err, "error procesing uploaded file")
}
return err
}
var _ RequestFiller = NewPageUpload()

384
crm/rest/request/record.go Normal file
View File

@@ -0,0 +1,384 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `record.go`, `record.util.go` or `record_test.go` to
implement your API calls, helper functions and tests. The file `record.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"encoding/json"
"io"
"mime/multipart"
"net/http"
"strings"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"github.com/crusttech/crust/crm/types"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// Record report request parameters
type RecordReport struct {
Metrics string
Dimensions string
Filter string
ModuleID uint64 `json:",string"`
}
func NewRecordReport() *RecordReport {
return &RecordReport{}
}
func (rReq *RecordReport) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(rReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["metrics"]; ok {
rReq.Metrics = val
}
if val, ok := get["dimensions"]; ok {
rReq.Dimensions = val
}
if val, ok := get["filter"]; ok {
rReq.Filter = val
}
rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewRecordReport()
// Record list request parameters
type RecordList struct {
Filter string
Page int
PerPage int
Sort string
ModuleID uint64 `json:",string"`
}
func NewRecordList() *RecordList {
return &RecordList{}
}
func (rReq *RecordList) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(rReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["filter"]; ok {
rReq.Filter = val
}
if val, ok := get["page"]; ok {
rReq.Page = parseInt(val)
}
if val, ok := get["perPage"]; ok {
rReq.PerPage = parseInt(val)
}
if val, ok := get["sort"]; ok {
rReq.Sort = val
}
rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewRecordList()
// Record create request parameters
type RecordCreate struct {
Values types.RecordValueSet
ModuleID uint64 `json:",string"`
}
func NewRecordCreate() *RecordCreate {
return &RecordCreate{}
}
func (rReq *RecordCreate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(rReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewRecordCreate()
// Record read request parameters
type RecordRead struct {
RecordID uint64 `json:",string"`
ModuleID uint64 `json:",string"`
}
func NewRecordRead() *RecordRead {
return &RecordRead{}
}
func (rReq *RecordRead) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(rReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
rReq.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewRecordRead()
// Record update request parameters
type RecordUpdate struct {
RecordID uint64 `json:",string"`
ModuleID uint64 `json:",string"`
Values types.RecordValueSet
}
func NewRecordUpdate() *RecordUpdate {
return &RecordUpdate{}
}
func (rReq *RecordUpdate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(rReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
rReq.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewRecordUpdate()
// Record delete request parameters
type RecordDelete struct {
RecordID uint64 `json:",string"`
ModuleID uint64 `json:",string"`
}
func NewRecordDelete() *RecordDelete {
return &RecordDelete{}
}
func (rReq *RecordDelete) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(rReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
rReq.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
return err
}
var _ RequestFiller = NewRecordDelete()
// Record upload request parameters
type RecordUpload struct {
RecordID uint64 `json:",string"`
FieldName string
ModuleID uint64 `json:",string"`
Upload *multipart.FileHeader
}
func NewRecordUpload() *RecordUpload {
return &RecordUpload{}
}
func (rReq *RecordUpload) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(rReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseMultipartForm(32 << 20); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
rReq.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
rReq.FieldName = chi.URLParam(r, "fieldName")
rReq.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
if _, rReq.Upload, err = r.FormFile("upload"); err != nil {
return errors.Wrap(err, "error procesing uploaded file")
}
return err
}
var _ RequestFiller = NewRecordUpload()

View File

@@ -26,6 +26,15 @@ func parseInt(s string) int {
return i return i
} }
// parseInt parses a string to int
func parseUint(s string) uint {
if s == "" {
return 0
}
i, _ := strconv.ParseUint(s, 10, 32)
return uint(i)
}
// parseInt64 parses a string to int64 // parseInt64 parses a string to int64
func parseInt64(s string) int64 { func parseInt64(s string) int64 {
if s == "" { if s == "" {

View File

@@ -10,10 +10,14 @@ import (
func MountRoutes() func(chi.Router) { func MountRoutes() func(chi.Router) {
var ( var (
module = Module{}.New() module = Module{}.New()
record = Record{}.New()
page = Page{}.New() page = Page{}.New()
chart = Chart{}.New() chart = Chart{}.New()
trigger = Trigger{}.New() trigger = Trigger{}.New()
notification = Notification{}.New() notification = Notification{}.New()
attachment = Attachment{}.New()
// pageAttachment = PageAttachment{}.New()
// recordAttachment = RecordAttachment{}.New()
) )
// Initialize handlers & controllers. // Initialize handlers & controllers.
@@ -25,9 +29,13 @@ func MountRoutes() func(chi.Router) {
handlers.NewPage(page).MountRoutes(r) handlers.NewPage(page).MountRoutes(r)
handlers.NewModule(module).MountRoutes(r) handlers.NewModule(module).MountRoutes(r)
handlers.NewRecord(record).MountRoutes(r)
handlers.NewChart(chart).MountRoutes(r) handlers.NewChart(chart).MountRoutes(r)
handlers.NewTrigger(trigger).MountRoutes(r) handlers.NewTrigger(trigger).MountRoutes(r)
handlers.NewNotification(notification).MountRoutes(r) handlers.NewNotification(notification).MountRoutes(r)
// Use alternative handlers that support file serving
handlers.NewAttachmentDownloadable(attachment).MountRoutes(r)
}) })
} }
} }

287
crm/service/attachment.go Normal file
View File

@@ -0,0 +1,287 @@
package service
import (
"bytes"
"context"
"image"
"image/gif"
"io"
"log"
"net/http"
"path"
"strings"
"github.com/disintegration/imaging"
"github.com/edwvee/exiffix"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/crm/repository"
"github.com/crusttech/crust/crm/types"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/store"
systemService "github.com/crusttech/crust/system/service"
)
const (
attachmentPreviewMaxWidth = 320
attachmentPreviewMaxHeight = 180
)
type (
attachment struct {
db *factory.DB
ctx context.Context
store store.Store
usr systemService.UserService
attachment repository.AttachmentRepository
}
AttachmentService interface {
With(ctx context.Context) AttachmentService
FindByID(id 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)
OpenOriginal(att *types.Attachment) (io.ReadSeeker, error)
OpenPreview(att *types.Attachment) (io.ReadSeeker, error)
}
)
func Attachment(store store.Store) AttachmentService {
return (&attachment{
store: store,
usr: systemService.DefaultUser,
}).With(context.Background())
}
func (svc *attachment) With(ctx context.Context) AttachmentService {
db := repository.DB(ctx)
return &attachment{
db: db,
ctx: ctx,
store: svc.store,
usr: svc.usr.With(ctx),
attachment: repository.Attachment(ctx, db),
}
}
func (svc *attachment) FindByID(id uint64) (*types.Attachment, error) {
// @todo [SECURITY] check if record/page can be accessed
return svc.attachment.FindByID(id)
}
func (svc *attachment) Find(filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error) {
// @todo [SECURITY] enforce filter combination (page / module+record+field) & check access
return svc.attachment.Find(filter)
}
func (svc *attachment) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) {
if len(att.Url) == 0 {
return nil, nil
}
return svc.store.Open(att.Url)
}
func (svc *attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) {
if len(att.PreviewUrl) == 0 {
return nil, nil
}
return svc.store.Open(att.PreviewUrl)
}
func (svc *attachment) CreatePageAttachment(name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error) {
var currentUserID uint64 = auth.GetIdentityFromContext(svc.ctx).Identity()
// @todo verify if current user can access this page
// @todo verify if current user can upload to this page
att := &types.Attachment{
ID: factory.Sonyflake.NextID(),
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) {
var currentUserID uint64 = auth.GetIdentityFromContext(svc.ctx).Identity()
// @todo verify if current user can access this record
// @todo verify if current user can upload to this record
att := &types.Attachment{
ID: factory.Sonyflake.NextID(),
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) {
if svc.store == nil {
return errors.New("Can not create attachment: store handler not set")
}
// Extract extension but make sure path.Ext is not confused by any leading/trailing dots
att.Meta.Original.Extension = strings.Trim(path.Ext(strings.Trim(name, ".")), ".")
att.Meta.Original.Size = size
if att.Meta.Original.Mimetype, err = svc.extractMimetype(fh); err != nil {
return
}
log.Printf(
"Processing uploaded file (name: %s, size: %d, mimetype: %s)",
att.Name,
att.Meta.Original.Size,
att.Meta.Original.Mimetype)
att.Url = svc.store.Original(att.ID, att.Meta.Original.Extension)
if err = svc.store.Save(att.Url, fh); err != nil {
log.Print(err.Error())
return
}
// Process image: extract width, height, make preview
log.Printf("Image processed, error: %v", svc.processImage(fh, att))
log.Printf("File %s stored as %s", att.Name, att.Url)
return svc.db.Transaction(func() (err error) {
if att, err = svc.attachment.Create(att); err != nil {
return
}
return nil
})
}
func (svc *attachment) extractMimetype(file io.ReadSeeker) (mimetype string, err error) {
if _, err = file.Seek(0, 0); err != nil {
return
}
// Make sure we rewind when we're done
defer file.Seek(0, 0)
// See http.DetectContentType about 512 bytes
var buf = make([]byte, 512)
if _, err = file.Read(buf); err != nil {
return
}
return http.DetectContentType(buf), nil
}
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
}
var (
preview image.Image
opts []imaging.EncodeOption
format imaging.Format
previewFormat imaging.Format
animated bool
f2m = map[imaging.Format]string{
imaging.JPEG: "image/jpeg",
imaging.GIF: "image/gif",
}
f2e = map[imaging.Format]string{
imaging.JPEG: "jpg",
imaging.GIF: "gif",
}
)
if _, err = original.Seek(0, 0); err != nil {
return
}
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
return errors.Wrapf(err, "Could not get format from extension '%s'", att.Meta.Original.Extension)
}
previewFormat = format
if imaging.JPEG == format {
// Rotate image if needed
if preview, _, err = exiffix.Decode(original); err != nil {
//return errors.Wrapf(err, "Could not decode EXIF from JPEG")
}
}
if imaging.GIF == format {
// Decode all and check loops & delay to determine if GIF is animated or not
if cfg, err := gif.DecodeAll(original); err == nil {
animated = cfg.LoopCount > 0 || len(cfg.Delay) > 1
// Use first image for the preview
preview = cfg.Image[0]
} else {
return errors.Wrapf(err, "Could not decode gif config")
}
} else {
// Use GIF preview for GIFs and JPEG for everything else!
previewFormat = imaging.JPEG
// Store with a bit lower quality
opts = append(opts, imaging.JPEGQuality(85))
}
// In case of JPEG we decode the image and rotate it beforehand
// other cases are handled here
if preview == nil {
if preview, err = imaging.Decode(original); err != nil {
return errors.Wrapf(err, "Could not decode original image")
}
}
var width, height = preview.Bounds().Max.X, preview.Bounds().Max.Y
att.SetOriginalImageMeta(width, height, animated)
if width > attachmentPreviewMaxWidth && width > height {
// Landscape does not fit
preview = imaging.Resize(preview, attachmentPreviewMaxWidth, 0, imaging.Lanczos)
} else if height > attachmentPreviewMaxHeight {
// Height does not fit
preview = imaging.Resize(preview, 0, attachmentPreviewMaxHeight, imaging.Lanczos)
}
// Get dimensions from the preview
width, height = preview.Bounds().Max.X, preview.Bounds().Max.Y
log.Printf("Generated preview %s (%dx%dpx)", previewFormat, width, height)
var buf = &bytes.Buffer{}
if err = imaging.Encode(buf, preview, previewFormat); err != nil {
return
}
meta := att.SetPreviewImageMeta(width, height, false)
meta.Size = int64(buf.Len())
meta.Mimetype = f2m[previewFormat]
meta.Extension = f2e[previewFormat]
// Can and how we make a preview of this attachment?
att.PreviewUrl = svc.store.Preview(att.ID, meta.Extension)
return svc.store.Save(att.PreviewUrl, buf)
}
var _ AttachmentService = &attachment{}

View File

@@ -1,7 +1,10 @@
package service package service
import ( import (
"log"
"sync" "sync"
"github.com/crusttech/crust/internal/store"
) )
type ( type (
@@ -19,10 +22,16 @@ var (
DefaultPage PageService DefaultPage PageService
DefaultNotification NotificationService DefaultNotification NotificationService
DefaultPermissions PermissionsService DefaultPermissions PermissionsService
DefaultAttachment AttachmentService
) )
func Init() { func Init() {
o.Do(func() { o.Do(func() {
fs, err := store.New("var/store")
if err != nil {
log.Fatalf("Failed to initialize store: %v", err)
}
DefaultRecord = Record() DefaultRecord = Record()
DefaultModule = Module() DefaultModule = Module()
DefaultTrigger = Trigger() DefaultTrigger = Trigger()
@@ -30,5 +39,6 @@ func Init() {
DefaultChart = Chart() DefaultChart = Chart()
DefaultNotification = Notification() DefaultNotification = Notification()
DefaultPermissions = Permissions() DefaultPermissions = Permissions()
DefaultAttachment = Attachment(fs)
}) })
} }

View File

@@ -0,0 +1,67 @@
package types
// Hello! This file is auto-generated.
type (
// AttachmentSet slice of Attachment
//
// This type is auto-generated.
AttachmentSet []*Attachment
)
// Walk iterates through every slice item and calls w(Attachment) err
//
// This function is auto-generated.
func (set AttachmentSet) Walk(w func(*Attachment) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Attachment) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set AttachmentSet) Filter(f func(*Attachment) (bool, error)) (out AttachmentSet, err error) {
var ok bool
out = AttachmentSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set AttachmentSet) FindByID(ID uint64) *Attachment {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set AttachmentSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}

104
crm/types/attachment.go Normal file
View File

@@ -0,0 +1,104 @@
package types
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/pkg/errors"
)
type (
Attachment struct {
ID uint64 `db:"id" json:"attachmentID,omitempty"`
OwnerID uint64 `db:"rel_owner" json:"ownerID,omitempty"`
Kind string `db:"kind" json:"-"`
Url string `db:"url" json:"url,omitempty"`
PreviewUrl string `db:"preview_url"json:"previewUrl,omitempty"`
Name string `db:"name" json:"name,omitempty"`
Meta attachmentMeta `db:"meta" json:"meta"`
CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"`
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
}
// AttachmentFilter is used for filtering and as a return value from Find
AttachmentFilter struct {
Kind string `json:"kind,omitempty"`
PageID uint64 `json:"pageID,string,omitempty"`
RecordID uint64 `json:"recordID,string,omitempty"`
ModuleID uint64 `json:"moduleID,string,omitempty"`
FieldName string `json:"fieldName,omitempty"`
Filter string `json:"filter"`
Page uint `json:"page"`
PerPage uint `json:"perPage"`
Sort string `json:"sort"`
Count uint `json:"count"`
}
attachmentImageMeta struct {
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Animated bool `json:"animated"`
}
attachmentFileMeta struct {
Size int64 `json:"size"`
Extension string `json:"ext"`
Mimetype string `json:"mimetype"`
Image *attachmentImageMeta `json:"image,omitempty"`
}
attachmentMeta struct {
Original attachmentFileMeta `json:"original"`
Preview *attachmentFileMeta `json:"preview,omitempty"`
}
)
const (
PageAttachment string = "page"
RecordAttachment string = "record"
)
func (a *Attachment) SetOriginalImageMeta(width, height int, animated bool) *attachmentFileMeta {
a.imageMeta(&a.Meta.Original, width, height, animated)
return &a.Meta.Original
}
func (a *Attachment) SetPreviewImageMeta(width, height int, animated bool) *attachmentFileMeta {
if a.Meta.Preview == nil {
a.Meta.Preview = &attachmentFileMeta{}
}
a.imageMeta(a.Meta.Preview, width, height, animated)
return a.Meta.Preview
}
func (a *Attachment) imageMeta(in *attachmentFileMeta, width, height int, animated bool) {
if in.Image == nil {
in.Image = &attachmentImageMeta{}
}
if width > 0 && height > 0 {
in.Image.Animated = animated
in.Image.Width = width
in.Image.Height = height
}
}
func (meta *attachmentMeta) Scan(value interface{}) error {
switch value.(type) {
case nil:
*meta = attachmentMeta{}
case []uint8:
if err := json.Unmarshal(value.([]byte), meta); err != nil {
return errors.Wrapf(err, "Can not scan '%v' into attachmentMeta", value)
}
}
return nil
}
func (meta attachmentMeta) Value() (driver.Value, error) {
return json.Marshal(meta)
}

View File

@@ -150,7 +150,7 @@ func (set ModuleFieldSet) FilterByModule(moduleID uint64) (ff ModuleFieldSet) {
// IsRef tells us if value of this field be a reference to something (another record, user)? // IsRef tells us if value of this field be a reference to something (another record, user)?
func (f ModuleField) IsRef() bool { func (f ModuleField) IsRef() bool {
return f.Kind == "Record" || f.Kind == "Owner" return f.Kind == "Record" || f.Kind == "Owner" || f.Kind == "File"
} }
// UserIDs returns a slice of user IDs from all items in the set // UserIDs returns a slice of user IDs from all items in the set

View File

@@ -1,3 +1,76 @@
# Attachments
## List, filter all page attachments
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| pageID | uint64 | GET | Filter attachments by page ID | N/A | NO |
| moduleID | uint64 | GET | Filter attachments by mnodule ID | N/A | NO |
| recordID | uint64 | GET | Filter attachments by record ID | N/A | NO |
| fieldName | string | GET | Filter attachments by field name | N/A | NO |
| page | uint | GET | Page number (0 based) | N/A | NO |
| perPage | uint | GET | Returned items per page (default 50) | N/A | NO |
| kind | string | PATH | Attachment kind | N/A | YES |
## Attachment details
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| kind | string | PATH | Attachment kind | N/A | YES |
## Serves attached file
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}/original/{name}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| download | bool | GET | Force file download | N/A | NO |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| name | string | PATH | File name | N/A | YES |
| kind | string | PATH | Attachment kind | N/A | YES |
## Serves preview of an attached file
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}/preview.{ext}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| ext | string | PATH | Preview extension/format | N/A | YES |
| kind | string | PATH | Attachment kind | N/A | YES |
# Charts # Charts
## List/read charts from module section ## List/read charts from module section
@@ -154,102 +227,6 @@ CRM module definitions
| --------- | ---- | ------ | ----------- | ------- | --------- | | --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES | | moduleID | uint64 | PATH | Module ID | N/A | YES |
## Generates report from module records
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/report` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| metrics | string | GET | Metrics (eg: 'SUM(money), MAX(calls)') | N/A | NO |
| dimensions | string | GET | Dimensions (eg: 'DATE(foo), status') | N/A | YES |
| filter | string | GET | Filter (eg: 'DATE(foo) > 2010') | N/A | NO |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## List/read records from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| filter | string | GET | Filtering condition | N/A | NO |
| page | int | GET | Page number (0 based) | N/A | NO |
| perPage | int | GET | Returned items per page (default 50) | N/A | NO |
| sort | string | GET | Sort field (default id desc) | N/A | NO |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Create record in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| values | types.RecordValueSet | POST | Record values | N/A | YES |
## Read records by ID from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| recordID | uint64 | PATH | Record ID | N/A | YES |
## Update records in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| values | types.RecordValueSet | POST | Record values | N/A | YES |
## Delete record row from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| recordID | uint64 | PATH | Record ID | N/A | YES |
@@ -391,6 +368,141 @@ CRM module pages
| --------- | ---- | ------ | ----------- | ------- | --------- | | --------- | ---- | ------ | ----------- | ------- | --------- |
| pageID | uint64 | PATH | Page ID | N/A | YES | | pageID | uint64 | PATH | Page ID | N/A | YES |
## Uploads attachment to page
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/{pageID}/attachment` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| pageID | uint64 | PATH | Page ID | N/A | YES |
| upload | *multipart.FileHeader | POST | File to upload | N/A | YES |
# Records
CRM records
## Generates report from module records
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/report` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| metrics | string | GET | Metrics (eg: 'SUM(money), MAX(calls)') | N/A | NO |
| dimensions | string | GET | Dimensions (eg: 'DATE(foo), status') | N/A | YES |
| filter | string | GET | Filter (eg: 'DATE(foo) > 2010') | N/A | NO |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## List/read records from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| filter | string | GET | Filtering condition | N/A | NO |
| page | int | GET | Page number (0 based) | N/A | NO |
| perPage | int | GET | Returned items per page (default 50) | N/A | NO |
| sort | string | GET | Sort field (default id desc) | N/A | NO |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Create record in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| values | types.RecordValueSet | POST | Record values | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Read records by ID from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Update records in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| values | types.RecordValueSet | POST | Record values | N/A | YES |
## Delete record row from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Uploads attachment and validates it against record field requirements
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}/{fieldName}/attachment` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| fieldName | string | PATH | Field name | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| upload | *multipart.FileHeader | POST | File to upload | N/A | YES |

View File

@@ -28,7 +28,7 @@ func Init() {
o.Do(func() { o.Do(func() {
fs, err := store.New("var/store") fs, err := store.New("var/store")
if err != nil { if err != nil {
log.Fatalf("Failed to initialize stor: %v", err) log.Fatalf("Failed to initialize store: %v", err)
} }
DefaultPermissions = Permissions() DefaultPermissions = Permissions()

File diff suppressed because one or more lines are too long