github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/db/script.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 "crypto/md5" 24 "encoding/hex" 25 "fmt" 26 "strings" 27 "time" 28 29 "github.com/jackc/pgerrcode" 30 "github.com/jackc/pgx/v5/pgconn" 31 "github.com/pkg/errors" 32 "gorm.io/gorm" 33 34 . "github.com/e154/smart-home/common" 35 "github.com/e154/smart-home/common/apperr" 36 ) 37 38 // Scripts ... 39 type Scripts struct { 40 Db *gorm.DB 41 } 42 43 type ScriptInfo struct { 44 AlexaIntents int `gorm:"->"` 45 EntityActions int `gorm:"->"` 46 EntityScripts int `gorm:"->"` 47 AutomationTriggers int `gorm:"->"` 48 AutomationConditions int `gorm:"->"` 49 AutomationActions int `gorm:"->"` 50 } 51 52 // Script ... 53 type Script struct { 54 ScriptInfo 55 Id int64 `gorm:"primary_key"` 56 Lang ScriptLang 57 Name string 58 Source string 59 Description string 60 Compiled string 61 Versions []*ScriptVersion 62 CreatedAt time.Time `gorm:"<-:create"` 63 UpdatedAt time.Time 64 } 65 66 type ScriptsStatistic struct { 67 Total int32 68 Used int32 69 Unused int32 70 CoffeeScript int32 71 TypeScript int32 72 JavaScript int32 73 } 74 75 // TableName ... 76 func (d *Script) TableName() string { 77 return "scripts" 78 } 79 80 // Add ... 81 func (n Scripts) Add(ctx context.Context, script *Script) (id int64, err error) { 82 if err = n.Db.WithContext(ctx).Create(&script).Error; err != nil { 83 var pgErr *pgconn.PgError 84 if errors.As(err, &pgErr) { 85 switch pgErr.Code { 86 case pgerrcode.UniqueViolation: 87 if strings.Contains(pgErr.Message, "name_at_scripts_unq") { 88 err = errors.Wrap(apperr.ErrScriptAdd, fmt.Sprintf("script name \"%s\" not unique", script.Name)) 89 return 90 } 91 default: 92 fmt.Printf("unknown code \"%s\"\n", pgErr.Code) 93 } 94 } 95 err = errors.Wrap(apperr.ErrScriptAdd, err.Error()) 96 return 97 } 98 id = script.Id 99 return 100 } 101 102 // GetById ... 103 func (n Scripts) GetById(ctx context.Context, scriptId int64) (script *Script, err error) { 104 script = &Script{} 105 err = n.Db.WithContext(ctx).Model(script). 106 Where("id = ?", scriptId). 107 Preload("Versions"). 108 First(&script).Error 109 110 if err != nil { 111 if errors.Is(err, gorm.ErrRecordNotFound) { 112 err = errors.Wrap(apperr.ErrScriptNotFound, fmt.Sprintf("id \"%d\"", scriptId)) 113 return 114 } 115 err = errors.Wrap(apperr.ErrScriptGet, err.Error()) 116 } 117 118 err = n.Db.WithContext(ctx).Raw(` 119 select 120 (select count(*) from alexa_intents where script_id = scripts.id) as alexa_intents, 121 (select count(*) from entity_actions where script_id = scripts.id) as entity_actions, 122 (select count(*) from entity_scripts where script_id = scripts.id) as entity_scripts, 123 (select count(*) from triggers where script_id = scripts.id) as automation_triggers, 124 (select count(*) from conditions where script_id = scripts.id) as automation_conditions, 125 (select count(*) from actions where script_id = scripts.id) as automation_actions 126 from scripts where id = ?`, scriptId). 127 First(&script.ScriptInfo). 128 Error 129 130 return 131 } 132 133 // GetByName ... 134 func (n Scripts) GetByName(ctx context.Context, name string) (script *Script, err error) { 135 script = &Script{} 136 err = n.Db.WithContext(ctx).Model(script). 137 Where("name = ?", name). 138 Preload("Versions"). 139 First(&script).Error 140 141 if err != nil { 142 if errors.Is(err, gorm.ErrRecordNotFound) { 143 err = errors.Wrap(apperr.ErrScriptNotFound, fmt.Sprintf("name \"%s\"", name)) 144 return 145 } 146 err = errors.Wrap(apperr.ErrScriptGet, err.Error()) 147 } 148 149 err = n.Db.WithContext(ctx).Raw(` 150 select 151 (select count(*) from alexa_intents where script_id = scripts.id) as alexa_intents, 152 (select count(*) from entity_actions where script_id = scripts.id) as entity_actions, 153 (select count(*) from entity_scripts where script_id = scripts.id) as entity_scripts, 154 (select count(*) from triggers where script_id = scripts.id) as automation_triggers, 155 (select count(*) from conditions where script_id = scripts.id) as automation_conditions, 156 (select count(*) from actions where script_id = scripts.id) as automation_actions 157 from scripts where name = ?`, name). 158 First(&script.ScriptInfo). 159 Error 160 161 return 162 } 163 164 // Update ... 165 func (n Scripts) Update(ctx context.Context, script *Script) (err error) { 166 err = n.Db.WithContext(ctx).Model(&Script{Id: script.Id}).Updates(map[string]interface{}{ 167 "name": script.Name, 168 "description": script.Description, 169 "lang": script.Lang, 170 "source": script.Source, 171 "compiled": script.Compiled, 172 }).Error 173 if err != nil { 174 var pgErr *pgconn.PgError 175 if errors.As(err, &pgErr) { 176 switch pgErr.Code { 177 case pgerrcode.UniqueViolation: 178 if strings.Contains(pgErr.Message, "name_at_scripts_unq") { 179 err = errors.Wrap(apperr.ErrScriptUpdate, fmt.Sprintf("script name \"%s\" not unique", script.Name)) 180 return 181 } 182 default: 183 fmt.Printf("unknown code \"%s\"\n", pgErr.Code) 184 } 185 } 186 err = errors.Wrap(apperr.ErrScriptUpdate, err.Error()) 187 return 188 } 189 190 hash := md5.Sum([]byte(script.Source)) 191 version := &ScriptVersion{ 192 Lang: script.Lang, 193 Source: script.Source, 194 ScriptId: script.Id, 195 Sum: []byte(hex.EncodeToString(hash[:])), 196 } 197 if err = n.Db.WithContext(ctx).Create(version).Error; err != nil { 198 err = errors.Wrap(apperr.ErrScriptVersionAdd, err.Error()) 199 return 200 } 201 202 q := `delete from script_versions 203 where id not in ( 204 select id 205 from script_versions 206 where script_id = ? 207 order by created_at desc 208 limit 10 209 ) and script_id = ?` 210 if _, err = n.Db.WithContext(ctx).Raw(q, script.Id, script.Id).Rows(); err != nil { 211 err = errors.Wrap(apperr.ErrScriptVersionDelete, err.Error()) 212 return 213 } 214 215 return 216 } 217 218 // Delete ... 219 func (n Scripts) Delete(ctx context.Context, scriptId int64) (err error) { 220 if err = n.Db.WithContext(ctx).Delete(&Script{Id: scriptId}).Error; err != nil { 221 err = errors.Wrap(apperr.ErrScriptDelete, err.Error()) 222 } 223 return 224 } 225 226 // List ... 227 func (n *Scripts) List(ctx context.Context, limit, offset int, orderBy, sort string, query *string, ids *[]uint64) (list []*Script, total int64, err error) { 228 229 list = make([]*Script, 0) 230 q := n.Db.WithContext(ctx).Model(Script{}) 231 if query != nil { 232 q = q.Where("name LIKE ? or source LIKE ?", "%"+*query+"%", "%"+*query+"%") 233 } 234 if ids != nil { 235 q = q.Where("id IN (?)", *ids) 236 } 237 if err = q.Count(&total).Error; err != nil { 238 err = errors.Wrap(apperr.ErrScriptList, err.Error()) 239 return 240 } 241 err = q. 242 Limit(limit). 243 Offset(offset). 244 Order(fmt.Sprintf("%s %s", sort, orderBy)). 245 Find(&list). 246 Error 247 if err != nil { 248 err = errors.Wrap(apperr.ErrScriptList, err.Error()) 249 } 250 return 251 } 252 253 // Search ... 254 func (n *Scripts) Search(ctx context.Context, query string, limit, offset int) (list []*Script, total int64, err error) { 255 256 q := n.Db.WithContext(ctx).Model(&Script{}). 257 Where("name LIKE ?", "%"+query+"%") 258 259 if err = q.Count(&total).Error; err != nil { 260 err = errors.Wrap(apperr.ErrScriptSearch, err.Error()) 261 return 262 } 263 264 q = q. 265 Limit(limit). 266 Offset(offset). 267 Order("name ASC") 268 269 list = make([]*Script, 0) 270 if err = q.Find(&list).Error; err != nil { 271 err = errors.Wrap(apperr.ErrScriptSearch, err.Error()) 272 } 273 return 274 } 275 276 // Statistic ... 277 func (n *Scripts) Statistic(ctx context.Context) (statistic *ScriptsStatistic, err error) { 278 279 statistic = &ScriptsStatistic{} 280 281 var usedList []struct { 282 Count int32 283 Used bool 284 } 285 err = n.Db.WithContext(ctx).Raw(` 286 select count(scripts.id), 287 (exists(select * from alexa_intents where script_id = scripts.id) or 288 exists(select * from entity_actions where script_id = scripts.id) or 289 exists(select * from entity_scripts where script_id = scripts.id) or 290 exists(select * from triggers where script_id = scripts.id) or 291 exists(select * from conditions where script_id = scripts.id) or 292 exists(select * from actions where script_id = scripts.id) ) as used 293 from scripts 294 group by used`). 295 Scan(&usedList). 296 Error 297 298 if err != nil { 299 err = errors.Wrap(apperr.ErrScriptStat, err.Error()) 300 return 301 } 302 303 for _, item := range usedList { 304 statistic.Total += item.Count 305 if item.Used { 306 statistic.Used = item.Count 307 308 continue 309 } 310 statistic.Unused = item.Count 311 } 312 313 var langList []struct { 314 Lang string 315 Count int32 316 } 317 err = n.Db.WithContext(ctx).Raw(` 318 select scripts.lang, count(scripts.*) 319 from scripts 320 group by lang`). 321 Scan(&langList). 322 Error 323 324 if err != nil { 325 err = errors.Wrap(apperr.ErrScriptStat, err.Error()) 326 return 327 } 328 329 for _, item := range langList { 330 switch item.Lang { 331 case "coffeescript": 332 statistic.CoffeeScript = item.Count 333 case "ts": 334 statistic.TypeScript = item.Count 335 case "javascript": 336 statistic.JavaScript = item.Count 337 } 338 } 339 340 return 341 }