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 }