github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/db/entity.go (about)

     1  // This file is part of the Smart Home
     2  // Program complex distribution https://github.com/e154/smart-home
     3  // Copyright (C) 2016-2023, Filippov Alex
     4  //
     5  // This library is free software: you can redistribute it and/or
     6  // modify it under the terms of the GNU Lesser General Public
     7  // License as published by the Free Software Foundation; either
     8  // version 3 of the License, or (at your option) any later version.
     9  //
    10  // This library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    13  // Library General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public
    16  // License along with this library.  If not, see
    17  // <https://www.gnu.org/licenses/>.
    18  
    19  package db
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/jackc/pgerrcode"
    29  	"github.com/jackc/pgx/v5/pgconn"
    30  	"github.com/pkg/errors"
    31  	"gorm.io/gorm"
    32  
    33  	"github.com/e154/smart-home/common"
    34  	"github.com/e154/smart-home/common/apperr"
    35  )
    36  
    37  // Entities ...
    38  type Entities struct {
    39  	Db *gorm.DB
    40  }
    41  
    42  // Entity ...
    43  type Entity struct {
    44  	Id           common.EntityId `gorm:"primary_key"`
    45  	Description  string
    46  	PluginName   string
    47  	Image        *Image
    48  	ImageId      *int64
    49  	States       []*EntityState
    50  	Actions      []*EntityAction
    51  	AreaId       *int64
    52  	Area         *Area
    53  	Metrics      []*Metric `gorm:"many2many:entity_metrics;"`
    54  	Scripts      []*Script `gorm:"many2many:entity_scripts;"`
    55  	Tags         []*Tag    `gorm:"many2many:entity_tags;"`
    56  	Icon         *string
    57  	Payload      json.RawMessage `gorm:"type:jsonb;not null"`
    58  	Settings     json.RawMessage `gorm:"type:jsonb;not null"`
    59  	Storage      []*EntityStorage
    60  	AutoLoad     bool
    61  	RestoreState bool
    62  	ParentId     *common.EntityId `gorm:"column:parent_id"`
    63  	CreatedAt    time.Time        `gorm:"<-:create"`
    64  	UpdatedAt    time.Time
    65  }
    66  
    67  // TableName ...
    68  func (d *Entity) TableName() string {
    69  	return "entities"
    70  }
    71  
    72  type EntitiesStatistic struct {
    73  	Total  int32
    74  	Used   int32
    75  	Unused int32
    76  }
    77  
    78  // Add ...
    79  func (n Entities) Add(ctx context.Context, v *Entity) (err error) {
    80  	err = n.Db.WithContext(ctx).Omit("Metrics.*").Omit("Tags.*").Omit("Scripts.*").Create(&v).Error
    81  	if err != nil {
    82  		var pgErr *pgconn.PgError
    83  		if errors.As(err, &pgErr) {
    84  			switch pgErr.Code {
    85  			case pgerrcode.UniqueViolation:
    86  				if strings.Contains(pgErr.Message, "entities_pkey") {
    87  					err = errors.Wrap(apperr.ErrEntityAdd, fmt.Sprintf("entity name \"%s\" not unique", v.Id))
    88  					return
    89  				}
    90  			default:
    91  				fmt.Printf("unknown code \"%s\"\n", pgErr.Code)
    92  			}
    93  		}
    94  		err = errors.Wrap(apperr.ErrEntityAdd, err.Error())
    95  	}
    96  	return
    97  }
    98  
    99  // Update ...
   100  func (n Entities) Update(ctx context.Context, v *Entity) (err error) {
   101  
   102  	err = n.Db.WithContext(ctx).
   103  		Omit("Metrics.*").
   104  		Omit("Tags.*").
   105  		Omit("Scripts.*").
   106  		Save(v).Error
   107  
   108  	if err != nil {
   109  		err = errors.Wrap(apperr.ErrEntityUpdate, err.Error())
   110  	}
   111  	return
   112  }
   113  
   114  // GetById ...
   115  func (n Entities) GetById(ctx context.Context, id common.EntityId) (v *Entity, err error) {
   116  	v = &Entity{}
   117  	err = n.Db.WithContext(ctx).Model(v).
   118  		Where("id = ?", id).
   119  		Preload("Image").
   120  		Preload("States").
   121  		Preload("States.Image").
   122  		Preload("Actions").
   123  		Preload("Actions.Image").
   124  		Preload("Actions.Script").
   125  		Preload("Area").
   126  		Preload("Metrics").
   127  		Preload("Scripts").
   128  		Preload("Tags").
   129  		Preload("Storage", func(db *gorm.DB) *gorm.DB {
   130  			return db.Limit(1).Order("entity_storage.created_at DESC")
   131  		}).
   132  		First(&v).Error
   133  
   134  	if err != nil {
   135  		if errors.Is(err, gorm.ErrRecordNotFound) {
   136  			err = errors.Wrap(apperr.ErrEntityNotFound, fmt.Sprintf("id \"%s\"", id))
   137  			return
   138  		}
   139  		err = errors.Wrap(apperr.ErrEntityGet, err.Error())
   140  		return
   141  	}
   142  
   143  	return
   144  }
   145  
   146  // GetByIds ...
   147  func (n Entities) GetByIds(ctx context.Context, ids []common.EntityId) (list []*Entity, err error) {
   148  
   149  	list = make([]*Entity, 0)
   150  	err = n.Db.WithContext(ctx).Model(Entity{}).
   151  		Where("id IN (?)", ids).
   152  		Preload("Image").
   153  		Preload("States").
   154  		Preload("States.Image").
   155  		Preload("Actions").
   156  		Preload("Actions.Image").
   157  		Preload("Actions.Script").
   158  		Preload("Area").
   159  		Preload("Metrics").
   160  		Preload("Scripts").
   161  		Preload("Tags").
   162  		//Preload("Storage", func(db *gorm.DB) *gorm.DB {
   163  		//	return db.Limit(1).Order("entity_storage.created_at DESC")
   164  		//}).
   165  		Find(&list).Error
   166  
   167  	if err != nil {
   168  		err = errors.Wrap(apperr.ErrEntityGet, err.Error())
   169  		return
   170  	}
   171  
   172  	if err = n.PreloadStorage(ctx, list); err != nil {
   173  		err = errors.Wrap(apperr.ErrEntityGet, err.Error())
   174  		return
   175  	}
   176  
   177  	return
   178  }
   179  
   180  // GetByIdsSimple ...
   181  func (n Entities) GetByIdsSimple(ctx context.Context, ids []common.EntityId) (list []*Entity, err error) {
   182  
   183  	list = make([]*Entity, 0)
   184  	err = n.Db.WithContext(ctx).Model(Entity{}).
   185  		Preload("States").
   186  		Where("id IN (?)", ids).
   187  		Find(&list).Error
   188  
   189  	if err != nil {
   190  		err = errors.Wrap(apperr.ErrEntityGet, err.Error())
   191  		return
   192  	}
   193  
   194  	return
   195  }
   196  
   197  // Delete ...
   198  func (n Entities) Delete(ctx context.Context, id common.EntityId) (err error) {
   199  
   200  	if err = n.Db.WithContext(ctx).Delete(&Entity{Id: id}).Error; err != nil {
   201  		err = errors.Wrap(apperr.ErrEntityDelete, err.Error())
   202  		return
   203  	}
   204  
   205  	return
   206  }
   207  
   208  // List ...
   209  func (n *Entities) List(ctx context.Context, limit, offset int, orderBy, sort string, autoLoad bool,
   210  	query, plugin *string, areaId *int64) (list []*Entity, total int64, err error) {
   211  
   212  	list = make([]*Entity, 0)
   213  	q := n.Db.WithContext(ctx).Model(Entity{})
   214  	if autoLoad {
   215  		q = q.Where("auto_load = ?", true)
   216  	}
   217  	if query != nil {
   218  		q = q.Where("id LIKE ?", "%"+*query+"%")
   219  	}
   220  	if plugin != nil {
   221  		q = q.Where("plugin_name = ?", *plugin)
   222  	}
   223  	if areaId != nil {
   224  		q = q.Where("area_id = ?", *areaId)
   225  	}
   226  	if err = q.Count(&total).Error; err != nil {
   227  		err = errors.Wrap(apperr.ErrEntityList, err.Error())
   228  		return
   229  	}
   230  	q = q.
   231  		Preload("Image").
   232  		Preload("States").
   233  		Preload("States.Image").
   234  		Preload("Actions").
   235  		Preload("Actions.Image").
   236  		Preload("Actions.Script").
   237  		Preload("Area").
   238  		Preload("Metrics").
   239  		Preload("Scripts").
   240  		Preload("Tags").
   241  		//Preload("Storage", func(db *gorm.DB) *gorm.DB {
   242  		//	return db.Limit(1).Order("entity_storage.created_at DESC")
   243  		//}).
   244  		Limit(limit).
   245  		Offset(offset)
   246  
   247  	if sort != "" && orderBy != "" {
   248  		q = q.Order(fmt.Sprintf("%s %s", sort, orderBy))
   249  	}
   250  
   251  	err = q.
   252  		WithContext(ctx).
   253  		Find(&list).
   254  		Error
   255  
   256  	if err != nil {
   257  		err = errors.Wrap(apperr.ErrEntityList, err.Error())
   258  		return
   259  	}
   260  
   261  	if err = n.PreloadStorage(ctx, list); err != nil {
   262  		err = errors.Wrap(apperr.ErrEntityGet, err.Error())
   263  		return
   264  	}
   265  
   266  	return
   267  }
   268  
   269  // ListPlain ...
   270  func (n *Entities) ListPlain(ctx context.Context, limit, offset int, orderBy, sort string, autoLoad bool,
   271  	query, plugin *string, areaId *int64, tags *[]string) (list []*Entity, total int64, err error) {
   272  
   273  	list = make([]*Entity, 0)
   274  	q := n.Db.WithContext(ctx).Model(Entity{})
   275  	if autoLoad {
   276  		q = q.Where("auto_load = ?", true)
   277  	}
   278  	if query != nil {
   279  		q = q.Where("id LIKE ?", "%"+*query+"%")
   280  	}
   281  	if plugin != nil {
   282  		q = q.Where("plugin_name = ?", *plugin)
   283  	}
   284  	if areaId != nil {
   285  		q = q.Where("area_id = ?", *areaId)
   286  	}
   287  	if tags != nil {
   288  		q = q.Joins(`left join entity_tags on entity_tags.entity_id = entities.id`)
   289  		q = q.Joins(`left join tags on entity_tags.tag_id = tags.id`)
   290  		q = q.Where("tags.name in (?)", *tags)
   291  	}
   292  	if err = q.Count(&total).Error; err != nil {
   293  		err = errors.Wrap(apperr.ErrEntityList, err.Error())
   294  		return
   295  	}
   296  	q = q.
   297  		Preload("Tags").
   298  		Preload("Area").
   299  		Group("entities.id").
   300  		Limit(limit).
   301  		Offset(offset)
   302  
   303  	if sort != "" && orderBy != "" {
   304  		q = q.Order(fmt.Sprintf("%s %s", sort, orderBy))
   305  	}
   306  
   307  	err = q.
   308  		WithContext(ctx).
   309  		Find(&list).
   310  		Error
   311  
   312  	if err != nil {
   313  		err = errors.Wrap(apperr.ErrEntityList, err.Error())
   314  		return
   315  	}
   316  
   317  	return
   318  }
   319  
   320  // GetByType ...
   321  func (n *Entities) GetByType(ctx context.Context, t string, limit, offset int) (list []*Entity, err error) {
   322  
   323  	list = make([]*Entity, 0)
   324  	err = n.Db.WithContext(ctx).
   325  		Model(&Entity{}).
   326  		Where("plugin_name = ? and auto_load = true", t).
   327  		Preload("Image").
   328  		Preload("States").
   329  		Preload("States.Image").
   330  		Preload("Actions").
   331  		Preload("Actions.Image").
   332  		Preload("Actions.Script").
   333  		Preload("Area").
   334  		Preload("Metrics").
   335  		Preload("Scripts").
   336  		Preload("Tags").
   337  		//Preload("Storage", func(db *gorm.DB) *gorm.DB {
   338  		//	return db.Order("entity_storage.created_at DESC").Limit(1)
   339  		//}).
   340  		Limit(limit).
   341  		Offset(offset).
   342  		Find(&list).
   343  		Error
   344  
   345  	if err != nil {
   346  		err = errors.Wrap(apperr.ErrEntityGet, err.Error())
   347  		return
   348  	}
   349  
   350  	// todo: remove
   351  	if err = n.PreloadStorage(ctx, list); err != nil {
   352  		err = errors.Wrap(apperr.ErrEntityGet, err.Error())
   353  		return
   354  	}
   355  
   356  	return
   357  }
   358  
   359  // Search ...
   360  func (n *Entities) Search(ctx context.Context, query string, limit, offset int) (list []*Entity, total int64, err error) {
   361  
   362  	q := n.Db.WithContext(ctx).Model(&Entity{}).
   363  		Where("id LIKE ?", "%"+query+"%")
   364  
   365  	if err = q.Count(&total).Error; err != nil {
   366  		err = errors.Wrap(apperr.ErrEntitySearch, err.Error())
   367  		return
   368  	}
   369  
   370  	q = q.
   371  		Limit(limit).
   372  		Offset(offset).
   373  		Order("id ASC")
   374  
   375  	list = make([]*Entity, 0)
   376  	if err = q.Find(&list).Error; err != nil {
   377  		err = errors.Wrap(apperr.ErrEntitySearch, err.Error())
   378  	}
   379  
   380  	return
   381  }
   382  
   383  // UpdateAutoload ...
   384  func (n Entities) UpdateAutoload(ctx context.Context, entityId common.EntityId, autoLoad bool) (err error) {
   385  	q := map[string]interface{}{
   386  		"auto_load": autoLoad,
   387  	}
   388  
   389  	if err = n.Db.WithContext(ctx).Model(&Entity{Id: entityId}).Updates(q).Error; err != nil {
   390  		err = errors.Wrap(apperr.ErrEntityUpdate, err.Error())
   391  	}
   392  	return
   393  }
   394  
   395  // DeleteScripts ...
   396  func (n Entities) DeleteScripts(ctx context.Context, id common.EntityId) (err error) {
   397  	if err = n.Db.WithContext(ctx).Model(&Entity{Id: id}).Association("Scripts").Clear(); err != nil {
   398  		err = errors.Wrap(apperr.ErrEntityDeleteScript, err.Error())
   399  	}
   400  	return
   401  }
   402  
   403  // DeleteTags ...
   404  func (n Entities) DeleteTags(ctx context.Context, id common.EntityId) (err error) {
   405  	if err = n.Db.WithContext(ctx).Model(&Entity{Id: id}).Association("Tags").Clear(); err != nil {
   406  		err = errors.Wrap(apperr.ErrEntityDeleteTag, err.Error())
   407  	}
   408  	return
   409  }
   410  
   411  // PreloadStorage ...
   412  func (n Entities) PreloadStorage(ctx context.Context, list []*Entity) (err error) {
   413  
   414  	//todo: fix
   415  	// temporary solution because Preload("Storage", func(db *gorm.DB) *gorm.DB { - does not work ...
   416  	for _, item := range list {
   417  		err = n.Db.WithContext(ctx).Model(&EntityStorage{}).
   418  			Order("created_at desc").
   419  			Limit(2).
   420  			Find(&item.Storage, "entity_id = ?", item.Id).
   421  			Error
   422  		if err != nil {
   423  			err = errors.Wrap(apperr.ErrEntityStorageGet, err.Error())
   424  			return
   425  		}
   426  	}
   427  
   428  	return
   429  }
   430  
   431  // Statistic ...
   432  func (n *Entities) Statistic(ctx context.Context) (statistic *EntitiesStatistic, err error) {
   433  
   434  	statistic = &EntitiesStatistic{}
   435  	//
   436  	var usedList []struct {
   437  		Count    int32
   438  		AutoLoad bool
   439  	}
   440  	err = n.Db.WithContext(ctx).Raw(`
   441  select count(e.id), e.auto_load
   442  from entities as e
   443  group by e.auto_load`).
   444  		Scan(&usedList).
   445  		Error
   446  
   447  	if err != nil {
   448  		err = errors.Wrap(apperr.ErrEntityStat, err.Error())
   449  		return
   450  	}
   451  
   452  	for _, item := range usedList {
   453  		statistic.Total += item.Count
   454  		if item.AutoLoad {
   455  			statistic.Used = item.Count
   456  
   457  			continue
   458  		}
   459  		statistic.Unused = item.Count
   460  	}
   461  
   462  	return
   463  }