feat: tags, ancestors

This commit is contained in:
Cristian Vidmar 2024-11-15 16:24:52 +01:00
parent d9813740eb
commit 2ed1e208e1
7 changed files with 213 additions and 32 deletions

View File

@ -149,14 +149,17 @@ retrieve referenced entries return a more flexible and useful _[]\*EntryReferenc
a reference you need a ContentTypeSys.
```go
(vo *CfPerson) GetParents() (parents []EntryReference, err error)
(ref *EntryReference) GetParents(cc *ContentfulClient) (parents []EntryReference, err error)
(vo *CfPerson) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error)
(ref *EntryReference) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error)
```
Return a slice of EntryReference objects that represent entries that reference the value object or the entry reference.
Note that in case of parents of an entry reference you need to pass a pointer to a ContentfulClient because
EntryReference objects are generic and can't carry any.
```go
HasAncestor(ctx context.Context, contentType string, entry entryOrRef, visited map[string]bool) (*EntryReference, error)
```
Returns the ancestor, if any, of the contentType specified for the entryOrRef. The function travels back in the references graph until it finds an ancestor, or no parent, or hits an infinite loop condition.
```go
(vo *CfPerson) GetPublishingStatus() string

View File

@ -102,9 +102,14 @@ type EntryReference struct {
ContentType string
ID string
VO interface{}
CC *ContentfulClient
FromField string
}
type entryOrRef interface {
GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error)
}
type BrokenReference struct {
ParentID string `json:"parentId"`
ParentType string `json:"parentType"`
@ -420,17 +425,56 @@ func (cc *ContentfulClient) GetContentTypeOfID(ctx context.Context, id string) (
return vo.Sys.ContentType.Sys.ID, nil
}
func (ref *EntryReference) GetParents(cc *ContentfulClient) (parents []EntryReference, err error) {
if ref == nil {
func (ref EntryReference) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error) {
if ref.ID == "" {
return nil, errors.New("GetParents: reference is nil")
}
var candidateParents []EntryReference
var cc *ContentfulClient
if ref.CC != nil {
cc = ref.CC
} else if ref.VO != nil {
v := reflect.ValueOf(ref.VO)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, errors.New("GetParents: no client available, tried reflection to no avail")
}
v = v.Elem()
}
field := v.FieldByName("CC")
if !field.IsValid() {
return nil, errors.New("GetParents: VO has no CC field")
}
if field.Kind() != reflect.Ptr {
return nil, errors.New("GetParents: VO field CC is not a pointer")
}
if field.IsNil() {
return nil, errors.New("GetParents: VO field CC is nil")
}
var ok bool
cc, ok = field.Interface().(*ContentfulClient)
if !ok {
return nil, errors.New("GetParents: VO field CC is not of type *ContentfulClient")
}
}
if cc == nil {
return nil, errors.New("GetParents: Contentful client is nil")
return nil, errors.New("GetParents: contentful client is nil")
}
if cc.Cache == nil {
return nil, errors.New("GetParents: only available in cached mode")
}
return cc.Cache.parentMap[ref.ID], nil
if len(contentType) == 0 {
return cc.Cache.parentMap[ref.ID], nil
} else {
cType := contentType[0]
for _, candidateParent := range cc.Cache.parentMap[ref.ID] {
if candidateParent.ContentType == cType {
candidateParents = append(candidateParents, candidateParent)
}
}
return candidateParents, nil
}
return nil, errors.New("GetParents: reference VO and CC are both nil")
}
func HtmlToRichText(htmlSrc string) *RichTextNode {
@ -537,7 +581,7 @@ func NewContentfulClient(ctx context.Context, spaceID string, clientMode ClientM
logLevel: logLevel,
optimisticPageSize: optimisticPageSize,
SpaceID: spaceID,
sync: clientMode == ClientModeCDA,
sync: false,
}
_, err = cc.Client.Spaces.Get(ctx, spaceID)
if err != nil {
@ -1321,6 +1365,13 @@ func (cc *ContentfulClient) syncCache(ctx context.Context, contentTypes []string
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "syncCache"}, LogInfo, InfoCacheUpdateQueued)
}
allTags, err := cc.getAllTags(ctx, false)
if err != nil {
return nil, nil, errors.New("syncCache failed for tags")
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.Cache.tags = allTags
cc.cacheMutex.sharedDataGcLock.Unlock()
var syncdEntries map[string][]string
var syncdAssets []string
for {
@ -2295,6 +2346,13 @@ func updateCacheForContentTypeAndEntity(ctx context.Context, cc *ContentfulClien
}
{{ end }}
}
allTags, err := cc.getAllTags(ctx, false)
if err != nil {
return fmt.Errorf("syncCache failed for tags: %s",err.Error())
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.Cache.tags = allTags
cc.cacheMutex.sharedDataGcLock.Unlock()
return nil
}
@ -2354,6 +2412,7 @@ func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, cont
ContentType: entry.Sys.ContentType.Sys.ID,
ID: entry.Sys.ID,
VO: &parentVO,
CC: cc,
})
{{ end }} }
}
@ -2471,3 +2530,24 @@ func (rawFields RawFields) GetChildIDs() (childIDs []string) {
}
return childIDs
}
func HasAncestor(ctx context.Context, contentType string, entry entryOrRef, visited map[string]bool) (*EntryReference, error) {
if visited == nil {
visited = make(map[string]bool)
}
parents, err := entry.GetParents(ctx)
if err != nil {
return nil, err
}
for _, parent := range parents {
if visited[parent.ID] {
continue
}
visited[parent.ID] = true
if parent.ContentType == contentType {
return &parent, nil
}
return HasAncestor(ctx, contentType, parent, visited)
}
return nil, nil
}

View File

@ -292,7 +292,7 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) {{ firstCap $field.ID }}(ctx con
}
return nil
}
{{ $field.ID }} = append({{ $field.ID }},&EntryReference{ContentType: contentType, ID: eachLocalized{{ firstCap $field.ID }}.Sys.ID, VO: referencedVO})
{{ $field.ID }} = append({{ $field.ID }},&EntryReference{ContentType: contentType, ID: eachLocalized{{ firstCap $field.ID }}.Sys.ID, VO: referencedVO, CC: vo.CC})
{{ end }}
}
}
@ -352,7 +352,7 @@ func (vo *Cf{{ firstCap $contentType.Sys.ID }}) {{ firstCap $field.ID }}(ctx con
}
return nil
}
return &EntryReference{ContentType: contentType, ID: localized{{ firstCap $field.ID }}.Sys.ID, VO: referencedVO}
return &EntryReference{ContentType: contentType, ID: localized{{ firstCap $field.ID }}.Sys.ID, VO: referencedVO, CC: vo.CC}
{{ end }}
}
return nil
@ -750,6 +750,7 @@ func (cc *ContentfulClient) cacheAll{{ firstCap $contentType.Sys.ID }}(ctx conte
addEntry(child.Sys.ID, EntryReference{ContentType: {{ $contentType.Sys.ID }}.Sys.ContentType.Sys.ID,
ID: {{ $contentType.Sys.ID }}.Sys.ID,
VO: {{ $contentType.Sys.ID }},
CC: cc,
FromField: "{{ $field.ID }}",
})
}
@ -763,6 +764,7 @@ func (cc *ContentfulClient) cacheAll{{ firstCap $contentType.Sys.ID }}(ctx conte
addEntry(child.Sys.ID, EntryReference{ContentType: {{ $contentType.Sys.ID }}.Sys.ContentType.Sys.ID,
ID: {{ $contentType.Sys.ID }}.Sys.ID,
VO: {{ $contentType.Sys.ID }},
CC: cc,
FromField: "{{ $field.ID }}",
})
}
@ -850,7 +852,7 @@ func (cc *ContentfulClient) cache{{ firstCap $contentType.Sys.ID }}ByID(ctx cont
if cc.Cache.parentMap[child.Sys.ID] == nil {
cc.Cache.parentMap[child.Sys.ID] = []EntryReference{}
}
newParentRef := EntryReference{ContentType: {{ $contentType.Sys.ID }}.Sys.ContentType.Sys.ID, ID: {{ $contentType.Sys.ID }}.Sys.ID, VO: {{ $contentType.Sys.ID }}, FromField: "{{ $field.ID }}" }
newParentRef := EntryReference{ContentType: {{ $contentType.Sys.ID }}.Sys.ContentType.Sys.ID, ID: {{ $contentType.Sys.ID }}.Sys.ID, VO: {{ $contentType.Sys.ID }}, CC: cc, FromField: "{{ $field.ID }}" }
newParentSlice := []EntryReference{}
for _, parent := range cc.Cache.parentMap[child.Sys.ID] {
if parent.ID != id {
@ -871,7 +873,7 @@ func (cc *ContentfulClient) cache{{ firstCap $contentType.Sys.ID }}ByID(ctx cont
if cc.Cache.parentMap[child.Sys.ID] == nil {
cc.Cache.parentMap[child.Sys.ID] = []EntryReference{}
}
newParentRef := EntryReference{ContentType: {{ $contentType.Sys.ID }}.Sys.ContentType.Sys.ID, ID: {{ $contentType.Sys.ID }}.Sys.ID, VO: {{ $contentType.Sys.ID }}, FromField: "{{ $field.ID }}" }
newParentRef := EntryReference{ContentType: {{ $contentType.Sys.ID }}.Sys.ContentType.Sys.ID, ID: {{ $contentType.Sys.ID }}.Sys.ID, VO: {{ $contentType.Sys.ID }}, CC: cc, FromField: "{{ $field.ID }}" }
newParentSlice := []EntryReference{}
for _, parent := range cc.Cache.parentMap[child.Sys.ID] {
if parent.ID != id {

View File

@ -72,7 +72,17 @@ func TestGetParents(t *testing.T) {
product, err := contentfulClient.GetProductByID(context.TODO(), "6dbjWqNd9SqccegcqYq224")
require.NoError(t, err)
brandRef := product.Brand(context.TODO())
brandParents, err := brandRef.GetParents(contentfulClient)
brandParents, err := brandRef.GetParents(context.TODO())
require.NoError(t, err)
require.Equal(t, 2, len(brandParents))
brandParents, err = brandRef.GetParents(context.TODO(), testapi.ContentTypeProduct)
require.NoError(t, err)
require.Equal(t, 2, len(brandParents))
brandParents, err = brandRef.GetParents(context.TODO(), testapi.ContentTypeCategory)
require.NoError(t, err)
require.Equal(t, 0, len(brandParents))
brandRef.CC = nil
brandParents, err = brandRef.GetParents(context.TODO())
require.NoError(t, err)
require.Equal(t, 2, len(brandParents))
}

View File

@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"reflect"
"regexp"
"strings"
"sync"
@ -104,9 +105,14 @@ type EntryReference struct {
ContentType string
ID string
VO interface{}
CC *ContentfulClient
FromField string
}
type entryOrRef interface {
GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error)
}
type BrokenReference struct {
ParentID string `json:"parentId"`
ParentType string `json:"parentType"`
@ -448,17 +454,56 @@ func (cc *ContentfulClient) GetContentTypeOfID(ctx context.Context, id string) (
return vo.Sys.ContentType.Sys.ID, nil
}
func (ref *EntryReference) GetParents(cc *ContentfulClient) (parents []EntryReference, err error) {
if ref == nil {
func (ref EntryReference) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error) {
if ref.ID == "" {
return nil, errors.New("GetParents: reference is nil")
}
var candidateParents []EntryReference
var cc *ContentfulClient
if ref.CC != nil {
cc = ref.CC
} else if ref.VO != nil {
v := reflect.ValueOf(ref.VO)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, errors.New("GetParents: no client available, tried reflection to no avail")
}
v = v.Elem()
}
field := v.FieldByName("CC")
if !field.IsValid() {
return nil, errors.New("GetParents: VO has no CC field")
}
if field.Kind() != reflect.Ptr {
return nil, errors.New("GetParents: VO field CC is not a pointer")
}
if field.IsNil() {
return nil, errors.New("GetParents: VO field CC is nil")
}
var ok bool
cc, ok = field.Interface().(*ContentfulClient)
if !ok {
return nil, errors.New("GetParents: VO field CC is not of type *ContentfulClient")
}
}
if cc == nil {
return nil, errors.New("GetParents: Contentful client is nil")
return nil, errors.New("GetParents: contentful client is nil")
}
if cc.Cache == nil {
return nil, errors.New("GetParents: only available in cached mode")
}
return cc.Cache.parentMap[ref.ID], nil
if len(contentType) == 0 {
return cc.Cache.parentMap[ref.ID], nil
} else {
cType := contentType[0]
for _, candidateParent := range cc.Cache.parentMap[ref.ID] {
if candidateParent.ContentType == cType {
candidateParents = append(candidateParents, candidateParent)
}
}
return candidateParents, nil
}
return nil, errors.New("GetParents: reference VO and CC are both nil")
}
func HtmlToRichText(htmlSrc string) *RichTextNode {
@ -565,7 +610,7 @@ func NewContentfulClient(ctx context.Context, spaceID string, clientMode ClientM
logLevel: logLevel,
optimisticPageSize: optimisticPageSize,
SpaceID: spaceID,
sync: clientMode == ClientModeCDA,
sync: false,
}
_, err = cc.Client.Spaces.Get(ctx, spaceID)
if err != nil {
@ -1349,6 +1394,13 @@ func (cc *ContentfulClient) syncCache(ctx context.Context, contentTypes []string
if cc.logFn != nil && cc.logLevel <= LogInfo {
cc.logFn(map[string]interface{}{"task": "syncCache"}, LogInfo, InfoCacheUpdateQueued)
}
allTags, err := cc.getAllTags(ctx, false)
if err != nil {
return nil, nil, errors.New("syncCache failed for tags")
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.Cache.tags = allTags
cc.cacheMutex.sharedDataGcLock.Unlock()
var syncdEntries map[string][]string
var syncdAssets []string
for {
@ -2399,6 +2451,13 @@ func updateCacheForContentTypeAndEntity(ctx context.Context, cc *ContentfulClien
}
}
allTags, err := cc.getAllTags(ctx, false)
if err != nil {
return fmt.Errorf("syncCache failed for tags: %s", err.Error())
}
cc.cacheMutex.sharedDataGcLock.Lock()
cc.Cache.tags = allTags
cc.cacheMutex.sharedDataGcLock.Unlock()
return nil
}
@ -2458,6 +2517,7 @@ func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, cont
ContentType: entry.Sys.ContentType.Sys.ID,
ID: entry.Sys.ID,
VO: &parentVO,
CC: cc,
})
case ContentTypeCategory:
@ -2476,6 +2536,7 @@ func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, cont
ContentType: entry.Sys.ContentType.Sys.ID,
ID: entry.Sys.ID,
VO: &parentVO,
CC: cc,
})
case ContentTypeProduct:
@ -2494,6 +2555,7 @@ func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, cont
ContentType: entry.Sys.ContentType.Sys.ID,
ID: entry.Sys.ID,
VO: &parentVO,
CC: cc,
})
}
}
@ -2611,3 +2673,24 @@ func (rawFields RawFields) GetChildIDs() (childIDs []string) {
}
return childIDs
}
func HasAncestor(ctx context.Context, contentType string, entry entryOrRef, visited map[string]bool) (*EntryReference, error) {
if visited == nil {
visited = make(map[string]bool)
}
parents, err := entry.GetParents(ctx)
if err != nil {
return nil, err
}
for _, parent := range parents {
if visited[parent.ID] {
continue
}
visited[parent.ID] = true
if parent.ContentType == contentType {
return &parent, nil
}
return HasAncestor(ctx, contentType, parent, visited)
}
return nil, nil
}

View File

@ -497,7 +497,7 @@ func (vo *CfProduct) Categories(ctx context.Context, locale ...Locale) []*EntryR
}
return nil
}
categories = append(categories, &EntryReference{ContentType: contentType, ID: eachLocalizedCategories.Sys.ID, VO: referencedVO})
categories = append(categories, &EntryReference{ContentType: contentType, ID: eachLocalizedCategories.Sys.ID, VO: referencedVO, CC: vo.CC})
case ContentTypeCategory:
referencedVO, err := vo.CC.GetCategoryByID(ctx, eachLocalizedCategories.Sys.ID)
@ -507,7 +507,7 @@ func (vo *CfProduct) Categories(ctx context.Context, locale ...Locale) []*EntryR
}
return nil
}
categories = append(categories, &EntryReference{ContentType: contentType, ID: eachLocalizedCategories.Sys.ID, VO: referencedVO})
categories = append(categories, &EntryReference{ContentType: contentType, ID: eachLocalizedCategories.Sys.ID, VO: referencedVO, CC: vo.CC})
case ContentTypeProduct:
referencedVO, err := vo.CC.GetProductByID(ctx, eachLocalizedCategories.Sys.ID)
@ -517,7 +517,7 @@ func (vo *CfProduct) Categories(ctx context.Context, locale ...Locale) []*EntryR
}
return nil
}
categories = append(categories, &EntryReference{ContentType: contentType, ID: eachLocalizedCategories.Sys.ID, VO: referencedVO})
categories = append(categories, &EntryReference{ContentType: contentType, ID: eachLocalizedCategories.Sys.ID, VO: referencedVO, CC: vo.CC})
}
}
@ -613,7 +613,7 @@ func (vo *CfProduct) Brand(ctx context.Context, locale ...Locale) *EntryReferenc
}
return nil
}
return &EntryReference{ContentType: contentType, ID: localizedBrand.Sys.ID, VO: referencedVO}
return &EntryReference{ContentType: contentType, ID: localizedBrand.Sys.ID, VO: referencedVO, CC: vo.CC}
case ContentTypeCategory:
referencedVO, err := vo.CC.GetCategoryByID(ctx, localizedBrand.Sys.ID)
@ -623,7 +623,7 @@ func (vo *CfProduct) Brand(ctx context.Context, locale ...Locale) *EntryReferenc
}
return nil
}
return &EntryReference{ContentType: contentType, ID: localizedBrand.Sys.ID, VO: referencedVO}
return &EntryReference{ContentType: contentType, ID: localizedBrand.Sys.ID, VO: referencedVO, CC: vo.CC}
case ContentTypeProduct:
referencedVO, err := vo.CC.GetProductByID(ctx, localizedBrand.Sys.ID)
@ -633,7 +633,7 @@ func (vo *CfProduct) Brand(ctx context.Context, locale ...Locale) *EntryReferenc
}
return nil
}
return &EntryReference{ContentType: contentType, ID: localizedBrand.Sys.ID, VO: referencedVO}
return &EntryReference{ContentType: contentType, ID: localizedBrand.Sys.ID, VO: referencedVO, CC: vo.CC}
}
return nil
@ -691,7 +691,7 @@ func (vo *CfProduct) SubProduct(ctx context.Context, locale ...Locale) *EntryRef
}
return nil
}
return &EntryReference{ContentType: contentType, ID: localizedSubProduct.Sys.ID, VO: referencedVO}
return &EntryReference{ContentType: contentType, ID: localizedSubProduct.Sys.ID, VO: referencedVO, CC: vo.CC}
case ContentTypeCategory:
referencedVO, err := vo.CC.GetCategoryByID(ctx, localizedSubProduct.Sys.ID)
@ -701,7 +701,7 @@ func (vo *CfProduct) SubProduct(ctx context.Context, locale ...Locale) *EntryRef
}
return nil
}
return &EntryReference{ContentType: contentType, ID: localizedSubProduct.Sys.ID, VO: referencedVO}
return &EntryReference{ContentType: contentType, ID: localizedSubProduct.Sys.ID, VO: referencedVO, CC: vo.CC}
case ContentTypeProduct:
referencedVO, err := vo.CC.GetProductByID(ctx, localizedSubProduct.Sys.ID)
@ -711,7 +711,7 @@ func (vo *CfProduct) SubProduct(ctx context.Context, locale ...Locale) *EntryRef
}
return nil
}
return &EntryReference{ContentType: contentType, ID: localizedSubProduct.Sys.ID, VO: referencedVO}
return &EntryReference{ContentType: contentType, ID: localizedSubProduct.Sys.ID, VO: referencedVO, CC: vo.CC}
}
return nil
@ -1438,6 +1438,7 @@ func (cc *ContentfulClient) cacheAllProduct(ctx context.Context, resultChan chan
addEntry(child.Sys.ID, EntryReference{ContentType: product.Sys.ContentType.Sys.ID,
ID: product.Sys.ID,
VO: product,
CC: cc,
FromField: "categories",
})
}
@ -1450,6 +1451,7 @@ func (cc *ContentfulClient) cacheAllProduct(ctx context.Context, resultChan chan
addEntry(child.Sys.ID, EntryReference{ContentType: product.Sys.ContentType.Sys.ID,
ID: product.Sys.ID,
VO: product,
CC: cc,
FromField: "brand",
})
}
@ -1461,6 +1463,7 @@ func (cc *ContentfulClient) cacheAllProduct(ctx context.Context, resultChan chan
addEntry(child.Sys.ID, EntryReference{ContentType: product.Sys.ContentType.Sys.ID,
ID: product.Sys.ID,
VO: product,
CC: cc,
FromField: "subProduct",
})
}
@ -1546,7 +1549,7 @@ func (cc *ContentfulClient) cacheProductByID(ctx context.Context, id string, ent
if cc.Cache.parentMap[child.Sys.ID] == nil {
cc.Cache.parentMap[child.Sys.ID] = []EntryReference{}
}
newParentRef := EntryReference{ContentType: product.Sys.ContentType.Sys.ID, ID: product.Sys.ID, VO: product, FromField: "categories"}
newParentRef := EntryReference{ContentType: product.Sys.ContentType.Sys.ID, ID: product.Sys.ID, VO: product, CC: cc, FromField: "categories"}
newParentSlice := []EntryReference{}
for _, parent := range cc.Cache.parentMap[child.Sys.ID] {
if parent.ID != id {
@ -1566,7 +1569,7 @@ func (cc *ContentfulClient) cacheProductByID(ctx context.Context, id string, ent
if cc.Cache.parentMap[child.Sys.ID] == nil {
cc.Cache.parentMap[child.Sys.ID] = []EntryReference{}
}
newParentRef := EntryReference{ContentType: product.Sys.ContentType.Sys.ID, ID: product.Sys.ID, VO: product, FromField: "brand"}
newParentRef := EntryReference{ContentType: product.Sys.ContentType.Sys.ID, ID: product.Sys.ID, VO: product, CC: cc, FromField: "brand"}
newParentSlice := []EntryReference{}
for _, parent := range cc.Cache.parentMap[child.Sys.ID] {
if parent.ID != id {
@ -1585,7 +1588,7 @@ func (cc *ContentfulClient) cacheProductByID(ctx context.Context, id string, ent
if cc.Cache.parentMap[child.Sys.ID] == nil {
cc.Cache.parentMap[child.Sys.ID] = []EntryReference{}
}
newParentRef := EntryReference{ContentType: product.Sys.ContentType.Sys.ID, ID: product.Sys.ID, VO: product, FromField: "subProduct"}
newParentRef := EntryReference{ContentType: product.Sys.ContentType.Sys.ID, ID: product.Sys.ID, VO: product, CC: cc, FromField: "subProduct"}
newParentSlice := []EntryReference{}
for _, parent := range cc.Cache.parentMap[child.Sys.ID] {
if parent.ID != id {

View File

@ -1,2 +1,2 @@
// gocontentful version: latest
// gocontentful version: 1.1.1
package testapi