github.com/sharovik/devbot@v1.0.1-0.20240308094637-4a0387c40516/internal/database/service.go (about)

     1  package database
     2  
     3  import (
     4  	"database/sql"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/sharovik/devbot/internal/dto"
    10  	"github.com/sharovik/devbot/internal/dto/databasedto"
    11  	"github.com/sharovik/devbot/internal/helper"
    12  	"github.com/sharovik/orm/clients"
    13  	cdto "github.com/sharovik/orm/dto"
    14  	cquery "github.com/sharovik/orm/query"
    15  )
    16  
    17  // Dictionary the sqlite dictionary object
    18  type Dictionary struct {
    19  	db clients.BaseClientInterface
    20  }
    21  
    22  // GetDBClient method returns the client connection
    23  func (d *Dictionary) GetDBClient() clients.BaseClientInterface {
    24  	return d.db
    25  }
    26  
    27  // InitDatabaseConnection initialise the database connection
    28  func (d *Dictionary) InitDatabaseConnection(cfg clients.DatabaseConfig) error {
    29  	var err error
    30  	d.db, err = clients.InitClient(cfg)
    31  
    32  	return err
    33  }
    34  
    35  // CloseDatabaseConnection method for database connection close
    36  func (d *Dictionary) CloseDatabaseConnection() error {
    37  	return d.db.Disconnect()
    38  }
    39  
    40  // FindAnswer used for searching of message in the database
    41  func (d *Dictionary) FindAnswer(message string) (dto.DictionaryMessage, error) {
    42  	var (
    43  		dmAnswer dto.DictionaryMessage
    44  		regexID  int64
    45  		err      error
    46  	)
    47  
    48  	//We do that because it can be that we can parse this question by available regex. If so, it will help main query to find the answer for this message
    49  	regexID, err = d.parsedByAvailableRegex(message)
    50  	if err != nil {
    51  		return dto.DictionaryMessage{}, err
    52  	}
    53  
    54  	dmAnswer, err = d.answerByQuestionString(message, regexID)
    55  	if err != nil {
    56  		return dto.DictionaryMessage{}, err
    57  	}
    58  
    59  	//Finally we parse data by using selected regex in our question
    60  	if dmAnswer.Regex != "" {
    61  		matches := helper.FindMatches(dmAnswer.Regex, message)
    62  
    63  		if len(matches) != 0 && dmAnswer.MainGroupIndexInRegex != "" && matches[dmAnswer.MainGroupIndexInRegex] != "" {
    64  			dmAnswer.Answer = fmt.Sprintf(dmAnswer.Answer, matches[dmAnswer.MainGroupIndexInRegex])
    65  		}
    66  	}
    67  
    68  	return dmAnswer, nil
    69  }
    70  
    71  func (d *Dictionary) parsedByAvailableRegex(question string) (int64, error) {
    72  	availableRegex, err := d.GetAllRegex()
    73  	if err != nil {
    74  		return int64(0), err
    75  	}
    76  
    77  	for regexID, regex := range availableRegex {
    78  		matches := helper.FindMatches(regex, question)
    79  		if len(matches) != 0 {
    80  			return regexID, nil
    81  		}
    82  	}
    83  
    84  	return 0, nil
    85  }
    86  
    87  // answerByQuestionString method retrieves the answer data by selected question string
    88  func (d *Dictionary) answerByQuestionString(questionText string, regexID int64) (dto.DictionaryMessage, error) {
    89  	query := new(clients.Query).
    90  		Select([]interface{}{
    91  			"scenarios.id",
    92  			"scenarios.event_id",
    93  			"questions.id as question_id",
    94  			"questions.answer",
    95  			"questions.question",
    96  			"questions_regex.regex as question_regex",
    97  			"questions_regex.regex_group as question_regex_group",
    98  			"events.alias",
    99  		}).From(&cdto.BaseModel{TableName: "questions"}).
   100  		Join(cquery.Join{
   101  			Target:    cquery.Reference{Table: "scenarios", Key: "id"},
   102  			With:      cquery.Reference{Table: "questions", Key: "scenario_id"},
   103  			Condition: "=",
   104  			Type:      cquery.InnerJoinType,
   105  		}).
   106  		Join(cquery.Join{
   107  			Target:    cquery.Reference{Table: "questions_regex", Key: "id"},
   108  			With:      cquery.Reference{Table: "questions", Key: "regex_id"},
   109  			Condition: "=",
   110  			Type:      cquery.LeftJoinType,
   111  		}).
   112  		Join(cquery.Join{
   113  			Target:    cquery.Reference{Table: "events", Key: "id"},
   114  			With:      cquery.Reference{Table: "scenarios", Key: "event_id"},
   115  			Condition: "=",
   116  			Type:      cquery.LeftJoinType,
   117  		})
   118  
   119  	if regexID != 0 {
   120  		query.Where(cquery.Where{
   121  			First:    "questions.regex_id",
   122  			Operator: "=",
   123  			Second: cquery.Bind{
   124  				Field: "regex_id",
   125  				Value: regexID,
   126  			},
   127  		})
   128  	} else {
   129  		query.Where(cquery.Where{
   130  			First:    "questions.question",
   131  			Operator: "LIKE",
   132  			Second: cquery.Bind{
   133  				Field: "question",
   134  				Value: questionText + "%",
   135  			},
   136  		})
   137  	}
   138  
   139  	query.
   140  		OrderBy("questions.id", cquery.OrderDirectionAsc).
   141  		Limit(cquery.Limit{From: 0, To: 1})
   142  
   143  	res, err := d.db.Execute(query)
   144  
   145  	if err == sql.ErrNoRows {
   146  		return dto.DictionaryMessage{}, nil
   147  	} else if err != nil {
   148  		return dto.DictionaryMessage{}, err
   149  	}
   150  
   151  	if len(res.Items()) == 0 {
   152  		return dto.DictionaryMessage{}, nil
   153  	}
   154  
   155  	//We take first item and use it as the result
   156  	item := res.Items()[0]
   157  
   158  	var (
   159  		r  string
   160  		rg string
   161  	)
   162  
   163  	if item.GetField("question_regex").Value != nil {
   164  		r = item.GetField("question_regex").Value.(string)
   165  	}
   166  
   167  	if item.GetField("question_regex_group").Value != nil {
   168  		rg = item.GetField("question_regex_group").Value.(string)
   169  	}
   170  
   171  	return dto.DictionaryMessage{
   172  		ScenarioID:            int64(item.GetField("id").Value.(int)),
   173  		EventID:               int64(item.GetField("event_id").Value.(int)),
   174  		Answer:                item.GetField("answer").Value.(string),
   175  		QuestionID:            int64(item.GetField("question_id").Value.(int)),
   176  		Question:              item.GetField("question").Value.(string),
   177  		Regex:                 r,
   178  		MainGroupIndexInRegex: rg,
   179  		ReactionType:          item.GetField("alias").Value.(string),
   180  	}, nil
   181  }
   182  
   183  // InsertScenario used for scenario creation
   184  func (d *Dictionary) InsertScenario(name string, eventID int64) (int64, error) {
   185  	var model = cdto.BaseModel{
   186  		TableName: "scenarios",
   187  		Fields: []interface{}{
   188  			cdto.ModelField{
   189  				Name:  "name",
   190  				Value: name,
   191  			},
   192  			cdto.ModelField{
   193  				Name:  "event_id",
   194  				Value: eventID,
   195  			},
   196  		},
   197  	}
   198  
   199  	res, err := d.db.Execute(new(clients.Query).Insert(&model))
   200  	if err != nil {
   201  		return 0, err
   202  	}
   203  
   204  	return res.LastInsertID(), nil
   205  }
   206  
   207  // FindScenarioByID search scenario by id
   208  func (d *Dictionary) FindScenarioByID(scenarioID int64) (int64, error) {
   209  	query := new(clients.Query).
   210  		Select([]interface{}{"id"}).
   211  		From(&cdto.BaseModel{TableName: "scenarios"}).
   212  		Where(cquery.Where{
   213  			First:    "id",
   214  			Operator: "=",
   215  			Second: cquery.Bind{
   216  				Field: "id",
   217  				Value: scenarioID,
   218  			},
   219  		})
   220  	res, err := d.db.Execute(query)
   221  	if err == sql.ErrNoRows {
   222  		return 0, nil
   223  	} else if err != nil {
   224  		return 0, err
   225  	}
   226  
   227  	if len(res.Items()) == 0 {
   228  		return 0, nil
   229  	}
   230  
   231  	return scenarioID, nil
   232  }
   233  
   234  // GetLastScenarioID retrieve the last scenario id
   235  func (d *Dictionary) GetLastScenarioID() (int64, error) {
   236  	query := new(clients.Query).
   237  		Select([]interface{}{cdto.ModelField{
   238  			Name: "id",
   239  			Type: cdto.IntegerColumnType,
   240  		}}).
   241  		From(databasedto.ScenariosModel).
   242  		OrderBy("id", cquery.OrderDirectionDesc).
   243  		Limit(cquery.Limit{From: 0, To: 1})
   244  	res, err := d.db.Execute(query)
   245  	if err == sql.ErrNoRows {
   246  		return 0, nil
   247  	} else if err != nil {
   248  		return 0, err
   249  	}
   250  
   251  	if len(res.Items()) == 0 {
   252  		return 0, nil
   253  	}
   254  
   255  	item := res.Items()[0]
   256  	return int64(item.GetField("id").Value.(int)), nil
   257  }
   258  
   259  // FindEventByAlias search event by alias
   260  func (d *Dictionary) FindEventByAlias(eventAlias string) (int64, error) {
   261  	query := new(clients.Query).
   262  		Select([]interface{}{"id"}).
   263  		From(&cdto.BaseModel{TableName: "events"}).
   264  		Where(cquery.Where{
   265  			First:    "alias",
   266  			Operator: "=",
   267  			Second: cquery.Bind{
   268  				Field: "alias",
   269  				Value: eventAlias,
   270  			},
   271  		})
   272  	res, err := d.db.Execute(query)
   273  	if err == sql.ErrNoRows {
   274  		return 0, nil
   275  	} else if err != nil {
   276  		return 0, err
   277  	}
   278  
   279  	if len(res.Items()) == 0 {
   280  		return 0, nil
   281  	}
   282  
   283  	item := res.Items()[0]
   284  	return int64(item.GetField("id").Value.(int)), nil
   285  }
   286  
   287  // FindEventBy search event by alias and version
   288  func (d *Dictionary) FindEventBy(eventAlias string, version string) (int64, error) {
   289  	query := new(clients.Query).
   290  		Select([]interface{}{"id"}).
   291  		From(&cdto.BaseModel{TableName: "events"}).
   292  		Where(cquery.Where{
   293  			First:    "alias",
   294  			Operator: "=",
   295  			Second: cquery.Bind{
   296  				Field: "alias",
   297  				Value: eventAlias,
   298  			},
   299  		}).
   300  		Where(cquery.Where{
   301  			First:    "installed_version",
   302  			Operator: "=",
   303  			Second: cquery.Bind{
   304  				Field: "installed_version",
   305  				Value: version,
   306  			},
   307  			Type: cquery.WhereOrType,
   308  		})
   309  	res, err := d.db.Execute(query)
   310  	if err == sql.ErrNoRows {
   311  		return 0, nil
   312  	} else if err != nil {
   313  		return 0, err
   314  	}
   315  
   316  	if len(res.Items()) == 0 {
   317  		return 0, nil
   318  	}
   319  
   320  	item := res.Items()[0]
   321  	return int64(item.GetField("id").Value.(int)), nil
   322  }
   323  
   324  // InsertEvent used for event creation
   325  func (d *Dictionary) InsertEvent(alias string, version string) (int64, error) {
   326  	var model = cdto.BaseModel{
   327  		TableName: "events",
   328  		Fields: []interface{}{
   329  			cdto.ModelField{
   330  				Name:  "alias",
   331  				Value: alias,
   332  			},
   333  			cdto.ModelField{
   334  				Name:  "installed_version",
   335  				Value: version,
   336  			},
   337  		},
   338  	}
   339  
   340  	res, err := d.db.Execute(new(clients.Query).Insert(&model))
   341  	if err != nil {
   342  		return 0, err
   343  	}
   344  
   345  	return res.LastInsertID(), nil
   346  }
   347  
   348  // InsertQuestion inserts the question into the database
   349  func (d *Dictionary) InsertQuestion(question string, answer string, scenarioID int64, questionRegex string, questionRegexGroup string, isVariable bool) (int64, error) {
   350  	var (
   351  		regexID int64
   352  		err     error
   353  	)
   354  	if questionRegex != "" {
   355  		//We need to find the existing regexID
   356  		regexID, err = d.FindRegex(questionRegex)
   357  		if err != nil {
   358  			return 0, err
   359  		}
   360  
   361  		//If we don't have this regex in our database, then we need to add it
   362  		if regexID == 0 {
   363  			regexID, err = d.InsertQuestionRegex(questionRegex, questionRegexGroup)
   364  			if err != nil {
   365  				return 0, err
   366  			}
   367  		}
   368  	}
   369  
   370  	var model = cdto.BaseModel{
   371  		TableName: "questions",
   372  		Fields: []interface{}{
   373  			cdto.ModelField{
   374  				Name:  "question",
   375  				Value: question,
   376  			},
   377  			cdto.ModelField{
   378  				Name:  "answer",
   379  				Value: answer,
   380  			},
   381  			cdto.ModelField{
   382  				Name:  "scenario_id",
   383  				Value: scenarioID,
   384  			},
   385  			cdto.ModelField{
   386  				Name:  "is_variable",
   387  				Value: isVariable,
   388  			},
   389  		},
   390  	}
   391  
   392  	if regexID != 0 {
   393  		model.AddModelField(cdto.ModelField{
   394  			Name:  "regex_id",
   395  			Value: regexID,
   396  		})
   397  	}
   398  
   399  	res, err := d.db.Execute(new(clients.Query).Insert(&model))
   400  	if err != nil {
   401  		return 0, err
   402  	}
   403  
   404  	return res.LastInsertID(), nil
   405  }
   406  
   407  // FindRegex search regex by regex string
   408  func (d *Dictionary) FindRegex(regex string) (int64, error) {
   409  	query := new(clients.Query).
   410  		Select([]interface{}{"id"}).
   411  		From(&cdto.BaseModel{TableName: "questions_regex"}).
   412  		Where(cquery.Where{
   413  			First:    "regex",
   414  			Operator: "=",
   415  			Second: cquery.Bind{
   416  				Field: "regex",
   417  				Value: regex,
   418  			},
   419  		})
   420  	res, err := d.db.Execute(query)
   421  	if err == sql.ErrNoRows {
   422  		return 0, nil
   423  	} else if err != nil {
   424  		return 0, err
   425  	}
   426  
   427  	if len(res.Items()) == 0 {
   428  		return 0, nil
   429  	}
   430  
   431  	item := res.Items()[0]
   432  	return int64(item.GetField("id").Value.(int)), nil
   433  }
   434  
   435  // InsertQuestionRegex method insert the regex and returns the regexId. This regex can be connected to the multiple questions
   436  func (d *Dictionary) InsertQuestionRegex(questionRegex string, questionRegexGroup string) (int64, error) {
   437  	var model = cdto.BaseModel{
   438  		TableName: "questions_regex",
   439  		Fields: []interface{}{
   440  			cdto.ModelField{
   441  				Name:  "regex",
   442  				Value: questionRegex,
   443  			},
   444  			cdto.ModelField{
   445  				Name:  "regex_group",
   446  				Value: questionRegexGroup,
   447  			},
   448  		},
   449  	}
   450  
   451  	res, err := d.db.Execute(new(clients.Query).Insert(&model))
   452  	if err != nil {
   453  		return 0, err
   454  	}
   455  
   456  	return res.LastInsertID(), nil
   457  }
   458  
   459  // GetAllRegex method retrieves all available regexs
   460  func (d *Dictionary) GetAllRegex() (res map[int64]string, err error) {
   461  	rows, err := d.db.Execute(new(clients.Query).Select(databasedto.QuestionsRegexModel.GetColumns()).From(&cdto.BaseModel{TableName: "questions_regex"}))
   462  	if err == sql.ErrNoRows {
   463  		return res, nil
   464  	} else if err != nil {
   465  		return res, err
   466  	}
   467  
   468  	res = map[int64]string{}
   469  	if len(rows.Items()) == 0 {
   470  		return nil, nil
   471  	}
   472  
   473  	for _, item := range rows.Items() {
   474  		res[int64(item.GetField("id").Value.(int))] = item.GetField("regex").Value.(string)
   475  	}
   476  
   477  	return res, nil
   478  }
   479  
   480  // RunMigrations method for migrations load from specified path
   481  func (d *Dictionary) RunMigrations(pathToFiles string) error {
   482  	if _, err := os.Stat(pathToFiles); os.IsNotExist(err) {
   483  		return nil
   484  	}
   485  
   486  	var files = map[string]string{}
   487  	err := filepath.Walk(pathToFiles, func(path string, info os.FileInfo, err error) error {
   488  		if !info.IsDir() {
   489  			files[info.Name()] = path
   490  		}
   491  
   492  		return nil
   493  	})
   494  	if err != nil {
   495  		return err
   496  	}
   497  
   498  	var db = d.GetDBClient().GetClient()
   499  	for file, filePath := range files {
   500  		migrationData, err := os.ReadFile(filePath)
   501  		if err != nil {
   502  			return err
   503  		}
   504  
   505  		isMigrationAlreadyExecuted, err := d.IsMigrationAlreadyExecuted(file)
   506  		if err != nil {
   507  			return err
   508  		}
   509  
   510  		if isMigrationAlreadyExecuted {
   511  			continue
   512  		}
   513  
   514  		_, err = db.Exec(string(migrationData))
   515  		if err != nil {
   516  			return err
   517  		}
   518  
   519  		if err := d.MarkMigrationExecuted(file); err != nil {
   520  			return err
   521  		}
   522  	}
   523  
   524  	return nil
   525  }
   526  
   527  // IsMigrationAlreadyExecuted checks if the migration name was already executed
   528  func (d *Dictionary) IsMigrationAlreadyExecuted(version string) (executed bool, err error) {
   529  	query := new(clients.Query).
   530  		Select([]interface{}{"id"}).
   531  		From(&cdto.BaseModel{TableName: "migration"}).
   532  		Where(cquery.Where{
   533  			First:    "version",
   534  			Operator: "=",
   535  			Second: cquery.Bind{
   536  				Field: "version",
   537  				Value: version,
   538  			},
   539  		})
   540  	rows, err := d.db.Execute(query)
   541  	if err == sql.ErrNoRows {
   542  		return false, nil
   543  	} else if err != nil {
   544  		return false, err
   545  	}
   546  
   547  	if len(rows.Items()) == 0 {
   548  		return false, nil
   549  	}
   550  
   551  	return true, err
   552  }
   553  
   554  // MarkMigrationExecuted marks the selected migration version as executed
   555  func (d *Dictionary) MarkMigrationExecuted(version string) (err error) {
   556  	var model = cdto.BaseModel{
   557  		TableName: "migration",
   558  		Fields: []interface{}{
   559  			cdto.ModelField{
   560  				Name:  "version",
   561  				Value: version,
   562  			},
   563  		},
   564  	}
   565  
   566  	_, err = d.db.Execute(new(clients.Query).Insert(&model))
   567  	return
   568  }
   569  
   570  // InstallEvent method installs the event(if it wasn't installed before) and creates the scenario for selected event with selected question and answer
   571  // Deprecated: please use InstallNewEventScenario instead
   572  func (d *Dictionary) InstallEvent(eventName string, eventVersion string, question string, answer string, questionRegex string, questionRegexGroup string) error {
   573  	eventID, err := d.FindEventByAlias(eventName)
   574  	if err != nil {
   575  		return err
   576  	}
   577  
   578  	if eventID != 0 {
   579  		return nil
   580  	}
   581  
   582  	eventID, err = d.InsertEvent(eventName, eventVersion)
   583  	if err != nil {
   584  		return err
   585  	}
   586  
   587  	scenarioID, err := d.InsertScenario(eventName, eventID)
   588  	if err != nil {
   589  		return err
   590  	}
   591  
   592  	_, err = d.InsertQuestion(question, answer, scenarioID, questionRegex, questionRegexGroup, false)
   593  	if err != nil {
   594  		return err
   595  	}
   596  
   597  	return nil
   598  }
   599  
   600  // GetQuestionsByScenarioID method retrieves all available questions and answers for selected scenarioID
   601  func (d *Dictionary) GetQuestionsByScenarioID(scenarioID int64, isVariable bool) (result []QuestionObject, err error) {
   602  	return getQuestionsByScenarioID(d, scenarioID, isVariable)
   603  }
   604  
   605  // InstallNewEventScenario the method for installing of the new event scenario
   606  func (d *Dictionary) InstallNewEventScenario(scenario EventScenario) error {
   607  	return installNewEventScenario(d, scenario)
   608  }