UNPKG

@stackbit/cms-sanity

Version:
301 lines (273 loc) 10.6 kB
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;