@stackbit/cms-sanity
Version:
Stackbit Sanity CMS Interface
301 lines (273 loc) • 10.6 kB
JavaScript
const _ = require('lodash');
const { RICH_TEXT_HINT_MAX_LENGTH } = require('@stackbit/types');
const { IMAGE_MODEL } = require('@stackbit/cms-core');
class SanityEncoderDelegate {
constructor({ schema, projectId, projectUrl, dataset, noEncodeFields = [], omitFields = [], encodedFieldTypes = null }) {
this.projectId = projectId;
this.projectUrl = projectUrl;
this.dataset = dataset;
this.schema = schema;
this.models = schema.models;
this.modelsByName = _.keyBy(this.models, 'name');
this.noEncodeFields = noEncodeFields;
this.omitFields = omitFields;
this.encodedFieldTypes = encodedFieldTypes;
}
getModelsByName() {
return this.modelsByName;
}
getEncodedFieldTypes() {
return this.encodedFieldTypes;
}
getNoEncodeFields() {
return this.noEncodeFields;
}
getItemId(item) {
// root items, which are documents have an "_id" property
const itemId = _.get(item, '_id', null);
return itemId;
}
getReferenceId(item) {
// referenced objects, have "_type" = "reference" and have an "_ref" property with id of the referenced object
let refId = _.get(item, '_ref', null);
if (refId) {
refId = refId.replace(/^drafts\./, '');
}
return refId;
}
getItemType(item) {
return _.get(item, '_type');
}
getModelForRootItem(rootItem) {
const modelName = this.getItemType(rootItem);
if (modelName === 'sanity.imageAsset') {
return IMAGE_MODEL;
} else {
const modelsByName = this.getModelsByName();
return _.get(modelsByName, modelName, null);
}
}
getItemTypeForListItem(listItem, fieldModel) {
const itemModels = _.get(fieldModel, 'items');
const type = this.getItemType(listItem);
if (!type) {
const type = typeof listItem;
return _.defaults({ type: type });
}
if (type === 'reference') {
return _.find(itemModels, { type: 'reference' });
} else if (type === 'block') {
return _.find(itemModels, { type: 'richText' });
} else {
return _.find(itemModels, (itemModel) => {
if (itemModel.type === 'model') {
return _.includes(itemModel.models, type);
} else {
// if field was one of base types (object, image, slug, etc.)
// and it had a "name" property, then the "_type" will be equal to that name,
// otherwise the "_type" will be equal to the base type
return itemModel.name === type || itemModel.type === type;
}
});
}
}
isLinkItem(item) {
const itemType = this.getItemType(item);
return itemType === 'reference';
}
getModelForItemOfReferenceType(item) {
return this.getModelForRootItem(item);
}
getModelForItemOfModelsType(item) {
return this.getModelForRootItem(item);
}
getItemMetadata(item, model) {
// _id only available on root documents
const itemId = _.get(item, '_id', null);
const itemType = _.get(item, '_type', null);
if (itemId) {
// if item has an '_id' property, then it is a root object of one of following model types:
// 'document', 'sanity.imageAsset' or 'sanity.fileAsset'
const isChanged = _.startsWith(itemId, 'drafts.');
const status = isChanged ? 'modified' : 'published';
const pureObjectId = itemId.replace(/^drafts\./, '');
const isImageType = ['sanity.imageAsset', 'cloudinary.asset'].includes(itemType);
if (isImageType || itemType === 'sanity.fileAsset') {
// if _type is 'sanity.imageAsset' or 'sanity.fileAsset' then it is image or file
return {
type: isImageType ? 'image' : 'file',
isChanged,
status,
createdAt: _.get(item, '_createdAt', null),
// createdBy,
updatedAt: _.get(item, '_updatedAt', null),
// updatedBy,
srcType: 'sanity',
srcProjectId: this.projectId,
srcProjectUrl: this.projectUrl,
srcEnvironment: this.dataset,
srcObjectId: pureObjectId,
srcObjectUrl: item.url,
srcObjectLabel: this.getItemLabelFieldValue(item, model),
srcModelName: IMAGE_MODEL.name,
srcModelLabel: 'Image'
};
} else {
// otherwise, the object if of the following model types
return {
// root models can be 'document', 'object', 'array'
type: 'object',
isChanged,
status,
createdAt: _.get(item, '_createdAt', null),
// createdBy, // todo: get data about author from sanity history api
updatedAt: _.get(item, '_updatedAt', null),
// updatedBy,
srcType: 'sanity',
srcProjectId: this.projectId,
srcProjectUrl: this.projectUrl,
srcEnvironment: this.dataset,
srcObjectId: pureObjectId,
srcObjectUrl: this.projectUrl + '/desk/' + model.name + ';' + pureObjectId,
srcObjectLabel: this.getItemLabelFieldValue(item, model),
srcModelName: model.name,
srcModelLabel: model.label
};
}
}
return {
type: 'object',
srcObjectLabel: this.getItemLabelFieldValue(item, model),
srcModelName: model.name,
srcModelLabel: model.label
};
}
getItemLabelFieldValue(item, model) {
const labelField = _.get(model, 'labelField');
let label = null;
if (labelField) {
label = _.get(item, labelField, null);
}
if (!label) {
label = _.get(model, 'label', null);
}
if (!label && model.name) {
label = _.startCase(model.name);
}
return label;
}
getItemFields(item, model) {
const modelFields = _.transform(
model.fields,
(accum, fieldModel) => {
accum[fieldModel.name] = null;
},
{}
);
const mergedFields = _.assign(modelFields, item);
// omit internal fields starting with underscore
const userFields = _.omitBy(mergedFields, (value, key) => _.startsWith(key, '_'));
// remove omitted fields
const fields = _.omit(userFields, this.omitFields);
const fieldModelsByName = _.keyBy(model.fields, 'name');
return _.map(fields, (value, key) => {
const name = key;
// in sanity, field might exist on item but not on schema
const fieldModel = _.get(fieldModelsByName, key);
let encodedDataPath = [key];
// some field types can have values nested inside objects
// rewrite field encode path if needed
if (model.type === 'image' && key === 'title') encodedDataPath = ['originalFilename'];
if (_.get(fieldModel, 'type') === 'slug') {
encodedDataPath = [key, 'current'];
}
value = _.get(item, encodedDataPath);
return {
name: name,
value: value,
encodedDataPath: encodedDataPath,
unset: !_.has(item, key)
};
});
}
encodeField(fieldValue, fieldModel) {
if (!fieldValue) {
return { fieldData: { isUnset: true } };
}
if (fieldModel.type === 'richText') {
return this._encodeRichText(fieldValue);
} else if (['image', 'file'].includes(fieldModel.type)) {
if (fieldModel.source === 'cloudinary') {
return {
fieldData: {
fields: {
title: {
type: 'string',
value: fieldValue.public_id
},
url: {
type: 'string',
value: fieldValue.derived?.[0]?.secure_url ?? fieldValue.secure_url
}
}
}
};
}
return {
fieldData: {
type: 'unresolved_reference',
refId: _.get(fieldValue, 'asset._ref'),
refType: fieldModel.type
}
};
}
return null;
}
_encodeRichText(fieldValue) {
return {
encodedData: [
{
_type: 'block',
style: 'normal',
children: [
{
_type: 'span',
text: 'text'
}
]
}
],
encodedFieldPath: [0, 'children', 0, 'text'],
fieldData: {
hint: this._flattenRichText(fieldValue).substring(0, RICH_TEXT_HINT_MAX_LENGTH),
multiElement: true
}
};
}
_flattenRichText(richTextArray) {
return _.reduce(
richTextArray,
(accum, node) => {
if (_.get(node, '_type') !== 'block') {
return accum;
}
const children = _.get(node, 'children', []);
return (
accum +
_.reduce(
children,
(accum, node) => {
if (_.get(node, '_type') !== 'span') {
return accum;
}
return accum + _.get(node, 'text', '');
},
''
)
);
},
''
);
}
}
module.exports = SanityEncoderDelegate;