@stackbit/cms-contentful
Version:
Stackbit Contentful CMS Interface
344 lines (309 loc) • 12.1 kB
JavaScript
const _ = require('lodash');
const { RICH_TEXT_HINT_MAX_LENGTH } = require('@stackbit/types');
const { IMAGE_MODEL } = require('@stackbit/cms-core');
const ITEM_TYPES = {
ENTRY: 'Entry',
ASSET: 'Asset',
LINK: 'Link'
};
class ContentfulEncoderDelegate {
constructor({ schema, noEncodeFields = [], omitFields = [], encodedFieldTypes = null, defaultLocale = 'en-US', users = [] }) {
this.schema = schema;
this.models = schema.models;
this.modelsByName = _.keyBy(this.models, 'name');
this.noEncodeFields = noEncodeFields;
this.omitFields = omitFields;
this.encodedFieldTypes = encodedFieldTypes;
this.defaultLocale = defaultLocale;
this.users = users;
}
getModelsByName() {
return this.modelsByName;
}
getEncodedFieldTypes() {
return this.encodedFieldTypes;
}
getNoEncodeFields() {
return this.noEncodeFields;
}
getItemId(item) {
return _.get(item, 'sys.id');
}
getReferenceId(item) {
// in Contentful, reference fields store id of the object they reference
// in the same way an object stores its own id
return this.getItemId(item);
}
getItemType(item) {
return _.get(item, 'sys.type');
}
getItemModelName(item) {
return _.get(item, 'sys.contentType.sys.id', null);
}
itemIsMultiLocale(item) {
// https://www.contentful.com/developers/docs/references/content-preview-api/#/reference/localization/retrieve-localized-entries
// If the result contains only a single locale, resources will include the property sys.locale indicating the locale of that object.
return !_.has(item, 'sys.locale');
}
getItemLocale(item) {
return _.get(item, 'sys.locale');
}
getModelForRootItem(rootItem) {
const itemType = this.getItemType(rootItem);
if (itemType === ITEM_TYPES.ENTRY) {
const modelName = this.getItemModelName(rootItem);
const modelsByName = this.getModelsByName();
return _.get(modelsByName, modelName, null);
} else if (itemType === ITEM_TYPES.ASSET) {
return IMAGE_MODEL;
} else if (itemType === ITEM_TYPES.LINK) {
// not supporting links
return null;
}
// not supporting anything else
return null;
}
isLinkItem(item) {
const itemType = this.getItemType(item);
return itemType === ITEM_TYPES.LINK;
}
getModelForItemOfReferenceType(item) {
return this.getModelForRootItem(item);
}
getUserEmailById(userId) {
const user = _.find(this.users, { sys: { id: userId } });
if (!user) {
return null;
}
return user.email;
}
getItemMetadata(item, model) {
const itemType = this.getItemType(item);
const itemId = this.getItemId(item);
const spaceId = _.get(item, 'sys.space.sys.id');
const environment = _.get(item, 'sys.environment.sys.id');
let envParamPair = '';
if (environment && environment !== 'master') {
envParamPair = `/environments/${environment}`;
}
let type;
let srcObjectUrl;
let srcModelName;
let srcModelLabel;
let label;
if (itemType === ITEM_TYPES.ENTRY) {
type = 'object';
label = this.getItemLabelFieldValue(item, model);
srcObjectUrl = `https://app.contentful.com/spaces/${spaceId}${envParamPair}/entries/${itemId}`;
srcModelName = this.getItemModelName(item);
srcModelLabel = _.get(model, 'label', null);
} else if (itemType === ITEM_TYPES.ASSET) {
type = 'image';
label = this.getItemLabelFieldValue(item, model);
srcObjectUrl = `https://app.contentful.com/spaces/${spaceId}${envParamPair}/assets/${itemId}`;
srcModelName = IMAGE_MODEL.name;
srcModelLabel = 'Image';
} else {
return null;
}
// currently we only expose isChanged that is treated as isDraft and isChanged because in Sanity there is no way to tell the difference
const isDraft = !_.get(item, 'sys.publishedVersion') && !_.get(item, 'sys.archivedVersion');
const isChanged = !!_.get(item, 'sys.publishedVersion') && _.get(item, 'sys.version') >= _.get(item, 'sys.publishedVersion') + 2;
const status = isDraft ? 'added' : isChanged ? 'modified' : 'published';
const createdByEmail = this.getUserEmailById(_.get(item, 'sys.createdBy.sys.id'));
const updatedByEmail = this.getUserEmailById(_.get(item, 'sys.updatedBy.sys.id'));
const updatedByList = updatedByEmail ? [updatedByEmail] : [];
return {
type: type,
isChanged: isDraft || isChanged,
status,
createdAt: _.get(item, 'sys.createdAt', null),
createdBy: createdByEmail,
updatedAt: _.get(item, 'sys.updatedAt', null),
updatedBy: updatedByList,
srcType: 'contentful',
srcProjectId: spaceId,
srcProjectUrl: `https://app.contentful.com/spaces/${spaceId}/home`,
srcEnvironment: environment,
srcObjectId: itemId,
srcObjectUrl: srcObjectUrl,
srcObjectLabel: label,
srcModelName: srcModelName,
srcModelLabel: srcModelLabel,
srcFieldNames: null
};
}
getItemLabelFieldValue(item, model) {
const labelField = _.get(model, 'labelField');
let label = null;
if (labelField) {
const fields = _.get(item, 'fields');
const field = _.get(fields, labelField, null);
const multiLocale = this.itemIsMultiLocale(item);
label = multiLocale ? this.getFirstLocalizedFieldValue(field) : field;
}
if (!label) {
label = _.get(model, 'label', null);
}
if (!label && model.name) {
label = _.startCase(model.name);
}
return label;
}
getItemFields(item, model) {
const itemType = this.getItemType(item);
if (!_.includes([ITEM_TYPES.ENTRY, ITEM_TYPES.ASSET], itemType)) {
return null;
}
// get item fields
const itemFields = _.get(item, 'fields');
const multiLocale = this.itemIsMultiLocale(item);
// get model fields, for code consistency we set field locale to en-US, but it will not be used anywhere
// because model fields that do not exist in item fields are not encoded
const modelFields = _.transform(
model.fields,
(accum, fieldModel) => {
_.set(accum, fieldModel.name, multiLocale ? { [this.defaultLocale]: null } : null);
},
{}
);
// merge model fields with item fields
const mergedFields = _.assign(modelFields, itemFields);
// remove omitted fields
const fields = _.omit(mergedFields, this.omitFields);
const getEncodedDataPathAndKey = (item, model, key, locale) => {
const encodedDataPath = ['fields', key];
if (multiLocale) {
encodedDataPath.push(locale);
}
// rewrite encodedDataPath with locale if needed
// Contentful's assets store the url inside fields.file.url, and with
// locale it is fields.file[locale].url (fields.file['en-US'].url)
if (model.type === 'image' && key === 'file') {
encodedDataPath.push('url');
}
const value = _.get(item, encodedDataPath);
return { value, encodedDataPath, locale };
};
return _.map(fields, (fieldValue, key) => {
const name = key;
let fieldRes;
// Our internal image model stores image url in 'fields.url' field,
// while Contentful's assets store the url in 'fields.file.url'.
// Update the 'key' so the encoded path will be pointing to 'fields.file'
if (model.type === 'image' && key === 'url') {
key = 'file';
}
if (!multiLocale) {
const locale = this.getItemLocale(item);
fieldRes = getEncodedDataPathAndKey(item, model, key, locale);
} else {
const fieldLocales = this.getFieldLocales(fieldValue);
if (fieldLocales.length > 1) {
fieldRes = {
locales: _.map(fieldLocales, (locale) => {
return getEncodedDataPathAndKey(item, model, key, locale);
})
};
} else if (fieldLocales.length === 1) {
const locale = _.head(fieldLocales);
fieldRes = getEncodedDataPathAndKey(item, model, key, locale);
}
}
return {
name: name,
unset: !_.has(itemFields, key), // use the altered key
...fieldRes
};
});
}
getFirstLocalizedFieldValue(field) {
const firstLocale = this.getFirstFieldLocale(field);
return _.get(field, firstLocale, null);
}
getFirstFieldLocale(field) {
const locales = this.getFieldLocales(field);
return _.head(locales);
}
getFieldLocales(field) {
return _.keys(field);
}
encodeField(fieldValue, fieldModel) {
if (!fieldValue) {
return { fieldData: { isUnset: true } };
}
if (fieldModel.type === 'richText') {
return this.encodeRichText(fieldValue);
} else if (fieldModel.type === 'image') {
if (fieldModel.source === 'cloudinary') {
return {
fieldData: {
fields: {
title: {
type: 'string',
value: fieldValue[0]?.public_id
},
url: {
type: 'string',
value: fieldValue[0]?.derived?.[0]?.secure_url ?? fieldValue[0]?.secure_url
}
}
}
};
}
return {
fieldData: {
type: 'unresolved_reference',
refId: this.getReferenceId(fieldValue),
refType: 'image'
}
};
}
return null;
}
encodeRichText(fieldValue) {
return {
encodedData: {
nodeType: 'document',
data: {},
content: [
{
nodeType: 'paragraph',
content: [
{
nodeType: 'text',
value: '',
marks: [],
data: {}
}
],
data: {}
}
]
},
encodedFieldPath: ['content', 0, 'content', 0, 'value'],
fieldData: {
hint: this.flattenRichText(fieldValue).substring(0, RICH_TEXT_HINT_MAX_LENGTH),
multiElement: true,
value: fieldValue
}
};
}
flattenRichText(node) {
if (_.get(node, 'nodeType') === 'text') {
return _.get(node, 'value', '');
}
const content = _.get(node, 'content');
if (content) {
return _.reduce(
content,
(accum, node) => {
return accum + this.flattenRichText(node);
},
''
);
}
return '';
}
}
module.exports = ContentfulEncoderDelegate;