@stackbit/cms-sanity
Version:
Stackbit Sanity CMS Interface
664 lines • 28.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.localizedValue = exports.mapUpdateOperationFieldToSanityValue = exports.Operations = exports.convertUpdateOperation = void 0;
const lodash_1 = __importDefault(require("lodash"));
const uuid_1 = require("uuid");
const tinycolor2_1 = __importDefault(require("tinycolor2"));
const utils_1 = require("./utils");
function convertUpdateOperation({ operation, ...rest }) {
switch (operation.opType) {
case 'set':
return exports.Operations.set({ operation, ...rest });
case 'unset':
return exports.Operations.unset({ operation, ...rest });
case 'insert':
return exports.Operations.insert({ operation, ...rest });
case 'remove':
return exports.Operations.remove({ operation, ...rest });
case 'reorder':
return exports.Operations.reorder({ operation, ...rest });
}
}
exports.convertUpdateOperation = convertUpdateOperation;
exports.Operations = {
set: ({ operation, sanityDocument, getModelByName, model }) => {
const { field, fieldPath, modelField, locale } = operation;
const { patchFieldPath, modelFieldPath, localizedLeaf, isInList } = getPatchPathAndModelFieldPaths({
fieldPath,
sanityDocument,
getModelByName,
addValueToI18NLeafs: true,
allowUndefinedI18NLeafArrays: true,
locale
});
let value = mapUpdateOperationFieldToSanityValue({
updateOperationField: field,
getModelByName,
modelField,
rootModel: model,
modelFieldPath,
locale,
isInList
});
if ((0, utils_1.isLocalizedModelField)(modelField) && localizedLeaf !== 'value') {
value = localizedValue({
value,
model,
modelFieldPath,
locale
});
if (localizedLeaf === 'newItem') {
return {
insert: {
after: `${patchFieldPath}[-1]`,
items: [value]
}
};
}
else if (localizedLeaf === 'undefinedArray') {
return { set: { [patchFieldPath]: [value] } };
}
}
return { set: { [patchFieldPath]: value } };
},
unset: ({ operation, sanityDocument, getModelByName }) => {
const { fieldPath, locale } = operation;
const { patchFieldPath } = getPatchPathAndModelFieldPaths({
fieldPath,
sanityDocument,
getModelByName,
locale
});
return { unset: [patchFieldPath] };
},
insert: ({ operation, sanityDocument, getModelByName, model }) => {
const { item, fieldPath, modelField, index, locale } = operation;
const listItemModelField = modelField.items ?? { type: 'string' };
const { patchFieldPath, modelFieldPath, currentValue, localizedLeaf } = getPatchPathAndModelFieldPaths({
fieldPath,
sanityDocument,
getModelByName,
addValueToI18NLeafs: true,
allowUndefinedI18NLeafArrays: true,
locale
});
let value = mapUpdateOperationFieldToSanityValue({
updateOperationField: item,
getModelByName,
modelField: listItemModelField,
rootModel: model,
modelFieldPath: modelFieldPath.concat('items'),
locale,
isInList: true
});
// In the case of a localized array field, the field will contain an
// array of objects with localized arrays:
// [
// {
// _key: 'en',
// _type: 'internationalizedArrayLocalizedArray',
// value: ['en value 1', 'en value 2', 'en value 3']
// },
// {
// _key: 'es',
// _type: 'internationalizedArrayLocalizedArray',
// value: ['es value 1', 'es value 2', 'es value 3']
// }
// ]
if ((0, utils_1.isLocalizedModelField)(modelField) && localizedLeaf !== 'value') {
// When there is no array for a given locale, create a new localized
// array with a single value and insert it into the localized array:
// {
// _key: 'es',
// _type: 'internationalizedArrayLocalizedArray',
// value: [value]
// }
value = localizedValue({
value: [value],
model,
modelFieldPath,
locale
});
if (localizedLeaf === 'newItem') {
return {
insert: {
after: `${patchFieldPath}[-1]`,
items: [value]
}
};
}
else if (localizedLeaf === 'undefinedArray') {
return { set: { [patchFieldPath]: [value] } };
}
}
if (!currentValue) {
return { set: { [patchFieldPath]: [value] } };
}
else if (lodash_1.default.isNil(index) || index >= currentValue.length) {
return {
insert: {
after: `${patchFieldPath}[-1]`,
items: [value]
}
};
}
return {
insert: {
before: `${patchFieldPath}[${index}]`,
items: [value]
}
};
},
remove: ({ sanityDocument, operation, getModelByName }) => {
const { fieldPath, index, locale } = operation;
const { patchFieldPath } = getPatchPathAndModelFieldPaths({
fieldPath: fieldPath.concat(index),
sanityDocument,
getModelByName,
locale
});
return {
unset: [patchFieldPath]
};
},
reorder: ({ sanityDocument, operation, getModelByName }) => {
const { fieldPath, order, locale } = operation;
const { patchFieldPath, currentValue } = getPatchPathAndModelFieldPaths({
fieldPath,
sanityDocument,
getModelByName,
addValueToI18NLeafs: true,
locale
});
const newEntryArr = order.map((newIndex) => currentValue[newIndex]);
return { set: { [patchFieldPath]: newEntryArr } };
}
};
function mapUpdateOperationFieldToSanityValue({ updateOperationField, getModelByName, modelField, rootModel, modelFieldPath, locale, isInList }) {
switch (updateOperationField.type) {
case 'string':
case 'url':
case 'text':
case 'markdown':
case 'html':
case 'boolean':
case 'date':
case 'datetime':
case 'enum':
case 'style':
case 'json':
case 'richText':
case 'file': {
return updateOperationField.value;
}
case 'number': {
return Number(updateOperationField.value);
}
case 'slug': {
return {
_type: (0, utils_1.getSanityAliasFieldType)({
resolvedType: 'slug',
model: rootModel,
modelFieldPath
}),
current: updateOperationField.value
};
}
case 'color': {
const color = (0, tinycolor2_1.default)(updateOperationField.value);
return {
_type: (0, utils_1.getSanityAliasFieldType)({
resolvedType: 'color',
model: rootModel,
modelFieldPath
}),
hex: color.toHexString(),
alpha: color.getAlpha(),
hsl: {
_type: 'hslaColor',
...color.toHsl()
},
hsv: {
_type: 'hsvaColor',
...color.toHsv()
},
rgb: {
_type: 'rgbaColor',
...color.toRgb()
}
};
}
case 'image': {
const value = updateOperationField?.value;
if (modelField.type === 'image') {
if (modelField.source === 'cloudinary' || modelField.source === 'aprimo') {
const type = modelField.source === 'cloudinary' ? 'cloudinary.asset' : 'aprimo.cdnasset';
return addKeyIfInList({
_type: type,
...value
}, isInList);
}
else if (modelField.source === 'bynder') {
let imageValue = value;
if (imageValue?.__typename) {
imageValue = lodash_1.default.omitBy({
id: value.id,
name: value.name,
databaseId: value.databaseId,
type: value.type,
previewUrl: value.type === 'VIDEO' ? value.previewUrls[0] : value.files.webImage.url,
previewImg: value.files.webImage.url,
datUrl: value.files.transformBaseUrl?.url,
videoUrl: value.type === 'VIDEO' ? value.files.original?.url : null,
description: value.description,
aspectRatio: value.height / value.width
}, lodash_1.default.isUndefined);
}
return addKeyIfInList({
_type: 'bynder.asset',
...imageValue
}, isInList);
}
}
// TODO: there is a bug right now because documentField is inferred from the model which is an "image", not reference
return addKeyIfInList(linkForAssetId(value), isInList);
}
case 'object': {
if (modelField.type !== 'object') {
throw new Error(`Operation field type 'object' does not match model field type '${modelField.type}'.`);
}
// Sanity array fields may consist of anonymous 'object' with names.
// When creating such objects, the '_type' should be set to their
// name to identify them among other 'object' types.
const fieldAlias = rootModel.context?.fieldAliasMap?.[modelFieldPath.join('.')] ?? [];
const typeName = fieldAlias?.find((alias) => alias.resolvedTypeName === 'object')?.origTypeName;
const object = addKeyIfInList({
...(typeName ? { _type: typeName } : null)
}, isInList);
return lodash_1.default.reduce(updateOperationField.fields, (result, childUpdateOperationField, fieldName) => {
const childModelField = lodash_1.default.find(modelField.fields, (field) => field.name === fieldName);
if (!childModelField) {
throw new Error(`No model field found for field '${fieldName}' in model '${rootModel.name}' at field path ${modelFieldPath.join('.')}.`);
}
const childModelFieldPath = modelFieldPath.concat(fieldName);
const value = mapUpdateOperationFieldToSanityValue({
updateOperationField: childUpdateOperationField,
getModelByName,
modelField: childModelField,
rootModel,
modelFieldPath: childModelFieldPath,
locale
});
if ((0, utils_1.isLocalizedModelField)(childModelField)) {
lodash_1.default.set(result, fieldName, [
localizedValue({
value,
model: rootModel,
modelFieldPath: childModelFieldPath,
locale
})
]);
}
else {
lodash_1.default.set(result, fieldName, value);
}
return result;
}, object);
}
case 'model': {
if (modelField.type !== 'model') {
throw new Error(`Operation field type 'model' does not match model field type '${modelField.type}'.`);
}
const modelName = updateOperationField.modelName;
const childModel = getModelByName(modelName);
if (!childModel) {
throw new Error(`No model '${modelName}' was found for field at '${modelFieldPath.join('.')}' in model '${rootModel.name}'.`);
}
const object = addKeyIfInList({
_type: (0, utils_1.getSanityAliasFieldType)({
resolvedType: modelName,
model: rootModel,
modelFieldPath
})
}, isInList);
return lodash_1.default.reduce(updateOperationField.fields, (result, childUpdateOperationField, fieldName) => {
const childModelField = lodash_1.default.find(childModel?.fields, (field) => field.name === fieldName);
if (!childModelField) {
throw new Error(`No model field found for field '${fieldName}' in model '${childModel.name}'.`);
}
const childModelFieldPath = [fieldName];
const value = mapUpdateOperationFieldToSanityValue({
updateOperationField: childUpdateOperationField,
getModelByName,
modelField: childModelField,
rootModel: childModel,
modelFieldPath: childModelFieldPath,
locale
});
if ((0, utils_1.isLocalizedModelField)(childModelField)) {
lodash_1.default.set(result, fieldName, [
localizedValue({
value,
model: rootModel,
modelFieldPath: childModelFieldPath,
locale
})
]);
}
else {
lodash_1.default.set(result, fieldName, value);
}
return result;
}, object);
}
case 'reference': {
const value = updateOperationField.refType === 'document'
? {
_ref: updateOperationField.refId,
_type: (0, utils_1.getSanityAliasFieldType)({
resolvedType: 'reference',
model: rootModel,
modelFieldPath
}),
// TODO: this is a bug!
// The _weak is not always `true`. Lookup the referenced document
// by id, and the original model field's `weak` value.
// If the referenced document was published (status === 'published'
// or status === 'modified'), then the _weak value should be set to
// the `weak` value defined in Sanity model field's.
// Otherwise, if the document was not published (status === 'added'),
// then the _weak value should be set to `true` and the
// _strengthenOnPublish.weak should be set to the `weak` value
// defined in the model field's.
_weak: true,
// TODO: Lookup the referenced document by id, and the original model
// field's `weak` value. If the referenced document was never published
// (status === 'added'), then add the _strengthenOnPublish object and
// set its `weak` property to the model field's `weak` value.
// When publishing objects with reference fields having the
// _strengthenOnPublish object, update the field's _weak with that of
// _strengthenOnPublish.weak.
// For more info: https://www.sanity.io/blog/obvious-features-aren-t-obviously-made#2c38c9f38060
_strengthenOnPublish: {
// type: <type for referenced item>,
// weak: <the _weak value of the original field>
}
}
: linkForAssetId(updateOperationField.refId);
return addKeyIfInList(value, isInList);
}
case 'cross-reference': {
throw new Error('Sanity crossDatasetReference fields not supported.');
}
case 'list': {
if (modelField.type !== 'list') {
throw new Error(`Operation field type 'list' does not match model field type '${modelField.type}'.`);
}
return updateOperationField.items.map((item, index) => {
let listItemModelField = modelField.items;
if (lodash_1.default.isArray(modelField.items)) {
const itemModel = modelField.items.find((listItemsModel) => listItemsModel.type === item.type);
if (!itemModel) {
throw new Error(`No list item model found for item type '${item.type}' in model '${rootModel.name}' at field path ${modelFieldPath.join('.')}.`);
}
listItemModelField = itemModel;
}
return mapUpdateOperationFieldToSanityValue({
updateOperationField: item,
getModelByName,
modelField: listItemModelField,
rootModel,
modelFieldPath: modelFieldPath.concat('items'),
locale,
isInList: true
});
});
}
default: {
const _exhaustiveCheck = updateOperationField;
return _exhaustiveCheck;
}
}
}
exports.mapUpdateOperationFieldToSanityValue = mapUpdateOperationFieldToSanityValue;
function addKeyIfInList(object, isInList) {
if (isInList) {
lodash_1.default.set(object, '_key', (0, uuid_1.v4)());
}
return object;
}
/**
* In Sanity, a localized field is represented by an array of objects containing the localized field values.
* Each object has three properties:
* - `_key` holds the field's locale
* - `_type` holds the type of the localized value
* - `value` holds the localized value.
*
* Note: the `_type` is not the regular type such as `string` or `object`,
* but a special localized type generated by the Sanity Internationalized Array plugin.
* The map between the localized fields and these types is stored in the model's context.
* title: [
* {
* _key: 'us',
* _type: 'internationalizedArrayStringValue',
* value: 'hello',
* }, {
* _key: 'es',
* _type: 'internationalizedArrayStringValue',
* value: 'hola',
* }
* ]
*/
function localizedValue({ value, model, modelFieldPath, locale }) {
const localizedFieldModelNameMap = model.context?.localizedFieldsModelMap?.[modelFieldPath.join('.')];
if (!localizedFieldModelNameMap) {
throw Error(`Internationalized array model for localized field at path ${modelFieldPath.join('.')} of model ${model.name} not found.`);
}
if (!locale) {
throw Error(`No locale provided for localized field at path ${modelFieldPath.join('.')} of model ${model.name}.`);
}
return {
_key: locale,
_type: localizedFieldModelNameMap.arrayValueModelName,
value: value
};
}
exports.localizedValue = localizedValue;
function linkForAssetId(assetId) {
return {
_type: 'image',
asset: {
_ref: assetId,
_type: 'reference'
}
};
}
/**
* Receives a `fieldPath` of a target field in a `sanityDocument` and returns
* an object with following properties:
*
* - `model`: The closest ancestor model of the target field.
* - `modelFieldPath`: The field path of the target field from the `model`.
* The model's field path doesn't identify a specific document field,
* therefore it does not include list indexes.
* - `patchFieldPath`: A Sanity specific field path for patching fields.
* For array items, the field path will include [_key="..."] when possible.
* For localized fields, which represented by arrays in Sanity, the path
* will point to the localized array item using the `_key`:
* `sections[_key=="es"].value[3].title`
* - `localizedLeaf`: A string specifying the leaf value in the `patchFieldPath`
* when the target field is localized:
* - `value`: The `patchFieldPath` points to the localized value:
* `sections[_key=="es"].value
* - `newItem`: The `patchFieldPath` points to the existing localized array,
* which does not include an item for the provided locale.
* Returned only when `allowUndefinedI18NLeafArrays` set to `true`.
* - `undefinedArray`: The `patchFieldPath` points to undefined localized array.
* Returned only when `allowUndefinedI18NLeafArrays` set to `true`.
* - `currentValue`: The value of the target field. If the target field is
* localized but doesn't have a value for the provided locale,
* the `currentValue` will be undefined.
*/
function getPatchPathAndModelFieldPaths({ fieldPath, sanityDocument, getModelByName, addValueToI18NLeafs, allowUndefinedI18NLeafArrays, locale }) {
function iterateObject({ object, model, rootModel, modelFieldPath, fieldPath, first = false }) {
const [fieldName, ...fieldPathTail] = fieldPath;
if (typeof fieldName === 'undefined') {
throw new Error('The fieldPath cannot be empty.');
}
const modelField = (model.fields ?? []).find((field) => field.name === fieldName);
if (!modelField) {
throw new Error(`Model field for field '${fieldName}' not found.`);
}
let patchFieldPath = '';
if (/\W/.test(fieldName)) {
// field name is a string with non-alphanumeric characters
patchFieldPath += `['${fieldName}']`;
}
else {
if (!first) {
patchFieldPath += '.';
}
patchFieldPath += fieldName;
}
let value = object[fieldName];
let localizedLeaf;
if (modelField?.localized) {
if (Array.isArray(value)) {
const localizedItem = value.find((item) => item._key === locale);
if (localizedItem) {
patchFieldPath += `[_key=="${locale}"]`;
if (fieldPathTail.length === 0) {
localizedLeaf = 'value';
if (addValueToI18NLeafs) {
patchFieldPath += '.value';
}
}
else {
patchFieldPath += '.value';
}
value = localizedItem.value;
}
else if (fieldPathTail.length === 0 && allowUndefinedI18NLeafArrays) {
localizedLeaf = 'newItem';
value = undefined;
}
else {
throw new Error(`The localized field '${fieldName}' has no value for locale '${locale}'.`);
}
}
else if (fieldPathTail.length === 0 && allowUndefinedI18NLeafArrays) {
localizedLeaf = 'undefinedArray';
value = undefined;
}
else {
throw new Error(`The localized field '${fieldName}' has no localization array defined for locale '${locale}'.`);
}
}
const result = iterateField({
value,
modelField,
fieldPath: fieldPathTail,
rootModel,
modelFieldPath: modelFieldPath.concat(fieldName),
isInList: false
});
return {
model: result.model,
modelFieldPath: result.modelFieldPath,
patchFieldPath: patchFieldPath + result.patchFieldPath,
localizedLeaf: localizedLeaf ?? result.localizedLeaf,
currentValue: result.currentValue,
isInList: result.isInList
};
}
function iterateField({ value, modelField, fieldPath, rootModel, modelFieldPath, isInList }) {
if (fieldPath.length === 0) {
return {
patchFieldPath: '',
currentValue: value,
model: rootModel,
modelFieldPath,
isInList: isInList
};
}
if (typeof value === 'undefined') {
throw new Error(`Field path has more items [${fieldPath.join('.')}], but value is undefined.`);
}
if (modelField?.type === 'object') {
return iterateObject({
object: value,
model: modelField,
rootModel,
modelFieldPath,
fieldPath
});
}
else if (modelField?.type === 'model') {
const modelName = (0, utils_1.resolvedFieldType)({
sanityFieldType: value._type,
model: rootModel,
modelFieldPath
});
const model = getModelByName(modelName);
if (!model) {
throw new Error(`Model '${modelName}' not found.`);
}
return iterateObject({
object: value,
model,
rootModel: model,
modelFieldPath: [],
fieldPath
});
}
else if (modelField?.type === 'list') {
const [itemIndex, ...fieldPathTail] = fieldPath;
const listItem = value[itemIndex];
const itemModel = (0, utils_1.getItemTypeForListItem)(listItem, modelField);
if (!itemModel) {
throw new Error('Could not resolve type of a list item.');
}
const result = iterateField({
value: listItem,
modelField: itemModel,
rootModel,
modelFieldPath: modelFieldPath.concat('items'),
fieldPath: fieldPathTail,
isInList: true
});
// try to use Sanity _key as explicit accessor
const key = listItem?._key;
const patchFieldPath = key ? `[_key=="${key}"]` : `[${Number(itemIndex)}]`;
return {
// model fieldPath doesn't include array indexes.
// return fieldPath with field names only, no list indexes
model: result.model,
modelFieldPath: result.modelFieldPath,
// fieldPath for Sanity patches should always include array indexes.
patchFieldPath: patchFieldPath + result.patchFieldPath,
currentValue: result.currentValue,
isInList: result.isInList
};
}
else {
throw new Error(`Field path has more items [${fieldPath.join('.')}], but no objects/arrays left to iterate.`);
}
}
const modelName = sanityDocument._type;
const model = getModelByName(modelName);
if (!model) {
throw new Error(`Model '${modelName}' not found.`);
}
return iterateObject({
object: sanityDocument,
model,
rootModel: model,
modelFieldPath: [],
fieldPath,
first: true
});
}
//# sourceMappingURL=sanity-operation-converter.js.map