Adds the main Property System Architecture components (#29644)

* Adds the main Property System Architecture components

This change adds the necessary migrations for the Property Groups,
Fields and Values tables to be created, the store layer and a Property
Service that can be used from the app layer.

* Update property field type to use user instead of person

* Update PropertyFields to allow for unique nondeleted fields and remove redundant indexes

* Update PropertyValues to allow for unique nondeleted fields and remove redundant indexes

* Use StringMap instead of the map[string]any on property fields

* Add i18n strings

* Revert "Use StringMap instead of the map[string]any on property fields"

This reverts commit e2735ab0f8.

* Cast JSON binary data to string and add todo note for StringMap use

* Add mocks to the retrylayer tests

* Cast JSON binary data to string in property value store

* Check for binary parameter instead of casting to string for JSON data

* Check property field type is one of the allowed ones

* Avoid reusing err variable to be explicit about the returned value

* Merge Property System Migrations into one file

* Adds NOT NULL to timestamps at the DB level

* Update stores to use tableSelectQuery instead of a slice var

* Update PropertyField model translations to be more explicit and avoid repetition

* Update PropertyValue model translations to be more explicit and avoid repetition

* Use ExecBuilder instead of ToSql&Exec

* Update property field errors to add context

* Ensure PerPage is greater than zero

* Update store errors to give more context

* Use ExecBuilder in the property stores where possible

* Add an on conflict suffix to the group register to avoid race conditions

* Remove badly used translation string

* Remove unused get in register group method

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Miguel de la Cruz 2025-01-13 12:41:44 +01:00 committed by GitHub
parent d2b334e605
commit ecdce71fc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 3793 additions and 0 deletions

View file

@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package properties
import (
"github.com/mattermost/mattermost/server/public/model"
)
func (ps *PropertyService) CreatePropertyField(field *model.PropertyField) (*model.PropertyField, error) {
return ps.fieldStore.Create(field)
}
func (ps *PropertyService) GetPropertyField(id string) (*model.PropertyField, error) {
return ps.fieldStore.Get(id)
}
func (ps *PropertyService) GetPropertyFields(ids []string) ([]*model.PropertyField, error) {
return ps.fieldStore.GetMany(ids)
}
func (ps *PropertyService) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
return ps.fieldStore.SearchPropertyFields(opts)
}
func (ps *PropertyService) UpdatePropertyField(field *model.PropertyField) (*model.PropertyField, error) {
fields, err := ps.UpdatePropertyFields([]*model.PropertyField{field})
if err != nil {
return nil, err
}
return fields[0], nil
}
func (ps *PropertyService) UpdatePropertyFields(fields []*model.PropertyField) ([]*model.PropertyField, error) {
return ps.fieldStore.Update(fields)
}
func (ps *PropertyService) DeletePropertyField(id string) error {
if err := ps.valueStore.DeleteForField(id); err != nil {
return err
}
return ps.fieldStore.Delete(id)
}

View file

@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package properties
import (
"github.com/mattermost/mattermost/server/public/model"
)
func (ps *PropertyService) RegisterPropertyGroup(name string) (*model.PropertyGroup, error) {
return ps.groupStore.Register(name)
}
func (ps *PropertyService) GetPropertyGroup(name string) (*model.PropertyGroup, error) {
return ps.groupStore.Get(name)
}

View file

@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package properties
import (
"github.com/mattermost/mattermost/server/public/model"
)
func (ps *PropertyService) CreatePropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) {
return ps.valueStore.Create(value)
}
func (ps *PropertyService) GetPropertyValue(id string) (*model.PropertyValue, error) {
return ps.valueStore.Get(id)
}
func (ps *PropertyService) GetPropertyValues(ids []string) ([]*model.PropertyValue, error) {
return ps.valueStore.GetMany(ids)
}
func (ps *PropertyService) SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
return ps.valueStore.SearchPropertyValues(opts)
}
func (ps *PropertyService) UpdatePropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) {
values, err := ps.UpdatePropertyValues([]*model.PropertyValue{value})
if err != nil {
return nil, err
}
return values[0], nil
}
func (ps *PropertyService) UpdatePropertyValues(values []*model.PropertyValue) ([]*model.PropertyValue, error) {
return ps.valueStore.Update(values)
}
func (ps *PropertyService) DeletePropertyValue(id string) error {
return ps.valueStore.Delete(id)
}

View file

@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package properties
import (
"errors"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
type PropertyService struct {
groupStore store.PropertyGroupStore
fieldStore store.PropertyFieldStore
valueStore store.PropertyValueStore
}
type ServiceConfig struct {
PropertyGroupStore store.PropertyGroupStore
PropertyFieldStore store.PropertyFieldStore
PropertyValueStore store.PropertyValueStore
}
func New(c ServiceConfig) (*PropertyService, error) {
if err := c.validate(); err != nil {
return nil, err
}
return &PropertyService{
groupStore: c.PropertyGroupStore,
fieldStore: c.PropertyFieldStore,
valueStore: c.PropertyValueStore,
}, nil
}
func (c *ServiceConfig) validate() error {
if c.PropertyGroupStore == nil || c.PropertyFieldStore == nil || c.PropertyValueStore == nil {
return errors.New("required parameters are not provided")
}
return nil
}

View file

@ -35,6 +35,7 @@ import (
"github.com/mattermost/mattermost/server/public/shared/timezones"
"github.com/mattermost/mattermost/server/v8/channels/app/email"
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
"github.com/mattermost/mattermost/server/v8/channels/app/properties"
"github.com/mattermost/mattermost/server/v8/channels/app/teams"
"github.com/mattermost/mattermost/server/v8/channels/app/users"
"github.com/mattermost/mattermost/server/v8/channels/audit"
@ -136,6 +137,7 @@ type Server struct {
telemetryService *telemetry.TelemetryService
userService *users.UserService
teamService *teams.TeamService
propertyService *properties.PropertyService
serviceMux sync.RWMutex
remoteClusterService remotecluster.RemoteClusterServiceIFace
@ -239,6 +241,15 @@ func NewServer(options ...Option) (*Server, error) {
return nil, errors.Wrapf(err, "unable to create teams service")
}
s.propertyService, err = properties.New(properties.ServiceConfig{
PropertyGroupStore: s.Store().PropertyGroup(),
PropertyFieldStore: s.Store().PropertyField(),
PropertyValueStore: s.Store().PropertyValue(),
})
if err != nil {
return nil, errors.Wrapf(err, "unable to create properties service")
}
// It is important to initialize the hub only after the global logger is set
// to avoid race conditions while logging from inside the hub.
// Step 4: Start platform

View file

@ -253,6 +253,8 @@ channels/db/migrations/mysql/000127_add_mfa_used_ts_to_users.down.sql
channels/db/migrations/mysql/000127_add_mfa_used_ts_to_users.up.sql
channels/db/migrations/mysql/000128_create_scheduled_posts.down.sql
channels/db/migrations/mysql/000128_create_scheduled_posts.up.sql
channels/db/migrations/mysql/000129_add_property_system_architecture.down.sql
channels/db/migrations/mysql/000129_add_property_system_architecture.up.sql
channels/db/migrations/postgres/000001_create_teams.down.sql
channels/db/migrations/postgres/000001_create_teams.up.sql
channels/db/migrations/postgres/000002_create_team_members.down.sql
@ -507,3 +509,5 @@ channels/db/migrations/postgres/000127_add_mfa_used_ts_to_users.down.sql
channels/db/migrations/postgres/000127_add_mfa_used_ts_to_users.up.sql
channels/db/migrations/postgres/000128_create_scheduled_posts.down.sql
channels/db/migrations/postgres/000128_create_scheduled_posts.up.sql
channels/db/migrations/postgres/000129_add_property_system_architecture.down.sql
channels/db/migrations/postgres/000129_add_property_system_architecture.up.sql

View file

@ -0,0 +1,3 @@
DROP TABLE IF EXISTS PropertyGroups;
DROP TABLE IF EXISTS PropertyFields;
DROP TABLE IF EXISTS PropertyValues;

View file

@ -0,0 +1,47 @@
CREATE TABLE IF NOT EXISTS PropertyGroups (
ID varchar(26) PRIMARY KEY,
Name varchar(64) NOT NULL,
UNIQUE(Name)
);
CREATE TABLE IF NOT EXISTS PropertyFields (
ID varchar(26) PRIMARY KEY,
GroupID varchar(26) NOT NULL,
Name varchar(255) NOT NULL,
Type enum('text', 'select', 'multiselect', 'date', 'user', 'multiuser'),
Attrs json,
TargetID varchar(255),
TargetType varchar(255),
CreateAt bigint(20),
UpdateAt bigint(20),
DeleteAt bigint(20),
UNIQUE(GroupID, TargetID, Name, DeleteAt)
);
CREATE TABLE IF NOT EXISTS PropertyValues (
ID varchar(26) PRIMARY KEY,
TargetID varchar(255) NOT NULL,
TargetType varchar(255) NOT NULL,
GroupID varchar(26) NOT NULL,
FieldID varchar(26) NOT NULL,
Value json,
CreateAt bigint(20),
UpdateAt bigint(20),
DeleteAt bigint(20),
UNIQUE(GroupID, TargetID, FieldID, DeleteAt)
);
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name = 'PropertyValues'
AND table_schema = DATABASE()
AND index_name = 'idx_propertyvalues_targetid_groupid'
) > 0,
'SELECT 1',
'CREATE INDEX idx_propertyvalues_targetid_groupid ON PropertyValues (TargetID, GroupID);'
));
PREPARE createIndexIfNotExists FROM @preparedStatement;
EXECUTE createIndexIfNotExists;
DEALLOCATE PREPARE createIndexIfNotExists;

View file

@ -0,0 +1,3 @@
DROP TABLE IF EXISTS PropertyGroups;
DROP TABLE IF EXISTS PropertyFields;
DROP TABLE IF EXISTS PropertyValues;

View file

@ -0,0 +1,55 @@
CREATE TABLE IF NOT EXISTS PropertyGroups (
ID varchar(26) PRIMARY KEY,
Name varchar(64) NOT NULL,
UNIQUE(Name)
);
DO
$$
BEGIN
IF NOT EXISTS (SELECT * FROM pg_type typ
INNER JOIN pg_namespace nsp ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = current_schema()
AND typ.typname = 'property_field_type') THEN
CREATE TYPE property_field_type AS ENUM (
'text',
'select',
'multiselect',
'date',
'user',
'multiuser'
);
END IF;
END;
$$
LANGUAGE plpgsql;
CREATE TABLE IF NOT EXISTS PropertyFields (
ID varchar(26) PRIMARY KEY,
GroupID varchar(26) NOT NULL,
Name varchar(255) NOT NULL,
Type property_field_type,
Attrs jsonb,
TargetID varchar(255),
TargetType varchar(255),
CreateAt bigint NOT NULL,
UpdateAt bigint NOT NULL,
DeleteAt bigint NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_propertyfields_unique ON PropertyFields (GroupID, TargetID, Name) WHERE DeleteAt = 0;
CREATE TABLE IF NOT EXISTS PropertyValues (
ID varchar(26) PRIMARY KEY,
TargetID varchar(255) NOT NULL,
TargetType varchar(255) NOT NULL,
GroupID varchar(26) NOT NULL,
FieldID varchar(26) NOT NULL,
Value jsonb NOT NULL,
CreateAt bigint NOT NULL,
UpdateAt bigint NOT NULL,
DeleteAt bigint NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_propertyvalues_unique ON PropertyValues (GroupID, TargetID, FieldID) WHERE DeleteAt = 0;
CREATE INDEX IF NOT EXISTS idx_propertyvalues_targetid_groupid ON PropertyValues (TargetID, GroupID);

View file

@ -46,6 +46,9 @@ type OpenTracingLayer struct {
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
PropertyFieldStore store.PropertyFieldStore
PropertyGroupStore store.PropertyGroupStore
PropertyValueStore store.PropertyValueStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
@ -175,6 +178,18 @@ func (s *OpenTracingLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *OpenTracingLayer) PropertyField() store.PropertyFieldStore {
return s.PropertyFieldStore
}
func (s *OpenTracingLayer) PropertyGroup() store.PropertyGroupStore {
return s.PropertyGroupStore
}
func (s *OpenTracingLayer) PropertyValue() store.PropertyValueStore {
return s.PropertyValueStore
}
func (s *OpenTracingLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
@ -386,6 +401,21 @@ type OpenTracingLayerProductNoticesStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerPropertyFieldStore struct {
store.PropertyFieldStore
Root *OpenTracingLayer
}
type OpenTracingLayerPropertyGroupStore struct {
store.PropertyGroupStore
Root *OpenTracingLayer
}
type OpenTracingLayerPropertyValueStore struct {
store.PropertyValueStore
Root *OpenTracingLayer
}
type OpenTracingLayerReactionStore struct {
store.ReactionStore
Root *OpenTracingLayer
@ -7730,6 +7760,276 @@ func (s *OpenTracingLayerProductNoticesStore) View(userID string, notices []stri
return err
}
func (s *OpenTracingLayerPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyFieldStore.Create")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyFieldStore.Create(field)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyFieldStore) Delete(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyFieldStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PropertyFieldStore.Delete(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPropertyFieldStore) Get(id string) (*model.PropertyField, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyFieldStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyFieldStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyFieldStore) GetMany(ids []string) ([]*model.PropertyField, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyFieldStore.GetMany")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyFieldStore.GetMany(ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyFieldStore.SearchPropertyFields")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyFieldStore.SearchPropertyFields(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyFieldStore) Update(field []*model.PropertyField) ([]*model.PropertyField, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyFieldStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyFieldStore.Update(field)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyGroupStore) Get(name string) (*model.PropertyGroup, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyGroupStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyGroupStore.Get(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyGroupStore) Register(name string) (*model.PropertyGroup, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyGroupStore.Register")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyGroupStore.Register(name)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyValueStore) Create(value *model.PropertyValue) (*model.PropertyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyValueStore.Create")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyValueStore.Create(value)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyValueStore) Delete(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyValueStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PropertyValueStore.Delete(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPropertyValueStore) DeleteForField(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyValueStore.DeleteForField")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PropertyValueStore.DeleteForField(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPropertyValueStore) Get(id string) (*model.PropertyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyValueStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyValueStore.Get(id)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyValueStore) GetMany(ids []string) ([]*model.PropertyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyValueStore.GetMany")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyValueStore.GetMany(ids)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyValueStore) SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyValueStore.SearchPropertyValues")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyValueStore.SearchPropertyValues(opts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPropertyValueStore) Update(field []*model.PropertyValue) ([]*model.PropertyValue, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PropertyValueStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PropertyValueStore.Update(field)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.BulkGetForPosts")
@ -13853,6 +14153,9 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore.PostPriorityStore = &OpenTracingLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &OpenTracingLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &OpenTracingLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.PropertyFieldStore = &OpenTracingLayerPropertyFieldStore{PropertyFieldStore: childStore.PropertyField(), Root: &newStore}
newStore.PropertyGroupStore = &OpenTracingLayerPropertyGroupStore{PropertyGroupStore: childStore.PropertyGroup(), Root: &newStore}
newStore.PropertyValueStore = &OpenTracingLayerPropertyValueStore{PropertyValueStore: childStore.PropertyValue(), Root: &newStore}
newStore.ReactionStore = &OpenTracingLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &OpenTracingLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &OpenTracingLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}

View file

@ -50,6 +50,9 @@ type RetryLayer struct {
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
PropertyFieldStore store.PropertyFieldStore
PropertyGroupStore store.PropertyGroupStore
PropertyValueStore store.PropertyValueStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
@ -179,6 +182,18 @@ func (s *RetryLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *RetryLayer) PropertyField() store.PropertyFieldStore {
return s.PropertyFieldStore
}
func (s *RetryLayer) PropertyGroup() store.PropertyGroupStore {
return s.PropertyGroupStore
}
func (s *RetryLayer) PropertyValue() store.PropertyValueStore {
return s.PropertyValueStore
}
func (s *RetryLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
@ -390,6 +405,21 @@ type RetryLayerProductNoticesStore struct {
Root *RetryLayer
}
type RetryLayerPropertyFieldStore struct {
store.PropertyFieldStore
Root *RetryLayer
}
type RetryLayerPropertyGroupStore struct {
store.PropertyGroupStore
Root *RetryLayer
}
type RetryLayerPropertyValueStore struct {
store.PropertyValueStore
Root *RetryLayer
}
type RetryLayerReactionStore struct {
store.ReactionStore
Root *RetryLayer
@ -8793,6 +8823,321 @@ func (s *RetryLayerProductNoticesStore) View(userID string, notices []string) er
}
func (s *RetryLayerPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
tries := 0
for {
result, err := s.PropertyFieldStore.Create(field)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyFieldStore) Delete(id string) error {
tries := 0
for {
err := s.PropertyFieldStore.Delete(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyFieldStore) Get(id string) (*model.PropertyField, error) {
tries := 0
for {
result, err := s.PropertyFieldStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyFieldStore) GetMany(ids []string) ([]*model.PropertyField, error) {
tries := 0
for {
result, err := s.PropertyFieldStore.GetMany(ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
tries := 0
for {
result, err := s.PropertyFieldStore.SearchPropertyFields(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyFieldStore) Update(field []*model.PropertyField) ([]*model.PropertyField, error) {
tries := 0
for {
result, err := s.PropertyFieldStore.Update(field)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyGroupStore) Get(name string) (*model.PropertyGroup, error) {
tries := 0
for {
result, err := s.PropertyGroupStore.Get(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyGroupStore) Register(name string) (*model.PropertyGroup, error) {
tries := 0
for {
result, err := s.PropertyGroupStore.Register(name)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyValueStore) Create(value *model.PropertyValue) (*model.PropertyValue, error) {
tries := 0
for {
result, err := s.PropertyValueStore.Create(value)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyValueStore) Delete(id string) error {
tries := 0
for {
err := s.PropertyValueStore.Delete(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyValueStore) DeleteForField(id string) error {
tries := 0
for {
err := s.PropertyValueStore.DeleteForField(id)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyValueStore) Get(id string) (*model.PropertyValue, error) {
tries := 0
for {
result, err := s.PropertyValueStore.Get(id)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyValueStore) GetMany(ids []string) ([]*model.PropertyValue, error) {
tries := 0
for {
result, err := s.PropertyValueStore.GetMany(ids)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyValueStore) SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
tries := 0
for {
result, err := s.PropertyValueStore.SearchPropertyValues(opts)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPropertyValueStore) Update(field []*model.PropertyValue) ([]*model.PropertyValue, error) {
tries := 0
for {
result, err := s.PropertyValueStore.Update(field)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
tries := 0
@ -15806,6 +16151,9 @@ func New(childStore store.Store) *RetryLayer {
newStore.PostPriorityStore = &RetryLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &RetryLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &RetryLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.PropertyFieldStore = &RetryLayerPropertyFieldStore{PropertyFieldStore: childStore.PropertyField(), Root: &newStore}
newStore.PropertyGroupStore = &RetryLayerPropertyGroupStore{PropertyGroupStore: childStore.PropertyGroup(), Root: &newStore}
newStore.PropertyValueStore = &RetryLayerPropertyValueStore{PropertyValueStore: childStore.PropertyValue(), Root: &newStore}
newStore.ReactionStore = &RetryLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &RetryLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &RetryLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}

View file

@ -63,6 +63,9 @@ func genStore() *mocks.Store {
mock.On("DesktopTokens").Return(&mocks.DesktopTokensStore{})
mock.On("ChannelBookmark").Return(&mocks.ChannelBookmarkStore{})
mock.On("ScheduledPost").Return(&mocks.ScheduledPostStore{})
mock.On("PropertyField").Return(&mocks.PropertyFieldStore{})
mock.On("PropertyGroup").Return(&mocks.PropertyGroupStore{})
mock.On("PropertyValue").Return(&mocks.PropertyValueStore{})
return mock
}

View file

@ -0,0 +1,321 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func (s *SqlPropertyFieldStore) propertyFieldToInsertMap(field *model.PropertyField) (map[string]any, error) {
attrsJSON, err := json.Marshal(field.Attrs)
if err != nil {
return nil, errors.Wrap(err, "property_field_to_insert_map_marshal_attrs")
}
if s.IsBinaryParamEnabled() {
attrsJSON = AppendBinaryFlag(attrsJSON)
}
return map[string]any{
"ID": field.ID,
"GroupID": field.GroupID,
"Name": field.Name,
"Type": field.Type,
"Attrs": attrsJSON,
"TargetID": field.TargetID,
"TargetType": field.TargetType,
"CreateAt": field.CreateAt,
"UpdateAt": field.UpdateAt,
"DeleteAt": field.DeleteAt,
}, nil
}
func (s *SqlPropertyFieldStore) propertyFieldToUpdateMap(field *model.PropertyField) (map[string]any, error) {
attrsJSON, err := json.Marshal(field.Attrs)
if err != nil {
return nil, errors.Wrap(err, "property_field_to_update_map_marshal_attrs")
}
if s.IsBinaryParamEnabled() {
attrsJSON = AppendBinaryFlag(attrsJSON)
}
return map[string]any{
"Name": field.Name,
"Type": field.Type,
"Attrs": attrsJSON,
"TargetID": field.TargetID,
"TargetType": field.TargetType,
"UpdateAt": field.UpdateAt,
"DeleteAt": field.DeleteAt,
}, nil
}
func propertyFieldsFromRows(rows *sql.Rows) ([]*model.PropertyField, error) {
results := []*model.PropertyField{}
for rows.Next() {
var field model.PropertyField
var attrsJSON string
err := rows.Scan(
&field.ID,
&field.GroupID,
&field.Name,
&field.Type,
&attrsJSON,
&field.TargetID,
&field.TargetType,
&field.CreateAt,
&field.UpdateAt,
&field.DeleteAt,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(attrsJSON), &field.Attrs); err != nil {
return nil, errors.Wrap(err, "property_fields_from_rows_unmarshal_attrs")
}
results = append(results, &field)
}
return results, nil
}
func propertyFieldFromRows(rows *sql.Rows) (*model.PropertyField, error) {
fields, err := propertyFieldsFromRows(rows)
if err != nil {
return nil, err
}
if len(fields) > 0 {
return fields[0], nil
}
return nil, sql.ErrNoRows
}
type SqlPropertyFieldStore struct {
*SqlStore
tableSelectQuery sq.SelectBuilder
}
func newPropertyFieldStore(sqlStore *SqlStore) store.PropertyFieldStore {
s := SqlPropertyFieldStore{SqlStore: sqlStore}
s.tableSelectQuery = s.getQueryBuilder().
Select("ID", "GroupID", "Name", "Type", "Attrs", "TargetID", "TargetType", "CreateAt", "UpdateAt", "DeleteAt").
From("PropertyFields")
return &s
}
func (s *SqlPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
if field.ID != "" {
return nil, store.NewErrInvalidInput("PropertyField", "id", field.ID)
}
field.PreSave()
if err := field.IsValid(); err != nil {
return nil, errors.Wrap(err, "property_field_create_isvalid")
}
insertMap, err := s.propertyFieldToInsertMap(field)
if err != nil {
return nil, err
}
builder := s.getQueryBuilder().
Insert("PropertyFields").
SetMap(insertMap)
if _, err := s.GetMaster().ExecBuilder(builder); err != nil {
return nil, errors.Wrap(err, "property_field_create_insert")
}
return field, nil
}
func (s *SqlPropertyFieldStore) Get(id string) (*model.PropertyField, error) {
queryString, args, err := s.tableSelectQuery.
Where(sq.Eq{"id": id}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_field_get_tosql")
}
rows, err := s.GetReplica().Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "property_field_get_select")
}
defer rows.Close()
field, err := propertyFieldFromRows(rows)
if err != nil {
return nil, errors.Wrap(err, "property_field_get_propertyfieldfromrows")
}
return field, nil
}
func (s *SqlPropertyFieldStore) GetMany(ids []string) ([]*model.PropertyField, error) {
queryString, args, err := s.tableSelectQuery.
Where(sq.Eq{"id": ids}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_field_get_many_tosql")
}
rows, err := s.GetReplica().Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "property_field_get_many_query")
}
defer rows.Close()
fields, err := propertyFieldsFromRows(rows)
if err != nil {
return nil, errors.Wrap(err, "property_field_get_many_propertyfieldfromrows")
}
if len(fields) < len(ids) {
return nil, fmt.Errorf("missmatch results: got %d results of the %d ids passed", len(fields), len(ids))
}
return fields, nil
}
func (s *SqlPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
if opts.Page < 0 {
return nil, errors.New("page must be positive integer")
}
if opts.PerPage < 1 {
return nil, errors.New("per page must be positive integer greater than zero")
}
query := s.tableSelectQuery.
OrderBy("CreateAt ASC").
Offset(uint64(opts.Page * opts.PerPage)).
Limit(uint64(opts.PerPage))
if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
if opts.GroupID != "" {
query = query.Where(sq.Eq{"GroupID": opts.GroupID})
}
if opts.TargetType != "" {
query = query.Where(sq.Eq{"TargetType": opts.TargetType})
}
if opts.TargetID != "" {
query = query.Where(sq.Eq{"TargetID": opts.TargetID})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_field_search_tosql")
}
rows, err := s.GetReplica().Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "property_field_search_query")
}
defer rows.Close()
fields, err := propertyFieldsFromRows(rows)
if err != nil {
return nil, errors.Wrap(err, "property_field_search_propertyfieldfromrows")
}
return fields, nil
}
func (s *SqlPropertyFieldStore) Update(fields []*model.PropertyField) (_ []*model.PropertyField, err error) {
if len(fields) == 0 {
return nil, nil
}
transaction, err := s.GetMaster().Beginx()
if err != nil {
return nil, errors.Wrap(err, "property_field_update_begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
updateTime := model.GetMillis()
for _, field := range fields {
field.UpdateAt = updateTime
if vErr := field.IsValid(); vErr != nil {
return nil, errors.Wrap(vErr, "property_field_update_isvalid")
}
updateMap, err := s.propertyFieldToUpdateMap(field)
if err != nil {
return nil, err
}
queryString, args, err := s.getQueryBuilder().
Update("PropertyFields").
SetMap(updateMap).
Where(sq.Eq{"id": field.ID}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_field_update_tosql")
}
result, err := transaction.Exec(queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to update property field with id: %s", field.ID)
}
count, err := result.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "property_field_update_rowsaffected")
}
if count == 0 {
return nil, store.NewErrNotFound("PropertyField", field.ID)
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "property_field_update_commit")
}
return fields, nil
}
func (s *SqlPropertyFieldStore) Delete(id string) error {
builder := s.getQueryBuilder().
Update("PropertyFields").
Set("DeleteAt", model.GetMillis()).
Where(sq.Eq{"id": id})
result, err := s.GetMaster().ExecBuilder(builder)
if err != nil {
return errors.Wrapf(err, "failed to delete property field with id: %s", id)
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "property_field_delete_rowsaffected")
}
if count == 0 {
return store.NewErrNotFound("PropertyField", id)
}
return nil
}

View file

@ -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/v8/channels/store/storetest"
)
func TestPropertyFieldStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestPropertyFieldStore)
}

View file

@ -0,0 +1,78 @@
// 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/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
var propertyGroupColumns = []string{"ID", "Name"}
type SqlPropertyGroupStore struct {
*SqlStore
}
func newPropertyGroupStore(sqlStore *SqlStore) store.PropertyGroupStore {
return &SqlPropertyGroupStore{sqlStore}
}
func (s *SqlPropertyGroupStore) Register(name string) (*model.PropertyGroup, error) {
if name == "" {
return nil, store.NewErrInvalidInput("PropertyGroup", "name", name)
}
group := &model.PropertyGroup{Name: name}
group.PreSave()
builder := s.getQueryBuilder().
Insert("PropertyGroups").
Columns("ID", "Name").
Values(group.ID, group.Name)
if s.DriverName() == model.DatabaseDriverMysql {
builder = builder.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Name=Name"))
} else {
builder = builder.SuffixExpr(sq.Expr("ON CONFLICT (Name) DO NOTHING"))
}
r, err := s.GetMaster().ExecBuilder(builder)
if err != nil {
return nil, errors.Wrap(err, "property_group_register_insert")
}
rowsAffected, err := r.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "property_group_register_rows_affected")
}
// there was a conflict during the insert, so we need to fetch the
// group to get its data
if rowsAffected == 0 {
return s.Get(name)
}
return group, nil
}
func (s *SqlPropertyGroupStore) Get(name string) (*model.PropertyGroup, error) {
queryString, args, err := s.getQueryBuilder().
Select(propertyGroupColumns...).
From("PropertyGroups").
Where(sq.Eq{"Name": name}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_group_get_tosql")
}
var propertyGroup model.PropertyGroup
if err := s.GetReplica().Get(&propertyGroup, queryString, args...); err != nil {
return nil, store.NewErrNotFound("PropertyGroup", name)
}
return &propertyGroup, nil
}

View file

@ -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/v8/channels/store/storetest"
)
func TestPropertyGroupStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestPropertyGroupStore)
}

View file

@ -0,0 +1,332 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"fmt"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func (s *SqlPropertyValueStore) propertyValueToInsertMap(value *model.PropertyValue) (map[string]any, error) {
valueJSON, err := json.Marshal(value.Value)
if err != nil {
return nil, errors.Wrap(err, "property_value_to_insert_map_marshal_value")
}
if s.IsBinaryParamEnabled() {
valueJSON = AppendBinaryFlag(valueJSON)
}
return map[string]any{
"ID": value.ID,
"TargetID": value.TargetID,
"TargetType": value.TargetType,
"GroupID": value.GroupID,
"FieldID": value.FieldID,
"Value": valueJSON,
"CreateAt": value.CreateAt,
"UpdateAt": value.UpdateAt,
"DeleteAt": value.DeleteAt,
}, nil
}
func (s *SqlPropertyValueStore) propertyValueToUpdateMap(value *model.PropertyValue) (map[string]any, error) {
valueJSON, err := json.Marshal(value.Value)
if err != nil {
return nil, errors.Wrap(err, "property_value_to_udpate_map_marshal_value")
}
if s.IsBinaryParamEnabled() {
valueJSON = AppendBinaryFlag(valueJSON)
}
return map[string]any{
"Value": valueJSON,
"UpdateAt": value.UpdateAt,
"DeleteAt": value.DeleteAt,
}, nil
}
func propertyValuesFromRows(rows *sql.Rows) ([]*model.PropertyValue, error) {
results := []*model.PropertyValue{}
for rows.Next() {
var value model.PropertyValue
var valueJSON string
err := rows.Scan(
&value.ID,
&value.TargetID,
&value.TargetType,
&value.GroupID,
&value.FieldID,
&valueJSON,
&value.CreateAt,
&value.UpdateAt,
&value.DeleteAt,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(valueJSON), &value.Value); err != nil {
return nil, errors.Wrap(err, "property_values_from_rows_unmarshal_value")
}
results = append(results, &value)
}
return results, nil
}
func propertyValueFromRows(rows *sql.Rows) (*model.PropertyValue, error) {
values, err := propertyValuesFromRows(rows)
if err != nil {
return nil, err
}
if len(values) > 0 {
return values[0], nil
}
return nil, sql.ErrNoRows
}
type SqlPropertyValueStore struct {
*SqlStore
tableSelectQuery sq.SelectBuilder
}
func newPropertyValueStore(sqlStore *SqlStore) store.PropertyValueStore {
s := SqlPropertyValueStore{SqlStore: sqlStore}
s.tableSelectQuery = s.getQueryBuilder().
Select("ID", "TargetID", "TargetType", "GroupID", "FieldID", "Value", "CreateAt", "UpdateAt", "DeleteAt").
From("PropertyValues")
return &s
}
func (s *SqlPropertyValueStore) Create(value *model.PropertyValue) (*model.PropertyValue, error) {
if value.ID != "" {
return nil, store.NewErrInvalidInput("PropertyValue", "id", value.ID)
}
value.PreSave()
if err := value.IsValid(); err != nil {
return nil, errors.Wrap(err, "property_value_create_isvalid")
}
insertMap, err := s.propertyValueToInsertMap(value)
if err != nil {
return nil, err
}
builder := s.getQueryBuilder().
Insert("PropertyValues").
SetMap(insertMap)
if _, err := s.GetMaster().ExecBuilder(builder); err != nil {
return nil, errors.Wrap(err, "property_value_create_insert")
}
return value, nil
}
func (s *SqlPropertyValueStore) Get(id string) (*model.PropertyValue, error) {
queryString, args, err := s.tableSelectQuery.
Where(sq.Eq{"id": id}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_value_get_tosql")
}
rows, err := s.GetReplica().Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "property_value_get_select")
}
defer rows.Close()
value, err := propertyValueFromRows(rows)
if err != nil {
return nil, errors.Wrap(err, "property_value_get_propertyvaluefromrows")
}
return value, nil
}
func (s *SqlPropertyValueStore) GetMany(ids []string) ([]*model.PropertyValue, error) {
queryString, args, err := s.tableSelectQuery.
Where(sq.Eq{"id": ids}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_value_get_many_tosql")
}
rows, err := s.GetReplica().Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "property_value_get_many_query")
}
defer rows.Close()
values, err := propertyValuesFromRows(rows)
if err != nil {
return nil, errors.Wrap(err, "property_value_get_many_propertyvaluesfromrows")
}
if len(values) < len(ids) {
return nil, fmt.Errorf("missmatch results: got %d results of the %d ids passed", len(values), len(ids))
}
return values, nil
}
func (s *SqlPropertyValueStore) SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
if opts.Page < 0 {
return nil, errors.New("page must be positive integer")
}
if opts.PerPage < 1 {
return nil, errors.New("per page must be positive integer greater than zero")
}
query := s.tableSelectQuery.
OrderBy("CreateAt ASC").
Offset(uint64(opts.Page * opts.PerPage)).
Limit(uint64(opts.PerPage))
if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0})
}
if opts.GroupID != "" {
query = query.Where(sq.Eq{"GroupID": opts.GroupID})
}
if opts.TargetType != "" {
query = query.Where(sq.Eq{"TargetType": opts.TargetType})
}
if opts.TargetID != "" {
query = query.Where(sq.Eq{"TargetID": opts.TargetID})
}
if opts.FieldID != "" {
query = query.Where(sq.Eq{"FieldID": opts.FieldID})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_value_search_tosql")
}
rows, err := s.GetReplica().Query(queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "property_value_search_query")
}
defer rows.Close()
values, err := propertyValuesFromRows(rows)
if err != nil {
return nil, errors.Wrap(err, "property_value_search_propertyvaluesfromrows")
}
return values, nil
}
func (s *SqlPropertyValueStore) Update(values []*model.PropertyValue) (_ []*model.PropertyValue, err error) {
if len(values) == 0 {
return nil, nil
}
transaction, err := s.GetMaster().Beginx()
if err != nil {
return nil, errors.Wrap(err, "property_value_update_begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
updateTime := model.GetMillis()
for _, value := range values {
value.UpdateAt = updateTime
if err := value.IsValid(); err != nil {
return nil, errors.Wrap(err, "property_value_update_isvalid")
}
updateMap, err := s.propertyValueToUpdateMap(value)
if err != nil {
return nil, err
}
queryString, args, err := s.getQueryBuilder().
Update("PropertyValues").
SetMap(updateMap).
Where(sq.Eq{"id": value.ID}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "property_value_update_tosql")
}
result, err := transaction.Exec(queryString, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to update property value with id: %s", value.ID)
}
count, err := result.RowsAffected()
if err != nil {
return nil, errors.Wrap(err, "property_value_update_rowsaffected")
}
if count == 0 {
return nil, store.NewErrNotFound("PropertyValue", value.ID)
}
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "property_value_update_commit")
}
return values, nil
}
func (s *SqlPropertyValueStore) Delete(id string) error {
builder := s.getQueryBuilder().
Update("PropertyValues").
Set("DeleteAt", model.GetMillis()).
Where(sq.Eq{"id": id})
result, err := s.GetMaster().ExecBuilder(builder)
if err != nil {
return errors.Wrapf(err, "failed to delete property value with id: %s", id)
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "property_value_delete_rowsaffected")
}
if count == 0 {
return store.NewErrNotFound("PropertyValue", id)
}
return nil
}
func (s *SqlPropertyValueStore) DeleteForField(fieldID string) error {
builder := s.getQueryBuilder().
Update("PropertyValues").
Set("DeleteAt", model.GetMillis()).
Where(sq.Eq{"FieldID": fieldID})
if _, err := s.GetMaster().ExecBuilder(builder); err != nil {
return errors.Wrap(err, "property_value_delete_for_field_exec")
}
return nil
}

View file

@ -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/v8/channels/store/storetest"
)
func TestPropertyValueStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestPropertyValueStore)
}

View file

@ -115,6 +115,9 @@ type SqlStoreStores struct {
desktopTokens store.DesktopTokensStore
channelBookmarks store.ChannelBookmarkStore
scheduledPost store.ScheduledPostStore
propertyGroup store.PropertyGroupStore
propertyField store.PropertyFieldStore
propertyValue store.PropertyValueStore
}
type SqlStore struct {
@ -257,6 +260,9 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface
store.stores.desktopTokens = newSqlDesktopTokensStore(store, metrics)
store.stores.channelBookmarks = newSqlChannelBookmarkStore(store)
store.stores.scheduledPost = newScheduledPostStore(store)
store.stores.propertyGroup = newPropertyGroupStore(store)
store.stores.propertyField = newPropertyFieldStore(store)
store.stores.propertyValue = newPropertyValueStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
@ -1060,6 +1066,18 @@ func (ss *SqlStore) ChannelBookmark() store.ChannelBookmarkStore {
return ss.stores.channelBookmarks
}
func (ss *SqlStore) PropertyGroup() store.PropertyGroupStore {
return ss.stores.propertyGroup
}
func (ss *SqlStore) PropertyField() store.PropertyFieldStore {
return ss.stores.propertyField
}
func (ss *SqlStore) PropertyValue() store.PropertyValueStore {
return ss.stores.propertyValue
}
func (ss *SqlStore) DropAllTables() {
if ss.DriverName() == model.DatabaseDriverPostgres {
ss.masterX.Exec(`DO

View file

@ -92,6 +92,9 @@ type Store interface {
DesktopTokens() DesktopTokensStore
ChannelBookmark() ChannelBookmarkStore
ScheduledPost() ScheduledPostStore
PropertyGroup() PropertyGroupStore
PropertyField() PropertyFieldStore
PropertyValue() PropertyValueStore
}
type RetentionPolicyStore interface {
@ -1069,6 +1072,30 @@ type ScheduledPostStore interface {
PermanentDeleteByUser(userId string) error
}
type PropertyGroupStore interface {
Register(name string) (*model.PropertyGroup, error)
Get(name string) (*model.PropertyGroup, error)
}
type PropertyFieldStore interface {
Create(field *model.PropertyField) (*model.PropertyField, error)
Get(id string) (*model.PropertyField, error)
GetMany(ids []string) ([]*model.PropertyField, error)
SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error)
Update(field []*model.PropertyField) ([]*model.PropertyField, error)
Delete(id string) error
}
type PropertyValueStore interface {
Create(value *model.PropertyValue) (*model.PropertyValue, error)
Get(id string) (*model.PropertyValue, error)
GetMany(ids []string) ([]*model.PropertyValue, error)
SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error)
Update(field []*model.PropertyValue) ([]*model.PropertyValue, error)
Delete(id string) error
DeleteForField(id string) error
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.

View file

@ -0,0 +1,197 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
mock "github.com/stretchr/testify/mock"
)
// PropertyFieldStore is an autogenerated mock type for the PropertyFieldStore type
type PropertyFieldStore struct {
mock.Mock
}
// Create provides a mock function with given fields: field
func (_m *PropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
ret := _m.Called(field)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 *model.PropertyField
var r1 error
if rf, ok := ret.Get(0).(func(*model.PropertyField) (*model.PropertyField, error)); ok {
return rf(field)
}
if rf, ok := ret.Get(0).(func(*model.PropertyField) *model.PropertyField); ok {
r0 = rf(field)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PropertyField)
}
}
if rf, ok := ret.Get(1).(func(*model.PropertyField) error); ok {
r1 = rf(field)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: id
func (_m *PropertyFieldStore) Delete(id string) error {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: id
func (_m *PropertyFieldStore) Get(id string) (*model.PropertyField, error) {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *model.PropertyField
var r1 error
if rf, ok := ret.Get(0).(func(string) (*model.PropertyField, error)); ok {
return rf(id)
}
if rf, ok := ret.Get(0).(func(string) *model.PropertyField); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PropertyField)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMany provides a mock function with given fields: ids
func (_m *PropertyFieldStore) GetMany(ids []string) ([]*model.PropertyField, error) {
ret := _m.Called(ids)
if len(ret) == 0 {
panic("no return value specified for GetMany")
}
var r0 []*model.PropertyField
var r1 error
if rf, ok := ret.Get(0).(func([]string) ([]*model.PropertyField, error)); ok {
return rf(ids)
}
if rf, ok := ret.Get(0).(func([]string) []*model.PropertyField); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PropertyField)
}
}
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchPropertyFields provides a mock function with given fields: opts
func (_m *PropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
ret := _m.Called(opts)
if len(ret) == 0 {
panic("no return value specified for SearchPropertyFields")
}
var r0 []*model.PropertyField
var r1 error
if rf, ok := ret.Get(0).(func(model.PropertyFieldSearchOpts) ([]*model.PropertyField, error)); ok {
return rf(opts)
}
if rf, ok := ret.Get(0).(func(model.PropertyFieldSearchOpts) []*model.PropertyField); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PropertyField)
}
}
if rf, ok := ret.Get(1).(func(model.PropertyFieldSearchOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: field
func (_m *PropertyFieldStore) Update(field []*model.PropertyField) ([]*model.PropertyField, error) {
ret := _m.Called(field)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 []*model.PropertyField
var r1 error
if rf, ok := ret.Get(0).(func([]*model.PropertyField) ([]*model.PropertyField, error)); ok {
return rf(field)
}
if rf, ok := ret.Get(0).(func([]*model.PropertyField) []*model.PropertyField); ok {
r0 = rf(field)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PropertyField)
}
}
if rf, ok := ret.Get(1).(func([]*model.PropertyField) error); ok {
r1 = rf(field)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewPropertyFieldStore creates a new instance of PropertyFieldStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewPropertyFieldStore(t interface {
mock.TestingT
Cleanup(func())
}) *PropertyFieldStore {
mock := &PropertyFieldStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -0,0 +1,89 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
mock "github.com/stretchr/testify/mock"
)
// PropertyGroupStore is an autogenerated mock type for the PropertyGroupStore type
type PropertyGroupStore struct {
mock.Mock
}
// Get provides a mock function with given fields: name
func (_m *PropertyGroupStore) Get(name string) (*model.PropertyGroup, error) {
ret := _m.Called(name)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *model.PropertyGroup
var r1 error
if rf, ok := ret.Get(0).(func(string) (*model.PropertyGroup, error)); ok {
return rf(name)
}
if rf, ok := ret.Get(0).(func(string) *model.PropertyGroup); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PropertyGroup)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Register provides a mock function with given fields: name
func (_m *PropertyGroupStore) Register(name string) (*model.PropertyGroup, error) {
ret := _m.Called(name)
if len(ret) == 0 {
panic("no return value specified for Register")
}
var r0 *model.PropertyGroup
var r1 error
if rf, ok := ret.Get(0).(func(string) (*model.PropertyGroup, error)); ok {
return rf(name)
}
if rf, ok := ret.Get(0).(func(string) *model.PropertyGroup); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PropertyGroup)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewPropertyGroupStore creates a new instance of PropertyGroupStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewPropertyGroupStore(t interface {
mock.TestingT
Cleanup(func())
}) *PropertyGroupStore {
mock := &PropertyGroupStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -0,0 +1,215 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
mock "github.com/stretchr/testify/mock"
)
// PropertyValueStore is an autogenerated mock type for the PropertyValueStore type
type PropertyValueStore struct {
mock.Mock
}
// Create provides a mock function with given fields: value
func (_m *PropertyValueStore) Create(value *model.PropertyValue) (*model.PropertyValue, error) {
ret := _m.Called(value)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 *model.PropertyValue
var r1 error
if rf, ok := ret.Get(0).(func(*model.PropertyValue) (*model.PropertyValue, error)); ok {
return rf(value)
}
if rf, ok := ret.Get(0).(func(*model.PropertyValue) *model.PropertyValue); ok {
r0 = rf(value)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PropertyValue)
}
}
if rf, ok := ret.Get(1).(func(*model.PropertyValue) error); ok {
r1 = rf(value)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: id
func (_m *PropertyValueStore) Delete(id string) error {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteForField provides a mock function with given fields: id
func (_m *PropertyValueStore) DeleteForField(id string) error {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for DeleteForField")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: id
func (_m *PropertyValueStore) Get(id string) (*model.PropertyValue, error) {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *model.PropertyValue
var r1 error
if rf, ok := ret.Get(0).(func(string) (*model.PropertyValue, error)); ok {
return rf(id)
}
if rf, ok := ret.Get(0).(func(string) *model.PropertyValue); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PropertyValue)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMany provides a mock function with given fields: ids
func (_m *PropertyValueStore) GetMany(ids []string) ([]*model.PropertyValue, error) {
ret := _m.Called(ids)
if len(ret) == 0 {
panic("no return value specified for GetMany")
}
var r0 []*model.PropertyValue
var r1 error
if rf, ok := ret.Get(0).(func([]string) ([]*model.PropertyValue, error)); ok {
return rf(ids)
}
if rf, ok := ret.Get(0).(func([]string) []*model.PropertyValue); ok {
r0 = rf(ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PropertyValue)
}
}
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchPropertyValues provides a mock function with given fields: opts
func (_m *PropertyValueStore) SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
ret := _m.Called(opts)
if len(ret) == 0 {
panic("no return value specified for SearchPropertyValues")
}
var r0 []*model.PropertyValue
var r1 error
if rf, ok := ret.Get(0).(func(model.PropertyValueSearchOpts) ([]*model.PropertyValue, error)); ok {
return rf(opts)
}
if rf, ok := ret.Get(0).(func(model.PropertyValueSearchOpts) []*model.PropertyValue); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PropertyValue)
}
}
if rf, ok := ret.Get(1).(func(model.PropertyValueSearchOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: field
func (_m *PropertyValueStore) Update(field []*model.PropertyValue) ([]*model.PropertyValue, error) {
ret := _m.Called(field)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 []*model.PropertyValue
var r1 error
if rf, ok := ret.Get(0).(func([]*model.PropertyValue) ([]*model.PropertyValue, error)); ok {
return rf(field)
}
if rf, ok := ret.Get(0).(func([]*model.PropertyValue) []*model.PropertyValue); ok {
r0 = rf(field)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PropertyValue)
}
}
if rf, ok := ret.Get(1).(func([]*model.PropertyValue) error); ok {
r1 = rf(field)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewPropertyValueStore creates a new instance of PropertyValueStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewPropertyValueStore(t interface {
mock.TestingT
Cleanup(func())
}) *PropertyValueStore {
mock := &PropertyValueStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -798,6 +798,66 @@ func (_m *Store) ProductNotices() store.ProductNoticesStore {
return r0
}
// PropertyField provides a mock function with given fields:
func (_m *Store) PropertyField() store.PropertyFieldStore {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for PropertyField")
}
var r0 store.PropertyFieldStore
if rf, ok := ret.Get(0).(func() store.PropertyFieldStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PropertyFieldStore)
}
}
return r0
}
// PropertyGroup provides a mock function with given fields:
func (_m *Store) PropertyGroup() store.PropertyGroupStore {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for PropertyGroup")
}
var r0 store.PropertyGroupStore
if rf, ok := ret.Get(0).(func() store.PropertyGroupStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PropertyGroupStore)
}
}
return r0
}
// PropertyValue provides a mock function with given fields:
func (_m *Store) PropertyValue() store.PropertyValueStore {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for PropertyValue")
}
var r0 store.PropertyValueStore
if rf, ok := ret.Get(0).(func() store.PropertyValueStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PropertyValueStore)
}
}
return r0
}
// Reaction provides a mock function with given fields:
func (_m *Store) Reaction() store.ReactionStore {
ret := _m.Called()

View file

@ -0,0 +1,461 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/stretchr/testify/require"
)
func TestPropertyFieldStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("CreatePropertyField", func(t *testing.T) { testCreatePropertyField(t, rctx, ss) })
t.Run("GetPropertyField", func(t *testing.T) { testGetPropertyField(t, rctx, ss) })
t.Run("GetManyPropertyFields", func(t *testing.T) { testGetManyPropertyFields(t, rctx, ss) })
t.Run("UpdatePropertyField", func(t *testing.T) { testUpdatePropertyField(t, rctx, ss) })
t.Run("DeletePropertyField", func(t *testing.T) { testDeletePropertyField(t, rctx, ss) })
t.Run("SearchPropertyFields", func(t *testing.T) { testSearchPropertyFields(t, rctx, ss) })
}
func testCreatePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail if the property field already has an ID set", func(t *testing.T) {
newField := &model.PropertyField{ID: "sampleid"}
field, err := ss.PropertyField().Create(newField)
require.Zero(t, field)
var eii *store.ErrInvalidInput
require.ErrorAs(t, err, &eii)
})
t.Run("should fail if the property field is not valid", func(t *testing.T) {
newField := &model.PropertyField{GroupID: ""}
field, err := ss.PropertyField().Create(newField)
require.Zero(t, field)
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
newField = &model.PropertyField{GroupID: model.NewId(), Name: ""}
field, err = ss.PropertyField().Create(newField)
require.Zero(t, field)
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
})
newField := &model.PropertyField{
GroupID: model.NewId(),
Name: "My new property field",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"locked": true,
"special": "value",
},
}
t.Run("should be able to create a property field", func(t *testing.T) {
field, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotZero(t, field.ID)
require.NotZero(t, field.CreateAt)
require.NotZero(t, field.UpdateAt)
require.Zero(t, field.DeleteAt)
})
t.Run("should enforce the field's uniqueness", func(t *testing.T) {
newField.ID = ""
field, err := ss.PropertyField().Create(newField)
require.Error(t, err)
require.Empty(t, field)
})
}
func testGetPropertyField(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting field", func(t *testing.T) {
field, err := ss.PropertyField().Get(model.NewId())
require.Zero(t, field)
require.ErrorIs(t, err, sql.ErrNoRows)
})
t.Run("should be able to retrieve an existing property field", func(t *testing.T) {
newField := &model.PropertyField{
GroupID: model.NewId(),
Name: "My new property field",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"locked": true,
"special": "value",
},
}
_, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotZero(t, newField.ID)
field, err := ss.PropertyField().Get(newField.ID)
require.NoError(t, err)
require.Equal(t, newField.ID, field.ID)
require.True(t, field.Attrs["locked"].(bool))
require.Equal(t, "value", field.Attrs["special"])
})
}
func testGetManyPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting fields", func(t *testing.T) {
fields, err := ss.PropertyField().GetMany([]string{model.NewId(), model.NewId()})
require.Empty(t, fields)
require.ErrorContains(t, err, "missmatch results")
})
newFields := []*model.PropertyField{}
for _, fieldName := range []string{"field1", "field2", "field3"} {
newField := &model.PropertyField{
GroupID: model.NewId(),
Name: fieldName,
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotZero(t, newField.ID)
newFields = append(newFields, newField)
}
t.Run("should fail if at least one of the ids is nonexistent", func(t *testing.T) {
fields, err := ss.PropertyField().GetMany([]string{newFields[0].ID, newFields[1].ID, model.NewId()})
require.Empty(t, fields)
require.ErrorContains(t, err, "missmatch results")
})
t.Run("should be able to retrieve existing property fields", func(t *testing.T) {
fields, err := ss.PropertyField().GetMany([]string{newFields[0].ID, newFields[1].ID, newFields[2].ID})
require.NoError(t, err)
require.Len(t, fields, 3)
require.ElementsMatch(t, newFields, fields)
})
}
func testUpdatePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting field", func(t *testing.T) {
field := &model.PropertyField{
ID: model.NewId(),
GroupID: model.NewId(),
Name: "My property field",
Type: model.PropertyFieldTypeText,
CreateAt: model.GetMillis(),
}
updatedField, err := ss.PropertyField().Update([]*model.PropertyField{field})
require.Zero(t, updatedField)
var enf *store.ErrNotFound
require.ErrorAs(t, err, &enf)
})
t.Run("should fail if the property field is not valid", func(t *testing.T) {
field := &model.PropertyField{
GroupID: model.NewId(),
Name: "My property field",
Type: model.PropertyFieldTypeText,
}
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
require.NotZero(t, field.ID)
field.GroupID = ""
updatedField, err := ss.PropertyField().Update([]*model.PropertyField{field})
require.Zero(t, updatedField)
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
field.GroupID = model.NewId()
field.Name = ""
updatedField, err = ss.PropertyField().Update([]*model.PropertyField{field})
require.Zero(t, updatedField)
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
})
t.Run("should be able to update multiple property fields", func(t *testing.T) {
field1 := &model.PropertyField{
GroupID: model.NewId(),
Name: "First field",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"locked": true,
"special": "value",
},
}
field2 := &model.PropertyField{
GroupID: model.NewId(),
Name: "Second field",
Type: model.PropertyFieldTypeSelect,
Attrs: map[string]any{
"options": []string{"a", "b"},
},
}
for _, field := range []*model.PropertyField{field1, field2} {
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
require.NotZero(t, field.ID)
}
time.Sleep(10 * time.Millisecond)
field1.Name = "Updated first"
field1.Type = model.PropertyFieldTypeSelect
field1.Attrs = map[string]any{
"locked": false,
"new_field": "new_value",
}
field2.Name = "Updated second"
field2.Attrs = map[string]any{
"options": []string{"x", "y", "z"},
}
_, err := ss.PropertyField().Update([]*model.PropertyField{field1, field2})
require.NoError(t, err)
// Verify first field
updated1, err := ss.PropertyField().Get(field1.ID)
require.NoError(t, err)
require.Equal(t, "Updated first", updated1.Name)
require.Equal(t, model.PropertyFieldTypeSelect, updated1.Type)
require.False(t, updated1.Attrs["locked"].(bool))
require.NotContains(t, updated1.Attrs, "special")
require.Equal(t, "new_value", updated1.Attrs["new_field"])
require.Greater(t, updated1.UpdateAt, updated1.CreateAt)
// Verify second field
updated2, err := ss.PropertyField().Get(field2.ID)
require.NoError(t, err)
require.Equal(t, "Updated second", updated2.Name)
require.Equal(t, model.PropertyFieldTypeSelect, updated2.Type)
require.ElementsMatch(t, []string{"x", "y", "z"}, updated2.Attrs["options"])
require.Greater(t, updated2.UpdateAt, updated2.CreateAt)
})
t.Run("should not update any fields if one update is invalid", func(t *testing.T) {
// Create two valid fields
groupID := model.NewId()
field1 := &model.PropertyField{
GroupID: groupID,
Name: "Field 1",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"key": "value",
},
}
field2 := &model.PropertyField{
GroupID: groupID,
Name: "Field 2",
Type: model.PropertyFieldTypeText,
Attrs: map[string]any{
"key": "value",
},
}
for _, field := range []*model.PropertyField{field1, field2} {
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
}
originalUpdateAt1 := field1.UpdateAt
originalUpdateAt2 := field2.UpdateAt
// Try to update both fields, but make one invalid
field1.Name = "Valid update"
field2.GroupID = "Invalid ID"
_, err := ss.PropertyField().Update([]*model.PropertyField{field1, field2})
require.ErrorContains(t, err, "model.property_field.is_valid.app_error")
// Check that fields were not updated
updated1, err := ss.PropertyField().Get(field1.ID)
require.NoError(t, err)
require.Equal(t, "Field 1", updated1.Name)
require.Equal(t, originalUpdateAt1, updated1.UpdateAt)
updated2, err := ss.PropertyField().Get(field2.ID)
require.NoError(t, err)
require.Equal(t, groupID, updated2.GroupID)
require.Equal(t, originalUpdateAt2, updated2.UpdateAt)
})
}
func testDeletePropertyField(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting field", func(t *testing.T) {
err := ss.PropertyField().Delete(model.NewId())
var enf *store.ErrNotFound
require.ErrorAs(t, err, &enf)
})
newField := &model.PropertyField{
GroupID: model.NewId(),
Name: "My property field",
Type: model.PropertyFieldTypeText,
}
t.Run("should be able to delete an existing property field", func(t *testing.T) {
field, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotEmpty(t, field.ID)
err = ss.PropertyField().Delete(field.ID)
require.NoError(t, err)
// Verify the field was soft-deleted
deletedField, err := ss.PropertyField().Get(field.ID)
require.NoError(t, err)
require.NotZero(t, deletedField.DeleteAt)
})
t.Run("should be able to create a new field with the same details as the deleted one", func(t *testing.T) {
newField.ID = ""
field, err := ss.PropertyField().Create(newField)
require.NoError(t, err)
require.NotEmpty(t, field.ID)
})
}
func testSearchPropertyFields(t *testing.T, _ request.CTX, ss store.Store) {
groupID := model.NewId()
targetID := model.NewId()
// Define test property fields
field1 := &model.PropertyField{
GroupID: groupID,
Name: "Field 1",
Type: model.PropertyFieldTypeText,
TargetID: targetID,
TargetType: "test_type",
}
field2 := &model.PropertyField{
GroupID: groupID,
Name: "Field 2",
Type: model.PropertyFieldTypeSelect,
TargetID: targetID,
TargetType: "other_type",
}
field3 := &model.PropertyField{
GroupID: model.NewId(),
Name: "Field 3",
Type: model.PropertyFieldTypeText,
TargetType: "test_type",
}
field4 := &model.PropertyField{
GroupID: groupID,
Name: "Field 4",
Type: model.PropertyFieldTypeText,
TargetType: "test_type",
}
for _, field := range []*model.PropertyField{field1, field2, field3, field4} {
_, err := ss.PropertyField().Create(field)
require.NoError(t, err)
time.Sleep(10 * time.Millisecond)
}
// Delete one field for deletion tests
require.NoError(t, ss.PropertyField().Delete(field4.ID))
tests := []struct {
name string
opts model.PropertyFieldSearchOpts
expectedError bool
expectedIDs []string
}{
{
name: "negative page",
opts: model.PropertyFieldSearchOpts{
Page: -1,
PerPage: 10,
},
expectedError: true,
},
{
name: "negative per_page",
opts: model.PropertyFieldSearchOpts{
Page: 0,
PerPage: -1,
},
expectedError: true,
},
{
name: "filter by group_id",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
Page: 0,
PerPage: 10,
},
expectedIDs: []string{field1.ID, field2.ID},
},
{
name: "filter by group_id including deleted",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
Page: 0,
PerPage: 10,
IncludeDeleted: true,
},
expectedIDs: []string{field1.ID, field2.ID, field4.ID},
},
{
name: "filter by target_type",
opts: model.PropertyFieldSearchOpts{
TargetType: "test_type",
Page: 0,
PerPage: 10,
},
expectedIDs: []string{field1.ID, field3.ID},
},
{
name: "filter by target_id",
opts: model.PropertyFieldSearchOpts{
TargetID: targetID,
Page: 0,
PerPage: 10,
},
expectedIDs: []string{field1.ID, field2.ID},
},
{
name: "pagination page 0",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
Page: 0,
PerPage: 2,
IncludeDeleted: true,
},
expectedIDs: []string{field1.ID, field2.ID},
},
{
name: "pagination page 1",
opts: model.PropertyFieldSearchOpts{
GroupID: groupID,
Page: 1,
PerPage: 2,
IncludeDeleted: true,
},
expectedIDs: []string{field4.ID},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
results, err := ss.PropertyField().SearchPropertyFields(tc.opts)
if tc.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
var ids = make([]string, len(results))
for i, field := range results {
ids[i] = field.ID
}
require.ElementsMatch(t, tc.expectedIDs, ids)
})
}
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/stretchr/testify/require"
)
func TestPropertyGroupStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("RegisterAndGetPropertyGroup", func(t *testing.T) { testRegisterAndGetPropertyGroup(t, rctx, ss) })
}
func testRegisterAndGetPropertyGroup(t *testing.T, _ request.CTX, ss store.Store) {
groupName := "samplename"
var id string
t.Run("should be able to register a new group", func(t *testing.T) {
group, err := ss.PropertyGroup().Register(groupName)
require.NoError(t, err)
require.NotZero(t, group.ID)
require.Equal(t, groupName, group.Name)
id = group.ID
})
t.Run("should be able to retrieve an existing group", func(t *testing.T) {
group, err := ss.PropertyGroup().Register(groupName)
require.NoError(t, err)
require.Equal(t, groupName, group.Name)
require.Equal(t, id, group.ID)
})
}

View file

@ -0,0 +1,535 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"database/sql"
"fmt"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/stretchr/testify/require"
)
func TestPropertyValueStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("CreatePropertyValue", func(t *testing.T) { testCreatePropertyValue(t, rctx, ss) })
t.Run("GetPropertyValue", func(t *testing.T) { testGetPropertyValue(t, rctx, ss) })
t.Run("GetManyPropertyValues", func(t *testing.T) { testGetManyPropertyValues(t, rctx, ss) })
t.Run("UpdatePropertyValue", func(t *testing.T) { testUpdatePropertyValue(t, rctx, ss) })
t.Run("DeletePropertyValue", func(t *testing.T) { testDeletePropertyValue(t, rctx, ss) })
t.Run("SearchPropertyValues", func(t *testing.T) { testSearchPropertyValues(t, rctx, ss) })
t.Run("DeleteForField", func(t *testing.T) { testDeleteForField(t, rctx, ss) })
}
func testCreatePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail if the property value already has an ID set", func(t *testing.T) {
newValue := &model.PropertyValue{ID: "sampleid"}
value, err := ss.PropertyValue().Create(newValue)
require.Zero(t, value)
var eii *store.ErrInvalidInput
require.ErrorAs(t, err, &eii)
})
t.Run("should fail if the property value is not valid", func(t *testing.T) {
newValue := &model.PropertyValue{TargetID: ""}
value, err := ss.PropertyValue().Create(newValue)
require.Zero(t, value)
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
newValue = &model.PropertyValue{TargetID: model.NewId(), TargetType: ""}
value, err = ss.PropertyValue().Create(newValue)
require.Zero(t, value)
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
})
newValue := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: "test value",
}
t.Run("should be able to create a property value", func(t *testing.T) {
value, err := ss.PropertyValue().Create(newValue)
require.NoError(t, err)
require.NotZero(t, value.ID)
require.NotZero(t, value.CreateAt)
require.NotZero(t, value.UpdateAt)
require.Zero(t, value.DeleteAt)
})
t.Run("should enforce the value's uniqueness", func(t *testing.T) {
newValue.ID = ""
value, err := ss.PropertyValue().Create(newValue)
require.Error(t, err)
require.Zero(t, value)
})
}
func testGetPropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting value", func(t *testing.T) {
value, err := ss.PropertyValue().Get(model.NewId())
require.Zero(t, value)
require.ErrorIs(t, err, sql.ErrNoRows)
})
t.Run("should be able to retrieve an existing property value", func(t *testing.T) {
newValue := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: "test value",
}
_, err := ss.PropertyValue().Create(newValue)
require.NoError(t, err)
require.NotZero(t, newValue.ID)
value, err := ss.PropertyValue().Get(newValue.ID)
require.NoError(t, err)
require.Equal(t, newValue.ID, value.ID)
require.Equal(t, newValue.Value, value.Value)
})
}
func testGetManyPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting values", func(t *testing.T) {
values, err := ss.PropertyValue().GetMany([]string{model.NewId(), model.NewId()})
require.Empty(t, values)
require.ErrorContains(t, err, "missmatch results")
})
newValues := []*model.PropertyValue{}
for i := 0; i < 3; i++ {
newValue := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: fmt.Sprintf("test value %d", i),
}
_, err := ss.PropertyValue().Create(newValue)
require.NoError(t, err)
require.NotZero(t, newValue.ID)
newValues = append(newValues, newValue)
}
t.Run("should fail if at least one of the ids is nonexistent", func(t *testing.T) {
values, err := ss.PropertyValue().GetMany([]string{newValues[0].ID, newValues[1].ID, model.NewId()})
require.Empty(t, values)
require.ErrorContains(t, err, "missmatch results")
})
t.Run("should be able to retrieve existing property values", func(t *testing.T) {
values, err := ss.PropertyValue().GetMany([]string{newValues[0].ID, newValues[1].ID, newValues[2].ID})
require.NoError(t, err)
require.Len(t, values, 3)
require.ElementsMatch(t, newValues, values)
})
}
func testUpdatePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting value", func(t *testing.T) {
value := &model.PropertyValue{
ID: model.NewId(),
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: "test value",
CreateAt: model.GetMillis(),
}
updatedValue, err := ss.PropertyValue().Update([]*model.PropertyValue{value})
require.Zero(t, updatedValue)
var enf *store.ErrNotFound
require.ErrorAs(t, err, &enf)
})
t.Run("should fail if the property value is not valid", func(t *testing.T) {
value := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: "test value",
}
_, err := ss.PropertyValue().Create(value)
require.NoError(t, err)
require.NotZero(t, value.ID)
value.TargetID = ""
updatedValue, err := ss.PropertyValue().Update([]*model.PropertyValue{value})
require.Zero(t, updatedValue)
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
value.TargetID = model.NewId()
value.GroupID = ""
updatedValue, err = ss.PropertyValue().Update([]*model.PropertyValue{value})
require.Zero(t, updatedValue)
require.ErrorContains(t, err, "model.property_value.is_valid.app_error")
})
t.Run("should be able to update multiple property values", func(t *testing.T) {
value1 := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: "value 1",
}
value2 := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: "value 2",
}
for _, value := range []*model.PropertyValue{value1, value2} {
_, err := ss.PropertyValue().Create(value)
require.NoError(t, err)
require.NotZero(t, value.ID)
}
time.Sleep(10 * time.Millisecond)
value1.Value = "updated value 1"
value2.Value = "updated value 2"
_, err := ss.PropertyValue().Update([]*model.PropertyValue{value1, value2})
require.NoError(t, err)
// Verify first value
updated1, err := ss.PropertyValue().Get(value1.ID)
require.NoError(t, err)
require.Equal(t, "updated value 1", updated1.Value)
require.Greater(t, updated1.UpdateAt, updated1.CreateAt)
// Verify second value
updated2, err := ss.PropertyValue().Get(value2.ID)
require.NoError(t, err)
require.Equal(t, "updated value 2", updated2.Value)
require.Greater(t, updated2.UpdateAt, updated2.CreateAt)
})
t.Run("should not update any fields if one update is invalid", func(t *testing.T) {
// Create two valid values
groupID := model.NewId()
value1 := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: groupID,
FieldID: model.NewId(),
Value: "Value 1",
}
value2 := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: groupID,
FieldID: model.NewId(),
Value: "Value 2",
}
for _, value := range []*model.PropertyValue{value1, value2} {
_, err := ss.PropertyValue().Create(value)
require.NoError(t, err)
}
originalUpdateAt1 := value1.UpdateAt
originalUpdateAt2 := value2.UpdateAt
// Try to update both value, but make one invalid
value1.Value = "Valid update"
value2.GroupID = "Invalid ID"
_, err := ss.PropertyValue().Update([]*model.PropertyValue{value1, value2})
require.Error(t, err)
require.Contains(t, err.Error(), "model.property_value.is_valid.app_error")
// Check that values were not updated
updated1, err := ss.PropertyValue().Get(value1.ID)
require.NoError(t, err)
require.Equal(t, "Value 1", updated1.Value)
require.Equal(t, originalUpdateAt1, updated1.UpdateAt)
updated2, err := ss.PropertyValue().Get(value2.ID)
require.NoError(t, err)
require.Equal(t, groupID, updated2.GroupID)
require.Equal(t, originalUpdateAt2, updated2.UpdateAt)
})
}
func testDeletePropertyValue(t *testing.T, _ request.CTX, ss store.Store) {
t.Run("should fail on nonexisting value", func(t *testing.T) {
err := ss.PropertyValue().Delete(model.NewId())
var enf *store.ErrNotFound
require.ErrorAs(t, err, &enf)
})
t.Run("should be able to delete an existing property value", func(t *testing.T) {
newValue := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: "test value",
}
value, err := ss.PropertyValue().Create(newValue)
require.NoError(t, err)
require.NotEmpty(t, value.ID)
err = ss.PropertyValue().Delete(value.ID)
require.NoError(t, err)
// Verify the value was soft-deleted
deletedValue, err := ss.PropertyValue().Get(value.ID)
require.NoError(t, err)
require.NotZero(t, deletedValue.DeleteAt)
})
t.Run("should be able to create a new value with the same details as the deleted one", func(t *testing.T) {
sameDetailsValue := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(),
Value: "test value",
}
value, err := ss.PropertyValue().Create(sameDetailsValue)
require.NoError(t, err)
require.NotEmpty(t, value.ID)
require.Equal(t, sameDetailsValue.Value, value.Value)
})
}
func testSearchPropertyValues(t *testing.T, _ request.CTX, ss store.Store) {
groupID := model.NewId()
targetID := model.NewId()
fieldID := model.NewId()
// Define test property values
value1 := &model.PropertyValue{
GroupID: groupID,
TargetID: targetID,
TargetType: "test_type",
FieldID: fieldID,
Value: "value 1",
}
value2 := &model.PropertyValue{
GroupID: groupID,
TargetID: targetID,
TargetType: "other_type",
FieldID: model.NewId(),
Value: "value 2",
}
value3 := &model.PropertyValue{
GroupID: model.NewId(),
TargetID: model.NewId(),
TargetType: "test_type",
FieldID: model.NewId(),
Value: "value 3",
}
value4 := &model.PropertyValue{
GroupID: groupID,
TargetID: model.NewId(),
TargetType: "test_type",
FieldID: fieldID,
Value: "value 4",
}
for _, value := range []*model.PropertyValue{value1, value2, value3, value4} {
_, err := ss.PropertyValue().Create(value)
require.NoError(t, err)
time.Sleep(10 * time.Millisecond)
}
// Delete one value for deletion tests
require.NoError(t, ss.PropertyValue().Delete(value4.ID))
tests := []struct {
name string
opts model.PropertyValueSearchOpts
expectedError bool
expectedIDs []string
}{
{
name: "negative page",
opts: model.PropertyValueSearchOpts{
Page: -1,
PerPage: 10,
},
expectedError: true,
},
{
name: "negative per_page",
opts: model.PropertyValueSearchOpts{
Page: 0,
PerPage: -1,
},
expectedError: true,
},
{
name: "filter by group_id",
opts: model.PropertyValueSearchOpts{
GroupID: groupID,
Page: 0,
PerPage: 10,
},
expectedIDs: []string{value1.ID, value2.ID},
},
{
name: "filter by group_id and target_type",
opts: model.PropertyValueSearchOpts{
GroupID: groupID,
TargetType: "test_type",
Page: 0,
PerPage: 10,
},
expectedIDs: []string{value1.ID},
},
{
name: "filter by group_id and target_type including deleted",
opts: model.PropertyValueSearchOpts{
GroupID: groupID,
TargetType: "test_type",
IncludeDeleted: true,
Page: 0,
PerPage: 10,
},
expectedIDs: []string{value1.ID, value4.ID},
},
{
name: "filter by target_id",
opts: model.PropertyValueSearchOpts{
TargetID: targetID,
Page: 0,
PerPage: 10,
},
expectedIDs: []string{value1.ID, value2.ID},
},
{
name: "filter by group_id and target_id",
opts: model.PropertyValueSearchOpts{
GroupID: groupID,
TargetID: targetID,
Page: 0,
PerPage: 10,
},
expectedIDs: []string{value1.ID, value2.ID},
},
{
name: "filter by field_id",
opts: model.PropertyValueSearchOpts{
FieldID: fieldID,
Page: 0,
PerPage: 10,
},
expectedIDs: []string{value1.ID},
},
{
name: "filter by field_id including deleted",
opts: model.PropertyValueSearchOpts{
FieldID: fieldID,
IncludeDeleted: true,
Page: 0,
PerPage: 10,
},
expectedIDs: []string{value1.ID, value4.ID},
},
{
name: "pagination page 0",
opts: model.PropertyValueSearchOpts{
GroupID: groupID,
Page: 0,
PerPage: 1,
},
expectedIDs: []string{value1.ID},
},
{
name: "pagination page 1",
opts: model.PropertyValueSearchOpts{
GroupID: groupID,
Page: 1,
PerPage: 1,
},
expectedIDs: []string{value2.ID},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
results, err := ss.PropertyValue().SearchPropertyValues(tc.opts)
if tc.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
var ids = make([]string, len(results))
for i, value := range results {
ids[i] = value.ID
}
require.ElementsMatch(t, tc.expectedIDs, ids)
})
}
}
func testDeleteForField(t *testing.T, _ request.CTX, ss store.Store) {
fieldID := model.NewId()
// Create test values
value1 := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: fieldID,
Value: "value 1",
}
value2 := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: fieldID,
Value: "value 2",
}
value3 := &model.PropertyValue{
TargetID: model.NewId(),
TargetType: "test_type",
GroupID: model.NewId(),
FieldID: model.NewId(), // Different field ID
Value: "value 3",
}
for _, value := range []*model.PropertyValue{value1, value2, value3} {
_, err := ss.PropertyValue().Create(value)
require.NoError(t, err)
}
// Delete values for the field
err := ss.PropertyValue().DeleteForField(fieldID)
require.NoError(t, err)
// Verify values were soft-deleted
deletedValues, err := ss.PropertyValue().GetMany([]string{value1.ID, value2.ID})
require.NoError(t, err)
require.Len(t, deletedValues, 2)
require.NotZero(t, deletedValues[0].DeleteAt)
require.NotZero(t, deletedValues[1].DeleteAt)
// Verify value with different field ID was not deleted
nonDeletedValue, err := ss.PropertyValue().Get(value3.ID)
require.NoError(t, err)
require.Zero(t, nonDeletedValue.DeleteAt)
}

View file

@ -66,6 +66,9 @@ type Store struct {
DesktopTokensStore mocks.DesktopTokensStore
ChannelBookmarkStore mocks.ChannelBookmarkStore
ScheduledPostStore mocks.ScheduledPostStore
PropertyGroupStore mocks.PropertyGroupStore
PropertyFieldStore mocks.PropertyFieldStore
PropertyValueStore mocks.PropertyValueStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
@ -119,6 +122,9 @@ func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMet
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore }
func (s *Store) ScheduledPost() store.ScheduledPostStore { return &s.ScheduledPostStore }
func (s *Store) PropertyGroup() store.PropertyGroupStore { return &s.PropertyGroupStore }
func (s *Store) PropertyField() store.PropertyFieldStore { return &s.PropertyFieldStore }
func (s *Store) PropertyValue() store.PropertyValueStore { return &s.PropertyValueStore }
func (s *Store) PostAcknowledgement() store.PostAcknowledgementStore {
return &s.PostAcknowledgementStore
}

View file

@ -46,6 +46,9 @@ type TimerLayer struct {
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
PropertyFieldStore store.PropertyFieldStore
PropertyGroupStore store.PropertyGroupStore
PropertyValueStore store.PropertyValueStore
ReactionStore store.ReactionStore
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
@ -175,6 +178,18 @@ func (s *TimerLayer) ProductNotices() store.ProductNoticesStore {
return s.ProductNoticesStore
}
func (s *TimerLayer) PropertyField() store.PropertyFieldStore {
return s.PropertyFieldStore
}
func (s *TimerLayer) PropertyGroup() store.PropertyGroupStore {
return s.PropertyGroupStore
}
func (s *TimerLayer) PropertyValue() store.PropertyValueStore {
return s.PropertyValueStore
}
func (s *TimerLayer) Reaction() store.ReactionStore {
return s.ReactionStore
}
@ -386,6 +401,21 @@ type TimerLayerProductNoticesStore struct {
Root *TimerLayer
}
type TimerLayerPropertyFieldStore struct {
store.PropertyFieldStore
Root *TimerLayer
}
type TimerLayerPropertyGroupStore struct {
store.PropertyGroupStore
Root *TimerLayer
}
type TimerLayerPropertyValueStore struct {
store.PropertyValueStore
Root *TimerLayer
}
type TimerLayerReactionStore struct {
store.ReactionStore
Root *TimerLayer
@ -6979,6 +7009,246 @@ func (s *TimerLayerProductNoticesStore) View(userID string, notices []string) er
return err
}
func (s *TimerLayerPropertyFieldStore) Create(field *model.PropertyField) (*model.PropertyField, error) {
start := time.Now()
result, err := s.PropertyFieldStore.Create(field)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyFieldStore.Create", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyFieldStore) Delete(id string) error {
start := time.Now()
err := s.PropertyFieldStore.Delete(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyFieldStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPropertyFieldStore) Get(id string) (*model.PropertyField, error) {
start := time.Now()
result, err := s.PropertyFieldStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyFieldStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyFieldStore) GetMany(ids []string) ([]*model.PropertyField, error) {
start := time.Now()
result, err := s.PropertyFieldStore.GetMany(ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyFieldStore.GetMany", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyFieldStore) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
start := time.Now()
result, err := s.PropertyFieldStore.SearchPropertyFields(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyFieldStore.SearchPropertyFields", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyFieldStore) Update(field []*model.PropertyField) ([]*model.PropertyField, error) {
start := time.Now()
result, err := s.PropertyFieldStore.Update(field)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyFieldStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyGroupStore) Get(name string) (*model.PropertyGroup, error) {
start := time.Now()
result, err := s.PropertyGroupStore.Get(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyGroupStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyGroupStore) Register(name string) (*model.PropertyGroup, error) {
start := time.Now()
result, err := s.PropertyGroupStore.Register(name)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyGroupStore.Register", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyValueStore) Create(value *model.PropertyValue) (*model.PropertyValue, error) {
start := time.Now()
result, err := s.PropertyValueStore.Create(value)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyValueStore.Create", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyValueStore) Delete(id string) error {
start := time.Now()
err := s.PropertyValueStore.Delete(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyValueStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPropertyValueStore) DeleteForField(id string) error {
start := time.Now()
err := s.PropertyValueStore.DeleteForField(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyValueStore.DeleteForField", success, elapsed)
}
return err
}
func (s *TimerLayerPropertyValueStore) Get(id string) (*model.PropertyValue, error) {
start := time.Now()
result, err := s.PropertyValueStore.Get(id)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyValueStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyValueStore) GetMany(ids []string) ([]*model.PropertyValue, error) {
start := time.Now()
result, err := s.PropertyValueStore.GetMany(ids)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyValueStore.GetMany", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyValueStore) SearchPropertyValues(opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) {
start := time.Now()
result, err := s.PropertyValueStore.SearchPropertyValues(opts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyValueStore.SearchPropertyValues", success, elapsed)
}
return result, err
}
func (s *TimerLayerPropertyValueStore) Update(field []*model.PropertyValue) ([]*model.PropertyValue, error) {
start := time.Now()
result, err := s.PropertyValueStore.Update(field)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PropertyValueStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
start := time.Now()
@ -12479,6 +12749,9 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
newStore.PostPriorityStore = &TimerLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &TimerLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &TimerLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
newStore.PropertyFieldStore = &TimerLayerPropertyFieldStore{PropertyFieldStore: childStore.PropertyField(), Root: &newStore}
newStore.PropertyGroupStore = &TimerLayerPropertyGroupStore{PropertyGroupStore: childStore.PropertyGroup(), Root: &newStore}
newStore.PropertyValueStore = &TimerLayerPropertyValueStore{PropertyValueStore: childStore.PropertyValue(), Root: &newStore}
newStore.ReactionStore = &TimerLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
newStore.RemoteClusterStore = &TimerLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &TimerLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}

View file

@ -114,6 +114,10 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
pluginStore := mocks.PluginStore{}
pluginStore.On("List", mock.Anything, mock.Anything, mock.Anything).Return([]string{}, nil)
propertyGroupStore := mocks.PropertyGroupStore{}
propertyFieldStore := mocks.PropertyFieldStore{}
propertyValueStore := mocks.PropertyValueStore{}
mockStore.On("System").Return(&systemStore)
mockStore.On("User").Return(&userStore)
mockStore.On("Post").Return(&postStore)
@ -130,6 +134,9 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
mockStore.On("Group").Return(&groupStore)
mockStore.On("GetDBSchemaVersion").Return(1, nil)
mockStore.On("Plugin").Return(&pluginStore)
mockStore.On("PropertyGroup").Return(&propertyGroupStore)
mockStore.On("PropertyField").Return(&propertyFieldStore)
mockStore.On("PropertyValue").Return(&propertyValueStore)
return &mockStore
}

View file

@ -9652,6 +9652,14 @@
"id": "model.preference.is_valid.value.app_error",
"translation": "Value is too long."
},
{
"id": "model.property_field.is_valid.app_error",
"translation": "Invalid property field: {{.FieldName}} ({{.Reason}})."
},
{
"id": "model.property_value.is_valid.app_error",
"translation": "Invalid property value: {{.FieldName}} ({{.Reason}})."
},
{
"id": "model.reaction.is_valid.create_at.app_error",
"translation": "Create at must be a valid time."

View file

@ -0,0 +1,83 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import "net/http"
type PropertyFieldType string
const (
PropertyFieldTypeText PropertyFieldType = "text"
PropertyFieldTypeSelect PropertyFieldType = "select"
PropertyFieldTypeMultiselect PropertyFieldType = "multiselect"
PropertyFieldTypeDate PropertyFieldType = "date"
PropertyFieldTypeUser PropertyFieldType = "user"
PropertyFieldTypeMultiuser PropertyFieldType = "multiuser"
)
type PropertyField struct {
ID string `json:"id"`
GroupID string `json:"group_id"`
Name string `json:"name"`
Type PropertyFieldType `json:"type"`
Attrs map[string]any `json:"attrs"`
TargetID string `json:"target_id"`
TargetType string `json:"target_type"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
}
func (pf *PropertyField) PreSave() {
if pf.ID == "" {
pf.ID = NewId()
}
if pf.CreateAt == 0 {
pf.CreateAt = GetMillis()
}
pf.UpdateAt = pf.CreateAt
}
func (pf *PropertyField) IsValid() error {
if !IsValidId(pf.ID) {
return NewAppError("PropertyField.IsValid", "model.property_field.is_valid.app_error", map[string]any{"FieldName": "id", "Reason": "invalid id"}, "", http.StatusBadRequest)
}
if !IsValidId(pf.GroupID) {
return NewAppError("PropertyField.IsValid", "model.property_field.is_valid.app_error", map[string]any{"FieldName": "group_id", "Reason": "invalid id"}, "id="+pf.ID, http.StatusBadRequest)
}
if pf.Name == "" {
return NewAppError("PropertyField.IsValid", "model.property_field.is_valid.app_error", map[string]any{"FieldName": "name", "Reason": "value cannot be empty"}, "id="+pf.ID, http.StatusBadRequest)
}
if !(pf.Type == PropertyFieldTypeText ||
pf.Type == PropertyFieldTypeSelect ||
pf.Type == PropertyFieldTypeMultiselect ||
pf.Type == PropertyFieldTypeDate ||
pf.Type == PropertyFieldTypeUser ||
pf.Type == PropertyFieldTypeMultiuser) {
return NewAppError("PropertyField.IsValid", "model.property_field.is_valid.app_error", map[string]any{"FieldName": "type", "Reason": "unknown value"}, "id="+pf.ID, http.StatusBadRequest)
}
if pf.CreateAt == 0 {
return NewAppError("PropertyField.IsValid", "model.property_field.is_valid.app_error", map[string]any{"FieldName": "create_at", "Reason": "value cannot be zero"}, "id="+pf.ID, http.StatusBadRequest)
}
if pf.UpdateAt == 0 {
return NewAppError("PropertyField.IsValid", "model.property_field.is_valid.app_error", map[string]any{"FieldName": "update_at", "Reason": "value cannot be zero"}, "id="+pf.ID, http.StatusBadRequest)
}
return nil
}
type PropertyFieldSearchOpts struct {
GroupID string
TargetType string
TargetID string
IncludeDeleted bool
Page int
PerPage int
}

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type PropertyGroup struct {
ID string
Name string
}
func (pg *PropertyGroup) PreSave() {
if pg.ID == "" {
pg.ID = NewId()
}
}

View file

@ -0,0 +1,71 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import "net/http"
type PropertyValue struct {
ID string `json:"id"`
TargetID string `json:"target_id"`
TargetType string `json:"target_type"`
GroupID string `json:"group_id"`
FieldID string `json:"field_id"`
Value string `json:"value"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
}
func (pv *PropertyValue) PreSave() {
if pv.ID == "" {
pv.ID = NewId()
}
if pv.CreateAt == 0 {
pv.CreateAt = GetMillis()
}
pv.UpdateAt = pv.CreateAt
}
func (pv *PropertyValue) IsValid() error {
if !IsValidId(pv.ID) {
return NewAppError("PropertyValue.IsValid", "model.property_value.is_valid.app_error", map[string]any{"FieldName": "id", "Reason": "invalid id"}, "", http.StatusBadRequest)
}
if !IsValidId(pv.TargetID) {
return NewAppError("PropertyValue.IsValid", "model.property_value.is_valid.app_error", map[string]any{"FieldName": "target_id", "Reason": "invalid id"}, "id="+pv.ID, http.StatusBadRequest)
}
if pv.TargetType == "" {
return NewAppError("PropertyValue.IsValid", "model.property_value.is_valid.app_error", map[string]any{"FieldName": "target_type", "Reason": "value cannot be empty"}, "id="+pv.ID, http.StatusBadRequest)
}
if !IsValidId(pv.GroupID) {
return NewAppError("PropertyValue.IsValid", "model.property_value.is_valid.app_error", map[string]any{"FieldName": "group_id", "Reason": "invalid id"}, "id="+pv.ID, http.StatusBadRequest)
}
if !IsValidId(pv.FieldID) {
return NewAppError("PropertyValue.IsValid", "model.property_value.is_valid.app_error", map[string]any{"FieldName": "field_id", "Reason": "invalid id"}, "id="+pv.ID, http.StatusBadRequest)
}
if pv.CreateAt == 0 {
return NewAppError("PropertyValue.IsValid", "model.property_value.is_valid.app_error", map[string]any{"FieldName": "create_at", "Reason": "value cannot be zero"}, "id="+pv.ID, http.StatusBadRequest)
}
if pv.UpdateAt == 0 {
return NewAppError("PropertyValue.IsValid", "model.property_value.is_valid.app_error", map[string]any{"FieldName": "update_at", "Reason": "value cannot be zero"}, "id="+pv.ID, http.StatusBadRequest)
}
return nil
}
type PropertyValueSearchOpts struct {
GroupID string
TargetType string
TargetID string
FieldID string
IncludeDeleted bool
Page int
PerPage int
}