Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

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 Обычный файл
Просмотреть файл

@@ -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, ","))
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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()
})
}

Просмотреть файл

@@ -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()
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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()
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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]
}

Просмотреть файл

@@ -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)
})
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
})
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

14776
server/channels/store/retrylayer/retrylayer.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)
})
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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)
}
}

Просмотреть файл

@@ -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()
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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"}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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, "@")
}

Просмотреть файл

@@ -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)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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 Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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)
}
}
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -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)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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.")
}
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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()
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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()
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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 Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше