Skip to content
Snippets Groups Projects
Commit ecb2f118 authored by Pavel Antonov's avatar Pavel Antonov :asterisk:
Browse files

Merge branch 'fix/PRXS-3296-RenameItemFieldTags' into 'master'

Исправлена выгрузка полей Item в JSON, приведены к camelCase

See merge request perxis/perxis-go!473
parents db2d8043 9b0d4c3a
No related branches found
No related tags found
No related merge requests found
......@@ -13,6 +13,7 @@ import (
"git.perx.ru/perxis/perxis-go/pkg/schema/localizer"
"git.perx.ru/perxis/perxis-go/pkg/schema/walk"
pb "git.perx.ru/perxis/perxis-go/proto/items"
"github.com/mitchellh/mapstructure"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
......@@ -106,7 +107,7 @@ type Item struct {
// При создании или обновлении идентификатор локали в котором создается запись, опционально.
// Если указан, то создается перевод для указанного языка, поле translations игнорируется
LocaleID string `json:"locale_id" bson:"-"`
LocaleID string `json:"localeId,omitempty" bson:"-"`
// Позволяет одновременно установить/получить несколько переводов и производить манипуляции с переводами
// Ключами является идентификатор локали, значениями - данные переводы
......@@ -117,11 +118,11 @@ type Item struct {
// - {"lang":map{...}} - установка перевода для языка
// - {"lang":map{...}, "*":nil} - установка перевода для языка, сброс остальных переводов
// - {"*":nil} - сброс всех переводов
Translations map[string]map[string]interface{} `json:"translations" bson:"translations,omitempty"`
Translations map[string]map[string]interface{} `json:"translations,omitempty" bson:"translations,omitempty"`
// Список идентификаторов локалей, для которых есть переводы.
// Соответствует ключам в translations
TranslationsIDs []string `json:"translations_ids" bson:"translations_ids,omitempty"`
TranslationsIDs []string `json:"translationsIds,omitempty" bson:"translations_ids,omitempty"`
RevisionID string `json:"revId,omitempty" bson:"revision_id"`
RevisionDescription string `json:"revDescription,omitempty" bson:"revision_description"`
......@@ -130,9 +131,9 @@ type Item struct {
// Релеватность элемента при полнотекстовом поиске
SearchScore float64 `json:"searchScore,omitempty" bson:"search_score,omitempty"`
Deleted bool `json:"deleted" bson:"deleted,omitempty"`
Hidden bool `json:"hidden" bson:"hidden,omitempty"`
Template bool `json:"template" bson:"template,omitempty"`
Deleted bool `json:"deleted,omitempty" bson:"deleted,omitempty"`
Hidden bool `json:"hidden,omitempty" bson:"hidden,omitempty"`
Template bool `json:"template,omitempty" bson:"template,omitempty"`
}
func NewItem(spaceID, envID, collID, id string, data map[string]interface{}, translations map[string]map[string]interface{}) *Item {
......@@ -169,6 +170,8 @@ func (i *Item) Clone() *Item {
return &itm
}
// ToMap конвертирует текущий элемент в map[string]any.
// DEPRECATED, используйте ToMap.
func (i *Item) ToMap() map[string]interface{} {
return map[string]interface{}{
"id": i.ID,
......@@ -194,6 +197,86 @@ func (i *Item) ToMap() map[string]interface{} {
}
}
// ToMap конвертирует переданный Item в map[string]any, кодируя данные согласно схеме.
// Вычисляемые поля удаляются из результата.
func ToMap(item *Item, sch *schema.Schema) (map[string]any, error) {
if item == nil {
return nil, errors.New("item must not be nil")
}
if sch == nil {
return nil, errors.New("schema must not be nil")
}
output := make(map[string]any)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
Result: &output,
})
if err != nil {
return nil, errors.Wrap(err, "failed to create decoder")
}
item, err = item.Encode(context.Background(), sch)
if err != nil {
return nil, errors.Wrap(err, "failed to encode item by schema")
}
err = decoder.Decode(item)
if err != nil {
return nil, errors.Wrap(err, "failed to encode item")
}
// Кодируем системные поля со временем
output["createdRevAt"] = item.CreatedRevAt.Format(time.RFC3339)
output["createdAt"] = item.CreatedAt.Format(time.RFC3339)
output["updatedAt"] = item.UpdatedAt.Format(time.RFC3339)
// Удаляем вычисляемые поля
delete(output, "permissions")
delete(output, "searchScore")
return output, nil
}
// FromMap конвертирует переданный map[string]any в Item, декодируя данные согласно схеме.
// Вычисляемые поля игнорируются при декодировании.
func FromMap(input map[string]any, sch *schema.Schema) (*Item, error) {
if len(input) == 0 {
return nil, errors.New("input map must not be empty or nil")
}
if sch == nil {
return nil, errors.New("schema must not be nil")
}
item := &Item{}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339),
TagName: "json",
Result: item,
})
if err != nil {
return nil, errors.Wrap(err, "failed to create decoder")
}
// Удаляем вычисляемые поля
delete(input, "permissions")
delete(input, "searchScore")
err = decoder.Decode(input)
if err != nil {
return nil, errors.Wrap(err, "failed to decode")
}
item, err = item.Decode(context.Background(), sch)
if err != nil {
return nil, errors.Wrap(err, "failed to decode item by schema")
}
return item, nil
}
// SetData устанавливает перевод в нужное поле записи
func (i *Item) SetData(dt map[string]interface{}, localeID string) {
if localeID == "" || localeID == locales.DefaultID {
......
......@@ -506,3 +506,243 @@ func Test_mergeItemData(t *testing.T) {
})
}
}
func TestToMap(t *testing.T) {
sch := schema.New(
"str", field.String(),
"num", field.Number(field.NumberFormatFloat),
"embedded", field.Object(
"now", field.Time(),
),
"timestamp", field.Timestamp(),
)
tests := []struct {
name string
input *Item
sch *schema.Schema
want map[string]any
assertErr assert.ErrorAssertionFunc
}{
{
name: "nil item",
input: nil,
sch: nil,
want: nil,
assertErr: assert.Error,
},
{
name: "nil schema",
input: &Item{},
sch: nil,
want: nil,
assertErr: assert.Error,
},
{
name: "simple",
input: &Item{
ID: "item_id",
SpaceID: "space_id",
EnvID: "env_id",
CollectionID: "coll_id",
State: StatePublished,
CreatedRevAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
CreatedBy: "created_by",
CreatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
UpdatedBy: "updated_by",
Data: map[string]any{
"str": "string",
"num": 2.7,
"embedded": map[string]any{
"now": time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
},
"timestamp": 1723420800,
},
LocaleID: "locale_id",
Translations: map[string]map[string]any{
"ru": {
"str": "строка",
"num": 3.14,
},
},
TranslationsIDs: []string{"ru"},
RevisionID: "rev_id",
RevisionDescription: "rev_desc",
Permissions: &Permissions{
Edit: true,
Archive: false,
Publish: true,
SoftDelete: false,
HardDelete: false,
},
SearchScore: 123.0,
Deleted: false,
Hidden: true,
Template: true,
},
sch: sch,
want: map[string]any{
"id": "item_id",
"spaceId": "space_id",
"envId": "env_id",
"collectionId": "coll_id",
"state": StatePublished,
"createdRevAt": "2024-08-12T00:00:00Z",
"createdBy": "created_by",
"createdAt": "2024-08-12T00:00:00Z",
"updatedAt": "2024-08-12T00:00:00Z",
"updatedBy": "updated_by",
"data": map[string]any{
"str": "string",
"num": 2.7,
"embedded": map[string]any{
"now": "2024-08-12T00:00:00Z",
},
"timestamp": int64(1723420800),
},
"localeId": "locale_id",
"translations": map[string]map[string]any{
"ru": {
"str": "строка",
"num": 3.14,
},
},
"translationsIds": []string{"ru"},
"revId": "rev_id",
"revDescription": "rev_desc",
"hidden": true,
"template": true,
},
assertErr: assert.NoError,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := ToMap(tc.input, tc.sch)
tc.assertErr(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestFromMap(t *testing.T) {
sch := schema.New(
"str", field.String(),
"num", field.Number(field.NumberFormatFloat),
"embedded", field.Object(
"now", field.Time(),
),
"timestamp", field.Timestamp(),
)
tests := []struct {
name string
input map[string]any
want *Item
sch *schema.Schema
assertErr assert.ErrorAssertionFunc
}{
{
name: "nil item",
input: nil,
sch: nil,
want: nil,
assertErr: assert.Error,
},
{
name: "empty item",
input: map[string]any{},
sch: nil,
want: nil,
assertErr: assert.Error,
},
{
name: "nil schema",
input: map[string]any{},
sch: nil,
assertErr: assert.Error,
},
{
name: "simple",
input: map[string]any{
"id": "item_id",
"spaceId": "space_id",
"envId": "env_id",
"collectionId": "coll_id",
"state": StatePublished,
"createdRevAt": "2024-08-12T00:00:00Z",
"createdBy": "created_by",
"createdAt": "2024-08-12T00:00:00Z",
"updatedAt": "2024-08-12T00:00:00Z",
"updatedBy": "updated_by",
"data": map[string]any{
"str": "string",
"num": 2.7,
"embedded": map[string]any{
"now": "2024-08-12T00:00:00Z",
},
"timestamp": 1723420800,
},
"localeId": "locale_id",
"translations": map[string]map[string]any{
"ru": {
"str": "строка",
"num": 3.14,
},
},
"translationsIds": []string{"ru"},
"revId": "rev_id",
"revDescription": "rev_desc",
"deleted": false,
"hidden": true,
"template": true,
},
sch: sch,
want: &Item{
ID: "item_id",
SpaceID: "space_id",
EnvID: "env_id",
CollectionID: "coll_id",
State: StatePublished,
CreatedRevAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
CreatedBy: "created_by",
CreatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
UpdatedBy: "updated_by",
Data: map[string]any{
"str": "string",
"num": 2.7,
"embedded": map[string]any{
"now": time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
},
"timestamp": int64(1723420800),
},
LocaleID: "locale_id",
Translations: map[string]map[string]any{
"ru": {
"str": "строка",
"num": 3.14,
},
},
TranslationsIDs: []string{"ru"},
RevisionID: "rev_id",
RevisionDescription: "rev_desc",
Permissions: nil,
SearchScore: 0.0,
Deleted: false,
Hidden: true,
Template: true,
},
assertErr: assert.NoError,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := FromMap(tc.input, sch)
tc.assertErr(t, err)
assert.Equal(t, tc.want, got)
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment