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