1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-28 13:48:49 +08:00
Mirko Teodorovic d2af0602a0
NOISSUE - Fix dates not being init properly on save, change path construction, replace UUID with ULID for group ID (#1300)
* fix path, group saving with parent

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* change path, enable name change

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert changes for port

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* replace UUID with ULID

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add ulid

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* fix migrations

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert user groups

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add ulid provider

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* enable group name change

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* use null string for parent id

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove migrations, disable group delete if group not empty

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* revert docker compose

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add ulid provider

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove dash character replacment

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* add ulid lib

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename UUIDProvider

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* rename package alias

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

* remove unused error

Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
2020-12-17 17:08:28 +01:00

602 lines
15 KiB
Go

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/gofrs/uuid"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/mainflux/mainflux/internal/groups"
"github.com/mainflux/mainflux/pkg/errors"
)
const maxLevel = 5
var (
errDeleteGroupDB = errors.New("delete group failed")
errSelectDb = errors.New("select group from db error")
errConvertingStringToUUID = errors.New("error converting string")
)
var _ groups.Repository = (*groupRepository)(nil)
type groupRepository struct {
db Database
}
// NewGroupRepo instantiates a PostgreSQL implementation of group
// repository.
func NewGroupRepo(db Database) groups.Repository {
return &groupRepository{
db: db,
}
}
func (gr groupRepository) Save(ctx context.Context, g groups.Group) (groups.Group, error) {
var id string
q := `INSERT INTO thing_groups (name, description, id, owner_id, metadata, path, created_at, updated_at)
VALUES (:name, :description, :id, :owner_id, :metadata, CAST(:id AS ltree), now(), now()) RETURNING id`
if g.ParentID != "" {
q = `INSERT INTO thing_groups (name, description, id, owner_id, parent_id, metadata, path, created_at, updated_at)
SELECT :name, :description, :id, :owner_id, :parent_id, :metadata, text2ltree(ltree2text(tg.path) || '.' || CAST(:id AS TEXT)), now(), now() FROM thing_groups tg WHERE id = :parent_id RETURNING id`
}
dbu, err := toDBGroup(g)
if err != nil {
return groups.Group{}, 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 groups.Group{}, errors.Wrap(groups.ErrMalformedEntity, err)
case errDuplicate:
return groups.Group{}, errors.Wrap(groups.ErrGroupConflict, err)
}
}
return groups.Group{}, errors.Wrap(groups.ErrCreateGroup, err)
}
defer row.Close()
row.Next()
if err := row.Scan(&id); err != nil {
return groups.Group{}, err
}
g.ID = id
return g, nil
}
func (gr groupRepository) Update(ctx context.Context, g groups.Group) (groups.Group, error) {
q := `UPDATE thing_groups SET description = :description, name = :name, metadata = :metadata, updated_at = now() WHERE id = :id`
dbu, err := toDBGroup(g)
if err != nil {
return groups.Group{}, errors.Wrap(errUpdateDB, err)
}
if _, err := gr.db.NamedExecContext(ctx, q, dbu); err != nil {
return groups.Group{}, errors.Wrap(errUpdateDB, err)
}
return g, nil
}
func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
qd := `DELETE FROM thing_groups WHERE id = :id`
group := groups.Group{
ID: groupID,
}
dbg, err := toDBGroup(group)
if err != nil {
return errors.Wrap(errUpdateDB, err)
}
res, err := gr.db.NamedExecContext(ctx, qd, dbg)
if err != nil {
return errors.Wrap(errDeleteGroupDB, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(errDeleteGroupDB, err)
}
if cnt != 1 {
return errors.Wrap(groups.ErrDeleteGroup, err)
}
return nil
}
func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (groups.Group, error) {
dbu := dbGroup{
ID: id,
}
q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level FROM thing_groups WHERE id = $1`
if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
if err == sql.ErrNoRows {
return groups.Group{}, errors.Wrap(groups.ErrNotFound, err)
}
return groups.Group{}, errors.Wrap(errRetrieveDB, err)
}
return toGroup(dbu)
}
func (gr groupRepository) RetrieveAll(ctx context.Context, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
}
if mq != "" {
mq = fmt.Sprintf("AND %s", mq)
}
q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM thing_groups
WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
cq := fmt.Sprintf("SELECT COUNT(*) FROM thing_groups WHERE nlevel(path) <= :level %s", mq)
dbPage, err := toDBGroupPage("", "", "", "", level, gm)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
items, err := processRows(rows)
if err != nil {
return groups.GroupPage{}, err
}
total, err := total(ctx, gr.db, cq, dbPage)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
page := groups.GroupPage{
Groups: items,
PageMetadata: groups.PageMetadata{
Total: total,
},
}
return page, nil
}
func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
if groupID == "" {
return groups.GroupPage{}, nil
}
_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
}
if mq != "" {
mq = fmt.Sprintf("AND %s", mq)
}
q := fmt.Sprintf(`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 thing_groups parent, thing_groups g
WHERE parent.id = :parent_id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level %s`, mq)
cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_groups parent, thing_groups g WHERE parent.id = :parent_id AND g.path @> parent.path %s`, mq)
if level > maxLevel {
level = maxLevel
}
dbPage, err := toDBGroupPage("", "", groupID, "", level, gm)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
items, err := processRows(rows)
if err != nil {
return groups.GroupPage{}, err
}
total, err := total(ctx, gr.db, cq, dbPage)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
page := groups.GroupPage{
Groups: items,
PageMetadata: groups.PageMetadata{
Total: total,
},
}
return page, nil
}
func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
if groupID == "" {
return groups.GroupPage{}, nil
}
_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
}
if mq != "" {
mq = fmt.Sprintf("AND %s", mq)
}
q := fmt.Sprintf(`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 thing_groups parent, thing_groups g
WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) <= :level %s`, mq)
cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_groups parent, thing_groups g WHERE parent.id = :id AND g.path <@ parent.path %s`, mq)
if level > maxLevel {
level = maxLevel
}
dbPage, err := toDBGroupPage("", groupID, "", "", level, gm)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
items, err := processRows(rows)
if err != nil {
return groups.GroupPage{}, err
}
total, err := total(ctx, gr.db, cq, dbPage)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
page := groups.GroupPage{
Groups: items,
PageMetadata: groups.PageMetadata{
Total: total,
},
}
return page, nil
}
func (gr groupRepository) Members(ctx context.Context, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
m, mq, err := getGroupsMetadataQuery("things_group", gm)
if err != nil {
return groups.MemberPage{}, errors.Wrap(errRetrieveDB, err)
}
q := fmt.Sprintf(`SELECT th.id, th.name, th.key, th.metadata FROM things th, thing_group_relations g
WHERE th.id = g.thing_id AND g.group_id = :group
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
params := map[string]interface{}{
"group": groupID,
"limit": limit,
"offset": offset,
"metadata": m,
}
rows, err := gr.db.NamedQueryContext(ctx, q, params)
if err != nil {
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
var items []groups.Member
for rows.Next() {
dbTh := dbThing{}
if err := rows.StructScan(&dbTh); err != nil {
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
}
thing, err := toThing(dbTh)
if err != nil {
return groups.MemberPage{}, err
}
items = append(items, thing)
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM things th, thing_group_relations g
WHERE th.id = g.thing_id AND g.group_id = :group %s;`, mq)
total, err := total(ctx, gr.db, cq, params)
if err != nil {
return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
}
page := groups.MemberPage{
Members: items,
PageMetadata: groups.PageMetadata{
Total: total,
Offset: offset,
Limit: limit,
},
}
return page, nil
}
func (gr groupRepository) Memberships(ctx context.Context, userID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
m, mq, err := getGroupsMetadataQuery("thing_groups", gm)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errRetrieveDB, 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 thing_group_relations gr, thing_groups g
WHERE gr.group_id = g.id and gr.thing_id = :userID
%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
params := map[string]interface{}{
"userID": userID,
"limit": limit,
"offset": offset,
"metadata": m,
}
rows, err := gr.db.NamedQueryContext(ctx, q, params)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
defer rows.Close()
var items []groups.Group
for rows.Next() {
dbgr := dbGroup{}
if err := rows.StructScan(&dbgr); err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
gr, err := toGroup(dbgr)
if err != nil {
return groups.GroupPage{}, err
}
items = append(items, gr)
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_group_relations gr, thing_groups g
WHERE gr.group_id = g.id and gr.thing_id = :userID %s;`, mq)
total, err := total(ctx, gr.db, cq, params)
if err != nil {
return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
}
page := groups.GroupPage{
Groups: items,
PageMetadata: groups.PageMetadata{
Total: total,
Offset: offset,
Limit: limit,
},
}
return page, nil
}
func (gr groupRepository) Assign(ctx context.Context, thingID, groupID string) error {
dbr, err := toDBGroupRelation(thingID, groupID)
if err != nil {
return errors.Wrap(groups.ErrAssignToGroup, err)
}
qIns := `INSERT INTO thing_group_relations (group_id, thing_id) VALUES (:group_id, :thing_id)`
_, err = gr.db.NamedQueryContext(ctx, qIns, dbr)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return errors.Wrap(groups.ErrMalformedEntity, err)
case errDuplicate:
return errors.Wrap(groups.ErrGroupConflict, err)
case errFK:
return errors.Wrap(groups.ErrNotFound, err)
}
}
return errors.Wrap(groups.ErrAssignToGroup, err)
}
return nil
}
func (gr groupRepository) Unassign(ctx context.Context, userID, groupID string) error {
q := `DELETE FROM thing_group_relations WHERE thing_id = :thing_id AND group_id = :group_id`
dbr, err := toDBGroupRelation(userID, groupID)
if err != nil {
return errors.Wrap(groups.ErrNotFound, err)
}
if _, err := gr.db.NamedExecContext(ctx, q, dbr); err != nil {
return errors.Wrap(groups.ErrGroupConflict, err)
}
return nil
}
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"`
Size uint64 `db:"size"`
}
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 "", errConvertingStringToUUID
}
func toDBGroup(g groups.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(ownerID, id, parentID, path string, level uint64, metadata groups.Metadata) (dbGroupPage, error) {
owner, err := toUUID(ownerID)
if err != nil {
return dbGroupPage{}, err
}
if err != nil {
return dbGroupPage{}, err
}
return dbGroupPage{
Metadata: dbMetadata(metadata),
ID: id,
OwnerID: owner,
Level: level,
Path: path,
ParentID: parentID,
}, nil
}
func toGroup(dbu dbGroup) (groups.Group, error) {
ownerID, err := toString(dbu.OwnerID)
if err != nil {
return groups.Group{}, err
}
return groups.Group{
ID: dbu.ID,
Name: dbu.Name,
ParentID: dbu.ParentID.String,
OwnerID: ownerID,
Description: dbu.Description,
Metadata: groups.Metadata(dbu.Metadata),
Level: dbu.Level,
Path: dbu.Path,
UpdatedAt: dbu.UpdatedAt,
CreatedAt: dbu.CreatedAt,
}, nil
}
type dbGroupRelation struct {
GroupID string `db:"group_id"`
ThingID uuid.UUID `db:"thing_id"`
}
func toDBGroupRelation(thingID, groupID string) (dbGroupRelation, error) {
thID, err := uuid.FromString(thingID)
if err != nil {
return dbGroupRelation{}, err
}
return dbGroupRelation{
GroupID: groupID,
ThingID: thID,
}, nil
}
func getGroupsMetadataQuery(db string, m groups.Metadata) ([]byte, string, error) {
mq := ""
mb := []byte("{}")
if len(m) > 0 {
mq = db + `.metadata @> :metadata`
if db == "" {
mq = `metadata @> :metadata`
}
b, err := json.Marshal(m)
if err != nil {
return nil, "", err
}
mb = b
}
return mb, mq, nil
}
func processRows(rows *sqlx.Rows) ([]groups.Group, error) {
var items []groups.Group
for rows.Next() {
dbgr := dbGroup{}
if err := rows.StructScan(&dbgr); err != nil {
return items, errors.Wrap(errSelectDb, err)
}
gr, err := toGroup(dbgr)
if err != nil {
continue
}
items = append(items, gr)
}
return items, nil
}