UNPKG

@stackbit/utils

Version:
723 lines 29.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.updateDocumentWithOperation = exports.updateDocumentWithOperations = exports.createDocumentFieldsWithUpdateOperationFields = void 0; /** * Takes object of UpdateOperationField and creates a new object by mapping the * UpdateOperationField to DocumentField. * * @param updateOperationFields * @param modelName * @param getModelByName * @param locale */ function createDocumentFieldsWithUpdateOperationFields({ updateOperationFields, modelName, getModelByName, locale }) { try { const model = getModelByName(modelName); if (!model) { throw new Error(`The document's model '${modelName}' was not found.`); } return Object.entries(updateOperationFields).reduce((documentFields, [fieldName, updateOperationField]) => { const modelField = (model.fields ?? []).find((field) => field.name === fieldName); if (!modelField) { throw new Error(`Field '${fieldName}' in model '${modelName}' was not found.`); } const documentField = convertOperationFieldToDocumentField({ operationField: updateOperationField, modelField, getModelByName, locale }); if (typeof documentField !== 'undefined') { documentFields[fieldName] = documentField; } return documentFields; }, {}); } catch (error) { throw new Error(`Error creating document. ${error.message}`); } } exports.createDocumentFieldsWithUpdateOperationFields = createDocumentFieldsWithUpdateOperationFields; /** * Updates `document` with array of `UpdateOperation`. * * This method doesn't mutate the passed `document`, it returns a new deep copied * document with the updated data. * * @param document * @param operations * @param getModelByName */ function updateDocumentWithOperations({ document, operations, getModelByName }) { return operations.reduce((document, updateOperation) => { return updateDocumentWithOperation({ document, updateOperation, getModelByName }); }, document); } exports.updateDocumentWithOperations = updateDocumentWithOperations; /** * Updates `document` with `updateOperation`. * * This method doesn't mutate the passed `document`, it returns a new deep copied * document with the updated data. * * @param document * @param updateOperation * @param getModelByName */ function updateDocumentWithOperation({ document, updateOperation, getModelByName }) { try { if (updateOperation.fieldPath.length === 0) { throw new Error(`The operation's fieldPath cannot be empty.`); } const [fieldName, ...fieldPathTail] = updateOperation.fieldPath; if (typeof fieldName !== 'string') { throw new Error(`The first item in operation's fieldPath must be a string.`); } const model = getModelByName(document.modelName); if (!model) { throw new Error(`The document's model '${document.modelName}' was not found.`); } const modelField = getModelFieldByName(model, fieldName); if (!modelField) { throw new Error(`The field '${fieldName}' in operation's fieldPath doesn't match any field of model '${model.name}'.`); } const { [fieldName]: documentField, ...restFields } = document.fields; const updatedDocField = updateDocumentFieldAtFieldPath({ documentField, modelField, fieldPath: fieldPathTail, updateOperation, getModelByName }); return { ...document, fields: { ...restFields, ...wrapDocumentFieldOrUndefined(fieldName, updatedDocField) } }; } catch (error) { throw new Error(`Error updating document '${document.id}' with '${updateOperation.opType}' operation ` + `at field path '${updateOperation.fieldPath.join('.')}'. ${error.message}`); } } exports.updateDocumentWithOperation = updateDocumentWithOperation; /** * Updates `documentField` at the specified `fieldPath` with the `updateOperation`. * This method doesn't mutate the passed `documentField`, it returns a new deep * copied document field with the updated data. * * The `fieldPath` specifies the path from the current `documentField` to the * document field that needs to be updated with the operation. The `fieldPath` * is always a slice of the `updateOperation.fieldPath`. * * The `updateOperation.fieldPath` is a path from the root of the document * containing the `documentField` to the document field that needs to be updated. */ function updateDocumentFieldAtFieldPath({ documentField, modelField, fieldPath, updateOperation, getModelByName }) { // If no more items left in the fieldPath, we found our target documentField, update it. if (fieldPath.length === 0) { return updateDocumentFieldWithOperation({ documentField, updateOperation, getModelByName }); } if (!documentField) { throw new Error(`The field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' points to an undefined document field.`); } const locale = updateOperation.locale; const [fieldName, ...fieldPathTail] = fieldPath; if (documentField.type === 'object') { if (modelField.type !== 'object') { throw new Error(`The 'object' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` + `does not match the model field type '${modelField.type}'.`); } if (typeof fieldName !== 'string') { throw new Error(`The '${fieldName}' in the field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' must be a string.`); } const childModelField = getModelFieldByName(modelField, fieldName); if (!childModelField) { throw new Error(`The '${fieldName}' in the field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' doesn't match any model field.`); } if (documentField.localized) { const { localizedValue, restLocalizedValues, locale: resolvedLocale } = getLocalizedValueForLocale({ documentField, fieldPath: getFieldPathPrefix(updateOperation, fieldPath), locale }); const { [fieldName]: childDocField, ...restFields } = localizedValue.fields; const updatedValue = updateDocumentFieldAtFieldPath({ documentField: childDocField, modelField: childModelField, fieldPath: fieldPathTail, updateOperation, getModelByName }); return { ...documentField, locales: { ...restLocalizedValues, [resolvedLocale]: { ...localizedValue, fields: { ...restFields, ...wrapDocumentFieldOrUndefined(fieldName, updatedValue) } } } }; } const { [fieldName]: childDocField, ...restFields } = documentField.fields; const updatedValue = updateDocumentFieldAtFieldPath({ documentField: childDocField, modelField: childModelField, fieldPath: fieldPathTail, updateOperation, getModelByName }); return { ...documentField, fields: { ...restFields, ...wrapDocumentFieldOrUndefined(fieldName, updatedValue) } }; } else if (documentField.type === 'model') { if (modelField.type !== 'model') { throw new Error(`The 'model' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` + `does not match the model field type '${modelField.type}'.`); } if (typeof fieldName !== 'string') { throw new Error(`The '${fieldName}' in the field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' must be a string.`); } if (documentField.localized) { const { localizedValue, restLocalizedValues, locale: resolvedLocale } = getLocalizedValueForLocale({ documentField, fieldPath: getFieldPathPrefix(updateOperation, fieldPath), locale }); const model = getModelByName(localizedValue.modelName); if (!model) { throw new Error(`The 'model' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' ` + `references a non existing model '${localizedValue.modelName}'.`); } const childModelField = getModelFieldByName(model, fieldName); if (!childModelField) { throw new Error(`The '${fieldName}' in field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' doesn't match any model field.`); } const { [fieldName]: childDocField, ...restFields } = localizedValue.fields; const updatedValue = updateDocumentFieldAtFieldPath({ documentField: childDocField, modelField: childModelField, fieldPath: fieldPathTail, updateOperation, getModelByName }); return { ...documentField, locales: { ...restLocalizedValues, [resolvedLocale]: { ...localizedValue, fields: { ...restFields, ...wrapDocumentFieldOrUndefined(fieldName, updatedValue) } } } }; } const model = getModelByName(documentField.modelName); if (!model) { throw new Error(`The 'model' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPathTail)}' ` + `references a non existing model '${documentField.modelName}'.`); } const childModelField = getModelFieldByName(model, fieldName); if (!childModelField) { throw new Error(`The field '${fieldName}' in field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' doesn't match any model field.`); } const { [fieldName]: childDocField, ...restFields } = documentField.fields; const updatedValue = updateDocumentFieldAtFieldPath({ documentField: childDocField, modelField: childModelField, fieldPath: fieldPathTail, updateOperation, getModelByName }); return { ...documentField, fields: { ...restFields, ...wrapDocumentFieldOrUndefined(fieldName, updatedValue) } }; } else if (documentField.type === 'list') { if (modelField.type !== 'list') { throw new Error(`The 'list' document field at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` + `does not match the model field type '${modelField.type}'.`); } const itemIndex = fieldName; if (documentField.localized) { const { localizedValue, restLocalizedValues, locale: resolvedLocale } = getLocalizedValueForLocale({ documentField, fieldPath: getFieldPathPrefix(updateOperation, fieldPath), locale }); const updatedItem = updateDocumentFieldAtFieldPath({ documentField: localizedValue.items[itemIndex], modelField: modelField.items, fieldPath: fieldPathTail, updateOperation, getModelByName }); if (typeof updatedItem === 'undefined') { throw new Error(`Cannot set list item at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` + `to 'undefined', use 'remove' operation to remove list item.`); } return { ...documentField, locales: { ...restLocalizedValues, [resolvedLocale]: { ...localizedValue, items: splice(localizedValue.items, itemIndex, 1, updatedItem) } } }; } const updatedItem = updateDocumentFieldAtFieldPath({ documentField: documentField.items[itemIndex], modelField: modelField.items, fieldPath: fieldPathTail, updateOperation, getModelByName }); if (typeof updatedItem === 'undefined') { throw new Error(`Cannot set list item at field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' ` + `to 'undefined', use 'remove' operation to remove list item.`); } return { ...documentField, items: splice(documentField.items, itemIndex, 1, updatedItem) }; } else { throw new Error(`The partial field path '${getFieldPathPrefixStr(updateOperation, fieldPath)}' points to a non-object or non-list field`); } } function getModelFieldByName(model, fieldName) { return model.fields?.find((field) => field.name === fieldName); } function wrapDocumentFieldOrUndefined(fieldName, documentField) { if (typeof documentField === 'undefined') { return undefined; } return { [fieldName]: documentField }; } function getFieldPathPrefixStr(updateOperation, fieldPath) { return getFieldPathPrefix(updateOperation, fieldPath).join('.'); } function getFieldPathPrefix(updateOperation, fieldPathTail) { return updateOperation.fieldPath.slice(0, updateOperation.fieldPath.length - fieldPathTail.length); } function updateDocumentFieldWithOperation({ documentField, updateOperation, getModelByName }) { const locale = updateOperation.locale; switch (updateOperation.opType) { case 'set': { return convertOperationFieldToDocumentField({ operationField: updateOperation.field, modelField: updateOperation.modelField, documentField, getModelByName, locale }); } case 'unset': { if (documentField?.localized) { if (!locale) { throw new Error(`The '${documentField.type}' document field at field path '${updateOperation.fieldPath.join('.')}' ` + `is localized, but no locale was provided in the update operation.`); } // Remove the locale property to unset the value for the specified locale. const { [locale]: currentLocale, ...restLocales } = documentField.locales; return { ...documentField, locales: { ...restLocales } }; } return undefined; } case 'insert': { // Empty lists can be undefined, in this case, create a dummy // document list field with empty items to insert the first item const documentFieldRes = documentField ?? (updateOperation.modelField.localized ? { type: 'list', localized: true, locales: locale ? { [locale]: { locale, items: [] } } : {} } : { type: 'list', items: [] }); if (documentFieldRes.type !== 'list') { throw new Error(`The 'insert' operation can be performed on 'list' document fields only, got '${documentFieldRes.type}'.`); } const newItem = convertOperationFieldToDocumentField({ operationField: updateOperation.item, modelField: updateOperation.modelField.items, getModelByName, locale }); if (documentFieldRes.localized) { const { localizedValue, restLocalizedValues, locale: resolvedLocale } = getLocalizedValueForLocale({ documentField: documentFieldRes, fieldPath: updateOperation.fieldPath, locale }); const insertionIndex = updateOperation.index ?? localizedValue.items.length; return { type: 'list', localized: true, locales: { ...restLocalizedValues, [resolvedLocale]: { ...localizedValue, items: splice(localizedValue.items, insertionIndex, 0, newItem) } } }; } const insertionIndex = updateOperation.index ?? documentFieldRes.items.length; return { type: 'list', items: splice(documentFieldRes.items, insertionIndex, 0, newItem) }; } case 'remove': { if (documentField?.type !== 'list') { throw new Error(`The 'remove' operation can be performed on 'list' document fields only, got '${documentField?.type}'.`); } if (documentField.localized) { const { localizedValue, restLocalizedValues, locale: resolvedLocale } = getLocalizedValueForLocale({ documentField, fieldPath: updateOperation.fieldPath, locale }); return { ...documentField, locales: { ...restLocalizedValues, [resolvedLocale]: { ...localizedValue, items: splice(localizedValue.items, updateOperation.index, 1) } } }; } return { ...documentField, items: splice(documentField.items, updateOperation.index, 1) }; } case 'reorder': { if (documentField?.type !== 'list') { throw new Error(`The 'reorder' operation can be performed on 'list' document fields only, got '${documentField?.type}'.`); } if (documentField.localized) { const { localizedValue, restLocalizedValues, locale: resolvedLocale } = getLocalizedValueForLocale({ documentField, fieldPath: updateOperation.fieldPath, locale }); return { ...documentField, locales: { ...restLocalizedValues, [resolvedLocale]: { ...localizedValue, items: updateOperation.order.map((newIndex) => localizedValue.items[newIndex]) } } }; } return { ...documentField, items: updateOperation.order.map((newIndex) => documentField.items[newIndex]) }; } default: { const _exhaustiveCheck = updateOperation; return _exhaustiveCheck; } } } function splice(array, start, deleteCount, ...items) { const copy = array.slice(); copy.splice(start, deleteCount, ...items); return copy; } function getLocalizedValueForLocale({ documentField, fieldPath, locale }) { if (!locale) { throw new Error(`The '${documentField.type}' document field at field path '${fieldPath.join('.')}' ` + `is localized, but no locale was provided in the update operation.`); } const { [locale]: localizedValue, ...restLocalizedValues } = documentField.locales; if (!localizedValue) { throw new Error(`The '${documentField.type}' document field at field path '${fieldPath.join('.')}' ` + `is localized, but it has no value for the specified locale '${locale}'.`); } return { locale, localizedValue: localizedValue, restLocalizedValues }; } function convertOperationFieldToDocumentField({ operationField, modelField, documentField, getModelByName, locale }) { const localized = 'localized' in modelField && modelField.localized; if (localized && !locale) { throw new Error(`The '${modelField.type}' field is localized, but it has no value for the specified locale '${locale}'.`); } if (documentField && documentField.type !== operationField.type) { throw new Error(`The updateOperation.field.type '${operationField.type}' doesn't match the target document field type '${documentField.type}'.`); } const documentFieldLocales = documentField && 'locales' in documentField ? documentField.locales : null; switch (operationField.type) { case 'string': case 'url': case 'slug': case 'text': case 'markdown': case 'html': case 'number': case 'boolean': case 'date': case 'datetime': case 'color': case 'json': case 'richText': case 'file': case 'enum': case 'style': { if (localized) { return { type: operationField.type, localized: true, locales: { ...documentFieldLocales, [locale]: { locale, value: operationField.value } } }; } return { type: operationField.type, value: operationField.value }; } case 'image': { if (modelField.type !== 'image') { throw new Error(`The model's field type '${modelField.type}' doesn't match the operation's field type '${operationField.type}'.`); } if (localized) { return { type: operationField.type, localized: true, source: modelField.source, locales: { ...documentFieldLocales, [locale]: { locale, sourceData: operationField.value } } }; } return { type: operationField.type, source: modelField.source, sourceData: operationField.value }; } case 'object': { if (modelField.type !== 'object') { throw new Error(`The model's field type '${modelField.type}' doesn't match the operation's field type '${operationField.type}'.`); } const updatedFields = Object.entries(operationField.fields).reduce((fields, [fieldName, operationField]) => { const childModelField = modelField.fields.find((field) => field.name === fieldName); if (!childModelField) { throw new Error(`Model field with name '${fieldName}' was not found.`); } fields[fieldName] = convertOperationFieldToDocumentField({ operationField, modelField: childModelField, getModelByName, locale }); return fields; }, {}); if (localized) { return { type: operationField.type, localized: true, locales: { ...documentFieldLocales, [locale]: { locale, fields: updatedFields } } }; } return { type: operationField.type, fields: updatedFields }; } case 'model': { if (modelField.type !== 'model') { throw new Error(`The model's field type '${modelField.type}' doesn't match the operation's field type '${operationField.type}'.`); } const model = getModelByName(operationField.modelName); if (!model) { throw new Error(`Model with name '${operationField.modelName}' not found.`); } const modelFields = model.fields ?? []; const updatedFields = Object.entries(operationField.fields).reduce((fields, [fieldName, operationField]) => { const childModelField = modelFields.find((field) => field.name === fieldName); if (!childModelField) { throw new Error(`Model field with name '${fieldName}' was not found in model '${model.name}'.`); } fields[fieldName] = convertOperationFieldToDocumentField({ operationField, modelField: childModelField, getModelByName, locale }); return fields; }, {}); if (localized) { return { type: operationField.type, localized: true, locales: { ...documentFieldLocales, [locale]: { locale, modelName: operationField.modelName, fields: updatedFields } } }; } return { type: operationField.type, modelName: operationField.modelName, fields: updatedFields }; } case 'reference': { if (localized) { return { type: operationField.type, refType: operationField.refType, localized: true, locales: { ...documentFieldLocales, [locale]: { locale, refId: operationField.refId } } }; } return { type: operationField.type, refType: operationField.refType, refId: operationField.refId }; } case 'cross-reference': { if (localized) { return { type: operationField.type, refType: 'document', localized: true, locales: { ...documentFieldLocales, [locale]: { locale, refSrcType: operationField.value.refSrcType, refProjectId: operationField.value.refProjectId, refId: operationField.value.refId } } }; } return { type: operationField.type, refType: 'document', refSrcType: operationField.value.refSrcType, refProjectId: operationField.value.refProjectId, refId: operationField.value.refId }; } case 'list': { if (modelField.type !== 'list') { throw new Error(`The model's field type '${modelField.type}' doesn't match the operation's field type '${operationField.type}'.`); } const items = operationField.items.map((item) => { return convertOperationFieldToDocumentField({ operationField: item, modelField: modelField.items, getModelByName, locale }); }); if (localized) { return { type: operationField.type, localized: true, locales: { ...documentFieldLocales, [locale]: { locale, items } } }; } return { type: operationField.type, items }; } default: { const _exhaustiveCheck = operationField; return _exhaustiveCheck; } } } //# sourceMappingURL=update-document-operation.js.map