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 }