diff --git a/perxis-proto b/perxis-proto index d05d75325479800baef608bcde55c46b42b019e0..64fe5194e41d6d1023d6d78e794e7458ecef1888 160000 --- a/perxis-proto +++ b/perxis-proto @@ -1 +1 @@ -Subproject commit d05d75325479800baef608bcde55c46b42b019e0 +Subproject commit 64fe5194e41d6d1023d6d78e794e7458ecef1888 diff --git a/pkg/items/item.go b/pkg/items/item.go index c808c719628eb512d71589be53c9cd2f073b9c3d..1a226a7627a0435a2b77480ebc2095e126dae37d 100644 --- a/pkg/items/item.go +++ b/pkg/items/item.go @@ -107,8 +107,15 @@ type Item struct { // Если указан, то создается перевод для указанного языка, поле translations игнорируется LocaleID string `json:"locale_id" bson:"-"` - // Рспользуется РїСЂРё одновременной установке/получении нескольких переводов - // Ключами является идентификатор локали, значениями - данные переводов + // Позволяет одновременно установить/получить несколько переводов Рё производить манипуляции СЃ переводами + // Ключами является идентификатор локали, значениями - данные переводы + // РџСЂРё обновлении РЅРµ РїСЂРѕРёСЃС…РѕРґРёС‚ валидация или модификация каждого РёР· переводов РІ соответствие СЃРѕ схемой, + // поэтому обновление через поле `translations` стоит выполнять СЃ аккуратностью + // Для удаления переводов реализована следующая логика: + // - {"lang":nil|{}} - СЃР±СЂРѕСЃ перевода для языка + // - {"lang":map{...}} - установка перевода для языка + // - {"lang":map{...}, "*":nil} - установка перевода для языка, СЃР±СЂРѕСЃ остальных переводов + // - {"*":nil} - СЃР±СЂРѕСЃ всех переводов Translations map[string]map[string]interface{} `json:"translations" bson:"translations,omitempty"` // РЎРїРёСЃРѕРє идентификаторов локалей, для которых есть переводы. @@ -177,31 +184,44 @@ func (i *Item) ToMap() map[string]interface{} { } } -// SetData устанавливает перевод РІ РЅСѓР¶РЅРѕРµ поле записи, предварительно рассчитав РёР· него дельту РїРѕ -// отношению Рє основным данным -func (i *Item) SetData(dt map[string]interface{}, localizer *localizer.Localizer) (err error) { - if localizer != nil && localizer.LocaleID() != locales.DefaultID { - if i.Translations == nil { - i.Translations = make(map[string]map[string]interface{}) - } - i.Translations[localizer.LocaleID()] = dt - i.Translations[localizer.LocaleID()], err = localizer.ExtractTranslation(i.Data, i.Translations) - if !data.Contains(localizer.LocaleID(), i.TranslationsIDs) { - i.TranslationsIDs = append(i.TranslationsIDs, localizer.LocaleID()) - } +// SetData устанавливает перевод РІ РЅСѓР¶РЅРѕРµ поле записи +func (i *Item) SetData(dt map[string]interface{}, localeID string) { + if localeID == "" || localeID == locales.DefaultID { + i.Data = dt return } - - i.Data = dt + if i.Translations == nil { + i.Translations = map[string]map[string]interface{}{localeID: dt} + if !data.Contains(localeID, i.TranslationsIDs) { + i.TranslationsIDs = append(i.TranslationsIDs, localeID) + } + } return } // GetData возвращает полные локализованные данные записи -func (i *Item) GetData(localizer *localizer.Localizer) (map[string]interface{}, error) { - if localizer != nil { - return localizer.Localize(i.Data, i.Translations) +func (i *Item) GetData(localeID string) map[string]interface{} { + if localeID == "" || localeID == locales.DefaultID { + return i.Data + } + if i.Translations != nil { + return i.Translations[localeID] + } + return nil +} + +func (i *Item) AddTranslations(translations map[string]map[string]interface{}) { + if i.Translations == nil { + i.Translations = make(map[string]map[string]interface{}, len(translations)) + } + for l, t := range translations { + i.Translations[l] = t + } + for k := range translations { + if !data.Contains(k, i.TranslationsIDs) { + i.TranslationsIDs = append(i.TranslationsIDs, k) + } } - return i.Data, nil } func (i *Item) Localize(localizer *localizer.Localizer) (err error) { @@ -217,25 +237,25 @@ func (i *Item) Localize(localizer *localizer.Localizer) (err error) { return nil } -// UnsetTranslation устанавливает значение перевода РІ nil, для СЃР±СЂРѕСЃР° перевода для языка -// "localeID":map{...}, "*":nil} - установка перевода для языка, СЃР±СЂРѕСЃ остальных переводов -// {"*":nil} -СЃР±СЂРѕСЃ всех переводов -func (i *Item) UnsetTranslation(localeID string) { - if i.Translations == nil { - i.Translations = make(map[string]map[string]interface{}) - } - - i.Translations[localeID] = nil -} - -// SetTranslation устанавливает перевод для языка -func (i *Item) SetTranslation(dt map[string]interface{}, localeID string) { - if i.Translations == nil { - i.Translations = make(map[string]map[string]interface{}) - } - - i.Translations[localeID] = dt -} +// // UnsetTranslation устанавливает значение перевода РІ nil, для СЃР±СЂРѕСЃР° перевода для языка +// // "localeID":map{...}, "*":nil} - установка перевода для языка, СЃР±СЂРѕСЃ остальных переводов +// // {"*":nil} -СЃР±СЂРѕСЃ всех переводов +// func (i *Item) UnsetTranslation(localeID string) { +// if i.Translations == nil { +// i.Translations = make(map[string]map[string]interface{}) +// } +// +// i.Translations[localeID] = nil +// } +// +// // SetTranslation устанавливает перевод для языка +// func (i *Item) SetTranslation(dt map[string]interface{}, localeID string) { +// if i.Translations == nil { +// i.Translations = make(map[string]map[string]interface{}) +// } +// +// i.Translations[localeID] = dt +// } func (i *Item) Encode(ctx context.Context, s *schema.Schema) (*Item, error) { res := *i @@ -250,9 +270,12 @@ func (i *Item) Encode(ctx context.Context, s *schema.Schema) (*Item, error) { if len(i.Translations) > 0 { res.Translations = make(map[string]map[string]interface{}, len(i.Translations)) for l, v := range i.Translations { + if len(v) == 0 { + res.Translations[l] = v + continue + } dt, err := schema.Encode(ctx, s, v) if err != nil { - // return errors.WithField(err, fmt.Sprintf("translations.%s", l)) return nil, err } res.Translations[l] = dt.(map[string]interface{}) @@ -268,12 +291,15 @@ func (i *Item) Decode(ctx context.Context, s *schema.Schema) (*Item, error) { res.Data, err = s.Decode(ctx, i.Data) if err != nil { return nil, err - // return errors.WithField(err, "data") } } if len(i.Translations) > 0 { res.Translations = make(map[string]map[string]interface{}, len(i.Translations)) for l, v := range i.Translations { + if len(v) == 0 { + res.Translations[l] = v + continue + } dt, err := schema.Decode(ctx, s, v) if err != nil { return nil, err @@ -298,18 +324,13 @@ func (i *Item) ProcessData(ctx context.Context, sch *schema.Schema, fn ProcessDa tr := make(map[string]map[string]interface{}) for _, l := range locales { - data, err := i.GetData(localizer.NewLocalizer(localizer.Config{ - Schema: sch, - Locales: locales, - LocaleID: l.ID, - AllowNoPublished: false, - AllowDisabled: false, - })) + itm := *i + err := itm.Localize(localizer.NewLocalizer(localizer.Config{Schema: sch, Locales: locales, LocaleID: l.ID})) if err != nil { return nil, errors.WithField(err, fmt.Sprintf("translations.%s", l.ID)) } - dt, err := fn(ctx, sch, data) + dt, err := fn(ctx, sch, itm.Data) if err != nil { return nil, errors.WithField(err, fmt.Sprintf("translations.%s", l.ID)) } @@ -531,7 +552,11 @@ func ItemToProto(item *Item) *pb.Item { if item.Translations != nil { protoItem.Translations = make(map[string]*structpb.Struct, len(item.Translations)) for k, v := range item.Translations { - protoItem.Translations[k], _ = structpb.NewStruct(v) + var t *structpb.Struct + if v != nil { + t, _ = structpb.NewStruct(v) + } + protoItem.Translations[k] = t } } @@ -582,8 +607,16 @@ func ItemFromProto(protoItem *pb.Item) *Item { if protoItem.Translations != nil { item.Translations = make(map[string]map[string]interface{}, len(protoItem.Translations)) + } + if protoItem.Translations != nil { for k, v := range protoItem.Translations { - item.Translations[k] = v.AsMap() + // РџСЂРё proto.Marshal/Unmarshal `nil`-значение превращается РІ `Struct{}`, поэтому логика + // РјРЅРѕРіРѕ смысла РЅРµ несет, РЅРѕ хотя Р±С‹ здесь сохраним различие пустого Рё nil значений + var t map[string]interface{} + if v != nil { + t = v.AsMap() + } + item.Translations[k] = t } } diff --git a/pkg/items/item_test.go b/pkg/items/item_test.go index 47e7f02be2014b3c94e4a7c0d29d4ccab7dbca63..2cef5e75414f55e09d3e5f42d4ed4ee56f0cd866 100644 --- a/pkg/items/item_test.go +++ b/pkg/items/item_test.go @@ -1,6 +1,7 @@ package items import ( + "context" "fmt" "testing" "time" @@ -8,6 +9,7 @@ import ( "git.perx.ru/perxis/perxis-go/pkg/schema" "git.perx.ru/perxis/perxis-go/pkg/schema/field" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestItem_Set(t *testing.T) { @@ -95,3 +97,169 @@ func TestGetField(t *testing.T) { }) } } + +func TestItem_Proto(t *testing.T) { + w := time.Now().UTC() + tests := []struct { + name string + item *Item + }{ + { + name: "All fields are filled", + item: &Item{ + ID: "id-1", + SpaceID: "space-1", + EnvID: "env-1", + CollectionID: "coll-1", + State: StateDraft, + CreatedRevAt: w.Add(time.Hour), + CreatedBy: "user-1", + CreatedAt: w, + UpdatedAt: w.Add(time.Hour), + UpdatedBy: "user-2", + Data: map[string]any{"a": "b", "c": "d", "x": nil}, + LocaleID: "ru", + Translations: map[string]map[string]interface{}{ + "ru": {"a": "B"}, + "en": nil, + }, + TranslationsIDs: []string{"ru", "en"}, + RevisionID: "rev-1", + RevisionDescription: "desc-1", + Permissions: PermissionsAllowAny, + SearchScore: 100.0, + Deleted: false, + Hidden: true, + Template: false, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.item, ItemFromProto(ItemToProto(tc.item))) + }) + } +} + +func TestItem_Encode_Decode(t *testing.T) { + w := time.Now().UTC() + tests := []struct { + name string + item *Item + }{ + { + name: "Data", + item: &Item{ + ID: "id-1", + SpaceID: "space-1", + EnvID: "env-1", + CollectionID: "coll-1", + State: StateDraft, + CreatedRevAt: w.Add(time.Hour), + CreatedBy: "user-1", + CreatedAt: w, + UpdatedAt: w.Add(time.Hour), + UpdatedBy: "user-2", + Data: map[string]any{ + "a": "text-a", + "b": 124.1, + "c": map[string]interface{}{"x": "y"}, + "d": []interface{}{"k", "l", "m"}, + }, + RevisionID: "rev-1", + RevisionDescription: "desc-1", + Permissions: PermissionsAllowAny, + SearchScore: 100.0, + Deleted: false, + Hidden: true, + Template: false, + }, + }, + { + name: "Data and Translations", + item: &Item{ + ID: "id-1", + SpaceID: "space-1", + EnvID: "env-1", + CollectionID: "coll-1", + State: StateDraft, + CreatedRevAt: w.Add(time.Hour), + CreatedBy: "user-1", + CreatedAt: w, + UpdatedAt: w.Add(time.Hour), + UpdatedBy: "user-2", + Data: map[string]any{ + "a": "text-a", + "b": 124.1, + "c": map[string]interface{}{"x": "y"}, + "d": []interface{}{"k", "l", "m"}, + }, + LocaleID: "ru", + Translations: map[string]map[string]interface{}{ + "ru": {"a": "ru-a"}, + "en": {"a": "en-a"}, + }, + TranslationsIDs: []string{"ru", "en"}, + RevisionID: "rev-1", + RevisionDescription: "desc-1", + Permissions: PermissionsAllowAny, + SearchScore: 100.0, + Deleted: false, + Hidden: false, + Template: false, + }, + }, + { + name: "Nil Translation", + item: &Item{ + ID: "id-1", + SpaceID: "space-1", + EnvID: "env-1", + CollectionID: "coll-1", + State: StateDraft, + CreatedRevAt: w.Add(time.Hour), + CreatedBy: "user-1", + CreatedAt: w, + UpdatedAt: w.Add(time.Hour), + UpdatedBy: "user-2", + Data: map[string]any{ + "a": "text-a", + "b": 124.1, + "c": map[string]interface{}{"x": "y"}, + "d": []interface{}{"k", "l", "m"}, + }, + LocaleID: "ru", + Translations: map[string]map[string]interface{}{ + "ru": {"a": "ru-a"}, + "en": nil, + }, + TranslationsIDs: []string{"ru", "en"}, + RevisionID: "rev-1", + RevisionDescription: "desc-1", + Permissions: PermissionsAllowAny, + SearchScore: 100.0, + Deleted: false, + Hidden: false, + Template: false, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + sch := schema.New( + "a", field.String(), + "b", field.Number(field.NumberFormatFloat), + "c", field.Object("x", field.String()), + "d", field.Array(field.String()), + ) + enc, err := tc.item.Encode(ctx, sch) + require.NoError(t, err) + dec, err := enc.Decode(ctx, sch) + require.NoError(t, err) + assert.Equal(t, tc.item, dec) + }) + } +} diff --git a/proto/items/items.pb.go b/proto/items/items.pb.go index c71dcb83bf0e0b1db89ab919391f79efa43bc6e4..8a6ed540cf9a1c9f21bd313f830d587d3468bd1d 100644 --- a/proto/items/items.pb.go +++ b/proto/items/items.pb.go @@ -384,14 +384,23 @@ type Item struct { Template bool `protobuf:"varint,21,opt,name=template,proto3" json:"template,omitempty"` Permissions *Permissions `protobuf:"bytes,22,opt,name=permissions,proto3" json:"permissions,omitempty"` SearchScore float64 `protobuf:"fixed64,23,opt,name=search_score,json=searchScore,proto3" json:"search_score,omitempty"` // релеватность элемента РїСЂРё полнотекстовом РїРѕРёСЃРєРµ - // РџСЂРё создании или обновлении идентификатор локали РІ котором создается запись, опционально. + // Рдентификатор локали полученной записи. + // РџСЂРё создании или обновлении идентификатор локали, РІ которой создается запись, опционально. // Если указан, то создается перевод для указанного языка, поле translations игнорируется - LocaleId string `protobuf:"bytes,100,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // идентификатор локали полученной записи + LocaleId string `protobuf:"bytes,100,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Позволяет одновременно установить/получить несколько переводов Рё производить манипуляции СЃ переводами // Ключами является идентификатор локали, значениями - данные переводы + // РџСЂРё обновлении РЅРµ РїСЂРѕРёСЃС…РѕРґРёС‚ валидация или модификация каждого РёР· переводов РІ соответствие СЃРѕ схемой, + // поэтому обновление через поле `translations` стоит выполнять СЃ аккуратностью + // Для удаления переводов реализована следующая логика: + // - {"lang":nil|{}} - СЃР±СЂРѕСЃ перевода для языка + // - {"lang":map{...}} - установка перевода для языка + // - {"lang":map{...}, "*":nil} - установка перевода для языка, СЃР±СЂРѕСЃ остальных переводов + // - {"*":nil} - СЃР±СЂРѕСЃ всех переводов Translations map[string]*structpb.Struct `protobuf:"bytes,12,rep,name=translations,proto3" json:"translations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - // СЃРїРёСЃРѕРє идентификаторов локалей для которых есть переводы - // соотвествует ключам РІ translations + // РЎРїРёСЃРѕРє идентификаторов локалей, для которых есть переводы + // Соотвествует СЃРїРёСЃРєСѓ переводов РІ translations, РїСЂРё получении записи всегда возвращается + // полный СЃРїРёСЃРѕРє. Невозможно обновить вручную: формируется системой TranslationsIds []string `protobuf:"bytes,101,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` } @@ -1044,13 +1053,15 @@ type FindOptions struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Options *common.FindOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` - Deleted bool `protobuf:"varint,3,opt,name=deleted,proto3" json:"deleted,omitempty"` - Regular bool `protobuf:"varint,4,opt,name=regular,proto3" json:"regular,omitempty"` - Hidden bool `protobuf:"varint,5,opt,name=hidden,proto3" json:"hidden,omitempty"` - Templates bool `protobuf:"varint,6,opt,name=templates,proto3" json:"templates,omitempty"` - LocaleId string `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если РЅРµ указан, то возвращаются данные для языка РїРѕ умолчанию - TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` // РЎРїРёСЃРѕРє идентификаторов переводов/локалей, которых должны быть включены РІ результат + Options *common.FindOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` + Deleted bool `protobuf:"varint,3,opt,name=deleted,proto3" json:"deleted,omitempty"` + Regular bool `protobuf:"varint,4,opt,name=regular,proto3" json:"regular,omitempty"` + Hidden bool `protobuf:"varint,5,opt,name=hidden,proto3" json:"hidden,omitempty"` + Templates bool `protobuf:"varint,6,opt,name=templates,proto3" json:"templates,omitempty"` + LocaleId string `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если РЅРµ указан, то возвращаются данные для языка РїРѕ умолчанию + // РЎРїРёСЃРѕРє идентификаторов переводов/локалей, которых должны быть включены РІ результат + // Р’РѕР·РјРѕР¶РЅРѕ указание '*' для получения всех переводов + TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` } func (x *FindOptions) Reset() { @@ -1186,8 +1197,10 @@ type GetPublishedOptions struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - LocaleId string `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если РЅРµ указан, то возвращаются данные для языка РїРѕ умолчанию - TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` // РЎРїРёСЃРѕРє идентификаторов переводов/локалей, которых должны быть включены РІ результат + LocaleId string `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если РЅРµ указан, то возвращаются данные для языка РїРѕ умолчанию + // РЎРїРёСЃРѕРє идентификаторов переводов/локалей, которых должны быть включены РІ результат + // Р’РѕР·РјРѕР¶РЅРѕ указание '*' для получения всех переводов + TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` } func (x *GetPublishedOptions) Reset() { @@ -1439,11 +1452,13 @@ type FindPublishedOptions struct { Options *common.FindOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` // string locale_id = 3; // язык для РїРѕРёСЃРєР° переводов. Если РЅРµ указан, то возвращаются данные для языка РїРѕ умолчанию - Regular bool `protobuf:"varint,4,opt,name=regular,proto3" json:"regular,omitempty"` - Hidden bool `protobuf:"varint,5,opt,name=hidden,proto3" json:"hidden,omitempty"` - Templates bool `protobuf:"varint,6,opt,name=templates,proto3" json:"templates,omitempty"` - LocaleId string `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если РЅРµ указан, то возвращаются данные для языка РїРѕ умолчанию - TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` // РЎРїРёСЃРѕРє идентификаторов переводов/локалей, которых должны быть включены РІ результат + Regular bool `protobuf:"varint,4,opt,name=regular,proto3" json:"regular,omitempty"` + Hidden bool `protobuf:"varint,5,opt,name=hidden,proto3" json:"hidden,omitempty"` + Templates bool `protobuf:"varint,6,opt,name=templates,proto3" json:"templates,omitempty"` + LocaleId string `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если РЅРµ указан, то возвращаются данные для языка РїРѕ умолчанию + // РЎРїРёСЃРѕРє идентификаторов переводов/локалей, которых должны быть включены РІ результат + // Р’РѕР·РјРѕР¶РЅРѕ указание '*' для получения всех переводов + TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` } func (x *FindPublishedOptions) Reset() {