[MM-62553]+[MM-62554] Property Architecture: cursor based pagination (#30119)

* refactor: Replace pagination with cursor-based pagination for custom profile attributes

* remove pagination loop on property value retrieval for CPA

* add migrations to optimize pagination on property fields and values

* adapt test to remove pagination check

* update migrations list

* postgres: drop index concurrently

* concurrent index manipulation must be done outside of a Tx

* fix: Correct SQL index drop syntax from "OM" to "ON" in migration files

* test: Add CountForGroup test cases for property field store

* refactor: Add CountForGroup method to PropertyFieldStore interface and implementations

* Fix style and i18n

* feat: Add optional deleted property field filtering to CountForGroup method

* refactor: Update CountForGroup to support optional deleted property fields

* test: Add comprehensive tests for CountForGroup with includeDeleted parameter

* adapt test + gen layers

* rename property service method and set the includeDelete to false

* refactor: Remove redundant constant and use CustomProfileAttributesFieldLimit directly

* fix tests

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Julien Tant
2025-02-13 08:23:50 -07:00
коммит произвёл GitHub
родитель 4615ca5f28
Коммит 632a60b332
25 изменённых файлов: 820 добавлений и 61 удалений

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

@@ -12,7 +12,9 @@ import (
"github.com/pkg/errors"
)
const CustomProfileAttributesFieldLimit = 20
const (
CustomProfileAttributesFieldLimit = 20
)
var cpaGroupID string
@@ -58,7 +60,6 @@ func (a *App) ListCPAFields() ([]*model.PropertyField, *model.AppError) {
opts := model.PropertyFieldSearchOpts{
GroupID: groupID,
Page: 0,
PerPage: CustomProfileAttributesFieldLimit,
}
@@ -76,12 +77,12 @@ func (a *App) CreateCPAField(field *model.PropertyField) (*model.PropertyField,
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
existingFields, appErr := a.ListCPAFields()
if appErr != nil {
return nil, appErr
fieldCount, err := a.Srv().propertyService.CountActivePropertyFieldsForGroup(groupID)
if err != nil {
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.count_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(existingFields) >= CustomProfileAttributesFieldLimit {
if fieldCount >= CustomProfileAttributesFieldLimit {
return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.limit_reached.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
}
@@ -171,19 +172,16 @@ func (a *App) ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppEr
return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
opts := model.PropertyValueSearchOpts{
GroupID: groupID,
TargetID: userID,
Page: 0,
PerPage: 999999,
IncludeDeleted: false,
}
fields, err := a.Srv().propertyService.SearchPropertyValues(opts)
values, err := a.Srv().propertyService.SearchPropertyValues(model.PropertyValueSearchOpts{
GroupID: groupID,
TargetID: userID,
PerPage: CustomProfileAttributesFieldLimit,
})
if err != nil {
return nil, model.NewAppError("ListCPAValues", "app.custom_profile_attributes.list_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return fields, nil
return values, nil
}
func (a *App) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError) {

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

@@ -517,3 +517,60 @@ func TestPatchCPAValue(t *testing.T) {
require.Equal(t, userID, updatedValue.TargetID)
})
}
func TestListCPAValues(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
th := Setup(t).InitBasic()
defer th.TearDown()
cpaGroupID, cErr := th.App.cpaGroupID()
require.NoError(t, cErr)
userID := model.NewId()
t.Run("should return empty list when user has no values", func(t *testing.T) {
values, appErr := th.App.ListCPAValues(userID)
require.Nil(t, appErr)
require.Empty(t, values)
})
t.Run("should list all values for a user", func(t *testing.T) {
var expectedValues []json.RawMessage
for i := 1; i <= CustomProfileAttributesFieldLimit; i++ {
field := &model.PropertyField{
GroupID: cpaGroupID,
Name: fmt.Sprintf("Field %d", i),
Type: model.PropertyFieldTypeText,
}
_, err := th.App.Srv().propertyService.CreatePropertyField(field)
require.NoError(t, err)
value := &model.PropertyValue{
TargetID: userID,
TargetType: "user",
GroupID: cpaGroupID,
FieldID: field.ID,
Value: json.RawMessage(fmt.Sprintf(`"Value %d"`, i)),
}
_, err = th.App.Srv().propertyService.CreatePropertyValue(value)
require.NoError(t, err)
expectedValues = append(expectedValues, value.Value)
}
// List values for original user
values, appErr := th.App.ListCPAValues(userID)
require.Nil(t, appErr)
require.Len(t, values, CustomProfileAttributesFieldLimit)
actualValues := make([]json.RawMessage, len(values))
for i, value := range values {
require.Equal(t, userID, value.TargetID)
require.Equal(t, "user", value.TargetType)
require.Equal(t, cpaGroupID, value.GroupID)
actualValues[i] = value.Value
}
require.ElementsMatch(t, expectedValues, actualValues)
})
}

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

@@ -19,6 +19,10 @@ func (ps *PropertyService) GetPropertyFields(ids []string) ([]*model.PropertyFie
return ps.fieldStore.GetMany(ids)
}
func (ps *PropertyService) CountActivePropertyFieldsForGroup(groupID string) (int64, error) {
return ps.fieldStore.CountForGroup(groupID, false)
}
func (ps *PropertyService) SearchPropertyFields(opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) {
return ps.fieldStore.SearchPropertyFields(opts)
}