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  }