mirror of
https://github.com/foomo/gocontentful.git
synced 2025-10-16 12:25:39 +00:00
2933 lines
87 KiB
Go
2933 lines
87 KiB
Go
// Code generated by https://github.com/foomo/gocontentful - DO NOT EDIT.
|
|
package testapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/foomo/contentful"
|
|
"golang.org/x/net/html"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
type cacheEntryMaps struct {
|
|
brand map[string]*CfBrand
|
|
category map[string]*CfCategory
|
|
product map[string]*CfProduct
|
|
}
|
|
|
|
type ClientMode string
|
|
|
|
type ContentfulCache struct {
|
|
assets assetCacheMap
|
|
contentTypes []string
|
|
entryMaps cacheEntryMaps
|
|
genericEntries map[string]*GenericEntry
|
|
idContentTypeMap map[string]string
|
|
parentMap map[string][]EntryReference
|
|
tags tagsCacheMap
|
|
}
|
|
|
|
type ContentfulCacheMutex struct {
|
|
fullCacheGcLock sync.RWMutex
|
|
sharedDataGcLock sync.RWMutex
|
|
assetsGcLock sync.RWMutex
|
|
tagGcLock sync.RWMutex
|
|
idContentTypeMapGcLock sync.RWMutex
|
|
parentMapGcLock sync.RWMutex
|
|
genericEntriesGcLock sync.RWMutex
|
|
brandGcLock sync.RWMutex
|
|
categoryGcLock sync.RWMutex
|
|
productGcLock sync.RWMutex
|
|
}
|
|
|
|
type assetCacheMap map[string]*contentful.Asset
|
|
|
|
type tagsCacheMap map[string]string
|
|
|
|
type ContentfulClient struct {
|
|
Cache *ContentfulCache
|
|
cacheInit bool
|
|
cacheMutex *ContentfulCacheMutex
|
|
cacheQueue chan struct{}
|
|
cacheDone chan struct{}
|
|
cacheUpdateTimeout int64
|
|
cacheWorkerOnce sync.Once
|
|
clientMode ClientMode
|
|
Client *contentful.Contentful
|
|
locales []Locale
|
|
logFn func(
|
|
fields map[string]interface{},
|
|
level int,
|
|
args ...interface{},
|
|
)
|
|
logLevel int
|
|
optimisticPageSize uint16
|
|
SpaceID string
|
|
offline bool
|
|
offlineTemp offlineTemp
|
|
sync bool
|
|
syncToken string
|
|
textJanitor bool
|
|
}
|
|
|
|
type offlineTemp struct {
|
|
Entries []contentful.Entry `json:"entries"`
|
|
Assets []contentful.Asset `json:"assets"`
|
|
Tags []contentful.Tag `json:"tags"`
|
|
}
|
|
|
|
type ContentTypeResult struct {
|
|
EntryID string
|
|
ContentType string
|
|
References map[string][]EntryReference
|
|
}
|
|
|
|
type ContentTypeInfo struct {
|
|
ContentType string
|
|
Title string
|
|
Description string
|
|
}
|
|
|
|
type ContentTypeInfoMap map[string]ContentTypeInfo
|
|
|
|
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"`
|
|
FromField string `json:"fromField"`
|
|
ChildID string `json:"childId"`
|
|
}
|
|
|
|
type ImageResolverFunc func(assetID string, locale Locale) (attrs map[string]string, customHTML string, resolveError error)
|
|
type EntryLinkResolverFunc func(entryID string, locale Locale) (resolvedAttrs map[string]string, resolveError error)
|
|
type LinkResolverFunc func(url string) (resolvedAttrs map[string]string, resolveError error)
|
|
type EmbeddedEntryResolverFunc func(entryID string, locale Locale) (htmlSnippet string, resolveError error)
|
|
|
|
type Locale string
|
|
|
|
const (
|
|
ClientModeCDA ClientMode = "CDA"
|
|
ClientModeCPA ClientMode = "CPA"
|
|
ClientModeCMA ClientMode = "CMA"
|
|
)
|
|
|
|
const (
|
|
LogDebug = 0
|
|
LogInfo = 1
|
|
LogWarn = 2
|
|
LogError = 3
|
|
)
|
|
|
|
const SpaceLocaleGerman Locale = "de"
|
|
const SpaceLocaleFrench Locale = "fr"
|
|
|
|
var SpaceLocales = []Locale{
|
|
"de",
|
|
"fr",
|
|
}
|
|
|
|
const DefaultLocale Locale = SpaceLocaleGerman
|
|
|
|
var localeFallback = map[Locale]Locale{SpaceLocaleGerman: "", SpaceLocaleFrench: ""}
|
|
|
|
const (
|
|
assetPageSize = 1000
|
|
assetWorkerType = "_asset"
|
|
tagWorkerType = "_tag"
|
|
)
|
|
|
|
const cacheUpdateConcurrency = 4
|
|
|
|
const (
|
|
sysTypeEntry = "Entry"
|
|
sysTypeAsset = "Asset"
|
|
sysTypeDeletedEntry = "DeletedEntry"
|
|
sysTypeDeletedAsset = "DeletedAsset"
|
|
)
|
|
|
|
var (
|
|
ErrLocaleUnsupported = errors.New("locale not supported by this space")
|
|
ErrNotSet = errors.New("field value not set")
|
|
ErrNotSetNoFallback = errors.New("field value not set and no fallback locale available")
|
|
ErrRefNotIncludes = errors.New("referenced entry not found in includes")
|
|
ErrNoTypeOfRefEntry = errors.New("couldn't get contentType of referenced entry")
|
|
ErrNoTypeOfRefAsset = errors.New("couldn't get contentType of referenced asset")
|
|
InfoUpdatedEntityCache = "updated cache for entity"
|
|
InfoCachedAllEntries = "cached all entries of content type"
|
|
InfoCachedAllAssets = "cached all assets"
|
|
InfoCachedAllTags = "cached all tags"
|
|
InfoFallingBackToFile = "gonna use a local file"
|
|
InfoLoadingFromFile = "loading space from local file"
|
|
InfoCacheIsNil = "contentful cache is nil"
|
|
InfoCacheWorkerStart = "contentful cache worker starting"
|
|
InfoCacheUpdateQueued = "contentful cache update queued"
|
|
InfoCacheUpdateCanceled = "contentful cache update canceled"
|
|
InfoCacheUpdateDone = "contentful cache update returning"
|
|
InfoCacheUpdateSkipped = "contentful cache update skipped, already one in the queue"
|
|
InfoCacheSyncOp = "contentful cache sync op done"
|
|
InfoOfflineEntitiesLoaded = "downloaded entries and assets from offline file"
|
|
InfoPreservingExistingCache = "could not connect for cache update, preserving the existing cache"
|
|
InfoUpdateCacheTime = "space caching done, time recorded"
|
|
ErrorEnvironmentSetToMaster = "environment was empty string, set to master"
|
|
ErrorEntryIsNil = "entry is nil"
|
|
ErrorEntrySysIsNil = "entry.Sys is nil"
|
|
ErrorEntryNotFound = "entry not found"
|
|
ErrorEntrySysContentTypeIsNil = "entry.Sys.ContentType is nil"
|
|
ErrorEntrySysContentTypeSysIsNil = "entry.Sys.ContentType.Sys is nil"
|
|
ErrorEntryCachingFailed = "entry caching failed"
|
|
)
|
|
|
|
var spaceContentTypes = []string{ContentTypeBrand, ContentTypeCategory, ContentTypeProduct}
|
|
var SpaceContentTypeInfoMap = ContentTypeInfoMap{
|
|
"brand": ContentTypeInfo{
|
|
ContentType: "brand",
|
|
Title: "Brand",
|
|
Description: "",
|
|
},
|
|
"category": ContentTypeInfo{
|
|
ContentType: "category",
|
|
Title: "Category",
|
|
Description: "",
|
|
},
|
|
"product": ContentTypeInfo{
|
|
ContentType: "product",
|
|
Title: "Product",
|
|
Description: "",
|
|
},
|
|
}
|
|
|
|
func (cc *ContentfulClient) BrokenReferences() (brokenReferences []BrokenReference) {
|
|
if cc.Cache == nil || cc.cacheMutex == nil {
|
|
return
|
|
}
|
|
cc.cacheMutex.parentMapGcLock.Lock()
|
|
defer cc.cacheMutex.parentMapGcLock.Unlock()
|
|
cc.cacheMutex.idContentTypeMapGcLock.Lock()
|
|
defer cc.cacheMutex.idContentTypeMapGcLock.Unlock()
|
|
for childID, parents := range cc.Cache.parentMap {
|
|
if _, okGotEntry := cc.Cache.idContentTypeMap[childID]; !okGotEntry {
|
|
for _, parent := range parents {
|
|
brokenReferences = append(brokenReferences, BrokenReference{
|
|
ParentID: parent.ID,
|
|
ParentType: parent.ContentType,
|
|
FromField: parent.FromField,
|
|
ChildID: childID,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (cc *ContentfulClient) CacheHasContentType(contentTypeID string) bool {
|
|
if cc.Cache == nil {
|
|
return false
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
defer cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
for _, cachedContentType := range cc.Cache.contentTypes {
|
|
if cachedContentType == contentTypeID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (cc *ContentfulClient) ClientStats() {
|
|
if cc == nil {
|
|
cc.logFn(nil, LogWarn, "ClientStats: no client available")
|
|
return
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
defer cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
if cc.logFn != nil {
|
|
fieldsMap := map[string]interface{}{
|
|
"space ID": cc.SpaceID,
|
|
"environment": cc.Client.Environment,
|
|
"clientMode": cc.clientMode,
|
|
"contentTypes": strings.Join(cc.Cache.contentTypes, ","),
|
|
"locales": cc.locales,
|
|
"cached": cc.cacheInit,
|
|
}
|
|
if cc.cacheInit {
|
|
fieldsMap["cache asset count"] = len(cc.Cache.assets)
|
|
fieldsMap["cache entry count"] = len(cc.Cache.idContentTypeMap)
|
|
fieldsMap["cache parentMap children"] = len(cc.Cache.parentMap)
|
|
cc.cacheMutex.parentMapGcLock.RLock()
|
|
defer cc.cacheMutex.parentMapGcLock.RUnlock()
|
|
referenceCount := 0
|
|
for _, parents := range cc.Cache.parentMap {
|
|
referenceCount += len(parents)
|
|
}
|
|
fieldsMap["cache parentMap parents"] = referenceCount
|
|
cc.cacheMutex.genericEntriesGcLock.RLock()
|
|
defer cc.cacheMutex.genericEntriesGcLock.RUnlock()
|
|
fieldsMap["cache genericEntries"] = len(cc.Cache.genericEntries)
|
|
}
|
|
cc.logFn(fieldsMap, LogInfo, "Contentful ClientStats")
|
|
}
|
|
}
|
|
|
|
func (ref ContentfulReferencedEntry) ContentType() (contentType string) {
|
|
return ref.Entry.Sys.ContentType.Sys.ID
|
|
}
|
|
|
|
func (cc *ContentfulClient) UpsertAsset(ctx context.Context, asset *contentful.Asset) error {
|
|
if cc == nil || cc.Client == nil {
|
|
return errors.New("UpsertAsset: No client available")
|
|
}
|
|
if cc.clientMode != ClientModeCMA {
|
|
return errors.New("UpsertAsset: Only available in ClientModeCMA")
|
|
}
|
|
if asset.Fields != nil && asset.Fields.File != nil {
|
|
for _, locale := range SpaceLocales {
|
|
if asset.Fields.File[string(locale)] != nil {
|
|
asset.Fields.File[string(locale)].URL = strings.ReplaceAll(asset.Fields.File[string(locale)].URL, "https:", "")
|
|
}
|
|
}
|
|
}
|
|
errUpsert := cc.Client.Assets.Upsert(ctx, cc.SpaceID, asset)
|
|
if errUpsert != nil && !strings.Contains(errUpsert.Error(), "Not upserted") {
|
|
return errUpsert
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) PublishAsset(ctx context.Context, asset *contentful.Asset) error {
|
|
if cc == nil || cc.Client == nil {
|
|
return errors.New("PublishAsset: No client available")
|
|
}
|
|
if cc.clientMode != ClientModeCMA {
|
|
return errors.New("PublishAsset: Only available in ClientModeCMA")
|
|
}
|
|
errPublish := cc.Client.Assets.Publish(ctx, cc.SpaceID, asset)
|
|
if errPublish != nil && !strings.Contains(errPublish.Error(), "Not published") {
|
|
return errPublish
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetAssetPublishingStatus(asset *contentful.Asset) string {
|
|
if asset == nil {
|
|
return ""
|
|
}
|
|
if asset.Sys.PublishedVersion == 0 {
|
|
return StatusDraft
|
|
}
|
|
if asset.Sys.Version-asset.Sys.PublishedVersion == 1 {
|
|
return StatusPublished
|
|
}
|
|
return StatusChanged
|
|
}
|
|
|
|
func (cc *ContentfulClient) UpdateAsset(ctx context.Context, asset *contentful.Asset) (err error) {
|
|
if asset == nil {
|
|
return errors.New("UpdateAsset: Generic Entry is nil")
|
|
}
|
|
if cc == nil {
|
|
return errors.New("UpdateAsset: Generic Entry has nil Contentful client")
|
|
}
|
|
if cc.clientMode != ClientModeCMA {
|
|
return errors.New("UpdateAsset: Generic Entry not in ClientModeCMA")
|
|
}
|
|
publishingStatus := GetAssetPublishingStatus(asset)
|
|
err = cc.UpsertAsset(ctx, asset)
|
|
if err != nil {
|
|
return fmt.Errorf("UpdateAsset: upsert operation failed: %w", err)
|
|
}
|
|
if publishingStatus == StatusPublished {
|
|
err = cc.PublishAsset(ctx, asset)
|
|
if err != nil {
|
|
return fmt.Errorf("UpdateAsset: publish operation failed: %w", err)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (cc *ContentfulClient) DeleteAsset(ctx context.Context, asset *contentful.Asset) error {
|
|
if cc == nil || cc.Client == nil {
|
|
return errors.New("DeleteAsset: No client available")
|
|
}
|
|
if cc.clientMode != ClientModeCMA {
|
|
return errors.New("DeleteAsset: Only available in ClientModeCMA")
|
|
}
|
|
errUnpublish := cc.Client.Assets.Unpublish(ctx, cc.SpaceID, asset)
|
|
if errUnpublish != nil && !strings.Contains(errUnpublish.Error(), "Not published") {
|
|
return errUnpublish
|
|
}
|
|
errDelete := cc.Client.Assets.Delete(ctx, cc.SpaceID, asset)
|
|
if errDelete != nil {
|
|
return errDelete
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) DeleteAssetFromCache(key string) error {
|
|
return cc.deleteAssetFromCache(key)
|
|
}
|
|
|
|
func (cc *ContentfulClient) DisableTextJanitor() {
|
|
cc.textJanitor = false
|
|
}
|
|
|
|
func (cc *ContentfulClient) EnableTextJanitor() {
|
|
cc.textJanitor = true
|
|
}
|
|
|
|
func (cc *ContentfulClient) GetAllAssets(ctx context.Context) (map[string]*contentful.Asset, error) {
|
|
return cc.getAllAssets(ctx, true)
|
|
}
|
|
|
|
func (cc *ContentfulClient) GetAssetsByTag(ctx context.Context, tagName string) (vos []*contentful.Asset, err error) {
|
|
if cc == nil || cc.Client == nil {
|
|
return nil, errors.New("GetAssetsByTag: No client available")
|
|
}
|
|
if !cc.cacheInit {
|
|
return nil, errors.New("GetAssetsByTag: only available with cache")
|
|
}
|
|
tags, err := cc.getAllTags(ctx, true)
|
|
if err != nil {
|
|
return nil, errors.New("GetAssetsByTag could not get tags from cache: " + err.Error())
|
|
}
|
|
cc.cacheMutex.assetsGcLock.RLock()
|
|
defer cc.cacheMutex.assetsGcLock.RUnlock()
|
|
if _, tagExists := tags[tagName]; !tagExists {
|
|
return nil, nil
|
|
}
|
|
tagID := tags[tagName]
|
|
for _, vo := range cc.Cache.assets {
|
|
for _, voTag := range vo.Metadata.Tags {
|
|
if voTag.Sys.ID == tagID {
|
|
vos = append(vos, vo)
|
|
}
|
|
}
|
|
}
|
|
return vos, nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) GetAllTags(ctx context.Context) (map[string]string, error) {
|
|
return cc.getAllTags(ctx, true)
|
|
}
|
|
|
|
func (cc *ContentfulClient) GetAssetByID(ctx context.Context, id string, forceNoCache ...bool) (*contentful.Asset, error) {
|
|
if cc == nil || cc.Client == nil {
|
|
return nil, errors.New("GetAssetByID: No client available")
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
cacheInit := cc.cacheInit
|
|
cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
cc.cacheMutex.assetsGcLock.Lock()
|
|
defer cc.cacheMutex.assetsGcLock.Unlock()
|
|
if cacheInit && cc.Cache.assets != nil && (len(forceNoCache) == 0 || !forceNoCache[0]) {
|
|
asset, okAsset := cc.Cache.assets[id]
|
|
if okAsset {
|
|
return asset, nil
|
|
} else {
|
|
return nil, errors.New("GetAssetByID: not found")
|
|
}
|
|
}
|
|
col := cc.Client.Assets.List(ctx, cc.SpaceID)
|
|
col.Query.Locale("*").Equal("sys.id", id)
|
|
_, err := col.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(col.Items) == 0 {
|
|
return nil, errors.New("GetAssetByID: Not found " + id)
|
|
}
|
|
item := col.Items[0]
|
|
asset := contentful.Asset{}
|
|
byt, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(byt, &asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} {
|
|
if _, ok := asset.Fields.File[string(loc)]; ok {
|
|
asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL
|
|
}
|
|
}
|
|
return &asset, nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) GetContentTypeOfID(ctx context.Context, id string) (string, error) {
|
|
if cc == nil || cc.Client == nil {
|
|
return "", errors.New("GetContentTypeOfID: No client available")
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
cacheInit := cc.cacheInit
|
|
cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
if cacheInit {
|
|
okVo := false
|
|
|
|
cc.cacheMutex.brandGcLock.Lock()
|
|
_, okVo = cc.Cache.entryMaps.brand[id]
|
|
cc.cacheMutex.brandGcLock.Unlock()
|
|
if okVo {
|
|
return ContentTypeBrand, nil
|
|
}
|
|
|
|
cc.cacheMutex.categoryGcLock.Lock()
|
|
_, okVo = cc.Cache.entryMaps.category[id]
|
|
cc.cacheMutex.categoryGcLock.Unlock()
|
|
if okVo {
|
|
return ContentTypeCategory, nil
|
|
}
|
|
|
|
cc.cacheMutex.productGcLock.Lock()
|
|
_, okVo = cc.Cache.entryMaps.product[id]
|
|
cc.cacheMutex.productGcLock.Unlock()
|
|
if okVo {
|
|
return ContentTypeProduct, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("GetContentTypeOfID: %s Not found in cache", id)
|
|
}
|
|
col := cc.Client.Entries.List(ctx, cc.SpaceID)
|
|
col.Query.Include(0).Equal("sys.id", id)
|
|
_, err := col.GetAll()
|
|
if err != nil {
|
|
return "", errors.New("GetContentTypeOfID: " + err.Error())
|
|
}
|
|
if len(col.Items) == 0 {
|
|
return "", fmt.Errorf("GetContentTypeOfID: %s Not found online", id)
|
|
}
|
|
var vo genericEntryNoFields
|
|
byteArray, _ := json.Marshal(col.Items[0])
|
|
err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&vo)
|
|
if err != nil {
|
|
return "", errors.New("GetContentTypeOfID: " + err.Error())
|
|
}
|
|
return vo.Sys.ContentType.Sys.ID, nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) Locales() []Locale {
|
|
return cc.locales
|
|
}
|
|
|
|
func (cc *ContentfulClient) LocalesAsStrings() []string {
|
|
locales := []string{}
|
|
for _, locale := range cc.locales {
|
|
locales = append(locales, string(locale))
|
|
}
|
|
return locales
|
|
}
|
|
|
|
func (vo *GenericEntry) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error) {
|
|
if vo == nil {
|
|
return nil, errors.New("GetParents: Value Object is nil")
|
|
}
|
|
if vo.CC == nil {
|
|
return nil, errors.New("GetParents: Value Object has no Contentful Client set")
|
|
}
|
|
return commonGetParents(ctx, vo.CC, vo.Sys.ID, contentType)
|
|
}
|
|
|
|
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")
|
|
}
|
|
if cc.Cache == nil {
|
|
return nil, errors.New("GetParents: only available in cached mode")
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
func GetOrInheritFieldValue(ctx context.Context, contentfulShop *ContentfulClient, entryID string, field string, parentContentTypes []string, locale Locale) (any, error) {
|
|
entry, err := contentfulShop.GetGenericEntry(entryID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get entry %s: %v", entryID, err)
|
|
}
|
|
|
|
// Try to get the field value from the current entry first
|
|
val, err := entry.FieldAsAny(field, locale)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
|
|
return inheritFieldValueRecursive(ctx, contentfulShop, entry, field, parentContentTypes, locale, make(map[string]bool))
|
|
}
|
|
|
|
func inheritFieldValueRecursive(ctx context.Context, contentfulShop *ContentfulClient, entry *GenericEntry, field string, parentContentTypes []string, locale Locale, visited map[string]bool) (any, error) {
|
|
if visited[entry.Sys.ID] {
|
|
return nil, fmt.Errorf("circular reference detected for entry %s", entry.Sys.ID)
|
|
}
|
|
visited[entry.Sys.ID] = true
|
|
|
|
parentRefs, err := commonGetParents(ctx, entry.CC, entry.Sys.ID, parentContentTypes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get parents for entry %s: %v", entry.Sys.ID, err)
|
|
}
|
|
|
|
for _, parentRef := range parentRefs {
|
|
parentEntry, err := entry.CC.GetGenericEntry(parentRef.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get parent entry %s: %v", parentRef.ID, err)
|
|
}
|
|
|
|
val, err := parentEntry.FieldAsAny(field, locale)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
|
|
inheritedVal, err := inheritFieldValueRecursive(ctx, contentfulShop, parentEntry, field, parentContentTypes, locale, visited)
|
|
if err == nil {
|
|
return inheritedVal, nil
|
|
}
|
|
}
|
|
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func HtmlToRichText(htmlSrc string) *RichTextNode {
|
|
htmlClean := strings.TrimSpace(htmlSrc)
|
|
htmlClean = strings.ReplaceAll(htmlClean, ` `, ` `)
|
|
htmlClean = strings.ReplaceAll(htmlClean, "&", "&")
|
|
htmlClean = strings.ReplaceAll(htmlClean, "\t", "")
|
|
htmlClean = regexp.MustCompile(`[\s]{2,}`).ReplaceAllString(htmlClean, " ")
|
|
re := regexp.MustCompile(`>([^\n])`)
|
|
htmlClean = re.ReplaceAllString(htmlClean, fmt.Sprintf(">\n"+`$1`))
|
|
re = regexp.MustCompile(`(.{1})<`)
|
|
htmlClean = re.ReplaceAllString(htmlClean, `$1`+"\n<")
|
|
htmlLines := strings.Split(htmlClean, "\n")
|
|
rtnd := &RichTextNode{
|
|
NodeType: RichTextNodeDocument,
|
|
}
|
|
var isBasic bool
|
|
rtnd.Content, _, isBasic = richTextHtmlLinesToNode(htmlLines, 0, "", nil, true)
|
|
if isBasic {
|
|
basicRich := &RichTextNode{
|
|
NodeType: RichTextNodeDocument,
|
|
Content: rtnd.Content,
|
|
}
|
|
return basicRich
|
|
}
|
|
return rtnd
|
|
}
|
|
|
|
func NewAssetFromURL(id string, uploadUrl string, imageFileType string, title string, locale ...Locale) *contentful.Asset {
|
|
autoID := fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Millisecond))
|
|
if id == "" {
|
|
id = autoID
|
|
}
|
|
if title == "" {
|
|
title = id
|
|
}
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return nil
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
asset := &contentful.Asset{
|
|
Sys: &contentful.Sys{
|
|
ID: id,
|
|
},
|
|
Fields: &contentful.FileFields{
|
|
File: map[string]*contentful.File{
|
|
string(loc): {
|
|
UploadURL: uploadUrl,
|
|
ContentType: imageFileType,
|
|
Name: id,
|
|
},
|
|
},
|
|
Title: map[string]string{
|
|
string(loc): title,
|
|
},
|
|
},
|
|
}
|
|
return asset
|
|
}
|
|
|
|
func NewContentfulClient(ctx context.Context, spaceID string, clientMode ClientMode, clientKey string, optimisticPageSize uint16, logFn func(fields map[string]interface{}, level int, args ...interface{}), logLevel int, debug bool) (*ContentfulClient, error) {
|
|
if spaceID == "" {
|
|
return nil, errors.New("NewContentfulClient: SpaceID cannot be empty")
|
|
}
|
|
if clientMode != ClientModeCMA && clientMode != ClientModeCPA && clientMode != ClientModeCDA {
|
|
return nil, errors.New("NewContentfulClient: clientMode not supported")
|
|
}
|
|
if optimisticPageSize < 10 {
|
|
return nil, errors.New("NewContentfulClient: optimisticPageSize must be 10 or bigger")
|
|
}
|
|
if logLevel < 0 || logLevel > 3 {
|
|
return nil, errors.New("NewContentfulClient: logLevel must be between 0 and 3")
|
|
}
|
|
if clientKey == "" {
|
|
return nil, errors.New("NewContentfulClient: Please provide an API key")
|
|
}
|
|
apiClient, err := getContentfulAPIClient(clientMode, clientKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
apiClient.Debug = debug
|
|
cc := &ContentfulClient{
|
|
clientMode: clientMode,
|
|
Client: apiClient,
|
|
Cache: &ContentfulCache{
|
|
assets: assetCacheMap{},
|
|
contentTypes: []string{},
|
|
entryMaps: cacheEntryMaps{},
|
|
genericEntries: map[string]*GenericEntry{},
|
|
idContentTypeMap: map[string]string{},
|
|
parentMap: map[string][]EntryReference{},
|
|
},
|
|
cacheMutex: &ContentfulCacheMutex{},
|
|
cacheQueue: make(chan struct{}, 1),
|
|
cacheDone: make(chan struct{}, 1),
|
|
cacheUpdateTimeout: 120,
|
|
locales: []Locale{SpaceLocaleGerman, SpaceLocaleFrench},
|
|
logFn: logFn,
|
|
logLevel: logLevel,
|
|
optimisticPageSize: optimisticPageSize,
|
|
SpaceID: spaceID,
|
|
sync: false,
|
|
}
|
|
_, err = cc.Client.Spaces.Get(ctx, spaceID)
|
|
if err != nil {
|
|
_, ok := err.(contentful.NotFoundError)
|
|
if ok {
|
|
return nil, errors.New("NewContentfulClient: That is not the space you're looking for")
|
|
}
|
|
return nil, errors.New("NewContentfulClient: " + err.Error())
|
|
}
|
|
return cc, nil
|
|
}
|
|
|
|
func NewOfflineContentfulClient(file []byte, logFn func(fields map[string]interface{}, level int, args ...interface{}), logLevel int, cacheAssets bool, textJanitor bool) (*ContentfulClient, error) {
|
|
offlineTemp, err := getOfflineSpaceFromFile(file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("NewOfflineContentfulClient could not parse space export file: %v", err)
|
|
}
|
|
cc := &ContentfulClient{
|
|
clientMode: ClientModeCDA,
|
|
Client: contentful.NewCDA(""),
|
|
Cache: &ContentfulCache{
|
|
contentTypes: []string{},
|
|
idContentTypeMap: map[string]string{},
|
|
genericEntries: map[string]*GenericEntry{},
|
|
parentMap: map[string][]EntryReference{},
|
|
},
|
|
cacheMutex: &ContentfulCacheMutex{},
|
|
cacheQueue: make(chan struct{}, 1),
|
|
cacheDone: make(chan struct{}, 1),
|
|
cacheUpdateTimeout: 120,
|
|
locales: []Locale{
|
|
SpaceLocaleGerman,
|
|
SpaceLocaleFrench,
|
|
},
|
|
logFn: logFn,
|
|
logLevel: logLevel,
|
|
SpaceID: "OFFLINE",
|
|
offline: true,
|
|
offlineTemp: *offlineTemp,
|
|
textJanitor: textJanitor,
|
|
}
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"entries": len(offlineTemp.Entries), "assets": len(offlineTemp.Assets)}, LogInfo, InfoLoadingFromFile)
|
|
}
|
|
_, _, err = cc.UpdateCache(context.Background(), spaceContentTypes, cacheAssets)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("NewOfflineContentfulClient could not cache offline space: %v", err)
|
|
}
|
|
return cc, nil
|
|
}
|
|
|
|
func RichTextToHtml(rt interface{}, linkResolver LinkResolverFunc, entryLinkResolver EntryLinkResolverFunc, imageResolver ImageResolverFunc, embeddedEntryResolver EmbeddedEntryResolverFunc, locale Locale) (string, error) {
|
|
w := bytes.NewBuffer([]byte{})
|
|
node := &RichTextGenericNode{}
|
|
byt, err := json.Marshal(rt)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = json.Unmarshal(byt, node)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = node.richTextRenderHTML(w, linkResolver, entryLinkResolver, imageResolver, embeddedEntryResolver, locale)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
out := w.String()
|
|
if out == "<p></p>" {
|
|
return "", nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func RichTextToPlainText(rt interface{}, locale Locale) (string, error) {
|
|
htmlStr, err := RichTextToHtml(rt, nil, nil, nil, nil, locale)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
doc, err := html.Parse(strings.NewReader(htmlStr))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var f func(*html.Node)
|
|
text := ""
|
|
f = func(n *html.Node) {
|
|
if n.Type == html.TextNode {
|
|
text += n.Data
|
|
}
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
f(c)
|
|
}
|
|
}
|
|
f(doc)
|
|
return strings.TrimSpace(text), nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) GetGenericEntry(entryID string) (*GenericEntry, error) {
|
|
if cc.Cache == nil {
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"task": "GetGenericEntry", "entryId": entryID}, LogWarn, InfoCacheIsNil)
|
|
}
|
|
return nil, errors.New(InfoCacheIsNil)
|
|
}
|
|
if _, ok := cc.Cache.genericEntries[entryID]; !ok {
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"task": "GetGenericEntry", "entryId": entryID}, LogWarn, ErrorEntryNotFound)
|
|
}
|
|
return nil, errors.New(ErrorEntryNotFound)
|
|
}
|
|
return cc.Cache.genericEntries[entryID], nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) GetAllGenericEntries() (map[string]*GenericEntry, error) {
|
|
if cc.Cache == nil {
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"task": "GetAllGenericEntries"}, LogWarn, InfoCacheIsNil)
|
|
}
|
|
return nil, errors.New(InfoCacheIsNil)
|
|
}
|
|
return cc.Cache.genericEntries, nil
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) FieldAsString(fieldName string, locale ...Locale) (string, error) {
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return "", ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
if _, ok := genericEntry.RawFields[fieldName]; !ok {
|
|
return "", ErrNotSet
|
|
}
|
|
switch field := genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]interface{}:
|
|
var fieldLoc any
|
|
if _, ok := field[string(loc)]; ok {
|
|
fieldLoc = field[string(loc)]
|
|
} else {
|
|
fieldLoc = field[string(DefaultLocale)]
|
|
}
|
|
switch fieldLocStr := fieldLoc.(type) {
|
|
case string:
|
|
return fieldLocStr, nil
|
|
default:
|
|
return "", ErrNotSet
|
|
}
|
|
default:
|
|
return "", ErrNotSet
|
|
}
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) InheritAsString(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (string, error) {
|
|
val, err := genericEntry.FieldAsString(fieldName, locale...)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(parentRefs) > 0 {
|
|
for _, parentRef := range parentRefs {
|
|
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parentVal, err := genericParent.FieldAsString(fieldName, locale...)
|
|
if err != nil {
|
|
return "", err
|
|
} else {
|
|
return parentVal, nil
|
|
}
|
|
}
|
|
}
|
|
return "", ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) FieldAsStringSlice(fieldName string, locale ...Locale) ([]string, error) {
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return nil, ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
if _, ok := genericEntry.RawFields[fieldName]; !ok {
|
|
return nil, ErrNotSet
|
|
}
|
|
switch field := genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]any:
|
|
var fieldLoc any
|
|
if _, ok := field[string(loc)]; ok {
|
|
fieldLoc = field[string(loc)]
|
|
} else {
|
|
fieldLoc = field[string(DefaultLocale)]
|
|
}
|
|
if fieldLoc != nil {
|
|
switch fieldLoc.(type) {
|
|
case []any:
|
|
var out []string
|
|
for _, v := range fieldLoc.([]any) {
|
|
switch v.(type) {
|
|
case string:
|
|
out = append(out, v.(string))
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) InheritAsStringSlice(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) ([]string, error) {
|
|
val, err := genericEntry.FieldAsStringSlice(fieldName, locale...)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(parentRefs) > 0 {
|
|
for _, parentRef := range parentRefs {
|
|
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parentVal, err := genericParent.FieldAsStringSlice(fieldName, locale...)
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
return parentVal, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) FieldAsFloat64(fieldName string, locale ...Locale) (float64, error) {
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return 0, ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
if _, ok := genericEntry.RawFields[fieldName]; !ok {
|
|
return 0, ErrNotSet
|
|
}
|
|
switch field := genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]interface{}:
|
|
fieldLoc := field[string(loc)]
|
|
if fieldLoc == 0 && field[string(DefaultLocale)] != 0 {
|
|
fieldLoc = field[string(DefaultLocale)]
|
|
}
|
|
switch fieldLocFloat := fieldLoc.(type) {
|
|
case float64:
|
|
return fieldLocFloat, nil
|
|
default:
|
|
return 0, ErrNotSet
|
|
}
|
|
default:
|
|
return 0, ErrNotSet
|
|
}
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) InheritAsFloat64(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (float64, error) {
|
|
val, err := genericEntry.FieldAsFloat64(fieldName, locale...)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(parentRefs) > 0 {
|
|
for _, parentRef := range parentRefs {
|
|
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
parentVal, err := genericParent.FieldAsFloat64(fieldName, locale...)
|
|
if err != nil {
|
|
return 0, err
|
|
} else {
|
|
return parentVal, nil
|
|
}
|
|
}
|
|
}
|
|
return 0, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) FieldAsReference(fieldName string, locale ...Locale) (*EntryReference, error) {
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return nil, ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
var cts ContentTypeSys
|
|
if _, ok := genericEntry.RawFields[fieldName]; !ok {
|
|
return nil, ErrNotSet
|
|
}
|
|
switch field := genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]interface{}:
|
|
fieldVal := field[string(loc)]
|
|
if fieldVal == nil {
|
|
fieldVal = field[string(DefaultLocale)]
|
|
}
|
|
byt, err := json.Marshal(fieldVal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(byt, &cts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cts.Sys.ID == "" {
|
|
return nil, errors.New("not a reference")
|
|
}
|
|
referencedEntry, err := genericEntry.CC.GetGenericEntry(cts.Sys.ID)
|
|
if err != nil || referencedEntry == nil {
|
|
return nil, err
|
|
}
|
|
ref := EntryReference{
|
|
ID: cts.Sys.ID,
|
|
ContentType: referencedEntry.Sys.ContentType.Sys.ID,
|
|
FromField: fieldName,
|
|
}
|
|
return &ref, nil
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) InheritAsReference(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (*EntryReference, error) {
|
|
val, err := genericEntry.FieldAsReference(fieldName, locale...)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(parentRefs) > 0 {
|
|
for _, parentRef := range parentRefs {
|
|
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parentVal, err := genericParent.FieldAsReference(fieldName, locale...)
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
return parentVal, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) FieldAsAsset(ctx context.Context, fieldName string, locale ...Locale) (*contentful.AssetNoLocale, error) {
|
|
var loc Locale
|
|
reqLoc := DefaultLocale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
reqLoc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return nil, ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
var cts ContentTypeSys
|
|
if _, ok := genericEntry.RawFields[fieldName]; !ok {
|
|
return nil, ErrNotSet
|
|
}
|
|
switch field := genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]interface{}:
|
|
fieldVal := field[string(loc)]
|
|
if fieldVal == nil {
|
|
fieldVal = field[string(DefaultLocale)]
|
|
}
|
|
byt, err := json.Marshal(fieldVal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(byt, &cts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
asset, err := genericEntry.CC.GetAssetByID(ctx, cts.Sys.ID)
|
|
if err != nil {
|
|
if genericEntry.CC.logFn != nil && genericEntry.CC.logLevel == LogDebug {
|
|
genericEntry.CC.logFn(map[string]interface{}{"content type": genericEntry.Sys.ContentType.Sys.ID, "entry ID": genericEntry.Sys.ID, "method": "HeaderImage()"}, LogError, ErrNoTypeOfRefAsset)
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
tempAsset := &contentful.AssetNoLocale{}
|
|
tempAsset.Sys = asset.Sys
|
|
tempAsset.Fields = &contentful.FileFieldsNoLocale{}
|
|
if _, ok := asset.Fields.Title[string(reqLoc)]; ok {
|
|
tempAsset.Fields.Title = asset.Fields.Title[string(reqLoc)]
|
|
} else {
|
|
tempAsset.Fields.Title = asset.Fields.Title[string(loc)]
|
|
}
|
|
if _, ok := asset.Fields.Description[string(reqLoc)]; ok {
|
|
tempAsset.Fields.Description = asset.Fields.Description[string(reqLoc)]
|
|
} else {
|
|
tempAsset.Fields.Description = asset.Fields.Description[string(loc)]
|
|
}
|
|
if _, ok := asset.Fields.File[string(reqLoc)]; ok {
|
|
tempAsset.Fields.File = asset.Fields.File[string(reqLoc)]
|
|
} else {
|
|
tempAsset.Fields.File = asset.Fields.File[string(loc)]
|
|
}
|
|
return tempAsset, nil
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) FieldAsMultipleReference(fieldName string, locale ...Locale) ([]*EntryReference, error) {
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return nil, ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
var ctss []ContentTypeSys
|
|
if _, ok := genericEntry.RawFields[fieldName]; !ok {
|
|
return nil, ErrNotSet
|
|
}
|
|
switch field := genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]interface{}:
|
|
fieldVal := field[string(loc)]
|
|
if fieldVal == nil {
|
|
fieldVal = field[string(DefaultLocale)]
|
|
}
|
|
byt, err := json.Marshal(fieldVal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(byt, &ctss)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var refs []*EntryReference
|
|
for _, cts := range ctss {
|
|
if cts.Sys.ID == "" {
|
|
continue
|
|
}
|
|
referencedEntry, err := genericEntry.CC.GetGenericEntry(cts.Sys.ID)
|
|
if err != nil || referencedEntry == nil {
|
|
continue
|
|
}
|
|
refs = append(refs, &EntryReference{
|
|
ID: cts.Sys.ID,
|
|
ContentType: referencedEntry.Sys.ContentType.Sys.ID,
|
|
FromField: fieldName,
|
|
})
|
|
}
|
|
return refs, nil
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) InheritAsMultipleReference(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) ([]*EntryReference, error) {
|
|
val, err := genericEntry.FieldAsMultipleReference(fieldName, locale...)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(parentRefs) > 0 {
|
|
for _, parentRef := range parentRefs {
|
|
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parentVal, err := genericParent.FieldAsMultipleReference(fieldName, locale...)
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
return parentVal, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) FieldAsBool(fieldName string, locale ...Locale) (bool, error) {
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return false, ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
if _, ok := genericEntry.RawFields[fieldName]; !ok {
|
|
return false, ErrNotSet
|
|
}
|
|
switch field := genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]interface{}:
|
|
var fieldLoc any
|
|
if _, ok := field[string(loc)]; ok {
|
|
fieldLoc = field[string(loc)]
|
|
} else {
|
|
fieldLoc = field[string(DefaultLocale)]
|
|
}
|
|
switch fieldLocStr := fieldLoc.(type) {
|
|
case bool:
|
|
return fieldLocStr, nil
|
|
default:
|
|
return false, ErrNotSet
|
|
}
|
|
default:
|
|
return false, ErrNotSet
|
|
}
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) InheritAsBool(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (bool, error) {
|
|
val, err := genericEntry.FieldAsBool(fieldName, locale...)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if len(parentRefs) > 0 {
|
|
for _, parentRef := range parentRefs {
|
|
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
parentVal, err := genericParent.FieldAsBool(fieldName, locale...)
|
|
if err != nil {
|
|
return false, err
|
|
} else {
|
|
return parentVal, nil
|
|
}
|
|
}
|
|
}
|
|
return false, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) FieldAsAny(fieldName string, locale ...Locale) (any, error) {
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return nil, ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
if _, ok := genericEntry.RawFields[fieldName]; !ok {
|
|
return nil, ErrNotSet
|
|
}
|
|
switch field := genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]interface{}:
|
|
var fieldLoc any
|
|
if _, ok := field[string(loc)]; ok {
|
|
fieldLoc = field[string(loc)]
|
|
} else {
|
|
fieldLoc = field[string(DefaultLocale)]
|
|
}
|
|
if fieldLoc != nil {
|
|
return fieldLoc, nil
|
|
}
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) InheritAsAny(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (any, error) {
|
|
val, err := genericEntry.FieldAsAny(fieldName, locale...)
|
|
if err == nil {
|
|
return val, nil
|
|
}
|
|
parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(parentRefs) > 0 {
|
|
for _, parentRef := range parentRefs {
|
|
genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parentVal, err := genericParent.FieldAsAny(fieldName, locale...)
|
|
if err != nil {
|
|
return nil, err
|
|
} else {
|
|
return parentVal, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) SetField(fieldName string, fieldValue interface{}, locale ...Locale) error {
|
|
var loc Locale
|
|
if len(locale) != 0 {
|
|
loc = locale[0]
|
|
if _, ok := localeFallback[loc]; !ok {
|
|
return ErrLocaleUnsupported
|
|
}
|
|
} else {
|
|
loc = DefaultLocale
|
|
}
|
|
if genericEntry.RawFields == nil {
|
|
genericEntry.RawFields = make(RawFields)
|
|
}
|
|
if genericEntry.RawFields[fieldName] == nil {
|
|
genericEntry.RawFields[fieldName] = map[string]interface{}{}
|
|
}
|
|
switch genericEntry.RawFields[fieldName].(type) {
|
|
case map[string]interface{}:
|
|
genericEntry.RawFields[fieldName].(map[string]interface{})[string(loc)] = fieldValue
|
|
return nil
|
|
}
|
|
return ErrNotSet
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) Upsert(ctx context.Context) error {
|
|
cfEntry := &contentful.Entry{
|
|
Fields: map[string]interface{}{},
|
|
}
|
|
// get the generic entry sys into the cfEntry sys
|
|
tmp, errMarshal := json.Marshal(genericEntry)
|
|
if errMarshal != nil {
|
|
return errors.New("can't marshal JSON from entry")
|
|
}
|
|
errUnmarshal := json.Unmarshal(tmp, &cfEntry)
|
|
if errUnmarshal != nil {
|
|
return errors.New("can't unmarshal JSON into CF entry")
|
|
}
|
|
// copy fields
|
|
for key, fieldValue := range genericEntry.RawFields {
|
|
cfEntry.Fields[key] = fieldValue
|
|
}
|
|
// upsert the entry
|
|
err := genericEntry.CC.Client.Entries.Upsert(ctx, genericEntry.CC.SpaceID, cfEntry)
|
|
if err != nil {
|
|
if genericEntry.CC.logFn != nil && genericEntry.CC.logLevel <= LogWarn {
|
|
genericEntry.CC.logFn(map[string]interface{}{"task": "UpdateCache"}, LogWarn, fmt.Errorf("CfAkeneoSettings UpsertEntry: Operation failed: %w", err))
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) Update(ctx context.Context) (err error) {
|
|
if genericEntry == nil {
|
|
return errors.New("Update: Generic Entry is nil")
|
|
}
|
|
if genericEntry.CC == nil {
|
|
return errors.New("Update: Generic Entry has nil Contentful client")
|
|
}
|
|
if genericEntry.CC.clientMode != ClientModeCMA {
|
|
return errors.New("Update: Generic Entry not in ClientModeCMA")
|
|
}
|
|
publishingStatus := genericEntry.GetPublishingStatus()
|
|
cfEntry := &contentful.Entry{}
|
|
tmp, errMarshal := json.Marshal(genericEntry)
|
|
if errMarshal != nil {
|
|
return errors.New("Update: Can't marshal JSON from VO")
|
|
}
|
|
errUnmarshal := json.Unmarshal(tmp, &cfEntry)
|
|
if errUnmarshal != nil {
|
|
return errors.New("Update: Can't unmarshal JSON into CF entry")
|
|
}
|
|
|
|
err = genericEntry.CC.Client.Entries.Upsert(ctx, genericEntry.CC.SpaceID, cfEntry)
|
|
if err != nil {
|
|
return fmt.Errorf("Update: upsert operation failed: %w", err)
|
|
}
|
|
tmp, errMarshal = json.Marshal(cfEntry)
|
|
if errMarshal != nil {
|
|
return errors.New("Update: Can't marshal JSON back from CF entry")
|
|
}
|
|
errUnmarshal = json.Unmarshal(tmp, &genericEntry)
|
|
if errUnmarshal != nil {
|
|
return errors.New("Update: Can't unmarshal JSON back into Generic Entry")
|
|
}
|
|
if publishingStatus == StatusPublished {
|
|
genericEntry.Sys.Version++
|
|
err = genericEntry.CC.Client.Entries.Publish(ctx, genericEntry.CC.SpaceID, cfEntry)
|
|
if err != nil {
|
|
return fmt.Errorf("Update: publish operation failed: %w", err)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (genericEntry *GenericEntry) GetPublishingStatus() string {
|
|
if genericEntry == nil {
|
|
return ""
|
|
}
|
|
if genericEntry.Sys.PublishedVersion == 0 {
|
|
return StatusDraft
|
|
}
|
|
if genericEntry.Sys.Version-genericEntry.Sys.PublishedVersion == 1 {
|
|
return StatusPublished
|
|
}
|
|
return StatusChanged
|
|
}
|
|
|
|
func (cc *ContentfulClient) ClientMode() ClientMode {
|
|
return cc.clientMode
|
|
}
|
|
|
|
func (cc *ContentfulClient) SetCacheUpdateTimeout(seconds int64) {
|
|
cc.cacheUpdateTimeout = seconds
|
|
}
|
|
|
|
func (cc *ContentfulClient) SetEnvironment(environment string) {
|
|
if environment == "" {
|
|
cc.Client.Environment = "master"
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogWarn, ErrorEnvironmentSetToMaster)
|
|
}
|
|
return
|
|
}
|
|
cc.Client.Environment = environment
|
|
}
|
|
|
|
func (cc *ContentfulClient) SetOfflineFallback(file []byte) error {
|
|
offlineTemp, err := getOfflineSpaceFromFile(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cc.offlineTemp = *offlineTemp
|
|
return nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) SetSyncMode(mode bool) error {
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
defer cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
if cc.offline {
|
|
return errors.New("SetSyncMode: client is set offline, can't enable sync")
|
|
}
|
|
cc.sync = mode
|
|
return nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) ResetSync() {
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
defer cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
cc.syncToken = ""
|
|
}
|
|
|
|
func (cc *ContentfulClient) UpdateCache(ctx context.Context, contentTypes []string, cacheAssets bool) (map[string][]string, []string, error) {
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
ctxAtWork, cancel := context.WithTimeout(ctx, time.Second*time.Duration(cc.cacheUpdateTimeout))
|
|
defer cancel()
|
|
localOffline := cc.offline
|
|
isSync := cc.sync
|
|
cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
|
|
if localOffline {
|
|
ctxAtWork = ctx
|
|
}
|
|
if !localOffline {
|
|
time.Sleep(time.Second * 2)
|
|
}
|
|
if contentTypes == nil {
|
|
contentTypes = spaceContentTypes
|
|
} else {
|
|
for _, contentType := range contentTypes {
|
|
if !stringSliceContains(spaceContentTypes, contentType) {
|
|
return nil, nil, fmt.Errorf("UpdateCache: Content Type %q not available in this space", contentType)
|
|
}
|
|
}
|
|
}
|
|
tags, err := cc.GetAllTags(ctx)
|
|
if err != nil {
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache", "error": err.Error()}, LogWarn, "failed to cache tags")
|
|
}
|
|
}
|
|
cc.cacheMutex.tagGcLock.Lock()
|
|
cc.Cache.tags = tags
|
|
cc.cacheMutex.tagGcLock.Unlock()
|
|
|
|
if isSync {
|
|
return cc.syncCache(ctxAtWork, contentTypes)
|
|
}
|
|
cc.cacheWorkerOnce.Do(func() {
|
|
go cc.cacheWorker(ctx, contentTypes, cacheAssets)
|
|
})
|
|
if len(cc.cacheQueue) == 0 {
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateQueued)
|
|
}
|
|
cc.cacheQueue <- struct{}{}
|
|
select {
|
|
case <-cc.cacheDone:
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateDone)
|
|
}
|
|
case <-ctxAtWork.Done():
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateCanceled)
|
|
}
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
cc.cacheInit = true
|
|
cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateDone)
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateSkipped)
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) syncCache(ctx context.Context, contentTypes []string) (map[string][]string, []string, error) {
|
|
start := time.Now()
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
cc.Cache.contentTypes = contentTypes
|
|
cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
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 {
|
|
if ctx.Err() != nil {
|
|
return nil, nil, ctx.Err()
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
col := cc.Client.Entries.Sync(
|
|
ctx,
|
|
cc.SpaceID,
|
|
cc.syncToken == "",
|
|
cc.syncToken,
|
|
)
|
|
cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
if _, err := col.GetAll(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
cc.syncToken = col.SyncToken
|
|
cc.cacheInit = true
|
|
cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
if len(col.Items) == 0 {
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"time elapsed": fmt.Sprint(time.Since(start)), "task": "syncCache"}, LogInfo, InfoUpdateCacheTime)
|
|
}
|
|
return syncdEntries, syncdAssets, nil
|
|
}
|
|
var entries []*contentful.Entry
|
|
var assets []*contentful.Asset
|
|
for _, item := range col.Items {
|
|
if ctx.Err() != nil {
|
|
return nil, nil, ctx.Err()
|
|
}
|
|
entry := &contentful.Entry{}
|
|
byteArray, _ := json.Marshal(item)
|
|
errEntry := json.NewDecoder(bytes.NewReader(byteArray)).Decode(entry)
|
|
if errEntry == nil && entry.Sys != nil && (entry.Sys.Type == sysTypeAsset || entry.Sys.Type == sysTypeDeletedAsset) {
|
|
asset := &contentful.Asset{}
|
|
errAsset := json.NewDecoder(bytes.NewReader(byteArray)).Decode(asset)
|
|
if errAsset != nil {
|
|
continue
|
|
}
|
|
if asset.Sys != nil {
|
|
assets = append(assets, asset)
|
|
}
|
|
continue
|
|
}
|
|
if entry.Sys != nil {
|
|
entries = append(entries, entry)
|
|
}
|
|
}
|
|
for _, entry := range entries {
|
|
if ctx.Err() != nil {
|
|
return nil, nil, ctx.Err()
|
|
}
|
|
switch entry.Sys.Type {
|
|
case sysTypeEntry:
|
|
if !stringSliceContains(spaceContentTypes, entry.Sys.ContentType.Sys.ID) {
|
|
continue
|
|
}
|
|
if err := updateCacheForContentTypeAndEntity(ctx, cc, entry.Sys.ContentType.Sys.ID, entry.Sys.ID, entry, false); err != nil {
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"id": entry.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to update cache for entry")
|
|
}
|
|
} else {
|
|
if syncdEntries == nil {
|
|
syncdEntries = map[string][]string{}
|
|
}
|
|
if _, ok := syncdEntries[entry.Sys.ContentType.Sys.ID]; !ok {
|
|
syncdEntries[entry.Sys.ContentType.Sys.ID] = []string{}
|
|
}
|
|
syncdEntries[entry.Sys.ContentType.Sys.ID] = append(syncdEntries[entry.Sys.ContentType.Sys.ID], entry.Sys.ID)
|
|
}
|
|
case sysTypeDeletedEntry:
|
|
cc.cacheMutex.idContentTypeMapGcLock.RLock()
|
|
contentType := cc.Cache.idContentTypeMap[entry.Sys.ID]
|
|
cc.cacheMutex.idContentTypeMapGcLock.RUnlock()
|
|
if err := updateCacheForContentTypeAndEntity(ctx, cc, contentType, entry.Sys.ID, entry, true); err != nil {
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"id": entry.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to delete cache for entry")
|
|
}
|
|
} else {
|
|
if syncdEntries == nil {
|
|
syncdEntries = map[string][]string{}
|
|
}
|
|
if _, ok := syncdEntries[contentType]; !ok {
|
|
syncdEntries[contentType] = []string{}
|
|
}
|
|
syncdEntries[contentType] = append(syncdEntries[contentType], entry.Sys.ID)
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
if cc.logFn != nil && len(entries) > 0 && cc.logLevel <= LogInfo {
|
|
for contentType, ids := range syncdEntries {
|
|
cc.logFn(map[string]interface{}{"task": "syncCache", "contentType": contentType, "syncEntryCount": len(ids)}, LogInfo, InfoCacheSyncOp)
|
|
}
|
|
}
|
|
for _, asset := range assets {
|
|
if ctx.Err() != nil {
|
|
return nil, nil, ctx.Err()
|
|
}
|
|
switch asset.Sys.Type {
|
|
case sysTypeAsset:
|
|
if err := updateCacheForContentTypeAndEntity(ctx, cc, assetWorkerType, asset.Sys.ID, asset, false); err != nil {
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"id": asset.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to update cache for entry")
|
|
}
|
|
} else {
|
|
syncdAssets = append(syncdAssets, asset.Sys.ID)
|
|
}
|
|
case sysTypeDeletedAsset:
|
|
if err := updateCacheForContentTypeAndEntity(ctx, cc, assetWorkerType, asset.Sys.ID, nil, true); err != nil {
|
|
if cc.logFn != nil && cc.logLevel <= LogWarn {
|
|
cc.logFn(map[string]interface{}{"id": asset.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to delete cache for entry")
|
|
}
|
|
} else {
|
|
syncdAssets = append(syncdAssets, asset.Sys.ID)
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
if cc.logFn != nil && len(assets) > 0 && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "syncCache", "syncAssetCount": len(syncdAssets)}, LogInfo, InfoCacheSyncOp)
|
|
}
|
|
}
|
|
return syncdEntries, syncdAssets, nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) cacheWorker(ctx context.Context, contentTypes []string, cacheAssets bool) {
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheWorkerStart)
|
|
}
|
|
for range cc.cacheQueue {
|
|
cc.cacheSpace(ctx, contentTypes, cacheAssets)
|
|
cc.cacheDone <- struct{}{}
|
|
}
|
|
}
|
|
func (cc *ContentfulClient) cacheSpace(ctx context.Context, contentTypes []string, cacheAssets bool) {
|
|
start := time.Now()
|
|
tempCache := &ContentfulCache{
|
|
contentTypes: contentTypes,
|
|
idContentTypeMap: map[string]string{},
|
|
genericEntries: map[string]*GenericEntry{},
|
|
parentMap: map[string][]EntryReference{},
|
|
}
|
|
if cacheAssets {
|
|
contentTypes = append([]string{assetWorkerType, tagWorkerType}, contentTypes...)
|
|
}
|
|
_, errCanWeEvenConnect := cc.Client.Spaces.Get(ctx, cc.SpaceID)
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
offlinePreviousState := cc.offline
|
|
cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
if errCanWeEvenConnect != nil {
|
|
if len(cc.offlineTemp.Entries) > 0 && (cc.Cache == nil || offlinePreviousState) {
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoFallingBackToFile)
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
cc.offline = true
|
|
cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
} else {
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoPreservingExistingCache)
|
|
}
|
|
}
|
|
}
|
|
results := make(chan ContentTypeResult, 16)
|
|
resultsDone := make(chan struct{})
|
|
contentTypeChan := make(chan string)
|
|
group, gctx := errgroup.WithContext(ctx)
|
|
for i := 0; i < cacheUpdateConcurrency; i++ {
|
|
group.Go(func() error {
|
|
for contentType := range contentTypeChan {
|
|
err := updateCacheForContentType(gctx, results, cc, tempCache, contentType)
|
|
if err != nil {
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache", "contentType": contentType, "clientMode": cc.clientMode}, LogError, err.Error())
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
go func() {
|
|
for _, contentType := range contentTypes {
|
|
contentTypeChan <- contentType
|
|
}
|
|
close(contentTypeChan)
|
|
}()
|
|
go func() {
|
|
for res := range results {
|
|
tempCache.idContentTypeMap[res.EntryID] = res.ContentType
|
|
for childID, references := range res.References {
|
|
tempCache.parentMap[childID] = append(tempCache.parentMap[childID], references...)
|
|
}
|
|
}
|
|
resultsDone <- struct{}{}
|
|
}()
|
|
err := group.Wait()
|
|
close(results)
|
|
if err != nil {
|
|
// drain contentTypeChan
|
|
for range contentTypeChan {
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
cc.offline = offlinePreviousState
|
|
cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogError, err.Error())
|
|
}
|
|
return
|
|
}
|
|
// Signal that the cache build is done
|
|
<-resultsDone
|
|
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"time elapsed": fmt.Sprint(time.Since(start)), "task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoUpdateCacheTime)
|
|
}
|
|
cc.cacheMutex.fullCacheGcLock.Lock()
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
cc.cacheMutex.assetsGcLock.Lock()
|
|
cc.cacheMutex.idContentTypeMapGcLock.Lock()
|
|
cc.cacheMutex.parentMapGcLock.Lock()
|
|
cc.cacheMutex.genericEntriesGcLock.Lock()
|
|
cc.cacheMutex.brandGcLock.Lock()
|
|
cc.cacheMutex.categoryGcLock.Lock()
|
|
cc.cacheMutex.productGcLock.Lock()
|
|
|
|
cc.Cache = tempCache
|
|
cc.offline = offlinePreviousState
|
|
cc.cacheMutex.brandGcLock.Unlock()
|
|
cc.cacheMutex.categoryGcLock.Unlock()
|
|
cc.cacheMutex.productGcLock.Unlock()
|
|
|
|
cc.cacheMutex.parentMapGcLock.Unlock()
|
|
cc.cacheMutex.idContentTypeMapGcLock.Unlock()
|
|
cc.cacheMutex.assetsGcLock.Unlock()
|
|
cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
cc.cacheMutex.fullCacheGcLock.Unlock()
|
|
cc.cacheMutex.genericEntriesGcLock.Unlock()
|
|
}
|
|
|
|
func ToAssetReference(asset *contentful.Asset) (refSys ContentTypeSys) {
|
|
refSys.Sys.ID = asset.Sys.ID
|
|
refSys.Sys.Type = FieldTypeLink
|
|
refSys.Sys.LinkType = FieldLinkTypeAsset
|
|
return
|
|
}
|
|
|
|
func (cc *ContentfulClient) UpdateCacheForEntity(ctx context.Context, sysType string, contentType string, entityID string) error {
|
|
if sysType == sysTypeEntry && cc.entryMapForContentTypeIsNil(contentType) {
|
|
return fmt.Errorf("UpdateCacheForEntity: Content Type %q not available in cache", contentType)
|
|
}
|
|
if sysType == sysTypeAsset {
|
|
contentType = assetWorkerType
|
|
}
|
|
if contentType != assetWorkerType && !stringSliceContains(spaceContentTypes, contentType) {
|
|
return fmt.Errorf("UpdateCache: Content Type %q not available in this space", contentType)
|
|
}
|
|
return updateCacheForContentTypeAndEntity(ctx, cc, contentType, entityID, nil, false)
|
|
}
|
|
|
|
func FieldToObject(jsonField interface{}, targetObject interface{}) error {
|
|
byteArray, err := json.Marshal(jsonField)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.Unmarshal(byteArray, &targetObject)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) cacheGcAssetByID(ctx context.Context, id string, asset *contentful.Asset) error {
|
|
if asset == nil {
|
|
if cc.Client == nil {
|
|
return errors.New("cacheGcAssetByID: No client available")
|
|
}
|
|
col := cc.Client.Assets.List(ctx, cc.SpaceID)
|
|
col.Query.Locale("*").Equal("sys.id", id)
|
|
_, err := col.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(col.Items) == 0 {
|
|
return errors.New("cacheGcAssetByID: Not found " + id)
|
|
}
|
|
item := col.Items[0]
|
|
byt, err := json.Marshal(item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.Unmarshal(byt, &asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} {
|
|
if _, ok := asset.Fields.File[string(loc)]; ok {
|
|
asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL
|
|
}
|
|
}
|
|
cc.cacheMutex.assetsGcLock.Lock()
|
|
cc.Cache.assets[id] = asset
|
|
cc.cacheMutex.assetsGcLock.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) deleteAssetFromCache(key string) error {
|
|
if cc.Cache == nil || cc.cacheMutex == nil {
|
|
return errors.New("no cache available")
|
|
}
|
|
cc.cacheMutex.assetsGcLock.Lock()
|
|
if _, ok := cc.Cache.assets[key]; ok {
|
|
delete(cc.Cache.assets, key)
|
|
cc.cacheMutex.assetsGcLock.Unlock()
|
|
return nil
|
|
}
|
|
cc.cacheMutex.assetsGcLock.Unlock()
|
|
return errors.New("asset not found in cache, could not delete")
|
|
}
|
|
|
|
func (cc *ContentfulClient) entryMapForContentTypeIsNil(contentType string) bool {
|
|
switch contentType {
|
|
|
|
case ContentTypeBrand:
|
|
if cc.Cache.entryMaps.brand == nil {
|
|
return true
|
|
}
|
|
|
|
case ContentTypeCategory:
|
|
if cc.Cache.entryMaps.category == nil {
|
|
return true
|
|
}
|
|
|
|
case ContentTypeProduct:
|
|
if cc.Cache.entryMaps.product == nil {
|
|
return true
|
|
}
|
|
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getContentfulAPIClient(clientMode ClientMode, clientKey string) (*contentful.Contentful, error) {
|
|
switch clientMode {
|
|
case ClientModeCDA:
|
|
return contentful.NewCDA(clientKey), nil
|
|
case ClientModeCPA:
|
|
return contentful.NewCPA(clientKey), nil
|
|
case ClientModeCMA:
|
|
return contentful.NewCMA(clientKey), nil
|
|
default:
|
|
return nil, errors.New("NewContentfulClient: Unknown ClientMode")
|
|
}
|
|
}
|
|
|
|
func (cc *ContentfulClient) getAllAssets(ctx context.Context, tryCacheFirst bool) (map[string]*contentful.Asset, error) {
|
|
if cc == nil || cc.Client == nil {
|
|
return nil, errors.New("getAllAssets: No client available")
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
offline := cc.offline
|
|
cacheInit := cc.cacheInit
|
|
cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
|
|
if cacheInit && cc.Cache.assets != nil && tryCacheFirst {
|
|
return cc.Cache.assets, nil
|
|
}
|
|
allItems := []interface{}{}
|
|
assets := map[string]*contentful.Asset{}
|
|
if offline {
|
|
for _, asset := range cc.offlineTemp.Assets {
|
|
allItems = append(allItems, asset)
|
|
}
|
|
} else {
|
|
col := cc.Client.Assets.List(ctx, cc.SpaceID)
|
|
col.Query.Locale("*").Limit(assetPageSize)
|
|
for {
|
|
_, err := col.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allItems = append(allItems, col.Items...)
|
|
if uint16(len(col.Items)) < assetPageSize {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for _, item := range allItems {
|
|
asset := contentful.Asset{}
|
|
byt, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(byt, &asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} {
|
|
if _, ok := asset.Fields.File[string(loc)]; ok {
|
|
asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL
|
|
}
|
|
}
|
|
assets[asset.Sys.ID] = &asset
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) getAllTags(ctx context.Context, tryCacheFirst bool) (map[string]string, error) {
|
|
if cc == nil || cc.Client == nil {
|
|
return nil, errors.New("getAllTags: No client available")
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
offline := cc.offline
|
|
cacheInit := cc.cacheInit
|
|
cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
cc.cacheMutex.tagGcLock.RLock()
|
|
defer cc.cacheMutex.tagGcLock.RUnlock()
|
|
if cacheInit && cc.Cache.tags != nil && tryCacheFirst {
|
|
return cc.Cache.tags, nil
|
|
}
|
|
allItems := []interface{}{}
|
|
tags := map[string]string{}
|
|
if offline {
|
|
for _, asset := range cc.offlineTemp.Tags {
|
|
allItems = append(allItems, asset)
|
|
}
|
|
} else {
|
|
col := cc.Client.Tags.List(ctx, cc.SpaceID)
|
|
col.Query.Limit(1000)
|
|
_, err := col.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allItems = col.Items
|
|
}
|
|
for _, item := range allItems {
|
|
tag := contentful.Tag{}
|
|
byt, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(byt, &tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tags[tag.Name] = tag.Sys.ID
|
|
}
|
|
return tags, nil
|
|
}
|
|
|
|
func getOfflineSpaceFromFile(file []byte) (*offlineTemp, error) {
|
|
offlineTemp := &offlineTemp{}
|
|
err := json.Unmarshal(file, offlineTemp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getOfflineSpaceFromFile could not parse space export file: %v", err)
|
|
}
|
|
return offlineTemp, nil
|
|
}
|
|
|
|
func (cc *ContentfulClient) optimisticPageSizeGetAll(ctx context.Context, contentType string, limit uint16) (*contentful.Collection, error) {
|
|
col := cc.Client.Entries.List(ctx, cc.SpaceID)
|
|
col.Query.ContentType(contentType).Locale("*").Include(0).Limit(limit)
|
|
allItems := []interface{}{}
|
|
var err error
|
|
for {
|
|
_, err = col.Next()
|
|
if err != nil {
|
|
break
|
|
}
|
|
allItems = append(allItems, col.Items...)
|
|
if uint16(len(col.Items)) < limit {
|
|
break
|
|
}
|
|
}
|
|
col.Items = allItems
|
|
switch errTyped := err.(type) {
|
|
case contentful.ErrorResponse:
|
|
msg := errTyped.Message
|
|
if (strings.Contains(msg, "Response size too big") || strings.Contains(msg, "Too many links")) && limit >= 20 {
|
|
smallerPageCol, err := cc.optimisticPageSizeGetAll(ctx, contentType, limit/2)
|
|
return smallerPageCol, err
|
|
}
|
|
return nil, err
|
|
case nil:
|
|
default:
|
|
return nil, err
|
|
}
|
|
return col, nil
|
|
}
|
|
|
|
func richTextGetAttribute(htmlLine string, attribute string) string {
|
|
re := regexp.MustCompile(` href=["']([^"']+)["']`)
|
|
matches := re.FindStringSubmatch(htmlLine)
|
|
if len(matches) == 2 {
|
|
return strings.ToLower(matches[1])
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func richTextGetMark(mark string) string {
|
|
switch mark {
|
|
case HtmlItalic, HtmlEm:
|
|
return RichTextMarkItalic
|
|
case HtmlBold, HtmlStrong:
|
|
return RichTextMarkBold
|
|
case HtmlUnderline:
|
|
return RichTextMarkUnderline
|
|
case HtmlCode:
|
|
return RichTextMarkCode
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func richTextHtmlLinesToNode(htmlLines []string, start int, pendingTag string, marks []string, isBasic bool) ([]interface{}, int, bool) {
|
|
nodeSlice := make([]interface{}, 0)
|
|
for i := start; i < len(htmlLines); i++ {
|
|
htmlLine := htmlLines[i]
|
|
if richTextIsHtmlClosingPending(htmlLine, pendingTag) {
|
|
return nodeSlice, i, isBasic
|
|
}
|
|
currentNode := RichTextNode{}
|
|
if richTextIsHtmlTag(htmlLine) {
|
|
tt := richTextHtmlTagType(htmlLine)
|
|
switch tt {
|
|
case HtmlParagraph, HtmlHeading1, HtmlHeading2, HtmlHeading3, HtmlHeading4, HtmlHeading5, HtmlHeading6, HtmlUnorderedList, HtmlOrderedList:
|
|
isBasic = false
|
|
currentNode.NodeType = richTextMapTagNodeType(tt)
|
|
var nextLine int
|
|
currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic)
|
|
nodeSlice = append(nodeSlice, currentNode)
|
|
if nextLine == -1 {
|
|
return nodeSlice, -1, isBasic
|
|
}
|
|
i = nextLine
|
|
case HtmlBlockquote, HtmlListItem:
|
|
isBasic = false
|
|
currentNode.NodeType = richTextMapTagNodeType(tt)
|
|
currentNode.Content = make([]interface{}, 0)
|
|
innerContent, nextLine, isBasic := richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic)
|
|
if len(innerContent) == 1 {
|
|
switch innerContent[0].(type) {
|
|
case RichTextNodeTextNode:
|
|
currentNode.Content = append(currentNode.Content, RichTextNode{
|
|
NodeType: RichTextNodeParagraph,
|
|
Content: innerContent,
|
|
})
|
|
}
|
|
} else {
|
|
for _, ct := range innerContent {
|
|
switch ct.(type) {
|
|
case RichTextNode:
|
|
currentNode.Content = append(currentNode.Content, ct)
|
|
case RichTextNodeTextNode:
|
|
currentNode.Content = append(currentNode.Content, RichTextNode{
|
|
NodeType: RichTextNodeParagraph,
|
|
Content: []interface{}{ct},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
nodeSlice = append(nodeSlice, currentNode)
|
|
if nextLine == -1 {
|
|
return nodeSlice, -1, isBasic
|
|
}
|
|
i = nextLine
|
|
case HtmlCode:
|
|
isBasic = false
|
|
currentNode.NodeType = RichTextNodeParagraph
|
|
var nextLine int
|
|
currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, HtmlCode, []string{HtmlCode}, isBasic)
|
|
nodeSlice = append(nodeSlice, currentNode)
|
|
if nextLine == -1 {
|
|
return nodeSlice, -1, isBasic
|
|
}
|
|
i = nextLine
|
|
case HtmlAnchor:
|
|
isBasic = false
|
|
currentNode.NodeType = RichTextNodeHyperlink
|
|
var nextLine int
|
|
anchorURI := richTextGetAttribute(htmlLine, HtmlAttributeHref)
|
|
if anchorURI == "" {
|
|
anchorURI = "/"
|
|
}
|
|
currentNode.Data = RichTextData{URI: anchorURI}
|
|
currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic)
|
|
nodeSlice = append(nodeSlice, currentNode)
|
|
if nextLine == -1 {
|
|
return nodeSlice, -1, isBasic
|
|
}
|
|
i = nextLine
|
|
case HtmlHorizontalRule:
|
|
isBasic = false
|
|
currentNode.NodeType = RichTextNodeHR
|
|
currentNode.Content = make([]interface{}, 0)
|
|
nodeSlice = append(nodeSlice, currentNode)
|
|
case HtmlItalic, HtmlEm, HtmlBold, HtmlStrong, HtmlUnderline:
|
|
marks = append(marks, tt)
|
|
case HtmlBreak:
|
|
if len(nodeSlice) > 0 {
|
|
myNode := nodeSlice[len(nodeSlice)-1]
|
|
switch myNodeTyped := myNode.(type) {
|
|
case RichTextNodeTextNode:
|
|
if myNodeTyped.NodeType == RichTextNodeText {
|
|
myNodeTyped.Value += "\n"
|
|
nodeSlice[len(nodeSlice)-1] = myNodeTyped
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue //unsupported tags will be ignored but content is preserved
|
|
}
|
|
if richTextIsHtmlClosingTag(htmlLine) {
|
|
continue //closing tags that are not pending equal to unknown
|
|
}
|
|
if htmlLine == " " {
|
|
continue
|
|
}
|
|
currentNodeTextNode := RichTextNodeTextNode{}
|
|
currentNodeTextNode.NodeType = RichTextNodeText
|
|
currentNodeTextNode.Marks = []RichTextMark{}
|
|
currentNodeTextNode.Value = htmlLine
|
|
for _, mark := range marks {
|
|
currentNodeTextNode.Marks = append(currentNodeTextNode.Marks, RichTextMark{
|
|
Type: richTextGetMark(mark),
|
|
})
|
|
}
|
|
marks = nil
|
|
if pendingTag == "" {
|
|
nodeSlice = append(nodeSlice, RichTextNode{
|
|
NodeType: RichTextNodeParagraph,
|
|
Content: []interface{}{currentNodeTextNode},
|
|
})
|
|
} else {
|
|
nodeSlice = append(nodeSlice, currentNodeTextNode)
|
|
}
|
|
}
|
|
return nodeSlice, -1, isBasic
|
|
}
|
|
|
|
func richTextIsHtmlClosingPending(htmlLine, tag string) bool {
|
|
return regexp.MustCompile(`</` + strings.ToLower(tag) + `.+$`).MatchString(strings.ToLower(htmlLine))
|
|
}
|
|
|
|
func richTextIsHtmlClosingTag(htmlLine string) bool {
|
|
return regexp.MustCompile(`</[a-zA-Z]+.+$`).MatchString(htmlLine)
|
|
}
|
|
|
|
func richTextIsHtmlTag(htmlLine string) bool {
|
|
return regexp.MustCompile(`<[a-zA-Z]+.+$`).MatchString(htmlLine)
|
|
}
|
|
|
|
func richTextMapTagNodeType(tag string) string {
|
|
switch tag {
|
|
case HtmlParagraph:
|
|
return RichTextNodeParagraph
|
|
case HtmlAnchor:
|
|
return RichTextNodeHyperlink
|
|
case HtmlHeading1:
|
|
return RichTextNodeHeading1
|
|
case HtmlHeading2:
|
|
return RichTextNodeHeading2
|
|
case HtmlHeading3:
|
|
return RichTextNodeHeading3
|
|
case HtmlHeading4:
|
|
return RichTextNodeHeading4
|
|
case HtmlHeading5:
|
|
return RichTextNodeHeading5
|
|
case HtmlHeading6:
|
|
return RichTextNodeHeading6
|
|
case HtmlBlockquote:
|
|
return RichTextNodeBlockquote
|
|
case HtmlUnorderedList:
|
|
return RichTextNodeUnorderedList
|
|
case HtmlOrderedList:
|
|
return RichTextNodeOrderedList
|
|
case HtmlListItem:
|
|
return RichTextNodeListItem
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (ts richTextHtmlTags) richTextHtmlTagsOpen(w io.Writer) {
|
|
|
|
for _, t := range ts {
|
|
if t.customHTML != "" {
|
|
w.Write([]byte(t.customHTML))
|
|
continue
|
|
}
|
|
tagString := "<" + t.name
|
|
if len(t.attrs) > 0 {
|
|
for name, value := range t.attrs {
|
|
tagString += " " + name + `="` + html.EscapeString(value) + `"`
|
|
}
|
|
}
|
|
w.Write([]byte(tagString + ">"))
|
|
}
|
|
}
|
|
|
|
func (ts richTextHtmlTags) richTextHtmlTagsClose(w io.Writer) {
|
|
for _, t := range ts {
|
|
if t.customHTML != "" {
|
|
continue
|
|
}
|
|
w.Write([]byte("</" + t.name + ">"))
|
|
}
|
|
}
|
|
|
|
func richTextHtmlTagType(htmlLine string) string {
|
|
re := regexp.MustCompile(`^<([a-zA-Z0-9]+)[\s]*>*`)
|
|
matches := re.FindStringSubmatch(htmlLine)
|
|
if len(matches) == 2 {
|
|
return strings.ToLower(matches[1])
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (n *RichTextGenericNode) richTextRenderHTML(w io.Writer, linkResolver LinkResolverFunc, entryLinkResolver EntryLinkResolverFunc, imageResolver ImageResolverFunc, embeddedEntryResolver EmbeddedEntryResolverFunc, locale Locale) (err error) {
|
|
if linkResolver == nil {
|
|
linkResolver = func(url string) (transformedAttrs map[string]string, err error) {
|
|
return map[string]string{
|
|
"href": url,
|
|
}, nil
|
|
}
|
|
}
|
|
if entryLinkResolver == nil {
|
|
entryLinkResolver = func(entryID string, locale Locale) (transformedAttrs map[string]string, err error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
}
|
|
if embeddedEntryResolver == nil {
|
|
embeddedEntryResolver = func(entryID string, locale Locale) (htmlSnippet string, err error) {
|
|
return "", nil
|
|
}
|
|
}
|
|
if imageResolver == nil {
|
|
imageResolver = func(assetID string, locale Locale) (attrs map[string]string, customHTML string, resolveError error) {
|
|
return map[string]string{}, "", nil
|
|
}
|
|
}
|
|
tags := richTextHtmlTags{}
|
|
switch n.NodeType {
|
|
case RichTextNodeParagraph:
|
|
tags = []richTextHtmlTag{{name: HtmlParagraph}}
|
|
case RichTextNodeHeading1:
|
|
tags = []richTextHtmlTag{{name: HtmlHeading1}}
|
|
case RichTextNodeHeading2:
|
|
tags = []richTextHtmlTag{{name: HtmlHeading2}}
|
|
case RichTextNodeHeading3:
|
|
tags = []richTextHtmlTag{{name: HtmlHeading3}}
|
|
case RichTextNodeHeading4:
|
|
tags = []richTextHtmlTag{{name: HtmlHeading4}}
|
|
case RichTextNodeHeading5:
|
|
tags = []richTextHtmlTag{{name: HtmlHeading5}}
|
|
case RichTextNodeHeading6:
|
|
tags = []richTextHtmlTag{{name: HtmlHeading6}}
|
|
case RichTextNodeOrderedList:
|
|
tags = []richTextHtmlTag{{name: HtmlOrderedList}}
|
|
case RichTextNodeUnorderedList:
|
|
tags = []richTextHtmlTag{{name: HtmlUnorderedList}}
|
|
case RichTextNodeListItem:
|
|
tags = []richTextHtmlTag{{name: HtmlListItem}}
|
|
case RichTextNodeHR:
|
|
tags = []richTextHtmlTag{{name: HtmlHorizontalRule}}
|
|
case RichTextNodeBlockquote:
|
|
tags = []richTextHtmlTag{{name: HtmlBlockquote}}
|
|
case RichTextNodeText:
|
|
tags = []richTextHtmlTag{}
|
|
case RichTextNodeHyperlink:
|
|
if n.Data != nil {
|
|
uri := n.Data["uri"]
|
|
attrs := map[string]string{}
|
|
switch uriString := uri.(type) {
|
|
case string:
|
|
if uriString != "" {
|
|
resolvedAttrs, errResolveAttrs := linkResolver(uriString)
|
|
if errResolveAttrs != nil {
|
|
err = errResolveAttrs
|
|
return
|
|
}
|
|
attrs = resolvedAttrs
|
|
}
|
|
}
|
|
tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlAnchor, attrs: attrs}}
|
|
}
|
|
case RichTextNodeEntryHyperlink:
|
|
if n.Data != nil {
|
|
target := n.Data["target"]
|
|
attrs := map[string]string{}
|
|
switch target.(type) {
|
|
case map[string]interface{}:
|
|
targetSys, ok := target.(map[string]interface{})["sys"]
|
|
if ok {
|
|
entryID := targetSys.(map[string]interface{})["id"].(string)
|
|
resolvedAttrs, errResolveAttrs := entryLinkResolver(entryID, locale)
|
|
if errResolveAttrs != nil {
|
|
err = errResolveAttrs
|
|
return
|
|
}
|
|
attrs = resolvedAttrs
|
|
}
|
|
}
|
|
tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlAnchor, attrs: attrs}}
|
|
}
|
|
case RichTextNodeEmbeddedAsset:
|
|
if imageResolver == nil {
|
|
return errors.New("can't resolve image asset URL")
|
|
}
|
|
dataObj := RichTextData{}
|
|
byt, err := json.Marshal(n.Data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.Unmarshal(byt, &dataObj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if dataObj.Target == nil {
|
|
return errors.New("data target is empty")
|
|
}
|
|
attrs, customHTML, err := imageResolver(dataObj.Target.Sys.ID, locale)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlImage, attrs: attrs, customHTML: customHTML}}
|
|
case RichTextNodeEmbeddedEntry:
|
|
dataObj := RichTextData{}
|
|
byt, errMarshal := json.Marshal(n.Data)
|
|
if errMarshal != nil {
|
|
return errMarshal
|
|
}
|
|
errUnmarshal := json.Unmarshal(byt, &dataObj)
|
|
if errUnmarshal != nil {
|
|
return errUnmarshal
|
|
}
|
|
if dataObj.Target == nil {
|
|
return errors.New("data target is empty")
|
|
}
|
|
rawHTML, errResolve := embeddedEntryResolver(dataObj.Target.Sys.ID, locale)
|
|
if errResolve != nil {
|
|
return errResolve
|
|
}
|
|
w.Write([]byte(rawHTML))
|
|
return
|
|
case RichTextNodeTable:
|
|
tags = []richTextHtmlTag{{name: HtmlTable}}
|
|
case RichTextNodeTableRow:
|
|
tags = []richTextHtmlTag{{name: HtmlTableRow}}
|
|
case RichTextNodeTableHeaderCell:
|
|
tags = []richTextHtmlTag{{name: HtmlTableHeaderCell}}
|
|
case RichTextNodeTableCell:
|
|
tags = []richTextHtmlTag{{name: HtmlTableCell}}
|
|
default:
|
|
}
|
|
for _, m := range n.Marks {
|
|
markTag := ""
|
|
switch m.Type {
|
|
case RichTextMarkBold:
|
|
markTag = HtmlBold
|
|
case RichTextMarkItalic:
|
|
markTag = HtmlItalic
|
|
case RichTextMarkUnderline:
|
|
markTag = HtmlUnderline
|
|
case RichTextMarkCode:
|
|
markTag = HtmlCode
|
|
case RichTextNodeBlockquote:
|
|
markTag = HtmlBlockquote
|
|
}
|
|
if markTag != "" {
|
|
tags = append(tags, richTextHtmlTag{name: markTag})
|
|
}
|
|
}
|
|
|
|
tags.richTextHtmlTagsOpen(w)
|
|
cleanString := strings.Replace(html.EscapeString(n.Value), "\n", "<br/>", -1)
|
|
cleanString = strings.ReplaceAll(cleanString, "\u00a0", " ")
|
|
w.Write([]byte(cleanString))
|
|
for _, subNode := range n.Content {
|
|
errSubNode := subNode.richTextRenderHTML(w, linkResolver, entryLinkResolver, imageResolver, embeddedEntryResolver, locale)
|
|
if errSubNode != nil {
|
|
err = errSubNode
|
|
return
|
|
}
|
|
}
|
|
tags.richTextHtmlTagsClose(w)
|
|
return
|
|
}
|
|
|
|
// MarshalJSON implements custom JSON marshalling for RichTextGenericNode for compatibility with Contentful's upsert API
|
|
func (n *RichTextGenericNode) MarshalJSON() ([]byte, error) {
|
|
if n == nil {
|
|
return []byte("null"), nil
|
|
}
|
|
if n.Data == nil {
|
|
n.Data = make(map[string]interface{})
|
|
}
|
|
if n.NodeType == "text" {
|
|
// For text nodes, include Data, Value and Marks, but not Content
|
|
return json.Marshal(&struct {
|
|
NodeType string `json:"nodeType"`
|
|
Data map[string]interface{} `json:"data"`
|
|
Value string `json:"value"`
|
|
Marks []RichTextMark `json:"marks"`
|
|
}{
|
|
NodeType: n.NodeType,
|
|
Data: n.Data,
|
|
Value: n.Value,
|
|
Marks: n.Marks,
|
|
})
|
|
} else {
|
|
// For non-text nodes, exclude Data and Marks
|
|
return json.Marshal(&struct {
|
|
NodeType string `json:"nodeType"`
|
|
Data map[string]interface{} `json:"data"`
|
|
Content []*RichTextGenericNode `json:"content"`
|
|
}{
|
|
NodeType: n.NodeType,
|
|
Content: n.Content,
|
|
Data: n.Data,
|
|
})
|
|
}
|
|
}
|
|
|
|
func stringSliceContains(s []string, e string) bool {
|
|
for _, a := range s {
|
|
if a == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func updateCacheForContentType(ctx context.Context, results chan ContentTypeResult, cc *ContentfulClient, tempCache *ContentfulCache, contentType string) error {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
switch contentType {
|
|
|
|
case ContentTypeBrand:
|
|
allBrand, err := cc.cacheAllBrand(ctx, results)
|
|
if err != nil {
|
|
return errors.New("updateCacheForContentType failed for contentType brand: " + err.Error())
|
|
}
|
|
tempCache.entryMaps.brand = allBrand
|
|
cc.cacheMutex.genericEntriesGcLock.Lock()
|
|
for _, brand := range allBrand {
|
|
tempCache.genericEntries[brand.Sys.ID] = &GenericEntry{
|
|
Sys: brand.Sys,
|
|
RawFields: brand.RawFields,
|
|
CC: brand.CC,
|
|
}
|
|
}
|
|
cc.cacheMutex.genericEntriesGcLock.Unlock()
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"contentType": "brand", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allBrand)}, LogInfo, InfoCachedAllEntries)
|
|
}
|
|
|
|
case ContentTypeCategory:
|
|
allCategory, err := cc.cacheAllCategory(ctx, results)
|
|
if err != nil {
|
|
return errors.New("updateCacheForContentType failed for contentType category: " + err.Error())
|
|
}
|
|
tempCache.entryMaps.category = allCategory
|
|
cc.cacheMutex.genericEntriesGcLock.Lock()
|
|
for _, category := range allCategory {
|
|
tempCache.genericEntries[category.Sys.ID] = &GenericEntry{
|
|
Sys: category.Sys,
|
|
RawFields: category.RawFields,
|
|
CC: category.CC,
|
|
}
|
|
}
|
|
cc.cacheMutex.genericEntriesGcLock.Unlock()
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"contentType": "category", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allCategory)}, LogInfo, InfoCachedAllEntries)
|
|
}
|
|
|
|
case ContentTypeProduct:
|
|
allProduct, err := cc.cacheAllProduct(ctx, results)
|
|
if err != nil {
|
|
return errors.New("updateCacheForContentType failed for contentType product: " + err.Error())
|
|
}
|
|
tempCache.entryMaps.product = allProduct
|
|
cc.cacheMutex.genericEntriesGcLock.Lock()
|
|
for _, product := range allProduct {
|
|
tempCache.genericEntries[product.Sys.ID] = &GenericEntry{
|
|
Sys: product.Sys,
|
|
RawFields: product.RawFields,
|
|
CC: product.CC,
|
|
}
|
|
}
|
|
cc.cacheMutex.genericEntriesGcLock.Unlock()
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"contentType": "product", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allProduct)}, LogInfo, InfoCachedAllEntries)
|
|
}
|
|
|
|
case assetWorkerType:
|
|
allAssets, err := cc.getAllAssets(ctx, false)
|
|
if err != nil {
|
|
return errors.New("updateCacheForContentType failed for assets")
|
|
}
|
|
tempCache.assets = allAssets
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"contentType": "asset", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allAssets)}, LogInfo, InfoCachedAllAssets)
|
|
}
|
|
|
|
case tagWorkerType:
|
|
allTags, err := cc.getAllTags(ctx, false)
|
|
if err != nil {
|
|
return errors.New("updateCacheForContentType failed for tags")
|
|
}
|
|
tempCache.tags = allTags
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo {
|
|
cc.logFn(map[string]interface{}{"contentType": "tag", "method": "updateCacheForContentType", "clientMode": cc.clientMode, "size": len(allTags)}, LogInfo, InfoCachedAllTags)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateCacheForContentTypeAndEntity(ctx context.Context, cc *ContentfulClient, contentType string, entityID string, entityPayload interface{}, entryDelete bool) error {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
var entityEntry *contentful.Entry
|
|
var entityAsset *contentful.Asset
|
|
switch contentType {
|
|
case assetWorkerType:
|
|
if entryDelete {
|
|
err := cc.deleteAssetFromCache(entityID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if entityPayload != nil {
|
|
entityAsset = entityPayload.(*contentful.Asset)
|
|
}
|
|
err := cc.cacheGcAssetByID(ctx, entityID, entityAsset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo && entityPayload == nil {
|
|
cc.logFn(map[string]interface{}{"contentType": "Asset", "method": "updateCacheForContentTypeAndEntity", "clientMode": cc.clientMode, "entityID": entityID}, LogInfo, InfoUpdatedEntityCache)
|
|
}
|
|
|
|
case ContentTypeBrand:
|
|
if entityPayload != nil {
|
|
entityEntry = entityPayload.(*contentful.Entry)
|
|
}
|
|
err := cc.cacheBrandByID(ctx, entityID, entityEntry, entryDelete)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo && entityPayload == nil {
|
|
cc.logFn(map[string]interface{}{"contentType": "brand", "method": "updateCacheForContentTypeAndEntity", "clientMode": cc.clientMode, "entityID": entityID}, LogInfo, InfoUpdatedEntityCache)
|
|
}
|
|
|
|
case ContentTypeCategory:
|
|
if entityPayload != nil {
|
|
entityEntry = entityPayload.(*contentful.Entry)
|
|
}
|
|
err := cc.cacheCategoryByID(ctx, entityID, entityEntry, entryDelete)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo && entityPayload == nil {
|
|
cc.logFn(map[string]interface{}{"contentType": "category", "method": "updateCacheForContentTypeAndEntity", "clientMode": cc.clientMode, "entityID": entityID}, LogInfo, InfoUpdatedEntityCache)
|
|
}
|
|
|
|
case ContentTypeProduct:
|
|
if entityPayload != nil {
|
|
entityEntry = entityPayload.(*contentful.Entry)
|
|
}
|
|
err := cc.cacheProductByID(ctx, entityID, entityEntry, entryDelete)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cc.logFn != nil && cc.logLevel <= LogInfo && entityPayload == nil {
|
|
cc.logFn(map[string]interface{}{"contentType": "product", "method": "updateCacheForContentTypeAndEntity", "clientMode": cc.clientMode, "entityID": entityID}, LogInfo, InfoUpdatedEntityCache)
|
|
}
|
|
|
|
}
|
|
allTags, err := cc.getAllTags(ctx, false)
|
|
if err != nil {
|
|
return fmt.Errorf("syncCache failed for tags: %s, clientMode: %s", err.Error(), cc.clientMode)
|
|
}
|
|
cc.cacheMutex.sharedDataGcLock.Lock()
|
|
cc.Cache.tags = allTags
|
|
cc.cacheMutex.sharedDataGcLock.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func commonGetParents(ctx context.Context, cc *ContentfulClient, id string, contentTypes []string) (parents []EntryReference, err error) {
|
|
parents = []EntryReference{}
|
|
cc.cacheMutex.sharedDataGcLock.RLock()
|
|
cacheInit := cc.cacheInit
|
|
cc.cacheMutex.sharedDataGcLock.RUnlock()
|
|
if cacheInit {
|
|
cc.cacheMutex.parentMapGcLock.RLock()
|
|
defer cc.cacheMutex.parentMapGcLock.RUnlock()
|
|
if len(contentTypes) != 0 {
|
|
for _, parent := range cc.Cache.parentMap[id] {
|
|
for _, contentType := range contentTypes {
|
|
if parent.ContentType == contentType {
|
|
parents = append(parents, parent)
|
|
}
|
|
}
|
|
}
|
|
return parents, nil
|
|
}
|
|
return cc.Cache.parentMap[id], nil
|
|
}
|
|
col := cc.Client.Entries.List(ctx, cc.SpaceID)
|
|
col.Query.Equal("links_to_entry", id).Locale("*")
|
|
_, err = col.GetAll()
|
|
if err != nil {
|
|
return nil, errors.New("GetParents: " + err.Error())
|
|
}
|
|
for _, item := range col.Items {
|
|
var entry contentful.Entry
|
|
byteArray, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(byteArray, &entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(contentTypes) == 1 && contentTypes[0] != entry.Sys.ContentType.Sys.ID {
|
|
continue
|
|
}
|
|
switch entry.Sys.ContentType.Sys.ID {
|
|
case ContentTypeBrand:
|
|
var parentVO CfBrand
|
|
byteArray, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, errors.New("GetParents: " + err.Error())
|
|
}
|
|
err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&parentVO)
|
|
if err != nil {
|
|
return nil, errors.New("GetParents: " + err.Error())
|
|
}
|
|
parentVO.CC = cc
|
|
parents = append(
|
|
parents, EntryReference{
|
|
ContentType: entry.Sys.ContentType.Sys.ID,
|
|
ID: entry.Sys.ID,
|
|
VO: &parentVO,
|
|
CC: cc,
|
|
})
|
|
|
|
case ContentTypeCategory:
|
|
var parentVO CfCategory
|
|
byteArray, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, errors.New("GetParents: " + err.Error())
|
|
}
|
|
err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&parentVO)
|
|
if err != nil {
|
|
return nil, errors.New("GetParents: " + err.Error())
|
|
}
|
|
parentVO.CC = cc
|
|
parents = append(
|
|
parents, EntryReference{
|
|
ContentType: entry.Sys.ContentType.Sys.ID,
|
|
ID: entry.Sys.ID,
|
|
VO: &parentVO,
|
|
CC: cc,
|
|
})
|
|
|
|
case ContentTypeProduct:
|
|
var parentVO CfProduct
|
|
byteArray, err := json.Marshal(item)
|
|
if err != nil {
|
|
return nil, errors.New("GetParents: " + err.Error())
|
|
}
|
|
err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&parentVO)
|
|
if err != nil {
|
|
return nil, errors.New("GetParents: " + err.Error())
|
|
}
|
|
parentVO.CC = cc
|
|
parents = append(
|
|
parents, EntryReference{
|
|
ContentType: entry.Sys.ContentType.Sys.ID,
|
|
ID: entry.Sys.ID,
|
|
VO: &parentVO,
|
|
CC: cc,
|
|
})
|
|
}
|
|
}
|
|
return parents, nil
|
|
}
|
|
|
|
// Unicode clean-up
|
|
|
|
func cleanUpStringField(field map[string]string) map[string]string {
|
|
cleanField := map[string]string{}
|
|
for locale, value := range field {
|
|
cleanField[locale] = stripInvisibleUnicodeChars(value)
|
|
}
|
|
return cleanField
|
|
}
|
|
|
|
func cleanUpStringSliceField(field map[string][]string) map[string][]string {
|
|
cleanField := map[string][]string{}
|
|
for locale, value := range field {
|
|
cleanLocalizedSliceElems := []string{}
|
|
for _, sliceElem := range value {
|
|
cleanLocalizedSliceElems = append(cleanLocalizedSliceElems, stripInvisibleUnicodeChars(sliceElem))
|
|
}
|
|
cleanField[locale] = cleanLocalizedSliceElems
|
|
}
|
|
return cleanField
|
|
}
|
|
|
|
func cleanUpRichTextField(field map[string]interface{}) map[string]interface{} {
|
|
cleanField := map[string]interface{}{}
|
|
for locale, value := range field {
|
|
node, err := objectToRichTextGenericNode(value)
|
|
if err != nil {
|
|
return field
|
|
}
|
|
cleanNode := cleanUpRichTextIterateNode(node)
|
|
cleanField[locale] = cleanNode
|
|
}
|
|
return cleanField
|
|
}
|
|
|
|
func cleanUpRichTextIterateNode(node *RichTextGenericNode) *RichTextGenericNode {
|
|
cleanNode := &RichTextGenericNode{
|
|
NodeType: node.NodeType,
|
|
Data: node.Data,
|
|
Value: stripInvisibleUnicodeChars(node.Value),
|
|
Marks: node.Marks,
|
|
}
|
|
for _, childNode := range node.Content {
|
|
cleanNode.Content = append(cleanNode.Content, cleanUpRichTextIterateNode(childNode))
|
|
}
|
|
return cleanNode
|
|
}
|
|
|
|
func isFieldRichText(field map[string]interface{}) bool {
|
|
for _, value := range field {
|
|
if value == nil {
|
|
continue
|
|
}
|
|
node, err := objectToRichTextGenericNode(value)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if node.NodeType == "document" && node.Content != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func objectToRichTextGenericNode(value interface{}) (*RichTextGenericNode, error) {
|
|
node := &RichTextGenericNode{}
|
|
byt, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(byt, node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
func stripInvisibleUnicodeChars(dirty string) string {
|
|
clean := strings.Map(func(r rune) rune {
|
|
if unicode.IsGraphic(r) || unicode.IsControl(r) {
|
|
return r
|
|
}
|
|
return -1
|
|
}, dirty)
|
|
return clean
|
|
}
|
|
|
|
func (rawFields RawFields) GetChildIDs() (childIDs []string) {
|
|
for fieldKey, localizedField := range rawFields {
|
|
if !strings.HasSuffix(fieldKey, "_") {
|
|
continue
|
|
}
|
|
byt, err := json.Marshal(localizedField)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
fieldMap := map[string][]*ContentTypeSys{}
|
|
err = json.Unmarshal(byt, &fieldMap)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
for _, fieldValue := range fieldMap {
|
|
if fieldValue != nil {
|
|
for _, childItem := range fieldValue {
|
|
childIDs = append(childIDs, childItem.Sys.ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|