1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-27 13:48:49 +08:00
Manuel Imperiale 9972d1d1a4
MF-1240 - Return to service transport layer only service errors (#1559)
* MF-1240 - Return to service transport layer only service errors

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove unecessary errors

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm duplicated errors and fix transport

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Revert http endpoint_test

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix conflict

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

Co-authored-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2022-02-14 22:49:23 +01:00

743 lines
20 KiB
Go

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/gofrs/uuid"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/mainflux/mainflux/auth"
"github.com/mainflux/mainflux/pkg/errors"
)
var (
errStringToUUID = errors.New("error converting string to uuid")
errGetTotal = errors.New("failed to get total number of groups")
errCreateMetadataQuery = errors.New("failed to create query for metadata")
errTruncation = "string_data_right_truncation"
errFK = "foreign_key_violation"
groupIDFkeyy = "group_relations_group_id_fkey"
)
var _ auth.GroupRepository = (*groupRepository)(nil)
type groupRepository struct {
db Database
}
// NewGroupRepo instantiates a PostgreSQL implementation of group
// repository.
func NewGroupRepo(db Database) auth.GroupRepository {
return &groupRepository{
db: db,
}
}
func (gr groupRepository) Save(ctx context.Context, g auth.Group) (auth.Group, error) {
// For root group path is initialized with id
q := `INSERT INTO groups (name, description, id, path, owner_id, metadata, created_at, updated_at)
VALUES (:name, :description, :id, :id, :owner_id, :metadata, :created_at, :updated_at)
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
if g.ParentID != "" {
// Path is constructed in insert_group_tr - init.go
q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, created_at, updated_at)
VALUES ( :name, :description, :id, :owner_id, :parent_id, :metadata, :created_at, :updated_at)
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
}
dbg, err := toDBGroup(g)
if err != nil {
return auth.Group{}, err
}
row, err := gr.db.NamedQueryContext(ctx, q, dbg)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
case errFK:
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, err)
case errDuplicate:
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
}
}
return auth.Group{}, errors.Wrap(errors.ErrCreateEntity, errors.New(pqErr.Message))
}
defer row.Close()
row.Next()
dbg = dbGroup{}
if err := row.StructScan(&dbg); err != nil {
return auth.Group{}, err
}
return toGroup(dbg)
}
func (gr groupRepository) Update(ctx context.Context, g auth.Group) (auth.Group, error) {
q := `UPDATE groups SET name = :name, description = :description, metadata = :metadata, updated_at = :updated_at WHERE id = :id
RETURNING id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at`
dbu, err := toDBGroup(g)
if err != nil {
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, err)
}
row, err := gr.db.NamedQueryContext(ctx, q, dbu)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return auth.Group{}, errors.Wrap(errors.ErrMalformedEntity, err)
case errDuplicate:
return auth.Group{}, errors.Wrap(errors.ErrConflict, err)
}
}
return auth.Group{}, errors.Wrap(errors.ErrUpdateEntity, errors.New(pqErr.Message))
}
defer row.Close()
row.Next()
dbu = dbGroup{}
if err := row.StructScan(&dbu); err != nil {
return g, errors.Wrap(errors.ErrUpdateEntity, err)
}
return toGroup(dbu)
}
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
qd := `DELETE FROM groups WHERE id = :id`
group := auth.Group{
ID: groupID,
}
dbg, err := toDBGroup(group)
if err != nil {
return errors.Wrap(errors.ErrUpdateEntity, err)
}
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(errors.ErrMalformedEntity, err)
case errFK:
switch pqErr.Constraint {
case groupIDFkeyy:
return errors.Wrap(auth.ErrGroupNotEmpty, err)
}
return errors.Wrap(errors.ErrConflict, err)
}
}
return errors.Wrap(errors.ErrUpdateEntity, errors.New(pqErr.Message))
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(errors.ErrRemoveEntity, err)
}
if cnt != 1 {
return errors.Wrap(errors.ErrRemoveEntity, err)
}
return nil
}
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (auth.Group, error) {
dbu := dbGroup{
ID: id,
}
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups WHERE id = $1`
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return auth.Group{}, errors.Wrap(errors.ErrNotFound, err)
}
return auth.Group{}, errors.Wrap(errors.ErrViewEntity, err)
}
return toGroup(dbu)
}
func (gr groupRepository) RetrieveAll(ctx context.Context, pm auth.PageMetadata) (auth.GroupPage, error) {
_, metaQuery, err := getGroupsMetadataQuery("groups", pm.Metadata)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
var mq string
if metaQuery != "" {
mq = fmt.Sprintf(" AND %s", metaQuery)
}
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM groups
WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
dbPage, err := toDBGroupPage("", "", pm)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
defer rows.Close()
items, err := gr.processRows(rows)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
cq := "SELECT COUNT(*) FROM groups"
if metaQuery != "" {
cq = fmt.Sprintf(" %s WHERE %s", cq, metaQuery)
}
total, err := total(ctx, gr.db, cq, dbPage)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveAll, err)
}
page := auth.GroupPage{
Groups: items,
PageMetadata: auth.PageMetadata{
Total: total,
Size: uint64(len(items)),
},
}
return page, nil
}
func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
FROM groups parent, groups g
WHERE parent.id = :id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level`
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path @> parent.path`
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveParents, err)
}
return gp, nil
}
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, pm auth.PageMetadata) (auth.GroupPage, error) {
q := `SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
FROM groups parent, groups g
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) < :level`
cq := `SELECT COUNT(*) FROM groups parent, groups g WHERE parent.id = :id AND g.path <@ parent.path `
gp, err := gr.retrieve(ctx, groupID, q, cq, pm)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveChildren, err)
}
return gp, nil
}
func (gr groupRepository) retrieve(ctx context.Context, groupID, retQuery, cntQuery string, pm auth.PageMetadata) (auth.GroupPage, error) {
if groupID == "" {
return auth.GroupPage{}, nil
}
_, mq, err := getGroupsMetadataQuery("g", pm.Metadata)
if err != nil {
return auth.GroupPage{}, err
}
if mq != "" {
mq = fmt.Sprintf("AND %s", mq)
}
retQuery = fmt.Sprintf(`%s %s`, retQuery, mq)
cntQuery = fmt.Sprintf(`%s %s`, cntQuery, mq)
dbPage, err := toDBGroupPage(groupID, "", pm)
if err != nil {
return auth.GroupPage{}, err
}
rows, err := gr.db.NamedQueryContext(ctx, retQuery, dbPage)
if err != nil {
return auth.GroupPage{}, err
}
defer rows.Close()
items, err := gr.processRows(rows)
if err != nil {
return auth.GroupPage{}, err
}
total, err := total(ctx, gr.db, cntQuery, dbPage)
if err != nil {
return auth.GroupPage{}, err
}
page := auth.GroupPage{
Groups: items,
PageMetadata: auth.PageMetadata{
Level: pm.Level,
Total: total,
Size: uint64(len(items)),
},
}
return page, nil
}
func (gr groupRepository) Members(ctx context.Context, groupID, groupType string, pm auth.PageMetadata) (auth.MemberPage, error) {
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
if err != nil {
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
}
q := fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
WHERE gr.group_id = :group_id AND gr.type = :type %s`, mq)
if groupType == "" {
q = fmt.Sprintf(`SELECT gr.member_id, gr.group_id, gr.type, gr.created_at, gr.updated_at FROM group_relations gr
WHERE gr.group_id = :group_id %s`, mq)
}
params, err := toDBMemberPage("", groupID, groupType, pm)
if err != nil {
return auth.MemberPage{}, err
}
rows, err := gr.db.NamedQueryContext(ctx, q, params)
if err != nil {
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
}
defer rows.Close()
var items []auth.Member
for rows.Next() {
member := dbMember{}
if err := rows.StructScan(&member); err != nil {
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
}
if err != nil {
return auth.MemberPage{}, err
}
items = append(items, auth.Member{ID: member.MemberID, Type: member.Type})
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups g, group_relations gr
WHERE gr.group_id = :group_id AND gr.group_id = g.id AND gr.type = :type %s;`, mq)
total, err := total(ctx, gr.db, cq, params)
if err != nil {
return auth.MemberPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembers, err)
}
page := auth.MemberPage{
Members: items,
PageMetadata: auth.PageMetadata{
Total: total,
Offset: pm.Offset,
Limit: pm.Limit,
Size: uint64(len(items)),
},
}
return page, nil
}
func (gr groupRepository) Memberships(ctx context.Context, memberID string, pm auth.PageMetadata) (auth.GroupPage, error) {
_, mq, err := getGroupsMetadataQuery("groups", pm.Metadata)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
}
if mq != "" {
mq = fmt.Sprintf("AND %s", mq)
}
q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata
FROM group_relations gr, groups g
WHERE gr.group_id = g.id and gr.member_id = :member_id
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
params, err := toDBMemberPage(memberID, "", "", pm)
if err != nil {
return auth.GroupPage{}, err
}
rows, err := gr.db.NamedQueryContext(ctx, q, params)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
}
defer rows.Close()
var items []auth.Group
for rows.Next() {
dbg := dbGroup{}
if err := rows.StructScan(&dbg); err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
}
gr, err := toGroup(dbg)
if err != nil {
return auth.GroupPage{}, err
}
items = append(items, gr)
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM group_relations gr, groups g
WHERE gr.group_id = g.id and gr.member_id = :member_id %s `, mq)
total, err := total(ctx, gr.db, cq, params)
if err != nil {
return auth.GroupPage{}, errors.Wrap(auth.ErrFailedToRetrieveMembership, err)
}
page := auth.GroupPage{
Groups: items,
PageMetadata: auth.PageMetadata{
Total: total,
Offset: pm.Offset,
Limit: pm.Limit,
Size: uint64(len(items)),
},
}
return page, nil
}
func (gr groupRepository) Assign(ctx context.Context, groupID, groupType string, ids ...string) error {
tx, err := gr.db.BeginTxx(ctx, nil)
if err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
qIns := `INSERT INTO group_relations (group_id, member_id, type, created_at, updated_at)
VALUES(:group_id, :member_id, :type, :created_at, :updated_at)`
for _, id := range ids {
dbg, err := toDBGroupRelation(id, groupID, groupType)
if err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
created := time.Now()
dbg.CreatedAt = created
dbg.UpdatedAt = created
if _, err := tx.NamedExecContext(ctx, qIns, dbg); err != nil {
tx.Rollback()
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(errors.ErrMalformedEntity, err)
case errFK:
return errors.Wrap(errors.ErrConflict, errors.New(pqErr.Detail))
case errDuplicate:
return errors.Wrap(auth.ErrMemberAlreadyAssigned, errors.New(pqErr.Detail))
}
}
return errors.Wrap(auth.ErrAssignToGroup, err)
}
}
if err = tx.Commit(); err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
return nil
}
func (gr groupRepository) Unassign(ctx context.Context, groupID string, ids ...string) error {
tx, err := gr.db.BeginTxx(ctx, nil)
if err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
qDel := `DELETE from group_relations WHERE group_id = :group_id AND member_id = :member_id`
for _, id := range ids {
dbg, err := toDBGroupRelation(id, groupID, "")
if err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
if _, err := tx.NamedExecContext(ctx, qDel, dbg); err != nil {
tx.Rollback()
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(errors.ErrMalformedEntity, err)
case errDuplicate:
return errors.Wrap(errors.ErrConflict, err)
}
}
return errors.Wrap(auth.ErrAssignToGroup, err)
}
}
if err = tx.Commit(); err != nil {
return errors.Wrap(auth.ErrAssignToGroup, err)
}
return nil
}
type dbMember struct {
MemberID string `db:"member_id"`
GroupID string `db:"group_id"`
Type string `db:"type"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type dbGroup struct {
ID string `db:"id"`
ParentID sql.NullString `db:"parent_id"`
OwnerID uuid.NullUUID `db:"owner_id"`
Name string `db:"name"`
Description string `db:"description"`
Metadata dbMetadata `db:"metadata"`
Level int `db:"level"`
Path string `db:"path"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type dbGroupPage struct {
ID string `db:"id"`
ParentID string `db:"parent_id"`
OwnerID uuid.NullUUID `db:"owner_id"`
Metadata dbMetadata `db:"metadata"`
Path string `db:"path"`
Level uint64 `db:"level"`
Total uint64 `db:"total"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
}
type dbMemberPage struct {
GroupID string `db:"group_id"`
MemberID string `db:"member_id"`
Type string `db:"type"`
Metadata dbMetadata `db:"metadata"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
Size uint64
}
func toUUID(id string) (uuid.NullUUID, error) {
var uid uuid.NullUUID
if id == "" {
return uuid.NullUUID{UUID: uuid.Nil, Valid: false}, nil
}
err := uid.Scan(id)
return uid, err
}
func toString(id uuid.NullUUID) (string, error) {
if id.Valid {
return id.UUID.String(), nil
}
if id.UUID == uuid.Nil {
return "", nil
}
return "", errStringToUUID
}
func toDBGroup(g auth.Group) (dbGroup, error) {
ownerID, err := toUUID(g.OwnerID)
if err != nil {
return dbGroup{}, err
}
var parentID sql.NullString
if g.ParentID != "" {
parentID = sql.NullString{String: g.ParentID, Valid: true}
}
meta := dbMetadata(g.Metadata)
return dbGroup{
ID: g.ID,
Name: g.Name,
ParentID: parentID,
OwnerID: ownerID,
Description: g.Description,
Metadata: meta,
Path: g.Path,
CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt,
}, nil
}
func toDBGroupPage(id, path string, pm auth.PageMetadata) (dbGroupPage, error) {
level := auth.MaxLevel
if pm.Level < auth.MaxLevel {
level = pm.Level
}
return dbGroupPage{
Metadata: dbMetadata(pm.Metadata),
ID: id,
Path: path,
Level: level,
Total: pm.Total,
Offset: pm.Offset,
Limit: pm.Limit,
}, nil
}
func toDBMemberPage(memberID, groupID, groupType string, pm auth.PageMetadata) (dbMemberPage, error) {
return dbMemberPage{
GroupID: groupID,
MemberID: memberID,
Type: groupType,
Metadata: dbMetadata(pm.Metadata),
Offset: pm.Offset,
Limit: pm.Limit,
}, nil
}
func toGroup(dbu dbGroup) (auth.Group, error) {
ownerID, err := toString(dbu.OwnerID)
if err != nil {
return auth.Group{}, err
}
return auth.Group{
ID: dbu.ID,
Name: dbu.Name,
ParentID: dbu.ParentID.String,
OwnerID: ownerID,
Description: dbu.Description,
Metadata: auth.GroupMetadata(dbu.Metadata),
Level: dbu.Level,
Path: dbu.Path,
UpdatedAt: dbu.UpdatedAt,
CreatedAt: dbu.CreatedAt,
}, nil
}
type dbGroupRelation struct {
GroupID sql.NullString `db:"group_id"`
MemberID sql.NullString `db:"member_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Type string `db:"type"`
}
func toDBGroupRelation(memberID, groupID, groupType string) (dbGroupRelation, error) {
var grID sql.NullString
if groupID != "" {
grID = sql.NullString{String: groupID, Valid: true}
}
var mID sql.NullString
if memberID != "" {
mID = sql.NullString{String: memberID, Valid: true}
}
return dbGroupRelation{
GroupID: grID,
MemberID: mID,
Type: groupType,
}, nil
}
func getGroupsMetadataQuery(db string, m auth.GroupMetadata) (mb []byte, mq string, err error) {
if len(m) > 0 {
mq = `metadata @> :metadata`
if db != "" {
mq = db + "." + mq
}
b, err := json.Marshal(m)
if err != nil {
return nil, "", errors.Wrap(err, errCreateMetadataQuery)
}
mb = b
}
return mb, mq, nil
}
func (gr groupRepository) processRows(rows *sqlx.Rows) ([]auth.Group, error) {
var items []auth.Group
for rows.Next() {
dbg := dbGroup{}
if err := rows.StructScan(&dbg); err != nil {
return items, err
}
group, err := toGroup(dbg)
if err != nil {
return items, err
}
items = append(items, group)
}
return items, nil
}
func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
rows, err := db.NamedQueryContext(ctx, query, params)
if err != nil {
return 0, errors.Wrap(errGetTotal, err)
}
defer rows.Close()
total := uint64(0)
if rows.Next() {
if err := rows.Scan(&total); err != nil {
return 0, errors.Wrap(errGetTotal, err)
}
}
return total, nil
}
// dbMetadata type for handling metadata properly in database/sql
type dbMetadata map[string]interface{}
// Scan - Implement the database/sql scanner interface
func (m *dbMetadata) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return errors.ErrScanMetadata
}
if err := json.Unmarshal(b, m); err != nil {
return err
}
return nil
}
// Value Implements valuer
func (m dbMetadata) Value() (driver.Value, error) {
if len(m) == 0 {
return nil, nil
}
b, err := json.Marshal(m)
if err != nil {
return nil, err
}
return b, err
}