Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
15
server/channels/store/constants.go
Обычный файл
15
server/channels/store/constants.go
Обычный файл
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package store
|
||||
|
||||
const (
|
||||
ChannelExistsError = "store.sql_channel.save_channel.exists.app_error"
|
||||
|
||||
UserSearchOptionNamesOnly = "names_only"
|
||||
UserSearchOptionNamesOnlyNoFullName = "names_only_no_full_name"
|
||||
UserSearchOptionAllNoFullName = "all_no_full_name"
|
||||
UserSearchOptionAllowInactive = "allow_inactive"
|
||||
|
||||
FeatureTogglePrefix = "feature_enabled_"
|
||||
)
|
||||
185
server/channels/store/errors.go
Обычный файл
185
server/channels/store/errors.go
Обычный файл
@@ -0,0 +1,185 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrInvalidInput indicates an error that has occurred due to an invalid input.
|
||||
type ErrInvalidInput struct {
|
||||
Entity string // The entity which was sent as the input.
|
||||
Field string // The field of the entity which was invalid.
|
||||
Value any // The actual value of the field.
|
||||
wrapped error // The original error
|
||||
}
|
||||
|
||||
func NewErrInvalidInput(entity, field string, value any) *ErrInvalidInput {
|
||||
return &ErrInvalidInput{
|
||||
Entity: entity,
|
||||
Field: field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrInvalidInput) Error() string {
|
||||
if e.wrapped != nil {
|
||||
return fmt.Sprintf("invalid input: entity: %s field: %s value: %s error: %s", e.Entity, e.Field, e.Value, e.wrapped)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("invalid input: entity: %s field: %s value: %s", e.Entity, e.Field, e.Value)
|
||||
}
|
||||
|
||||
func (e *ErrInvalidInput) Wrap(err error) *ErrInvalidInput {
|
||||
e.wrapped = err
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ErrInvalidInput) Unwrap() error {
|
||||
return e.wrapped
|
||||
}
|
||||
|
||||
func (e *ErrInvalidInput) InvalidInputInfo() (entity string, field string, value any) {
|
||||
entity = e.Entity
|
||||
field = e.Field
|
||||
value = e.Value
|
||||
return
|
||||
}
|
||||
|
||||
// ErrLimitExceeded indicates an error that has occurred because some value exceeded a limit.
|
||||
type ErrLimitExceeded struct {
|
||||
What string // What was the object that exceeded.
|
||||
Count int // The value of the object.
|
||||
meta string // Any additional metadata.
|
||||
}
|
||||
|
||||
func NewErrLimitExceeded(what string, count int, meta string) *ErrLimitExceeded {
|
||||
return &ErrLimitExceeded{
|
||||
What: what,
|
||||
Count: count,
|
||||
meta: meta,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrLimitExceeded) Error() string {
|
||||
return fmt.Sprintf("limit exceeded: what: %s count: %d metadata: %s", e.What, e.Count, e.meta)
|
||||
}
|
||||
|
||||
// ErrConflict indicates a conflict that occurred.
|
||||
type ErrConflict struct {
|
||||
Resource string // The resource which created the conflict.
|
||||
err error // Internal error.
|
||||
meta string // Any additional metadata.
|
||||
}
|
||||
|
||||
func NewErrConflict(resource string, err error, meta string) *ErrConflict {
|
||||
return &ErrConflict{
|
||||
Resource: resource,
|
||||
err: err,
|
||||
meta: meta,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrConflict) Error() string {
|
||||
msg := e.Resource + "exists " + e.meta
|
||||
if e.err != nil {
|
||||
msg += " " + e.err.Error()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (e *ErrConflict) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// IsErrConflict allows easy type assertion without adding store as a dependency.
|
||||
func (e *ErrConflict) IsErrConflict() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ErrNotFound indicates that a resource was not found
|
||||
type ErrNotFound struct {
|
||||
resource string
|
||||
ID string
|
||||
wrapped error
|
||||
}
|
||||
|
||||
func NewErrNotFound(resource, id string) *ErrNotFound {
|
||||
return &ErrNotFound{
|
||||
resource: resource,
|
||||
ID: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrNotFound) Wrap(err error) *ErrNotFound {
|
||||
e.wrapped = err
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ErrNotFound) Error() string {
|
||||
if e.wrapped != nil {
|
||||
return fmt.Sprintf("resource: %s id: %s error: %s", e.resource, e.ID, e.wrapped)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("resource: %s id: %s", e.resource, e.ID)
|
||||
}
|
||||
|
||||
// IsErrNotFound allows easy type assertion without adding store as a dependency.
|
||||
func (e *ErrNotFound) IsErrNotFound() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ErrOutOfBounds indicates that the requested total numbers of rows
|
||||
// was greater than the allowed limit.
|
||||
type ErrOutOfBounds struct {
|
||||
value int
|
||||
}
|
||||
|
||||
func (e *ErrOutOfBounds) Error() string {
|
||||
return fmt.Sprintf("invalid limit parameter: %d", e.value)
|
||||
}
|
||||
|
||||
func NewErrOutOfBounds(value int) *ErrOutOfBounds {
|
||||
return &ErrOutOfBounds{value: value}
|
||||
}
|
||||
|
||||
// ErrNotImplemented indicates that some feature or requirement is not implemented yet.
|
||||
type ErrNotImplemented struct {
|
||||
detail string
|
||||
}
|
||||
|
||||
func (e *ErrNotImplemented) Error() string {
|
||||
return e.detail
|
||||
}
|
||||
|
||||
func NewErrNotImplemented(detail string) *ErrNotImplemented {
|
||||
return &ErrNotImplemented{detail: detail}
|
||||
}
|
||||
|
||||
type ErrUniqueConstraint struct {
|
||||
Columns []string
|
||||
}
|
||||
|
||||
// NewErrUniqueConstraint creates a uniqueness constraint error for the given column(s).
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// store.NewErrUniqueConstraint("DisplayName") // single column constraint
|
||||
// store.NewErrUniqueConstraint("Name", "Source") // multi-column constraint
|
||||
func NewErrUniqueConstraint(columns ...string) *ErrUniqueConstraint {
|
||||
return &ErrUniqueConstraint{
|
||||
Columns: columns,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrUniqueConstraint) Error() string {
|
||||
var tmpl string
|
||||
if len(e.Columns) > 1 {
|
||||
tmpl = "unique constraint: (%s)"
|
||||
} else {
|
||||
tmpl = "unique constraint: %s"
|
||||
}
|
||||
return fmt.Sprintf(tmpl, strings.Join(e.Columns, ","))
|
||||
}
|
||||
326
server/channels/store/layer_generators/main.go
Обычный файл
326
server/channels/store/layer_generators/main.go
Обычный файл
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenTracingParamsMarker = "@openTracingParams"
|
||||
ErrorType = "error"
|
||||
)
|
||||
|
||||
func isError(typeName string) bool {
|
||||
return strings.Contains(typeName, ErrorType)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := buildTimerLayer(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := buildOpenTracingLayer(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := buildRetryLayer(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func buildRetryLayer() error {
|
||||
code, err := generateLayer("RetryLayer", "retry_layer.go.tmpl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatedCode, err := format.Source(code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path.Join("retrylayer/retrylayer.go"), formatedCode, 0644)
|
||||
}
|
||||
|
||||
func buildTimerLayer() error {
|
||||
code, err := generateLayer("TimerLayer", "timer_layer.go.tmpl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatedCode, err := format.Source(code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path.Join("timerlayer", "timerlayer.go"), formatedCode, 0644)
|
||||
}
|
||||
|
||||
func buildOpenTracingLayer() error {
|
||||
code, err := generateLayer("OpenTracingLayer", "opentracing_layer.go.tmpl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatedCode, err := format.Source(code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path.Join("opentracinglayer", "opentracinglayer.go"), formatedCode, 0644)
|
||||
}
|
||||
|
||||
type methodParam struct {
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
type methodData struct {
|
||||
Params []methodParam
|
||||
Results []string
|
||||
ParamsToTrace map[string]bool
|
||||
}
|
||||
|
||||
type subStore struct {
|
||||
Methods map[string]methodData
|
||||
}
|
||||
|
||||
type storeMetadata struct {
|
||||
Name string
|
||||
SubStores map[string]subStore
|
||||
Methods map[string]methodData
|
||||
}
|
||||
|
||||
func extractMethodMetadata(method *ast.Field, src []byte) methodData {
|
||||
params := []methodParam{}
|
||||
results := []string{}
|
||||
paramsToTrace := map[string]bool{}
|
||||
ast.Inspect(method.Type, func(expr ast.Node) bool {
|
||||
switch e := expr.(type) {
|
||||
case *ast.FuncType:
|
||||
if method.Doc != nil {
|
||||
for _, comment := range method.Doc.List {
|
||||
s := comment.Text
|
||||
if idx := strings.Index(s, OpenTracingParamsMarker); idx != -1 {
|
||||
for _, p := range strings.Split(s[idx+len(OpenTracingParamsMarker):], ",") {
|
||||
paramsToTrace[strings.TrimSpace(p)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.Params != nil {
|
||||
for _, param := range e.Params.List {
|
||||
for _, paramName := range param.Names {
|
||||
params = append(params, methodParam{Name: paramName.Name, Type: string(src[param.Type.Pos()-1 : param.Type.End()-1])})
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.Results != nil {
|
||||
for _, result := range e.Results.List {
|
||||
results = append(results, string(src[result.Type.Pos()-1:result.Type.End()-1]))
|
||||
}
|
||||
}
|
||||
|
||||
for paramName := range paramsToTrace {
|
||||
found := false
|
||||
for _, param := range params {
|
||||
if param.Name == paramName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Fatalf("Unable to find a parameter called '%s' (method '%s') that is mentioned in the '%s' comment. Maybe it was renamed?", paramName, method.Names[0].Name, OpenTracingParamsMarker)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return methodData{Params: params, Results: results, ParamsToTrace: paramsToTrace}
|
||||
}
|
||||
|
||||
func extractStoreMetadata() (*storeMetadata, error) {
|
||||
// Create the AST by parsing src.
|
||||
fset := token.NewFileSet() // positions are relative to fset
|
||||
|
||||
file, err := os.Open("store.go")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open store/store.go file: %w", err)
|
||||
}
|
||||
src, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file.Close()
|
||||
f, err := parser.ParseFile(fset, "", src, parser.AllErrors|parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
topLevelFunctions := map[string]bool{
|
||||
"MarkSystemRanUnitTests": false,
|
||||
"Close": false,
|
||||
"LockToMaster": false,
|
||||
"UnlockFromMaster": false,
|
||||
"DropAllTables": false,
|
||||
"TotalMasterDbConnections": true,
|
||||
"TotalReadDbConnections": true,
|
||||
"SetContext": true,
|
||||
"TotalSearchDbConnections": true,
|
||||
"GetCurrentSchemaVersion": true,
|
||||
}
|
||||
|
||||
metadata := storeMetadata{Methods: map[string]methodData{}, SubStores: map[string]subStore{}}
|
||||
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
switch x := n.(type) {
|
||||
case *ast.TypeSpec:
|
||||
if x.Name.Name == "Store" {
|
||||
for _, method := range x.Type.(*ast.InterfaceType).Methods.List {
|
||||
methodName := method.Names[0].Name
|
||||
if _, ok := topLevelFunctions[methodName]; ok {
|
||||
metadata.Methods[methodName] = extractMethodMetadata(method, src)
|
||||
}
|
||||
}
|
||||
} else if strings.HasSuffix(x.Name.Name, "Store") {
|
||||
subStoreName := strings.TrimSuffix(x.Name.Name, "Store")
|
||||
metadata.SubStores[subStoreName] = subStore{Methods: map[string]methodData{}}
|
||||
for _, method := range x.Type.(*ast.InterfaceType).Methods.List {
|
||||
methodName := method.Names[0].Name
|
||||
metadata.SubStores[subStoreName].Methods[methodName] = extractMethodMetadata(method, src)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
func generateLayer(name, templateFile string) ([]byte, error) {
|
||||
out := bytes.NewBufferString("")
|
||||
metadata, err := extractStoreMetadata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata.Name = name
|
||||
|
||||
myFuncs := template.FuncMap{
|
||||
"joinResults": func(results []string) string {
|
||||
return strings.Join(results, ", ")
|
||||
},
|
||||
"joinResultsForSignature": func(results []string) string {
|
||||
if len(results) == 0 {
|
||||
return ""
|
||||
}
|
||||
returns := []string{}
|
||||
for _, result := range results {
|
||||
switch result {
|
||||
case "*PostReminderMetadata":
|
||||
returns = append(returns, fmt.Sprintf("*store.%s", strings.TrimPrefix(result, "*")))
|
||||
default:
|
||||
returns = append(returns, result)
|
||||
}
|
||||
}
|
||||
|
||||
if len(returns) == 1 {
|
||||
return strings.Join(returns, ", ")
|
||||
}
|
||||
return fmt.Sprintf("(%s)", strings.Join(returns, ", "))
|
||||
},
|
||||
"genResultsVars": func(results []string, withNilError bool) string {
|
||||
vars := []string{}
|
||||
for i, typeName := range results {
|
||||
if isError(typeName) {
|
||||
if withNilError {
|
||||
vars = append(vars, "nil")
|
||||
} else {
|
||||
vars = append(vars, "err")
|
||||
}
|
||||
} else if i == 0 {
|
||||
vars = append(vars, "result")
|
||||
} else {
|
||||
vars = append(vars, fmt.Sprintf("resultVar%d", i))
|
||||
}
|
||||
}
|
||||
return strings.Join(vars, ", ")
|
||||
},
|
||||
"errorToBoolean": func(results []string) string {
|
||||
for _, typeName := range results {
|
||||
if isError(typeName) {
|
||||
return "err == nil"
|
||||
}
|
||||
}
|
||||
return "true"
|
||||
},
|
||||
"errorPresent": func(results []string) bool {
|
||||
for _, typeName := range results {
|
||||
if isError(typeName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
"errorVar": func(results []string) string {
|
||||
for _, typeName := range results {
|
||||
if isError(typeName) {
|
||||
return "err"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"joinParams": func(params []methodParam) string {
|
||||
paramsNames := make([]string, 0, len(params))
|
||||
for _, param := range params {
|
||||
tParams := ""
|
||||
if strings.HasPrefix(param.Type, "...") {
|
||||
tParams = "..."
|
||||
}
|
||||
paramsNames = append(paramsNames, param.Name+tParams)
|
||||
}
|
||||
return strings.Join(paramsNames, ", ")
|
||||
},
|
||||
"joinParamsWithType": func(params []methodParam) string {
|
||||
paramsWithType := []string{}
|
||||
for _, param := range params {
|
||||
switch param.Type {
|
||||
case "ChannelSearchOpts", "UserGetByIdsOpts", "ThreadMembershipOpts":
|
||||
paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type))
|
||||
case "*UserGetByIdsOpts", "*ChannelMemberGraphQLSearchOpts", "*SidebarCategorySearchOpts":
|
||||
paramsWithType = append(paramsWithType, fmt.Sprintf("%s *store.%s", param.Name, strings.TrimPrefix(param.Type, "*")))
|
||||
default:
|
||||
paramsWithType = append(paramsWithType, fmt.Sprintf("%s %s", param.Name, param.Type))
|
||||
}
|
||||
}
|
||||
return strings.Join(paramsWithType, ", ")
|
||||
},
|
||||
"joinParamsWithTypeOutsideStore": func(params []methodParam) string {
|
||||
paramsWithType := []string{}
|
||||
for _, param := range params {
|
||||
switch param.Type {
|
||||
case "ChannelSearchOpts", "UserGetByIdsOpts", "ThreadMembershipOpts":
|
||||
paramsWithType = append(paramsWithType, fmt.Sprintf("%s store.%s", param.Name, param.Type))
|
||||
case "*UserGetByIdsOpts", "*ChannelMemberGraphQLSearchOpts", "*SidebarCategorySearchOpts":
|
||||
paramsWithType = append(paramsWithType, fmt.Sprintf("%s *store.%s", param.Name, strings.TrimPrefix(param.Type, "*")))
|
||||
default:
|
||||
paramsWithType = append(paramsWithType, fmt.Sprintf("%s %s", param.Name, param.Type))
|
||||
}
|
||||
}
|
||||
return strings.Join(paramsWithType, ", ")
|
||||
},
|
||||
}
|
||||
|
||||
t := template.Must(template.New(templateFile).Funcs(myFuncs).ParseFiles("layer_generators/" + templateFile))
|
||||
if err = t.Execute(out, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
84
server/channels/store/layer_generators/opentracing_layer.go.tmpl
Обычный файл
84
server/channels/store/layer_generators/opentracing_layer.go.tmpl
Обычный файл
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make store-layers"
|
||||
// DO NOT EDIT
|
||||
|
||||
package opentracinglayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/tracing"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/opentracing/opentracing-go/ext"
|
||||
spanlog "github.com/opentracing/opentracing-go/log"
|
||||
)
|
||||
|
||||
type {{.Name}} struct {
|
||||
store.Store
|
||||
{{range $index, $element := .SubStores}} {{$index}}Store store.{{$index}}Store
|
||||
{{end}}
|
||||
}
|
||||
|
||||
{{range $index, $element := .SubStores}}func (s *{{$.Name}}) {{$index}}() store.{{$index}}Store {
|
||||
return s.{{$index}}Store
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
{{range $index, $element := .SubStores}}type {{$.Name}}{{$index}}Store struct {
|
||||
store.{{$index}}Store
|
||||
Root *{{$.Name}}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
{{range $substoreName, $substore := .SubStores}}
|
||||
{{range $index, $element := $substore.Methods}}
|
||||
func (s *{{$.Name}}{{$substoreName}}Store) {{$index}}({{$element.Params | joinParamsWithType}}) {{$element.Results | joinResultsForSignature}} {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "{{$substoreName}}Store.{{$index}}")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func(){
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
{{range $paramName, $param := $element.Params}}
|
||||
{{if index $element.ParamsToTrace $param.Name }}
|
||||
span.SetTag("{{$param.Name}}", {{$param.Name}})
|
||||
{{end}}
|
||||
{{end}}
|
||||
defer span.Finish()
|
||||
{{- if $element.Results | len | eq 0}}
|
||||
s.{{$substoreName}}Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{else}}
|
||||
{{genResultsVars $element.Results false }} := s.{{$substoreName}}Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{- if $element.Results | errorPresent }}
|
||||
if {{$element.Results | errorVar}} != nil {
|
||||
span.LogFields(spanlog.Error({{$element.Results | errorVar}}))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
{{end}}
|
||||
return {{ genResultsVars $element.Results false -}}
|
||||
{{end}}
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{range $index, $element := .Methods}}
|
||||
func (s *{{$.Name}}) {{$index}}({{$element.Params | joinParamsWithType}}) {{$element.Results | joinResultsForSignature}} {
|
||||
{{if $element.Results | len | eq 0}}s.Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{else}}return s.Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{end}}}
|
||||
{{end}}
|
||||
|
||||
func New(childStore store.Store, ctx context.Context) *{{.Name}} {
|
||||
newStore := {{.Name}}{
|
||||
Store: childStore,
|
||||
}
|
||||
{{range $substoreName, $substore := .SubStores}}
|
||||
newStore.{{$substoreName}}Store = &{{$.Name}}{{$substoreName}}Store{{"{"}}{{$substoreName}}Store: childStore.{{$substoreName}}(), Root: &newStore}{{end}}
|
||||
return &newStore
|
||||
}
|
||||
103
server/channels/store/layer_generators/retry_layer.go.tmpl
Обычный файл
103
server/channels/store/layer_generators/retry_layer.go.tmpl
Обычный файл
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make store-layers"
|
||||
// DO NOT EDIT
|
||||
|
||||
package retrylayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
timepkg "time"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const mySQLDeadlockCode = uint16(1213)
|
||||
|
||||
type {{.Name}} struct {
|
||||
store.Store
|
||||
{{range $index, $element := .SubStores}} {{$index}}Store store.{{$index}}Store
|
||||
{{end}}
|
||||
}
|
||||
|
||||
{{range $index, $element := .SubStores}}func (s *{{$.Name}}) {{$index}}() store.{{$index}}Store {
|
||||
return s.{{$index}}Store
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
{{range $index, $element := .SubStores}}type {{$.Name}}{{$index}}Store struct {
|
||||
store.{{$index}}Store
|
||||
Root *{{$.Name}}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
func isRepeatableError(err error) bool {
|
||||
var pqErr *pq.Error
|
||||
var mysqlErr *mysql.MySQLError
|
||||
switch {
|
||||
case errors.As(errors.Cause(err), &pqErr):
|
||||
if pqErr.Code == "40001" || pqErr.Code == "40P01" {
|
||||
return true
|
||||
}
|
||||
case errors.As(errors.Cause(err), &mysqlErr):
|
||||
if mysqlErr.Number == mySQLDeadlockCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
{{range $substoreName, $substore := .SubStores}}
|
||||
{{range $index, $element := $substore.Methods}}
|
||||
func (s *{{$.Name}}{{$substoreName}}Store) {{$index}}({{$element.Params | joinParamsWithTypeOutsideStore}}) {{$element.Results | joinResultsForSignature}} {
|
||||
{{if $element.Results | len | eq 0}}
|
||||
s.{{$substoreName}}Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{else}}
|
||||
{{if $element.Results | errorPresent}}
|
||||
tries := 0
|
||||
for {
|
||||
{{genResultsVars $element.Results false }} := s.{{$substoreName}}Store.{{$index}}({{$element.Params | joinParams}})
|
||||
if {{$element.Results | errorVar}} == nil {
|
||||
return {{genResultsVars $element.Results true }}
|
||||
}
|
||||
if !isRepeatableError({{$element.Results | errorVar}}) {
|
||||
return {{genResultsVars $element.Results false }}
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
{{$element.Results | errorVar}} = errors.Wrap({{$element.Results | errorVar}}, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return {{genResultsVars $element.Results false }}
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
{{else}}
|
||||
return s.{{$substoreName}}Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{end}}
|
||||
{{end}}
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{range $index, $element := .Methods}}
|
||||
func (s *{{$.Name}}) {{$index}}({{$element.Params | joinParamsWithTypeOutsideStore}}) {{$element.Results | joinResultsForSignature}} {
|
||||
{{if $element.Results | len | eq 0}}s.Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{else}}return s.Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{end}}}
|
||||
{{end}}
|
||||
|
||||
func New(childStore store.Store) *{{.Name}} {
|
||||
newStore := {{.Name}}{
|
||||
Store: childStore,
|
||||
}
|
||||
{{range $substoreName, $substore := .SubStores}}
|
||||
newStore.{{$substoreName}}Store = &{{$.Name}}{{$substoreName}}Store{{"{"}}{{$substoreName}}Store: childStore.{{$substoreName}}(), Root: &newStore}{{end}}
|
||||
return &newStore
|
||||
}
|
||||
79
server/channels/store/layer_generators/timer_layer.go.tmpl
Обычный файл
79
server/channels/store/layer_generators/timer_layer.go.tmpl
Обычный файл
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make store-layers"
|
||||
// DO NOT EDIT
|
||||
|
||||
package timerlayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type {{.Name}} struct {
|
||||
store.Store
|
||||
Metrics einterfaces.MetricsInterface
|
||||
{{range $index, $element := .SubStores}} {{$index}}Store store.{{$index}}Store
|
||||
{{end}}
|
||||
}
|
||||
|
||||
{{range $index, $element := .SubStores}}func (s *{{$.Name}}) {{$index}}() store.{{$index}}Store {
|
||||
return s.{{$index}}Store
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
{{range $index, $element := .SubStores}}type {{$.Name}}{{$index}}Store struct {
|
||||
store.{{$index}}Store
|
||||
Root *{{$.Name}}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
{{range $substoreName, $substore := .SubStores}}
|
||||
{{range $index, $element := $substore.Methods}}
|
||||
func (s *{{$.Name}}{{$substoreName}}Store) {{$index}}({{$element.Params | joinParamsWithType}}) {{$element.Results | joinResultsForSignature}} {
|
||||
start := time.Now()
|
||||
{{if $element.Results | len | eq 0}}
|
||||
s.{{$substoreName}}Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{else}}
|
||||
{{genResultsVars $element.Results false }} := s.{{$substoreName}}Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{end}}
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if {{$element.Results | errorToBoolean}} {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("{{$substoreName}}Store.{{$index}}", success, elapsed)
|
||||
{{ with (genResultsVars $element.Results false ) -}}
|
||||
}
|
||||
return {{ . }}
|
||||
{{- else -}}
|
||||
}
|
||||
{{- end }}
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{range $index, $element := .Methods}}
|
||||
func (s *{{$.Name}}) {{$index}}({{$element.Params | joinParamsWithType}}) {{$element.Results | joinResultsForSignature}} {
|
||||
{{if $element.Results | len | eq 0}}s.Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{else}}return s.Store.{{$index}}({{$element.Params | joinParams}})
|
||||
{{end}}}
|
||||
{{end}}
|
||||
|
||||
func New(childStore store.Store, metrics einterfaces.MetricsInterface) *{{.Name}} {
|
||||
newStore := {{.Name}}{
|
||||
Store: childStore,
|
||||
Metrics: metrics,
|
||||
}
|
||||
{{range $substoreName, $substore := .SubStores}}
|
||||
newStore.{{$substoreName}}Store = &{{$.Name}}{{$substoreName}}Store{{"{"}}{{$substoreName}}Store: childStore.{{$substoreName}}(), Root: &newStore}{{end}}
|
||||
return &newStore
|
||||
}
|
||||
264
server/channels/store/localcachelayer/channel_layer.go
Обычный файл
264
server/channels/store/localcachelayer/channel_layer.go
Обычный файл
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type LocalCacheChannelStore struct {
|
||||
store.ChannelStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelMemberCounts(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.channelMemberCountsCache.Purge()
|
||||
} else {
|
||||
s.rootStore.channelMemberCountsCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelPinnedPostCount(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.channelPinnedPostCountsCache.Purge()
|
||||
} else {
|
||||
s.rootStore.channelPinnedPostCountsCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelGuestCounts(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.channelGuestCountCache.Purge()
|
||||
} else {
|
||||
s.rootStore.channelGuestCountCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheChannelStore) handleClusterInvalidateChannelById(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.channelByIdCache.Purge()
|
||||
} else {
|
||||
s.rootStore.channelByIdCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) ClearCaches() {
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.channelMemberCountsCache)
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.channelPinnedPostCountsCache)
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.channelGuestCountCache)
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.channelByIdCache)
|
||||
s.ChannelStore.ClearCaches()
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Purge")
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Purge")
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Guest Count - Purge")
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel - Purge")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) InvalidatePinnedPostCount(channelId string) {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelPinnedPostCountsCache, channelId)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Remove by ChannelId")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) InvalidateMemberCount(channelId string) {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelMemberCountsCache, channelId)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Remove by ChannelId")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) InvalidateGuestCount(channelId string) {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelGuestCountCache, channelId)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel Guests Count - Remove by channelId")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) InvalidateChannel(channelId string) {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.channelByIdCache, channelId)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Channel - Remove by ChannelId")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) GetMemberCount(channelId string, allowFromCache bool) (int64, error) {
|
||||
if allowFromCache {
|
||||
var count int64
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelId, &count); err == nil {
|
||||
return count, nil
|
||||
}
|
||||
}
|
||||
count, err := s.ChannelStore.GetMemberCount(channelId, allowFromCache)
|
||||
|
||||
if allowFromCache && err == nil {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.channelMemberCountsCache, channelId, count)
|
||||
}
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, error) {
|
||||
if allowFromCache {
|
||||
var count int64
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.channelGuestCountCache, channelId, &count); err == nil {
|
||||
return count, nil
|
||||
}
|
||||
}
|
||||
count, err := s.ChannelStore.GetGuestCount(channelId, allowFromCache)
|
||||
|
||||
if allowFromCache && err == nil {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.channelGuestCountCache, channelId, count)
|
||||
}
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) GetMemberCountFromCache(channelId string) int64 {
|
||||
var count int64
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelId, &count); err == nil {
|
||||
return count
|
||||
}
|
||||
|
||||
count, err := s.GetMemberCount(channelId, true)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, error) {
|
||||
if allowFromCache {
|
||||
var count int64
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.channelPinnedPostCountsCache, channelId, &count); err == nil {
|
||||
return count, nil
|
||||
}
|
||||
}
|
||||
|
||||
count, err := s.ChannelStore.GetPinnedPostCount(channelId, allowFromCache)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if allowFromCache {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.channelPinnedPostCountsCache, channelId, count)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
|
||||
|
||||
if allowFromCache {
|
||||
var cacheItem *model.Channel
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.channelByIdCache, id, &cacheItem); err == nil {
|
||||
return cacheItem, nil
|
||||
}
|
||||
}
|
||||
|
||||
ch, err := s.ChannelStore.Get(id, allowFromCache)
|
||||
|
||||
if allowFromCache && err == nil {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, id, ch)
|
||||
}
|
||||
|
||||
return ch, err
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) GetMany(ids []string, allowFromCache bool) (model.ChannelList, error) {
|
||||
var foundChannels []*model.Channel
|
||||
var channelsToQuery []string
|
||||
|
||||
if allowFromCache {
|
||||
for _, id := range ids {
|
||||
var ch *model.Channel
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.channelByIdCache, id, &ch); err == nil {
|
||||
foundChannels = append(foundChannels, ch)
|
||||
} else {
|
||||
channelsToQuery = append(channelsToQuery, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if channelsToQuery == nil {
|
||||
return foundChannels, nil
|
||||
}
|
||||
|
||||
channels, err := s.ChannelStore.GetMany(channelsToQuery, allowFromCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ch := range channels {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.channelByIdCache, ch.Id, ch)
|
||||
}
|
||||
|
||||
return append(foundChannels, channels...), nil
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, error) {
|
||||
member, err := s.ChannelStore.SaveMember(member)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.InvalidateMemberCount(member.ChannelId)
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
|
||||
members, err := s.ChannelStore.SaveMultipleMembers(members)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, member := range members {
|
||||
s.InvalidateMemberCount(member.ChannelId)
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
|
||||
member, err := s.ChannelStore.UpdateMember(member)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.InvalidateMemberCount(member.ChannelId)
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, error) {
|
||||
members, err := s.ChannelStore.UpdateMultipleMembers(members)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, member := range members {
|
||||
s.InvalidateMemberCount(member.ChannelId)
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) RemoveMember(channelId, userId string) error {
|
||||
err := s.ChannelStore.RemoveMember(channelId, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.InvalidateMemberCount(channelId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s LocalCacheChannelStore) RemoveMembers(channelId string, userIds []string) error {
|
||||
err := s.ChannelStore.RemoveMembers(channelId, userIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.InvalidateMemberCount(channelId)
|
||||
return nil
|
||||
}
|
||||
319
server/channels/store/localcachelayer/channel_layer_test.go
Обычный файл
319
server/channels/store/localcachelayer/channel_layer_test.go
Обычный файл
@@ -0,0 +1,319 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestChannelStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestReactionStore)
|
||||
}
|
||||
|
||||
func TestChannelStoreChannelMemberCountsCache(t *testing.T) {
|
||||
countResult := int64(10)
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := cachedStore.Channel().GetMemberCount("id", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, count, countResult)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
|
||||
count, err = cachedStore.Channel().GetMemberCount("id", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, count, countResult)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetMemberCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
|
||||
cachedStore.Channel().GetMemberCount("id", false)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetMemberCount("id", false)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
|
||||
cachedStore.Channel().GetMemberCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
|
||||
cachedStore.Channel().GetMemberCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call with GetMemberCountFromCache not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
count := cachedStore.Channel().GetMemberCountFromCache("id")
|
||||
assert.Equal(t, count, countResult)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
|
||||
count = cachedStore.Channel().GetMemberCountFromCache("id")
|
||||
assert.Equal(t, count, countResult)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetMemberCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
|
||||
cachedStore.Channel().ClearCaches()
|
||||
cachedStore.Channel().GetMemberCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetMemberCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 1)
|
||||
cachedStore.Channel().InvalidateMemberCount("id")
|
||||
cachedStore.Channel().GetMemberCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetMemberCount", 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) {
|
||||
countResult := int64(10)
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, count, countResult)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
|
||||
count, err = cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, count, countResult)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
|
||||
cachedStore.Channel().GetPinnedPostCount("id", false)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetPinnedPostCount("id", false)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
|
||||
cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
|
||||
cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
|
||||
cachedStore.Channel().ClearCaches()
|
||||
cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 1)
|
||||
cachedStore.Channel().InvalidatePinnedPostCount("id")
|
||||
cachedStore.Channel().GetPinnedPostCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetPinnedPostCount", 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChannelStoreGuestCountCache(t *testing.T) {
|
||||
countResult := int64(12)
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := cachedStore.Channel().GetGuestCount("id", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, count, countResult)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
|
||||
count, err = cachedStore.Channel().GetGuestCount("id", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, count, countResult)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetGuestCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
|
||||
cachedStore.Channel().GetGuestCount("id", false)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call force not cached, second not cached, third cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetGuestCount("id", false)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
|
||||
cachedStore.Channel().GetGuestCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
|
||||
cachedStore.Channel().GetGuestCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetGuestCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
|
||||
cachedStore.Channel().ClearCaches()
|
||||
cachedStore.Channel().GetGuestCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().GetGuestCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 1)
|
||||
cachedStore.Channel().InvalidateGuestCount("id")
|
||||
cachedStore.Channel().GetGuestCount("id", true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetGuestCount", 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChannelStoreChannel(t *testing.T) {
|
||||
channelId := "channel1"
|
||||
fakeChannel := model.Channel{Id: channelId}
|
||||
t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel, err := cachedStore.Channel().Get(channelId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, channel, &fakeChannel)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
channel, err = cachedStore.Channel().Get(channelId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, channel, &fakeChannel)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force no cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().Get(channelId, true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Channel().Get(channelId, false)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call force no cached, second not cached, third cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
cachedStore.Channel().Get(channelId, false)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Channel().Get(channelId, true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
cachedStore.Channel().Get(channelId, true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, clear cache, second call not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Channel().Get(channelId, true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Channel().ClearCaches()
|
||||
cachedStore.Channel().Get(channelId, true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
cachedStore.Channel().Get(channelId, true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Channel().InvalidateChannel(channelId)
|
||||
cachedStore.Channel().Get(channelId, true)
|
||||
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
}
|
||||
142
server/channels/store/localcachelayer/emoji_layer.go
Обычный файл
142
server/channels/store/localcachelayer/emoji_layer.go
Обычный файл
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
|
||||
)
|
||||
|
||||
type LocalCacheEmojiStore struct {
|
||||
store.EmojiStore
|
||||
rootStore *LocalCacheStore
|
||||
emojiByIdMut sync.Mutex
|
||||
emojiByIdInvalidations map[string]bool
|
||||
emojiByNameMut sync.Mutex
|
||||
emojiByNameInvalidations map[string]bool
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiById(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
es.rootStore.emojiCacheById.Purge()
|
||||
} else {
|
||||
es.emojiByIdMut.Lock()
|
||||
es.emojiByIdInvalidations[string(msg.Data)] = true
|
||||
es.emojiByIdMut.Unlock()
|
||||
es.rootStore.emojiCacheById.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) handleClusterInvalidateEmojiIdByName(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
es.rootStore.emojiIdCacheByName.Purge()
|
||||
} else {
|
||||
es.emojiByNameMut.Lock()
|
||||
es.emojiByNameInvalidations[string(msg.Data)] = true
|
||||
es.emojiByNameMut.Unlock()
|
||||
es.rootStore.emojiIdCacheByName.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
|
||||
if allowFromCache {
|
||||
if emoji, ok := es.getFromCacheById(id); ok {
|
||||
return emoji, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If it was invalidated, then we need to query master.
|
||||
es.emojiByIdMut.Lock()
|
||||
if es.emojiByIdInvalidations[id] {
|
||||
// And then remove the key from the map.
|
||||
ctx = sqlstore.WithMaster(ctx)
|
||||
delete(es.emojiByIdInvalidations, id)
|
||||
}
|
||||
es.emojiByIdMut.Unlock()
|
||||
|
||||
emoji, err := es.EmojiStore.Get(ctx, id, allowFromCache)
|
||||
|
||||
if allowFromCache && err == nil {
|
||||
es.addToCache(emoji)
|
||||
}
|
||||
|
||||
return emoji, err
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
|
||||
if id, ok := model.GetSystemEmojiId(name); ok {
|
||||
return es.Get(ctx, id, allowFromCache)
|
||||
}
|
||||
|
||||
if allowFromCache {
|
||||
if emoji, ok := es.getFromCacheByName(name); ok {
|
||||
return emoji, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If it was invalidated, then we need to query master.
|
||||
es.emojiByNameMut.Lock()
|
||||
if es.emojiByNameInvalidations[name] {
|
||||
ctx = sqlstore.WithMaster(ctx)
|
||||
// And then remove the key from the map.
|
||||
delete(es.emojiByNameInvalidations, name)
|
||||
}
|
||||
es.emojiByNameMut.Unlock()
|
||||
|
||||
emoji, err := es.EmojiStore.GetByName(ctx, name, allowFromCache)
|
||||
|
||||
if allowFromCache && err == nil {
|
||||
es.addToCache(emoji)
|
||||
}
|
||||
|
||||
return emoji, err
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) Delete(emoji *model.Emoji, time int64) error {
|
||||
err := es.EmojiStore.Delete(emoji, time)
|
||||
|
||||
if err == nil {
|
||||
es.removeFromCache(emoji)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) addToCache(emoji *model.Emoji) {
|
||||
es.rootStore.doStandardAddToCache(es.rootStore.emojiCacheById, emoji.Id, emoji)
|
||||
es.rootStore.doStandardAddToCache(es.rootStore.emojiIdCacheByName, emoji.Name, emoji.Id)
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) getFromCacheById(id string) (*model.Emoji, bool) {
|
||||
var emoji *model.Emoji
|
||||
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiCacheById, id, &emoji); err == nil {
|
||||
return emoji, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) getFromCacheByName(name string) (*model.Emoji, bool) {
|
||||
var emojiId string
|
||||
if err := es.rootStore.doStandardReadCache(es.rootStore.emojiIdCacheByName, name, &emojiId); err == nil {
|
||||
return es.getFromCacheById(emojiId)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (es *LocalCacheEmojiStore) removeFromCache(emoji *model.Emoji) {
|
||||
es.emojiByIdMut.Lock()
|
||||
es.emojiByIdInvalidations[emoji.Id] = true
|
||||
es.emojiByIdMut.Unlock()
|
||||
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiCacheById, emoji.Id)
|
||||
|
||||
es.emojiByNameMut.Lock()
|
||||
es.emojiByNameInvalidations[emoji.Name] = true
|
||||
es.emojiByNameMut.Unlock()
|
||||
es.rootStore.doInvalidateCacheCluster(es.rootStore.emojiIdCacheByName, emoji.Name)
|
||||
}
|
||||
185
server/channels/store/localcachelayer/emoji_layer_test.go
Обычный файл
185
server/channels/store/localcachelayer/emoji_layer_test.go
Обычный файл
@@ -0,0 +1,185 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestEmojiStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestEmojiStore)
|
||||
}
|
||||
|
||||
func TestEmojiStoreCache(t *testing.T) {
|
||||
fakeEmoji := model.Emoji{Id: "123", Name: "name123"}
|
||||
ctxEmoji := model.Emoji{Id: "master", Name: "name123"}
|
||||
|
||||
t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
emoji, err := cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, emoji, &fakeEmoji)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
emoji, err = cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, emoji, &fakeEmoji)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
})
|
||||
|
||||
t.Run("first call by name not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
emoji, err := cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, emoji, &fakeEmoji)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
emoji, err = cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, emoji, &fakeEmoji)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
})
|
||||
|
||||
t.Run("first call by id not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Emoji().Get(context.Background(), "123", false)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call by name not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", false)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
|
||||
})
|
||||
|
||||
t.Run("first call by id force not cached, second not cached, third cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().Get(context.Background(), "123", false)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call by name force not cached, second not cached, third cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", false)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
|
||||
})
|
||||
|
||||
t.Run("first call by id, second call by name cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 0)
|
||||
})
|
||||
|
||||
t.Run("first call by name, second call by id cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 0)
|
||||
})
|
||||
|
||||
t.Run("first call by id not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Emoji().Delete(&fakeEmoji, 0)
|
||||
cachedStore.Emoji().Get(context.Background(), "123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("call by id, use master", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().Get(context.Background(), "master", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Emoji().Delete(&ctxEmoji, 0)
|
||||
cachedStore.Emoji().Get(context.Background(), "master", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call by name not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
cachedStore.Emoji().Delete(&fakeEmoji, 0)
|
||||
cachedStore.Emoji().GetByName(context.Background(), "name123", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
|
||||
})
|
||||
|
||||
t.Run("call by name, use master", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Emoji().GetByName(context.Background(), "master", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
cachedStore.Emoji().Delete(&ctxEmoji, 0)
|
||||
cachedStore.Emoji().GetByName(context.Background(), "master", true)
|
||||
mockStore.Emoji().(*mocks.EmojiStore).AssertNumberOfCalls(t, "GetByName", 2)
|
||||
})
|
||||
}
|
||||
99
server/channels/store/localcachelayer/file_info_layer.go
Обычный файл
99
server/channels/store/localcachelayer/file_info_layer.go
Обычный файл
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type LocalCacheFileInfoStore struct {
|
||||
store.FileInfoStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheFileInfoStore) handleClusterInvalidateFileInfo(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.fileInfoCache.Purge()
|
||||
return
|
||||
}
|
||||
s.rootStore.fileInfoCache.Remove(string(msg.Data))
|
||||
}
|
||||
|
||||
func (s LocalCacheFileInfoStore) GetForPost(postId string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error) {
|
||||
if !allowFromCache {
|
||||
return s.FileInfoStore.GetForPost(postId, readFromMaster, includeDeleted, allowFromCache)
|
||||
}
|
||||
|
||||
cacheKey := postId
|
||||
if includeDeleted {
|
||||
cacheKey += "_deleted"
|
||||
}
|
||||
|
||||
var fileInfo []*model.FileInfo
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, cacheKey, &fileInfo); err == nil {
|
||||
return fileInfo, nil
|
||||
}
|
||||
|
||||
fileInfos, err := s.FileInfoStore.GetForPost(postId, readFromMaster, includeDeleted, allowFromCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(fileInfos) > 0 {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, cacheKey, fileInfos)
|
||||
}
|
||||
|
||||
return fileInfos, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheFileInfoStore) ClearCaches() {
|
||||
s.rootStore.fileInfoCache.Purge()
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Purge")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheFileInfoStore) InvalidateFileInfosForPostCache(postId string, deleted bool) {
|
||||
cacheKey := postId
|
||||
if deleted {
|
||||
cacheKey += "_deleted"
|
||||
}
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.fileInfoCache, cacheKey)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("File Info Cache - Remove by PostId")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheFileInfoStore) GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error) {
|
||||
storageUsageKey := "storage_usage"
|
||||
if includeDeleted {
|
||||
storageUsageKey += "_deleted"
|
||||
}
|
||||
|
||||
if !allowFromCache {
|
||||
usage, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, storageUsageKey, usage)
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
var usage int64
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.fileInfoCache, storageUsageKey, &usage); err == nil {
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
usage, err := s.FileInfoStore.GetStorageUsage(allowFromCache, includeDeleted)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.fileInfoCache, storageUsageKey, usage)
|
||||
return usage, nil
|
||||
}
|
||||
63
server/channels/store/localcachelayer/file_info_layer_test.go
Обычный файл
63
server/channels/store/localcachelayer/file_info_layer_test.go
Обычный файл
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestFileInfoStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestFileInfoStore)
|
||||
}
|
||||
|
||||
func TestFileInfoStoreCache(t *testing.T) {
|
||||
fakeFileInfo := model.FileInfo{PostId: "123"}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
fileInfos, err := cachedStore.FileInfo().GetForPost("123", true, true, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fileInfos, []*model.FileInfo{&fakeFileInfo})
|
||||
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
assert.Equal(t, fileInfos, []*model.FileInfo{&fakeFileInfo})
|
||||
cachedStore.FileInfo().GetForPost("123", true, true, true)
|
||||
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force no cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.FileInfo().GetForPost("123", true, true, true)
|
||||
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
cachedStore.FileInfo().GetForPost("123", true, true, false)
|
||||
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.FileInfo().GetForPost("123", true, true, true)
|
||||
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
cachedStore.FileInfo().InvalidateFileInfosForPostCache("123", true)
|
||||
cachedStore.FileInfo().GetForPost("123", true, true, true)
|
||||
mockStore.FileInfo().(*mocks.FileInfoStore).AssertNumberOfCalls(t, "GetForPost", 2)
|
||||
})
|
||||
}
|
||||
460
server/channels/store/localcachelayer/layer.go
Обычный файл
460
server/channels/store/localcachelayer/layer.go
Обычный файл
@@ -0,0 +1,460 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
ReactionCacheSize = 20000
|
||||
ReactionCacheSec = 30 * 60
|
||||
|
||||
RoleCacheSize = 20000
|
||||
RoleCacheSec = 30 * 60
|
||||
|
||||
SchemeCacheSize = 20000
|
||||
SchemeCacheSec = 30 * 60
|
||||
|
||||
FileInfoCacheSize = 25000
|
||||
FileInfoCacheSec = 30 * 60
|
||||
|
||||
ChannelGuestCountCacheSize = model.ChannelCacheSize
|
||||
ChannelGuestCountCacheSec = 30 * 60
|
||||
|
||||
WebhookCacheSize = 25000
|
||||
WebhookCacheSec = 15 * 60
|
||||
|
||||
EmojiCacheSize = 5000
|
||||
EmojiCacheSec = 30 * 60
|
||||
|
||||
ChannelPinnedPostsCountsCacheSize = model.ChannelCacheSize
|
||||
ChannelPinnedPostsCountsCacheSec = 30 * 60
|
||||
|
||||
ChannelMembersCountsCacheSize = model.ChannelCacheSize
|
||||
ChannelMembersCountsCacheSec = 30 * 60
|
||||
|
||||
LastPostsCacheSize = 20000
|
||||
LastPostsCacheSec = 30 * 60
|
||||
PostsUsageCacheSize = 1
|
||||
PostsUsageCacheSec = 30 * 60
|
||||
|
||||
TermsOfServiceCacheSize = 20000
|
||||
TermsOfServiceCacheSec = 30 * 60
|
||||
LastPostTimeCacheSize = 25000
|
||||
LastPostTimeCacheSec = 15 * 60
|
||||
|
||||
UserProfileByIDCacheSize = 20000
|
||||
UserProfileByIDSec = 30 * 60
|
||||
|
||||
ProfilesInChannelCacheSize = model.ChannelCacheSize
|
||||
ProfilesInChannelCacheSec = 15 * 60
|
||||
|
||||
TeamCacheSize = 20000
|
||||
TeamCacheSec = 30 * 60
|
||||
|
||||
ChannelCacheSec = 15 * 60 // 15 mins
|
||||
)
|
||||
|
||||
var clearCacheMessageData = []byte("")
|
||||
|
||||
type LocalCacheStore struct {
|
||||
store.Store
|
||||
metrics einterfaces.MetricsInterface
|
||||
cluster einterfaces.ClusterInterface
|
||||
|
||||
reaction LocalCacheReactionStore
|
||||
reactionCache cache.Cache
|
||||
|
||||
fileInfo LocalCacheFileInfoStore
|
||||
fileInfoCache cache.Cache
|
||||
|
||||
role LocalCacheRoleStore
|
||||
roleCache cache.Cache
|
||||
rolePermissionsCache cache.Cache
|
||||
|
||||
scheme LocalCacheSchemeStore
|
||||
schemeCache cache.Cache
|
||||
|
||||
emoji *LocalCacheEmojiStore
|
||||
emojiCacheById cache.Cache
|
||||
emojiIdCacheByName cache.Cache
|
||||
|
||||
channel LocalCacheChannelStore
|
||||
channelMemberCountsCache cache.Cache
|
||||
channelGuestCountCache cache.Cache
|
||||
channelPinnedPostCountsCache cache.Cache
|
||||
channelByIdCache cache.Cache
|
||||
|
||||
webhook LocalCacheWebhookStore
|
||||
webhookCache cache.Cache
|
||||
|
||||
post LocalCachePostStore
|
||||
postLastPostsCache cache.Cache
|
||||
lastPostTimeCache cache.Cache
|
||||
postsUsageCache cache.Cache
|
||||
|
||||
user *LocalCacheUserStore
|
||||
userProfileByIdsCache cache.Cache
|
||||
profilesInChannelCache cache.Cache
|
||||
|
||||
team LocalCacheTeamStore
|
||||
teamAllTeamIdsForUserCache cache.Cache
|
||||
|
||||
termsOfService LocalCacheTermsOfServiceStore
|
||||
termsOfServiceCache cache.Cache
|
||||
}
|
||||
|
||||
func NewLocalCacheLayer(baseStore store.Store, metrics einterfaces.MetricsInterface, cluster einterfaces.ClusterInterface, cacheProvider cache.Provider) (localCacheStore LocalCacheStore, err error) {
|
||||
localCacheStore = LocalCacheStore{
|
||||
Store: baseStore,
|
||||
cluster: cluster,
|
||||
metrics: metrics,
|
||||
}
|
||||
// Reactions
|
||||
if localCacheStore.reactionCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: ReactionCacheSize,
|
||||
Name: "Reaction",
|
||||
DefaultExpiry: ReactionCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForReactions,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.reaction = LocalCacheReactionStore{ReactionStore: baseStore.Reaction(), rootStore: &localCacheStore}
|
||||
|
||||
// Roles
|
||||
if localCacheStore.roleCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: RoleCacheSize,
|
||||
Name: "Role",
|
||||
DefaultExpiry: RoleCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRoles,
|
||||
Striped: true,
|
||||
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if localCacheStore.rolePermissionsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: RoleCacheSize,
|
||||
Name: "RolePermission",
|
||||
DefaultExpiry: RoleCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForRolePermissions,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.role = LocalCacheRoleStore{RoleStore: baseStore.Role(), rootStore: &localCacheStore}
|
||||
|
||||
// Schemes
|
||||
if localCacheStore.schemeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: SchemeCacheSize,
|
||||
Name: "Scheme",
|
||||
DefaultExpiry: SchemeCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForSchemes,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.scheme = LocalCacheSchemeStore{SchemeStore: baseStore.Scheme(), rootStore: &localCacheStore}
|
||||
|
||||
// FileInfo
|
||||
if localCacheStore.fileInfoCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: FileInfoCacheSize,
|
||||
Name: "FileInfo",
|
||||
DefaultExpiry: FileInfoCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForFileInfos,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.fileInfo = LocalCacheFileInfoStore{FileInfoStore: baseStore.FileInfo(), rootStore: &localCacheStore}
|
||||
|
||||
// Webhooks
|
||||
if localCacheStore.webhookCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: WebhookCacheSize,
|
||||
Name: "Webhook",
|
||||
DefaultExpiry: WebhookCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForWebhooks,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.webhook = LocalCacheWebhookStore{WebhookStore: baseStore.Webhook(), rootStore: &localCacheStore}
|
||||
|
||||
// Emojis
|
||||
if localCacheStore.emojiCacheById, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: EmojiCacheSize,
|
||||
Name: "EmojiById",
|
||||
DefaultExpiry: EmojiCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForEmojisById,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if localCacheStore.emojiIdCacheByName, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: EmojiCacheSize,
|
||||
Name: "EmojiByName",
|
||||
DefaultExpiry: EmojiCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForEmojisIdByName,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.emoji = &LocalCacheEmojiStore{
|
||||
EmojiStore: baseStore.Emoji(),
|
||||
rootStore: &localCacheStore,
|
||||
emojiByIdInvalidations: make(map[string]bool),
|
||||
emojiByNameInvalidations: make(map[string]bool),
|
||||
}
|
||||
|
||||
// Channels
|
||||
if localCacheStore.channelPinnedPostCountsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: ChannelPinnedPostsCountsCacheSize,
|
||||
Name: "ChannelPinnedPostsCounts",
|
||||
DefaultExpiry: ChannelPinnedPostsCountsCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelPinnedpostsCounts,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if localCacheStore.channelMemberCountsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: ChannelMembersCountsCacheSize,
|
||||
Name: "ChannelMemberCounts",
|
||||
DefaultExpiry: ChannelMembersCountsCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelMemberCounts,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if localCacheStore.channelGuestCountCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: ChannelGuestCountCacheSize,
|
||||
Name: "ChannelGuestsCount",
|
||||
DefaultExpiry: ChannelGuestCountCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannelGuestCount,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if localCacheStore.channelByIdCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: model.ChannelCacheSize,
|
||||
Name: "channelById",
|
||||
DefaultExpiry: ChannelCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForChannel,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.channel = LocalCacheChannelStore{ChannelStore: baseStore.Channel(), rootStore: &localCacheStore}
|
||||
|
||||
// Posts
|
||||
if localCacheStore.postLastPostsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: LastPostsCacheSize,
|
||||
Name: "LastPost",
|
||||
DefaultExpiry: LastPostsCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForLastPosts,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if localCacheStore.lastPostTimeCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: LastPostTimeCacheSize,
|
||||
Name: "LastPostTime",
|
||||
DefaultExpiry: LastPostTimeCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForLastPostTime,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if localCacheStore.postsUsageCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: PostsUsageCacheSize,
|
||||
Name: "PostsUsage",
|
||||
DefaultExpiry: PostsUsageCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForPostsUsage,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.post = LocalCachePostStore{PostStore: baseStore.Post(), rootStore: &localCacheStore}
|
||||
|
||||
// TOS
|
||||
if localCacheStore.termsOfServiceCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: TermsOfServiceCacheSize,
|
||||
Name: "TermsOfService",
|
||||
DefaultExpiry: TermsOfServiceCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTermsOfService,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.termsOfService = LocalCacheTermsOfServiceStore{TermsOfServiceStore: baseStore.TermsOfService(), rootStore: &localCacheStore}
|
||||
|
||||
// Users
|
||||
if localCacheStore.userProfileByIdsCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: UserProfileByIDCacheSize,
|
||||
Name: "UserProfileByIds",
|
||||
DefaultExpiry: UserProfileByIDSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForProfileByIds,
|
||||
Striped: true,
|
||||
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
if localCacheStore.profilesInChannelCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: ProfilesInChannelCacheSize,
|
||||
Name: "ProfilesInChannel",
|
||||
DefaultExpiry: ProfilesInChannelCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForProfileInChannel,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.user = &LocalCacheUserStore{
|
||||
UserStore: baseStore.User(),
|
||||
rootStore: &localCacheStore,
|
||||
userProfileByIdsInvalidations: make(map[string]bool),
|
||||
}
|
||||
|
||||
// Teams
|
||||
if localCacheStore.teamAllTeamIdsForUserCache, err = cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: TeamCacheSize,
|
||||
Name: "Team",
|
||||
DefaultExpiry: TeamCacheSec * time.Second,
|
||||
InvalidateClusterEvent: model.ClusterEventInvalidateCacheForTeams,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
localCacheStore.team = LocalCacheTeamStore{TeamStore: baseStore.Team(), rootStore: &localCacheStore}
|
||||
|
||||
if cluster != nil {
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForReactions, localCacheStore.reaction.handleClusterInvalidateReaction)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRoles, localCacheStore.role.handleClusterInvalidateRole)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForRolePermissions, localCacheStore.role.handleClusterInvalidateRolePermissions)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForSchemes, localCacheStore.scheme.handleClusterInvalidateScheme)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForFileInfos, localCacheStore.fileInfo.handleClusterInvalidateFileInfo)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForLastPostTime, localCacheStore.post.handleClusterInvalidateLastPostTime)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForPostsUsage, localCacheStore.post.handleClusterInvalidatePostsUsage)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForWebhooks, localCacheStore.webhook.handleClusterInvalidateWebhook)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForEmojisById, localCacheStore.emoji.handleClusterInvalidateEmojiById)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForEmojisIdByName, localCacheStore.emoji.handleClusterInvalidateEmojiIdByName)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelPinnedpostsCounts, localCacheStore.channel.handleClusterInvalidateChannelPinnedPostCount)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelMemberCounts, localCacheStore.channel.handleClusterInvalidateChannelMemberCounts)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannelGuestCount, localCacheStore.channel.handleClusterInvalidateChannelGuestCounts)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForChannel, localCacheStore.channel.handleClusterInvalidateChannelById)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForLastPosts, localCacheStore.post.handleClusterInvalidateLastPosts)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTermsOfService, localCacheStore.termsOfService.handleClusterInvalidateTermsOfService)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForProfileByIds, localCacheStore.user.handleClusterInvalidateScheme)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForProfileInChannel, localCacheStore.user.handleClusterInvalidateProfilesInChannel)
|
||||
cluster.RegisterClusterMessageHandler(model.ClusterEventInvalidateCacheForTeams, localCacheStore.team.handleClusterInvalidateTeam)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) Reaction() store.ReactionStore {
|
||||
return s.reaction
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) Role() store.RoleStore {
|
||||
return s.role
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) Scheme() store.SchemeStore {
|
||||
return s.scheme
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) FileInfo() store.FileInfoStore {
|
||||
return s.fileInfo
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) Webhook() store.WebhookStore {
|
||||
return s.webhook
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) Emoji() store.EmojiStore {
|
||||
return s.emoji
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) Channel() store.ChannelStore {
|
||||
return s.channel
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) Post() store.PostStore {
|
||||
return s.post
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) TermsOfService() store.TermsOfServiceStore {
|
||||
return s.termsOfService
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) User() store.UserStore {
|
||||
return s.user
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) Team() store.TeamStore {
|
||||
return s.team
|
||||
}
|
||||
|
||||
func (s LocalCacheStore) DropAllTables() {
|
||||
s.Invalidate()
|
||||
s.Store.DropAllTables()
|
||||
}
|
||||
|
||||
func (s *LocalCacheStore) doInvalidateCacheCluster(cache cache.Cache, key string) {
|
||||
cache.Remove(key)
|
||||
if s.cluster != nil {
|
||||
msg := &model.ClusterMessage{
|
||||
Event: cache.GetInvalidateClusterEvent(),
|
||||
SendType: model.ClusterSendBestEffort,
|
||||
Data: []byte(key),
|
||||
}
|
||||
s.cluster.SendClusterMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheStore) doStandardAddToCache(cache cache.Cache, key string, value any) {
|
||||
cache.SetWithDefaultExpiry(key, value)
|
||||
}
|
||||
|
||||
func (s *LocalCacheStore) doStandardReadCache(cache cache.Cache, key string, value any) error {
|
||||
err := cache.Get(key, value)
|
||||
if err == nil {
|
||||
if s.metrics != nil {
|
||||
s.metrics.IncrementMemCacheHitCounter(cache.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if s.metrics != nil {
|
||||
s.metrics.IncrementMemCacheMissCounter(cache.Name())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *LocalCacheStore) doClearCacheCluster(cache cache.Cache) {
|
||||
cache.Purge()
|
||||
if s.cluster != nil {
|
||||
msg := &model.ClusterMessage{
|
||||
Event: cache.GetInvalidateClusterEvent(),
|
||||
SendType: model.ClusterSendBestEffort,
|
||||
Data: clearCacheMessageData,
|
||||
}
|
||||
s.cluster.SendClusterMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheStore) Invalidate() {
|
||||
s.doClearCacheCluster(s.reactionCache)
|
||||
s.doClearCacheCluster(s.schemeCache)
|
||||
s.doClearCacheCluster(s.roleCache)
|
||||
s.doClearCacheCluster(s.fileInfoCache)
|
||||
s.doClearCacheCluster(s.webhookCache)
|
||||
s.doClearCacheCluster(s.emojiCacheById)
|
||||
s.doClearCacheCluster(s.emojiIdCacheByName)
|
||||
s.doClearCacheCluster(s.channelMemberCountsCache)
|
||||
s.doClearCacheCluster(s.channelPinnedPostCountsCache)
|
||||
s.doClearCacheCluster(s.channelGuestCountCache)
|
||||
s.doClearCacheCluster(s.channelByIdCache)
|
||||
s.doClearCacheCluster(s.postLastPostsCache)
|
||||
s.doClearCacheCluster(s.termsOfServiceCache)
|
||||
s.doClearCacheCluster(s.lastPostTimeCache)
|
||||
s.doClearCacheCluster(s.userProfileByIdsCache)
|
||||
s.doClearCacheCluster(s.profilesInChannelCache)
|
||||
s.doClearCacheCluster(s.teamAllTeamIdsForUserCache)
|
||||
s.doClearCacheCluster(s.rolePermissionsCache)
|
||||
}
|
||||
136
server/channels/store/localcachelayer/layer_test.go
Обычный файл
136
server/channels/store/localcachelayer/layer_test.go
Обычный файл
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
type storeType struct {
|
||||
Name string
|
||||
SqlSettings *model.SqlSettings
|
||||
SqlStore *sqlstore.SqlStore
|
||||
Store store.Store
|
||||
}
|
||||
|
||||
var storeTypes []*storeType
|
||||
|
||||
func newStoreType(name, driver string) *storeType {
|
||||
return &storeType{
|
||||
Name: name,
|
||||
SqlSettings: storetest.MakeSqlSettings(driver, false),
|
||||
}
|
||||
}
|
||||
|
||||
func StoreTest(t *testing.T, f func(*testing.T, store.Store)) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
tearDownStores()
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
for _, st := range storeTypes {
|
||||
st := st
|
||||
t.Run(st.Name, func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
f(t, st.Store)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func StoreTestWithSqlStore(t *testing.T, f func(*testing.T, store.Store, storetest.SqlStore)) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
tearDownStores()
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
for _, st := range storeTypes {
|
||||
st := st
|
||||
t.Run(st.Name, func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
f(t, st.Store, sqlstore.NewStoreTestWrapper(st.SqlStore))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initStores() {
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
|
||||
// In CI, we already run the entire test suite for both mysql and postgres in parallel.
|
||||
// So we just run the tests for the current database set.
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
switch os.Getenv("MM_SQLSETTINGS_DRIVERNAME") {
|
||||
case "mysql":
|
||||
storeTypes = append(storeTypes, newStoreType("LocalCache+MySQL", model.DatabaseDriverMysql))
|
||||
case "postgres":
|
||||
storeTypes = append(storeTypes, newStoreType("LocalCache+PostgreSQL", model.DatabaseDriverPostgres))
|
||||
}
|
||||
} else {
|
||||
storeTypes = append(storeTypes, newStoreType("LocalCache+MySQL", model.DatabaseDriverMysql),
|
||||
newStoreType("LocalCache+PostgreSQL", model.DatabaseDriverPostgres))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
tearDownStores()
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
var wg sync.WaitGroup
|
||||
for _, st := range storeTypes {
|
||||
st := st
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
var err error
|
||||
defer wg.Done()
|
||||
st.SqlStore = sqlstore.New(*st.SqlSettings, nil)
|
||||
st.Store, err = NewLocalCacheLayer(st.SqlStore, nil, nil, getMockCacheProvider())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
st.Store.DropAllTables()
|
||||
st.Store.MarkSystemRanUnitTests()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
var tearDownStoresOnce sync.Once
|
||||
|
||||
func tearDownStores() {
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
tearDownStoresOnce.Do(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(storeTypes))
|
||||
for _, st := range storeTypes {
|
||||
st := st
|
||||
go func() {
|
||||
if st.Store != nil {
|
||||
st.Store.Close()
|
||||
}
|
||||
if st.SqlSettings != nil {
|
||||
storetest.CleanupSqlSettings(st.SqlSettings)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
180
server/channels/store/localcachelayer/main_test.go
Обычный файл
180
server/channels/store/localcachelayer/main_test.go
Обычный файл
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/cache"
|
||||
cachemocks "github.com/mattermost/mattermost-server/v6/server/platform/services/cache/mocks"
|
||||
)
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
|
||||
func getMockCacheProvider() cache.Provider {
|
||||
mockCacheProvider := cachemocks.Provider{}
|
||||
mockCacheProvider.On("NewCache", mock.Anything).
|
||||
Return(cache.NewLRU(cache.LRUOptions{Size: 128}), nil)
|
||||
return &mockCacheProvider
|
||||
}
|
||||
|
||||
func getMockStore() *mocks.Store {
|
||||
mockStore := mocks.Store{}
|
||||
|
||||
fakeReaction := model.Reaction{PostId: "123"}
|
||||
mockReactionsStore := mocks.ReactionStore{}
|
||||
mockReactionsStore.On("Save", &fakeReaction).Return(&model.Reaction{}, nil)
|
||||
mockReactionsStore.On("Delete", &fakeReaction).Return(&model.Reaction{}, nil)
|
||||
mockReactionsStore.On("GetForPost", "123", false).Return([]*model.Reaction{&fakeReaction}, nil)
|
||||
mockReactionsStore.On("GetForPost", "123", true).Return([]*model.Reaction{&fakeReaction}, nil)
|
||||
mockStore.On("Reaction").Return(&mockReactionsStore)
|
||||
|
||||
fakeRole := model.Role{Id: "123", Name: "role-name"}
|
||||
mockRolesStore := mocks.RoleStore{}
|
||||
mockRolesStore.On("Save", &fakeRole).Return(&model.Role{}, nil)
|
||||
mockRolesStore.On("Delete", "123").Return(&fakeRole, nil)
|
||||
mockRolesStore.On("GetByName", context.Background(), "role-name").Return(&fakeRole, nil)
|
||||
mockRolesStore.On("GetByNames", []string{"role-name"}).Return([]*model.Role{&fakeRole}, nil)
|
||||
mockRolesStore.On("PermanentDeleteAll").Return(nil)
|
||||
mockStore.On("Role").Return(&mockRolesStore)
|
||||
|
||||
fakeScheme := model.Scheme{Id: "123", Name: "scheme-name"}
|
||||
mockSchemesStore := mocks.SchemeStore{}
|
||||
mockSchemesStore.On("Save", &fakeScheme).Return(&model.Scheme{}, nil)
|
||||
mockSchemesStore.On("Delete", "123").Return(&model.Scheme{}, nil)
|
||||
mockSchemesStore.On("Get", "123").Return(&fakeScheme, nil)
|
||||
mockSchemesStore.On("PermanentDeleteAll").Return(nil)
|
||||
mockStore.On("Scheme").Return(&mockSchemesStore)
|
||||
|
||||
fakeFileInfo := model.FileInfo{PostId: "123"}
|
||||
mockFileInfoStore := mocks.FileInfoStore{}
|
||||
mockFileInfoStore.On("GetForPost", "123", true, true, false).Return([]*model.FileInfo{&fakeFileInfo}, nil)
|
||||
mockFileInfoStore.On("GetForPost", "123", true, true, true).Return([]*model.FileInfo{&fakeFileInfo}, nil)
|
||||
mockStore.On("FileInfo").Return(&mockFileInfoStore)
|
||||
|
||||
fakeWebhook := model.IncomingWebhook{Id: "123"}
|
||||
mockWebhookStore := mocks.WebhookStore{}
|
||||
mockWebhookStore.On("GetIncoming", "123", true).Return(&fakeWebhook, nil)
|
||||
mockWebhookStore.On("GetIncoming", "123", false).Return(&fakeWebhook, nil)
|
||||
mockStore.On("Webhook").Return(&mockWebhookStore)
|
||||
|
||||
fakeEmoji := model.Emoji{Id: "123", Name: "name123"}
|
||||
ctxEmoji := model.Emoji{Id: "master", Name: "name123"}
|
||||
mockEmojiStore := mocks.EmojiStore{}
|
||||
mockEmojiStore.On("Get", mock.Anything, "123", true).Return(&fakeEmoji, nil)
|
||||
mockEmojiStore.On("Get", mock.Anything, "123", false).Return(&fakeEmoji, nil)
|
||||
mockEmojiStore.On("Get", context.Background(), "master", true).Return(&ctxEmoji, nil)
|
||||
mockEmojiStore.On("Get", sqlstore.WithMaster(context.Background()), "master", true).Return(&ctxEmoji, nil)
|
||||
mockEmojiStore.On("GetByName", mock.Anything, "name123", true).Return(&fakeEmoji, nil)
|
||||
mockEmojiStore.On("GetByName", mock.Anything, "name123", false).Return(&fakeEmoji, nil)
|
||||
mockEmojiStore.On("GetByName", context.Background(), "master", true).Return(&ctxEmoji, nil)
|
||||
mockEmojiStore.On("GetByName", sqlstore.WithMaster(context.Background()), "master", false).Return(&ctxEmoji, nil)
|
||||
mockEmojiStore.On("Delete", &fakeEmoji, int64(0)).Return(nil)
|
||||
mockEmojiStore.On("Delete", &ctxEmoji, int64(0)).Return(nil)
|
||||
mockStore.On("Emoji").Return(&mockEmojiStore)
|
||||
|
||||
mockCount := int64(10)
|
||||
mockGuestCount := int64(12)
|
||||
channelId := "channel1"
|
||||
fakeChannelId := model.Channel{Id: channelId}
|
||||
mockChannelStore := mocks.ChannelStore{}
|
||||
mockChannelStore.On("ClearCaches").Return()
|
||||
mockChannelStore.On("GetMemberCount", "id", true).Return(mockCount, nil)
|
||||
mockChannelStore.On("GetMemberCount", "id", false).Return(mockCount, nil)
|
||||
mockChannelStore.On("GetGuestCount", "id", true).Return(mockGuestCount, nil)
|
||||
mockChannelStore.On("GetGuestCount", "id", false).Return(mockGuestCount, nil)
|
||||
mockChannelStore.On("Get", channelId, true).Return(&fakeChannelId, nil)
|
||||
mockChannelStore.On("Get", channelId, false).Return(&fakeChannelId, nil)
|
||||
mockStore.On("Channel").Return(&mockChannelStore)
|
||||
|
||||
mockPinnedPostsCount := int64(10)
|
||||
mockChannelStore.On("GetPinnedPostCount", "id", true).Return(mockPinnedPostsCount, nil)
|
||||
mockChannelStore.On("GetPinnedPostCount", "id", false).Return(mockPinnedPostsCount, nil)
|
||||
|
||||
fakePosts := &model.PostList{}
|
||||
fakeOptions := model.GetPostsOptions{ChannelId: "123", PerPage: 30}
|
||||
mockPostStore := mocks.PostStore{}
|
||||
mockPostStore.On("GetPosts", fakeOptions, true, map[string]bool{}).Return(fakePosts, nil)
|
||||
mockPostStore.On("GetPosts", fakeOptions, false, map[string]bool{}).Return(fakePosts, nil)
|
||||
mockPostStore.On("InvalidateLastPostTimeCache", "12360")
|
||||
|
||||
mockPostStoreOptions := model.GetPostsSinceOptions{
|
||||
ChannelId: "channelId",
|
||||
Time: 1,
|
||||
SkipFetchThreads: false,
|
||||
}
|
||||
|
||||
mockPostStoreEtagResult := fmt.Sprintf("%v.%v", model.CurrentVersion, 1)
|
||||
mockPostStore.On("ClearCaches")
|
||||
mockPostStore.On("InvalidateLastPostTimeCache", "channelId")
|
||||
mockPostStore.On("GetEtag", "channelId", true, false).Return(mockPostStoreEtagResult)
|
||||
mockPostStore.On("GetEtag", "channelId", false, false).Return(mockPostStoreEtagResult)
|
||||
mockPostStore.On("GetPostsSince", mockPostStoreOptions, true, map[string]bool{}).Return(model.NewPostList(), nil)
|
||||
mockPostStore.On("GetPostsSince", mockPostStoreOptions, false, map[string]bool{}).Return(model.NewPostList(), nil)
|
||||
mockStore.On("Post").Return(&mockPostStore)
|
||||
|
||||
fakeTermsOfService := model.TermsOfService{Id: "123", CreateAt: 11111, UserId: "321", Text: "Terms of service test"}
|
||||
mockTermsOfServiceStore := mocks.TermsOfServiceStore{}
|
||||
mockTermsOfServiceStore.On("InvalidateTermsOfService", "123")
|
||||
mockTermsOfServiceStore.On("Save", &fakeTermsOfService).Return(&fakeTermsOfService, nil)
|
||||
mockTermsOfServiceStore.On("GetLatest", true).Return(&fakeTermsOfService, nil)
|
||||
mockTermsOfServiceStore.On("GetLatest", false).Return(&fakeTermsOfService, nil)
|
||||
mockTermsOfServiceStore.On("Get", "123", true).Return(&fakeTermsOfService, nil)
|
||||
mockTermsOfServiceStore.On("Get", "123", false).Return(&fakeTermsOfService, nil)
|
||||
mockStore.On("TermsOfService").Return(&mockTermsOfServiceStore)
|
||||
|
||||
fakeUser := []*model.User{{
|
||||
Id: "123",
|
||||
AuthData: model.NewString("authData"),
|
||||
AuthService: "authService",
|
||||
}}
|
||||
mockUserStore := mocks.UserStore{}
|
||||
mockUserStore.On("GetProfileByIds", mock.Anything, []string{"123"}, &store.UserGetByIdsOpts{}, true).Return(fakeUser, nil)
|
||||
mockUserStore.On("GetProfileByIds", mock.Anything, []string{"123"}, &store.UserGetByIdsOpts{}, false).Return(fakeUser, nil)
|
||||
|
||||
fakeProfilesInChannelMap := map[string]*model.User{
|
||||
"456": {Id: "456"},
|
||||
}
|
||||
mockUserStore.On("GetAllProfilesInChannel", mock.Anything, "123", true).Return(fakeProfilesInChannelMap, nil)
|
||||
mockUserStore.On("GetAllProfilesInChannel", mock.Anything, "123", false).Return(fakeProfilesInChannelMap, nil)
|
||||
|
||||
mockUserStore.On("Get", mock.Anything, "123").Return(fakeUser[0], nil)
|
||||
users := []*model.User{
|
||||
fakeUser[0],
|
||||
{
|
||||
Id: "456",
|
||||
AuthData: model.NewString("authData"),
|
||||
AuthService: "authService",
|
||||
},
|
||||
}
|
||||
mockUserStore.On("GetMany", mock.Anything, []string{"123", "456"}).Return(users, nil)
|
||||
mockUserStore.On("GetMany", mock.Anything, []string{"123"}).Return(users[0:1], nil)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
|
||||
fakeUserTeamIds := []string{"1", "2", "3"}
|
||||
mockTeamStore := mocks.TeamStore{}
|
||||
mockTeamStore.On("GetUserTeamIds", "123", true).Return(fakeUserTeamIds, nil)
|
||||
mockTeamStore.On("GetUserTeamIds", "123", false).Return(fakeUserTeamIds, nil)
|
||||
mockStore.On("Team").Return(&mockTeamStore)
|
||||
|
||||
return &mockStore
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
mainHelper = testlib.NewMainHelperWithOptions(nil)
|
||||
defer mainHelper.Close()
|
||||
|
||||
initStores()
|
||||
mainHelper.Main(m)
|
||||
tearDownStores()
|
||||
}
|
||||
166
server/channels/store/localcachelayer/post_layer.go
Обычный файл
166
server/channels/store/localcachelayer/post_layer.go
Обычный файл
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type LocalCachePostStore struct {
|
||||
store.PostStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCachePostStore) handleClusterInvalidateLastPostTime(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.lastPostTimeCache.Purge()
|
||||
} else {
|
||||
s.rootStore.lastPostTimeCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCachePostStore) handleClusterInvalidateLastPosts(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.postLastPostsCache.Purge()
|
||||
} else {
|
||||
s.rootStore.postLastPostsCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCachePostStore) handleClusterInvalidatePostsUsage(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.postsUsageCache.Purge()
|
||||
} else {
|
||||
s.rootStore.postsUsageCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCachePostStore) ClearCaches() {
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.lastPostTimeCache)
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.postLastPostsCache)
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.postsUsageCache)
|
||||
s.PostStore.ClearCaches()
|
||||
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Purge")
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Purge")
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Posts Usage Cache - Purge")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCachePostStore) InvalidateLastPostTimeCache(channelId string) {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.lastPostTimeCache, channelId)
|
||||
|
||||
// Keys are "{channelid}{limit}" and caching only occurs on limits of 30 and 60
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.postLastPostsCache, channelId+"30")
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.postLastPostsCache, channelId+"60")
|
||||
|
||||
s.PostStore.InvalidateLastPostTimeCache(channelId)
|
||||
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Post Time - Remove by Channel Id")
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Last Posts Cache - Remove by Channel Id")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCachePostStore) GetEtag(channelId string, allowFromCache, collapsedThreads bool) string {
|
||||
if allowFromCache {
|
||||
var lastTime int64
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.lastPostTimeCache, channelId, &lastTime); err == nil {
|
||||
return fmt.Sprintf("%v.%v", model.CurrentVersion, lastTime)
|
||||
}
|
||||
}
|
||||
|
||||
result := s.PostStore.GetEtag(channelId, allowFromCache, collapsedThreads)
|
||||
|
||||
splittedResult := strings.Split(result, ".")
|
||||
|
||||
lastTime, _ := strconv.ParseInt((splittedResult[len(splittedResult)-1]), 10, 64)
|
||||
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.lastPostTimeCache, channelId, lastTime)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s LocalCachePostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
|
||||
if allowFromCache {
|
||||
// If the last post in the channel's time is less than or equal to the time we are getting posts since,
|
||||
// we can safely return no posts.
|
||||
var lastTime int64
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.lastPostTimeCache, options.ChannelId, &lastTime); err == nil && lastTime <= options.Time {
|
||||
list := model.NewPostList()
|
||||
return list, nil
|
||||
}
|
||||
}
|
||||
|
||||
list, err := s.PostStore.GetPostsSince(options, allowFromCache, sanitizeOptions)
|
||||
|
||||
latestUpdate := options.Time
|
||||
if err == nil {
|
||||
for _, p := range list.ToSlice() {
|
||||
if latestUpdate < p.UpdateAt {
|
||||
latestUpdate = p.UpdateAt
|
||||
}
|
||||
}
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.lastPostTimeCache, options.ChannelId, latestUpdate)
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (s LocalCachePostStore) GetPosts(options model.GetPostsOptions, allowFromCache bool, sanitizeOptions map[string]bool) (*model.PostList, error) {
|
||||
if !allowFromCache {
|
||||
return s.PostStore.GetPosts(options, allowFromCache, sanitizeOptions)
|
||||
}
|
||||
|
||||
offset := options.PerPage * options.Page
|
||||
// Caching only occurs on limits of 30 and 60, the common limits requested by MM clients
|
||||
if offset == 0 && (options.PerPage == 60 || options.PerPage == 30) {
|
||||
var cacheItem *model.PostList
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.postLastPostsCache, fmt.Sprintf("%s%v", options.ChannelId, options.PerPage), &cacheItem); err == nil {
|
||||
return cacheItem, nil
|
||||
}
|
||||
}
|
||||
|
||||
list, err := s.PostStore.GetPosts(options, false, sanitizeOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Caching only occurs on limits of 30 and 60, the common limits requested by MM clients
|
||||
if offset == 0 && (options.PerPage == 60 || options.PerPage == 30) {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.postLastPostsCache, fmt.Sprintf("%s%v", options.ChannelId, options.PerPage), list)
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// AnalyticsPostCount looks up cache only when ExcludeDeleted and UsersPostsOnly are true and rest are falsy.
|
||||
func (s LocalCachePostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
|
||||
if !options.AllowFromCache || options.MustHaveFile || options.MustHaveHashtag || !options.UsersPostsOnly || !options.ExcludeDeleted || options.TeamId != "" {
|
||||
return s.PostStore.AnalyticsPostCount(options)
|
||||
}
|
||||
|
||||
// Currently cache only for app > usage > GetPostsUsage()
|
||||
// Other filter combinations can be cached if required
|
||||
cacheKey := "posts_usage"
|
||||
var count int64
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.postsUsageCache, cacheKey, &count); err == nil {
|
||||
return count, nil
|
||||
}
|
||||
|
||||
count, err := s.PostStore.AnalyticsPostCount(options)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.postsUsageCache, cacheKey, count)
|
||||
return count, nil
|
||||
}
|
||||
195
server/channels/store/localcachelayer/post_layer_test.go
Обычный файл
195
server/channels/store/localcachelayer/post_layer_test.go
Обычный файл
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestPostStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestPostStore)
|
||||
}
|
||||
|
||||
func TestPostStoreLastPostTimeCache(t *testing.T) {
|
||||
var fakeLastTime int64 = 1
|
||||
channelId := "channelId"
|
||||
fakeOptions := model.GetPostsSinceOptions{
|
||||
ChannelId: channelId,
|
||||
Time: fakeLastTime,
|
||||
SkipFetchThreads: false,
|
||||
}
|
||||
|
||||
t.Run("GetEtag: first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedResult := fmt.Sprintf("%v.%v", model.CurrentVersion, fakeLastTime)
|
||||
|
||||
etag := cachedStore.Post().GetEtag(channelId, true, false)
|
||||
assert.Equal(t, etag, expectedResult)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
|
||||
|
||||
etag = cachedStore.Post().GetEtag(channelId, true, false)
|
||||
assert.Equal(t, etag, expectedResult)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
|
||||
})
|
||||
|
||||
t.Run("GetEtag: first call not cached, second force no cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Post().GetEtag(channelId, true, false)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
|
||||
cachedStore.Post().GetEtag(channelId, false, false)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 2)
|
||||
})
|
||||
|
||||
t.Run("GetEtag: first call not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Post().GetEtag(channelId, true, false)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
|
||||
cachedStore.Post().InvalidateLastPostTimeCache(channelId)
|
||||
cachedStore.Post().GetEtag(channelId, true, false)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 2)
|
||||
})
|
||||
|
||||
t.Run("GetEtag: first call not cached, clear caches, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Post().GetEtag(channelId, true, false)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 1)
|
||||
cachedStore.Post().ClearCaches()
|
||||
cachedStore.Post().GetEtag(channelId, true, false)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetEtag", 2)
|
||||
})
|
||||
|
||||
t.Run("GetPostsSince: first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedResult := model.NewPostList()
|
||||
|
||||
list, err := cachedStore.Post().GetPostsSince(fakeOptions, true, map[string]bool{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, list, expectedResult)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
|
||||
|
||||
list, err = cachedStore.Post().GetPostsSince(fakeOptions, true, map[string]bool{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, list, expectedResult)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
|
||||
})
|
||||
|
||||
t.Run("GetPostsSince: first call not cached, second force no cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Post().GetPostsSince(fakeOptions, true, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
|
||||
cachedStore.Post().GetPostsSince(fakeOptions, false, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 2)
|
||||
})
|
||||
|
||||
t.Run("GetPostsSince: first call not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Post().GetPostsSince(fakeOptions, true, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
|
||||
cachedStore.Post().InvalidateLastPostTimeCache(channelId)
|
||||
cachedStore.Post().GetPostsSince(fakeOptions, true, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 2)
|
||||
})
|
||||
|
||||
t.Run("GetPostsSince: first call not cached, clear caches, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Post().GetPostsSince(fakeOptions, true, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 1)
|
||||
cachedStore.Post().ClearCaches()
|
||||
cachedStore.Post().GetPostsSince(fakeOptions, true, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPostsSince", 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostStoreCache(t *testing.T) {
|
||||
fakePosts := &model.PostList{}
|
||||
fakeOptions := model.GetPostsOptions{ChannelId: "123", PerPage: 30}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotPosts, err := cachedStore.Post().GetPosts(fakeOptions, true, map[string]bool{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakePosts, gotPosts)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
|
||||
|
||||
_, _ = cachedStore.Post().GetPosts(fakeOptions, true, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotPosts, err := cachedStore.Post().GetPosts(fakeOptions, true, map[string]bool{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakePosts, gotPosts)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
|
||||
|
||||
_, _ = cachedStore.Post().GetPosts(fakeOptions, false, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotPosts, err := cachedStore.Post().GetPosts(fakeOptions, true, map[string]bool{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakePosts, gotPosts)
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
|
||||
|
||||
cachedStore.Post().InvalidateLastPostTimeCache("12360")
|
||||
|
||||
_, _ = cachedStore.Post().GetPosts(fakeOptions, true, map[string]bool{})
|
||||
mockStore.Post().(*mocks.PostStore).AssertNumberOfCalls(t, "GetPosts", 1)
|
||||
|
||||
})
|
||||
}
|
||||
61
server/channels/store/localcachelayer/reaction_layer.go
Обычный файл
61
server/channels/store/localcachelayer/reaction_layer.go
Обычный файл
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type LocalCacheReactionStore struct {
|
||||
store.ReactionStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheReactionStore) handleClusterInvalidateReaction(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.reactionCache.Purge()
|
||||
} else {
|
||||
s.rootStore.reactionCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheReactionStore) Save(reaction *model.Reaction) (*model.Reaction, error) {
|
||||
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.reactionCache, reaction.PostId)
|
||||
return s.ReactionStore.Save(reaction)
|
||||
}
|
||||
|
||||
func (s LocalCacheReactionStore) Delete(reaction *model.Reaction) (*model.Reaction, error) {
|
||||
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.reactionCache, reaction.PostId)
|
||||
return s.ReactionStore.Delete(reaction)
|
||||
}
|
||||
|
||||
func (s LocalCacheReactionStore) GetForPost(postId string, allowFromCache bool) ([]*model.Reaction, error) {
|
||||
if !allowFromCache {
|
||||
return s.ReactionStore.GetForPost(postId, false)
|
||||
}
|
||||
|
||||
var reaction []*model.Reaction
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.reactionCache, postId, &reaction); err == nil {
|
||||
return reaction, nil
|
||||
}
|
||||
|
||||
reaction, err := s.ReactionStore.GetForPost(postId, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.reactionCache, postId, reaction)
|
||||
|
||||
return reaction, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheReactionStore) DeleteAllWithEmojiName(emojiName string) error {
|
||||
// This could be improved. Right now we just clear the whole
|
||||
// cache because we don't have a way find what post Ids have this emoji name.
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.reactionCache)
|
||||
return s.ReactionStore.DeleteAllWithEmojiName(emojiName)
|
||||
}
|
||||
76
server/channels/store/localcachelayer/reaction_layer_test.go
Обычный файл
76
server/channels/store/localcachelayer/reaction_layer_test.go
Обычный файл
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestReactionStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestReactionStore)
|
||||
}
|
||||
|
||||
func TestReactionStoreCache(t *testing.T) {
|
||||
fakeReaction := model.Reaction{PostId: "123"}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
reaction, err := cachedStore.Reaction().GetForPost("123", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, reaction, []*model.Reaction{&fakeReaction})
|
||||
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
assert.Equal(t, reaction, []*model.Reaction{&fakeReaction})
|
||||
cachedStore.Reaction().GetForPost("123", true)
|
||||
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Reaction().GetForPost("123", true)
|
||||
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
cachedStore.Reaction().GetForPost("123", false)
|
||||
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, save, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Reaction().GetForPost("123", true)
|
||||
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
cachedStore.Reaction().Save(&fakeReaction)
|
||||
cachedStore.Reaction().GetForPost("123", true)
|
||||
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Reaction().GetForPost("123", true)
|
||||
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 1)
|
||||
cachedStore.Reaction().Delete(&fakeReaction)
|
||||
cachedStore.Reaction().GetForPost("123", true)
|
||||
mockStore.Reaction().(*mocks.ReactionStore).AssertNumberOfCalls(t, "GetForPost", 2)
|
||||
})
|
||||
}
|
||||
117
server/channels/store/localcachelayer/role_layer.go
Обычный файл
117
server/channels/store/localcachelayer/role_layer.go
Обычный файл
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type LocalCacheRoleStore struct {
|
||||
store.RoleStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheRoleStore) handleClusterInvalidateRole(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.roleCache.Purge()
|
||||
} else {
|
||||
s.rootStore.roleCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheRoleStore) handleClusterInvalidateRolePermissions(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.rolePermissionsCache.Purge()
|
||||
} else {
|
||||
s.rootStore.rolePermissionsCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheRoleStore) Save(role *model.Role) (*model.Role, error) {
|
||||
if role.Name != "" {
|
||||
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name)
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
|
||||
}
|
||||
return s.RoleStore.Save(role)
|
||||
}
|
||||
|
||||
func (s LocalCacheRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
|
||||
var role *model.Role
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.roleCache, name, &role); err == nil {
|
||||
return role, nil
|
||||
}
|
||||
|
||||
role, err := s.RoleStore.GetByName(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.roleCache, name, role)
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheRoleStore) GetByNames(names []string) ([]*model.Role, error) {
|
||||
var foundRoles []*model.Role
|
||||
var rolesToQuery []string
|
||||
|
||||
for _, roleName := range names {
|
||||
var role *model.Role
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.roleCache, roleName, &role); err == nil {
|
||||
foundRoles = append(foundRoles, role)
|
||||
} else {
|
||||
rolesToQuery = append(rolesToQuery, roleName)
|
||||
}
|
||||
}
|
||||
|
||||
roles, err := s.RoleStore.GetByNames(rolesToQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.roleCache, role.Name, role)
|
||||
}
|
||||
|
||||
return append(foundRoles, roles...), nil
|
||||
}
|
||||
|
||||
func (s LocalCacheRoleStore) Delete(roleId string) (*model.Role, error) {
|
||||
role, err := s.RoleStore.Delete(roleId)
|
||||
|
||||
if err == nil {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name)
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
|
||||
}
|
||||
return role, err
|
||||
}
|
||||
|
||||
func (s LocalCacheRoleStore) PermanentDeleteAll() error {
|
||||
defer s.rootStore.roleCache.Purge()
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
|
||||
|
||||
return s.RoleStore.PermanentDeleteAll()
|
||||
}
|
||||
|
||||
func (s LocalCacheRoleStore) ChannelHigherScopedPermissions(roleNames []string) (map[string]*model.RolePermissions, error) {
|
||||
sort.Strings(roleNames)
|
||||
cacheKey := strings.Join(roleNames, "/")
|
||||
var rolePermissionsMap map[string]*model.RolePermissions
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.rolePermissionsCache, cacheKey, &rolePermissionsMap); err == nil {
|
||||
return rolePermissionsMap, nil
|
||||
}
|
||||
|
||||
rolePermissionsMap, err := s.RoleStore.ChannelHigherScopedPermissions(roleNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.rolePermissionsCache, cacheKey, rolePermissionsMap)
|
||||
return rolePermissionsMap, nil
|
||||
}
|
||||
79
server/channels/store/localcachelayer/role_layer_test.go
Обычный файл
79
server/channels/store/localcachelayer/role_layer_test.go
Обычный файл
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestRoleStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestRoleStore)
|
||||
}
|
||||
|
||||
func TestRoleStoreCache(t *testing.T) {
|
||||
fakeRole := model.Role{Id: "123", Name: "role-name"}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
role, err := cachedStore.Role().GetByName(context.Background(), "role-name")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, role, &fakeRole)
|
||||
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, role, &fakeRole)
|
||||
cachedStore.Role().GetByName(context.Background(), "role-name")
|
||||
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, save, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Role().GetByName(context.Background(), "role-name")
|
||||
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
cachedStore.Role().Save(&fakeRole)
|
||||
cachedStore.Role().GetByName(context.Background(), "role-name")
|
||||
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Role().GetByName(context.Background(), "role-name")
|
||||
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
cachedStore.Role().Delete("123")
|
||||
cachedStore.Role().GetByName(context.Background(), "role-name")
|
||||
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, permanent delete all, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Role().GetByName(context.Background(), "role-name")
|
||||
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
|
||||
cachedStore.Role().PermanentDeleteAll()
|
||||
cachedStore.Role().GetByName(context.Background(), "role-name")
|
||||
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
|
||||
})
|
||||
}
|
||||
61
server/channels/store/localcachelayer/scheme_layer.go
Обычный файл
61
server/channels/store/localcachelayer/scheme_layer.go
Обычный файл
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type LocalCacheSchemeStore struct {
|
||||
store.SchemeStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheSchemeStore) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.schemeCache.Purge()
|
||||
} else {
|
||||
s.rootStore.schemeCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheSchemeStore) Save(scheme *model.Scheme) (*model.Scheme, error) {
|
||||
if scheme.Id != "" {
|
||||
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.schemeCache, scheme.Id)
|
||||
}
|
||||
return s.SchemeStore.Save(scheme)
|
||||
}
|
||||
|
||||
func (s LocalCacheSchemeStore) Get(schemeId string) (*model.Scheme, error) {
|
||||
var scheme *model.Scheme
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.schemeCache, schemeId, &scheme); err == nil {
|
||||
return scheme, nil
|
||||
}
|
||||
|
||||
scheme, err := s.SchemeStore.Get(schemeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.schemeCache, schemeId, scheme)
|
||||
|
||||
return scheme, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheSchemeStore) Delete(schemeId string) (*model.Scheme, error) {
|
||||
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.schemeCache, schemeId)
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
|
||||
return s.SchemeStore.Delete(schemeId)
|
||||
}
|
||||
|
||||
func (s LocalCacheSchemeStore) PermanentDeleteAll() error {
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.schemeCache)
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.roleCache)
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
|
||||
return s.SchemeStore.PermanentDeleteAll()
|
||||
}
|
||||
78
server/channels/store/localcachelayer/scheme_layer_test.go
Обычный файл
78
server/channels/store/localcachelayer/scheme_layer_test.go
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestSchemeStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestSchemeStore)
|
||||
}
|
||||
|
||||
func TestSchemeStoreCache(t *testing.T) {
|
||||
fakeScheme := model.Scheme{Id: "123", Name: "scheme-name"}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
scheme, err := cachedStore.Scheme().Get("123")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, scheme, &fakeScheme)
|
||||
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, scheme, &fakeScheme)
|
||||
cachedStore.Scheme().Get("123")
|
||||
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, save, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Scheme().Get("123")
|
||||
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Scheme().Save(&fakeScheme)
|
||||
cachedStore.Scheme().Get("123")
|
||||
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, delete, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Scheme().Get("123")
|
||||
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Scheme().Delete("123")
|
||||
cachedStore.Scheme().Get("123")
|
||||
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, permanent delete all, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Scheme().Get("123")
|
||||
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.Scheme().PermanentDeleteAll()
|
||||
cachedStore.Scheme().Get("123")
|
||||
mockStore.Scheme().(*mocks.SchemeStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
}
|
||||
83
server/channels/store/localcachelayer/team_layer.go
Обычный файл
83
server/channels/store/localcachelayer/team_layer.go
Обычный файл
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type LocalCacheTeamStore struct {
|
||||
store.TeamStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheTeamStore) handleClusterInvalidateTeam(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.teamAllTeamIdsForUserCache.Purge()
|
||||
} else {
|
||||
s.rootStore.teamAllTeamIdsForUserCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheTeamStore) ClearCaches() {
|
||||
s.rootStore.teamAllTeamIdsForUserCache.Purge()
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("All Team Ids for User - Purge")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheTeamStore) InvalidateAllTeamIdsForUser(userId string) {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.teamAllTeamIdsForUserCache, userId)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("All Team Ids for User - Remove by UserId")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheTeamStore) GetUserTeamIds(userID string, allowFromCache bool) ([]string, error) {
|
||||
if !allowFromCache {
|
||||
return s.TeamStore.GetUserTeamIds(userID, allowFromCache)
|
||||
}
|
||||
|
||||
var userTeamIds []string
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.teamAllTeamIdsForUserCache, userID, &userTeamIds); err == nil {
|
||||
return userTeamIds, nil
|
||||
}
|
||||
|
||||
userTeamIds, err := s.TeamStore.GetUserTeamIds(userID, allowFromCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(userTeamIds) > 0 {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.teamAllTeamIdsForUserCache, userID, userTeamIds)
|
||||
}
|
||||
|
||||
return userTeamIds, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheTeamStore) Update(team *model.Team) (*model.Team, error) {
|
||||
var oldTeam *model.Team
|
||||
var err error
|
||||
if team.DeleteAt != 0 {
|
||||
oldTeam, err = s.TeamStore.Get(team.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tm, err := s.TeamStore.Update(team)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
|
||||
|
||||
if oldTeam != nil && oldTeam.DeleteAt == 0 {
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.teamAllTeamIdsForUserCache)
|
||||
}
|
||||
|
||||
return tm, err
|
||||
}
|
||||
77
server/channels/store/localcachelayer/team_layer_test.go
Обычный файл
77
server/channels/store/localcachelayer/team_layer_test.go
Обычный файл
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestTeamStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestTeamStore)
|
||||
}
|
||||
|
||||
func TestTeamStoreCache(t *testing.T) {
|
||||
fakeUserId := "123"
|
||||
fakeUserTeamIds := []string{"1", "2", "3"}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUserTeamIds, err := cachedStore.Team().GetUserTeamIds(fakeUserId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
|
||||
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 1)
|
||||
|
||||
gotUserTeamIds, err = cachedStore.Team().GetUserTeamIds(fakeUserId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
|
||||
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUserTeamIds, err := cachedStore.Team().GetUserTeamIds(fakeUserId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
|
||||
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 1)
|
||||
|
||||
gotUserTeamIds, err = cachedStore.Team().GetUserTeamIds(fakeUserId, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
|
||||
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUserTeamIds, err := cachedStore.Team().GetUserTeamIds(fakeUserId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
|
||||
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 1)
|
||||
|
||||
cachedStore.Team().InvalidateAllTeamIdsForUser(fakeUserId)
|
||||
|
||||
gotUserTeamIds, err = cachedStore.Team().GetUserTeamIds(fakeUserId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUserTeamIds, gotUserTeamIds)
|
||||
mockStore.Team().(*mocks.TeamStore).AssertNumberOfCalls(t, "GetUserTeamIds", 2)
|
||||
})
|
||||
|
||||
}
|
||||
83
server/channels/store/localcachelayer/terms_of_service_layer.go
Обычный файл
83
server/channels/store/localcachelayer/terms_of_service_layer.go
Обычный файл
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
const (
|
||||
LatestKey = "latest"
|
||||
)
|
||||
|
||||
type LocalCacheTermsOfServiceStore struct {
|
||||
store.TermsOfServiceStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheTermsOfServiceStore) handleClusterInvalidateTermsOfService(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.termsOfServiceCache.Purge()
|
||||
} else {
|
||||
s.rootStore.termsOfServiceCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheTermsOfServiceStore) ClearCaches() {
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.termsOfServiceCache)
|
||||
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Terms Of Service - Purge")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheTermsOfServiceStore) Save(termsOfService *model.TermsOfService) (*model.TermsOfService, error) {
|
||||
tos, err := s.TermsOfServiceStore.Save(termsOfService)
|
||||
|
||||
if err == nil {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, tos.Id, tos)
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.termsOfServiceCache, LatestKey)
|
||||
}
|
||||
return tos, err
|
||||
}
|
||||
|
||||
func (s LocalCacheTermsOfServiceStore) GetLatest(allowFromCache bool) (*model.TermsOfService, error) {
|
||||
if allowFromCache {
|
||||
if len, err := s.rootStore.termsOfServiceCache.Len(); err == nil && len != 0 {
|
||||
var cacheItem *model.TermsOfService
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.termsOfServiceCache, LatestKey, &cacheItem); err == nil {
|
||||
return cacheItem, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
termsOfService, err := s.TermsOfServiceStore.GetLatest(allowFromCache)
|
||||
|
||||
if allowFromCache && err == nil {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, termsOfService.Id, termsOfService)
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, LatestKey, termsOfService)
|
||||
}
|
||||
|
||||
return termsOfService, err
|
||||
}
|
||||
|
||||
func (s LocalCacheTermsOfServiceStore) Get(id string, allowFromCache bool) (*model.TermsOfService, error) {
|
||||
if allowFromCache {
|
||||
var cacheItem *model.TermsOfService
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.termsOfServiceCache, id, &cacheItem); err == nil {
|
||||
return cacheItem, nil
|
||||
}
|
||||
}
|
||||
|
||||
termsOfService, err := s.TermsOfServiceStore.Get(id, allowFromCache)
|
||||
|
||||
if allowFromCache && err == nil {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.termsOfServiceCache, termsOfService.Id, termsOfService)
|
||||
}
|
||||
|
||||
return termsOfService, err
|
||||
}
|
||||
146
server/channels/store/localcachelayer/terms_of_service_layer_test.go
Обычный файл
146
server/channels/store/localcachelayer/terms_of_service_layer_test.go
Обычный файл
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestTermsOfServiceStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestTermsOfServiceStore)
|
||||
}
|
||||
|
||||
func TestTermsOfServiceStoreTermsOfServiceCache(t *testing.T) {
|
||||
|
||||
fakeTermsOfService := model.TermsOfService{Id: "123", CreateAt: 11111, UserId: "321", Text: "Terms of service test"}
|
||||
|
||||
t.Run("first call by latest not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
termsOfService, err := cachedStore.TermsOfService().GetLatest(true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, termsOfService, &fakeTermsOfService)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
|
||||
termsOfService, err = cachedStore.TermsOfService().GetLatest(true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, termsOfService, &fakeTermsOfService)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
|
||||
})
|
||||
|
||||
t.Run("first call by id not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
termsOfService, err := cachedStore.TermsOfService().Get("123", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, termsOfService, &fakeTermsOfService)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
termsOfService, err = cachedStore.TermsOfService().Get("123", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, termsOfService, &fakeTermsOfService)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
})
|
||||
|
||||
t.Run("first call by id not cached, second force no cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.TermsOfService().Get("123", true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.TermsOfService().Get("123", false)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call latest not cached, second force no cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.TermsOfService().GetLatest(true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
|
||||
cachedStore.TermsOfService().GetLatest(false)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 2)
|
||||
})
|
||||
|
||||
t.Run("first call by id force no cached, second not cached, third cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.TermsOfService().Get("123", false)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.TermsOfService().Get("123", true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
cachedStore.TermsOfService().Get("123", true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first call latest force no cached, second not cached, third cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.TermsOfService().GetLatest(false)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
|
||||
cachedStore.TermsOfService().GetLatest(true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 2)
|
||||
cachedStore.TermsOfService().GetLatest(true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 2)
|
||||
})
|
||||
|
||||
t.Run("first call latest, second call by id cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.TermsOfService().GetLatest(true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
|
||||
cachedStore.TermsOfService().Get("123", true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 0)
|
||||
})
|
||||
|
||||
t.Run("first call by id not cached, save, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.TermsOfService().Get("123", false)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
cachedStore.TermsOfService().Save(&fakeTermsOfService)
|
||||
cachedStore.TermsOfService().Get("123", false)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("first get latest not cached, save new, then get latest, returning different data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.TermsOfService().GetLatest(true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 1)
|
||||
cachedStore.TermsOfService().Save(&fakeTermsOfService)
|
||||
cachedStore.TermsOfService().GetLatest(true)
|
||||
mockStore.TermsOfService().(*mocks.TermsOfServiceStore).AssertNumberOfCalls(t, "GetLatest", 2)
|
||||
})
|
||||
}
|
||||
263
server/channels/store/localcachelayer/user_layer.go
Обычный файл
263
server/channels/store/localcachelayer/user_layer.go
Обычный файл
@@ -0,0 +1,263 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
|
||||
)
|
||||
|
||||
type LocalCacheUserStore struct {
|
||||
store.UserStore
|
||||
rootStore *LocalCacheStore
|
||||
userProfileByIdsMut sync.Mutex
|
||||
userProfileByIdsInvalidations map[string]bool
|
||||
}
|
||||
|
||||
func (s *LocalCacheUserStore) handleClusterInvalidateScheme(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.userProfileByIdsCache.Purge()
|
||||
} else {
|
||||
s.userProfileByIdsMut.Lock()
|
||||
s.userProfileByIdsInvalidations[string(msg.Data)] = true
|
||||
s.userProfileByIdsMut.Unlock()
|
||||
s.rootStore.userProfileByIdsCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheUserStore) handleClusterInvalidateProfilesInChannel(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.profilesInChannelCache.Purge()
|
||||
} else {
|
||||
s.rootStore.profilesInChannelCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheUserStore) ClearCaches() {
|
||||
s.rootStore.userProfileByIdsCache.Purge()
|
||||
s.rootStore.profilesInChannelCache.Purge()
|
||||
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Purge")
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Purge")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheUserStore) InvalidateProfileCacheForUser(userId string) {
|
||||
s.userProfileByIdsMut.Lock()
|
||||
s.userProfileByIdsInvalidations[userId] = true
|
||||
s.userProfileByIdsMut.Unlock()
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.userProfileByIdsCache, userId)
|
||||
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profile By Ids - Remove")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheUserStore) InvalidateProfilesInChannelCacheByUser(userId string) {
|
||||
keys, err := s.rootStore.profilesInChannelCache.Keys()
|
||||
if err == nil {
|
||||
for _, key := range keys {
|
||||
var userMap map[string]*model.User
|
||||
if err = s.rootStore.profilesInChannelCache.Get(key, &userMap); err == nil {
|
||||
if _, userInCache := userMap[userId]; userInCache {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.profilesInChannelCache, key)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by User")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheUserStore) InvalidateProfilesInChannelCache(channelID string) {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.profilesInChannelCache, channelID)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Profiles in Channel - Remove by Channel")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalCacheUserStore) GetAllProfilesInChannel(ctx context.Context, channelId string, allowFromCache bool) (map[string]*model.User, error) {
|
||||
if allowFromCache {
|
||||
var cachedMap map[string]*model.User
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.profilesInChannelCache, channelId, &cachedMap); err == nil {
|
||||
return cachedMap, nil
|
||||
}
|
||||
}
|
||||
|
||||
userMap, err := s.UserStore.GetAllProfilesInChannel(ctx, channelId, allowFromCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if allowFromCache {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.profilesInChannelCache, channelId, model.UserMap(userMap))
|
||||
}
|
||||
|
||||
return userMap, nil
|
||||
}
|
||||
|
||||
func (s *LocalCacheUserStore) GetProfileByIds(ctx context.Context, userIds []string, options *store.UserGetByIdsOpts, allowFromCache bool) ([]*model.User, error) {
|
||||
if !allowFromCache {
|
||||
return s.UserStore.GetProfileByIds(ctx, userIds, options, false)
|
||||
}
|
||||
|
||||
if options == nil {
|
||||
options = &store.UserGetByIdsOpts{}
|
||||
}
|
||||
|
||||
users := []*model.User{}
|
||||
remainingUserIds := make([]string, 0)
|
||||
|
||||
fromMaster := false
|
||||
for _, userId := range userIds {
|
||||
var cacheItem *model.User
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, userId, &cacheItem); err == nil {
|
||||
if options.Since == 0 || cacheItem.UpdateAt > options.Since {
|
||||
users = append(users, cacheItem)
|
||||
}
|
||||
} else {
|
||||
// If it was invalidated, then we need to query master.
|
||||
s.userProfileByIdsMut.Lock()
|
||||
if s.userProfileByIdsInvalidations[userId] {
|
||||
fromMaster = true
|
||||
// And then remove the key from the map.
|
||||
delete(s.userProfileByIdsInvalidations, userId)
|
||||
}
|
||||
s.userProfileByIdsMut.Unlock()
|
||||
remainingUserIds = append(remainingUserIds, userId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(remainingUserIds) > 0 {
|
||||
if fromMaster {
|
||||
ctx = sqlstore.WithMaster(ctx)
|
||||
}
|
||||
remainingUsers, err := s.UserStore.GetProfileByIds(ctx, remainingUserIds, options, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range remainingUsers {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, user.Id, user)
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Get is a cache wrapper around the SqlStore method to get a user profile by id.
|
||||
// It checks if the user entry is present in the cache, returning the entry from cache
|
||||
// if it is present. Otherwise, it fetches the entry from the store and stores it in the
|
||||
// cache.
|
||||
func (s *LocalCacheUserStore) Get(ctx context.Context, id string) (*model.User, error) {
|
||||
var cacheItem *model.User
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, id, &cacheItem); err == nil {
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.AddMemCacheHitCounter("Profile By Id", float64(1))
|
||||
}
|
||||
return cacheItem, nil
|
||||
}
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.AddMemCacheMissCounter("Profile By Id", float64(1))
|
||||
}
|
||||
|
||||
// If it was invalidated, then we need to query master.
|
||||
s.userProfileByIdsMut.Lock()
|
||||
if s.userProfileByIdsInvalidations[id] {
|
||||
ctx = sqlstore.WithMaster(ctx)
|
||||
// And then remove the key from the map.
|
||||
delete(s.userProfileByIdsInvalidations, id)
|
||||
}
|
||||
s.userProfileByIdsMut.Unlock()
|
||||
|
||||
user, err := s.UserStore.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, id, user)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetMany is a cache wrapper around the SqlStore method to get a user profiles by ids.
|
||||
// It checks if the user entries are present in the cache, returning the entries from cache
|
||||
// if it is present. Otherwise, it fetches the entries from the store and stores it in the
|
||||
// cache.
|
||||
func (s *LocalCacheUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
|
||||
// we are doing a loop instead of caching the full set in the cache because the number of permutations that we can have
|
||||
// in this func is making caching of the total set not beneficial.
|
||||
var cachedUsers []*model.User
|
||||
var notCachedUserIds []string
|
||||
uniqIDs := dedup(ids)
|
||||
|
||||
fromMaster := false
|
||||
for _, id := range uniqIDs {
|
||||
var cachedUser *model.User
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.userProfileByIdsCache, id, &cachedUser); err == nil {
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.AddMemCacheHitCounter("Profile By Id", float64(1))
|
||||
}
|
||||
cachedUsers = append(cachedUsers, cachedUser)
|
||||
} else {
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.AddMemCacheMissCounter("Profile By Id", float64(1))
|
||||
}
|
||||
// If it was invalidated, then we need to query master.
|
||||
s.userProfileByIdsMut.Lock()
|
||||
if s.userProfileByIdsInvalidations[id] {
|
||||
fromMaster = true
|
||||
// And then remove the key from the map.
|
||||
delete(s.userProfileByIdsInvalidations, id)
|
||||
}
|
||||
s.userProfileByIdsMut.Unlock()
|
||||
|
||||
notCachedUserIds = append(notCachedUserIds, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(notCachedUserIds) > 0 {
|
||||
if fromMaster {
|
||||
ctx = sqlstore.WithMaster(ctx)
|
||||
}
|
||||
dbUsers, err := s.UserStore.GetMany(ctx, notCachedUserIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range dbUsers {
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.userProfileByIdsCache, user.Id, user)
|
||||
cachedUsers = append(cachedUsers, user)
|
||||
}
|
||||
}
|
||||
|
||||
return cachedUsers, nil
|
||||
}
|
||||
|
||||
func dedup(elements []string) []string {
|
||||
if len(elements) == 0 {
|
||||
return elements
|
||||
}
|
||||
|
||||
sort.Strings(elements)
|
||||
|
||||
j := 0
|
||||
for i := 1; i < len(elements); i++ {
|
||||
if elements[j] == elements[i] {
|
||||
continue
|
||||
}
|
||||
j++
|
||||
// preserve the original data
|
||||
// in[i], in[j] = in[j], in[i]
|
||||
// only set what is required
|
||||
elements[j] = elements[i]
|
||||
}
|
||||
|
||||
return elements[:j+1]
|
||||
}
|
||||
313
server/channels/store/localcachelayer/user_layer_test.go
Обычный файл
313
server/channels/store/localcachelayer/user_layer_test.go
Обычный файл
@@ -0,0 +1,313 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestUserStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestUserStore)
|
||||
}
|
||||
|
||||
func TestUserStoreCache(t *testing.T) {
|
||||
fakeUserIds := []string{"123"}
|
||||
fakeUser := []*model.User{{
|
||||
Id: "123",
|
||||
AuthData: model.NewString("authData"),
|
||||
AuthService: "authService",
|
||||
}}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUser, err := cachedStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUser, gotUser)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 1)
|
||||
|
||||
_, _ = cachedStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, true)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUser, err := cachedStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUser, gotUser)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 1)
|
||||
|
||||
_, _ = cachedStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, false)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUser, err := cachedStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUser, gotUser)
|
||||
|
||||
cachedStore.User().InvalidateProfileCacheForUser("123")
|
||||
|
||||
_, _ = cachedStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, true)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetProfileByIds", 2)
|
||||
})
|
||||
|
||||
t.Run("should always return a copy of the stored data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
storedUsers, err := mockStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
originalProps := make([]model.StringMap, len(storedUsers))
|
||||
|
||||
for i := 0; i < len(storedUsers); i++ {
|
||||
originalProps[i] = storedUsers[i].NotifyProps
|
||||
storedUsers[i].NotifyProps = map[string]string{}
|
||||
storedUsers[i].NotifyProps["key"] = "somevalue"
|
||||
}
|
||||
|
||||
cachedUsers, err := cachedStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := 0; i < len(storedUsers); i++ {
|
||||
assert.Equal(t, storedUsers[i].Id, cachedUsers[i].Id)
|
||||
}
|
||||
|
||||
cachedUsers, err = cachedStore.User().GetProfileByIds(context.Background(), fakeUserIds, &store.UserGetByIdsOpts{}, true)
|
||||
require.NoError(t, err)
|
||||
for i := 0; i < len(storedUsers); i++ {
|
||||
storedUsers[i].Props = model.StringMap{}
|
||||
storedUsers[i].Timezone = model.StringMap{}
|
||||
assert.Equal(t, storedUsers[i], cachedUsers[i])
|
||||
if storedUsers[i] == cachedUsers[i] {
|
||||
assert.Fail(t, "should be different pointers")
|
||||
}
|
||||
cachedUsers[i].NotifyProps["key"] = "othervalue"
|
||||
assert.NotEqual(t, storedUsers[i], cachedUsers[i])
|
||||
}
|
||||
|
||||
for i := 0; i < len(storedUsers); i++ {
|
||||
storedUsers[i].NotifyProps = originalProps[i]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserStoreProfilesInChannelCache(t *testing.T) {
|
||||
fakeChannelId := "123"
|
||||
fakeUserId := "456"
|
||||
fakeMap := map[string]*model.User{
|
||||
fakeUserId: {Id: "456"},
|
||||
}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotMap, err := cachedStore.User().GetAllProfilesInChannel(context.Background(), fakeChannelId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeMap, gotMap)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
|
||||
|
||||
_, _ = cachedStore.User().GetAllProfilesInChannel(context.Background(), fakeChannelId, true)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotMap, err := cachedStore.User().GetAllProfilesInChannel(context.Background(), fakeChannelId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeMap, gotMap)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
|
||||
|
||||
_, _ = cachedStore.User().GetAllProfilesInChannel(context.Background(), fakeChannelId, false)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate by channel, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotMap, err := cachedStore.User().GetAllProfilesInChannel(context.Background(), fakeChannelId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeMap, gotMap)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
|
||||
|
||||
cachedStore.User().InvalidateProfilesInChannelCache("123")
|
||||
|
||||
_, _ = cachedStore.User().GetAllProfilesInChannel(context.Background(), fakeChannelId, true)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate by user, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotMap, err := cachedStore.User().GetAllProfilesInChannel(context.Background(), fakeChannelId, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeMap, gotMap)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 1)
|
||||
|
||||
cachedStore.User().InvalidateProfilesInChannelCacheByUser("456")
|
||||
|
||||
_, _ = cachedStore.User().GetAllProfilesInChannel(context.Background(), fakeChannelId, true)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetAllProfilesInChannel", 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserStoreGetCache(t *testing.T) {
|
||||
fakeUserId := "123"
|
||||
fakeUser := &model.User{
|
||||
Id: "123",
|
||||
AuthData: model.NewString("authData"),
|
||||
AuthService: "authService",
|
||||
}
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUser, err := cachedStore.User().Get(context.Background(), fakeUserId)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUser, gotUser)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
|
||||
_, _ = cachedStore.User().Get(context.Background(), fakeUserId)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUser, err := cachedStore.User().Get(context.Background(), fakeUserId)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fakeUser, gotUser)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "Get", 1)
|
||||
|
||||
cachedStore.User().InvalidateProfileCacheForUser("123")
|
||||
|
||||
_, _ = cachedStore.User().Get(context.Background(), fakeUserId)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "Get", 2)
|
||||
})
|
||||
|
||||
t.Run("should always return a copy of the stored data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
storedUser, err := mockStore.User().Get(context.Background(), fakeUserId)
|
||||
require.NoError(t, err)
|
||||
originalProps := storedUser.NotifyProps
|
||||
|
||||
storedUser.NotifyProps = map[string]string{}
|
||||
storedUser.NotifyProps["key"] = "somevalue"
|
||||
|
||||
cachedUser, err := cachedStore.User().Get(context.Background(), fakeUserId)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, storedUser, cachedUser)
|
||||
|
||||
storedUser.Props = model.StringMap{}
|
||||
storedUser.Timezone = model.StringMap{}
|
||||
cachedUser, err = cachedStore.User().Get(context.Background(), fakeUserId)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, storedUser, cachedUser)
|
||||
if storedUser == cachedUser {
|
||||
assert.Fail(t, "should be different pointers")
|
||||
}
|
||||
cachedUser.NotifyProps["key"] = "othervalue"
|
||||
assert.NotEqual(t, storedUser, cachedUser)
|
||||
|
||||
storedUser.NotifyProps = originalProps
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserStoreGetManyCache(t *testing.T) {
|
||||
fakeUser := &model.User{
|
||||
Id: "123",
|
||||
AuthData: model.NewString("authData"),
|
||||
AuthService: "authService",
|
||||
}
|
||||
otherFakeUser := &model.User{
|
||||
Id: "456",
|
||||
AuthData: model.NewString("authData"),
|
||||
AuthService: "authService",
|
||||
}
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUsers, err := cachedStore.User().GetMany(context.Background(), []string{fakeUser.Id, otherFakeUser.Id})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, gotUsers, 2)
|
||||
assert.Contains(t, gotUsers, fakeUser)
|
||||
assert.Contains(t, gotUsers, otherFakeUser)
|
||||
|
||||
gotUsers, err = cachedStore.User().GetMany(context.Background(), []string{fakeUser.Id, otherFakeUser.Id})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, gotUsers, 2)
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetMany", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate one user, and then check that one is cached and one is fetched from db", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotUsers, err := cachedStore.User().GetMany(context.Background(), []string{fakeUser.Id, otherFakeUser.Id})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, gotUsers, 2)
|
||||
assert.Contains(t, gotUsers, fakeUser)
|
||||
assert.Contains(t, gotUsers, otherFakeUser)
|
||||
|
||||
cachedStore.User().InvalidateProfileCacheForUser("123")
|
||||
|
||||
gotUsers, err = cachedStore.User().GetMany(context.Background(), []string{fakeUser.Id, otherFakeUser.Id})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, gotUsers, 2)
|
||||
mockStore.User().(*mocks.UserStore).AssertCalled(t, "GetMany", mock.Anything, []string{"123"})
|
||||
mockStore.User().(*mocks.UserStore).AssertNumberOfCalls(t, "GetMany", 2)
|
||||
})
|
||||
}
|
||||
89
server/channels/store/localcachelayer/webhook_layer.go
Обычный файл
89
server/channels/store/localcachelayer/webhook_layer.go
Обычный файл
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type LocalCacheWebhookStore struct {
|
||||
store.WebhookStore
|
||||
rootStore *LocalCacheStore
|
||||
}
|
||||
|
||||
func (s *LocalCacheWebhookStore) handleClusterInvalidateWebhook(msg *model.ClusterMessage) {
|
||||
if bytes.Equal(msg.Data, clearCacheMessageData) {
|
||||
s.rootStore.webhookCache.Purge()
|
||||
} else {
|
||||
s.rootStore.webhookCache.Remove(string(msg.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheWebhookStore) ClearCaches() {
|
||||
s.rootStore.doClearCacheCluster(s.rootStore.webhookCache)
|
||||
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Webhook - Purge")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheWebhookStore) InvalidateWebhookCache(webhookId string) {
|
||||
s.rootStore.doInvalidateCacheCluster(s.rootStore.webhookCache, webhookId)
|
||||
if s.rootStore.metrics != nil {
|
||||
s.rootStore.metrics.IncrementMemCacheInvalidationCounter("Webhook - Remove by WebhookId")
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalCacheWebhookStore) GetIncoming(id string, allowFromCache bool) (*model.IncomingWebhook, error) {
|
||||
if !allowFromCache {
|
||||
return s.WebhookStore.GetIncoming(id, allowFromCache)
|
||||
}
|
||||
|
||||
var incomingWebhook *model.IncomingWebhook
|
||||
if err := s.rootStore.doStandardReadCache(s.rootStore.webhookCache, id, &incomingWebhook); err == nil {
|
||||
return incomingWebhook, nil
|
||||
}
|
||||
|
||||
incomingWebhook, err := s.WebhookStore.GetIncoming(id, allowFromCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.rootStore.doStandardAddToCache(s.rootStore.webhookCache, id, incomingWebhook)
|
||||
|
||||
return incomingWebhook, nil
|
||||
}
|
||||
|
||||
func (s LocalCacheWebhookStore) DeleteIncoming(webhookId string, time int64) error {
|
||||
err := s.WebhookStore.DeleteIncoming(webhookId, time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.InvalidateWebhookCache(webhookId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s LocalCacheWebhookStore) PermanentDeleteIncomingByUser(userId string) error {
|
||||
err := s.WebhookStore.PermanentDeleteIncomingByUser(userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.ClearCaches()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s LocalCacheWebhookStore) PermanentDeleteIncomingByChannel(channelId string) error {
|
||||
err := s.WebhookStore.PermanentDeleteIncomingByChannel(channelId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.ClearCaches()
|
||||
return nil
|
||||
}
|
||||
64
server/channels/store/localcachelayer/webhook_layer_test.go
Обычный файл
64
server/channels/store/localcachelayer/webhook_layer_test.go
Обычный файл
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package localcachelayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func TestWebhookStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestWebhookStore)
|
||||
}
|
||||
|
||||
func TestWebhookStoreCache(t *testing.T) {
|
||||
fakeWebhook := model.IncomingWebhook{Id: "123"}
|
||||
|
||||
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
incomingWebhook, err := cachedStore.Webhook().GetIncoming("123", true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, incomingWebhook, &fakeWebhook)
|
||||
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 1)
|
||||
|
||||
assert.Equal(t, incomingWebhook, &fakeWebhook)
|
||||
cachedStore.Webhook().GetIncoming("123", true)
|
||||
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 1)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, second force not cached", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Webhook().GetIncoming("123", true)
|
||||
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 1)
|
||||
cachedStore.Webhook().GetIncoming("123", false)
|
||||
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 2)
|
||||
})
|
||||
|
||||
t.Run("first call not cached, invalidate, and then not cached again", func(t *testing.T) {
|
||||
mockStore := getMockStore()
|
||||
mockCacheProvider := getMockCacheProvider()
|
||||
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||
require.NoError(t, err)
|
||||
|
||||
cachedStore.Webhook().GetIncoming("123", true)
|
||||
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 1)
|
||||
cachedStore.Webhook().InvalidateWebhookCache("123")
|
||||
cachedStore.Webhook().GetIncoming("123", true)
|
||||
mockStore.Webhook().(*mocks.WebhookStore).AssertNumberOfCalls(t, "GetIncoming", 2)
|
||||
})
|
||||
}
|
||||
12968
server/channels/store/opentracinglayer/opentracinglayer.go
Обычный файл
12968
server/channels/store/opentracinglayer/opentracinglayer.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
14776
server/channels/store/retrylayer/retrylayer.go
Обычный файл
14776
server/channels/store/retrylayer/retrylayer.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
129
server/channels/store/retrylayer/retrylayer_test.go
Обычный файл
129
server/channels/store/retrylayer/retrylayer_test.go
Обычный файл
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package retrylayer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/lib/pq"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
)
|
||||
|
||||
func genStore() *mocks.Store {
|
||||
mock := &mocks.Store{}
|
||||
mock.On("Audit").Return(&mocks.AuditStore{})
|
||||
mock.On("Bot").Return(&mocks.BotStore{})
|
||||
mock.On("Channel").Return(&mocks.ChannelStore{})
|
||||
mock.On("ChannelMemberHistory").Return(&mocks.ChannelMemberHistoryStore{})
|
||||
mock.On("ClusterDiscovery").Return(&mocks.ClusterDiscoveryStore{})
|
||||
mock.On("RemoteCluster").Return(&mocks.RemoteClusterStore{})
|
||||
mock.On("Command").Return(&mocks.CommandStore{})
|
||||
mock.On("CommandWebhook").Return(&mocks.CommandWebhookStore{})
|
||||
mock.On("Compliance").Return(&mocks.ComplianceStore{})
|
||||
mock.On("Emoji").Return(&mocks.EmojiStore{})
|
||||
mock.On("FileInfo").Return(&mocks.FileInfoStore{})
|
||||
mock.On("UploadSession").Return(&mocks.UploadSessionStore{})
|
||||
mock.On("Group").Return(&mocks.GroupStore{})
|
||||
mock.On("Job").Return(&mocks.JobStore{})
|
||||
mock.On("License").Return(&mocks.LicenseStore{})
|
||||
mock.On("LinkMetadata").Return(&mocks.LinkMetadataStore{})
|
||||
mock.On("SharedChannel").Return(&mocks.SharedChannelStore{})
|
||||
mock.On("OAuth").Return(&mocks.OAuthStore{})
|
||||
mock.On("Plugin").Return(&mocks.PluginStore{})
|
||||
mock.On("Post").Return(&mocks.PostStore{})
|
||||
mock.On("Thread").Return(&mocks.ThreadStore{})
|
||||
mock.On("Preference").Return(&mocks.PreferenceStore{})
|
||||
mock.On("ProductNotices").Return(&mocks.ProductNoticesStore{})
|
||||
mock.On("Reaction").Return(&mocks.ReactionStore{})
|
||||
mock.On("RetentionPolicy").Return(&mocks.RetentionPolicyStore{})
|
||||
mock.On("Role").Return(&mocks.RoleStore{})
|
||||
mock.On("Scheme").Return(&mocks.SchemeStore{})
|
||||
mock.On("Session").Return(&mocks.SessionStore{})
|
||||
mock.On("Status").Return(&mocks.StatusStore{})
|
||||
mock.On("System").Return(&mocks.SystemStore{})
|
||||
mock.On("Team").Return(&mocks.TeamStore{})
|
||||
mock.On("TermsOfService").Return(&mocks.TermsOfServiceStore{})
|
||||
mock.On("Token").Return(&mocks.TokenStore{})
|
||||
mock.On("User").Return(&mocks.UserStore{})
|
||||
mock.On("UserAccessToken").Return(&mocks.UserAccessTokenStore{})
|
||||
mock.On("UserTermsOfService").Return(&mocks.UserTermsOfServiceStore{})
|
||||
mock.On("Webhook").Return(&mocks.WebhookStore{})
|
||||
mock.On("NotifyAdmin").Return(&mocks.NotifyAdminStore{})
|
||||
mock.On("Draft").Return(&mocks.DraftStore{})
|
||||
mock.On("PostPriority").Return(&mocks.PostPriorityStore{})
|
||||
mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{})
|
||||
mock.On("TrueUpReview").Return(&mocks.TrueUpReviewStore{})
|
||||
return mock
|
||||
}
|
||||
|
||||
func TestRetry(t *testing.T) {
|
||||
t.Run("on regular error should not retry", func(t *testing.T) {
|
||||
mock := genStore()
|
||||
mockBotStore := mock.Bot().(*mocks.BotStore)
|
||||
mockBotStore.On("Get", "test", false).Return(nil, errors.New("regular error")).Times(1)
|
||||
mock.On("Bot").Return(&mockBotStore)
|
||||
layer := New(mock)
|
||||
layer.Bot().Get("test", false)
|
||||
mockBotStore.AssertExpectations(t)
|
||||
})
|
||||
t.Run("on success should not retry", func(t *testing.T) {
|
||||
mock := genStore()
|
||||
mockBotStore := mock.Bot().(*mocks.BotStore)
|
||||
mockBotStore.On("Get", "test", false).Return(&model.Bot{}, nil).Times(1)
|
||||
mock.On("Bot").Return(&mockBotStore)
|
||||
layer := New(mock)
|
||||
layer.Bot().Get("test", false)
|
||||
mockBotStore.AssertExpectations(t)
|
||||
})
|
||||
t.Run("on mysql repeatable error should retry", func(t *testing.T) {
|
||||
mock := genStore()
|
||||
mockBotStore := mock.Bot().(*mocks.BotStore)
|
||||
mysqlErr := mysql.MySQLError{Number: uint16(1213), Message: "Deadlock"}
|
||||
mockBotStore.On("Get", "test", false).Return(nil, errors.Wrap(&mysqlErr, "test-error")).Times(3)
|
||||
mock.On("Bot").Return(&mockBotStore)
|
||||
layer := New(mock)
|
||||
layer.Bot().Get("test", false)
|
||||
mockBotStore.AssertExpectations(t)
|
||||
})
|
||||
t.Run("on mysql not repeatable error should not retry", func(t *testing.T) {
|
||||
mock := genStore()
|
||||
mockBotStore := mock.Bot().(*mocks.BotStore)
|
||||
mysqlErr := mysql.MySQLError{Number: uint16(1000), Message: "Not repeatable error"}
|
||||
mockBotStore.On("Get", "test", false).Return(nil, errors.Wrap(&mysqlErr, "test-error")).Times(1)
|
||||
mock.On("Bot").Return(&mockBotStore)
|
||||
layer := New(mock)
|
||||
layer.Bot().Get("test", false)
|
||||
mockBotStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("on postgres repeatable error should retry", func(t *testing.T) {
|
||||
for _, errCode := range []string{"40001", "40P01"} {
|
||||
t.Run("error "+errCode, func(t *testing.T) {
|
||||
mock := genStore()
|
||||
mockBotStore := mock.Bot().(*mocks.BotStore)
|
||||
pqErr := pq.Error{Code: pq.ErrorCode(errCode)}
|
||||
mockBotStore.On("Get", "test", false).Return(nil, errors.Wrap(&pqErr, "test-error")).Times(3)
|
||||
mock.On("Bot").Return(&mockBotStore)
|
||||
layer := New(mock)
|
||||
layer.Bot().Get("test", false)
|
||||
mockBotStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("on postgres not repeatable error should not retry", func(t *testing.T) {
|
||||
mock := genStore()
|
||||
mockBotStore := mock.Bot().(*mocks.BotStore)
|
||||
pqErr := pq.Error{Code: "20000"}
|
||||
mockBotStore.On("Get", "test", false).Return(nil, errors.Wrap(&pqErr, "test-error")).Times(1)
|
||||
mock.On("Bot").Return(&mockBotStore)
|
||||
layer := New(mock)
|
||||
layer.Bot().Get("test", false)
|
||||
mockBotStore.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
327
server/channels/store/searchlayer/channel_layer.go
Обычный файл
327
server/channels/store/searchlayer/channel_layer.go
Обычный файл
@@ -0,0 +1,327 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SearchChannelStore struct {
|
||||
store.ChannelStore
|
||||
rootStore *SearchStore
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) deleteChannelIndex(channel *model.Channel) {
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeleteChannel(channel); err != nil {
|
||||
mlog.Warn("Encountered error deleting channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Removed channel from index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) indexChannel(channel *model.Channel) {
|
||||
var userIDs, teamMemberIDs []string
|
||||
var err error
|
||||
if channel.Type == model.ChannelTypePrivate {
|
||||
userIDs, err = c.GetAllChannelMembersById(channel.Id)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error while indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
teamMemberIDs, err = c.GetTeamMembersForChannel(channel.Id)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error while indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.IndexChannel(channel, userIDs, teamMemberIDs); err != nil {
|
||||
mlog.Warn("Encountered error indexing channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Indexed channel in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) Save(channel *model.Channel, maxChannels int64) (*model.Channel, error) {
|
||||
newChannel, err := c.ChannelStore.Save(channel, maxChannels)
|
||||
if err == nil {
|
||||
c.indexChannel(newChannel)
|
||||
}
|
||||
return newChannel, err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
|
||||
updatedChannel, err := c.ChannelStore.Update(channel)
|
||||
if err == nil {
|
||||
c.indexChannel(updatedChannel)
|
||||
}
|
||||
return updatedChannel, err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) UpdateMember(cm *model.ChannelMember) (*model.ChannelMember, error) {
|
||||
member, err := c.ChannelStore.UpdateMember(cm)
|
||||
if err == nil {
|
||||
c.rootStore.indexUserFromID(cm.UserId)
|
||||
channel, channelErr := c.ChannelStore.Get(member.ChannelId, true)
|
||||
if channelErr != nil {
|
||||
mlog.Warn("Encountered error indexing user in channel", mlog.String("channel_id", member.ChannelId), mlog.Err(channelErr))
|
||||
} else {
|
||||
c.indexChannel(channel)
|
||||
c.rootStore.indexUserFromID(channel.CreatorId)
|
||||
}
|
||||
}
|
||||
return member, err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) SaveMember(cm *model.ChannelMember) (*model.ChannelMember, error) {
|
||||
member, err := c.ChannelStore.SaveMember(cm)
|
||||
if err == nil {
|
||||
c.rootStore.indexUserFromID(cm.UserId)
|
||||
channel, channelErr := c.ChannelStore.Get(member.ChannelId, true)
|
||||
if channelErr != nil {
|
||||
mlog.Warn("Encountered error indexing user in channel", mlog.String("channel_id", member.ChannelId), mlog.Err(channelErr))
|
||||
} else {
|
||||
c.indexChannel(channel)
|
||||
c.rootStore.indexUserFromID(channel.CreatorId)
|
||||
}
|
||||
}
|
||||
return member, err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) RemoveMember(channelID, userIdToRemove string) error {
|
||||
err := c.ChannelStore.RemoveMember(channelID, userIdToRemove)
|
||||
if err == nil {
|
||||
c.rootStore.indexUserFromID(userIdToRemove)
|
||||
}
|
||||
|
||||
channel, err := c.ChannelStore.Get(channelID, true)
|
||||
if err == nil {
|
||||
c.indexChannel(channel)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) RemoveMembers(channelID string, userIds []string) error {
|
||||
if err := c.ChannelStore.RemoveMembers(channelID, userIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channel, err := c.ChannelStore.Get(channelID, true)
|
||||
if err == nil {
|
||||
c.indexChannel(channel)
|
||||
}
|
||||
|
||||
for _, uid := range userIds {
|
||||
c.rootStore.indexUserFromID(uid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) CreateDirectChannel(user *model.User, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, error) {
|
||||
channel, err := c.ChannelStore.CreateDirectChannel(user, otherUser, channelOptions...)
|
||||
if err == nil {
|
||||
c.rootStore.indexUserFromID(user.Id)
|
||||
c.rootStore.indexUserFromID(otherUser.Id)
|
||||
c.indexChannel(channel)
|
||||
}
|
||||
return channel, err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) SaveDirectChannel(directchannel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
|
||||
channel, err := c.ChannelStore.SaveDirectChannel(directchannel, member1, member2)
|
||||
if err == nil {
|
||||
c.rootStore.indexUserFromID(member1.UserId)
|
||||
c.rootStore.indexUserFromID(member2.UserId)
|
||||
c.indexChannel(channel)
|
||||
}
|
||||
return channel, err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) Autocomplete(userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
|
||||
var channelList model.ChannelListWithTeamData
|
||||
var err error
|
||||
|
||||
allFailed := true
|
||||
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsAutocompletionEnabled() {
|
||||
channelList, err = c.searchAutocompleteChannelsAllTeams(engine, userID, term, includeDeleted, isGuest)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error on AutocompleteChannels through SearchEngine. Falling back to default autocompletion.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
allFailed = false
|
||||
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allFailed {
|
||||
mlog.Debug("Using database search because no other search engine is available")
|
||||
channelList, err = c.ChannelStore.Autocomplete(userID, term, includeDeleted, isGuest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to autocomplete channels in team")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return channelList, err
|
||||
}
|
||||
|
||||
return channelList, nil
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) AutocompleteInTeam(teamID, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
|
||||
var channelList model.ChannelList
|
||||
var err error
|
||||
|
||||
allFailed := true
|
||||
for _, engine := range c.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsAutocompletionEnabled() {
|
||||
channelList, err = c.searchAutocompleteChannels(engine, teamID, userID, term, includeDeleted, isGuest)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error on AutocompleteChannels through SearchEngine. Falling back to default autocompletion.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
allFailed = false
|
||||
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allFailed {
|
||||
mlog.Debug("Using database search because no other search engine is available")
|
||||
channelList, err = c.ChannelStore.AutocompleteInTeam(teamID, userID, term, includeDeleted, isGuest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to autocomplete channels in team")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return channelList, err
|
||||
}
|
||||
|
||||
return channelList, nil
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) searchAutocompleteChannels(engine searchengine.SearchEngineInterface, teamId, userID, term string, includeDeleted, isGuest bool) (model.ChannelList, error) {
|
||||
channelIds, err := engine.SearchChannels(teamId, userID, term, isGuest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channelList := model.ChannelList{}
|
||||
var nErr error
|
||||
if len(channelIds) > 0 {
|
||||
channelList, nErr = c.ChannelStore.GetChannelsByIds(channelIds, includeDeleted)
|
||||
if nErr != nil {
|
||||
return nil, errors.Wrap(nErr, "Failed to get channels by ids")
|
||||
}
|
||||
}
|
||||
|
||||
return channelList, nil
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) searchAutocompleteChannelsAllTeams(engine searchengine.SearchEngineInterface, userID, term string, includeDeleted, isGuest bool) (model.ChannelListWithTeamData, error) {
|
||||
channelIds, err := engine.SearchChannels("", userID, term, isGuest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channelList := model.ChannelListWithTeamData{}
|
||||
var nErr error
|
||||
if len(channelIds) > 0 {
|
||||
channelList, nErr = c.ChannelStore.GetChannelsWithTeamDataByIds(channelIds, includeDeleted)
|
||||
if nErr != nil {
|
||||
return nil, errors.Wrap(nErr, "Failed to get channels by ids")
|
||||
}
|
||||
}
|
||||
|
||||
return channelList, nil
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) PermanentDeleteMembersByUser(userId string) error {
|
||||
channels, errGetChannels := c.ChannelStore.GetChannelsByUser(userId, false, 0, -1, "")
|
||||
if errGetChannels != nil {
|
||||
mlog.Warn("Encountered error indexing channel after removing user", mlog.String("user_id", userId), mlog.Err(errGetChannels))
|
||||
}
|
||||
|
||||
err := c.ChannelStore.PermanentDeleteMembersByUser(userId)
|
||||
if err == nil {
|
||||
c.rootStore.indexUserFromID(userId)
|
||||
if errGetChannels == nil {
|
||||
for _, ch := range channels {
|
||||
c.indexChannel(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) RemoveAllDeactivatedMembers(channelId string) error {
|
||||
profiles, errProfiles := c.rootStore.User().GetAllProfilesInChannel(context.Background(), channelId, true)
|
||||
if errProfiles != nil {
|
||||
mlog.Warn("Encountered error indexing users for channel", mlog.String("channel_id", channelId), mlog.Err(errProfiles))
|
||||
}
|
||||
|
||||
err := c.ChannelStore.RemoveAllDeactivatedMembers(channelId)
|
||||
if err == nil && errProfiles == nil {
|
||||
for _, user := range profiles {
|
||||
if user.DeleteAt != 0 {
|
||||
c.rootStore.indexUser(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) PermanentDeleteMembersByChannel(channelId string) error {
|
||||
profiles, errProfiles := c.rootStore.User().GetAllProfilesInChannel(context.Background(), channelId, true)
|
||||
if errProfiles != nil {
|
||||
mlog.Warn("Encountered error indexing users for channel", mlog.String("channel_id", channelId), mlog.Err(errProfiles))
|
||||
}
|
||||
|
||||
err := c.ChannelStore.PermanentDeleteMembersByChannel(channelId)
|
||||
if err == nil && errProfiles == nil {
|
||||
for _, user := range profiles {
|
||||
c.rootStore.indexUser(user)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *SearchChannelStore) PermanentDelete(channelId string) error {
|
||||
channel, channelErr := c.ChannelStore.Get(channelId, true)
|
||||
if channelErr != nil {
|
||||
mlog.Warn("Encountered error deleting channel", mlog.String("channel_id", channelId), mlog.Err(channelErr))
|
||||
}
|
||||
err := c.ChannelStore.PermanentDelete(channelId)
|
||||
if err == nil && channelErr == nil {
|
||||
c.deleteChannelIndex(channel)
|
||||
}
|
||||
return err
|
||||
}
|
||||
195
server/channels/store/searchlayer/file_info_layer.go
Обычный файл
195
server/channels/store/searchlayer/file_info_layer.go
Обычный файл
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SearchFileInfoStore struct {
|
||||
store.FileInfoStore
|
||||
rootStore *SearchStore
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) indexFile(file *model.FileInfo) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if file.PostId == "" {
|
||||
return
|
||||
}
|
||||
post, postErr := s.rootStore.Post().GetSingle(file.PostId, false)
|
||||
if postErr != nil {
|
||||
mlog.Error("Couldn't get post for file for SearchEngine indexing.", mlog.String("post_id", file.PostId), mlog.String("search_engine", engineCopy.GetName()), mlog.String("file_info_id", file.Id), mlog.Err(postErr))
|
||||
return
|
||||
}
|
||||
|
||||
if err := engineCopy.IndexFile(file, post.ChannelId); err != nil {
|
||||
mlog.Error("Encountered error indexing file", mlog.String("file_info_id", file.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) deleteFileIndex(fileID string) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeleteFile(fileID); err != nil {
|
||||
mlog.Error("Encountered error deleting file", mlog.String("file_info_id", fileID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) deleteFileIndexForUser(userID string) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeleteUserFiles(userID); err != nil {
|
||||
mlog.Error("Encountered error deleting files for user", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Removed user's files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", userID))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) deleteFileIndexForPost(postID string) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeletePostFiles(postID); err != nil {
|
||||
mlog.Error("Encountered error deleting files for post", mlog.String("post_id", postID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Removed post's files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", postID))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) deleteFileIndexBatch(endTime, limit int64) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeleteFilesBatch(endTime, limit); err != nil {
|
||||
mlog.Error("Encountered error deleting a batch of files", mlog.Int64("limit", limit), mlog.Int64("end_time", endTime), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Removed batch of files from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.Int64("end_time", endTime), mlog.Int64("limit", limit))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
|
||||
nfile, err := s.FileInfoStore.Save(info)
|
||||
if err == nil {
|
||||
s.indexFile(nfile)
|
||||
}
|
||||
return nfile, err
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) SetContent(fileID, content string) error {
|
||||
err := s.FileInfoStore.SetContent(fileID, content)
|
||||
if err == nil {
|
||||
nfile, err2 := s.FileInfoStore.GetFromMaster(fileID)
|
||||
if err2 == nil {
|
||||
nfile.Content = content
|
||||
s.indexFile(nfile)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) AttachToPost(fileId, postId, creatorId string) error {
|
||||
err := s.FileInfoStore.AttachToPost(fileId, postId, creatorId)
|
||||
if err == nil {
|
||||
nFileInfo, err2 := s.FileInfoStore.GetFromMaster(fileId)
|
||||
if err2 == nil {
|
||||
s.indexFile(nFileInfo)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) DeleteForPost(postId string) (string, error) {
|
||||
result, err := s.FileInfoStore.DeleteForPost(postId)
|
||||
if err == nil {
|
||||
s.deleteFileIndexForPost(postId)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) PermanentDelete(fileId string) error {
|
||||
err := s.FileInfoStore.PermanentDelete(fileId)
|
||||
if err == nil {
|
||||
s.deleteFileIndex(fileId)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
|
||||
result, err := s.FileInfoStore.PermanentDeleteBatch(endTime, limit)
|
||||
if err == nil {
|
||||
s.deleteFileIndexBatch(endTime, limit)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) PermanentDeleteByUser(userId string) (int64, error) {
|
||||
result, err := s.FileInfoStore.PermanentDeleteByUser(userId)
|
||||
if err == nil {
|
||||
s.deleteFileIndexForUser(userId)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s SearchFileInfoStore) Search(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.FileInfoList, error) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsSearchEnabled() {
|
||||
userChannels, nErr := s.rootStore.Channel().GetChannels(teamId, userId, &model.ChannelSearchOpts{
|
||||
IncludeDeleted: paramsList[0].IncludeDeletedChannels,
|
||||
LastDeleteAt: 0,
|
||||
})
|
||||
if nErr != nil {
|
||||
return nil, nErr
|
||||
}
|
||||
fileIds, appErr := engine.SearchFiles(userChannels, paramsList, page, perPage)
|
||||
if appErr != nil {
|
||||
mlog.Error("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(appErr))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the files
|
||||
filesList := model.NewFileInfoList()
|
||||
if len(fileIds) > 0 {
|
||||
files, nErr := s.FileInfoStore.GetByIds(fileIds)
|
||||
if nErr != nil {
|
||||
return nil, nErr
|
||||
}
|
||||
for _, f := range files {
|
||||
filesList.AddFileInfo(f)
|
||||
filesList.AddOrder(f.Id)
|
||||
}
|
||||
}
|
||||
return filesList, nil
|
||||
}
|
||||
}
|
||||
|
||||
if *s.rootStore.getConfig().SqlSettings.DisableDatabaseSearch {
|
||||
return model.NewFileInfoList(), nil
|
||||
}
|
||||
|
||||
return s.FileInfoStore.Search(paramsList, userId, teamId, page, perPage)
|
||||
}
|
||||
126
server/channels/store/searchlayer/layer.go
Обычный файл
126
server/channels/store/searchlayer/layer.go
Обычный файл
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SearchStore struct {
|
||||
store.Store
|
||||
searchEngine *searchengine.Broker
|
||||
user *SearchUserStore
|
||||
team *SearchTeamStore
|
||||
channel *SearchChannelStore
|
||||
post *SearchPostStore
|
||||
fileInfo *SearchFileInfoStore
|
||||
configValue atomic.Value
|
||||
}
|
||||
|
||||
func NewSearchLayer(baseStore store.Store, searchEngine *searchengine.Broker, cfg *model.Config) *SearchStore {
|
||||
searchStore := &SearchStore{
|
||||
Store: baseStore,
|
||||
searchEngine: searchEngine,
|
||||
}
|
||||
searchStore.configValue.Store(cfg)
|
||||
searchStore.channel = &SearchChannelStore{ChannelStore: baseStore.Channel(), rootStore: searchStore}
|
||||
searchStore.post = &SearchPostStore{PostStore: baseStore.Post(), rootStore: searchStore}
|
||||
searchStore.team = &SearchTeamStore{TeamStore: baseStore.Team(), rootStore: searchStore}
|
||||
searchStore.user = &SearchUserStore{UserStore: baseStore.User(), rootStore: searchStore}
|
||||
searchStore.fileInfo = &SearchFileInfoStore{FileInfoStore: baseStore.FileInfo(), rootStore: searchStore}
|
||||
|
||||
return searchStore
|
||||
}
|
||||
|
||||
func (s *SearchStore) UpdateConfig(cfg *model.Config) {
|
||||
s.configValue.Store(cfg)
|
||||
}
|
||||
|
||||
func (s *SearchStore) getConfig() *model.Config {
|
||||
return s.configValue.Load().(*model.Config)
|
||||
}
|
||||
|
||||
func (s *SearchStore) Channel() store.ChannelStore {
|
||||
return s.channel
|
||||
}
|
||||
|
||||
func (s *SearchStore) Post() store.PostStore {
|
||||
return s.post
|
||||
}
|
||||
|
||||
func (s *SearchStore) FileInfo() store.FileInfoStore {
|
||||
return s.fileInfo
|
||||
}
|
||||
|
||||
func (s *SearchStore) Team() store.TeamStore {
|
||||
return s.team
|
||||
}
|
||||
|
||||
func (s *SearchStore) User() store.UserStore {
|
||||
return s.user
|
||||
}
|
||||
|
||||
func (s *SearchStore) indexUserFromID(userId string) {
|
||||
user, err := s.User().Get(context.Background(), userId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.indexUser(user)
|
||||
}
|
||||
|
||||
func (s *SearchStore) indexUser(user *model.User) {
|
||||
for _, engine := range s.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
userTeams, nErr := s.Team().GetTeamsByUserId(user.Id)
|
||||
if nErr != nil {
|
||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(nErr))
|
||||
return
|
||||
}
|
||||
|
||||
userTeamsIds := []string{}
|
||||
for _, team := range userTeams {
|
||||
userTeamsIds = append(userTeamsIds, team.Id)
|
||||
}
|
||||
|
||||
userChannelMembers, err := s.Channel().GetAllChannelMembersForUser(user.Id, false, true)
|
||||
if err != nil {
|
||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
userChannelsIds := []string{}
|
||||
for channelId := range userChannelMembers {
|
||||
userChannelsIds = append(userChannelsIds, channelId)
|
||||
}
|
||||
|
||||
if err := engineCopy.IndexUser(user, userTeamsIds, userChannelsIds); err != nil {
|
||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Indexed user in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Runs an indexing function synchronously or asynchronously depending on the engine
|
||||
func runIndexFn(engine searchengine.SearchEngineInterface, indexFn func(searchengine.SearchEngineInterface)) {
|
||||
if engine.IsIndexingSync() {
|
||||
indexFn(engine)
|
||||
if err := engine.RefreshIndexes(); err != nil {
|
||||
mlog.Error("Encountered error refresh the indexes", mlog.Err(err))
|
||||
}
|
||||
} else {
|
||||
go (func(engineCopy searchengine.SearchEngineInterface) {
|
||||
indexFn(engineCopy)
|
||||
})(engine)
|
||||
}
|
||||
}
|
||||
45
server/channels/store/searchlayer/layer_test.go
Обычный файл
45
server/channels/store/searchlayer/layer_test.go
Обычный файл
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/searchlayer"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
|
||||
)
|
||||
|
||||
// Test to verify race condition on UpdateConfig. The test must run with -race flag in order to verify
|
||||
// that there is no race. Ref: (#MM-30868)
|
||||
func TestUpdateConfigRace(t *testing.T) {
|
||||
driverName := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
|
||||
if driverName == "" {
|
||||
driverName = model.DatabaseDriverPostgres
|
||||
}
|
||||
settings := storetest.MakeSqlSettings(driverName, false)
|
||||
store := sqlstore.New(*settings, nil)
|
||||
|
||||
cfg := &model.Config{}
|
||||
cfg.SetDefaults()
|
||||
cfg.ClusterSettings.MaxIdleConns = model.NewInt(1)
|
||||
searchEngine := searchengine.NewBroker(cfg)
|
||||
layer := searchlayer.NewSearchLayer(&testlib.TestStore{Store: store}, searchEngine, cfg)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(5)
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
layer.UpdateConfig(cfg.Clone())
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
195
server/channels/store/searchlayer/post_layer.go
Обычный файл
195
server/channels/store/searchlayer/post_layer.go
Обычный файл
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SearchPostStore struct {
|
||||
store.PostStore
|
||||
rootStore *SearchStore
|
||||
}
|
||||
|
||||
func (s SearchPostStore) indexPost(post *model.Post) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
channel, chanErr := s.rootStore.Channel().Get(post.ChannelId, true)
|
||||
if chanErr != nil {
|
||||
mlog.Error("Couldn't get channel for post for SearchEngine indexing.", mlog.String("channel_id", post.ChannelId), mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", post.Id), mlog.Err(chanErr))
|
||||
return
|
||||
}
|
||||
if err := engineCopy.IndexPost(post, channel.TeamId); err != nil {
|
||||
mlog.Warn("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchPostStore) deletePostIndex(post *model.Post) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeletePost(post); err != nil {
|
||||
mlog.Warn("Encountered error deleting post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchPostStore) deleteChannelPostsIndex(channelID string) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeleteChannelPosts(channelID); err != nil {
|
||||
mlog.Warn("Encountered error deleting channel posts", mlog.String("channel_id", channelID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Removed all channel posts from the index in search engine", mlog.String("channel_id", channelID), mlog.String("search_engine", engineCopy.GetName()))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchPostStore) deleteUserPostsIndex(userID string) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeleteUserPosts(userID); err != nil {
|
||||
mlog.Warn("Encountered error deleting user posts", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Removed all user posts from the index in search engine", mlog.String("user_id", userID), mlog.String("search_engine", engineCopy.GetName()))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SearchPostStore) Update(newPost, oldPost *model.Post) (*model.Post, error) {
|
||||
post, err := s.PostStore.Update(newPost, oldPost)
|
||||
|
||||
if err == nil {
|
||||
s.indexPost(post)
|
||||
}
|
||||
return post, err
|
||||
}
|
||||
|
||||
func (s *SearchPostStore) Overwrite(post *model.Post) (*model.Post, error) {
|
||||
post, err := s.PostStore.Overwrite(post)
|
||||
if err == nil {
|
||||
s.indexPost(post)
|
||||
}
|
||||
return post, err
|
||||
}
|
||||
|
||||
func (s SearchPostStore) Save(post *model.Post) (*model.Post, error) {
|
||||
npost, err := s.PostStore.Save(post)
|
||||
|
||||
if err == nil {
|
||||
s.indexPost(npost)
|
||||
}
|
||||
return npost, err
|
||||
}
|
||||
|
||||
func (s SearchPostStore) Delete(postId string, date int64, deletedByID string) error {
|
||||
err := s.PostStore.Delete(postId, date, deletedByID)
|
||||
|
||||
if err == nil {
|
||||
opts := model.GetPostsOptions{
|
||||
SkipFetchThreads: true,
|
||||
}
|
||||
postList, err2 := s.PostStore.Get(context.Background(), postId, opts, "", map[string]bool{})
|
||||
if postList != nil && len(postList.Order) > 0 {
|
||||
if err2 != nil {
|
||||
s.deletePostIndex(postList.Posts[postList.Order[0]])
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SearchPostStore) PermanentDeleteByUser(userID string) error {
|
||||
err := s.PostStore.PermanentDeleteByUser(userID)
|
||||
if err == nil {
|
||||
s.deleteUserPostsIndex(userID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SearchPostStore) PermanentDeleteByChannel(channelID string) error {
|
||||
err := s.PostStore.PermanentDeleteByChannel(channelID)
|
||||
if err == nil {
|
||||
s.deleteChannelPostsIndex(channelID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SearchPostStore) searchPostsForUserByEngine(engine searchengine.SearchEngineInterface, paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) {
|
||||
if err := model.IsSearchParamsListValid(paramsList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We only allow the user to search in channels they are a member of.
|
||||
userChannels, err2 := s.rootStore.Channel().GetChannels(teamId, userId,
|
||||
&model.ChannelSearchOpts{
|
||||
IncludeDeleted: paramsList[0].IncludeDeletedChannels,
|
||||
LastDeleteAt: 0,
|
||||
})
|
||||
if err2 != nil {
|
||||
return nil, errors.Wrap(err2, "error getting channel for user")
|
||||
}
|
||||
|
||||
postIds, matches, err := engine.SearchPosts(userChannels, paramsList, page, perPage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the posts
|
||||
postList := model.NewPostList()
|
||||
if len(postIds) > 0 {
|
||||
posts, err := s.PostStore.GetPostsByIds(postIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range posts {
|
||||
if p.DeleteAt == 0 {
|
||||
postList.AddPost(p)
|
||||
postList.AddOrder(p.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return model.MakePostSearchResults(postList, matches), nil
|
||||
}
|
||||
|
||||
func (s SearchPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsSearchEnabled() {
|
||||
results, err := s.searchPostsForUserByEngine(engine, paramsList, userId, teamId, page, perPage)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error on SearchPostsInTeamForUser.", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
return results, err
|
||||
}
|
||||
}
|
||||
|
||||
if *s.rootStore.getConfig().SqlSettings.DisableDatabaseSearch {
|
||||
return &model.PostSearchResults{PostList: model.NewPostList(), Matches: model.PostSearchMatches{}}, nil
|
||||
}
|
||||
|
||||
return s.PostStore.SearchPostsForUser(paramsList, userId, teamId, page, perPage)
|
||||
}
|
||||
7
server/channels/store/searchlayer/stop_word.go
Обычный файл
7
server/channels/store/searchlayer/stop_word.go
Обычный файл
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer
|
||||
|
||||
var MySQLStopWords = []string{"a", "about", "an", "are", "as", "at", "be", "by", "com", "de", "en", "for", "from", "how", "i", "in", "is", "it", "la", "of",
|
||||
"on", "or", "that", "the", "this", "to", "was", "what", "when", "where", "who", "will", "with", "und", "the", "www"}
|
||||
46
server/channels/store/searchlayer/team_layer.go
Обычный файл
46
server/channels/store/searchlayer/team_layer.go
Обычный файл
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer
|
||||
|
||||
import (
|
||||
model "github.com/mattermost/mattermost-server/v6/model"
|
||||
store "github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SearchTeamStore struct {
|
||||
store.TeamStore
|
||||
rootStore *SearchStore
|
||||
}
|
||||
|
||||
func (s SearchTeamStore) SaveMember(teamMember *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, error) {
|
||||
member, err := s.TeamStore.SaveMember(teamMember, maxUsersPerTeam)
|
||||
if err == nil {
|
||||
s.rootStore.indexUserFromID(member.UserId)
|
||||
}
|
||||
return member, err
|
||||
}
|
||||
|
||||
func (s SearchTeamStore) UpdateMember(teamMember *model.TeamMember) (*model.TeamMember, error) {
|
||||
member, err := s.TeamStore.UpdateMember(teamMember)
|
||||
if err == nil {
|
||||
s.rootStore.indexUserFromID(member.UserId)
|
||||
}
|
||||
return member, err
|
||||
}
|
||||
|
||||
func (s SearchTeamStore) RemoveMember(teamId string, userId string) error {
|
||||
err := s.TeamStore.RemoveMember(teamId, userId)
|
||||
if err == nil {
|
||||
s.rootStore.indexUserFromID(userId)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SearchTeamStore) RemoveAllMembersByUser(userId string) error {
|
||||
err := s.TeamStore.RemoveAllMembersByUser(userId)
|
||||
if err == nil {
|
||||
s.rootStore.indexUserFromID(userId)
|
||||
}
|
||||
return err
|
||||
}
|
||||
236
server/channels/store/searchlayer/user_layer.go
Обычный файл
236
server/channels/store/searchlayer/user_layer.go
Обычный файл
@@ -0,0 +1,236 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SearchUserStore struct {
|
||||
store.UserStore
|
||||
rootStore *SearchStore
|
||||
}
|
||||
|
||||
func (s *SearchUserStore) deleteUserIndex(user *model.User) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsIndexingEnabled() {
|
||||
runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) {
|
||||
if err := engineCopy.DeleteUser(user); err != nil {
|
||||
mlog.Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Debug("Removed user from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SearchUserStore) Search(teamId, term string, options *model.UserSearchOptions) ([]*model.User, error) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsSearchEnabled() {
|
||||
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, "", options.ViewRestrictions)
|
||||
if nErr != nil {
|
||||
mlog.Warn("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
||||
continue
|
||||
}
|
||||
|
||||
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
|
||||
return []*model.User{}, nil
|
||||
}
|
||||
|
||||
sanitizedTerm := sanitizeSearchTerm(term)
|
||||
|
||||
usersIds, err := engine.SearchUsersInTeam(teamId, listOfAllowedChannels, sanitizedTerm, options)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
users, nErr := s.UserStore.GetProfileByIds(context.Background(), usersIds, nil, false)
|
||||
if nErr != nil {
|
||||
mlog.Warn("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
||||
continue
|
||||
}
|
||||
|
||||
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
|
||||
return users, nil
|
||||
}
|
||||
}
|
||||
|
||||
mlog.Debug("Using database search because no other search engine is available")
|
||||
|
||||
return s.UserStore.Search(teamId, term, options)
|
||||
}
|
||||
|
||||
func (s *SearchUserStore) Update(user *model.User, trustedUpdateData bool) (*model.UserUpdate, error) {
|
||||
userUpdate, err := s.UserStore.Update(user, trustedUpdateData)
|
||||
|
||||
if err == nil {
|
||||
s.rootStore.indexUser(userUpdate.New)
|
||||
}
|
||||
return userUpdate, err
|
||||
}
|
||||
|
||||
func (s *SearchUserStore) Save(user *model.User) (*model.User, error) {
|
||||
nuser, err := s.UserStore.Save(user)
|
||||
|
||||
if err == nil {
|
||||
s.rootStore.indexUser(nuser)
|
||||
}
|
||||
return nuser, err
|
||||
}
|
||||
|
||||
func (s *SearchUserStore) PermanentDelete(userId string) error {
|
||||
user, userErr := s.UserStore.Get(context.Background(), userId)
|
||||
if userErr != nil {
|
||||
mlog.Warn("Encountered error deleting user", mlog.String("user_id", userId), mlog.Err(userErr))
|
||||
}
|
||||
err := s.UserStore.PermanentDelete(userId)
|
||||
if err == nil && userErr == nil {
|
||||
s.deleteUserIndex(user)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine.SearchEngineInterface, teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
|
||||
var err *model.AppError
|
||||
uchanIds := []string{}
|
||||
nuchanIds := []string{}
|
||||
sanitizedTerm := sanitizeSearchTerm(term)
|
||||
if channelId != "" && options.ListOfAllowedChannels != nil && !strings.Contains(strings.Join(options.ListOfAllowedChannels, "."), channelId) {
|
||||
nuchanIds, err = engine.SearchUsersInTeam(teamId, options.ListOfAllowedChannels, sanitizedTerm, options)
|
||||
} else {
|
||||
uchanIds, nuchanIds, err = engine.SearchUsersInChannel(teamId, channelId, options.ListOfAllowedChannels, sanitizedTerm, options)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uchan := make(chan store.StoreResult, 1)
|
||||
go func() {
|
||||
users, nErr := s.UserStore.GetProfileByIds(context.Background(), uchanIds, nil, false)
|
||||
uchan <- store.StoreResult{Data: users, NErr: nErr}
|
||||
close(uchan)
|
||||
}()
|
||||
|
||||
nuchan := make(chan store.StoreResult, 1)
|
||||
go func() {
|
||||
users, nErr := s.UserStore.GetProfileByIds(context.Background(), nuchanIds, nil, false)
|
||||
nuchan <- store.StoreResult{Data: users, NErr: nErr}
|
||||
close(nuchan)
|
||||
}()
|
||||
|
||||
autocomplete := &model.UserAutocompleteInChannel{}
|
||||
|
||||
result := <-uchan
|
||||
if result.NErr != nil {
|
||||
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
|
||||
}
|
||||
inUsers := result.Data.([]*model.User)
|
||||
autocomplete.InChannel = inUsers
|
||||
|
||||
result = <-nuchan
|
||||
if result.NErr != nil {
|
||||
return nil, errors.Wrap(result.NErr, "failed to get user profiles by ids")
|
||||
}
|
||||
outUsers := result.Data.([]*model.User)
|
||||
autocomplete.OutOfChannel = outUsers
|
||||
|
||||
return autocomplete, nil
|
||||
}
|
||||
|
||||
// getListOfAllowedChannels return the list of allowed channels to search user based on the
|
||||
//
|
||||
// next scenarios:
|
||||
// - If there isn't view restrictions (team or channel) and no team id to filter them, then all
|
||||
// channels are allowed (nil return)
|
||||
// - If we receive a team Id and either we don't have view restrictions or the provided team id is included in the
|
||||
// list of restricted teams, then we return all the team channels
|
||||
// - If we don't receive team id or the provided team id is not in the list of allowed teams to search of and we
|
||||
// don't have channel restrictions then we return an empty result because we cannot get channels
|
||||
// - If we receive channels restrictions we get:
|
||||
// - If we don't have team id, we get those restricted channels (guest accounts and quick search)
|
||||
// - If we have a team id then we only return those restricted channels that belongs to that team
|
||||
func (s *SearchUserStore) getListOfAllowedChannels(teamId, channelId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) {
|
||||
var listOfAllowedChannels []string
|
||||
if viewRestrictions == nil && teamId == "" {
|
||||
// nil return without error means all channels are allowed
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if teamId != "" && (viewRestrictions == nil || strings.Contains(strings.Join(viewRestrictions.Teams, "."), teamId)) {
|
||||
channels, err := s.rootStore.Channel().GetTeamChannels(teamId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get team channels")
|
||||
}
|
||||
for _, channel := range channels {
|
||||
listOfAllowedChannels = append(listOfAllowedChannels, channel.Id)
|
||||
}
|
||||
|
||||
if channelId != "" {
|
||||
ch, err := s.rootStore.Channel().Get(channelId, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get channel with id: %s", channelId)
|
||||
}
|
||||
// Check if DM/GM channel, and add to the list.
|
||||
// This is because GetTeamChannels does not return DM/GM channels.
|
||||
// And since the channelId is passed from the API layer, it is already
|
||||
// auth checked to confirm that the user has permission.
|
||||
if ch.IsGroupOrDirect() {
|
||||
listOfAllowedChannels = append(listOfAllowedChannels, channelId)
|
||||
}
|
||||
}
|
||||
return listOfAllowedChannels, nil
|
||||
}
|
||||
|
||||
if len(viewRestrictions.Channels) > 0 {
|
||||
channels, err := s.rootStore.Channel().GetChannelsByIds(viewRestrictions.Channels, false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get channels by ids")
|
||||
}
|
||||
for _, c := range channels {
|
||||
if teamId == "" || (teamId != "" && c.TeamId == teamId) {
|
||||
listOfAllowedChannels = append(listOfAllowedChannels, c.Id)
|
||||
}
|
||||
}
|
||||
return listOfAllowedChannels, nil
|
||||
}
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
|
||||
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
|
||||
if engine.IsAutocompletionEnabled() {
|
||||
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, channelId, options.ViewRestrictions)
|
||||
if nErr != nil {
|
||||
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
||||
continue
|
||||
}
|
||||
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
|
||||
return &model.UserAutocompleteInChannel{}, nil
|
||||
}
|
||||
options.ListOfAllowedChannels = listOfAllowedChannels
|
||||
|
||||
autocomplete, nErr := s.autocompleteUsersInChannelByEngine(engine, teamId, channelId, term, options)
|
||||
if nErr != nil {
|
||||
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
|
||||
continue
|
||||
}
|
||||
mlog.Debug("Using the first available search engine", mlog.String("search_engine", engine.GetName()))
|
||||
return autocomplete, nil
|
||||
}
|
||||
}
|
||||
|
||||
mlog.Debug("Using database search because no other search engine is available")
|
||||
return s.UserStore.AutocompleteUsersInChannel(teamId, channelId, term, options)
|
||||
}
|
||||
12
server/channels/store/searchlayer/utils.go
Обычный файл
12
server/channels/store/searchlayer/utils.go
Обычный файл
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchlayer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func sanitizeSearchTerm(term string) string {
|
||||
return strings.TrimLeft(term, "@")
|
||||
}
|
||||
233
server/channels/store/searchtest/channel_layer.go
Обычный файл
233
server/channels/store/searchtest/channel_layer.go
Обычный файл
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchtest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
var searchChannelStoreTests = []searchTest{
|
||||
{
|
||||
Name: "Should be able to autocomplete a channel by name",
|
||||
Fn: testAutocompleteChannelByName,
|
||||
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a channel by name (Postgres)",
|
||||
Fn: testAutocompleteChannelByNamePostgres,
|
||||
Tags: []string{EnginePostgres},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a channel by display name",
|
||||
Fn: testAutocompleteChannelByDisplayName,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a channel by a part of its name when has parts splitted by - character",
|
||||
Fn: testAutocompleteChannelByNameSplittedWithDashChar,
|
||||
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a channel by a part of its name when has parts splitted by - character (Postgres)",
|
||||
Fn: testAutocompleteChannelByNameSplittedWithDashCharPostgres,
|
||||
Tags: []string{EnginePostgres},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a channel by a part of its name when has parts splitted by _ character",
|
||||
Fn: testAutocompleteChannelByNameSplittedWithUnderscoreChar,
|
||||
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a channel by a part of its display name when has parts splitted by whitespace character",
|
||||
Fn: testAutocompleteChannelByDisplayNameSplittedByWhitespaces,
|
||||
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete retrieving all channels if the term is empty",
|
||||
Fn: testAutocompleteAllChannelsIfTermIsEmpty,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete channels in a case insensitive manner",
|
||||
Fn: testSearchChannelsInCaseInsensitiveManner,
|
||||
Tags: []string{EngineMySql, EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete channels in a case insensitive manner (Postgres)",
|
||||
Fn: testSearchChannelsInCaseInsensitiveMannerPostgres,
|
||||
Tags: []string{EnginePostgres},
|
||||
},
|
||||
{
|
||||
Name: "Should support to autocomplete having a hyphen as the last character",
|
||||
Fn: testSearchShouldSupportHavingHyphenAsLastCharacter,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should support to autocomplete with archived channels",
|
||||
Fn: testSearchShouldSupportAutocompleteWithArchivedChannels,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSearchChannelStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) {
|
||||
th := &SearchTestHelper{
|
||||
Store: s,
|
||||
}
|
||||
err := th.SetupBasicFixtures()
|
||||
require.NoError(t, err)
|
||||
defer th.CleanFixtures()
|
||||
runTestSearch(t, testEngine, searchChannelStoreTests, th)
|
||||
}
|
||||
|
||||
func testAutocompleteChannelByName(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "Channel Alternate", "Channel Alternate", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
|
||||
private, err := th.createChannel(th.Team.Id, "channel-altprivate", "Channel AltPrivate", "Channel Private", model.ChannelTypePrivate, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(private)
|
||||
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id, private.Id}, res)
|
||||
|
||||
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "channel-a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelBasic.Id, alternate.Id, private.Id, th.ChannelAnotherTeam.Id}, res2)
|
||||
}
|
||||
|
||||
func testAutocompleteChannelByNamePostgres(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "Channel Alternate", "Channel Alternate", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res)
|
||||
}
|
||||
|
||||
func testAutocompleteChannelByDisplayName(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
|
||||
private, err := th.createChannel(th.Team.Id, "channel-altprivate", "ChannelAltPrivate", "Channel Private", model.ChannelTypePrivate, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(private)
|
||||
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "ChannelA", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id, private.Id}, res)
|
||||
|
||||
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "ChannelA", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelBasic.Id, alternate.Id, private.Id, th.ChannelAnotherTeam.Id}, res2)
|
||||
}
|
||||
|
||||
func testAutocompleteChannelByNameSplittedWithDashChar(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id}, res)
|
||||
}
|
||||
|
||||
func testAutocompleteChannelByNameSplittedWithDashCharPostgres(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res)
|
||||
}
|
||||
|
||||
func testAutocompleteChannelByNameSplittedWithUnderscoreChar(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel_alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel_a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{alternate.Id}, res)
|
||||
|
||||
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "channel_a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatchWithTeamData(t, []string{alternate.Id}, res2)
|
||||
}
|
||||
|
||||
func testAutocompleteChannelByDisplayNameSplittedByWhitespaces(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "Channel Alternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer th.deleteChannel(alternate)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "Channel A", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{alternate.Id}, res)
|
||||
}
|
||||
func testAutocompleteAllChannelsIfTermIsEmpty(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "Channel Alternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
other, err := th.createChannel(th.Team.Id, "other-channel", "Other Channel", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
defer th.deleteChannel(other)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id, other.Id}, res)
|
||||
}
|
||||
|
||||
func testSearchChannelsInCaseInsensitiveManner(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channela", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id}, res)
|
||||
res, err = th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "ChAnNeL-a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id}, res)
|
||||
|
||||
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "channela", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelAnotherTeam.Id, th.ChannelBasic.Id, alternate.Id}, res2)
|
||||
res2, err = th.Store.Channel().Autocomplete(th.User.Id, "ChAnNeL-a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelAnotherTeam.Id, th.ChannelBasic.Id, alternate.Id}, res2)
|
||||
}
|
||||
|
||||
func testSearchChannelsInCaseInsensitiveMannerPostgres(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channela", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, alternate.Id}, res)
|
||||
res, err = th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "ChAnNeL-a", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res)
|
||||
}
|
||||
|
||||
func testSearchShouldSupportHavingHyphenAsLastCharacter(t *testing.T, th *SearchTestHelper) {
|
||||
alternate, err := th.createChannel(th.Team.Id, "channel-alternate", "ChannelAlternate", "", model.ChannelTypeOpen, th.User, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteChannel(alternate)
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res)
|
||||
|
||||
res2, err := th.Store.Channel().Autocomplete(th.User.Id, "channel-", false, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatchWithTeamData(t, []string{th.ChannelAnotherTeam.Id, th.ChannelBasic.Id, th.ChannelPrivate.Id, alternate.Id}, res2)
|
||||
}
|
||||
|
||||
func testSearchShouldSupportAutocompleteWithArchivedChannels(t *testing.T, th *SearchTestHelper) {
|
||||
res, err := th.Store.Channel().AutocompleteInTeam(th.Team.Id, th.User.Id, "channel-", true, false)
|
||||
require.NoError(t, err)
|
||||
th.checkChannelIdsMatch(t, []string{th.ChannelBasic.Id, th.ChannelPrivate.Id, th.ChannelDeleted.Id}, res)
|
||||
}
|
||||
1646
server/channels/store/searchtest/file_info_layer.go
Обычный файл
1646
server/channels/store/searchtest/file_info_layer.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
512
server/channels/store/searchtest/helper.go
Обычный файл
512
server/channels/store/searchtest/helper.go
Обычный файл
@@ -0,0 +1,512 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchtest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SearchTestHelper struct {
|
||||
Store store.Store
|
||||
Team *model.Team
|
||||
AnotherTeam *model.Team
|
||||
User *model.User
|
||||
User2 *model.User
|
||||
UserAnotherTeam *model.User
|
||||
ChannelBasic *model.Channel
|
||||
ChannelPrivate *model.Channel
|
||||
ChannelAnotherTeam *model.Channel
|
||||
ChannelDeleted *model.Channel
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) SetupBasicFixtures() error {
|
||||
// Remove users from previous tests
|
||||
err := th.cleanAllUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create teams
|
||||
team, err := th.createTeam("searchtest-team", "Searchtest team", model.TeamOpen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
anotherTeam, err := th.createTeam("another-searchtest-team", "Another Searchtest team", model.TeamOpen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create users
|
||||
user, err := th.createUser("basicusername1", "basicnickname1", "basicfirstname1", "basiclastname1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user2, err := th.createUser("basicusername2", "basicnickname2", "basicfirstname2", "basiclastname2")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
useranother, err := th.createUser("basicusername3", "basicnickname3", "basicfirstname3", "basiclastname3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create channels
|
||||
channelBasic, err := th.createChannel(team.Id, "channel-a", "ChannelA", "", model.ChannelTypeOpen, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
channelPrivate, err := th.createChannel(team.Id, "channel-private", "ChannelPrivate", "", model.ChannelTypePrivate, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
channelDeleted, err := th.createChannel(team.Id, "channel-deleted", "ChannelA (deleted)", "", model.ChannelTypeOpen, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
channelAnotherTeam, err := th.createChannel(anotherTeam.Id, "channel-a", "ChannelA", "", model.ChannelTypeOpen, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.addUserToTeams(user, []string{team.Id, anotherTeam.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.addUserToTeams(user2, []string{team.Id, anotherTeam.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.addUserToTeams(useranother, []string{anotherTeam.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.addUserToChannels(user, []string{channelBasic.Id, channelPrivate.Id, channelDeleted.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.addUserToChannels(user2, []string{channelPrivate.Id, channelDeleted.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.addUserToChannels(useranother, []string{channelAnotherTeam.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
th.Team = team
|
||||
th.AnotherTeam = anotherTeam
|
||||
th.User = user
|
||||
th.User2 = user2
|
||||
th.UserAnotherTeam = useranother
|
||||
th.ChannelBasic = channelBasic
|
||||
th.ChannelPrivate = channelPrivate
|
||||
th.ChannelAnotherTeam = channelAnotherTeam
|
||||
th.ChannelDeleted = channelDeleted
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) CleanFixtures() error {
|
||||
err := th.deleteChannels([]*model.Channel{
|
||||
th.ChannelBasic, th.ChannelPrivate, th.ChannelAnotherTeam, th.ChannelDeleted,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.deleteTeam(th.Team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.deleteTeam(th.AnotherTeam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = th.cleanAllUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createTeam(name, displayName, teamType string) (*model.Team, error) {
|
||||
return th.Store.Team().Save(&model.Team{
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
Type: teamType,
|
||||
})
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) deleteTeam(team *model.Team) error {
|
||||
err := th.Store.Team().RemoveAllMembersByTeam(team.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return th.Store.Team().PermanentDelete(team.Id)
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) makeEmail() string {
|
||||
return "success_" + model.NewId() + "@simulator.amazon.com"
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createUser(username, nickname, firstName, lastName string) (*model.User, error) {
|
||||
return th.Store.User().Save(&model.User{
|
||||
Username: username,
|
||||
Password: username,
|
||||
Nickname: nickname,
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: th.makeEmail(),
|
||||
})
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createGuest(username, nickname, firstName, lastName string) (*model.User, error) {
|
||||
return th.Store.User().Save(&model.User{
|
||||
Username: username,
|
||||
Password: username,
|
||||
Nickname: nickname,
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: th.makeEmail(),
|
||||
Roles: model.SystemGuestRoleId,
|
||||
})
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) deleteUser(user *model.User) error {
|
||||
return th.Store.User().PermanentDelete(user.Id)
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) deleteBotUser(botID string) error {
|
||||
if err := th.deleteBot(botID); err != nil {
|
||||
return err
|
||||
}
|
||||
return th.Store.User().PermanentDelete(botID)
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) cleanAllUsers() error {
|
||||
users, err := th.Store.User().GetAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
err := th.deleteUser(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createBot(username, displayName, ownerID string) (*model.Bot, error) {
|
||||
botModel := &model.Bot{
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
OwnerId: ownerID,
|
||||
}
|
||||
|
||||
user, err := th.Store.User().Save(model.UserFromBot(botModel))
|
||||
if err != nil {
|
||||
return nil, errors.New(err.Error())
|
||||
}
|
||||
|
||||
botModel.UserId = user.Id
|
||||
bot, err := th.Store.Bot().Save(botModel)
|
||||
if err != nil {
|
||||
th.Store.User().PermanentDelete(bot.UserId)
|
||||
return nil, errors.New(err.Error())
|
||||
}
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) deleteBot(botID string) error {
|
||||
err := th.Store.Bot().PermanentDelete(botID)
|
||||
if err != nil {
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createChannel(teamID, name, displayName, purpose string, channelType model.ChannelType, user *model.User, deleted bool) (*model.Channel, error) {
|
||||
channel, err := th.Store.Channel().Save(&model.Channel{
|
||||
TeamId: teamID,
|
||||
DisplayName: displayName,
|
||||
Name: name,
|
||||
Type: channelType,
|
||||
Purpose: purpose,
|
||||
}, 999)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
err = th.addUserToChannels(user, []string{channel.Id})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if deleted {
|
||||
err := th.Store.Channel().Delete(channel.Id, model.GetMillis())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createDirectChannel(teamID, name, displayName string, users []*model.User) (*model.Channel, error) {
|
||||
channel := &model.Channel{
|
||||
TeamId: teamID,
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
Type: model.ChannelTypeDirect,
|
||||
}
|
||||
|
||||
m1 := &model.ChannelMember{}
|
||||
m1.ChannelId = channel.Id
|
||||
m1.UserId = users[0].Id
|
||||
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
|
||||
|
||||
m2 := &model.ChannelMember{}
|
||||
m2.ChannelId = channel.Id
|
||||
m2.UserId = users[0].Id
|
||||
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
|
||||
|
||||
channel, err := th.Store.Channel().SaveDirectChannel(channel, m1, m2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createGroupChannel(teamID, displayName string, users []*model.User) (*model.Channel, error) {
|
||||
userIDS := make([]string, len(users))
|
||||
for _, user := range users {
|
||||
userIDS = append(userIDS, user.Id)
|
||||
}
|
||||
|
||||
group := &model.Channel{
|
||||
TeamId: teamID,
|
||||
Name: model.GetGroupNameFromUserIds(userIDS),
|
||||
DisplayName: displayName,
|
||||
Type: model.ChannelTypeGroup,
|
||||
}
|
||||
|
||||
channel, err := th.Store.Channel().Save(group, 10000)
|
||||
if err != nil {
|
||||
return nil, errors.New(err.Error())
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
err := th.addUserToChannels(user, []string{channel.Id})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) deleteChannel(channel *model.Channel) error {
|
||||
err := th.Store.Channel().PermanentDeleteMembersByChannel(channel.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return th.Store.Channel().PermanentDelete(channel.Id)
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) deleteChannels(channels []*model.Channel) error {
|
||||
for _, channel := range channels {
|
||||
err := th.deleteChannel(channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createPostModel(userID, channelID, message, hashtags, postType string, createAt int64, pinned bool) *model.Post {
|
||||
return &model.Post{
|
||||
Message: message,
|
||||
ChannelId: channelID,
|
||||
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
||||
UserId: userID,
|
||||
Hashtags: hashtags,
|
||||
IsPinned: pinned,
|
||||
CreateAt: createAt,
|
||||
Type: postType,
|
||||
}
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createPost(userID, channelID, message, hashtags, postType string, createAt int64, pinned bool) (*model.Post, error) {
|
||||
var creationTime int64 = 1000000
|
||||
if createAt > 0 {
|
||||
creationTime = createAt
|
||||
}
|
||||
postModel := th.createPostModel(userID, channelID, message, hashtags, postType, creationTime, pinned)
|
||||
return th.Store.Post().Save(postModel)
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createFileInfoModel(creatorID, postID, name, content, extension, mimeType string, createAt, size int64) *model.FileInfo {
|
||||
return &model.FileInfo{
|
||||
CreatorId: creatorID,
|
||||
PostId: postID,
|
||||
CreateAt: createAt,
|
||||
UpdateAt: createAt,
|
||||
DeleteAt: 0,
|
||||
Name: name,
|
||||
Content: content,
|
||||
Path: name,
|
||||
Extension: extension,
|
||||
Size: size,
|
||||
MimeType: mimeType,
|
||||
}
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createFileInfo(creatorID, postID, name, content, extension, mimeType string, createAt, size int64) (*model.FileInfo, error) {
|
||||
var creationTime int64 = 1000000
|
||||
if createAt > 0 {
|
||||
creationTime = createAt
|
||||
}
|
||||
fileInfoModel := th.createFileInfoModel(creatorID, postID, name, content, extension, mimeType, creationTime, size)
|
||||
return th.Store.FileInfo().Save(fileInfoModel)
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) createReply(userID, message, hashtags string, parent *model.Post, createAt int64, pinned bool) (*model.Post, error) {
|
||||
replyModel := th.createPostModel(userID, parent.ChannelId, message, hashtags, parent.Type, createAt, pinned)
|
||||
replyModel.RootId = parent.Id
|
||||
return th.Store.Post().Save(replyModel)
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) deleteUserPosts(userID string) error {
|
||||
err := th.Store.Post().PermanentDeleteByUser(userID)
|
||||
if err != nil {
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) deleteUserFileInfos(userID string) error {
|
||||
if _, err := th.Store.FileInfo().PermanentDeleteByUser(userID); err != nil {
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) addUserToTeams(user *model.User, teamIDS []string) error {
|
||||
for _, teamID := range teamIDS {
|
||||
_, err := th.Store.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: user.Id}, -1)
|
||||
if err != nil {
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) addUserToChannels(user *model.User, channelIDS []string) error {
|
||||
for _, channelID := range channelIDS {
|
||||
_, err := th.Store.Channel().SaveMember(&model.ChannelMember{
|
||||
ChannelId: channelID,
|
||||
UserId: user.Id,
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) assertUsersMatchInAnyOrder(t *testing.T, expected, actual []*model.User) {
|
||||
expectedUsernames := make([]string, 0, len(expected))
|
||||
for _, user := range expected {
|
||||
user.Sanitize(map[string]bool{})
|
||||
expectedUsernames = append(expectedUsernames, user.Username)
|
||||
}
|
||||
|
||||
actualUsernames := make([]string, 0, len(actual))
|
||||
for _, user := range actual {
|
||||
user.Sanitize(map[string]bool{})
|
||||
actualUsernames = append(actualUsernames, user.Username)
|
||||
}
|
||||
|
||||
if assert.ElementsMatch(t, expectedUsernames, actualUsernames) {
|
||||
assert.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) checkPostInSearchResults(t *testing.T, postID string, searchResults map[string]*model.Post) {
|
||||
t.Helper()
|
||||
postIDS := make([]string, len(searchResults))
|
||||
for ID := range searchResults {
|
||||
postIDS = append(postIDS, ID)
|
||||
}
|
||||
assert.Contains(t, postIDS, postID, "Did not find expected post in search results.")
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) checkFileInfoInSearchResults(t *testing.T, fileID string, searchResults map[string]*model.FileInfo) {
|
||||
t.Helper()
|
||||
fileIDS := make([]string, len(searchResults))
|
||||
for ID := range searchResults {
|
||||
fileIDS = append(fileIDS, ID)
|
||||
}
|
||||
assert.Contains(t, fileIDS, fileID, "Did not find expected file in search results.")
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) checkChannelIdsMatch(t *testing.T, expected []string, results model.ChannelList) {
|
||||
t.Helper()
|
||||
channelIds := make([]string, len(results))
|
||||
for i, channel := range results {
|
||||
channelIds[i] = channel.Id
|
||||
}
|
||||
require.ElementsMatch(t, expected, channelIds)
|
||||
}
|
||||
|
||||
func (th *SearchTestHelper) checkChannelIdsMatchWithTeamData(t *testing.T, expected []string, results model.ChannelListWithTeamData) {
|
||||
t.Helper()
|
||||
channelIds := make([]string, len(results))
|
||||
for i, channel := range results {
|
||||
channelIds[i] = channel.Id
|
||||
}
|
||||
require.ElementsMatch(t, expected, channelIds)
|
||||
}
|
||||
|
||||
type ByChannelDisplayName model.ChannelList
|
||||
|
||||
func (s ByChannelDisplayName) Len() int { return len(s) }
|
||||
func (s ByChannelDisplayName) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
func (s ByChannelDisplayName) Less(i, j int) bool {
|
||||
if s[i].DisplayName != s[j].DisplayName {
|
||||
return s[i].DisplayName < s[j].DisplayName
|
||||
}
|
||||
|
||||
return s[i].Id < s[j].Id
|
||||
}
|
||||
1845
server/channels/store/searchtest/post_layer.go
Обычный файл
1845
server/channels/store/searchtest/post_layer.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
78
server/channels/store/searchtest/testlib.go
Обычный файл
78
server/channels/store/searchtest/testlib.go
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchtest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
EngineAll = "all"
|
||||
EngineMySql = "mysql"
|
||||
EnginePostgres = "postgres"
|
||||
EngineElasticSearch = "elasticsearch"
|
||||
EngineBleve = "bleve"
|
||||
)
|
||||
|
||||
type SearchTestEngine struct {
|
||||
Driver string
|
||||
BeforeTest func(*testing.T, store.Store)
|
||||
AfterTest func(*testing.T, store.Store)
|
||||
}
|
||||
|
||||
type searchTest struct {
|
||||
Name string
|
||||
Fn func(*testing.T, *SearchTestHelper)
|
||||
Tags []string
|
||||
Skip bool
|
||||
SkipMessage string
|
||||
}
|
||||
|
||||
func filterTestsByTag(tests []searchTest, tags ...string) []searchTest {
|
||||
filteredTests := []searchTest{}
|
||||
for _, test := range tests {
|
||||
if utils.StringInSlice(EngineAll, test.Tags) {
|
||||
filteredTests = append(filteredTests, test)
|
||||
continue
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if utils.StringInSlice(tag, test.Tags) {
|
||||
filteredTests = append(filteredTests, test)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTests
|
||||
}
|
||||
|
||||
func runTestSearch(t *testing.T, testEngine *SearchTestEngine, tests []searchTest, th *SearchTestHelper) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping advanced search test")
|
||||
return
|
||||
}
|
||||
|
||||
filteredTests := filterTestsByTag(tests, testEngine.Driver)
|
||||
|
||||
for _, test := range filteredTests {
|
||||
|
||||
if test.Skip {
|
||||
t.Log("SKIPPED: " + test.Name + ". Reason: " + test.SkipMessage)
|
||||
continue
|
||||
}
|
||||
|
||||
if testEngine.BeforeTest != nil {
|
||||
testEngine.BeforeTest(t, th.Store)
|
||||
}
|
||||
testName := test.Name
|
||||
testFn := test.Fn
|
||||
t.Run(testName, func(t *testing.T) { testFn(t, th) })
|
||||
if testEngine.AfterTest != nil {
|
||||
testEngine.AfterTest(t, th.Store)
|
||||
}
|
||||
}
|
||||
}
|
||||
875
server/channels/store/searchtest/user_layer.go
Обычный файл
875
server/channels/store/searchtest/user_layer.go
Обычный файл
@@ -0,0 +1,875 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package searchtest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
var searchUserStoreTests = []searchTest{
|
||||
{
|
||||
Name: "Should retrieve all users in a channel if the search term is empty",
|
||||
Fn: testGetAllUsersInChannelWithEmptyTerm,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should honor channel restrictions when autocompleting users",
|
||||
Fn: testHonorChannelRestrictionsAutocompletingUsers,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should honor team restrictions when autocompleting users",
|
||||
Fn: testHonorTeamRestrictionsAutocompletingUsers,
|
||||
Tags: []string{EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should return nothing if the user can't access the channels of a given search",
|
||||
Fn: testShouldReturnNothingWithoutProperAccess,
|
||||
Tags: []string{EngineAll},
|
||||
Skip: true,
|
||||
SkipMessage: "Failing when the ListOfAllowedChannels property is empty",
|
||||
},
|
||||
{
|
||||
Name: "Should autocomplete for user using username",
|
||||
Fn: testAutocompleteUserByUsername,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should autocomplete user searching by first name",
|
||||
Fn: testAutocompleteUserByFirstName,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should autocomplete user searching by last name",
|
||||
Fn: testAutocompleteUserByLastName,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should autocomplete for user using nickname",
|
||||
Fn: testAutocompleteUserByNickName,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should autocomplete for user using email",
|
||||
Fn: testAutocompleteUserByEmail,
|
||||
Tags: []string{EngineAll},
|
||||
Skip: true,
|
||||
SkipMessage: "Failing for multiple different reasons in the engines",
|
||||
},
|
||||
{
|
||||
Name: "Should be able not to match specific queries with mail",
|
||||
Fn: testShouldNotMatchSpecificQueriesEmail,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a user by part of its username splitted by Dot",
|
||||
Fn: testAutocompleteUserByUsernameWithDot,
|
||||
Tags: []string{EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a user by part of its username splitted by underscore",
|
||||
Fn: testAutocompleteUserByUsernameWithUnderscore,
|
||||
Tags: []string{EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to autocomplete a user by part of its username splitted by hyphen",
|
||||
Fn: testAutocompleteUserByUsernameWithHyphen,
|
||||
Tags: []string{EngineElasticSearch, EngineBleve},
|
||||
},
|
||||
{
|
||||
Name: "Should escape the percentage character",
|
||||
Fn: testShouldEscapePercentageCharacter,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should escape the dash character",
|
||||
Fn: testShouldEscapeUnderscoreCharacter,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to search inactive users",
|
||||
Fn: testShouldBeAbleToSearchInactiveUsers,
|
||||
Tags: []string{EngineMySql, EnginePostgres, EngineElasticSearch},
|
||||
},
|
||||
{
|
||||
Name: "Should be able to search filtering by role",
|
||||
Fn: testShouldBeAbleToSearchFilteringByRole,
|
||||
Tags: []string{EngineMySql, EnginePostgres, EngineElasticSearch},
|
||||
},
|
||||
{
|
||||
Name: "Should ignore leading @ when searching users",
|
||||
Fn: testShouldIgnoreLeadingAtSymbols,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should search users in a case insensitive manner",
|
||||
Fn: testSearchUsersShouldBeCaseInsensitive,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should support one or two character usernames and first/last names in search",
|
||||
Fn: testSearchOneTwoCharUsernamesAndFirstLastNames,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should support Korean characters",
|
||||
Fn: testShouldSupportKoreanCharacters,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should support search with a hyphen at the end of the term",
|
||||
Fn: testSearchWithHyphenAtTheEndOfTheTerm,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should support search all users in a team",
|
||||
Fn: testSearchUsersInTeam,
|
||||
Tags: []string{EngineElasticSearch},
|
||||
},
|
||||
{
|
||||
Name: "Should support search users by full name",
|
||||
Fn: testSearchUsersByFullName,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should support search all users in a team with username containing a dot",
|
||||
Fn: testSearchUsersInTeamUsernameWithDot,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should support search all users in a team with username containing a hyphen",
|
||||
Fn: testSearchUsersInTeamUsernameWithHyphen,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
{
|
||||
Name: "Should support search all users in a team with username containing a underscore",
|
||||
Fn: testSearchUsersInTeamUsernameWithUnderscore,
|
||||
Tags: []string{EngineAll},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSearchUserStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) {
|
||||
th := &SearchTestHelper{
|
||||
Store: s,
|
||||
}
|
||||
err := th.SetupBasicFixtures()
|
||||
require.NoError(t, err)
|
||||
defer th.CleanFixtures()
|
||||
runTestSearch(t, testEngine, searchUserStoreTests, th)
|
||||
}
|
||||
|
||||
func testGetAllUsersInChannelWithEmptyTerm(t *testing.T, th *SearchTestHelper) {
|
||||
options := &model.UserSearchOptions{
|
||||
AllowFullNames: true,
|
||||
Limit: model.UserSearchDefaultLimit,
|
||||
}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
|
||||
t.Run("Should be able to correctly honor limit when autocompleting", func(t *testing.T) {
|
||||
result, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.InChannel, 1)
|
||||
require.Len(t, result.OutOfChannel, 1)
|
||||
})
|
||||
|
||||
t.Run("Return all users in team", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
|
||||
t.Run("Return all users in teams even though some of them don't have a team associated", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
userAlternate, err := th.createUser("user-alternate", "user-alternate", "user", "alternate")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
userGuest, err := th.createGuest("user-guest", "user-guest", "user", "guest")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userGuest)
|
||||
|
||||
// In case teamId and channelId are empty our current logic goes through Search
|
||||
users, err := th.Store.User().Search("", "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2, th.UserAnotherTeam,
|
||||
userAlternate, userGuest}, users)
|
||||
})
|
||||
}
|
||||
|
||||
func testHonorChannelRestrictionsAutocompletingUsers(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("user-alternate", "user-alternate", "user", "alternate")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
guest, err := th.createGuest("guest", "guest", "guest", "one")
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToTeams(guest, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(guest, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(guest)
|
||||
t.Run("Autocomplete users with channel restrictions", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{th.ChannelBasic.Id}}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, userAlternate, guest}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Autocomplete users with term and channel restrictions", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{th.ChannelBasic.Id}}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alt", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Autocomplete users with all channels restricted", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{}, Channels: []string{}}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Autocomplete users with all channels restricted but with empty team", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{}, Channels: []string{}}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel("", th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Autocomplete users with empty team and channels restricted", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{th.ChannelBasic.Id}}
|
||||
// In case teamId and channelId are empty our current logic goes through Search
|
||||
users, err := th.Store.User().Search("", "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate, guest, th.User}, users)
|
||||
})
|
||||
}
|
||||
|
||||
func testHonorTeamRestrictionsAutocompletingUsers(t *testing.T, th *SearchTestHelper) {
|
||||
t.Run("Should return results for users in the team", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{th.Team.Id}}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return empty because we're filtering all the teams", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{}, Channels: []string{}}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return empty when searching in one team and filtering by another", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Teams: []string{th.AnotherTeam.Id}}
|
||||
users, err := th.Store.User().Search(th.Team.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users)
|
||||
|
||||
acusers, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, acusers.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, acusers.OutOfChannel)
|
||||
})
|
||||
}
|
||||
func testShouldReturnNothingWithoutProperAccess(t *testing.T, th *SearchTestHelper) {
|
||||
t.Run("Should return results users for the defined channel in the list", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ListOfAllowedChannels = []string{th.ChannelBasic.Id}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return empty because we're filtering all the channels", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
options.ListOfAllowedChannels = []string{}
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
func testAutocompleteUserByUsername(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternateusername", "alternatenick", "user", "alternate")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
}
|
||||
func testAutocompleteUserByFirstName(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("user-alternate", "user-alternate", "altfirstname", "lastname")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should autocomplete users when the first name is unique", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "altfirstname", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should autocomplete users for in the channel and out of the channel with the same first name", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicfirstname", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
func testAutocompleteUserByLastName(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("user-alternate", "user-alternate", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should return results when the last name is unique", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "altlastname", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return results for in the channel and out of the channel with the same last name", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basiclastname", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
func testAutocompleteUserByNickName(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternateusername", "alternatenickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should return results when the nickname is unique", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternatenickname", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return users that share the same part of the nickname", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicnickname", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
func testAutocompleteUserByEmail(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternateusername", "alternatenickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
userAlternate.Email = "useralt@test.email.com"
|
||||
_, err = th.Store.User().Update(userAlternate, false)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should autocomplete users when the email is unique", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, true, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "useralt@test.email.com", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should autocomplete users that share the same email user prefix", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, true, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "success_", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should autocomplete users that share the same email domain", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, true, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "simulator.amazon.com", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should search users when the email is unique", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, true, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "useralt@test.email.com", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
|
||||
})
|
||||
t.Run("Should search users that share the same email user prefix", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, true, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "success_", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
|
||||
})
|
||||
t.Run("Should search users that share the same email domain", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, true, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "simulator.amazon.com", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
|
||||
})
|
||||
}
|
||||
func testShouldNotMatchSpecificQueriesEmail(t *testing.T, th *SearchTestHelper) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "success_", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
}
|
||||
func testAutocompleteUserByUsernameWithDot(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate.username", "alternatenickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should return results when searching for the whole username with Dot", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate.username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return results when searching for part of the username including the Dot", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, ".username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return results when searching for part of the username not including the Dot", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
func testAutocompleteUserByUsernameWithUnderscore(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate_username", "alternatenickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should return results when searching for the whole username with underscore", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate_username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return results when searching for part of the username including the underscore", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "_username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return results when searching for part of the username not including the underscore", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
func testAutocompleteUserByUsernameWithHyphen(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate-username", "alternatenickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should return results when searching for the whole username with hyphen", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate-username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return results when searching for part of the username including the hyphen", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "-username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should return results when searching for part of the username not including the hyphen", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "username", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
func testShouldEscapePercentageCharacter(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternateusername", "alternate%nickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should autocomplete users escaping percentage symbol", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate%", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should search users escaping percentage symbol", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "alternate%", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
|
||||
})
|
||||
}
|
||||
func testShouldEscapeUnderscoreCharacter(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate_username", "alternatenickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should autocomplete users escaping underscore symbol", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate_", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should search users escaping underscore symbol", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "alternate_", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
|
||||
})
|
||||
}
|
||||
|
||||
func testShouldBeAbleToSearchInactiveUsers(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("basicusernamealternate", "alternatenickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
userAlternate.DeleteAt = model.GetMillis()
|
||||
_, err = th.Store.User().Update(userAlternate, true)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should autocomplete inactive users if we allow it", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, true)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should search inactive users if we allow it", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, true)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2, userAlternate}, users)
|
||||
})
|
||||
t.Run("Shouldn't autocomplete inactive users if we don't allow it", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Shouldn't search inactive users if we don't allow it", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2}, users)
|
||||
})
|
||||
}
|
||||
|
||||
func testShouldBeAbleToSearchFilteringByRole(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("basicusernamealternate", "alternatenickname", "firstname", "altlastname")
|
||||
require.NoError(t, err)
|
||||
userAlternate.Roles = "system_admin system_user"
|
||||
_, err = th.Store.User().Update(userAlternate, true)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
userAlternate2, err := th.createUser("basicusernamealternate2", "alternatenickname2", "firstname2", "altlastname2")
|
||||
require.NoError(t, err)
|
||||
userAlternate2.Roles = "system_user"
|
||||
_, err = th.Store.User().Update(userAlternate2, true)
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate2)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToTeams(userAlternate2, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should autocomplete users filtering by roles", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, true)
|
||||
options.Role = "system_admin"
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should search users filtering by roles", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, true)
|
||||
options.Role = "system_admin"
|
||||
users, err := th.Store.User().Search(th.Team.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
|
||||
})
|
||||
}
|
||||
|
||||
func testShouldIgnoreLeadingAtSymbols(t *testing.T, th *SearchTestHelper) {
|
||||
t.Run("Should autocomplete ignoring the @ symbol at the beginning", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "@basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should search ignoring the @ symbol at the beginning", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "@basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2}, users)
|
||||
})
|
||||
}
|
||||
|
||||
func testSearchUsersShouldBeCaseInsensitive(t *testing.T, th *SearchTestHelper) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "BaSiCUsErNaMe", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User2}, users.OutOfChannel)
|
||||
}
|
||||
|
||||
func testSearchOneTwoCharUsernamesAndFirstLastNames(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("ho", "alternatenickname", "zi", "k")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should support two characters in the full name", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "zi", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should support two characters in the username", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "ho", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
|
||||
func testShouldSupportKoreanCharacters(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate-username", "alternate-nickname", "서강준", "안신원")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
t.Run("Should support hanja korean characters", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "서강준", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
t.Run("Should support hangul korean characters", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "안신원", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
})
|
||||
}
|
||||
|
||||
func testSearchWithHyphenAtTheEndOfTheTerm(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate-username", "alternate-nickname", "altfirst", "altlast")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().AutocompleteUsersInChannel(th.Team.Id, th.ChannelBasic.Id, "alternate-", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users.InChannel)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users.OutOfChannel)
|
||||
}
|
||||
|
||||
func testSearchUsersInTeam(t *testing.T, th *SearchTestHelper) {
|
||||
t.Run("Should return all the team users", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2}, users)
|
||||
})
|
||||
t.Run("Should return all the team users with no team id", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search("", "basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2, th.UserAnotherTeam}, users)
|
||||
})
|
||||
t.Run("Should return all the team users filtered by username", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "basicusername1", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
|
||||
})
|
||||
t.Run("Should not return spurious results", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "falseuser", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users)
|
||||
})
|
||||
t.Run("Should return all the team users filtered by username and with channel restrictions", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{th.ChannelBasic.Id}}
|
||||
users, err := th.Store.User().Search(th.Team.Id, "basicusername", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
|
||||
})
|
||||
t.Run("Should return all the team users filtered by username and with all channel restricted", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
options.ViewRestrictions = &model.ViewUsersRestrictions{Channels: []string{}}
|
||||
users, err := th.Store.User().Search(th.Team.Id, "basicusername1", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users)
|
||||
})
|
||||
t.Run("Should honor the limit when searching users in team", func(t *testing.T) {
|
||||
optionsWithLimit := &model.UserSearchOptions{
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
users, err := th.Store.User().Search(th.Team.Id, "", optionsWithLimit)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func testSearchUsersInTeamUsernameWithDot(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate.username", "altnickname", "altfirst", "altlast")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "alternate.", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
|
||||
}
|
||||
|
||||
func testSearchUsersInTeamUsernameWithHyphen(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate-username", "altnickname", "altfirst", "altlast")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "alternate-", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
|
||||
}
|
||||
|
||||
func testSearchUsersInTeamUsernameWithUnderscore(t *testing.T, th *SearchTestHelper) {
|
||||
userAlternate, err := th.createUser("alternate_username", "altnickname", "altfirst", "altlast")
|
||||
require.NoError(t, err)
|
||||
defer th.deleteUser(userAlternate)
|
||||
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
|
||||
require.NoError(t, err)
|
||||
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
|
||||
require.NoError(t, err)
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "alternate_", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
|
||||
}
|
||||
|
||||
func testSearchUsersByFullName(t *testing.T, th *SearchTestHelper) {
|
||||
t.Run("Should search users by full name", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "basicfirstname", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User, th.User2}, users)
|
||||
})
|
||||
t.Run("Should search user by full name", func(t *testing.T) {
|
||||
options := createDefaultOptions(true, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "basicfirstname1", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{th.User}, users)
|
||||
})
|
||||
t.Run("Should return empty when search by full name and is deactivated", func(t *testing.T) {
|
||||
options := createDefaultOptions(false, false, false)
|
||||
users, err := th.Store.User().Search(th.Team.Id, "basicfirstname1", options)
|
||||
require.NoError(t, err)
|
||||
th.assertUsersMatchInAnyOrder(t, []*model.User{}, users)
|
||||
})
|
||||
}
|
||||
|
||||
func createDefaultOptions(allowFullName, allowEmails, allowInactive bool) *model.UserSearchOptions {
|
||||
return &model.UserSearchOptions{
|
||||
AllowFullNames: allowFullName,
|
||||
AllowEmails: allowEmails,
|
||||
AllowInactive: allowInactive,
|
||||
Limit: model.UserSearchDefaultLimit,
|
||||
}
|
||||
}
|
||||
65
server/channels/store/sqlstore/adapters.go
Обычный файл
65
server/channels/store/sqlstore/adapters.go
Обычный файл
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type jsonArray []string
|
||||
|
||||
func (a jsonArray) Value() (driver.Value, error) {
|
||||
var out bytes.Buffer
|
||||
if err := out.WriteByte('['); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, item := range a {
|
||||
if _, err := out.WriteString(strconv.Quote(item)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Skip the last element.
|
||||
if i < len(a)-1 {
|
||||
if err := out.WriteByte(','); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := out.WriteByte(']')
|
||||
return out.Bytes(), err
|
||||
}
|
||||
|
||||
type jsonStringVal string
|
||||
|
||||
func (str jsonStringVal) Value() (driver.Value, error) {
|
||||
return strconv.Quote(string(str)), nil
|
||||
}
|
||||
|
||||
type jsonKeyPath string
|
||||
|
||||
func (str jsonKeyPath) Value() (driver.Value, error) {
|
||||
return "{" + string(str) + "}", nil
|
||||
}
|
||||
|
||||
type TraceOnAdapter struct{}
|
||||
|
||||
func (t *TraceOnAdapter) Printf(format string, v ...any) {
|
||||
originalString := fmt.Sprintf(format, v...)
|
||||
newString := strings.ReplaceAll(originalString, "\n", " ")
|
||||
newString = strings.ReplaceAll(newString, "\t", " ")
|
||||
newString = strings.ReplaceAll(newString, "\"", "")
|
||||
mlog.Debug(newString)
|
||||
}
|
||||
|
||||
type JSONSerializable interface {
|
||||
ToJSON() string
|
||||
}
|
||||
21
server/channels/store/sqlstore/adapters_test.go
Обычный файл
21
server/channels/store/sqlstore/adapters_test.go
Обычный файл
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestJSONArray(t *testing.T) {
|
||||
input := []string{"a", "b"}
|
||||
|
||||
out, err := jsonArray(input).Value()
|
||||
require.NoError(t, err)
|
||||
outBuf, ok := out.([]byte)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []byte(`["a","b"]`), outBuf)
|
||||
}
|
||||
68
server/channels/store/sqlstore/audit_store.go
Обычный файл
68
server/channels/store/sqlstore/audit_store.go
Обычный файл
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlAuditStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlAuditStore(sqlStore *SqlStore) store.AuditStore {
|
||||
return &SqlAuditStore{sqlStore}
|
||||
}
|
||||
|
||||
func (s SqlAuditStore) Save(audit *model.Audit) error {
|
||||
audit.Id = model.NewId()
|
||||
audit.CreateAt = model.GetMillis()
|
||||
|
||||
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Audits
|
||||
(Id, CreateAt, UserId, Action, ExtraInfo, IpAddress, SessionId)
|
||||
VALUES
|
||||
(:Id, :CreateAt, :UserId, :Action, :ExtraInfo, :IpAddress, :SessionId)`, audit); err != nil {
|
||||
return errors.Wrapf(err, "failed to save Audit with userId=%s and action=%s", audit.UserId, audit.Action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlAuditStore) Get(userId string, offset int, limit int) (model.Audits, error) {
|
||||
if limit > 1000 {
|
||||
return nil, store.NewErrOutOfBounds(limit)
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Audits").
|
||||
OrderBy("CreateAt DESC").
|
||||
Limit(uint64(limit)).
|
||||
Offset(uint64(offset))
|
||||
|
||||
if userId != "" {
|
||||
query = query.Where(sq.Eq{"UserId": userId})
|
||||
}
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "audits_tosql")
|
||||
}
|
||||
|
||||
var audits model.Audits
|
||||
if err := s.GetReplicaX().Select(&audits, queryString, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get Audit list for userId=%s", userId)
|
||||
}
|
||||
return audits, nil
|
||||
}
|
||||
|
||||
func (s SqlAuditStore) PermanentDeleteByUser(userId string) error {
|
||||
if _, err := s.GetMasterX().Exec("DELETE FROM Audits WHERE UserId = ?", userId); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete Audit with userId=%s", userId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/audit_store_test.go
Обычный файл
14
server/channels/store/sqlstore/audit_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestAuditStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestAuditStore)
|
||||
}
|
||||
221
server/channels/store/sqlstore/bot_store.go
Обычный файл
221
server/channels/store/sqlstore/bot_store.go
Обычный файл
@@ -0,0 +1,221 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
// bot is a subset of the model.Bot type, omitting the model.User fields.
|
||||
type bot struct {
|
||||
UserId string `json:"user_id"`
|
||||
Description string `json:"description"`
|
||||
OwnerId string `json:"owner_id"`
|
||||
LastIconUpdate int64 `json:"last_icon_update"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
}
|
||||
|
||||
func botFromModel(b *model.Bot) *bot {
|
||||
return &bot{
|
||||
UserId: b.UserId,
|
||||
Description: b.Description,
|
||||
OwnerId: b.OwnerId,
|
||||
LastIconUpdate: b.LastIconUpdate,
|
||||
CreateAt: b.CreateAt,
|
||||
UpdateAt: b.UpdateAt,
|
||||
DeleteAt: b.DeleteAt,
|
||||
}
|
||||
}
|
||||
|
||||
// SqlBotStore is a store for managing bots in the database.
|
||||
// Bots are otherwise normal users with extra metadata record in the Bots table. The primary key
|
||||
// for a bot matches the primary key value for corresponding User record.
|
||||
type SqlBotStore struct {
|
||||
*SqlStore
|
||||
metrics einterfaces.MetricsInterface
|
||||
}
|
||||
|
||||
// newSqlBotStore creates an instance of SqlBotStore, registering the table schema in question.
|
||||
func newSqlBotStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.BotStore {
|
||||
return &SqlBotStore{
|
||||
SqlStore: sqlStore,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
// Get fetches the given bot in the database.
|
||||
func (us SqlBotStore) Get(botUserId string, includeDeleted bool) (*model.Bot, error) {
|
||||
var excludeDeletedSql = "AND b.DeleteAt = 0"
|
||||
if includeDeleted {
|
||||
excludeDeletedSql = ""
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
b.UserId,
|
||||
u.Username,
|
||||
u.FirstName AS DisplayName,
|
||||
b.Description,
|
||||
b.OwnerId,
|
||||
COALESCE(b.LastIconUpdate, 0) AS LastIconUpdate,
|
||||
b.CreateAt,
|
||||
b.UpdateAt,
|
||||
b.DeleteAt
|
||||
FROM
|
||||
Bots b
|
||||
JOIN
|
||||
Users u ON (u.Id = b.UserId)
|
||||
WHERE
|
||||
b.UserId = ?
|
||||
` + excludeDeletedSql + `
|
||||
`
|
||||
|
||||
var bot model.Bot
|
||||
if err := us.GetReplicaX().Get(&bot, query, botUserId); err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("Bot", botUserId)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "selectone: user_id=%s", botUserId)
|
||||
}
|
||||
|
||||
return &bot, nil
|
||||
}
|
||||
|
||||
// GetAll fetches from all bots in the database.
|
||||
func (us SqlBotStore) GetAll(options *model.BotGetOptions) ([]*model.Bot, error) {
|
||||
var conditions []string
|
||||
var conditionsSql string
|
||||
var additionalJoin string
|
||||
var args []any
|
||||
|
||||
if !options.IncludeDeleted {
|
||||
conditions = append(conditions, "b.DeleteAt = 0")
|
||||
}
|
||||
if options.OwnerId != "" {
|
||||
conditions = append(conditions, "b.OwnerId = ?")
|
||||
args = append(args, options.OwnerId)
|
||||
}
|
||||
if options.OnlyOrphaned {
|
||||
additionalJoin = "JOIN Users o ON (o.Id = b.OwnerId)"
|
||||
conditions = append(conditions, "o.DeleteAt != 0")
|
||||
}
|
||||
|
||||
if len(conditions) > 0 {
|
||||
conditionsSql = "WHERE " + strings.Join(conditions, " AND ")
|
||||
}
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
b.UserId,
|
||||
u.Username,
|
||||
u.FirstName AS DisplayName,
|
||||
b.Description,
|
||||
b.OwnerId,
|
||||
COALESCE(b.LastIconUpdate, 0) AS LastIconUpdate,
|
||||
b.CreateAt,
|
||||
b.UpdateAt,
|
||||
b.DeleteAt
|
||||
FROM
|
||||
Bots b
|
||||
JOIN
|
||||
Users u ON (u.Id = b.UserId)
|
||||
` + additionalJoin + `
|
||||
` + conditionsSql + `
|
||||
ORDER BY
|
||||
b.CreateAt ASC,
|
||||
u.Username ASC
|
||||
LIMIT
|
||||
?
|
||||
OFFSET
|
||||
?
|
||||
`
|
||||
// append limit, offset
|
||||
args = append(args, options.PerPage, options.Page*options.PerPage)
|
||||
|
||||
bots := []*model.Bot{}
|
||||
if err := us.GetReplicaX().Select(&bots, sql, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting all bots")
|
||||
}
|
||||
|
||||
return bots, nil
|
||||
}
|
||||
|
||||
// Save persists a new bot to the database.
|
||||
// It assumes the corresponding user was saved via the user store.
|
||||
func (us SqlBotStore) Save(bot *model.Bot) (*model.Bot, error) {
|
||||
bot = bot.Clone()
|
||||
bot.PreSave()
|
||||
|
||||
if err := bot.IsValid(); err != nil { // TODO: change to return error in v6.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := us.GetMasterX().NamedExec(`INSERT INTO Bots
|
||||
(UserId, Description, OwnerId, LastIconUpdate, CreateAt, UpdateAt, DeleteAt)
|
||||
VALUES
|
||||
(:UserId, :Description, :OwnerId, :LastIconUpdate, :CreateAt, :UpdateAt, :DeleteAt)`, botFromModel(bot)); err != nil {
|
||||
return nil, errors.Wrapf(err, "insert: user_id=%s", bot.UserId)
|
||||
}
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// Update persists an updated bot to the database.
|
||||
// It assumes the corresponding user was updated via the user store.
|
||||
func (us SqlBotStore) Update(bot *model.Bot) (*model.Bot, error) {
|
||||
bot = bot.Clone()
|
||||
|
||||
bot.PreUpdate()
|
||||
if err := bot.IsValid(); err != nil { // TODO: needs to return error in v6
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldBot, err := us.Get(bot.UserId, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldBot.Description = bot.Description
|
||||
oldBot.OwnerId = bot.OwnerId
|
||||
oldBot.LastIconUpdate = bot.LastIconUpdate
|
||||
oldBot.UpdateAt = bot.UpdateAt
|
||||
oldBot.DeleteAt = bot.DeleteAt
|
||||
bot = oldBot
|
||||
|
||||
res, err := us.GetMasterX().NamedExec(`UPDATE Bots
|
||||
SET Description=:Description, OwnerId=:OwnerId, LastIconUpdate=:LastIconUpdate,
|
||||
UpdateAt=:UpdateAt, DeleteAt=:DeleteAt
|
||||
WHERE UserId=:UserId`, botFromModel(bot))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "update: user_id=%s", bot.UserId)
|
||||
}
|
||||
count, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error while getting rows_affected")
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, fmt.Errorf("unexpected count while updating bot: count=%d, userId=%s", count, bot.UserId)
|
||||
}
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// PermanentDelete removes the bot from the database altogether.
|
||||
// If the corresponding user is to be deleted, it must be done via the user store.
|
||||
func (us SqlBotStore) PermanentDelete(botUserId string) error {
|
||||
query := "DELETE FROM Bots WHERE UserId = ?"
|
||||
if _, err := us.GetMasterX().Exec(query, botUserId); err != nil {
|
||||
return store.NewErrInvalidInput("Bot", "UserId", botUserId).Wrap(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/bot_store_test.go
Обычный файл
14
server/channels/store/sqlstore/bot_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestBotStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestBotStore)
|
||||
}
|
||||
269
server/channels/store/sqlstore/channel_member_history_store.go
Обычный файл
269
server/channels/store/sqlstore/channel_member_history_store.go
Обычный файл
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SqlChannelMemberHistoryStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlChannelMemberHistoryStore(sqlStore *SqlStore) store.ChannelMemberHistoryStore {
|
||||
return &SqlChannelMemberHistoryStore{
|
||||
SqlStore: sqlStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SqlChannelMemberHistoryStore) LogJoinEvent(userId string, channelId string, joinTime int64) error {
|
||||
channelMemberHistory := &model.ChannelMemberHistory{
|
||||
UserId: userId,
|
||||
ChannelId: channelId,
|
||||
JoinTime: joinTime,
|
||||
}
|
||||
|
||||
if _, err := s.GetMasterX().NamedExec(`INSERT INTO ChannelMemberHistory
|
||||
(UserId, ChannelId, JoinTime)
|
||||
VALUES
|
||||
(:UserId, :ChannelId, :JoinTime)`, channelMemberHistory); err != nil {
|
||||
return errors.Wrapf(err, "LogJoinEvent userId=%s channelId=%s joinTime=%d", userId, channelId, joinTime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlChannelMemberHistoryStore) LogLeaveEvent(userId string, channelId string, leaveTime int64) error {
|
||||
query, params, err := s.getQueryBuilder().
|
||||
Update("ChannelMemberHistory").
|
||||
Set("LeaveTime", leaveTime).
|
||||
Where(sq.And{
|
||||
sq.Eq{"UserId": userId},
|
||||
sq.Eq{"ChannelId": channelId},
|
||||
sq.Eq{"LeaveTime": nil},
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "channel_member_history_to_sql")
|
||||
}
|
||||
sqlResult, err := s.GetMasterX().Exec(query, params...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "LogLeaveEvent userId=%s channelId=%s leaveTime=%d", userId, channelId, leaveTime)
|
||||
}
|
||||
|
||||
if rows, err := sqlResult.RowsAffected(); err == nil && rows != 1 {
|
||||
// there was no join event to update - this is best effort, so no need to raise an error
|
||||
mlog.Warn("Channel join event for user and channel not found", mlog.String("user", userId), mlog.String("channel", channelId))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
|
||||
useChannelMemberHistory, err := s.hasDataAtOrBefore(startTime)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "hasDataAtOrBefore startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
|
||||
}
|
||||
|
||||
if useChannelMemberHistory {
|
||||
// the export period starts after the ChannelMemberHistory table was first introduced, so we can use the
|
||||
// data from it for our export
|
||||
channelMemberHistories, err2 := s.getFromChannelMemberHistoryTable(startTime, endTime, channelId)
|
||||
if err2 != nil {
|
||||
return nil, errors.Wrapf(err2, "getFromChannelMemberHistoryTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
|
||||
}
|
||||
return channelMemberHistories, nil
|
||||
}
|
||||
// the export period starts before the ChannelMemberHistory table was introduced, so we need to fake the
|
||||
// data by assuming that anybody who has ever joined the channel in question was present during the export period.
|
||||
// this may not always be true, but it's better than saying that somebody wasn't there when they were
|
||||
channelMemberHistories, err := s.getFromChannelMembersTable(startTime, endTime, channelId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "getFromChannelMembersTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
|
||||
}
|
||||
return channelMemberHistories, nil
|
||||
}
|
||||
|
||||
func (s SqlChannelMemberHistoryStore) hasDataAtOrBefore(time int64) (bool, error) {
|
||||
type NullableCountResult struct {
|
||||
Min sql.NullInt64
|
||||
}
|
||||
query, _, err := s.getQueryBuilder().Select("MIN(JoinTime) as Min").From("ChannelMemberHistory").ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "channel_member_history_to_sql")
|
||||
}
|
||||
var result NullableCountResult
|
||||
if err := s.GetReplicaX().Get(&result, query); err != nil {
|
||||
return false, err
|
||||
} else if result.Min.Valid {
|
||||
return result.Min.Int64 <= time, nil
|
||||
} else {
|
||||
// if the result was null, there are no rows in the table, so there is no data from before
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
|
||||
query, args, err := s.getQueryBuilder().
|
||||
Select(`cmh.*, u.Email AS "Email", u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt`).
|
||||
From("ChannelMemberHistory cmh").
|
||||
Join("Users u ON cmh.UserId = u.Id").
|
||||
LeftJoin("Bots ON Bots.UserId = u.Id").
|
||||
Where(sq.And{
|
||||
sq.Eq{"cmh.ChannelId": channelId},
|
||||
sq.LtOrEq{"cmh.JoinTime": endTime},
|
||||
sq.Or{
|
||||
sq.Eq{"cmh.LeaveTime": nil},
|
||||
sq.GtOrEq{"cmh.LeaveTime": startTime},
|
||||
},
|
||||
}).
|
||||
OrderBy("cmh.JoinTime ASC").ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "channel_member_history_to_sql")
|
||||
}
|
||||
histories := []*model.ChannelMemberHistoryResult{}
|
||||
if err := s.GetReplicaX().Select(&histories, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return histories, nil
|
||||
}
|
||||
|
||||
func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
|
||||
query, args, err := s.getQueryBuilder().
|
||||
Select(`ch.ChannelId, ch.UserId, u.Email AS "Email", u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt`).
|
||||
Distinct().
|
||||
From("ChannelMembers ch").
|
||||
Join("Users u ON ch.UserId = u.id").
|
||||
LeftJoin("Bots ON Bots.UserId = u.id").
|
||||
Where(sq.Eq{"ch.ChannelId": channelId}).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "channel_member_history_to_sql")
|
||||
}
|
||||
|
||||
histories := []*model.ChannelMemberHistoryResult{}
|
||||
if err := s.GetReplicaX().Select(&histories, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// we have to fill in the join/leave times, because that data doesn't exist in the channel members table
|
||||
for _, channelMemberHistory := range histories {
|
||||
channelMemberHistory.JoinTime = startTime
|
||||
channelMemberHistory.LeaveTime = model.NewInt64(endTime)
|
||||
}
|
||||
return histories, nil
|
||||
}
|
||||
|
||||
// PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by
|
||||
// the global or a granular retention policy.
|
||||
// See `genericPermanentDeleteBatchForRetentionPolicies` for details.
|
||||
func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
|
||||
builder := s.getQueryBuilder().
|
||||
Select("ChannelMemberHistory.ChannelId, ChannelMemberHistory.UserId, ChannelMemberHistory.JoinTime").
|
||||
From("ChannelMemberHistory")
|
||||
return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
|
||||
BaseBuilder: builder,
|
||||
Table: "ChannelMemberHistory",
|
||||
TimeColumn: "LeaveTime",
|
||||
PrimaryKeys: []string{"ChannelId", "UserId", "JoinTime"},
|
||||
ChannelIDTable: "ChannelMemberHistory",
|
||||
NowMillis: now,
|
||||
GlobalPolicyEndTime: globalPolicyEndTime,
|
||||
Limit: limit,
|
||||
}, s.SqlStore, cursor)
|
||||
}
|
||||
|
||||
// DeleteOrphanedRows removes entries from ChannelMemberHistory when a corresponding channel no longer exists.
|
||||
func (s SqlChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
|
||||
// We need the extra level of nesting to deal with MySQL's locking
|
||||
const query = `
|
||||
DELETE FROM ChannelMemberHistory WHERE (ChannelId, UserId, JoinTime) IN (
|
||||
SELECT * FROM (
|
||||
SELECT ChannelId, UserId, JoinTime FROM ChannelMemberHistory
|
||||
LEFT JOIN Channels ON ChannelMemberHistory.ChannelId = Channels.Id
|
||||
WHERE Channels.Id IS NULL
|
||||
LIMIT ?
|
||||
) AS A
|
||||
)`
|
||||
result, err := s.GetMasterX().Exec(query, limit)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
func (s SqlChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
|
||||
var (
|
||||
query string
|
||||
args []any
|
||||
err error
|
||||
)
|
||||
|
||||
if s.DriverName() == model.DatabaseDriverPostgres {
|
||||
var innerSelect string
|
||||
innerSelect, args, err = s.getQueryBuilder().
|
||||
Select("ctid").
|
||||
From("ChannelMemberHistory").
|
||||
Where(sq.And{
|
||||
sq.NotEq{"LeaveTime": nil},
|
||||
sq.LtOrEq{"LeaveTime": endTime},
|
||||
}).Limit(uint64(limit)).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "channel_member_history_to_sql")
|
||||
}
|
||||
query, _, err = s.getQueryBuilder().
|
||||
Delete("ChannelMemberHistory").
|
||||
Where(fmt.Sprintf(
|
||||
"ctid IN (%s)", innerSelect,
|
||||
)).ToSql()
|
||||
} else {
|
||||
query, args, err = s.getQueryBuilder().
|
||||
Delete("ChannelMemberHistory").
|
||||
Where(sq.And{
|
||||
sq.NotEq{"LeaveTime": nil},
|
||||
sq.LtOrEq{"LeaveTime": endTime},
|
||||
}).
|
||||
Limit(uint64(limit)).ToSql()
|
||||
}
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "channel_member_history_to_sql")
|
||||
}
|
||||
sqlResult, err := s.GetMasterX().Exec(query, args...)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "PermanentDeleteBatch endTime=%d limit=%d", endTime, limit)
|
||||
}
|
||||
|
||||
rowsAffected, err := sqlResult.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "PermanentDeleteBatch endTime=%d limit=%d", endTime, limit)
|
||||
}
|
||||
return rowsAffected, nil
|
||||
}
|
||||
|
||||
// GetChannelsLeftSince returns list of channels that the user has left after a given time,
|
||||
// but has not rejoined again.
|
||||
func (s SqlChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) {
|
||||
query, params, err := s.getQueryBuilder().
|
||||
Select("ChannelId").
|
||||
From("ChannelMemberHistory").
|
||||
GroupBy("ChannelId").
|
||||
Where(sq.Eq{"UserId": userID}).
|
||||
Having("MAX(LeaveTime) > MAX(JoinTime) AND MAX(LeaveTime) IS NOT NULL AND MAX(LeaveTime) >= ?", since).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "channel_member_history_to_sql")
|
||||
}
|
||||
channelIds := []string{}
|
||||
err = s.GetReplicaX().Select(&channelIds, query, params...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "GetChannelsLeftSince userId=%s since=%d", userID, since)
|
||||
}
|
||||
|
||||
return channelIds, nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestChannelMemberHistoryStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestChannelMemberHistoryStore)
|
||||
}
|
||||
4714
server/channels/store/sqlstore/channel_store.go
Обычный файл
4714
server/channels/store/sqlstore/channel_store.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
1127
server/channels/store/sqlstore/channel_store_categories.go
Обычный файл
1127
server/channels/store/sqlstore/channel_store_categories.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
14
server/channels/store/sqlstore/channel_store_categories_test.go
Обычный файл
14
server/channels/store/sqlstore/channel_store_categories_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestChannelStoreCategories(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestChannelStoreCategories)
|
||||
}
|
||||
1397
server/channels/store/sqlstore/channel_store_test.go
Обычный файл
1397
server/channels/store/sqlstore/channel_store_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
139
server/channels/store/sqlstore/cluster_discovery_store.go
Обычный файл
139
server/channels/store/sqlstore/cluster_discovery_store.go
Обычный файл
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type sqlClusterDiscoveryStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlClusterDiscoveryStore(sqlStore *SqlStore) store.ClusterDiscoveryStore {
|
||||
return &sqlClusterDiscoveryStore{sqlStore}
|
||||
}
|
||||
|
||||
func (s sqlClusterDiscoveryStore) Save(ClusterDiscovery *model.ClusterDiscovery) error {
|
||||
ClusterDiscovery.PreSave()
|
||||
if err := ClusterDiscovery.IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := s.GetMasterX().NamedExec(`
|
||||
INSERT INTO
|
||||
ClusterDiscovery
|
||||
(Id, Type, ClusterName, Hostname, GossipPort, Port, CreateAt, LastPingAt)
|
||||
VALUES
|
||||
(:Id, :Type, :ClusterName, :Hostname, :GossipPort, :Port, :CreateAt, :LastPingAt)
|
||||
`, ClusterDiscovery); err != nil {
|
||||
return errors.Wrap(err, "failed to save ClusterDiscovery")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s sqlClusterDiscoveryStore) Delete(ClusterDiscovery *model.ClusterDiscovery) (bool, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Delete("ClusterDiscovery").
|
||||
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
|
||||
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
|
||||
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "cluster_discovery_tosql")
|
||||
}
|
||||
|
||||
res, err := s.GetMasterX().Exec(queryString, args...)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to delete ClusterDiscovery")
|
||||
}
|
||||
|
||||
count, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to count rows affected")
|
||||
}
|
||||
|
||||
return count != 0, nil
|
||||
}
|
||||
|
||||
func (s sqlClusterDiscoveryStore) Exists(ClusterDiscovery *model.ClusterDiscovery) (bool, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("COUNT(*)").
|
||||
From("ClusterDiscovery").
|
||||
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
|
||||
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
|
||||
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "cluster_discovery_tosql")
|
||||
}
|
||||
|
||||
var count int
|
||||
if err := s.GetMasterX().Get(&count, queryString, args...); err != nil {
|
||||
return false, errors.Wrap(err, "failed to count ClusterDiscovery")
|
||||
}
|
||||
|
||||
return count != 0, nil
|
||||
}
|
||||
|
||||
func (s sqlClusterDiscoveryStore) GetAll(ClusterDiscoveryType, clusterName string) ([]*model.ClusterDiscovery, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("*").
|
||||
From("ClusterDiscovery").
|
||||
Where(sq.Eq{"Type": ClusterDiscoveryType}).
|
||||
Where(sq.Eq{"ClusterName": clusterName}).
|
||||
Where(sq.Gt{"LastPingAt": model.GetMillis() - model.CDSOfflineAfterMillis})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cluster_discovery_tosql")
|
||||
}
|
||||
|
||||
list := []*model.ClusterDiscovery{}
|
||||
if err := s.GetMasterX().Select(&list, queryString, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find ClusterDiscovery")
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s sqlClusterDiscoveryStore) SetLastPingAt(ClusterDiscovery *model.ClusterDiscovery) error {
|
||||
query := s.getQueryBuilder().
|
||||
Update("ClusterDiscovery").
|
||||
Set("LastPingAt", model.GetMillis()).
|
||||
Where(sq.Eq{"Type": ClusterDiscovery.Type}).
|
||||
Where(sq.Eq{"ClusterName": ClusterDiscovery.ClusterName}).
|
||||
Where(sq.Eq{"Hostname": ClusterDiscovery.Hostname})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cluster_discovery_tosql")
|
||||
}
|
||||
|
||||
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to update ClusterDiscovery")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s sqlClusterDiscoveryStore) Cleanup() error {
|
||||
query := s.getQueryBuilder().
|
||||
Delete("ClusterDiscovery").
|
||||
Where(sq.Lt{"LastPingAt": model.GetMillis() - model.CDSOfflineAfterMillis})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cluster_discovery_tosql")
|
||||
}
|
||||
|
||||
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to delete ClusterDiscoveries")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/cluster_discovery_store_test.go
Обычный файл
14
server/channels/store/sqlstore/cluster_discovery_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestClusterDiscoveryStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestClusterDiscoveryStore)
|
||||
}
|
||||
230
server/channels/store/sqlstore/command_store.go
Обычный файл
230
server/channels/store/sqlstore/command_store.go
Обычный файл
@@ -0,0 +1,230 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlCommandStore struct {
|
||||
*SqlStore
|
||||
|
||||
commandsQuery sq.SelectBuilder
|
||||
}
|
||||
|
||||
func newSqlCommandStore(sqlStore *SqlStore) store.CommandStore {
|
||||
s := &SqlCommandStore{SqlStore: sqlStore}
|
||||
|
||||
s.commandsQuery = s.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Commands")
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) Save(command *model.Command) (*model.Command, error) {
|
||||
if command.Id != "" {
|
||||
return nil, store.NewErrInvalidInput("Command", "CommandId", command.Id)
|
||||
}
|
||||
|
||||
command.PreSave()
|
||||
if err := command.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Trigger is a keyword
|
||||
trigger := s.toReserveCase("trigger")
|
||||
|
||||
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Commands (Id, Token, CreateAt,
|
||||
UpdateAt, DeleteAt, CreatorId, TeamId, `+trigger+`, Method, Username,
|
||||
IconURL, AutoComplete, AutoCompleteDesc, AutoCompleteHint, DisplayName, Description,
|
||||
URL, PluginId)
|
||||
VALUES (:Id, :Token, :CreateAt, :UpdateAt, :DeleteAt, :CreatorId, :TeamId, :Trigger, :Method,
|
||||
:Username, :IconURL, :AutoComplete, :AutoCompleteDesc, :AutoCompleteHint, :DisplayName,
|
||||
:Description, :URL, :PluginId)`, command); err != nil {
|
||||
return nil, errors.Wrapf(err, "insert: command_id=%s", command.Id)
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) Get(id string) (*model.Command, error) {
|
||||
var command model.Command
|
||||
|
||||
query, args, err := s.commandsQuery.
|
||||
Where(sq.Eq{"Id": id, "DeleteAt": 0}).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "commands_tosql")
|
||||
}
|
||||
if err = s.GetReplicaX().Get(&command, query, args...); err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("Command", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "selectone: command_id=%s", id)
|
||||
}
|
||||
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) GetByTeam(teamId string) ([]*model.Command, error) {
|
||||
commands := []*model.Command{}
|
||||
|
||||
sql, args, err := s.commandsQuery.
|
||||
Where(sq.Eq{"TeamId": teamId, "DeleteAt": 0}).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "commands_tosql")
|
||||
}
|
||||
if err := s.GetReplicaX().Select(&commands, sql, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "select: team_id=%s", teamId)
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) GetByTrigger(teamId string, trigger string) (*model.Command, error) {
|
||||
var command model.Command
|
||||
var triggerStr string
|
||||
if s.DriverName() == "mysql" {
|
||||
triggerStr = "`Trigger`"
|
||||
} else {
|
||||
triggerStr = "\"trigger\""
|
||||
}
|
||||
|
||||
query, args, err := s.commandsQuery.
|
||||
Where(sq.Eq{"TeamId": teamId, "DeleteAt": 0, triggerStr: trigger}).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "commands_tosql")
|
||||
}
|
||||
|
||||
if err := s.GetReplicaX().Get(&command, query, args...); err == sql.ErrNoRows {
|
||||
errorId := "teamId=" + teamId + ", trigger=" + trigger
|
||||
return nil, store.NewErrNotFound("Command", errorId)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "selectone: team_id=%s, trigger=%s", teamId, trigger)
|
||||
}
|
||||
|
||||
return &command, nil
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) Delete(commandId string, time int64) error {
|
||||
sql, args, err := s.getQueryBuilder().
|
||||
Update("Commands").
|
||||
SetMap(sq.Eq{"DeleteAt": time, "UpdateAt": time}).
|
||||
Where(sq.Eq{"Id": commandId}).ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "commands_tosql")
|
||||
}
|
||||
|
||||
_, err = s.GetMasterX().Exec(sql, args...)
|
||||
if err != nil {
|
||||
errors.Wrapf(err, "delete: command_id=%s", commandId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) PermanentDeleteByTeam(teamId string) error {
|
||||
sql, args, err := s.getQueryBuilder().
|
||||
Delete("Commands").
|
||||
Where(sq.Eq{"TeamId": teamId}).ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "commands_tosql")
|
||||
}
|
||||
_, err = s.GetMasterX().Exec(sql, args...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "delete: team_id=%s", teamId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) PermanentDeleteByUser(userId string) error {
|
||||
sql, args, err := s.getQueryBuilder().
|
||||
Delete("Commands").
|
||||
Where(sq.Eq{"CreatorId": userId}).ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "commands_tosql")
|
||||
}
|
||||
_, err = s.GetMasterX().Exec(sql, args...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "delete: user_id=%s", userId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) Update(cmd *model.Command) (*model.Command, error) {
|
||||
cmd.UpdateAt = model.GetMillis()
|
||||
|
||||
if err := cmd.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Update("Commands").
|
||||
Set("Token", cmd.Token).
|
||||
Set("CreateAt", cmd.CreateAt).
|
||||
Set("UpdateAt", cmd.UpdateAt).
|
||||
Set("CreatorId", cmd.CreatorId).
|
||||
Set("TeamId", cmd.TeamId).
|
||||
Set("Method", cmd.Method).
|
||||
Set("Username", cmd.Username).
|
||||
Set("IconURL", cmd.IconURL).
|
||||
Set("AutoComplete", cmd.AutoComplete).
|
||||
Set("AutoCompleteDesc", cmd.AutoCompleteDesc).
|
||||
Set("AutoCompleteHint", cmd.AutoCompleteHint).
|
||||
Set("DisplayName", cmd.DisplayName).
|
||||
Set("Description", cmd.Description).
|
||||
Set("URL", cmd.URL).
|
||||
Set("PluginId", cmd.PluginId).
|
||||
Where(sq.Eq{"Id": cmd.Id})
|
||||
|
||||
// Trigger is a keyword
|
||||
query = query.Set(s.toReserveCase("trigger"), cmd.Trigger)
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "commands_tosql")
|
||||
}
|
||||
|
||||
res, err := s.GetMasterX().Exec(queryString, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to update commands")
|
||||
}
|
||||
count, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error while getting rows_affected")
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, fmt.Errorf("unexpected count while updating commands: count=%d, Id=%s", count, cmd.Id)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (s SqlCommandStore) AnalyticsCommandCount(teamId string) (int64, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("COUNT(*)").
|
||||
From("Commands").
|
||||
Where(sq.Eq{"DeleteAt": 0})
|
||||
|
||||
if teamId != "" {
|
||||
query = query.Where(sq.Eq{"TeamId": teamId})
|
||||
}
|
||||
|
||||
sql, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "commands_tosql")
|
||||
}
|
||||
|
||||
var c int64
|
||||
err = s.GetReplicaX().Get(&c, sql, args...)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "unable to count the commands: team_id=%s", teamId)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/command_store_test.go
Обычный файл
14
server/channels/store/sqlstore/command_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestCommandStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestCommandStore)
|
||||
}
|
||||
109
server/channels/store/sqlstore/command_webhook_store.go
Обычный файл
109
server/channels/store/sqlstore/command_webhook_store.go
Обычный файл
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SqlCommandWebhookStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlCommandWebhookStore(sqlStore *SqlStore) store.CommandWebhookStore {
|
||||
return &SqlCommandWebhookStore{sqlStore}
|
||||
}
|
||||
|
||||
func (s SqlCommandWebhookStore) Save(webhook *model.CommandWebhook) (*model.CommandWebhook, error) {
|
||||
if webhook.Id != "" {
|
||||
return nil, store.NewErrInvalidInput("CommandWebhook", "id", webhook.Id)
|
||||
}
|
||||
|
||||
webhook.PreSave()
|
||||
if err := webhook.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := s.GetMasterX().NamedExec(`INSERT INTO CommandWebhooks
|
||||
(Id,CreateAt,CommandId,UserId,ChannelId,RootId,UseCount)
|
||||
Values
|
||||
(:Id, :CreateAt, :CommandId, :UserId, :ChannelId, :RootId, :UseCount)`, webhook); err != nil {
|
||||
return nil, errors.Wrapf(err, "save: id=%s", webhook.Id)
|
||||
}
|
||||
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
func (s SqlCommandWebhookStore) Get(id string) (*model.CommandWebhook, error) {
|
||||
var webhook model.CommandWebhook
|
||||
|
||||
exptime := model.GetMillis() - model.CommandWebhookLifetime
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("*").
|
||||
From("CommandWebhooks").
|
||||
Where(sq.Eq{"Id": id}).
|
||||
Where(sq.Gt{"CreateAt": exptime})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get_tosql")
|
||||
}
|
||||
|
||||
if err := s.GetReplicaX().Get(&webhook, queryString, args...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("CommandWebhook", id)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "get: id=%s", id)
|
||||
}
|
||||
|
||||
return &webhook, nil
|
||||
}
|
||||
|
||||
func (s SqlCommandWebhookStore) TryUse(id string, limit int) error {
|
||||
query := s.getQueryBuilder().
|
||||
Update("CommandWebhooks").
|
||||
Set("UseCount", sq.Expr("UseCount + 1")).
|
||||
Where(sq.Eq{"Id": id}).
|
||||
Where(sq.Lt{"UseCount": limit})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tryuse_tosql")
|
||||
}
|
||||
|
||||
if sqlResult, err := s.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return errors.Wrapf(err, "tryuse: id=%s limit=%d", id, limit)
|
||||
} else if rows, err := sqlResult.RowsAffected(); rows == 0 {
|
||||
return store.NewErrInvalidInput("CommandWebhook", "id", id).Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlCommandWebhookStore) Cleanup() {
|
||||
mlog.Debug("Cleaning up command webhook store.")
|
||||
exptime := model.GetMillis() - model.CommandWebhookLifetime
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Delete("CommandWebhooks").
|
||||
Where(sq.Lt{"CreateAt": exptime})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
mlog.Error("Failed to build query when trying to perform a cleanup in command webhook store.")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
mlog.Error("Unable to cleanup command webhook store.")
|
||||
}
|
||||
}
|
||||
14
server/channels/store/sqlstore/command_webhook_store_test.go
Обычный файл
14
server/channels/store/sqlstore/command_webhook_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestCommandWebhookStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestCommandWebhookStore)
|
||||
}
|
||||
329
server/channels/store/sqlstore/compliance_store.go
Обычный файл
329
server/channels/store/sqlstore/compliance_store.go
Обычный файл
@@ -0,0 +1,329 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlComplianceStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlComplianceStore(sqlStore *SqlStore) store.ComplianceStore {
|
||||
return &SqlComplianceStore{sqlStore}
|
||||
}
|
||||
|
||||
func (s SqlComplianceStore) Save(compliance *model.Compliance) (*model.Compliance, error) {
|
||||
compliance.PreSave()
|
||||
if err := compliance.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// DESC is a keyword
|
||||
desc := s.toReserveCase("desc")
|
||||
|
||||
query := `INSERT INTO Compliances (Id, CreateAt, UserId, Status, Count, ` + desc + `, Type, StartAt, EndAt, Keywords, Emails)
|
||||
VALUES
|
||||
(:Id, :CreateAt, :UserId, :Status, :Count, :Desc, :Type, :StartAt, :EndAt, :Keywords, :Emails)`
|
||||
if _, err := s.GetMasterX().NamedExec(query, compliance); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save Compliance")
|
||||
}
|
||||
return compliance, nil
|
||||
}
|
||||
|
||||
func (s SqlComplianceStore) Update(compliance *model.Compliance) (*model.Compliance, error) {
|
||||
if err := compliance.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Update("Compliances").
|
||||
Set("CreateAt", compliance.CreateAt).
|
||||
Set("UserId", compliance.UserId).
|
||||
Set("Status", compliance.Status).
|
||||
Set("Count", compliance.Count).
|
||||
Set("Type", compliance.Type).
|
||||
Set("StartAt", compliance.StartAt).
|
||||
Set("EndAt", compliance.EndAt).
|
||||
Set("Keywords", compliance.Keywords).
|
||||
Set("Emails", compliance.Emails).
|
||||
Where(sq.Eq{"Id": compliance.Id})
|
||||
|
||||
// DESC is a keyword
|
||||
query = query.Set(s.toReserveCase("desc"), compliance.Desc)
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "compliances_tosql")
|
||||
}
|
||||
|
||||
res, err := s.GetMasterX().Exec(queryString, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to update Compliance")
|
||||
}
|
||||
count, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error while getting rows_affected")
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, fmt.Errorf("unexpected count while updating compliances: count=%d, Id=%s", count, compliance.Id)
|
||||
}
|
||||
return compliance, nil
|
||||
}
|
||||
|
||||
func (s SqlComplianceStore) GetAll(offset, limit int) (model.Compliances, error) {
|
||||
query := "SELECT * FROM Compliances ORDER BY CreateAt DESC LIMIT ? OFFSET ?"
|
||||
compliances := model.Compliances{}
|
||||
if err := s.GetReplicaX().Select(&compliances, query, limit, offset); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find all Compliances")
|
||||
}
|
||||
return compliances, nil
|
||||
}
|
||||
|
||||
func (s SqlComplianceStore) Get(id string) (*model.Compliance, error) {
|
||||
var compliance model.Compliance
|
||||
if err := s.GetReplicaX().Get(&compliance, `SELECT * FROM Compliances WHERE Id = ?`, id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("Compliances", id)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to get Compliance with id=%s", id)
|
||||
}
|
||||
if compliance.Id == "" {
|
||||
return nil, store.NewErrNotFound("Compliance", id)
|
||||
}
|
||||
return &compliance, nil
|
||||
}
|
||||
|
||||
func (s SqlComplianceStore) ComplianceExport(job *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) {
|
||||
keywordQuery := ""
|
||||
var argsKeywords []any
|
||||
keywords := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(job.Keywords, ",", " ", -1))))
|
||||
if len(keywords) > 0 {
|
||||
clauses := make([]string, len(keywords))
|
||||
|
||||
for i, keyword := range keywords {
|
||||
keyword = sanitizeSearchTerm(keyword, "\\")
|
||||
clauses[i] = "LOWER(Posts.Message) LIKE ?"
|
||||
argsKeywords = append(argsKeywords, "%"+keyword+"%")
|
||||
}
|
||||
|
||||
keywordQuery = "AND (" + strings.Join(clauses, " OR ") + ")"
|
||||
}
|
||||
|
||||
emailQuery := ""
|
||||
var argsEmails []any
|
||||
emails := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(job.Emails, ",", " ", -1))))
|
||||
if len(emails) > 0 {
|
||||
clauses := make([]string, len(emails))
|
||||
|
||||
for i, email := range emails {
|
||||
clauses[i] = "Users.Email = ?"
|
||||
argsEmails = append(argsEmails, email)
|
||||
}
|
||||
|
||||
emailQuery = "AND (" + strings.Join(clauses, " OR ") + ")"
|
||||
}
|
||||
|
||||
// The idea is to first iterate over the channel posts, and then when we run out of those,
|
||||
// start iterating over the direct message posts.
|
||||
|
||||
channelPosts := []*model.CompliancePost{}
|
||||
channelsQuery := ""
|
||||
var argsChannelsQuery []any
|
||||
if !cursor.ChannelsQueryCompleted {
|
||||
if cursor.LastChannelsQueryPostCreateAt == 0 {
|
||||
cursor.LastChannelsQueryPostCreateAt = job.StartAt
|
||||
}
|
||||
// append the named parameters of SQL query in the correct order to argsChannelsQuery
|
||||
argsChannelsQuery = append(argsChannelsQuery, cursor.LastChannelsQueryPostCreateAt, cursor.LastChannelsQueryPostCreateAt, cursor.LastChannelsQueryPostID, job.EndAt)
|
||||
argsChannelsQuery = append(argsChannelsQuery, argsEmails...)
|
||||
argsChannelsQuery = append(argsChannelsQuery, argsKeywords...)
|
||||
argsChannelsQuery = append(argsChannelsQuery, limit)
|
||||
channelsQuery = `
|
||||
SELECT
|
||||
Teams.Name AS TeamName,
|
||||
Teams.DisplayName AS TeamDisplayName,
|
||||
Channels.Name AS ChannelName,
|
||||
Channels.DisplayName AS ChannelDisplayName,
|
||||
Channels.Type AS ChannelType,
|
||||
Users.Username AS UserUsername,
|
||||
Users.Email AS UserEmail,
|
||||
Users.Nickname AS UserNickname,
|
||||
Posts.Id AS PostId,
|
||||
Posts.CreateAt AS PostCreateAt,
|
||||
Posts.UpdateAt AS PostUpdateAt,
|
||||
Posts.DeleteAt AS PostDeleteAt,
|
||||
Posts.RootId AS PostRootId,
|
||||
Posts.OriginalId AS PostOriginalId,
|
||||
Posts.Message AS PostMessage,
|
||||
Posts.Type AS PostType,
|
||||
Posts.Props AS PostProps,
|
||||
Posts.Hashtags AS PostHashtags,
|
||||
Posts.FileIds AS PostFileIds,
|
||||
Bots.UserId IS NOT NULL AS IsBot
|
||||
FROM
|
||||
Teams,
|
||||
Channels,
|
||||
Users,
|
||||
Posts
|
||||
LEFT JOIN
|
||||
Bots ON Bots.UserId = Posts.UserId
|
||||
WHERE
|
||||
Teams.Id = Channels.TeamId
|
||||
AND Posts.ChannelId = Channels.Id
|
||||
AND Posts.UserId = Users.Id
|
||||
AND (
|
||||
Posts.CreateAt > ?
|
||||
OR (Posts.CreateAt = ? AND Posts.Id > ?)
|
||||
)
|
||||
AND Posts.CreateAt < ?
|
||||
` + emailQuery + `
|
||||
` + keywordQuery + `
|
||||
ORDER BY Posts.CreateAt, Posts.Id
|
||||
LIMIT ?`
|
||||
if err := s.GetReplicaX().Select(&channelPosts, channelsQuery, argsChannelsQuery...); err != nil {
|
||||
return nil, cursor, errors.Wrap(err, "unable to export compliance")
|
||||
}
|
||||
if len(channelPosts) < limit {
|
||||
cursor.ChannelsQueryCompleted = true
|
||||
} else {
|
||||
cursor.LastChannelsQueryPostCreateAt = channelPosts[len(channelPosts)-1].PostCreateAt
|
||||
cursor.LastChannelsQueryPostID = channelPosts[len(channelPosts)-1].PostId
|
||||
}
|
||||
}
|
||||
|
||||
directMessagePosts := []*model.CompliancePost{}
|
||||
directMessagesQuery := ""
|
||||
var argsDirectMessagesQuery []any
|
||||
if !cursor.DirectMessagesQueryCompleted && len(channelPosts) < limit {
|
||||
if cursor.LastDirectMessagesQueryPostCreateAt == 0 {
|
||||
cursor.LastDirectMessagesQueryPostCreateAt = job.StartAt
|
||||
}
|
||||
// append the named parameters of SQL query in the correct order to argsDirectMessagesQuery
|
||||
argsDirectMessagesQuery = append(argsDirectMessagesQuery, cursor.LastDirectMessagesQueryPostCreateAt, cursor.LastDirectMessagesQueryPostCreateAt, cursor.LastDirectMessagesQueryPostID, job.EndAt)
|
||||
argsDirectMessagesQuery = append(argsDirectMessagesQuery, argsEmails...)
|
||||
argsDirectMessagesQuery = append(argsDirectMessagesQuery, argsKeywords...)
|
||||
argsDirectMessagesQuery = append(argsDirectMessagesQuery, limit-len(channelPosts))
|
||||
directMessagesQuery = `
|
||||
SELECT
|
||||
'direct-messages' AS TeamName,
|
||||
'Direct Messages' AS TeamDisplayName,
|
||||
Channels.Name AS ChannelName,
|
||||
Channels.DisplayName AS ChannelDisplayName,
|
||||
Channels.Type AS ChannelType,
|
||||
Users.Username AS UserUsername,
|
||||
Users.Email AS UserEmail,
|
||||
Users.Nickname AS UserNickname,
|
||||
Posts.Id AS PostId,
|
||||
Posts.CreateAt AS PostCreateAt,
|
||||
Posts.UpdateAt AS PostUpdateAt,
|
||||
Posts.DeleteAt AS PostDeleteAt,
|
||||
Posts.RootId AS PostRootId,
|
||||
Posts.OriginalId AS PostOriginalId,
|
||||
Posts.Message AS PostMessage,
|
||||
Posts.Type AS PostType,
|
||||
Posts.Props AS PostProps,
|
||||
Posts.Hashtags AS PostHashtags,
|
||||
Posts.FileIds AS PostFileIds,
|
||||
Bots.UserId IS NOT NULL AS IsBot
|
||||
FROM
|
||||
Channels,
|
||||
Users,
|
||||
Posts
|
||||
LEFT JOIN
|
||||
Bots ON Bots.UserId = Posts.UserId
|
||||
WHERE
|
||||
Channels.TeamId = ''
|
||||
AND Posts.ChannelId = Channels.Id
|
||||
AND Posts.UserId = Users.Id
|
||||
AND (
|
||||
Posts.CreateAt > ?
|
||||
OR (Posts.CreateAt = ? AND Posts.Id > ?)
|
||||
)
|
||||
AND Posts.CreateAt < ?
|
||||
` + emailQuery + `
|
||||
` + keywordQuery + `
|
||||
ORDER BY Posts.CreateAt, Posts.Id
|
||||
LIMIT ?`
|
||||
|
||||
if err := s.GetReplicaX().Select(&directMessagePosts, directMessagesQuery, argsDirectMessagesQuery...); err != nil {
|
||||
return nil, cursor, errors.Wrap(err, "unable to export compliance")
|
||||
}
|
||||
if len(directMessagePosts) < limit {
|
||||
cursor.DirectMessagesQueryCompleted = true
|
||||
} else {
|
||||
cursor.LastDirectMessagesQueryPostCreateAt = directMessagePosts[len(directMessagePosts)-1].PostCreateAt
|
||||
cursor.LastDirectMessagesQueryPostID = directMessagePosts[len(directMessagePosts)-1].PostId
|
||||
}
|
||||
}
|
||||
|
||||
return append(channelPosts, directMessagePosts...), cursor, nil
|
||||
}
|
||||
|
||||
func (s SqlComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) {
|
||||
var args []any
|
||||
args = append(args, model.ChannelTypeDirect, model.ChannelTypeGroup, cursor.LastPostUpdateAt, cursor.LastPostUpdateAt, cursor.LastPostId, limit)
|
||||
query :=
|
||||
`SELECT
|
||||
Posts.Id AS PostId,
|
||||
Posts.CreateAt AS PostCreateAt,
|
||||
Posts.UpdateAt AS PostUpdateAt,
|
||||
Posts.DeleteAt AS PostDeleteAt,
|
||||
Posts.Message AS PostMessage,
|
||||
Posts.Type AS PostType,
|
||||
Posts.Props AS PostProps,
|
||||
Posts.OriginalId AS PostOriginalId,
|
||||
Posts.RootId AS PostRootId,
|
||||
Posts.FileIds AS PostFileIds,
|
||||
Teams.Id AS TeamId,
|
||||
Teams.Name AS TeamName,
|
||||
Teams.DisplayName AS TeamDisplayName,
|
||||
Channels.Id AS ChannelId,
|
||||
CASE
|
||||
WHEN Channels.Type = ? THEN 'Direct Message'
|
||||
WHEN Channels.Type = ? THEN 'Group Message'
|
||||
ELSE Channels.DisplayName
|
||||
END AS ChannelDisplayName,
|
||||
Channels.Name AS ChannelName,
|
||||
Channels.Type AS ChannelType,
|
||||
Users.Id AS UserId,
|
||||
Users.Email AS UserEmail,
|
||||
Users.Username,
|
||||
Bots.UserId IS NOT NULL AS IsBot
|
||||
FROM
|
||||
Posts
|
||||
LEFT OUTER JOIN Channels ON Posts.ChannelId = Channels.Id
|
||||
LEFT OUTER JOIN Teams ON Channels.TeamId = Teams.Id
|
||||
LEFT OUTER JOIN Users ON Posts.UserId = Users.Id
|
||||
LEFT JOIN Bots ON Bots.UserId = Posts.UserId
|
||||
WHERE (
|
||||
Posts.UpdateAt > ?
|
||||
OR (
|
||||
Posts.UpdateAt = ?
|
||||
AND Posts.Id > ?
|
||||
)
|
||||
) AND Posts.Type NOT LIKE 'system_%'
|
||||
ORDER BY PostUpdateAt, PostId
|
||||
LIMIT ?`
|
||||
|
||||
cposts := []*model.MessageExport{}
|
||||
if err := s.GetReplicaX().SelectCtx(ctx, &cposts, query, args...); err != nil {
|
||||
return nil, cursor, errors.Wrap(err, "unable to export messages")
|
||||
}
|
||||
if len(cposts) > 0 {
|
||||
cursor.LastPostUpdateAt = *cposts[len(cposts)-1].PostUpdateAt
|
||||
cursor.LastPostId = *cposts[len(cposts)-1].PostId
|
||||
}
|
||||
return cposts, cursor, nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/compliance_store_test.go
Обычный файл
14
server/channels/store/sqlstore/compliance_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestComplianceStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestComplianceStore)
|
||||
}
|
||||
42
server/channels/store/sqlstore/context.go
Обычный файл
42
server/channels/store/sqlstore/context.go
Обычный файл
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// storeContextKey is the base type for all context keys for the store.
|
||||
type storeContextKey string
|
||||
|
||||
// contextValue is a type to hold some pre-determined context values.
|
||||
type contextValue string
|
||||
|
||||
// Different possible values of contextValue.
|
||||
const (
|
||||
useMaster contextValue = "useMaster"
|
||||
)
|
||||
|
||||
// WithMaster adds the context value that master DB should be selected for this request.
|
||||
func WithMaster(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, storeContextKey(useMaster), true)
|
||||
}
|
||||
|
||||
// hasMaster is a helper function to check whether master DB should be selected or not.
|
||||
func hasMaster(ctx context.Context) bool {
|
||||
if v := ctx.Value(storeContextKey(useMaster)); v != nil {
|
||||
if res, ok := v.(bool); ok && res {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DBXFromContext is a helper utility that returns the sqlx DB handle from a given context.
|
||||
func (ss *SqlStore) DBXFromContext(ctx context.Context) *sqlxDBWrapper {
|
||||
if hasMaster(ctx) {
|
||||
return ss.GetMasterX()
|
||||
}
|
||||
return ss.GetReplicaX()
|
||||
}
|
||||
18
server/channels/store/sqlstore/context_test.go
Обычный файл
18
server/channels/store/sqlstore/context_test.go
Обычный файл
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContextMaster(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
m := WithMaster(ctx)
|
||||
assert.True(t, hasMaster(m))
|
||||
}
|
||||
260
server/channels/store/sqlstore/draft_store.go
Обычный файл
260
server/channels/store/sqlstore/draft_store.go
Обычный файл
@@ -0,0 +1,260 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sync"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SqlDraftStore struct {
|
||||
*SqlStore
|
||||
metrics einterfaces.MetricsInterface
|
||||
maxDraftSizeOnce sync.Once
|
||||
maxDraftSizeCached int
|
||||
}
|
||||
|
||||
func draftSliceColumns() []string {
|
||||
return []string{
|
||||
"CreateAt",
|
||||
"UpdateAt",
|
||||
"DeleteAt",
|
||||
"Message",
|
||||
"RootId",
|
||||
"ChannelId",
|
||||
"UserId",
|
||||
"FileIds",
|
||||
"Props",
|
||||
"Priority",
|
||||
}
|
||||
}
|
||||
|
||||
func draftToSlice(draft *model.Draft) []interface{} {
|
||||
return []interface{}{
|
||||
draft.CreateAt,
|
||||
draft.UpdateAt,
|
||||
draft.DeleteAt,
|
||||
draft.Message,
|
||||
draft.RootId,
|
||||
draft.ChannelId,
|
||||
draft.UserId,
|
||||
model.ArrayToJSON(draft.FileIds),
|
||||
model.StringInterfaceToJSON(draft.Props),
|
||||
model.StringInterfaceToJSON(draft.Priority),
|
||||
}
|
||||
}
|
||||
|
||||
func newSqlDraftStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.DraftStore {
|
||||
return &SqlDraftStore{
|
||||
SqlStore: sqlStore,
|
||||
metrics: metrics,
|
||||
maxDraftSizeCached: model.PostMessageMaxRunesV1,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SqlDraftStore) Get(userId, channelId, rootId string, includeDeleted bool) (*model.Draft, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select(draftSliceColumns()...).
|
||||
From("Drafts").
|
||||
Where(sq.Eq{
|
||||
"UserId": userId,
|
||||
"ChannelId": channelId,
|
||||
"RootId": rootId,
|
||||
})
|
||||
|
||||
if !includeDeleted {
|
||||
query = query.Where(sq.Eq{"DeleteAt": 0})
|
||||
}
|
||||
|
||||
dt := model.Draft{}
|
||||
err := s.GetReplicaX().GetBuilder(&dt, query)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("Draft", channelId)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to find draft with channelid = %s", channelId)
|
||||
}
|
||||
|
||||
return &dt, nil
|
||||
}
|
||||
|
||||
func (s *SqlDraftStore) Save(draft *model.Draft) (*model.Draft, error) {
|
||||
draft.PreSave()
|
||||
maxDraftSize := s.GetMaxDraftSize()
|
||||
if err := draft.IsValid(maxDraftSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
builder := s.getQueryBuilder().Insert("Drafts").Columns(draftSliceColumns()...).Values(draftToSlice(draft)...)
|
||||
query, args, err := builder.ToSql()
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "save_draft_tosql")
|
||||
}
|
||||
|
||||
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save Draft")
|
||||
}
|
||||
|
||||
return draft, nil
|
||||
}
|
||||
|
||||
func (s *SqlDraftStore) Update(draft *model.Draft) (*model.Draft, error) {
|
||||
draft.PreUpdate()
|
||||
|
||||
maxDraftSize := s.GetMaxDraftSize()
|
||||
if err := draft.IsValid(maxDraftSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Update("Drafts").
|
||||
Set("UpdateAt", draft.UpdateAt).
|
||||
Set("Message", draft.Message).
|
||||
Set("Props", draft.Props).
|
||||
Set("FileIds", draft.FileIds).
|
||||
Set("Priority", draft.Priority).
|
||||
Set("DeleteAt", 0).
|
||||
Where(sq.Eq{
|
||||
"UserId": draft.UserId,
|
||||
"ChannelId": draft.ChannelId,
|
||||
"RootId": draft.RootId,
|
||||
})
|
||||
|
||||
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to update Draft with channelid=%s", draft.ChannelId)
|
||||
}
|
||||
|
||||
return draft, nil
|
||||
}
|
||||
|
||||
func (s *SqlDraftStore) GetDraftsForUser(userID, teamID string) ([]*model.Draft, error) {
|
||||
var drafts []*model.Draft
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select(
|
||||
"Drafts.CreateAt",
|
||||
"Drafts.UpdateAt",
|
||||
"Drafts.Message",
|
||||
"Drafts.RootId",
|
||||
"Drafts.ChannelId",
|
||||
"Drafts.UserId",
|
||||
"Drafts.FileIds",
|
||||
"Drafts.Props",
|
||||
"Drafts.Priority",
|
||||
).
|
||||
From("Drafts").
|
||||
InnerJoin("ChannelMembers ON ChannelMembers.ChannelId = Drafts.ChannelId").
|
||||
Where(sq.And{
|
||||
sq.Eq{"Drafts.DeleteAt": 0},
|
||||
sq.Eq{"Drafts.UserId": userID},
|
||||
sq.Eq{"ChannelMembers.UserId": userID},
|
||||
}).
|
||||
OrderBy("Drafts.UpdateAt DESC")
|
||||
|
||||
if teamID != "" {
|
||||
query = query.
|
||||
Join("Channels ON Drafts.ChannelId = Channels.Id").
|
||||
Where(sq.Or{
|
||||
sq.Eq{"Channels.TeamId": teamID},
|
||||
sq.Eq{"Channels.TeamId": ""},
|
||||
})
|
||||
}
|
||||
|
||||
err := s.GetReplicaX().SelectBuilder(&drafts, query)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get user drafts")
|
||||
}
|
||||
|
||||
return drafts, nil
|
||||
}
|
||||
|
||||
func (s *SqlDraftStore) Delete(userID, channelID, rootID string) error {
|
||||
time := model.GetMillis()
|
||||
query := s.getQueryBuilder().
|
||||
Update("Drafts").
|
||||
Set("UpdateAt", time).
|
||||
Set("DeleteAt", time).
|
||||
Where(sq.Eq{
|
||||
"UserId": userID,
|
||||
"ChannelId": channelID,
|
||||
"RootId": rootID,
|
||||
})
|
||||
|
||||
sql, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to convert to sql")
|
||||
}
|
||||
|
||||
_, err = s.GetMasterX().Exec(sql, args...)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to delete Draft")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMaxDraftSize returns the maximum number of runes that may be stored in a post.
|
||||
func (s *SqlDraftStore) GetMaxDraftSize() int {
|
||||
s.maxDraftSizeOnce.Do(func() {
|
||||
s.maxDraftSizeCached = s.determineMaxDraftSize()
|
||||
})
|
||||
return s.maxDraftSizeCached
|
||||
}
|
||||
|
||||
func (s *SqlDraftStore) determineMaxDraftSize() int {
|
||||
var maxDraftSizeBytes int32
|
||||
|
||||
if s.DriverName() == model.DatabaseDriverPostgres {
|
||||
// The Draft.Message column in Postgres has historically been VARCHAR(4000), but
|
||||
// may be manually enlarged to support longer drafts.
|
||||
if err := s.GetReplicaX().Get(&maxDraftSizeBytes, `
|
||||
SELECT
|
||||
COALESCE(character_maximum_length, 0)
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE
|
||||
table_name = 'drafts'
|
||||
AND column_name = 'message'
|
||||
`); err != nil {
|
||||
mlog.Warn("Unable to determine the maximum supported draft size", mlog.Err(err))
|
||||
}
|
||||
} else if s.DriverName() == model.DatabaseDriverMysql {
|
||||
// The Draft.Message column in MySQL has historically been TEXT, with a maximum
|
||||
// limit of 65535.
|
||||
if err := s.GetReplicaX().Get(&maxDraftSizeBytes, `
|
||||
SELECT
|
||||
COALESCE(CHARACTER_MAXIMUM_LENGTH, 0)
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
table_schema = DATABASE()
|
||||
AND table_name = 'Drafts'
|
||||
AND column_name = 'Message'
|
||||
LIMIT 0, 1
|
||||
`); err != nil {
|
||||
mlog.Warn("Unable to determine the maximum supported draft size", mlog.Err(err))
|
||||
}
|
||||
} else {
|
||||
mlog.Warn("No implementation found to determine the maximum supported draft size")
|
||||
}
|
||||
|
||||
// Assume a worst-case representation of four bytes per rune.
|
||||
maxDraftSize := int(maxDraftSizeBytes) / 4
|
||||
|
||||
mlog.Info("Draft.Message has size restrictions", mlog.Int("max_characters", maxDraftSize), mlog.Int32("max_bytes", maxDraftSizeBytes))
|
||||
|
||||
return maxDraftSize
|
||||
}
|
||||
14
server/channels/store/sqlstore/draft_store_test.go
Обычный файл
14
server/channels/store/sqlstore/draft_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestDraftStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestDraftStore)
|
||||
}
|
||||
157
server/channels/store/sqlstore/emoji_store.go
Обычный файл
157
server/channels/store/sqlstore/emoji_store.go
Обычный файл
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlEmojiStore struct {
|
||||
*SqlStore
|
||||
metrics einterfaces.MetricsInterface
|
||||
}
|
||||
|
||||
func newSqlEmojiStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.EmojiStore {
|
||||
return &SqlEmojiStore{
|
||||
SqlStore: sqlStore,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) Save(emoji *model.Emoji) (*model.Emoji, error) {
|
||||
emoji.PreSave()
|
||||
if err := emoji.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := es.GetMasterX().NamedExec(`INSERT INTO Emoji
|
||||
(Id, CreateAt, UpdateAt, DeleteAt, CreatorId, Name)
|
||||
VALUES
|
||||
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :CreatorId, :Name)`, emoji); err != nil {
|
||||
return nil, errors.Wrap(err, "error saving emoji")
|
||||
}
|
||||
|
||||
return emoji, nil
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error) {
|
||||
return es.getBy(ctx, "Id", id)
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) GetByName(ctx context.Context, name string, allowFromCache bool) (*model.Emoji, error) {
|
||||
return es.getBy(ctx, "Name", name)
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) GetMultipleByName(names []string) ([]*model.Emoji, error) {
|
||||
// Creating (?, ?, ?) len(names) number of times.
|
||||
keys := strings.Join(strings.Fields(strings.Repeat("? ", len(names))), ",")
|
||||
args := makeStringArgs(names)
|
||||
|
||||
emojis := []*model.Emoji{}
|
||||
if err := es.GetReplicaX().Select(&emojis,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
Emoji
|
||||
WHERE
|
||||
Name IN (`+keys+`)
|
||||
AND DeleteAt = 0`, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "error getting emoji by names %v", names)
|
||||
}
|
||||
return emojis, nil
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) GetList(offset, limit int, sort string) ([]*model.Emoji, error) {
|
||||
emojis := []*model.Emoji{}
|
||||
|
||||
query := "SELECT * FROM Emoji WHERE DeleteAt = 0"
|
||||
|
||||
if sort == model.EmojiSortByName {
|
||||
query += " ORDER BY Name"
|
||||
}
|
||||
|
||||
query += " LIMIT ? OFFSET ?"
|
||||
|
||||
if err := es.GetReplicaX().Select(&emojis, query, limit, offset); err != nil {
|
||||
return nil, errors.Wrap(err, "could not get list of emojis")
|
||||
}
|
||||
return emojis, nil
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) Delete(emoji *model.Emoji, time int64) error {
|
||||
if sqlResult, err := es.GetMasterX().Exec(
|
||||
`UPDATE
|
||||
Emoji
|
||||
SET
|
||||
DeleteAt = ?,
|
||||
UpdateAt = ?
|
||||
WHERE
|
||||
Id = ?
|
||||
AND DeleteAt = 0`, time, time, emoji.Id); err != nil {
|
||||
return errors.Wrap(err, "could not delete emoji")
|
||||
} else if rows, err := sqlResult.RowsAffected(); rows == 0 {
|
||||
return store.NewErrNotFound("Emoji", emoji.Id).Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) Search(name string, prefixOnly bool, limit int) ([]*model.Emoji, error) {
|
||||
emojis := []*model.Emoji{}
|
||||
|
||||
name = sanitizeSearchTerm(name, "\\")
|
||||
|
||||
term := ""
|
||||
if !prefixOnly {
|
||||
term = "%"
|
||||
}
|
||||
|
||||
term += name + "%"
|
||||
|
||||
if err := es.GetReplicaX().Select(&emojis,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
Emoji
|
||||
WHERE
|
||||
Name LIKE ?
|
||||
AND DeleteAt = 0
|
||||
ORDER BY Name
|
||||
LIMIT ?`, term, limit); err != nil {
|
||||
return nil, errors.Wrapf(err, "could not search emojis by name %s", name)
|
||||
}
|
||||
return emojis, nil
|
||||
}
|
||||
|
||||
// getBy returns one active (not deleted) emoji, found by any one column (what/key).
|
||||
func (es SqlEmojiStore) getBy(ctx context.Context, what, key string) (*model.Emoji, error) {
|
||||
var emoji model.Emoji
|
||||
|
||||
err := es.DBXFromContext(ctx).Get(&emoji,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
Emoji
|
||||
WHERE
|
||||
`+what+` = ?
|
||||
AND DeleteAt = 0`, key)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("Emoji", fmt.Sprintf("%s=%s", what, key))
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "could not get emoji by %s with value %s", what, key)
|
||||
}
|
||||
|
||||
return &emoji, nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/emoji_store_test.go
Обычный файл
14
server/channels/store/sqlstore/emoji_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestEmojiStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestEmojiStore)
|
||||
}
|
||||
801
server/channels/store/sqlstore/file_info_store.go
Обычный файл
801
server/channels/store/sqlstore/file_info_store.go
Обычный файл
@@ -0,0 +1,801 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type fileInfoWithChannelID struct {
|
||||
Id string
|
||||
CreatorId string
|
||||
PostId string
|
||||
ChannelId string
|
||||
CreateAt int64
|
||||
UpdateAt int64
|
||||
DeleteAt int64
|
||||
Path string
|
||||
ThumbnailPath string
|
||||
PreviewPath string
|
||||
Name string
|
||||
Extension string
|
||||
Size int64
|
||||
MimeType string
|
||||
Width int
|
||||
Height int
|
||||
HasPreviewImage bool
|
||||
MiniPreview *[]byte
|
||||
Content string
|
||||
RemoteId *string
|
||||
Archived bool
|
||||
}
|
||||
|
||||
func (fi fileInfoWithChannelID) ToModel() *model.FileInfo {
|
||||
return &model.FileInfo{
|
||||
Id: fi.Id,
|
||||
CreatorId: fi.CreatorId,
|
||||
PostId: fi.PostId,
|
||||
ChannelId: fi.ChannelId,
|
||||
CreateAt: fi.CreateAt,
|
||||
UpdateAt: fi.UpdateAt,
|
||||
DeleteAt: fi.DeleteAt,
|
||||
Path: fi.Path,
|
||||
ThumbnailPath: fi.ThumbnailPath,
|
||||
PreviewPath: fi.PreviewPath,
|
||||
Name: fi.Name,
|
||||
Extension: fi.Extension,
|
||||
Size: fi.Size,
|
||||
MimeType: fi.MimeType,
|
||||
Width: fi.Width,
|
||||
Height: fi.Height,
|
||||
HasPreviewImage: fi.HasPreviewImage,
|
||||
MiniPreview: fi.MiniPreview,
|
||||
Content: fi.Content,
|
||||
RemoteId: fi.RemoteId,
|
||||
}
|
||||
}
|
||||
|
||||
type SqlFileInfoStore struct {
|
||||
*SqlStore
|
||||
metrics einterfaces.MetricsInterface
|
||||
queryFields []string
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) ClearCaches() {
|
||||
}
|
||||
|
||||
func newSqlFileInfoStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.FileInfoStore {
|
||||
s := &SqlFileInfoStore{
|
||||
SqlStore: sqlStore,
|
||||
metrics: metrics,
|
||||
}
|
||||
|
||||
s.queryFields = []string{
|
||||
"FileInfo.Id",
|
||||
"FileInfo.CreatorId",
|
||||
"FileInfo.PostId",
|
||||
"FileInfo.CreateAt",
|
||||
"FileInfo.UpdateAt",
|
||||
"FileInfo.DeleteAt",
|
||||
"FileInfo.Path",
|
||||
"FileInfo.ThumbnailPath",
|
||||
"FileInfo.PreviewPath",
|
||||
"FileInfo.Name",
|
||||
"FileInfo.Extension",
|
||||
"FileInfo.Size",
|
||||
"FileInfo.MimeType",
|
||||
"FileInfo.Width",
|
||||
"FileInfo.Height",
|
||||
"FileInfo.HasPreviewImage",
|
||||
"FileInfo.MiniPreview",
|
||||
"Coalesce(FileInfo.Content, '') AS Content",
|
||||
"Coalesce(FileInfo.RemoteId, '') AS RemoteId",
|
||||
"FileInfo.Archived",
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
|
||||
info.PreSave()
|
||||
if err := info.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO FileInfo
|
||||
(Id, CreatorId, PostId, CreateAt, UpdateAt, DeleteAt, Path, ThumbnailPath, PreviewPath,
|
||||
Name, Extension, Size, MimeType, Width, Height, HasPreviewImage, MiniPreview, Content, RemoteId)
|
||||
VALUES
|
||||
(:Id, :CreatorId, :PostId, :CreateAt, :UpdateAt, :DeleteAt, :Path, :ThumbnailPath, :PreviewPath,
|
||||
:Name, :Extension, :Size, :MimeType, :Width, :Height, :HasPreviewImage, :MiniPreview, :Content, :RemoteId)
|
||||
`
|
||||
|
||||
if _, err := fs.GetMasterX().NamedExec(query, info); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save FileInfo")
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
|
||||
query := fs.getQueryBuilder().
|
||||
Select(append(fs.queryFields, "COALESCE(P.ChannelId, '') as ChannelId")...).
|
||||
From("FileInfo").
|
||||
LeftJoin("Posts as P ON FileInfo.PostId=P.Id").
|
||||
Where(sq.Eq{"FileInfo.Id": ids}).
|
||||
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
|
||||
OrderBy("FileInfo.CreateAt DESC")
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
|
||||
items := []fileInfoWithChannelID{}
|
||||
if err := fs.GetReplicaX().Select(&items, queryString, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find FileInfos")
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
infos := make([]*model.FileInfo, 0, len(items))
|
||||
for _, item := range items {
|
||||
infos = append(infos, item.ToModel())
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error) {
|
||||
info.PreSave()
|
||||
if err := info.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queryString, args, err := fs.getQueryBuilder().
|
||||
Update("FileInfo").
|
||||
SetMap(map[string]any{
|
||||
"UpdateAt": info.UpdateAt,
|
||||
"DeleteAt": info.DeleteAt,
|
||||
"Path": info.Path,
|
||||
"ThumbnailPath": info.ThumbnailPath,
|
||||
"PreviewPath": info.PreviewPath,
|
||||
"Name": info.Name,
|
||||
"Extension": info.Extension,
|
||||
"Size": info.Size,
|
||||
"MimeType": info.MimeType,
|
||||
"Width": info.Width,
|
||||
"Height": info.Height,
|
||||
"HasPreviewImage": info.HasPreviewImage,
|
||||
"MiniPreview": info.MiniPreview,
|
||||
"Content": info.Content,
|
||||
"RemoteId": info.RemoteId,
|
||||
}).
|
||||
Where(sq.Eq{"Id": info.Id}).
|
||||
ToSql()
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
|
||||
sqlResult, err := fs.GetMasterX().Exec(queryString, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to update FileInfo")
|
||||
}
|
||||
count, err := sqlResult.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to retrieve rows affected")
|
||||
}
|
||||
if count == 0 {
|
||||
return fs.Save(info)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) get(id string, fromMaster bool) (*model.FileInfo, error) {
|
||||
info := &model.FileInfo{}
|
||||
|
||||
query := fs.getQueryBuilder().
|
||||
Select(fs.queryFields...).
|
||||
From("FileInfo").
|
||||
Where(sq.Eq{"Id": id}).
|
||||
Where(sq.Eq{"DeleteAt": 0})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
|
||||
db := fs.GetReplicaX()
|
||||
if fromMaster {
|
||||
db = fs.GetMasterX()
|
||||
}
|
||||
|
||||
if err := db.Get(info, queryString, args...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("FileInfo", id)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to get FileInfo with id=%s", id)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) Get(id string) (*model.FileInfo, error) {
|
||||
return fs.get(id, false)
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetFromMaster(id string) (*model.FileInfo, error) {
|
||||
return fs.get(id, true)
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetWithOptions(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, error) {
|
||||
if perPage < 0 {
|
||||
return nil, store.NewErrLimitExceeded("perPage", perPage, "value used in pagination while getting FileInfos")
|
||||
} else if page < 0 {
|
||||
return nil, store.NewErrLimitExceeded("page", page, "value used in pagination while getting FileInfos")
|
||||
}
|
||||
if perPage == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if opt == nil {
|
||||
opt = &model.GetFileInfosOptions{}
|
||||
}
|
||||
|
||||
query := fs.getQueryBuilder().
|
||||
Select(fs.queryFields...).
|
||||
From("FileInfo")
|
||||
|
||||
if len(opt.ChannelIds) > 0 {
|
||||
query = query.Join("Posts ON FileInfo.PostId = Posts.Id").
|
||||
Where(sq.Eq{"Posts.ChannelId": opt.ChannelIds})
|
||||
}
|
||||
|
||||
if len(opt.UserIds) > 0 {
|
||||
query = query.Where(sq.Eq{"FileInfo.CreatorId": opt.UserIds})
|
||||
}
|
||||
|
||||
if opt.Since > 0 {
|
||||
query = query.Where(sq.GtOrEq{"FileInfo.CreateAt": opt.Since})
|
||||
}
|
||||
|
||||
if !opt.IncludeDeleted {
|
||||
query = query.Where("FileInfo.DeleteAt = 0")
|
||||
}
|
||||
|
||||
if opt.SortBy == "" {
|
||||
opt.SortBy = model.FileinfoSortByCreated
|
||||
}
|
||||
sortDirection := "ASC"
|
||||
if opt.SortDescending {
|
||||
sortDirection = "DESC"
|
||||
}
|
||||
|
||||
switch opt.SortBy {
|
||||
case model.FileinfoSortByCreated:
|
||||
query = query.OrderBy("FileInfo.CreateAt " + sortDirection)
|
||||
case model.FileinfoSortBySize:
|
||||
query = query.OrderBy("FileInfo.Size " + sortDirection)
|
||||
default:
|
||||
return nil, store.NewErrInvalidInput("FileInfo", "<sortOption>", opt.SortBy)
|
||||
}
|
||||
|
||||
query = query.OrderBy("FileInfo.Id ASC") // secondary sort for sort stability
|
||||
|
||||
query = query.Limit(uint64(perPage)).Offset(uint64(perPage * page))
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
infos := []*model.FileInfo{}
|
||||
if err := fs.GetReplicaX().Select(&infos, queryString, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find FileInfos")
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetByPath(path string) (*model.FileInfo, error) {
|
||||
info := &model.FileInfo{}
|
||||
|
||||
query := fs.getQueryBuilder().
|
||||
Select(fs.queryFields...).
|
||||
From("FileInfo").
|
||||
Where(sq.Eq{"Path": path}).
|
||||
Where(sq.Eq{"DeleteAt": 0}).
|
||||
Limit(1)
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
|
||||
if err := fs.GetReplicaX().Get(info, queryString, args...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("FileInfo", fmt.Sprintf("path=%s", path))
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "failed to get FileInfo with path=%s", path)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) InvalidateFileInfosForPostCache(postId string, deleted bool) {
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetForPost(postId string, readFromMaster, includeDeleted, allowFromCache bool) ([]*model.FileInfo, error) {
|
||||
infos := []*model.FileInfo{}
|
||||
|
||||
dbmap := fs.GetReplicaX()
|
||||
|
||||
if readFromMaster {
|
||||
dbmap = fs.GetMasterX()
|
||||
}
|
||||
|
||||
query := fs.getQueryBuilder().
|
||||
Select(fs.queryFields...).
|
||||
From("FileInfo").
|
||||
Where(sq.Eq{"PostId": postId}).
|
||||
OrderBy("CreateAt")
|
||||
|
||||
if !includeDeleted {
|
||||
query = query.Where("DeleteAt = 0")
|
||||
}
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
|
||||
if err := dbmap.Select(&infos, queryString, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find FileInfos with postId=%s", postId)
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetForUser(userId string) ([]*model.FileInfo, error) {
|
||||
infos := []*model.FileInfo{}
|
||||
|
||||
query := fs.getQueryBuilder().
|
||||
Select(fs.queryFields...).
|
||||
From("FileInfo").
|
||||
Where(sq.Eq{"CreatorId": userId}).
|
||||
Where(sq.Eq{"DeleteAt": 0}).
|
||||
OrderBy("CreateAt")
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
|
||||
if err := fs.GetReplicaX().Select(&infos, queryString, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find FileInfos with creatorId=%s", userId)
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) AttachToPost(fileId, postId, creatorId string) error {
|
||||
query := fs.getQueryBuilder().
|
||||
Update("FileInfo").
|
||||
Set("PostId", postId).
|
||||
Where(sq.And{
|
||||
sq.Eq{"Id": fileId},
|
||||
sq.Eq{"PostId": ""},
|
||||
sq.Or{
|
||||
sq.Eq{"CreatorId": creatorId},
|
||||
sq.Eq{"CreatorId": "nouser"},
|
||||
},
|
||||
})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
sqlResult, err := fs.GetMasterX().Exec(queryString, args...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to update FileInfo with id=%s and postId=%s", fileId, postId)
|
||||
}
|
||||
|
||||
count, err := sqlResult.RowsAffected()
|
||||
if err != nil {
|
||||
// RowsAffected should never fail with the MySQL or Postgres drivers
|
||||
return errors.Wrap(err, "unable to retrieve rows affected")
|
||||
} else if count == 0 {
|
||||
// Could not attach the file to the post
|
||||
return store.NewErrInvalidInput("FileInfo", "<id, postId, creatorId>", fmt.Sprintf("<%s, %s, %s>", fileId, postId, creatorId))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) SetContent(fileId, content string) error {
|
||||
query := fs.getQueryBuilder().
|
||||
Update("FileInfo").
|
||||
Set("Content", content).
|
||||
Where(sq.Eq{"Id": fileId})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
|
||||
_, err = fs.GetMasterX().Exec(queryString, args...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to update FileInfo content with id=%s", fileId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) DeleteForPost(postId string) (string, error) {
|
||||
if _, err := fs.GetMasterX().Exec(
|
||||
`UPDATE
|
||||
FileInfo
|
||||
SET
|
||||
DeleteAt = ?
|
||||
WHERE
|
||||
PostId = ?`, model.GetMillis(), postId); err != nil {
|
||||
return "", errors.Wrapf(err, "failed to update FileInfo with postId=%s", postId)
|
||||
}
|
||||
return postId, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) PermanentDelete(fileId string) error {
|
||||
if _, err := fs.GetMasterX().Exec(`DELETE FROM FileInfo WHERE Id = ?`, fileId); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete FileInfo with id=%s", fileId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
|
||||
var query string
|
||||
if fs.DriverName() == "postgres" {
|
||||
query = "DELETE from FileInfo WHERE Id = any (array (SELECT Id FROM FileInfo WHERE CreateAt < ? LIMIT ?))"
|
||||
} else {
|
||||
query = "DELETE from FileInfo WHERE CreateAt < ? LIMIT ?"
|
||||
}
|
||||
|
||||
sqlResult, err := fs.GetMasterX().Exec(query, endTime, limit)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to delete FileInfos in batch")
|
||||
}
|
||||
|
||||
rowsAffected, err := sqlResult.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "unable to retrieve rows affected")
|
||||
}
|
||||
|
||||
return rowsAffected, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) PermanentDeleteByUser(userId string) (int64, error) {
|
||||
query := "DELETE from FileInfo WHERE CreatorId = ?"
|
||||
|
||||
sqlResult, err := fs.GetMasterX().Exec(query, userId)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "failed to delete FileInfo with creatorId=%s", userId)
|
||||
}
|
||||
|
||||
rowsAffected, err := sqlResult.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "unable to retrieve rows affected")
|
||||
}
|
||||
|
||||
return rowsAffected, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) Search(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.FileInfoList, error) {
|
||||
// Since we don't support paging for DB search, we just return nothing for later pages
|
||||
if page > 0 {
|
||||
return model.NewFileInfoList(), nil
|
||||
}
|
||||
if err := model.IsSearchParamsListValid(paramsList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := fs.getQueryBuilder().
|
||||
Select(append(fs.queryFields, "Coalesce(P.ChannelId, '') AS ChannelId")...).
|
||||
From("FileInfo").
|
||||
LeftJoin("Posts as P ON FileInfo.PostId=P.Id").
|
||||
LeftJoin("Channels as C ON C.Id=P.ChannelId").
|
||||
LeftJoin("ChannelMembers as CM ON C.Id=CM.ChannelId").
|
||||
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
|
||||
OrderBy("FileInfo.CreateAt DESC").
|
||||
Limit(100)
|
||||
|
||||
if teamId != "" {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Eq{"C.TeamId": teamId},
|
||||
sq.Eq{"C.TeamId": ""},
|
||||
})
|
||||
}
|
||||
|
||||
now := model.GetMillis()
|
||||
for _, params := range paramsList {
|
||||
if params.Modifier == model.ModifierFiles {
|
||||
// Deliberately keeping non-alphanumeric characters to
|
||||
// prevent surprises in UI.
|
||||
buf, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = fs.stores.post.LogRecentSearch(userId, buf, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ")
|
||||
|
||||
if !params.IncludeDeletedChannels {
|
||||
query = query.Where(sq.Eq{"C.DeleteAt": 0})
|
||||
}
|
||||
|
||||
if !params.SearchWithoutUserId {
|
||||
query = query.Where(sq.Eq{"CM.UserId": userId})
|
||||
}
|
||||
|
||||
if len(params.InChannels) != 0 {
|
||||
query = query.Where(sq.Eq{"C.Id": params.InChannels})
|
||||
}
|
||||
|
||||
if len(params.Extensions) != 0 {
|
||||
query = query.Where(sq.Eq{"FileInfo.Extension": params.Extensions})
|
||||
}
|
||||
|
||||
if len(params.ExcludedExtensions) != 0 {
|
||||
query = query.Where(sq.NotEq{"FileInfo.Extension": params.ExcludedExtensions})
|
||||
}
|
||||
|
||||
if len(params.ExcludedChannels) != 0 {
|
||||
query = query.Where(sq.NotEq{"C.Id": params.ExcludedChannels})
|
||||
}
|
||||
|
||||
if len(params.FromUsers) != 0 {
|
||||
query = query.Where(sq.Eq{"FileInfo.CreatorId": params.FromUsers})
|
||||
}
|
||||
|
||||
if len(params.ExcludedUsers) != 0 {
|
||||
query = query.Where(sq.NotEq{"FileInfo.CreatorId": params.ExcludedUsers})
|
||||
}
|
||||
|
||||
// handle after: before: on: filters
|
||||
if params.OnDate != "" {
|
||||
onDateStart, onDateEnd := params.GetOnDateMillis()
|
||||
query = query.Where(sq.Expr("FileInfo.CreateAt BETWEEN ? AND ?", strconv.FormatInt(onDateStart, 10), strconv.FormatInt(onDateEnd, 10)))
|
||||
} else {
|
||||
if params.ExcludedDate != "" {
|
||||
excludedDateStart, excludedDateEnd := params.GetExcludedDateMillis()
|
||||
query = query.Where(sq.Expr("FileInfo.CreateAt NOT BETWEEN ? AND ?", strconv.FormatInt(excludedDateStart, 10), strconv.FormatInt(excludedDateEnd, 10)))
|
||||
}
|
||||
|
||||
if params.AfterDate != "" {
|
||||
afterDate := params.GetAfterDateMillis()
|
||||
query = query.Where(sq.GtOrEq{"FileInfo.CreateAt": strconv.FormatInt(afterDate, 10)})
|
||||
}
|
||||
|
||||
if params.BeforeDate != "" {
|
||||
beforeDate := params.GetBeforeDateMillis()
|
||||
query = query.Where(sq.LtOrEq{"FileInfo.CreateAt": strconv.FormatInt(beforeDate, 10)})
|
||||
}
|
||||
|
||||
if params.ExcludedAfterDate != "" {
|
||||
afterDate := params.GetExcludedAfterDateMillis()
|
||||
query = query.Where(sq.Lt{"FileInfo.CreateAt": strconv.FormatInt(afterDate, 10)})
|
||||
}
|
||||
|
||||
if params.ExcludedBeforeDate != "" {
|
||||
beforeDate := params.GetExcludedBeforeDateMillis()
|
||||
query = query.Where(sq.Gt{"FileInfo.CreateAt": strconv.FormatInt(beforeDate, 10)})
|
||||
}
|
||||
}
|
||||
|
||||
terms := params.Terms
|
||||
excludedTerms := params.ExcludedTerms
|
||||
|
||||
for _, c := range fs.specialSearchChars() {
|
||||
terms = strings.Replace(terms, c, " ", -1)
|
||||
excludedTerms = strings.Replace(excludedTerms, c, " ", -1)
|
||||
}
|
||||
|
||||
if terms == "" && excludedTerms == "" {
|
||||
// we've already confirmed that we have a channel or user to search for
|
||||
} else if fs.DriverName() == model.DatabaseDriverPostgres {
|
||||
// Parse text for wildcards
|
||||
if wildcard, err := regexp.Compile(`\*($| )`); err == nil {
|
||||
terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
|
||||
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
|
||||
}
|
||||
|
||||
excludeClause := ""
|
||||
if excludedTerms != "" {
|
||||
excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")"
|
||||
}
|
||||
|
||||
queryTerms := ""
|
||||
if params.OrTerms {
|
||||
queryTerms = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause
|
||||
} else {
|
||||
queryTerms = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause
|
||||
}
|
||||
|
||||
query = query.Where(sq.Or{
|
||||
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', FileInfo.Name) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
|
||||
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', Translate(FileInfo.Name, '.,-', ' ')) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
|
||||
sq.Expr(fmt.Sprintf("to_tsvector('%[1]s', FileInfo.Content) @@ to_tsquery('%[1]s', ?)", fs.pgDefaultTextSearchConfig), queryTerms),
|
||||
})
|
||||
} else if fs.DriverName() == model.DatabaseDriverMysql {
|
||||
var err error
|
||||
terms, err = removeMysqlStopWordsFromTerms(terms)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to remove Mysql stop-words from terms")
|
||||
}
|
||||
|
||||
if terms == "" {
|
||||
return model.NewFileInfoList(), nil
|
||||
}
|
||||
|
||||
excludeClause := ""
|
||||
if excludedTerms != "" {
|
||||
excludeClause = " -(" + excludedTerms + ")"
|
||||
}
|
||||
|
||||
queryTerms := ""
|
||||
if params.OrTerms {
|
||||
queryTerms = terms + excludeClause
|
||||
} else {
|
||||
splitTerms := []string{}
|
||||
for _, t := range strings.Fields(terms) {
|
||||
splitTerms = append(splitTerms, "+"+t)
|
||||
}
|
||||
queryTerms = strings.Join(splitTerms, " ") + excludeClause
|
||||
}
|
||||
query = query.Where(sq.Or{
|
||||
sq.Expr("MATCH (FileInfo.Name) AGAINST (? IN BOOLEAN MODE)", queryTerms),
|
||||
sq.Expr("MATCH (FileInfo.Content) AGAINST (? IN BOOLEAN MODE)", queryTerms),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "file_info_tosql")
|
||||
}
|
||||
|
||||
list := model.NewFileInfoList()
|
||||
|
||||
items := []fileInfoWithChannelID{}
|
||||
err = fs.GetSearchReplicaX().Select(&items, queryString, args...)
|
||||
if err != nil {
|
||||
mlog.Warn("Query error searching files.", mlog.Err(err))
|
||||
// Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results.
|
||||
} else {
|
||||
for _, item := range items {
|
||||
info := item.ToModel()
|
||||
list.AddFileInfo(info)
|
||||
list.AddOrder(info.Id)
|
||||
}
|
||||
}
|
||||
list.MakeNonNil()
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) CountAll() (int64, error) {
|
||||
query := fs.getQueryBuilder().
|
||||
Select("COUNT(*)").
|
||||
From("FileInfo").
|
||||
Where("DeleteAt = 0")
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return int64(0), errors.Wrap(err, "count_tosql")
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = fs.GetReplicaX().Get(&count, queryString, args...)
|
||||
if err != nil {
|
||||
return int64(0), errors.Wrap(err, "failed to count Files")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
|
||||
files := []*model.FileForIndexing{}
|
||||
sql, args, _ := fs.getQueryBuilder().
|
||||
Select(append(fs.queryFields, "Coalesce(p.ChannelId, '') AS ChannelId")...).
|
||||
From("FileInfo").
|
||||
LeftJoin("Posts AS p ON FileInfo.PostId = p.Id").
|
||||
Where(sq.Or{
|
||||
sq.Gt{"FileInfo.CreateAt": startTime},
|
||||
sq.And{
|
||||
sq.Eq{"FileInfo.CreateAt": startTime},
|
||||
sq.Gt{"FileInfo.Id": startFileID},
|
||||
},
|
||||
}).
|
||||
OrderBy("FileInfo.CreateAt ASC, FileInfo.Id ASC").
|
||||
Limit(uint64(limit)).
|
||||
ToSql()
|
||||
|
||||
err := fs.GetSearchReplicaX().Select(&files, sql, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find Files")
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (fs SqlFileInfoStore) GetStorageUsage(allowFromCache, includeDeleted bool) (int64, error) {
|
||||
query := fs.getQueryBuilder().
|
||||
Select("COALESCE(SUM(Size), 0)").
|
||||
From("FileInfo")
|
||||
|
||||
if !includeDeleted {
|
||||
query = query.Where("DeleteAt = 0")
|
||||
}
|
||||
|
||||
var size int64
|
||||
err := fs.GetReplicaX().GetBuilder(&size, query)
|
||||
if err != nil {
|
||||
return int64(0), errors.Wrap(err, "failed to get storage usage")
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
// GetUptoNSizeFileTime returns the CreateAt time of the last accessible file with a running-total size upto n bytes.
|
||||
func (fs *SqlFileInfoStore) GetUptoNSizeFileTime(n int64) (int64, error) {
|
||||
if n <= 0 {
|
||||
return 0, errors.New("n can't be less than 1")
|
||||
}
|
||||
|
||||
var sizeSubQuery sq.SelectBuilder
|
||||
// Separate query for MySql, as current min-version 5.x doesn't support window-functions
|
||||
if fs.DriverName() == model.DatabaseDriverMysql {
|
||||
sizeSubQuery = sq.
|
||||
Select("(@runningSum := @runningSum + fi.Size) RunningTotal", "fi.CreateAt").
|
||||
From("FileInfo fi").
|
||||
Join("(SELECT @runningSum := 0) as tmp").
|
||||
Where(sq.Eq{"fi.DeleteAt": 0}).
|
||||
OrderBy("fi.CreateAt DESC, fi.Id")
|
||||
} else {
|
||||
sizeSubQuery = sq.
|
||||
Select("SUM(fi.Size) OVER(ORDER BY CreateAt DESC, fi.Id) RunningTotal", "fi.CreateAt").
|
||||
From("FileInfo fi").
|
||||
Where(sq.Eq{"fi.DeleteAt": 0})
|
||||
}
|
||||
|
||||
builder := fs.getQueryBuilder().
|
||||
Select("fi2.CreateAt").
|
||||
FromSelect(sizeSubQuery, "fi2").
|
||||
Where(sq.LtOrEq{"fi2.RunningTotal": n}).
|
||||
OrderBy("fi2.CreateAt").
|
||||
Limit(1)
|
||||
|
||||
query, queryArgs, err := builder.ToSql()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "GetUptoNSizeFileTime_tosql")
|
||||
}
|
||||
|
||||
var createAt int64
|
||||
if err := fs.GetReplicaX().Get(&createAt, query, queryArgs...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, store.NewErrNotFound("File", "none")
|
||||
}
|
||||
|
||||
return 0, errors.Wrapf(err, "failed to get the File for size upto=%d", n)
|
||||
}
|
||||
|
||||
return createAt, nil
|
||||
}
|
||||
19
server/channels/store/sqlstore/file_info_store_test.go
Обычный файл
19
server/channels/store/sqlstore/file_info_store_test.go
Обычный файл
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/searchtest"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestFileInfoStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestFileInfoStore)
|
||||
}
|
||||
|
||||
func TestSearchFileInfoStore(t *testing.T) {
|
||||
StoreTestWithSearchTestEngine(t, searchtest.TestSearchFileInfoStore)
|
||||
}
|
||||
2025
server/channels/store/sqlstore/group_store.go
Обычный файл
2025
server/channels/store/sqlstore/group_store.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
14
server/channels/store/sqlstore/group_store_test.go
Обычный файл
14
server/channels/store/sqlstore/group_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestGroupStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestGroupStore)
|
||||
}
|
||||
12
server/channels/store/sqlstore/init_test.go
Обычный файл
12
server/channels/store/sqlstore/init_test.go
Обычный файл
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
func InitTest() {
|
||||
initStores()
|
||||
}
|
||||
|
||||
func TearDownTest() {
|
||||
tearDownStores()
|
||||
}
|
||||
536
server/channels/store/sqlstore/integrity.go
Обычный файл
536
server/channels/store/sqlstore/integrity.go
Обычный файл
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
sq "github.com/mattermost/squirrel"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type relationalCheckConfig struct {
|
||||
parentName string
|
||||
parentIdAttr string
|
||||
childName string
|
||||
childIdAttr string
|
||||
canParentIdBeEmpty bool
|
||||
sortRecords bool
|
||||
filter any
|
||||
}
|
||||
|
||||
func getOrphanedRecords(ss *SqlStore, cfg relationalCheckConfig) ([]model.OrphanedRecord, error) {
|
||||
records := []model.OrphanedRecord{}
|
||||
|
||||
sub := ss.getQueryBuilder().
|
||||
Select("TRUE").
|
||||
From(cfg.parentName + " AS PT").
|
||||
Prefix("NOT EXISTS (").
|
||||
Suffix(")").
|
||||
Where("PT.id = CT." + cfg.parentIdAttr)
|
||||
|
||||
main := ss.getQueryBuilder().
|
||||
Select().
|
||||
Column("CT." + cfg.parentIdAttr + " AS ParentId").
|
||||
From(cfg.childName + " AS CT").
|
||||
Where(sub)
|
||||
|
||||
if cfg.childIdAttr != "" {
|
||||
main = main.Column("CT." + cfg.childIdAttr + " AS ChildId")
|
||||
}
|
||||
|
||||
if cfg.canParentIdBeEmpty {
|
||||
main = main.Where(sq.NotEq{"CT." + cfg.parentIdAttr: ""})
|
||||
}
|
||||
|
||||
if cfg.filter != nil {
|
||||
main = main.Where(cfg.filter)
|
||||
}
|
||||
|
||||
if cfg.sortRecords {
|
||||
main = main.OrderBy("CT." + cfg.parentIdAttr)
|
||||
}
|
||||
|
||||
query, args, err := main.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = ss.GetMasterX().Select(&records, query, args...)
|
||||
return records, err
|
||||
}
|
||||
|
||||
func checkParentChildIntegrity(ss *SqlStore, config relationalCheckConfig) model.IntegrityCheckResult {
|
||||
var result model.IntegrityCheckResult
|
||||
var data model.RelationalIntegrityCheckData
|
||||
|
||||
config.sortRecords = true
|
||||
data.Records, result.Err = getOrphanedRecords(ss, config)
|
||||
if result.Err != nil {
|
||||
mlog.Error("Error while getting orphaned records", mlog.Err(result.Err))
|
||||
return result
|
||||
}
|
||||
data.ParentName = config.parentName
|
||||
data.ChildName = config.childName
|
||||
data.ParentIdAttr = config.parentIdAttr
|
||||
data.ChildIdAttr = config.childIdAttr
|
||||
result.Data = data
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func checkChannelsCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Channels",
|
||||
parentIdAttr: "ChannelId",
|
||||
childName: "CommandWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkChannelsChannelMemberHistoryIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Channels",
|
||||
parentIdAttr: "ChannelId",
|
||||
childName: "ChannelMemberHistory",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkChannelsChannelMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Channels",
|
||||
parentIdAttr: "ChannelId",
|
||||
childName: "ChannelMembers",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkChannelsIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Channels",
|
||||
parentIdAttr: "ChannelId",
|
||||
childName: "IncomingWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkChannelsOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Channels",
|
||||
parentIdAttr: "ChannelId",
|
||||
childName: "OutgoingWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkChannelsPostsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Channels",
|
||||
parentIdAttr: "ChannelId",
|
||||
childName: "Posts",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkCommandsCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Commands",
|
||||
parentIdAttr: "CommandId",
|
||||
childName: "CommandWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkPostsFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Posts",
|
||||
parentIdAttr: "PostId",
|
||||
childName: "FileInfo",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkPostsPostsRootIdIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Posts",
|
||||
parentIdAttr: "RootId",
|
||||
childName: "Posts",
|
||||
childIdAttr: "Id",
|
||||
canParentIdBeEmpty: true,
|
||||
})
|
||||
}
|
||||
|
||||
func checkPostsReactionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Posts",
|
||||
parentIdAttr: "PostId",
|
||||
childName: "Reactions",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkSchemesChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Schemes",
|
||||
parentIdAttr: "SchemeId",
|
||||
childName: "Channels",
|
||||
childIdAttr: "Id",
|
||||
canParentIdBeEmpty: true,
|
||||
})
|
||||
}
|
||||
|
||||
func checkSchemesTeamsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Schemes",
|
||||
parentIdAttr: "SchemeId",
|
||||
childName: "Teams",
|
||||
childIdAttr: "Id",
|
||||
canParentIdBeEmpty: true,
|
||||
})
|
||||
}
|
||||
|
||||
func checkSessionsAuditsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Sessions",
|
||||
parentIdAttr: "SessionId",
|
||||
childName: "Audits",
|
||||
childIdAttr: "Id",
|
||||
canParentIdBeEmpty: true,
|
||||
})
|
||||
}
|
||||
|
||||
func checkTeamsChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
res1 := checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Teams",
|
||||
parentIdAttr: "TeamId",
|
||||
childName: "Channels",
|
||||
childIdAttr: "Id",
|
||||
filter: sq.NotEq{"CT.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
|
||||
})
|
||||
res2 := checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Teams",
|
||||
parentIdAttr: "TeamId",
|
||||
childName: "Channels",
|
||||
childIdAttr: "Id",
|
||||
canParentIdBeEmpty: true,
|
||||
filter: sq.Eq{"CT.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
|
||||
})
|
||||
data1 := res1.Data.(model.RelationalIntegrityCheckData)
|
||||
data2 := res2.Data.(model.RelationalIntegrityCheckData)
|
||||
data1.Records = append(data1.Records, data2.Records...)
|
||||
res1.Data = data1
|
||||
return res1
|
||||
}
|
||||
|
||||
func checkTeamsCommandsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Teams",
|
||||
parentIdAttr: "TeamId",
|
||||
childName: "Commands",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkTeamsIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Teams",
|
||||
parentIdAttr: "TeamId",
|
||||
childName: "IncomingWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkTeamsOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Teams",
|
||||
parentIdAttr: "TeamId",
|
||||
childName: "OutgoingWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkTeamsTeamMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Teams",
|
||||
parentIdAttr: "TeamId",
|
||||
childName: "TeamMembers",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersAuditsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "Audits",
|
||||
childIdAttr: "Id",
|
||||
canParentIdBeEmpty: true,
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "CommandWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersChannelMemberHistoryIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "ChannelMemberHistory",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersChannelMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "ChannelMembers",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersChannelsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "CreatorId",
|
||||
childName: "Channels",
|
||||
childIdAttr: "Id",
|
||||
canParentIdBeEmpty: true,
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersCommandsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "CreatorId",
|
||||
childName: "Commands",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersCompliancesIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "Compliances",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersEmojiIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "CreatorId",
|
||||
childName: "Emoji",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "CreatorId",
|
||||
childName: "FileInfo",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersIncomingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "IncomingWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersOAuthAccessDataIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "OAuthAccessData",
|
||||
childIdAttr: "Token",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersOAuthAppsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "CreatorId",
|
||||
childName: "OAuthApps",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersOAuthAuthDataIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "OAuthAuthData",
|
||||
childIdAttr: "Code",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersOutgoingWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "CreatorId",
|
||||
childName: "OutgoingWebhooks",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersPostsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "Posts",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersPreferencesIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "Preferences",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersReactionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "Reactions",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersSessionsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "Sessions",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersStatusIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "Status",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersTeamMembersIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "TeamMembers",
|
||||
childIdAttr: "",
|
||||
})
|
||||
}
|
||||
|
||||
func checkUsersUserAccessTokensIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Users",
|
||||
parentIdAttr: "UserId",
|
||||
childName: "UserAccessTokens",
|
||||
childIdAttr: "Id",
|
||||
})
|
||||
}
|
||||
|
||||
func checkChannelsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
|
||||
results <- checkChannelsCommandWebhooksIntegrity(ss)
|
||||
results <- checkChannelsChannelMemberHistoryIntegrity(ss)
|
||||
results <- checkChannelsChannelMembersIntegrity(ss)
|
||||
results <- checkChannelsIncomingWebhooksIntegrity(ss)
|
||||
results <- checkChannelsOutgoingWebhooksIntegrity(ss)
|
||||
results <- checkChannelsPostsIntegrity(ss)
|
||||
}
|
||||
|
||||
func checkCommandsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
|
||||
results <- checkCommandsCommandWebhooksIntegrity(ss)
|
||||
}
|
||||
|
||||
func checkPostsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
|
||||
results <- checkPostsFileInfoIntegrity(ss)
|
||||
results <- checkPostsPostsRootIdIntegrity(ss)
|
||||
results <- checkPostsReactionsIntegrity(ss)
|
||||
results <- checkThreadsTeamsIntegrity(ss)
|
||||
}
|
||||
|
||||
func checkSchemesIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
|
||||
results <- checkSchemesChannelsIntegrity(ss)
|
||||
results <- checkSchemesTeamsIntegrity(ss)
|
||||
}
|
||||
|
||||
func checkSessionsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
|
||||
results <- checkSessionsAuditsIntegrity(ss)
|
||||
}
|
||||
|
||||
func checkTeamsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
|
||||
results <- checkTeamsChannelsIntegrity(ss)
|
||||
results <- checkTeamsCommandsIntegrity(ss)
|
||||
results <- checkTeamsIncomingWebhooksIntegrity(ss)
|
||||
results <- checkTeamsOutgoingWebhooksIntegrity(ss)
|
||||
results <- checkTeamsTeamMembersIntegrity(ss)
|
||||
}
|
||||
|
||||
func checkUsersIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
|
||||
results <- checkUsersAuditsIntegrity(ss)
|
||||
results <- checkUsersCommandWebhooksIntegrity(ss)
|
||||
results <- checkUsersChannelMemberHistoryIntegrity(ss)
|
||||
results <- checkUsersChannelMembersIntegrity(ss)
|
||||
results <- checkUsersChannelsIntegrity(ss)
|
||||
results <- checkUsersCommandsIntegrity(ss)
|
||||
results <- checkUsersCompliancesIntegrity(ss)
|
||||
results <- checkUsersEmojiIntegrity(ss)
|
||||
results <- checkUsersFileInfoIntegrity(ss)
|
||||
results <- checkUsersIncomingWebhooksIntegrity(ss)
|
||||
results <- checkUsersOAuthAccessDataIntegrity(ss)
|
||||
results <- checkUsersOAuthAppsIntegrity(ss)
|
||||
results <- checkUsersOAuthAuthDataIntegrity(ss)
|
||||
results <- checkUsersOutgoingWebhooksIntegrity(ss)
|
||||
results <- checkUsersPostsIntegrity(ss)
|
||||
results <- checkUsersPreferencesIntegrity(ss)
|
||||
results <- checkUsersReactionsIntegrity(ss)
|
||||
results <- checkUsersSessionsIntegrity(ss)
|
||||
results <- checkUsersStatusIntegrity(ss)
|
||||
results <- checkUsersTeamMembersIntegrity(ss)
|
||||
results <- checkUsersUserAccessTokensIntegrity(ss)
|
||||
}
|
||||
|
||||
func checkThreadsTeamsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
|
||||
return checkParentChildIntegrity(ss, relationalCheckConfig{
|
||||
parentName: "Teams",
|
||||
parentIdAttr: "ThreadTeamId",
|
||||
childName: "Threads",
|
||||
childIdAttr: "PostId",
|
||||
canParentIdBeEmpty: false,
|
||||
})
|
||||
}
|
||||
|
||||
func CheckRelationalIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {
|
||||
mlog.Info("Starting relational integrity checks...")
|
||||
checkChannelsIntegrity(ss, results)
|
||||
checkCommandsIntegrity(ss, results)
|
||||
checkPostsIntegrity(ss, results)
|
||||
checkSchemesIntegrity(ss, results)
|
||||
checkSessionsIntegrity(ss, results)
|
||||
checkTeamsIntegrity(ss, results)
|
||||
checkUsersIntegrity(ss, results)
|
||||
mlog.Info("Done with relational integrity checks")
|
||||
close(results)
|
||||
}
|
||||
1643
server/channels/store/sqlstore/integrity_test.go
Обычный файл
1643
server/channels/store/sqlstore/integrity_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
344
server/channels/store/sqlstore/job_store.go
Обычный файл
344
server/channels/store/sqlstore/job_store.go
Обычный файл
@@ -0,0 +1,344 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
const (
|
||||
jobsCleanupDelay = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
type SqlJobStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlJobStore(sqlStore *SqlStore) store.JobStore {
|
||||
return &SqlJobStore{sqlStore}
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) Save(job *model.Job) (*model.Job, error) {
|
||||
jsonData, err := json.Marshal(job.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed marshalling job data")
|
||||
}
|
||||
if jss.IsBinaryParamEnabled() {
|
||||
jsonData = AppendBinaryFlag(jsonData)
|
||||
}
|
||||
query := jss.getQueryBuilder().
|
||||
Insert("Jobs").
|
||||
Columns("Id", "Type", "Priority", "CreateAt", "StartAt", "LastActivityAt", "Status", "Progress", "Data").
|
||||
Values(job.Id, job.Type, job.Priority, job.CreateAt, job.StartAt, job.LastActivityAt, job.Status, job.Progress, jsonData)
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate sqlquery")
|
||||
}
|
||||
|
||||
if _, err = jss.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save Preference")
|
||||
}
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) UpdateOptimistically(job *model.Job, currentStatus string) (bool, error) {
|
||||
dataJSON, jsonErr := json.Marshal(job.Data)
|
||||
if jsonErr != nil {
|
||||
return false, errors.Wrap(jsonErr, "failed to encode job's data to JSON")
|
||||
}
|
||||
if jss.IsBinaryParamEnabled() {
|
||||
dataJSON = AppendBinaryFlag(dataJSON)
|
||||
}
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Update("Jobs").
|
||||
Set("LastActivityAt", model.GetMillis()).
|
||||
Set("Status", job.Status).
|
||||
Set("Data", dataJSON).
|
||||
Set("Progress", job.Progress).
|
||||
Where(sq.Eq{"Id": job.Id, "Status": currentStatus}).ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
sqlResult, err := jss.GetMasterX().Exec(query, args...)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to update Job")
|
||||
}
|
||||
|
||||
rows, err := sqlResult.RowsAffected()
|
||||
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "unable to get rows affected")
|
||||
}
|
||||
|
||||
if rows != 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) UpdateStatus(id string, status string) (*model.Job, error) {
|
||||
job := &model.Job{
|
||||
Id: id,
|
||||
Status: status,
|
||||
LastActivityAt: model.GetMillis(),
|
||||
}
|
||||
|
||||
if _, err := jss.GetMasterX().NamedExec(`UPDATE Jobs
|
||||
SET Status=:Status, LastActivityAt=:LastActivityAt
|
||||
WHERE Id=:Id`, job); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to update Job with id=%s", id)
|
||||
}
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) UpdateStatusOptimistically(id string, currentStatus string, newStatus string) (bool, error) {
|
||||
builder := jss.getQueryBuilder().
|
||||
Update("Jobs").
|
||||
Set("LastActivityAt", model.GetMillis()).
|
||||
Set("Status", newStatus).
|
||||
Where(sq.Eq{"Id": id, "Status": currentStatus})
|
||||
|
||||
if newStatus == model.JobStatusInProgress {
|
||||
builder = builder.Set("StartAt", model.GetMillis())
|
||||
}
|
||||
query, args, err := builder.ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
|
||||
sqlResult, err := jss.GetMasterX().Exec(query, args...)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to update Job with id=%s", id)
|
||||
}
|
||||
rows, err := sqlResult.RowsAffected()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "unable to get rows affected")
|
||||
}
|
||||
if rows != 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) Get(id string) (*model.Job, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Jobs").
|
||||
Where(sq.Eq{"Id": id}).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
var status model.Job
|
||||
if err = jss.GetReplicaX().Get(&status, query, args...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("Job", id)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to get Job with id=%s", id)
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetAllPage(offset int, limit int) ([]*model.Job, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Jobs").
|
||||
OrderBy("CreateAt DESC").
|
||||
Limit(uint64(limit)).
|
||||
Offset(uint64(offset)).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
|
||||
statuses := []*model.Job{}
|
||||
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find Jobs")
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetAllByTypesPage(jobTypes []string, offset int, limit int) ([]*model.Job, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Jobs").
|
||||
Where(sq.Eq{"Type": jobTypes}).
|
||||
OrderBy("CreateAt DESC").
|
||||
Limit(uint64(limit)).
|
||||
Offset(uint64(offset)).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
|
||||
var jobs []*model.Job
|
||||
if err = jss.GetReplicaX().Select(&jobs, query, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find Jobs with types")
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetAllByType(jobType string) ([]*model.Job, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Jobs").
|
||||
Where(sq.Eq{"Type": jobType}).
|
||||
OrderBy("CreateAt DESC").ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
statuses := []*model.Job{}
|
||||
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetAllByTypeAndStatus(jobType string, status string) ([]*model.Job, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Jobs").
|
||||
Where(sq.Eq{"Type": jobType, "Status": status}).
|
||||
OrderBy("CreateAt DESC").ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
jobs := []*model.Job{}
|
||||
if err = jss.GetReplicaX().Select(&jobs, query, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetAllByTypePage(jobType string, offset int, limit int) ([]*model.Job, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Jobs").
|
||||
Where(sq.Eq{"Type": jobType}).
|
||||
OrderBy("CreateAt DESC").
|
||||
Limit(uint64(limit)).
|
||||
Offset(uint64(offset)).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
|
||||
statuses := []*model.Job{}
|
||||
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find Jobs with type=%s", jobType)
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetAllByStatus(status string) ([]*model.Job, error) {
|
||||
statuses := []*model.Job{}
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Jobs").
|
||||
Where(sq.Eq{"Status": status}).
|
||||
OrderBy("CreateAt ASC").ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
|
||||
if err = jss.GetReplicaX().Select(&statuses, query, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find Jobs with status=%s", status)
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetNewestJobByStatusAndType(status string, jobType string) (*model.Job, error) {
|
||||
return jss.GetNewestJobByStatusesAndType([]string{status}, jobType)
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetNewestJobByStatusesAndType(status []string, jobType string) (*model.Job, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("*").
|
||||
From("Jobs").
|
||||
Where(sq.Eq{"Status": status, "Type": jobType}).
|
||||
OrderBy("CreateAt DESC").
|
||||
Limit(1).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
|
||||
var job model.Job
|
||||
if err = jss.GetReplicaX().Get(&job, query, args...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("Job", fmt.Sprintf("<status, type>=<%s, %s>", strings.Join(status, ","), jobType))
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to find Job with statuses=%s and type=%s", strings.Join(status, ","), jobType)
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) GetCountByStatusAndType(status string, jobType string) (int64, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Select("COUNT(*)").
|
||||
From("Jobs").
|
||||
Where(sq.Eq{"Status": status, "Type": jobType}).ToSql()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = jss.GetReplicaX().Get(&count, query, args...)
|
||||
if err != nil {
|
||||
return int64(0), errors.Wrapf(err, "failed to count Jobs with status=%s and type=%s", status, jobType)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) Delete(id string) (string, error) {
|
||||
query, args, err := jss.getQueryBuilder().
|
||||
Delete("Jobs").
|
||||
Where(sq.Eq{"Id": id}).ToSql()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "job_tosql")
|
||||
}
|
||||
|
||||
if _, err = jss.GetMasterX().Exec(query, args...); err != nil {
|
||||
return "", errors.Wrapf(err, "failed to delete Job with id=%s", id)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (jss SqlJobStore) Cleanup(expiryTime int64, batchSize int) error {
|
||||
var query string
|
||||
if jss.DriverName() == model.DatabaseDriverPostgres {
|
||||
query = "DELETE FROM Jobs WHERE Id IN (SELECT Id FROM Jobs WHERE CreateAt < ? AND (Status != ? AND Status != ?) ORDER BY CreateAt ASC LIMIT ?)"
|
||||
} else {
|
||||
query = "DELETE FROM Jobs WHERE CreateAt < ? AND (Status != ? AND Status != ?) ORDER BY CreateAt ASC LIMIT ?"
|
||||
}
|
||||
|
||||
var rowsAffected int64 = 1
|
||||
|
||||
for rowsAffected > 0 {
|
||||
sqlResult, err := jss.GetMasterX().Exec(query,
|
||||
expiryTime, model.JobStatusInProgress, model.JobStatusPending, batchSize)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to delete jobs")
|
||||
}
|
||||
var rowErr error
|
||||
rowsAffected, rowErr = sqlResult.RowsAffected()
|
||||
if rowErr != nil {
|
||||
return errors.Wrap(err, "unable to delete jobs")
|
||||
}
|
||||
|
||||
time.Sleep(jobsCleanupDelay)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/job_store_test.go
Обычный файл
14
server/channels/store/sqlstore/job_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestJobStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestJobStore)
|
||||
}
|
||||
99
server/channels/store/sqlstore/license_store.go
Обычный файл
99
server/channels/store/sqlstore/license_store.go
Обычный файл
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
// SqlLicenseStore encapsulates the database writes and reads for
|
||||
// model.LicenseRecord objects.
|
||||
type SqlLicenseStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlLicenseStore(sqlStore *SqlStore) store.LicenseStore {
|
||||
return &SqlLicenseStore{sqlStore}
|
||||
}
|
||||
|
||||
// Save validates and stores the license instance in the database. The Id
|
||||
// and Bytes fields are mandatory. The Bytes field is limited to a maximum
|
||||
// of 10000 bytes. If the license ID matches an existing license in the
|
||||
// database it returns the license stored in the database. If not, it saves the
|
||||
// new database and returns the created license with the CreateAt field
|
||||
// updated.
|
||||
func (ls SqlLicenseStore) Save(license *model.LicenseRecord) (*model.LicenseRecord, error) {
|
||||
license.PreSave()
|
||||
if err := license.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := ls.getQueryBuilder().
|
||||
Select("Id, CreateAt, Bytes").
|
||||
From("Licenses").
|
||||
Where(sq.Eq{"Id": license.Id})
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "license_tosql")
|
||||
}
|
||||
var storedLicense model.LicenseRecord
|
||||
if err := ls.GetReplicaX().Get(&storedLicense, queryString, args...); err != nil {
|
||||
// Only insert if not exists
|
||||
query, args, err := ls.getQueryBuilder().
|
||||
Insert("Licenses").
|
||||
Columns("Id", "CreateAt", "Bytes").
|
||||
Values(license.Id, license.CreateAt, license.Bytes).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "license_record_tosql")
|
||||
}
|
||||
if _, err := ls.GetMasterX().Exec(query, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get License with licenseId=%s", license.Id)
|
||||
}
|
||||
return license, nil
|
||||
}
|
||||
return &storedLicense, nil
|
||||
}
|
||||
|
||||
// Get obtains the license with the provided id parameter from the database.
|
||||
// If the license doesn't exist it returns a model.AppError with
|
||||
// http.StatusNotFound in the StatusCode field.
|
||||
func (ls SqlLicenseStore) Get(id string) (*model.LicenseRecord, error) {
|
||||
query := ls.getQueryBuilder().
|
||||
Select("Id, CreateAt, Bytes").
|
||||
From("Licenses").
|
||||
Where(sq.Eq{"Id": id})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "license_record_tosql")
|
||||
}
|
||||
|
||||
license := &model.LicenseRecord{}
|
||||
if err := ls.GetReplicaX().Get(license, queryString, args...); err != nil {
|
||||
return nil, store.NewErrNotFound("License", id)
|
||||
}
|
||||
return license, nil
|
||||
}
|
||||
|
||||
func (ls SqlLicenseStore) GetAll() ([]*model.LicenseRecord, error) {
|
||||
query := ls.getQueryBuilder().
|
||||
Select("Id, CreateAt, Bytes").
|
||||
From("Licenses")
|
||||
|
||||
queryString, _, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "license_tosql")
|
||||
}
|
||||
|
||||
licenses := []*model.LicenseRecord{}
|
||||
if err := ls.GetReplicaX().Select(&licenses, queryString); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch licenses")
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/license_store_test.go
Обычный файл
14
server/channels/store/sqlstore/license_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestLicenseStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestLicenseStore)
|
||||
}
|
||||
87
server/channels/store/sqlstore/link_metadata_store.go
Обычный файл
87
server/channels/store/sqlstore/link_metadata_store.go
Обычный файл
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlLinkMetadataStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlLinkMetadataStore(sqlStore *SqlStore) store.LinkMetadataStore {
|
||||
return &SqlLinkMetadataStore{sqlStore}
|
||||
}
|
||||
|
||||
func (s SqlLinkMetadataStore) Save(metadata *model.LinkMetadata) (*model.LinkMetadata, error) {
|
||||
if err := metadata.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadata.PreSave()
|
||||
metadataBytes, err := json.Marshal(metadata.Data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not serialize metadataBytes to JSON")
|
||||
}
|
||||
if s.IsBinaryParamEnabled() {
|
||||
metadataBytes = AppendBinaryFlag(metadataBytes)
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Insert("LinkMetadata").
|
||||
Columns("Hash", "URL", "Timestamp", "Type", "Data").
|
||||
Values(metadata.Hash, metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes)
|
||||
|
||||
if s.DriverName() == model.DatabaseDriverMysql {
|
||||
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE URL = ?, Timestamp = ?, Type = ?, Data = ?", metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes))
|
||||
} else {
|
||||
query = query.SuffixExpr(sq.Expr("ON CONFLICT (hash) DO UPDATE SET URL = ?, Timestamp = ?, Type = ?, Data = ?", metadata.URL, metadata.Timestamp, metadata.Type, metadataBytes))
|
||||
}
|
||||
|
||||
q, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "metadata_tosql")
|
||||
}
|
||||
|
||||
_, err = s.GetMasterX().Exec(q, args...)
|
||||
if err != nil && !IsUniqueConstraintError(err, []string{"PRIMARY", "linkmetadata_pkey"}) {
|
||||
return nil, errors.Wrap(err, "could not save link metadata")
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func (s SqlLinkMetadataStore) Get(url string, timestamp int64) (*model.LinkMetadata, error) {
|
||||
var metadata model.LinkMetadata
|
||||
query, args, err := s.getQueryBuilder().
|
||||
Select("*").
|
||||
From("LinkMetadata").
|
||||
Where(sq.Eq{"URL": url, "Timestamp": timestamp}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create query with querybuilder")
|
||||
}
|
||||
err = s.GetReplicaX().Get(&metadata, query, args...)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("LinkMetadata", "url="+url)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "could not get metadata with selectone: url=%s", url)
|
||||
}
|
||||
|
||||
err = metadata.DeserializeDataToConcreteType()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not deserialize metadata to concrete type for url=%s", url)
|
||||
}
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/link_metadata_store_test.go
Обычный файл
14
server/channels/store/sqlstore/link_metadata_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestLinkMetadataStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestLinkMetadataStore)
|
||||
}
|
||||
23
server/channels/store/sqlstore/main_test.go
Обычный файл
23
server/channels/store/sqlstore/main_test.go
Обычный файл
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
|
||||
)
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
mainHelper = testlib.NewMainHelperWithOptions(nil)
|
||||
defer mainHelper.Close()
|
||||
|
||||
sqlstore.InitTest()
|
||||
|
||||
mainHelper.Main(m)
|
||||
sqlstore.TearDownTest()
|
||||
}
|
||||
96
server/channels/store/sqlstore/notify_admin_store.go
Обычный файл
96
server/channels/store/sqlstore/notify_admin_store.go
Обычный файл
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlNotifyAdminStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlNotifyAdminStore(sqlStore *SqlStore) store.NotifyAdminStore {
|
||||
return &SqlNotifyAdminStore{sqlStore}
|
||||
}
|
||||
|
||||
func (s SqlNotifyAdminStore) insert(data *model.NotifyAdminData) (sql.Result, error) {
|
||||
query := `INSERT INTO NotifyAdmin (UserId, CreateAt, RequiredPlan, RequiredFeature, Trial) VALUES (:UserId, :CreateAt, :RequiredPlan, :RequiredFeature, :Trial)`
|
||||
return s.GetMasterX().NamedExec(query, data)
|
||||
}
|
||||
|
||||
func (s SqlNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
|
||||
if err := data.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data.PreSave()
|
||||
|
||||
_, err := s.insert(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save Notify Admin data")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s SqlNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
|
||||
data := []*model.NotifyAdminData{}
|
||||
query, args, err := s.getQueryBuilder().
|
||||
Select("*").
|
||||
From("NotifyAdmin").
|
||||
Where(sq.Eq{"UserId": userId, "RequiredFeature": feature}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not build sql query to get all notification data by user id and required feature")
|
||||
}
|
||||
|
||||
if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("NotifyAdmin", fmt.Sprintf("user id: %s and required feature: %s", userId, feature))
|
||||
}
|
||||
return nil, errors.Wrapf(err, "notifcation data by user id: %s and required feature: %s", userId, feature)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s SqlNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
|
||||
data := []*model.NotifyAdminData{}
|
||||
query, args, err := s.getQueryBuilder().
|
||||
Select("*").
|
||||
From("NotifyAdmin").
|
||||
Where(sq.Eq{"Trial": trial}).
|
||||
Where("(SentAt IS NULL)").
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not build sql query to get all notifcation data")
|
||||
}
|
||||
|
||||
if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "notifcation data")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s SqlNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
|
||||
if _, err := s.GetMasterX().Exec("DELETE FROM NotifyAdmin WHERE Trial = ? AND CreateAt < ? AND SentAt IS NULL", trial, now); err != nil {
|
||||
return errors.Wrapf(err, "failed to remove all notification data with trial=%t", trial)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
|
||||
if _, err := s.GetMasterX().Exec("UPDATE NotifyAdmin SET SentAt = ? WHERE UserId = ? AND RequiredPlan = ? AND RequiredFeature = ?", now, userId, requiredPlan, requiredFeature); err != nil {
|
||||
return errors.Wrapf(err, "failed to update SentAt for userId=%s and requiredPlan=%s", userId, requiredPlan)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/notify_admin_store_test.go
Обычный файл
14
server/channels/store/sqlstore/notify_admin_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestNotifyAdminStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestNotifyAdminStore)
|
||||
}
|
||||
322
server/channels/store/sqlstore/oauth_store.go
Обычный файл
322
server/channels/store/sqlstore/oauth_store.go
Обычный файл
@@ -0,0 +1,322 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlOAuthStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlOAuthStore(sqlStore *SqlStore) store.OAuthStore {
|
||||
return &SqlOAuthStore{sqlStore}
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) SaveApp(app *model.OAuthApp) (*model.OAuthApp, error) {
|
||||
if app.Id != "" {
|
||||
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
|
||||
}
|
||||
|
||||
app.PreSave()
|
||||
if err := app.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthApps
|
||||
(Id, CreatorId, CreateAt, UpdateAt, ClientSecret, Name, Description, IconURL, CallbackUrls, Homepage, IsTrusted, MattermostAppID)
|
||||
VALUES
|
||||
(:Id, :CreatorId, :CreateAt, :UpdateAt, :ClientSecret, :Name, :Description, :IconURL, :CallbackUrls, :Homepage, :IsTrusted, :MattermostAppID)`, app); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save OAuthApp")
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) UpdateApp(app *model.OAuthApp) (*model.OAuthApp, error) {
|
||||
app.PreUpdate()
|
||||
|
||||
if err := app.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var oldApp model.OAuthApp
|
||||
err := as.GetMasterX().Get(&oldApp, `SELECT * FROM OAuthApps
|
||||
WHERE id=?`, app.Id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get OAuthApp with id=%s", app.Id)
|
||||
}
|
||||
if oldApp.Id == "" {
|
||||
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
|
||||
}
|
||||
|
||||
app.CreateAt = oldApp.CreateAt
|
||||
app.CreatorId = oldApp.CreatorId
|
||||
|
||||
res, err := as.GetMasterX().NamedExec(`UPDATE OAuthApps
|
||||
SET UpdateAt=:UpdateAt, ClientSecret=:ClientSecret, Name=:Name,
|
||||
Description=:Description, IconURL=:IconURL, CallbackUrls=:CallbackUrls,
|
||||
Homepage=:Homepage, IsTrusted=:IsTrusted, MattermostAppID=:MattermostAppID
|
||||
WHERE Id=:Id`, app)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to update OAuthApp with id=%s", app.Id)
|
||||
}
|
||||
count, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error while getting rows_affected")
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, store.NewErrInvalidInput("OAuthApp", "Id", app.Id)
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetApp(id string) (*model.OAuthApp, error) {
|
||||
var app model.OAuthApp
|
||||
if err := as.GetReplicaX().Get(&app, `SELECT * FROM OAuthApps WHERE Id=?`, id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("OAuthApp", id)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to get OAuthApp with id=%s", id)
|
||||
}
|
||||
if app.Id == "" {
|
||||
return nil, store.NewErrNotFound("OAuthApp", id)
|
||||
}
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetAppByUser(userId string, offset, limit int) ([]*model.OAuthApp, error) {
|
||||
apps := []*model.OAuthApp{}
|
||||
|
||||
if err := as.GetReplicaX().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = ? LIMIT ? OFFSET ?", userId, limit, offset); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find OAuthApps with userId=%s", userId)
|
||||
}
|
||||
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetApps(offset, limit int) ([]*model.OAuthApp, error) {
|
||||
apps := []*model.OAuthApp{}
|
||||
|
||||
if err := as.GetReplicaX().Select(&apps, "SELECT * FROM OAuthApps LIMIT ? OFFSET ?", limit, offset); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find OAuthApps")
|
||||
}
|
||||
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetAuthorizedApps(userId string, offset, limit int) ([]*model.OAuthApp, error) {
|
||||
apps := []*model.OAuthApp{}
|
||||
|
||||
if err := as.GetReplicaX().Select(&apps,
|
||||
`SELECT o.* FROM OAuthApps AS o INNER JOIN
|
||||
Preferences AS p ON p.Name=o.Id AND p.UserId=? LIMIT ? OFFSET ?`, userId, limit, offset); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find OAuthApps with userId=%s", userId)
|
||||
}
|
||||
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) DeleteApp(id string) (err error) {
|
||||
// wrap in a transaction so that if one fails, everything fails
|
||||
transaction, err := as.GetMasterX().Beginx()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "begin_transaction")
|
||||
}
|
||||
defer finalizeTransactionX(transaction, &err)
|
||||
|
||||
if err := as.deleteApp(transaction, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := transaction.Commit(); err != nil {
|
||||
// don't need to rollback here since the transaction is already closed
|
||||
return errors.Wrap(err, "commit_transaction")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) {
|
||||
if err := accessData.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthAccessData
|
||||
(ClientId, UserId, Token, RefreshToken, RedirectUri, ExpiresAt, Scope)
|
||||
VALUES
|
||||
(:ClientId, :UserId, :Token, :RefreshToken, :RedirectUri, :ExpiresAt, :Scope)`, accessData); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save AccessData")
|
||||
}
|
||||
return accessData, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetAccessData(token string) (*model.AccessData, error) {
|
||||
accessData := model.AccessData{}
|
||||
|
||||
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE Token = ?", token); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get OAuthAccessData with token=%s", token)
|
||||
}
|
||||
return &accessData, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetAccessDataByUserForApp(userID, clientID string) ([]*model.AccessData, error) {
|
||||
accessData := []*model.AccessData{}
|
||||
|
||||
if err := as.GetReplicaX().Select(&accessData,
|
||||
"SELECT * FROM OAuthAccessData WHERE UserId = ? AND ClientId = ?", userID, clientID); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to delete OAuthAccessData with userId=%s and clientId=%s", userID, clientID)
|
||||
}
|
||||
return accessData, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetAccessDataByRefreshToken(token string) (*model.AccessData, error) {
|
||||
accessData := model.AccessData{}
|
||||
|
||||
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE RefreshToken = ?", token); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find OAuthAccessData with refreshToken=%s", token)
|
||||
}
|
||||
return &accessData, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetPreviousAccessData(userID, clientID string) (*model.AccessData, error) {
|
||||
accessData := model.AccessData{}
|
||||
|
||||
if err := as.GetReplicaX().Get(&accessData, "SELECT * FROM OAuthAccessData WHERE ClientId = ? AND UserId = ?", clientID, userID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "failed to get AccessData with clientId=%s and userId=%s", clientID, userID)
|
||||
}
|
||||
return &accessData, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) {
|
||||
if err := accessData.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := as.GetMasterX().NamedExec("UPDATE OAuthAccessData SET Token = :Token, ExpiresAt = :ExpiresAt, RefreshToken = :RefreshToken WHERE ClientId = :ClientId AND UserID = :UserId", accessData); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to update OAuthAccessData with userId=%s and clientId=%s", accessData.UserId, accessData.ClientId)
|
||||
}
|
||||
return accessData, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) RemoveAccessData(token string) error {
|
||||
if _, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData WHERE Token = ?", token); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete OAuthAccessData with token=%s", token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) RemoveAllAccessData() error {
|
||||
if _, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData"); err != nil {
|
||||
return errors.Wrap(err, "failed to delete OAuthAccessData")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, error) {
|
||||
authData.PreSave()
|
||||
if err := authData.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := as.GetMasterX().NamedExec(`INSERT INTO OAuthAuthData
|
||||
(ClientId, UserId, Code, ExpiresIn, CreateAt, RedirectUri, State, Scope)
|
||||
VALUES
|
||||
(:ClientId, :UserId, :Code, :ExpiresIn, :CreateAt, :RedirectUri, :State, :Scope)`, authData); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save AuthData")
|
||||
}
|
||||
return authData, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetAuthData(code string) (*model.AuthData, error) {
|
||||
var authData model.AuthData
|
||||
err := as.GetReplicaX().Get(&authData, `SELECT * FROM OAuthAuthData WHERE Code=?`, code)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("AuthData", fmt.Sprintf("code=%s", code))
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to get AuthData with code=%s", code)
|
||||
}
|
||||
if authData.Code == "" {
|
||||
return nil, store.NewErrNotFound("AuthData", fmt.Sprintf("code=%s", code))
|
||||
}
|
||||
return &authData, nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) RemoveAuthData(code string) error {
|
||||
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAuthData WHERE Code = ?", code)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to delete AuthData with code=%s", code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) error {
|
||||
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAuthData WHERE ClientId = ? and UserId = ?", clientId, userId)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to delete AuthData with clientId=%s and userId=%s", clientId, userId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) PermanentDeleteAuthDataByUser(userId string) error {
|
||||
_, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData WHERE UserId = ?", userId)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to delete OAuthAccessData with userId=%s", userId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) deleteApp(transaction *sqlxTxWrapper, clientId string) error {
|
||||
if _, err := transaction.Exec("DELETE FROM OAuthApps WHERE Id = ?", clientId); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete OAuthApp with id=%s", clientId)
|
||||
}
|
||||
|
||||
return as.deleteOAuthAppSessions(transaction, clientId)
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) deleteOAuthAppSessions(transaction *sqlxTxWrapper, clientId string) error {
|
||||
query := ""
|
||||
if as.DriverName() == model.DatabaseDriverPostgres {
|
||||
query = "DELETE FROM Sessions s USING OAuthAccessData o WHERE o.Token = s.Token AND o.ClientId = ?"
|
||||
} else if as.DriverName() == model.DatabaseDriverMysql {
|
||||
query = "DELETE s.* FROM Sessions s INNER JOIN OAuthAccessData o ON o.Token = s.Token WHERE o.ClientId = ?"
|
||||
}
|
||||
|
||||
if _, err := transaction.Exec(query, clientId); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete Session with OAuthAccessData.Id=%s", clientId)
|
||||
}
|
||||
|
||||
return as.deleteOAuthTokens(transaction, clientId)
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) deleteOAuthTokens(transaction *sqlxTxWrapper, clientId string) error {
|
||||
if _, err := transaction.Exec("DELETE FROM OAuthAccessData WHERE ClientId = ?", clientId); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete OAuthAccessData with id=%s", clientId)
|
||||
}
|
||||
|
||||
return as.deleteAppExtras(transaction, clientId)
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) deleteAppExtras(transaction *sqlxTxWrapper, clientId string) error {
|
||||
if _, err := transaction.Exec(
|
||||
`DELETE FROM
|
||||
Preferences
|
||||
WHERE
|
||||
Category = ?
|
||||
AND Name = ?`, model.PreferenceCategoryAuthorizedOAuthApp, clientId); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete Preferences with name=%s", clientId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/oauth_store_test.go
Обычный файл
14
server/channels/store/sqlstore/oauth_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestOAuthStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestOAuthStore)
|
||||
}
|
||||
358
server/channels/store/sqlstore/plugin_store.go
Обычный файл
358
server/channels/store/sqlstore/plugin_store.go
Обычный файл
@@ -0,0 +1,358 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPluginKeyFetchLimit = 10
|
||||
)
|
||||
|
||||
type SqlPluginStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlPluginStore(sqlStore *SqlStore) store.PluginStore {
|
||||
return &SqlPluginStore{sqlStore}
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) SaveOrUpdate(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
|
||||
if err := kv.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if kv.Value == nil {
|
||||
// Setting a key to nil is the same as removing it
|
||||
err := ps.Delete(kv.PluginId, kv.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
query := ps.getQueryBuilder().
|
||||
Insert("PluginKeyValueStore").
|
||||
Columns("PluginId", "PKey", "PValue", "ExpireAt").
|
||||
Values(kv.PluginId, kv.Key, kv.Value, kv.ExpireAt)
|
||||
if ps.DriverName() == model.DatabaseDriverPostgres {
|
||||
query = query.SuffixExpr(sq.Expr("ON CONFLICT (pluginid, pkey) DO UPDATE SET PValue = ?, ExpireAt = ?", kv.Value, kv.ExpireAt))
|
||||
} else if ps.DriverName() == model.DatabaseDriverMysql {
|
||||
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE PValue = ?, ExpireAt = ?", kv.Value, kv.ExpireAt))
|
||||
}
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to upsert PluginKeyValue")
|
||||
}
|
||||
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) CompareAndSet(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
|
||||
if err := kv.IsValid(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if kv.Value == nil {
|
||||
// Setting a key to nil is the same as removing it
|
||||
return ps.CompareAndDelete(kv, oldValue)
|
||||
}
|
||||
|
||||
if oldValue == nil {
|
||||
// Delete any existing, expired value.
|
||||
query := ps.getQueryBuilder().
|
||||
Delete("PluginKeyValueStore").
|
||||
Where(sq.Eq{"PluginId": kv.PluginId}).
|
||||
Where(sq.Eq{"PKey": kv.Key}).
|
||||
Where(sq.NotEq{"ExpireAt": int(0)}).
|
||||
Where(sq.Lt{"ExpireAt": model.GetMillis()})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
if _, err = ps.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return false, errors.Wrap(err, "failed to delete PluginKeyValue")
|
||||
}
|
||||
|
||||
// Insert if oldValue is nil
|
||||
queryString, args, err = ps.getQueryBuilder().
|
||||
Insert("PluginKeyValueStore").
|
||||
Columns("PluginId", "PKey", "PValue", "ExpireAt").
|
||||
Values(kv.PluginId, kv.Key, kv.Value, kv.ExpireAt).ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
// If the error is from unique constraints violation, it's the result of a
|
||||
// race condition, return false and no error. Otherwise we have a real error and
|
||||
// need to return it.
|
||||
if IsUniqueConstraintError(err, []string{"PRIMARY", "PluginId", "Key", "PKey", "pkey"}) {
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.Wrap(err, "failed to insert PluginKeyValue")
|
||||
}
|
||||
} else {
|
||||
currentTime := model.GetMillis()
|
||||
|
||||
// Update if oldValue is not nil
|
||||
query := ps.getQueryBuilder().
|
||||
Update("PluginKeyValueStore").
|
||||
Set("PValue", kv.Value).
|
||||
Set("ExpireAt", kv.ExpireAt).
|
||||
Where(sq.Eq{"PluginId": kv.PluginId}).
|
||||
Where(sq.Eq{"PKey": kv.Key}).
|
||||
Where(sq.Eq{"PValue": oldValue}).
|
||||
Where(sq.Or{
|
||||
sq.Eq{"ExpireAt": int(0)},
|
||||
sq.Gt{"ExpireAt": currentTime},
|
||||
})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
updateResult, err := ps.GetMasterX().Exec(queryString, args...)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to update PluginKeyValue")
|
||||
}
|
||||
|
||||
if rowsAffected, err := updateResult.RowsAffected(); err != nil {
|
||||
// Failed to update
|
||||
return false, errors.Wrap(err, "unable to get rows affected")
|
||||
} else if rowsAffected == 0 {
|
||||
if ps.DriverName() == model.DatabaseDriverMysql && bytes.Equal(oldValue, kv.Value) {
|
||||
// ROW_COUNT on MySQL is zero even if the row existed but no changes to the row were required.
|
||||
// Check if the row exists with the required value to distinguish this case. Strictly speaking,
|
||||
// this isn't a good use of CompareAndSet anyway, since there's no corresponding guarantee of
|
||||
// atomicity. Nevertheless, let's return results consistent with Postgres and with what might
|
||||
// be expected in this case.
|
||||
query := ps.getQueryBuilder().
|
||||
Select("COUNT(*)").
|
||||
From("PluginKeyValueStore").
|
||||
Where(sq.Eq{"PluginId": kv.PluginId}).
|
||||
Where(sq.Eq{"PKey": kv.Key}).
|
||||
Where(sq.Eq{"PValue": kv.Value}).
|
||||
Where(sq.Or{
|
||||
sq.Eq{"ExpireAt": int(0)},
|
||||
sq.Gt{"ExpireAt": currentTime},
|
||||
})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = ps.GetReplicaX().Get(&count, queryString, args...)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to count PluginKeyValue with pluginId=%s and key=%s", kv.PluginId, kv.Key)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return false, nil
|
||||
} else if count == 1 {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, errors.Wrapf(err, "got too many rows when counting PluginKeyValue with pluginId=%s, key=%s, rows=%d", kv.PluginId, kv.Key, count)
|
||||
}
|
||||
}
|
||||
|
||||
// No rows were affected by the update, where condition was not satisfied,
|
||||
// return false, but no error.
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) CompareAndDelete(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
|
||||
if err := kv.IsValid(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if oldValue == nil {
|
||||
// nil can't be stored. Return showing that we didn't do anything
|
||||
return false, nil
|
||||
}
|
||||
|
||||
query := ps.getQueryBuilder().
|
||||
Delete("PluginKeyValueStore").
|
||||
Where(sq.Eq{"PluginId": kv.PluginId}).
|
||||
Where(sq.Eq{"PKey": kv.Key}).
|
||||
Where(sq.Eq{"PValue": oldValue}).
|
||||
Where(sq.Or{
|
||||
sq.Eq{"ExpireAt": int(0)},
|
||||
sq.Gt{"ExpireAt": model.GetMillis()},
|
||||
})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
deleteResult, err := ps.GetMasterX().Exec(queryString, args...)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to delete PluginKeyValue")
|
||||
}
|
||||
|
||||
if rowsAffected, err := deleteResult.RowsAffected(); err != nil {
|
||||
return false, errors.Wrap(err, "unable to get rows affected")
|
||||
} else if rowsAffected == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) SetWithOptions(pluginId string, key string, value []byte, opt model.PluginKVSetOptions) (bool, error) {
|
||||
if err := opt.IsValid(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
kv, err := model.NewPluginKeyValueFromOptions(pluginId, key, value, opt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if opt.Atomic {
|
||||
return ps.CompareAndSet(kv, opt.OldValue)
|
||||
}
|
||||
|
||||
savedKv, nErr := ps.SaveOrUpdate(kv)
|
||||
if nErr != nil {
|
||||
return false, nErr
|
||||
}
|
||||
|
||||
return savedKv != nil, nil
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) Get(pluginId, key string) (*model.PluginKeyValue, error) {
|
||||
currentTime := model.GetMillis()
|
||||
query := ps.getQueryBuilder().Select("PluginId, PKey, PValue, ExpireAt").
|
||||
From("PluginKeyValueStore").
|
||||
Where(sq.Eq{"PluginId": pluginId}).
|
||||
Where(sq.Eq{"PKey": key}).
|
||||
Where(sq.Or{sq.Eq{"ExpireAt": 0}, sq.Gt{"ExpireAt": currentTime}})
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
row := ps.GetReplicaX().QueryRowx(queryString, args...)
|
||||
var kv model.PluginKeyValue
|
||||
if err := row.Scan(&kv.PluginId, &kv.Key, &kv.Value, &kv.ExpireAt); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("PluginKeyValue", fmt.Sprintf("pluginId=%s, key=%s", pluginId, key))
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to get PluginKeyValue with pluginId=%s and key=%s", pluginId, key)
|
||||
}
|
||||
|
||||
return &kv, nil
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) Delete(pluginId, key string) error {
|
||||
query := ps.getQueryBuilder().
|
||||
Delete("PluginKeyValueStore").
|
||||
Where(sq.Eq{"PluginId": pluginId}).
|
||||
Where(sq.Eq{"Pkey": key})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete PluginKeyValue with pluginId=%s and key=%s", pluginId, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) DeleteAllForPlugin(pluginId string) error {
|
||||
query := ps.getQueryBuilder().
|
||||
Delete("PluginKeyValueStore").
|
||||
Where(sq.Eq{"PluginId": pluginId})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return errors.Wrapf(err, "failed to get all PluginKeyValues with pluginId=%s ", pluginId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) DeleteAllExpired() error {
|
||||
currentTime := model.GetMillis()
|
||||
query := ps.getQueryBuilder().
|
||||
Delete("PluginKeyValueStore").
|
||||
Where(sq.NotEq{"ExpireAt": 0}).
|
||||
Where(sq.Lt{"ExpireAt": currentTime})
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
if _, err := ps.GetMasterX().Exec(queryString, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to delete all expired PluginKeyValues")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps SqlPluginStore) List(pluginId string, offset int, limit int) ([]string, error) {
|
||||
if limit <= 0 {
|
||||
limit = defaultPluginKeyFetchLimit
|
||||
}
|
||||
|
||||
if offset <= 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
query := ps.getQueryBuilder().
|
||||
Select("Pkey").
|
||||
From("PluginKeyValueStore").
|
||||
Where(sq.Eq{"PluginId": pluginId}).
|
||||
Where(sq.Or{
|
||||
sq.Eq{"ExpireAt": int(0)},
|
||||
sq.Gt{"ExpireAt": model.GetMillis()},
|
||||
}).
|
||||
OrderBy("PKey").
|
||||
Limit(uint64(limit)).
|
||||
Offset(uint64(offset))
|
||||
|
||||
queryString, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "plugin_tosql")
|
||||
}
|
||||
|
||||
keys := []string{}
|
||||
err = ps.GetReplicaX().Select(&keys, queryString, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get PluginKeyValues with pluginId=%s", pluginId)
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/plugin_store_test.go
Обычный файл
14
server/channels/store/sqlstore/plugin_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestPluginStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestPluginStore)
|
||||
}
|
||||
192
server/channels/store/sqlstore/post_acknowledgements_store.go
Обычный файл
192
server/channels/store/sqlstore/post_acknowledgements_store.go
Обычный файл
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlPostAcknowledgementStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlPostAcknowledgementStore(sqlStore *SqlStore) store.PostAcknowledgementStore {
|
||||
return &SqlPostAcknowledgementStore{sqlStore}
|
||||
}
|
||||
|
||||
func (s *SqlPostAcknowledgementStore) Get(postID, userID string) (*model.PostAcknowledgement, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("PostId", "UserId", "AcknowledgedAt").
|
||||
From("PostAcknowledgements").
|
||||
Where(sq.And{
|
||||
sq.Eq{"PostId": postID},
|
||||
sq.Eq{"UserId": userID},
|
||||
sq.NotEq{"AcknowledgedAt": 0},
|
||||
})
|
||||
|
||||
var acknowledgement model.PostAcknowledgement
|
||||
err := s.GetReplicaX().GetBuilder(&acknowledgement, query)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("PostAcknowledgement", postID)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &acknowledgement, nil
|
||||
}
|
||||
|
||||
func (s *SqlPostAcknowledgementStore) Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
|
||||
if acknowledgedAt == 0 {
|
||||
acknowledgedAt = model.GetMillis()
|
||||
}
|
||||
|
||||
acknowledgement := &model.PostAcknowledgement{
|
||||
UserId: userID,
|
||||
PostId: postID,
|
||||
AcknowledgedAt: acknowledgedAt,
|
||||
}
|
||||
|
||||
if err := acknowledgement.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transaction, err := s.GetMasterX().Beginx()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "begin_transaction")
|
||||
}
|
||||
defer finalizeTransactionX(transaction, &err)
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Insert("PostAcknowledgements").
|
||||
Columns("PostId", "UserId", "AcknowledgedAt").
|
||||
Values(acknowledgement.PostId, acknowledgement.UserId, acknowledgement.AcknowledgedAt)
|
||||
|
||||
if s.DriverName() == model.DatabaseDriverMysql {
|
||||
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE AcknowledgedAt = ?", acknowledgement.AcknowledgedAt))
|
||||
} else {
|
||||
query = query.SuffixExpr(sq.Expr("ON CONFLICT (postid, userid) DO UPDATE SET AcknowledgedAt = ?", acknowledgement.AcknowledgedAt))
|
||||
}
|
||||
|
||||
_, err = transaction.ExecBuilder(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = updatePost(transaction, acknowledgement.PostId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = transaction.Commit()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "commit_transaction")
|
||||
}
|
||||
|
||||
return acknowledgement, nil
|
||||
}
|
||||
|
||||
func (s *SqlPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
|
||||
transaction, err := s.GetMasterX().Beginx()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "begin_transaction")
|
||||
}
|
||||
defer finalizeTransactionX(transaction, &err)
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Update("PostAcknowledgements").
|
||||
Set("AcknowledgedAt", 0).
|
||||
Where(sq.And{
|
||||
sq.Eq{"PostId": acknowledgement.PostId},
|
||||
sq.Eq{"UserId": acknowledgement.UserId},
|
||||
})
|
||||
|
||||
_, err = transaction.ExecBuilder(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updatePost(transaction, acknowledgement.PostId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = transaction.Commit()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "commit_transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
|
||||
var acknowledgements []*model.PostAcknowledgement
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("PostId", "UserId", "AcknowledgedAt").
|
||||
From("PostAcknowledgements").
|
||||
Where(sq.And{
|
||||
sq.NotEq{"AcknowledgedAt": 0},
|
||||
sq.Eq{"PostId": postID},
|
||||
})
|
||||
|
||||
err := s.GetReplicaX().SelectBuilder(&acknowledgements, query)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for postID=%s", postID)
|
||||
}
|
||||
|
||||
return acknowledgements, nil
|
||||
}
|
||||
|
||||
func (s *SqlPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
|
||||
var acknowledgements []*model.PostAcknowledgement
|
||||
|
||||
perPage := 200
|
||||
for i := 0; i < len(postIds); i += perPage {
|
||||
j := i + perPage
|
||||
if len(postIds) < j {
|
||||
j = len(postIds)
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("PostId", "UserId", "AcknowledgedAt").
|
||||
From("PostAcknowledgements").
|
||||
Where(sq.And{
|
||||
sq.Eq{"PostId": postIds[i:j]},
|
||||
sq.NotEq{"AcknowledgedAt": 0},
|
||||
})
|
||||
|
||||
var acknowledgementsBatch []*model.PostAcknowledgement
|
||||
err := s.GetReplicaX().SelectBuilder(&acknowledgementsBatch, query)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for post list")
|
||||
}
|
||||
|
||||
acknowledgements = append(acknowledgements, acknowledgementsBatch...)
|
||||
}
|
||||
|
||||
return acknowledgements, nil
|
||||
}
|
||||
|
||||
func updatePost(transaction *sqlxTxWrapper, postId string) error {
|
||||
_, err := transaction.Exec(
|
||||
`UPDATE
|
||||
Posts
|
||||
SET
|
||||
UpdateAt = ?
|
||||
WHERE
|
||||
Id = ?`,
|
||||
model.GetMillis(),
|
||||
postId,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestPostAcknowledgementsStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestPostAcknowledgementsStore)
|
||||
}
|
||||
64
server/channels/store/sqlstore/post_priority_store.go
Обычный файл
64
server/channels/store/sqlstore/post_priority_store.go
Обычный файл
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
sq "github.com/mattermost/squirrel"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
)
|
||||
|
||||
type SqlPostPriorityStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func newSqlPostPriorityStore(sqlStore *SqlStore) store.PostPriorityStore {
|
||||
return &SqlPostPriorityStore{
|
||||
SqlStore: sqlStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SqlPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("Priority", "RequestedAck", "PersistentNotifications").
|
||||
From("PostsPriority").
|
||||
Where(sq.Eq{"PostId": postId})
|
||||
|
||||
var postPriority model.PostPriority
|
||||
err := s.GetReplicaX().GetBuilder(&postPriority, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &postPriority, nil
|
||||
}
|
||||
|
||||
func (s *SqlPostPriorityStore) GetForPosts(postIds []string) ([]*model.PostPriority, error) {
|
||||
var priority []*model.PostPriority
|
||||
|
||||
perPage := 200
|
||||
for i := 0; i < len(postIds); i += perPage {
|
||||
j := i + perPage
|
||||
if len(postIds) < j {
|
||||
j = len(postIds)
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("PostId", "Priority", "RequestedAck", "PersistentNotifications").
|
||||
From("PostsPriority").
|
||||
Where(sq.Eq{"PostId": postIds[i:j]})
|
||||
|
||||
var priorityBatch []*model.PostPriority
|
||||
err := s.GetReplicaX().SelectBuilder(&priority, query)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priority = append(priority, priorityBatch...)
|
||||
}
|
||||
|
||||
return priority, nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/post_priority_store_test.go
Обычный файл
14
server/channels/store/sqlstore/post_priority_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestPostPriorityStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestPostPriorityStore)
|
||||
}
|
||||
3379
server/channels/store/sqlstore/post_store.go
Обычный файл
3379
server/channels/store/sqlstore/post_store.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user