@nacelle/compatibility-connector
Version:
Connect @nacelle/client-js-sdk to Nacelle's v2 back end with minimal code changes
195 lines (177 loc) • 5.88 kB
text/typescript
import {
destructureNacelleEntryId,
reconstructContentfulEntry,
toUnixTimestamp,
transformArticleLists,
transformAuthor,
transformMedia,
transformRelatedArticles,
transformSanityContent,
transformSourceEntryId
} from '.';
import type { NacelleContent } from '@nacelle/types';
import type { Content } from 'storefrontSdkV1';
import type { SourceName } from './destructureNacelleEntryId';
export interface AnyObject {
[key: string]: AnyObject | string | unknown;
}
export interface NacelleContentEntry {
fields?: AnyObject;
type?: string;
nacelleEntryId?: string;
tags?: string[];
}
export const isObject = (x: unknown): x is AnyObject =>
Boolean(x) && typeof x === 'object' && !Array.isArray(x);
/**
* Determine if an object is a Nacelle v2 `Content` entry.
*/
export const isNacelleContentEntry = (entry: {
nacelleEntryId?: string;
type?: string;
fields?: object;
}): entry is NacelleContentEntry =>
isObject(entry) &&
typeof entry.nacelleEntryId === 'string' &&
typeof entry.type === 'string' &&
typeof entry.fields === 'object';
interface ContentTransformers<CmsContent> {
/** A transformer for Nacelle v2 `NacelleReference`s to top-level content */
entries?: (x: NacelleContentEntry) => CmsContent;
/** A transformer for arbitrary content */
arbitraryContent?: (x: AnyObject) => CmsContent;
}
/**
* Apply content transformation functions to content. This takes care of the boilerplate
* associated with applying transformations to arrays and objects, which allows the
* CMS-specific transformer utilities to be less complex.
* @param content A single content object or array of arbitrary content objects.
* @param transformers Functions that transform top-level content entries and arbitrary content.
* @returns Content that's been transformed into a Nacelle v1 shape.
*/
export function applyContentTransformation<CmsContent>(
content: AnyObject | AnyObject[],
transformers: ContentTransformers<CmsContent>
): NacelleContentEntry | NacelleContentEntry[] | CmsContent {
if (Array.isArray(content)) {
return content.map(
(entry) =>
applyContentTransformation(entry, transformers) as NacelleContentEntry
);
}
if (isObject(content)) {
for (const property of Object.keys(content)) {
const key = property as keyof NacelleContentEntry;
if (isObject(content[key]) || Array.isArray(content[key])) {
content[key] = applyContentTransformation(
content[key] as AnyObject,
transformers
);
}
}
if (isNacelleContentEntry(content) && transformers.entries) {
// `content` is a reference to a top-level content entry
return transformers.entries(content);
} else if (transformers.arbitraryContent) {
// `content` is arbitrary content
return transformers.arbitraryContent(content);
}
}
return content;
}
/**
* Apply CMS-specific transformations to content.
*/
function applyCmsTransforms(data: AnyObject | AnyObject[], cms: SourceName) {
switch (cms) {
case 'CONTENTFUL': {
return applyContentTransformation(data, {
// With Contentful content, we only want to reconstruct top-level
// content entries (from `NacelleReference`s) with `sys` metadata.
// Any other flattening and transformation is handled by other
// (more specialized) transformers like `transformAuthor`.
entries: reconstructContentfulEntry
});
}
case 'SANITY': {
return applyContentTransformation(data, {
// With Sanity content, we can apply the same transformation to
// top-level content entries (from `NacelleReference`s) and arbitrary
// content data.
arbitraryContent: transformSanityContent,
entries: transformSanityContent
});
}
default: {
return data;
}
}
}
export function transformContent(
entries: Content[],
locale: string
): NacelleContent[] {
return entries
.filter((entry) => entry.handle)
.map((entry) => {
const {
title,
handle,
createdAt,
updatedAt,
indexedAt,
type,
sourceEntryId,
nacelleEntryId
} = entry;
const { sourceName: cms } = destructureNacelleEntryId(nacelleEntryId);
const {
articles = null,
author = null,
blogHandle = null,
collectionHandle = null,
content = null,
contentHtml = null,
description = null,
excerpt = null,
featuredMedia = null,
publishDate = null,
relatedArticles = null,
sections = null,
tags = null,
...fields
} = entry.fields || {};
const contentIsString = typeof content === 'string';
if (content && !contentIsString && typeof content === 'object') {
fields.content = content;
}
return {
articleLists: transformArticleLists(articles, blogHandle, locale),
author: transformAuthor(author, cms),
blogHandle,
cmsSyncSource: cms.toLowerCase(),
cmsSyncSourceDomain: '',
cmsSyncSourceContentId: transformSourceEntryId(sourceEntryId),
collectionHandle,
content: contentIsString ? content : null,
contentHtml,
createdAt,
description,
excerpt,
featuredMedia: transformMedia(featuredMedia, cms),
fields: applyCmsTransforms(fields, cms),
globalHandle: `${handle}::${locale}`,
handle: handle as string,
id: nacelleEntryId,
indexedAt: indexedAt || 0,
locale,
publishDate: toUnixTimestamp(publishDate) as number,
relatedArticles: transformRelatedArticles(relatedArticles, cms),
sections: applyCmsTransforms(sections, cms),
tags,
title,
type: type || '',
updatedAt
};
});
}